- 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
248 lines
8.3 KiB
Python
248 lines
8.3 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, 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)")
|