term.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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.py: 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. class MMGenTerm(object):
  37. tdim = namedtuple('terminal_dimensions',['width','height'])
  38. @classmethod
  39. def init(cls):
  40. pass
  41. @classmethod
  42. def kb_hold_protect(cls):
  43. return None
  44. class MMGenTermLinux(MMGenTerm):
  45. @classmethod
  46. def init(cls):
  47. cls.stdin_fd = sys.stdin.fileno()
  48. cls.old_term = termios.tcgetattr(cls.stdin_fd)
  49. @classmethod
  50. def get_terminal_size(cls):
  51. try:
  52. ret = os.get_terminal_size()
  53. except:
  54. try:
  55. ret = (os.environ['COLUMNS'],os.environ['LINES'])
  56. except:
  57. ret = (80,25)
  58. return cls.tdim(*ret)
  59. @classmethod
  60. def kb_hold_protect(cls):
  61. if g.test_suite:
  62. return
  63. tty.setcbreak(cls.stdin_fd)
  64. timeout = 0.3
  65. while True:
  66. key = select([sys.stdin], [], [], timeout)[0]
  67. if key:
  68. sys.stdin.read(1)
  69. else:
  70. termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.old_term)
  71. break
  72. @classmethod
  73. def get_char(cls,prompt='',immed_chars='',prehold_protect=True,num_chars=5,sleep=None):
  74. """
  75. Use os.read(), not file.read(), to get a variable number of bytes without blocking.
  76. Request 5 bytes to cover escape sequences generated by F1, F2, .. Fn keys (5 bytes)
  77. as well as UTF8 chars (4 bytes max).
  78. """
  79. timeout = 0.3
  80. tty.setcbreak(cls.stdin_fd)
  81. if sleep:
  82. time.sleep(sleep)
  83. msg_r(prompt)
  84. if g.test_suite:
  85. prehold_protect = False
  86. while True:
  87. # Protect against held-down key before read()
  88. key = select([sys.stdin], [], [], timeout)[0]
  89. s = os.read(cls.stdin_fd,num_chars).decode()
  90. if prehold_protect and key:
  91. continue
  92. if s in immed_chars:
  93. break
  94. # Protect against long keypress
  95. key = select([sys.stdin], [], [], timeout)[0]
  96. if not key:
  97. break
  98. termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.old_term)
  99. return s
  100. @classmethod
  101. def get_char_raw(cls,prompt='',num_chars=5,sleep=None):
  102. tty.setcbreak(cls.stdin_fd)
  103. if sleep:
  104. time.sleep(sleep)
  105. msg_r(prompt)
  106. s = os.read(cls.stdin_fd,num_chars).decode()
  107. termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.old_term)
  108. return s
  109. class MMGenTermLinuxStub(MMGenTermLinux):
  110. @classmethod
  111. def init(cls):
  112. cls.stdin_fd = sys.stdin.fileno()
  113. @classmethod
  114. def get_char(cls,prompt='',immed_chars='',prehold_protect=None,num_chars=None,sleep=None):
  115. if sleep:
  116. time.sleep(0.1)
  117. msg_r(prompt)
  118. return sys.stdin.read(1)
  119. get_char_raw = get_char
  120. @classmethod
  121. def kb_hold_protect(cls):
  122. pass
  123. class MMGenTermMSWin(MMGenTerm):
  124. @classmethod
  125. def get_terminal_size(cls):
  126. import struct
  127. x,y = 0,0
  128. try:
  129. from ctypes import windll,create_string_buffer
  130. # handles - stdin: -10, stdout: -11, stderr: -12
  131. csbi = create_string_buffer(22)
  132. h = windll.kernel32.GetStdHandle(-12)
  133. res = windll.kernel32.GetConsoleScreenBufferInfo(h,csbi)
  134. assert res, 'failed to get console screen buffer info'
  135. left,top,right,bottom = struct.unpack('hhhhHhhhhhh', csbi.raw)[5:9]
  136. x = right - left + 1
  137. y = bottom - top + 1
  138. except:
  139. pass
  140. if x and y:
  141. return cls.tdim(x,y)
  142. else:
  143. msg(yellow('Warning: could not get terminal size. Using fallback dimensions.'))
  144. return cls.tdim(80,25)
  145. @classmethod
  146. def kb_hold_protect(cls):
  147. timeout = 0.5
  148. while True:
  149. hit_time = time.time()
  150. while True:
  151. if msvcrt.kbhit():
  152. msvcrt.getch()
  153. break
  154. if time.time() - hit_time > timeout:
  155. return
  156. @classmethod
  157. def get_char(cls,prompt='',immed_chars='',prehold_protect=True,num_chars=None,sleep=None):
  158. """
  159. always return a single character, ignore num_chars
  160. first character of 2-character sequence returned by F1-F12 keys is discarded
  161. prehold_protect is ignored
  162. """
  163. if sleep:
  164. time.sleep(sleep)
  165. msg_r(prompt)
  166. timeout = 0.5
  167. while True:
  168. if msvcrt.kbhit():
  169. ch = chr(msvcrt.getch()[0])
  170. if ch == '\x03':
  171. raise KeyboardInterrupt
  172. if ch in immed_chars:
  173. return ch
  174. hit_time = time.time()
  175. while True:
  176. if msvcrt.kbhit():
  177. break
  178. if time.time() - hit_time > timeout:
  179. return ch
  180. @classmethod
  181. def get_char_raw(cls,prompt='',num_chars=None,sleep=None):
  182. """
  183. always return a single character, ignore num_chars
  184. first character of 2-character sequence returned by F1-F12 keys is discarded
  185. """
  186. while True:
  187. if sleep:
  188. time.sleep(sleep)
  189. msg_r(prompt)
  190. ch = chr(msvcrt.getch()[0])
  191. if ch in '\x00\xe0': # first char of 2-char sequence for F1-F12 keys
  192. continue
  193. if ch == '\x03':
  194. raise KeyboardInterrupt
  195. return ch
  196. class MMGenTermMSWinStub(MMGenTermMSWin):
  197. @classmethod
  198. def get_char(cls,prompt='',immed_chars='',prehold_protect=None,num_chars=None,sleep=None):
  199. if sleep:
  200. time.sleep(0.1)
  201. msg_r(prompt)
  202. return os.read(0,1).decode()
  203. get_char_raw = get_char
  204. def init_term():
  205. term = {
  206. 'linux': (MMGenTermLinux if sys.stdin.isatty() else MMGenTermLinuxStub),
  207. 'mswin': (MMGenTermMSWin if sys.stdin.isatty() else MMGenTermMSWinStub),
  208. }[_platform]
  209. term.init()
  210. import mmgen.term as self
  211. for var in ('get_char','get_char_raw','kb_hold_protect','get_terminal_size'):
  212. setattr( self, var, getattr(term,var) )