@thesmurph/agentlink 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/src/ui.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { emitKeypressEvents } from "node:readline";
2
+
3
+ /**
4
+ * A dependency-free checkbox list.
5
+ *
6
+ * Uses raw mode plus ANSI line control rather than a TUI library: the whole
7
+ * point of agentlink is that it works with nothing installed.
8
+ * Falls back to the caller's defaults when stdin is not a TTY.
9
+ */
10
+
11
+ export interface Choice {
12
+ id: string;
13
+ label: string;
14
+ hint?: string;
15
+ checked: boolean;
16
+ group?: string;
17
+ }
18
+
19
+ export interface SelectOptions {
20
+ title: string;
21
+ help?: string;
22
+ }
23
+
24
+ const ESC = String.fromCharCode(27);
25
+ const CURSOR_UP = (n: number) => (n > 0 ? `${ESC}[${n}A` : "");
26
+ const CLEAR_LINE = `${ESC}[2K`;
27
+ const DIM = `${ESC}[2m`;
28
+ const BOLD = `${ESC}[1m`;
29
+ const RESET = `${ESC}[0m`;
30
+ const CYAN = `${ESC}[36m`;
31
+
32
+ export async function selectMany(choices: Choice[], options: SelectOptions): Promise<string[] | null> {
33
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
34
+ return choices.filter((c) => c.checked).map((c) => c.id);
35
+ }
36
+
37
+ return new Promise((resolve) => {
38
+ emitKeypressEvents(process.stdin);
39
+ let cursor = 0;
40
+ let drawnLines = 0;
41
+
42
+ const lines = () => {
43
+ const out: string[] = [];
44
+ out.push(`${BOLD}${options.title}${RESET}`);
45
+ if (options.help) out.push(`${DIM}${options.help}${RESET}`);
46
+ choices.forEach((choice, index) => {
47
+ const active = index === cursor;
48
+ const box = choice.checked ? "[x]" : "[ ]";
49
+ const pointer = active ? `${CYAN}>${RESET}` : " ";
50
+ const hint = choice.hint ? ` ${DIM}${choice.hint}${RESET}` : "";
51
+ const label = active ? `${BOLD}${choice.label}${RESET}` : choice.label;
52
+ out.push(`${pointer} ${box} ${label}${hint}`);
53
+ });
54
+ return out;
55
+ };
56
+
57
+ const draw = () => {
58
+ const rendered = lines();
59
+ let output = CURSOR_UP(drawnLines);
60
+ for (const line of rendered) output += `${CLEAR_LINE}${line}\n`;
61
+ if (rendered.length < drawnLines) {
62
+ for (let i = rendered.length; i < drawnLines; i += 1) output += `${CLEAR_LINE}\n`;
63
+ output += CURSOR_UP(drawnLines - rendered.length);
64
+ }
65
+ drawnLines = rendered.length;
66
+ process.stdout.write(output);
67
+ };
68
+
69
+ const cleanup = (result: string[] | null) => {
70
+ process.stdin.removeListener("keypress", onKey);
71
+ process.stdin.setRawMode?.(false);
72
+ process.stdin.pause();
73
+ resolve(result);
74
+ };
75
+
76
+ const onKey = (_str: string | undefined, key: { name?: string; ctrl?: boolean; shift?: boolean } | undefined) => {
77
+ if (!key) return;
78
+ const name = key.name ?? "";
79
+ if (key.ctrl && name === "c") {
80
+ process.stdout.write("\n");
81
+ cleanup(null);
82
+ return;
83
+ }
84
+ if (name === "escape") {
85
+ cleanup(null);
86
+ return;
87
+ }
88
+ if (name === "return" || name === "enter") {
89
+ process.stdout.write("\n");
90
+ cleanup(choices.filter((c) => c.checked).map((c) => c.id));
91
+ return;
92
+ }
93
+ if (name === "up" || name === "k") {
94
+ cursor = (cursor + choices.length - 1) % choices.length;
95
+ draw();
96
+ return;
97
+ }
98
+ if (name === "down" || name === "j") {
99
+ cursor = (cursor + 1) % choices.length;
100
+ draw();
101
+ return;
102
+ }
103
+ if (name === "space") {
104
+ const choice = choices[cursor];
105
+ if (choice) choice.checked = !choice.checked;
106
+ draw();
107
+ return;
108
+ }
109
+ if (name === "a") {
110
+ const allOn = choices.every((c) => c.checked);
111
+ choices.forEach((c) => {
112
+ c.checked = !allOn;
113
+ });
114
+ draw();
115
+ return;
116
+ }
117
+ if (name === "i") {
118
+ choices.forEach((c) => {
119
+ c.checked = c.group === "detected" ? true : false;
120
+ });
121
+ draw();
122
+ return;
123
+ }
124
+ };
125
+
126
+ emitKeypressEvents(process.stdin);
127
+ process.stdin.setRawMode?.(true);
128
+ process.stdin.resume();
129
+ process.stdin.on("keypress", onKey);
130
+ draw();
131
+ });
132
+ }