bunmicro 0.9.20 → 0.9.22

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.22] - 2026-06-09
4
+ - Clicking on icons toggles prompts
5
+ - Unsaved star triggers save cmd
6
+ - URLs support save cursor
7
+
8
+ ## [0.9.21] - 2026-06-09
9
+ - Prompt mouse click repositions cursor (command and shell prompt)
10
+ - Clicking > or $ label toggles between command/shell prompt, preserving input
11
+ - Prompt mouse double click: left=key-down, middle=key-up, right=key-enter (command and shell prompt)
12
+ - js and eval js/py/sh commands
13
+
3
14
  ## [0.9.20] - 2026-06-07
4
15
  - Added cursor shape option
5
16
  - Fixed selection hide cursor
package/README.md CHANGED
@@ -24,12 +24,13 @@
24
24
  - Alt-s to enter selection mode
25
25
  - Useful without a mouse on Android
26
26
  - Also available: Ctrl-E act SelectRight
27
- ## js plugin
27
+ ## js plugin / command
28
28
  - Instead of writing Lua, use your familiar JavaScript to extend functionalities
29
29
  - runtime/jsplugins/`name`/`name`.js
30
30
  - a full documentation in example.js
31
31
  - an example plugin named chapter for turning to the next/prev page by number.
32
32
  - It registers 2 commands: next/prevchapter
33
+ - js and eval js command explained at the bottom
33
34
  ## Output highlighted text to terminal
34
35
  - Works like bat ccat glow
35
36
  - bunmicro -bat file
@@ -179,16 +180,51 @@ bun x bunmicro
179
180
  - Encoding: Reopen with a specific encoding.
180
181
  * Show supported encodings by bunmicro --version
181
182
  - Alt-G: Show nano-like key bindings menu
183
+ - Command/Shell Prompt row:
184
+ * See the next section
182
185
 
183
186
  # Command/Shell Prompts
184
187
  ## Command
185
188
  - Internal commands for automating / tuning bunmicro
186
189
  - Press Tab for available commands, arrow keys for selection
187
190
  - In this Bun version, I added more commands like
188
- * js to eval JavaScript
189
191
  * act/action to do automation actions.
190
192
  * Press tab after act to get a list of them
191
193
  * or use help actions to show the list
194
+
195
+ ## eval — run code in py/js/sh
196
+ - Executed in a separate processs
197
+ - `eval js` — run selected text as JavaScript (via Bun)
198
+ - `eval py` — run selected text as Python (python3 / python on Windows)
199
+ - `eval sh` — run selected text as shell script (/bin/sh, or Bun shell on Windows)
200
+ - If no text is selected, append code inline:
201
+ * `eval js console.log("hi")`
202
+ * `eval py print("hi")`
203
+ * `eval sh echo hi`
204
+ - Code after the language name is taken literally (no shell quoting)
205
+ - Output is shown in the terminal (same as Ctrl-B shell mode)
206
+ - Runs via a temp file (`bunmicro-tmpXXXXXXXX.js/py/sh`), deleted after execution
207
+
208
+ ## js — eval JavaScript inline
209
+ - Executed inside bunmicro's jsplugin context,
210
+ - `js <expression or code>`
211
+ - Code is passed raw to await eval (bypass shell quoting), result shown via alert
212
+ - Examples:
213
+ - `js new Date().toISOString()`
214
+ - `js let ln=micro.getLine(); ln`
215
+ - Implemented in `runtime/jsplugins/example/example.js` as a demo of `micro.MakeCommand`
192
216
  ## Shell
193
217
  - Executes a given shell command like sh -c
194
218
  - Outputs the result to the original terminal before entering bunmicro
219
+ ## Prompt mouse gestures
220
+ - Click anywhere: move cursor to that position
221
+ - Click `>` or `$` label: toggle between Command and Shell mode (input is preserved)
222
+ - Double click left ⅓ of row: newer history (same as ↓ key)
223
+ - Double click middle ⅓ of row: older history (same as ↑ key)
224
+ - Double click right ⅓ of row: execute the current input (same as Enter)
225
+ ## Prompt keyboard shortcuts
226
+ - `Ctrl-U`: delete everything before cursor
227
+ - `Ctrl-K`: delete everything after cursor
228
+ - `Ctrl-A` / `Home`: move cursor to start
229
+ - `Ctrl-E` / `End`: move cursor to end
230
+ - `↑` / `↓`: navigate command history
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunmicro",
3
- "version": "0.9.20",
3
+ "version": "0.9.22",
4
4
  "description": "Bun JavaScript rewrite of the micro editor originally in Golang",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -41,6 +41,8 @@ Flat buffer helpers (all 1-based line numbers, omit → cursor line):
41
41
  Other micro APIs:
42
42
  micro.CurPane() — returns pane adapter for active pane
43
43
  micro.MakeCommand(name, fn) — register Ctrl+E command; fn(bp, args[])
44
+ args.raw = full original input string (bypass shellSplit)
45
+ e.g. for command "js 1+1": args.raw = "js 1+1", args.raw.slice(3) = "1+1"
44
46
  micro.RegisterAction(name, fn) — register bindable action
45
47
  micro.TermMessage(msg) — show msg in editor status row
46
48
  micro.alert(msg) — suspend editor, print msg, wait for Enter
@@ -70,12 +72,10 @@ micro.on("init", () => {
70
72
 
71
73
  micro.MakeCommand("js", async (bp, args) =>
72
74
  {
73
- let scriptText=args.join("\n");
74
-
75
- await micro.alert(
76
- await eval(scriptText)
77
- )
78
-
75
+ // args.raw is the full original input string, e.g. "js console.log('hi')"
76
+ // slice(3) skips the "js " prefix (2-char name + 1 space)
77
+ const scriptText = args.raw?.slice(3) ?? args.join(" ");
78
+ await micro.alert(await eval(scriptText));
79
79
  });
80
80
 
81
81
 
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import child_process from "node:child_process"
4
- import { accessSync, constants, existsSync, readdirSync, statSync } from "node:fs";
4
+ import { accessSync, constants, existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
5
5
  import { mkdir } from "node:fs/promises";
6
6
  import { dirname, basename, join, resolve, sep } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
@@ -1884,8 +1884,14 @@ class App {
1884
1884
  let sx = 0, x0;
1885
1885
  // name
1886
1886
  x0 = sx;
1887
- sx = putText(this.screen, sx, statusRow, ` ${name}${dirty} `, isReadonlyBuffer(buf) ? redStatus : baseStatus, this.cols - sx);
1887
+ sx = putText(this.screen, sx, statusRow, ` ${name}`, isReadonlyBuffer(buf) ? redStatus : baseStatus, this.cols - sx);
1888
1888
  markZone("name", x0, sx);
1889
+ if (dirty) {
1890
+ x0 = sx;
1891
+ sx = putText(this.screen, sx, statusRow, dirty, baseStatus, this.cols - sx);
1892
+ markZone("dirty", x0, sx);
1893
+ }
1894
+ sx = putText(this.screen, sx, statusRow, " ", baseStatus, this.cols - sx);
1889
1895
  // (row,col)
1890
1896
  sx = putText(this.screen, sx, statusRow, "(", baseStatus, this.cols - sx);
1891
1897
  x0 = sx;
@@ -1935,7 +1941,11 @@ class App {
1935
1941
  const totalText = this.prompt.label + this.prompt.value;
1936
1942
  const labelW = displayWidth(this.prompt.label);
1937
1943
  const cursorInTotal = labelW + displayWidth(this.prompt.value.slice(0, this.prompt.cursor));
1938
- const scrollX = Math.max(0, cursorInTotal - (this.cols - 1));
1944
+ let scrollX = this._promptScrollX ?? 0;
1945
+ if (cursorInTotal > scrollX + this.cols - 1) scrollX = cursorInTotal - (this.cols - 1);
1946
+ if (cursorInTotal < scrollX) scrollX = cursorInTotal;
1947
+ scrollX = Math.max(0, scrollX);
1948
+ this._promptScrollX = scrollX;
1939
1949
  const startIdx = scrollX > 0 ? visualColToCharIdx(totalText, 0, scrollX) : 0;
1940
1950
  putText(this.screen, 0, promptRow, totalText.slice(startIdx), promptStyle, this.cols);
1941
1951
  this.screen.setCursor(cursorInTotal - scrollX, promptRow, true, "bar");
@@ -3191,6 +3201,61 @@ class App {
3191
3201
  this._suppressMouseUntilUp = false;
3192
3202
  }
3193
3203
 
3204
+ // Prompt row click: reposition cursor, toggle shell/command mode, or zone double-click
3205
+ if (this.prompt && event.y === this.rows - 1) {
3206
+ if ((event.action === "down" || event.action === "drag") && event.button === "left") {
3207
+ const totalText = this.prompt.label + this.prompt.value;
3208
+ const scrollX = this._promptScrollX ?? 0;
3209
+ const startIdx = scrollX > 0 ? visualColToCharIdx(totalText, 0, scrollX) : 0;
3210
+ const clickedCharIdx = visualColToCharIdx(totalText, startIdx, event.x);
3211
+
3212
+ if (event.action === "down") {
3213
+ // Double-click zone: divide prompt row into thirds
3214
+ const third = Math.floor(this.cols / 3);
3215
+ const zone = event.x < third ? "left" : event.x < third * 2 ? "middle" : "right";
3216
+ const now = Date.now();
3217
+ const isDoubleClick = this._lastPromptClickZone === zone &&
3218
+ now - (this._lastPromptClickTime ?? 0) < 400;
3219
+ this._lastPromptClickTime = now;
3220
+ this._lastPromptClickZone = zone;
3221
+
3222
+ if (isDoubleClick && !this.prompt.yn) {
3223
+ if (zone === "left") {
3224
+ this.prompt.historyDown();
3225
+ } else if (zone === "middle") {
3226
+ this.prompt.historyUp();
3227
+ } else {
3228
+ const prompt = this.prompt;
3229
+ this.prompt = null;
3230
+ prompt.commit();
3231
+ await prompt.callback(prompt.value);
3232
+ }
3233
+ this.render();
3234
+ return;
3235
+ }
3236
+
3237
+ // Single click on label (> or $): toggle Command ↔ Shell
3238
+ if (clickedCharIdx < this.prompt.label.length &&
3239
+ (this.prompt.type === "Command" || this.prompt.type === "Shell")) {
3240
+ const val = this.prompt.value;
3241
+ if (this.prompt.type === "Command") {
3242
+ this.openShellMode();
3243
+ this.prompt.value = val;
3244
+ this.prompt.cursor = val.length;
3245
+ } else {
3246
+ this.openCommandMode(val);
3247
+ }
3248
+ this.render();
3249
+ return;
3250
+ }
3251
+ }
3252
+
3253
+ this.prompt.cursor = Math.max(0, Math.min(clickedCharIdx - this.prompt.label.length, this.prompt.value.length));
3254
+ this.render();
3255
+ }
3256
+ return;
3257
+ }
3258
+
3194
3259
  if (this.handleSuggestionMouse(event)) {
3195
3260
  this.render();
3196
3261
  return;
@@ -3419,6 +3484,12 @@ class App {
3419
3484
  this.nextTab();
3420
3485
  }
3421
3486
  break;
3487
+ case "dirty": {
3488
+ const name = buf?.name ?? "No name";
3489
+ const filename = /^[^\s"'\\]+$/.test(name) ? name : JSON.stringify(name);
3490
+ this.openCommandMode(`save ${filename}`);
3491
+ break;
3492
+ }
3422
3493
  case "row":
3423
3494
  if (isTerm) {
3424
3495
  this.pane.terminal?.write("\x12");
@@ -3481,10 +3552,10 @@ class App {
3481
3552
  await this.addTab();
3482
3553
  break;
3483
3554
  case "cmdmode":
3484
- this.openCommandMode();
3555
+ await this.togglePromptMode("Command");
3485
3556
  break;
3486
3557
  case "shellmode":
3487
- this.openShellMode();
3558
+ await this.togglePromptMode("Shell");
3488
3559
  break;
3489
3560
  }
3490
3561
  return true;
@@ -3611,6 +3682,19 @@ class App {
3611
3682
  this.prompt.cursor = 0;
3612
3683
  } else if (key === "end" || key === "ctrl-e") {
3613
3684
  this.prompt.cursor = this.prompt.value.length;
3685
+ } else if (key === "ctrl-u") {
3686
+ const prompt = this.prompt;
3687
+ prompt.value = prompt.value.slice(prompt.cursor);
3688
+ prompt.cursor = 0;
3689
+ prompt.resetCompletion();
3690
+ this.message = "";
3691
+ prompt.onDelta?.(prompt.value);
3692
+ } else if (key === "ctrl-k") {
3693
+ const prompt = this.prompt;
3694
+ prompt.value = prompt.value.slice(0, prompt.cursor);
3695
+ prompt.resetCompletion();
3696
+ this.message = "";
3697
+ prompt.onDelta?.(prompt.value);
3614
3698
  } else if (key === "backspace") {
3615
3699
  const prompt = this.prompt;
3616
3700
  if (prompt.cursor > 0) {
@@ -3674,12 +3758,25 @@ class App {
3674
3758
 
3675
3759
  openPrompt(label, callback, options = {}) {
3676
3760
  this.prompt = new Prompt(label, callback, options);
3761
+ this._promptScrollX = 0;
3677
3762
  }
3678
3763
 
3679
3764
  openYNPrompt(label, callback, { onCancel = null } = {}) {
3680
3765
  this.prompt = new Prompt(label, callback, { yn: true, onCancel });
3681
3766
  }
3682
3767
 
3768
+ async togglePromptMode(type) {
3769
+ if (this.prompt?.type === type) {
3770
+ const prompt = this.prompt;
3771
+ this.prompt = null;
3772
+ await prompt.onCancel?.();
3773
+ return;
3774
+ }
3775
+
3776
+ if (type === "Command") this.openCommandMode();
3777
+ else this.openShellMode();
3778
+ }
3779
+
3683
3780
  async checkExternalReload() {
3684
3781
  if (this.prompt || this.pane?.type !== "editor") return false;
3685
3782
  const buf = this.buffer;
@@ -4554,9 +4651,53 @@ class App {
4554
4651
  this.message = toSpaces ? "Retabbed to spaces" : "Retabbed to tabs";
4555
4652
  break;
4556
4653
  }
4557
- case "eval":
4558
- this.message = "Eval unsupported";
4654
+ case "eval": {
4655
+ const lang = cmdArgs[0];
4656
+ if (!lang) { this.message = "Usage: eval js|py|sh [code]"; break; }
4657
+ if (lang !== "js" && lang !== "py" && lang !== "sh") {
4658
+ this.message = `eval: unknown language '${lang}' — use js, py, or sh`;
4659
+ break;
4660
+ }
4661
+ // Code source: inline (raw, bypass shell quoting) or selection
4662
+ let evalCode;
4663
+ const inlineMatch = /^\s*eval\s+(?:js|py|sh)\s+(.+)$/s.exec(input);
4664
+ if (inlineMatch) {
4665
+ evalCode = inlineMatch[1];
4666
+ } else {
4667
+ const sel = this.pane?.selection;
4668
+ evalCode = (sel && !sameLoc(sel.start, sel.end)) ? getSelectionText(buf, sel) : null;
4669
+ if (!evalCode) { this.message = `eval ${lang}: select text, or use: eval ${lang} <code>`; break; }
4670
+ }
4671
+ // Build temp file
4672
+ const { tmpdir } = await import("node:os");
4673
+ const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 8);
4674
+ let ext, execArgs, fileContent = evalCode;
4675
+ if (lang === "js") {
4676
+ ext = "js";
4677
+ execArgs = [Bun.which("bun") ?? "bun"];
4678
+ } else if (lang === "py") {
4679
+ ext = "py";
4680
+ const pyBin = process.platform === "win32" ? "python" : "python3";
4681
+ execArgs = [Bun.which(pyBin) ?? pyBin];
4682
+ } else { // sh
4683
+ if (existsSync("/bin/sh")) {
4684
+ ext = "sh";
4685
+ execArgs = ["/bin/sh"];
4686
+ } else {
4687
+ ext = "js";
4688
+ fileContent = `import { $ } from "bun";\nawait $\`\n${evalCode}\n\`;\n`;
4689
+ execArgs = [Bun.which("bun") ?? "bun"];
4690
+ }
4691
+ }
4692
+ const evalTmpFile = join(tmpdir(), `bunmicro-tmp${suffix}.${ext}`);
4693
+ await Bun.write(evalTmpFile, fileContent);
4694
+ try {
4695
+ await this.runInteractiveShell([...execArgs, evalTmpFile]);
4696
+ } finally {
4697
+ try { unlinkSync(evalTmpFile); } catch {}
4698
+ }
4559
4699
  break;
4700
+ }
4560
4701
  case "bind":
4561
4702
  case "unbind":
4562
4703
  this.message = `${cmd}: keybinding system not yet implemented`;
@@ -4660,6 +4801,7 @@ class App {
4660
4801
  const pluginCmd = this.context.plugins?.commands?.get(cmd);
4661
4802
  if (pluginCmd) {
4662
4803
  try {
4804
+ cmdArgs.raw = input;
4663
4805
  await pluginCmd(makePaneAdapter(this.buffer, this), cmdArgs);
4664
4806
  } catch (e) {
4665
4807
  this.message = String(e.message ?? e);
@@ -5531,17 +5673,18 @@ function commandHasStartupJump(command = {}) {
5531
5673
  }
5532
5674
 
5533
5675
  async function loadBufferForPath(pathOrUrl, context, command = {}) {
5676
+ let buffer;
5534
5677
  if (isHttpUrl(pathOrUrl)) {
5535
5678
  let encoding = context.config?.globalSettings?.encoding ?? DEFAULT_SETTINGS.encoding;
5536
5679
  const decoded = await fetchTextWithEncoding(pathOrUrl, encoding);
5537
5680
  const text = decoded.text;
5538
5681
  encoding = decoded.encoding;
5539
5682
  const urlPath = pathOrUrl.replace(/[?#].*$/, "");
5540
- const buffer = new BufferModel({ path: pathOrUrl, text, command, encoding });
5683
+ buffer = new BufferModel({ path: pathOrUrl, text, command, encoding });
5541
5684
  attachSyntax(buffer, context, urlPath, text);
5542
- return buffer;
5685
+ } else {
5686
+ buffer = await BufferModel.fromFile(pathOrUrl, command, context);
5543
5687
  }
5544
- const buffer = await BufferModel.fromFile(pathOrUrl, command, context);
5545
5688
  if (DEFAULT_SETTINGS.savecursor && !commandHasStartupJump(command) && context?.cursorStates?.[pathOrUrl]) {
5546
5689
  const saved = context.cursorStates[pathOrUrl];
5547
5690
  const y = clamp(saved.y ?? 0, 0, buffer.lines.length - 1);