remove touchpad related code
This commit is contained in:
@@ -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 |
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)."""
|
||||||
|
|||||||
Reference in New Issue
Block a user