Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
escape.py638 linesDownload Raw Back to display
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 
codekingpro/portable-devtools · Team Ai