@xingjisoft/neutrino 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.
@@ -0,0 +1,197 @@
1
+ import process from "node:process";
2
+ const ESC_TIMEOUT = 30;
3
+ export class InputReader {
4
+ decoder = new TextDecoder("utf-8");
5
+ pending = "";
6
+ state = "normal";
7
+ csiBuf = "";
8
+ escTimer = null;
9
+ handler = null;
10
+ dataHandler = (chunk) => this.onData(chunk);
11
+ start(handler) {
12
+ this.handler = handler;
13
+ const stdin = process.stdin;
14
+ if (stdin.isTTY)
15
+ stdin.setRawMode(true);
16
+ stdin.resume();
17
+ stdin.on("data", this.dataHandler);
18
+ }
19
+ stop() {
20
+ const stdin = process.stdin;
21
+ stdin.removeListener("data", this.dataHandler);
22
+ if (this.escTimer)
23
+ clearTimeout(this.escTimer);
24
+ if (stdin.isTTY)
25
+ stdin.setRawMode(false);
26
+ stdin.pause();
27
+ }
28
+ onData(chunk) {
29
+ this.pending += this.decoder.decode(chunk, { stream: true });
30
+ this.process();
31
+ }
32
+ process() {
33
+ const s = this.pending;
34
+ let i = 0;
35
+ while (i < s.length) {
36
+ const c = s[i];
37
+ if (this.state === "normal") {
38
+ if (c === "\x1b") {
39
+ this.state = "esc";
40
+ this.scheduleEscTimeout();
41
+ i++;
42
+ continue;
43
+ }
44
+ const code = c.charCodeAt(0);
45
+ if (c === "\r" || c === "\n") {
46
+ this.emitKey("return", false, false, false, c);
47
+ i++;
48
+ continue;
49
+ }
50
+ if (c === "\t") {
51
+ this.emitKey("tab", false, false, false, c);
52
+ i++;
53
+ continue;
54
+ }
55
+ if (c === "\x7f" || c === "\x08") {
56
+ this.emitKey("backspace", false, false, false, c);
57
+ i++;
58
+ continue;
59
+ }
60
+ if (code >= 1 && code <= 26) {
61
+ const name = String.fromCharCode(code + 96);
62
+ this.emitKey(name, true, false, false, c);
63
+ i++;
64
+ continue;
65
+ }
66
+ if (code === 0) {
67
+ this.emitKey(" ", true, false, false, c);
68
+ i++;
69
+ continue;
70
+ }
71
+ this.emitKey(c, false, false, false, c);
72
+ i++;
73
+ continue;
74
+ }
75
+ if (this.state === "esc") {
76
+ this.cancelEscTimeout();
77
+ if (c === "[" || c === "O") {
78
+ this.state = "csi";
79
+ this.csiBuf = "";
80
+ i++;
81
+ continue;
82
+ }
83
+ if (c === "\x1b") {
84
+ this.emitKey("escape", false, false, false, "\x1b");
85
+ this.state = "esc";
86
+ this.scheduleEscTimeout();
87
+ i++;
88
+ continue;
89
+ }
90
+ this.emitKey(c, false, true, false, "\x1b" + c);
91
+ this.state = "normal";
92
+ i++;
93
+ continue;
94
+ }
95
+ // csi
96
+ this.csiBuf += c;
97
+ const cc = c.charCodeAt(0);
98
+ if (cc >= 0x40 && cc <= 0x7e) {
99
+ this.finishCsi();
100
+ this.state = "normal";
101
+ }
102
+ i++;
103
+ }
104
+ this.pending = "";
105
+ }
106
+ scheduleEscTimeout() {
107
+ if (this.escTimer)
108
+ clearTimeout(this.escTimer);
109
+ this.escTimer = setTimeout(() => {
110
+ this.escTimer = null;
111
+ if (this.state === "esc") {
112
+ this.state = "normal";
113
+ this.emitKey("escape", false, false, false, "\x1b");
114
+ }
115
+ }, ESC_TIMEOUT);
116
+ }
117
+ cancelEscTimeout() {
118
+ if (this.escTimer) {
119
+ clearTimeout(this.escTimer);
120
+ this.escTimer = null;
121
+ }
122
+ }
123
+ finishCsi() {
124
+ const s = this.csiBuf;
125
+ const final = s[s.length - 1];
126
+ if (final === "A")
127
+ this.emitKey("up", false, false, false, "\x1b[A");
128
+ else if (final === "B")
129
+ this.emitKey("down", false, false, false, "\x1b[B");
130
+ else if (final === "C")
131
+ this.emitKey("right", false, false, false, "\x1b[C");
132
+ else if (final === "D")
133
+ this.emitKey("left", false, false, false, "\x1b[D");
134
+ else if (final === "H")
135
+ this.emitKey("home", false, false, false, "\x1b[H");
136
+ else if (final === "F")
137
+ this.emitKey("end", false, false, false, "\x1b[F");
138
+ else if (final === "Z")
139
+ this.emitKey("tab", false, false, true, "\x1b[Z");
140
+ else if (final === "M" || final === "m") {
141
+ if (s[0] === "<")
142
+ this.handleMouse(s, final);
143
+ }
144
+ else if (final === "~") {
145
+ const param = s.slice(0, -1);
146
+ if (param === "3")
147
+ this.emitKey("delete", false, false, false, "\x1b[3~");
148
+ else if (param === "5")
149
+ this.emitKey("pageup", false, false, false, "\x1b[5~");
150
+ else if (param === "6")
151
+ this.emitKey("pagedown", false, false, false, "\x1b[6~");
152
+ else if (param === "1" || param === "7")
153
+ this.emitKey("home", false, false, false, "\x1b[1~");
154
+ else if (param === "4" || param === "8")
155
+ this.emitKey("end", false, false, false, "\x1b[4~");
156
+ }
157
+ }
158
+ handleMouse(s, final) {
159
+ const parts = s.slice(1).split(";");
160
+ if (parts.length < 3)
161
+ return;
162
+ const b = parseInt(parts[0], 10);
163
+ const x = parseInt(parts[1], 10) - 1;
164
+ const y = parseInt(parts[2], 10) - 1;
165
+ if (Number.isNaN(b) || Number.isNaN(x) || Number.isNaN(y))
166
+ return;
167
+ if (b === 64) {
168
+ this.emitMouse({ type: "wheel", dir: -1, x, y });
169
+ }
170
+ else if (b === 65) {
171
+ this.emitMouse({ type: "wheel", dir: 1, x, y });
172
+ }
173
+ else if (final === "M" && (b === 0 || b === 1 || b === 2)) {
174
+ this.emitMouse({ type: "click", x, y, button: b });
175
+ }
176
+ }
177
+ emitKey(name, ctrl, meta, shift, char) {
178
+ if (!this.handler)
179
+ return;
180
+ this.handler({
181
+ type: "key",
182
+ key: {
183
+ name,
184
+ ctrl,
185
+ meta,
186
+ shift,
187
+ char,
188
+ sequence: char,
189
+ },
190
+ });
191
+ }
192
+ emitMouse(event) {
193
+ if (!this.handler)
194
+ return;
195
+ this.handler({ type: "mouse", event });
196
+ }
197
+ }
@@ -0,0 +1,96 @@
1
+ import process from "node:process";
2
+ function styleKey(st) {
3
+ let k = "";
4
+ if (st.fg)
5
+ k += "f" + st.fg.join(",") + ";";
6
+ if (st.bg)
7
+ k += "b" + st.bg.join(",") + ";";
8
+ if (st.bold)
9
+ k += "B";
10
+ if (st.dim)
11
+ k += "d";
12
+ if (st.inverse)
13
+ k += "i";
14
+ return k;
15
+ }
16
+ function sgr(st) {
17
+ const codes = [];
18
+ if (st.bold)
19
+ codes.push("1");
20
+ if (st.dim)
21
+ codes.push("2");
22
+ if (st.inverse)
23
+ codes.push("7");
24
+ if (st.fg)
25
+ codes.push(`38;2;${st.fg[0]};${st.fg[1]};${st.fg[2]}`);
26
+ if (st.bg)
27
+ codes.push(`48;2;${st.bg[0]};${st.bg[1]};${st.bg[2]}`);
28
+ if (codes.length === 0)
29
+ return "\x1b[0m";
30
+ return "\x1b[0;" + codes.join(";") + "m";
31
+ }
32
+ function rowSignature(row) {
33
+ let out = "";
34
+ for (let i = 0; i < row.length; i++) {
35
+ const c = row[i];
36
+ out += c.ch + "\u0000" + styleKey(c.style) + "\u0001";
37
+ }
38
+ return out;
39
+ }
40
+ export class Renderer {
41
+ prevSig = [];
42
+ prevW = 0;
43
+ prevH = 0;
44
+ out = process.stdout;
45
+ init() {
46
+ this.write("\x1b[?1049h"); // alternate screen
47
+ this.write("\x1b[?25l"); // hide cursor
48
+ this.write("\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?1015h"); // mouse tracking
49
+ }
50
+ dispose() {
51
+ this.write("\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1015l");
52
+ this.write("\x1b[?25h");
53
+ this.write("\x1b[?1049l");
54
+ this.write("\x1b[0m");
55
+ }
56
+ render(buf) {
57
+ const w = buf.width;
58
+ const h = buf.height;
59
+ if (w !== this.prevW || h !== this.prevH) {
60
+ this.prevSig = new Array(h).fill("");
61
+ this.prevW = w;
62
+ this.prevH = h;
63
+ }
64
+ let out = "";
65
+ for (let y = 0; y < h; y++) {
66
+ const row = buf.cells[y];
67
+ const sig = rowSignature(row);
68
+ if (sig !== this.prevSig[y]) {
69
+ out += this.renderRow(y, row);
70
+ this.prevSig[y] = sig;
71
+ }
72
+ }
73
+ if (out)
74
+ this.write(out);
75
+ }
76
+ renderRow(y, row) {
77
+ let s = `\x1b[${y + 1};1H`;
78
+ let lastKey = "";
79
+ for (let x = 0; x < row.length; x++) {
80
+ const cell = row[x];
81
+ if (cell.ch === "")
82
+ continue;
83
+ const sk = styleKey(cell.style);
84
+ if (sk !== lastKey) {
85
+ s += sgr(cell.style);
86
+ lastKey = sk;
87
+ }
88
+ s += cell.ch;
89
+ }
90
+ s += "\x1b[0m";
91
+ return s;
92
+ }
93
+ write(chunk) {
94
+ this.out.write(chunk);
95
+ }
96
+ }
@@ -0,0 +1,6 @@
1
+ export class Screen {
2
+ app;
3
+ onMouse(_ev) { }
4
+ onActivate() { }
5
+ onDeactivate() { }
6
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,150 @@
1
+ import { C } from "./colors.js";
2
+ import { displayWidth, truncateByWidth, wcwidth } from "./width.js";
3
+ function chunkByWidth(text, width) {
4
+ const chunks = [];
5
+ let cur = "";
6
+ let curW = 0;
7
+ for (const ch of text) {
8
+ const w = wcwidth(ch.codePointAt(0) ?? 0);
9
+ if (curW + w > width && cur.length > 0) {
10
+ chunks.push(cur);
11
+ cur = ch;
12
+ curW = w;
13
+ }
14
+ else {
15
+ cur += ch;
16
+ curW += w;
17
+ }
18
+ }
19
+ if (cur.length > 0)
20
+ chunks.push(cur);
21
+ return chunks;
22
+ }
23
+ export function wrapText(text, width) {
24
+ if (width <= 0)
25
+ width = 1;
26
+ const lines = [];
27
+ for (const para of text.split("\n")) {
28
+ if (para.length === 0) {
29
+ lines.push("");
30
+ continue;
31
+ }
32
+ let line = "";
33
+ let lineW = 0;
34
+ for (const word of para.split(" ")) {
35
+ const wordW = displayWidth(word);
36
+ if (line.length === 0) {
37
+ if (wordW <= width) {
38
+ line = word;
39
+ lineW = wordW;
40
+ }
41
+ else {
42
+ const chunks = chunkByWidth(word, width);
43
+ for (let i = 0; i < chunks.length - 1; i++)
44
+ lines.push(chunks[i]);
45
+ line = chunks[chunks.length - 1];
46
+ lineW = displayWidth(line);
47
+ }
48
+ continue;
49
+ }
50
+ if (lineW + 1 + wordW <= width) {
51
+ line += " " + word;
52
+ lineW += 1 + wordW;
53
+ }
54
+ else {
55
+ lines.push(line);
56
+ if (wordW <= width) {
57
+ line = word;
58
+ lineW = wordW;
59
+ }
60
+ else {
61
+ const chunks = chunkByWidth(word, width);
62
+ for (let i = 0; i < chunks.length - 1; i++)
63
+ lines.push(chunks[i]);
64
+ line = chunks[chunks.length - 1];
65
+ lineW = displayWidth(line);
66
+ }
67
+ }
68
+ }
69
+ lines.push(line);
70
+ }
71
+ return lines;
72
+ }
73
+ export function drawInputBox(buf, x, y, w, value, cursorPos, opts = {}) {
74
+ const active = opts.active ?? true;
75
+ const dimmed = opts.dimmed ?? false;
76
+ const placeholder = opts.placeholder ?? "";
77
+ const h = 3;
78
+ const innerX = x + 2;
79
+ const innerW = w - 4;
80
+ const innerY = y + 1;
81
+ if (active)
82
+ buf.fillRect(x, y, w, h, { bg: C.highlightBg });
83
+ const textStyle = dimmed
84
+ ? { fg: C.dim, bg: C.highlightBg }
85
+ : active
86
+ ? { fg: C.white, bg: C.highlightBg }
87
+ : { fg: C.dim };
88
+ if (value.length === 0) {
89
+ buf.writeText(innerX, innerY, truncateByWidth(placeholder, innerW), textStylePlaceholder(active));
90
+ return;
91
+ }
92
+ const chars = Array.from(value);
93
+ const widths = chars.map((ch) => wcwidth(ch.codePointAt(0) ?? 0));
94
+ const cursorCp = Math.min(Array.from(value.slice(0, cursorPos)).length, chars.length);
95
+ let cursorCol = 0;
96
+ for (let i = 0; i < cursorCp; i++)
97
+ cursorCol += widths[i];
98
+ let startCp = 0;
99
+ if (cursorCol >= innerW) {
100
+ let acc = 0;
101
+ startCp = cursorCp;
102
+ while (startCp > 0) {
103
+ const pw = widths[startCp - 1];
104
+ if (acc + pw <= innerW - 1) {
105
+ acc += pw;
106
+ startCp--;
107
+ }
108
+ else {
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ let prefixW = 0;
114
+ for (let i = 0; i < startCp; i++)
115
+ prefixW += widths[i];
116
+ let endCp = chars.length;
117
+ let acc = 0;
118
+ for (let i = startCp; i < chars.length; i++) {
119
+ if (acc + widths[i] > innerW) {
120
+ endCp = i;
121
+ break;
122
+ }
123
+ acc += widths[i];
124
+ }
125
+ const visible = chars.slice(startCp, endCp).join("");
126
+ buf.writeText(innerX, innerY, visible, textStyle);
127
+ const cursorScreenCol = cursorCol - prefixW;
128
+ if (active && cursorScreenCol >= 0 && cursorScreenCol < innerW) {
129
+ buf.set(innerX + cursorScreenCol, innerY, " ", { fg: C.black, bg: C.accent });
130
+ }
131
+ }
132
+ function textStylePlaceholder(active) {
133
+ return active ? { fg: C.dim, bg: C.highlightBg } : { fg: C.dim };
134
+ }
135
+ export function drawTabButton(buf, x, y, w, label, selected, align = "center") {
136
+ const h = 3;
137
+ const labelW = displayWidth(label);
138
+ const lx = align === "left" ? x + 3 : x + Math.floor((w - labelW) / 2);
139
+ if (selected) {
140
+ buf.fillRect(x, y, w, h, { bg: C.highlightBg });
141
+ buf.writeText(lx, y + 1, label, { fg: C.accent, bg: C.highlightBg, bold: true });
142
+ }
143
+ else {
144
+ buf.writeText(lx, y + 1, label, { fg: C.dim });
145
+ }
146
+ }
147
+ export function centerVertically(totalHeight, contentHeight) {
148
+ const top = Math.floor((totalHeight - contentHeight) / 2);
149
+ return Math.max(1, top);
150
+ }