@xingjisoft/neutrino 0.1.0 → 0.1.1
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 +16 -2
- package/dist/app.js +20 -0
- package/dist/colors.js +1 -0
- package/dist/header.js +11 -6
- package/dist/markdown.js +630 -0
- package/dist/screens/agentConversation.js +12 -14
- package/dist/screens/agentHistory.js +76 -24
- package/dist/screens/agentSelect.js +54 -7
- package/dist/screens/chat.js +42 -21
- package/dist/screens/history.js +73 -20
- package/dist/screens/modelSelect.js +66 -7
- package/dist/screens/settings.js +64 -2
- package/dist/screens/start.js +96 -10
- package/dist/state.js +1 -0
- package/dist/terminal/input.js +14 -0
- package/dist/terminal/renderer.js +6 -2
- package/dist/ui.js +70 -25
- package/package.json +1 -1
package/dist/screens/settings.js
CHANGED
|
@@ -2,9 +2,11 @@ import { Screen } from "../terminal/screen.js";
|
|
|
2
2
|
import { C } from "../colors.js";
|
|
3
3
|
import { INTENSITY_LEVELS, MODEL_FILE, loadConfig, saveConfig } from "../config.js";
|
|
4
4
|
import { drawHeader } from "../header.js";
|
|
5
|
-
import { centerVertically } from "../ui.js";
|
|
5
|
+
import { centerVertically, drawHint } from "../ui.js";
|
|
6
6
|
export class SettingsScreen extends Screen {
|
|
7
7
|
intensityIdx = 2;
|
|
8
|
+
lastHoverX = -1;
|
|
9
|
+
lastHoverY = -1;
|
|
8
10
|
onActivate() {
|
|
9
11
|
const config = loadConfig();
|
|
10
12
|
const current = String(config.default_intensity ?? "High");
|
|
@@ -28,6 +30,66 @@ export class SettingsScreen extends Screen {
|
|
|
28
30
|
this.app.popScreen();
|
|
29
31
|
}
|
|
30
32
|
}
|
|
33
|
+
onMouse(ev) {
|
|
34
|
+
const layout = this.layout();
|
|
35
|
+
if (!layout)
|
|
36
|
+
return;
|
|
37
|
+
if (ev.type === "move") {
|
|
38
|
+
if (ev.x === this.lastHoverX && ev.y === this.lastHoverY)
|
|
39
|
+
return;
|
|
40
|
+
this.lastHoverX = ev.x;
|
|
41
|
+
this.lastHoverY = ev.y;
|
|
42
|
+
for (let i = 0; i < layout.optionYs.length; i++) {
|
|
43
|
+
if (ev.y === layout.optionYs[i] && i !== this.intensityIdx) {
|
|
44
|
+
this.intensityIdx = i;
|
|
45
|
+
this.app.render();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (ev.type === "wheel") {
|
|
52
|
+
this.intensityIdx =
|
|
53
|
+
(this.intensityIdx + (ev.dir === -1 ? -1 : 1) + INTENSITY_LEVELS.length) % INTENSITY_LEVELS.length;
|
|
54
|
+
this.app.render();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (ev.type === "click") {
|
|
58
|
+
if (ev.button === 2) {
|
|
59
|
+
this.save();
|
|
60
|
+
this.app.popScreen();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0; i < layout.optionYs.length; i++) {
|
|
64
|
+
if (ev.y === layout.optionYs[i]) {
|
|
65
|
+
this.intensityIdx = i;
|
|
66
|
+
this.save();
|
|
67
|
+
this.app.render();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (ev.y === layout.hintY) {
|
|
72
|
+
this.save();
|
|
73
|
+
this.app.popScreen();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
layout() {
|
|
78
|
+
const h = this.app.height;
|
|
79
|
+
const lines = [
|
|
80
|
+
"Settings",
|
|
81
|
+
"",
|
|
82
|
+
`Model: ${this.app.state.selectedProvider}/${this.app.state.selectedModelName}`,
|
|
83
|
+
`Default intensity:`,
|
|
84
|
+
];
|
|
85
|
+
const total = lines.length + INTENSITY_LEVELS.length + 2;
|
|
86
|
+
const top = centerVertically(h, total);
|
|
87
|
+
const optionYs = INTENSITY_LEVELS.map((_, i) => top + 4 + i);
|
|
88
|
+
const hintY = top + 4 + INTENSITY_LEVELS.length + 2;
|
|
89
|
+
if (hintY > h - 1)
|
|
90
|
+
return null;
|
|
91
|
+
return { optionYs, hintY };
|
|
92
|
+
}
|
|
31
93
|
save() {
|
|
32
94
|
const config = loadConfig();
|
|
33
95
|
config.default_intensity = INTENSITY_LEVELS[this.intensityIdx];
|
|
@@ -64,7 +126,7 @@ export class SettingsScreen extends Screen {
|
|
|
64
126
|
buf.centerText(y, `model.json: ${MODEL_FILE}`, { fg: C.dim });
|
|
65
127
|
y += 1;
|
|
66
128
|
if (y < h) {
|
|
67
|
-
buf
|
|
129
|
+
drawHint(buf, y, "↑/↓ change · Enter apply · click to choose · right-click back · Esc back");
|
|
68
130
|
}
|
|
69
131
|
}
|
|
70
132
|
}
|
package/dist/screens/start.js
CHANGED
|
@@ -2,14 +2,19 @@ import { Screen } from "../terminal/screen.js";
|
|
|
2
2
|
import { C } from "../colors.js";
|
|
3
3
|
import { loadLogo } from "../config.js";
|
|
4
4
|
import { drawHeader } from "../header.js";
|
|
5
|
-
import { centerVertically, drawInputBox, drawTabButton } from "../ui.js";
|
|
5
|
+
import { centerVertically, computeInputWindow, cursorFromColumn, drawHint, drawInputBox, drawTabButton, inRect, } from "../ui.js";
|
|
6
6
|
const TABS = ["Model", "History", "Settings"];
|
|
7
7
|
const LOGO = loadLogo();
|
|
8
8
|
const FOCUS_COUNT = TABS.length + 1; // input box + 3 tabs
|
|
9
|
+
const INPUT_H = 3;
|
|
10
|
+
const TAB_H = 3;
|
|
9
11
|
export class StartScreen extends Screen {
|
|
10
12
|
inputValue = "";
|
|
11
13
|
cursor = 0;
|
|
12
14
|
focusIdx = 0; // 0 = input box, 1..3 = tabs
|
|
15
|
+
hoverIdx = -1; // -1 = nothing, 0 = input box, 1..3 = tabs
|
|
16
|
+
lastHoverX = -1;
|
|
17
|
+
lastHoverY = -1;
|
|
13
18
|
onKey(key) {
|
|
14
19
|
if (key.name === "left" || key.name === "up") {
|
|
15
20
|
this.move(-1);
|
|
@@ -72,8 +77,92 @@ export class StartScreen extends Screen {
|
|
|
72
77
|
this.app.render();
|
|
73
78
|
}
|
|
74
79
|
}
|
|
80
|
+
onMouse(ev) {
|
|
81
|
+
if (ev.type === "move") {
|
|
82
|
+
// avoid redundant work when the pointer hasn't moved to a new cell
|
|
83
|
+
if (ev.x === this.lastHoverX && ev.y === this.lastHoverY)
|
|
84
|
+
return;
|
|
85
|
+
this.lastHoverX = ev.x;
|
|
86
|
+
this.lastHoverY = ev.y;
|
|
87
|
+
const layout = this.layout();
|
|
88
|
+
let hover = -1;
|
|
89
|
+
if (layout && inRect(ev.x, ev.y, { x: layout.inputX, y: layout.inputY, w: layout.inputW, h: INPUT_H })) {
|
|
90
|
+
hover = 0;
|
|
91
|
+
}
|
|
92
|
+
else if (layout && ev.y >= layout.tabY && ev.y < layout.tabY + TAB_H) {
|
|
93
|
+
for (let i = 0; i < layout.tabs.length; i++) {
|
|
94
|
+
if (inRect(ev.x, ev.y, { x: layout.tabs[i].x, y: layout.tabY, w: layout.tabs[i].w, h: TAB_H })) {
|
|
95
|
+
hover = i + 1;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (hover !== this.hoverIdx) {
|
|
101
|
+
this.hoverIdx = hover;
|
|
102
|
+
this.app.render();
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (ev.type === "click") {
|
|
107
|
+
const layout = this.layout();
|
|
108
|
+
if (!layout)
|
|
109
|
+
return;
|
|
110
|
+
if (ev.button === 2)
|
|
111
|
+
return; // right click: no-op on start screen
|
|
112
|
+
if (ev.y === 0) {
|
|
113
|
+
this.app.openModelConfig();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (inRect(ev.x, ev.y, { x: layout.inputX, y: layout.inputY, w: layout.inputW, h: INPUT_H })) {
|
|
117
|
+
this.focusIdx = 0;
|
|
118
|
+
if (ev.y === layout.inputY + 1 && ev.x >= layout.inputX + 2) {
|
|
119
|
+
const innerW = layout.inputW - 4;
|
|
120
|
+
const col = Math.min(innerW - 1, ev.x - (layout.inputX + 2));
|
|
121
|
+
const win = computeInputWindow(this.inputValue, this.cursor, innerW);
|
|
122
|
+
this.cursor = cursorFromColumn(this.inputValue, col, win);
|
|
123
|
+
}
|
|
124
|
+
this.app.render();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (ev.y >= layout.tabY && ev.y < layout.tabY + TAB_H) {
|
|
128
|
+
for (let i = 0; i < layout.tabs.length; i++) {
|
|
129
|
+
if (inRect(ev.x, ev.y, { x: layout.tabs[i].x, y: layout.tabY, w: layout.tabs[i].w, h: TAB_H })) {
|
|
130
|
+
this.focusIdx = i + 1;
|
|
131
|
+
this.hoverIdx = i + 1;
|
|
132
|
+
this.enterTab();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
layout() {
|
|
141
|
+
const w = this.app.width;
|
|
142
|
+
const h = this.app.height;
|
|
143
|
+
const inputW = Math.min(78, w - 4);
|
|
144
|
+
const inputX = Math.floor((w - inputW) / 2);
|
|
145
|
+
const logoLines = LOGO.replace(/\n+$/, "").split("\n");
|
|
146
|
+
const gap = 1;
|
|
147
|
+
const contentH = logoLines.length + 1 + INPUT_H + gap + TAB_H;
|
|
148
|
+
const inputY = centerVertically(h, contentH) + logoLines.length + 1;
|
|
149
|
+
const tabY = inputY + INPUT_H + gap;
|
|
150
|
+
const tabGap = 2;
|
|
151
|
+
const tabInner = inputW - tabGap * (TABS.length - 1);
|
|
152
|
+
const baseW = Math.floor(tabInner / TABS.length);
|
|
153
|
+
const extra = tabInner - baseW * TABS.length;
|
|
154
|
+
const tabs = [];
|
|
155
|
+
let tx = inputX;
|
|
156
|
+
for (let i = 0; i < TABS.length; i++) {
|
|
157
|
+
const tw = baseW + (i < extra ? 1 : 0);
|
|
158
|
+
tabs.push({ x: tx, w: tw });
|
|
159
|
+
tx += tw + tabGap;
|
|
160
|
+
}
|
|
161
|
+
return { inputX, inputY, inputW, tabY, tabs };
|
|
162
|
+
}
|
|
75
163
|
move(delta) {
|
|
76
164
|
this.focusIdx = (this.focusIdx + delta + FOCUS_COUNT) % FOCUS_COUNT;
|
|
165
|
+
this.hoverIdx = -1;
|
|
77
166
|
this.app.render();
|
|
78
167
|
}
|
|
79
168
|
enterTab() {
|
|
@@ -94,9 +183,7 @@ export class StartScreen extends Screen {
|
|
|
94
183
|
const logoMax = logoLines.reduce((m, l) => Math.max(m, l.length), 0);
|
|
95
184
|
const logoX = Math.max(0, Math.floor((w - logoMax) / 2));
|
|
96
185
|
const gap = 1;
|
|
97
|
-
const
|
|
98
|
-
const tabH = 3;
|
|
99
|
-
const contentH = logoLines.length + 1 + inputBoxH + gap + tabH;
|
|
186
|
+
const contentH = logoLines.length + 1 + INPUT_H + gap + TAB_H;
|
|
100
187
|
const top = centerVertically(h, contentH);
|
|
101
188
|
let y = top;
|
|
102
189
|
for (const line of logoLines) {
|
|
@@ -108,7 +195,7 @@ export class StartScreen extends Screen {
|
|
|
108
195
|
active: this.focusIdx === 0,
|
|
109
196
|
placeholder: "Type a message to start a new chat...",
|
|
110
197
|
});
|
|
111
|
-
y +=
|
|
198
|
+
y += INPUT_H + gap;
|
|
112
199
|
const tabGap = 2;
|
|
113
200
|
const tabInner = inputW - tabGap * (TABS.length - 1);
|
|
114
201
|
const baseW = Math.floor(tabInner / TABS.length);
|
|
@@ -116,14 +203,13 @@ export class StartScreen extends Screen {
|
|
|
116
203
|
let tx = inputX;
|
|
117
204
|
for (let i = 0; i < TABS.length; i++) {
|
|
118
205
|
const tw = baseW + (i < extra ? 1 : 0);
|
|
119
|
-
|
|
206
|
+
const selected = i === this.focusIdx - 1 || i === this.hoverIdx - 1;
|
|
207
|
+
drawTabButton(buf, tx, y, tw, TABS[i], selected);
|
|
120
208
|
tx += tw + tabGap;
|
|
121
209
|
}
|
|
122
|
-
const hintY = y +
|
|
210
|
+
const hintY = y + TAB_H + 1;
|
|
123
211
|
if (hintY < h) {
|
|
124
|
-
buf
|
|
125
|
-
fg: C.dim,
|
|
126
|
-
});
|
|
212
|
+
drawHint(buf, hintY, "←/→ / ↑/↓ switch · Enter confirm · click to choose · Ctrl+E model · Ctrl+Q quit");
|
|
127
213
|
}
|
|
128
214
|
}
|
|
129
215
|
}
|
package/dist/state.js
CHANGED
package/dist/terminal/input.js
CHANGED
|
@@ -155,6 +155,16 @@ export class InputReader {
|
|
|
155
155
|
this.emitKey("end", false, false, false, "\x1b[4~");
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Parse SGR (extended) mouse sequences: ESC [ < b ; x ; y M/m
|
|
160
|
+
*
|
|
161
|
+
* Button codes (SGR mode, any-event tracking ?1003):
|
|
162
|
+
* 0/1/2 left/middle/right press
|
|
163
|
+
* 3 release (ignored)
|
|
164
|
+
* 32 motion with no buttons held (hover)
|
|
165
|
+
* 35 motion with left button held (drag)
|
|
166
|
+
* 64/65 wheel up / wheel down
|
|
167
|
+
*/
|
|
158
168
|
handleMouse(s, final) {
|
|
159
169
|
const parts = s.slice(1).split(";");
|
|
160
170
|
if (parts.length < 3)
|
|
@@ -170,6 +180,10 @@ export class InputReader {
|
|
|
170
180
|
else if (b === 65) {
|
|
171
181
|
this.emitMouse({ type: "wheel", dir: 1, x, y });
|
|
172
182
|
}
|
|
183
|
+
else if (final === "M" && (b === 32 || b === 35)) {
|
|
184
|
+
// hover / drag motion
|
|
185
|
+
this.emitMouse({ type: "move", x, y });
|
|
186
|
+
}
|
|
173
187
|
else if (final === "M" && (b === 0 || b === 1 || b === 2)) {
|
|
174
188
|
this.emitMouse({ type: "click", x, y, button: b });
|
|
175
189
|
}
|
|
@@ -11,6 +11,8 @@ function styleKey(st) {
|
|
|
11
11
|
k += "d";
|
|
12
12
|
if (st.inverse)
|
|
13
13
|
k += "i";
|
|
14
|
+
if (st.italic)
|
|
15
|
+
k += "I";
|
|
14
16
|
return k;
|
|
15
17
|
}
|
|
16
18
|
function sgr(st) {
|
|
@@ -19,6 +21,8 @@ function sgr(st) {
|
|
|
19
21
|
codes.push("1");
|
|
20
22
|
if (st.dim)
|
|
21
23
|
codes.push("2");
|
|
24
|
+
if (st.italic)
|
|
25
|
+
codes.push("3");
|
|
22
26
|
if (st.inverse)
|
|
23
27
|
codes.push("7");
|
|
24
28
|
if (st.fg)
|
|
@@ -45,10 +49,10 @@ export class Renderer {
|
|
|
45
49
|
init() {
|
|
46
50
|
this.write("\x1b[?1049h"); // alternate screen
|
|
47
51
|
this.write("\x1b[?25l"); // hide cursor
|
|
48
|
-
this.write("\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?1015h"); // mouse tracking
|
|
52
|
+
this.write("\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h\x1b[?1015h"); // mouse tracking (incl. any-event for hover)
|
|
49
53
|
}
|
|
50
54
|
dispose() {
|
|
51
|
-
this.write("\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1015l");
|
|
55
|
+
this.write("\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l");
|
|
52
56
|
this.write("\x1b[?25h");
|
|
53
57
|
this.write("\x1b[?1049l");
|
|
54
58
|
this.write("\x1b[0m");
|
package/dist/ui.js
CHANGED
|
@@ -70,25 +70,12 @@ export function wrapText(text, width) {
|
|
|
70
70
|
}
|
|
71
71
|
return lines;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
}
|
|
73
|
+
/**
|
|
74
|
+
* Compute which part of the input value is visible inside the box, given the
|
|
75
|
+
* cursor position. Shared by `drawInputBox` and mouse click handling so the
|
|
76
|
+
* rendered text and click-to-position-cursor always agree.
|
|
77
|
+
*/
|
|
78
|
+
export function computeInputWindow(value, cursorPos, innerW) {
|
|
92
79
|
const chars = Array.from(value);
|
|
93
80
|
const widths = chars.map((ch) => wcwidth(ch.codePointAt(0) ?? 0));
|
|
94
81
|
const cursorCp = Math.min(Array.from(value.slice(0, cursorPos)).length, chars.length);
|
|
@@ -113,22 +100,80 @@ export function drawInputBox(buf, x, y, w, value, cursorPos, opts = {}) {
|
|
|
113
100
|
let prefixW = 0;
|
|
114
101
|
for (let i = 0; i < startCp; i++)
|
|
115
102
|
prefixW += widths[i];
|
|
116
|
-
let
|
|
103
|
+
let visibleCount = 0;
|
|
117
104
|
let acc = 0;
|
|
118
105
|
for (let i = startCp; i < chars.length; i++) {
|
|
119
|
-
if (acc + widths[i] > innerW)
|
|
120
|
-
endCp = i;
|
|
106
|
+
if (acc + widths[i] > innerW)
|
|
121
107
|
break;
|
|
122
|
-
}
|
|
123
108
|
acc += widths[i];
|
|
109
|
+
visibleCount++;
|
|
124
110
|
}
|
|
125
|
-
|
|
111
|
+
return { startCp, prefixW, visibleCount };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Map a column inside the input box (0-based, relative to the inner area) to a
|
|
115
|
+
* cursor index in the full value.
|
|
116
|
+
*/
|
|
117
|
+
export function cursorFromColumn(value, col, win) {
|
|
118
|
+
const chars = Array.from(value);
|
|
119
|
+
if (col <= 0)
|
|
120
|
+
return win.startCp;
|
|
121
|
+
let acc = 0;
|
|
122
|
+
let i = win.startCp;
|
|
123
|
+
while (i < chars.length) {
|
|
124
|
+
const w = wcwidth(chars[i].codePointAt(0) ?? 0);
|
|
125
|
+
if (col < acc + w)
|
|
126
|
+
return i;
|
|
127
|
+
acc += w;
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
return i;
|
|
131
|
+
}
|
|
132
|
+
export function inRect(px, py, r) {
|
|
133
|
+
return px >= r.x && px < r.x + r.w && py >= r.y && py < r.y + r.h;
|
|
134
|
+
}
|
|
135
|
+
export function drawHint(buf, y, text) {
|
|
136
|
+
const max = buf.width;
|
|
137
|
+
const t = displayWidth(text) <= max ? text : truncateByWidth(text, max - 1) + "…";
|
|
138
|
+
buf.centerText(y, t, { fg: C.dim });
|
|
139
|
+
}
|
|
140
|
+
export function drawInputBox(buf, x, y, w, value, cursorPos, opts = {}) {
|
|
141
|
+
const active = opts.active ?? true;
|
|
142
|
+
const dimmed = opts.dimmed ?? false;
|
|
143
|
+
const placeholder = opts.placeholder ?? "";
|
|
144
|
+
const h = 3;
|
|
145
|
+
const innerX = x + 2;
|
|
146
|
+
const innerW = w - 4;
|
|
147
|
+
const innerY = y + 1;
|
|
148
|
+
if (active)
|
|
149
|
+
buf.fillRect(x, y, w, h, { bg: C.highlightBg });
|
|
150
|
+
const textStyle = dimmed
|
|
151
|
+
? { fg: C.dim, bg: C.highlightBg }
|
|
152
|
+
: active
|
|
153
|
+
? { fg: C.white, bg: C.highlightBg }
|
|
154
|
+
: { fg: C.dim };
|
|
155
|
+
if (value.length === 0) {
|
|
156
|
+
buf.writeText(innerX, innerY, truncateByWidth(placeholder, innerW), textStylePlaceholder(active));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const win = computeInputWindow(value, cursorPos, innerW);
|
|
160
|
+
const chars = Array.from(value);
|
|
161
|
+
const visible = chars.slice(win.startCp, win.startCp + win.visibleCount).join("");
|
|
126
162
|
buf.writeText(innerX, innerY, visible, textStyle);
|
|
127
|
-
const cursorScreenCol =
|
|
163
|
+
const cursorScreenCol = cursorColAt(value, cursorPos, win);
|
|
128
164
|
if (active && cursorScreenCol >= 0 && cursorScreenCol < innerW) {
|
|
129
165
|
buf.set(innerX + cursorScreenCol, innerY, " ", { fg: C.black, bg: C.accent });
|
|
130
166
|
}
|
|
131
167
|
}
|
|
168
|
+
function cursorColAt(value, cursorPos, win) {
|
|
169
|
+
const chars = Array.from(value);
|
|
170
|
+
const cursorCp = Math.min(Array.from(value.slice(0, cursorPos)).length, chars.length);
|
|
171
|
+
let col = win.prefixW;
|
|
172
|
+
for (let i = win.startCp; i < cursorCp; i++) {
|
|
173
|
+
col += wcwidth(chars[i].codePointAt(0) ?? 0);
|
|
174
|
+
}
|
|
175
|
+
return col - win.prefixW;
|
|
176
|
+
}
|
|
132
177
|
function textStylePlaceholder(active) {
|
|
133
178
|
return active ? { fg: C.dim, bg: C.highlightBg } : { fg: C.dim };
|
|
134
179
|
}
|