#!/usr/bin/env python3 # # MMGen Wallet, a terminal-based cryptocurrency wallet # Copyright (C)2013-2025 The MMGen Project # Licensed under the GNU General Public License, Version 3: # https://www.gnu.org/licenses # Public project repositories: # https://github.com/mmgen/mmgen-wallet # https://gitlab.com/mmgen/mmgen-wallet """ ui: Interactive user interface functions for the MMGen suite """ import sys, os from .util import msg, msg_r, Msg, die def confirm_or_raise(cfg, message, action, *, expect='YES', exit_msg='Exiting at user request'): if message: msg(message) if line_input( cfg, (f'{action} ' if action[0].isupper() else f'Are you sure you want to {action}?\n') + f'Type uppercase {expect!r} to confirm: ' ).strip() != expect: die('UserNonConfirmation', exit_msg) def get_words_from_user(cfg, prompt): words = line_input(cfg, prompt, echo=cfg.echo_passphrase).split() if cfg.debug: msg('Sanitized input: [{}]'.format(' '.join(words))) return words def get_data_from_user(cfg, *, desc='data'): # user input MUST be UTF-8 data = line_input(cfg, f'Enter {desc}: ', echo=cfg.echo_passphrase) if cfg.debug: msg(f'User input: [{data}]') return data def line_input(cfg, prompt, *, echo=True, insert_txt='', hold_protect=True): """ multi-line prompts OK one-line prompts must begin at beginning of line empty prompts forbidden due to interactions with readline """ assert prompt, 'calling line_input() with an empty prompt forbidden' def get_readline(): try: import readline return readline except ImportError: return False if hold_protect: from .term import kb_hold_protect kb_hold_protect() if cfg.test_suite_popen_spawn: msg(prompt) sys.stderr.flush() # required by older Pythons (e.g. v3.7) reply = os.read(0, 4096).decode().rstrip('\n') # strip NL to mimic behavior of input() elif not sys.stdin.isatty(): msg_r(prompt) reply = input('') elif echo: readline = get_readline() if readline and insert_txt: readline.set_startup_hook(lambda: readline.insert_text(insert_txt)) reply = input(prompt) if readline and insert_txt: readline.set_startup_hook(lambda: readline.insert_text('')) else: from getpass import getpass reply = getpass(prompt) if hold_protect: kb_hold_protect() return reply.strip() def keypress_confirm( cfg, prompt, *, default_yes = False, verbose = False, no_nl = False, complete_prompt = False, do_exit = False, exit_msg = 'Exiting at user request'): def do_return(retval): if do_exit and not retval: die(1, exit_msg) return retval if not complete_prompt: prompt = '{} {}: '.format(prompt, '(Y/n)' if default_yes else '(y/N)') nl = f'\r{" "*len(prompt)}\r' if no_nl else '\n' if cfg.accept_defaults: msg(prompt) return do_return(default_yes) from .term import get_char while True: match get_char(prompt, immed_chars='yYnN').strip('\n\r'): case '': msg_r(nl) return do_return(default_yes) case 'y' | 'Y' | 'n' | 'N' as reply: msg_r(nl) return do_return(reply in 'yY') case _: msg_r('\nInvalid reply\n' if verbose else '\r') def do_pager(text): pagers = ['less', 'more'] end_msg = '\n(end of text)\n\n' os.environ['LESS'] = '--jump-target=2 --shift=4 --tabs=4 --RAW-CONTROL-CHARS --chop-long-lines' if 'PAGER' in os.environ and os.environ['PAGER'] != pagers[0]: pagers = [os.environ['PAGER']] + pagers from subprocess import run from .color import set_vt100 for pager in pagers: try: m = text + ('' if pager == 'less' else end_msg) run([pager], input=m.encode(), check=True) msg_r('\r') except: pass else: break else: Msg(text+end_msg) set_vt100() def do_license_msg(cfg, *, immed=False): if cfg.quiet or cfg.no_license or cfg.yes or not cfg.stdin_tty: return from .contrib import license as gpl from .cfg import gc msg(gpl.warning.format(gc=gc)) from .term import get_char prompt = "Press 'w' for conditions and warranty info, or 'c' to continue: " while True: match get_char(prompt, immed_chars=('', 'wc')[bool(immed)]): case 'w': do_pager(gpl.conditions) case 'c': msg('') break case _: msg_r('\r') msg('')