@hellix-ai/cubes 0.1.4 → 0.2.0

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.
@@ -0,0 +1,133 @@
1
+ // The small amount of terminal the split view needs: how wide a string really is, how to cut one to a
2
+ // column budget, and what the bytes a terminal sends for a key or a mouse click mean.
3
+ /** Columns a code point occupies: 0 for combining marks and joiners, 2 for wide (CJK, emoji), else 1.
4
+ * Not the whole of Unicode's East Asian Width table - the ranges that actually show up in session
5
+ * titles and in our own glyphs. Getting this wrong shifts every column after the character. */
6
+ export function cpWidth(cp) {
7
+ if (cp === 0)
8
+ return 0;
9
+ if (cp < 0x20 || (cp >= 0x7f && cp < 0xa0))
10
+ return 0;
11
+ if ((cp >= 0x300 && cp <= 0x36f) || cp === 0x200b || cp === 0x200c || cp === 0x200d ||
12
+ (cp >= 0xfe00 && cp <= 0xfe0f) || (cp >= 0x1f3fb && cp <= 0x1f3ff) || (cp >= 0xe0100 && cp <= 0xe01ef))
13
+ return 0;
14
+ if ((cp >= 0x1100 && cp <= 0x115f) || (cp >= 0x2e80 && cp <= 0xa4cf) || (cp >= 0xac00 && cp <= 0xd7a3) ||
15
+ (cp >= 0xf900 && cp <= 0xfaff) || (cp >= 0xfe30 && cp <= 0xfe4f) || (cp >= 0xff00 && cp <= 0xff60) ||
16
+ (cp >= 0xffe0 && cp <= 0xffe6) || (cp >= 0x1f300 && cp <= 0x1f64f) || (cp >= 0x1f900 && cp <= 0x1f9ff) ||
17
+ (cp >= 0x1f680 && cp <= 0x1f6ff) || (cp >= 0x2600 && cp <= 0x26ff && isEmojiPresentation(cp)) ||
18
+ cp === 0x270b || cp === 0x2705 || cp === 0x274c || (cp >= 0x20000 && cp <= 0x3fffd))
19
+ return 2;
20
+ return 1;
21
+ }
22
+ // The few symbols in U+2600-26FF that terminals draw two cells wide (emoji presentation by default).
23
+ function isEmojiPresentation(cp) {
24
+ return [0x2614, 0x2615, 0x2648, 0x267f, 0x2693, 0x26a1, 0x26aa, 0x26ab, 0x26bd, 0x26be, 0x26c4, 0x26c5,
25
+ 0x26ce, 0x26d4, 0x26ea, 0x26f2, 0x26f3, 0x26f5, 0x26fa, 0x26fd].includes(cp);
26
+ }
27
+ export function strWidth(s) {
28
+ let w = 0;
29
+ for (const ch of s)
30
+ w += cpWidth(ch.codePointAt(0));
31
+ return w;
32
+ }
33
+ /** Exactly `w` columns: cut with an ellipsis when too long, padded with spaces when short. */
34
+ export function fit(s, w) {
35
+ if (w <= 0)
36
+ return "";
37
+ let out = "";
38
+ let used = 0;
39
+ const total = strWidth(s);
40
+ const budget = total > w ? w - 1 : w;
41
+ for (const ch of s) {
42
+ const cw = cpWidth(ch.codePointAt(0));
43
+ if (used + cw > budget)
44
+ break;
45
+ out += ch;
46
+ used += cw;
47
+ }
48
+ if (total > w) {
49
+ out += "…";
50
+ used += 1;
51
+ }
52
+ return out + " ".repeat(Math.max(0, w - used));
53
+ }
54
+ /** Nothing from the server may reach the screen as a control character. */
55
+ export function printable(s) {
56
+ return s.replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
57
+ }
58
+ const CSI_KEYS = {
59
+ A: "up", B: "down", C: "right", D: "left", H: "home", F: "end", Z: "backtab",
60
+ };
61
+ const TILDE_KEYS = {
62
+ "1": "home", "7": "home", "4": "end", "8": "end", "5": "pgup", "6": "pgdn",
63
+ };
64
+ /** Turn one read of raw input into keys. A lone ESC is the Escape key (tmux delivers a real escape
65
+ * sequence in one read; the split view sets tmux's escape-time low so Escape is not held back). */
66
+ export function parseKeys(input) {
67
+ const keys = [];
68
+ let i = 0;
69
+ while (i < input.length) {
70
+ const c = input[i];
71
+ if (c === "\x1b") {
72
+ const rest = input.slice(i);
73
+ const mouse = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/.exec(rest);
74
+ if (mouse) {
75
+ keys.push({ kind: "mouse", button: Number(mouse[1]), x: Number(mouse[2]), y: Number(mouse[3]), release: mouse[4] === "m" });
76
+ i += mouse[0].length;
77
+ continue;
78
+ }
79
+ const csi = /^\x1b[[O](\d*)(?:;\d+)*([A-Za-z~])/.exec(rest);
80
+ if (csi) {
81
+ const kind = csi[2] === "~" ? TILDE_KEYS[csi[1]] : CSI_KEYS[csi[2]];
82
+ if (kind)
83
+ keys.push({ kind });
84
+ i += csi[0].length;
85
+ continue;
86
+ }
87
+ keys.push({ kind: "escape" });
88
+ i += 1;
89
+ continue;
90
+ }
91
+ i += 1;
92
+ if (c === "\r" || c === "\n")
93
+ keys.push({ kind: "enter" });
94
+ else if (c === "\t")
95
+ keys.push({ kind: "tab" });
96
+ else if (c === "\x7f" || c === "\b")
97
+ keys.push({ kind: "backspace" });
98
+ else if (c === "\x03")
99
+ keys.push({ kind: "ctrl-c" });
100
+ else if (c === "\x1d")
101
+ keys.push({ kind: "ctrl-]" });
102
+ else if (c === "\x0c")
103
+ keys.push({ kind: "ctrl-l" });
104
+ else if (c >= " ") {
105
+ // A surrogate pair is one character.
106
+ const cp = input.codePointAt(i - 1);
107
+ const ch = String.fromCodePoint(cp);
108
+ if (ch.length === 2)
109
+ i += 1;
110
+ keys.push({ kind: "char", ch });
111
+ }
112
+ }
113
+ return keys;
114
+ }
115
+ export const ESC = {
116
+ altOn: "\x1b[?1049h",
117
+ altOff: "\x1b[?1049l",
118
+ hideCursor: "\x1b[?25l",
119
+ showCursor: "\x1b[?25h",
120
+ mouseOn: "\x1b[?1000h\x1b[?1002h\x1b[?1006h",
121
+ mouseOff: "\x1b[?1000l\x1b[?1002l\x1b[?1006l",
122
+ syncOn: "\x1b[?2026h",
123
+ syncOff: "\x1b[?2026l",
124
+ home: "\x1b[H",
125
+ clear: "\x1b[2J",
126
+ reset: "\x1b[0m",
127
+ dim: "\x1b[2m",
128
+ bold: "\x1b[1m",
129
+ inverse: "\x1b[7m",
130
+ fg: (n) => `\x1b[38;5;${n}m`,
131
+ bg: (n) => `\x1b[48;5;${n}m`,
132
+ at: (row, col) => `\x1b[${row};${col}H`,
133
+ };
@@ -0,0 +1,49 @@
1
+ // The local tmux the split view runs in: its own socket, so it never touches a tmux the person uses,
2
+ // and every call synchronous - these are a handful of tiny commands, never a stream.
3
+ import { spawnSync } from "node:child_process";
4
+ export const SOCKET = "cubes-tui";
5
+ export const SESSION = "cubes";
6
+ /** Run `tmux -L cubes-tui <args>`. `TMUX` is dropped from the child's env: inside someone's own tmux
7
+ * it would make every command refuse ("sessions should be nested with care"). */
8
+ export function tmux(args, opts = {}) {
9
+ const env = { ...process.env };
10
+ delete env.TMUX;
11
+ const r = spawnSync("tmux", ["-L", SOCKET, ...args], { encoding: "utf8", env, input: opts.input });
12
+ if (r.error)
13
+ return { code: 127, out: "", err: r.error.message };
14
+ return { code: r.status ?? 1, out: (r.stdout ?? "").trim(), err: (r.stderr ?? "").trim() };
15
+ }
16
+ /** tmux's version as a number (3.3a -> 3.3), or null when there is no tmux at all. */
17
+ export function tmuxVersion() {
18
+ const r = spawnSync("tmux", ["-V"], { encoding: "utf8" });
19
+ if (r.error || r.status !== 0)
20
+ return null;
21
+ const m = /(\d+)\.(\d+)/.exec(r.stdout ?? "");
22
+ return m ? Number(`${m[1]}.${m[2]}`) : null;
23
+ }
24
+ /** Single-quote for the `sh -c` tmux runs a pane's command through. */
25
+ export function shq(s) {
26
+ return `'${s.replace(/'/g, "'\\''")}'`;
27
+ }
28
+ /** A tmux FORMAT is parsed for `#{...}` and `#[...]`; a session's title must never be either. */
29
+ export function fmtText(s) {
30
+ return s.replace(/#/g, "##");
31
+ }
32
+ /** The command line that runs this same CLI (same node, same script) with `args`. */
33
+ export function selfCommand(args) {
34
+ return [process.execPath, process.argv[1] ?? "", ...args].map(shq).join(" ");
35
+ }
36
+ /** Set a pane option on THIS pane (the one the calling process runs in). Best-effort: a header that
37
+ * failed to update must never take the pane down. */
38
+ export function setPaneOption(name, value) {
39
+ const pane = process.env.TMUX_PANE;
40
+ if (!pane)
41
+ return;
42
+ tmux(["set-option", "-p", "-t", pane, name, value]);
43
+ }
44
+ export function setSessionOption(name, value) {
45
+ tmux(["set-option", "-t", SESSION, name, value]);
46
+ }
47
+ export function getSessionOption(name) {
48
+ return tmux(["show-options", "-v", "-q", "-t", SESSION, name]).out;
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hellix-ai/cubes",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Attach to your Cubes sessions from your own terminal (Warp, iTerm, Ghostty, anything).",
5
5
  "type": "module",
6
6
  "bin": {