Shift+Up, Shift+Down added
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. |
|
| `vterm/mouse.py` | Wheel and click handling, and the trackpad poller. |
|
||||||
| `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`. |
|
||||||
|
|
||||||
@@ -74,8 +74,9 @@ Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-s
|
|||||||
- **`SchemeManager`** (`scheme.py`) — module singleton `scheme_manager`. Owns the
|
- **`SchemeManager`** (`scheme.py`) — module singleton `scheme_manager`. Owns the
|
||||||
generated scheme file and the mapping from colour combination to scope name.
|
generated scheme file and the mapping from colour combination to scope name.
|
||||||
- **`VtermRenderCommand`** (`render.py`) — Sublime keeps **one command instance per
|
- **`VtermRenderCommand`** (`render.py`) — Sublime keeps **one command instance per
|
||||||
view**, so its `colored_lines`, `_row_cache`, `_key_counter` and `_lastrow` are that
|
view**, so its `colored_lines`, `_row_cache`, `_key_counter`, the scrollback chunk
|
||||||
view's render state.
|
bookkeeping (`_archived_chunks`, `_open_rows`, `_open_chunk`, `_trimmed`) and
|
||||||
|
`_lastrow` are that view's render state.
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
@@ -132,6 +133,8 @@ 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
|
||||||
|
│ trackpad poller while the app owns the wheel
|
||||||
│
|
│
|
||||||
└─ UI thread ──────────────────────────────────────────────────
|
└─ UI thread ──────────────────────────────────────────────────
|
||||||
VtermRenderCommand.run(edit)
|
VtermRenderCommand.run(edit)
|
||||||
@@ -180,6 +183,9 @@ 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
|
||||||
@@ -189,8 +195,10 @@ keeps a full-screen repaint from costing hundreds of cross-process calls.
|
|||||||
**The follow-scroll gate.** `following()` compares the viewport against
|
**The follow-scroll gate.** `following()` compares the viewport against
|
||||||
`terminal.viewport_y`, where the last auto-scroll left it. Above that, the user scrolled
|
`terminal.viewport_y`, where the last auto-scroll left it. Above that, the user scrolled
|
||||||
up by hand, and trimming, caret placement and scrolling are all skipped so their position
|
up by hand, and trimming, caret placement and scrolling are all skipped so their position
|
||||||
survives the next frame. `scroll_page` (the PgUp/PgDn chord) closes the gate the same way,
|
survives the next frame. `scroll_view` (the PgUp/PgDn and Up/Down chords) closes the gate
|
||||||
and paging down clamps to the anchor `bottom_y` returns, which re-opens it.
|
the same way, and scrolling down clamps to the anchor `bottom_y` returns, which re-opens
|
||||||
|
it. A one-row scroll lands exactly on the tolerance boundary, so one Up closes the gate
|
||||||
|
and one Down re-opens it.
|
||||||
|
|
||||||
### How colour is actually drawn
|
### How colour is actually drawn
|
||||||
|
|
||||||
@@ -236,11 +244,15 @@ 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 and
|
While an app owns the alternate buffer, `sync_alt_fold` folds rows `0..offset-1` away, so
|
||||||
sets `scroll_past_end` to false, leaving the view with no scroll range — the app's own
|
the view shows the app's screen alone. The scroll range is deliberately left in place: the
|
||||||
scrolling is the only scrolling. 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. The emulator side
|
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
|
||||||
(`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.
|
||||||
|
|
||||||
@@ -253,8 +265,8 @@ normal mode) would leave the typed character sitting in the view.
|
|||||||
```
|
```
|
||||||
keymap binding (context: setting.vterm_view)
|
keymap binding (context: setting.vterm_view)
|
||||||
→ VtermKeypressCommand.run(key=…, ctrl=…, alt=…, shift=…)
|
→ VtermKeypressCommand.run(key=…, ctrl=…, alt=…, shift=…)
|
||||||
→ page_scroll() PgUp/PgDn with page_scroll_modifier scrolls the view
|
→ view_scrolled() with scroll_modifier, PgUp/PgDn scroll the view a page
|
||||||
and stops here; never on the alternate screen
|
and Up/Down a row; stops here, never on the alt screen
|
||||||
→ Terminal.send_key() adds application_mode (DECCKM), new_line_mode (LNM)
|
→ Terminal.send_key() adds application_mode (DECCKM), new_line_mode (LNM)
|
||||||
→ keys.get_key_code() modified nav keys → CSI 1;<mod><final>
|
→ keys.get_key_code() modified nav keys → CSI 1;<mod><final>
|
||||||
ctrl → control byte; alt → ESC prefix
|
ctrl → control byte; alt → ESC prefix
|
||||||
@@ -291,6 +303,17 @@ which produces three cases:
|
|||||||
3. **Neither** → the binding does not match at all and Sublime scrolls the view over the
|
3. **Neither** → the binding does not match at all and Sublime scrolls the view over the
|
||||||
real scrollback text.
|
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.
|
||||||
|
|
||||||
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
|
||||||
native. The first click on an unfocused tab only focuses it — we replaced the command
|
native. The first click on an unfocused tab only focuses it — we replaced the command
|
||||||
@@ -312,7 +335,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`, clipboard | Sublime API; small pty writes go out here too |
|
| UI | commands, listeners, `Terminal.start`, `scroll_to_cursor`, `_trackpad_tick`, 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 |
|
||||||
@@ -334,9 +357,9 @@ replaces both together.
|
|||||||
|
|
||||||
**Plugin settings** (`vterm.sublime-settings`, read through the cached `settings()`
|
**Plugin settings** (`vterm.sublime-settings`, read through the cached `settings()`
|
||||||
handle): `auto_close`, `scrollback_history_size`, `honor_erase_scrollback`,
|
handle): `auto_close`, `scrollback_history_size`, `honor_erase_scrollback`,
|
||||||
`page_scroll_modifier`, `min_columns`, `max_columns`, `theme`, `ansi_colors`, `terminal`,
|
`scroll_modifier`, `min_columns`, `max_columns`, `theme`, `ansi_colors`, `terminal`,
|
||||||
`parameters`, `debug`. `page_scroll_modifier` is the modifier that turns PgUp/PgDn into a
|
`parameters`, `debug`. `scroll_modifier` is the modifier that turns PgUp/PgDn and Up/Down
|
||||||
page scroll of the view instead of a key for the app. `terminal` and
|
into a scroll of the view — a page, a row — instead of a key for the app. `terminal` and
|
||||||
`parameters` name the external terminal spawned by *vterm: Open Terminal in Project
|
`parameters` name the external terminal spawned by *vterm: Open Terminal in Project
|
||||||
Folder* (`%CWD%` in `parameters` becomes the project folder). ED 3 never touches the visible screen (as in
|
Folder* (`%CWD%` in `parameters` becomes the project folder). ED 3 never touches the visible screen (as in
|
||||||
xterm), and `honor_erase_scrollback` is off, so it does not drop the scrollback either:
|
xterm), and `honor_erase_scrollback` is off, so it does not drop the scrollback either:
|
||||||
@@ -353,9 +376,8 @@ defuses Vintage mode inside a terminal). The view is also set scratch, so closin
|
|||||||
prompts to save.
|
prompts to save.
|
||||||
|
|
||||||
**Written at runtime**: `vterm_view.finished` (latched by cleanup; also disables the
|
**Written at runtime**: `vterm_view.finished` (latched by cleanup; also disables the
|
||||||
Escape binding for a dead tab), `vterm_view.wheel_captured` (the mousemap gate),
|
Escape binding for a dead tab), `vterm_view.wheel_captured` (the mousemap gate), and
|
||||||
`scroll_past_end` (toggled by the alt-screen fold), and `block_caret` / `caret_style`
|
`block_caret` / `caret_style` (from DECSCUSR; style 0 is the terminal default, a blinking block).
|
||||||
(from DECSCUSR; style 0 is the terminal default, a blinking block).
|
|
||||||
|
|
||||||
**Contexts.** Keymap bindings use the native `setting.vterm_view`, evaluated in
|
**Contexts.** Keymap bindings use the native `setting.vterm_view`, evaluated in
|
||||||
Sublime's own process — a plugin `on_query_context` would cost a round trip on every
|
Sublime's own process — a plugin `on_query_context` would cost a round trip on every
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -42,40 +42,33 @@ takes `cmd`, `cwd`, `env` and `title`:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Keys: Shift+Enter sends `ESC CR` (newline in both agents). Shift+PgUp/PgDn page
|
Keys: Shift+Enter sends `ESC CR` (newline in both agents). Shift+PgUp/PgDn page
|
||||||
the view over the scrollback instead of reaching the app, as xterm and kitty do
|
the view over the scrollback and Shift+Up/Down move it one line, instead of
|
||||||
— `page_scroll_modifier` moves that to Ctrl, or turns it off. Copy/paste use the
|
reaching the app, as xterm and kitty do — `scroll_modifier` moves that to Ctrl,
|
||||||
normal Sublime keys, and pastes are bracketed when the app asks for it. Copying
|
or turns it off. Copy/paste use the normal Sublime keys, and pastes are
|
||||||
re-joins soft-wrapped lines. The wheel scrolls the view normally; while the app
|
bracketed when the app asks for it. Copying re-joins soft-wrapped lines. The
|
||||||
tracks the mouse the wheel and left click go to it instead, and on the alternate
|
wheel scrolls the view normally; while the app tracks the mouse the wheel and
|
||||||
screen without tracking each wheel tick sends arrow keys (alternate scroll, as
|
left click go to it instead, and on the alternate screen without tracking each
|
||||||
kitty/iTerm2 do — this is what scrolls Claude Code's fullscreen UI before it
|
wheel tick sends arrow keys (alternate scroll, as kitty/iTerm2 do — this is
|
||||||
enables mouse reporting). While an app owns the alternate screen the scrollback
|
what scrolls Claude Code's fullscreen UI before it enables mouse reporting).
|
||||||
above it is folded away, kitty-style, and unfolds when the app leaves it.
|
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,
|
||||||
The caret follows DECSCUSR, so zsh vi-mode can show a block cursor in normal
|
sends it as scroll lines and pins the view back. A jump of a whole screen or
|
||||||
mode. The shell must emit the sequence — add to `~/.zshrc`:
|
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
|
||||||
```zsh
|
alternate screen the scrollback above it is folded away, kitty-style, and
|
||||||
function zle-keymap-select zle-line-init {
|
unfolds when the app leaves it.
|
||||||
case $KEYMAP in
|
|
||||||
vicmd) print -n '\e[1 q' ;; # blinking block
|
|
||||||
*) print -n '\e[5 q' ;; # blinking bar
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
zle -N zle-keymap-select
|
|
||||||
zle -N zle-line-init
|
|
||||||
```
|
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
`vterm.sublime-settings`: `auto_close` (close the tab when the process exits; off
|
`vterm.sublime-settings`: `auto_close` (close the tab when the process exits;
|
||||||
leaves it open with a `[process exited with code N]` line),
|
off leaves it open with a `[process exited with code N]` line),
|
||||||
`scrollback_history_size`, `page_scroll_modifier` (the modifier for the PgUp/PgDn
|
`scrollback_history_size`, `scroll_modifier` (the modifier that scrolls the
|
||||||
page scroll; `null` gives both keys to the app), `min_columns` / `max_columns`,
|
view with PgUp/PgDn and Up/Down; `null` gives all four keys to the app),
|
||||||
`terminal` / `parameters` (the external terminal for *vterm: Open Terminal in
|
`min_columns` / `max_columns`, `terminal` / `parameters` (the external terminal
|
||||||
Project Folder*; `%CWD%` in `parameters` is replaced with the project folder,
|
for *vterm: Open Terminal in Project Folder*; `%CWD%` in `parameters` is
|
||||||
defaulting to Apple Terminal, with kitty commented out beside it), `debug`
|
replaced with the project folder, defaulting to Apple Terminal, with kitty
|
||||||
(verbose logging to the ST console), plus the colours:
|
commented out beside it), `debug` (verbose logging to the ST console), plus the
|
||||||
|
colours:
|
||||||
|
|
||||||
- `theme` — `"default"` (Terminal.app Basic, light), `"homebrew"`, `"pro"`,
|
- `theme` — `"default"` (Terminal.app Basic, light), `"homebrew"`, `"pro"`,
|
||||||
`"slate"`, or `null` to follow the active Sublime colour scheme. Set it with
|
`"slate"`, or `null` to follow the active Sublime colour scheme. Set it with
|
||||||
@@ -123,7 +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.
|
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.
|
||||||
|
|
||||||
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
|
||||||
@@ -151,6 +146,8 @@ 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
|
||||||
|
coarser than the wheel in a native terminal.
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
|
|||||||
@@ -540,9 +540,9 @@ assert sublime._timeouts == [], sublime._timeouts
|
|||||||
print("trackpad tests passed (displacement harvested, fractions kept, jumps browse, poller unhooks)")
|
print("trackpad tests passed (displacement harvested, fractions kept, jumps browse, poller unhooks)")
|
||||||
|
|
||||||
|
|
||||||
# PgUp/PgDn with the configured modifier pages the view instead of reaching the
|
# with the configured modifier PgUp/PgDn page the view and Up/Down move it one
|
||||||
# app; every other combination is sent as before. 24 scrollback rows above an
|
# row, instead of reaching the app; every other combination is sent as before.
|
||||||
# 8-row screen, viewport at the follow anchor
|
# 24 scrollback rows above an 8-row screen, viewport at the follow anchor
|
||||||
terminal, stream = make_terminal(rows=8, cols=20)
|
terminal, stream = make_terminal(rows=8, cols=20)
|
||||||
Terminal._terminals[id(terminal)] = terminal
|
Terminal._terminals[id(terminal)] = terminal
|
||||||
terminal.view.id = lambda: id(terminal)
|
terminal.view.id = lambda: id(terminal)
|
||||||
@@ -581,6 +581,21 @@ assert y == anchor, y
|
|||||||
# paging back onto the anchor re-opens the gate
|
# paging back onto the anchor re-opens the gate
|
||||||
assert following(terminal.view, terminal)
|
assert following(terminal.view, terminal)
|
||||||
|
|
||||||
|
# the arrows move one row rather than a screen
|
||||||
|
y = press(key="up", shift=True)
|
||||||
|
assert y == anchor - LINE_HEIGHT, y
|
||||||
|
assert terminal.process.written == "", repr(terminal.process.written)
|
||||||
|
# one row up is the edge of the gate's one-line tolerance; it must still close
|
||||||
|
assert not following(terminal.view, terminal)
|
||||||
|
|
||||||
|
for _ in range(30):
|
||||||
|
y = press(key="up", shift=True)
|
||||||
|
assert y == 0, y
|
||||||
|
for _ in range(30):
|
||||||
|
y = press(key="down", shift=True)
|
||||||
|
assert y == anchor, y
|
||||||
|
assert following(terminal.view, terminal)
|
||||||
|
|
||||||
# a bare or differently modified key still belongs to the app
|
# a bare or differently modified key still belongs to the app
|
||||||
press(key="pageup")
|
press(key="pageup")
|
||||||
assert terminal.process.written == "\x1b[5~", repr(terminal.process.written)
|
assert terminal.process.written == "\x1b[5~", repr(terminal.process.written)
|
||||||
@@ -589,28 +604,41 @@ assert terminal.process.written == "\x1b[5;5~", repr(terminal.process.written)
|
|||||||
y = press(key="pageup", ctrl=True, shift=True)
|
y = press(key="pageup", ctrl=True, shift=True)
|
||||||
assert terminal.process.written == "\x1b[5;6~", repr(terminal.process.written)
|
assert terminal.process.written == "\x1b[5;6~", repr(terminal.process.written)
|
||||||
assert y == anchor, "the app's keys must not scroll"
|
assert y == anchor, "the app's keys must not scroll"
|
||||||
|
press(key="up")
|
||||||
|
assert terminal.process.written == "\x1b[A", repr(terminal.process.written)
|
||||||
|
y = press(key="up", ctrl=True, shift=True)
|
||||||
|
assert terminal.process.written == "\x1b[1;6A", repr(terminal.process.written)
|
||||||
|
assert y == anchor, "the app's keys must not scroll"
|
||||||
|
|
||||||
# the alt screen is folded away: nothing to page, so the app keeps the key
|
# the alt screen is folded away: nothing to scroll, so the app keeps the keys
|
||||||
stream.feed("\x1b[?1049h")
|
stream.feed("\x1b[?1049h")
|
||||||
press(key="pageup", shift=True)
|
press(key="pageup", shift=True)
|
||||||
assert terminal.process.written == "\x1b[5;2~", repr(terminal.process.written)
|
assert terminal.process.written == "\x1b[5;2~", repr(terminal.process.written)
|
||||||
|
press(key="up", shift=True)
|
||||||
|
assert terminal.process.written == "\x1b[1;2A", repr(terminal.process.written)
|
||||||
stream.feed("\x1b[?1049l")
|
stream.feed("\x1b[?1049l")
|
||||||
|
|
||||||
# the modifier is configurable, and null hands both keys back to the app
|
# the modifier is configurable, and null hands every key back to the app
|
||||||
plugin_settings.set("page_scroll_modifier", "ctrl")
|
plugin_settings.set("scroll_modifier", "ctrl")
|
||||||
y = press(key="pageup", ctrl=True)
|
y = press(key="pageup", ctrl=True)
|
||||||
assert y == anchor - PAGE, y
|
assert y == anchor - PAGE, y
|
||||||
|
y = press(key="up", ctrl=True)
|
||||||
|
assert y == anchor - PAGE - LINE_HEIGHT, y
|
||||||
press(key="pageup", shift=True)
|
press(key="pageup", shift=True)
|
||||||
assert terminal.process.written == "\x1b[5;2~", repr(terminal.process.written)
|
assert terminal.process.written == "\x1b[5;2~", repr(terminal.process.written)
|
||||||
|
press(key="up", shift=True)
|
||||||
|
assert terminal.process.written == "\x1b[1;2A", repr(terminal.process.written)
|
||||||
|
|
||||||
plugin_settings.set("page_scroll_modifier", None)
|
plugin_settings.set("scroll_modifier", None)
|
||||||
press(key="pageup", shift=True)
|
press(key="pageup", shift=True)
|
||||||
assert terminal.process.written == "\x1b[5;2~", repr(terminal.process.written)
|
assert terminal.process.written == "\x1b[5;2~", repr(terminal.process.written)
|
||||||
|
press(key="up", shift=True)
|
||||||
|
assert terminal.process.written == "\x1b[1;2A", repr(terminal.process.written)
|
||||||
press(key="pagedown", ctrl=True)
|
press(key="pagedown", ctrl=True)
|
||||||
assert terminal.process.written == "\x1b[6;5~", repr(terminal.process.written)
|
assert terminal.process.written == "\x1b[6;5~", repr(terminal.process.written)
|
||||||
plugin_settings.set("page_scroll_modifier", "shift")
|
plugin_settings.set("scroll_modifier", "shift")
|
||||||
|
|
||||||
print("page scroll tests passed (one screen per press, clamped to the anchor, configurable modifier)")
|
print("view scroll tests passed (a screen or a row per press, clamped to the anchor, configurable modifier)")
|
||||||
|
|
||||||
|
|
||||||
# a click in an unfocused group must focus the tab instead of reporting: we
|
# a click in an unfocused group must focus the tab instead of reporting: we
|
||||||
|
|||||||
@@ -10,9 +10,10 @@
|
|||||||
// cmd+shift+k is then the only way to empty the tab.
|
// cmd+shift+k is then the only way to empty the tab.
|
||||||
"honor_erase_scrollback": false,
|
"honor_erase_scrollback": false,
|
||||||
|
|
||||||
// modifier that turns PgUp/PgDn into a page scroll of the view:
|
// modifier that turns PgUp/PgDn and Up/Down into a scroll of the view
|
||||||
// "shift", "ctrl", "alt" or a "+" combination; null sends both to the app
|
// (a page / a line): "shift", "ctrl", "alt" or a "+" combination;
|
||||||
"page_scroll_modifier": "shift",
|
// null sends the keys to the app
|
||||||
|
"scroll_modifier": "shift",
|
||||||
|
|
||||||
"min_columns": 20,
|
"min_columns": 20,
|
||||||
"max_columns": 500,
|
"max_columns": 500,
|
||||||
@@ -27,12 +28,12 @@
|
|||||||
|
|
||||||
// External terminal for "vterm: Open Terminal in Project Folder".
|
// External terminal for "vterm: Open Terminal in Project Folder".
|
||||||
// %CWD% in parameters is replaced with the project folder.
|
// %CWD% in parameters is replaced with the project folder.
|
||||||
"terminal": "/usr/bin/open",
|
// "terminal": "/usr/bin/open",
|
||||||
"parameters": ["-a", "Terminal", "%CWD%"],
|
// "parameters": ["-a", "Terminal", "%CWD%"],
|
||||||
|
|
||||||
// kitty instead of Apple Terminal:
|
// kitty instead of Apple Terminal:
|
||||||
// "terminal": "/opt/homebrew/bin/kitty",
|
"terminal": "/opt/homebrew/bin/kitty",
|
||||||
// "parameters": ["-d", "%CWD%"],
|
"parameters": ["-d", "%CWD%"],
|
||||||
|
|
||||||
"debug": false
|
"debug": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import subprocess
|
|||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
from .render import CONTINUATION, scroll_page
|
from .render import CONTINUATION, scroll_view
|
||||||
from .scheme import SCHEME_PATH, scheme_manager
|
from .scheme import SCHEME_PATH, scheme_manager
|
||||||
from .terminal import Terminal
|
from .terminal import Terminal
|
||||||
from .themes import THEMES
|
from .themes import THEMES
|
||||||
@@ -14,6 +14,9 @@ from .utils import SETTINGS_FILE, settings
|
|||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# nav keys the view may consume from the app, with the modifier configured
|
||||||
|
SCROLL_KEYS = ("pageup", "pagedown", "up", "down")
|
||||||
|
|
||||||
|
|
||||||
class VtermOpenCommand(sublime_plugin.WindowCommand):
|
class VtermOpenCommand(sublime_plugin.WindowCommand):
|
||||||
"""Open a terminal in a new tab. `vterm_open`, the only entry point users bind."""
|
"""Open a terminal in a new tab. `vterm_open`, the only entry point users bind."""
|
||||||
@@ -143,29 +146,31 @@ class VtermKeypressCommand(sublime_plugin.TextCommand):
|
|||||||
terminal = Terminal.from_id(self.view.id())
|
terminal = Terminal.from_id(self.view.id())
|
||||||
if not terminal or not terminal.alive:
|
if not terminal or not terminal.alive:
|
||||||
return
|
return
|
||||||
if self.page_scrolled(terminal, **kwargs):
|
if self.view_scrolled(terminal, **kwargs):
|
||||||
return
|
return
|
||||||
# drop any selection so the caret follows the terminal cursor
|
# drop any selection so the caret follows the terminal cursor
|
||||||
self.view.sel().clear()
|
self.view.sel().clear()
|
||||||
self.view.run_command("vterm_show_cursor")
|
self.view.run_command("vterm_show_cursor")
|
||||||
terminal.send_key(**kwargs)
|
terminal.send_key(**kwargs)
|
||||||
|
|
||||||
def page_scrolled(self, terminal, key=None, **mods):
|
def view_scrolled(self, terminal, key=None, **mods):
|
||||||
"""Page the view one screen over the scrollback, before vterm_show_cursor,
|
"""Scroll the view over the scrollback, before vterm_show_cursor, whose
|
||||||
whose scroll would pull the viewport straight back down. The alt screen
|
scroll would pull the viewport straight back down. PgUp/PgDn move a
|
||||||
has its scrollback folded away, so an app there keeps the key.
|
screen, Up/Down one line. The alt screen has its scrollback folded
|
||||||
|
away, so an app there keeps the key.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: the view consumed the key; False sends it to the app.
|
bool: the view consumed the key; False sends it to the app.
|
||||||
"""
|
"""
|
||||||
# the cheap tests first: this runs on every keystroke
|
# the cheap tests first: this runs on every keystroke
|
||||||
if key not in ("pageup", "pagedown") or not mods or terminal.screen.alternate_buffer_mode:
|
if key not in SCROLL_KEYS or not mods or terminal.screen.alternate_buffer_mode:
|
||||||
return False
|
return False
|
||||||
modifier = settings().get("page_scroll_modifier", "shift")
|
modifier = settings().get("scroll_modifier", "shift")
|
||||||
# exact match: ctrl+shift+pageup still belongs to the app
|
# exact match: ctrl+shift+pageup still belongs to the app
|
||||||
if not modifier or {m for m, on in mods.items() if on} != set(modifier.split("+")):
|
if not modifier or {m for m, on in mods.items() if on} != set(modifier.split("+")):
|
||||||
return False
|
return False
|
||||||
scroll_page(self.view, terminal, up=(key == "pageup"))
|
lines = 1 if key in ("up", "down") else terminal.screen.lines
|
||||||
|
scroll_view(self.view, terminal, up=key in ("up", "pageup"), lines=lines)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -554,14 +554,14 @@ def scroll_to_cursor(view, terminal):
|
|||||||
view.set_viewport_position((0, y), False)
|
view.set_viewport_position((0, y), False)
|
||||||
|
|
||||||
|
|
||||||
def scroll_page(view, terminal, up):
|
def scroll_view(view, terminal, up, lines):
|
||||||
"""Move the viewport one screen over the scrollback.
|
"""Move the viewport `lines` rows over the scrollback.
|
||||||
|
|
||||||
Not sublime's scroll_lines: it animates, and while the viewport is still
|
Not sublime's scroll_lines: it animates, and while the viewport is still
|
||||||
within a line of where it started the follow-scroll gate reads that as "at
|
within a line of where it started the follow-scroll gate reads that as "at
|
||||||
the bottom" and pulls it back down.
|
the bottom" and pulls it back down.
|
||||||
"""
|
"""
|
||||||
delta = terminal.screen.lines * view.line_height()
|
delta = lines * view.line_height()
|
||||||
y = view.viewport_position()[1]
|
y = view.viewport_position()[1]
|
||||||
y = max(y - delta, 0) if up else min(y + delta, bottom_y(view, terminal))
|
y = max(y - delta, 0) if up else min(y + delta, bottom_y(view, terminal))
|
||||||
view.set_viewport_position((0, y), False)
|
view.set_viewport_position((0, y), False)
|
||||||
|
|||||||
Reference in New Issue
Block a user