@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 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, cursorInfo) {
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, cursorInfo);
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, cursorInfo) {
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, cursorInfo);
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, cursorInfo) {
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 (cursorInfo && cursorInfo.nodeId === node.focusId) {
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 (cursorInfo && cursorInfo.nodeId === node.focusId) {
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
- const existing = fb.get(cursorX, cursorY);
1256
- const cursorChar = existing?.ch && existing.ch !== " " ? existing.ch : "\u258C";
1257
- const cursorFg = inherited.bg ?? "black";
1258
- const cursorBg = inherited.color ?? "white";
1259
- fb.setChar(
1260
- cursorX,
1261
- cursorY,
1262
- cursorChar,
1263
- cursorFg,
1264
- cursorBg,
1265
- existing?.bold,
1266
- existing?.dim,
1267
- existing?.italic,
1268
- false
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 (cursorInfo && cursorInfo.nodeId === node.focusId) {
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
- const existing = fb.get(cursorX, innerY);
1300
- const cursorChar = existing?.ch && existing.ch !== " " ? existing.ch : "\u258C";
1301
- const cursorFg = inherited.bg ?? "black";
1302
- const cursorBg = inherited.color ?? "white";
1303
- fb.setChar(
1304
- cursorX,
1305
- innerY,
1306
- cursorChar,
1307
- cursorFg,
1308
- cursorBg,
1309
- existing?.bold,
1310
- existing?.dim,
1311
- existing?.italic,
1312
- false
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, cursorInfo);
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
- if (key.shift) {
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") {