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 +6 -0
- package/README.md +38 -2
- package/package.json +1 -1
- package/runtime/jsplugins/example/example.js +6 -6
- package/src/index.js +122 -4
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
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|