@xingjisoft/neutrino 0.1.0 → 0.1.2

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/dist/ui.js CHANGED
@@ -70,25 +70,12 @@ export function wrapText(text, width) {
70
70
  }
71
71
  return lines;
72
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
- }
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,20 +100,99 @@ 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 endCp = chars.length;
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++;
110
+ }
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 cursorOn = opts.cursorOn ?? true;
145
+ const h = 3;
146
+ const innerX = x + 2;
147
+ const innerW = w - 4;
148
+ const innerY = y + 1;
149
+ if (active)
150
+ buf.fillRect(x, y, w, h, { bg: C.highlightBg });
151
+ const textStyle = dimmed
152
+ ? { fg: C.dim, bg: C.highlightBg }
153
+ : active
154
+ ? { fg: C.white, bg: C.highlightBg }
155
+ : { fg: C.dim };
156
+ if (value.length === 0) {
157
+ buf.writeText(innerX, innerY, truncateByWidth(placeholder, innerW), textStylePlaceholder(active));
158
+ return;
124
159
  }
125
- const visible = chars.slice(startCp, endCp).join("");
160
+ const win = computeInputWindow(value, cursorPos, innerW);
161
+ const chars = Array.from(value);
162
+ // Codepoint index of the cursor (cursorPos is a UTF-16 offset).
163
+ const cursorCp = Math.min(Array.from(value.slice(0, cursorPos)).length, chars.length);
164
+ const visible = chars.slice(win.startCp, win.startCp + win.visibleCount).join("");
126
165
  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 });
166
+ // Blinking block cursor. While it is "on" the character underneath is drawn
167
+ // inside the block in the opposite colour so the text stays legible instead
168
+ // of being hidden by the cursor.
169
+ const cursorScreenCol = cursorColAt(chars, cursorCp, win);
170
+ if (active && cursorOn && cursorScreenCol >= 0 && cursorScreenCol < innerW) {
171
+ drawBlockCursor(buf, innerX + cursorScreenCol, innerY, chars, cursorCp, cursorScreenCol, innerW);
172
+ }
173
+ }
174
+ function cursorColAt(chars, cursorCp, win) {
175
+ let col = win.prefixW;
176
+ for (let i = win.startCp; i < cursorCp; i++) {
177
+ col += wcwidth(chars[i].codePointAt(0) ?? 0);
178
+ }
179
+ return col - win.prefixW;
180
+ }
181
+ /**
182
+ * Draw a single block cursor cell. The glyph under the cursor (the character
183
+ * immediately to its right) is rendered in the opposite colour — a dark glyph
184
+ * on the accent block — so it remains readable. Past the end of the text, or
185
+ * when the glyph would not fit, a solid block is drawn instead.
186
+ */
187
+ function drawBlockCursor(buf, x, y, chars, cursorCp, col, innerW) {
188
+ const cursorStyle = { fg: C.black, bg: C.accent };
189
+ const ch = cursorCp < chars.length ? chars[cursorCp] : "";
190
+ const cw = ch ? wcwidth(ch.codePointAt(0) ?? 0) : 0;
191
+ if (ch && ch !== " " && cw >= 1 && col + cw <= innerW) {
192
+ buf.writeText(x, y, ch, cursorStyle);
193
+ }
194
+ else {
195
+ buf.set(x, y, " ", cursorStyle);
130
196
  }
131
197
  }
132
198
  function textStylePlaceholder(active) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingjisoft/neutrino",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "XINGJI Neutrino - a CLI coding agent in the terminal.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsc -p tsconfig.json",
20
+ "test": "npm run build && node scripts/test-mouse.mjs && node scripts/test-cursor.mjs && node scripts/test-selection.mjs && node scripts/test-render.mjs",
20
21
  "prepare": "npm run build"
21
22
  },
22
23
  "keywords": [