5 Commits

Author SHA1 Message Date
Vaclav Tvrdik
93253aa564 fix opening kitty settings 2026-10-25 11:14:39 +01:00
Vaclav Tvrdik
2715ecddbb Report a failed spawn, drop the paste wrapper and the unbound close command, sync docs
- vterm_open shows an error dialog when the command cannot start,
  instead of a tab that silently vanishes
- on_text_command hands paste straight to vterm_paste_text;
  VtermPasteCommand only forwarded
- VtermCloseCommand was bound nowhere; closing the tab already kills
  the process
- wheel_lines fallback matches the shipped 1; README said three
- Architecture.md: per-theme scheme files and managers, command table
2026-10-25 10:41:29 +01:00
Vaclav Tvrdik
a556dab638 Drop OSC 52 clipboard writes, force-kill on close, bound the reader, find the ST libs for tests
- OSC 52 is ignored: any output, cat'd or remote, could plant a command
  on the clipboard to paste
- kill(): terminate(force=True), so a child ignoring HUP and INT does
  not outlive its tab with the pty open
- reader pauses once 64 unparsed reads wait, so a large dump blocks the
  child at the pty instead of being buffered whole in memory
- tests/_bootstrap.py: add every Lib/python3* directory; Package Control
  puts the libraries under the active plugin host's own directory
2026-10-25 10:40:25 +01:00
Vaclav Tvrdik
e478f41f7c Fix text loss after zero-width chars, grey 248 quantization, scrollback cap, no-theme sampling, shipped terminal default
- draw(): a zero-width non-combining char (VS16, ZWJ) or a stray control
  byte dropped the rest of the run; skip the char instead
- quantize_256: grey 248 mapped to slot 256 and aborted the SGR
- trim_history: the cap now counts rows above the screen, so a value
  below the screen height (0: no scrollback) no longer cuts the screen
- "No theme" sampled the focused terminal's own colours; sample a
  non-terminal view instead
- vterm.sublime-settings: Apple Terminal is the default again, kitty
  commented out, as the README says
2026-10-25 10:39:13 +01:00
Vaclav Tvrdik
7b536a7efa Keep scrollback under a full-screen scroll region 2026-10-20 11:00:04 +02:00
14 changed files with 125 additions and 79 deletions

View File

@@ -11,7 +11,7 @@
| `vterm/render.py` | Screen → view text, colour overlays, caret, folding, trimming. |
| `vterm/scheme.py` | `SchemeManager`: generates one theme's colour scheme, the scopes the renderer paints with. `get_scheme_manager` caches one instance per theme name, each terminal frozen to the instance it was opened or rethemed with. |
| `vterm/themes.py` | The built-in palettes (`default`, `homebrew`, `pro`, `slate`, `red-alert`). |
| `vterm/commands.py` | The Sublime commands: open, keypress, copy/paste, theme picker, clear, cleanup, close. |
| `vterm/commands.py` | The Sublime commands: open, keypress, copy/paste, theme picker, clear, cleanup. |
| `vterm/listeners.py` | `VtermCoreEventListener`: focus, IME capture, copy/paste rerouting, the mousemap context. |
| `vterm/mouse.py` | Wheel and click handling. |
| `vterm/keys.py` | Key name + modifiers → escape sequence tables. |
@@ -21,14 +21,15 @@
| File | Role |
|---|---|
| `vterm.sublime-settings` | Defaults for the eleven plugin settings. |
| `vterm.sublime-settings` | Defaults for the twelve plugin settings. |
| `vterm.sublime-commands` | Four palette entries: open, open terminal in project folder, clear screen, select theme. |
| `vterm.sublime-syntax` | An intentionally empty hidden syntax, so terminal text is never tokenized. |
| `Default.sublime-keymap` | 245 generated bindings. **Do not edit by hand** — regenerate with `tools/gen_keymap.py`. |
| `Default (OSX).sublime-mousemap` | Two wheel bindings and the middle button. |
| `dependencies.json` | Package Control libraries. |
Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-scheme`.
Generated at runtime, not in the repo: `Packages/User/vterm/vterm-<theme>.hidden-color-scheme`,
one per theme (`vterm-system` for "no theme").
## Classes
@@ -50,8 +51,8 @@ Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-s
into the next one. Parity is enforced by a differential test (`run_fast_path_cases`).
- **`Cursor`** (`emulator.py`) — replaces pyte's so the default attribute set can be a
`Char` carrying the extra `linefeed` field.
- **`SchemeManager`** (`scheme.py`) — module singleton `scheme_manager`. Owns the
generated scheme file and the mapping from colour combination to scope name.
- **`SchemeManager`** (`scheme.py`) — one per theme name, cached by `get_scheme_manager`.
Owns the generated scheme file and the mapping from colour combination to scope name.
- **`VtermRenderCommand`** (`render.py`) — the per-frame logic. Sublime rebuilds every
`TextCommand` instance whenever any plugin loads, so the view's render state
(`RenderState`: `colored_lines`, `row_cache`, `key_counter`, the scrollback chunk
@@ -68,12 +69,11 @@ Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-s
| `vterm_keypress` | `VtermKeypressCommand` | 244 keymap bindings |
| `vterm_render` | `VtermRenderCommand` | renderer thread |
| `vterm_show_cursor` | `VtermShowCursorCommand` | internal |
| `vterm_copy` / `vterm_paste` / `vterm_paste_text` | `commands.py` | `on_text_command`, IME capture |
| `vterm_copy` / `vterm_paste_text` | `commands.py` | `on_text_command`, IME capture |
| `vterm_paste_selection` | `commands.py` | mousemap, middle button |
| `vterm_select_theme` | `VtermSelectThemeCommand` | palette |
| `vterm_clear_screen` | `VtermClearScreenCommand` | palette, `super+shift+k` |
| `vterm_cleanup` | `VtermCleanupCommand` | renderer, on process exit |
| `vterm_close` | `VtermCloseCommand` | nothing — available for a user keybinding |
| `vterm_clear_undo_stack` / `vterm_noop` | `commands.py` | internal |
| `vterm_scroll` / `vterm_mouse_press` / `vterm_mouse_release` | `mouse.py` | mousemap |
@@ -100,7 +100,8 @@ are marked ✅.
truecolor, `ESC[0m` reset. This is what TUIs emit most.
- **OSC** ✅ — Operating System Command, `ESC ]`, ending at BEL or ST. Carries strings
rather than numbers: `OSC 0/2` set the window title, `OSC 10/11` query the default
foreground/background, `OSC 52` reads or writes the system clipboard.
foreground/background, `OSC 52` reads or writes the system clipboard (vterm
ignores it: untrusted output must not plant a paste).
- **DCS / APC** — Device Control String (`ESC P`) and Application Program Command
(`ESC _`). Long payloads: sixel images, kitty graphics. vterm parses their framing and
discards the payload, so an image never leaks into the text.

View File

@@ -87,8 +87,8 @@ fullscreen UI before it enables mouse reporting).
Sublime raises no wheel event at all for the trackpad (sublime_text#1838): set
a utility such as [LinearMouse](https://linearmouse.app/) to discrete scrolling
for Sublime Text and it arrives as wheel notches like any other. A notch sends the
app three scroll lines, as iTerm2 does; `wheel_lines` lowers that when a swipe
scrolls too far. While an app owns the
app one scroll line; `wheel_lines` raises that (iTerm2 sends 3) when the wheel
feels slow. While an app owns the
alternate screen the scrollback above it is folded away, kitty-style, and
unfolds when the app leaves it.
@@ -126,7 +126,8 @@ terminal protocols — see `Architecture.md`.
multi-digit OSC. It answers DA1 (Claude Code's query sentinel, which has no
timeout) and DA2, CPR, XTVERSION + DECRQM 2026 (this is what unlocks Claude
Code's flicker-free synchronized output on a generic `TERM`), the kitty keyboard
probe, and OSC 10/11 colour queries; OSC 52 goes to the Sublime clipboard.
probe, and OSC 10/11 colour queries; OSC 52 (clipboard write) is ignored,
since any output could plant a command to paste.
Combining characters are merged as NFC, because Sublime draws decomposed
sequences badly.
- `vterm/terminal.py` — pty process, reader and renderer threads at ~33 Hz.
@@ -209,6 +210,6 @@ python3 benchmarks/profiling.py # feed-path hotspots, per-char micro-b
`harness_live.py` is the useful one: it drives a real `pi` or `claude` through the
emulator with no Sublime involved, and prints the queries answered plus the
resulting screen. Tests import pyte/ptyprocess/wcwidth from Sublime's
`Lib/python38` (pure Python, so any Python 3 runs them).
`Lib/python3*` (pure Python, so any Python 3 runs them).
Code style is in `CODE.md`, working agreements in `CLAUDE.md`.

View File

@@ -12,14 +12,12 @@ from .vterm.commands import (
VtermClearScreenCommand,
VtermClearUndoStackCommand,
VtermCleanupCommand,
VtermCloseCommand,
VtermCopyCommand,
VtermInitializeViewCommand,
VtermKeypressCommand,
VtermNoopCommand,
VtermOpenCommand,
VtermOpenTerminalProjectFolderCommand,
VtermPasteCommand,
VtermPasteSelectionCommand,
VtermPasteTextCommand,
VtermSelectThemeCommand,

View File

@@ -3,14 +3,16 @@
They run as plain scripts (see README), so each imports this first to get the
sublime stubs and the Package Control libraries on the path.
"""
import glob
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ST_LIB = os.path.expanduser(
"~/Library/Application Support/Sublime Text/Lib/python38")
# Package Control puts the libraries under the plugin host's own directory
ST_LIBS = sorted(glob.glob(os.path.expanduser(
"~/Library/Application Support/Sublime Text/Lib/python3*")))
for path in (ROOT, ST_LIB, os.path.join(ROOT, "tests", "stubs")):
for path in [ROOT] + ST_LIBS + [os.path.join(ROOT, "tests", "stubs")]:
if path not in sys.path:
sys.path.insert(0, path)

View File

@@ -25,6 +25,7 @@ class FakeTerminalView:
self._id = FakeTerminalView._next_id
FakeTerminalView._next_id += 1
self._settings = sublime.Settings()
self._settings.set("vterm_view", True)
def id(self):
return self._id
@@ -36,7 +37,24 @@ class FakeTerminalView:
return True
def style(self):
return {}
# a terminal view reports its own theme's colours, not the editor's
return {"foreground": "#f2f2f2", "background": "#000000"}
class FakeEditorView:
"""An ordinary file view, the one the "No theme" palette is sampled from."""
def settings(self):
return sublime.Settings()
def is_valid(self):
return True
def style(self):
return {"foreground": "#111111", "background": "#eeeeee"}
editor = FakeEditorView()
class FakeWindow:
@@ -46,6 +64,9 @@ class FakeWindow:
def active_view(self):
return self._view
def views(self):
return [self._view, editor]
def show_quick_panel(self, items, on_done, selected_index=-1, on_highlight=None):
panel.update(items=items, on_done=on_done,
selected_index=selected_index, on_highlight=on_highlight)
@@ -105,6 +126,12 @@ command = open_picker(fake_terminal("slate"))
panel["on_done"](0)
assert settings.get("theme") is None, settings.get("theme")
# ... and samples the editor's colours, not the themed terminal's own: that
# would retint every "no theme" terminal with the theme being left
system = get_scheme_manager(None)
assert (system.foreground, system.background) == ("#111111", "#eeeeee"), \
(system.foreground, system.background)
print("theme picker tests passed ({} themes offered)".format(len(THEMES)))

View File

@@ -83,13 +83,11 @@ def run_case(char_by_char):
assert proc.written == "\x1b]11;rgb:1a1a/2b2b/3c3c\x1b\\", (tag, proc.written)
assert screen.buffer[0][0].data == "y", tag
# OSC 52 clipboard (base64 "hi")
got = []
proc = FakeProcess()
screen = VtScreen(80, 24, process=proc, clipboard_callback=got.append)
stream = VtStream(screen)
feed(stream, "\x1b]52;c;aGk=\x07", char_by_char)
assert got == ["hi"], (tag, got)
# OSC 52 (clipboard write) is dropped, not drawn: untrusted output must
# not plant a paste
proc, screen, stream = make()
feed(stream, "\x1b]52;c;aGk=\x07z", char_by_char)
assert screen.buffer[0][0].data == "z" and proc.written == "", tag
# bracketed paste and sync modes tracked
proc, screen, stream = make()
@@ -119,6 +117,21 @@ def run_case(char_by_char):
assert screen.buffer[0][0].fg == "ff0000" and screen.buffer[0][0].bold, tag
assert screen.buffer[0][1].fg == "ff0000", (tag, screen.buffer[0][1].fg)
# every grey lands inside the palette; 248 used to map to slot 256 and
# abort the whole SGR
from vterm.emulator import quantize_256
assert all(0 <= quantize_256(v, v, v) <= 255 for v in range(256))
proc, screen, stream = make()
feed(stream, "\x1b[38;2;248;248;248mX", char_by_char)
assert screen.buffer[0][0].fg == "ffffff", (tag, screen.buffer[0][0].fg)
# a zero-width non-combining char (VS16, ZWJ) or a stray control byte is
# dropped; the text after it must survive
proc, screen, stream = make()
feed(stream, "⚠️ warn \U0001F468‍\U0001F4BB ab\x01cd", char_by_char)
row = "".join(screen.buffer[0][x].data for x in range(20)).rstrip()
assert row == "⚠ warn \U0001F468\U0001F4BB abcd", (tag, repr(row))
# SGR 5/25 (blink): ignored, not a crash — Char has no blink field
proc, screen, stream = make()
feed(stream, "\x1b[5;31mX\x1b[25mY", char_by_char)
@@ -141,6 +154,11 @@ def run_case(char_by_char):
assert len(screen.history) == 1, (tag, len(screen.history))
assert screen.history[0][0].data == "a", tag
# an explicit full-screen scroll region still feeds scrollback
proc, screen, stream = make(10, 3)
feed(stream, "\x1b[1;3ra\r\nb\r\nc\r\nd", char_by_char)
assert len(screen.history) == 1, (tag, len(screen.history))
# ED 3 clears history and fires callback
cleared = []
proc = FakeProcess()
@@ -310,7 +328,7 @@ GAUNTLET = (
"\x1b[2;5r\x1b[Sscrolled\x1b[T\x1b[r"
"\x1b[38:5:100mcolon\x1b[m\r\n"
"\x1b[2\x073m bel mid-CSI \x1b[3\x18X can cancels\r\n"
"wide: 漢字 \x1b[7m漢\x1b[m\r\n"
"wide: 漢字 \x1b[7m漢\x1b[m ⚠️ after\r\n"
"\x1b[m\x1b[;5m\x1b[5;m\x1b[25m params\r\n"
)

View File

@@ -191,17 +191,30 @@ command.trim_history(None, terminal)
assert terminal.view.text == before, \
"alt screen view was trimmed: {!r}".format(terminal.view.text)
# the configured cap still applies on the primary screen
# the configured cap still applies on the primary screen: that many rows of
# scrollback above the screen
terminal, stream = make_terminal(rows=5, scrollback=20)
terminal.offset = 40
terminal.view = FakeView("\n".join("row {}".format(i) for i in range(40)))
command = make_command(terminal)
command.trim_history(None, terminal)
rows_left = terminal.view.text.count("\n") + 1
assert rows_left <= 20, rows_left
assert rows_left <= 25, rows_left
assert "row 39" in terminal.view.text, "newest rows must survive"
assert "row 0" not in terminal.view.text, "oldest rows must be dropped"
# a cap below the screen height (0: no scrollback) must never cut the screen
terminal, stream = make_terminal(rows=10, cols=20, scrollback=0)
command = make_command(terminal)
for i in range(3):
stream.feed("\r\n".join("row{}".format(i * 10 + j) for j in range(10)) + "\r\n")
command.update_lines(None, terminal)
command.trim_trailing_spaces(None, terminal)
command.trim_history(None, terminal)
terminal.screen.dirty.clear()
assert terminal.offset >= 0, terminal.offset
assert "row29" in terminal.view.text, terminal.view.text
# below the cap nothing is dropped
terminal, stream = make_terminal(rows=5, scrollback=5000)
terminal.offset = 3
@@ -442,6 +455,8 @@ Terminal._terminals[id(terminal)] = terminal
terminal.view.id = lambda: id(terminal)
scroll = VtermScrollCommand.__new__(VtermScrollCommand)
scroll.view = terminal.view
# three per notch, so "one report per line" is visible
sublime.load_settings(SETTINGS_FILE).set("wheel_lines", 3)
stream.feed("\x1b[?1049h")
scroll.run(None, up=True)

View File

@@ -37,8 +37,8 @@
// "parameters": ["-a", "Terminal", "%CWD%"],
// kitty instead of Apple Terminal:
"terminal": "/opt/homebrew/bin/kitty",
"parameters": ["-d", "%CWD%"],
"terminal": "/usr/bin/open",
"parameters": ["-a", "Kitty", "%CWD%"],
"debug": false
}

View File

@@ -18,6 +18,12 @@ LOG = logging.getLogger(__name__)
SCROLL_KEYS = ("pageup", "pagedown", "up", "down")
def editor_view(window):
"""A view to sample the editor's colours from: never a terminal, whose
colours are its own theme's."""
return next((v for v in window.views() if not v.settings().get("vterm_view")), None)
class VtermOpenCommand(sublime_plugin.WindowCommand):
"""Open a terminal in a new tab. `vterm_open`, the only entry point users bind."""
@@ -43,7 +49,7 @@ class VtermOpenCommand(sublime_plugin.WindowCommand):
cwd = os.path.expanduser("~")
cwd = sublime.expand_variables(cwd, window.extract_variables())
scheme = get_scheme_manager(settings().get("theme"), host_view=window.active_view())
scheme = get_scheme_manager(settings().get("theme"), host_view=editor_view(window))
view = window.new_file()
view.run_command("vterm_initialize_view", {"color_scheme": scheme.path})
@@ -54,6 +60,7 @@ class VtermOpenCommand(sublime_plugin.WindowCommand):
except Exception:
LOG.exception("failed to start terminal: cmd=%s", cmd)
view.close()
sublime.error_message("vterm: could not start {}".format(" ".join(cmd)))
return
window.focus_view(view)
@@ -183,14 +190,6 @@ class VtermCopyCommand(sublime_plugin.TextCommand):
sublime.set_clipboard(selection_text(self.view))
class VtermPasteCommand(sublime_plugin.TextCommand):
"""Pastes the clipboard into the app; replaces sublime's own paste."""
def run(self, edit):
"""Hand the clipboard to vterm_paste_text."""
self.view.run_command("vterm_paste_text", {"text": sublime.get_clipboard()})
class VtermPasteTextCommand(sublime_plugin.TextCommand):
"""Sends arbitrary text to the app, bracketed when it asked for that."""
@@ -258,7 +257,7 @@ class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
def _apply(self, name):
"""Retheme the focused terminal alone."""
scheme = get_scheme_manager(name, host_view=self.terminal.view)
scheme = get_scheme_manager(name, host_view=editor_view(self.window))
self.terminal.retheme(scheme)
def _preview(self, index):
@@ -328,20 +327,6 @@ class VtermCleanupCommand(sublime_plugin.TextCommand):
view.sel().clear()
class VtermCloseCommand(sublime_plugin.TextCommand):
"""Kills the process and closes the tab. Registered but bound nowhere; for user keybindings."""
def run(self, _):
"""Kill the process, then close the view."""
view = self.view
if not view.settings().get("vterm_view"):
return
terminal = Terminal.from_id(view.id())
if terminal:
terminal.kill()
view.close()
class VtermClearUndoStackCommand(sublime_plugin.TextCommand):
"""Drops the undo history, so rendered output can never be undone into view."""

View File

@@ -1,5 +1,4 @@
""" Terminal emulation: the screen and a parser for prefixed CSI, DECRQM and multi-digit OSC """
import base64
import logging
import re
import time
@@ -83,7 +82,8 @@ def quantize_256(r, g, b):
if r == g == b:
if r < 8:
return 16
if r > 248:
# 238 is the top of the grey ramp (255); above the midpoint, white
if r > 246:
return 231
return 232 + (r - 8) // 10
@@ -118,7 +118,7 @@ class Cursor:
class VtScreen(pyte.Screen):
"""Screen with alternate buffer, scrollback, synchronized output,
mouse/focus/paste modes, cursor styles, query replies and OSC handling.
mouse/focus/paste modes, cursor styles, query replies and OSC titles/colours.
Mutated only by the renderer thread, so it carries no lock.
"""
@@ -136,7 +136,6 @@ class VtScreen(pyte.Screen):
history=5000,
erase_scrollback=True,
clear_callback=lambda: None,
clipboard_callback=lambda text: None,
fg_color="ffffff",
bg_color="000000",
is_dark=True,
@@ -145,7 +144,6 @@ class VtScreen(pyte.Screen):
self._process = process
self._erase_scrollback = erase_scrollback
self._clear_callback = clear_callback
self._clipboard_callback = clipboard_callback
self.primary_buffer = {}
self._default_char = Char(data=" ", fg="default", bg="default", reverse=False)
# SGR transitions repeat endlessly in TUI output; memoized per (attrs, params)
@@ -432,7 +430,8 @@ class VtScreen(pyte.Screen):
self.buffer[pos[0]][pos[1]] = last._replace(data=normalized)
self.dirty.add(pos[0])
else:
break
# other zero-width (VS16, ZWJ) or control: drop it, keep the rest
continue
if char_width > 0:
x = cursor.x + char_width
@@ -442,7 +441,9 @@ class VtScreen(pyte.Screen):
def index(self):
"""Line feed; at the bottom of the primary screen the top row goes to scrollback."""
if not self.alternate_buffer_mode and self.cursor.y == self.lines - 1 and self.margins is None:
# a region spanning the whole screen (CSI 1;<lines> r) is no region
full = self.margins is None or self.margins == (0, self.lines - 1)
if not self.alternate_buffer_mode and self.cursor.y == self.lines - 1 and full:
self.push_lines_into_history(1)
super().index()
@@ -646,7 +647,7 @@ class VtScreen(pyte.Screen):
self.write_process_input("\x1b[?0u")
def handle_osc(self, code, payload, terminator):
"""Dispatch an OSC: 0/2 title, 1 icon, 10/11 colour query, 52 clipboard."""
"""Dispatch an OSC: 0/2 title, 1 icon, 10/11 colour query."""
if code in (0, 2):
self.set_title(payload)
elif code == 1:
@@ -657,14 +658,8 @@ class VtScreen(pyte.Screen):
rgb = "rgb:{0}{0}/{1}{1}/{2}{2}".format(color[0:2], color[2:4], color[4:6])
# answer with the terminator asked with: meli only ends an OSC at ST
self.write_process_input("\x1b]{};{}{}".format(code, rgb, terminator))
elif code == 52:
parts = payload.split(";", 1)
if len(parts) == 2 and parts[1] and parts[1] != "?":
try:
self._clipboard_callback(base64.b64decode(parts[1]).decode("utf-8", "replace"))
except ValueError:
LOG.warning("osc52: undecodable payload", exc_info=True)
# other OSC codes are intentionally dropped
# other OSC codes are intentionally dropped, 52 (clipboard write) among
# them: any output, cat'd or remote, could plant a command to paste
# a complete plain CSI: params then a final byte. No private marker, prefix or

View File

@@ -104,6 +104,6 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
if name == "copy":
return ("vterm_copy", None)
elif name in ("paste", "paste_and_indent"):
return ("vterm_paste", None)
return ("vterm_paste_text", {"text": sublime.get_clipboard()})
elif name == "undo":
return ("vterm_noop", None)

View File

@@ -119,7 +119,7 @@ class VtermScrollCommand(sublime_plugin.TextCommand):
cell = None
if terminal.screen.mouse_reporting_enabled() and event:
cell = _event_to_cell(self.view, terminal, event, clamp=True)
lines = max(0, int(settings().get("wheel_lines", 3)))
lines = max(0, int(settings().get("wheel_lines", 1)))
send_scroll(terminal, up, lines, cell)
def want_event(self):

View File

@@ -470,7 +470,8 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
view = self.view
screen = terminal.screen
lastrow = view.rowcol(view.size())[0]
n = terminal.scrollback_size
# the setting counts rows above the screen; 0 keeps the screen alone
n = terminal.scrollback_size + screen.lines
if lastrow + 1 > n:
# queued rows are about to be renumbered or erased: merge them first
self.flush_chunk()

View File

@@ -26,6 +26,9 @@ UNFOCUSED_RENDER_PERIOD = 0.075
# max chars parsed per tick, so a giant burst still renders intermediate frames.
FEED_LIMIT = 48 * 1024
# unparsed 8K reads the reader may hold before it pauses: ~10 ticks of parsing
PENDING_CHUNKS = 64
# SUPPRESS_HISTORY_AFTER_CLEAR in emulator.py must outlast this
REPAINT_NUDGE_DELAY = 0.25
@@ -200,7 +203,6 @@ class Terminal:
history=self.scrollback_size,
erase_scrollback=settings().get("honor_erase_scrollback", False),
clear_callback=self._clear_callback,
clipboard_callback=self._clipboard_callback,
fg_color=self.scheme.foreground.lstrip("#"),
bg_color=self.scheme.background.lstrip("#"),
is_dark=self.scheme.is_dark(),
@@ -233,7 +235,8 @@ class Terminal:
self.alive = False
self._wake.set()
try:
self.process.terminate()
# force: a child ignoring HUP and INT would otherwise outlive the tab
self.process.terminate(force=True)
except Exception:
LOG.warning("terminate failed", exc_info=True)
vid = self.view.id()
@@ -244,10 +247,6 @@ class Terminal:
"""Screen asked for a clear; the next render performs it on the UI thread."""
self.pending_clear = True
def _clipboard_callback(self, text):
"""OSC 52 payload; the clipboard may only be touched on the UI thread."""
sublime.set_timeout(lambda: sublime.set_clipboard(text))
def _need_to_render(self):
"""Whether anything changed since the last frame: dirty rows, a clear, or the cursor."""
screen = self.screen
@@ -283,6 +282,10 @@ class Terminal:
break
chunks.append(self._decoder.decode(raw))
self._wake.set()
# backpressure: once this much waits unparsed, stop reading so
# the pty fills and the child blocks, as under a real terminal
while len(chunks) > PENDING_CHUNKS and not done[0] and self.is_hosted():
time.sleep(RENDER_PERIOD)
if done[0] or not self.is_hosted():
LOG.debug("[reader] stops")
break