buninu 0.4.21 → 0.6.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/apps/bunmsh/CHANGELOG.md +4 -0
  3. package/apps/bunmsh/package.json +1 -1
  4. package/apps/bunmsh/src/shell.js +1 -1
  5. package/apps/jsgotty/LICENSES/MIT-jsmdcui.txt +22 -0
  6. package/apps/jsgotty/NOTICE +5 -0
  7. package/apps/jsgotty/README.en.md +57 -0
  8. package/apps/jsgotty/README.md +57 -0
  9. package/apps/jsgotty/client-modes.mjs +117 -0
  10. package/apps/jsgotty/client.mjs +100 -10
  11. package/apps/jsgotty/clipboard-powershell.js +18 -0
  12. package/apps/jsgotty/clipboard.js +148 -0
  13. package/apps/jsgotty/gotty.js +17 -2
  14. package/apps/jsgotty/package-lock.json +2 -2
  15. package/apps/jsgotty/package.json +1 -1
  16. package/apps/jsgotty/static/css/index.css +7 -1
  17. package/apps/jsgotty/static/index.html +1 -0
  18. package/apps/jsgotty/static/js/gotty.js +175 -19
  19. package/apps/jsgotty/static/js/kitty-keyboard.js +193 -0
  20. package/apps/jsgotty/terminal-modes.js +332 -0
  21. package/apps/jsgotty/tests/android-alt.test.js +115 -0
  22. package/apps/jsgotty/tests/arrow-modifiers.test.js +80 -0
  23. package/apps/jsgotty/tests/client-exit.test.mjs +45 -0
  24. package/apps/jsgotty/tests/client-modes.test.mjs +105 -0
  25. package/apps/jsgotty/tests/clipboard-browser.test.js +81 -0
  26. package/apps/jsgotty/tests/clipboard.test.js +75 -0
  27. package/apps/jsgotty/tests/kitty-keyboard-browser.test.js +53 -0
  28. package/apps/jsgotty/tests/kitty-keyboard.test.js +102 -0
  29. package/apps/jsgotty/tests/manual/README.md +20 -0
  30. package/apps/jsgotty/tests/manual/keyboard-collector.cjs +55 -0
  31. package/apps/jsgotty/tests/manual/keyboard-debug.js +74 -0
  32. package/apps/jsgotty/tests/manual/kitty-keyboard.js +80 -0
  33. package/apps/jsgotty/tests/manual-touch-diagnostics.js +33 -0
  34. package/apps/jsgotty/tests/manual-touch-diagnostics.mjs +22 -0
  35. package/apps/jsgotty/tests/nested-cli-pty.test.mjs +116 -0
  36. package/apps/jsgotty/tests/reconnect-modes-browser.test.js +144 -0
  37. package/apps/jsgotty/tests/reconnect-modes.md +44 -0
  38. package/apps/jsgotty/tests/reconnect-modes.test.js +166 -0
  39. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const vm = require('node:vm');
6
+ const { TerminalModeTracker, modeTransition } = require('../terminal-modes.js');
7
+
8
+ const source = fs.readFileSync(path.join(__dirname, '../static/js/gotty.js'), 'utf8');
9
+ const methods = source.slice(source.indexOf(' enqueueTerminal(task)'), source.indexOf(' setDisconnected(e)'));
10
+
11
+ function feed(tracker, ...chunks) {
12
+ for (const chunk of chunks) tracker.consume(Buffer.from(chunk, 'latin1'));
13
+ return tracker.snapshot();
14
+ }
15
+
16
+ function client({ alternate = false, dec = {}, mouse = 'NONE', encoding = 'DEFAULT', cursorHidden = false } = {}) {
17
+ const Wrapper = vm.runInNewContext(`(class {${methods}})`);
18
+ const w = new Wrapper();
19
+ const writes = [];
20
+ const defaults = { applicationCursorKeys: false, applicationKeypad: false, bracketedPasteMode: false,
21
+ origin: false, reverseWraparound: false, sendFocus: false, wraparound: true };
22
+ w.term = {
23
+ _core: { coreService: { decPrivateModes: { ...defaults, ...dec }, isCursorHidden: cursorHidden },
24
+ coreMouseService: { activeProtocol: mouse, activeEncoding: encoding } },
25
+ buffer: { active: { type: alternate ? 'alternate' : 'normal' } },
26
+ write: (s, callback) => { if (s) writes.push(s); callback?.(); },
27
+ };
28
+ w.kittyKeyboard = { states: { normal: { flags: 0, stack: [] }, alternate: { flags: 0, stack: [] } } };
29
+ return { w, writes };
30
+ }
31
+
32
+ test('tracker follows DEC, mouse, alternate screen and kitty keyboard modes', () => {
33
+ const state = feed(new TerminalModeTracker(),
34
+ 'hi\x1b[?1049h\x1b[?1;2004h\x1b=\x1b[?25l',
35
+ '\x1b[?1002h\x1b[?10', '06h', // split across chunks
36
+ '\x1b[>1u\x1b[>11u\x1b]0;title \x1b[?2004l\x07');
37
+ assert.equal(state.alternate, true);
38
+ assert.equal(state.dec.applicationCursorKeys, true);
39
+ assert.equal(state.dec.bracketedPasteMode, true, 'OSC payload is not parsed as CSI');
40
+ assert.equal(state.dec.applicationKeypad, true);
41
+ assert.equal(state.cursorHidden, true);
42
+ assert.equal(state.mouse, 'DRAG');
43
+ assert.equal(state.encoding, 'SGR');
44
+ assert.deepEqual(state.kitty.alternate, { flags: 11, stack: [0, 1] });
45
+ assert.deepEqual(state.kitty.normal, { flags: 0, stack: [] });
46
+ });
47
+
48
+ test('tracker handles mode resets, kitty pop/set and RIS', () => {
49
+ const tracker = new TerminalModeTracker();
50
+ let state = feed(tracker, '\x1b[?1003h\x1b[?1006h\x1b[?1000l\x1b[?1006l\x1b[>1u\x1b[=8;2u\x1b[<u');
51
+ assert.equal(state.mouse, 'NONE');
52
+ assert.equal(state.encoding, 'DEFAULT');
53
+ assert.equal(state.kitty.normal.flags, 0);
54
+ state = feed(tracker, '\x1b[?1049h\x1b[?2004h\x1b[?1002h\x1b[!p');
55
+ assert.equal(state.dec.bracketedPasteMode, false, 'DECSTR resets DEC modes');
56
+ assert.equal(state.alternate, true, 'DECSTR keeps the active screen');
57
+ assert.equal(state.mouse, 'DRAG', 'DECSTR keeps mouse reporting');
58
+ state = feed(tracker, '\x1bc');
59
+ assert.deepEqual(state, new TerminalModeTracker().snapshot());
60
+ });
61
+
62
+ test('fresh client gets the full state restored', () => {
63
+ const state = feed(new TerminalModeTracker(), '\x1b[?1049h\x1b[?2004h\x1b[?1002h\x1b[?1006h\x1b[?25l\x1b[>11u');
64
+ const { w, writes } = client();
65
+ w.applySessionModes(state);
66
+ assert.equal(writes.length, 1);
67
+ for (const seq of ['\x1b[?1049h', '\x1b[?2004h', '\x1b[?1002h', '\x1b[?1006h', '\x1b[?25l'])
68
+ assert.ok(writes[0].includes(seq), JSON.stringify(seq));
69
+ assert.equal(w.kittyKeyboard.states.alternate.flags, 11);
70
+ });
71
+
72
+ test('browser masks kitty flags it does not implement', () => {
73
+ const { w } = client();
74
+ w.applySessionModes(feed(new TerminalModeTracker(), '\x1b[>31u\x1b[>31u'));
75
+ assert.equal(JSON.stringify(w.kittyKeyboard.states.normal), JSON.stringify({ flags: 11, stack: [0, 11] }));
76
+ });
77
+
78
+ test('socket-only reconnect with matching xterm state writes nothing', () => {
79
+ const state = feed(new TerminalModeTracker(), '\x1b[?1049h\x1b[?6h\x1b[?2004h\x1b[?1002h\x1b[?1006h\x1b[>1u\x1b[>11u');
80
+ const { w, writes } = client({ alternate: true, dec: { origin: true, bracketedPasteMode: true }, mouse: 'DRAG', encoding: 'SGR' });
81
+ w.applySessionModes(state);
82
+ assert.equal(writes.length, 0, 'no DECOM cursor homing or 1049 cursor save');
83
+ w.applySessionModes(state);
84
+ assert.deepEqual(JSON.parse(JSON.stringify(w.kittyKeyboard.states.alternate)), { flags: 11, stack: [0, 1] }, 'push stack does not grow');
85
+ });
86
+
87
+ test('stale client modes are turned off when the program disabled them while disconnected', () => {
88
+ const state = new TerminalModeTracker().snapshot();
89
+ const { w, writes } = client({ alternate: true, dec: { bracketedPasteMode: true }, mouse: 'ANY', encoding: 'SGR' });
90
+ w.applySessionModes(state);
91
+ assert.equal(writes.length, 1);
92
+ for (const seq of ['\x1b[?1049l', '\x1b[?2004l', '\x1b[?1000l', '\x1b[?1006l'])
93
+ assert.ok(writes[0].includes(seq), JSON.stringify(seq));
94
+ });
95
+
96
+ test('transport disconnect and reconnect clear held keys without resetting negotiation', () => {
97
+ const start = source.indexOf(' reset() {\n // Reconnecting');
98
+ const reset = source.slice(start, source.indexOf(' close() {', start));
99
+ assert.match(reset, /clearHeld\(false\)/);
100
+ assert.doesNotMatch(reset, /kittyKeyboard.reset/);
101
+ assert.match(source, /setDisconnected\(e\) \{\s*if \(e\) this.kittyKeyboard.clearHeld\(false\)/);
102
+ });
103
+
104
+ const sessions = {
105
+ vim: '\x1b[?1049h\x1b[?1h\x1b=\x1b[?2004h\x1b[?1002h\x1b[?1006h\x1b[?25l',
106
+ kittyBothScreens: '\x1b[>1u\x1b[?1049h\x1b[>31u\x1b[>3u\x1b[?1003h\x1b[?1016h',
107
+ kittySetThenPush: '\x1b[=5;1u\x1b[>8u\x1b[?1004h\x1b[?6h',
108
+ };
109
+
110
+ for (const [name, output] of Object.entries(sessions)) {
111
+ test(`CLI resume and cleanup round-trip: ${name}`, () => {
112
+ const target = feed(new TerminalModeTracker(), output);
113
+ const local = new TerminalModeTracker();
114
+ assert.deepEqual(feed(local, modeTransition(local.snapshot(), target)), target, 'fresh terminal reaches PTY state');
115
+ assert.equal(modeTransition(local.snapshot(), target), '', 'nothing left to send once in sync');
116
+ assert.deepEqual(feed(local, modeTransition(local.snapshot())), new TerminalModeTracker().snapshot(), 'cleanup returns to defaults');
117
+ });
118
+ }
119
+
120
+ test('kitty flags are kept unmasked for terminals that support all of them', () => {
121
+ assert.equal(feed(new TerminalModeTracker(), '\x1b[>31u').kitty.normal.flags, 31);
122
+ });
123
+
124
+ test('incremental Kitty restoration preserves a non-default parent stack', () => {
125
+ const local = new TerminalModeTracker();
126
+ feed(local, '\x1b[=1u\x1b[>11u');
127
+ const parent = local.snapshot();
128
+ const remote = feed(new TerminalModeTracker(), '\x1b[>3u\x1b[>9u');
129
+ const attach = modeTransition(parent, remote);
130
+ assert.doesNotMatch(attach, /<100u/);
131
+ assert.deepEqual(feed(local, attach), remote);
132
+ assert.deepEqual(feed(local, modeTransition(local.snapshot(), parent)), parent);
133
+ });
134
+
135
+ test('unknown original modes are not replaced with guessed defaults on cleanup', () => {
136
+ const unknown = new TerminalModeTracker().snapshot();
137
+ for (const key of Object.keys(unknown.dec)) unknown.dec[key] = null;
138
+ for (const key of ['alternate','cursorHidden','mouse','encoding']) unknown[key] = null;
139
+ unknown.kitty.normal.flags = unknown.kitty.alternate.flags = null;
140
+ assert.equal(modeTransition(feed(new TerminalModeTracker(), '\x1b[?1049h\x1b[>11u'), unknown), '');
141
+ });
142
+
143
+ test('hidden Kitty restoration is negotiated and never toggles the screen',()=>{
144
+ for(const alternate of [false,true]){
145
+ const local=new TerminalModeTracker();if(alternate)feed(local,'\x1b[?1049h');
146
+ const target=local.snapshot(),hidden=alternate?'normal':'alternate';
147
+ target.kitty[hidden]={flags:9,stack:[0,1]};
148
+ assert.equal(modeTransition(local.snapshot(),target),'');
149
+ const sequence=modeTransition(local.snapshot(),target,{kittyRestore:true});
150
+ assert.doesNotMatch(sequence,/\[\?(?:47|1047|1049)[hl]/);
151
+ // Every-byte fragmentation must be handled by intermediate server trackers.
152
+ for(const byte of Buffer.from(sequence))local.consume(Buffer.from([byte]));
153
+ assert.deepEqual(local.snapshot(),target);
154
+ }
155
+ });
156
+ test('Kitty restore tracker rejects malformed or unrelated string commands',()=>{
157
+ const t=new TerminalModeTracker(),before=t.snapshot();
158
+ for(const payload of [
159
+ '\x1b]777;jsgotty-kitty-v1;normal;32;0\x07',
160
+ '\x1b]777;jsgotty-kitty-v1;normal;1;'+Array(33).fill('0').join(',')+'\x07',
161
+ '\x1bP777;jsgotty-kitty-v1;normal;1;0\x1b\\',
162
+ '\x1b]777;jsgotty-kitty-v1;normal;1;0\x18',
163
+ ]){feed(t,payload);assert.deepEqual(t.snapshot(),before);}
164
+ feed(t,'\x1b]777;jsgotty-kitty-v1;normal;1;0\x1b\\');
165
+ assert.deepEqual(t.kitty.normal,{flags:1,stack:[0]});
166
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.4.21",
3
+ "version": "0.6.0",
4
4
  "description": "Buninu is a portable, self-bootstrapping Unix-like userspace built on Bun that runs across operating systems",
5
5
  "type": "module",
6
6
  "main": "./bin/init.js",