select theme applies only to current vterm and newly opened vterms
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
| `vterm/terminal.py` | `Terminal`: pty process, emulator ownership, the reader/renderer threads, and everything written back to the app. |
|
| `vterm/terminal.py` | `Terminal`: pty process, emulator ownership, the reader/renderer threads, and everything written back to the app. |
|
||||||
| `vterm/emulator.py` | `VtScreen` and `VtStream`: pyte subclasses adding the terminal features these agents need. |
|
| `vterm/emulator.py` | `VtScreen` and `VtStream`: pyte subclasses adding the terminal features these agents need. |
|
||||||
| `vterm/render.py` | Screen → view text, colour overlays, caret, folding, trimming. |
|
| `vterm/render.py` | Screen → view text, colour overlays, caret, folding, trimming. |
|
||||||
| `vterm/scheme.py` | `SchemeManager`: generates the colour scheme that defines the scopes the renderer paints with. |
|
| `vterm/scheme.py` | `SchemeManager`: generates one theme's colour scheme, the scopes the renderer paints with. `get_scheme_manager` caches one instance per theme name, each terminal frozen to the instance it was opened or rethemed with. |
|
||||||
| `vterm/themes.py` | The built-in palettes (`default`, `homebrew`, `pro`, `slate`, `red-alert`). |
|
| `vterm/themes.py` | The built-in palettes (`default`, `homebrew`, `pro`, `slate`, `red-alert`). |
|
||||||
| `vterm/commands.py` | The Sublime commands: open, keypress, copy/paste, theme picker, clear, cleanup, close. |
|
| `vterm/commands.py` | The Sublime commands: open, keypress, copy/paste, theme picker, clear, cleanup, close. |
|
||||||
| `vterm/listeners.py` | `VtermCoreEventListener`: focus, IME capture, copy/paste rerouting, the mousemap context. |
|
| `vterm/listeners.py` | `VtermCoreEventListener`: focus, IME capture, copy/paste rerouting, the mousemap context. |
|
||||||
|
|||||||
@@ -104,9 +104,10 @@ commented out beside it), `debug` (verbose logging to the ST console), plus the
|
|||||||
colours:
|
colours:
|
||||||
|
|
||||||
- `theme` — `"default"` (Terminal.app Basic, light), `"homebrew"`, `"pro"`,
|
- `theme` — `"default"` (Terminal.app Basic, light), `"homebrew"`, `"pro"`,
|
||||||
`"slate"`, `"red-alert"`, or `null` to follow the active Sublime colour scheme. Set it with
|
`"slate"`, `"red-alert"`, or `null` to follow the active Sublime colour scheme. The
|
||||||
*vterm: Select Theme* or by hand; either way open terminals restyle in place,
|
default for terminals opened from now on. *vterm: Select Theme* retints the
|
||||||
no restart needed.
|
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"}`,
|
- `ansi_colors` — override individual base colours, e.g. `{"red": "#ff0000"}`,
|
||||||
applied on top of the theme.
|
applied on top of the theme.
|
||||||
|
|
||||||
|
|||||||
13
main.py
13
main.py
@@ -5,6 +5,8 @@ registry would orphan open terminal tabs.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import sublime
|
||||||
|
|
||||||
# imported for sublime's command discovery, not called here
|
# imported for sublime's command discovery, not called here
|
||||||
from .vterm.commands import (
|
from .vterm.commands import (
|
||||||
VtermClearScreenCommand,
|
VtermClearScreenCommand,
|
||||||
@@ -28,7 +30,7 @@ from .vterm.mouse import (
|
|||||||
VtermMouseEventListener,
|
VtermMouseEventListener,
|
||||||
VtermScrollCommand,
|
VtermScrollCommand,
|
||||||
)
|
)
|
||||||
from .vterm.scheme import scheme_manager
|
from .vterm.scheme import build_startup_schemes
|
||||||
from .vterm.utils import settings
|
from .vterm.utils import settings
|
||||||
from .vterm.render import (
|
from .vterm.render import (
|
||||||
VtermRenderCommand,
|
VtermRenderCommand,
|
||||||
@@ -37,9 +39,12 @@ from .vterm.render import (
|
|||||||
|
|
||||||
|
|
||||||
def plugin_loaded():
|
def plugin_loaded():
|
||||||
"""Write the colour scheme and set the log level, once per plugin host."""
|
"""Write any missing theme's colour scheme and set the log level, once per
|
||||||
# synchronous: a restored tab's color_scheme is read during startup
|
plugin host: a restored tab's color_scheme is read during startup and may
|
||||||
scheme_manager.ensure_file()
|
name any of them.
|
||||||
|
"""
|
||||||
|
view = sublime.active_window().active_view() if sublime.active_window() else None
|
||||||
|
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
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
|
|||||||
@@ -87,3 +87,7 @@ def expand_variables(value, variables):
|
|||||||
|
|
||||||
def version():
|
def version():
|
||||||
return "4200"
|
return "4200"
|
||||||
|
|
||||||
|
|
||||||
|
def active_window():
|
||||||
|
return None
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ from _bootstrap import FakeProcess
|
|||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
from vterm.commands import VtermPasteSelectionCommand, VtermSelectThemeCommand, selection_text
|
from vterm.commands import VtermPasteSelectionCommand, VtermSelectThemeCommand, selection_text
|
||||||
|
from vterm.emulator import VtScreen
|
||||||
from vterm.render import CONTINUATION
|
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.utils import SETTINGS_FILE
|
||||||
from vterm.themes import THEMES
|
from vterm.themes import THEMES
|
||||||
|
|
||||||
@@ -13,72 +16,113 @@ settings = sublime.load_settings(SETTINGS_FILE)
|
|||||||
panel = {}
|
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:
|
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):
|
def show_quick_panel(self, items, on_done, selected_index=-1, on_highlight=None):
|
||||||
panel.update(items=items, on_done=on_done,
|
panel.update(items=items, on_done=on_done,
|
||||||
selected_index=selected_index, on_highlight=on_highlight)
|
selected_index=selected_index, on_highlight=on_highlight)
|
||||||
|
|
||||||
|
|
||||||
def open_picker(current):
|
def fake_terminal(theme_name):
|
||||||
settings.set("theme", current)
|
"""A terminal frozen to a theme, registered the way a real one is."""
|
||||||
command = VtermSelectThemeCommand(FakeWindow())
|
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()
|
command.run()
|
||||||
return command
|
return command
|
||||||
|
|
||||||
|
|
||||||
# every theme is offered, with "no theme" first
|
# 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 [i[0] for i in panel["items"]] == ["No theme"] + sorted(THEMES), panel["items"]
|
||||||
assert panel["selected_index"] == 0
|
assert panel["selected_index"] == 0
|
||||||
|
|
||||||
# the configured theme starts highlighted
|
# the focused terminal's own theme starts highlighted, not the global default
|
||||||
command = open_picker("pro")
|
settings.set("theme", "homebrew")
|
||||||
|
terminal = fake_terminal("pro")
|
||||||
|
command = open_picker(terminal)
|
||||||
assert panel["selected_index"] == command.choices.index("pro")
|
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)
|
saves = len(sublime._saved)
|
||||||
panel["on_highlight"](command.choices.index("slate"))
|
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"
|
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)
|
panel["on_done"](-1)
|
||||||
assert settings.get("theme") == "pro", settings.get("theme")
|
assert terminal.scheme.theme_name == "pro", terminal.scheme.theme_name
|
||||||
|
|
||||||
# confirming persists
|
# confirming retheme the focused terminal and persists the default for new terminals
|
||||||
command = open_picker("pro")
|
command = open_picker(terminal)
|
||||||
panel["on_done"](command.choices.index("homebrew"))
|
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 settings.get("theme") == "homebrew", settings.get("theme")
|
||||||
assert sublime._saved[-1] == SETTINGS_FILE
|
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
|
# "no theme" persists as null rather than a name
|
||||||
command = open_picker("slate")
|
command = open_picker(fake_terminal("slate"))
|
||||||
panel["on_done"](0)
|
panel["on_done"](0)
|
||||||
assert settings.get("theme") is None, settings.get("theme")
|
assert settings.get("theme") is None, settings.get("theme")
|
||||||
|
|
||||||
print("theme picker tests passed ({} themes offered)".format(len(THEMES)))
|
print("theme picker tests passed ({} themes offered)".format(len(THEMES)))
|
||||||
|
|
||||||
|
|
||||||
# a palette change must reach terminals that are already running: the screen
|
# a palette change must reach the screen it was applied to: it answers OSC 10/11
|
||||||
# answers OSC 10/11 and DSR 996 from these values
|
# and DSR 996 from these values
|
||||||
from vterm.emulator import VtScreen, VtStream
|
from vterm.emulator import VtStream
|
||||||
from vterm.scheme import scheme_manager
|
|
||||||
from vterm.terminal import Terminal
|
|
||||||
|
|
||||||
|
|
||||||
recorder = FakeProcess()
|
recorder = FakeProcess()
|
||||||
# real __init__ (view unused here); process/screen stand in for start()
|
terminal = fake_terminal("default")
|
||||||
terminal = Terminal(None)
|
|
||||||
terminal.screen = VtScreen(20, 3, process=recorder, fg_color="000000",
|
terminal.screen = VtScreen(20, 3, process=recorder, fg_color="000000",
|
||||||
bg_color="ffffff", is_dark=False)
|
bg_color="ffffff", is_dark=False)
|
||||||
terminal.process = recorder
|
terminal.process = recorder
|
||||||
stream = VtStream(terminal.screen)
|
stream = VtStream(terminal.screen)
|
||||||
|
|
||||||
scheme_manager.foreground = "#eeeeee"
|
terminal.scheme = get_scheme_manager("slate")
|
||||||
scheme_manager.background = "#111111"
|
|
||||||
terminal.apply_scheme()
|
terminal.apply_scheme()
|
||||||
assert terminal.screen.default_bg_color == "111111", terminal.screen.default_bg_color
|
assert terminal.screen.default_fg_color == terminal.scheme.foreground.lstrip("#")
|
||||||
assert terminal.screen.theme_is_dark is True
|
assert terminal.screen.default_bg_color == terminal.scheme.background.lstrip("#")
|
||||||
|
|
||||||
# without mode 2031 the app is not interrupted
|
# without mode 2031 the app is not interrupted
|
||||||
assert recorder.written == "", repr(recorder.written)
|
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
|
# and the emulator answers a colour query with the new palette
|
||||||
recorder.written = ""
|
recorder.written = ""
|
||||||
stream.feed("\x1b]11;?\x07")
|
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)")
|
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
|
# 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
|
import os
|
||||||
from vterm.scheme import SCHEME_FILE_NAME
|
|
||||||
|
|
||||||
path = os.path.join(scheme_manager._directory(), SCHEME_FILE_NAME)
|
scheme = get_scheme_manager("pro")
|
||||||
if os.path.exists(path):
|
path = os.path.join(scheme._directory(), scheme.file_name)
|
||||||
os.remove(path)
|
assert os.path.exists(path), "get_scheme_manager did not write the scheme"
|
||||||
|
|
||||||
scheme_manager.ensure_file()
|
# the cached instance is reused, not rebuilt
|
||||||
assert os.path.exists(path), "ensure_file did not write the scheme"
|
assert get_scheme_manager("pro") is scheme, "theme managers must be cached"
|
||||||
assert not scheme_manager._initialized, \
|
|
||||||
"ensure_file must leave the palette open to refinement by initialize()"
|
|
||||||
|
|
||||||
# a second call is a no-op: same file, untouched
|
# build is idempotent
|
||||||
stamp = os.path.getmtime(path)
|
with scheme.lock:
|
||||||
scheme_manager.ensure_file()
|
first = len(scheme.rules)
|
||||||
assert os.path.getmtime(path) == stamp, "ensure_file rewrote an existing scheme"
|
scheme.build()
|
||||||
|
assert len(scheme.rules) == first, \
|
||||||
# _build is idempotent, which is what makes the ensure_file + initialize pair safe
|
"rules duplicated: {} -> {}".format(first, len(scheme.rules))
|
||||||
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))
|
|
||||||
|
|
||||||
print("scheme file tests passed ({} seeded rules, idempotent)".format(first))
|
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"
|
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")
|
load_sublime_json("Default (OSX).sublime-mousemap")
|
||||||
|
|
||||||
# scheme manager produces scopes and rules
|
# a theme's scheme manager produces scopes and rules
|
||||||
import vterm.scheme as scheme_module
|
from vterm.scheme import get_scheme_manager
|
||||||
from vterm.scheme import scheme_manager
|
scope = get_scheme_manager("default").scope("ff0000", "default", "b")
|
||||||
scope = scheme_manager.scope("ff0000", "default", "b")
|
|
||||||
assert scope == "vterm.ff0000.default.b", scope
|
assert scope == "vterm.ff0000.default.b", scope
|
||||||
|
|
||||||
# every command referenced by the palette and the keymap must exist, and the
|
# 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")
|
configured = load_sublime_json("vterm.sublime-settings").get("theme")
|
||||||
assert configured is None or configured in THEMES, configured
|
assert configured is None or configured in THEMES, configured
|
||||||
|
|
||||||
# plugin_loaded must create the colour scheme: sublime loads a restored tab's
|
# plugin_loaded must create every theme's colour scheme: sublime loads a restored
|
||||||
# color_scheme during startup, before any terminal opens
|
# 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 -
|
# 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 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 = types.ModuleType("vterm")
|
||||||
pkg.__path__ = [ROOT]
|
pkg.__path__ = [ROOT]
|
||||||
sys.modules["vterm"] = pkg
|
sys.modules["vterm"] = pkg
|
||||||
main = importlib.import_module("vterm.main")
|
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()
|
main.plugin_loaded()
|
||||||
assert os.path.exists(scheme_file), \
|
for name, scheme_file in zip(ALL_THEME_NAMES, scheme_files):
|
||||||
"plugin_loaded must leave the colour scheme on disk"
|
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, "
|
print("import smoke test passed ({} keymap bindings, {} pending flags, "
|
||||||
"{} themes)".format(len(keymap), len(flags), len(THEMES)))
|
"{} themes)".format(len(keymap), len(flags), len(THEMES)))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from vterm.render import (
|
|||||||
following,
|
following,
|
||||||
scroll_to_cursor,
|
scroll_to_cursor,
|
||||||
)
|
)
|
||||||
|
from vterm.scheme import get_scheme_manager
|
||||||
from vterm.terminal import Terminal
|
from vterm.terminal import Terminal
|
||||||
from vterm.utils import SETTINGS_FILE
|
from vterm.utils import SETTINGS_FILE
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ class FakeView:
|
|||||||
|
|
||||||
def make_terminal(rows=10, cols=20, scrollback=None):
|
def make_terminal(rows=10, cols=20, scrollback=None):
|
||||||
# real __init__ for the plain attributes; process/screen stand in for start()
|
# 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:
|
if scrollback is not None:
|
||||||
terminal.scrollback_size = scrollback
|
terminal.scrollback_size = scrollback
|
||||||
terminal.process = FakeProcess()
|
terminal.process = FakeProcess()
|
||||||
@@ -165,9 +166,12 @@ def make_terminal(rows=10, cols=20, scrollback=None):
|
|||||||
return terminal, VtStream(terminal.screen)
|
return terminal, VtStream(terminal.screen)
|
||||||
|
|
||||||
|
|
||||||
def make_command(view):
|
def make_command(terminal):
|
||||||
# the real __init__, so the fake cannot drift from the command's attributes
|
# the real __init__, so the fake cannot drift from the command's attributes;
|
||||||
return VtermRenderCommand(view)
|
# 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
|
# 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"
|
assert terminal.screen.history.maxlen == 0, "alt screen zeroes the deque"
|
||||||
|
|
||||||
terminal.view = FakeView("\n".join("row {}".format(i) for i in range(10)))
|
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
|
before = terminal.view.text
|
||||||
command.trim_history(None, terminal)
|
command.trim_history(None, terminal)
|
||||||
assert terminal.view.text == before, \
|
assert terminal.view.text == before, \
|
||||||
@@ -188,7 +192,7 @@ assert terminal.view.text == before, \
|
|||||||
terminal, stream = make_terminal(rows=5, scrollback=20)
|
terminal, stream = make_terminal(rows=5, scrollback=20)
|
||||||
terminal.offset = 40
|
terminal.offset = 40
|
||||||
terminal.view = FakeView("\n".join("row {}".format(i) for i in range(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)
|
command.trim_history(None, terminal)
|
||||||
rows_left = terminal.view.text.count("\n") + 1
|
rows_left = terminal.view.text.count("\n") + 1
|
||||||
assert rows_left <= 20, rows_left
|
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, stream = make_terminal(rows=5, scrollback=5000)
|
||||||
terminal.offset = 3
|
terminal.offset = 3
|
||||||
terminal.view = FakeView("\n".join("row {}".format(i) for i in range(8)))
|
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)
|
command.trim_history(None, terminal)
|
||||||
assert "row 0" in terminal.view.text and "row 7" in terminal.view.text, \
|
assert "row 0" in terminal.view.text and "row 7" in terminal.view.text, \
|
||||||
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)
|
terminal, stream = make_terminal(rows=4)
|
||||||
stream.feed("\x1b[?1049h")
|
stream.feed("\x1b[?1049h")
|
||||||
terminal.view = FakeView("top\nrow\n\n")
|
terminal.view = FakeView("top\nrow\n\n")
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal)
|
||||||
before = terminal.view.text
|
before = terminal.view.text
|
||||||
command.trim_trailing_spaces(None, terminal)
|
command.trim_trailing_spaces(None, terminal)
|
||||||
assert terminal.view.text == before, \
|
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
|
# on the primary screen the blank tail below the cursor still goes
|
||||||
terminal, stream = make_terminal(rows=4)
|
terminal, stream = make_terminal(rows=4)
|
||||||
terminal.view = FakeView("top\nrow\n\n")
|
terminal.view = FakeView("top\nrow\n\n")
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal)
|
||||||
command.trim_trailing_spaces(None, terminal)
|
command.trim_trailing_spaces(None, terminal)
|
||||||
assert terminal.view.text == "top\nrow", terminal.view.text
|
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
|
# batches the runs that share a scope into one add_regions call
|
||||||
terminal, stream = make_terminal(rows=3, cols=20)
|
terminal, stream = make_terminal(rows=3, cols=20)
|
||||||
terminal.view = FakeView()
|
terminal.view = FakeView()
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal)
|
||||||
stream.feed("\x1b[31mRED\x1b[0m..\x1b[31mred\x1b[0m..\x1b[32mgreen")
|
stream.feed("\x1b[31mRED\x1b[0m..\x1b[31mred\x1b[0m..\x1b[32mgreen")
|
||||||
command.update_lines(None, terminal)
|
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
|
# the whole session, and merging every frame would re-send the whole chunk
|
||||||
terminal, stream = make_terminal(rows=3, cols=20)
|
terminal, stream = make_terminal(rows=3, cols=20)
|
||||||
terminal.view = FakeView()
|
terminal.view = FakeView()
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal)
|
||||||
for i in range(300):
|
for i in range(300):
|
||||||
stream.feed("\x1b[31mred{}\x1b[0m\r\n".format(i))
|
stream.feed("\x1b[31mred{}\x1b[0m\r\n".format(i))
|
||||||
command.update_lines(None, terminal)
|
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
|
# trimming settles the open chunk, then erases the keys of chunks above the cut
|
||||||
terminal, stream = make_terminal(rows=3, scrollback=30)
|
terminal, stream = make_terminal(rows=3, scrollback=30)
|
||||||
terminal.view = FakeView()
|
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)))
|
stream.feed("".join("\x1b[31mr{}\x1b[0m\r\n".format(i) for i in range(300)))
|
||||||
command.update_lines(None, terminal)
|
command.update_lines(None, terminal)
|
||||||
terminal.screen.dirty.clear()
|
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._terminals[id(terminal)] = terminal
|
||||||
terminal.view.id = lambda: id(terminal)
|
terminal.view.id = lambda: id(terminal)
|
||||||
terminal.default_title = "zsh"
|
terminal.default_title = "zsh"
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal)
|
||||||
stream.feed("[vterm] %")
|
stream.feed("[vterm] %")
|
||||||
command.run(None)
|
command.run(None)
|
||||||
assert all(r.empty() for r in terminal.view.sel()), \
|
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)")
|
print("render diff tests passed (unchanged dirty rows skipped)")
|
||||||
|
|
||||||
# the generated scheme must define a selection colour, or sublime uses blue
|
# the generated scheme must define a selection colour, or sublime uses blue
|
||||||
from vterm.scheme import blend, scheme_manager
|
from vterm.scheme import blend
|
||||||
scheme_manager.background = "#303841"
|
assert blend("#303841", "#d8dee9", 0.3) == "#626a73"
|
||||||
scheme_manager.foreground = "#d8dee9"
|
|
||||||
assert blend(scheme_manager.background, scheme_manager.foreground, 0.3) == "#626a73"
|
|
||||||
|
|
||||||
print("selection artefact tests passed (fresh prompt unselected, user selection kept)")
|
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,
|
# DECSCUSR drives the caret shape through view settings; before any app asks,
|
||||||
# and for 0, the caret is the terminal default: a blinking block
|
# and for 0, the caret is the terminal default: a blinking block
|
||||||
terminal, stream = make_terminal(rows=3, cols=20)
|
terminal, stream = make_terminal(rows=3, cols=20)
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal)
|
||||||
settings = terminal.view.settings()
|
settings = terminal.view.settings()
|
||||||
|
|
||||||
command.sync_cursor_style(terminal)
|
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.view = FakeView("old1\nold2\nold3\nr0\nr1\nr2\nr3\nr4\n")
|
||||||
terminal.offset = 3
|
terminal.offset = 3
|
||||||
terminal.viewport_y = 400
|
terminal.viewport_y = 400
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal)
|
||||||
fold_end = terminal.view.text_point(3, 0) - 1
|
fold_end = terminal.view.text_point(3, 0) - 1
|
||||||
|
|
||||||
stream.feed("\x1b[?1049h")
|
stream.feed("\x1b[?1049h")
|
||||||
@@ -649,7 +651,7 @@ assert terminal.view.folds == [], terminal.view.folds
|
|||||||
# without scrollback there is nothing to fold
|
# without scrollback there is nothing to fold
|
||||||
terminal, stream = make_terminal(rows=5, cols=20)
|
terminal, stream = make_terminal(rows=5, cols=20)
|
||||||
terminal.view = FakeView("r0\nr1\nr2\nr3\nr4\n")
|
terminal.view = FakeView("r0\nr1\nr2\nr3\nr4\n")
|
||||||
command = make_command(terminal.view)
|
command = make_command(terminal)
|
||||||
stream.feed("\x1b[?1049h")
|
stream.feed("\x1b[?1049h")
|
||||||
command.sync_alt_fold(terminal)
|
command.sync_alt_fold(terminal)
|
||||||
assert terminal.view.folds == [], terminal.view.folds
|
assert terminal.view.folds == [], terminal.view.folds
|
||||||
|
|||||||
@@ -22,8 +22,9 @@
|
|||||||
"min_columns": 20,
|
"min_columns": 20,
|
||||||
"max_columns": 500,
|
"max_columns": 500,
|
||||||
|
|
||||||
// colour palette: "default", "homebrew", "pro", "slate", or null to
|
// colour palette: "default", "homebrew", "pro", "slate", "red-alert", or null
|
||||||
// follow the active Sublime colour scheme. Applies to open tabs too.
|
// to follow the active Sublime colour scheme. Default for terminals opened
|
||||||
|
// from now on; vterm: Select Theme retints the focused terminal and sets this.
|
||||||
"theme": null,
|
"theme": null,
|
||||||
|
|
||||||
// override any of the 16 ANSI colors, e.g. {"red": "#ff0000"}
|
// override any of the 16 ANSI colors, e.g. {"red": "#ff0000"}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import sublime
|
|||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
from .render import CONTINUATION, scroll_view
|
from .render import CONTINUATION, scroll_view
|
||||||
from .scheme import SCHEME_PATH, scheme_manager
|
from .scheme import get_scheme_manager
|
||||||
from .terminal import Terminal
|
from .terminal import Terminal
|
||||||
from .themes import THEMES
|
from .themes import ALL_THEME_NAMES, THEMES
|
||||||
from .utils import SETTINGS_FILE, settings
|
from .utils import SETTINGS_FILE, settings
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -43,12 +43,12 @@ class VtermOpenCommand(sublime_plugin.WindowCommand):
|
|||||||
cwd = os.path.expanduser("~")
|
cwd = os.path.expanduser("~")
|
||||||
cwd = sublime.expand_variables(cwd, window.extract_variables())
|
cwd = sublime.expand_variables(cwd, window.extract_variables())
|
||||||
|
|
||||||
scheme_manager.initialize(window.active_view())
|
scheme = get_scheme_manager(settings().get("theme"), host_view=window.active_view())
|
||||||
|
|
||||||
view = window.new_file()
|
view = window.new_file()
|
||||||
view.run_command("vterm_initialize_view")
|
view.run_command("vterm_initialize_view", {"color_scheme": scheme.path})
|
||||||
|
|
||||||
terminal = Terminal(view)
|
terminal = Terminal(view, scheme)
|
||||||
try:
|
try:
|
||||||
terminal.start(cmd, cwd=cwd, env=env, default_title=title or os.path.basename(cmd[0]))
|
terminal.start(cmd, cwd=cwd, env=env, default_title=title or os.path.basename(cmd[0]))
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -106,7 +106,7 @@ class VtermOpenTerminalProjectFolderCommand(sublime_plugin.WindowCommand):
|
|||||||
class VtermInitializeViewCommand(sublime_plugin.TextCommand):
|
class VtermInitializeViewCommand(sublime_plugin.TextCommand):
|
||||||
"""Stamps a fresh view with the settings that make it a terminal."""
|
"""Stamps a fresh view with the settings that make it a terminal."""
|
||||||
|
|
||||||
def run(self, _):
|
def run(self, _, color_scheme):
|
||||||
"""Apply the view settings; the pty starts only after this."""
|
"""Apply the view settings; the pty starts only after this."""
|
||||||
view = self.view
|
view = self.view
|
||||||
view_settings = view.settings()
|
view_settings = view.settings()
|
||||||
@@ -125,7 +125,7 @@ class VtermInitializeViewCommand(sublime_plugin.TextCommand):
|
|||||||
view_settings.set("draw_indent_guides", False)
|
view_settings.set("draw_indent_guides", False)
|
||||||
view_settings.set("scroll_past_end", False)
|
view_settings.set("scroll_past_end", False)
|
||||||
view_settings.set("scroll_context_lines", 0)
|
view_settings.set("scroll_context_lines", 0)
|
||||||
view_settings.set("color_scheme", SCHEME_PATH)
|
view_settings.set("color_scheme", color_scheme)
|
||||||
view_settings.set("syntax", "Packages/vterm/vterm.sublime-syntax")
|
view_settings.set("syntax", "Packages/vterm/vterm.sublime-syntax")
|
||||||
view_settings.set("spell_check", False)
|
view_settings.set("spell_check", False)
|
||||||
view_settings.set("detect_indentation", False)
|
view_settings.set("detect_indentation", False)
|
||||||
@@ -225,13 +225,20 @@ class VtermPasteSelectionCommand(sublime_plugin.TextCommand):
|
|||||||
|
|
||||||
|
|
||||||
class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
|
class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
|
||||||
"""Pick a colour palette, previewing each as it is highlighted"""
|
"""Retheme the focused terminal, previewing each choice as it is highlighted.
|
||||||
|
|
||||||
|
The chosen theme also becomes the default for terminals opened from now on;
|
||||||
|
it does not touch any other already-open terminal.
|
||||||
|
"""
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Show the palette picker, starting on the configured theme."""
|
"""Show the palette picker, starting on the focused terminal's own theme."""
|
||||||
self.settings = settings()
|
view = self.window.active_view()
|
||||||
self.original = self.settings.get("theme")
|
self.terminal = Terminal.from_id(view.id()) if view else None
|
||||||
self.choices = [None] + sorted(THEMES)
|
if not self.terminal:
|
||||||
|
return
|
||||||
|
self.original = self.terminal.scheme.theme_name
|
||||||
|
self.choices = ALL_THEME_NAMES
|
||||||
|
|
||||||
items = [["No theme", "follow the Sublime color scheme"]]
|
items = [["No theme", "follow the Sublime color scheme"]]
|
||||||
for name in self.choices[1:]:
|
for name in self.choices[1:]:
|
||||||
@@ -241,16 +248,32 @@ class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
|
|||||||
current = self.choices.index(self.original) if self.original in self.choices else 0
|
current = self.choices.index(self.original) if self.original in self.choices else 0
|
||||||
self.window.show_quick_panel(items, self._done, selected_index=current, on_highlight=self._preview)
|
self.window.show_quick_panel(items, self._done, selected_index=current, on_highlight=self._preview)
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
"""Greys the palette entry out when the focused view is not a terminal."""
|
||||||
|
view = self.window.active_view()
|
||||||
|
return view is not None and Terminal.from_id(view.id()) is not None
|
||||||
|
|
||||||
|
def _apply(self, name):
|
||||||
|
"""Retheme the focused terminal alone."""
|
||||||
|
scheme = get_scheme_manager(name, host_view=self.terminal.view)
|
||||||
|
self.terminal.retheme(scheme)
|
||||||
|
|
||||||
def _preview(self, index):
|
def _preview(self, index):
|
||||||
"""Apply a palette while it is highlighted, without saving it."""
|
"""Apply a palette to the focused terminal while it is highlighted."""
|
||||||
self.settings.set("theme", self.choices[index])
|
self._apply(self.choices[index])
|
||||||
|
|
||||||
def _done(self, index):
|
def _done(self, index):
|
||||||
"""Persist the choice, or restore the original if the panel was cancelled."""
|
"""On cancel, restore the focused terminal's original theme.
|
||||||
|
|
||||||
|
On confirm, keep the focused terminal on its new theme and persist the
|
||||||
|
choice as the default for terminals opened from now on.
|
||||||
|
"""
|
||||||
if index == -1:
|
if index == -1:
|
||||||
self.settings.set("theme", self.original)
|
self._apply(self.original)
|
||||||
return
|
return
|
||||||
self.settings.set("theme", self.choices[index])
|
name = self.choices[index]
|
||||||
|
self._apply(name)
|
||||||
|
settings().set("theme", name)
|
||||||
sublime.save_settings(SETTINGS_FILE)
|
sublime.save_settings(SETTINGS_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from wcwidth import wcswidth
|
|||||||
# per-cell call in segment_buffer_line
|
# per-cell call in segment_buffer_line
|
||||||
cell_width = lru_cache(maxsize=4096)(wcswidth)
|
cell_width = lru_cache(maxsize=4096)(wcswidth)
|
||||||
|
|
||||||
from .scheme import scheme_manager
|
|
||||||
from .terminal import Terminal
|
from .terminal import Terminal
|
||||||
from .utils import rev_wcwidth
|
from .utils import rev_wcwidth
|
||||||
|
|
||||||
@@ -124,6 +123,8 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Set up this view's render caches."""
|
"""Set up this view's render caches."""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
# the terminal's current SchemeManager; set at the top of each run()
|
||||||
|
self._scheme = None
|
||||||
self.colored_lines = {}
|
self.colored_lines = {}
|
||||||
# view row -> (segments, lf) as last rendered; the dirty set is only a
|
# view row -> (segments, lf) as last rendered; the dirty set is only a
|
||||||
# repaint hint, so most dirty rows are identical frame to frame
|
# repaint hint, so most dirty rows are identical frame to frame
|
||||||
@@ -156,6 +157,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
terminal = Terminal.from_id(view.id())
|
terminal = Terminal.from_id(view.id())
|
||||||
if not terminal:
|
if not terminal:
|
||||||
return
|
return
|
||||||
|
self._scheme = terminal.scheme
|
||||||
|
|
||||||
screen = terminal.screen
|
screen = terminal.screen
|
||||||
# the caret and the tab name are the user's; an unfocused terminal leaves
|
# the caret and the tab name are the user's; an unfocused terminal leaves
|
||||||
@@ -320,7 +322,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
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(scheme_manager.scope(fg, bg, style), []).append((start, end))
|
by_scope.setdefault(self._scheme.scope(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:
|
||||||
@@ -391,7 +393,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 = scheme_manager.scope(fg, bg, style)
|
scope = self._scheme.scope(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())
|
||||||
|
|||||||
123
vterm/scheme.py
123
vterm/scheme.py
@@ -8,16 +8,12 @@ import sublime
|
|||||||
|
|
||||||
from .emulator import ANSI_COLORS
|
from .emulator import ANSI_COLORS
|
||||||
from .utils import settings
|
from .utils import settings
|
||||||
from .themes import THEMES
|
from .themes import ALL_THEME_NAMES, THEMES
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
# written under Packages/User so it survives a package reinstall
|
# written under Packages/User so it survives a package reinstall
|
||||||
SCHEME_DIR_NAME = "User/vterm"
|
SCHEME_DIR_NAME = "User/vterm"
|
||||||
# "hidden-" keeps the generated scheme out of the scheme picker
|
|
||||||
SCHEME_FILE_NAME = "vterm.hidden-color-scheme"
|
|
||||||
# the value terminal views get as their color_scheme setting
|
|
||||||
SCHEME_PATH = "Packages/{}/{}".format(SCHEME_DIR_NAME, SCHEME_FILE_NAME)
|
|
||||||
|
|
||||||
# each write makes sublime reload the scheme and restyle every view
|
# each write makes sublime reload the scheme and restyle every view
|
||||||
WRITE_DEBOUNCE_MS = 250
|
WRITE_DEBOUNCE_MS = 250
|
||||||
@@ -69,89 +65,46 @@ def _is_dark(color_text):
|
|||||||
|
|
||||||
|
|
||||||
class SchemeManager:
|
class SchemeManager:
|
||||||
"""Owns the generated scheme file and the set of known scopes."""
|
"""Owns one theme's generated scheme file and set of known scopes."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, theme_name):
|
||||||
"""Start on the built-in palette; initialize() picks up the real one."""
|
"""State only; build() resolves the palette and writes the file."""
|
||||||
|
self.theme_name = theme_name
|
||||||
|
self.file_name = "vterm-{}.hidden-color-scheme".format(theme_name or "system")
|
||||||
|
self.path = "Packages/{}/{}".format(SCHEME_DIR_NAME, self.file_name)
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.rules = {}
|
self.rules = {}
|
||||||
# key -> scope name; the render thread reads it without the lock, so it
|
# key -> scope name; the render thread reads it without the lock, so it
|
||||||
# must never outlive self.rules (cleared together in _build)
|
# must never outlive self.rules (cleared together in build)
|
||||||
self._names = {}
|
self._names = {}
|
||||||
self.foreground = "#ffffff"
|
self.foreground = "#ffffff"
|
||||||
self.background = "#000000"
|
self.background = "#000000"
|
||||||
self._editor_fg = "#ffffff"
|
|
||||||
self._editor_bg = "#000000"
|
|
||||||
self.ansi = dict(DEFAULT_ANSI)
|
self.ansi = dict(DEFAULT_ANSI)
|
||||||
self._write_pending = False
|
self._write_pending = False
|
||||||
self._initialized = False
|
|
||||||
self._dir = None
|
self._dir = None
|
||||||
self._host_view = None
|
|
||||||
self._listeners = []
|
|
||||||
|
|
||||||
def initialize(self, view):
|
def build(self, host_view=None):
|
||||||
"""Pick up the palette, seed the rules, write the file. Main thread only"""
|
|
||||||
if self._initialized:
|
|
||||||
return
|
|
||||||
self._initialized = True
|
|
||||||
self._host_view = view
|
|
||||||
|
|
||||||
settings().add_on_change("vterm.theme", self.reload)
|
|
||||||
self._build()
|
|
||||||
|
|
||||||
def ensure_file(self):
|
|
||||||
"""Write the scheme if missing: a restored tab's color_scheme is loaded during startup"""
|
|
||||||
if os.path.exists(os.path.join(self._directory(), SCHEME_FILE_NAME)):
|
|
||||||
return
|
|
||||||
with self.lock:
|
|
||||||
self._build()
|
|
||||||
|
|
||||||
def add_listener(self, callback):
|
|
||||||
"""Called after every palette rebuild"""
|
|
||||||
self._listeners.append(callback)
|
|
||||||
|
|
||||||
def remove_listener(self, callback):
|
|
||||||
"""Drop a rebuild subscriber; terminals unsubscribe when they die."""
|
|
||||||
if callback in self._listeners:
|
|
||||||
self._listeners.remove(callback)
|
|
||||||
|
|
||||||
def reload(self):
|
|
||||||
"""Rebuild every rule for a new palette, then notify the terminals"""
|
|
||||||
if not self._initialized:
|
|
||||||
# no scheme file to maintain yet; initialize() will read the setting
|
|
||||||
return
|
|
||||||
with self.lock:
|
|
||||||
self._build()
|
|
||||||
for callback in list(self._listeners):
|
|
||||||
try:
|
|
||||||
callback()
|
|
||||||
except Exception:
|
|
||||||
LOG.warning("scheme listener failed", exc_info=True)
|
|
||||||
|
|
||||||
def _build(self):
|
|
||||||
"""Resolve the palette and regenerate every rule, then write the file.
|
"""Resolve the palette and regenerate every rule, then write the file.
|
||||||
|
|
||||||
Rules cannot be appended after a palette change: each one stores resolved
|
Rules cannot be appended after a palette change: each one stores resolved
|
||||||
colours, so they all have to be rebuilt. Caller holds the lock.
|
colours, so they all have to be rebuilt. Callable again later (e.g. to
|
||||||
|
re-sample the "No theme" palette from the editor's current colours).
|
||||||
"""
|
"""
|
||||||
|
with self.lock:
|
||||||
self._names = {}
|
self._names = {}
|
||||||
self.rules = {}
|
self.rules = {}
|
||||||
self.ansi = dict(DEFAULT_ANSI)
|
self.ansi = dict(DEFAULT_ANSI)
|
||||||
|
|
||||||
theme = THEMES.get(settings().get("theme"))
|
theme = THEMES.get(self.theme_name)
|
||||||
|
|
||||||
if theme:
|
if theme:
|
||||||
self.foreground = theme["foreground"]
|
self.foreground = theme["foreground"]
|
||||||
self.background = theme["background"]
|
self.background = theme["background"]
|
||||||
self.ansi.update({name: theme[name] for name in ANSI_COLORS if name in theme})
|
self.ansi.update({name: theme[name] for name in ANSI_COLORS if name in theme})
|
||||||
else:
|
elif host_view is not None and host_view.is_valid():
|
||||||
# captured values outlive the view they came from
|
style = host_view.style()
|
||||||
if self._host_view is not None and self._host_view.is_valid():
|
self.foreground = style.get("foreground", self.foreground)
|
||||||
style = self._host_view.style()
|
self.background = style.get("background", self.background)
|
||||||
self._editor_fg = style.get("foreground", self._editor_fg)
|
|
||||||
self._editor_bg = style.get("background", self._editor_bg)
|
|
||||||
self.foreground = self._editor_fg
|
|
||||||
self.background = self._editor_bg
|
|
||||||
|
|
||||||
# user overrides sit on top of whichever palette was chosen
|
# user overrides sit on top of whichever palette was chosen
|
||||||
user_ansi = settings().get("ansi_colors", {})
|
user_ansi = settings().get("ansi_colors", {})
|
||||||
@@ -258,11 +211,49 @@ class SchemeManager:
|
|||||||
},
|
},
|
||||||
"rules": list(self.rules.values()),
|
"rules": list(self.rules.values()),
|
||||||
}
|
}
|
||||||
path = os.path.join(self._directory(), SCHEME_FILE_NAME)
|
path = os.path.join(self._directory(), self.file_name)
|
||||||
with open(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))
|
||||||
|
|
||||||
|
|
||||||
scheme_manager = SchemeManager()
|
# one manager per theme name, built once and kept for the plugin host's life: the
|
||||||
|
# built-in theme set is small and fixed, so there is nothing to garbage-collect
|
||||||
|
_managers = {}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
terminal a "no theme" scheme gets one that is actually current.
|
||||||
|
"""
|
||||||
|
manager = _managers.get(theme_name)
|
||||||
|
if manager is None:
|
||||||
|
manager = SchemeManager(theme_name)
|
||||||
|
_managers[theme_name] = manager
|
||||||
|
manager.build(host_view)
|
||||||
|
elif theme_name is None:
|
||||||
|
manager.build(host_view)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
def build_startup_schemes(host_view=None):
|
||||||
|
"""Make sure every theme's colour scheme is on disk before Sublime reads a
|
||||||
|
restored tab's color_scheme at startup.
|
||||||
|
|
||||||
|
A named theme is static, so an existing file is left alone - rebuilding it
|
||||||
|
would just rewrite the same bytes, and each write makes sublime reload the
|
||||||
|
scheme and restyle every view using it. None always rebuilds, same as
|
||||||
|
get_scheme_manager.
|
||||||
|
"""
|
||||||
|
for name in ALL_THEME_NAMES:
|
||||||
|
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)):
|
||||||
|
get_scheme_manager(name)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from pyte import modes as mo
|
|||||||
|
|
||||||
from .emulator import APP_CURSOR_MODE, VtScreen, VtStream
|
from .emulator import APP_CURSOR_MODE, VtScreen, VtStream
|
||||||
from .keys import get_key_code
|
from .keys import get_key_code
|
||||||
from .scheme import scheme_manager
|
|
||||||
from .utils import intermission, responsive, settings
|
from .utils import intermission, responsive, settings
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
@@ -98,9 +97,10 @@ class Terminal:
|
|||||||
|
|
||||||
_terminals = {}
|
_terminals = {}
|
||||||
|
|
||||||
def __init__(self, view):
|
def __init__(self, view, scheme=None):
|
||||||
"""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
|
||||||
# process.isalive() costs two waitpid syscalls; UI hooks read this instead
|
# process.isalive() costs two waitpid syscalls; UI hooks read this instead
|
||||||
self.alive = True
|
self.alive = True
|
||||||
self.default_title = ""
|
self.default_title = ""
|
||||||
@@ -198,12 +198,11 @@ class Terminal:
|
|||||||
erase_scrollback=settings().get("honor_erase_scrollback", False),
|
erase_scrollback=settings().get("honor_erase_scrollback", False),
|
||||||
clear_callback=self._clear_callback,
|
clear_callback=self._clear_callback,
|
||||||
clipboard_callback=self._clipboard_callback,
|
clipboard_callback=self._clipboard_callback,
|
||||||
fg_color=scheme_manager.foreground.lstrip("#"),
|
fg_color=self.scheme.foreground.lstrip("#"),
|
||||||
bg_color=scheme_manager.background.lstrip("#"),
|
bg_color=self.scheme.background.lstrip("#"),
|
||||||
is_dark=scheme_manager.is_dark(),
|
is_dark=self.scheme.is_dark(),
|
||||||
)
|
)
|
||||||
self.stream = VtStream(self.screen)
|
self.stream = VtStream(self.screen)
|
||||||
scheme_manager.add_listener(self.apply_scheme)
|
|
||||||
|
|
||||||
self._start_rendering()
|
self._start_rendering()
|
||||||
|
|
||||||
@@ -211,18 +210,23 @@ class Terminal:
|
|||||||
"""Adopt a new palette; mode 2031 subscribers are told so they can restyle"""
|
"""Adopt a new palette; mode 2031 subscribers are told so they can restyle"""
|
||||||
# the screen answers OSC 10/11 and DSR 996 from these values
|
# the screen answers OSC 10/11 and DSR 996 from these values
|
||||||
screen = self.screen
|
screen = self.screen
|
||||||
screen.default_fg_color = scheme_manager.foreground.lstrip("#")
|
screen.default_fg_color = self.scheme.foreground.lstrip("#")
|
||||||
screen.default_bg_color = scheme_manager.background.lstrip("#")
|
screen.default_bg_color = self.scheme.background.lstrip("#")
|
||||||
screen.theme_is_dark = scheme_manager.is_dark()
|
screen.theme_is_dark = self.scheme.is_dark()
|
||||||
if screen.theme_notify_enabled():
|
if screen.theme_notify_enabled():
|
||||||
self.send_string(screen.theme_report(), normalized=False)
|
self.send_string(screen.theme_report(), normalized=False)
|
||||||
|
|
||||||
|
def retheme(self, scheme):
|
||||||
|
"""Switch to a different scheme: vterm: Select Theme, scoped to this terminal."""
|
||||||
|
self.scheme = scheme
|
||||||
|
self.view.settings().set("color_scheme", scheme.path)
|
||||||
|
self.apply_scheme()
|
||||||
|
|
||||||
def kill(self):
|
def kill(self):
|
||||||
"""Terminate the child and drop this terminal from the registry."""
|
"""Terminate the child and drop this terminal from the registry."""
|
||||||
LOG.debug("[kill] terminating process")
|
LOG.debug("[kill] terminating process")
|
||||||
self.alive = False
|
self.alive = False
|
||||||
self._wake.set()
|
self._wake.set()
|
||||||
scheme_manager.remove_listener(self.apply_scheme)
|
|
||||||
try:
|
try:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -71,3 +71,6 @@ THEMES = {
|
|||||||
"light_white": "#ffffff",
|
"light_white": "#ffffff",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# every selectable theme, including None for "follow the active Sublime colour scheme"
|
||||||
|
ALL_THEME_NAMES = [None] + sorted(THEMES)
|
||||||
|
|||||||
Reference in New Issue
Block a user