From 1d8b908c7ca0ba5234403a7cd9078a75c666c497 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 17 Oct 2022 18:37:22 +0000 Subject: [PATCH] util.py: relocate user-prompting functions to ui.py --- mmgen/addrfile.py | 21 ++--- mmgen/cfg.py | 2 + mmgen/crypto.py | 6 +- mmgen/fileutil.py | 6 +- mmgen/main_addrgen.py | 2 + mmgen/main_addrimport.py | 1 + mmgen/main_passgen.py | 2 + mmgen/main_seedjoin.py | 2 + mmgen/main_tool.py | 16 +++- mmgen/main_txbump.py | 1 + mmgen/main_txsend.py | 1 + mmgen/main_txsign.py | 1 + mmgen/main_wallet.py | 19 ++-- mmgen/opts.py | 2 +- mmgen/passwdlist.py | 3 +- mmgen/proto/btc/regtest.py | 1 + mmgen/proto/eth/tx/new.py | 3 +- mmgen/protocol.py | 3 +- mmgen/tool/fileutil.py | 2 +- mmgen/tool/help.py | 3 +- mmgen/tw/common.py | 5 +- mmgen/tw/json.py | 11 +-- mmgen/tw/unspent.py | 14 ++- mmgen/tx/base.py | 9 +- mmgen/tx/bump.py | 16 ++-- mmgen/tx/info.py | 3 +- mmgen/tx/new.py | 8 +- mmgen/tx/online.py | 5 +- mmgen/ui.py | 159 +++++++++++++++++++++++++++++++++ mmgen/util.py | 158 ++------------------------------ mmgen/wallet/__init__.py | 3 + mmgen/wallet/base.py | 3 +- mmgen/wallet/dieroll.py | 4 +- mmgen/wallet/incog_base.py | 3 +- mmgen/wallet/incog_hidden.py | 14 +-- mmgen/wallet/mmgen.py | 3 +- mmgen/wallet/mnemonic.py | 3 +- mmgen/wallet/unenc.py | 3 +- mmgen/xmrwallet.py | 1 + scripts/uninstall-mmgen.py | 1 + test/misc/password_entry.py | 1 + test/misc/term.py | 1 + test/test.py | 2 + test/test_py_d/common.py | 1 + test/test_py_d/ts_xmrwallet.py | 1 + 45 files changed, 300 insertions(+), 229 deletions(-) create mode 100755 mmgen/ui.py diff --git a/mmgen/addrfile.py b/mmgen/addrfile.py index 07cb6b73..da80e55d 100755 --- a/mmgen/addrfile.py +++ b/mmgen/addrfile.py @@ -20,14 +20,7 @@ addrfile.py: Address and password file classes for the MMGen suite """ -from .util import ( - msg, - qmsg, - qmsg_r, - die, - capfirst, - keypress_confirm, -) +from .util import msg,qmsg,qmsg_r,die,capfirst from .protocol import init_proto from .obj import MMGenObject,TwComment,WalletPassword,MMGenPWIDString from .seed import SeedID,is_seed_id @@ -166,8 +159,8 @@ class AddrFile(MMGenObject): ret.append(a) if p.has_keys and p.ka_validity_chk != False: - from .opts import opt - if opt.yes or p.ka_validity_chk == True or keypress_confirm('Check key-to-address validity?'): + + def verify_keys(): from .addrgen import KeyGenerator,AddrGenerator kg = KeyGenerator(p.proto,p.al_id.mmtype.pubkey_type) ag = AddrGenerator(p.proto,p.al_id.mmtype) @@ -178,6 +171,14 @@ class AddrFile(MMGenObject): f'Key doesn’t match address!\n {e.sec.wif}\n {e.addr}') qmsg(' - done') + from .opts import opt + if opt.yes or p.ka_validity_chk == True: + verify_keys() + else: + from .ui import keypress_confirm + if keypress_confirm('Check key-to-address validity?'): + verify_keys() + return ret def parse_file(self,fn,buf=[],exit_on_error=True): diff --git a/mmgen/cfg.py b/mmgen/cfg.py index 23e8c733..5ea61fc2 100755 --- a/mmgen/cfg.py +++ b/mmgen/cfg.py @@ -271,6 +271,8 @@ class CfgFileSampleUsr(CfgFileSample): {' ' + fmt_list(bad,fmt='bare')} """ ymsg(fmt(m,indent=' ',strip_char='\t')) + + from .ui import keypress_confirm,do_pager while True: if not keypress_confirm(self.details_confirm_prompt,no_nl=True): return diff --git a/mmgen/crypto.py b/mmgen/crypto.py index cb185d51..5b9ccedc 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -34,8 +34,6 @@ from .util import ( qmsg, fmt, die, - line_input, - get_words_from_user, make_chksum_8, compare_chksums, oneshot_warning, @@ -246,6 +244,7 @@ def _get_random_data_from_user(uchars,desc): if g.debug: msg(f'USER ENTROPY (user input + keystroke timings):\n{ret}') + from .ui import line_input line_input('User random data successfully acquired. Press ENTER to continue: ') return ret.encode() @@ -299,6 +298,7 @@ def get_hash_preset_from_user( f'Enter hash preset for {data_desc},\n' + f'or hit ENTER to accept the default value ({hash_preset!r}): ' ) + from .ui import line_input while True: ret = line_input(prompt) if ret: @@ -324,6 +324,7 @@ def get_new_passphrase(data_desc,hash_preset,passwd_file,pw_desc='passphrase'): quiet = pwfile_reuse_warning(passwd_file).warning_shown )) else: qmsg('\n'+fmt(message,indent=' ')) + from .ui import get_words_from_user if opt.echo_passphrase: pw = ' '.join(get_words_from_user(f'Enter {pw_desc} for {data_desc}: ')) else: @@ -352,6 +353,7 @@ def get_passphrase(data_desc,passwd_file,pw_desc='passphrase'): desc = f'{pw_desc} for {data_desc}', quiet = pwfile_reuse_warning(passwd_file).warning_shown )) else: + from .ui import get_words_from_user return ' '.join(get_words_from_user(f'Enter {pw_desc} for {data_desc}: ')) mmenc_salt_len = 32 diff --git a/mmgen/fileutil.py b/mmgen/fileutil.py index 904ff581..dd4a4f94 100755 --- a/mmgen/fileutil.py +++ b/mmgen/fileutil.py @@ -29,13 +29,11 @@ from .util import ( qmsg, dmsg, die, - confirm_or_raise, get_extension, is_utf8, capfirst, make_full_path, strip_comments, - keypress_confirm, ) def check_or_create_dir(path): @@ -181,6 +179,7 @@ def write_data_to_file( outfile,data,desc='data', if no_tty: die(2,f'Printing {desc} to screen is not allowed') if (ask_tty and not opt.quiet) or binary: + from .ui import confirm_or_raise confirm_or_raise( message = '', action = f'output {desc} to screen' ) @@ -193,6 +192,7 @@ def write_data_to_file( outfile,data,desc='data', if no_tty: die(2,f'Writing {desc} to pipe is not allowed') if ask_tty and not opt.quiet: + from .ui import confirm_or_raise confirm_or_raise( message = '', action = f'output {desc} to pipe' ) @@ -219,12 +219,14 @@ def write_data_to_file( outfile,data,desc='data', if ask_write: if not ask_write_prompt: ask_write_prompt = f'Save {desc}?' + from .ui import keypress_confirm if not keypress_confirm(ask_write_prompt, default_yes=ask_write_default_yes): die(1,f'{capfirst(desc)} not saved') hush = False if os.path.lexists(outfile) and ask_overwrite: + from .ui import confirm_or_raise confirm_or_raise( message = '', action = f'File {outfile!r} already exists\nOverwrite?' ) diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index a424ad96..3062bdc9 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -148,6 +148,7 @@ idxs = mmgen.addrlist.AddrIdxList( fmt_str=cmd_args.pop() ) from .fileutil import get_seed_file sf = get_seed_file(cmd_args,1) +from .ui import do_license_msg do_license_msg() ss = Wallet(sf) @@ -170,6 +171,7 @@ af.format() if al.gen_addrs and opt.print_checksum: Die(0,al.checksum) +from .ui import keypress_confirm if al.gen_keys and keypress_confirm('Encrypt key list?'): af.encrypt() af.write( diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py index 2569fce2..ebfe069c 100755 --- a/mmgen/main_addrimport.py +++ b/mmgen/main_addrimport.py @@ -120,6 +120,7 @@ def check_opts(tw): rescan = False if rescan and not opt.quiet: + from .ui import keypress_confirm if not keypress_confirm( '\n{}\n\nContinue?'.format(addrimport_msgs['rescan']), default_yes = True ): diff --git a/mmgen/main_passgen.py b/mmgen/main_passgen.py index 1c3b7b0b..647bad9f 100755 --- a/mmgen/main_passgen.py +++ b/mmgen/main_passgen.py @@ -154,6 +154,7 @@ PasswordList( pw_fmt = pw_fmt, chk_params_only = True ) +from .ui import do_license_msg do_license_msg() ss = Wallet(sf) @@ -170,6 +171,7 @@ af = al.get_file() af.format() +from .ui import keypress_confirm if keypress_confirm('Encrypt password list?'): af.encrypt() af.write(binary=True,desc='encrypted password list') diff --git a/mmgen/main_seedjoin.py b/mmgen/main_seedjoin.py index dc02be5b..ea525272 100755 --- a/mmgen/main_seedjoin.py +++ b/mmgen/main_seedjoin.py @@ -119,10 +119,12 @@ if opt.id_str and not opt.master_share: die(1,'--id-str option meaningless in context of non-master-share join') from .fileutil import check_infile +from .wallet import check_wallet_extension for arg in cmd_args: check_wallet_extension(arg) check_infile(arg) +from .ui import do_license_msg do_license_msg() qmsg('Input files:\n {}\n'.format('\n '.join(cmd_args))) diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index 3ac4168d..f79e7fc7 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -294,15 +294,24 @@ def process_args(cmd,cmd_args,cls): return ( args, kwargs ) def process_result(ret,pager=False,print_result=False): - from .util import Msg,die """ Convert result to something suitable for output to screen and return it. If result is bytes and not convertible to utf8, output as binary using os.write(). If 'print_result' is True, send the converted result directly to screen or pager instead of returning it. """ + + from .util import Msg,die + def triage_result(o): - return o if not print_result else do_pager(o) if pager else Msg(o) + if print_result: + if pager: + from .ui import do_pager + do_pager(o) + else: + Msg(o) + else: + return o if ret == True: return True @@ -316,8 +325,7 @@ def process_result(ret,pager=False,print_result=False): return triage_result('\n'.join([r.decode() if isinstance(r,bytes) else r for r in ret])) elif isinstance(ret,bytes): try: - o = ret.decode() - return o if not print_result else do_pager(o) if pager else Msg(o) + return triage_result(ret.decode()) except: # don't add NL to binary data if it can't be converted to utf8 if print_result: diff --git a/mmgen/main_txbump.py b/mmgen/main_txbump.py index e7cab13e..8b648cae 100755 --- a/mmgen/main_txbump.py +++ b/mmgen/main_txbump.py @@ -111,6 +111,7 @@ from .tx.sign import * seed_files = get_seed_files(opt,cmd_args) if (cmd_args or opt.send) else None +from .ui import do_license_msg do_license_msg() silent = opt.yes and opt.tx_fee != None and opt.output_to_reduce != None diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 4ad886c2..471fe733 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -48,6 +48,7 @@ else: opts.usage() if not opt.status: + from .ui import do_license_msg do_license_msg() async def main(): diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index 9542f99e..16694ced 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -106,6 +106,7 @@ for i in infiles: check_infile(i) if not opt.info and not opt.terse_info: + from .ui import do_license_msg do_license_msg(immed=True) from .tx.sign import * diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index 0c1fe5b6..c54a5867 100755 --- a/mmgen/main_wallet.py +++ b/mmgen/main_wallet.py @@ -176,6 +176,7 @@ if cmd_args: sf = get_seed_file(cmd_args,nargs,invoked_as=invoked_as) if invoked_as != 'chk': + from .ui import do_license_msg do_license_msg() if invoked_as == 'gen': @@ -242,6 +243,7 @@ if invoked_as == 'passchg': return old_fn if ss_in.infile.dirname == g.data_dir: + from .ui import confirm_or_raise confirm_or_raise( message = yellow('Confirmation of default wallet update'), action = 'update the default wallet', @@ -252,18 +254,21 @@ if invoked_as == 'passchg': else: old_wallet = rename_old_wallet_maybe(silent=False) ss_out.write_to_file() + from .ui import keypress_confirm if keypress_confirm(f'Securely delete old wallet {old_wallet!r}?'): secure_delete( old_wallet ) elif invoked_as == 'gen' and not opt.outdir and not opt.stdout: from .filename import find_file_in_dir - if ( - not find_file_in_dir( get_wallet_cls('mmgen'), g.data_dir ) - and keypress_confirm( - 'Make this wallet your default and move it to the data directory?', - default_yes = True ) ): - ss_out.write_to_file(outdir=g.data_dir) - else: + if find_file_in_dir( get_wallet_cls('mmgen'), g.data_dir ): ss_out.write_to_file() + else: + from .ui import keypress_confirm + if keypress_confirm( + 'Make this wallet your default and move it to the data directory?', + default_yes = True ): + ss_out.write_to_file(outdir=g.data_dir) + else: + ss_out.write_to_file() else: ss_out.write_to_file() diff --git a/mmgen/opts.py b/mmgen/opts.py index 72102fec..4585abb5 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -78,7 +78,7 @@ def print_help(po,opts_data,opt_filter): opts_data['text']['long_options'] = d remove_unneeded_long_opts() - from .util import do_pager + from .ui import do_pager do_pager( mmgen.share.Opts.make_help( proto, diff --git a/mmgen/passwdlist.py b/mmgen/passwdlist.py index 95bb5f5b..44766b8d 100755 --- a/mmgen/passwdlist.py +++ b/mmgen/passwdlist.py @@ -22,7 +22,7 @@ passwdlist.py: Password list class for the MMGen suite from collections import namedtuple -from .util import ymsg,is_int,keypress_confirm,die +from .util import ymsg,is_int,die from .obj import ImmutableAttr,ListItemAttr,MMGenPWIDString,TwComment from .key import PrivKey from .addr import MMGenPasswordType,AddrIdx,AddrListID @@ -184,6 +184,7 @@ class PasswordList(AddrList): ).format(good_pw_len) ) if pf in ('bip39','hex') and pw_bytes < seed.byte_len: + from .ui import keypress_confirm if not keypress_confirm( f'WARNING: requested {self.pw_info[pf].desc} length has less entropy ' + 'than underlying seed!\nIs this what you want?', diff --git a/mmgen/proto/btc/regtest.py b/mmgen/proto/btc/regtest.py index 1220bf69..2ac16030 100755 --- a/mmgen/proto/btc/regtest.py +++ b/mmgen/proto/btc/regtest.py @@ -30,6 +30,7 @@ def create_data_dir(data_dir): try: os.stat(os.path.join(data_dir,'regtest')) except: pass else: + from ...ui import keypress_confirm if keypress_confirm( f'Delete your existing MMGen regtest setup at {data_dir!r} and create a new one?'): shutil.rmtree(data_dir) diff --git a/mmgen/proto/eth/tx/new.py b/mmgen/proto/eth/tx/new.py index 714b5b52..4722f98c 100755 --- a/mmgen/proto/eth/tx/new.py +++ b/mmgen/proto/eth/tx/new.py @@ -19,7 +19,7 @@ from .base import Base,TokenBase from ....opts import opt from ....obj import Int,ETHNonce,MMGenTxID,Str,HexStr from ....amt import ETHAmt -from ....util import msg,line_input,is_int,is_hex_str,make_chksum_6 +from ....util import msg,is_int,is_hex_str,make_chksum_6 from ....tw.ctl import TrackingWallet from ....addr import is_mmgen_id,is_coin_addr from ..contract import Token @@ -88,6 +88,7 @@ class New(Base,TxBase.New): self.process_cmd_arg(a,ad_f,ad_w) def select_unspent(self,unspent): + from ....ui import line_input while True: reply = line_input('Enter an account to spend from: ').strip() if reply: diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 54cf59da..f6376f2e 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -302,7 +302,7 @@ def warn_trustlevel(coinsym): Are you sure you want to continue? """ - from .util import qmsg,fmt,keypress_confirm + from .util import qmsg,fmt from .color import red,yellow,green warning = fmt(m).strip().format( @@ -319,6 +319,7 @@ def warn_trustlevel(coinsym): qmsg(warning) return + from .ui import keypress_confirm if not keypress_confirm(warning,default_yes=True): import sys sys.exit(0) diff --git a/mmgen/tool/fileutil.py b/mmgen/tool/fileutil.py index 1d5bda51..29801e8b 100755 --- a/mmgen/tool/fileutil.py +++ b/mmgen/tool/fileutil.py @@ -152,7 +152,7 @@ class tool_cmd(tool_cmd_base): def extract_key_from_geth_wallet( self, wallet_file:str, check_addr=True ): "decrypt the encrypted private key in a Geth keystore wallet, returning the decrypted key" - from ..util import line_input + from ..ui import line_input from ..opts import opt from ..proto.eth.misc import extract_key_from_geth_keystore_wallet passwd = line_input( 'Enter passphrase: ', echo=opt.echo_passphrase ).strip().encode() diff --git a/mmgen/tool/help.py b/mmgen/tool/help.py index e78ea032..209d540b 100755 --- a/mmgen/tool/help.py +++ b/mmgen/tool/help.py @@ -184,7 +184,7 @@ def gen_tool_cmd_usage(mod,cmdname): def usage(cmdname=None,exit_val=1): - from ..util import Msg,die,do_pager + from ..util import Msg,die if cmdname: for mod,cmdlist in main_tool.mods.items(): @@ -194,6 +194,7 @@ def usage(cmdname=None,exit_val=1): else: die(1,f'{cmdname!r}: no such tool command') else: + from ..ui import do_pager do_pager('\n'.join(gen_tool_usage())) import sys diff --git a/mmgen/tw/common.py b/mmgen/tw/common.py index 8d6bd8b5..8819d081 100755 --- a/mmgen/tw/common.py +++ b/mmgen/tw/common.py @@ -26,7 +26,7 @@ from ..globalvars import g from ..objmethods import Hilite,InitErrors,MMGenObject from ..obj import TwComment,get_obj,MMGenIdx,MMGenList from ..color import nocolor,yellow,green -from ..util import msg,msg_r,fmt,die,line_input,do_pager,capfirst,make_timestr +from ..util import msg,msg_r,fmt,die,capfirst,make_timestr from ..addr import MMGenID # mixin class for TwUnspentOutputs,TwAddrList,TwTxHistory: @@ -313,10 +313,12 @@ class TwCommon: parent.oneshot_msg = green(f'Data written to {outfile!r}\n\n') async def a_view(self,parent): + from ..ui import do_pager do_pager( await parent.format_squeezed(color=True,cached=True) ) self.post_view(parent) async def a_view_detail(self,parent): + from ..ui import do_pager do_pager( await parent.format_detail(color=True) ) self.post_view(parent) @@ -329,6 +331,7 @@ class TwCommon: async def run(self,parent,action): msg('') + from ..ui import line_input while True: ret = line_input(f'Enter {parent.item_desc} number (or RETURN to return to main menu): ') if ret == '': diff --git a/mmgen/tw/json.py b/mmgen/tw/json.py index 32c139a1..b740085d 100755 --- a/mmgen/tw/json.py +++ b/mmgen/tw/json.py @@ -15,15 +15,7 @@ tw.json: export and import tracking wallet to JSON format import json from collections import namedtuple -from ..util import ( - msg, - ymsg, - fmt, - die, - make_timestamp, - make_chksum_8, - keypress_confirm, - compare_or_die ) +from ..util import msg,ymsg,fmt,die,make_timestamp,make_chksum_8,compare_or_die from ..base_obj import AsyncInit from ..objmethods import MMGenObject from ..rpc import json_encoder @@ -121,6 +113,7 @@ class TwJSON: msg('\n'+fmt(self.info_msg.strip(),indent=' ')) + from ..ui import keypress_confirm if not keypress_confirm('Continue?'): msg('Exiting at user request') return False diff --git a/mmgen/tw/unspent.py b/mmgen/tw/unspent.py index 7a35c666..cb42d6c7 100755 --- a/mmgen/tw/unspent.py +++ b/mmgen/tw/unspent.py @@ -25,15 +25,7 @@ from collections import namedtuple from ..globalvars import g from ..color import red,yellow -from ..util import ( - msg, - die, - capfirst, - suf, - fmt, - keypress_confirm, - line_input, -) +from ..util import msg,die,capfirst,suf,fmt from ..base_obj import AsyncInit from ..objmethods import MMGenObject from ..obj import ImmutableAttr,ListItemAttr,MMGenListItem,TwComment,get_obj,HexStr,CoinTxID,MMGenList @@ -258,6 +250,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): class item_action(TwCommon.item_action): async def a_balance_refresh(self,uo,idx): + from ..ui import keypress_confirm if not keypress_confirm( f'Refreshing tracking wallet {uo.item_desc} #{idx}. Is this what you want?'): return 'redo' @@ -266,6 +259,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): uo.oneshot_msg = yellow(f'{uo.proto.dcoin} balance for account #{idx} refreshed\n\n') async def a_addr_delete(self,uo,idx): + from ..ui import keypress_confirm if not keypress_confirm( f'Removing {uo.item_desc} #{idx} from tracking wallet. Is this what you want?'): return 'redo' @@ -297,6 +291,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): cur_lbl = uo.data[idx-1].label msg('Current label: {}'.format(cur_lbl.hl() if cur_lbl else '(none)')) + from ..ui import line_input res = line_input( "Enter label text (or ENTER to return to main menu): ", insert_txt = cur_lbl ) @@ -304,6 +299,7 @@ class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit): if res == cur_lbl: return None elif res == '': + from ..ui import keypress_confirm return (await do_lbl_add('')) if keypress_confirm( f'Removing label for {desc}. Is this what you want?') else 'redo' else: diff --git a/mmgen/tx/base.py b/mmgen/tx/base.py index 73efef73..83b359c8 100755 --- a/mmgen/tx/base.py +++ b/mmgen/tx/base.py @@ -16,7 +16,7 @@ from ..globalvars import * from ..objmethods import MMGenObject from ..obj import ImmutableAttr,ListItemAttr,MMGenListItem,MMGenTxLabel,TwComment,CoinTxID,HexStr from ..addr import MMGenID,CoinAddr -from ..util import msg,ymsg,fmt,remove_dups,keypress_confirm,make_timestamp,line_input,die +from ..util import msg,ymsg,fmt,remove_dups,make_timestamp,die from ..opts import opt class MMGenTxIO(MMGenListItem): @@ -153,6 +153,7 @@ class Base(MMGenObject): self.label = MMGenTxLabel(get_data_from_file(infile,'transaction comment')) else: # get comment from user, or edit existing comment m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)] + from ..ui import keypress_confirm,line_input if keypress_confirm(m,default_yes=False): while True: s = MMGenTxLabel(line_input('Comment: ',insert_txt=self.label)) @@ -180,5 +181,7 @@ class Base(MMGenObject): die( 'UserOptError', f'\n{indent}ERROR: {m}\n' ) else: msg(f'\n{indent}WARNING: {m}\n') - if not (opt.yes or keypress_confirm('Continue?',default_yes=True)): - die(1,'Exiting at user request') + if not opt.yes: + from ..ui import keypress_confirm + if not keypress_confirm('Continue?',default_yes=True): + die(1,'Exiting at user request') diff --git a/mmgen/tx/bump.py b/mmgen/tx/bump.py index e42445d6..3e08f4f8 100755 --- a/mmgen/tx/bump.py +++ b/mmgen/tx/bump.py @@ -15,7 +15,7 @@ tx.bump: transaction bump class from .new import New from .completed import Completed from ..opts import opt -from ..util import line_input,is_int,keypress_confirm +from ..util import is_int class Bump(Completed,New): desc = 'fee-bumped transaction' @@ -60,6 +60,7 @@ class Bump(Completed,New): while True: if init_reply == None: + from ..ui import line_input m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): ' reply = line_input(m) or 'c' else: @@ -77,8 +78,11 @@ class Bump(Completed,New): cm = ' (change output)' if chg_idx == idx else '' prompt = f'Fee will be deducted from output {idx+1}{cm} ({o_amt} {self.coin})' if check_sufficient_funds(o_amt): - if opt.yes or keypress_confirm(prompt+'. OK?',default_yes=True): - if opt.yes: - msg(prompt) - self.bump_output_idx = idx - return idx + if opt.yes: + msg(prompt) + else: + from ..ui import keypress_confirm + if not keypress_confirm(prompt+'. OK?',default_yes=True): + continue + self.bump_output_idx = idx + return idx diff --git a/mmgen/tx/info.py b/mmgen/tx/info.py index 1a05eec2..a9b5a4a7 100755 --- a/mmgen/tx/info.py +++ b/mmgen/tx/info.py @@ -15,7 +15,7 @@ tx.info: transaction info class from ..globalvars import * from ..color import red,green,orange from ..opts import opt -from ..util import msg,msg_r,do_pager +from ..util import msg,msg_r import importlib @@ -107,6 +107,7 @@ class TxInfo: def view(self,pager=False,pause=True,terse=False): o = self.format(terse=terse) if pager: + from ..ui import do_pager do_pager(o) else: msg_r(o) diff --git a/mmgen/tx/new.py b/mmgen/tx/new.py index 7bbb9a6f..f7cf26a7 100755 --- a/mmgen/tx/new.py +++ b/mmgen/tx/new.py @@ -17,7 +17,7 @@ from ..opts import opt from .base import Base from ..color import pink from ..obj import get_obj,MMGenList -from ..util import msg,qmsg,fmt,die,suf,remove_dups,get_extension,keypress_confirm,do_license_msg,line_input +from ..util import msg,qmsg,fmt,die,suf,remove_dups,get_extension from ..addr import is_mmgen_id,CoinAddr,is_coin_addr def mmaddr2coinaddr(mmaddr,ad_w,ad_f,proto): @@ -50,6 +50,7 @@ def mmaddr2coinaddr(mmaddr,ad_w,ad_f,proto): coin_addr = ad_f.mmaddr2coinaddr(mmaddr) if coin_addr: msg(wmsg('addr_in_addrfile_only')) + from ..ui import keypress_confirm if not (opt.yes or keypress_confirm('Continue anyway?')): sys.exit(1) else: @@ -108,6 +109,7 @@ class New(Base): def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'): abs_fee = None + from ..ui import line_input while True: if tx_fee: abs_fee = self.convert_and_check_fee(tx_fee,desc) @@ -122,6 +124,7 @@ class New(Base): self.coin, pink(str(self.fee_abs2rel(abs_fee))), self.rel_fee_disp) + from ..ui import keypress_confirm if opt.yes or keypress_confirm(prompt+'OK?',default_yes=True): if opt.yes: msg(prompt) @@ -231,6 +234,7 @@ class New(Base): # inputs methods def select_unspent(self,unspent): prompt = 'Enter a range or space-separated list of outputs to spend: ' + from ..ui import line_input while True: reply = line_input(prompt).strip() if reply: @@ -307,6 +311,7 @@ class New(Base): if funds_left >= 0: p = self.final_inputs_ok_msg(funds_left) + from ..ui import keypress_confirm if opt.yes or keypress_confirm(p+'. OK?',default_yes=True): if opt.yes: msg(p) @@ -331,6 +336,7 @@ class New(Base): if not do_info: await self.get_outputs_from_cmdline(cmd_args) + from ..ui import do_license_msg do_license_msg() if not opt.inputs: diff --git a/mmgen/tx/online.py b/mmgen/tx/online.py index ba262378..5aa14b9c 100755 --- a/mmgen/tx/online.py +++ b/mmgen/tx/online.py @@ -13,8 +13,6 @@ tx.online: online signed transaction class """ from .signed import Signed -from ..util import msg,confirm_or_raise -from ..opts import opt class OnlineSigned(Signed): @@ -24,6 +22,9 @@ class OnlineSigned(Signed): return _base_proto_subclass('Status','status',self.proto)(self) def confirm_send(self): + from ..opts import opt + from ..util import msg + from ..ui import confirm_or_raise confirm_or_raise( message = '' if opt.quiet else 'Once this transaction is sent, there’s no taking it back!', action = f'broadcast this transaction to the {self.proto.coin} {self.proto.network.upper()} network', diff --git a/mmgen/ui.py b/mmgen/ui.py new file mode 100755 index 00000000..0f65185e --- /dev/null +++ b/mmgen/ui.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 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 +# https://gitlab.com/mmgen/mmgen + +""" +ui.py: Interactive user interface functions for the MMGen suite +""" + +import sys,os + +from .globalvars import g +from .opts import opt +from .util import msg,msg_r,Msg,dmsg,die + +def confirm_or_raise(message,action,expect='YES',exit_msg='Exiting at user request'): + if message: + msg(message) + if line_input( + (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(prompt): + words = line_input(prompt, echo=opt.echo_passphrase).split() + dmsg('Sanitized input: [{}]'.format(' '.join(words))) + return words + +def get_data_from_user(desc='data'): # user input MUST be UTF-8 + data = line_input(f'Enter {desc}: ',echo=opt.echo_passphrase) + dmsg(f'User input: [{data}]') + return data + +def line_input(prompt,echo=True,insert_txt=''): + """ + 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 init_readline(): + try: + import readline + except ImportError: + return False + else: + if insert_txt: + readline.set_startup_hook(lambda: readline.insert_text(insert_txt)) + return True + else: + return False + + if not sys.stdout.isatty(): + msg_r(prompt) + prompt = '' + + from .term import kb_hold_protect + kb_hold_protect() + + if g.test_suite_popen_spawn: + msg(prompt) + sys.stderr.flush() + reply = os.read(0,4096).decode().rstrip('\n') # strip NL to mimic behavior of input() + elif echo or not sys.stdin.isatty(): + clear_buffer = init_readline() if sys.stdin.isatty() else False + reply = input(prompt) + if clear_buffer: + import readline + readline.set_startup_hook(lambda: readline.insert_text('')) + else: + from getpass import getpass + if g.platform == 'win': + # MSWin hack - getpass('foo') doesn't flush stderr + msg_r(prompt.strip()) # getpass('') adds a space + sys.stderr.flush() + reply = getpass('') + else: + reply = getpass(prompt) + + kb_hold_protect() + + return reply.strip() + +def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False,complete_prompt=False): + + 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 g.accept_defaults: + msg(prompt) + return default_yes + + from .term import get_char + while True: + reply = get_char(prompt,immed_chars='yYnN').strip('\n\r') + if not reply: + msg_r(nl) + return True if default_yes else False + elif reply in 'yYnN': + msg_r(nl) + return True if reply in 'yY' else False + else: + msg_r('\nInvalid reply\n' if verbose else '\r') + +def do_pager(text): + + pagers = ['less','more'] + end_msg = '\n(end of text)\n\n' + # --- Non-MSYS Windows code deleted --- + # raw, chop, horiz scroll 8 chars, disable buggy line chopping in MSYS + os.environ['LESS'] = (('--shift 8 -RS'),('-cR -#1'))[g.platform=='win'] + + 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) + p = 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(immed=False): + + if opt.quiet or g.no_license or opt.yes or not g.stdin_tty: + return + + import mmgen.contrib.license as gpl + msg(gpl.warning) + + from .term import get_char + prompt = "Press 'w' for conditions and warranty info, or 'c' to continue: " + while True: + reply = get_char(prompt, immed_chars=('','wc')[bool(immed)]) + if reply == 'w': + do_pager(gpl.conditions) + elif reply == 'c': + msg('') + break + else: + msg_r('\r') + msg('') diff --git a/mmgen/util.py b/mmgen/util.py index 638a18a1..97e740d0 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -17,7 +17,7 @@ # along with this program. If not, see . """ -util.py: Frequently-used variables, classes and functions for the MMGen suite +util.py: Frequently-used variables, classes and utility functions for the MMGen suite """ import sys,os,time,re @@ -188,10 +188,6 @@ def remove_dups(iterable,edesc='element',desc='list',quiet=False,hide=False): ret.append(e) return ret if type(iterable).__name__ == 'generator' else type(iterable)(ret) -def exit_if_mswin(feature): - if g.platform == 'win': - die(2, capfirst(feature) + ' not supported on the MSWin / MSYS2 platform' ) - def suf(arg,suf_type='s',verb='none'): suf_types = { 'none': { @@ -334,32 +330,9 @@ def compare_or_die(val1, desc1, val2, desc2, e='Error'): dmsg(f'{capfirst(desc2)} OK ({val2})') return True -def check_wallet_extension(fn): - from .wallet import get_wallet_data - get_wallet_data( ext=get_extension(fn), die_on_fail=True ) # raises exception on failure - def make_full_path(outdir,outfile): return os.path.normpath(os.path.join(outdir, os.path.basename(outfile))) -def confirm_or_raise(message,action,expect='YES',exit_msg='Exiting at user request'): - if message: - msg(message) - if line_input( - (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(prompt): - words = line_input(prompt, echo=opt.echo_passphrase).split() - dmsg('Sanitized input: [{}]'.format(' '.join(words))) - return words - -def get_data_from_user(desc='data'): # user input MUST be UTF-8 - data = line_input(f'Enter {desc}: ',echo=opt.echo_passphrase) - dmsg(f'User input: [{data}]') - return data - class oneshot_warning: color = 'nocolor' @@ -395,129 +368,12 @@ class oneshot_warning_group(oneshot_warning): def __init__(self,wcls,div=None,fmt_args=[],reverse=False): self.do(getattr(self,wcls),div,fmt_args,reverse) -def line_input(prompt,echo=True,insert_txt=''): - """ - 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 init_readline(): - try: - import readline - except ImportError: - return False - else: - if insert_txt: - readline.set_startup_hook(lambda: readline.insert_text(insert_txt)) - return True - else: - return False - - if not sys.stdout.isatty(): - msg_r(prompt) - prompt = '' - - from .term import kb_hold_protect - kb_hold_protect() - - if g.test_suite_popen_spawn: - msg(prompt) - sys.stderr.flush() - reply = os.read(0,4096).decode().rstrip('\n') # strip NL to mimic behavior of input() - elif echo or not sys.stdin.isatty(): - clear_buffer = init_readline() if sys.stdin.isatty() else False - reply = input(prompt) - if clear_buffer: - import readline - readline.set_startup_hook(lambda: readline.insert_text('')) - else: - from getpass import getpass - if g.platform == 'win': - # MSWin hack - getpass('foo') doesn't flush stderr - msg_r(prompt.strip()) # getpass('') adds a space - sys.stderr.flush() - reply = getpass('') - else: - reply = getpass(prompt) - - kb_hold_protect() - - return reply.strip() - -def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False,complete_prompt=False): - - 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 g.accept_defaults: - msg(prompt) - return default_yes - - from .term import get_char - while True: - reply = get_char(prompt,immed_chars='yYnN').strip('\n\r') - if not reply: - msg_r(nl) - return True if default_yes else False - elif reply in 'yYnN': - msg_r(nl) - return True if reply in 'yY' else False - else: - msg_r('\nInvalid reply\n' if verbose else '\r') - def stdout_or_pager(s): - (do_pager if opt.pager else Msg_r)(s) - -def do_pager(text): - - pagers = ['less','more'] - end_msg = '\n(end of text)\n\n' - # --- Non-MSYS Windows code deleted --- - # raw, chop, horiz scroll 8 chars, disable buggy line chopping in MSYS - os.environ['LESS'] = (('--shift 8 -RS'),('-cR -#1'))[g.platform=='win'] - - 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) - p = run([pager],input=m.encode(),check=True) - msg_r('\r') - except: - pass - else: - break + if opt.pager: + from .ui import do_pager + do_pager(s) else: - Msg(text+end_msg) - set_vt100() - -def do_license_msg(immed=False): - - if opt.quiet or g.no_license or opt.yes or not g.stdin_tty: - return - - import mmgen.contrib.license as gpl - msg(gpl.warning) - - from .term import get_char - prompt = "Press 'w' for conditions and warranty info, or 'c' to continue: " - while True: - reply = get_char(prompt, immed_chars=('','wc')[bool(immed)]) - if reply == 'w': - do_pager(gpl.conditions) - elif reply == 'c': - msg('') - break - else: - msg_r('\r') - msg('') + Msg_r(s) def get_subclasses(cls,names=False): def gen(cls): @@ -558,3 +414,7 @@ def wrap_ripemd160(called=[]): hashlib_new = hashlib.new hashlib.new = hashlib_new_wrapper called.append(True) + +def exit_if_mswin(feature): + if g.platform == 'win': + die(2, capfirst(feature) + ' not supported on the MSWin / MSYS2 platform' ) diff --git a/mmgen/wallet/__init__.py b/mmgen/wallet/__init__.py index dbf62ab8..ae33fd0e 100755 --- a/mmgen/wallet/__init__.py +++ b/mmgen/wallet/__init__.py @@ -153,3 +153,6 @@ def Wallet( passwd_file = passwd_file ) return me + +def check_wallet_extension(fn): + get_wallet_data( ext=get_extension(fn), die_on_fail=True ) # raises exception on failure diff --git a/mmgen/wallet/base.py b/mmgen/wallet/base.py index 236bfdb0..07fc436c 100755 --- a/mmgen/wallet/base.py +++ b/mmgen/wallet/base.py @@ -16,7 +16,7 @@ import os from ..globalvars import g from ..opts import opt -from ..util import msg,qmsg,die,get_data_from_user +from ..util import msg,qmsg,die from ..objmethods import MMGenObject from . import Wallet,wallet_data,get_wallet_cls @@ -82,6 +82,7 @@ class wallet(MMGenObject,metaclass=WalletMeta): self.fmt_data = self._get_data_from_user(self.desc) def _get_data_from_user(self,desc): + from ..ui import get_data_from_user return get_data_from_user(desc) def _deformat_once(self): diff --git a/mmgen/wallet/dieroll.py b/mmgen/wallet/dieroll.py index 2ad196ae..d1d145b0 100755 --- a/mmgen/wallet/dieroll.py +++ b/mmgen/wallet/dieroll.py @@ -15,7 +15,7 @@ wallet.dieroll: dieroll wallet class import time from ..globalvars import g from ..opts import opt -from ..util import msg,msg_r,die,fmt,remove_whitespace,keypress_confirm +from ..util import msg,msg_r,die,fmt,remove_whitespace from ..util2 import block_format from ..seed import Seed from ..baseconv import baseconv @@ -54,6 +54,7 @@ class wallet(wallet): seed_bytes = bc.tobytes( d, pad='seed' )[-seed_len:] if self.interactive_input and opt.usr_randchars: + from ..ui import keypress_confirm if keypress_confirm(self.user_entropy_prompt): from ..crypto import add_user_random seed_bytes = add_user_random( @@ -70,6 +71,7 @@ class wallet(wallet): def _get_data_from_user(self,desc): if not g.stdin_tty: + from ..ui import get_data_from_user return get_data_from_user(desc) bc = baseconv('b6d') diff --git a/mmgen/wallet/incog_base.py b/mmgen/wallet/incog_base.py index af645dcf..e101f2b9 100755 --- a/mmgen/wallet/incog_base.py +++ b/mmgen/wallet/incog_base.py @@ -15,7 +15,7 @@ wallet.incog_base: incognito wallet base class from ..globalvars import g from ..opts import opt from ..seed import Seed -from ..util import msg,vmsg,qmsg,make_chksum_8,keypress_confirm +from ..util import msg,vmsg,qmsg,make_chksum_8 from .enc import wallet import mmgen.crypto as crypto @@ -146,6 +146,7 @@ class wallet(wallet): def _verify_seed_oldfmt(self,seed): m = f'Seed ID: {make_chksum_8(seed)}. Is the Seed ID correct?' + from ..ui import keypress_confirm if keypress_confirm(m, True): return seed else: diff --git a/mmgen/wallet/incog_hidden.py b/mmgen/wallet/incog_hidden.py index 4b4347cf..95c198e2 100755 --- a/mmgen/wallet/incog_hidden.py +++ b/mmgen/wallet/incog_hidden.py @@ -17,17 +17,7 @@ import os from ..globalvars import g from ..opts import opt from ..seed import Seed -from ..util import ( - msg, - dmsg, - qmsg, - die, - compare_or_die, - keypress_confirm, - line_input, - capfirst, - confirm_or_raise -) +from ..util import msg,dmsg,qmsg,die,compare_or_die,capfirst from ..util2 import parse_bytespec from .incog_base import wallet @@ -108,6 +98,7 @@ class wallet(wallet): try: os.stat(fn) except: + from ..ui import keypress_confirm,line_input if keypress_confirm( f'Requested file {fn!r} does not exist. Create?', default_yes = True ): @@ -136,6 +127,7 @@ class wallet(wallet): if check_offset: self._check_valid_offset(f,'write') if not opt.quiet: + from ..ui import confirm_or_raise confirm_or_raise( message = '', action = f'alter file {f.name!r}' ) diff --git a/mmgen/wallet/mmgen.py b/mmgen/wallet/mmgen.py index 040d3f74..d8e7ab98 100755 --- a/mmgen/wallet/mmgen.py +++ b/mmgen/wallet/mmgen.py @@ -17,7 +17,7 @@ import os from ..globalvars import g from ..opts import opt from ..seed import Seed -from ..util import msg,qmsg,line_input,make_timestamp,make_chksum_6,split_into_cols,is_chksum_6,compare_chksums +from ..util import msg,qmsg,make_timestamp,make_chksum_6,split_into_cols,is_chksum_6,compare_chksums from ..obj import MMGenWalletLabel,get_obj from ..baseconv import baseconv @@ -43,6 +43,7 @@ class wallet(wallet): prompt = 'Enter a wallet label, or hit ENTER {}: '.format( 'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else 'for no label' ) + from ..ui import line_input while True: ret = line_input(prompt) if ret: diff --git a/mmgen/wallet/mnemonic.py b/mmgen/wallet/mnemonic.py index d068877b..15f0d1ca 100755 --- a/mmgen/wallet/mnemonic.py +++ b/mmgen/wallet/mnemonic.py @@ -14,7 +14,7 @@ wallet.mnemonic: MMGen mnemonic wallet base class from ..globalvars import g from ..baseconv import baseconv -from ..util import msg,compare_or_die,get_data_from_user +from ..util import msg,compare_or_die from ..seed import Seed from .unenc import wallet @@ -32,6 +32,7 @@ class wallet(wallet): def _get_data_from_user(self,desc): if not g.stdin_tty: + from ..ui import get_data_from_user return get_data_from_user(desc) from ..mn_entry import mn_entry # import here to catch cfg var errors diff --git a/mmgen/wallet/unenc.py b/mmgen/wallet/unenc.py index a9ef9472..e3b9740a 100755 --- a/mmgen/wallet/unenc.py +++ b/mmgen/wallet/unenc.py @@ -14,7 +14,7 @@ wallet.unenc: unencrypted wallet base class from ..globalvars import g from ..color import blue,yellow -from ..util import msg,msg_r,capfirst,is_int,keypress_confirm +from ..util import msg,msg_r,capfirst,is_int from .base import wallet class wallet(wallet): @@ -53,5 +53,6 @@ class wallet(wallet): while True: usr_len = choose_len() prompt = self.choose_seedlen_confirm.format(usr_len) + from ..ui import keypress_confirm if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite): return usr_len diff --git a/mmgen/xmrwallet.py b/mmgen/xmrwallet.py index 905fe31d..4fd770ef 100755 --- a/mmgen/xmrwallet.py +++ b/mmgen/xmrwallet.py @@ -33,6 +33,7 @@ from .addrlist import KeyAddrList,AddrIdxList from .rpc import json_encoder from .proto.xmr.rpc import MoneroRPCClientRaw,MoneroWalletRPCClient from .proto.xmr.daemon import MoneroWalletDaemon +from .ui import keypress_confirm xmrwallet_uarg_info = ( lambda e,hp: { diff --git a/scripts/uninstall-mmgen.py b/scripts/uninstall-mmgen.py index d1fe730b..a6892c2c 100755 --- a/scripts/uninstall-mmgen.py +++ b/scripts/uninstall-mmgen.py @@ -83,6 +83,7 @@ if opt.list_paths: if not opt.no_prompt: m = 'Deleting the following paths and files:\n {}\nProceed?' + from mmgen.ui import keypress_confirm if not keypress_confirm(m.format('\n '.join(del_list))): die(1,'Exiting at user request') diff --git a/test/misc/password_entry.py b/test/misc/password_entry.py index 868b72c8..d3656783 100755 --- a/test/misc/password_entry.py +++ b/test/misc/password_entry.py @@ -12,5 +12,6 @@ cmd_args = opts.init({'text': { 'desc': '', 'usage':'', 'options':'-e, --echo-pa p = ('Enter passphrase: ','Enter passphrase (echoed): ')[bool(opt.echo_passphrase)] +from mmgen.ui import get_words_from_user pw = get_words_from_user(p) msg('Entered: {}'.format(' '.join(pw))) diff --git a/test/misc/term.py b/test/misc/term.py index f4106999..3bcf25a2 100755 --- a/test/misc/term.py +++ b/test/misc/term.py @@ -22,6 +22,7 @@ opts_data = { cmd_args = opts.init(opts_data) from mmgen.term import get_char,get_char_raw,get_terminal_size +from mmgen.ui import line_input,keypress_confirm,do_license_msg import mmgen.term as term_mod def cmsg(m): diff --git a/test/test.py b/test/test.py index e921241d..3df5b527 100755 --- a/test/test.py +++ b/test/test.py @@ -261,6 +261,7 @@ def list_cmds(): for cmd in sorted(utils): yield ' {:{w}} - {}'.format( cmd, utils[cmd], w=w ) + from mmgen.ui import do_pager do_pager('\n'.join(gen_output())) sys.exit(0) @@ -479,6 +480,7 @@ class CmdGroupMgr(object): for k,v in cls.cmd_subgroups.items(): yield ' + {} · {}'.format( cyan(k.ljust(max_w+1)), v[0] ) + from mmgen.ui import do_pager do_pager('\n'.join(gen_output())) Msg( '\n' + ' '.join(e[0] for e in ginfo) ) diff --git a/test/test_py_d/common.py b/test/test_py_d/common.py index 5e68442f..6b93ee29 100755 --- a/test/test_py_d/common.py +++ b/test/test_py_d/common.py @@ -78,6 +78,7 @@ def skip(name,reason=None): return 'skip' def confirm_continue(): + from mmgen.ui import keypress_confirm if keypress_confirm(blue('Continue? (Y/n): '),default_yes=True,complete_prompt=True): if opt.verbose or opt.exact_output: sys.stderr.write('\n') else: diff --git a/test/test_py_d/ts_xmrwallet.py b/test/test_py_d/ts_xmrwallet.py index 0303770b..780d01cd 100755 --- a/test/test_py_d/ts_xmrwallet.py +++ b/test/test_py_d/ts_xmrwallet.py @@ -169,6 +169,7 @@ class TestSuiteXMRWallet(TestSuiteBase): {' '.join(a+b2)} """,indent=' ',strip_char='\t')) + from mmgen.ui import keypress_confirm if keypress_confirm('Continue?'): start_proxy() else: