bunmicro 0.9.10 → 0.9.20
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/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/runtime/help/options.md +8 -0
- package/runtime/jsplugins/example/example.js +3 -1
- package/src/config/clean.js +172 -0
- package/src/config/config.js +3 -1
- package/src/config/defaults.js +8 -1
- package/src/index.js +530 -152
- package/src/platform/clipboard.js +125 -7
- package/src/plugins/js-bridge.js +10 -8
- package/src/screen/screen.js +16 -4
- package/todo.txt +8 -3
|
@@ -6,14 +6,31 @@ const internalRegisters = new Map();
|
|
|
6
6
|
export class ClipboardManager {
|
|
7
7
|
constructor() {
|
|
8
8
|
this.backend = detectClipboardBackend();
|
|
9
|
+
this._writeBackend = null;
|
|
10
|
+
this._altBackend = null;
|
|
11
|
+
this._readFromInternal = false;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
methodName() {
|
|
15
|
+
return (this._writeBackend ?? this.backend).name;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
readMethodName(register = "clipboard") {
|
|
19
|
+
if (register !== "clipboard" && register !== "primary") return "internal";
|
|
20
|
+
if (register === "clipboard" && this._readFromInternal) return "internal";
|
|
21
|
+
if (register === "primary" && !this.backend.supportsPrimary) return "internal";
|
|
12
22
|
return this.backend.name;
|
|
13
23
|
}
|
|
14
24
|
|
|
25
|
+
altMethodName() {
|
|
26
|
+
return this._altBackend?.name ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
fallbackToInternal() {
|
|
16
30
|
this.backend = internalClipboard();
|
|
31
|
+
this._writeBackend = null;
|
|
32
|
+
this._altBackend = null;
|
|
33
|
+
this._readFromInternal = false;
|
|
17
34
|
return this.backend;
|
|
18
35
|
}
|
|
19
36
|
|
|
@@ -21,6 +38,13 @@ export class ClipboardManager {
|
|
|
21
38
|
if (register !== "clipboard" && register !== "primary") {
|
|
22
39
|
return internalRegisters.get(register) ?? "";
|
|
23
40
|
}
|
|
41
|
+
if (register === "primary" && !this.backend.supportsPrimary) {
|
|
42
|
+
return internalRegisters.get(register) ?? "";
|
|
43
|
+
}
|
|
44
|
+
// terminal mode: paste from internal to avoid OSC 52 read issues over SSH
|
|
45
|
+
if (register === "clipboard" && this._readFromInternal) {
|
|
46
|
+
return internalRegisters.get(register) ?? "";
|
|
47
|
+
}
|
|
24
48
|
try {
|
|
25
49
|
const text = this.backend.read?.(register);
|
|
26
50
|
if (text == null) return internalRegisters.get(register) ?? "";
|
|
@@ -33,15 +57,62 @@ export class ClipboardManager {
|
|
|
33
57
|
write(text, register = "clipboard") {
|
|
34
58
|
internalRegisters.set(register, text);
|
|
35
59
|
if (register !== "clipboard" && register !== "primary") return true;
|
|
60
|
+
const wb = this._writeBackend ?? this.backend;
|
|
61
|
+
if (register === "primary" && !wb.supportsPrimary) return true;
|
|
36
62
|
try {
|
|
37
|
-
const ok =
|
|
38
|
-
if (!ok)
|
|
63
|
+
const ok = wb.write?.(text, register) ?? true;
|
|
64
|
+
if (!ok) {
|
|
65
|
+
if (wb === this._writeBackend) this._writeBackend = null;
|
|
66
|
+
else this.fallbackToInternal();
|
|
67
|
+
}
|
|
39
68
|
return true;
|
|
40
69
|
} catch {
|
|
41
|
-
this.
|
|
70
|
+
if (wb === this._writeBackend) this._writeBackend = null;
|
|
71
|
+
else this.fallbackToInternal();
|
|
42
72
|
return true;
|
|
43
73
|
}
|
|
44
74
|
}
|
|
75
|
+
|
|
76
|
+
writeAlt(text, register = "clipboard") {
|
|
77
|
+
if (!this._altBackend) return false;
|
|
78
|
+
internalRegisters.set(register, text);
|
|
79
|
+
try {
|
|
80
|
+
return this._altBackend.write?.(text, register) ?? true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async initFromSetting(setting, ttyIn, ttyOut, timeoutMs = 150) {
|
|
87
|
+
this.backend = detectClipboardBackend();
|
|
88
|
+
this._writeBackend = null;
|
|
89
|
+
this._altBackend = null;
|
|
90
|
+
this._readFromInternal = false;
|
|
91
|
+
if (setting === "internal") {
|
|
92
|
+
this.fallbackToInternal();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (setting === "terminal") {
|
|
96
|
+
// skip probe — directly enable OSC 52 write (handles write-only terminals)
|
|
97
|
+
if (ttyOut) {
|
|
98
|
+
this._altBackend = this.backend; // external as clickable alt
|
|
99
|
+
this._writeBackend = osc52Clipboard(ttyOut);
|
|
100
|
+
this._readFromInternal = true; // paste via internal (SSH-safe)
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
// "external" (default): probe OSC 52 as optional alt
|
|
104
|
+
if (ttyIn && ttyOut && process.stdout?.isTTY) {
|
|
105
|
+
const ok = await probeOSC52(ttyIn, ttyOut, timeoutMs);
|
|
106
|
+
if (ok) this._altBackend = osc52Clipboard(ttyOut);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// kept for backward compatibility (--version probe etc.)
|
|
112
|
+
async probeAndUpgradeOSC52(ttyIn, ttyOut, timeoutMs = 150) {
|
|
113
|
+
const ok = await probeOSC52(ttyIn, ttyOut, timeoutMs);
|
|
114
|
+
if (ok) this._writeBackend = osc52Clipboard(ttyOut);
|
|
115
|
+
}
|
|
45
116
|
}
|
|
46
117
|
|
|
47
118
|
function detectClipboardBackend() {
|
|
@@ -109,14 +180,24 @@ function termuxClipboard(set, get) {
|
|
|
109
180
|
function wlClipboard(wlCopy, wlPaste) {
|
|
110
181
|
return {
|
|
111
182
|
name: "wl-clipboard",
|
|
112
|
-
|
|
113
|
-
|
|
183
|
+
supportsPrimary: true,
|
|
184
|
+
read: (register) => {
|
|
185
|
+
const args = [wlPaste, "--no-newline"];
|
|
186
|
+
if (register === "primary") args.push("--primary");
|
|
187
|
+
return outputOrThrow(runSync(args, { timeout: CLIPBOARD_TIMEOUT_MS }));
|
|
188
|
+
},
|
|
189
|
+
write: (text, register) => {
|
|
190
|
+
const args = [wlCopy];
|
|
191
|
+
if (register === "primary") args.push("--primary");
|
|
192
|
+
return runSync(args, { stdin: text, stdout: "ignore", timeout: CLIPBOARD_TIMEOUT_MS }).ok;
|
|
193
|
+
},
|
|
114
194
|
};
|
|
115
195
|
}
|
|
116
196
|
|
|
117
197
|
function xclipClipboard(xclip) {
|
|
118
198
|
return {
|
|
119
199
|
name: "xclip",
|
|
200
|
+
supportsPrimary: true,
|
|
120
201
|
read: (register) => {
|
|
121
202
|
const selection = register === "primary" ? "primary" : "clipboard";
|
|
122
203
|
return outputOrThrow(runSync([xclip, "-selection", selection, "-o"], { timeout: CLIPBOARD_TIMEOUT_MS }));
|
|
@@ -131,6 +212,7 @@ function xclipClipboard(xclip) {
|
|
|
131
212
|
function xselClipboard(xsel) {
|
|
132
213
|
return {
|
|
133
214
|
name: "xsel",
|
|
215
|
+
supportsPrimary: true,
|
|
134
216
|
read: (register) => {
|
|
135
217
|
const selection = register === "primary" ? "--primary" : "--clipboard";
|
|
136
218
|
return outputOrThrow(runSync([xsel, selection, "--output"], { timeout: CLIPBOARD_TIMEOUT_MS }));
|
|
@@ -146,8 +228,11 @@ function powershellClipboard(shell) {
|
|
|
146
228
|
return {
|
|
147
229
|
name: "powershell",
|
|
148
230
|
// Get-Clipboard -Raw appends \r\n to stdout; strip exactly one trailing line ending.
|
|
149
|
-
read: () => outputOrThrow(runSync([shell, "-NoProfile", "-Command", "Get-Clipboard -Raw"], {
|
|
150
|
-
write: (text) =>
|
|
231
|
+
read: () => outputOrThrow(runSync([shell, "-NoProfile", "-Command", "Get-Clipboard -Raw"], {})).replace(/\r?\n$/, ""),
|
|
232
|
+
write: (text) => {
|
|
233
|
+
text=(text+'').replaceAll("'","''") ;
|
|
234
|
+
return runSync([shell, "-NoProfile", "-Command", `Set-Clipboard '${text}'`], { stdout: "ignore" }).ok ;
|
|
235
|
+
},
|
|
151
236
|
};
|
|
152
237
|
}
|
|
153
238
|
|
|
@@ -155,3 +240,36 @@ function outputOrThrow(result) {
|
|
|
155
240
|
if (!result.ok) throw new Error(result.stderr || result.stdout || "clipboard command failed");
|
|
156
241
|
return result.stdout;
|
|
157
242
|
}
|
|
243
|
+
|
|
244
|
+
export function osc52Clipboard(stdout) {
|
|
245
|
+
const inTmux = !!process.env.TMUX;
|
|
246
|
+
return {
|
|
247
|
+
name: "OSC 52",
|
|
248
|
+
write(text) {
|
|
249
|
+
const b64 = Buffer.from(text, "utf-8").toString("base64");
|
|
250
|
+
stdout.write(inTmux
|
|
251
|
+
? `\x1bPtmux;\x1b\x1b]52;c;${b64}\x07\x1b\\`
|
|
252
|
+
: `\x1b]52;c;${b64}\x07`);
|
|
253
|
+
return true;
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function probeOSC52(ttyIn, ttyOut, timeoutMs) {
|
|
259
|
+
if (process.env.TMUX) return true;
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
let done = false;
|
|
262
|
+
const timer = setTimeout(() => {
|
|
263
|
+
if (!done) { done = true; ttyIn.removeListener("data", onData); resolve(false); }
|
|
264
|
+
}, timeoutMs);
|
|
265
|
+
function onData(chunk) {
|
|
266
|
+
if (done) return;
|
|
267
|
+
const s = Buffer.isBuffer(chunk) ? chunk.toString("latin1") : String(chunk);
|
|
268
|
+
if (s.includes("\x1b]52;")) {
|
|
269
|
+
done = true; clearTimeout(timer); ttyIn.removeListener("data", onData); resolve(true);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
ttyIn.on("data", onData);
|
|
273
|
+
ttyOut.write("\x1b]52;c;?\x07");
|
|
274
|
+
});
|
|
275
|
+
}
|
package/src/plugins/js-bridge.js
CHANGED
|
@@ -51,8 +51,8 @@ function registerBuiltinActions() {
|
|
|
51
51
|
reg("CursorEnd", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveEndOfBuffer(); app.scrollCursorToBoundary?.(app.pane, "end"); });
|
|
52
52
|
reg("ParagraphPrevious", (app) => { app.pane && (app.pane.selection = null); app.buffer?.paragraphPrevious(); });
|
|
53
53
|
reg("ParagraphNext", (app) => { app.pane && (app.pane.selection = null); app.buffer?.paragraphNext(); });
|
|
54
|
-
reg("PageUp", (app) => { app.pane && (app.pane.selection = null); app.
|
|
55
|
-
reg("PageDown", (app) => { app.pane && (app.pane.selection = null); app.
|
|
54
|
+
reg("PageUp", (app) => { app.pane && (app.pane.selection = null); app.pageScroll?.(app.pane, -1); });
|
|
55
|
+
reg("PageDown", (app) => { app.pane && (app.pane.selection = null); app.pageScroll?.(app.pane, 1); });
|
|
56
56
|
|
|
57
57
|
// Selection — extend
|
|
58
58
|
reg("SelectUp", (app) => _actExtendSel(app, (buf) => buf._moveUpVisual?.() ?? buf.moveUp?.()));
|
|
@@ -67,8 +67,8 @@ function registerBuiltinActions() {
|
|
|
67
67
|
reg("SelectToEndOfLine", (app) => _actExtendSel(app, (buf) => buf.moveEnd?.()));
|
|
68
68
|
reg("SelectToStart", (app) => _actExtendSel(app, (buf) => buf.moveStartOfBuffer?.()));
|
|
69
69
|
reg("SelectToEnd", (app) => _actExtendSel(app, (buf) => buf.moveEndOfBuffer?.()));
|
|
70
|
-
reg("SelectPageUp", (app) =>
|
|
71
|
-
reg("SelectPageDown", (app) =>
|
|
70
|
+
reg("SelectPageUp", (app) => app.cursorPage?.(app.pane, -1, { select: true }));
|
|
71
|
+
reg("SelectPageDown", (app) => app.cursorPage?.(app.pane, 1, { select: true }));
|
|
72
72
|
reg("SelectToParagraphPrevious", (app) => _actExtendSel(app, (buf) => buf.paragraphPrevious?.()));
|
|
73
73
|
reg("SelectToParagraphNext", (app) => _actExtendSel(app, (buf) => buf.paragraphNext?.()));
|
|
74
74
|
|
|
@@ -298,10 +298,10 @@ function registerBuiltinActions() {
|
|
|
298
298
|
reg("End", (app) => { app.pane && (app.pane.selection = null); app.buffer?._lastVisX != null && (app.buffer._lastVisX = null); app.buffer?.moveEndOfBuffer(); app.scrollCursorToBoundary?.(app.pane, "end"); });
|
|
299
299
|
|
|
300
300
|
// Page aliases
|
|
301
|
-
reg("CursorPageUp", (app) =>
|
|
302
|
-
reg("CursorPageDown", (app) =>
|
|
303
|
-
reg("HalfPageUp", (app) =>
|
|
304
|
-
reg("HalfPageDown", (app) =>
|
|
301
|
+
reg("CursorPageUp", (app) => app.cursorPage?.(app.pane, -1));
|
|
302
|
+
reg("CursorPageDown", (app) => app.cursorPage?.(app.pane, 1));
|
|
303
|
+
reg("HalfPageUp", (app) => app.cursorPage?.(app.pane, -1, { amount: Math.max(1, Math.floor((app.pane?.h ?? 24) / 2)) }));
|
|
304
|
+
reg("HalfPageDown", (app) => app.cursorPage?.(app.pane, 1, { amount: Math.max(1, Math.floor((app.pane?.h ?? 24) / 2)) }));
|
|
305
305
|
|
|
306
306
|
// Cursor-to-view-boundary
|
|
307
307
|
reg("CursorToViewTop", (app) => {
|
|
@@ -857,6 +857,8 @@ function _makePaneAPI(buffer, app) {
|
|
|
857
857
|
return {
|
|
858
858
|
get Buf() { return _makeBufAPI(buffer); },
|
|
859
859
|
get Cursor() { return _makeCursorAPI(buffer); },
|
|
860
|
+
CursorLocation: () => app?.formatCursorLocation?.(buffer) ?? "+1.0:1",
|
|
861
|
+
AbsoluteCursorLocation: () => app?.formatAbsoluteCursorLocation?.(buffer) ?? "+1:1",
|
|
860
862
|
|
|
861
863
|
Save: async () => app?.save?.(),
|
|
862
864
|
Quit: async () => app?.quit?.(),
|
package/src/screen/screen.js
CHANGED
|
@@ -3,6 +3,20 @@ import { styleToAnsi } from "../display/ansi-style.js";
|
|
|
3
3
|
import { CellBuffer } from "./cell-buffer.js";
|
|
4
4
|
import { DISABLE_MOUSE, DISABLE_PASTE, ENABLE_MOUSE, ENABLE_PASTE, ResizeEvent } from "./events.js";
|
|
5
5
|
|
|
6
|
+
const CURSOR_SHAPE_SEQUENCE = {
|
|
7
|
+
default: "\x1b[0 q",
|
|
8
|
+
"blinking-block": "\x1b[1 q",
|
|
9
|
+
block: "\x1b[2 q",
|
|
10
|
+
"blinking-underline": "\x1b[3 q",
|
|
11
|
+
underline: "\x1b[4 q",
|
|
12
|
+
"blinking-bar": "\x1b[5 q",
|
|
13
|
+
bar: "\x1b[6 q",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function cursorShapeSequence(shape) {
|
|
17
|
+
return CURSOR_SHAPE_SEQUENCE[shape] ?? CURSOR_SHAPE_SEQUENCE.block;
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
export class Screen {
|
|
7
21
|
constructor({ mouse = true } = {}) {
|
|
8
22
|
this.mouse = mouse;
|
|
@@ -23,7 +37,7 @@ export class Screen {
|
|
|
23
37
|
fini() {
|
|
24
38
|
if (this.mouse) this.write(DISABLE_MOUSE);
|
|
25
39
|
this.write(DISABLE_PASTE);
|
|
26
|
-
this.write("\x1b[?25h\x1b[?1049l\x1b[0m");
|
|
40
|
+
this.write("\x1b[0 q\x1b[?25h\x1b[?1049l\x1b[0m");
|
|
27
41
|
}
|
|
28
42
|
|
|
29
43
|
SetContent(x, y, ch, combining = [], style = null) {
|
|
@@ -81,9 +95,7 @@ export class Screen {
|
|
|
81
95
|
}
|
|
82
96
|
out += "\x1b[0m";
|
|
83
97
|
if (this.cursor && this.cursorVisible) {
|
|
84
|
-
|
|
85
|
-
const shape = this.cursor.shape === "bar" ? "\x1b[5 q" : this.cursor.shape === "steady-block" ? "\x1b[2 q" : "\x1b[1 q";
|
|
86
|
-
out += shape + this.move(this.cursor.y + 1, this.cursor.x + 1) + "\x1b[?25h";
|
|
98
|
+
out += cursorShapeSequence(this.cursor.shape) + this.move(this.cursor.y + 1, this.cursor.x + 1) + "\x1b[?25h";
|
|
87
99
|
} else out += "\x1b[?25l";
|
|
88
100
|
this.write(out);
|
|
89
101
|
this.previous = this.cells.clone();
|
package/todo.txt
CHANGED
|
@@ -15,6 +15,7 @@ Current handoff notes
|
|
|
15
15
|
- Always emits ANSI codes regardless of TTY state.
|
|
16
16
|
- -profile removed from usage (flag parsed but never implemented in Go either; -debug kept for future log.txt work).
|
|
17
17
|
[x] Recent editor UX parity implemented:
|
|
18
|
+
- Global cursorshape option supports common DECSCUSR cursor shapes: default, steady/blinking block, underline, and bar, with command completion.
|
|
18
19
|
- Tab autocomplete candidate row cycles highlight correctly and only highlights the selected candidate text.
|
|
19
20
|
- Tab autocomplete single-match case fixed: was silently no-op due to acHas=false; now inserts suffix directly.
|
|
20
21
|
- Mouse click on autocomplete candidates no longer moves the editor cursor to the click location.
|
|
@@ -283,11 +284,15 @@ Clipboard / platform integration
|
|
|
283
284
|
Remaining: real platform verification for wl-copy/wl-paste, xclip, xsel.
|
|
284
285
|
[ ] Verify Android behavior only when process.platform reports android; Linux must not use Termux clipboard commands.
|
|
285
286
|
[ ] Verify macOS pbcopy/pbpaste and Windows PowerShell Set-Clipboard/Get-Clipboard.
|
|
286
|
-
[
|
|
287
|
-
|
|
287
|
+
[~] Implement terminal OSC 52 clipboard method.
|
|
288
|
+
Done: osc52Clipboard backend + probeOSC52 exported from clipboard.js; App.start() probes on startup (150ms, before main data handler), upgrades _writeBackend if supported; tmux $TMUX env skips probe and uses DCS passthrough format; --version probes last and prints Clipboard: result after other fields.
|
|
289
|
+
Remaining: OSC 52 read (async response handling for Ctrl+V from system clipboard via escape sequence).
|
|
290
|
+
[~] Implement primary selection behavior for Linux where applicable.
|
|
291
|
+
Done: wlClipboard read/write now pass --primary flag when register==="primary"; _syncPrimarySelection() called at end of every _dispatchInput event loop (keyboard+shift+arrows+alt-s+ctrl-a+mouse drag/double-click/gutter); middle mouse button click moves cursor to click position and pastes primary register.
|
|
292
|
+
Remaining: real platform testing; primary selection for macOS (no direct equivalent); wl-clipboard backend primary verification.
|
|
288
293
|
[~] Implement internal multi-register clipboard and multi-cursor clipboard parity.
|
|
289
294
|
Done: internal register map exists and is used as fallback; Ctrl-C/Ctrl-X/Ctrl-V/Ctrl-Y handle current selection and line copy/cut/paste.
|
|
290
|
-
Remaining: Go micro multi-register semantics,
|
|
295
|
+
Remaining: Go micro multi-register semantics, multi-cursor clipboard behavior.
|
|
291
296
|
|
|
292
297
|
Shell / jobs / terminal pane
|
|
293
298
|
----------------------------
|