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:
@@ -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
|
||||
|
||||
3
main.py
3
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,7 +239,9 @@ 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:
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user