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/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/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/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
│ moved cursor
│ view.run_command("vterm_render")
│ _sync_wheel_flag() publishes the mousemap gate, and starts the
│ trackpad poller while the app owns the wheel
│ _sync_wheel_flag() publishes the mousemap gate
│
└─ UI thread ──────────────────────────────────────────────────
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
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.
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
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
and one Down re-opens it.
**Pinning waits for rest.** Two pollers put the viewport back on the anchor: the renderer
tick, for a viewport dragged into sublime's past-end room on a terminal too idle to draw
a frame, and the trackpad poller, when a browsing gesture returns to the anchor. Both
compare the viewport against the position they saw one tick earlier and act only when the
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.
**Pinning waits for rest.** The renderer pins a viewport dragged into sublime's past-end
room on a terminal too idle to draw a frame, but only once two ticks agree it has come to
rest: macOS keeps scrolling after the fingers lift, and a hard `set_viewport_position`
against that momentum yanks the view back for as long as the glide lasts.
### How colour is actually drawn
@@ -253,14 +246,12 @@ Details worth knowing before touching `scheme.py`:
### The alternate screen
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
trackpad poller pins the viewport and needs the slack to read a gesture. The fold is
re-checked every frame, not just on transitions, because the screen's first row is
rewritten on every region scroll and an edit at the fold boundary can silently break a
fold. Entering the alt screen collapses the layout, so that transition also zeroes
`terminal.viewport_y`; a stale anchor would hold the follow-scroll gate shut. A fold
merely repaired keeps the anchor, or the boundary row apps rewrite on every scroll would
thrash the poller. The emulator side
the view shows the app's screen alone. The fold is re-checked every frame, not just on
transitions, because the screen's first row is rewritten on every region scroll and an
edit at the fold boundary can silently break a fold. Entering the alt screen collapses the
layout, so that transition also zeroes `terminal.viewport_y`; a stale anchor would hold
the follow-scroll gate shut. A fold merely repaired keeps the anchor, or the boundary row
apps rewrite on every scroll would reset it every frame. The emulator side
(`switch_to_screen`) swaps in a `maxlen=0` history, so a full-screen app never pushes its
frames into scrollback.
@@ -314,15 +305,12 @@ which produces three cases:
real scrollback text.
**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
up, `start_trackpad` runs a 24 ms timer chain, and any displacement from the follow anchor
is a gesture — whole lines of it go out through the same `send_scroll` as the wheel, the
fraction is carried into the next tick, and the viewport is pinned back to the anchor. A
displacement of a viewport or more is a scrollbar drag or a page scroll instead: the poller
disengages and lets the user browse until they return to the anchor. `_sync_wheel_flag`
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.
(sublime_text#1838) and dispatches no command, so a plugin cannot see it at all. Its only
in-API trace is the viewport moving, and reading a gesture that way means letting the view
move before pinning it back — visibly, at every poll. vterm does not try: the trackpad is
converted outside the plugin, by a utility such as LinearMouse set to discrete scrolling
for Sublime Text, and arrives as ordinary wheel notches on the path above. Nothing in the
plugin is trackpad-aware, and the viewport is never moved to measure input.
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
@@ -345,7 +333,7 @@ that normally does that — and subsequent clicks send a press/release pair.
| 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 |
| renderer | `stream.feed`, then `run_command("vterm_render")` | owns the emulator |
| 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
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).
Sublime raises no wheel event at all for the trackpad (sublime_text#1838), so
while the app owns the wheel a poller reads the viewport movement instead,
sends it as scroll lines and pins the view back. A jump of a whole screen or
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
Sublime raises no wheel event at all for the trackpad (sublime_text#1838): set
a utility such as [LinearMouse](https://linearmouse.app/) to discrete scrolling
for Sublime Text and it arrives as wheel notches like any other. While an app owns the
alternate screen the scrollback above it is folded away, kitty-style, and
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
fallback for IME and composed input.
- `vterm/mouse.py` — SGR mouse reporting (wheel, click) when the app enables
tracking; native selection and scrolling otherwise. The trackpad reaches no
mousemap on macOS, so while the app owns the wheel a 24 ms poller harvests
viewport displacement into the same reports.
tracking; native selection and scrolling otherwise. The trackpad is not
handled: it reaches no mousemap on macOS and is converted to wheel notches
outside the plugin.
Supported: alt screen (1049), synchronized output (2026), bracketed paste (2004),
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.
- CJK and emoji rely on wcwidth agreeing with Sublime's font metrics; where they
disagree the caret can drift.
- Trackpad scroll into an app is polled and rounded to whole lines, so it is
coarser than the wheel in a native terminal.
- Trackpad scrolling needs a helper on macOS: Sublime never delivers it to a
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

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)")
# 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
# 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
@@ -728,16 +593,14 @@ stream.feed("\x1b[?1049h")
command.sync_alt_fold(terminal)
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
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
command.sync_alt_fold(terminal)
assert terminal.view.folds == [(0, fold_end)], terminal.view.folds
# 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
# poller read the thrashing anchor as phantom down-lines (dead trackpad-up)
# apps repaint the boundary row every scroll, and a per-repair reset dropped
# the user's scrollback position on every frame
terminal.viewport_y = 60
terminal.view.folds = [(0, fold_end - 2)]
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)")
# 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
# (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]
try:
subprocess.Popen(
[terminal] + parameters, cwd=cwd, start_new_session=True,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
[terminal] + parameters,
cwd=cwd,
start_new_session=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except OSError:
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_unicode_white_space", 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("color_scheme", SCHEME_PATH)
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 """
import sublime
""" SGR mouse reporting while the app tracks the mouse; otherwise sublime keeps drag_select
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
from .render import following
from .terminal import Terminal
# 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
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):
"""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):
"""Ask sublime for the wheel coordinates."""
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
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.sync_alt_fold(terminal)
@@ -208,8 +200,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
def sync_alt_fold(self, terminal):
"""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
poller pins the viewport and needs the slack to read the trackpad.
(the view text is its only copy).
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
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."""
terminal._scroll_pending = False
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
view.set_viewport_position((0, y), False)

View File

@@ -109,14 +109,6 @@ class Terminal:
self._send_lock = threading.Lock()
self._wheel_captured = 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_screen = False
# 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
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):
"""Whether an unfocused terminal may draw again.
@@ -317,7 +305,7 @@ class Terminal:
# the wheel can drag the screen off the top into
# sublime's past-end room; a frame undoes that, and
# 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
# ticks to agree that it has come to rest
y = self.view.viewport_position()[1]
@@ -421,11 +409,6 @@ class Terminal:
if captured != self._wheel_captured:
self._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):
"""Report focus gain or loss, when the app asked for it (?1004)."""