diff --git a/cat_win/cat.py b/cat_win/cat.py index 91dc1090..bb2352d3 100644 --- a/cat_win/cat.py +++ b/cat_win/cat.py @@ -947,14 +947,14 @@ def main(): holder.set_temp_file_stdin(temp_file) else: if holder.args_id[ARGS_EDITOR]: - unknown_files = [file for file in unknown_files if Editor.open(file, arg_parser.file_encoding, stdinhelper.write_file, on_windows_os)] + unknown_files = [file for file in unknown_files if Editor.open(file, arg_parser.file_encoding, stdinhelper.write_file, on_windows_os, holder.args_id[ARGS_DEBUG])] else: unknown_files = stdinhelper.read_write_files_from_stdin( unknown_files, arg_parser.file_encoding, on_windows_os, holder.args_id[ARGS_ONELINE]) if holder.args_id[ARGS_EDITOR]: for file in known_files: - Editor.open(file, arg_parser.file_encoding, stdinhelper.write_file, on_windows_os) + Editor.open(file, arg_parser.file_encoding, stdinhelper.write_file, on_windows_os, holder.args_id[ARGS_DEBUG]) if len(known_files) + len(unknown_files) == 0: return diff --git a/cat_win/util/editor.py b/cat_win/util/editor.py index bcc35f51..9326f9f9 100644 --- a/cat_win/util/editor.py +++ b/cat_win/util/editor.py @@ -5,6 +5,380 @@ CURSES_MODULE_ERROR = True import sys +from cat_win.util.editorhelper import UNIFY_HOTKEYS, KEY_HOTKEYS, ACTION_HOTKEYS + + +class _Editor: + def __init__(self, file: str, file_encoding: str, debug_mode: bool) -> None: + self.curse_window = None + + self.file = file + self.file_encoding = file_encoding + self.line_sep = '\n' + self.window_content = [] + + self.debug_mode = debug_mode + + self.status_bar_size = 1 + self.error_bar = '' + self.unsaved_progress = False + self.changes_made = False + + self.cur_col = 0 + self.cur_row = 0 + self.x = 0 + self.y = 0 + + self._setup_file() + + def _setup_file(self) -> None: + try: + self.line_sep = Editor.get_newline(self.file) + with open(self.file, 'r', encoding=self.file_encoding) as f: + for line in f.read().split('\n'): + self.window_content.append([*line]) + except (OSError, UnicodeDecodeError) as e: + self.window_content.append([]) + self.status_bar_size = 2 + self.error_bar = str(e) + self.unsaved_progress = True + + def getxymax(self) -> tuple: + max_y, max_x = self.curse_window.getmaxyx() + return (max_y-self.status_bar_size, max_x) + + def _key_enter(self) -> bool: + new_line = self.window_content[self.cur_row][self.cur_col:] + self.window_content[self.cur_row] = self.window_content[self.cur_row][:self.cur_col] + self.cur_row += 1 + self.cur_col = 0 + self.window_content.insert(self.cur_row, [] + new_line) + self.unsaved_progress = True + + def _key_dc(self) -> None: + if self.cur_col < len(self.window_content[self.cur_row]): + del self.window_content[self.cur_row][self.cur_col] + self.unsaved_progress = True + elif self.cur_row < len(self.window_content)-1: + self.window_content[self.cur_row] += self.window_content[self.cur_row+1] + del self.window_content[self.cur_row+1] + self.unsaved_progress = True + + def _key_dl(self) -> None: + if self.cur_col == len(self.window_content[self.cur_row])-1: + self.window_content[self.cur_row] = self.window_content[self.cur_row][:self.cur_col] + self.unsaved_progress = True + elif self.cur_col < len(self.window_content[self.cur_row])-1: + cur_col = self.cur_col+1 + tp = self.window_content[self.cur_row][cur_col].isalnum() + while cur_col < len(self.window_content[self.cur_row]) and tp == self.window_content[self.cur_row][cur_col].isalnum(): + cur_col += 1 + self.window_content[self.cur_row] = self.window_content[self.cur_row][:self.cur_col] + self.window_content[self.cur_row][cur_col:] + self.unsaved_progress = True + elif self.cur_row < len(self.window_content)-1: + self.window_content[self.cur_row] += self.window_content[self.cur_row+1] + del self.window_content[self.cur_row+1] + self.unsaved_progress = True + + def _key_backspace(self) -> None: + if self.cur_col: # delete char + self.cur_col -= 1 + del self.window_content[self.cur_row][self.cur_col] + self.unsaved_progress = True + elif self.cur_row: # or delete line + line = self.window_content[self.cur_row] + del self.window_content[self.cur_row] + self.cur_row -= 1 + self.cur_col = len(self.window_content[self.cur_row]) + self.window_content[self.cur_row] += line + self.unsaved_progress = True + + def _key_ctl_backspace(self) -> None: + if self.cur_col == 1: # delete char + self.cur_col = 0 + del self.window_content[self.cur_row][self.cur_col] + self.unsaved_progress = True + elif self.cur_col > 1: + old_col = self.cur_col + self.cur_col -= 2 + tp = self.window_content[self.cur_row][self.cur_col].isalnum() + while self.cur_col > 0 and tp == self.window_content[self.cur_row][self.cur_col].isalnum(): + self.cur_col -= 1 + if self.cur_col: + self.cur_col += 1 + del self.window_content[self.cur_row][self.cur_col:old_col] + self.unsaved_progress = True + elif self.cur_row: # or delete line + line = self.window_content[self.cur_row] + del self.window_content[self.cur_row] + self.cur_row -= 1 + self.cur_col = len(self.window_content[self.cur_row]) + self.window_content[self.cur_row] += line + self.unsaved_progress = True + + def _key_left(self) -> None: + if self.cur_col: + self.cur_col -= 1 + elif self.cur_row: + self.cur_row -= 1 + self.cur_col = len(self.window_content[self.cur_row]) + + def _key_right(self) -> None: + if self.cur_col < len(self.window_content[self.cur_row]): + self.cur_col += 1 + elif self.cur_row < len(self.window_content)-1: + self.cur_row += 1 + self.cur_col = 0 + + def _key_up(self) -> None: + if self.cur_row: + self.cur_row -= 1 + + def _key_down(self) -> None: + if self.cur_row < len(self.window_content)-1: + self.cur_row += 1 + + def _key_ctl_left(self) -> None: + if self.cur_col == 1: + self.cur_col = 0 + elif self.cur_col > 1: + self.cur_col -= 2 + tp = self.window_content[self.cur_row][self.cur_col].isalnum() + while self.cur_col > 0 and tp == self.window_content[self.cur_row][self.cur_col].isalnum(): + self.cur_col -= 1 + if self.cur_col: + self.cur_col += 1 + elif self.cur_row: + self.cur_row -= 1 + self.cur_col = len(self.window_content[self.cur_row]) + + def _key_ctl_right(self) -> None: + if self.cur_col == len(self.window_content[self.cur_row])-1: + self.cur_col = len(self.window_content[self.cur_row]) + elif self.cur_col < len(self.window_content[self.cur_row])-1: + self.cur_col += 1 + tp = self.window_content[self.cur_row][self.cur_col].isalnum() + while self.cur_col < len(self.window_content[self.cur_row]) and tp == self.window_content[self.cur_row][self.cur_col].isalnum(): + self.cur_col += 1 + elif self.cur_row < len(self.window_content)-1: + self.cur_row += 1 + self.cur_col = 0 + + def _key_ctl_up(self) -> None: + if self.cur_row >= 10: + self.cur_row -= 10 + else: + self.cur_row = 0 + + def _key_ctl_down(self) -> None: + if self.cur_row < len(self.window_content)-10: + self.cur_row += 10 + else: + self.cur_row = len(self.window_content)-1 + + def _key_page_up(self) -> None: + max_y, _ = self.getxymax() + self.y = max(self.y-max_y, 0) + self.cur_row = max(self.cur_row-max_y, 0) + + def _key_page_down(self) -> None: + max_y, _ = self.getxymax() + self.y = max(min(self.y+max_y, len(self.window_content)-1-max_y), 0) + self.cur_row = min(self.cur_row+max_y, len(self.window_content)-1) + + def _key_end(self) -> None: + self.cur_col = len(self.window_content[self.cur_row]) + + def _key_ctl_end(self) -> None: + max_y, _ = self.getxymax() + self.y = max(len(self.window_content)-1-max_y, 0) + self.cur_row = len(self.window_content)-1 + self.cur_col = len(self.window_content[-1]) + + def _key_home(self) -> None: + self.cur_col = 0 + + def _key_ctl_home(self) -> None: + self.cur_row = 0 + self.cur_col = 0 + + def _key_char(self, wchar: str) -> None: + self.unsaved_progress = True + self.window_content[self.cur_row].insert(self.cur_col, wchar) + self.cur_col += 1 + + def _action_save(self, write_func) -> bool: + content = self.line_sep.join([''.join(line) for line in self.window_content]) + try: + write_func(content, self.file, self.file_encoding) + self.changes_made = True + self.unsaved_progress = False + self.error_bar = '' + self.status_bar_size = 1 + except OSError as e: + self.unsaved_progress = True + self.error_bar = str(e) + self.status_bar_size = 2 + if self.debug_mode: + print(self.error_bar, file=sys.stderr) + return True + + def _action_quit(self, write_func) -> bool: + if self.unsaved_progress: + max_y, max_x = self.getxymax() + save_message = 'Save changes? [y]es, [n]o'[:max_x] + self.curse_window.addstr(max_y + self.status_bar_size - 1, 0, save_message, self._get_color(5)) + if max_x > len(save_message): + self.curse_window.addstr(max_y + self.status_bar_size - 1, len(save_message), ' ' * (max_x-len(save_message)-1), self._get_color(5)) + self.curse_window.refresh() + + wchar = '' + while self.unsaved_progress and wchar.upper() != 'N': + wchar, key = self._get_new_char() + if key in ACTION_HOTKEYS: + if key == b'_action_quit': + break + getattr(self, key.decode(), lambda *_: False)(write_func) + elif wchar.upper() in ['Y', 'J']: + self._action_save(write_func) + break + + return False + + def _action_interrupt(self, _) -> bool: + if self.debug_mode: + print('Interrupting...', file=sys.stderr) + raise KeyboardInterrupt() + + def _action_resize(self, _) -> bool: + try: + curses.resize_term(*self.curse_window.getmaxyx()) + except curses.error: + pass + self.curse_window.clear() + return True + + def _get_new_char(self) -> tuple: + # get next char + wchar = -1 + while wchar == -1: + try: # try-except in case of no delay mode + wchar = self.curse_window.get_wch() + except curses.error: + pass + _key = curses.keyname(wchar if isinstance(wchar, int) else ord(wchar)) + key = UNIFY_HOTKEYS.get(_key, _key) + if self.debug_mode: + print(f"__DEBUG__: Received wchar \t{repr(wchar)} \t{repr(chr(wchar)) if isinstance(wchar, int) else ord(wchar)} \t{str(_key).ljust(15)} \t{key}", file=sys.stderr) + return (wchar, key) + + def _get_color(self, id: int) -> int: + if not curses.has_colors(): + return 0 + return curses.color_pair(id) + + def _render_scr(self) -> tuple: + max_y, max_x = self.getxymax() + + # fix cursor position (makes movement hotkeys easier) + row = self.window_content[self.cur_row] if self.cur_row < len(self.window_content) else None + rowlen = len(row) if row is not None else 0 + if self.cur_col > rowlen: + self.cur_col = rowlen + + # set/enforce the boundaries + curses.curs_set(0) + try: + self.curse_window.move(0, 0) + except curses.error: + pass + if self.cur_row < self.y: + self.y = self.cur_row + elif self.cur_row >= self.y + max_y: + self.y = self.cur_row - max_y + 1 + if self.cur_col < self.x: + self.x = self.cur_col + elif self.cur_col >= self.x + max_x: + self.x = self.cur_col - max_x + 1 + # display screen + for row in range(max_y): + brow = row + self.y + for col in range(max_x): + bcol = col + self.x + try: + if self.window_content[brow][bcol] == '\t': + self.curse_window.addch(row, col, '>', self._get_color(4)) + elif not self.window_content[brow][bcol].isprintable(): + self.curse_window.addch(row, col, '?', self._get_color(5)) + elif all(map(lambda c: c.isspace(), self.window_content[brow][bcol:])): + self.curse_window.addch(row, col, self.window_content[brow][bcol], self._get_color(3)) + else: + self.curse_window.addch(row, col, self.window_content[brow][bcol]) + except (IndexError, curses.error): + break + self.curse_window.clrtoeol() + try: + self.curse_window.addch('\n') + except curses.error: + break + # display status/error_bar + try: + if self.error_bar: + self.curse_window.addstr(max_y + self.status_bar_size - 2, 0, self.error_bar[:max_x], self._get_color(2)) + if (max_x - len(self.error_bar) - 1) > 0: + self.curse_window.addstr(max_y + self.status_bar_size - 2, len(self.error_bar), " " * (max_x - len(self.error_bar) - 1), self._get_color(2)) + + status_bar = f"File: {self.file} | Exit: ^q | Save: ^s | Pos: {self.cur_col}, {self.cur_row} | {'NOT ' * self.unsaved_progress}Saved!" + if len(status_bar) > max_x: + necc_space = max(0, max_x - (len(status_bar) - len(self.file) + 3)) + status_bar = f"File: ...{self.file[-necc_space:] * bool(necc_space)} | Exit: ^q | Save: ^s | Pos: {self.cur_col}, {self.cur_row} | {'NOT ' * self.unsaved_progress}Saved!"[:max_x] + self.curse_window.addstr(max_y + self.status_bar_size - 1, 0, status_bar, self._get_color(1)) + if (max_x - len(status_bar) - 1) > 0: + self.curse_window.addstr(max_y + self.status_bar_size - 1, len(status_bar), " " * (max_x - len(status_bar) - 1), self._get_color(1)) + except curses.error: + pass + + try: + self.curse_window.move(max(self.cur_row-self.y, 0), max(self.cur_col-self.x, 0)) + except curses.error: + pass + curses.curs_set(1) + self.curse_window.refresh() + + def _open(self, write_func) -> None: + running = True + + while running: + self._render_scr() + + wchar, key = self._get_new_char() + + # handle new wchar + if key in KEY_HOTKEYS: + getattr(self, key.decode(), lambda: None)() + elif key in ACTION_HOTKEYS: + running &= getattr(self, key.decode(), lambda *_: False)(write_func) + + # insert new key + elif isinstance(wchar, str) and wchar.isprintable() or wchar == '\t': + self._key_char(wchar) + + def _run(self, curse_window, write_func) -> None: + self.curse_window = curse_window + if curses.can_change_color(): + curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) # status_bar + curses.init_pair(2, curses.COLOR_RED , curses.COLOR_WHITE) # error_bar + curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_RED ) # trailing_whitespace + curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_GREEN) # tab-char + curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_RED ) # special char (not printable) & quit-prompt + curses.raw() + self.curse_window.nodelay(False) + self._open(write_func) + + def run(self, write_func) -> bool: + curses.wrapper(self._run, write_func) + return self.changes_made + class Editor: def get_newline(file: str) -> str: @@ -25,179 +399,7 @@ def get_newline(file: str) -> str: l += b'\n' * bool(not l[-1:] or l[-1:] not in b'\r\n') return '\r\n' if l[-2:] == b'\r\n' else l[-1:].decode() - - def _open(curse_window, file: str, file_encoding: str, write_func) -> bool: - """ - See Editor.open() method - """ - curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) - curses.init_pair(2, curses.COLOR_RED, curses.COLOR_WHITE) - curses.raw() - curse_window.nodelay(True) - - try: - curses_CTL_UP = curses.CTL_UP - curses_CTL_DOWN = curses.CTL_DOWN - except AttributeError: - curses_CTL_UP = 567 - curses_CTL_DOWN = 526 - - window_content = [] - error_bar = '' - unsaved_progress = False - status_bar_size = 1 - x, cur_col = 0, 0 - y, cur_row = 0, 0 - - has_written = False - line_sep = '\n' - - try: - line_sep = Editor.get_newline(file) - with open(file, 'r', encoding=file_encoding) as f: - for line in f.read().split('\n'): - window_content.append([ord(char) for char in line]) - except (OSError, UnicodeDecodeError) as e: - unsaved_progress = True - status_bar_size = 2 - error_bar = str(e) - window_content.append([]) - - while True: - max_y, max_x = curse_window.getmaxyx() - - curses.curs_set(0) - curse_window.move(0, 0) - # set the boundaries - if cur_row < y: - y = cur_row - if cur_row >= y + max_y-status_bar_size: - y = cur_row - max_y+1+status_bar_size - if cur_col < x: - x = cur_col - if cur_col >= x + max_x: - x = cur_col - max_x+1 - for row in range(max_y-1): - brow = row + y - for col in range(max_x): - bcol = col + x - try: - curse_window.addch(row, col, window_content[brow][bcol]) - except (IndexError, curses.error): - break - curse_window.clrtoeol() - try: - curse_window.addch('\n') - except curses.error: - break - try: - if error_bar: - curse_window.addstr(max_y - 2, 0, error_bar, curses.color_pair(2)) - curse_window.addstr(max_y - 2, len(error_bar), " " * (max_x - len(error_bar) - 1), curses.color_pair(2)) - - status_bar = f"File: {file} | Exit: ^q | Save: ^s | Pos: {cur_col}, {cur_row} | {'NOT ' * unsaved_progress}Saved!" - if len(status_bar) > max_x: - necc_space = max(0, max_x - (len(status_bar) - len(file) + 3)) - status_bar = f"File: ...{file[-necc_space:] * bool(necc_space)} | Exit: ^q | Save: ^s | Pos: {cur_col}, {cur_row} | {'NOT ' * unsaved_progress}Saved!" - curse_window.addstr(max_y - 1, 0, status_bar, curses.color_pair(1)) - curse_window.addstr(max_y - 1, len(status_bar), " " * (max_x - len(status_bar) - 1), curses.color_pair(1)) - except curses.error: - pass - - curse_window.move(cur_row-y, cur_col-x) - curses.curs_set(1) - curse_window.refresh() - - # get next char - char = -1 - while char == -1: - char = curse_window.getch() - - # default ascii char or TAB - if char != ((char) & 0x1F) and char < 128 or char == 9: - unsaved_progress = True - window_content[cur_row].insert(cur_col, char) - cur_col += 1 - # essentially 'enter' - elif chr(char) in '\n\r': - unsaved_progress = True - new_line = window_content[cur_row][cur_col:] - window_content[cur_row] = window_content[cur_row][:cur_col] - cur_row += 1 - cur_col = 0 - window_content.insert(cur_row, [] + new_line) - elif char in [ord('\b'), curses.KEY_BACKSPACE]: - unsaved_progress = True - if cur_col: # delete char - cur_col -= 1 - del window_content[cur_row][cur_col] - elif cur_row: # or delete line - line = window_content[cur_row] - del window_content[cur_row] - cur_row -= 1 - cur_col = len(window_content[cur_row]) - window_content[cur_row] += line - # delete (rest of) line (Ctrl-DEL) - elif char in [520, 527]: - unsaved_progress = True - window_content[cur_row] = window_content[cur_row][:cur_col] - elif char == curses.KEY_DC: - unsaved_progress = True - if cur_col < len(window_content[cur_row]): - del window_content[cur_row][cur_col] - elif cur_row < len(window_content)-1: - window_content[cur_row] += window_content[cur_row+1] - del window_content[cur_row+1] - elif char == curses.KEY_LEFT: - if cur_col: - cur_col -= 1 - elif cur_row: - cur_row -= 1 - cur_col = len(window_content[cur_row]) - elif char == curses.KEY_RIGHT: - if cur_col < len(window_content[cur_row]): - cur_col += 1 - elif cur_row < len(window_content)-1: - cur_row += 1 - cur_col = 0 - elif char == curses.KEY_UP and cur_row: - cur_row -= 1 - elif char == curses.KEY_DOWN and cur_row < len(window_content)-1: - cur_row += 1 - elif char == curses_CTL_UP and cur_row >= 10: - cur_row -= 10 - elif char == curses_CTL_DOWN and cur_row < len(window_content)-10: - cur_row += 10 - - row = window_content[cur_row] if cur_row < len(window_content) else None - rowlen = len(row) if row is not None else 0 - if cur_col > rowlen: - cur_col = rowlen - - # save - if char == (ord('s') & 0x1F): - content = line_sep.join([''.join([chr(char) for char in line]) for line in window_content]) - try: - write_func(content, file, file_encoding) - has_written = True - unsaved_progress = False - error_bar = '' - status_bar_size = 1 - except OSError as e: - unsaved_progress = True - error_bar = str(e) - status_bar_size = 2 - print(error_bar, file=sys.stderr) - # quit - elif char == (ord('q') & 0x1F): - break - # interrupt - elif char == (ord('c') & 0x1F): - raise KeyboardInterrupt() - - return has_written - - def open(file: str, file_encoding: str, write_func, on_windows_os: bool) -> bool: + def open(file: str, file_encoding: str, write_func, on_windows_os: bool, debug_mode: bool = False) -> bool: """ simple editor to change the contents of any provided file. the first file in the list will be loaded as a basis but all @@ -212,10 +414,12 @@ def open(file: str, file_encoding: str, write_func, on_windows_os: bool) -> bool stdinhelper.write_file [simply writes a file] on_windows_os (bool): indicates if the user is on windows OS using platform.system() == 'Windows' + debug_mode (bool): + indicates if debug information should be displayed Returns: (bool): - indicates whether or not the editor has written any content to the provided files + indicates whether or not the editor has written any content to the provided files """ if CURSES_MODULE_ERROR: print("The Editor could not be loaded. No Module 'curses' was found.", file=sys.stderr) @@ -226,4 +430,5 @@ def open(file: str, file_encoding: str, write_func, on_windows_os: bool) -> bool # print("The Editor could not be loaded.", file=sys.stderr) # return False - return curses.wrapper(Editor._open, file, file_encoding, write_func) + editor = _Editor(file, file_encoding, debug_mode) + return editor.run(write_func) diff --git a/cat_win/util/editorhelper.py b/cat_win/util/editorhelper.py new file mode 100644 index 00000000..af1abf17 --- /dev/null +++ b/cat_win/util/editorhelper.py @@ -0,0 +1,79 @@ +UNIFY_HOTKEYS = { + # newline + b'^M' : b'_key_enter', # CR + b'^J' : b'_key_enter', # LF + b'PADENTER' : b'_key_enter', # numpad + b'KEY_ENTER' : b'_key_enter', # 'fn' mode + # ctrl - newline + b'CTL_ENTER' : b'_key_enter', # windows + b'CTL_PADENTER' : b'_key_enter', # numpad + # delete + b'KEY_DC' : b'_key_dc', # windows & xterm + b'^D' : b'_key_dc', # some unix machines + b'PADSTOP' : b'_key_dc', # numpad + # ctrl - del + b'CTL_DEL' : b'_key_dl', # windows + b'kDC5' : b'_key_dl', # xterm + b'CTL_PADSTOP' : b'_key_dl', # numpad + # backspace + b'^H' : b'_key_backspace', # windows (ctrl-backspace on xterm...) + b'KEY_BACKSPACE': b'_key_backspace', # xterm + # ctrl-backspace + b'^?' : b'_key_ctl_backspace', # windows + # arrows + b'KEY_LEFT' : b'_key_left', # windows & xterm + b'KEY_RIGHT' : b'_key_right', + b'KEY_UP' : b'_key_up', + b'KEY_DOWN' : b'_key_down', + b'KEY_B1' : b'_key_left', # numpad + b'KEY_B3' : b'_key_right', + b'KEY_A2' : b'_key_up', + b'KEY_C2' : b'_key_down', + # ctrl-arrows + b'CTL_LEFT' : b'_key_ctl_left', # windows + b'CTL_RIGHT' : b'_key_ctl_right', + b'CTL_UP' : b'_key_ctl_up', + b'CTL_DOWN' : b'_key_ctl_down', + b'kLFT5' : b'_key_ctl_left', # xterm + b'kRIT5' : b'_key_ctl_right', + b'kUP5' : b'_key_ctl_up', + b'kDN5' : b'_key_ctl_down', + b'CTL_PAD4' : b'_key_ctl_left', # numpad + b'CTL_PAD6' : b'_key_ctl_right', + b'CTL_PAD8' : b'_key_ctl_up', + b'CTL_PAD2' : b'_key_ctl_down', + # page + b'KEY_PPAGE' : b'_key_page_up', # windows & xterm + b'KEY_NPAGE' : b'_key_page_down', + b'KEY_A3' : b'_key_page_up', # numpad + b'KEY_C3' : b'_key_page_down', + # ctrl - page + b'CTL_PGUP' : b'_key_page_up', # windows + b'CTL_PGDN' : b'_key_page_down', + b'kPRV5' : b'_key_page_up', # xterm + b'kNXT5' : b'_key_page_down', + b'CTL_PAD9' : b'_key_page_up', # numpad + b'CTL_PAD3' : b'_key_page_down', + # end + b'KEY_END' : b'_key_end', # windows & xterm + b'KEY_C1' : b'_key_end', # numpad + # ctrl - end + b'CTL_END' : b'_key_ctl_end', # windows + b'kEND5' : b'_key_ctl_end', # xterm + b'CTL_PAD1' : b'_key_ctl_end', # numpad + # pos/home + b'KEY_HOME' : b'_key_home', # windows & xterm + b'KEY_A1' : b'_key_home', # numpad + # ctrl - pos/home + b'CTL_HOME' : b'_key_ctl_home', # windows + b'kHOM5' : b'_key_ctl_home', # xterm + b'CTL_PAD7' : b'_key_ctl_home', # numpad + # actions + b'^S' : b'_action_save', + b'^Q' : b'_action_quit', + b'^C' : b'_action_interrupt', + b'KEY_RESIZE' : b'_action_resize', +} + +KEY_HOTKEYS = set(v for v in UNIFY_HOTKEYS.values() if v.startswith(b'_key' )) +ACTION_HOTKEYS = set(v for v in UNIFY_HOTKEYS.values() if v.startswith(b'_action'))