@sigrun/tui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +3 -3
- package/dist/actions.d.ts +0 -78
- package/dist/actions.js +0 -12
- package/dist/api.d.ts +0 -147
- package/dist/api.js +0 -185
- package/dist/clipboard.d.ts +0 -20
- package/dist/clipboard.js +0 -19
- package/dist/editor-launch.d.ts +0 -34
- package/dist/editor-launch.js +0 -70
- package/dist/keymaps/emacs.d.ts +0 -18
- package/dist/keymaps/emacs.js +0 -133
- package/dist/keymaps/standard.d.ts +0 -17
- package/dist/keymaps/standard.js +0 -131
- package/dist/keymaps/types.d.ts +0 -43
- package/dist/keymaps/types.js +0 -28
- package/dist/keymaps/vim.d.ts +0 -48
- package/dist/keymaps/vim.js +0 -254
- package/dist/launch.d.ts +0 -149
- package/dist/launch.js +0 -433
- package/dist/sse.d.ts +0 -101
- package/dist/sse.js +0 -374
- package/dist/theme.d.ts +0 -51
- package/dist/theme.js +0 -105
- package/dist/ui/ambient.d.ts +0 -81
- package/dist/ui/ambient.js +0 -301
- package/dist/ui/app.d.ts +0 -209
- package/dist/ui/app.js +0 -1241
- package/dist/ui/composer.d.ts +0 -66
- package/dist/ui/composer.js +0 -125
- package/dist/ui/conversation-pane.d.ts +0 -123
- package/dist/ui/conversation-pane.js +0 -463
- package/dist/ui/help.d.ts +0 -15
- package/dist/ui/help.js +0 -80
- package/dist/ui/interactions.d.ts +0 -90
- package/dist/ui/interactions.js +0 -335
- package/dist/ui/picker.d.ts +0 -26
- package/dist/ui/picker.js +0 -130
- package/dist/ui/sidebar.d.ts +0 -76
- package/dist/ui/sidebar.js +0 -205
- package/dist/ui/status-bar.d.ts +0 -77
- package/dist/ui/status-bar.js +0 -146
package/dist/keymaps/emacs.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The emacs keymap (Task C5, UI refresh): modeless, chord-driven. `C-x`
|
|
3
|
-
* ("\x18") arms a 1 000 ms chord: `C-x b` conversation picker, `C-x o`
|
|
4
|
-
* focus cycle, `C-x C-c` quit, `C-x k` a consumed no-op (a mismatched
|
|
5
|
-
* follow-up is the emacs undefined-key beep — consumed, never typed).
|
|
6
|
-
* `M-x` opens the command picker; `C-s` / `C-r` search forward / backward
|
|
7
|
-
* (the app's search overlay turns them into next / previous while open);
|
|
8
|
-
* `C-g` cancels a live run, closes the search/picker (it passes through
|
|
9
|
-
* to the open overlay), or focuses the composer. `M-p` / `M-n` jump
|
|
10
|
-
* between prompts; `M-w` copy, `M-f` fork, `M-e` edit are outside-
|
|
11
|
-
* composer actions (in the composer `M-f` is the editor's word-forward —
|
|
12
|
-
* all three pass). `C-n` / `C-p` move outside the composer; `C-o`
|
|
13
|
-
* answers an interaction, `C-l` redraws, `C-h` opens the key reference.
|
|
14
|
-
* Every {@link RESERVED_KEYS} chord passes in every focus — ctrl+b is
|
|
15
|
-
* NEVER bound (no `C-x C-b` either).
|
|
16
|
-
*/
|
|
17
|
-
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
18
|
-
import { RESERVED_KEYS } from "./types.js";
|
|
19
|
-
/** The C-x chord window. */
|
|
20
|
-
const CHORD_MS = 1_000;
|
|
21
|
-
const BINDINGS = [
|
|
22
|
-
{ keys: "C-x b", action: "conversation picker" },
|
|
23
|
-
{ keys: "C-x o", action: "cycle focus" },
|
|
24
|
-
{ keys: "C-x C-c", action: "quit" },
|
|
25
|
-
{ keys: "M-x", action: "command picker" },
|
|
26
|
-
{ keys: "C-s / C-r", action: "search forward / backward" },
|
|
27
|
-
{ keys: "C-g", action: "cancel run / close / focus composer" },
|
|
28
|
-
{ keys: "M-p / M-n", action: "previous / next prompt" },
|
|
29
|
-
{ keys: "M-w", action: "copy turn" },
|
|
30
|
-
{ keys: "M-f", action: "fork here" },
|
|
31
|
-
{ keys: "M-e", action: "edit here" },
|
|
32
|
-
{ keys: "C-n / C-p", action: "move down / up" },
|
|
33
|
-
{ keys: "enter", action: "open / fold" },
|
|
34
|
-
{ keys: "C-o", action: "answer interaction" },
|
|
35
|
-
{ keys: "C-l", action: "redraw" },
|
|
36
|
-
{ keys: "C-h", action: "key reference" },
|
|
37
|
-
];
|
|
38
|
-
/** Match `data` against any reserved chord (they always pass). */
|
|
39
|
-
function isReserved(data) {
|
|
40
|
-
return RESERVED_KEYS.some((id) => matchesKey(data, id));
|
|
41
|
-
}
|
|
42
|
-
export function createEmacsKeymap() {
|
|
43
|
-
/** The armed C-x prefix timestamp (undefined = no chord pending). */
|
|
44
|
-
let chordAt;
|
|
45
|
-
/**
|
|
46
|
-
* Resolve the C-x chord against `data`. Returns the chord's result,
|
|
47
|
-
* "consume" when `data` ARMS the prefix, or undefined to process
|
|
48
|
-
* `data` fresh (no prefix pending, or the window expired).
|
|
49
|
-
*/
|
|
50
|
-
const chord = (data) => {
|
|
51
|
-
const now = Date.now();
|
|
52
|
-
if (chordAt !== undefined) {
|
|
53
|
-
const armed = chordAt;
|
|
54
|
-
chordAt = undefined;
|
|
55
|
-
if (now - armed <= CHORD_MS) {
|
|
56
|
-
if (data === "b")
|
|
57
|
-
return { type: "picker", kind: "conversation" };
|
|
58
|
-
if (data === "o")
|
|
59
|
-
return { type: "focus-cycle", direction: 1 };
|
|
60
|
-
if (data === "\x03")
|
|
61
|
-
return { type: "quit" }; // C-x C-c
|
|
62
|
-
if (data === "k")
|
|
63
|
-
return "consume"; // C-x k: a no-op, consumed
|
|
64
|
-
return "consume"; // undefined chord: the emacs beep
|
|
65
|
-
}
|
|
66
|
-
// Expired: `data` is processed fresh below.
|
|
67
|
-
}
|
|
68
|
-
if (data === "\x18") {
|
|
69
|
-
chordAt = now;
|
|
70
|
-
return "consume";
|
|
71
|
-
}
|
|
72
|
-
return undefined;
|
|
73
|
-
};
|
|
74
|
-
return {
|
|
75
|
-
name: "emacs",
|
|
76
|
-
modeLabel: () => undefined,
|
|
77
|
-
handle(data, ctx) {
|
|
78
|
-
// The composer's readline chords + the tmux prefix: NEVER bound.
|
|
79
|
-
// A reserved pass-through disarms a pending C-x — otherwise
|
|
80
|
-
// `C-x C-b b` would fire the picker with the user's "b".
|
|
81
|
-
if (isReserved(data)) {
|
|
82
|
-
chordAt = undefined;
|
|
83
|
-
return "pass";
|
|
84
|
-
}
|
|
85
|
-
// A modal overlay owns the keys; C-g passes too — the picker and
|
|
86
|
-
// the search overlay close themselves on it.
|
|
87
|
-
if (ctx.overlayOpen)
|
|
88
|
-
return "pass";
|
|
89
|
-
const chorded = chord(data);
|
|
90
|
-
if (chorded !== undefined)
|
|
91
|
-
return chorded;
|
|
92
|
-
if (data === "\x1bx")
|
|
93
|
-
return { type: "picker", kind: "command" };
|
|
94
|
-
if (data === "\x13")
|
|
95
|
-
return { type: "search", direction: "forward" };
|
|
96
|
-
if (data === "\x12")
|
|
97
|
-
return { type: "search", direction: "backward" };
|
|
98
|
-
if (data === "\x07") {
|
|
99
|
-
return ctx.busy
|
|
100
|
-
? { type: "cancel-run" }
|
|
101
|
-
: { type: "focus", target: "composer" };
|
|
102
|
-
}
|
|
103
|
-
if (data === "\x1bp")
|
|
104
|
-
return { type: "prompt-jump", delta: -1 };
|
|
105
|
-
if (data === "\x1bn")
|
|
106
|
-
return { type: "prompt-jump", delta: 1 };
|
|
107
|
-
if (data === "\x0f")
|
|
108
|
-
return { type: "answer-interaction" };
|
|
109
|
-
if (data === "\x0c")
|
|
110
|
-
return { type: "redraw" };
|
|
111
|
-
if (data === "\x08")
|
|
112
|
-
return { type: "help" };
|
|
113
|
-
if (ctx.focus === "composer")
|
|
114
|
-
return "pass";
|
|
115
|
-
// Outside the composer: the transcript/list actions.
|
|
116
|
-
if (data === "\x1bw")
|
|
117
|
-
return { type: "copy" };
|
|
118
|
-
if (data === "\x1bf")
|
|
119
|
-
return { type: "fork-here" };
|
|
120
|
-
if (data === "\x1be")
|
|
121
|
-
return { type: "edit-here" };
|
|
122
|
-
if (data === "\x0e")
|
|
123
|
-
return { type: "move", delta: 1 };
|
|
124
|
-
if (data === "\x10")
|
|
125
|
-
return { type: "move", delta: -1 };
|
|
126
|
-
if (matchesKey(data, Key.enter) || data === "\r")
|
|
127
|
-
return { type: "open" };
|
|
128
|
-
return "pass";
|
|
129
|
-
},
|
|
130
|
-
hints: () => ["C-x b chats", "M-x commands", "C-h keys"],
|
|
131
|
-
describe: () => BINDINGS,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The standard keymap (Task C3, UI refresh): readline-friendly globals
|
|
3
|
-
* plus vi-flavoured navigation OUTSIDE the composer. The composer owns
|
|
4
|
-
* every printable key and every {@link RESERVED_KEYS} chord — nothing
|
|
5
|
-
* here shadows a line-editing key (ctrl+d/ctrl+u included), and ctrl+b
|
|
6
|
-
* is never bound anywhere.
|
|
7
|
-
*
|
|
8
|
-
* Global (every focus): ctrl+p conversation picker, ctrl+n new
|
|
9
|
-
* conversation, ctrl+t cycle thinking, ctrl+o answer interaction,
|
|
10
|
-
* ctrl+l redraw, ctrl+c quit, tab/shift+tab focus cycle, Esc (abort a
|
|
11
|
-
* live run, else focus the composer; passes through while the
|
|
12
|
-
* composer's autocomplete is open). Outside the composer: arrows move,
|
|
13
|
-
* PgUp/PgDn page, Home/End jump, Enter open, y copy, f fork, e edit,
|
|
14
|
-
* / search, n/N next match, ? help.
|
|
15
|
-
*/
|
|
16
|
-
import { type Keymap } from "./types.js";
|
|
17
|
-
export declare function createStandardKeymap(): Keymap;
|
package/dist/keymaps/standard.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The standard keymap (Task C3, UI refresh): readline-friendly globals
|
|
3
|
-
* plus vi-flavoured navigation OUTSIDE the composer. The composer owns
|
|
4
|
-
* every printable key and every {@link RESERVED_KEYS} chord — nothing
|
|
5
|
-
* here shadows a line-editing key (ctrl+d/ctrl+u included), and ctrl+b
|
|
6
|
-
* is never bound anywhere.
|
|
7
|
-
*
|
|
8
|
-
* Global (every focus): ctrl+p conversation picker, ctrl+n new
|
|
9
|
-
* conversation, ctrl+t cycle thinking, ctrl+o answer interaction,
|
|
10
|
-
* ctrl+l redraw, ctrl+c quit, tab/shift+tab focus cycle, Esc (abort a
|
|
11
|
-
* live run, else focus the composer; passes through while the
|
|
12
|
-
* composer's autocomplete is open). Outside the composer: arrows move,
|
|
13
|
-
* PgUp/PgDn page, Home/End jump, Enter open, y copy, f fork, e edit,
|
|
14
|
-
* / search, n/N next match, ? help.
|
|
15
|
-
*/
|
|
16
|
-
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
17
|
-
import { RESERVED_KEYS } from "./types.js";
|
|
18
|
-
/** The help table (also the source of the status-bar hints). */
|
|
19
|
-
const BINDINGS = [
|
|
20
|
-
{ keys: "ctrl+p", action: "conversation picker" },
|
|
21
|
-
{ keys: "ctrl+n", action: "new conversation" },
|
|
22
|
-
{ keys: "ctrl+t", action: "cycle thinking" },
|
|
23
|
-
{ keys: "ctrl+o", action: "answer interaction" },
|
|
24
|
-
{ keys: "ctrl+l", action: "redraw" },
|
|
25
|
-
{ keys: "ctrl+c", action: "quit" },
|
|
26
|
-
{ keys: "tab / shift+tab", action: "cycle focus" },
|
|
27
|
-
{ keys: "esc", action: "abort run / focus composer" },
|
|
28
|
-
{ keys: "↑ ↓", action: "move (list/transcript)" },
|
|
29
|
-
{ keys: "pgup pgdn", action: "page (list/transcript)" },
|
|
30
|
-
{ keys: "home end", action: "top / bottom" },
|
|
31
|
-
{ keys: "enter", action: "open / select" },
|
|
32
|
-
{ keys: "y", action: "copy turn" },
|
|
33
|
-
{ keys: "f", action: "fork here" },
|
|
34
|
-
{ keys: "e", action: "edit here" },
|
|
35
|
-
{ keys: "/", action: "search transcript" },
|
|
36
|
-
{ keys: "n / N", action: "next / previous match" },
|
|
37
|
-
{ keys: "?", action: "help" },
|
|
38
|
-
];
|
|
39
|
-
/** Match `data` against any reserved chord (they always pass). */
|
|
40
|
-
function isReserved(data) {
|
|
41
|
-
return RESERVED_KEYS.some((id) => matchesKey(data, id));
|
|
42
|
-
}
|
|
43
|
-
export function createStandardKeymap() {
|
|
44
|
-
const global = (data, ctx) => {
|
|
45
|
-
if (matchesKey(data, Key.ctrl("p")))
|
|
46
|
-
return { type: "picker", kind: "conversation" };
|
|
47
|
-
if (matchesKey(data, Key.ctrl("n")))
|
|
48
|
-
return { type: "new-conversation" };
|
|
49
|
-
if (matchesKey(data, Key.ctrl("t")))
|
|
50
|
-
return { type: "cycle-thinking" };
|
|
51
|
-
if (matchesKey(data, Key.ctrl("o")))
|
|
52
|
-
return { type: "answer-interaction" };
|
|
53
|
-
if (matchesKey(data, Key.ctrl("l")))
|
|
54
|
-
return { type: "redraw" };
|
|
55
|
-
if (matchesKey(data, Key.ctrl("c")))
|
|
56
|
-
return { type: "quit" };
|
|
57
|
-
if (matchesKey(data, Key.tab) || matchesKey(data, "tab")) {
|
|
58
|
-
return ctx.autocompleteOpen ? "pass" : { type: "focus-cycle", direction: 1 };
|
|
59
|
-
}
|
|
60
|
-
if (matchesKey(data, "shift+tab"))
|
|
61
|
-
return { type: "focus-cycle", direction: -1 };
|
|
62
|
-
if (matchesKey(data, Key.escape)) {
|
|
63
|
-
if (ctx.autocompleteOpen)
|
|
64
|
-
return "pass";
|
|
65
|
-
if (ctx.busy)
|
|
66
|
-
return { type: "cancel-run" };
|
|
67
|
-
return { type: "focus", target: "composer" };
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
70
|
-
};
|
|
71
|
-
const outsideComposer = (data) => {
|
|
72
|
-
if (matchesKey(data, Key.up) || matchesKey(data, "up"))
|
|
73
|
-
return { type: "move", delta: -1 };
|
|
74
|
-
if (matchesKey(data, Key.down) || matchesKey(data, "down"))
|
|
75
|
-
return { type: "move", delta: 1 };
|
|
76
|
-
if (matchesKey(data, Key.pageUp) || matchesKey(data, "pageUp"))
|
|
77
|
-
return { type: "page", direction: -1, half: false };
|
|
78
|
-
if (matchesKey(data, Key.pageDown) || matchesKey(data, "pageDown"))
|
|
79
|
-
return { type: "page", direction: 1, half: false };
|
|
80
|
-
if (matchesKey(data, Key.home) || matchesKey(data, "home"))
|
|
81
|
-
return { type: "edge", to: "top" };
|
|
82
|
-
if (matchesKey(data, Key.end) || matchesKey(data, "end"))
|
|
83
|
-
return { type: "edge", to: "bottom" };
|
|
84
|
-
if (matchesKey(data, Key.enter))
|
|
85
|
-
return { type: "open" };
|
|
86
|
-
if (data === "y")
|
|
87
|
-
return { type: "copy" };
|
|
88
|
-
if (data === "f")
|
|
89
|
-
return { type: "fork-here" };
|
|
90
|
-
if (data === "e")
|
|
91
|
-
return { type: "edit-here" };
|
|
92
|
-
if (data === "/")
|
|
93
|
-
return { type: "search", direction: "forward" };
|
|
94
|
-
if (data === "n")
|
|
95
|
-
return { type: "search-next", reverse: false };
|
|
96
|
-
if (data === "N")
|
|
97
|
-
return { type: "search-next", reverse: true };
|
|
98
|
-
if (data === "?")
|
|
99
|
-
return { type: "help" };
|
|
100
|
-
return undefined;
|
|
101
|
-
};
|
|
102
|
-
return {
|
|
103
|
-
name: "standard",
|
|
104
|
-
modeLabel: () => undefined,
|
|
105
|
-
handle(data, ctx) {
|
|
106
|
-
// The composer's readline chords + the tmux prefix: NEVER bound.
|
|
107
|
-
if (isReserved(data))
|
|
108
|
-
return "pass";
|
|
109
|
-
// A modal overlay owns the keys; only quit still works.
|
|
110
|
-
if (ctx.overlayOpen) {
|
|
111
|
-
return matchesKey(data, Key.ctrl("c")) ? { type: "quit" } : "pass";
|
|
112
|
-
}
|
|
113
|
-
const globalResult = global(data, ctx);
|
|
114
|
-
if (globalResult !== undefined)
|
|
115
|
-
return globalResult;
|
|
116
|
-
if (ctx.focus !== "composer") {
|
|
117
|
-
const navResult = outsideComposer(data);
|
|
118
|
-
if (navResult !== undefined)
|
|
119
|
-
return navResult;
|
|
120
|
-
}
|
|
121
|
-
return "pass";
|
|
122
|
-
},
|
|
123
|
-
hints(ctx) {
|
|
124
|
-
if (ctx.focus === "composer") {
|
|
125
|
-
return ["Tab focus", "^P chats", "? keys"];
|
|
126
|
-
}
|
|
127
|
-
return ["enter open", "y copy", "f fork", "e edit", "/ search", "? help"];
|
|
128
|
-
},
|
|
129
|
-
describe: () => BINDINGS,
|
|
130
|
-
};
|
|
131
|
-
}
|
package/dist/keymaps/types.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The keymap contract (Task C3, UI refresh): a keymap maps raw terminal
|
|
3
|
-
* input to an {@link Action}, `"pass"` (the focused component — usually
|
|
4
|
-
* the composer — gets the key), or `"consume"` (swallow silently). The
|
|
5
|
-
* launcher's single input listener asks the keymap first, then dispatches
|
|
6
|
-
* / consumes / passes.
|
|
7
|
-
*
|
|
8
|
-
* RESERVED_KEYS are the composer's line-editing chords (readline-style)
|
|
9
|
-
* and the tmux prefix: a keymap must NEVER bind them — it returns "pass"
|
|
10
|
-
* for every one in every focus so the editor (or tmux) always sees them.
|
|
11
|
-
* Ctrl+B in particular is the user's hard rule.
|
|
12
|
-
*/
|
|
13
|
-
import type { Action, Focus } from "../actions.js";
|
|
14
|
-
/** The state a keymap decision depends on. */
|
|
15
|
-
export interface KeyContext {
|
|
16
|
-
readonly focus: Focus;
|
|
17
|
-
/** A live run is in flight (Esc aborts it). */
|
|
18
|
-
readonly busy: boolean;
|
|
19
|
-
/** The composer's autocomplete dropdown is open (Esc/Tab pass to it). */
|
|
20
|
-
readonly autocompleteOpen: boolean;
|
|
21
|
-
/** A modal overlay (picker/search/answer) owns the keys. */
|
|
22
|
-
readonly overlayOpen: boolean;
|
|
23
|
-
}
|
|
24
|
-
/** What the launcher does with the key: run the action, swallow, or pass. */
|
|
25
|
-
export type KeyResult = Action | "pass" | "consume";
|
|
26
|
-
export interface Keymap {
|
|
27
|
-
readonly name: "standard" | "vim" | "emacs";
|
|
28
|
-
/** The status-bar mode chip text; undefined = no chip (standard). */
|
|
29
|
-
modeLabel(): string | undefined;
|
|
30
|
-
handle(data: string, ctx: KeyContext): KeyResult;
|
|
31
|
-
/** The one-line hint cluster for the status bar. */
|
|
32
|
-
hints(ctx: KeyContext): readonly string[];
|
|
33
|
-
/** The help listing: every binding with a human description. */
|
|
34
|
-
describe(): readonly {
|
|
35
|
-
keys: string;
|
|
36
|
-
action: string;
|
|
37
|
-
}[];
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Keys no keymap may bind: the composer's readline chords plus the tmux
|
|
41
|
-
* prefix (ctrl+b). Keymaps return "pass" for all of them in every focus.
|
|
42
|
-
*/
|
|
43
|
-
export declare const RESERVED_KEYS: readonly string[];
|
package/dist/keymaps/types.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The keymap contract (Task C3, UI refresh): a keymap maps raw terminal
|
|
3
|
-
* input to an {@link Action}, `"pass"` (the focused component — usually
|
|
4
|
-
* the composer — gets the key), or `"consume"` (swallow silently). The
|
|
5
|
-
* launcher's single input listener asks the keymap first, then dispatches
|
|
6
|
-
* / consumes / passes.
|
|
7
|
-
*
|
|
8
|
-
* RESERVED_KEYS are the composer's line-editing chords (readline-style)
|
|
9
|
-
* and the tmux prefix: a keymap must NEVER bind them — it returns "pass"
|
|
10
|
-
* for every one in every focus so the editor (or tmux) always sees them.
|
|
11
|
-
* Ctrl+B in particular is the user's hard rule.
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Keys no keymap may bind: the composer's readline chords plus the tmux
|
|
15
|
-
* prefix (ctrl+b). Keymaps return "pass" for all of them in every focus.
|
|
16
|
-
*/
|
|
17
|
-
export const RESERVED_KEYS = [
|
|
18
|
-
"ctrl+b",
|
|
19
|
-
"ctrl+a",
|
|
20
|
-
"ctrl+e",
|
|
21
|
-
"ctrl+f",
|
|
22
|
-
"ctrl+k",
|
|
23
|
-
"ctrl+u",
|
|
24
|
-
"ctrl+w",
|
|
25
|
-
"ctrl+y",
|
|
26
|
-
"ctrl+d",
|
|
27
|
-
"ctrl+j",
|
|
28
|
-
];
|
package/dist/keymaps/vim.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The vim keymap (Task C4, UI refresh): real insert/normal/command modes
|
|
3
|
-
* over the C3 action layer. Starts in INSERT with the composer focused —
|
|
4
|
-
* every key types through except Esc (→ NORMAL, focus transcript),
|
|
5
|
-
* Ctrl+C (abort a live run; otherwise consumed — quitting is ZZ or :q),
|
|
6
|
-
* and Ctrl+O (answer an interaction). Every {@link RESERVED_KEYS} chord
|
|
7
|
-
* passes in INSERT (the composer's readline chords and the tmux prefix
|
|
8
|
-
* are never shadowed; ctrl+b is NEVER bound anywhere).
|
|
9
|
-
*
|
|
10
|
-
* NORMAL owns the transcript/list: j/k move, h/l focus list/transcript,
|
|
11
|
-
* ctrl+d/ctrl+u half-page (bound here — RESERVED exists to protect the
|
|
12
|
-
* composer's editing, and in NORMAL the composer is not focused),
|
|
13
|
-
* gg/G edges, {/} prompt jumps, Enter/za fold, o open, y/yy copy,
|
|
14
|
-
* f fork, e edit, v edit-in-vim, //?/n/N search, i/a back to INSERT,
|
|
15
|
-
* `:` the command line, ctrl+p picker, ctrl+l redraw, ZZ quit. UNBOUND
|
|
16
|
-
* PRINTABLE KEYS ARE CONSUMED — they must never leak into the composer.
|
|
17
|
-
*
|
|
18
|
-
* COMMAND accumulates a line after `:` (the app renders ":"+buffer in
|
|
19
|
-
* the status bar via {@link VimKeymap.getCommandLine}); Enter dispatches
|
|
20
|
-
* run-command, Esc cancels, Tab completes the command name.
|
|
21
|
-
*
|
|
22
|
-
* The two-key chords (gg, yy, za, ZZ) share one pending-key window of
|
|
23
|
-
* 500 ms; a timed-out or mismatched prefix is consumed silently.
|
|
24
|
-
*/
|
|
25
|
-
import { type Keymap } from "./types.js";
|
|
26
|
-
export type VimMode = "insert" | "normal" | "command";
|
|
27
|
-
/** The keymap plus the mode/command-line seam the app polls after each key. */
|
|
28
|
-
export interface VimKeymap extends Keymap {
|
|
29
|
-
readonly mode: VimMode;
|
|
30
|
-
/** The ":" line while in command mode; undefined otherwise. */
|
|
31
|
-
getCommandLine(): string | undefined;
|
|
32
|
-
/**
|
|
33
|
-
* Drop to INSERT (the app calls this after an external edit-in-vim so
|
|
34
|
-
* the mode agrees with the restored composer focus).
|
|
35
|
-
*/
|
|
36
|
-
enterInsertMode(): void;
|
|
37
|
-
}
|
|
38
|
-
/** Options for {@link createVimKeymap}. */
|
|
39
|
-
export interface VimKeymapOptions {
|
|
40
|
-
/**
|
|
41
|
-
* The slash-command catalog's names (C3's picker source); they join
|
|
42
|
-
* COMMAND_NAMES in the Tab completion table as `"/"`-prefixed
|
|
43
|
-
* entries. Defaults to none (the launcher fills a shared array once
|
|
44
|
-
* the catalog fetch lands).
|
|
45
|
-
*/
|
|
46
|
-
readonly catalogNames?: readonly string[];
|
|
47
|
-
}
|
|
48
|
-
export declare function createVimKeymap(options?: VimKeymapOptions): VimKeymap;
|
package/dist/keymaps/vim.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The vim keymap (Task C4, UI refresh): real insert/normal/command modes
|
|
3
|
-
* over the C3 action layer. Starts in INSERT with the composer focused —
|
|
4
|
-
* every key types through except Esc (→ NORMAL, focus transcript),
|
|
5
|
-
* Ctrl+C (abort a live run; otherwise consumed — quitting is ZZ or :q),
|
|
6
|
-
* and Ctrl+O (answer an interaction). Every {@link RESERVED_KEYS} chord
|
|
7
|
-
* passes in INSERT (the composer's readline chords and the tmux prefix
|
|
8
|
-
* are never shadowed; ctrl+b is NEVER bound anywhere).
|
|
9
|
-
*
|
|
10
|
-
* NORMAL owns the transcript/list: j/k move, h/l focus list/transcript,
|
|
11
|
-
* ctrl+d/ctrl+u half-page (bound here — RESERVED exists to protect the
|
|
12
|
-
* composer's editing, and in NORMAL the composer is not focused),
|
|
13
|
-
* gg/G edges, {/} prompt jumps, Enter/za fold, o open, y/yy copy,
|
|
14
|
-
* f fork, e edit, v edit-in-vim, //?/n/N search, i/a back to INSERT,
|
|
15
|
-
* `:` the command line, ctrl+p picker, ctrl+l redraw, ZZ quit. UNBOUND
|
|
16
|
-
* PRINTABLE KEYS ARE CONSUMED — they must never leak into the composer.
|
|
17
|
-
*
|
|
18
|
-
* COMMAND accumulates a line after `:` (the app renders ":"+buffer in
|
|
19
|
-
* the status bar via {@link VimKeymap.getCommandLine}); Enter dispatches
|
|
20
|
-
* run-command, Esc cancels, Tab completes the command name.
|
|
21
|
-
*
|
|
22
|
-
* The two-key chords (gg, yy, za, ZZ) share one pending-key window of
|
|
23
|
-
* 500 ms; a timed-out or mismatched prefix is consumed silently.
|
|
24
|
-
*/
|
|
25
|
-
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
26
|
-
import { RESERVED_KEYS } from "./types.js";
|
|
27
|
-
/** The two-key chord window (gg, yy, za, ZZ). */
|
|
28
|
-
const CHORD_MS = 500;
|
|
29
|
-
/** The command-mode completion table (Tab completes the command name). */
|
|
30
|
-
const COMMAND_NAMES = ["new", "fork", "model", "set", "b", "ls", "q", "quit"];
|
|
31
|
-
const BINDINGS = [
|
|
32
|
-
{ keys: "esc (insert)", action: "normal mode" },
|
|
33
|
-
{ keys: "i / a", action: "insert mode" },
|
|
34
|
-
{ keys: "j / k", action: "move down / up" },
|
|
35
|
-
{ keys: "h / l", action: "focus list / transcript" },
|
|
36
|
-
{ keys: "ctrl+d / ctrl+u", action: "half page down / up" },
|
|
37
|
-
{ keys: "gg / G", action: "top / bottom" },
|
|
38
|
-
{ keys: "{ / }", action: "previous / next prompt" },
|
|
39
|
-
{ keys: "enter / za", action: "toggle fold" },
|
|
40
|
-
{ keys: "o", action: "open" },
|
|
41
|
-
{ keys: "y / yy", action: "copy turn" },
|
|
42
|
-
{ keys: "f", action: "fork here" },
|
|
43
|
-
{ keys: "e", action: "edit here" },
|
|
44
|
-
{ keys: "v", action: "edit in vim" },
|
|
45
|
-
{ keys: "/ / ?", action: "search forward / backward" },
|
|
46
|
-
{ keys: "n / N", action: "next / previous match" },
|
|
47
|
-
{ keys: ":", action: "command line" },
|
|
48
|
-
{ keys: "ctrl+p", action: "conversation picker" },
|
|
49
|
-
{ keys: "ctrl+o", action: "answer interaction" },
|
|
50
|
-
{ keys: "ctrl+l", action: "redraw" },
|
|
51
|
-
{ keys: "ctrl+c", action: "abort run (busy)" },
|
|
52
|
-
{ keys: "g?", action: "key reference" },
|
|
53
|
-
{ keys: "ZZ / :q", action: "quit" },
|
|
54
|
-
];
|
|
55
|
-
function isReserved(data) {
|
|
56
|
-
return RESERVED_KEYS.some((id) => matchesKey(data, id));
|
|
57
|
-
}
|
|
58
|
-
/** A printable single character (space through ~, plus unicode text). */
|
|
59
|
-
function isPrintable(data) {
|
|
60
|
-
return data.length === 1 && data >= " ";
|
|
61
|
-
}
|
|
62
|
-
export function createVimKeymap(options) {
|
|
63
|
-
const catalogNames = options?.catalogNames ?? [];
|
|
64
|
-
/** Tab completion table: the vim-native commands plus `/`-catalog names. */
|
|
65
|
-
const completionNames = () => [
|
|
66
|
-
...COMMAND_NAMES,
|
|
67
|
-
...catalogNames.map((name) => `/${name}`),
|
|
68
|
-
];
|
|
69
|
-
let mode = "insert";
|
|
70
|
-
let commandLine = "";
|
|
71
|
-
/** The pending chord prefix ("g" | "y" | "z" | "Z") and its timestamp. */
|
|
72
|
-
let pending;
|
|
73
|
-
/**
|
|
74
|
-
* Resolve the pending two-key chord (gg → top, ZZ → quit, za → fold)
|
|
75
|
-
* against `data`. Returns the chord's action, "consume" when `data`
|
|
76
|
-
* STARTS a chord, or undefined to process `data` fresh (a mismatched
|
|
77
|
-
* or timed-out follow-up).
|
|
78
|
-
*/
|
|
79
|
-
const chord = (data) => {
|
|
80
|
-
const now = Date.now();
|
|
81
|
-
if (pending !== undefined) {
|
|
82
|
-
const prefix = pending;
|
|
83
|
-
pending = undefined;
|
|
84
|
-
if (now - prefix.at <= CHORD_MS) {
|
|
85
|
-
if (prefix.key === "g" && data === "g")
|
|
86
|
-
return { type: "edge", to: "top" };
|
|
87
|
-
// g? is vim normal's help key — "?" stays search-backward.
|
|
88
|
-
if (prefix.key === "g" && data === "?")
|
|
89
|
-
return { type: "help" };
|
|
90
|
-
if (prefix.key === "Z" && data === "Z")
|
|
91
|
-
return { type: "quit" };
|
|
92
|
-
if (prefix.key === "z" && data === "a")
|
|
93
|
-
return { type: "toggle-fold" };
|
|
94
|
-
}
|
|
95
|
-
// Mismatch/timeout: `data` is processed fresh below.
|
|
96
|
-
}
|
|
97
|
-
if (data === "g" || data === "Z" || data === "z") {
|
|
98
|
-
pending = { key: data, at: now };
|
|
99
|
-
return "consume";
|
|
100
|
-
}
|
|
101
|
-
return undefined;
|
|
102
|
-
};
|
|
103
|
-
const insert = (data, ctx) => {
|
|
104
|
-
// The composer's readline chords + the tmux prefix: NEVER bound.
|
|
105
|
-
if (isReserved(data))
|
|
106
|
-
return "pass";
|
|
107
|
-
if (matchesKey(data, Key.escape)) {
|
|
108
|
-
if (ctx.autocompleteOpen)
|
|
109
|
-
return "pass";
|
|
110
|
-
mode = "normal";
|
|
111
|
-
return { type: "focus", target: "transcript" };
|
|
112
|
-
}
|
|
113
|
-
if (matchesKey(data, Key.ctrl("c"))) {
|
|
114
|
-
return ctx.busy ? { type: "cancel-run" } : "consume";
|
|
115
|
-
}
|
|
116
|
-
if (matchesKey(data, Key.ctrl("o")))
|
|
117
|
-
return { type: "answer-interaction" };
|
|
118
|
-
return "pass";
|
|
119
|
-
};
|
|
120
|
-
const normal = (data, ctx) => {
|
|
121
|
-
// ctrl+b / ctrl+f are NOT bound in vim normal — reserved chords pass.
|
|
122
|
-
const chorded = chord(data);
|
|
123
|
-
if (chorded !== undefined)
|
|
124
|
-
return chorded;
|
|
125
|
-
if (data === "y")
|
|
126
|
-
return { type: "copy" }; // yy is two copies — same action
|
|
127
|
-
if (data === "j")
|
|
128
|
-
return { type: "move", delta: 1 };
|
|
129
|
-
if (data === "k")
|
|
130
|
-
return { type: "move", delta: -1 };
|
|
131
|
-
if (data === "h")
|
|
132
|
-
return { type: "focus", target: "list" };
|
|
133
|
-
if (data === "l")
|
|
134
|
-
return { type: "focus", target: "transcript" };
|
|
135
|
-
if (matchesKey(data, Key.ctrl("d")))
|
|
136
|
-
return { type: "page", direction: 1, half: true };
|
|
137
|
-
if (matchesKey(data, Key.ctrl("u")))
|
|
138
|
-
return { type: "page", direction: -1, half: true };
|
|
139
|
-
if (data === "G")
|
|
140
|
-
return { type: "edge", to: "bottom" };
|
|
141
|
-
if (data === "{")
|
|
142
|
-
return { type: "prompt-jump", delta: -1 };
|
|
143
|
-
if (data === "}")
|
|
144
|
-
return { type: "prompt-jump", delta: 1 };
|
|
145
|
-
if (matchesKey(data, Key.enter))
|
|
146
|
-
return { type: "toggle-fold" };
|
|
147
|
-
if (data === "o")
|
|
148
|
-
return { type: "open" };
|
|
149
|
-
if (data === "f")
|
|
150
|
-
return { type: "fork-here" };
|
|
151
|
-
if (data === "e")
|
|
152
|
-
return { type: "edit-here" };
|
|
153
|
-
if (data === "v")
|
|
154
|
-
return { type: "edit-in-vim" };
|
|
155
|
-
if (data === "/")
|
|
156
|
-
return { type: "search", direction: "forward" };
|
|
157
|
-
if (data === "?")
|
|
158
|
-
return { type: "search", direction: "backward" };
|
|
159
|
-
if (data === "n")
|
|
160
|
-
return { type: "search-next", reverse: false };
|
|
161
|
-
if (data === "N")
|
|
162
|
-
return { type: "search-next", reverse: true };
|
|
163
|
-
if (data === "i" || data === "a") {
|
|
164
|
-
mode = "insert";
|
|
165
|
-
return { type: "focus", target: "composer" };
|
|
166
|
-
}
|
|
167
|
-
if (data === ":") {
|
|
168
|
-
mode = "command";
|
|
169
|
-
commandLine = "";
|
|
170
|
-
return "consume";
|
|
171
|
-
}
|
|
172
|
-
if (matchesKey(data, Key.ctrl("p")))
|
|
173
|
-
return { type: "picker", kind: "conversation" };
|
|
174
|
-
if (matchesKey(data, Key.ctrl("l")))
|
|
175
|
-
return { type: "redraw" };
|
|
176
|
-
if (matchesKey(data, Key.ctrl("c"))) {
|
|
177
|
-
return ctx.busy ? { type: "cancel-run" } : "consume";
|
|
178
|
-
}
|
|
179
|
-
// Reserved chords the composer owns pass even here (they are no-ops
|
|
180
|
-
// in a non-composer focus, but the invariant is one rule).
|
|
181
|
-
if (isReserved(data))
|
|
182
|
-
return "pass";
|
|
183
|
-
// UNBOUND PRINTABLE KEYS ARE CONSUMED: never typed into the composer.
|
|
184
|
-
if (isPrintable(data))
|
|
185
|
-
return "consume";
|
|
186
|
-
return "consume";
|
|
187
|
-
};
|
|
188
|
-
const command = (data) => {
|
|
189
|
-
if (matchesKey(data, Key.escape)) {
|
|
190
|
-
mode = "normal";
|
|
191
|
-
commandLine = "";
|
|
192
|
-
return "consume";
|
|
193
|
-
}
|
|
194
|
-
if (matchesKey(data, Key.enter) || data === "\r") {
|
|
195
|
-
const line = commandLine;
|
|
196
|
-
mode = "normal";
|
|
197
|
-
commandLine = "";
|
|
198
|
-
if (line.trim() === "")
|
|
199
|
-
return "consume";
|
|
200
|
-
return { type: "run-command", line };
|
|
201
|
-
}
|
|
202
|
-
if (matchesKey(data, Key.backspace) || data === "\x7f" || data === "\b") {
|
|
203
|
-
commandLine = commandLine.slice(0, -1);
|
|
204
|
-
return "consume";
|
|
205
|
-
}
|
|
206
|
-
if (matchesKey(data, Key.tab) || data === "\t") {
|
|
207
|
-
const match = completionNames().find((name) => name.startsWith(commandLine) && name !== commandLine);
|
|
208
|
-
if (match !== undefined)
|
|
209
|
-
commandLine = match;
|
|
210
|
-
return "consume";
|
|
211
|
-
}
|
|
212
|
-
if (isPrintable(data)) {
|
|
213
|
-
commandLine += data;
|
|
214
|
-
return "consume";
|
|
215
|
-
}
|
|
216
|
-
return "consume";
|
|
217
|
-
};
|
|
218
|
-
return {
|
|
219
|
-
name: "vim",
|
|
220
|
-
get mode() {
|
|
221
|
-
return mode;
|
|
222
|
-
},
|
|
223
|
-
getCommandLine: () => (mode === "command" ? `:${commandLine}` : undefined),
|
|
224
|
-
enterInsertMode: () => {
|
|
225
|
-
mode = "insert";
|
|
226
|
-
},
|
|
227
|
-
modeLabel: () => {
|
|
228
|
-
if (mode === "insert")
|
|
229
|
-
return "-- INSERT --";
|
|
230
|
-
if (mode === "normal")
|
|
231
|
-
return "-- NORMAL --";
|
|
232
|
-
return undefined; // the status bar shows the ":" line instead
|
|
233
|
-
},
|
|
234
|
-
handle(data, ctx) {
|
|
235
|
-
// A modal overlay owns the keys; only its cancel key is held back.
|
|
236
|
-
if (ctx.overlayOpen) {
|
|
237
|
-
return matchesKey(data, Key.escape) ? "consume" : "pass";
|
|
238
|
-
}
|
|
239
|
-
if (mode === "insert")
|
|
240
|
-
return insert(data, ctx);
|
|
241
|
-
if (mode === "normal")
|
|
242
|
-
return normal(data, ctx);
|
|
243
|
-
return command(data);
|
|
244
|
-
},
|
|
245
|
-
hints(ctx) {
|
|
246
|
-
if (mode === "command")
|
|
247
|
-
return ["enter run", "esc cancel", "tab complete"];
|
|
248
|
-
if (mode === "insert" || ctx.focus === "composer")
|
|
249
|
-
return ["Esc normal"];
|
|
250
|
-
return ["i insert", ": command", "g? keys"];
|
|
251
|
-
},
|
|
252
|
-
describe: () => BINDINGS,
|
|
253
|
-
};
|
|
254
|
-
}
|