bunmicro 0.9.19 → 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,15 @@
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
+
9
+ ## [0.9.20] - 2026-06-07
10
+ - Added cursor shape option
11
+ - Fixed selection hide cursor
12
+
3
13
  ## [0.9.19] - 2026-06-06
4
14
  - Fixed Windows clipboard
5
15
  - Fixed cli encoding help readme
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.19",
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",
@@ -94,6 +94,13 @@ Here are the available options:
94
94
 
95
95
  default value: `true`
96
96
 
97
+ * `cursorshape`: sets the editor cursor shape using the terminal's DECSCUSR
98
+ support. This setting is `global only`. Supported values are `default`,
99
+ `block`, `underline`, `bar`, and their `blinking-` variants. The plain
100
+ shape names use a steady cursor.
101
+
102
+ default value: `block`
103
+
97
104
  * `detectlimit`: if this is not set to 0, it will limit the amount of first
98
105
  lines in a file that are matched to determine the filetype.
99
106
  A higher limit means better accuracy of guessing the filetype, but also
@@ -566,6 +573,7 @@ so that you can see what the formatting should look like.
566
573
  "colorscheme": "default",
567
574
  "comment": true,
568
575
  "cursorline": true,
576
+ "cursorshape": "block",
569
577
  "detectlimit": 100,
570
578
  "diff": true,
571
579
  "diffgutter": false,
@@ -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
 
@@ -57,6 +57,7 @@ export const DEFAULT_GLOBAL_ONLY_SETTINGS = {
57
57
  autosave: 0,
58
58
  clipboard: "external",
59
59
  colorscheme: "default",
60
+ cursorshape: "block",
60
61
  savehistory: true,
61
62
  divchars: "|-",
62
63
  divreverse: true,
@@ -81,6 +82,12 @@ export const DEFAULT_GLOBAL_ONLY_SETTINGS = {
81
82
 
82
83
  export const OPTION_CHOICES = {
83
84
  clipboard: ["internal", "external", "terminal"],
85
+ cursorshape: [
86
+ "default",
87
+ "block", "blinking-block",
88
+ "underline", "blinking-underline",
89
+ "bar", "blinking-bar",
90
+ ],
84
91
  fileformat: ["unix", "dos"],
85
92
  helpsplit: ["hsplit", "vsplit"],
86
93
  matchbracestyle: ["underline", "highlight"],
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";
@@ -81,6 +81,7 @@ const DEFAULT_SETTINGS = {
81
81
  tabsize: 4,
82
82
  tabstospaces: false,
83
83
  autosave: 0,
84
+ cursorshape: "block",
84
85
  cursorline: true,
85
86
  diffgutter: false,
86
87
  eofnewline: true,
@@ -1249,6 +1250,9 @@ class BufferModel {
1249
1250
  SetOption(option, value) {
1250
1251
  const oldValue = this.Settings[option];
1251
1252
  const parsed = parseOptionValue(value);
1253
+ if (option === "cursorshape" && !OPTION_CHOICES.cursorshape.includes(String(parsed))) {
1254
+ throw new Error(`Invalid value for cursorshape: ${parsed}`);
1255
+ }
1252
1256
  if (option === "fileformat" && !OPTION_CHOICES.fileformat.includes(String(parsed))) {
1253
1257
  throw new Error(`Invalid value for fileformat: ${parsed}`);
1254
1258
  }
@@ -1263,6 +1267,9 @@ class BufferModel {
1263
1267
 
1264
1268
  DoSetOptionNative(option, value) {
1265
1269
  const oldValue = this.Settings[option];
1270
+ if (option === "cursorshape" && !OPTION_CHOICES.cursorshape.includes(String(value))) {
1271
+ throw new Error(`Invalid value for cursorshape: ${value}`);
1272
+ }
1266
1273
  if (option === "fileformat" && !OPTION_CHOICES.fileformat.includes(String(value))) {
1267
1274
  throw new Error(`Invalid value for fileformat: ${value}`);
1268
1275
  }
@@ -1928,7 +1935,11 @@ class App {
1928
1935
  const totalText = this.prompt.label + this.prompt.value;
1929
1936
  const labelW = displayWidth(this.prompt.label);
1930
1937
  const cursorInTotal = labelW + displayWidth(this.prompt.value.slice(0, this.prompt.cursor));
1931
- 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;
1932
1943
  const startIdx = scrollX > 0 ? visualColToCharIdx(totalText, 0, scrollX) : 0;
1933
1944
  putText(this.screen, 0, promptRow, totalText.slice(startIdx), promptStyle, this.cols);
1934
1945
  this.screen.setCursor(cursorInTotal - scrollX, promptRow, true, "bar");
@@ -1966,8 +1977,19 @@ class App {
1966
1977
  cursorCol = p.x + gutterW + displayWidth(line.slice(buf.scroll.x, cursorX));
1967
1978
  }
1968
1979
 
1969
- const cursorVisible = cursorRow >= p.y && cursorRow < p.y + p.h && cursorCol >= p.x && cursorCol < p.x + p.w;
1970
- this.screen.setCursor(clamp(cursorCol, 0, this.cols - 1), clamp(cursorRow, 0, this.rows - 1), cursorVisible, "steady-block");
1980
+ // Go micro hides the terminal cursor while a non-empty selection is
1981
+ // active. Otherwise its block cursor makes the exclusive selection end
1982
+ // look selected even though copy/cut correctly omit that character.
1983
+ const hasSelection = p.selection && !sameLoc(p.selection.start, p.selection.end);
1984
+ const cursorVisible = !hasSelection &&
1985
+ cursorRow >= p.y && cursorRow < p.y + p.h &&
1986
+ cursorCol >= p.x && cursorCol < p.x + p.w;
1987
+ this.screen.setCursor(
1988
+ clamp(cursorCol, 0, this.cols - 1),
1989
+ clamp(cursorRow, 0, this.rows - 1),
1990
+ cursorVisible,
1991
+ DEFAULT_SETTINGS.cursorshape,
1992
+ );
1971
1993
  } else if (!this.prompt && activePaneObj?.type !== "term") {
1972
1994
  this.screen.setCursor(0, 0, false);
1973
1995
  }
@@ -3173,6 +3195,61 @@ class App {
3173
3195
  this._suppressMouseUntilUp = false;
3174
3196
  }
3175
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
+
3176
3253
  if (this.handleSuggestionMouse(event)) {
3177
3254
  this.render();
3178
3255
  return;
@@ -3593,6 +3670,19 @@ class App {
3593
3670
  this.prompt.cursor = 0;
3594
3671
  } else if (key === "end" || key === "ctrl-e") {
3595
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);
3596
3686
  } else if (key === "backspace") {
3597
3687
  const prompt = this.prompt;
3598
3688
  if (prompt.cursor > 0) {
@@ -3656,6 +3746,7 @@ class App {
3656
3746
 
3657
3747
  openPrompt(label, callback, options = {}) {
3658
3748
  this.prompt = new Prompt(label, callback, options);
3749
+ this._promptScrollX = 0;
3659
3750
  }
3660
3751
 
3661
3752
  openYNPrompt(label, callback, { onCancel = null } = {}) {
@@ -4536,9 +4627,53 @@ class App {
4536
4627
  this.message = toSpaces ? "Retabbed to spaces" : "Retabbed to tabs";
4537
4628
  break;
4538
4629
  }
4539
- case "eval":
4540
- 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
+ }
4541
4675
  break;
4676
+ }
4542
4677
  case "bind":
4543
4678
  case "unbind":
4544
4679
  this.message = `${cmd}: keybinding system not yet implemented`;
@@ -4642,6 +4777,7 @@ class App {
4642
4777
  const pluginCmd = this.context.plugins?.commands?.get(cmd);
4643
4778
  if (pluginCmd) {
4644
4779
  try {
4780
+ cmdArgs.raw = input;
4645
4781
  await pluginCmd(makePaneAdapter(this.buffer, this), cmdArgs);
4646
4782
  } catch (e) {
4647
4783
  this.message = String(e.message ?? e);
@@ -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.