small refactoring
This commit is contained in:
@@ -338,8 +338,9 @@ handle): `auto_close`, `scrollback_history_size`, `honor_erase_scrollback`,
|
|||||||
`parameters`, `debug`. `page_scroll_modifier` is the modifier that turns PgUp/PgDn into a
|
`parameters`, `debug`. `page_scroll_modifier` is the modifier that turns PgUp/PgDn into a
|
||||||
page scroll of the view instead of a key for the app. `terminal` and
|
page scroll of the view instead of a key for the app. `terminal` and
|
||||||
`parameters` name the external terminal spawned by *vterm: Open Terminal in Project
|
`parameters` name the external terminal spawned by *vterm: Open Terminal in Project
|
||||||
Folder* (`%CWD%` in `parameters` becomes the project folder). `honor_erase_scrollback` is off, so ED 3
|
Folder* (`%CWD%` in `parameters` becomes the project folder). ED 3 never touches the visible screen (as in
|
||||||
blanks the screen without dropping the scrollback: pi and Claude Code repaint by erasing
|
xterm), and `honor_erase_scrollback` is off, so it does not drop the scrollback either:
|
||||||
|
pi and Claude Code repaint by erasing
|
||||||
saved lines, which would take the history with them (`cmd+shift+k` still empties the tab). Changing `theme` fires `SchemeManager.reload`, which rebuilds the
|
saved lines, which would take the history with them (`cmd+shift+k` still empties the tab). Changing `theme` fires `SchemeManager.reload`, which rebuilds the
|
||||||
palette and restyles open terminals in place.
|
palette and restyles open terminals in place.
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,24 @@ def run_resize_cases():
|
|||||||
feed(stream, "\x1b[?1049l", False)
|
feed(stream, "\x1b[?1049l", False)
|
||||||
assert max(screen.buffer[0]) < 8, sorted(screen.buffer[0])
|
assert max(screen.buffer[0]) < 8, sorted(screen.buffer[0])
|
||||||
|
|
||||||
|
# a shrink clamps the cursor (pyte does it via restore_cursor); without
|
||||||
|
# the clamp draw() writes to rows above or below the screen
|
||||||
|
proc, screen, stream = make(10, 8)
|
||||||
|
feed(stream, "\r\n".join("line{}".format(i) for i in range(8)), False)
|
||||||
|
feed(stream, "\x1b[1;1H", False) # full screen, cursor at home
|
||||||
|
screen.resize(4, 10)
|
||||||
|
assert screen.cursor.y == 0, screen.cursor.y
|
||||||
|
|
||||||
|
proc, screen, stream = make(10, 8)
|
||||||
|
feed(stream, "top\x1b[7;1H", False) # cursor on a blank row below content
|
||||||
|
screen.resize(4, 10)
|
||||||
|
assert screen.cursor.y == 3, screen.cursor.y
|
||||||
|
|
||||||
|
proc, screen, stream = make(20, 4)
|
||||||
|
feed(stream, "x" * 15, False)
|
||||||
|
screen.resize(4, 10)
|
||||||
|
assert screen.cursor.x == 10, screen.cursor.x
|
||||||
|
|
||||||
# mode 2031 is how a live theme change reaches the app
|
# mode 2031 is how a live theme change reaches the app
|
||||||
proc, screen, stream = make()
|
proc, screen, stream = make()
|
||||||
assert not screen.theme_notify_enabled()
|
assert not screen.theme_notify_enabled()
|
||||||
@@ -381,8 +399,7 @@ def run_clear_cases():
|
|||||||
assert len(screen.history) > 0, "history suppressed for too long"
|
assert len(screen.history) > 0, "history suppressed for too long"
|
||||||
|
|
||||||
# ED 3 is app-driven: it clears scrollback and fires the clear callback,
|
# ED 3 is app-driven: it clears scrollback and fires the clear callback,
|
||||||
# and (as in pyte/Terminus) also blanks the screen. Both agents pair it
|
# but (as in xterm, unlike pyte) leaves the visible screen alone
|
||||||
# with 2J and repaint, so the render side must not wipe or nudge again.
|
|
||||||
cleared = []
|
cleared = []
|
||||||
proc = FakeProcess()
|
proc = FakeProcess()
|
||||||
screen = VtScreen(10, 4, process=proc, clear_callback=lambda: cleared.append(1))
|
screen = VtScreen(10, 4, process=proc, clear_callback=lambda: cleared.append(1))
|
||||||
@@ -391,9 +408,10 @@ def run_clear_cases():
|
|||||||
assert len(screen.history) > 0
|
assert len(screen.history) > 0
|
||||||
feed(stream, "\x1b[3J", False)
|
feed(stream, "\x1b[3J", False)
|
||||||
assert len(screen.history) == 0 and cleared, (len(screen.history), cleared)
|
assert len(screen.history) == 0 and cleared, (len(screen.history), cleared)
|
||||||
|
assert screen.buffer[0][0].data == "a", screen.buffer[0][0]
|
||||||
|
|
||||||
# with erase_scrollback off, ED 3 still blanks the screen but the history
|
# with erase_scrollback off, ED 3 is a no-op: agents repaint with it and
|
||||||
# and the view survive: agents repaint with it and would take both
|
# would take the history and the view with them
|
||||||
cleared = []
|
cleared = []
|
||||||
proc = FakeProcess()
|
proc = FakeProcess()
|
||||||
screen = VtScreen(10, 4, process=proc, erase_scrollback=False,
|
screen = VtScreen(10, 4, process=proc, erase_scrollback=False,
|
||||||
@@ -405,7 +423,7 @@ def run_clear_cases():
|
|||||||
del cleared[:] # the screen's own reset() fired one at construction
|
del cleared[:] # the screen's own reset() fired one at construction
|
||||||
feed(stream, "\x1b[3J", False)
|
feed(stream, "\x1b[3J", False)
|
||||||
assert len(screen.history) == kept and not cleared, (len(screen.history), cleared)
|
assert len(screen.history) == kept and not cleared, (len(screen.history), cleared)
|
||||||
assert screen.buffer[0][0].data == " ", screen.buffer[0][0]
|
assert screen.buffer[0][0].data == "a", screen.buffer[0][0]
|
||||||
|
|
||||||
print("all clear-screen tests passed")
|
print("all clear-screen tests passed")
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,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)) if f.startswith("pending_")}
|
||||||
assert flags == {"pending_clear", "pending_clear_screen"}, 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:
|
||||||
# (?<!_) skips the private _pending_to_send_string write-queue flag
|
# (?<!_) skips the private _pending_to_send_string write-queue flag
|
||||||
|
|||||||
@@ -595,10 +595,21 @@ line[2] = Char("x", "blue", "default")
|
|||||||
runs = [r for r in segment_buffer_line(line) if r[1] != r[2]]
|
runs = [r for r in segment_buffer_line(line) if r[1] != r[2]]
|
||||||
assert runs == [("漢", 0, 1, "red", "default", ""), ("x", 1, 2, "blue", "default", "")], runs
|
assert runs == [("漢", 0, 1, "red", "default", ""), ("x", 1, 2, "blue", "default", "")], runs
|
||||||
|
|
||||||
|
# an orphaned wide-char filler (its wide half was overwritten with a narrow
|
||||||
|
# char) still occupies a column: rendered as a space so later spans stay aligned
|
||||||
|
line = StaticDefaultDict(Char(" "))
|
||||||
|
line[0] = Char("a", "default", "default")
|
||||||
|
line[1] = Char("", "red", "default")
|
||||||
|
line[2] = Char("x", "blue", "default")
|
||||||
|
runs = [r for r in segment_buffer_line(line) if r[1] != r[2]]
|
||||||
|
assert runs == [("a", 0, 1, "default", "default", ""),
|
||||||
|
(" ", 1, 2, "red", "default", ""),
|
||||||
|
("x", 2, 3, "blue", "default", "")], runs
|
||||||
|
|
||||||
# reverse video swaps the pair, and default becomes reverse_default
|
# reverse video swaps the pair, and default becomes reverse_default
|
||||||
line = StaticDefaultDict(Char(" "))
|
line = StaticDefaultDict(Char(" "))
|
||||||
line[0] = Char("r", "red", "default", False, False, False, False, True)
|
line[0] = Char("r", "red", "default", False, False, False, False, True)
|
||||||
runs = [r for r in segment_buffer_line(line) if r[1] != r[2]]
|
runs = [r for r in segment_buffer_line(line) if r[1] != r[2]]
|
||||||
assert runs == [("r", 0, 1, "reverse_default", "red", "")], runs
|
assert runs == [("r", 0, 1, "reverse_default", "red", "")], runs
|
||||||
|
|
||||||
print("segmenter tests passed (leading gap, wide char, reverse)")
|
print("segmenter tests passed (leading gap, wide char, orphan filler, reverse)")
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ BG_AIXTERM = {100 + i: ANSI_COLORS[8 + i] for i in range(8)}
|
|||||||
REPORTABLE_MODES = (mo.DECAWM >> 5, mo.DECTCEM >> 5, 1000, 1002, 1003, 1004, 1006, 1049, 2004, 2026)
|
REPORTABLE_MODES = (mo.DECAWM >> 5, mo.DECTCEM >> 5, 1000, 1002, 1003, 1004, 1006, 1049, 2004, 2026)
|
||||||
|
|
||||||
# DEC private modes, pre-shifted into pyte's internal representation.
|
# DEC private modes, pre-shifted into pyte's internal representation.
|
||||||
|
# 1: DECCKM, application cursor keys (arrows send SS3); pyte has no name for it
|
||||||
|
APP_CURSOR_MODE = 1 << 5
|
||||||
# 2026: synchronized output, the app is mid-frame and we should not draw
|
# 2026: synchronized output, the app is mid-frame and we should not draw
|
||||||
SYNC_MODE = 2026 << 5
|
SYNC_MODE = 2026 << 5
|
||||||
# 1049: alternate screen, a full-screen app owns the viewport
|
# 1049: alternate screen, a full-screen app owns the viewport
|
||||||
@@ -91,7 +93,7 @@ def quantize_256(r, g, b):
|
|||||||
return 16 + 36 * level(r) + 6 * level(g) + level(b)
|
return 16 + 36 * level(r) + 6 * level(g) + level(b)
|
||||||
|
|
||||||
|
|
||||||
# linefeed marks a hard wrap, so copy can re-join soft-wrapped lines
|
# linefeed marks an autowrap (soft wrap), so copy can re-join the halves
|
||||||
Char = namedtuple(
|
Char = namedtuple(
|
||||||
"Char",
|
"Char",
|
||||||
["data", "fg", "bg", "bold", "italics", "underscore", "strikethrough", "reverse", "linefeed"],
|
["data", "fg", "bg", "bold", "italics", "underscore", "strikethrough", "reverse", "linefeed"],
|
||||||
@@ -227,6 +229,10 @@ class VtScreen(pyte.Screen):
|
|||||||
line.pop(x, None)
|
line.pop(x, None)
|
||||||
|
|
||||||
self.lines, self.columns = lines, columns
|
self.lines, self.columns = lines, columns
|
||||||
|
# pyte clamps via restore_cursor; without it a shrink leaves the cursor
|
||||||
|
# negative or past the new edge. x == columns keeps a pending wrap
|
||||||
|
self.cursor.x = min(self.cursor.x, columns)
|
||||||
|
self.cursor.y = min(max(self.cursor.y, 0), lines - 1)
|
||||||
self.set_margins()
|
self.set_margins()
|
||||||
self.tabstops = set(range(8, self.columns, 8))
|
self.tabstops = set(range(8, self.columns, 8))
|
||||||
|
|
||||||
@@ -364,21 +370,14 @@ class VtScreen(pyte.Screen):
|
|||||||
elif char_width > 0:
|
elif char_width > 0:
|
||||||
cursor.x -= char_width
|
cursor.x -= char_width
|
||||||
|
|
||||||
if char_width == 1:
|
if char_width == 1 or char_width == 2:
|
||||||
if insert_mode:
|
if insert_mode:
|
||||||
self.insert_characters(char_width)
|
self.insert_characters(char_width)
|
||||||
cell = cache.get(char)
|
cell = cache.get(char)
|
||||||
if cell is None:
|
if cell is None:
|
||||||
cell = cache[char] = cursor.attrs._replace(data=char)
|
cell = cache[char] = cursor.attrs._replace(data=char)
|
||||||
line[cursor.x] = cell
|
line[cursor.x] = cell
|
||||||
elif char_width == 2:
|
if char_width == 2 and cursor.x + 1 < columns:
|
||||||
if insert_mode:
|
|
||||||
self.insert_characters(char_width)
|
|
||||||
cell = cache.get(char)
|
|
||||||
if cell is None:
|
|
||||||
cell = cache[char] = cursor.attrs._replace(data=char)
|
|
||||||
line[cursor.x] = cell
|
|
||||||
if cursor.x + 1 < columns:
|
|
||||||
filler = cache.get("")
|
filler = cache.get("")
|
||||||
if filler is None:
|
if filler is None:
|
||||||
filler = cache[""] = cursor.attrs._replace(data="")
|
filler = cache[""] = cursor.attrs._replace(data="")
|
||||||
@@ -430,11 +429,18 @@ class VtScreen(pyte.Screen):
|
|||||||
if not self.alternate_buffer_mode and (how == 2 or (how == 0 and self.cursor.x == 0 and self.cursor.y == 0)):
|
if not self.alternate_buffer_mode and (how == 2 or (how == 0 and self.cursor.x == 0 and self.cursor.y == 0)):
|
||||||
self.push_lines_into_history()
|
self.push_lines_into_history()
|
||||||
|
|
||||||
|
if how == 3:
|
||||||
|
# xterm's ED 3 erases the scrollback only; the screen stays
|
||||||
|
if self._erase_scrollback:
|
||||||
|
self.history.clear()
|
||||||
|
self._clear_callback()
|
||||||
|
return
|
||||||
|
|
||||||
if how == 0:
|
if how == 0:
|
||||||
interval = range(self.cursor.y + 1, self.lines)
|
interval = range(self.cursor.y + 1, self.lines)
|
||||||
elif how == 1:
|
elif how == 1:
|
||||||
interval = range(self.cursor.y)
|
interval = range(self.cursor.y)
|
||||||
elif how == 2 or how == 3:
|
elif how == 2:
|
||||||
interval = range(self.lines)
|
interval = range(self.lines)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
@@ -451,10 +457,6 @@ class VtScreen(pyte.Screen):
|
|||||||
if how == 0 or how == 1:
|
if how == 0 or how == 1:
|
||||||
self.erase_in_line(how)
|
self.erase_in_line(how)
|
||||||
|
|
||||||
if how == 3 and self._erase_scrollback:
|
|
||||||
self.history.clear()
|
|
||||||
self._clear_callback()
|
|
||||||
|
|
||||||
def select_graphic_rendition(self, *attrs, **kwargs):
|
def select_graphic_rendition(self, *attrs, **kwargs):
|
||||||
"""Apply an SGR sequence to the cursor attributes.
|
"""Apply an SGR sequence to the cursor attributes.
|
||||||
|
|
||||||
@@ -490,9 +492,9 @@ class VtScreen(pyte.Screen):
|
|||||||
if attr[1:] != "blink":
|
if attr[1:] != "blink":
|
||||||
replace[attr[1:]] = attr.startswith("+")
|
replace[attr[1:]] = attr.startswith("+")
|
||||||
elif attr in FG_AIXTERM:
|
elif attr in FG_AIXTERM:
|
||||||
replace.update(fg=FG_AIXTERM[attr])
|
replace["fg"] = FG_AIXTERM[attr]
|
||||||
elif attr in BG_AIXTERM:
|
elif attr in BG_AIXTERM:
|
||||||
replace.update(bg=BG_AIXTERM[attr])
|
replace["bg"] = BG_AIXTERM[attr]
|
||||||
elif attr in (g.FG_256, g.BG_256):
|
elif attr in (g.FG_256, g.BG_256):
|
||||||
key = "fg" if attr == g.FG_256 else "bg"
|
key = "fg" if attr == g.FG_256 else "bg"
|
||||||
try:
|
try:
|
||||||
@@ -592,7 +594,11 @@ class VtScreen(pyte.Screen):
|
|||||||
self.write_process_input("\x1b[{}{};{}R".format(marker, y, x))
|
self.write_process_input("\x1b[{}{};{}R".format(marker, y, x))
|
||||||
elif mode == 996 and private:
|
elif mode == 996 and private:
|
||||||
# color-scheme report, pi asks with a 2s timeout
|
# color-scheme report, pi asks with a 2s timeout
|
||||||
self.write_process_input("\x1b[?997;{}n".format(1 if self.theme_is_dark else 2))
|
self.write_process_input(self.theme_report())
|
||||||
|
|
||||||
|
def theme_report(self):
|
||||||
|
"""DSR 997 report: the palette is dark (1) or light (2). Also sent on a live theme change."""
|
||||||
|
return "\x1b[?997;{}n".format(1 if self.theme_is_dark else 2)
|
||||||
|
|
||||||
def report_xtversion(self):
|
def report_xtversion(self):
|
||||||
"""XTVERSION: naming ourselves is what unlocks ?2026 for apps on a generic TERM."""
|
"""XTVERSION: naming ourselves is what unlocks ?2026 for apps on a generic TERM."""
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
|
|||||||
terminal.focused = True
|
terminal.focused = True
|
||||||
terminal.send_focus(True)
|
terminal.send_focus(True)
|
||||||
view.run_command("vterm_clear_undo_stack")
|
view.run_command("vterm_clear_undo_stack")
|
||||||
# catch up on the frames, title and caret skipped while in the background
|
# catch up on the frames, title and caret skipped while in the
|
||||||
view.run_command("vterm_render")
|
# background — via the renderer tick: a render here would race the
|
||||||
|
# renderer thread, which owns the emulator
|
||||||
|
terminal.pending_render = True
|
||||||
view.run_command("vterm_show_cursor", {"scroll": False})
|
view.run_command("vterm_show_cursor", {"scroll": False})
|
||||||
elif view.settings().get("vterm_view") and not view.settings().get("vterm_view.finished"):
|
elif view.settings().get("vterm_view") and not view.settings().get("vterm_view.finished"):
|
||||||
# the plugin host restarted; this tab's terminal is unreachable
|
# the plugin host restarted; this tab's terminal is unreachable
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class VtermMouseEventListener(sublime_plugin.EventListener):
|
|||||||
if not terminal.screen.mouse_reporting_enabled():
|
if not terminal.screen.mouse_reporting_enabled():
|
||||||
# leave selection, caret and scrolling to sublime
|
# leave selection, caret and scrolling to sublime
|
||||||
return
|
return
|
||||||
if name == "drag_select" and args and "event" in args and args.get("event", {}).get("button") == 1 and len(args) == 1:
|
if name == "drag_select" and args and args.get("event", {}).get("button") == 1 and len(args) == 1:
|
||||||
return ("vterm_mouse_click", {"event": args["event"]})
|
return ("vterm_mouse_click", {"event": args["event"]})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ STYLE_FLAGS = (("bold", "b"), ("italics", "i"), ("underscore", "u"))
|
|||||||
ATTRS = slice(1, 8)
|
ATTRS = slice(1, 8)
|
||||||
|
|
||||||
|
|
||||||
|
def is_default_run(fg, bg, style):
|
||||||
|
"""True for a run that needs no colour overlay."""
|
||||||
|
return fg == "default" and bg == "default" and not style
|
||||||
|
|
||||||
|
|
||||||
def segment_buffer_line(buffer_line):
|
def segment_buffer_line(buffer_line):
|
||||||
"""Yield (text, start, end, fg, bg, style) runs of a screen row.
|
"""Yield (text, start, end, fg, bg, style) runs of a screen row.
|
||||||
|
|
||||||
@@ -82,7 +87,9 @@ def segment_buffer_line(buffer_line):
|
|||||||
start = column
|
start = column
|
||||||
attrs = char[ATTRS]
|
attrs = char[ATTRS]
|
||||||
|
|
||||||
run.append(char.data)
|
# an orphaned wide-char filler (its first half was overwritten) still
|
||||||
|
# occupies a column; render a space so later spans stay aligned
|
||||||
|
run.append(char.data or " ")
|
||||||
column += 1
|
column += 1
|
||||||
|
|
||||||
if attrs is not None:
|
if attrs is not None:
|
||||||
@@ -283,8 +290,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
|
|
||||||
for line in range(popped):
|
for line in range(popped):
|
||||||
buffer_line = history.pop()
|
buffer_line = history.pop()
|
||||||
lf = buffer_line[columns - 1].linefeed
|
self.update_line(edit, offset - line - 1, buffer_line, columns)
|
||||||
self.update_line(edit, offset - line - 1, buffer_line, lf)
|
|
||||||
|
|
||||||
# after the loop: a pop edits an earlier point than the one before
|
# after the loop: a pop edits an earlier point than the one before
|
||||||
# it, so spans measured mid-loop would be stale
|
# it, so spans measured mid-loop would be stale
|
||||||
@@ -292,17 +298,18 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
|
|
||||||
for line in dirty_lines:
|
for line in dirty_lines:
|
||||||
buffer_line = screen.buffer[line]
|
buffer_line = screen.buffer[line]
|
||||||
lf = buffer_line[columns - 1].linefeed
|
self.update_line(edit, line + offset, buffer_line, columns)
|
||||||
self.update_line(edit, line + offset, buffer_line, lf)
|
|
||||||
|
|
||||||
def update_line(self, edit, line, buffer_line, lf):
|
def update_line(self, edit, line, buffer_line, columns):
|
||||||
"""Rewrite one view row from a screen row, unless it is unchanged.
|
"""Rewrite one view row from a screen row, unless it is unchanged.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
line: absolute view row.
|
line: absolute view row.
|
||||||
lf: the row ended in a real newline, not a soft wrap.
|
columns: screen width; the last cell's linefeed flag marks a soft
|
||||||
|
wrap, which gets the CONTINUATION sentinel.
|
||||||
"""
|
"""
|
||||||
view = self.view
|
view = self.view
|
||||||
|
lf = buffer_line[columns - 1].linefeed
|
||||||
segments = list(segment_buffer_line(buffer_line))
|
segments = list(segment_buffer_line(buffer_line))
|
||||||
# pyte's dirty set is a repaint hint, not a diff: most dirty rows come
|
# pyte's dirty set is a repaint hint, not a diff: most dirty rows come
|
||||||
# back identical, and rewriting one costs an edit plus its region churn
|
# back identical, and rewriting one costs an edit plus its region churn
|
||||||
@@ -314,7 +321,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
by_scope = {}
|
by_scope = {}
|
||||||
needed = 0
|
needed = 0
|
||||||
for _, start, end, fg, bg, style in segments:
|
for _, start, end, fg, bg, style in segments:
|
||||||
if fg == "default" and bg == "default" and not 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(scheme_manager.scope(fg, bg, style), []).append((start, end))
|
||||||
@@ -392,7 +399,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
|
|||||||
segments = self._row_cache[row][0]
|
segments = self._row_cache[row][0]
|
||||||
start = view.line(view.text_point(row, 0)).begin()
|
start = view.line(view.text_point(row, 0)).begin()
|
||||||
for _, a, b, fg, bg, style in segments:
|
for _, a, b, fg, bg, style in segments:
|
||||||
if fg == "default" and bg == "default" and not style:
|
if is_default_run(fg, bg, style):
|
||||||
continue
|
continue
|
||||||
scope = scheme_manager.scope(fg, bg, style)
|
scope = scheme_manager.scope(fg, bg, style)
|
||||||
grouped.setdefault(scope, []).append(sublime.Region(start + a, start + b))
|
grouped.setdefault(scope, []).append(sublime.Region(start + a, start + b))
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ from queue import Empty, Queue
|
|||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
from ptyprocess import PtyProcess
|
from ptyprocess import PtyProcess
|
||||||
|
from pyte import modes as mo
|
||||||
|
|
||||||
from .emulator import 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 .scheme import scheme_manager
|
||||||
from .utils import intermission, responsive, settings
|
from .utils import intermission, responsive, settings
|
||||||
@@ -93,7 +94,7 @@ class Terminal:
|
|||||||
self.default_title = ""
|
self.default_title = ""
|
||||||
self.offset = 0
|
self.offset = 0
|
||||||
self.viewport_y = 0
|
self.viewport_y = 0
|
||||||
self.scrollback_size = 5000
|
self.scrollback_size = 50000
|
||||||
self._hosted = True
|
self._hosted = True
|
||||||
self._hosted_checked = 0
|
self._hosted_checked = 0
|
||||||
self.focused = True
|
self.focused = True
|
||||||
@@ -104,10 +105,15 @@ class Terminal:
|
|||||||
self.applied_alt_screen = False
|
self.applied_alt_screen = False
|
||||||
self._strings = Queue()
|
self._strings = Queue()
|
||||||
self._pending_to_send_string = False
|
self._pending_to_send_string = False
|
||||||
|
# guards the queue/flag handoff between send_string and the drain thread
|
||||||
|
self._send_lock = threading.Lock()
|
||||||
self._wheel_captured = False
|
self._wheel_captured = False
|
||||||
self._scroll_pending = False
|
self._scroll_pending = False
|
||||||
self.pending_clear = False
|
self.pending_clear = False
|
||||||
self.pending_clear_screen = False
|
self.pending_clear_screen = False
|
||||||
|
# a forced frame (set on activation); rendering stays on the renderer
|
||||||
|
# thread, which owns the emulator
|
||||||
|
self.pending_render = False
|
||||||
# a UTF-8 sequence split across read chunks must not be dropped
|
# a UTF-8 sequence split across read chunks must not be dropped
|
||||||
self._decoder = codecs.getincrementaldecoder("utf-8")("replace")
|
self._decoder = codecs.getincrementaldecoder("utf-8")("replace")
|
||||||
|
|
||||||
@@ -192,7 +198,7 @@ class Terminal:
|
|||||||
screen.default_bg_color = scheme_manager.background.lstrip("#")
|
screen.default_bg_color = scheme_manager.background.lstrip("#")
|
||||||
screen.theme_is_dark = scheme_manager.is_dark()
|
screen.theme_is_dark = scheme_manager.is_dark()
|
||||||
if screen.theme_notify_enabled():
|
if screen.theme_notify_enabled():
|
||||||
self.send_string("\x1b[?997;{}n".format(1 if screen.theme_is_dark else 2), normalized=False)
|
self.send_string(screen.theme_report(), normalized=False)
|
||||||
|
|
||||||
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."""
|
||||||
@@ -218,8 +224,9 @@ class Terminal:
|
|||||||
def _need_to_render(self):
|
def _need_to_render(self):
|
||||||
"""Whether anything changed since the last frame: dirty rows, a clear, or the cursor."""
|
"""Whether anything changed since the last frame: dirty rows, a clear, or the cursor."""
|
||||||
screen = self.screen
|
screen = self.screen
|
||||||
if self.pending_clear or self.pending_clear_screen:
|
if self.pending_render or self.pending_clear or self.pending_clear_screen:
|
||||||
# a pending clear must render even when the app is idle
|
# a pending clear or forced frame must render even when the app is idle
|
||||||
|
self.pending_render = False
|
||||||
return True
|
return True
|
||||||
cursor = (screen.cursor.x, screen.cursor.y, screen.cursor_hidden(), screen.cursor_style)
|
cursor = (screen.cursor.x, screen.cursor.y, screen.cursor_hidden(), screen.cursor_style)
|
||||||
if screen.dirty or cursor != self._cached_cursor:
|
if screen.dirty or cursor != self._cached_cursor:
|
||||||
@@ -329,8 +336,8 @@ class Terminal:
|
|||||||
|
|
||||||
def send_key(self, **kwargs):
|
def send_key(self, **kwargs):
|
||||||
"""Encode a key name plus modifiers for the app's current modes and send it."""
|
"""Encode a key name plus modifiers for the app's current modes and send it."""
|
||||||
kwargs["application_mode"] = (1 << 5) in self.screen.mode
|
kwargs["application_mode"] = APP_CURSOR_MODE in self.screen.mode
|
||||||
kwargs["new_line_mode"] = 20 in self.screen.mode
|
kwargs["new_line_mode"] = mo.LNM in self.screen.mode
|
||||||
self.send_string(get_key_code(**kwargs), normalized=False)
|
self.send_string(get_key_code(**kwargs), normalized=False)
|
||||||
|
|
||||||
def send_string(self, string, normalized=True):
|
def send_string(self, string, normalized=True):
|
||||||
@@ -342,22 +349,24 @@ class Terminal:
|
|||||||
"""
|
"""
|
||||||
if normalized:
|
if normalized:
|
||||||
string = string.replace("\r\n", "\n")
|
string = string.replace("\r\n", "\n")
|
||||||
if 20 in self.screen.mode:
|
if mo.LNM in self.screen.mode:
|
||||||
string = string.replace("\n", "\r\n")
|
string = string.replace("\n", "\r\n")
|
||||||
else:
|
else:
|
||||||
string = string.replace("\n", "\r")
|
string = string.replace("\n", "\r")
|
||||||
|
|
||||||
# chunked so a big paste cannot overrun the pty buffer; the blocking
|
# chunked so a big paste cannot overrun the pty buffer; the blocking
|
||||||
# pty write is the backpressure, no pacing sleep needed
|
# pty write is the backpressure, no pacing sleep needed
|
||||||
|
with self._send_lock:
|
||||||
no_queue = not self._pending_to_send_string
|
no_queue = not self._pending_to_send_string
|
||||||
if no_queue and len(string) <= 4096:
|
if not (no_queue and len(string) <= 4096):
|
||||||
self.process.write(string)
|
|
||||||
else:
|
|
||||||
for i in range(0, len(string), 4096):
|
for i in range(0, len(string), 4096):
|
||||||
self._strings.put(string[i : i + 4096])
|
self._strings.put(string[i : i + 4096])
|
||||||
if no_queue:
|
if no_queue:
|
||||||
self._pending_to_send_string = True
|
self._pending_to_send_string = True
|
||||||
threading.Thread(target=self._process_send_string, daemon=True).start()
|
threading.Thread(target=self._process_send_string, daemon=True).start()
|
||||||
|
return
|
||||||
|
# outside the lock: the pty write can block on backpressure
|
||||||
|
self.process.write(string)
|
||||||
|
|
||||||
def _process_send_string(self):
|
def _process_send_string(self):
|
||||||
"""Drain the queued paste chunks; the blocking pty write paces them."""
|
"""Drain the queued paste chunks; the blocking pty write paces them."""
|
||||||
@@ -365,10 +374,15 @@ class Terminal:
|
|||||||
try:
|
try:
|
||||||
# linger so a burst of sends does not respawn the thread
|
# linger so a burst of sends does not respawn the thread
|
||||||
string = self._strings.get(timeout=0.2)
|
string = self._strings.get(timeout=0.2)
|
||||||
self.process.write(string)
|
|
||||||
except Empty:
|
except Empty:
|
||||||
|
# under the lock: a chunk enqueued after the timeout but before
|
||||||
|
# the flag drops would otherwise strand until the next send
|
||||||
|
with self._send_lock:
|
||||||
|
if self._strings.empty():
|
||||||
self._pending_to_send_string = False
|
self._pending_to_send_string = False
|
||||||
return
|
return
|
||||||
|
continue
|
||||||
|
self.process.write(string)
|
||||||
|
|
||||||
def _sync_wheel_flag(self):
|
def _sync_wheel_flag(self):
|
||||||
"""Publish whether the app owns the wheel, for the mousemap context."""
|
"""Publish whether the app owns the wheel, for the mousemap context."""
|
||||||
|
|||||||
Reference in New Issue
Block a user