Files
vterm/tests/test_commands.py
Vaclav Tvrdik 2abbc503cf End DCS/OSC strings at any ESC, default the external terminal to Terminal.app
A string with no ST swallowed every later sequence, RIS included, and
Clear Screen could not recover the tab. Any ESC now ends it, as in xterm,
and the sequence it starts is dispatched. The shipped setting needed Kitty.

Also: REP, SCOSC/SCORC, DECOM-relative CPR, `:` subparams and more DECRQM
modes answered; new rows inserted in one edit; controls stripped from
pastes; wide-char mouse columns.
2026-11-01 20:23:08 +01:00

262 lines
9.0 KiB
Python

""" Theme picker flows and the selection-to-text helper.
Run with: python3 tests/test_commands.py """
import _bootstrap # sets sys.path for the imports below
from _bootstrap import FakeProcess
import sublime
from vterm.commands import VtermPasteSelectionCommand, VtermPasteTextCommand, VtermSelectThemeCommand, selection_text
from vterm.emulator import VtScreen
from vterm.render import CONTINUATION
from vterm.scheme import get_scheme_manager
from vterm.terminal import Terminal
from vterm.utils import SETTINGS_FILE
from vterm.themes import THEMES
settings = sublime.load_settings(SETTINGS_FILE)
panel = {}
class FakeTerminalView:
"""Just enough view for a Terminal to be rethemed: an id and its own settings."""
_next_id = 1
def __init__(self):
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
def settings(self):
return self._settings
def is_valid(self):
return True
def style(self):
# 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:
def __init__(self, view):
self._view = view
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)
def fake_terminal(theme_name):
"""A terminal frozen to a theme, registered the way a real one is."""
view = FakeTerminalView()
terminal = Terminal(view, get_scheme_manager(theme_name))
terminal.screen = VtScreen(20, 3, process=FakeProcess())
Terminal._terminals[view.id()] = terminal
return terminal
def open_picker(terminal):
command = VtermSelectThemeCommand(FakeWindow(terminal.view))
command.run()
return command
# every theme is offered, with "no theme" first
command = open_picker(fake_terminal(None))
assert [i[0] for i in panel["items"]] == ["No theme"] + sorted(THEMES), panel["items"]
assert panel["selected_index"] == 0
# the focused terminal's own theme starts highlighted, not the global default
settings.set("theme", "homebrew")
terminal = fake_terminal("pro")
command = open_picker(terminal)
assert panel["selected_index"] == command.choices.index("pro")
# highlighting previews the focused terminal alone, leaving the global default and
# every other terminal untouched
other = fake_terminal("default")
saves = len(sublime._saved)
panel["on_highlight"](command.choices.index("slate"))
assert terminal.scheme.theme_name == "slate", terminal.scheme.theme_name
assert terminal.view.settings().get("color_scheme") == terminal.scheme.path
assert other.scheme.theme_name == "default", "another open terminal must not retheme"
assert settings.get("theme") == "homebrew", "preview must not touch the global default"
assert len(sublime._saved) == saves, "preview must not save settings"
# cancelling restores the focused terminal's theme from before the panel opened
panel["on_done"](-1)
assert terminal.scheme.theme_name == "pro", terminal.scheme.theme_name
# confirming retheme the focused terminal and persists the default for new terminals
command = open_picker(terminal)
panel["on_done"](command.choices.index("homebrew"))
assert terminal.scheme.theme_name == "homebrew", terminal.scheme.theme_name
assert settings.get("theme") == "homebrew", settings.get("theme")
assert sublime._saved[-1] == SETTINGS_FILE
assert other.scheme.theme_name == "default", "another open terminal must not retheme"
# "no theme" persists as null rather than a name
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)))
# a palette change must reach the screen it was applied to: it answers OSC 10/11
# and DSR 996 from these values
from vterm.emulator import VtStream
recorder = FakeProcess()
terminal = fake_terminal("default")
terminal.screen = VtScreen(20, 3, process=recorder, fg_color="000000",
bg_color="ffffff", is_dark=False)
terminal.process = recorder
stream = VtStream(terminal.screen)
terminal.scheme = get_scheme_manager("slate")
terminal.apply_scheme()
assert terminal.screen.default_fg_color == terminal.scheme.foreground.lstrip("#")
assert terminal.screen.default_bg_color == terminal.scheme.background.lstrip("#")
# without mode 2031 the app is not interrupted
assert recorder.written == "", repr(recorder.written)
# with it, the dark/light report is sent so the app can restyle itself
stream.feed("\x1b[?2031h")
recorder.written = ""
terminal.apply_scheme()
assert recorder.written == "\x1b[?997;1n", repr(recorder.written)
# and the emulator answers a colour query with the new palette
recorder.written = ""
stream.feed("\x1b]11;?\x07")
bg = terminal.screen.default_bg_color
expected = "\x1b]11;rgb:{0}{0}/{1}{1}/{2}{2}\x07".format(bg[0:2], bg[2:4], bg[4:6])
assert recorder.written == expected, repr(recorder.written)
print("live theme tests passed (running screen adopts palette, app notified)")
# the scheme file must exist before any restored tab's color_scheme is loaded at
# startup, and building a theme twice must not duplicate its seeded rules
import os
scheme = get_scheme_manager("pro")
path = os.path.join(scheme._directory(), scheme.file_name)
assert os.path.exists(path), "get_scheme_manager did not write the scheme"
# the cached instance is reused, not rebuilt
assert get_scheme_manager("pro") is scheme, "theme managers must be cached"
# build is idempotent
with scheme.lock:
first = len(scheme.rules)
scheme.build()
assert len(scheme.rules) == first, \
"rules duplicated: {} -> {}".format(first, len(scheme.rules))
print("scheme file tests passed ({} seeded rules, idempotent)".format(first))
class FakeView:
"""Just enough view for selection_text and the paste-selection command."""
def __init__(self, text, regions):
self.text = text
self.regions = list(regions)
self.commands = []
def substr(self, region):
return self.text[region.begin():region.end()]
def sel(self):
return self
def __iter__(self):
return iter(self.regions)
def clear(self):
self.regions = []
def run_command(self, name, args=None):
self.commands.append((name, args))
def paste(view):
command = VtermPasteSelectionCommand.__new__(VtermPasteSelectionCommand)
command.view = view
command.run(None)
return view.commands
# soft-wrapped rows re-join: the sentinel and the newline behind it both go
wrapped = "abc" + CONTINUATION + "\ndef"
view = FakeView(wrapped, [sublime.Region(0, len(wrapped))])
assert selection_text(view) == "abcdef", repr(selection_text(view))
# a trailing sentinel with no newline is still stripped
view = FakeView("abc" + CONTINUATION, [sublime.Region(0, 3 + len(CONTINUATION))])
assert selection_text(view) == "abc", repr(selection_text(view))
# multiple regions join with a newline
view = FakeView("abcdef", [sublime.Region(0, 3), sublime.Region(3, 6)])
assert selection_text(view) == "abc\ndef", repr(selection_text(view))
# the middle button pastes the selection and drops it, so the caret follows again
view = FakeView("abcdef", [sublime.Region(0, 3)])
assert paste(view) == [("vterm_paste_text", {"text": "abc"})], view.commands
assert list(view.sel()) == [], "selection must be cleared"
# nothing selected, nothing sent
view = FakeView("abcdef", [sublime.Region(2, 2)])
assert paste(view) == [], view.commands
print("selection tests passed (sentinel stripping, paste, empty no-op)")
# a paste is a trust boundary: C0 controls bar tab/LF/CR go, so a pasted
# ESC[201~ cannot close the bracket and type the rest into the app
terminal = fake_terminal("default")
terminal.process = terminal.screen._process
terminal.screen.set_mode(2004, private=True)
terminal.view.run_command = lambda name, args=None: None
command = VtermPasteTextCommand.__new__(VtermPasteTextCommand)
command.view = terminal.view
command.run(None, "a\x1b[201~b\x00\tc\nd")
assert terminal.process.written == "\x1b[200~a[201~b\tc\rd\x1b[201~", repr(terminal.process.written)
print("paste tests passed (controls stripped, bracket kept whole)")