ui.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2022 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
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. ui: Interactive user interface functions for the MMGen suite
  12. """
  13. import sys,os
  14. from .globalvars import g
  15. from .opts import opt
  16. from .util import msg,msg_r,Msg,dmsg,die
  17. def confirm_or_raise(message,action,expect='YES',exit_msg='Exiting at user request'):
  18. if message:
  19. msg(message)
  20. if line_input(
  21. (f'{action} ' if action[0].isupper() else f'Are you sure you want to {action}?\n') +
  22. f'Type uppercase {expect!r} to confirm: '
  23. ).strip() != expect:
  24. die( 'UserNonConfirmation', exit_msg )
  25. def get_words_from_user(prompt):
  26. words = line_input(prompt, echo=opt.echo_passphrase).split()
  27. dmsg('Sanitized input: [{}]'.format(' '.join(words)))
  28. return words
  29. def get_data_from_user(desc='data'): # user input MUST be UTF-8
  30. data = line_input(f'Enter {desc}: ',echo=opt.echo_passphrase)
  31. dmsg(f'User input: [{data}]')
  32. return data
  33. def line_input(prompt,echo=True,insert_txt='',hold_protect=True):
  34. """
  35. multi-line prompts OK
  36. one-line prompts must begin at beginning of line
  37. empty prompts forbidden due to interactions with readline
  38. """
  39. assert prompt,'calling line_input() with an empty prompt forbidden'
  40. def get_readline():
  41. try:
  42. import readline
  43. return readline
  44. except ImportError:
  45. return False
  46. if not sys.stdout.isatty():
  47. msg_r(prompt)
  48. prompt = ''
  49. if hold_protect:
  50. from .term import kb_hold_protect
  51. kb_hold_protect()
  52. if g.test_suite_popen_spawn:
  53. msg(prompt)
  54. sys.stderr.flush()
  55. reply = os.read(0,4096).decode().rstrip('\n') # strip NL to mimic behavior of input()
  56. elif echo or not sys.stdin.isatty():
  57. readline = insert_txt and sys.stdin.isatty() and get_readline()
  58. if readline:
  59. readline.set_startup_hook(lambda: readline.insert_text(insert_txt))
  60. reply = input(prompt)
  61. if readline:
  62. readline.set_startup_hook(lambda: readline.insert_text(''))
  63. else:
  64. from getpass import getpass
  65. if g.platform == 'win':
  66. # MSYS2/MSWin hack - getpass('foo') doesn't flush stderr - TODO: has this been fixed?
  67. msg_r(prompt.strip()) # getpass('') adds a space
  68. sys.stderr.flush()
  69. reply = getpass('')
  70. else:
  71. reply = getpass(prompt)
  72. if hold_protect:
  73. kb_hold_protect()
  74. return reply.strip()
  75. def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False,complete_prompt=False):
  76. if not complete_prompt:
  77. prompt = '{} {}: '.format( prompt, '(Y/n)' if default_yes else '(y/N)' )
  78. nl = f'\r{" "*len(prompt)}\r' if no_nl else '\n'
  79. if g.accept_defaults:
  80. msg(prompt)
  81. return default_yes
  82. from .term import get_char
  83. while True:
  84. reply = get_char(prompt,immed_chars='yYnN').strip('\n\r')
  85. if not reply:
  86. msg_r(nl)
  87. return True if default_yes else False
  88. elif reply in 'yYnN':
  89. msg_r(nl)
  90. return True if reply in 'yY' else False
  91. else:
  92. msg_r('\nInvalid reply\n' if verbose else '\r')
  93. def do_pager(text):
  94. pagers = ['less','more']
  95. end_msg = '\n(end of text)\n\n'
  96. # --- Non-MSYS Windows code deleted ---
  97. # raw, chop, horiz scroll 8 chars, disable buggy line chopping in MSYS
  98. os.environ['LESS'] = (('--shift 8 -RS'),('-cR -#1'))[g.platform=='win']
  99. if 'PAGER' in os.environ and os.environ['PAGER'] != pagers[0]:
  100. pagers = [os.environ['PAGER']] + pagers
  101. from subprocess import run
  102. from .color import set_vt100
  103. for pager in pagers:
  104. try:
  105. m = text + ('' if pager == 'less' else end_msg)
  106. p = run([pager],input=m.encode(),check=True)
  107. msg_r('\r')
  108. except:
  109. pass
  110. else:
  111. break
  112. else:
  113. Msg(text+end_msg)
  114. set_vt100()
  115. def do_license_msg(immed=False):
  116. if opt.quiet or g.no_license or opt.yes or not g.stdin_tty:
  117. return
  118. import mmgen.contrib.license as gpl
  119. msg(gpl.warning)
  120. from .term import get_char
  121. prompt = "Press 'w' for conditions and warranty info, or 'c' to continue: "
  122. while True:
  123. reply = get_char(prompt, immed_chars=('','wc')[bool(immed)])
  124. if reply == 'w':
  125. do_pager(gpl.conditions)
  126. elif reply == 'c':
  127. msg('')
  128. break
  129. else:
  130. msg_r('\r')
  131. msg('')