Simplify per-terminal theme code; note ansi_colors needs restart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vaclav Tvrdik
2026-10-04 21:42:20 +02:00
parent c2a0c75359
commit 9656faa385
7 changed files with 31 additions and 21 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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())

View File

@@ -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)

View File

@@ -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()