remove touchpad related code

This commit is contained in:
Vaclav Tvrdik
2026-09-10 00:02:55 +02:00
parent 00bab25d5d
commit 1e31990e71
7 changed files with 49 additions and 301 deletions

View File

@@ -34,7 +34,7 @@ Constraints it works under:
| `vterm/themes.py` | The built-in palettes (`default`, `homebrew`, `pro`, `slate`). | | `vterm/themes.py` | The built-in palettes (`default`, `homebrew`, `pro`, `slate`). |
| `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. |
| `vterm/mouse.py` | Wheel and click handling, and the trackpad poller. | | `vterm/mouse.py` | Wheel and click handling. |
| `vterm/keys.py` | Key name + modifiers → escape sequence tables. | | `vterm/keys.py` | Key name + modifiers → escape sequence tables. |
| `vterm/utils.py` | `settings()`, the `responsive` throttle, the `intermission` pacer, `rev_wcwidth`. | | `vterm/utils.py` | `settings()`, the `responsive` throttle, the `intermission` pacer, `rev_wcwidth`. |
@@ -133,8 +133,7 @@ child writes
│ _need_to_render()? dirty rows, a pending clear, or a │ _need_to_render()? dirty rows, a pending clear, or a
│ moved cursor │ moved cursor
│ view.run_command("vterm_render") │ view.run_command("vterm_render")
│ _sync_wheel_flag() publishes the mousemap gate, and starts the │ _sync_wheel_flag() publishes the mousemap gate
│ trackpad poller while the app owns the wheel
│ │
└─ UI thread ────────────────────────────────────────────────── └─ UI thread ──────────────────────────────────────────────────
VtermRenderCommand.run(edit) VtermRenderCommand.run(edit)
@@ -183,9 +182,6 @@ view row of screen row 0. When the emulator retires lines into `screen.history`,
renderer drains them, advances `offset` by that many rows, and writes them at their now renderer drains them, advances `offset` by that many rows, and writes them at their now
permanent view rows. **Once a line leaves `screen.history`, its only copy is the view permanent view rows. **Once a line leaves `screen.history`, its only copy is the view
text** — which is why the alternate screen *folds* the scrollback rather than deleting it. text** — which is why the alternate screen *folds* the scrollback rather than deleting it.
A fresh view is given one blank scrollback row before anything else: Sublime lays out
nothing above row 0, and the trackpad poller needs that slack to see an upward gesture at
all — on the alt screen no line ever retires to provide it.
**The row cache.** pyte's `dirty` set is a repaint *hint*, not a diff: scrolling one line **The row cache.** pyte's `dirty` set is a repaint *hint*, not a diff: scrolling one line
marks every row dirty, and a TUI repaints whole frames. `update_line` compares marks every row dirty, and a TUI repaints whole frames. `update_line` compares
@@ -200,13 +196,10 @@ the same way, and scrolling down clamps to the anchor `bottom_y` returns, which
it. A one-row scroll lands exactly on the tolerance boundary, so one Up closes the gate it. A one-row scroll lands exactly on the tolerance boundary, so one Up closes the gate
and one Down re-opens it. and one Down re-opens it.
**Pinning waits for rest.** Two pollers put the viewport back on the anchor: the renderer **Pinning waits for rest.** The renderer pins a viewport dragged into sublime's past-end
tick, for a viewport dragged into sublime's past-end room on a terminal too idle to draw room on a terminal too idle to draw a frame, but only once two ticks agree it has come to
a frame, and the trackpad poller, when a browsing gesture returns to the anchor. Both rest: macOS keeps scrolling after the fingers lift, and a hard `set_viewport_position`
compare the viewport against the position they saw one tick earlier and act only when the against that momentum yanks the view back for as long as the glide lasts.
two agree. macOS keeps scrolling after the fingers lift, and a hard
`set_viewport_position` against that momentum used to yank the view back thirty times a
second for as long as the glide lasted — a visible shake at the bottom of every scroll.
### How colour is actually drawn ### How colour is actually drawn
@@ -253,14 +246,12 @@ Details worth knowing before touching `scheme.py`:
### The alternate screen ### The alternate screen
While an app owns the alternate buffer, `sync_alt_fold` folds rows `0..offset-1` away, so While an app owns the alternate buffer, `sync_alt_fold` folds rows `0..offset-1` away, so
the view shows the app's screen alone. The scroll range is deliberately left in place: the the view shows the app's screen alone. The fold is re-checked every frame, not just on
trackpad poller pins the viewport and needs the slack to read a gesture. The fold is transitions, because the screen's first row is rewritten on every region scroll and an
re-checked every frame, not just on transitions, because the screen's first row is edit at the fold boundary can silently break a fold. Entering the alt screen collapses the
rewritten on every region scroll and an edit at the fold boundary can silently break a layout, so that transition also zeroes `terminal.viewport_y`; a stale anchor would hold
fold. Entering the alt screen collapses the layout, so that transition also zeroes the follow-scroll gate shut. A fold merely repaired keeps the anchor, or the boundary row
`terminal.viewport_y`; a stale anchor would hold the follow-scroll gate shut. A fold apps rewrite on every scroll would reset it every frame. The emulator side
merely repaired keeps the anchor, or the boundary row apps rewrite on every scroll would
thrash the poller. The emulator side
(`switch_to_screen`) swaps in a `maxlen=0` history, so a full-screen app never pushes its (`switch_to_screen`) swaps in a `maxlen=0` history, so a full-screen app never pushes its
frames into scrollback. frames into scrollback.
@@ -314,15 +305,12 @@ which produces three cases:
real scrollback text. real scrollback text.
**The trackpad** fires none of that: on macOS it never reaches a mousemap binding **The trackpad** fires none of that: on macOS it never reaches a mousemap binding
(sublime_text#1838), and its only trace is the viewport moving. So while the wheel flag is (sublime_text#1838) and dispatches no command, so a plugin cannot see it at all. Its only
up, `start_trackpad` runs a 24 ms timer chain, and any displacement from the follow anchor in-API trace is the viewport moving, and reading a gesture that way means letting the view
is a gesture — whole lines of it go out through the same `send_scroll` as the wheel, the move before pinning it back — visibly, at every poll. vterm does not try: the trackpad is
fraction is carried into the next tick, and the viewport is pinned back to the anchor. A converted outside the plugin, by a utility such as LinearMouse set to discrete scrolling
displacement of a viewport or more is a scrollbar drag or a page scroll instead: the poller for Sublime Text, and arrives as ordinary wheel notches on the path above. Nothing in the
disengages and lets the user browse until they return to the anchor. `_sync_wheel_flag` plugin is trackpad-aware, and the viewport is never moved to measure input.
restarts the chain within a frame or two of it stopping on a hidden tab or a capture flap,
and `scroll_to_cursor` yields while the poller owns the pin — apps repaint mid-gesture, and
re-pinning there would discard displacement not yet harvested.
Clicks: `VtermMouseEventListener` intercepts `drag_select` only when the app tracks the Clicks: `VtermMouseEventListener` intercepts `drag_select` only when the app tracks the
mouse, and only the plain single-button form, so shift-click and add-selection stay mouse, and only the plain single-button form, so shift-click and add-selection stay
@@ -345,7 +333,7 @@ that normally does that — and subsequent clicks send a press/release pair.
| Thread | Runs | Touches | | Thread | Runs | Touches |
|---|---|---| |---|---|---|
| UI | commands, listeners, `Terminal.start`, `scroll_to_cursor`, `_trackpad_tick`, clipboard | Sublime API; small pty writes go out here too | | UI | commands, listeners, `Terminal.start`, `scroll_to_cursor`, clipboard | Sublime API; small pty writes go out here too |
| reader | `process.read`, decode, `chunks.append` | no Sublime API, no screen state | | reader | `process.read`, decode, `chunks.append` | no Sublime API, no screen state |
| renderer | `stream.feed`, then `run_command("vterm_render")` | owns the emulator | | renderer | `stream.feed`, then `run_command("vterm_render")` | owns the emulator |
| paste drain | transient; empties the 4096-byte chunk queue | pty only | | paste drain | transient; empties the 4096-byte chunk queue | pty only |

View File

@@ -52,11 +52,9 @@ wheel scrolls the view normally; while the app tracks the mouse the wheel and
left click go to it instead, and on the alternate screen without tracking each left click go to it instead, and on the alternate screen without tracking each
wheel tick sends arrow keys (alternate scroll, as kitty/iTerm2 do — this is wheel tick sends arrow keys (alternate scroll, as kitty/iTerm2 do — this is
what scrolls Claude Code's fullscreen UI before it enables mouse reporting). what scrolls Claude Code's fullscreen UI before it enables mouse reporting).
Sublime raises no wheel event at all for the trackpad (sublime_text#1838), so Sublime raises no wheel event at all for the trackpad (sublime_text#1838): set
while the app owns the wheel a poller reads the viewport movement instead, a utility such as [LinearMouse](https://linearmouse.app/) to discrete scrolling
sends it as scroll lines and pins the view back. A jump of a whole screen or for Sublime Text and it arrives as wheel notches like any other. While an app owns the
more — the scrollbar, the page keys — is left alone, so the scrollback is still
yours to browse until you come back to the bottom. While an app owns the
alternate screen the scrollback above it is folded away, kitty-style, and alternate screen the scrollback above it is folded away, kitty-style, and
unfolds when the app leaves it. unfolds when the app leaves it.
@@ -118,9 +116,9 @@ terminal protocols — see `Architecture.md`.
leave the character behind. `on_modified` in `vterm/listeners.py` remains the leave the character behind. `on_modified` in `vterm/listeners.py` remains the
fallback for IME and composed input. fallback for IME and composed input.
- `vterm/mouse.py` — SGR mouse reporting (wheel, click) when the app enables - `vterm/mouse.py` — SGR mouse reporting (wheel, click) when the app enables
tracking; native selection and scrolling otherwise. The trackpad reaches no tracking; native selection and scrolling otherwise. The trackpad is not
mousemap on macOS, so while the app owns the wheel a 24 ms poller harvests handled: it reaches no mousemap on macOS and is converted to wheel notches
viewport displacement into the same reports. outside the plugin.
Supported: alt screen (1049), synchronized output (2026), bracketed paste (2004), Supported: alt screen (1049), synchronized output (2026), bracketed paste (2004),
mouse tracking 1000/1002/1003 + SGR 1006 (wheel and left click are reported; no mouse tracking 1000/1002/1003 + SGR 1006 (wheel and left click are reported; no
@@ -150,8 +148,12 @@ instead), image protocols, Windows, panels.
input, so close it and open a new one. input, so close it and open a new one.
- CJK and emoji rely on wcwidth agreeing with Sublime's font metrics; where they - CJK and emoji rely on wcwidth agreeing with Sublime's font metrics; where they
disagree the caret can drift. disagree the caret can drift.
- Trackpad scroll into an app is polled and rounded to whole lines, so it is - Trackpad scrolling needs a helper on macOS: Sublime never delivers it to a
coarser than the wheel in a native terminal. plugin (sublime_text#1838), so set a utility such as
[LinearMouse](https://linearmouse.app/) to discrete/linear scrolling for
Sublime Text. It then arrives as wheel notches and scrolls the app. Without
one, the trackpad does nothing in a full-screen app; the mouse wheel,
`shift+pageup/pagedown` and `shift+up/down` work regardless.
## Dev ## Dev

View File

@@ -421,141 +421,6 @@ assert terminal.process.written == "", repr(terminal.process.written)
print("alternate scroll tests passed (3 lines per tick: arrows on alt screen, SGR when tracking, inert otherwise)") print("alternate scroll tests passed (3 lines per tick: arrows on alt screen, SGR when tracking, inert otherwise)")
# the trackpad never fires the mousemap on macOS (#1838): the poller harvests
# viewport displacement from the follow anchor into wheel lines, snaps back.
# ticks are stepped one at a time — the poller reschedules itself, so a
# blanket sublime.run_timeouts() would loop forever
terminal, stream = make_terminal(rows=5, cols=20)
Terminal._terminals[id(terminal)] = terminal
terminal.view.id = lambda: id(terminal)
terminal.view.text = "\n".join("row {}".format(i) for i in range(8))
terminal.offset = 3
ANCHOR = 3 * LINE_HEIGHT
terminal.viewport_y = ANCHOR
terminal.view.viewport = (0, ANCHOR)
stream.feed("\x1b[?1002h\x1b[?1006h")
del sublime._timeouts[:]
terminal._sync_wheel_flag()
assert terminal._trackpad_running
terminal._sync_wheel_flag()
assert len(sublime._timeouts) == 1, "one timer chain per terminal"
def tick():
sublime._timeouts.pop(0)()
def displace(pixels):
terminal.process.written = ""
terminal.view.viewport = (0, ANCHOR + pixels)
tick()
return terminal.process.written
# the first tick only arms the baseline; a gesture on it must stay silent
assert displace(50) == "", repr(terminal.process.written)
assert terminal.view.viewport == (0, ANCHOR + 50), "arming must not snap"
# 2.5 lines down: two wheel-down reports, snap back, the fraction kept
assert displace(50) == "\x1b[<65;1;3M" * 2
assert terminal.view.viewport == (0, ANCHOR), terminal.view.viewport
assert terminal._trackpad_acc == 0.5, terminal._trackpad_acc
# half a line more completes the kept fraction into one report
assert displace(10) == "\x1b[<65;1;3M"
# a resting tick clears the remainder, so it cannot fire a gesture later
assert displace(10) == ""
assert displace(0) == ""
assert terminal._trackpad_acc == 0, terminal._trackpad_acc
# upward displacement reports wheel-up
assert displace(-30) == "\x1b[<64;1;3M"
# a renderer re-anchor (fold reset, follow-scroll) is not a gesture
displace(0)
terminal.viewport_y = 0
terminal.view.viewport = (0, 0)
tick()
assert terminal.process.written == "", repr(terminal.process.written)
terminal.viewport_y = ANCHOR
terminal.view.viewport = (0, ANCHOR)
tick()
# a queued follow-scroll must not starve the poller: apps repaint while
# scrolling (less redraws per line, claude constantly), so this races every
# gesture and used to read as shaking
terminal._scroll_pending = True
assert displace(40) == "\x1b[<65;1;3M" * 2
terminal._scroll_pending = False
# nor may the follow-scroll itself discard un-harvested displacement while
# the anchor is unchanged: the poller owns the pin
terminal.view.viewport = (0, ANCHOR + 30)
scroll_to_cursor(terminal.view, terminal)
assert terminal.view.viewport == (0, ANCHOR + 30), terminal.view.viewport
terminal.process.written = ""
tick()
assert terminal.process.written == "\x1b[<65;1;3M", repr(terminal.process.written)
assert terminal.view.viewport == (0, ANCHOR), terminal.view.viewport
displace(0) # drop the half-line remainder
# a real anchor change still pins: that is the follow-scroll's job
terminal.view.text += "row 8\n"
scroll_to_cursor(terminal.view, terminal)
assert terminal.viewport_y == ANCHOR + LINE_HEIGHT, terminal.viewport_y
assert terminal.view.viewport == (0, ANCHOR + LINE_HEIGHT), terminal.view.viewport
terminal.view.text = terminal.view.text[:-len("row 8\n")]
terminal.viewport_y = ANCHOR
terminal.view.viewport = (0, ANCHOR)
tick() # settle back on the restored anchor
# a jump of a viewport or more (scrollbar, page scroll) disengages: the user
# browses the scrollback natively until they come back to the anchor
extent = terminal.view.viewport_extent()[1]
assert displace(-extent) == ""
assert not terminal._trackpad_engaged
assert terminal.view.viewport == (0, ANCHOR - extent), "browsing must not snap"
assert displace(-30) == "", "still browsing"
terminal.view.viewport = (0, ANCHOR)
tick() # back at the anchor, but still gliding: nothing may pin it yet
assert not terminal._trackpad_engaged
tick() # at rest on the anchor: re-engage
assert terminal._trackpad_engaged
assert displace(20) == "\x1b[<65;1;3M"
# momentum carrying past the anchor must glide unpinned — pinning it mid-glide
# is the shake — and be corrected in one step once it stops, without a report
assert displace(-extent) == ""
assert not terminal._trackpad_engaged
terminal.process.written = ""
terminal.view.viewport = (0, ANCHOR + 40)
tick()
assert terminal.view.viewport == (0, ANCHOR + 40), "gliding must not snap"
assert not terminal._trackpad_engaged
tick()
assert terminal._trackpad_engaged
assert terminal.view.viewport == (0, ANCHOR), terminal.view.viewport
assert terminal.process.written == "", repr(terminal.process.written)
# on the alt screen without tracking the same displacement turns into arrows
stream.feed("\x1b[?1002l\x1b[?1006l\x1b[?1049h")
terminal._sync_wheel_flag()
assert len(sublime._timeouts) == 1, "capture never lapsed, no second chain"
tick() # alt entry left the anchor alone, but spend a tick anyway
assert displace(-20) == "\x1b[A"
# capture released: the poller stops and unhooks itself
stream.feed("\x1b[?1049l")
terminal._sync_wheel_flag()
tick()
assert not terminal._trackpad_running
assert sublime._timeouts == [], sublime._timeouts
print("trackpad tests passed (displacement harvested, fractions kept, jumps browse, momentum settles, poller unhooks)")
# with the configured modifier PgUp/PgDn page the view and Up/Down move it one # with the configured modifier PgUp/PgDn page the view and Up/Down move it one
# row, instead of reaching the app; every other combination is sent as before. # row, instead of reaching the app; every other combination is sent as before.
# 24 scrollback rows above an 8-row screen, viewport at the follow anchor # 24 scrollback rows above an 8-row screen, viewport at the follow anchor
@@ -728,16 +593,14 @@ stream.feed("\x1b[?1049h")
command.sync_alt_fold(terminal) command.sync_alt_fold(terminal)
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
assert terminal.viewport_y == 0, terminal.viewport_y assert terminal.viewport_y == 0, terminal.viewport_y
assert terminal.view.settings().get("scroll_past_end") is True, \
"the scroll range must stay: the trackpad poller reads it"
# a second render leaves the fold alone # a second render leaves the fold alone
command.sync_alt_fold(terminal) command.sync_alt_fold(terminal)
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
# a disturbed fold is re-asserted on the next render, keeping the anchor: # a disturbed fold is re-asserted on the next render, keeping the anchor:
# apps repaint the boundary row every scroll, and a per-repair reset made the # apps repaint the boundary row every scroll, and a per-repair reset dropped
# poller read the thrashing anchor as phantom down-lines (dead trackpad-up) # the user's scrollback position on every frame
terminal.viewport_y = 60 terminal.viewport_y = 60
terminal.view.folds = [(0, fold_end - 2)] terminal.view.folds = [(0, fold_end - 2)]
command.sync_alt_fold(terminal) command.sync_alt_fold(terminal)
@@ -760,23 +623,6 @@ assert terminal.view.folds == [], terminal.view.folds
print("alt fold tests passed (scrollback folded on entry, restored on exit)") print("alt fold tests passed (scrollback folded on entry, restored on exit)")
# a fresh view gets one blank scrollback row: sublime has no layout above row
# 0, so without it the trackpad poller has no upward slack — and on the alt
# screen no line ever retires to provide it (trackpad-up was dead in less)
terminal, stream = make_terminal(rows=5, cols=20)
Terminal._terminals[id(terminal)] = terminal
terminal.view.id = lambda: id(terminal)
command = make_command(terminal.view)
stream.feed("hello")
command.run(None)
assert terminal.offset == 1, terminal.offset
assert terminal.view.text.startswith("\nhello"), repr(terminal.view.text)
command.run(None) # only a fresh view is seeded
assert terminal.offset == 1, terminal.offset
del sublime._timeouts[:] # drop the follow-scrolls the renders queued
print("seed row tests passed (blank row above a fresh screen, seeded once)")
# a row whose first cells were never written: they read back as default cells # a row whose first cells were never written: they read back as default cells
# (StaticDefaultDict), so they form their own uncoloured run and the coloured # (StaticDefaultDict), so they form their own uncoloured run and the coloured

View File

@@ -94,8 +94,12 @@ class VtermOpenTerminalProjectFolderCommand(sublime_plugin.WindowCommand):
parameters = [p.replace("%CWD%", cwd) for p in parameters] parameters = [p.replace("%CWD%", cwd) for p in parameters]
try: try:
subprocess.Popen( subprocess.Popen(
[terminal] + parameters, cwd=cwd, start_new_session=True, [terminal] + parameters,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd,
start_new_session=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
) )
except OSError: except OSError:
LOG.exception("failed to start external terminal: terminal=%s", terminal) LOG.exception("failed to start external terminal: terminal=%s", terminal)
@@ -126,7 +130,7 @@ class VtermInitializeViewCommand(sublime_plugin.TextCommand):
view_settings.set("draw_white_space", "none") view_settings.set("draw_white_space", "none")
view_settings.set("draw_unicode_white_space", False) view_settings.set("draw_unicode_white_space", False)
view_settings.set("draw_indent_guides", False) view_settings.set("draw_indent_guides", False)
view_settings.set("scroll_past_end", True) 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", SCHEME_PATH)
view_settings.set("syntax", "Packages/vterm/vterm.sublime-syntax") view_settings.set("syntax", "Packages/vterm/vterm.sublime-syntax")

View File

@@ -1,8 +1,12 @@
""" SGR mouse reporting while the app tracks the mouse; otherwise sublime keeps drag_select """ """ SGR mouse reporting while the app tracks the mouse; otherwise sublime keeps drag_select
import sublime
The trackpad is not handled here. On macOS it never reaches a mousemap binding
(sublime_text#1838), so it needs a utility such as LinearMouse to convert its
continuous scrolling into discrete wheel events; those arrive as ordinary wheel
notches and take the path below.
"""
import sublime_plugin import sublime_plugin
from .render import following
from .terminal import Terminal from .terminal import Terminal
# SGR button numbers for the wheel; 64 is the "scroll" bit plus direction # SGR button numbers for the wheel; 64 is the "scroll" bit plus direction
@@ -13,10 +17,6 @@ WHEEL_DOWN = 65
# line per report and has no setting of its own # line per report and has no setting of its own
WHEEL_LINES = 3 WHEEL_LINES = 3
# trackpad poll period: the snapped-back displacement grows with it, and the
# alt-screen up-scroll rate is capped at one line per tick (16 = one 60 Hz frame)
TRACKPAD_POLL_MS = 24
def send_scroll(terminal, up, lines, cell=None): def send_scroll(terminal, up, lines, cell=None):
"""Report `lines` scroll lines to the app, or fake arrow keys on the alt screen. """Report `lines` scroll lines to the app, or fake arrow keys on the alt screen.
@@ -111,64 +111,3 @@ class VtermScrollCommand(sublime_plugin.TextCommand):
def want_event(self): def want_event(self):
"""Ask sublime for the wheel coordinates.""" """Ask sublime for the wheel coordinates."""
return True return True
def start_trackpad(terminal):
"""Start the poller that turns trackpad scrolling into wheel lines.
The trackpad never fires the mousemap scroll bindings on macOS
(sublime_text#1838); its only trace is viewport movement. While the app
owns the wheel the mousemap keeps the physical wheel off the viewport, so
any displacement from the follow anchor is trackpad scroll: harvest whole
lines from it, send them like wheel lines, and snap the viewport back.
The caller checks _trackpad_running; the first tick arms the baseline."""
terminal._trackpad_running = True
terminal._trackpad_engaged = True
terminal._trackpad_baseline = None
terminal._trackpad_browse_y = None
sublime.set_timeout(lambda: _trackpad_tick(terminal), TRACKPAD_POLL_MS)
def _trackpad_tick(terminal):
"""One poll: harvest viewport displacement, reschedule while captured."""
view = terminal.view
if Terminal.from_id(view.id()) is not terminal or not terminal.alive or not terminal._wheel_captured or not terminal.is_visible():
terminal._trackpad_running = False
return
anchor = terminal.viewport_y
extent = view.viewport_extent()[1]
if (anchor, extent) != terminal._trackpad_baseline:
# the renderer moved the anchor (follow-scroll, fold, resize): re-arm
# around the new position instead of reading the move as a gesture
terminal._trackpad_baseline = (anchor, extent)
terminal._trackpad_acc = 0.0
elif not terminal._trackpad_engaged:
# re-engaging mid-glide harvests the momentum as a gesture and snaps
# the view back on every tick, which reads as shaking. Wait for two
# ticks to agree that it has come to rest, then correct the overshoot
# in one step — it was browsing, not a scroll the app should hear about
y = view.viewport_position()[1]
if y == terminal._trackpad_browse_y and following(view, terminal):
terminal._trackpad_engaged = True
terminal._trackpad_acc = 0.0
view.set_viewport_position((0, anchor), False)
terminal._trackpad_browse_y = y
else:
dy = view.viewport_position()[1] - anchor
if dy == 0:
# at rest a stale fraction must not fire on the next gesture
terminal._trackpad_acc = 0.0
elif abs(dy) >= extent:
# scrollbar/minimap jump or page scroll, not a trackpad gesture:
# let the user browse until they return to the anchor
terminal._trackpad_engaged = False
terminal._trackpad_browse_y = None
else:
terminal._trackpad_acc += dy / view.line_height()
lines = int(terminal._trackpad_acc)
terminal._trackpad_acc -= lines
if lines:
send_scroll(terminal, up=lines < 0, lines=abs(lines))
view.set_viewport_position((0, anchor), False)
sublime.set_timeout(lambda: _trackpad_tick(terminal), TRACKPAD_POLL_MS)

View File

@@ -172,14 +172,6 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
terminal.pending_clear_screen = False terminal.pending_clear_screen = False
self.clear_view(edit, terminal, wipe_screen=True) self.clear_view(edit, terminal, wipe_screen=True)
if terminal.offset == 0 and view.size() == 0:
# one blank scrollback row above a fresh screen: sublime has no
# layout above row 0, and the trackpad poller needs upward slack —
# without it, trackpad-up is dead until a line retires, which on
# the alt screen is never
view.insert(edit, 0, "\n")
terminal.offset = 1
self.update_lines(edit, terminal) self.update_lines(edit, terminal)
self.sync_alt_fold(terminal) self.sync_alt_fold(terminal)
@@ -208,8 +200,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
def sync_alt_fold(self, terminal): def sync_alt_fold(self, terminal):
"""Hide the scrollback while the app owns the alt screen: fold it away """Hide the scrollback while the app owns the alt screen: fold it away
(the view text is its only copy). The scroll range stays: the trackpad (the view text is its only copy).
poller pins the viewport and needs the slack to read the trackpad.
The fold is re-checked every render — the screen's first row is The fold is re-checked every render — the screen's first row is
replaced on every region scroll, and an edit at the fold boundary can replaced on every region scroll, and an edit at the fold boundary can
silently disturb a fold.""" silently disturb a fold."""
@@ -545,11 +536,6 @@ def scroll_to_cursor(view, terminal):
"""Follow the output: pin the viewport to the bottom of the live screen.""" """Follow the output: pin the viewport to the bottom of the live screen."""
terminal._scroll_pending = False terminal._scroll_pending = False
y = bottom_y(view, terminal) y = bottom_y(view, terminal)
if y == terminal.viewport_y and terminal.trackpad_pinning():
# the anchor is unchanged and the trackpad poller owns the pin:
# re-pinning here would discard displacement it has not harvested yet,
# and apps repaint while scrolling, so this races every gesture
return
terminal.viewport_y = y terminal.viewport_y = y
view.set_viewport_position((0, y), False) view.set_viewport_position((0, y), False)

View File

@@ -109,14 +109,6 @@ class Terminal:
self._send_lock = threading.Lock() self._send_lock = threading.Lock()
self._wheel_captured = False self._wheel_captured = False
self._scroll_pending = False self._scroll_pending = False
# trackpad poller (mouse.start_trackpad): reschedule guard, fractional
# line remainder, harvesting vs. browsing, last seen (anchor, extent),
# viewport of the previous browsing tick
self._trackpad_running = False
self._trackpad_acc = 0.0
self._trackpad_engaged = True
self._trackpad_baseline = None
self._trackpad_browse_y = None
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 # a forced frame (set on activation); rendering stays on the renderer
@@ -154,10 +146,6 @@ class Terminal:
self._visible = group >= 0 and window.active_view_in_group(group) == self.view self._visible = group >= 0 and window.active_view_in_group(group) == self.view
return self._visible return self._visible
def trackpad_pinning(self):
"""Whether the trackpad poller owns pinning the viewport to the anchor."""
return self._trackpad_running and self._trackpad_engaged
def is_render_due(self): def is_render_due(self):
"""Whether an unfocused terminal may draw again. """Whether an unfocused terminal may draw again.
@@ -317,7 +305,7 @@ class Terminal:
# the wheel can drag the screen off the top into # the wheel can drag the screen off the top into
# sublime's past-end room; a frame undoes that, and # sublime's past-end room; a frame undoes that, and
# an idle terminal draws none. Pinning while the # an idle terminal draws none. Pinning while the
# view is still gliding fights the trackpad's # view is still gliding fights the scroll's
# momentum, which reads as shaking, so wait for two # momentum, which reads as shaking, so wait for two
# ticks to agree that it has come to rest # ticks to agree that it has come to rest
y = self.view.viewport_position()[1] y = self.view.viewport_position()[1]
@@ -421,11 +409,6 @@ class Terminal:
if captured != self._wheel_captured: if captured != self._wheel_captured:
self._wheel_captured = captured self._wheel_captured = captured
self.view.settings().set("vterm_view.wheel_captured", captured) self.view.settings().set("vterm_view.wheel_captured", captured)
if captured and not self._trackpad_running and self.is_visible():
# checked every renderer tick, so a poller that stopped on a
# capture flap or a hidden tab restarts within a frame or two
from .mouse import start_trackpad # module-level import would cycle
start_trackpad(self)
def send_focus(self, focused): def send_focus(self, focused):
"""Report focus gain or loss, when the app asked for it (?1004).""" """Report focus gain or loss, when the app asked for it (?1004)."""