Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
vterm.py1780 linesDownload Raw Back to urwid
1# Urwid terminal emulation widget2#    Copyright (C) 2010  aszlig3#    Copyright (C) 2011  Ian Ward4#5#    This library is free software; you can redistribute it and/or6#    modify it under the terms of the GNU Lesser General Public7#    License as published by the Free Software Foundation; either8#    version 2.1 of the License, or (at your option) any later version.9#10#    This library is distributed in the hope that it will be useful,11#    but WITHOUT ANY WARRANTY; without even the implied warranty of12#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU13#    Lesser General Public License for more details.14#15#    You should have received a copy of the GNU Lesser General Public16#    License along with this library; if not, write to the Free Software17#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA18#19# Urwid web site: https://urwid.org/20 21 22from __future__ import annotations23 24import atexit25import copy26import errno27import fcntl28import os29import pty30import selectors31import signal32import struct33import sys34import termios35import time36import traceback37import typing38from collections import deque39from contextlib import suppress40from dataclasses import dataclass41 42from urwid import event_loop, util43from urwid.canvas import Canvas44from urwid.display import AttrSpec, RealTerminal45from urwid.display.escape import ALT_DEC_SPECIAL_CHARS, DEC_SPECIAL_CHARS46from urwid.widget import Sizing, Widget47 48from .display.common import _BASIC_COLORS, _color_desc_256, _color_desc_true49 50if typing.TYPE_CHECKING:51    from collections.abc import Callable, Iterable, Mapping, Sequence52 53    from typing_extensions import Literal54 55EOF = b""56ESC = chr(27)57ESC_B = b"\x1b"58 59KEY_TRANSLATIONS = {60    "enter": "\r",61    "backspace": chr(127),62    "tab": "\t",63    "esc": ESC,64    "up": f"{ESC}[A",65    "down": f"{ESC}[B",66    "right": f"{ESC}[C",67    "left": f"{ESC}[D",68    "home": f"{ESC}[1~",69    "insert": f"{ESC}[2~",70    "delete": f"{ESC}[3~",71    "end": f"{ESC}[4~",72    "page up": f"{ESC}[5~",73    "page down": f"{ESC}[6~",74    "begin paste": f"{ESC}[200~",75    "end paste": f"{ESC}[201~",76    "f1": f"{ESC}[[A",77    "f2": f"{ESC}[[B",78    "f3": f"{ESC}[[C",79    "f4": f"{ESC}[[D",80    "f5": f"{ESC}[[E",81    "f6": f"{ESC}[17~",82    "f7": f"{ESC}[18~",83    "f8": f"{ESC}[19~",84    "f9": f"{ESC}[20~",85    "f10": f"{ESC}[21~",86    "f11": f"{ESC}[23~",87    "f12": f"{ESC}[24~",88}89 90KEY_TRANSLATIONS_DECCKM = {91    "up": f"{ESC}OA",92    "down": f"{ESC}OB",93    "right": f"{ESC}OC",94    "left": f"{ESC}OD",95    "f1": f"{ESC}OP",96    "f2": f"{ESC}OQ",97    "f3": f"{ESC}OR",98    "f4": f"{ESC}OS",99    "f5": f"{ESC}[15~",100}101 102 103class CSIAlias(typing.NamedTuple):104    alias_mark: str  # can not have constructor with default first and non-default second arg105    alias: bytes106 107 108class CSICommand(typing.NamedTuple):109    num_args: int110    default: int111    callback: Callable[[TermCanvas, list[int], bool], typing.Any]  # return value ignored112 113 114CSI_COMMANDS: dict[bytes, CSIAlias | CSICommand] = {115    # possible values:116    #     None -> ignore sequence117    #     (<minimum number of args>, <fallback if no argument>, callback)118    #     ('alias', <symbol>)119    #120    # while callback is executed as:121    #     callback(<instance of TermCanvas>, arguments, has_question_mark)122    b"@": CSICommand(1, 1, lambda s, number, q: s.insert_chars(chars=number[0])),123    b"A": CSICommand(1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative=True)),124    b"B": CSICommand(1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative=True)),125    b"C": CSICommand(1, 1, lambda s, cols, q: s.move_cursor(cols[0], 0, relative=True)),126    b"D": CSICommand(1, 1, lambda s, cols, q: s.move_cursor(-cols[0], 0, relative=True)),127    b"E": CSICommand(1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative_y=True)),128    b"F": CSICommand(1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative_y=True)),129    b"G": CSICommand(1, 1, lambda s, col, q: s.move_cursor(col[0] - 1, 0, relative_y=True)),130    b"H": CSICommand(2, 1, lambda s, x_y, q: s.move_cursor(x_y[1] - 1, x_y[0] - 1)),131    b"J": CSICommand(1, 0, lambda s, mode, q: s.csi_erase_display(mode[0])),132    b"K": CSICommand(1, 0, lambda s, mode, q: s.csi_erase_line(mode[0])),133    b"L": CSICommand(1, 1, lambda s, number, q: s.insert_lines(lines=number[0])),134    b"M": CSICommand(1, 1, lambda s, number, q: s.remove_lines(lines=number[0])),135    b"P": CSICommand(1, 1, lambda s, number, q: s.remove_chars(chars=number[0])),136    b"X": CSICommand(137        1,138        1,139        lambda s, number, q: s.erase(s.term_cursor, (s.term_cursor[0] + number[0] - 1, s.term_cursor[1])),140    ),141    b"a": CSIAlias("alias", b"C"),142    b"c": CSICommand(0, 0, lambda s, none, q: s.csi_get_device_attributes(q)),143    b"d": CSICommand(1, 1, lambda s, row, q: s.move_cursor(0, row[0] - 1, relative_x=True)),144    b"e": CSIAlias("alias", b"B"),145    b"f": CSIAlias("alias", b"H"),146    b"g": CSICommand(1, 0, lambda s, mode, q: s.csi_clear_tabstop(mode[0])),147    b"h": CSICommand(1, 0, lambda s, modes, q: s.csi_set_modes(modes, q)),148    b"l": CSICommand(1, 0, lambda s, modes, q: s.csi_set_modes(modes, q, reset=True)),149    b"m": CSICommand(1, 0, lambda s, attrs, q: s.csi_set_attr(attrs)),150    b"n": CSICommand(1, 0, lambda s, mode, q: s.csi_status_report(mode[0])),151    b"q": CSICommand(1, 0, lambda s, mode, q: s.csi_set_keyboard_leds(mode[0])),152    b"r": CSICommand(2, 0, lambda s, t_b, q: s.csi_set_scroll(t_b[0], t_b[1])),153    b"s": CSICommand(0, 0, lambda s, none, q: s.save_cursor()),154    b"u": CSICommand(0, 0, lambda s, none, q: s.restore_cursor()),155    b"`": CSIAlias("alias", b"G"),156}157 158CHARSET_DEFAULT: Literal[1] = 1  # type annotated exclusively for buggy IDE159CHARSET_UTF8: Literal[2] = 2160 161 162@dataclass(eq=True, order=False)163class TermModes:164    # ECMA-48165    display_ctrl: bool = False166    insert: bool = False167    lfnl: bool = False168 169    # DEC private modes170    keys_decckm: bool = False171    reverse_video: bool = False172    constrain_scrolling: bool = False173    autowrap: bool = True174    visible_cursor: bool = True175    bracketed_paste: bool = False176 177    # charset stuff178    main_charset: Literal[1, 2] = CHARSET_DEFAULT179 180    def reset(self) -> None:181        # ECMA-48182        self.display_ctrl = False183        self.insert = False184        self.lfnl = False185 186        # DEC private modes187        self.keys_decckm = False188        self.reverse_video = False189        self.constrain_scrolling = False190        self.autowrap = True191        self.visible_cursor = True192 193        # charset stuff194        self.main_charset = CHARSET_DEFAULT195 196 197class TermCharset:198    __slots__ = ("_g", "_sgr_mapping", "active", "current")199 200    MAPPING: typing.ClassVar[dict[str, str | None]] = {201        "default": None,202        "vt100": "0",203        "ibmpc": "U",204        "user": None,205    }206 207    def __init__(self) -> None:208        self._g = [209            "default",210            "vt100",211        ]212 213        self._sgr_mapping = False214 215        # prepare defaults216        self.active = 0217        self.current: str | None = None218 219        self.activate(0)220 221    def define(self, g: int, charset: str) -> None:222        """223        Redefine G'g' with new mapping.224        """225        self._g[g] = charset226        self.activate(g=self.active)227 228    def activate(self, g: int) -> None:229        """230        Activate the given charset slot.231        """232        self.active = g233        self.current = self.MAPPING.get(self._g[g], None)234 235    def set_sgr_ibmpc(self) -> None:236        """237        Set graphics rendition mapping to IBM PC CP437.238        """239        self._sgr_mapping = True240 241    def reset_sgr_ibmpc(self) -> None:242        """243        Reset graphics rendition mapping to IBM PC CP437.244        """245        self._sgr_mapping = False246        self.activate(g=self.active)247 248    def apply_mapping(self, char: bytes) -> bytes:249        if self._sgr_mapping or self._g[self.active] == "ibmpc":250            if (dec_pos := DEC_SPECIAL_CHARS.find(char.decode("cp437"))) >= 0:251                self.current = "0"252                return ALT_DEC_SPECIAL_CHARS[dec_pos].encode("cp437")253 254            self.current = "U"255            return char256 257        return char258 259 260class TermCanvas(Canvas):261    cacheable = False262 263    def __init__(self, width: int, height: int, widget: Terminal) -> None:264        super().__init__()265 266        self.width, self.height = width, height267        self.widget = widget268        self.modes: TermModes = widget.term_modes269        self.has_focus = False270 271        self.scrollback_buffer: deque[list[tuple[AttrSpec | None, str | None, bytes]]] = deque(maxlen=10000)272        self.scrolling_up = 0273 274        self.utf8_eat_bytes: int | None = None275        self.utf8_buffer = bytearray()276        self.escbuf = b""277 278        self.coords["cursor"] = (0, 0, None)279 280        self.term_cursor: tuple[int, int] = (0, 0)  # do not allow to shoot in the leg at `set_term_cursor`281 282        self.within_escape = False283        self.parsestate = 0284 285        self.attrspec: AttrSpec | None = None286 287        self.charset = TermCharset()288 289        self.saved_cursor: tuple[int, int] | None = None290        self.saved_attrs: tuple[AttrSpec | None, TermCharset] | None = None291 292        self.is_rotten_cursor = False293 294        self.scrollregion_start = 0295        self.scrollregion_end = self.height - 1296 297        self.tabstops: list[int] = []298        self.term: list[list[tuple[AttrSpec | None, str | None, bytes]]] = []299 300        self.reset()301 302    def set_term_cursor(self, x: int | None = None, y: int | None = None) -> None:303        """304        Set terminal cursor to x/y and update canvas cursor. If one or both axes305        are omitted, use the values of the current position.306        """307        if x is None:308            x = self.term_cursor[0]309        if y is None:310            y = self.term_cursor[1]311 312        self.term_cursor = self.constrain_coords(x, y)313 314        if self.has_focus and self.modes.visible_cursor and self.scrolling_up < self.height - y:315            self.cursor = (x, y + self.scrolling_up)316        else:317            self.cursor = None318 319    def reset_scroll(self) -> None:320        """321        Reset scrolling region to full terminal size.322        """323        self.scrollregion_start = 0324        self.scrollregion_end = self.height - 1325 326    def scroll_buffer(self, up: bool = True, reset: bool = False, lines: int | None = None) -> None:327        """328        Scroll the scrolling buffer up (up=True) or down (up=False) the given329        amount of lines or half the screen height.330 331        If just 'reset' is True, set the scrollbuffer view to the current332        terminal content.333        """334        if reset:335            self.scrolling_up = 0336            self.set_term_cursor()337            return338 339        if lines is None:340            lines = self.height // 2341 342        if not up:343            lines = -lines344 345        maxscroll = len(self.scrollback_buffer)346        self.scrolling_up += lines347 348        if self.scrolling_up > maxscroll:349            self.scrolling_up = maxscroll350        elif self.scrolling_up < 0:351            self.scrolling_up = 0352 353        self.set_term_cursor()354 355    def reset(self) -> None:356        """357        Reset the terminal.358        """359        self.escbuf = b""360        self.within_escape = False361        self.parsestate = 0362 363        self.attrspec = None364        self.charset = TermCharset()365 366        self.saved_cursor = None367        self.saved_attrs = None368 369        self.is_rotten_cursor = False370 371        self.reset_scroll()372 373        self.init_tabstops()374 375        # terminal modes376        self.modes.reset()377 378        # initialize self.term379        self.clear()380 381    def init_tabstops(self, extend: bool = False) -> None:382        tablen, mod = divmod(self.width, 8)383        if mod > 0:384            tablen += 1385 386        if extend:387            while len(self.tabstops) < tablen:388                self.tabstops.append(1 << 0)389        else:390            self.tabstops = [1 << 0] * tablen391 392    def set_tabstop(self, x: int | None = None, remove: bool = False, clear: bool = False) -> None:393        if clear:394            for tab in range(len(self.tabstops)):395                self.tabstops[tab] = 0396            return397 398        if x is None:399            x = self.term_cursor[0]400 401        div, mod = divmod(x, 8)402        if remove:403            self.tabstops[div] &= ~(1 << mod)404        else:405            self.tabstops[div] |= 1 << mod406 407    def is_tabstop(self, x: int | None = None) -> bool:408        if x is None:409            x = self.term_cursor[0]410 411        div, mod = divmod(x, 8)412        return (self.tabstops[div] & (1 << mod)) > 0413 414    def empty_line(self, char: bytes = b" ") -> list[tuple[AttrSpec | None, str | None, bytes]]:415        return [self.empty_char(char)] * self.width416 417    def empty_char(self, char: bytes = b" ") -> tuple[AttrSpec | None, str | None, bytes]:418        return (self.attrspec, self.charset.current, char)419 420    def addstr(self, data: Iterable[int]) -> None:421        if self.width <= 0 or self.height <= 0:422            # not displayable, do nothing!423            return424 425        for byte in data:426            self.addbyte(byte)427 428    def resize(self, width: int, height: int) -> None:429        """430        Resize the terminal to the given width and height.431        """432        x, y = self.term_cursor433 434        if width > self.width:435            # grow436            for y in range(self.height):437                self.term[y] += [self.empty_char()] * (width - self.width)438        elif width < self.width:439            # shrink440            for y in range(self.height):441                self.term[y] = self.term[y][:width]442 443        self.width = width444 445        if height > self.height:446            # grow447            for _y in range(self.height, height):448                try:449                    last_line = self.scrollback_buffer.pop()450                except IndexError:451                    # nothing in scrollback buffer, append an empty line452                    self.term.append(self.empty_line())453                    self.scrollregion_end += 1454                    continue455 456                # adjust x axis of scrollback buffer to the current width457                if (padding := self.width - len(last_line)) > 0:458                    last_line += [self.empty_char()] * padding459                else:460                    last_line = last_line[: self.width]461 462                self.term.insert(0, last_line)463        elif height < self.height:464            # shrink465            for _y in range(height, self.height):466                self.scrollback_buffer.append(self.term.pop(0))467 468        self.height = height469 470        self.reset_scroll()471 472        x, y = self.constrain_coords(x, y)473        self.set_term_cursor(x, y)474 475        # extend tabs476        self.init_tabstops(extend=True)477 478    def set_g01(self, char: bytes, mod: bytes) -> None:479        """480        Set G0 or G1 according to 'char' and modifier 'mod'.481        """482        if self.modes.main_charset != CHARSET_DEFAULT:483            return484 485        if mod == b"(":486            g = 0487        else:488            g = 1489 490        if char == b"0":491            cset = "vt100"492        elif char == b"U":493            cset = "ibmpc"494        elif char == b"K":495            cset = "user"496        else:497            cset = "default"498 499        self.charset.define(g, cset)500 501    def parse_csi(self, char: bytes) -> None:502        """503        Parse ECMA-48 CSI (Control Sequence Introducer) sequences.504        """505        qmark = self.escbuf.startswith(b"?")506 507        escbuf = []508        for arg in self.escbuf[1 if qmark else 0 :].split(b";"):509            try:510                num = int(arg)511            except ValueError:512                num = None513 514            escbuf.append(num)515 516        if (cmd_ := CSI_COMMANDS[char]) is not None:517            if isinstance(cmd_, CSIAlias):518                csi_cmd: CSICommand = CSI_COMMANDS[cmd_.alias]  # type: ignore[assignment]519            elif isinstance(cmd_, CSICommand):520                csi_cmd = cmd_521            elif cmd_[0] == "alias":  # fallback, hard deprecated522                csi_cmd = CSI_COMMANDS[CSIAlias(*cmd_).alias]523            else:524                csi_cmd = CSICommand(*cmd_)  # fallback, hard deprecated525 526            number_of_args, default_value, cmd = csi_cmd527            while len(escbuf) < number_of_args:528                escbuf.append(default_value)529            for i in range(len(escbuf)):530                if escbuf[i] is None or escbuf[i] == 0:531                    escbuf[i] = default_value532 533            with suppress(ValueError):534                cmd(self, escbuf, qmark)535                # ignore commands that don't match the536                # unpacked tuples in CSI_COMMANDS.537 538    def parse_noncsi(self, char: bytes, mod: bytes = b"") -> None:539        """540        Parse escape sequences which are not CSI.541        """542        if mod == b"#" and char == b"8":543            self.decaln()544        elif mod == b"%":  # select main character set545            if char == b"@":546                self.modes.main_charset = CHARSET_DEFAULT547            elif char in b"G8":548                # 8 is obsolete and only for backwards compatibility549                self.modes.main_charset = CHARSET_UTF8550        elif mod in {b"(", b")"}:  # define G0/G1551            self.set_g01(char, mod)552        elif char == b"M":  # reverse line feed553            self.linefeed(reverse=True)554        elif char == b"D":  # line feed555            self.linefeed()556        elif char == b"c":  # reset terminal557            self.reset()558        elif char == b"E":  # newline559            self.newline()560        elif char == b"H":  # set tabstop561            self.set_tabstop()562        elif char == b"Z":  # DECID563            self.widget.respond(f"{ESC}[?6c")564        elif char == b"7":  # save current state565            self.save_cursor(with_attrs=True)566        elif char == b"8":  # restore current state567            self.restore_cursor(with_attrs=True)568 569    def parse_osc(self, buf: bytes) -> None:570        """571        Parse operating system command.572        """573        if buf.startswith((b";", b"0;", b"2;")):574            # set window title575            self.widget.set_title(buf.decode().partition(";")[2])576 577    def parse_escape(self, char: bytes) -> None:578        if self.parsestate == 1:579            # within CSI580            if char in CSI_COMMANDS:581                self.parse_csi(char)582                self.parsestate = 0583            elif char in b"0123456789;" or (not self.escbuf and char == b"?"):584                self.escbuf += char585                return586        elif self.parsestate == 0 and char == b"]":587            # start of OSC588            self.escbuf = b""589            self.parsestate = 2590            return591        elif self.parsestate == 2 and char == b"\a":592            # end of OSC593            self.parse_osc(self.escbuf.lstrip(b"0"))594        elif self.parsestate == 2 and self.escbuf[-1:] + char == f"{ESC}\\".encode("iso8859-1"):595            # end of OSC596            self.parse_osc(self.escbuf[:-1].lstrip(b"0"))597        elif self.parsestate == 2 and self.escbuf.startswith(b"P") and len(self.escbuf) == 8:598            # set palette (ESC]Pnrrggbb)599            pass600        elif self.parsestate == 2 and not self.escbuf and char == b"R":601            # reset palette602            pass603        elif self.parsestate == 2:604            self.escbuf += char605            return606        elif self.parsestate == 0 and char == b"[":607            # start of CSI608            self.escbuf = b""609            self.parsestate = 1610            return611        elif self.parsestate == 0 and char in {b"%", b"#", b"(", b")"}:612            # non-CSI sequence613            self.escbuf = char614            self.parsestate = 3615            return616        elif self.parsestate == 3:617            self.parse_noncsi(char, self.escbuf)618        elif char in {b"c", b"D", b"E", b"H", b"M", b"Z", b"7", b"8", b">", b"="}:619            self.parse_noncsi(char)620 621        self.leave_escape()622 623    def leave_escape(self) -> None:624        self.within_escape = False625        self.parsestate = 0626        self.escbuf = b""627 628    def get_utf8_len(self, bytenum: int) -> int:629        """630        Process startbyte and return the number of bytes following it to get a631        valid UTF-8 multibyte sequence.632 633        bytenum -- an integer ordinal634        """635        length = 0636 637        while bytenum & 0x40:638            bytenum <<= 1639            length += 1640 641        return length642 643    def addbyte(self, byte: int) -> None:644        """645        Parse main charset and add the processed byte(s) to the terminal state646        machine.647 648        byte -- an integer ordinal649        """650        if self.modes.main_charset == CHARSET_UTF8 or util.get_encoding() == "utf8":651            if byte >= 0xC0:652                # start multibyte sequence653                self.utf8_eat_bytes = self.get_utf8_len(byte)654                self.utf8_buffer = bytearray([byte])655                return656            if 0x80 <= byte < 0xC0 and self.utf8_eat_bytes is not None:657                if self.utf8_eat_bytes > 1:658                    # continue multibyte sequence659                    self.utf8_eat_bytes -= 1660                    self.utf8_buffer.append(byte)661                    return662 663                # end multibyte sequence664                self.utf8_eat_bytes = None665 666                if sequence := (self.utf8_buffer + bytes([byte])).decode("utf-8", "ignore"):667                    char = sequence.encode(util.get_encoding(), "replace")668 669                else:670                    # invalid multibyte sequence, stop processing671                    return672            else:673                self.utf8_eat_bytes = None674                char = bytes([byte])675        else:676            char = bytes([byte])677 678        self.process_char(char)679 680    def process_char(self, char: int | bytes) -> None:681        """682        Process a single character (single- and multi-byte).683 684        char -- a byte string685        """686        x, y = self.term_cursor687 688        if isinstance(char, int):689            char = char.to_bytes(1, "little")690 691        dc = self.modes.display_ctrl692 693        if char == ESC_B and self.parsestate != 2:  # escape694            self.within_escape = True695        elif not dc and char == b"\r":  # carriage return CR696            self.carriage_return()697        elif not dc and char == b"\x0f":  # activate G0698            self.charset.activate(0)699        elif not dc and char == b"\x0e":  # activate G1700            self.charset.activate(1)701        elif not dc and char in b"\n\v\f":  # line feed LF/VT/FF702            self.linefeed()703            if self.modes.lfnl:704                self.carriage_return()705        elif not dc and char == b"\t":  # char tab706            self.tab()707        elif not dc and char == b"\b":  # backspace BS708            if x > 0:709                self.set_term_cursor(x - 1, y)710        elif not dc and char == b"\a" and self.parsestate != 2:  # BEL711            # we need to check if we're in parsestate 2, as an OSC can be712            # terminated by the BEL character!713            self.widget.beep()714        elif not dc and char in b"\x18\x1a":  # CAN/SUB715            self.leave_escape()716        elif not dc and char in b"\x00\x7f":  # NUL/DEL717            pass  # this is ignored718        elif self.within_escape:719            self.parse_escape(char)720        elif not dc and char == b"\x9b":  # CSI (equivalent to "ESC [")721            self.within_escape = True722            self.escbuf = b""723            self.parsestate = 1724        else:725            self.push_cursor(char)726 727    def set_char(self, char: bytes, x: int | None = None, y: int | None = None) -> None:728        """729        Set character of either the current cursor position730        or a position given by 'x' and/or 'y' to 'char'.731        """732        if x is None:733            x = self.term_cursor[0]734        if y is None:735            y = self.term_cursor[1]736 737        x, y = self.constrain_coords(x, y)738        self.term[y][x] = (self.attrspec, self.charset.current, char)739 740    def constrain_coords(self, x: int, y: int, ignore_scrolling: bool = False) -> tuple[int, int]:741        """742        Checks if x/y are within the terminal and returns the corrected version.743        If 'ignore_scrolling' is set, constrain within the full size of the744        screen and not within scrolling region.745        """746        if x >= self.width:747            x = self.width - 1748        elif x < 0:749            x = 0750 751        if self.modes.constrain_scrolling and not ignore_scrolling:752            if y > self.scrollregion_end:753                y = self.scrollregion_end754            elif y < self.scrollregion_start:755                y = self.scrollregion_start756        else:  # noqa: PLR5501  # pylint: disable=else-if-used  # readability757            if y >= self.height:758                y = self.height - 1759            elif y < 0:760                y = 0761 762        return x, y763 764    def linefeed(self, reverse: bool = False) -> None:765        """766        Move the cursor down (or up if reverse is True) one line but don't reset767        horizontal position.768        """769        x, y = self.term_cursor770 771        if reverse:772            if y <= 0 < self.scrollregion_start:773                pass774            elif y == self.scrollregion_start:775                self.scroll(reverse=True)776            else:777                y -= 1778        else:  # noqa: PLR5501  # pylint: disable=else-if-used  # readability779            if y >= self.height - 1 > self.scrollregion_end:780                pass781            elif y == self.scrollregion_end:782                self.scroll()783            else:784                y += 1785 786        self.set_term_cursor(x, y)787 788    def carriage_return(self) -> None:789        self.set_term_cursor(0, self.term_cursor[1])790 791    def newline(self) -> None:792        """793        Do a carriage return followed by a line feed.794        """795        self.carriage_return()796        self.linefeed()797 798    def move_cursor(799        self,800        x: int,801        y: int,802        relative_x: bool = False,803        relative_y: bool = False,804        relative: bool = False,805    ) -> None:806        """807        Move cursor to position x/y while constraining terminal sizes.808        If 'relative' is True, x/y is relative to the current cursor809        position. 'relative_x' and 'relative_y' is the same but just with810        the corresponding axis.811        """812        if relative:813            relative_y = relative_x = True814 815        if relative_x:816            x += self.term_cursor[0]817 818        if relative_y:819            y += self.term_cursor[1]820        elif self.modes.constrain_scrolling:821            y += self.scrollregion_start822 823        self.set_term_cursor(x, y)824 825    def push_char(self, char: bytes | None, x: int, y: int) -> None:826        """827        Push one character to current position and advance cursor to x/y.828        """829        if char is not None:830            char = self.charset.apply_mapping(char)831            if self.modes.insert:832                self.insert_chars(char=char)833            else:834                self.set_char(char)835 836        self.set_term_cursor(x, y)837 838    def push_cursor(self, char: bytes | None = None) -> None:839        """840        Move cursor one character forward wrapping lines as needed.841        If 'char' is given, put the character into the former position.842        """843        x, y = self.term_cursor844 845        if self.modes.autowrap:846            if x + 1 >= self.width and not self.is_rotten_cursor:847                # "rotten cursor" - this is when the cursor gets to the rightmost848                # position of the screen, the cursor position remains the same but849                # one last set_char() is allowed for that piece of sh^H^H"border".850                self.is_rotten_cursor = True851                self.push_char(char, x, y)852            else:853                x += 1854 855                if x >= self.width and self.is_rotten_cursor:856                    if y >= self.scrollregion_end:857                        self.scroll()858                    else:859                        y += 1860 861                    x = 1862 863                    self.set_term_cursor(0, y)864 865                self.push_char(char, x, y)866 867                self.is_rotten_cursor = False868        else:869            if x + 1 < self.width:870                x += 1871 872            self.is_rotten_cursor = False873            self.push_char(char, x, y)874 875    def save_cursor(self, with_attrs: bool = False) -> None:876        self.saved_cursor = tuple(self.term_cursor)877        if with_attrs:878            self.saved_attrs = (copy.copy(self.attrspec), copy.copy(self.charset))879 880    def restore_cursor(self, with_attrs: bool = False) -> None:881        if self.saved_cursor is None:882            return883 884        x, y = self.saved_cursor885        self.set_term_cursor(x, y)886 887        if with_attrs and self.saved_attrs is not None:888            self.attrspec, self.charset = (copy.copy(self.saved_attrs[0]), copy.copy(self.saved_attrs[1]))889 890    def tab(self, tabstop: int = 8) -> None:891        """892        Moves cursor to the next 'tabstop' filling everything in between893        with spaces.894        """895        x, y = self.term_cursor896 897        while x < self.width - 1:898            self.set_char(b" ")899            x += 1900 901            if self.is_tabstop(x):902                break903 904        self.is_rotten_cursor = False905        self.set_term_cursor(x, y)906 907    def scroll(self, reverse: bool = False) -> None:908        """909        Append a new line at the bottom and put the topmost line into the910        scrollback buffer.911 912        If reverse is True, do exactly the opposite, but don't save into913        scrollback buffer.914        """915        if reverse:916            self.term.pop(self.scrollregion_end)917            self.term.insert(self.scrollregion_start, self.empty_line())918        else:919            killed = self.term.pop(self.scrollregion_start)920            self.scrollback_buffer.append(killed)921            self.term.insert(self.scrollregion_end, self.empty_line())922 923    def decaln(self) -> None:924        """925        DEC screen alignment test: Fill screen with E's.926        """927        for row in range(self.height):928            self.term[row] = self.empty_line(b"E")929 930    def blank_line(self, row: int) -> None:931        """932        Blank a single line at the specified row, without modifying other lines.933        """934        self.term[row] = self.empty_line()935 936    def insert_chars(937        self,938        position: tuple[int, int] | None = None,939        chars: int = 1,940        char: bytes | None = None,941    ) -> None:942        """943        Insert 'chars' number of either empty characters - or those specified by944        'char' - before 'position' (or the current position if not specified)945        pushing subsequent characters of the line to the right without wrapping.946        """947        if position is None:948            position = self.term_cursor949 950        if chars == 0:951            chars = 1952 953        if char is None:954            char_spec = self.empty_char()955        else:956            char_spec = (self.attrspec, self.charset.current, char)957 958        x, y = position959 960        while chars > 0:961            self.term[y].insert(x, char_spec)962            self.term[y].pop()963            chars -= 1964 965    def remove_chars(self, position: tuple[int, int] | None = None, chars: int = 1) -> None:966        """967        Remove 'chars' number of empty characters from 'position' (or the current968        position if not specified) pulling subsequent characters of the line to969        the left without joining any subsequent lines.970        """971        if position is None:972            position = self.term_cursor973 974        if chars == 0:975            chars = 1976 977        x, y = position978 979        while chars > 0:980            self.term[y].pop(x)981            self.term[y].append(self.empty_char())982            chars -= 1983 984    def insert_lines(self, row: int | None = None, lines: int = 1) -> None:985        """986        Insert 'lines' of empty lines after the specified row, pushing all987        subsequent lines to the bottom. If no 'row' is specified, the current988        row is used.989        """990        if row is None:991            row = self.term_cursor[1]992        else:993            row = self.scrollregion_start994 995        if lines == 0:996            lines = 1997 998        while lines > 0:999            self.term.insert(row, self.empty_line())1000            self.term.pop(self.scrollregion_end)1001            lines -= 11002 1003    def remove_lines(self, row: int | None = None, lines: int = 1) -> None:1004        """1005        Remove 'lines' number of lines at the specified row, pulling all1006        subsequent lines to the top. If no 'row' is specified, the current row1007        is used.1008        """1009        if row is None:1010            row = self.term_cursor[1]1011        else:1012            row = self.scrollregion_start1013 1014        if lines == 0:1015            lines = 11016 1017        while lines > 0:1018            self.term.pop(row)1019            self.term.insert(self.scrollregion_end, self.empty_line())1020            lines -= 11021 1022    def erase(1023        self,1024        start: tuple[int, int] | tuple[int, int, bool],1025        end: tuple[int, int] | tuple[int, int, bool],1026    ) -> None:1027        """1028        Erase a region of the terminal. The 'start' tuple (x, y) defines the1029        starting position of the erase, while end (x, y) the last position.1030 1031        For example if the terminal size is 4x3, start=(1, 1) and end=(1, 2)1032        would erase the following region:1033 1034        ....1035        .XXX1036        XX..1037        """1038        sx, sy = self.constrain_coords(*start)1039        ex, ey = self.constrain_coords(*end)1040 1041        # within a single row1042        if sy == ey:1043            for x in range(sx, ex + 1):1044                self.term[sy][x] = self.empty_char()1045            return1046 1047        # spans multiple rows1048        y = sy1049        while y <= ey:1050            if y == sy:1051                for x in range(sx, self.width):1052                    self.term[y][x] = self.empty_char()1053            elif y == ey:1054                for x in range(ex + 1):1055                    self.term[y][x] = self.empty_char()1056            else:1057                self.blank_line(y)1058 1059            y += 11060 1061    def sgi_to_attrspec(1062        self,1063        attrs: Sequence[int],1064        fg: int,1065        bg: int,1066        attributes: set[str],1067        prev_colors: int,1068    ) -> AttrSpec | None:1069        """1070        Parse SGI sequence and return an AttrSpec representing the sequence1071        including all earlier sequences specified as 'fg', 'bg' and1072        'attributes'.1073        """1074 1075        idx = 01076        colors = prev_colors1077 1078        while idx < len(attrs):1079            attr = attrs[idx]1080            if 30 <= attr <= 37:1081                fg = attr - 301082                colors = max(16, colors)1083            elif 40 <= attr <= 47:1084                bg = attr - 401085                colors = max(16, colors)1086            # AIXTERM bright color spec1087            # https://en.wikipedia.org/wiki/ANSI_escape_code1088            elif 90 <= attr <= 97:1089                fg = attr - 90 + 81090                colors = max(16, colors)1091            elif 100 <= attr <= 107:1092                bg = attr - 100 + 81093                colors = max(16, colors)1094            elif attr in {38, 48}:1095                if idx + 2 < len(attrs) and attrs[idx + 1] == 5:1096                    # 8 bit color specification1097                    color = attrs[idx + 2]1098                    colors = max(256, colors)1099                    if attr == 38:1100                        fg = color1101                    else:1102                        bg = color1103                    idx += 21104                elif idx + 4 < len(attrs) and attrs[idx + 1] == 2:1105                    # 24 bit color specification1106                    color = (attrs[idx + 2] << 16) + (attrs[idx + 3] << 8) + attrs[idx + 4]1107                    colors = 2**241108                    if attr == 38:1109                        fg = color1110                    else:1111                        bg = color1112                    idx += 41113            elif attr == 39:1114                # set default foreground color1115                fg = None1116            elif attr == 49:1117                # set default background color1118                bg = None1119            elif attr == 10:1120                self.charset.reset_sgr_ibmpc()1121                self.modes.display_ctrl = False1122            elif attr in {11, 12}:1123                self.charset.set_sgr_ibmpc()1124                self.modes.display_ctrl = True1125 1126            # set attributes1127            elif attr == 1:1128                attributes.add("bold")1129            elif attr == 4:1130                attributes.add("underline")1131            elif attr == 5:1132                attributes.add("blink")1133            elif attr == 7:1134                attributes.add("standout")1135 1136            # unset attributes1137            elif attr == 24:1138                attributes.discard("underline")1139            elif attr == 25:1140                attributes.discard("blink")1141            elif attr == 27:1142                attributes.discard("standout")1143            elif attr == 0:1144                # clear all attributes1145                fg = bg = None1146                attributes.clear()1147 1148            idx += 11149 1150        if "bold" in attributes and colors == 16 and fg is not None and fg < 8:1151            fg += 81152 1153        def _defaulter(color: int | None, colors: int) -> str:1154            if color is None:1155                return "default"1156            # Note: we can't detect 88 color mode1157            if color > 255 or colors == 2**24:1158                return _color_desc_true(color)1159            if color > 15 or colors == 256:1160                return _color_desc_256(color)1161            return _BASIC_COLORS[color]1162 1163        decoded_fg = _defaulter(fg, colors)1164        decoded_bg = _defaulter(bg, colors)1165 1166        if attributes:1167            decoded_fg = ",".join((decoded_fg, *list(attributes)))1168 1169        if decoded_fg == decoded_bg == "default":1170            return None1171 1172        if colors:1173            return AttrSpec(decoded_fg, decoded_bg, colors=colors)1174 1175        return AttrSpec(decoded_fg, decoded_bg)1176 1177    def csi_set_attr(self, attrs: Sequence[int]) -> None:1178        """1179        Set graphics rendition.1180        """1181        if attrs[-1] == 0:1182            self.attrspec = None1183 1184        attributes = set()1185        if self.attrspec is None:1186            fg = bg = None1187        else:1188            # set default values from previous attrspec1189            if "default" in self.attrspec.foreground:1190                fg = None1191            else:1192                fg = self.attrspec.foreground_number1193                if fg >= 8 and self.attrspec.colors == 16:1194                    fg -= 81195 1196            if "default" in self.attrspec.background:1197                bg = None1198            else:1199                bg = self.attrspec.background_number1200                if bg >= 8 and self.attrspec.colors == 16:

Showing the first 1,200 of 1780 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai