code comments changes

This commit is contained in:
Vaclav Tvrdik
2026-09-10 10:07:12 +02:00
parent d2b763a95a
commit 2a8fffb9c6
13 changed files with 123 additions and 208 deletions

View File

@@ -83,7 +83,7 @@ assert terminal.screen.theme_is_dark is True
# without mode 2031 the app is not interrupted
assert recorder.written == "", repr(recorder.written)
# with it, the dark/light report is sent so the agent can restyle itself
# with it, the dark/light report is sent so the app can restyle itself
stream.feed("\x1b[?2031h")
recorder.written = ""
terminal.apply_scheme()

View File

@@ -24,7 +24,7 @@ def feed(stream, data, char_by_char=False):
def run_case(char_by_char):
tag = "char-by-char" if char_by_char else "chunked"
# DA1 answered (Claude Code's untimed sentinel)
# DA1 answered; apps can wait on it with no timeout
proc, screen, stream = make()
feed(stream, "\x1b[c", char_by_char)
assert proc.written == "\x1b[?62;22c", (tag, proc.written)
@@ -34,7 +34,7 @@ def run_case(char_by_char):
feed(stream, "\x1b[5;10H\x1b[6n\x1b[?6n", char_by_char)
assert proc.written == "\x1b[5;10R\x1b[?5;10R", (tag, proc.written)
# XTVERSION then DECRQM 2026 (the Claude Code probe sequence)
# XTVERSION then DECRQM 2026, the sync-output probe sequence
proc, screen, stream = make()
feed(stream, "\x1b[>0q\x1b[c\x1b[?2026$p\x1b[c", char_by_char)
assert proc.written == (
@@ -54,8 +54,8 @@ def run_case(char_by_char):
# and nothing leaked to the screen
assert screen.buffer[0][0].data == " ", (tag, screen.buffer[0][0])
# modifyOtherKeys: the level is tracked (pi enables it and reads shift+enter
# no other way), and the sequence must not corrupt SGR state
# modifyOtherKeys: the level is tracked, and the sequence must not corrupt
# SGR state
proc, screen, stream = make()
assert screen.modify_other_keys == 0, tag
feed(stream, "\x1b[>4;2mhello", char_by_char)
@@ -211,8 +211,8 @@ def run_resize_cases():
feed(stream, "\x1b[?1049l", False)
assert max(screen.buffer[0]) < 8, sorted(screen.buffer[0])
# a shrink clamps the cursor (pyte does it via restore_cursor); without
# the clamp draw() writes to rows above or below the screen
# a shrink clamps the cursor; without the clamp draw() writes to rows
# above or below the screen
proc, screen, stream = make(10, 8)
feed(stream, "\r\n".join("line{}".format(i) for i in range(8)), False)
feed(stream, "\x1b[1;1H", False) # full screen, cursor at home
@@ -239,8 +239,8 @@ def run_resize_cases():
def run_private_csi_cases():
"""No private CSI may raise: an exception here used to kill the renderer
thread and freeze the tab. aerc probes XTSMGRAPHICS (ESC[?2;1;0S)."""
"""No private CSI may raise: an exception here kills the renderer thread
and freezes the tab. aerc probes XTSMGRAPHICS (ESC[?2;1;0S)."""
for final in "ABCDEFGHJKLMPSTXZbcdfghlmnqrstu@":
proc, screen, stream = make()
stream.feed("\x1b[?2;1;0" + final) # multi-parameter private CSI
@@ -269,9 +269,9 @@ def run_private_csi_cases():
def run_decscnm_cases():
"""A DECSCNM flash (vim's visual bell) must not outlive itself.
pyte points every line at the blank cell from inside set/reset_mode, so a
line kept a reverse default and painted the columns the app never wrote —
vim skips indentation with cursor-forward — as white blocks.
The lines point at the blank cell, so one keeping a reverse default paints
the columns the app never wrote — vim skips indentation with cursor-forward
— as white blocks.
"""
from vterm.render import segment_buffer_line
@@ -327,7 +327,7 @@ def run_fast_path_cases():
return (rows, c.x, c.y, c.attrs, set(screen.mode), screen.title)
def parser_only(blob):
"""The blob through the coroutine parser alone: pyte's original feed."""
"""The blob through the coroutine parser alone, bypassing the fast path."""
proc, screen, stream = make(columns=30, lines=8)
import pyte
pyte.Stream.feed(stream, blob)
@@ -407,7 +407,7 @@ def run_clear_cases():
assert len(screen.history) > 0, "history suppressed for too long"
# ED 3 is app-driven: it clears scrollback and fires the clear callback,
# but (as in xterm, unlike pyte) leaves the visible screen alone
# but (as in xterm) leaves the visible screen alone
cleared = []
proc = FakeProcess()
screen = VtScreen(10, 4, process=proc, clear_callback=lambda: cleared.append(1))
@@ -418,8 +418,8 @@ def run_clear_cases():
assert len(screen.history) == 0 and cleared, (len(screen.history), cleared)
assert screen.buffer[0][0].data == "a", screen.buffer[0][0]
# with erase_scrollback off, ED 3 is a no-op: agents repaint with it and
# would take the history and the view with them
# with erase_scrollback off, ED 3 is a no-op: an app repainting with it
# would take the history and the view with it
cleared = []
proc = FakeProcess()
screen = VtScreen(10, 4, process=proc, erase_scrollback=False,

View File

@@ -54,8 +54,8 @@ for char in string.ascii_letters + string.digits + string.punctuation:
assert get_key_code(key="space") == " ", "space must send a space"
# legacy encodings by default; while the app has modifyOtherKeys on, the keys
# whose modified form is ambiguous take the CSI 27 form instead. pi reads a
# bare ESC CR as alt+enter and submits on it
# whose modified form is ambiguous take the CSI 27 form instead. a bare ESC CR
# reads as alt+enter
assert get_key_code(key="enter", shift=True) == "\x1b\r"
assert get_key_code(key="enter", shift=True, modify_other_keys=2) == "\x1b[27;2;13~"
assert get_key_code(key="space", ctrl=True, modify_other_keys=2) == "\x1b[27;5;32~"

View File

@@ -226,8 +226,8 @@ assert len(selection) == 1 and selection[0].empty(), \
print("selection tests passed (user selection survives renders, caret still tracked)")
# an app that hides the cursor (pi hides it for good) still needs the caret:
# sublime drops a space keypress in a view that has no caret at all
# an app that hides the cursor still needs the caret: sublime drops a space
# keypress in a view that has no caret at all
selection.clear()
stream.feed("\x1b[?25l")
focus_cursor(terminal.view, None, terminal)
@@ -326,7 +326,7 @@ command.run(None)
kept = [(r.begin(), r.end()) for r in terminal.view.sel()]
assert any(b != e for b, e in kept), "user selection was dropped: {}".format(kept)
# pyte's dirty is a repaint hint: rows whose content did not change must not
# the dirty set is a repaint hint: rows whose content did not change must not
# be rewritten (each rewrite is a cross-process call and an on_modified)
rewritten = []
_replace = terminal.view.replace
@@ -523,8 +523,8 @@ print("view scroll tests passed (a screen or a row per press, clamped to the anc
# shift+enter through the whole keypress path: ESC CR by default, and the CSI 27
# form once the app turned modifyOtherKeys on. pi enables it (its kitty probe
# gets no flags from us) and reads a bare ESC CR as alt+enter, which submits
# form once the app turned modifyOtherKeys on, which it reaches for because a
# bare ESC CR reads as alt+enter
press(key="enter", shift=True)
assert terminal.process.written == "\x1b\r", repr(terminal.process.written)
stream.feed("\x1b[>4;2m")

View File

@@ -22,14 +22,7 @@ class VtermOpenCommand(sublime_plugin.WindowCommand):
"""Open a terminal in a new tab. `vterm_open`, the only entry point users bind."""
def run(self, cmd=None, cwd=None, env=None, title=None):
"""Start `cmd` (default: a login shell) in a fresh view.
Args:
cwd: defaults to the first project folder, else the active file's
directory, else the home directory.
env: extra environment on top of the child defaults.
title: tab name; the program name unless the app sets its own.
"""
"""Start `cmd` (default: a login shell) in a fresh view"""
window = self.window
if not cmd:
@@ -38,7 +31,7 @@ class VtermOpenCommand(sublime_plugin.WindowCommand):
elif isinstance(cmd, str):
cmd = [cmd]
# the app may override the title via OSC 0/2 later
# cwd: the first project folder, else the active file's directory, else home
if not cwd:
folders = window.folders()
view = window.active_view()
@@ -144,9 +137,7 @@ class VtermKeypressCommand(sublime_plugin.TextCommand):
"""Sends one key to the app. Every key binding in the generated keymap lands here."""
def run(self, _, **kwargs):
"""Args:
kwargs: key name plus modifier flags, straight from the keymap.
"""
"""Send one key; the name and modifier flags come straight from the keymap"""
terminal = Terminal.from_id(self.view.id())
if not terminal or not terminal.alive:
return
@@ -158,13 +149,11 @@ class VtermKeypressCommand(sublime_plugin.TextCommand):
terminal.send_key(**kwargs)
def view_scrolled(self, terminal, key=None, **mods):
"""Scroll the view over the scrollback, before vterm_show_cursor, whose
scroll would pull the viewport straight back down. PgUp/PgDn move a
screen, Up/Down one line. The alt screen has its scrollback folded
away, so an app there keeps the key.
"""Whether the view consumed the key by scrolling over the scrollback.
Returns:
bool: the view consumed the key; False sends it to the app.
Runs before vterm_show_cursor, whose scroll would pull the viewport
straight back down. PgUp/PgDn move a screen, Up/Down one line. On the
alt screen the scrollback is folded away, so the app keeps the key.
"""
# the cheap tests first: this runs on every keystroke
if key not in SCROLL_KEYS or not mods or terminal.screen.alternate_buffer_mode:
@@ -206,10 +195,7 @@ class VtermPasteTextCommand(sublime_plugin.TextCommand):
"""Sends arbitrary text to the app, bracketed when it asked for that."""
def run(self, edit, text, bracketed=True):
"""Args:
bracketed: wrap in paste brackets, if the app enabled ?2004. IME
text comes through unbracketed — it was typed, not pasted.
"""
"""Send text to the app, wrapped in paste brackets when it enabled ?2004"""
terminal = Terminal.from_id(self.view.id())
if not terminal:
return

View File

@@ -1,4 +1,4 @@
""" Terminal emulation: pyte screen and a parser that handles prefixed CSI, DECRQM and multi-digit OSC """
""" Terminal emulation: the screen and a parser for prefixed CSI, DECRQM and multi-digit OSC """
import base64
import logging
import re
@@ -36,7 +36,7 @@ ANSI_COLORS = [
"light_white",
]
# SGR 90-97 / 100-107: the bright colours, which stock pyte does not map
# SGR 90-97 / 100-107: the bright (aixterm) colours
FG_AIXTERM = {90 + i: ANSI_COLORS[8 + i] for i in range(8)}
BG_AIXTERM = {100 + i: ANSI_COLORS[8 + i] for i in range(8)}
@@ -44,7 +44,7 @@ BG_AIXTERM = {100 + i: ANSI_COLORS[8 + i] for i in range(8)}
REPORTABLE_MODES = (mo.DECAWM >> 5, mo.DECTCEM >> 5, 1000, 1002, 1003, 1004, 1006, 1049, 2004, 2026)
# DEC private modes, pre-shifted into pyte's internal representation.
# 1: DECCKM, application cursor keys (arrows send SS3); pyte has no name for it
# 1: DECCKM, application cursor keys (arrows send SS3)
APP_CURSOR_MODE = 1 << 5
# 2026: synchronized output, the app is mid-frame and we should not draw
SYNC_MODE = 2026 << 5
@@ -78,8 +78,8 @@ def _copy_line(line):
def quantize_256(r, g, b):
"""Nearest xterm-256 index: grays to the ramp, colours to the 6x6x6 cube.
Truecolor is folded into the 256 palette so the colour-scheme rule table
stays bounded (each distinct combo becomes a scheme rule and a file write)."""
Truecolor is folded into the palette so the scheme rule table stays bounded:
each distinct combo costs a rule and a file write."""
if r == g == b:
if r < 8:
return 16
@@ -102,18 +102,11 @@ Char = namedtuple(
class Cursor:
"""Cursor position and pending attributes.
Replaces pyte's own so a Char carrying the extra `linefeed` field can be
the default attribute set.
"""
"""Cursor position and the attributes new text inherits"""
__slots__ = ("x", "y", "attrs", "hidden")
def __init__(self, x, y, attrs=Char(" ")):
"""Args:
attrs: the Char whose styling new text inherits.
"""
self.x = x
self.y = y
self.attrs = attrs
@@ -121,18 +114,15 @@ class Cursor:
class VtScreen(pyte.Screen):
"""Screen with the terminal features these agents rely on.
Adds to pyte: the alternate screen, scrollback, synchronized output,
"""Screen with alternate buffer, scrollback, synchronized output,
mouse/focus/paste modes, cursor styles, query replies and OSC handling.
Mutated only by the renderer thread, so it carries no lock.
"""
@property
def default_char(self):
"""The blank cell an untouched column holds."""
# pyte's base property cannot be shadowed by an instance attribute;
# the value is maintained on reset and DECSCNM flips
"""The blank cell an untouched column holds"""
return self._default_char
def __init__(
@@ -148,14 +138,7 @@ class VtScreen(pyte.Screen):
bg_color="000000",
is_dark=True,
):
"""Args:
process: written to directly when the app queries the terminal.
history: scrollback depth, in lines.
erase_scrollback: whether ED 3 drops the scrollback, as a terminal should.
clear_callback: the app cleared the screen; the view follows.
clipboard_callback: an OSC 52 payload for the system clipboard.
fg_color, bg_color, is_dark: palette facts reported back to the app.
"""
"""Set up the buffers, the caches and the palette facts reported to the app"""
self._process = process
self._erase_scrollback = erase_scrollback
self._clear_callback = clear_callback
@@ -178,7 +161,7 @@ class VtScreen(pyte.Screen):
super().__init__(columns, lines)
def reset(self):
"""RIS: pyte's reset plus our cursor, scrollback and caches."""
"""RIS: reset the screen along with the cursor, scrollback and caches"""
self._default_char = Char(data=" ", fg="default", bg="default", reverse=False)
self._sgr_cache.clear()
super().reset()
@@ -194,20 +177,12 @@ class VtScreen(pyte.Screen):
self.cursor_style = style
def set_modify_other_keys(self, resource=0, value=0):
"""xterm CSI > 4 ; n m: send modified keys as CSI 27 ; mod ; code ~.
pi turns this on whenever the kitty probe answers with no flags, and
reads shift+enter no other way.
"""
"""xterm CSI > 4 ; n m: send modified keys as CSI 27 ; mod ; code ~"""
if resource == 4:
self.modify_other_keys = value
def resize(self, lines=None, columns=None):
"""Resize the screen, pushing scrolled-off lines into scrollback.
Unlike pyte's, shrinking preserves content: rows that no longer fit go
to history rather than being dropped.
"""
"""Resize the screen; on a shrink the rows that no longer fit go to scrollback"""
lines = lines or self.lines
columns = columns or self.columns
@@ -239,8 +214,7 @@ class VtScreen(pyte.Screen):
line.pop(x, None)
self.lines, self.columns = lines, columns
# pyte clamps via restore_cursor; without it a shrink leaves the cursor
# negative or past the new edge. x == columns keeps a pending wrap
# a shrink can leave the cursor past the new edge; x == columns keeps a pending wrap
self.cursor.x = min(self.cursor.x, columns)
self.cursor.y = min(max(self.cursor.y, 0), lines - 1)
self.set_margins()
@@ -272,9 +246,8 @@ class VtScreen(pyte.Screen):
if reverse != self._default_char.reverse:
self._default_char = self._default_char._replace(reverse=reverse)
self._sgr_cache.clear()
# pyte points every line at the old blank cell from inside
# set/reset_mode, before this runs; a stale one paints the columns
# the app never wrote reverse for good
# the lines still point at the old blank cell, which would paint
# untouched columns reversed for good
for line in self.buffer.values():
line.default = self._default_char
@@ -340,12 +313,11 @@ class VtScreen(pyte.Screen):
return BRACKETED_PASTE_MODE in self.mode
def draw(self, data):
"""Write text at the cursor, wcwidth-aware, marking hard wraps.
"""Write text at the cursor, wcwidth-aware, marking soft wraps.
Differs from pyte: double-width cells claim two columns, combining marks
are merged into the preceding cell as NFC (sublime renders decomposed
sequences badly), and an autowrap sets `linefeed` on the last cell so
copy can tell a soft wrap from a real newline.
Double-width cells claim two columns, combining marks merge into the
preceding cell as NFC, and an autowrap sets `linefeed` on the last cell
so copy can tell a soft wrap from a real newline.
"""
if self.charset:
data = data.translate(self.g1_charset)
@@ -359,8 +331,7 @@ class VtScreen(pyte.Screen):
cursor = self.cursor
buffer = self.buffer
columns = self.columns
# cells only differ by their data while attrs are unchanged, so a
# per-run cache turns the namedtuple rebuild into a dict lookup
# per-run cache: cells differ only by their data while attrs are unchanged
if cursor.attrs is not self._cache_attrs:
self._cache_attrs = cursor.attrs
self._char_cache = {}
@@ -431,10 +402,10 @@ class VtScreen(pyte.Screen):
super().index()
def erase_in_display(self, how=0, *args, **kwargs):
"""ED: erase part of the screen, keeping what scrolls away.
"""ED: erase part of the screen.
Unlike pyte's, a full clear (or a clear-to-end from home) pushes the
current screen into scrollback first, so `clear` does not lose history.
A full clear (or a clear-to-end from home) pushes the screen into
scrollback first, so `clear` does not lose history.
"""
if not self.alternate_buffer_mode and (how == 2 or (how == 0 and self.cursor.x == 0 and self.cursor.y == 0)):
self.push_lines_into_history()
@@ -470,8 +441,8 @@ class VtScreen(pyte.Screen):
def select_graphic_rendition(self, *attrs, **kwargs):
"""Apply an SGR sequence to the cursor attributes.
Adds to pyte: bright (aixterm) colours, and truecolor folded into the
256 palette. Transitions are memoized — TUIs cycle through a handful of
Covers bright (aixterm) colours and truecolor folded into the 256
palette. Transitions are memoized: TUIs cycle through a handful of
sequences thousands of times per frame.
"""
if not attrs or attrs == (0,):
@@ -575,14 +546,12 @@ class VtScreen(pyte.Screen):
count = self.first_non_empty_line_from_bottom() + 1
self.history.extend(_copy_line(self.buffer[y]) for y in range(count))
# replies go to the pty, never to the screen
def write_process_input(self, data):
"""Send a reply to the app. Query answers go to the pty, never to the screen."""
"""Send a query reply to the app: it goes to the pty, never to the screen"""
self._process.write(data)
def report_device_attributes(self, mode=0, **kwargs):
"""DA1: claim to be a VT220 with ANSI colour. Claude Code waits on this with no timeout."""
"""DA1: claim to be a VT220 with ANSI colour"""
if kwargs.get("private"):
return
if mode == 0:
@@ -603,7 +572,7 @@ class VtScreen(pyte.Screen):
marker = "?" if private else ""
self.write_process_input("\x1b[{}{};{}R".format(marker, y, x))
elif mode == 996 and private:
# color-scheme report, pi asks with a 2s timeout
# color-scheme report
self.write_process_input(self.theme_report())
def theme_report(self):
@@ -628,7 +597,7 @@ class VtScreen(pyte.Screen):
def report_kitty_keyboard(self):
"""Answer the kitty keyboard protocol probe."""
# answering at all means "supported"; 0 flags means none are enabled,
# which is what steers pi onto its modifyOtherKeys fallback
# which steers an app onto its modifyOtherKeys fallback
self.write_process_input("\x1b[?0u")
def handle_osc(self, code, payload, terminator):
@@ -656,24 +625,21 @@ class VtScreen(pyte.Screen):
# a complete plain CSI: params then a final byte. No private marker, prefix or
# intermediate — those forms, and sequences split across reads, take the parser
CSI_COMPLETE = re.compile("(?:\x1b\\[|\x9b)([0-9;]*)([@-~])")
# a read ending mid-CSI; held for the next read, as the parser would hold state.
# bounded so a run of digits with no final cannot buffer forever
# a read ending mid-CSI, held for the next read; bounded so a run of digits
# with no final byte cannot buffer forever
CSI_PARTIAL = re.compile("(?:\x1b\\[?|\x9b)[0-9;]{0,32}$")
class VtStream(pyte.Stream):
"""pyte.Stream with a parser for prefixed CSI, DECRQM and multi-digit OSC
"""Stream with a parser for `>`/`<`/`=` prefixed CSI, the DECRQM `$p`
intermediate and multi-digit OSC codes.
Stock pyte drops what these agents rely on: `>`/`<`/`=` prefixed CSI, the
DECRQM `$p` intermediate, and OSC codes longer than one digit.
feed() dispatches complete plain CSI and lone control characters directly:
pyte resumes the parser coroutine once per character, which was ~90% of the
cost of an escape-heavy flood.
feed() dispatches complete plain CSI and lone control characters directly,
rather than resuming the parser coroutine once per character.
"""
def __init__(self, *args, **kwargs):
"""Copy the CSI table before adding to it, so pyte's class-level one is untouched."""
"""Copy the CSI table before adding to it: the class-level one is shared"""
self.csi = dict(self.csi)
self.csi["S"] = "scroll_up"
self.csi["T"] = "scroll_down"
@@ -683,10 +649,10 @@ class VtStream(pyte.Stream):
super().__init__(*args, **kwargs)
def attach(self, screen):
"""Bind the fast dispatch tables when the listener is known."""
"""Bind the fast dispatch tables once the listener is known"""
super().attach(screen)
# same construction as the parser's create_dispatcher, so an unknown
# final lands on listener.debug either way
# built like the parser's dispatchers, so an unknown final byte lands
# on listener.debug either way
self._fast_csi = defaultdict(
lambda: screen.debug,
{final: getattr(screen, name) for final, name in self.csi.items()},
@@ -696,15 +662,13 @@ class VtStream(pyte.Stream):
self._fast_basic = {char: getattr(screen, name) for char, name in self.basic.items() if char not in (ctrl.SI, ctrl.SO)}
def feed(self, data):
"""pyte's feed with a ground-state fast path.
"""Parse data into the screen, with a ground-state fast path.
Plain text runs keep pyte's regex path. A complete unprefixed CSI is
matched whole and dispatched in one call, and basic controls (CR, LF,
tab...) in one lookup, instead of one coroutine resume per character.
Everything else — OSC, charsets, private/prefixed CSI, `:` subparams —
falls through to the parser, byte for byte as before. A read ending
mid-CSI is carried into the next read; the parser would hold it as
state, so nothing is dispatched either way until the sequence is whole.
A complete unprefixed CSI is dispatched in one call and a lone control
character in one lookup; everything else — OSC, charsets,
private/prefixed CSI, `:` subparams — falls through to the parser. A
read ending mid-CSI is carried into the next one, so nothing dispatches
until the sequence is whole.
"""
# the tail of the previous read ended mid-CSI; complete it first
if self._carry:
@@ -728,7 +692,7 @@ class VtStream(pyte.Stream):
offset = 0
while offset < length:
if taking_plain_text:
# printable run: hand it to draw() whole (pyte's own fast path)
# printable run: hand it to draw() whole
match = match_text(data, offset)
if match:
start, offset = match.span()
@@ -739,7 +703,6 @@ class VtStream(pyte.Stream):
# complete plain CSI: dispatch here, skipping the parser
match = match_csi(data, offset)
if match:
# params memo: SGR strings repeat across a whole frame
raw = match.group(1)
params = cache.get(raw)
if params is None:
@@ -750,7 +713,7 @@ class VtStream(pyte.Stream):
try:
csi[match.group(2)](*params)
except TypeError:
# same guard as _dispatch_csi: excess params cost one sequence, not the chunk
# excess params cost one sequence, not the chunk
LOG.debug("malformed CSI %s%s", params, match.group(2))
offset = match.end()
continue
@@ -776,11 +739,9 @@ class VtStream(pyte.Stream):
self._taking_plain_text = taking_plain_text
def _parser_fsm(self):
"""Coroutine pyte feeds one character at a time; `yield True` resumes bulk text.
"""Parser coroutine, fed one character at a time; `yield True` resumes bulk text.
A full copy of pyte's FSM rather than a hook, because the additions are
inside its inner loops. Locals are bound up front: this runs per byte of
every escape sequence.
Locals are bound up front: this runs per byte of every escape sequence.
"""
basic = self.basic
listener = self.listener
@@ -829,7 +790,7 @@ class VtStream(pyte.Stream):
elif char == "%":
self.select_other_charset((yield None))
elif char in "()":
# honoured even in UTF-8 mode, which pyte skips: smacs is ESC(0
# honoured even in UTF-8 mode: xterm-256color's smacs is ESC(0
listener.define_charset((yield None), mode=char)
else:
escape_dispatch[char]()
@@ -873,7 +834,7 @@ class VtStream(pyte.Stream):
self._dispatch_csi(csi_dispatch, listener, char, params, private, prefix, intermediate)
break
# OSC: a code of up to 8 digits, then a payload up to MAX_OSC,
# ending at BEL or ST. pyte only accepts a single-digit code
# ending at BEL or ST
elif char == OSC_C1:
code = ""
while True:
@@ -907,8 +868,8 @@ class VtStream(pyte.Stream):
def _dispatch_csi(self, csi_dispatch, listener, final, params, private, prefix, intermediate):
"""Route a CSI by its intermediate, prefix and private marker.
The forms pyte has no table for are handled here; a private sequence we
do not implement is dropped rather than run in its non-private meaning.
An unimplemented private sequence is dropped rather than run in its
non-private meaning.
"""
if intermediate == "$":
if final == "p":

View File

@@ -107,25 +107,18 @@ def _modifier_code(ctrl, alt, shift):
def get_key_code(key, ctrl=False, alt=False, shift=False, application_mode=False, new_line_mode=False,
modify_other_keys=0):
"""Escape sequence for a key name and its modifiers.
Args:
key: key name from the keymap, or a single literal character.
application_mode: DECCKM is set, so cursor keys take their SS3 form.
new_line_mode: LNM is set, so enter sends CR LF.
modify_other_keys: the level the app asked for with CSI > 4 ; n m.
Returns:
str: the bytes to write to the pty.
"""
"""Escape sequence for a key name and its modifiers, honouring the app's active modes"""
key_lo = key.lower() if len(key) > 1 else key
# the keys with an xterm modified form of their own
if (ctrl or alt or shift) and key_lo in _MODIFIABLE_CSI:
return _MODIFIABLE_CSI[key_lo].format(_modifier_code(ctrl, alt, shift))
# the ambiguous ones, while the app asked for CSI 27
if modify_other_keys and (ctrl or alt or shift) and key_lo in _OTHER_KEYS:
return "\x1b[27;{};{}~".format(_modifier_code(ctrl, alt, shift), _OTHER_KEYS[key_lo])
# ctrl folds to a control byte, alt prefixes ESC, shift is bare text
if ctrl:
base = key_lo
if base in _CTRL_KEY_MAP:

View File

@@ -13,7 +13,6 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
"""
def __init__(self):
"""Track carets per view, for the insert-detection in on_modified."""
# view id -> last caret position, to work out what sublime just inserted
self._cursor = {}
@@ -56,7 +55,6 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
raise a key event — it arrives as an `insert` command. Text the emulator
did not produce must never stay in the view.
"""
# printables and IME text arrive as sublime inserts, not keypresses
terminal = Terminal.from_id(view.id())
if not terminal:
return
@@ -86,7 +84,7 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
self._cursor[view.id()] = view.sel()[0].end()
def on_query_context(self, view, key, operator, operand, match_all):
"""Answer the contexts the mousemap needs; keymap contexts are native."""
"""Answer the contexts the mousemap needs"""
# keymap contexts are native "setting.x"; mousemaps do not evaluate
# those the same way, so their flags still need the hook
if key not in ("vterm_view", "vterm_view.wheel_captured"):

View File

@@ -13,16 +13,15 @@ from .terminal import Terminal
WHEEL_UP = 64
WHEEL_DOWN = 65
# lines per wheel tick, as iTerm2 delivers per notch; pi scrolls exactly one
# line per report and has no setting of its own
# lines per wheel tick, as iTerm2 delivers per notch
WHEEL_LINES = 3
def send_scroll(terminal, up, lines, cell=None):
"""Report `lines` scroll lines to the app, or fake arrow keys on the alt screen.
One report per line, never one batched write: pi matches the SGR form
against a whole stdin chunk and drops anything longer."""
One report per write, never a batch: an app may match the SGR form against
a whole stdin chunk and drop anything longer."""
if terminal.screen.mouse_reporting_enabled():
if cell is None:
cell = (1, terminal.screen.lines // 2 + 1)
@@ -38,8 +37,8 @@ def send_scroll(terminal, up, lines, cell=None):
def _event_to_cell(view, terminal, event, clamp=False):
"""Map a mouse event to 1-based (col, row) on the live screen, or None.
`clamp` pulls a point above or below the screen onto the nearest row: the
wheel still belongs to the app, while a click there does not."""
Clamping pulls a point above or below the screen onto the nearest row: the
wheel still belongs to the app there, a click does not."""
pt = view.window_to_text((event["x"], event["y"]))
row, col = view.rowcol(pt)
row -= terminal.offset

View File

@@ -12,7 +12,7 @@ import sublime
import sublime_plugin
from wcwidth import wcswidth
# per-cell call in segment_buffer_line; pyte caches its own wcwidth likewise
# per-cell call in segment_buffer_line
cell_width = lru_cache(maxsize=4096)(wcswidth)
from .scheme import scheme_manager
@@ -125,8 +125,8 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
"""Set up this view's render caches."""
super().__init__(*args, **kwargs)
self.colored_lines = {}
# view row -> (segments, lf) as last rendered; pyte's dirty means
# "repaint hint", so most dirty rows are identical frame to frame
# view row -> (segments, lf) as last rendered; the dirty set is only a
# repaint hint, so most dirty rows are identical frame to frame
self._row_cache = {}
self._key_counter = 0
# chunk ordinal -> scopes it holds a key for; ordinals are view row +
@@ -199,11 +199,11 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
screen.dirty.clear()
def sync_alt_fold(self, terminal):
"""Hide the scrollback while the app owns the alt screen: fold it away
(the view text is its only copy).
The fold is re-checked every render — the screen's first row is
replaced on every region scroll, and an edit at the fold boundary can
silently disturb a fold."""
"""Fold the scrollback away while the app owns the alt screen; the view text is its only copy.
Re-checked every render: an edit at the fold boundary can silently
disturb a fold, and the screen's first row is replaced on every scroll.
"""
view = self.view
alt = terminal.screen.alternate_buffer_mode
if not alt and not terminal.applied_alt_screen:
@@ -303,17 +303,11 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
self.update_line(edit, line + offset, buffer_line, columns)
def update_line(self, edit, line, buffer_line, columns):
"""Rewrite one view row from a screen row, unless it is unchanged.
Args:
line: absolute view row.
columns: screen width; the last cell's linefeed flag marks a soft
wrap, which gets the CONTINUATION sentinel.
"""
"""Rewrite one view row from a screen row, unless it is unchanged"""
view = self.view
lf = buffer_line[columns - 1].linefeed
segments = list(segment_buffer_line(buffer_line))
# pyte's dirty set is a repaint hint, not a diff: most dirty rows come
# the dirty set is a repaint hint, not a diff: most dirty rows come
# back identical, and rewriting one costs an edit plus its region churn
rendered = (segments, lf)
if self._row_cache.get(line) == rendered:
@@ -373,16 +367,10 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
def flush_chunk(self):
"""Merge the queued rows onto shared per-(scope, chunk) keys.
A scrollback row is never repainted, so its per-row keys used to live
until trimmed: a long coloured session held ~15k region keys, and every
key is a separate pass in sublime's redraw. Merging bounds the count at
scopes x (scrollback / CHUNK).
The merge waits for the chunk to close, because add_regions replaces a
key's whole set: merging every frame re-sent everything accumulated so
far, which is quadratic in CHUNK when output arrives a few rows a frame.
Until then the rows keep their per-row keys, so the count is bounded by
scopes x chunks plus one chunk's worth.
Every region key is a separate pass in sublime's redraw, so merging
bounds the count at scopes x (scrollback / CHUNK) plus one open chunk.
The merge waits for the chunk to close: add_regions replaces a key's
whole set, so merging every frame would be quadratic in CHUNK.
"""
view = self.view
rows, chunk = self._open_rows, self._open_chunk
@@ -526,8 +514,7 @@ def following(view, terminal):
def bottom_y(view, terminal):
"""Viewport y that pins the bottom of the live screen to the bottom of the view."""
# layout_extent is maintained incrementally; text_to_layout(size()) forced
# a whole-buffer layout on every call
# layout_extent is maintained incrementally, unlike text_to_layout(size())
viewport_y = view.layout_extent()[1] - view.viewport_extent()[1]
offset_y = view.text_to_layout(view.text_point(terminal.offset, 0))[1]
return max(offset_y, viewport_y)
@@ -566,9 +553,7 @@ class VtermShowCursorCommand(sublime_plugin.TextCommand):
which has no edit token of its own."""
def run(self, edit, scroll=True):
"""Args:
scroll: also follow the output down to the cursor.
"""
"""Place the caret at the terminal cursor, optionally following the output down"""
view = self.view
terminal = Terminal.from_id(view.id())
if not terminal:

View File

@@ -116,7 +116,7 @@ class SchemeManager:
self._listeners.remove(callback)
def reload(self):
"""Rebuild every rule for a new palette; colours resolve at add time, so they cannot be appended"""
"""Rebuild every rule for a new palette, then notify the terminals"""
if not self._initialized:
# no scheme file to maintain yet; initialize() will read the setting
return

View File

@@ -150,8 +150,7 @@ class Terminal:
"""Whether an unfocused terminal may draw again.
A frame costs a command dispatch and a dozen UI round trips however
little changed, so a terminal the user is only watching gets them at a
lower rate than one they are typing in.
little changed, so a terminal the user only watches draws at a lower rate.
"""
if self.focused:
return True
@@ -236,8 +235,7 @@ class Terminal:
def _start_rendering(self):
"""Start the reader and renderer threads that drive this terminal."""
# deque appends/poplefts are atomic, so the reader never blocks behind
# a render; a lock here used to backpressure the pty for whole frames
# deque appends/poplefts are atomic, so the reader never blocks behind a render
chunks = deque()
done = [False]
@@ -274,8 +272,8 @@ class Terminal:
try:
self.stream.feed("".join(parts))
except Exception:
# pyte resets its parser before re-raising, so the terminal
# can carry on; letting this escape would kill the thread
# the parser resets itself before re-raising; letting this
# escape would kill the renderer thread
LOG.exception("stream.feed failed, dropping the rest of a chunk")
def renderer():
@@ -322,7 +320,7 @@ class Terminal:
LOG.warning("resize failed", exc_info=True)
def nudge_repaint(self):
"""Make the app repaint by briefly narrowing the pty. Never write ctrl+L instead: it is /clear in Claude Code"""
"""Make the app repaint by briefly narrowing the pty. Never write ctrl+L instead: apps bind it themselves"""
rows, columns = self.screen.lines, self.screen.columns
self._set_winsize(rows, max(columns - 1, 1))
sublime.set_timeout(lambda: self._set_winsize(rows, columns), int(REPAINT_NUDGE_DELAY * 1000))
@@ -342,12 +340,9 @@ class Terminal:
self.send_string(get_key_code(**kwargs), normalized=False)
def send_string(self, string, normalized=True):
"""Write text to the pty, chunking anything large.
Args:
normalized: canonicalise line endings for the app's LNM setting;
off for escape sequences, which must go through untouched.
"""
"""Write text to the pty, chunking anything large"""
# escape sequences go through untouched; typed and pasted text has its
# line endings canonicalised for the app's LNM setting
if normalized:
string = string.replace("\r\n", "\n")
if mo.LNM in self.screen.mode:
@@ -387,8 +382,6 @@ class Terminal:
def _sync_wheel_flag(self):
"""Publish whether the app owns the wheel, for the mousemap context."""
# the mousemap routes the wheel to the app while it reports the mouse
# or owns the alt screen; VtermScrollCommand picks the encoding
captured = self.screen.mouse_reporting_enabled() or self.screen.alternate_buffer_mode
if captured != self._wheel_captured:
self._wheel_captured = captured

View File

@@ -52,10 +52,10 @@ def intermission(period=0.1):
def rev_wcwidth(text, width):
"""Return the index into `text` at which the substring reaches `width` cells.
"""Index into `text` at which the substring reaches `width` cells.
The inverse of wcswidth: maps a terminal column back onto a character
index, so a wide char or a tab does not put the caret in the wrong place.
The inverse of wcswidth: maps a terminal column back onto a character index,
so a wide char or a tab does not put the caret in the wrong place.
"""
if width == 0:
return -1