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.
@@ -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?.(),
@@ -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
- // cursor shape: 1/2=block, 3/4=underline, 5/6=bar; odd=blink even=steady
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
- [ ] Implement terminal OSC 52 clipboard method.
287
- [ ] Implement primary selection behavior for Linux where applicable.
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, primary selection parity, multi-cursor clipboard behavior.
295
+ Remaining: Go micro multi-register semantics, multi-cursor clipboard behavior.
291
296
 
292
297
  Shell / jobs / terminal pane
293
298
  ----------------------------