term.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. term: Terminal classes for the MMGen suite
  20. """
  21. # TODO: reimplement as instance instead of class
  22. import sys,os,time
  23. from collections import namedtuple
  24. from .globalvars import gc
  25. from .util import msg,msg_r,die
  26. try:
  27. import tty,termios
  28. from select import select
  29. _platform = 'linux'
  30. except:
  31. try:
  32. import msvcrt
  33. _platform = 'mswin'
  34. except:
  35. die(2,'Unable to set terminal mode')
  36. if not sys.stdin.isatty():
  37. msvcrt.setmode(sys.stdin.fileno(),os.O_BINARY)
  38. _term_dimensions = namedtuple('terminal_dimensions',['width','height'])
  39. class MMGenTerm(object):
  40. @classmethod
  41. def register_cleanup(cls):
  42. pass
  43. @classmethod
  44. def init(cls,noecho=False):
  45. pass
  46. @classmethod
  47. def set(cls,*args,**kwargs):
  48. pass
  49. @classmethod
  50. def reset(cls):
  51. pass
  52. @classmethod
  53. def kb_hold_protect(cls):
  54. return None
  55. class MMGenTermLinux(MMGenTerm):
  56. @classmethod
  57. def register_cleanup(cls):
  58. if not hasattr(cls,'cleanup_registered'):
  59. import atexit
  60. atexit.register(
  61. lambda: termios.tcsetattr(
  62. cls.stdin_fd,
  63. termios.TCSADRAIN,
  64. cls.orig_term) )
  65. cls.cleanup_registered = True
  66. @classmethod
  67. def reset(cls):
  68. termios.tcsetattr( cls.stdin_fd, termios.TCSANOW, cls.orig_term )
  69. cls.cur_term = cls.orig_term
  70. @classmethod
  71. def set(cls,setting):
  72. d = {
  73. 'echo': lambda t: t[:3] + [t[3] | (termios.ECHO | termios.ECHONL)] + t[4:], # echo input chars
  74. 'noecho': lambda t: t[:3] + [t[3] & ~(termios.ECHO | termios.ECHONL)] + t[4:], # don’t echo input chars
  75. }
  76. termios.tcsetattr( cls.stdin_fd, termios.TCSANOW, d[setting](cls.cur_term) )
  77. cls.cur_term = termios.tcgetattr(cls.stdin_fd)
  78. @classmethod
  79. def init(cls,noecho=False):
  80. cls.stdin_fd = sys.stdin.fileno()
  81. cls.cur_term = termios.tcgetattr(cls.stdin_fd)
  82. if not hasattr(cls,'orig_term'):
  83. cls.orig_term = cls.cur_term
  84. if noecho:
  85. cls.set('noecho')
  86. @classmethod
  87. def get_terminal_size(cls):
  88. try:
  89. ret = os.get_terminal_size()
  90. except:
  91. try:
  92. ret = (os.environ['COLUMNS'],os.environ['LINES'])
  93. except:
  94. ret = (80,25)
  95. return _term_dimensions(*ret)
  96. @classmethod
  97. def kb_hold_protect(cls):
  98. if cls.cfg.hold_protect_disable:
  99. return
  100. tty.setcbreak(cls.stdin_fd)
  101. timeout = 0.3
  102. while True:
  103. key = select([sys.stdin], [], [], timeout)[0]
  104. if key:
  105. sys.stdin.read(1)
  106. else:
  107. termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.cur_term)
  108. break
  109. @classmethod
  110. def get_char(cls,prompt='',immed_chars='',prehold_protect=True,num_bytes=5):
  111. """
  112. Use os.read(), not file.read(), to get a variable number of bytes without blocking.
  113. Request 5 bytes to cover escape sequences generated by F1, F2, .. Fn keys (5 bytes)
  114. as well as UTF8 chars (4 bytes max).
  115. """
  116. timeout = 0.3
  117. tty.setcbreak(cls.stdin_fd)
  118. msg_r(prompt)
  119. if cls.cfg.hold_protect_disable:
  120. prehold_protect = False
  121. while True:
  122. # Protect against held-down key before read()
  123. key = select([sys.stdin], [], [], timeout)[0]
  124. s = os.read(cls.stdin_fd,num_bytes).decode()
  125. if prehold_protect and key:
  126. continue
  127. if s in immed_chars:
  128. break
  129. # Protect against long keypress
  130. key = select([sys.stdin], [], [], timeout)[0]
  131. if not key:
  132. break
  133. termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.cur_term)
  134. return s
  135. @classmethod
  136. def get_char_raw(cls,prompt='',num_bytes=5,**kwargs):
  137. tty.setcbreak(cls.stdin_fd)
  138. msg_r(prompt)
  139. s = os.read(cls.stdin_fd,num_bytes).decode()
  140. termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.cur_term)
  141. return s
  142. class MMGenTermLinuxStub(MMGenTermLinux):
  143. @classmethod
  144. def register_cleanup(cls):
  145. pass
  146. @classmethod
  147. def init(cls,noecho=False):
  148. cls.stdin_fd = sys.stdin.fileno()
  149. @classmethod
  150. def set(cls,*args,**kwargs):
  151. pass
  152. @classmethod
  153. def reset(cls):
  154. pass
  155. @classmethod
  156. def get_char(cls,prompt='',immed_chars='',prehold_protect=None,num_bytes=5):
  157. msg_r(prompt)
  158. return os.read(0,num_bytes).decode()
  159. get_char_raw = get_char
  160. @classmethod
  161. def kb_hold_protect(cls):
  162. pass
  163. class MMGenTermMSWin(MMGenTerm):
  164. @classmethod
  165. def get_terminal_size(cls):
  166. import struct
  167. x,y = 0,0
  168. try:
  169. from ctypes import windll,create_string_buffer
  170. # handles - stdin: -10, stdout: -11, stderr: -12
  171. csbi = create_string_buffer(22)
  172. h = windll.kernel32.GetStdHandle(-12)
  173. res = windll.kernel32.GetConsoleScreenBufferInfo(h,csbi)
  174. assert res, 'failed to get console screen buffer info'
  175. left,top,right,bottom = struct.unpack('hhhhHhhhhhh', csbi.raw)[5:9]
  176. x = right - left + 1
  177. y = bottom - top + 1
  178. except:
  179. pass
  180. if x and y:
  181. return _term_dimensions(x,y)
  182. else:
  183. msg(yellow('Warning: could not get terminal size. Using fallback dimensions.'))
  184. return _term_dimensions(80,25)
  185. @classmethod
  186. def kb_hold_protect(cls):
  187. timeout = 0.5
  188. while True:
  189. hit_time = time.time()
  190. while True:
  191. if msvcrt.kbhit():
  192. msvcrt.getch()
  193. break
  194. if time.time() - hit_time > timeout:
  195. return
  196. @classmethod
  197. def get_char(cls,prompt='',immed_chars='',prehold_protect=True,num_bytes=None):
  198. """
  199. always return a single character, ignore num_bytes
  200. first character of 2-character sequence returned by F1-F12 keys is discarded
  201. prehold_protect is ignored
  202. """
  203. msg_r(prompt)
  204. timeout = 0.5
  205. while True:
  206. if msvcrt.kbhit():
  207. ch = chr(msvcrt.getch()[0])
  208. if ch == '\x03':
  209. raise KeyboardInterrupt
  210. if ch in immed_chars:
  211. return ch
  212. hit_time = time.time()
  213. while True:
  214. if msvcrt.kbhit():
  215. break
  216. if time.time() - hit_time > timeout:
  217. return ch
  218. @classmethod
  219. def get_char_raw(cls,prompt='',num_bytes=None,**kwargs):
  220. """
  221. return single ASCII char or 2-char escape sequence, ignoring num_bytes
  222. """
  223. msg_r(prompt)
  224. ret = msvcrt.getch()
  225. if ret in (b'\x00',b'\xe0'): # first byte of 2-byte escape sequence
  226. return chr(ret[0]) + chr(msvcrt.getch()[0])
  227. if ret == b'\x03':
  228. raise KeyboardInterrupt
  229. return chr(ret[0])
  230. class MMGenTermMSWinStub(MMGenTermMSWin):
  231. @classmethod
  232. def get_char(cls,prompt='',immed_chars='',prehold_protect=None,num_bytes=None):
  233. """
  234. Use stdin to allow UTF-8 and emulate the one-character behavior of MMGenTermMSWin
  235. """
  236. msg_r(prompt)
  237. return sys.stdin.read(1)
  238. get_char_raw = get_char
  239. def get_term():
  240. return {
  241. 'linux': (MMGenTermLinux if sys.stdin.isatty() else MMGenTermLinuxStub),
  242. 'mswin': (MMGenTermMSWin if sys.stdin.isatty() else MMGenTermMSWinStub),
  243. }[_platform]
  244. def init_term(cfg,noecho=False):
  245. term = get_term()
  246. term.init(noecho=noecho)
  247. import mmgen.term as self
  248. for var in ('get_char','get_char_raw','kb_hold_protect','get_terminal_size'):
  249. setattr( self, var, getattr(term,var) )
  250. term.cfg = cfg # setting the _class_ attribute
  251. def reset_term():
  252. get_term().reset()