feat(tui): add half-page transcript scrolling

closes #7735
This commit is contained in:
Armin Ronacher 2026-08-06 22:30:13 +02:00
parent 171c6b520b
commit a3e93ec85e
6 changed files with 54 additions and 1 deletions

View file

@ -95,12 +95,14 @@ Fullscreen transcript bindings take precedence over editor bindings. The default
| `pageUp`, `pageDown` | Editor | Transcript |
| `ctrl+pageUp`, `ctrl+pageDown` | Editor | Editor |
This routing remains configurable through the ordinary action bindings. For example, `"tui.altScreen.pageUp": "ctrl+pageUp"` makes `pageUp` control the editor and `ctrl+pageUp` control the transcript in fullscreen mode. Setting `"tui.altScreen.pageUp": []` disables that transcript shortcut entirely. User bindings replace the defaults for that action.
This routing remains configurable through the ordinary action bindings. For example, `"tui.altScreen.pageUp": "ctrl+pageUp"` makes `pageUp` control the editor and `ctrl+pageUp` control the transcript in fullscreen mode. Bind `tui.altScreen.halfPageUp` and `tui.altScreen.halfPageDown` for smaller transcript steps while keeping the full-page bindings. Setting `"tui.altScreen.pageUp": []` disables that transcript shortcut entirely. User bindings replace the defaults for that action.
| Keybinding id | Default | Description |
|--------|---------|-------------|
| `tui.altScreen.pageUp` | `pageUp` | Scroll the transcript up by one page |
| `tui.altScreen.pageDown` | `pageDown` | Scroll the transcript down by one page |
| `tui.altScreen.halfPageUp` | *(none)* | Scroll the transcript up by half a page |
| `tui.altScreen.halfPageDown` | *(none)* | Scroll the transcript down by half a page |
| `tui.altScreen.previousPrompt` | `ctrl+shift+up` | Jump to the previous marked message |
| `tui.altScreen.nextPrompt` | `ctrl+shift+down` | Jump to the next marked message |
| `tui.altScreen.top` | `home` | Scroll to the beginning of the transcript |

View file

@ -4,6 +4,7 @@
### Added
- Added unbound half-page transcript scrolling actions, `tui.altScreen.halfPageUp` and `tui.altScreen.halfPageDown`, for fullscreen TUI keybindings ([#7735](https://github.com/earendil-works/pi/issues/7735)).
- Added double-click word selection, word-aware drag selection, and triple-click line selection in the fullscreen TUI ([#7725](https://github.com/earendil-works/pi/issues/7725)).
### Fixed

View file

@ -44,6 +44,8 @@ export interface Keybindings {
// Alternate-screen viewport navigation
"tui.altScreen.pageUp": true;
"tui.altScreen.pageDown": true;
"tui.altScreen.halfPageUp": true;
"tui.altScreen.halfPageDown": true;
"tui.altScreen.previousPrompt": true;
"tui.altScreen.nextPrompt": true;
"tui.altScreen.top": true;
@ -157,6 +159,14 @@ export const TUI_KEYBINDINGS = {
defaultKeys: "pageDown",
description: "Scroll viewport down one page",
},
"tui.altScreen.halfPageUp": {
defaultKeys: [],
description: "Scroll viewport up half a page",
},
"tui.altScreen.halfPageDown": {
defaultKeys: [],
description: "Scroll viewport down half a page",
},
"tui.altScreen.previousPrompt": {
defaultKeys: "ctrl+shift+up",
description: "Jump to previous semantic prompt",

View file

@ -427,6 +427,14 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI {
}
return { consume: true };
}
if (keybindings.matches(data, "tui.altScreen.halfPageUp")) {
if (!isRelease) this.scrollBy(-Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2)));
return { consume: true };
}
if (keybindings.matches(data, "tui.altScreen.halfPageDown")) {
if (!isRelease) this.scrollBy(Math.max(1, Math.floor(this.getPrimaryScrollView().viewportHeight / 2)));
return { consume: true };
}
if (keybindings.matches(data, "tui.altScreen.previousPrompt")) {
if (!isRelease) this.scrollToPrompt(-1);
return { consume: true };

View file

@ -32,6 +32,8 @@ describe("KeybindingsManager", () => {
assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.pageUp"), ["pageUp"]);
assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.pageDown"), ["pageDown"]);
assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.halfPageUp"), []);
assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.halfPageDown"), []);
assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.previousPrompt"), ["ctrl+shift+up"]);
assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.nextPrompt"), ["ctrl+shift+down"]);
assert.deepStrictEqual(keybindings.getKeys("tui.altScreen.top"), ["home"]);

View file

@ -5,6 +5,7 @@ import { Image } from "../src/components/image.ts";
import { ScrollView } from "../src/components/scroll-view.ts";
import { Text } from "../src/components/text.ts";
import { VStack } from "../src/components/v-stack.ts";
import { getKeybindings, KeybindingsManager, setKeybindings, TUI_KEYBINDINGS } from "../src/keybindings.ts";
import {
encodeKitty,
hyperlink,
@ -352,6 +353,35 @@ describe("TuiAltScreen", () => {
tui.stop();
});
it("scrolls the transcript by half a page with custom bindings", async () => {
const originalKeybindings = getKeybindings();
const terminal = new VirtualTerminal(20, 10);
const tui = new TuiAltScreen(terminal);
setKeybindings(
new KeybindingsManager(TUI_KEYBINDINGS, {
"tui.altScreen.halfPageUp": "ctrl+u",
"tui.altScreen.halfPageDown": "ctrl+d",
}),
);
try {
tui.addChild(new Text(Array.from({ length: 30 }, (_, index) => `line ${index + 1}`).join("\n"), 0, 0));
tui.start();
await terminal.waitForRender();
assert.strictEqual(tui.viewportTop, 20);
terminal.sendInput("\x15");
await terminal.waitForRender();
assert.strictEqual(tui.viewportTop, 15);
terminal.sendInput("\x04");
await terminal.waitForRender();
assert.strictEqual(tui.viewportTop, 20);
} finally {
tui.stop();
setKeybindings(originalKeybindings);
}
});
it("routes Ctrl-modified viewport navigation to the focused component", async () => {
const terminal = new VirtualTerminal(20, 6);
const tui = new TuiAltScreen(terminal);