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 +11 -0
- package/README.md +38 -2
- package/package.json +1 -1
- package/runtime/jsplugins/example/example.js +6 -6
- package/src/index.js +153 -10
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
|
@@ -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";
|
|
@@ -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}
|
|
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
|
-
|
|
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.
|
|
3555
|
+
await this.togglePromptMode("Command");
|
|
3485
3556
|
break;
|
|
3486
3557
|
case "shellmode":
|
|
3487
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
5683
|
+
buffer = new BufferModel({ path: pathOrUrl, text, command, encoding });
|
|
5541
5684
|
attachSyntax(buffer, context, urlPath, text);
|
|
5542
|
-
|
|
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);
|