codekingpro/portable-devtools
114k
1# Urwid escape sequences common to curses_display and raw_display2# Copyright (C) 2004-2011 Ian Ward3#4# This library is free software; you can redistribute it and/or5# modify it under the terms of the GNU Lesser General Public6# License as published by the Free Software Foundation; either7# version 2.1 of the License, or (at your option) any later version.8#9# This library is distributed in the hope that it will be useful,10# but WITHOUT ANY WARRANTY; without even the implied warranty of11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU12# Lesser General Public License for more details.13#14# You should have received a copy of the GNU Lesser General Public15# License along with this library; if not, write to the Free Software16# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA17#18# Urwid web site: https://urwid.org/19 20 21"""22Terminal Escape Sequences for input and display23"""24 25from __future__ import annotations26 27import re28import sys29import typing30from collections.abc import MutableMapping, Sequence31 32from urwid import str_util33 34if typing.TYPE_CHECKING:35 from collections.abc import Iterable36 37# NOTE: because of circular imports (urwid.util -> urwid.escape -> urwid.util)38# from urwid.util import is_mouse_event -- will not work here39import urwid.util # isort: skip # pylint: disable=wrong-import-position40 41IS_WINDOWS = sys.platform == "win32"42 43within_double_byte = str_util.within_double_byte44 45SO = "\x0e"46SI = "\x0f"47IBMPC_ON = "\x1b[11m"48IBMPC_OFF = "\x1b[10m"49 50DEC_TAG = "0"51DEC_SPECIAL_CHARS = "▮◆▒␉␌␍␊°±␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·"52ALT_DEC_SPECIAL_CHARS = "_`abcdefghijklmnopqrstuvwxyz{|}~"53 54DEC_SPECIAL_CHARMAP = {}55if len(DEC_SPECIAL_CHARS) != len(ALT_DEC_SPECIAL_CHARS):56 raise RuntimeError(repr((DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS)))57 58for c, alt in zip(DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS):59 DEC_SPECIAL_CHARMAP[ord(c)] = SO + alt + SI60 61SAFE_ASCII_DEC_SPECIAL_RE = re.compile(f"^[ -~{DEC_SPECIAL_CHARS}]*$")62DEC_SPECIAL_RE = re.compile(f"[{DEC_SPECIAL_CHARS}]")63 64 65###################66# Input sequences67###################68 69 70class MoreInputRequired(Exception):71 pass72 73 74def escape_modifier(digit: str) -> str:75 mode = ord(digit) - ord("1")76 return "shift " * (mode & 1) + "meta " * ((mode & 2) // 2) + "ctrl " * ((mode & 4) // 4)77 78 79input_sequences = [80 ("[A", "up"),81 ("[B", "down"),82 ("[C", "right"),83 ("[D", "left"),84 ("[E", "5"),85 ("[F", "end"),86 ("[G", "5"),87 ("[H", "home"),88 ("[I", "focus in"),89 ("[O", "focus out"),90 ("[1~", "home"),91 ("[2~", "insert"),92 ("[3~", "delete"),93 ("[4~", "end"),94 ("[5~", "page up"),95 ("[6~", "page down"),96 ("[7~", "home"),97 ("[8~", "end"),98 ("[[A", "f1"),99 ("[[B", "f2"),100 ("[[C", "f3"),101 ("[[D", "f4"),102 ("[[E", "f5"),103 ("[11~", "f1"),104 ("[12~", "f2"),105 ("[13~", "f3"),106 ("[14~", "f4"),107 ("[15~", "f5"),108 ("[17~", "f6"),109 ("[18~", "f7"),110 ("[19~", "f8"),111 ("[20~", "f9"),112 ("[21~", "f10"),113 ("[23~", "f11"),114 ("[24~", "f12"),115 ("[25~", "f13"),116 ("[26~", "f14"),117 ("[28~", "f15"),118 ("[29~", "f16"),119 ("[31~", "f17"),120 ("[32~", "f18"),121 ("[33~", "f19"),122 ("[34~", "f20"),123 ("OA", "up"),124 ("OB", "down"),125 ("OC", "right"),126 ("OD", "left"),127 ("OH", "home"),128 ("OF", "end"),129 ("OP", "f1"),130 ("OQ", "f2"),131 ("OR", "f3"),132 ("OS", "f4"),133 ("Oo", "/"),134 ("Oj", "*"),135 ("Om", "-"),136 ("Ok", "+"),137 ("[Z", "shift tab"),138 ("On", "."),139 ("[200~", "begin paste"),140 ("[201~", "end paste"),141 *(142 (prefix + letter, modifier + key)143 for prefix, modifier in zip("O[", ("meta ", "shift "))144 for letter, key in zip("abcd", ("up", "down", "right", "left"))145 ),146 *(147 (f"[{digit}{symbol}", modifier + key)148 for modifier, symbol in zip(("shift ", "meta "), "$^")149 for digit, key in zip("235678", ("insert", "delete", "page up", "page down", "home", "end"))150 ),151 *((f"O{ord('p') + n:c}", str(n)) for n in range(10)),152 *(153 # modified cursor keys + home, end, 5 -- [#X and [1;#X forms154 (prefix + digit + letter, escape_modifier(digit) + key)155 for prefix in ("[", "[1;")156 for digit in "12345678"157 for letter, key in zip("ABCDEFGH", ("up", "down", "right", "left", "5", "end", "5", "home"))158 ),159 *(160 # modified F1-F4 keys - O#X form and [1;#X form161 (prefix + digit + letter, escape_modifier(digit) + f"f{number}")162 for prefix in ("O", "[1;")163 for digit in "12345678"164 for number, letter in enumerate("PQRS", start=1)165 ),166 *(167 # modified F1-F13 keys -- [XX;#~ form168 (f"[{num!s};{digit}~", escape_modifier(digit) + key)169 for digit in "12345678"170 for num, key in zip(171 (3, 5, 6, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34),172 (173 "delete",174 "page up",175 "page down",176 *(f"f{idx}" for idx in range(1, 21)),177 ),178 )179 ),180 # mouse reporting (special handling done in KeyqueueTrie)181 ("[M", "mouse"),182 # mouse reporting for SGR 1006183 ("[<", "sgrmouse"),184 # report status response185 ("[0n", "status ok"),186]187 188 189class KeyqueueTrie:190 __slots__ = ("data",)191 192 def __init__(self, sequences: Iterable[tuple[str, str]]) -> None:193 self.data: dict[int, str | dict[int, str | dict[int, str]]] = {}194 for s, result in sequences:195 if isinstance(result, dict):196 raise TypeError(result)197 self.add(self.data, s, result)198 199 def add(200 self,201 root: MutableMapping[int, str | MutableMapping[int, str | MutableMapping[int, str]]],202 s: str,203 result: str,204 ) -> None:205 if not isinstance(root, MutableMapping) or not s:206 raise RuntimeError("trie conflict detected")207 208 if ord(s[0]) in root:209 self.add(root[ord(s[0])], s[1:], result)210 return211 if len(s) > 1:212 d = {}213 root[ord(s[0])] = d214 self.add(d, s[1:], result)215 return216 root[ord(s)] = result217 218 def get(self, keys, more_available: bool):219 if result := self.get_recurse(self.data, keys, more_available):220 return result221 222 return self.read_cursor_position(keys, more_available)223 224 def get_recurse(225 self,226 root: (227 MutableMapping[int, str | MutableMapping[int, str | MutableMapping[int, str]]]228 | typing.Literal["mouse", "sgrmouse"]229 ),230 keys: Sequence[int],231 more_available: bool,232 ):233 if not isinstance(root, MutableMapping):234 if root == "mouse":235 return self.read_mouse_info(keys, more_available)236 237 if root == "sgrmouse":238 return self.read_sgrmouse_info(keys, more_available)239 240 return (root, keys)241 if not keys:242 # get more keys243 if more_available:244 raise MoreInputRequired()245 return None246 if keys[0] not in root:247 return None248 return self.get_recurse(root[keys[0]], keys[1:], more_available)249 250 def read_mouse_info(251 self,252 keys: Sequence[int],253 more_available: bool,254 ) -> tuple[tuple[str, int, int, int], Sequence[int]] | None:255 if len(keys) < 3:256 if more_available:257 raise MoreInputRequired()258 return None259 260 b = keys[0] - 32261 x, y = (keys[1] - 33) % 256, (keys[2] - 33) % 256 # supports 0-255262 263 prefixes = []264 if b & 4:265 prefixes.append("shift ")266 if b & 8:267 prefixes.append("meta ")268 if b & 16:269 prefixes.append("ctrl ")270 if (b & MOUSE_MULTIPLE_CLICK_MASK) >> 9 == 1:271 prefixes.append("double ")272 if (b & MOUSE_MULTIPLE_CLICK_MASK) >> 9 == 2:273 prefixes.append("triple ")274 prefix = "".join(prefixes)275 276 # 0->1, 1->2, 2->3, 64->4, 65->5277 button = ((b & 64) // 64 * 3) + (b & 3) + 1278 279 if b & 3 == 3:280 action = "release"281 button = 0282 elif b & MOUSE_RELEASE_FLAG:283 action = "release"284 elif b & MOUSE_DRAG_FLAG:285 action = "drag"286 elif b & MOUSE_MULTIPLE_CLICK_MASK:287 action = "click"288 else:289 action = "press"290 291 return ((f"{prefix}mouse {action}", button, x, y), keys[3:])292 293 def read_sgrmouse_info(294 self,295 keys: Sequence[int],296 more_available: bool,297 ) -> tuple[tuple[str, int, int, int], Sequence[int]] | None:298 # Helpful links:299 # https://stackoverflow.com/questions/5966903/how-to-get-mousemove-and-mouseclick-in-bash300 # http://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf301 302 if not keys:303 if more_available:304 raise MoreInputRequired()305 return None306 307 value = ""308 pos_m = 0309 found_m = False310 for k in keys:311 value += chr(k)312 if k in {ord("M"), ord("m")}:313 found_m = True314 break315 pos_m += 1316 if not found_m:317 if more_available:318 raise MoreInputRequired()319 return None320 321 (b, x, y) = (int(val) for val in value[:-1].split(";"))322 action = value[-1]323 # Double and triple clicks are not supported.324 # They can be implemented by using a timer.325 # This timer can check if the last registered click is below a certain threshold.326 # This threshold is normally set in the operating system itself,327 # so setting one here will cause an inconsistent behaviour.328 329 prefixes = []330 if b & 4:331 prefixes.append("shift ")332 if b & 8:333 prefixes.append("meta ")334 if b & 16:335 prefixes.append("ctrl ")336 prefix = "".join(prefixes)337 338 wheel_used: typing.Literal[0, 1] = (b & 64) >> 6339 340 button = (wheel_used * 3) + (b & 3) + 1341 x -= 1342 y -= 1343 344 if action == "M":345 if b & MOUSE_DRAG_FLAG:346 action = "drag"347 else:348 action = "press"349 elif action == "m":350 action = "release"351 else:352 raise ValueError(f"Unknown mouse action: {action!r}")353 354 return ((f"{prefix}mouse {action}", button, x, y), keys[pos_m + 1 :])355 356 def read_cursor_position(357 self,358 keys: Sequence[int],359 more_available: bool,360 ) -> tuple[tuple[str, int, int], Sequence[int]] | None:361 """362 Interpret cursor position information being sent by the363 user's terminal. Returned as ('cursor position', x, y)364 where (x, y) == (0, 0) is the top left of the screen.365 """366 if not keys:367 if more_available:368 raise MoreInputRequired()369 return None370 if keys[0] != ord("["):371 return None372 # read y value373 y = 0374 i = 1375 for k in keys[i:]:376 i += 1377 if k == ord(";"):378 if not y:379 return None380 break381 if k < ord("0") or k > ord("9"):382 return None383 if not y and k == ord("0"):384 return None385 y = y * 10 + k - ord("0")386 if not keys[i:]:387 if more_available:388 raise MoreInputRequired()389 return None390 # read x value391 x = 0392 for k in keys[i:]:393 i += 1394 if k == ord("R"):395 if not x:396 return None397 return (("cursor position", x - 1, y - 1), keys[i:])398 if k < ord("0") or k > ord("9"):399 return None400 if not x and k == ord("0"):401 return None402 x = x * 10 + k - ord("0")403 if not keys[i:] and more_available:404 raise MoreInputRequired()405 return None406 407 408# This is added to button value to signal mouse release by curses_display409# and raw_display when we know which button was released. NON-STANDARD410MOUSE_RELEASE_FLAG = 2048411 412# This 2-bit mask is used to check if the mouse release from curses or gpm413# is a double or triple release. 00 means single click, 01 double,414# 10 triple. NON-STANDARD415MOUSE_MULTIPLE_CLICK_MASK = 1536416 417# This is added to button value at mouse release to differentiate between418# single, double and triple press. Double release adds this times one,419# triple release adds this times two. NON-STANDARD420MOUSE_MULTIPLE_CLICK_FLAG = 512421 422# xterm adds this to the button value to signal a mouse drag event423MOUSE_DRAG_FLAG = 32424 425 426#################################################427# Build the input trie from input_sequences list428input_trie = KeyqueueTrie(input_sequences)429#################################################430 431_keyconv = {432 -1: None,433 8: "backspace",434 9: "tab",435 10: "enter",436 13: "enter",437 127: "backspace",438 # curses-only keycodes follow.. (XXX: are these used anymore?)439 258: "down",440 259: "up",441 260: "left",442 261: "right",443 262: "home",444 263: "backspace",445 265: "f1",446 266: "f2",447 267: "f3",448 268: "f4",449 269: "f5",450 270: "f6",451 271: "f7",452 272: "f8",453 273: "f9",454 274: "f10",455 275: "f11",456 276: "f12",457 277: "shift f1",458 278: "shift f2",459 279: "shift f3",460 280: "shift f4",461 281: "shift f5",462 282: "shift f6",463 283: "shift f7",464 284: "shift f8",465 285: "shift f9",466 286: "shift f10",467 287: "shift f11",468 288: "shift f12",469 330: "delete",470 331: "insert",471 338: "page down",472 339: "page up",473 343: "enter", # on numpad474 350: "5", # on numpad475 360: "end",476}477 478if IS_WINDOWS:479 _keyconv[351] = "shift tab"480 _keyconv[358] = "end"481 482 483def process_keyqueue(codes: Sequence[int], more_available: bool) -> tuple[list[str], Sequence[int]]:484 """485 codes -- list of key codes486 more_available -- if True then raise MoreInputRequired when in the487 middle of a character sequence (escape/utf8/wide) and caller488 will attempt to send more key codes on the next call.489 490 returns (list of input, list of remaining key codes).491 """492 code = codes[0]493 if 32 <= code <= 126:494 key = chr(code)495 return [key], codes[1:]496 if code in _keyconv:497 return [_keyconv[code]], codes[1:]498 if 0 < code < 27:499 return [f"ctrl {ord('a') + code - 1:c}"], codes[1:]500 if 27 < code < 32:501 return [f"ctrl {ord('A') + code - 1:c}"], codes[1:]502 503 em = str_util.get_byte_encoding()504 505 if (506 em == "wide"507 and code < 256508 and within_double_byte(509 code.to_bytes(1, "little"),510 0,511 0,512 )513 ):514 if not codes[1:] and more_available:515 raise MoreInputRequired()516 if codes[1:] and codes[1] < 256:517 db = chr(code) + chr(codes[1])518 if within_double_byte(db, 0, 1):519 return [db], codes[2:]520 521 if em == "utf8" and 127 < code < 256:522 if code & 0xE0 == 0xC0: # 2-byte form523 need_more = 1524 elif code & 0xF0 == 0xE0: # 3-byte form525 need_more = 2526 elif code & 0xF8 == 0xF0: # 4-byte form527 need_more = 3528 else:529 return [f"<{code:d}>"], codes[1:]530 531 for i in range(1, need_more + 1):532 if len(codes) <= i:533 if more_available:534 raise MoreInputRequired()535 536 return [f"<{code:d}>"], codes[1:]537 538 k = codes[i]539 if k > 256 or k & 0xC0 != 0x80:540 return [f"<{code:d}>"], codes[1:]541 542 s = bytes(codes[: need_more + 1])543 544 try:545 return [s.decode("utf-8")], codes[need_more + 1 :]546 except UnicodeDecodeError:547 return [f"<{code:d}>"], codes[1:]548 549 if 127 < code < 256:550 key = chr(code)551 return [key], codes[1:]552 if code != 27:553 return [f"<{code:d}>"], codes[1:]554 555 if (result := input_trie.get(codes[1:], more_available)) is not None:556 result, remaining_codes = result557 return [result], remaining_codes558 559 if codes[1:]:560 # Meta keys -- ESC+Key form561 run, remaining_codes = process_keyqueue(codes[1:], more_available)562 if urwid.util.is_mouse_event(run[0]):563 return ["esc", *run], remaining_codes564 if run[0] == "esc" or run[0].find("meta ") >= 0:565 return ["esc", *run], remaining_codes566 return [f"meta {run[0]}", *run[1:]], remaining_codes567 568 return ["esc"], codes[1:]569 570 571####################572# Output sequences573####################574 575ESC = "\x1b"576 577CURSOR_HOME = f"{ESC}[H"578CURSOR_HOME_COL = "\r"579 580APP_KEYPAD_MODE = f"{ESC}="581NUM_KEYPAD_MODE = f"{ESC}>"582 583SWITCH_TO_ALTERNATE_BUFFER = f"{ESC}[?1049h"584RESTORE_NORMAL_BUFFER = f"{ESC}[?1049l"585 586ENABLE_BRACKETED_PASTE_MODE = f"{ESC}[?2004h"587DISABLE_BRACKETED_PASTE_MODE = f"{ESC}[?2004l"588 589ENABLE_FOCUS_REPORTING = f"{ESC}[?1004h"590DISABLE_FOCUS_REPORTING = f"{ESC}[?1004l"591 592# RESET_SCROLL_REGION = ESC+"[;r"593# RESET = ESC+"c"594 595REPORT_STATUS = f"{ESC}[5n"596REPORT_CURSOR_POSITION = f"{ESC}[6n"597 598INSERT_ON = f"{ESC}[4h"599INSERT_OFF = f"{ESC}[4l"600 601 602def set_cursor_position(x: int, y: int) -> str:603 if not isinstance(x, int):604 raise TypeError(x)605 if not isinstance(y, int):606 raise TypeError(y)607 608 return ESC + f"[{y + 1:d};{x + 1:d}H"609 610 611def move_cursor_right(x: int) -> str:612 if x < 1:613 return ""614 return ESC + f"[{x:d}C"615 616 617def move_cursor_up(x: int) -> str:618 if x < 1:619 return ""620 return ESC + f"[{x:d}A"621 622 623def move_cursor_down(x: int) -> str:624 if x < 1:625 return ""626 return ESC + f"[{x:d}B"627 628 629HIDE_CURSOR = f"{ESC}[?25l"630SHOW_CURSOR = f"{ESC}[?25h"631 632MOUSE_TRACKING_ON = f"{ESC}[?1000h{ESC}[?1002h{ESC}[?1006h"633MOUSE_TRACKING_OFF = f"{ESC}[?1006l{ESC}[?1002l{ESC}[?1000l"634 635DESIGNATE_G1_SPECIAL = f"{ESC})0"636 637ERASE_IN_LINE_RIGHT = f"{ESC}[K"638 