bunmicro 0.9.20 → 0.9.21

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.21] - 2026-06-09
4
+ - Prompt mouse click repositions cursor (command and shell prompt)
5
+ - Clicking > or $ label toggles between command/shell prompt, preserving input
6
+ - Prompt mouse double click: left=key-down, middle=key-up, right=key-enter (command and shell prompt)
7
+ - js and eval js/py/sh commands
8
+
3
9
  ## [0.9.20] - 2026-06-07
4
10
  - Added cursor shape option
5
11
  - 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.21",
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";
@@ -1935,7 +1935,11 @@ class App {
1935
1935
  const totalText = this.prompt.label + this.prompt.value;
1936
1936
  const labelW = displayWidth(this.prompt.label);
1937
1937
  const cursorInTotal = labelW + displayWidth(this.prompt.value.slice(0, this.prompt.cursor));
1938
- const scrollX = Math.max(0, cursorInTotal - (this.cols - 1));
1938
+ let scrollX = this._promptScrollX ?? 0;
1939
+ if (cursorInTotal > scrollX + this.cols - 1) scrollX = cursorInTotal - (this.cols - 1);
1940
+ if (cursorInTotal < scrollX) scrollX = cursorInTotal;
1941
+ scrollX = Math.max(0, scrollX);
1942
+ this._promptScrollX = scrollX;
1939
1943
  const startIdx = scrollX > 0 ? visualColToCharIdx(totalText, 0, scrollX) : 0;
1940
1944
  putText(this.screen, 0, promptRow, totalText.slice(startIdx), promptStyle, this.cols);
1941
1945
  this.screen.setCursor(cursorInTotal - scrollX, promptRow, true, "bar");
@@ -3191,6 +3195,61 @@ class App {
3191
3195
  this._suppressMouseUntilUp = false;
3192
3196
  }
3193
3197
 
3198
+ // Prompt row click: reposition cursor, toggle shell/command mode, or zone double-click
3199
+ if (this.prompt && event.y === this.rows - 1) {
3200
+ if ((event.action === "down" || event.action === "drag") && event.button === "left") {
3201
+ const totalText = this.prompt.label + this.prompt.value;
3202
+ const scrollX = this._promptScrollX ?? 0;
3203
+ const startIdx = scrollX > 0 ? visualColToCharIdx(totalText, 0, scrollX) : 0;
3204
+ const clickedCharIdx = visualColToCharIdx(totalText, startIdx, event.x);
3205
+
3206
+ if (event.action === "down") {
3207
+ // Double-click zone: divide prompt row into thirds
3208
+ const third = Math.floor(this.cols / 3);
3209
+ const zone = event.x < third ? "left" : event.x < third * 2 ? "middle" : "right";
3210
+ const now = Date.now();
3211
+ const isDoubleClick = this._lastPromptClickZone === zone &&
3212
+ now - (this._lastPromptClickTime ?? 0) < 400;
3213
+ this._lastPromptClickTime = now;
3214
+ this._lastPromptClickZone = zone;
3215
+
3216
+ if (isDoubleClick && !this.prompt.yn) {
3217
+ if (zone === "left") {
3218
+ this.prompt.historyDown();
3219
+ } else if (zone === "middle") {
3220
+ this.prompt.historyUp();
3221
+ } else {
3222
+ const prompt = this.prompt;
3223
+ this.prompt = null;
3224
+ prompt.commit();
3225
+ await prompt.callback(prompt.value);
3226
+ }
3227
+ this.render();
3228
+ return;
3229
+ }
3230
+
3231
+ // Single click on label (> or $): toggle Command ↔ Shell
3232
+ if (clickedCharIdx < this.prompt.label.length &&
3233
+ (this.prompt.type === "Command" || this.prompt.type === "Shell")) {
3234
+ const val = this.prompt.value;
3235
+ if (this.prompt.type === "Command") {
3236
+ this.openShellMode();
3237
+ this.prompt.value = val;
3238
+ this.prompt.cursor = val.length;
3239
+ } else {
3240
+ this.openCommandMode(val);
3241
+ }
3242
+ this.render();
3243
+ return;
3244
+ }
3245
+ }
3246
+
3247
+ this.prompt.cursor = Math.max(0, Math.min(clickedCharIdx - this.prompt.label.length, this.prompt.value.length));
3248
+ this.render();
3249
+ }
3250
+ return;
3251
+ }
3252
+
3194
3253
  if (this.handleSuggestionMouse(event)) {
3195
3254
  this.render();
3196
3255
  return;
@@ -3611,6 +3670,19 @@ class App {
3611
3670
  this.prompt.cursor = 0;
3612
3671
  } else if (key === "end" || key === "ctrl-e") {
3613
3672
  this.prompt.cursor = this.prompt.value.length;
3673
+ } else if (key === "ctrl-u") {
3674
+ const prompt = this.prompt;
3675
+ prompt.value = prompt.value.slice(prompt.cursor);
3676
+ prompt.cursor = 0;
3677
+ prompt.resetCompletion();
3678
+ this.message = "";
3679
+ prompt.onDelta?.(prompt.value);
3680
+ } else if (key === "ctrl-k") {
3681
+ const prompt = this.prompt;
3682
+ prompt.value = prompt.value.slice(0, prompt.cursor);
3683
+ prompt.resetCompletion();
3684
+ this.message = "";
3685
+ prompt.onDelta?.(prompt.value);
3614
3686
  } else if (key === "backspace") {
3615
3687
  const prompt = this.prompt;
3616
3688
  if (prompt.cursor > 0) {
@@ -3674,6 +3746,7 @@ class App {
3674
3746
 
3675
3747
  openPrompt(label, callback, options = {}) {
3676
3748
  this.prompt = new Prompt(label, callback, options);
3749
+ this._promptScrollX = 0;
3677
3750
  }
3678
3751
 
3679
3752
  openYNPrompt(label, callback, { onCancel = null } = {}) {
@@ -4554,9 +4627,53 @@ class App {
4554
4627
  this.message = toSpaces ? "Retabbed to spaces" : "Retabbed to tabs";
4555
4628
  break;
4556
4629
  }
4557
- case "eval":
4558
- this.message = "Eval unsupported";
4630
+ case "eval": {
4631
+ const lang = cmdArgs[0];
4632
+ if (!lang) { this.message = "Usage: eval js|py|sh [code]"; break; }
4633
+ if (lang !== "js" && lang !== "py" && lang !== "sh") {
4634
+ this.message = `eval: unknown language '${lang}' — use js, py, or sh`;
4635
+ break;
4636
+ }
4637
+ // Code source: inline (raw, bypass shell quoting) or selection
4638
+ let evalCode;
4639
+ const inlineMatch = /^\s*eval\s+(?:js|py|sh)\s+(.+)$/s.exec(input);
4640
+ if (inlineMatch) {
4641
+ evalCode = inlineMatch[1];
4642
+ } else {
4643
+ const sel = this.pane?.selection;
4644
+ evalCode = (sel && !sameLoc(sel.start, sel.end)) ? getSelectionText(buf, sel) : null;
4645
+ if (!evalCode) { this.message = `eval ${lang}: select text, or use: eval ${lang} <code>`; break; }
4646
+ }
4647
+ // Build temp file
4648
+ const { tmpdir } = await import("node:os");
4649
+ const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 8);
4650
+ let ext, execArgs, fileContent = evalCode;
4651
+ if (lang === "js") {
4652
+ ext = "js";
4653
+ execArgs = [Bun.which("bun") ?? "bun"];
4654
+ } else if (lang === "py") {
4655
+ ext = "py";
4656
+ const pyBin = process.platform === "win32" ? "python" : "python3";
4657
+ execArgs = [Bun.which(pyBin) ?? pyBin];
4658
+ } else { // sh
4659
+ if (existsSync("/bin/sh")) {
4660
+ ext = "sh";
4661
+ execArgs = ["/bin/sh"];
4662
+ } else {
4663
+ ext = "js";
4664
+ fileContent = `import { $ } from "bun";\nawait $\`\n${evalCode}\n\`;\n`;
4665
+ execArgs = [Bun.which("bun") ?? "bun"];
4666
+ }
4667
+ }
4668
+ const evalTmpFile = join(tmpdir(), `bunmicro-tmp${suffix}.${ext}`);
4669
+ await Bun.write(evalTmpFile, fileContent);
4670
+ try {
4671
+ await this.runInteractiveShell([...execArgs, evalTmpFile]);
4672
+ } finally {
4673
+ try { unlinkSync(evalTmpFile); } catch {}
4674
+ }
4559
4675
  break;
4676
+ }
4560
4677
  case "bind":
4561
4678
  case "unbind":
4562
4679
  this.message = `${cmd}: keybinding system not yet implemented`;
@@ -4660,6 +4777,7 @@ class App {
4660
4777
  const pluginCmd = this.context.plugins?.commands?.get(cmd);
4661
4778
  if (pluginCmd) {
4662
4779
  try {
4780
+ cmdArgs.raw = input;
4663
4781
  await pluginCmd(makePaneAdapter(this.buffer, this), cmdArgs);
4664
4782
  } catch (e) {
4665
4783
  this.message = String(e.message ?? e);