ui.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. ui: Interactive user interface functions for the MMGen suite
  12. """
  13. import sys, os
  14. from .util import msg, msg_r, Msg, die
  15. def confirm_or_raise(cfg, message, action, *, expect='YES', exit_msg='Exiting at user request'):
  16. if message:
  17. msg(message)
  18. if line_input(
  19. cfg,
  20. (f'{action} ' if action[0].isupper() else f'Are you sure you want to {action}?\n') +
  21. f'Type uppercase {expect!r} to confirm: '
  22. ).strip() != expect:
  23. die('UserNonConfirmation', exit_msg)
  24. def get_words_from_user(cfg, prompt):
  25. words = line_input(cfg, prompt, echo=cfg.echo_passphrase).split()
  26. if cfg.debug:
  27. msg('Sanitized input: [{}]'.format(' '.join(words)))
  28. return words
  29. def get_data_from_user(cfg, *, desc='data'): # user input MUST be UTF-8
  30. data = line_input(cfg, f'Enter {desc}: ', echo=cfg.echo_passphrase)
  31. if cfg.debug:
  32. msg(f'User input: [{data}]')
  33. return data
  34. def line_input(cfg, prompt, *, echo=True, insert_txt='', hold_protect=True):
  35. """
  36. multi-line prompts OK
  37. one-line prompts must begin at beginning of line
  38. empty prompts forbidden due to interactions with readline
  39. """
  40. assert prompt, 'calling line_input() with an empty prompt forbidden'
  41. def get_readline():
  42. try:
  43. import readline
  44. return readline
  45. except ImportError:
  46. return False
  47. if hold_protect:
  48. from .term import kb_hold_protect
  49. kb_hold_protect()
  50. if cfg.test_suite_popen_spawn:
  51. msg(prompt)
  52. sys.stderr.flush() # required by older Pythons (e.g. v3.7)
  53. reply = os.read(0, 4096).decode().rstrip('\n') # strip NL to mimic behavior of input()
  54. elif not sys.stdin.isatty():
  55. msg_r(prompt)
  56. reply = input('')
  57. elif echo:
  58. readline = get_readline()
  59. if readline and insert_txt:
  60. readline.set_startup_hook(lambda: readline.insert_text(insert_txt))
  61. reply = input(prompt)
  62. if readline and insert_txt:
  63. readline.set_startup_hook(lambda: readline.insert_text(''))
  64. else:
  65. from getpass import getpass
  66. reply = getpass(prompt)
  67. if hold_protect:
  68. kb_hold_protect()
  69. return reply.strip()
  70. def keypress_confirm(
  71. cfg,
  72. prompt,
  73. *,
  74. default_yes = False,
  75. verbose = False,
  76. no_nl = False,
  77. complete_prompt = False,
  78. do_exit = False,
  79. exit_msg = 'Exiting at user request'):
  80. def do_return(retval):
  81. if do_exit and not retval:
  82. die(1, exit_msg)
  83. return retval
  84. if not complete_prompt:
  85. prompt = '{} {}: '.format(prompt, '(Y/n)' if default_yes else '(y/N)')
  86. nl = f'\r{" "*len(prompt)}\r' if no_nl else '\n'
  87. if cfg.accept_defaults:
  88. msg(prompt)
  89. return do_return(default_yes)
  90. from .term import get_char
  91. while True:
  92. reply = get_char(prompt, immed_chars='yYnN').strip('\n\r')
  93. if not reply:
  94. msg_r(nl)
  95. return do_return(default_yes)
  96. elif reply in 'yYnN':
  97. msg_r(nl)
  98. return do_return(reply in 'yY')
  99. else:
  100. msg_r('\nInvalid reply\n' if verbose else '\r')
  101. def do_pager(text):
  102. pagers = ['less', 'more']
  103. end_msg = '\n(end of text)\n\n'
  104. os.environ['LESS'] = '--jump-target=2 --shift=4 --tabs=4 --RAW-CONTROL-CHARS --chop-long-lines'
  105. if 'PAGER' in os.environ and os.environ['PAGER'] != pagers[0]:
  106. pagers = [os.environ['PAGER']] + pagers
  107. from subprocess import run
  108. from .color import set_vt100
  109. for pager in pagers:
  110. try:
  111. m = text + ('' if pager == 'less' else end_msg)
  112. run([pager], input=m.encode(), check=True)
  113. msg_r('\r')
  114. except:
  115. pass
  116. else:
  117. break
  118. else:
  119. Msg(text+end_msg)
  120. set_vt100()
  121. def do_license_msg(cfg, *, immed=False):
  122. if cfg.quiet or cfg.no_license or cfg.yes or not cfg.stdin_tty:
  123. return
  124. from .contrib import license as gpl
  125. from .cfg import gc
  126. msg(gpl.warning.format(gc=gc))
  127. from .term import get_char
  128. prompt = "Press 'w' for conditions and warranty info, or 'c' to continue: "
  129. while True:
  130. reply = get_char(prompt, immed_chars=('', 'wc')[bool(immed)])
  131. if reply == 'w':
  132. do_pager(gpl.conditions)
  133. elif reply == 'c':
  134. msg('')
  135. break
  136. else:
  137. msg_r('\r')
  138. msg('')