Added a mini curses class and generic-multi-select (#362)

* Added a mini curses class. It can do some simple tricks to iterate over menu options and indicate which ones are chosen using generic_multi_select().

* Include the default parameter if set.

* Modified 'select_kernel()' to use the new multi-select.

* Sneaky character got in.

* removed some debugging

* removed some debugging

* Spelling error

* Adding error handling and loop support.

* Enforce that 'default' is always selected if no other option is selected.

* Fixed backspace issues and ghosting.

Co-authored-by: Anton Hvornum <anton.feeds@gmail.com>
This commit is contained in:
Anton Hvornum 2021-04-28 13:18:28 +00:00 committed by GitHub
parent 754e4b8b61
commit 4079eebc70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 170 additions and 9 deletions

View File

@ -221,9 +221,6 @@ class Partition():
@encrypted.setter
def encrypted(self, value :bool):
if value:
log(f'Marking {self} as encrypted: {value}', level=logging.DEBUG)
log(f"Callstrack when marking the partition: {''.join(traceback.format_stack())}", level=logging.DEBUG)
self._encrypted = value

View File

@ -82,7 +82,6 @@ class Script():
self.examples = None
self.namespace = os.path.splitext(os.path.basename(self.path))[0]
self.original_namespace = self.namespace
log(f"Script {self} has been loaded with namespace '{self.namespace}'", level=logging.DEBUG)
def __enter__(self, *args, **kwargs):
self.execute()

View File

@ -1,5 +1,6 @@
import getpass, pathlib, os, shutil, re
import getpass, pathlib, os, shutil, re, time
import sys, time, signal, ipaddress, logging
import termios, tty, select # Used for char by char polling of sys.stdin
from .exceptions import *
from .profiles import Profile
from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, search_keyboard_layout
@ -95,6 +96,173 @@ def print_large_list(options, padding=5, margin_bottom=0, separator=': '):
print(f"{str(column): >{highest_index_number_length}}{separator}{options[column]}", end = spaces)
print()
return column, row
def generic_multi_select(options, text="Select one or more of the options above (leave blank to continue): ", sort=True, default=None, allow_empty=False):
if sort:
options = sorted(options)
section = MiniCurses(get_terminal_width(), len(options))
selected_options = []
while True:
if len(selected_options) <= 0 and default and default in options:
selected_options.append(default)
printed_options = []
for option in options:
if option in selected_options:
printed_options.append(f'>> {option}')
else:
printed_options.append(f'{option}')
section.clear(0, get_terminal_height()-section._cursor_y-1)
x, y = print_large_list(printed_options, margin_bottom=2)
section._cursor_y = len(printed_options)
section._cursor_x = 0
section.write_line(text)
section.input_pos = section._cursor_x
selected_option = section.get_keyboard_input(end=None)
if selected_option is None:
if len(selected_options) <= 0 and default:
selected_options = [default]
if len(selected_options) or allow_empty is True:
break
else:
log('* Need to select at least one option!', fg='red')
continue
elif selected_option.isdigit():
if (selected_option := int(selected_option)) >= len(options):
log('* Option is out of range, please select another one!', fg='red')
continue
selected_option = options[selected_option]
if selected_option in selected_options:
selected_options.remove(selected_option)
else:
selected_options.append(selected_option)
return selected_options
class MiniCurses():
def __init__(self, width, height):
self.width = width
self.height = height
self._cursor_y = 0
self._cursor_x = 0
self.input_pos = 0
def write_line(self, text, clear_line=True):
if clear_line:
sys.stdout.flush()
sys.stdout.write("\033[%dG" % 0)
sys.stdout.flush()
sys.stdout.write(" " * (get_terminal_width()-1))
sys.stdout.flush()
sys.stdout.write("\033[%dG" % 0)
sys.stdout.flush()
sys.stdout.write(text)
sys.stdout.flush()
self._cursor_x += len(text)
def clear(self, x, y):
if x < 0: x = 0
if y < 0: y = 0
#import time
#sys.stdout.write(f"Clearing from: {x, y}")
#sys.stdout.flush()
#time.sleep(2)
sys.stdout.flush()
sys.stdout.write('\033[%d;%df' % (y, x))
for line in range(get_terminal_height()-y-1, y):
sys.stdout.write(" " * (get_terminal_width()-1))
sys.stdout.flush()
sys.stdout.write('\033[%d;%df' % (y, x))
sys.stdout.flush()
def deal_with_control_characters(self, char):
mapper = {
'\x7f' : 'BACKSPACE',
'\r' : 'CR',
'\n' : 'NL'
}
if (mapped_char := mapper.get(char, None)) == 'BACKSPACE':
if self._cursor_x <= self.input_pos:
# Don't backspace futher back than the cursor start position during input
return True
# Move back to the current known position (BACKSPACE doesn't updated x-pos)
sys.stdout.flush()
sys.stdout.write("\033[%dG" % (self._cursor_x))
sys.stdout.flush()
# Write a blank space
sys.stdout.flush()
sys.stdout.write(" ")
sys.stdout.flush()
# And move back again
sys.stdout.flush()
sys.stdout.write("\033[%dG" % (self._cursor_x))
sys.stdout.flush()
self._cursor_x -= 1
return True
elif mapped_char in ('CR', 'NL'):
return True
return None
def get_keyboard_input(self, strip_rowbreaks=True, end='\n'):
assert end in ['\r', '\n', None]
poller = select.epoll()
response = ''
sys_fileno = sys.stdin.fileno()
old_settings = termios.tcgetattr(sys_fileno)
tty.setraw(sys_fileno)
poller.register(sys.stdin.fileno(), select.EPOLLIN)
EOF = False
while EOF is False:
for fileno, event in poller.poll(0.025):
char = sys.stdin.read(1)
#sys.stdout.write(f"{[char]}")
#sys.stdout.flush()
if (newline := (char in ('\n', '\r'))):
EOF = True
if not newline or strip_rowbreaks is False:
response += char
if self.deal_with_control_characters(char) is not True:
self.write_line(response[-1], clear_line=False)
termios.tcsetattr(sys_fileno, termios.TCSADRAIN, old_settings)
if end:
sys.stdout.write(end)
sys.stdout.flush()
self._cursor_x = 0
self._cursor_y += 1
if response:
return response
def ask_for_superuser_account(prompt='Username for required super-user with sudo privileges: ', forced=False):
while 1:
new_user = input(prompt).strip(' ')
@ -523,9 +691,6 @@ def select_kernel(options):
kernels = sorted(list(options))
if kernels:
selected_kernels = generic_select(kernels, f"Choose which kernel to use (leave blank for default: {DEFAULT_KERNEL}): ")
if not selected_kernels:
return DEFAULT_KERNEL
return selected_kernels
return generic_multi_select(kernels, f"Choose which kernel to use (leave blank for default: {DEFAULT_KERNEL}): ", default=DEFAULT_KERNEL)
raise RequirementError("Selecting kernels require a least one kernel to be given as an option.")