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
|
focused terminal immediately and sets this; other already-open terminals keep
|
||||||
whatever theme they had.
|
whatever theme they had.
|
||||||
- `ansi_colors` — override individual base colours, e.g. `{"red": "#ff0000"}`,
|
- `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
|
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
|
`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
|
plugin host: a restored tab's color_scheme is read during startup and may
|
||||||
name any of them.
|
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)
|
build_startup_schemes(view)
|
||||||
|
|
||||||
level = logging.DEBUG if settings().get("debug", False) else logging.INFO
|
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
|
# every pending_* name mentioned in the modules must be a real Terminal
|
||||||
# attribute, so a renamed flag cannot leave a writer pointing at nothing
|
# 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
|
assert flags == {"pending_clear", "pending_clear_screen", "pending_render"}, flags
|
||||||
for module in (commands, render, terminal_module):
|
for module in (commands, render, terminal_module):
|
||||||
with open(module.__file__, encoding="utf-8") as f:
|
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
|
# any of them
|
||||||
# import main.py the way sublime does - as a module of the package directory -
|
# 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
|
# so its relative imports and plugin_loaded body are actually executed
|
||||||
import sublime
|
from vterm.scheme import SchemeManager
|
||||||
from vterm.scheme import SCHEME_DIR_NAME, SchemeManager
|
|
||||||
from vterm.themes import ALL_THEME_NAMES
|
from vterm.themes import ALL_THEME_NAMES
|
||||||
|
|
||||||
scheme_dir = os.path.join(sublime.packages_path(), *SCHEME_DIR_NAME.split("/"))
|
scheme_files = [SchemeManager(name).file_path() for name in ALL_THEME_NAMES]
|
||||||
scheme_files = [os.path.join(scheme_dir, SchemeManager(name).file_name) for name in ALL_THEME_NAMES]
|
|
||||||
for scheme_file in scheme_files:
|
for scheme_file in scheme_files:
|
||||||
if os.path.exists(scheme_file):
|
if os.path.exists(scheme_file):
|
||||||
os.remove(scheme_file)
|
os.remove(scheme_file)
|
||||||
|
|||||||
@@ -233,8 +233,7 @@ class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Show the palette picker, starting on the focused terminal's own theme."""
|
"""Show the palette picker, starting on the focused terminal's own theme."""
|
||||||
view = self.window.active_view()
|
self.terminal = self._focused_terminal()
|
||||||
self.terminal = Terminal.from_id(view.id()) if view else None
|
|
||||||
if not self.terminal:
|
if not self.terminal:
|
||||||
return
|
return
|
||||||
self.original = self.terminal.scheme.theme_name
|
self.original = self.terminal.scheme.theme_name
|
||||||
@@ -250,8 +249,12 @@ class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
|
|||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
"""Greys the palette entry out when the focused view is not a terminal."""
|
"""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()
|
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):
|
def _apply(self, name):
|
||||||
"""Retheme the focused terminal alone."""
|
"""Retheme the focused terminal alone."""
|
||||||
|
|||||||
@@ -318,11 +318,12 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
|
|
||||||
by_scope = {}
|
by_scope = {}
|
||||||
needed = 0
|
needed = 0
|
||||||
|
scope_of = self._scheme.scope
|
||||||
for _, start, end, fg, bg, style in segments:
|
for _, start, end, fg, bg, style in segments:
|
||||||
if is_default_run(fg, bg, style):
|
if is_default_run(fg, bg, style):
|
||||||
continue
|
continue
|
||||||
needed = max(needed, end)
|
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)
|
text = "".join(s[0] for s in segments)
|
||||||
if lf:
|
if lf:
|
||||||
@@ -382,6 +383,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
|
|
||||||
grouped = {}
|
grouped = {}
|
||||||
stale = []
|
stale = []
|
||||||
|
scope_of = self._scheme.scope
|
||||||
for row in rows:
|
for row in rows:
|
||||||
keys = self.colored_lines.pop(row, None)
|
keys = self.colored_lines.pop(row, None)
|
||||||
if not keys:
|
if not keys:
|
||||||
@@ -393,7 +395,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
for _, a, b, fg, bg, style in segments:
|
for _, a, b, fg, bg, style in segments:
|
||||||
if is_default_run(fg, bg, style):
|
if is_default_run(fg, bg, style):
|
||||||
continue
|
continue
|
||||||
scope = self._scheme.scope(fg, bg, style)
|
scope = scope_of(fg, bg, style)
|
||||||
grouped.setdefault(scope, []).append(sublime.Region(start + a, start + b))
|
grouped.setdefault(scope, []).append(sublime.Region(start + a, start + b))
|
||||||
|
|
||||||
scopes = self._archived_chunks.setdefault(chunk, set())
|
scopes = self._archived_chunks.setdefault(chunk, set())
|
||||||
|
|||||||
@@ -181,6 +181,10 @@ class SchemeManager:
|
|||||||
rule["font_style"] = " ".join(font_style)
|
rule["font_style"] = " ".join(font_style)
|
||||||
self.rules[(fg, bg, style)] = rule
|
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):
|
def _directory(self):
|
||||||
"""Path of the generated scheme's directory, created on first use."""
|
"""Path of the generated scheme's directory, created on first use."""
|
||||||
if self._dir is None:
|
if self._dir is None:
|
||||||
@@ -211,8 +215,7 @@ class SchemeManager:
|
|||||||
},
|
},
|
||||||
"rules": list(self.rules.values()),
|
"rules": list(self.rules.values()),
|
||||||
}
|
}
|
||||||
path = os.path.join(self._directory(), self.file_name)
|
with open(self.file_path(), "w", encoding="utf-8") as f:
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
|
||||||
# compact: sublime reparses the whole file on every write
|
# compact: sublime reparses the whole file on every write
|
||||||
json.dump(scheme, f, separators=(",", ":"))
|
json.dump(scheme, f, separators=(",", ":"))
|
||||||
LOG.debug("scheme written: rules=%s", len(self.rules))
|
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"),
|
"""The manager for a theme name (None for "follow the Sublime colour scheme"),
|
||||||
building and caching it on first use.
|
building and caching it on first use.
|
||||||
|
|
||||||
The None entry is rebuilt on every call instead: it is a live sample of the
|
The None entry is rebuilt whenever the editor's colours have changed since: it
|
||||||
editor's current colours, not a static palette, so every caller handing a
|
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.
|
terminal a "no theme" scheme gets one that is actually current.
|
||||||
"""
|
"""
|
||||||
manager = _managers.get(theme_name)
|
manager = _managers.get(theme_name)
|
||||||
@@ -236,8 +239,10 @@ def get_scheme_manager(theme_name, host_view=None):
|
|||||||
manager = SchemeManager(theme_name)
|
manager = SchemeManager(theme_name)
|
||||||
_managers[theme_name] = manager
|
_managers[theme_name] = manager
|
||||||
manager.build(host_view)
|
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():
|
||||||
manager.build(host_view)
|
style = host_view.style()
|
||||||
|
if (style.get("foreground"), style.get("background")) != (manager.foreground, manager.background):
|
||||||
|
manager.build(host_view)
|
||||||
return manager
|
return manager
|
||||||
|
|
||||||
|
|
||||||
@@ -254,6 +259,5 @@ def build_startup_schemes(host_view=None):
|
|||||||
if name is None:
|
if name is None:
|
||||||
get_scheme_manager(None, host_view=host_view)
|
get_scheme_manager(None, host_view=host_view)
|
||||||
continue
|
continue
|
||||||
manager = SchemeManager(name)
|
if not os.path.exists(SchemeManager(name).file_path()):
|
||||||
if not os.path.exists(os.path.join(manager._directory(), manager.file_name)):
|
|
||||||
get_scheme_manager(name)
|
get_scheme_manager(name)
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class Terminal:
|
|||||||
|
|
||||||
_terminals = {}
|
_terminals = {}
|
||||||
|
|
||||||
def __init__(self, view, scheme=None):
|
def __init__(self, view, scheme):
|
||||||
"""State only; the pty and the threads come up in start()."""
|
"""State only; the pty and the threads come up in start()."""
|
||||||
self.view = view
|
self.view = view
|
||||||
self.scheme = scheme
|
self.scheme = scheme
|
||||||
@@ -218,6 +218,8 @@ class Terminal:
|
|||||||
|
|
||||||
def retheme(self, scheme):
|
def retheme(self, scheme):
|
||||||
"""Switch to a different scheme: vterm: Select Theme, scoped to this terminal."""
|
"""Switch to a different scheme: vterm: Select Theme, scoped to this terminal."""
|
||||||
|
if scheme is self.scheme:
|
||||||
|
return
|
||||||
self.scheme = scheme
|
self.scheme = scheme
|
||||||
self.view.settings().set("color_scheme", scheme.path)
|
self.view.settings().set("color_scheme", scheme.path)
|
||||||
self.apply_scheme()
|
self.apply_scheme()
|
||||||
|
|||||||
Reference in New Issue
Block a user