bunmicro 0.9.10 → 0.9.19

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.
@@ -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 = this.backend.write?.(text, register) ?? true;
38
- if (!ok) this.fallbackToInternal();
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.fallbackToInternal();
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
- read: () => outputOrThrow(runSync([wlPaste, "--no-newline"], { timeout: CLIPBOARD_TIMEOUT_MS })),
113
- write: (text) => runSync([wlCopy], { stdin: text, stdout: "ignore", timeout: CLIPBOARD_TIMEOUT_MS }).ok,
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"], { timeout: CLIPBOARD_TIMEOUT_MS })).replace(/\r?\n$/, ""),
150
- write: (text) => runSync([shell, "-NoProfile", "-Command", "Set-Clipboard"], { stdin: text, stdout: "ignore", timeout: CLIPBOARD_TIMEOUT_MS }).ok,
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
+ }
@@ -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.buffer?.page(-1, app.pane?.h ?? 24); });
55
- reg("PageDown", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page(1, app.pane?.h ?? 24); });
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) => _actExtendSel(app, (buf) => buf.page?.(-1, app.pane?.h ?? 24)));
71
- reg("SelectPageDown", (app) => _actExtendSel(app, (buf) => buf.page?.(1, app.pane?.h ?? 24)));
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) => { app.pane && (app.pane.selection = null); app.buffer?.page?.(-1, app.pane?.h ?? 24); });
302
- reg("CursorPageDown", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page?.(1, app.pane?.h ?? 24); });
303
- reg("HalfPageUp", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page?.(-1, Math.max(1, Math.floor((app.pane?.h ?? 24) / 2))); });
304
- reg("HalfPageDown", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page?.(1, Math.max(1, Math.floor((app.pane?.h ?? 24) / 2))); });
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/todo.txt CHANGED
@@ -283,11 +283,15 @@ Clipboard / platform integration
283
283
  Remaining: real platform verification for wl-copy/wl-paste, xclip, xsel.
284
284
  [ ] Verify Android behavior only when process.platform reports android; Linux must not use Termux clipboard commands.
285
285
  [ ] Verify macOS pbcopy/pbpaste and Windows PowerShell Set-Clipboard/Get-Clipboard.
286
- [ ] Implement terminal OSC 52 clipboard method.
287
- [ ] Implement primary selection behavior for Linux where applicable.
286
+ [~] Implement terminal OSC 52 clipboard method.
287
+ 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.
288
+ Remaining: OSC 52 read (async response handling for Ctrl+V from system clipboard via escape sequence).
289
+ [~] Implement primary selection behavior for Linux where applicable.
290
+ 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.
291
+ Remaining: real platform testing; primary selection for macOS (no direct equivalent); wl-clipboard backend primary verification.
288
292
  [~] Implement internal multi-register clipboard and multi-cursor clipboard parity.
289
293
  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, primary selection parity, multi-cursor clipboard behavior.
294
+ Remaining: Go micro multi-register semantics, multi-cursor clipboard behavior.
291
295
 
292
296
  Shell / jobs / terminal pane
293
297
  ----------------------------