bunmicro 0.9.30 → 1.0.0
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 +10 -0
- package/README.md +13 -0
- package/hlw.md +1 -0
- package/package.json +1 -1
- package/runtime/help/cdp.md +119 -0
- package/runtime/jsplugins/cdp/cdp-server.js +1161 -0
- package/runtime/jsplugins/cdp/cdp.js +192 -0
- package/src/index.js +24 -5
- package/src/platform/commands.js +1 -4
- package/src/plugins/js-bridge.js +5 -5
- package/tests/wv-client.js +96 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// No imports needed: `micro` is available as a global.
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
|
|
5
|
+
# JS Plugin Documentation
|
|
6
|
+
|
|
7
|
+
Available hooks (register with micro.on(hookName, fn)):
|
|
8
|
+
|
|
9
|
+
Lifecycle (no args):
|
|
10
|
+
"preinit" — before plugins load
|
|
11
|
+
"init" — main plugin setup, register commands/actions here
|
|
12
|
+
"postinit" — after all plugins loaded
|
|
13
|
+
|
|
14
|
+
Buffer events:
|
|
15
|
+
"onBufferOpen" (buffer) — buffer opened (raw BufferModel, not pane adapter)
|
|
16
|
+
"onBufferClose" (buffer) — buffer closed (raw BufferModel)
|
|
17
|
+
"onSetActive" (bp) — pane became active (tab switch, close, etc.)
|
|
18
|
+
"onSave" (bp) — buffer saved
|
|
19
|
+
|
|
20
|
+
Input events:
|
|
21
|
+
"onRune" (bp, ch) — printable character inserted (ch is the string)
|
|
22
|
+
|
|
23
|
+
Cancellable hooks (return false to cancel the action):
|
|
24
|
+
"preBackspace" (bp) — before backspace; return false to block
|
|
25
|
+
"preInsertNewline" (bp) — before Enter/newline; return false to block
|
|
26
|
+
|
|
27
|
+
bp is a pane adapter with:
|
|
28
|
+
bp.Buf.Line(n) bp.Buf.LinesNum() bp.Buf.FileType()
|
|
29
|
+
bp.Buf.Insert(loc, text) bp.Buf.Replace(s, e, text)
|
|
30
|
+
bp.Cursor.X bp.Cursor.Y bp.Cursor.Loc bp.Cursor.HasSelection()
|
|
31
|
+
bp.Save() bp.Backspace() bp.CursorLeft/Right() bp.InsertNewline()
|
|
32
|
+
|
|
33
|
+
Flat buffer helpers (all 1-based line numbers, omit → cursor line):
|
|
34
|
+
micro.getLine(n?) micro.putLine(text, n?) micro.delLine(n?)
|
|
35
|
+
micro.getLines(from?, to?) micro.getLinesCount()
|
|
36
|
+
micro.getAllText() — entire buffer as one string (lines joined by "\n")
|
|
37
|
+
micro.putAllText(text) — replace entire buffer content; pushes undo
|
|
38
|
+
micro.getSelection() micro.putSelection(text)
|
|
39
|
+
|
|
40
|
+
Other micro APIs:
|
|
41
|
+
micro.CurPane() — returns pane adapter for active pane
|
|
42
|
+
micro.MakeCommand(name, fn) — register Ctrl+E command; fn(bp, args[])
|
|
43
|
+
args.raw = full original input string (bypass shellSplit)
|
|
44
|
+
e.g. for command "js 1+1": args.raw = "js 1+1", args.raw.slice(3) = "1+1"
|
|
45
|
+
micro.RegisterAction(name, fn) — register bindable action
|
|
46
|
+
micro.TermMessage(msg) — show msg in editor status row
|
|
47
|
+
micro.alert(msg) — suspend editor, print msg, wait for Enter
|
|
48
|
+
micro.Log(...args) — console.log passthrough
|
|
49
|
+
micro.GetOption(name) micro.SetOption(name, value)
|
|
50
|
+
micro.cmd.save() — call any editor command via proxy
|
|
51
|
+
micro.action.CursorUp() — run any registered action via proxy
|
|
52
|
+
micro.shell.CMD(...args) — run CMD interactively (same as Ctrl-B); async
|
|
53
|
+
e.g. await micro.shell.ls('-l')
|
|
54
|
+
await micro.shell.git('diff', '--stat')
|
|
55
|
+
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
micro.on("init", () => {
|
|
59
|
+
// Register a custom Ctrl+E command
|
|
60
|
+
micro.MakeCommand("cdp", async (bp, args) =>
|
|
61
|
+
{
|
|
62
|
+
const addrFlag = args.find(a => a.startsWith("--address="))?.slice("--address=".length);
|
|
63
|
+
const isPublic = args.includes("--public");
|
|
64
|
+
const port = parseInt(args.find(a => /^\d+$/.test(a))) || parseInt(Bun.env.CDP_PORT) || 9222;
|
|
65
|
+
const hostname = addrFlag ?? (isPublic ? "0.0.0.0" : "127.0.0.1");
|
|
66
|
+
const path = bp?.Buf?.Path || "(no path)";
|
|
67
|
+
|
|
68
|
+
if(!micro.cdpContext)
|
|
69
|
+
{
|
|
70
|
+
micro.cdpContext={
|
|
71
|
+
title(){
|
|
72
|
+
return path;
|
|
73
|
+
},
|
|
74
|
+
async evaluate(txt){
|
|
75
|
+
return await eval(txt);
|
|
76
|
+
},
|
|
77
|
+
async navigate(url){
|
|
78
|
+
await micro.cmd.open('-f', url);
|
|
79
|
+
},
|
|
80
|
+
async click(x,y,opt){
|
|
81
|
+
x=x||1 ; y=y||1 ;
|
|
82
|
+
await micro.cmd.goto(y+':'+x);
|
|
83
|
+
},
|
|
84
|
+
async scroll(dx,dy){
|
|
85
|
+
const pane = micro.CurPane();
|
|
86
|
+
if (!pane) return;
|
|
87
|
+
|
|
88
|
+
dx = toInteger(dx);
|
|
89
|
+
dy = toInteger(dy);
|
|
90
|
+
|
|
91
|
+
const line = Math.max(1, pane.Cursor.Y + dy + 1);
|
|
92
|
+
const column = Math.max(1, pane.Cursor.X + dx + 1);
|
|
93
|
+
await micro.cmd.goto(`${line}:${column}`);
|
|
94
|
+
},
|
|
95
|
+
async scrollTo(selector){
|
|
96
|
+
const pattern = selectorToSearchPattern(selector);
|
|
97
|
+
await micro.cmd.find(pattern);
|
|
98
|
+
},
|
|
99
|
+
goBack(){
|
|
100
|
+
micro.action.PrevTab();
|
|
101
|
+
},
|
|
102
|
+
goForward(){
|
|
103
|
+
micro.action.NextTab();
|
|
104
|
+
},
|
|
105
|
+
async type(text){
|
|
106
|
+
const bp = micro.CurPane();
|
|
107
|
+
if (!bp) return;
|
|
108
|
+
bp.Insert(text);
|
|
109
|
+
},
|
|
110
|
+
async press(key, options){
|
|
111
|
+
const bp = micro.CurPane();
|
|
112
|
+
if (!bp) return;
|
|
113
|
+
|
|
114
|
+
// modifiers bitmask: Alt=1, Ctrl=2, Meta=4, Shift=8
|
|
115
|
+
const mod = options?.modifiers ?? 0;
|
|
116
|
+
const ctrl = !!(mod & 2);
|
|
117
|
+
const shift = !!(mod & 8);
|
|
118
|
+
|
|
119
|
+
if (ctrl) {
|
|
120
|
+
const ctrlMap = {
|
|
121
|
+
a: () => micro.action.SelectAll(),
|
|
122
|
+
c: () => micro.action.Copy(),
|
|
123
|
+
x: () => micro.action.Cut(),
|
|
124
|
+
v: () => micro.action.Paste(),
|
|
125
|
+
z: () => micro.action.Undo(),
|
|
126
|
+
y: () => micro.action.Redo(),
|
|
127
|
+
s: () => micro.action.Save(),
|
|
128
|
+
};
|
|
129
|
+
const h = ctrlMap[key.toLowerCase()];
|
|
130
|
+
if (h) await h();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const arrowAction = shift
|
|
135
|
+
? { ArrowUp: 'SelectUp', ArrowDown: 'SelectDown', ArrowLeft: 'SelectLeft', ArrowRight: 'SelectRight' }
|
|
136
|
+
: { ArrowUp: 'CursorUp', ArrowDown: 'CursorDown', ArrowLeft: 'CursorLeft', ArrowRight: 'CursorRight' };
|
|
137
|
+
|
|
138
|
+
const keyMap = {
|
|
139
|
+
...arrowAction,
|
|
140
|
+
Enter: () => micro.action.InsertNewline(),
|
|
141
|
+
Backspace: () => micro.action.Backspace(),
|
|
142
|
+
Delete: () => micro.action.Delete(),
|
|
143
|
+
Tab: () => micro.action.InsertTab(),
|
|
144
|
+
Escape: () => micro.action.Escape(),
|
|
145
|
+
Home: () => shift ? micro.action.SelectToStartOfLine() : micro.action.StartOfLine(),
|
|
146
|
+
End: () => shift ? micro.action.SelectToEndOfLine() : micro.action.EndOfLine(),
|
|
147
|
+
PageUp: () => shift ? micro.action.SelectPageUp() : micro.action.CursorPageUp(),
|
|
148
|
+
PageDown: () => shift ? micro.action.SelectPageDown() : micro.action.CursorPageDown(),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const entry = keyMap[key];
|
|
152
|
+
if (typeof entry === 'string') {
|
|
153
|
+
await micro.action[entry]();
|
|
154
|
+
} else if (typeof entry === 'function') {
|
|
155
|
+
await entry();
|
|
156
|
+
} else if (key.length === 1) {
|
|
157
|
+
bp.Insert(key);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let {CdpServer}=await import('./cdp-server.js');
|
|
163
|
+
|
|
164
|
+
micro.cdpPort = port;
|
|
165
|
+
CdpServer
|
|
166
|
+
.create(micro.cdpContext)
|
|
167
|
+
.listen(port, hostname);
|
|
168
|
+
|
|
169
|
+
const addr = isPublic ? `0.0.0.0:${port}` : `127.0.0.1:${port}`;
|
|
170
|
+
micro.TermMessage(`CDP@${addr} server running 伺服器啟動了`)
|
|
171
|
+
|
|
172
|
+
//await micro.alert(CdpServer)
|
|
173
|
+
} // server not running
|
|
174
|
+
else
|
|
175
|
+
{
|
|
176
|
+
micro.TermMessage(`CDP@${micro.cdpPort} already running a server 已有伺服器啟動`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
function selectorToSearchPattern(selector) {
|
|
185
|
+
const value = String(selector ?? "");
|
|
186
|
+
return value.startsWith("#") ? value.slice(1) : value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toInteger(value) {
|
|
190
|
+
const number = Number(value);
|
|
191
|
+
return Number.isFinite(number) ? Math.trunc(number) : 0;
|
|
192
|
+
}
|
package/src/index.js
CHANGED
|
@@ -516,6 +516,8 @@ function parseArgs(argv) {
|
|
|
516
516
|
debug: false,
|
|
517
517
|
profile: false,
|
|
518
518
|
plugin: "",
|
|
519
|
+
cdpPort: 0,
|
|
520
|
+
cdpAddress: "",
|
|
519
521
|
settings: new Map(),
|
|
520
522
|
};
|
|
521
523
|
const files = [];
|
|
@@ -532,7 +534,15 @@ function parseArgs(argv) {
|
|
|
532
534
|
else if (arg === "-profile") flags.profile = true;
|
|
533
535
|
else if (arg === "-config-dir") flags.configDir = argv[++i] ?? "";
|
|
534
536
|
else if (arg === "-plugin") flags.plugin = argv[++i] ?? "";
|
|
535
|
-
else if (arg.startsWith("-")
|
|
537
|
+
else if (arg.startsWith("--remote-debugging-port=")) {
|
|
538
|
+
flags.cdpPort = parseInt(arg.slice("--remote-debugging-port=".length)) || 9222;
|
|
539
|
+
} else if (arg === "--remote-debugging-port") {
|
|
540
|
+
flags.cdpPort = parseInt(argv[++i]) || 9222;
|
|
541
|
+
} else if (arg.startsWith("--remote-debugging-address=")) {
|
|
542
|
+
flags.cdpAddress = arg.slice("--remote-debugging-address=".length);
|
|
543
|
+
} else if (arg === "--remote-debugging-address") {
|
|
544
|
+
flags.cdpAddress = argv[++i] ?? "";
|
|
545
|
+
} else if (arg.startsWith("-") && arg.length > 1 && i + 1 < argv.length) {
|
|
536
546
|
flags.settings.set(arg.slice(1), argv[++i]);
|
|
537
547
|
} else {
|
|
538
548
|
files.push(arg);
|
|
@@ -576,7 +586,11 @@ function usage() {
|
|
|
576
586
|
" Show version+backend info & exit",
|
|
577
587
|
"--docs, --readme",
|
|
578
588
|
` Show ${pkg.name}'s README.md & exit`,
|
|
579
|
-
|
|
589
|
+
"",
|
|
590
|
+
"--remote-debugging-port=PORT",
|
|
591
|
+
" Start CDP (Chrome DevTools Protocol) server on PORT at launch",
|
|
592
|
+
"--remote-debugging-address=ADDRESS",
|
|
593
|
+
" Bind CDP server to ADDRESS (default: 127.0.0.1); use 0.0.0.0 for all interfaces",
|
|
580
594
|
|
|
581
595
|
].join("\n");
|
|
582
596
|
}
|
|
@@ -5811,7 +5825,7 @@ function detectTtsCmd() {
|
|
|
5811
5825
|
Bun.env.TTS_LANG = lang ;
|
|
5812
5826
|
|
|
5813
5827
|
if (platform === "android") {
|
|
5814
|
-
if (
|
|
5828
|
+
if (Bun.which("termux-tts-speak"))
|
|
5815
5829
|
return { cmd: ["termux-tts-speak", "-p", String(pitch), "-r", String(speed)], via: "arg" };
|
|
5816
5830
|
}
|
|
5817
5831
|
|
|
@@ -5832,7 +5846,7 @@ function detectTtsCmd() {
|
|
|
5832
5846
|
const pitchPct = Math.round((pitch - 1) * 100);
|
|
5833
5847
|
const pitchAttr = (pitchPct >= 0 ? "+" : "") + pitchPct + "%";
|
|
5834
5848
|
for (const shell of ["pwsh.exe", "powershell.exe"]) {
|
|
5835
|
-
if (
|
|
5849
|
+
if (Bun.which(shell)) {
|
|
5836
5850
|
const psCmd =
|
|
5837
5851
|
"Add-Type -AssemblyName System.Speech; " +
|
|
5838
5852
|
`$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $s.Rate = ${rate}; ` +
|
|
@@ -5847,7 +5861,7 @@ function detectTtsCmd() {
|
|
|
5847
5861
|
// Linux / Android fallback: espeak-ng / espeak
|
|
5848
5862
|
// Speed: -s <wpm> (175 = normal), Pitch: -p <n> (0-99, 50 = normal)
|
|
5849
5863
|
for (const bin of ["espeak-ng", "espeak"]) {
|
|
5850
|
-
if (
|
|
5864
|
+
if (Bun.which(bin)) {
|
|
5851
5865
|
const spd = Math.round(175 * speed);
|
|
5852
5866
|
const pit = Math.max(0, Math.min(99, Math.round(50 * pitch)));
|
|
5853
5867
|
return { cmd: [bin, '-s', spd, '-p', pit], via: "arg" };
|
|
@@ -6725,6 +6739,11 @@ async function main() {
|
|
|
6725
6739
|
for (const buffer of buffers) await plugins.run("onBufferOpen", buffer);
|
|
6726
6740
|
}
|
|
6727
6741
|
for (const buffer of buffers) await jsPlugins.run("onBufferOpen", buffer);
|
|
6742
|
+
if (flags.cdpPort) {
|
|
6743
|
+
const cdpArgs = [flags.cdpPort];
|
|
6744
|
+
if (flags.cdpAddress) cdpArgs.push(`--address=${flags.cdpAddress}`);
|
|
6745
|
+
await app.handleCommand(`cdp ${cdpArgs.join(" ")}`);
|
|
6746
|
+
}
|
|
6728
6747
|
await app.start();
|
|
6729
6748
|
}
|
|
6730
6749
|
|
package/src/platform/commands.js
CHANGED
|
@@ -118,10 +118,7 @@ export async function runBytes(command, options = {}) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export function hasCommand(name) {
|
|
121
|
-
|
|
122
|
-
return runSync(["where.exe", name], { stdout: "ignore", stderr: "ignore" }).ok;
|
|
123
|
-
}
|
|
124
|
-
return runSync(["sh", "-c", `command -v ${shellQuote(name)}`], { stdout: "ignore", stderr: "ignore" }).ok;
|
|
121
|
+
return Bun.which(name);
|
|
125
122
|
}
|
|
126
123
|
|
|
127
124
|
export function firstCommand(names) {
|
package/src/plugins/js-bridge.js
CHANGED
|
@@ -455,10 +455,7 @@ function registerBuiltinActions() {
|
|
|
455
455
|
if (pane?.buffer?.modified) try { await pane.buffer.save?.(); } catch {}
|
|
456
456
|
await app.stop?.(0);
|
|
457
457
|
});
|
|
458
|
-
reg("Escape", (app) =>
|
|
459
|
-
if (app.pane) app.pane.selection = null;
|
|
460
|
-
if (app.buffer) app.buffer.searchPattern = "";
|
|
461
|
-
});
|
|
458
|
+
reg("Escape", (app) => app._dispatchInput?.(new TextEncoder().encode("\x1b")));
|
|
462
459
|
|
|
463
460
|
// Toggle settings
|
|
464
461
|
reg("ToggleDiffGutter", (app) => {
|
|
@@ -768,7 +765,9 @@ export function buildMicroGlobal(jsManager) {
|
|
|
768
765
|
return async (...args) => {
|
|
769
766
|
const app = getApp();
|
|
770
767
|
if (!app) return;
|
|
771
|
-
|
|
768
|
+
const result = await app.handleCommand(buildCmdString(name, args));
|
|
769
|
+
app.render?.();
|
|
770
|
+
return result;
|
|
772
771
|
};
|
|
773
772
|
},
|
|
774
773
|
}),
|
|
@@ -872,6 +871,7 @@ function _makePaneAPI(buffer, app) {
|
|
|
872
871
|
EndOfLine: () => buffer.moveEnd(),
|
|
873
872
|
InsertNewline: () => buffer.newline(),
|
|
874
873
|
InsertTab: () => buffer.insertTab(),
|
|
874
|
+
Insert: (text) => { buffer.pushUndo?.(); buffer.insert(text); app?.render?.(); },
|
|
875
875
|
HandleCommand: (cmd) => app?.handleCommand?.(cmd),
|
|
876
876
|
|
|
877
877
|
// Run a named action on this pane
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const cdpUrl = Bun.argv[2] ?? "ws://127.0.0.1:9222";
|
|
2
|
+
|
|
3
|
+
const view = new Bun.WebView({
|
|
4
|
+
backend: {
|
|
5
|
+
type: "chrome",
|
|
6
|
+
url: cdpUrl,
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
view.onNavigated = (url, title) => {
|
|
11
|
+
console.log(`[navigated] ${title || "(no title)"} — ${url.slice(0, 80)}`);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const delay = Bun.sleep
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
|
|
18
|
+
await delay(2000);
|
|
19
|
+
|
|
20
|
+
// 1. navigate to first page
|
|
21
|
+
console.log("\n--- navigate: https://example.com ---");
|
|
22
|
+
await view.navigate("https://example.com");
|
|
23
|
+
await delay(2000);
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// 2. navigate to second page
|
|
27
|
+
console.log("\n--- evaluate: micro.cmd.tab() ---");
|
|
28
|
+
await view.evaluate("micro.cmd.tab()");
|
|
29
|
+
await delay(500);
|
|
30
|
+
|
|
31
|
+
console.log("\n--- navigate: github bunmicro hlw.md ---");
|
|
32
|
+
await view.navigate("https://raw.githubusercontent.com/jjtseng93/bunmicro/refs/heads/main/hlw.md");
|
|
33
|
+
await delay(2000);
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// 3. navigate to third page
|
|
37
|
+
console.log("\n--- evaluate: micro.cmd.tab() ---");
|
|
38
|
+
await view.evaluate("micro.cmd.tab()");
|
|
39
|
+
await delay(500);
|
|
40
|
+
|
|
41
|
+
console.log("\n--- navigate: https://bun.sh/docs dns ---");
|
|
42
|
+
await view.navigate("https://bun.com/docs/runtime/networking/dns");
|
|
43
|
+
await delay(2000);
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
// 4. go back twice
|
|
47
|
+
console.log("\n--- goBack (dns → hlw) ---");
|
|
48
|
+
await view.goBack();
|
|
49
|
+
await delay(2000);
|
|
50
|
+
|
|
51
|
+
console.log("\n--- goBack (hlw → example.com) ---");
|
|
52
|
+
await view.goBack();
|
|
53
|
+
await delay(2000);
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// 5. go forward
|
|
57
|
+
console.log("\n--- goForward (example.com → hlw) ---");
|
|
58
|
+
await view.goForward();
|
|
59
|
+
await delay(2000);
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
// 6. type before # hello & click
|
|
63
|
+
console.log("\n--- click #hello to focus ---");
|
|
64
|
+
await view.scrollTo("#hello");
|
|
65
|
+
await view.evaluate('micro.action.StartOfLine()');
|
|
66
|
+
await delay(2000);
|
|
67
|
+
|
|
68
|
+
console.log("\n--- type: '# Bun is great' ---");
|
|
69
|
+
await view.type("# Bun is great");
|
|
70
|
+
await delay(2000);
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
console.log("\n--- press Enter ---");
|
|
74
|
+
await view.press("Enter");
|
|
75
|
+
await delay(2000);
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
console.log("\n--- Click 3,3 ---");
|
|
79
|
+
await view.click(3,3); //點33
|
|
80
|
+
await delay(2000);
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
// 7. resize
|
|
84
|
+
console.log("\n--- resize 1280x720 ---");
|
|
85
|
+
await view.resize(1280, 720);
|
|
86
|
+
await delay(2000);
|
|
87
|
+
|
|
88
|
+
// 8. go forward to dns
|
|
89
|
+
console.log("\n--- goForward (hlw → dns) ---");
|
|
90
|
+
await view.goForward();
|
|
91
|
+
await delay(2000);
|
|
92
|
+
|
|
93
|
+
console.log("\nAll done.");
|
|
94
|
+
} finally {
|
|
95
|
+
view.close();
|
|
96
|
+
}
|