123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- #!/usr/bin/env python3
- #
- # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
- # Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- """
- term: Terminal classes for the MMGen suite
- """
- # TODO: reimplement as instance instead of class
- import sys,os,time
- from collections import namedtuple
- from .util import msg,msg_r,die
- if sys.platform == 'linux':
- import tty,termios
- from select import select
- elif sys.platform == 'win32':
- try:
- import msvcrt
- except:
- die(2,'Unable to set terminal mode')
- if not sys.stdin.isatty():
- msvcrt.setmode(sys.stdin.fileno(),os.O_BINARY)
- else:
- die(2,f'{sys.platform!r}: unsupported platform')
- _term_dimensions = namedtuple('terminal_dimensions',['width','height'])
- class MMGenTerm:
- @classmethod
- def register_cleanup(cls):
- pass
- @classmethod
- def init(cls,noecho=False):
- pass
- @classmethod
- def set(cls,*args,**kwargs):
- pass
- @classmethod
- def reset(cls):
- pass
- @classmethod
- def kb_hold_protect(cls):
- return None
- class MMGenTermLinux(MMGenTerm):
- @classmethod
- def register_cleanup(cls):
- if not hasattr(cls,'cleanup_registered'):
- import atexit
- atexit.register(
- lambda: termios.tcsetattr(
- cls.stdin_fd,
- termios.TCSADRAIN,
- cls.orig_term) )
- cls.cleanup_registered = True
- @classmethod
- def reset(cls):
- termios.tcsetattr( cls.stdin_fd, termios.TCSANOW, cls.orig_term )
- cls.cur_term = cls.orig_term
- @classmethod
- def set(cls,setting):
- d = {
- 'echo': lambda t: t[:3] + [t[3] | (termios.ECHO | termios.ECHONL)] + t[4:], # echo input chars
- 'noecho': lambda t: t[:3] + [t[3] & ~(termios.ECHO | termios.ECHONL)] + t[4:], # don’t echo input chars
- }
- termios.tcsetattr( cls.stdin_fd, termios.TCSANOW, d[setting](cls.cur_term) )
- cls.cur_term = termios.tcgetattr(cls.stdin_fd)
- @classmethod
- def init(cls,noecho=False):
- cls.stdin_fd = sys.stdin.fileno()
- cls.cur_term = termios.tcgetattr(cls.stdin_fd)
- if not hasattr(cls,'orig_term'):
- cls.orig_term = cls.cur_term
- if noecho:
- cls.set('noecho')
- @classmethod
- def get_terminal_size(cls):
- try:
- ret = os.get_terminal_size()
- except:
- try:
- ret = (
- int(os.environ['COLUMNS']),
- int(os.environ['LINES']) )
- except:
- ret = (80,25)
- return _term_dimensions(*ret)
- @classmethod
- def kb_hold_protect(cls):
- if cls.cfg.hold_protect_disable:
- return
- tty.setcbreak(cls.stdin_fd)
- timeout = 0.3
- while True:
- key = select([sys.stdin], [], [], timeout)[0]
- if key:
- sys.stdin.read(1)
- else:
- termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.cur_term)
- break
- @classmethod
- def get_char(cls,prompt='',immed_chars='',prehold_protect=True,num_bytes=5):
- """
- Use os.read(), not file.read(), to get a variable number of bytes without blocking.
- Request 5 bytes to cover escape sequences generated by F1, F2, .. Fn keys (5 bytes)
- as well as UTF8 chars (4 bytes max).
- """
- timeout = 0.3
- tty.setcbreak(cls.stdin_fd)
- msg_r(prompt)
- if cls.cfg.hold_protect_disable:
- prehold_protect = False
- while True:
- # Protect against held-down key before read()
- key = select([sys.stdin], [], [], timeout)[0]
- s = os.read(cls.stdin_fd,num_bytes).decode()
- if prehold_protect and key:
- continue
- if s in immed_chars:
- break
- # Protect against long keypress
- key = select([sys.stdin], [], [], timeout)[0]
- if not key:
- break
- termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.cur_term)
- return s
- @classmethod
- def get_char_raw(cls,prompt='',num_bytes=5,**kwargs):
- tty.setcbreak(cls.stdin_fd)
- msg_r(prompt)
- s = os.read(cls.stdin_fd,num_bytes).decode()
- termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.cur_term)
- return s
- class MMGenTermLinuxStub(MMGenTermLinux):
- @classmethod
- def register_cleanup(cls):
- pass
- @classmethod
- def init(cls,noecho=False):
- cls.stdin_fd = sys.stdin.fileno()
- @classmethod
- def set(cls,*args,**kwargs):
- pass
- @classmethod
- def reset(cls):
- pass
- @classmethod
- def get_char(cls,prompt='',immed_chars='',prehold_protect=None,num_bytes=5):
- msg_r(prompt)
- return os.read(0,num_bytes).decode()
- get_char_raw = get_char
- @classmethod
- def kb_hold_protect(cls):
- pass
- class MMGenTermMSWin(MMGenTerm):
- @classmethod
- def get_terminal_size(cls):
- import struct
- x,y = 0,0
- try:
- from ctypes import windll,create_string_buffer
- # handles - stdin: -10, stdout: -11, stderr: -12
- csbi = create_string_buffer(22)
- h = windll.kernel32.GetStdHandle(-12)
- res = windll.kernel32.GetConsoleScreenBufferInfo(h,csbi)
- assert res, 'failed to get console screen buffer info'
- left,top,right,bottom = struct.unpack('hhhhHhhhhhh', csbi.raw)[5:9]
- x = right - left + 1
- y = bottom - top + 1
- except:
- pass
- if x and y:
- return _term_dimensions(x,y)
- else:
- from .color import yellow
- msg(yellow('Warning: could not get terminal size. Using fallback dimensions.'))
- return _term_dimensions(80,25)
- @classmethod
- def kb_hold_protect(cls):
- timeout = 0.5
- while True:
- hit_time = time.time()
- while True:
- if msvcrt.kbhit():
- msvcrt.getch()
- break
- if time.time() - hit_time > timeout:
- return
- @classmethod
- def get_char(cls,prompt='',immed_chars='',prehold_protect=True,num_bytes=None):
- """
- always return a single character, ignore num_bytes
- first character of 2-character sequence returned by F1-F12 keys is discarded
- prehold_protect is ignored
- """
- msg_r(prompt)
- timeout = 0.5
- while True:
- if msvcrt.kbhit():
- ch = chr(msvcrt.getch()[0])
- if ch == '\x03':
- raise KeyboardInterrupt
- if ch in immed_chars:
- return ch
- hit_time = time.time()
- while True:
- if msvcrt.kbhit():
- break
- if time.time() - hit_time > timeout:
- return ch
- @classmethod
- def get_char_raw(cls,prompt='',num_bytes=None,**kwargs):
- """
- return single ASCII char or 2-char escape sequence, ignoring num_bytes
- """
- msg_r(prompt)
- ret = msvcrt.getch()
- if ret in (b'\x00',b'\xe0'): # first byte of 2-byte escape sequence
- return chr(ret[0]) + chr(msvcrt.getch()[0])
- if ret == b'\x03':
- raise KeyboardInterrupt
- return chr(ret[0])
- class MMGenTermMSWinStub(MMGenTermMSWin):
- @classmethod
- def get_char(cls,prompt='',immed_chars='',prehold_protect=None,num_bytes=None):
- """
- Use stdin to allow UTF-8 and emulate the one-character behavior of MMGenTermMSWin
- """
- msg_r(prompt)
- while True:
- try:
- return sys.stdin.read(1)
- except:
- msg('[read error, trying again]')
- time.sleep(0.5)
- get_char_raw = get_char
- def get_term():
- return {
- 'linux': (MMGenTermLinux if sys.stdin.isatty() else MMGenTermLinuxStub),
- 'win32': (MMGenTermMSWin if sys.stdin.isatty() else MMGenTermMSWinStub),
- }[sys.platform]
- def init_term(cfg,noecho=False):
- term = get_term()
- term.init(noecho=noecho)
- from . import term as self
- for var in ('get_char','get_char_raw','kb_hold_protect','get_terminal_size'):
- setattr( self, var, getattr(term,var) )
- term.cfg = cfg # setting the _class_ attribute
- def reset_term():
- get_term().reset()
|