@visual-z/xeno 0.1.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/README.md +498 -0
- package/docs/install.md +130 -0
- package/package.json +43 -0
- package/src/apps/approve.ts +49 -0
- package/src/apps/cli.ts +37 -0
- package/src/apps/commands.ts +226 -0
- package/src/apps/completions.ts +28 -0
- package/src/apps/input.ts +297 -0
- package/src/apps/render.ts +207 -0
- package/src/apps/shell.ts +2353 -0
- package/src/apps/startup.ts +1302 -0
- package/src/core/benchmark.ts +195 -0
- package/src/core/boundary.ts +174 -0
- package/src/core/churn.ts +59 -0
- package/src/core/completion-review.ts +66 -0
- package/src/core/context-policy.ts +60 -0
- package/src/core/context-window.ts +54 -0
- package/src/core/continuation.ts +114 -0
- package/src/core/cycle.ts +17 -0
- package/src/core/data-query.ts +201 -0
- package/src/core/deadline.ts +39 -0
- package/src/core/derive.ts +489 -0
- package/src/core/edit-plan.ts +200 -0
- package/src/core/environment.ts +29 -0
- package/src/core/event-validation.ts +99 -0
- package/src/core/events.ts +486 -0
- package/src/core/evidence-links.ts +71 -0
- package/src/core/evidence.ts +89 -0
- package/src/core/execution.ts +12 -0
- package/src/core/exhaustive.ts +18 -0
- package/src/core/experiment.ts +40 -0
- package/src/core/export.ts +169 -0
- package/src/core/file-references.ts +35 -0
- package/src/core/frontmatter.ts +213 -0
- package/src/core/handoff.ts +397 -0
- package/src/core/harness-comparison.ts +90 -0
- package/src/core/history-query.ts +103 -0
- package/src/core/history.ts +100 -0
- package/src/core/json-event.ts +135 -0
- package/src/core/json-pointer.ts +46 -0
- package/src/core/keymap.ts +241 -0
- package/src/core/observation-view.ts +87 -0
- package/src/core/outcome.ts +45 -0
- package/src/core/overflow.ts +59 -0
- package/src/core/permissions.ts +182 -0
- package/src/core/policy-evaluation.ts +55 -0
- package/src/core/progress.ts +101 -0
- package/src/core/project.ts +372 -0
- package/src/core/prompts.ts +83 -0
- package/src/core/queue.ts +60 -0
- package/src/core/read-bounds.ts +15 -0
- package/src/core/recall-analysis.ts +90 -0
- package/src/core/recall-trial.ts +153 -0
- package/src/core/response-recovery.ts +46 -0
- package/src/core/scheduling.ts +8 -0
- package/src/core/settings.ts +174 -0
- package/src/core/skills.ts +121 -0
- package/src/core/stall.ts +48 -0
- package/src/core/text-normalize.ts +81 -0
- package/src/core/theme.ts +179 -0
- package/src/core/tokens.ts +149 -0
- package/src/core/tool-contract.ts +102 -0
- package/src/core/tool-input.ts +38 -0
- package/src/core/transcript.ts +60 -0
- package/src/core/window-trial.ts +65 -0
- package/src/core/work-state.ts +103 -0
- package/src/core/yaml.ts +282 -0
- package/src/index.ts +10 -0
- package/src/runtime/auth.ts +255 -0
- package/src/runtime/benchmark.ts +187 -0
- package/src/runtime/catalog.ts +365 -0
- package/src/runtime/evidence-notes.ts +33 -0
- package/src/runtime/exec-sessions.ts +214 -0
- package/src/runtime/execution-control.ts +39 -0
- package/src/runtime/harness-comparison.ts +356 -0
- package/src/runtime/history-tools.ts +108 -0
- package/src/runtime/loop.ts +1983 -0
- package/src/runtime/model.ts +614 -0
- package/src/runtime/models-fetch.ts +155 -0
- package/src/runtime/oauth-device.ts +86 -0
- package/src/runtime/oauth.ts +759 -0
- package/src/runtime/openai-responses.ts +286 -0
- package/src/runtime/openai.ts +675 -0
- package/src/runtime/provider.ts +498 -0
- package/src/runtime/recall-trial.ts +226 -0
- package/src/runtime/resume.ts +66 -0
- package/src/runtime/retry.ts +297 -0
- package/src/runtime/tools.ts +1193 -0
- package/src/runtime/usage.ts +225 -0
- package/src/runtime/window-trial.ts +140 -0
- package/src/runtime/working-notes.ts +30 -0
- package/src/store/atomic.ts +31 -0
- package/src/store/auth-store.ts +50 -0
- package/src/store/blobs.ts +142 -0
- package/src/store/command.ts +75 -0
- package/src/store/config.ts +69 -0
- package/src/store/context-files.ts +356 -0
- package/src/store/file-index.ts +81 -0
- package/src/store/git-branch.ts +35 -0
- package/src/store/history-search.ts +19 -0
- package/src/store/log.ts +642 -0
- package/src/store/models.ts +41 -0
- package/src/store/observations.ts +181 -0
- package/src/store/query-source.ts +44 -0
- package/src/store/recovery.ts +39 -0
- package/src/store/replay.ts +63 -0
- package/src/store/resources.ts +361 -0
- package/src/store/restore.ts +302 -0
- package/src/store/search.ts +644 -0
- package/src/store/serial.ts +13 -0
- package/src/store/sessions.ts +463 -0
- package/src/store/trust.ts +69 -0
- package/src/store/workspace.ts +256 -0
- package/src/store/worktree.ts +233 -0
- package/src/tui/ansi.ts +67 -0
- package/src/tui/autocomplete.ts +29 -0
- package/src/tui/complete.ts +87 -0
- package/src/tui/diff.ts +160 -0
- package/src/tui/editor.ts +893 -0
- package/src/tui/exhaustive.ts +14 -0
- package/src/tui/fuzzy.ts +75 -0
- package/src/tui/host.ts +223 -0
- package/src/tui/input.ts +69 -0
- package/src/tui/keys.ts +378 -0
- package/src/tui/markdown.ts +17 -0
- package/src/tui/render.ts +153 -0
- package/src/tui/select.ts +240 -0
- package/src/tui/settings.ts +40 -0
- package/src/tui/statusline.ts +162 -0
- package/src/tui/style.ts +205 -0
- package/src/tui/terminal.ts +30 -0
- package/src/tui/text.ts +52 -0
- package/src/tui/toolview.ts +386 -0
- package/src/tui/transcript.ts +71 -0
- package/src/tui/tree.ts +405 -0
- package/src/tui/width.ts +567 -0
- package/src/vendor/pi-tui/LICENSE.upstream +21 -0
- package/src/vendor/pi-tui/VENDOR.md +23 -0
- package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
- package/src/vendor/pi-tui/autocomplete.ts +835 -0
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
- package/src/vendor/pi-tui/components/box.ts +167 -0
- package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
- package/src/vendor/pi-tui/components/editor.ts +2470 -0
- package/src/vendor/pi-tui/components/h-stack.ts +44 -0
- package/src/vendor/pi-tui/components/image.ts +127 -0
- package/src/vendor/pi-tui/components/input.ts +494 -0
- package/src/vendor/pi-tui/components/loader.ts +101 -0
- package/src/vendor/pi-tui/components/markdown.ts +1015 -0
- package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
- package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
- package/src/vendor/pi-tui/components/select-list.ts +273 -0
- package/src/vendor/pi-tui/components/settings-list.ts +328 -0
- package/src/vendor/pi-tui/components/spacer.ts +28 -0
- package/src/vendor/pi-tui/components/stack.ts +154 -0
- package/src/vendor/pi-tui/components/text.ts +107 -0
- package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
- package/src/vendor/pi-tui/components/v-stack.ts +33 -0
- package/src/vendor/pi-tui/editor-component.ts +74 -0
- package/src/vendor/pi-tui/fuzzy.ts +138 -0
- package/src/vendor/pi-tui/index.ts +156 -0
- package/src/vendor/pi-tui/keybindings.ts +320 -0
- package/src/vendor/pi-tui/keys.ts +1401 -0
- package/src/vendor/pi-tui/kill-ring.ts +46 -0
- package/src/vendor/pi-tui/latex.ts +1506 -0
- package/src/vendor/pi-tui/layout-node.ts +51 -0
- package/src/vendor/pi-tui/layout.ts +449 -0
- package/src/vendor/pi-tui/native-modifiers.ts +13 -0
- package/src/vendor/pi-tui/native-module-path.ts +31 -0
- package/src/vendor/pi-tui/native-platform.ts +63 -0
- package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
- package/src/vendor/pi-tui/terminal-colors.ts +73 -0
- package/src/vendor/pi-tui/terminal-image.ts +696 -0
- package/src/vendor/pi-tui/terminal.ts +547 -0
- package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
- package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
- package/src/vendor/pi-tui/tui.ts +1456 -0
- package/src/vendor/pi-tui/undo-stack.ts +28 -0
- package/src/vendor/pi-tui/utils.ts +1345 -0
- package/src/vendor/pi-tui/word-navigation.ts +117 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exhaustiveness for discriminated unions.
|
|
3
|
+
*
|
|
4
|
+
* Copied rather than imported from core: tui may not import src/ outside
|
|
5
|
+
* itself, and the boundary checker exists to keep that true. The two copies
|
|
6
|
+
* must stay one function; if they drift, the newer union is the one that
|
|
7
|
+
* will fail to compile first.
|
|
8
|
+
*/
|
|
9
|
+
export function assertNever(value: never, context: string): never {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`${context}: unhandled variant ${JSON.stringify(value)} -- ` +
|
|
12
|
+
"a union gained a member without every switch over it being updated",
|
|
13
|
+
);
|
|
14
|
+
}
|
package/src/tui/fuzzy.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subsequence fuzzy matching with a stable rank.
|
|
3
|
+
*
|
|
4
|
+
* Consecutive hits and word-start hits score better (pi-tui's scheme,
|
|
5
|
+
* lower is better). Ties keep the original index: an unstable sort would
|
|
6
|
+
* reshuffle the select list between two renders, and Enter would confirm
|
|
7
|
+
* a different item than the one under the cursor.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface FuzzyMatch {
|
|
11
|
+
readonly text: string;
|
|
12
|
+
readonly score: number;
|
|
13
|
+
readonly index: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Lower is better. `null` if `query` is not a subsequence of `text`.
|
|
18
|
+
* Case-insensitive. Empty query matches with score 0.
|
|
19
|
+
*/
|
|
20
|
+
export function fuzzyMatch(text: string, query: string): number | null {
|
|
21
|
+
if (query.length === 0) return 0;
|
|
22
|
+
return score(text, query);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function fuzzyFilter(candidates: readonly string[], query: string): string[] {
|
|
26
|
+
if (query.length === 0) return [...candidates];
|
|
27
|
+
const ranked: FuzzyMatch[] = [];
|
|
28
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
29
|
+
const text = candidates[index]!;
|
|
30
|
+
const s = score(text, query);
|
|
31
|
+
if (s === null) continue;
|
|
32
|
+
ranked.push({ text, score: s, index });
|
|
33
|
+
}
|
|
34
|
+
ranked.sort((a, b) => a.score - b.score || a.index - b.index);
|
|
35
|
+
return ranked.map((r) => r.text);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Leftmost greedy subsequence, scored like pi-tui:
|
|
40
|
+
* consecutive run bonus, word-boundary bonus, later-match penalty.
|
|
41
|
+
* Walks code points so a CJK query cannot land on a UTF-16 half-unit.
|
|
42
|
+
*/
|
|
43
|
+
function score(text: string, query: string): number | null {
|
|
44
|
+
const t = [...text.toLowerCase()];
|
|
45
|
+
const q = [...query.toLowerCase()];
|
|
46
|
+
if (q.length > t.length) return null;
|
|
47
|
+
|
|
48
|
+
let qi = 0;
|
|
49
|
+
let total = 0;
|
|
50
|
+
let last = -1;
|
|
51
|
+
let consecutive = 0;
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < t.length && qi < q.length; i++) {
|
|
54
|
+
if (t[i] !== q[qi]) continue;
|
|
55
|
+
const prev = i === 0 ? "" : t[i - 1]!;
|
|
56
|
+
const boundary = i === 0 || /[\s\-_./:]/.test(prev);
|
|
57
|
+
|
|
58
|
+
if (last === i - 1) {
|
|
59
|
+
consecutive++;
|
|
60
|
+
total -= consecutive * 5;
|
|
61
|
+
} else {
|
|
62
|
+
consecutive = 0;
|
|
63
|
+
if (last >= 0) total += (i - last - 1) * 2;
|
|
64
|
+
}
|
|
65
|
+
if (boundary) total -= 10;
|
|
66
|
+
total += i * 0.1;
|
|
67
|
+
|
|
68
|
+
last = i;
|
|
69
|
+
qi++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (qi < q.length) return null;
|
|
73
|
+
if (q.join("") === t.join("")) total -= 100;
|
|
74
|
+
return total;
|
|
75
|
+
}
|
package/src/tui/host.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter between xeno's tiny Terminal (columns/rows/write) and pi's TUI.
|
|
3
|
+
*
|
|
4
|
+
* Vendor is copied unmodified (D19). Anything we need to change — a stub
|
|
5
|
+
* terminal for tests, thinking-coloured editor borders, wrapping a string[]
|
|
6
|
+
* as a Component — lives here so the next recopy does not wipe it.
|
|
7
|
+
*/
|
|
8
|
+
import type { Terminal as SimpleTerminal } from "./terminal.ts";
|
|
9
|
+
import type { Style } from "./style.ts";
|
|
10
|
+
import { IDENTITY, PLAIN_ATTRS, paint, type Attrs } from "./style.ts";
|
|
11
|
+
import {
|
|
12
|
+
Container,
|
|
13
|
+
Markdown,
|
|
14
|
+
ProcessTerminal,
|
|
15
|
+
Text as PiText,
|
|
16
|
+
type Component,
|
|
17
|
+
type DefaultTextStyle,
|
|
18
|
+
type EditorTheme,
|
|
19
|
+
type MarkdownTheme,
|
|
20
|
+
type SelectListTheme,
|
|
21
|
+
type Terminal,
|
|
22
|
+
type TUI,
|
|
23
|
+
TuiMainScreen,
|
|
24
|
+
stripTerminalSequences,
|
|
25
|
+
} from "../vendor/pi-tui/index.ts";
|
|
26
|
+
|
|
27
|
+
export { Container, Markdown, PiText };
|
|
28
|
+
export type { TuiMainScreen };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Wraps the injectible {columns, rows, write} terminal tests already use.
|
|
32
|
+
* Tests inject a silent terminal; the application uses ProcessTerminal.
|
|
33
|
+
*/
|
|
34
|
+
export class AdapterTerminal implements Terminal {
|
|
35
|
+
private readonly inner: SimpleTerminal;
|
|
36
|
+
constructor(inner: SimpleTerminal) {
|
|
37
|
+
this.inner = inner;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
start(_onInput: (data: string) => void, _onResize: () => void): void {}
|
|
41
|
+
stop(): void {}
|
|
42
|
+
async drainInput(_maxMs?: number, _idleMs?: number): Promise<void> {}
|
|
43
|
+
write(data: string): void {
|
|
44
|
+
this.inner.write(data);
|
|
45
|
+
}
|
|
46
|
+
get columns(): number {
|
|
47
|
+
return this.inner.columns;
|
|
48
|
+
}
|
|
49
|
+
get rows(): number {
|
|
50
|
+
return this.inner.rows;
|
|
51
|
+
}
|
|
52
|
+
get kittyProtocolActive(): boolean {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
moveBy(lines: number): void {
|
|
56
|
+
if (lines < 0) this.write(`\x1b[${-lines}A`);
|
|
57
|
+
else if (lines > 0) this.write(`\x1b[${lines}B`);
|
|
58
|
+
}
|
|
59
|
+
hideCursor(): void {
|
|
60
|
+
this.write("\x1b[?25l");
|
|
61
|
+
}
|
|
62
|
+
showCursor(): void {
|
|
63
|
+
this.write("\x1b[?25h");
|
|
64
|
+
}
|
|
65
|
+
clearLine(): void {
|
|
66
|
+
this.write("\x1b[2K");
|
|
67
|
+
}
|
|
68
|
+
clearFromCursor(): void {
|
|
69
|
+
this.write("\x1b[J");
|
|
70
|
+
}
|
|
71
|
+
clearScreen(): void {
|
|
72
|
+
this.write("\x1b[2J\x1b[H");
|
|
73
|
+
}
|
|
74
|
+
setTitle(_title: string): void {}
|
|
75
|
+
setProgress(_active: boolean): void {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** TuiMainScreen without ProcessTerminal — tests inject a FakeTerminal. */
|
|
79
|
+
export function createInteractiveTui(term?: SimpleTerminal, showHardwareCursor = true): TuiMainScreen {
|
|
80
|
+
return new TuiMainScreen(term ? new AdapterTerminal(term) : new ProcessTerminal(), showHardwareCursor);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Vendor Editor.render only reads tui.terminal.rows. A full TuiMainScreen
|
|
85
|
+
* would write on construct; a silent stub keeps `new Editor().render()` a
|
|
86
|
+
* unit test.
|
|
87
|
+
*/
|
|
88
|
+
export function stubTui(rows = 24, columns = 80): TUI {
|
|
89
|
+
return new TuiMainScreen(new AdapterTerminal({ columns, rows, write() {} }), false);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function identitySelectListTheme(): SelectListTheme {
|
|
93
|
+
return {
|
|
94
|
+
selectedPrefix: (text) => text,
|
|
95
|
+
selectedText: (text) => text,
|
|
96
|
+
description: (text) => text,
|
|
97
|
+
scrollInfo: (text) => text,
|
|
98
|
+
noMatch: (text) => text,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function identityEditorTheme(): EditorTheme {
|
|
103
|
+
return {
|
|
104
|
+
borderColor: (text) => text,
|
|
105
|
+
selectList: identitySelectListTheme(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Border colour follows the thinking level, which is pi's cue that the
|
|
111
|
+
* next turn will spend a different budget. Uncoloured when the style
|
|
112
|
+
* emits no SGR (IDENTITY / NO_COLOR) so a pipe cannot pick up CSI.
|
|
113
|
+
*/
|
|
114
|
+
export function thinkingBorderColor(level: string, colored: boolean, style?: Style): (s: string) => string {
|
|
115
|
+
if (!colored) return (s) => s;
|
|
116
|
+
if (style?.token) return (s) => style.token!(`thinking${level[0]?.toUpperCase()}${level.slice(1)}`, s);
|
|
117
|
+
const code =
|
|
118
|
+
level === "max" || level === "xhigh" ? "35" :
|
|
119
|
+
level === "high" ? "33" :
|
|
120
|
+
level === "medium" ? "36" :
|
|
121
|
+
level === "low" ? "32" :
|
|
122
|
+
"90";
|
|
123
|
+
return (s) => `\x1b[${code}m${s}\x1b[0m`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function wrapAttr(style: Style, attrs: Attrs): (text: string) => string {
|
|
127
|
+
return (text) => paint(text, attrs, style);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function defaultTextStyleFrom(style: Style): DefaultTextStyle {
|
|
131
|
+
return { color: (text) => paint(text, PLAIN_ATTRS, style) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Vendor Markdown may emit OSC 8 even when the theme is identity. IDENTITY
|
|
136
|
+
* is the -p / NO_COLOR gate: those bytes must not leak into a pipe.
|
|
137
|
+
*/
|
|
138
|
+
export function renderMarkdownLines(text: string, width: number, style: Style): string[] {
|
|
139
|
+
const md = new Markdown(text, 0, 0, markdownThemeFromStyle(style), defaultTextStyleFrom(style));
|
|
140
|
+
const lines = md.render(Math.max(0, width));
|
|
141
|
+
// Vendor pads every row to `width` for backgrounds we do not use. Trailing
|
|
142
|
+
// spaces would change tool-view snapshots and a pipe's visible payload.
|
|
143
|
+
const trimmed = lines.map((line) => line.replace(/[ \t]+$/, ""));
|
|
144
|
+
return style === IDENTITY ? trimmed.map(stripTerminalSequences) : trimmed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Activity-area assistant text: pi's Markdown, themed from our Style. */
|
|
148
|
+
export function assistantMarkdown(text: string, style: Style): Component {
|
|
149
|
+
return new MarkdownBlock(text, style);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Keep Pi's parse/layout cache alive across keystrokes and streaming deltas. */
|
|
153
|
+
export class MarkdownBlock implements Component {
|
|
154
|
+
private readonly markdown: Markdown;
|
|
155
|
+
private readonly style: Style;
|
|
156
|
+
private text: string;
|
|
157
|
+
constructor(text: string, style: Style) {
|
|
158
|
+
this.text = text;
|
|
159
|
+
this.style = style;
|
|
160
|
+
this.markdown = new Markdown(text, 0, 0, markdownThemeFromStyle(style), defaultTextStyleFrom(style));
|
|
161
|
+
}
|
|
162
|
+
setText(text: string): void {
|
|
163
|
+
if (text === this.text) return;
|
|
164
|
+
this.text = text;
|
|
165
|
+
this.markdown.setText(text);
|
|
166
|
+
}
|
|
167
|
+
invalidate(): void { this.markdown.invalidate(); }
|
|
168
|
+
render(width: number): string[] {
|
|
169
|
+
if (width <= 0) return [""];
|
|
170
|
+
const lines = this.markdown.render(Math.max(1, width)).map((line) => line.trimEnd());
|
|
171
|
+
return this.style === IDENTITY ? lines.map(stripTerminalSequences) : lines;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function markdownThemeFromStyle(style: Style): MarkdownTheme {
|
|
176
|
+
const plain = style.open(PLAIN_ATTRS) === "" && style === IDENTITY;
|
|
177
|
+
if (plain) {
|
|
178
|
+
return {
|
|
179
|
+
heading: (t) => t,
|
|
180
|
+
link: (t) => t,
|
|
181
|
+
linkUrl: (t) => t,
|
|
182
|
+
code: (t) => t,
|
|
183
|
+
codeBlock: (t) => t,
|
|
184
|
+
codeBlockBorder: (t) => t,
|
|
185
|
+
quote: (t) => t,
|
|
186
|
+
quoteBorder: (t) => t,
|
|
187
|
+
hr: (t) => t,
|
|
188
|
+
listBullet: (t) => t,
|
|
189
|
+
bold: (t) => t,
|
|
190
|
+
italic: (t) => t,
|
|
191
|
+
strikethrough: (t) => t,
|
|
192
|
+
underline: (t) => t,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
heading: wrapAttr(style, { ...PLAIN_ATTRS, heading: true, bold: true }),
|
|
197
|
+
link: wrapAttr(style, { ...PLAIN_ATTRS, link: true }),
|
|
198
|
+
linkUrl: wrapAttr(style, { ...PLAIN_ATTRS, link: true, dim: true }),
|
|
199
|
+
code: wrapAttr(style, { ...PLAIN_ATTRS, code: true }),
|
|
200
|
+
codeBlock: wrapAttr(style, { ...PLAIN_ATTRS, code: true }),
|
|
201
|
+
codeBlockBorder: wrapAttr(style, { ...PLAIN_ATTRS, dim: true }),
|
|
202
|
+
quote: wrapAttr(style, { ...PLAIN_ATTRS, dim: true }),
|
|
203
|
+
quoteBorder: wrapAttr(style, { ...PLAIN_ATTRS, dim: true }),
|
|
204
|
+
hr: wrapAttr(style, { ...PLAIN_ATTRS, dim: true }),
|
|
205
|
+
listBullet: wrapAttr(style, { ...PLAIN_ATTRS, dim: true }),
|
|
206
|
+
bold: wrapAttr(style, { ...PLAIN_ATTRS, bold: true }),
|
|
207
|
+
italic: wrapAttr(style, { ...PLAIN_ATTRS, italic: true }),
|
|
208
|
+
strikethrough: (t) => t,
|
|
209
|
+
underline: (t) => t,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Lift a render(width) → string[] into pi's Component (needs invalidate). */
|
|
214
|
+
export class Lines implements Component {
|
|
215
|
+
private readonly paint: (width: number) => string[];
|
|
216
|
+
constructor(paint: (width: number) => string[]) {
|
|
217
|
+
this.paint = paint;
|
|
218
|
+
}
|
|
219
|
+
invalidate(): void {}
|
|
220
|
+
render(width: number): string[] {
|
|
221
|
+
return this.paint(width);
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/tui/input.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Pi owns decoding. Encoding exists only for programmatic actions and legacy tests. */
|
|
2
|
+
import { KeybindingsManager, TUI_KEYBINDINGS, type KeybindingsConfig } from "../vendor/pi-tui/keybindings.ts";
|
|
3
|
+
import { matchesKey, parseKey, type KeyId } from "../vendor/pi-tui/keys.ts";
|
|
4
|
+
import { formatTuiKey, type Key } from "./keys.ts";
|
|
5
|
+
|
|
6
|
+
export function keyMatches(data: string, chord: string): boolean {
|
|
7
|
+
return matchesKey(data, chord.replace("pageup", "pageUp").replace("pagedown", "pageDown") as KeyId);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function inputKey(data: string): Key | null {
|
|
11
|
+
const parsed = parseKey(data);
|
|
12
|
+
if (parsed === undefined) return data.startsWith("\x1b") ? null : { type: "char", grapheme: data };
|
|
13
|
+
const parts = parsed.toLowerCase().split("+");
|
|
14
|
+
const name = parts.pop()!;
|
|
15
|
+
const mods = { ctrl: parts.includes("ctrl"), alt: parts.includes("alt"), shift: parts.includes("shift") };
|
|
16
|
+
switch (name) {
|
|
17
|
+
case "enter": return mods.alt ? { type: "alt", key: "\r" } : { type: "enter", shift: mods.shift };
|
|
18
|
+
case "tab": return { type: "tab", shift: mods.shift };
|
|
19
|
+
case "escape": return { type: "escape" };
|
|
20
|
+
case "backspace": if (!mods.alt && !mods.ctrl) return { type: "backspace" }; break;
|
|
21
|
+
case "up": case "down": case "left": case "right": case "home": case "end":
|
|
22
|
+
case "delete": case "pageup": case "pagedown": return { type: name, mods };
|
|
23
|
+
}
|
|
24
|
+
if (mods.ctrl) return { type: "ctrl", key: name, shift: mods.shift };
|
|
25
|
+
if (mods.alt) return { type: "alt", key: name };
|
|
26
|
+
return { type: "char", grapheme: data };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function encodeKey(key: Key): string {
|
|
30
|
+
if (key.type === "char") return key.grapheme;
|
|
31
|
+
return encodeChord(formatTuiKey(key));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function encodeChord(chord: string): string {
|
|
35
|
+
const parts = chord.split("+");
|
|
36
|
+
const name = parts.pop()!;
|
|
37
|
+
const ctrl = parts.includes("ctrl");
|
|
38
|
+
const alt = parts.includes("alt");
|
|
39
|
+
const shift = parts.includes("shift");
|
|
40
|
+
const modifier = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
|
|
41
|
+
const arrow: Record<string, string> = { up: "A", down: "B", right: "C", left: "D", home: "H", end: "F" };
|
|
42
|
+
const suffix = arrow[name];
|
|
43
|
+
if (suffix) return modifier === 1 ? `\x1b[${suffix}` : `\x1b[1;${modifier}${suffix}`;
|
|
44
|
+
if (name === "escape") return "\x1b";
|
|
45
|
+
if (name === "tab") return shift ? "\x1b[Z" : "\t";
|
|
46
|
+
if (name === "backspace") return `${alt ? "\x1b" : ""}\x7f`;
|
|
47
|
+
const code = name === "enter" ? 13 : name === "space" ? 32 : name.codePointAt(0)!;
|
|
48
|
+
if (name === "delete" || name === "pageup" || name === "pagedown") {
|
|
49
|
+
const num = name === "delete" ? 3 : name === "pageup" ? 5 : 6;
|
|
50
|
+
return `\x1b[${num}${modifier === 1 ? "" : `;${modifier}`}~`;
|
|
51
|
+
}
|
|
52
|
+
if (shift || (ctrl && alt)) return `\x1b[${code};${modifier}u`;
|
|
53
|
+
if (ctrl) return String.fromCharCode(code & 31);
|
|
54
|
+
return `${alt ? "\x1b" : ""}${String.fromCodePoint(code)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const INPUT_ACTIONS: Record<string, string> = {
|
|
58
|
+
"editor.submit": "tui.input.submit", "editor.newLine": "tui.input.newLine", "editor.complete": "tui.input.tab",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Translate existing settings names once, without translating terminal bytes. */
|
|
62
|
+
export function editorBindings(byAction: Readonly<Record<string, readonly string[]>>): KeybindingsManager {
|
|
63
|
+
const config: KeybindingsConfig = {};
|
|
64
|
+
for (const [action, chords] of Object.entries(byAction)) {
|
|
65
|
+
const id = INPUT_ACTIONS[action] ?? `tui.${action}`;
|
|
66
|
+
if (id in TUI_KEYBINDINGS) config[id] = [...chords] as KeyId[];
|
|
67
|
+
}
|
|
68
|
+
return new KeybindingsManager(TUI_KEYBINDINGS, config);
|
|
69
|
+
}
|