@oxecli/oxe 1.0.66 → 1.0.67

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.
Files changed (3) hide show
  1. package/dist/tools.js +16 -6
  2. package/dist/ui.js +276 -35
  3. package/package.json +1 -1
package/dist/tools.js CHANGED
@@ -72,9 +72,12 @@ function displayDiff(pathName, oldContent, newContent) {
72
72
  const limit = kind === " " ? max_diff_context_lines : max_diff_lines;
73
73
  const hidden = run.length - limit;
74
74
  for (const r of run.slice(0, limit))
75
- shown.push(truncateDiffLine(r.line));
75
+ shown.push({ kind, line: truncateDiffLine(r.line) });
76
76
  if (hidden > 0) {
77
- shown.push(`… (${hidden} ${hidden === 1 ? "line" : "lines"} of diff hidden) …`);
77
+ shown.push({
78
+ kind: "~",
79
+ line: `… (${hidden} ${hidden === 1 ? "line" : "lines"} of diff hidden) …`,
80
+ });
78
81
  }
79
82
  run = [];
80
83
  };
@@ -87,9 +90,12 @@ function displayDiff(pathName, oldContent, newContent) {
87
90
  }
88
91
  flush();
89
92
  // Build the lines, stripping the leading + / - / space marker for display.
90
- const visible = shown.map((l) => {
91
- const kind = l[0] === "+" ? "+" : l[0] === "-" ? "-" : " ";
92
- return { kind, body: l.slice(1) };
93
+ // Summary lines ("~") are already plain text and are kept whole.
94
+ const visible = shown.map(({ kind, line }) => {
95
+ if (kind === "~")
96
+ return { kind, body: line };
97
+ const k = line[0] === "+" ? "+" : line[0] === "-" ? "-" : " ";
98
+ return { kind: k, body: line.slice(1) };
93
99
  });
94
100
  // A "… (N lines hidden) …" summary line is neutral context.
95
101
  const termW = terminalWidth();
@@ -110,7 +116,11 @@ function displayDiff(pathName, oldContent, newContent) {
110
116
  pendingDiffOutput.push(`\x1b[90m${top}\x1b[0m`);
111
117
  for (const v of visible) {
112
118
  let line;
113
- if (v.kind === "+") {
119
+ if (v.kind === "~") {
120
+ const body = truncateStyled(v.body, innerW - 2);
121
+ line = `\x1b[2m ${body}\x1b[0m`;
122
+ }
123
+ else if (v.kind === "+") {
114
124
  const body = truncateStyled(v.body, innerW - 2);
115
125
  line = `\x1b[32m+ ${body}\x1b[0m`;
116
126
  }
package/dist/ui.js CHANGED
@@ -9,7 +9,7 @@ export function enableRawStdin() {
9
9
  readline.emitKeypressEvents(process.stdin);
10
10
  if (process.stdin.isTTY) {
11
11
  process.stdin.setRawMode(true);
12
- process.stdout.write("\x1b[>1u");
12
+ process.stdout.write("\x1b[?9001h\x1b[>1u");
13
13
  }
14
14
  process.stdin.resume();
15
15
  hideCursor();
@@ -259,7 +259,7 @@ export function markdownToAnsi(text) {
259
259
  const highlighted = highlightCodeLine(cline, codeLang);
260
260
  out.push(`\x1b[90m│\x1b[0m ${highlighted}`);
261
261
  }
262
- out.push(`\x1b[90m╰${"─".repeat(barLen + plainLen(badge) + 2)}╯\x1b[0m`);
262
+ out.push(`\x1b[90m╰${"─".repeat(barLen + plainLen(badge) + 1)}╯\x1b[0m`);
263
263
  codeBuffer = [];
264
264
  codeLang = "";
265
265
  };
@@ -911,27 +911,63 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
911
911
  * modifyOtherKeys: ESC [ 27 ; M ; 13 ~
912
912
  * application: ESC O M
913
913
  */
914
- function csiShiftEnter(seq) {
915
- if (seq === "\x1bOM")
916
- return true;
917
- // modifyOtherKeys: ESC [ 27 ; M ; 13 ~
918
- const mod = seq.match(/^\x1b\[27;(\d+);13[~_]$/);
919
- if (mod) {
920
- const m = parseInt(mod[1], 10);
921
- return Number.isInteger(m) && ((m - 1) & 1) === 1;
914
+ /**
915
+ * Parses a complete (reassembled) escape-sequence encoding of Enter and
916
+ * classifies it as a newline (Shift+Enter) or a submit (plain Enter).
917
+ *
918
+ * Return values:
919
+ * "newline" — Shift+Enter (insert a new row)
920
+ * "submit" — plain Enter (submit the prompt)
921
+ * null — not an Enter sequence
922
+ *
923
+ * Handles every encoding a terminal can produce:
924
+ * CSI-u: ESC [ 13 ; M u (M = xterm bitmask + 1)
925
+ * legacy / kitty: ESC [ 13 ; M ~ / ESC [ 13 ; M _
926
+ * modifyOtherKeys: ESC [ 27 ; M ; 13 ~
927
+ * application (SS3): ESC O M
928
+ * win32-input-mode: ESC [ G ; R ; C ; VK ; MOD u
929
+ * (VK = 13 for Enter, MOD = raw bitmask where
930
+ * bit0 = Shift)
931
+ */
932
+ function parseEnterSequence(seq) {
933
+ if (!seq)
934
+ return null;
935
+ // win32-input-mode: ESC [ guard ; row ; col ; vk ; mod u
936
+ let m = seq.match(/^\x1b\[(\d+);(\d+);(\d+);(\d+);(\d+)u$/);
937
+ if (m) {
938
+ const vk = parseInt(m[4], 10);
939
+ if (vk !== 13)
940
+ return null;
941
+ const mod = parseInt(m[5], 10);
942
+ return mod & 1 ? "newline" : "submit";
922
943
  }
923
- // CSI-u / legacy: ESC [ 13 ; M u (or ~ or _)
924
- const csi = seq.match(/^\x1b\[13;(\d+)[~u_]$/);
925
- if (csi) {
926
- const m = parseInt(csi[1], 10);
927
- return Number.isInteger(m) && ((m - 1) & 1) === 1;
944
+ // modifyOtherKeys / kitty: ESC [ 27 ; M ; 13 ~ (or _)
945
+ m = seq.match(/^\x1b\[27;(\d+);13[~_]$/);
946
+ if (m) {
947
+ const mod = parseInt(m[1], 10);
948
+ if (mod > 0)
949
+ return (mod - 1) & 1 ? "newline" : "submit";
950
+ return null;
928
951
  }
929
- return false;
952
+ // CSI-u / legacy: ESC [ 13 ; M u (or ~ or _)
953
+ m = seq.match(/^\x1b\[13;(\d+)[~u_]$/);
954
+ if (m) {
955
+ const mod = parseInt(m[1], 10);
956
+ if (mod > 0)
957
+ return (mod - 1) & 1 ? "newline" : "submit";
958
+ return null;
959
+ }
960
+ // Application keypad Enter (SS3)
961
+ if (seq === "\x1bOM")
962
+ return "newline";
963
+ return null;
930
964
  }
931
965
  export function isShiftEnterKey(str, key) {
932
966
  if (!key)
933
967
  return false;
934
968
  const seq = key.sequence || str || "";
969
+ if (parseEnterSequence(seq) === "newline")
970
+ return true;
935
971
  // Shift modifier reported by readline on a return/enter/linefeed key.
936
972
  if (key.shift &&
937
973
  (key.name === "return" ||
@@ -941,8 +977,6 @@ export function isShiftEnterKey(str, key) {
941
977
  seq === "\n")) {
942
978
  return true;
943
979
  }
944
- if (csiShiftEnter(seq))
945
- return true;
946
980
  return false;
947
981
  }
948
982
  export const isNewlineKey = isShiftEnterKey;
@@ -952,6 +986,8 @@ export function isSubmitEnterKey(str, key) {
952
986
  if (isShiftEnterKey(str, key))
953
987
  return false;
954
988
  const seq = key.sequence || str || "";
989
+ if (parseEnterSequence(seq) === "submit")
990
+ return true;
955
991
  if ((key.name === "return" ||
956
992
  seq === "\r" ||
957
993
  seq === "\x1b[13u" ||
@@ -961,6 +997,115 @@ export function isSubmitEnterKey(str, key) {
961
997
  }
962
998
  return false;
963
999
  }
1000
+ /**
1001
+ * Maps a Windows virtual-key code + shift state to a printable character, or
1002
+ * null if the code isn't a printable character. US-layout approximation (the
1003
+ * common case); enough for a text prompt.
1004
+ */
1005
+ function vkToChar(vk, shift) {
1006
+ if (vk >= 48 && vk <= 57) {
1007
+ const symbols = [")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
1008
+ return shift ? symbols[vk - 48] : String(vk - 48);
1009
+ }
1010
+ if (vk >= 65 && vk <= 90) {
1011
+ return shift ? String.fromCharCode(vk) : String.fromCharCode(vk).toLowerCase();
1012
+ }
1013
+ if (vk === 186)
1014
+ return shift ? ":" : ";";
1015
+ if (vk === 187)
1016
+ return shift ? "+" : "=";
1017
+ if (vk === 188)
1018
+ return shift ? "<" : ",";
1019
+ if (vk === 189)
1020
+ return shift ? "_" : "-";
1021
+ if (vk === 190)
1022
+ return shift ? ">" : ".";
1023
+ if (vk === 191)
1024
+ return shift ? "?" : "/";
1025
+ if (vk === 192)
1026
+ return shift ? "~" : "`";
1027
+ if (vk === 219)
1028
+ return shift ? "{" : "[";
1029
+ if (vk === 220)
1030
+ return shift ? "|" : "\\";
1031
+ if (vk === 221)
1032
+ return shift ? "}" : "]";
1033
+ if (vk === 222)
1034
+ return shift ? '"' : "'";
1035
+ return null;
1036
+ }
1037
+ /**
1038
+ * Decodes a complete win32-input-mode sequence (ESC [ G ; R ; C ; VK ; MOD u)
1039
+ * into a key action. Returns a descriptor for the prompt to execute, or null
1040
+ * if the sequence isn't a win32-input-mode sequence.
1041
+ */
1042
+ function decodeWin32Key(seq) {
1043
+ const m = seq.match(/^\x1b\[(\d+);(\d+);(\d+);(\d+);(\d+)u$/);
1044
+ if (!m)
1045
+ return null;
1046
+ const vk = parseInt(m[4], 10);
1047
+ const mod = parseInt(m[5], 10);
1048
+ const shift = (mod & 1) === 1;
1049
+ const ctrl = (mod & 4) === 4;
1050
+ const alt = (mod & 2) === 2;
1051
+ const cmd = (c) => ({ type: "cmd", cmd: c });
1052
+ const text = (t) => ({ type: "text", text: t });
1053
+ // Control combos
1054
+ if (ctrl && vk === 67)
1055
+ return cmd("interrupt"); // Ctrl+C
1056
+ if (ctrl && vk === 68)
1057
+ return cmd("eof"); // Ctrl+D
1058
+ if (ctrl && vk === 90)
1059
+ return cmd("eof"); // Ctrl+Z
1060
+ if (ctrl && vk === 65)
1061
+ return cmd("home"); // Ctrl+A
1062
+ if (ctrl && vk === 69)
1063
+ return cmd("end"); // Ctrl+E
1064
+ if (ctrl && vk === 85)
1065
+ return cmd("clear-line"); // Ctrl+U
1066
+ if (ctrl && vk === 75)
1067
+ return cmd("kill-line"); // Ctrl+K
1068
+ if (ctrl && vk === 87)
1069
+ return cmd("delete-word"); // Ctrl+W
1070
+ switch (vk) {
1071
+ case 13: // Enter
1072
+ return shift ? cmd("newline") : cmd("submit");
1073
+ case 8: // Backspace
1074
+ return cmd(ctrl ? "delete-word" : "backspace");
1075
+ case 46: // Delete
1076
+ return cmd("delete");
1077
+ case 37: // Left
1078
+ return ctrl ? cmd("back-word") : cmd("left");
1079
+ case 39: // Right
1080
+ return ctrl ? cmd("forward-word") : cmd("right");
1081
+ case 38: // Up
1082
+ return cmd("up");
1083
+ case 40: // Down
1084
+ return cmd("down");
1085
+ case 36: // Home
1086
+ return cmd("home");
1087
+ case 35: // End
1088
+ return cmd("end");
1089
+ case 33: // PageUp
1090
+ return cmd("up");
1091
+ case 34: // PageDown
1092
+ return cmd("down");
1093
+ case 9: // Tab
1094
+ return cmd("tab");
1095
+ case 32: // Space
1096
+ return text(" ");
1097
+ case 27: // Escape
1098
+ return cmd("escape");
1099
+ default: {
1100
+ if (alt)
1101
+ return null; // Alt combos unsupported -> ignore
1102
+ const ch = vkToChar(vk, shift);
1103
+ if (ch)
1104
+ return text(ch);
1105
+ return cmd("ignore");
1106
+ }
1107
+ }
1108
+ }
964
1109
  export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
965
1110
  return new Promise((resolve, reject) => {
966
1111
  const isTTY = process.stdin.isTTY;
@@ -995,10 +1140,10 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
995
1140
  if (process.stdin.isTTY) {
996
1141
  process.stdin.setRawMode(true);
997
1142
  process.stdout.write("\x1b[?2004h");
998
- // Enable CSI-u "disambiguate" (flags=1) so a terminal that supports it
999
- // reports Shift+Enter as a distinct `ESC [ 13 ; 2 u` code instead of
1000
- // collapsing it into a plain Enter.
1001
- process.stdout.write("\x1b[>1u");
1143
+ // Enable win32-input-mode (preferred by Windows Terminal) and CSI-u
1144
+ // disambiguation so Shift+Enter arrives as a distinct code instead of a
1145
+ // plain Enter. Harmless if a terminal doesn't support them.
1146
+ process.stdout.write("\x1b[?9001h\x1b[>1u");
1002
1147
  }
1003
1148
  process.stdin.resume();
1004
1149
  hideCursor();
@@ -1021,7 +1166,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
1021
1166
  if (done)
1022
1167
  return;
1023
1168
  done = true;
1024
- process.stdout.write("\x1b[?2004l");
1169
+ process.stdout.write("\x1b[?2004l\x1b[?9001l");
1025
1170
  process.stdin.setRawMode(false);
1026
1171
  if (pasteBurstTimer)
1027
1172
  clearTimeout(pasteBurstTimer);
@@ -1342,26 +1487,122 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
1342
1487
  pasteBurstTimer = null;
1343
1488
  }, 500);
1344
1489
  };
1345
- const onKeypress = (str, key) => {
1346
- // Reassemble a modifier-prefixed CSI sequence that readline splits up,
1347
- // e.g. Shift+Enter arriving as `\x1b[27;2;` + "1" + "3" + "~".
1348
- if (key?.sequence === "\x1b[27;" || key?.sequence === "\x1b[27;2;") {
1349
- modSeq = key.sequence;
1490
+ const handleRawSequence = (full) => {
1491
+ // Enter sequences (CSI-u, kitty, legacy, SS3, win32).
1492
+ const enter = parseEnterSequence(full);
1493
+ if (enter === "newline") {
1494
+ insert("\n");
1495
+ repaint();
1496
+ return;
1497
+ }
1498
+ if (enter === "submit") {
1499
+ if (buffer.trim())
1500
+ settle([buffer, pasteSpans], false);
1501
+ return;
1502
+ }
1503
+ // Other win32-input-mode keys (arrows, backspace, etc.).
1504
+ const dec = decodeWin32Key(full);
1505
+ if (!dec)
1506
+ return;
1507
+ if (dec.type === "text") {
1508
+ insert(dec.text);
1509
+ repaint();
1350
1510
  return;
1351
1511
  }
1512
+ switch (dec.cmd) {
1513
+ case "interrupt":
1514
+ settle(new Error("interrupt"), true);
1515
+ return;
1516
+ case "eof":
1517
+ settle(new Error("eof"), true);
1518
+ return;
1519
+ case "backspace":
1520
+ backspace();
1521
+ repaint();
1522
+ return;
1523
+ case "delete-word":
1524
+ deleteWordBefore();
1525
+ repaint();
1526
+ return;
1527
+ case "delete":
1528
+ deleteChar();
1529
+ repaint();
1530
+ return;
1531
+ case "left":
1532
+ moveLeft();
1533
+ repaint();
1534
+ return;
1535
+ case "right":
1536
+ moveRight();
1537
+ repaint();
1538
+ return;
1539
+ case "back-word": {
1540
+ const sp = buffer.slice(0, cursor).lastIndexOf(" ");
1541
+ cursor = sp === -1 ? 0 : sp + 1;
1542
+ repaint();
1543
+ return;
1544
+ }
1545
+ case "forward-word": {
1546
+ const sp = buffer.indexOf(" ", cursor);
1547
+ cursor = sp === -1 ? buffer.length : sp + 1;
1548
+ repaint();
1549
+ return;
1550
+ }
1551
+ case "up":
1552
+ moveUp();
1553
+ repaint();
1554
+ return;
1555
+ case "down":
1556
+ moveDown();
1557
+ repaint();
1558
+ return;
1559
+ case "home":
1560
+ cursor = 0;
1561
+ repaint();
1562
+ return;
1563
+ case "end":
1564
+ cursor = buffer.length;
1565
+ repaint();
1566
+ return;
1567
+ case "tab":
1568
+ insert(" ");
1569
+ repaint();
1570
+ return;
1571
+ case "clear-line":
1572
+ buffer = "";
1573
+ cursor = 0;
1574
+ pasteSpans = [];
1575
+ repaint();
1576
+ return;
1577
+ case "kill-line":
1578
+ buffer = buffer.slice(0, cursor);
1579
+ repaint();
1580
+ return;
1581
+ default:
1582
+ return;
1583
+ }
1584
+ };
1585
+ const onKeypress = (str, key) => {
1586
+ // Reassemble modifier-prefixed / win32-input-mode CSI sequences, which
1587
+ // Node's readline splits into several keypress events (e.g. Shift+Enter
1588
+ // arriving as `\x1b[27;2;` + "1" + "3" + "~", or a win32-input-mode
1589
+ // sequence `\x1b[0;1;1;13;2u` arriving piecemeal). A "partial CSI" is
1590
+ // any ESC [ sequence readline left unterminated; we keep appending until
1591
+ // it ends in a terminator, then classify it.
1592
+ const seq = key?.sequence || str || "";
1352
1593
  if (modSeq) {
1353
- modSeq += str || key?.sequence || "";
1354
- if (/[~u_]/.test(modSeq.slice(-1)) || modSeq.endsWith("~")) {
1594
+ modSeq += str || seq || "";
1595
+ if (/^\x1b\[[\d;]*[a-zA-Z~_u]$/.test(modSeq)) {
1355
1596
  const full = modSeq;
1356
1597
  modSeq = "";
1357
- // e.g. `\x1b[27;2;13~` = Shift+Enter.
1358
- if (csiShiftEnter(full)) {
1359
- insert("\n");
1360
- repaint();
1361
- }
1598
+ handleRawSequence(full);
1362
1599
  }
1363
1600
  return;
1364
1601
  }
1602
+ if (/^\x1b\[[\d;]*$/.test(seq)) {
1603
+ modSeq = seq;
1604
+ return;
1605
+ }
1365
1606
  if (consumeBracketedPaste(str, key))
1366
1607
  return;
1367
1608
  if (pasteBurst && str && str.length === 1 && !key?.ctrl && !key?.meta) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.66",
3
+ "version": "1.0.67",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },