term.py 7.1 KB

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