select theme applies only to current vterm and newly opened vterms
This commit is contained in:
@@ -87,3 +87,7 @@ def expand_variables(value, variables):
|
||||
|
||||
def version():
|
||||
return "4200"
|
||||
|
||||
|
||||
def active_window():
|
||||
return None
|
||||
|
||||
@@ -5,7 +5,10 @@ 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
|
||||
|
||||
@@ -13,72 +16,113 @@ 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()
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def settings(self):
|
||||
return self._settings
|
||||
|
||||
def is_valid(self):
|
||||
return True
|
||||
|
||||
def style(self):
|
||||
return {}
|
||||
|
||||
|
||||
class FakeWindow:
|
||||
def __init__(self, view):
|
||||
self._view = view
|
||||
|
||||
def active_view(self):
|
||||
return self._view
|
||||
|
||||
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 open_picker(current):
|
||||
settings.set("theme", current)
|
||||
command = VtermSelectThemeCommand(FakeWindow())
|
||||
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(None)
|
||||
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 configured theme starts highlighted
|
||||
command = open_picker("pro")
|
||||
# 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 but must not persist
|
||||
# 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 settings.get("theme") == "slate", settings.get("theme")
|
||||
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 value from before the panel opened
|
||||
# cancelling restores the focused terminal's theme from before the panel opened
|
||||
panel["on_done"](-1)
|
||||
assert settings.get("theme") == "pro", settings.get("theme")
|
||||
assert terminal.scheme.theme_name == "pro", terminal.scheme.theme_name
|
||||
|
||||
# confirming persists
|
||||
command = open_picker("pro")
|
||||
# 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("slate")
|
||||
command = open_picker(fake_terminal("slate"))
|
||||
panel["on_done"](0)
|
||||
assert settings.get("theme") is None, settings.get("theme")
|
||||
|
||||
print("theme picker tests passed ({} themes offered)".format(len(THEMES)))
|
||||
|
||||
|
||||
# a palette change must reach terminals that are already running: the screen
|
||||
# answers OSC 10/11 and DSR 996 from these values
|
||||
from vterm.emulator import VtScreen, VtStream
|
||||
from vterm.scheme import scheme_manager
|
||||
from vterm.terminal import Terminal
|
||||
|
||||
# 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()
|
||||
# real __init__ (view unused here); process/screen stand in for start()
|
||||
terminal = Terminal(None)
|
||||
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)
|
||||
|
||||
scheme_manager.foreground = "#eeeeee"
|
||||
scheme_manager.background = "#111111"
|
||||
terminal.scheme = get_scheme_manager("slate")
|
||||
terminal.apply_scheme()
|
||||
assert terminal.screen.default_bg_color == "111111", terminal.screen.default_bg_color
|
||||
assert terminal.screen.theme_is_dark is True
|
||||
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)
|
||||
@@ -92,37 +136,30 @@ 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")
|
||||
assert recorder.written == "\x1b]11;rgb:1111/1111/1111\x07", repr(recorder.written)
|
||||
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 twice must not duplicate the seeded rules
|
||||
# startup, and building a theme twice must not duplicate its seeded rules
|
||||
import os
|
||||
from vterm.scheme import SCHEME_FILE_NAME
|
||||
|
||||
path = os.path.join(scheme_manager._directory(), SCHEME_FILE_NAME)
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
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"
|
||||
|
||||
scheme_manager.ensure_file()
|
||||
assert os.path.exists(path), "ensure_file did not write the scheme"
|
||||
assert not scheme_manager._initialized, \
|
||||
"ensure_file must leave the palette open to refinement by initialize()"
|
||||
# the cached instance is reused, not rebuilt
|
||||
assert get_scheme_manager("pro") is scheme, "theme managers must be cached"
|
||||
|
||||
# a second call is a no-op: same file, untouched
|
||||
stamp = os.path.getmtime(path)
|
||||
scheme_manager.ensure_file()
|
||||
assert os.path.getmtime(path) == stamp, "ensure_file rewrote an existing scheme"
|
||||
|
||||
# _build is idempotent, which is what makes the ensure_file + initialize pair safe
|
||||
with scheme_manager.lock:
|
||||
scheme_manager._build()
|
||||
first = len(scheme_manager.rules)
|
||||
scheme_manager._build()
|
||||
assert len(scheme_manager.rules) == first, \
|
||||
"rules duplicated: {} -> {}".format(first, len(scheme_manager.rules))
|
||||
# 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))
|
||||
|
||||
|
||||
@@ -63,10 +63,9 @@ assert get_key_code(key="enter", modify_other_keys=2) == "\r", "unmodified keys
|
||||
assert get_key_code(key="c", ctrl=True, modify_other_keys=2) == "\x03", "ctrl+c stays a control byte"
|
||||
load_sublime_json("Default (OSX).sublime-mousemap")
|
||||
|
||||
# scheme manager produces scopes and rules
|
||||
import vterm.scheme as scheme_module
|
||||
from vterm.scheme import scheme_manager
|
||||
scope = scheme_manager.scope("ff0000", "default", "b")
|
||||
# a theme's scheme manager produces scopes and rules
|
||||
from vterm.scheme import get_scheme_manager
|
||||
scope = get_scheme_manager("default").scope("ff0000", "default", "b")
|
||||
assert scope == "vterm.ff0000.default.b", scope
|
||||
|
||||
# every command referenced by the palette and the keymap must exist, and the
|
||||
@@ -114,21 +113,30 @@ for name, theme in THEMES.items():
|
||||
configured = load_sublime_json("vterm.sublime-settings").get("theme")
|
||||
assert configured is None or configured in THEMES, configured
|
||||
|
||||
# plugin_loaded must create the colour scheme: sublime loads a restored tab's
|
||||
# color_scheme during startup, before any terminal opens
|
||||
# plugin_loaded must create every theme's colour scheme: sublime loads a restored
|
||||
# tab's color_scheme during startup, before any terminal opens, and it may name
|
||||
# any of them
|
||||
# import main.py the way sublime does - as a module of the package directory -
|
||||
# so its relative imports and plugin_loaded body are actually executed
|
||||
import sublime
|
||||
from vterm.scheme import SCHEME_DIR_NAME, SchemeManager
|
||||
from vterm.themes import ALL_THEME_NAMES
|
||||
|
||||
scheme_dir = os.path.join(sublime.packages_path(), *SCHEME_DIR_NAME.split("/"))
|
||||
scheme_files = [os.path.join(scheme_dir, SchemeManager(name).file_name) for name in ALL_THEME_NAMES]
|
||||
for scheme_file in scheme_files:
|
||||
if os.path.exists(scheme_file):
|
||||
os.remove(scheme_file)
|
||||
|
||||
pkg = types.ModuleType("vterm")
|
||||
pkg.__path__ = [ROOT]
|
||||
sys.modules["vterm"] = pkg
|
||||
main = importlib.import_module("vterm.main")
|
||||
|
||||
scheme_file = os.path.join(main.scheme_manager._directory(), scheme_module.SCHEME_FILE_NAME)
|
||||
if os.path.exists(scheme_file):
|
||||
os.remove(scheme_file)
|
||||
main.plugin_loaded()
|
||||
assert os.path.exists(scheme_file), \
|
||||
"plugin_loaded must leave the colour scheme on disk"
|
||||
for name, scheme_file in zip(ALL_THEME_NAMES, scheme_files):
|
||||
assert os.path.exists(scheme_file), \
|
||||
"plugin_loaded must leave {}'s colour scheme on disk".format(name)
|
||||
|
||||
print("import smoke test passed ({} keymap bindings, {} pending flags, "
|
||||
"{} themes)".format(len(keymap), len(flags), len(THEMES)))
|
||||
|
||||
@@ -17,6 +17,7 @@ from vterm.render import (
|
||||
following,
|
||||
scroll_to_cursor,
|
||||
)
|
||||
from vterm.scheme import get_scheme_manager
|
||||
from vterm.terminal import Terminal
|
||||
from vterm.utils import SETTINGS_FILE
|
||||
|
||||
@@ -157,7 +158,7 @@ class FakeView:
|
||||
|
||||
def make_terminal(rows=10, cols=20, scrollback=None):
|
||||
# real __init__ for the plain attributes; process/screen stand in for start()
|
||||
terminal = Terminal(FakeView())
|
||||
terminal = Terminal(FakeView(), get_scheme_manager("default"))
|
||||
if scrollback is not None:
|
||||
terminal.scrollback_size = scrollback
|
||||
terminal.process = FakeProcess()
|
||||
@@ -165,9 +166,12 @@ def make_terminal(rows=10, cols=20, scrollback=None):
|
||||
return terminal, VtStream(terminal.screen)
|
||||
|
||||
|
||||
def make_command(view):
|
||||
# the real __init__, so the fake cannot drift from the command's attributes
|
||||
return VtermRenderCommand(view)
|
||||
def make_command(terminal):
|
||||
# the real __init__, so the fake cannot drift from the command's attributes;
|
||||
# run() itself sets _scheme, but these tests call its helpers directly
|
||||
command = VtermRenderCommand(terminal.view)
|
||||
command._scheme = terminal.scheme
|
||||
return command
|
||||
|
||||
|
||||
# an app on the alternate screen must not have its view trimmed away: the
|
||||
@@ -178,7 +182,7 @@ assert terminal.screen.alternate_buffer_mode
|
||||
assert terminal.screen.history.maxlen == 0, "alt screen zeroes the deque"
|
||||
|
||||
terminal.view = FakeView("\n".join("row {}".format(i) for i in range(10)))
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
before = terminal.view.text
|
||||
command.trim_history(None, terminal)
|
||||
assert terminal.view.text == before, \
|
||||
@@ -188,7 +192,7 @@ assert terminal.view.text == before, \
|
||||
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.view)
|
||||
command = make_command(terminal)
|
||||
command.trim_history(None, terminal)
|
||||
rows_left = terminal.view.text.count("\n") + 1
|
||||
assert rows_left <= 20, rows_left
|
||||
@@ -199,7 +203,7 @@ assert "row 0" not in terminal.view.text, "oldest rows must be dropped"
|
||||
terminal, stream = make_terminal(rows=5, scrollback=5000)
|
||||
terminal.offset = 3
|
||||
terminal.view = FakeView("\n".join("row {}".format(i) for i in range(8)))
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
command.trim_history(None, terminal)
|
||||
assert "row 0" in terminal.view.text and "row 7" in terminal.view.text, \
|
||||
terminal.view.text
|
||||
@@ -209,7 +213,7 @@ assert "row 0" in terminal.view.text and "row 7" in terminal.view.text, \
|
||||
terminal, stream = make_terminal(rows=4)
|
||||
stream.feed("\x1b[?1049h")
|
||||
terminal.view = FakeView("top\nrow\n\n")
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
before = terminal.view.text
|
||||
command.trim_trailing_spaces(None, terminal)
|
||||
assert terminal.view.text == before, \
|
||||
@@ -218,7 +222,7 @@ assert terminal.view.text == before, \
|
||||
# on the primary screen the blank tail below the cursor still goes
|
||||
terminal, stream = make_terminal(rows=4)
|
||||
terminal.view = FakeView("top\nrow\n\n")
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
command.trim_trailing_spaces(None, terminal)
|
||||
assert terminal.view.text == "top\nrow", terminal.view.text
|
||||
|
||||
@@ -261,7 +265,7 @@ print("cursor visibility tests passed (hidden cursor keeps the caret)")
|
||||
# batches the runs that share a scope into one add_regions call
|
||||
terminal, stream = make_terminal(rows=3, cols=20)
|
||||
terminal.view = FakeView()
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
stream.feed("\x1b[31mRED\x1b[0m..\x1b[31mred\x1b[0m..\x1b[32mgreen")
|
||||
command.update_lines(None, terminal)
|
||||
|
||||
@@ -291,7 +295,7 @@ print("colorize tests passed (arithmetic offsets, scope batching, key bookkeepin
|
||||
# the whole session, and merging every frame would re-send the whole chunk
|
||||
terminal, stream = make_terminal(rows=3, cols=20)
|
||||
terminal.view = FakeView()
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
for i in range(300):
|
||||
stream.feed("\x1b[31mred{}\x1b[0m\r\n".format(i))
|
||||
command.update_lines(None, terminal)
|
||||
@@ -310,7 +314,7 @@ assert len(row_keys) == 300 - CHUNK, \
|
||||
# trimming settles the open chunk, then erases the keys of chunks above the cut
|
||||
terminal, stream = make_terminal(rows=3, scrollback=30)
|
||||
terminal.view = FakeView()
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
stream.feed("".join("\x1b[31mr{}\x1b[0m\r\n".format(i) for i in range(300)))
|
||||
command.update_lines(None, terminal)
|
||||
terminal.screen.dirty.clear()
|
||||
@@ -332,7 +336,7 @@ terminal.view.sel().add(sublime.Region(0, 0)) # the caret of a new view
|
||||
Terminal._terminals[id(terminal)] = terminal
|
||||
terminal.view.id = lambda: id(terminal)
|
||||
terminal.default_title = "zsh"
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
stream.feed("[vterm] %")
|
||||
command.run(None)
|
||||
assert all(r.empty() for r in terminal.view.sel()), \
|
||||
@@ -359,10 +363,8 @@ assert all(text == "" for text in rewritten), "unchanged rows were rewritten: {}
|
||||
print("render diff tests passed (unchanged dirty rows skipped)")
|
||||
|
||||
# the generated scheme must define a selection colour, or sublime uses blue
|
||||
from vterm.scheme import blend, scheme_manager
|
||||
scheme_manager.background = "#303841"
|
||||
scheme_manager.foreground = "#d8dee9"
|
||||
assert blend(scheme_manager.background, scheme_manager.foreground, 0.3) == "#626a73"
|
||||
from vterm.scheme import blend
|
||||
assert blend("#303841", "#d8dee9", 0.3) == "#626a73"
|
||||
|
||||
print("selection artefact tests passed (fresh prompt unselected, user selection kept)")
|
||||
|
||||
@@ -370,7 +372,7 @@ print("selection artefact tests passed (fresh prompt unselected, user selection
|
||||
# DECSCUSR drives the caret shape through view settings; before any app asks,
|
||||
# and for 0, the caret is the terminal default: a blinking block
|
||||
terminal, stream = make_terminal(rows=3, cols=20)
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
settings = terminal.view.settings()
|
||||
|
||||
command.sync_cursor_style(terminal)
|
||||
@@ -621,7 +623,7 @@ terminal, stream = make_terminal(rows=5, cols=20)
|
||||
terminal.view = FakeView("old1\nold2\nold3\nr0\nr1\nr2\nr3\nr4\n")
|
||||
terminal.offset = 3
|
||||
terminal.viewport_y = 400
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
fold_end = terminal.view.text_point(3, 0) - 1
|
||||
|
||||
stream.feed("\x1b[?1049h")
|
||||
@@ -649,7 +651,7 @@ assert terminal.view.folds == [], terminal.view.folds
|
||||
# without scrollback there is nothing to fold
|
||||
terminal, stream = make_terminal(rows=5, cols=20)
|
||||
terminal.view = FakeView("r0\nr1\nr2\nr3\nr4\n")
|
||||
command = make_command(terminal.view)
|
||||
command = make_command(terminal)
|
||||
stream.feed("\x1b[?1049h")
|
||||
command.sync_alt_fold(terminal)
|
||||
assert terminal.view.folds == [], terminal.view.folds
|
||||
|
||||
Reference in New Issue
Block a user