@nick-skriabin/glyph 0.1.4 → 0.1.6
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 +26 -0
- package/dist/index.cjs +97 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +97 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -488,12 +488,38 @@ const app = render(<App />, {
|
|
|
488
488
|
stdout: process.stdout,
|
|
489
489
|
stdin: process.stdin,
|
|
490
490
|
debug: false,
|
|
491
|
+
useNativeCursor: true, // Use terminal's native cursor (default: true)
|
|
491
492
|
});
|
|
492
493
|
|
|
493
494
|
app.unmount(); // Tear down
|
|
494
495
|
app.exit(); // Unmount and exit process
|
|
495
496
|
```
|
|
496
497
|
|
|
498
|
+
### Options
|
|
499
|
+
|
|
500
|
+
| Option | Type | Default | Description |
|
|
501
|
+
|--------|------|---------|-------------|
|
|
502
|
+
| `stdout` | `NodeJS.WriteStream` | `process.stdout` | Output stream |
|
|
503
|
+
| `stdin` | `NodeJS.ReadStream` | `process.stdin` | Input stream |
|
|
504
|
+
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
505
|
+
| `useNativeCursor` | `boolean` | `true` | Use terminal's native cursor instead of simulated one |
|
|
506
|
+
|
|
507
|
+
### Native Cursor
|
|
508
|
+
|
|
509
|
+
By default, Glyph uses the terminal's native cursor, which enables:
|
|
510
|
+
|
|
511
|
+
- **Cursor shaders** in terminals that support them (e.g., Ghostty)
|
|
512
|
+
- **Custom cursor shapes** (block, beam, underline) from terminal settings
|
|
513
|
+
- **Cursor animations** and blinking behavior
|
|
514
|
+
|
|
515
|
+
The native cursor is automatically shown when an input is focused and hidden otherwise.
|
|
516
|
+
|
|
517
|
+
To use the simulated cursor instead (inverted colors, no shader support):
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
render(<App />, { useNativeCursor: false });
|
|
521
|
+
```
|
|
522
|
+
|
|
497
523
|
---
|
|
498
524
|
|
|
499
525
|
## Examples
|
package/dist/index.cjs
CHANGED
|
@@ -324,6 +324,18 @@ var Terminal = class {
|
|
|
324
324
|
showCursor() {
|
|
325
325
|
this.write(`${CSI}?25h`);
|
|
326
326
|
}
|
|
327
|
+
/** Move cursor to (x, y) position (0-indexed) */
|
|
328
|
+
moveCursor(x, y) {
|
|
329
|
+
this.write(`${CSI}${y + 1};${x + 1}H`);
|
|
330
|
+
}
|
|
331
|
+
/** Set cursor color using OSC 12 */
|
|
332
|
+
setCursorColor(color) {
|
|
333
|
+
this.write(`${ESC}]12;${color}\x07`);
|
|
334
|
+
}
|
|
335
|
+
/** Reset cursor color to terminal default */
|
|
336
|
+
resetCursorColor() {
|
|
337
|
+
this.write(`${ESC}]112\x07`);
|
|
338
|
+
}
|
|
327
339
|
enterAltScreen() {
|
|
328
340
|
this.write(`${CSI}?1049h`);
|
|
329
341
|
}
|
|
@@ -352,6 +364,7 @@ var Terminal = class {
|
|
|
352
364
|
this.escFlushTimer = null;
|
|
353
365
|
}
|
|
354
366
|
this.resetStyles();
|
|
367
|
+
this.resetCursorColor();
|
|
355
368
|
this.showCursor();
|
|
356
369
|
this.exitAltScreen();
|
|
357
370
|
this.exitRawMode();
|
|
@@ -365,6 +378,7 @@ var Terminal = class {
|
|
|
365
378
|
this.oscState = "normal";
|
|
366
379
|
this.oscAccum = "";
|
|
367
380
|
this.resetStyles();
|
|
381
|
+
this.resetCursorColor();
|
|
368
382
|
this.showCursor();
|
|
369
383
|
this.exitAltScreen();
|
|
370
384
|
this.exitRawMode();
|
|
@@ -815,6 +829,10 @@ function colorsEqual(a, b) {
|
|
|
815
829
|
}
|
|
816
830
|
return false;
|
|
817
831
|
}
|
|
832
|
+
function getContrastCursorColor(bg) {
|
|
833
|
+
if (!bg) return "white";
|
|
834
|
+
return isLightColor(bg) ? "black" : "white";
|
|
835
|
+
}
|
|
818
836
|
|
|
819
837
|
// src/paint/framebuffer.ts
|
|
820
838
|
var Framebuffer = class _Framebuffer {
|
|
@@ -1031,8 +1049,9 @@ function wordWrap(text, maxWidth) {
|
|
|
1031
1049
|
}
|
|
1032
1050
|
return lines.length > 0 ? lines : [""];
|
|
1033
1051
|
}
|
|
1034
|
-
function paintTree(roots, fb,
|
|
1052
|
+
function paintTree(roots, fb, options = {}) {
|
|
1035
1053
|
fb.clear();
|
|
1054
|
+
const result = {};
|
|
1036
1055
|
const entries = [];
|
|
1037
1056
|
const screenClip = { x: 0, y: 0, width: fb.width, height: fb.height };
|
|
1038
1057
|
for (const root of roots) {
|
|
@@ -1041,8 +1060,12 @@ function paintTree(roots, fb, cursorInfo) {
|
|
|
1041
1060
|
}
|
|
1042
1061
|
entries.sort((a, b) => a.zIndex - b.zIndex);
|
|
1043
1062
|
for (const entry of entries) {
|
|
1044
|
-
paintNode(entry.node, fb, entry.clip,
|
|
1063
|
+
const nodeResult = paintNode(entry.node, fb, entry.clip, options);
|
|
1064
|
+
if (nodeResult?.cursorPosition) {
|
|
1065
|
+
result.cursorPosition = nodeResult.cursorPosition;
|
|
1066
|
+
}
|
|
1045
1067
|
}
|
|
1068
|
+
return result;
|
|
1046
1069
|
}
|
|
1047
1070
|
function collectPaintEntries(node, parentClip, parentZ, entries) {
|
|
1048
1071
|
if (node.hidden) return;
|
|
@@ -1075,7 +1098,7 @@ function intersectClip(a, b) {
|
|
|
1075
1098
|
function isInClip(x, y, clip) {
|
|
1076
1099
|
return x >= clip.x && x < clip.x + clip.width && y >= clip.y && y < clip.y + clip.height;
|
|
1077
1100
|
}
|
|
1078
|
-
function paintNode(node, fb, clip,
|
|
1101
|
+
function paintNode(node, fb, clip, options = {}) {
|
|
1079
1102
|
const { x, y, width, height, innerX, innerY, innerWidth, innerHeight } = node.layout;
|
|
1080
1103
|
const style = node.style;
|
|
1081
1104
|
if (width <= 0 || height <= 0) return;
|
|
@@ -1112,8 +1135,9 @@ function paintNode(node, fb, clip, cursorInfo) {
|
|
|
1112
1135
|
if (node.type === "text") {
|
|
1113
1136
|
paintText(node, fb, clip);
|
|
1114
1137
|
} else if (node.type === "input") {
|
|
1115
|
-
paintInput(node, fb, clip,
|
|
1138
|
+
return paintInput(node, fb, clip, options);
|
|
1116
1139
|
}
|
|
1140
|
+
return void 0;
|
|
1117
1141
|
}
|
|
1118
1142
|
function setClipped(fb, clip, x, y, ch, fg, bg, bold, dim, italic, underline) {
|
|
1119
1143
|
if (isInClip(x, y, clip)) {
|
|
@@ -1166,7 +1190,8 @@ function paintText(node, fb, clip) {
|
|
|
1166
1190
|
}
|
|
1167
1191
|
}
|
|
1168
1192
|
}
|
|
1169
|
-
function paintInput(node, fb, clip,
|
|
1193
|
+
function paintInput(node, fb, clip, options = {}) {
|
|
1194
|
+
const { cursorInfo, useNativeCursor } = options;
|
|
1170
1195
|
const { innerX, innerY, innerWidth, innerHeight } = node.layout;
|
|
1171
1196
|
if (innerWidth <= 0 || innerHeight <= 0) return;
|
|
1172
1197
|
const value = node.props.value ?? node.props.defaultValue ?? "";
|
|
@@ -1180,13 +1205,15 @@ function paintInput(node, fb, clip, cursorInfo) {
|
|
|
1180
1205
|
const fg = isPlaceholder ? placeholderFg : autoFg ?? inherited.color ?? node.style.color;
|
|
1181
1206
|
const textFg = isPlaceholder ? placeholderFg : fg;
|
|
1182
1207
|
const textDim = isPlaceholder ? true : inherited.dim;
|
|
1208
|
+
const isFocused = cursorInfo && cursorInfo.nodeId === node.focusId;
|
|
1209
|
+
let result;
|
|
1183
1210
|
if (multiline && !isPlaceholder) {
|
|
1184
1211
|
const wrapMode = node.style.wrap ?? "wrap";
|
|
1185
1212
|
const rawLines = displayText.split("\n");
|
|
1186
1213
|
const wrappedLines = wrapLines(rawLines, innerWidth, wrapMode);
|
|
1187
1214
|
let cursorScreenLine = 0;
|
|
1188
1215
|
let cursorScreenCol = 0;
|
|
1189
|
-
if (
|
|
1216
|
+
if (isFocused) {
|
|
1190
1217
|
const pos = cursorInfo.position;
|
|
1191
1218
|
let logicalLine = 0;
|
|
1192
1219
|
let offsetInLogicalLine = pos;
|
|
@@ -1245,28 +1272,32 @@ function paintInput(node, fb, clip, cursorInfo) {
|
|
|
1245
1272
|
col += charWidth;
|
|
1246
1273
|
}
|
|
1247
1274
|
}
|
|
1248
|
-
if (
|
|
1275
|
+
if (isFocused) {
|
|
1249
1276
|
const screenRow = cursorScreenLine - scrollOffset;
|
|
1250
1277
|
if (screenRow >= 0 && screenRow < innerHeight) {
|
|
1251
1278
|
const cCol = Math.min(cursorScreenCol, innerWidth - 1);
|
|
1252
1279
|
const cursorX = innerX + cCol;
|
|
1253
1280
|
const cursorY = innerY + screenRow;
|
|
1254
1281
|
if (isInClip(cursorX, cursorY, clip) && cursorX < innerX + innerWidth) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1282
|
+
if (useNativeCursor) {
|
|
1283
|
+
result = { cursorPosition: { x: cursorX, y: cursorY, bg: inherited.bg } };
|
|
1284
|
+
} else {
|
|
1285
|
+
const existing = fb.get(cursorX, cursorY);
|
|
1286
|
+
const cursorChar = existing?.ch && existing.ch !== " " ? existing.ch : "\u258C";
|
|
1287
|
+
const cursorFg = inherited.bg ?? "black";
|
|
1288
|
+
const cursorBg = inherited.color ?? "white";
|
|
1289
|
+
fb.setChar(
|
|
1290
|
+
cursorX,
|
|
1291
|
+
cursorY,
|
|
1292
|
+
cursorChar,
|
|
1293
|
+
cursorFg,
|
|
1294
|
+
cursorBg,
|
|
1295
|
+
existing?.bold,
|
|
1296
|
+
existing?.dim,
|
|
1297
|
+
existing?.italic,
|
|
1298
|
+
false
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1270
1301
|
}
|
|
1271
1302
|
}
|
|
1272
1303
|
}
|
|
@@ -1292,28 +1323,33 @@ function paintInput(node, fb, clip, cursorInfo) {
|
|
|
1292
1323
|
}
|
|
1293
1324
|
col += charWidth;
|
|
1294
1325
|
}
|
|
1295
|
-
if (
|
|
1326
|
+
if (isFocused) {
|
|
1296
1327
|
const cursorCol = Math.min(cursorInfo.position, innerWidth - 1);
|
|
1297
1328
|
const cursorX = innerX + cursorCol;
|
|
1298
1329
|
if (isInClip(cursorX, innerY, clip) && cursorX < innerX + innerWidth) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1330
|
+
if (useNativeCursor) {
|
|
1331
|
+
result = { cursorPosition: { x: cursorX, y: innerY, bg: inherited.bg } };
|
|
1332
|
+
} else {
|
|
1333
|
+
const existing = fb.get(cursorX, innerY);
|
|
1334
|
+
const cursorChar = existing?.ch && existing.ch !== " " ? existing.ch : "\u258C";
|
|
1335
|
+
const cursorFg = inherited.bg ?? "black";
|
|
1336
|
+
const cursorBg = inherited.color ?? "white";
|
|
1337
|
+
fb.setChar(
|
|
1338
|
+
cursorX,
|
|
1339
|
+
innerY,
|
|
1340
|
+
cursorChar,
|
|
1341
|
+
cursorFg,
|
|
1342
|
+
cursorBg,
|
|
1343
|
+
existing?.bold,
|
|
1344
|
+
existing?.dim,
|
|
1345
|
+
existing?.italic,
|
|
1346
|
+
false
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1314
1349
|
}
|
|
1315
1350
|
}
|
|
1316
1351
|
}
|
|
1352
|
+
return result;
|
|
1317
1353
|
}
|
|
1318
1354
|
|
|
1319
1355
|
// src/paint/diff.ts
|
|
@@ -1542,8 +1578,10 @@ function render(element, opts = {}) {
|
|
|
1542
1578
|
const stdout = opts.stdout ?? process.stdout;
|
|
1543
1579
|
const stdin = opts.stdin ?? process.stdin;
|
|
1544
1580
|
const debug = opts.debug ?? false;
|
|
1581
|
+
const useNativeCursor = opts.useNativeCursor ?? true;
|
|
1545
1582
|
const terminal = new Terminal(stdout, stdin);
|
|
1546
1583
|
terminal.setup();
|
|
1584
|
+
let nativeCursorVisible = false;
|
|
1547
1585
|
terminal.queryPalette().then((palette) => {
|
|
1548
1586
|
setTerminalPalette(palette);
|
|
1549
1587
|
fullRedraw = true;
|
|
@@ -1712,11 +1750,30 @@ function render(element, opts = {}) {
|
|
|
1712
1750
|
};
|
|
1713
1751
|
}
|
|
1714
1752
|
}
|
|
1715
|
-
paintTree(container.children, currentFb,
|
|
1753
|
+
const paintResult = paintTree(container.children, currentFb, {
|
|
1754
|
+
cursorInfo,
|
|
1755
|
+
useNativeCursor
|
|
1756
|
+
});
|
|
1716
1757
|
const output = diffFramebuffers(prevFb, currentFb, fullRedraw);
|
|
1717
1758
|
if (output.length > 0) {
|
|
1718
1759
|
terminal.write(output);
|
|
1719
1760
|
}
|
|
1761
|
+
if (useNativeCursor) {
|
|
1762
|
+
if (paintResult.cursorPosition) {
|
|
1763
|
+
const cursorColor = getContrastCursorColor(paintResult.cursorPosition.bg);
|
|
1764
|
+
terminal.setCursorColor(cursorColor);
|
|
1765
|
+
terminal.moveCursor(paintResult.cursorPosition.x, paintResult.cursorPosition.y);
|
|
1766
|
+
if (!nativeCursorVisible) {
|
|
1767
|
+
terminal.showCursor();
|
|
1768
|
+
nativeCursorVisible = true;
|
|
1769
|
+
}
|
|
1770
|
+
} else {
|
|
1771
|
+
if (nativeCursorVisible) {
|
|
1772
|
+
terminal.hideCursor();
|
|
1773
|
+
nativeCursorVisible = false;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1720
1777
|
for (let i = 0; i < currentFb.cells.length; i++) {
|
|
1721
1778
|
prevFb.cells[i] = { ...currentFb.cells[i] };
|
|
1722
1779
|
}
|
|
@@ -2051,22 +2108,6 @@ function Input(props) {
|
|
|
2051
2108
|
}
|
|
2052
2109
|
return true;
|
|
2053
2110
|
}
|
|
2054
|
-
if (key.name === "u") {
|
|
2055
|
-
if (ml) {
|
|
2056
|
-
const { line, lines } = cursorToLineCol(val, pos);
|
|
2057
|
-
const lineStart = lineColToCursor(lines, line, 0);
|
|
2058
|
-
if (pos > lineStart) {
|
|
2059
|
-
const newVal = val.slice(0, lineStart) + val.slice(pos);
|
|
2060
|
-
updateValue(newVal, lineStart);
|
|
2061
|
-
}
|
|
2062
|
-
} else {
|
|
2063
|
-
if (pos > 0) {
|
|
2064
|
-
const newVal = val.slice(pos);
|
|
2065
|
-
updateValue(newVal, 0);
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
return true;
|
|
2069
|
-
}
|
|
2070
2111
|
if (key.name === "k") {
|
|
2071
2112
|
if (ml) {
|
|
2072
2113
|
const { line, lines } = cursorToLineCol(val, pos);
|
|
@@ -2467,9 +2508,7 @@ function ScrollView({
|
|
|
2467
2508
|
if (key.name === "d") {
|
|
2468
2509
|
setOffset(offset + halfPage);
|
|
2469
2510
|
} else if (key.name === "u") {
|
|
2470
|
-
|
|
2471
|
-
setOffset(offset - halfPage);
|
|
2472
|
-
}
|
|
2511
|
+
setOffset(offset - halfPage);
|
|
2473
2512
|
} else if (key.name === "f") {
|
|
2474
2513
|
setOffset(offset + fullPage);
|
|
2475
2514
|
} else if (key.name === "b") {
|