diff --git a/README.md b/README.md index fe9b8bd..2655701 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ colours: focused terminal immediately and sets this; other already-open terminals keep whatever theme they had. - `ansi_colors` — override individual base colours, e.g. `{"red": "#ff0000"}`, - applied on top of the theme. + applied on top of the theme. Takes effect after a Sublime restart. A light theme is reported to the running agent (OSC 11, DSR 996), so `pi` and `claude` switch to their light UI to match. An app subscribed to mode 2031 is also diff --git a/main.py b/main.py index 697cb3c..ce86753 100644 --- a/main.py +++ b/main.py @@ -43,7 +43,8 @@ def plugin_loaded(): plugin host: a restored tab's color_scheme is read during startup and may name any of them. """ - view = sublime.active_window().active_view() if sublime.active_window() else None + window = sublime.active_window() + view = window.active_view() if window else None build_startup_schemes(view) level = logging.DEBUG if settings().get("debug", False) else logging.INFO diff --git a/tests/test_imports.py b/tests/test_imports.py index 7de4157..1278099 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -88,7 +88,7 @@ for entry in load_sublime_json("vterm.sublime-commands") + keymap: # every pending_* name mentioned in the modules must be a real Terminal # attribute, so a renamed flag cannot leave a writer pointing at nothing -flags = {f for f in vars(Terminal(None)) if f.startswith("pending_")} +flags = {f for f in vars(Terminal(None, None)) if f.startswith("pending_")} assert flags == {"pending_clear", "pending_clear_screen", "pending_render"}, flags for module in (commands, render, terminal_module): with open(module.__file__, encoding="utf-8") as f: @@ -118,12 +118,10 @@ assert configured is None or configured in THEMES, configured # 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.scheme import 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] +scheme_files = [SchemeManager(name).file_path() for name in ALL_THEME_NAMES] for scheme_file in scheme_files: if os.path.exists(scheme_file): os.remove(scheme_file) diff --git a/vterm/commands.py b/vterm/commands.py index 12c39aa..5e1bff3 100644 --- a/vterm/commands.py +++ b/vterm/commands.py @@ -233,8 +233,7 @@ class VtermSelectThemeCommand(sublime_plugin.WindowCommand): def run(self): """Show the palette picker, starting on the focused terminal's own theme.""" - view = self.window.active_view() - self.terminal = Terminal.from_id(view.id()) if view else None + self.terminal = self._focused_terminal() if not self.terminal: return self.original = self.terminal.scheme.theme_name @@ -250,8 +249,12 @@ class VtermSelectThemeCommand(sublime_plugin.WindowCommand): def is_enabled(self): """Greys the palette entry out when the focused view is not a terminal.""" + return self._focused_terminal() is not None + + def _focused_terminal(self): + """The terminal in the focused view, if any.""" view = self.window.active_view() - return view is not None and Terminal.from_id(view.id()) is not None + return Terminal.from_id(view.id()) if view else None def _apply(self, name): """Retheme the focused terminal alone.""" diff --git a/vterm/render.py b/vterm/render.py index 48f5bd6..dd3b141 100644 --- a/vterm/render.py +++ b/vterm/render.py @@ -318,11 +318,12 @@ class VtermRenderCommand(sublime_plugin.TextCommand): by_scope = {} needed = 0 + scope_of = self._scheme.scope for _, start, end, fg, bg, style in segments: if is_default_run(fg, bg, style): continue needed = max(needed, end) - by_scope.setdefault(self._scheme.scope(fg, bg, style), []).append((start, end)) + by_scope.setdefault(scope_of(fg, bg, style), []).append((start, end)) text = "".join(s[0] for s in segments) if lf: @@ -382,6 +383,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand): grouped = {} stale = [] + scope_of = self._scheme.scope for row in rows: keys = self.colored_lines.pop(row, None) if not keys: @@ -393,7 +395,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand): for _, a, b, fg, bg, style in segments: if is_default_run(fg, bg, style): continue - scope = self._scheme.scope(fg, bg, style) + scope = scope_of(fg, bg, style) grouped.setdefault(scope, []).append(sublime.Region(start + a, start + b)) scopes = self._archived_chunks.setdefault(chunk, set()) diff --git a/vterm/scheme.py b/vterm/scheme.py index 50a27f0..91cd688 100644 --- a/vterm/scheme.py +++ b/vterm/scheme.py @@ -181,6 +181,10 @@ class SchemeManager: rule["font_style"] = " ".join(font_style) self.rules[(fg, bg, style)] = rule + def file_path(self): + """Filesystem path of the generated scheme file.""" + return os.path.join(self._directory(), self.file_name) + def _directory(self): """Path of the generated scheme's directory, created on first use.""" if self._dir is None: @@ -211,8 +215,7 @@ class SchemeManager: }, "rules": list(self.rules.values()), } - path = os.path.join(self._directory(), self.file_name) - with open(path, "w", encoding="utf-8") as f: + with open(self.file_path(), "w", encoding="utf-8") as f: # compact: sublime reparses the whole file on every write json.dump(scheme, f, separators=(",", ":")) LOG.debug("scheme written: rules=%s", len(self.rules)) @@ -227,8 +230,8 @@ def get_scheme_manager(theme_name, host_view=None): """The manager for a theme name (None for "follow the Sublime colour scheme"), building and caching it on first use. - The None entry is rebuilt on every call instead: it is a live sample of the - editor's current colours, not a static palette, so every caller handing a + The None entry is rebuilt whenever the editor's colours have changed since: it + is a live sample of them, not a static palette, so every caller handing a terminal a "no theme" scheme gets one that is actually current. """ manager = _managers.get(theme_name) @@ -236,8 +239,10 @@ def get_scheme_manager(theme_name, host_view=None): manager = SchemeManager(theme_name) _managers[theme_name] = manager manager.build(host_view) - elif theme_name is None: - manager.build(host_view) + elif theme_name is None and host_view is not None and host_view.is_valid(): + style = host_view.style() + if (style.get("foreground"), style.get("background")) != (manager.foreground, manager.background): + manager.build(host_view) return manager @@ -254,6 +259,5 @@ def build_startup_schemes(host_view=None): if name is None: get_scheme_manager(None, host_view=host_view) continue - manager = SchemeManager(name) - if not os.path.exists(os.path.join(manager._directory(), manager.file_name)): + if not os.path.exists(SchemeManager(name).file_path()): get_scheme_manager(name) diff --git a/vterm/terminal.py b/vterm/terminal.py index b2e4f68..2a48309 100644 --- a/vterm/terminal.py +++ b/vterm/terminal.py @@ -97,7 +97,7 @@ class Terminal: _terminals = {} - def __init__(self, view, scheme=None): + def __init__(self, view, scheme): """State only; the pty and the threads come up in start().""" self.view = view self.scheme = scheme @@ -218,6 +218,8 @@ class Terminal: def retheme(self, scheme): """Switch to a different scheme: vterm: Select Theme, scoped to this terminal.""" + if scheme is self.scheme: + return self.scheme = scheme self.view.settings().set("color_scheme", scheme.path) self.apply_scheme()