Fix text loss after zero-width chars, grey 248 quantization, scrollback cap, no-theme sampling, shipped terminal default
- draw(): a zero-width non-combining char (VS16, ZWJ) or a stray control byte dropped the rest of the run; skip the char instead - quantize_256: grey 248 mapped to slot 256 and aborted the SGR - trim_history: the cap now counts rows above the screen, so a value below the screen height (0: no scrollback) no longer cuts the screen - "No theme" sampled the focused terminal's own colours; sample a non-terminal view instead - vterm.sublime-settings: Apple Terminal is the default again, kitty commented out, as the README says
This commit is contained in:
@@ -25,6 +25,7 @@ class FakeTerminalView:
|
|||||||
self._id = FakeTerminalView._next_id
|
self._id = FakeTerminalView._next_id
|
||||||
FakeTerminalView._next_id += 1
|
FakeTerminalView._next_id += 1
|
||||||
self._settings = sublime.Settings()
|
self._settings = sublime.Settings()
|
||||||
|
self._settings.set("vterm_view", True)
|
||||||
|
|
||||||
def id(self):
|
def id(self):
|
||||||
return self._id
|
return self._id
|
||||||
@@ -36,7 +37,24 @@ class FakeTerminalView:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def style(self):
|
def style(self):
|
||||||
return {}
|
# a terminal view reports its own theme's colours, not the editor's
|
||||||
|
return {"foreground": "#f2f2f2", "background": "#000000"}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeEditorView:
|
||||||
|
"""An ordinary file view, the one the "No theme" palette is sampled from."""
|
||||||
|
|
||||||
|
def settings(self):
|
||||||
|
return sublime.Settings()
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def style(self):
|
||||||
|
return {"foreground": "#111111", "background": "#eeeeee"}
|
||||||
|
|
||||||
|
|
||||||
|
editor = FakeEditorView()
|
||||||
|
|
||||||
|
|
||||||
class FakeWindow:
|
class FakeWindow:
|
||||||
@@ -46,6 +64,9 @@ class FakeWindow:
|
|||||||
def active_view(self):
|
def active_view(self):
|
||||||
return self._view
|
return self._view
|
||||||
|
|
||||||
|
def views(self):
|
||||||
|
return [self._view, editor]
|
||||||
|
|
||||||
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)
|
||||||
@@ -105,6 +126,12 @@ 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")
|
||||||
|
|
||||||
|
# ... and samples the editor's colours, not the themed terminal's own: that
|
||||||
|
# would retint every "no theme" terminal with the theme being left
|
||||||
|
system = get_scheme_manager(None)
|
||||||
|
assert (system.foreground, system.background) == ("#111111", "#eeeeee"), \
|
||||||
|
(system.foreground, system.background)
|
||||||
|
|
||||||
print("theme picker tests passed ({} themes offered)".format(len(THEMES)))
|
print("theme picker tests passed ({} themes offered)".format(len(THEMES)))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,21 @@ def run_case(char_by_char):
|
|||||||
assert screen.buffer[0][0].fg == "ff0000" and screen.buffer[0][0].bold, tag
|
assert screen.buffer[0][0].fg == "ff0000" and screen.buffer[0][0].bold, tag
|
||||||
assert screen.buffer[0][1].fg == "ff0000", (tag, screen.buffer[0][1].fg)
|
assert screen.buffer[0][1].fg == "ff0000", (tag, screen.buffer[0][1].fg)
|
||||||
|
|
||||||
|
# every grey lands inside the palette; 248 used to map to slot 256 and
|
||||||
|
# abort the whole SGR
|
||||||
|
from vterm.emulator import quantize_256
|
||||||
|
assert all(0 <= quantize_256(v, v, v) <= 255 for v in range(256))
|
||||||
|
proc, screen, stream = make()
|
||||||
|
feed(stream, "\x1b[38;2;248;248;248mX", char_by_char)
|
||||||
|
assert screen.buffer[0][0].fg == "ffffff", (tag, screen.buffer[0][0].fg)
|
||||||
|
|
||||||
|
# a zero-width non-combining char (VS16, ZWJ) or a stray control byte is
|
||||||
|
# dropped; the text after it must survive
|
||||||
|
proc, screen, stream = make()
|
||||||
|
feed(stream, "⚠️ warn \U0001F468\U0001F4BB ab\x01cd", char_by_char)
|
||||||
|
row = "".join(screen.buffer[0][x].data for x in range(20)).rstrip()
|
||||||
|
assert row == "⚠ warn \U0001F468\U0001F4BB abcd", (tag, repr(row))
|
||||||
|
|
||||||
# SGR 5/25 (blink): ignored, not a crash — Char has no blink field
|
# SGR 5/25 (blink): ignored, not a crash — Char has no blink field
|
||||||
proc, screen, stream = make()
|
proc, screen, stream = make()
|
||||||
feed(stream, "\x1b[5;31mX\x1b[25mY", char_by_char)
|
feed(stream, "\x1b[5;31mX\x1b[25mY", char_by_char)
|
||||||
@@ -315,7 +330,7 @@ GAUNTLET = (
|
|||||||
"\x1b[2;5r\x1b[Sscrolled\x1b[T\x1b[r"
|
"\x1b[2;5r\x1b[Sscrolled\x1b[T\x1b[r"
|
||||||
"\x1b[38:5:100mcolon\x1b[m\r\n"
|
"\x1b[38:5:100mcolon\x1b[m\r\n"
|
||||||
"\x1b[2\x073m bel mid-CSI \x1b[3\x18X can cancels\r\n"
|
"\x1b[2\x073m bel mid-CSI \x1b[3\x18X can cancels\r\n"
|
||||||
"wide: 漢字 \x1b[7m漢\x1b[m\r\n"
|
"wide: 漢字 \x1b[7m漢\x1b[m ⚠️ after\r\n"
|
||||||
"\x1b[m\x1b[;5m\x1b[5;m\x1b[25m params\r\n"
|
"\x1b[m\x1b[;5m\x1b[5;m\x1b[25m params\r\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -191,17 +191,30 @@ command.trim_history(None, terminal)
|
|||||||
assert terminal.view.text == before, \
|
assert terminal.view.text == before, \
|
||||||
"alt screen view was trimmed: {!r}".format(terminal.view.text)
|
"alt screen view was trimmed: {!r}".format(terminal.view.text)
|
||||||
|
|
||||||
# the configured cap still applies on the primary screen
|
# the configured cap still applies on the primary screen: that many rows of
|
||||||
|
# scrollback above the screen
|
||||||
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)
|
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 <= 25, rows_left
|
||||||
assert "row 39" in terminal.view.text, "newest rows must survive"
|
assert "row 39" in terminal.view.text, "newest rows must survive"
|
||||||
assert "row 0" not in terminal.view.text, "oldest rows must be dropped"
|
assert "row 0" not in terminal.view.text, "oldest rows must be dropped"
|
||||||
|
|
||||||
|
# a cap below the screen height (0: no scrollback) must never cut the screen
|
||||||
|
terminal, stream = make_terminal(rows=10, cols=20, scrollback=0)
|
||||||
|
command = make_command(terminal)
|
||||||
|
for i in range(3):
|
||||||
|
stream.feed("\r\n".join("row{}".format(i * 10 + j) for j in range(10)) + "\r\n")
|
||||||
|
command.update_lines(None, terminal)
|
||||||
|
command.trim_trailing_spaces(None, terminal)
|
||||||
|
command.trim_history(None, terminal)
|
||||||
|
terminal.screen.dirty.clear()
|
||||||
|
assert terminal.offset >= 0, terminal.offset
|
||||||
|
assert "row29" in terminal.view.text, terminal.view.text
|
||||||
|
|
||||||
# below the cap nothing is dropped
|
# below the cap nothing is dropped
|
||||||
terminal, stream = make_terminal(rows=5, scrollback=5000)
|
terminal, stream = make_terminal(rows=5, scrollback=5000)
|
||||||
terminal.offset = 3
|
terminal.offset = 3
|
||||||
|
|||||||
@@ -33,12 +33,12 @@
|
|||||||
|
|
||||||
// External terminal for "vterm: Open Terminal in Project Folder".
|
// External terminal for "vterm: Open Terminal in Project Folder".
|
||||||
// %CWD% in parameters is replaced with the project folder.
|
// %CWD% in parameters is replaced with the project folder.
|
||||||
// "terminal": "/usr/bin/open",
|
"terminal": "/usr/bin/open",
|
||||||
// "parameters": ["-a", "Terminal", "%CWD%"],
|
"parameters": ["-a", "Terminal", "%CWD%"],
|
||||||
|
|
||||||
// kitty instead of Apple Terminal:
|
// kitty instead of Apple Terminal:
|
||||||
"terminal": "/opt/homebrew/bin/kitty",
|
// "terminal": "/opt/homebrew/bin/kitty",
|
||||||
"parameters": ["-d", "%CWD%"],
|
// "parameters": ["-d", "%CWD%"],
|
||||||
|
|
||||||
"debug": false
|
"debug": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ LOG = logging.getLogger(__name__)
|
|||||||
SCROLL_KEYS = ("pageup", "pagedown", "up", "down")
|
SCROLL_KEYS = ("pageup", "pagedown", "up", "down")
|
||||||
|
|
||||||
|
|
||||||
|
def editor_view(window):
|
||||||
|
"""A view to sample the editor's colours from: never a terminal, whose
|
||||||
|
colours are its own theme's."""
|
||||||
|
return next((v for v in window.views() if not v.settings().get("vterm_view")), None)
|
||||||
|
|
||||||
|
|
||||||
class VtermOpenCommand(sublime_plugin.WindowCommand):
|
class VtermOpenCommand(sublime_plugin.WindowCommand):
|
||||||
"""Open a terminal in a new tab. `vterm_open`, the only entry point users bind."""
|
"""Open a terminal in a new tab. `vterm_open`, the only entry point users bind."""
|
||||||
|
|
||||||
@@ -43,7 +49,7 @@ 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 = get_scheme_manager(settings().get("theme"), host_view=window.active_view())
|
scheme = get_scheme_manager(settings().get("theme"), host_view=editor_view(window))
|
||||||
|
|
||||||
view = window.new_file()
|
view = window.new_file()
|
||||||
view.run_command("vterm_initialize_view", {"color_scheme": scheme.path})
|
view.run_command("vterm_initialize_view", {"color_scheme": scheme.path})
|
||||||
@@ -258,7 +264,7 @@ class VtermSelectThemeCommand(sublime_plugin.WindowCommand):
|
|||||||
|
|
||||||
def _apply(self, name):
|
def _apply(self, name):
|
||||||
"""Retheme the focused terminal alone."""
|
"""Retheme the focused terminal alone."""
|
||||||
scheme = get_scheme_manager(name, host_view=self.terminal.view)
|
scheme = get_scheme_manager(name, host_view=editor_view(self.window))
|
||||||
self.terminal.retheme(scheme)
|
self.terminal.retheme(scheme)
|
||||||
|
|
||||||
def _preview(self, index):
|
def _preview(self, index):
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ def quantize_256(r, g, b):
|
|||||||
if r == g == b:
|
if r == g == b:
|
||||||
if r < 8:
|
if r < 8:
|
||||||
return 16
|
return 16
|
||||||
if r > 248:
|
# 238 is the top of the grey ramp (255); above the midpoint, white
|
||||||
|
if r > 246:
|
||||||
return 231
|
return 231
|
||||||
return 232 + (r - 8) // 10
|
return 232 + (r - 8) // 10
|
||||||
|
|
||||||
@@ -432,7 +433,8 @@ class VtScreen(pyte.Screen):
|
|||||||
self.buffer[pos[0]][pos[1]] = last._replace(data=normalized)
|
self.buffer[pos[0]][pos[1]] = last._replace(data=normalized)
|
||||||
self.dirty.add(pos[0])
|
self.dirty.add(pos[0])
|
||||||
else:
|
else:
|
||||||
break
|
# other zero-width (VS16, ZWJ) or control: drop it, keep the rest
|
||||||
|
continue
|
||||||
|
|
||||||
if char_width > 0:
|
if char_width > 0:
|
||||||
x = cursor.x + char_width
|
x = cursor.x + char_width
|
||||||
|
|||||||
@@ -470,7 +470,8 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
view = self.view
|
view = self.view
|
||||||
screen = terminal.screen
|
screen = terminal.screen
|
||||||
lastrow = view.rowcol(view.size())[0]
|
lastrow = view.rowcol(view.size())[0]
|
||||||
n = terminal.scrollback_size
|
# the setting counts rows above the screen; 0 keeps the screen alone
|
||||||
|
n = terminal.scrollback_size + screen.lines
|
||||||
if lastrow + 1 > n:
|
if lastrow + 1 > n:
|
||||||
# queued rows are about to be renumbered or erased: merge them first
|
# queued rows are about to be renumbered or erased: merge them first
|
||||||
self.flush_chunk()
|
self.flush_chunk()
|
||||||
|
|||||||
Reference in New Issue
Block a user