bunmicro 1.0.0 → 1.0.2

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.2] - 2026-06-24
4
+ - Added startup profiling flags
5
+ * -profile / --profile
6
+ - Added docs flags
7
+ * --changelog alongside --docs / --readme
8
+ - Disabled OSC 52 probing
9
+ * clipboard now treats OSC 52 as available without detection
10
+ - Added hex3 aliases
11
+ * --xxd / --hexdump => --cat -encoding hex3
12
+ * --hex3 => -encoding hex3
13
+ - Fixed stdin encoding handling
14
+ * hex3 now applies when reading from stdin
15
+ - Parallelized startup loading
16
+ * Lua, JS, and buffer init now run in parallel with safe degradation
17
+ * startup performance improves a lot
18
+
3
19
  ## [1.0.0] - 2026-06-22
4
20
  - Changed command -v to Bun.which
5
21
  * for performance improvement
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunmicro",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Bun JavaScript rewrite of the micro editor originally in Golang",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -40,15 +40,19 @@ export class SyntaxDefinition {
40
40
 
41
41
  export async function loadSyntaxDefinitions(runtime) {
42
42
  const headers = new Map();
43
- for (const file of runtime.list(4)) {
44
- headers.set(file.name, parseHeaderFile(await file.text()));
45
- }
43
+ const headerPromises = runtime.list(4).map(async (file) => {
44
+ try {
45
+ const text = await file.text();
46
+ headers.set(file.name, parseHeaderFile(text));
47
+ } catch {}
48
+ });
49
+ await Promise.allSettled(headerPromises);
46
50
 
47
- const definitions = [];
48
- for (const file of runtime.list(1)) {
51
+ const defPromises = runtime.list(1).map(async (file) => {
49
52
  let text = "";
50
53
  let activeFile = file;
51
54
  let source = null;
55
+ let usedFallback = false;
52
56
  try {
53
57
  text = await file.text();
54
58
  source = Bun.YAML.parse(text);
@@ -59,21 +63,26 @@ export async function loadSyntaxDefinitions(runtime) {
59
63
  text = await fallback.text();
60
64
  source = Bun.YAML.parse(text);
61
65
  activeFile = fallback;
66
+ usedFallback = true;
62
67
  console.error("Failed to load user syntax yaml, using built-in fallback:", file.name);
63
68
  } catch {}
64
69
  }
65
70
  }
66
-
71
+ const header = headers.get(activeFile.name) ?? (source ? parseHeaderYaml(source) : parseHeaderTextFallback(text, activeFile.name));
67
72
  if (!source) {
68
73
  console.error("Failed to load syntax yaml:", file.name);
69
74
  console.error(" Will not highlight this kind of file");
70
75
  console.error(" @ loadSyntaxDefinitions ");
76
+ } else if (usedFallback) {
77
+ // keep the fallback path visible in logs, but do not fail the load
71
78
  }
72
-
73
- const header = headers.get(activeFile.name) ?? (source ? parseHeaderYaml(source) : parseHeaderTextFallback(text, activeFile.name));
74
- definitions.push(new SyntaxDefinition(header, source ?? { rules: [] }, text));
75
- }
76
- return definitions;
79
+ return new SyntaxDefinition(header, source ?? { rules: [] }, text);
80
+ });
81
+
82
+ const definitions = await Promise.allSettled(defPromises);
83
+ return definitions
84
+ .filter((result) => result.status === "fulfilled")
85
+ .map((result) => result.value);
77
86
  }
78
87
 
79
88
  export function detectSyntax(definitions, { path = "", firstLine = "", lines = [] } = {}) {
package/src/index.js CHANGED
@@ -1,5 +1,94 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ const jsStart = globalThis.Bun ? Bun.nanoseconds() : Date.now() * 1e6;
4
+ const checkpoints = [
5
+ { name: "Bun Engine Boot", time: 0 },
6
+ { name: "JS Load & Module Imports", time: jsStart }
7
+ ];
8
+ function addCheckpoint(name) {
9
+ checkpoints.push({ name, time: globalThis.Bun ? Bun.nanoseconds() : Date.now() * 1e6 });
10
+ }
11
+
12
+ let parallelTimings = null;
13
+
14
+ function printProfileReport() {
15
+ console.log("\x1b[1m\x1b[36m=== Bunmicro Startup Performance Profile ===\x1b[0m\n");
16
+
17
+ let totalMs = 0;
18
+ const rows = [];
19
+
20
+ for (let i = 0; i < checkpoints.length - 1; i++) {
21
+ const current = checkpoints[i];
22
+ const next = checkpoints[i + 1];
23
+ const durationNs = next.time - current.time;
24
+ const durationMs = durationNs / 1e6;
25
+ totalMs += durationMs;
26
+
27
+ rows.push({
28
+ phase: current.name,
29
+ durationMs: durationMs,
30
+ cumulativeMs: totalMs
31
+ });
32
+ }
33
+
34
+ const colWidths = { phase: 32, duration: 15, cumulative: 15 };
35
+
36
+ const header =
37
+ "Phase".padEnd(colWidths.phase) + " | " +
38
+ "Duration (ms)".padStart(colWidths.duration) + " | " +
39
+ "Cumulative (ms)".padStart(colWidths.cumulative);
40
+
41
+ const separator =
42
+ "-".repeat(colWidths.phase) + "-+-" +
43
+ "-".repeat(colWidths.duration) + "-+-" +
44
+ "-".repeat(colWidths.cumulative);
45
+
46
+ console.log(header);
47
+ console.log(separator);
48
+
49
+ for (const row of rows) {
50
+ const phaseStr = row.phase.padEnd(colWidths.phase);
51
+ const durStr = row.durationMs.toFixed(3).padStart(colWidths.duration);
52
+ const cumStr = row.cumulativeMs.toFixed(3).padStart(colWidths.cumulative);
53
+
54
+ let color = "";
55
+ if (row.durationMs > 50) {
56
+ color = "\x1b[31m"; // Red
57
+ } else if (row.durationMs > 10) {
58
+ color = "\x1b[33m"; // Yellow
59
+ }
60
+
61
+ const reset = color ? "\x1b[0m" : "";
62
+ console.log(`${color}${phaseStr}${reset} | ${color}${durStr}${reset} | ${cumStr}`);
63
+ }
64
+
65
+ console.log(separator);
66
+
67
+ if (parallelTimings) {
68
+ console.log("\x1b[1mParallel Tasks Breakdown:\x1b[0m");
69
+ console.log(` ├── Lua Plugins & Hooks : ${parallelTimings.lua.toFixed(3).padStart(8)} ms`);
70
+ console.log(` ├── JS Plugins Load : ${parallelTimings.js.toFixed(3).padStart(8)} ms`);
71
+ console.log(` └── Buffer & History : ${parallelTimings.buffers.toFixed(3).padStart(8)} ms`);
72
+ console.log(separator);
73
+ }
74
+
75
+ console.log(`\x1b[1mTotal Startup Time: ${totalMs.toFixed(3)} ms\x1b[0m\n`);
76
+
77
+ const slowest = [...rows].sort((a, b) => b.durationMs - a.durationMs)[0];
78
+ if (slowest) {
79
+ console.log(`\x1b[1mSlowest Phase:\x1b[0m ${slowest.phase} (${slowest.durationMs.toFixed(3)} ms)`);
80
+ if (slowest.phase.includes("Clipboard")) {
81
+ console.log("\x1b[32mTip: Clipboard probing can be slow. Setting 'clipboard' option to 'terminal' or a specific tool can bypass auto-detection.\x1b[0m");
82
+ } else if (slowest.phase.includes("Plugin")) {
83
+ console.log("\x1b[32mTip: Disable unnecessary plugins to speed up startup.\x1b[0m");
84
+ } else if (slowest.phase.includes("Syntax")) {
85
+ console.log("\x1b[32mTip: Syntax loading parses many YAML/JSON files. You can pre-compile or bundle syntax definitions to speed this up.\x1b[0m");
86
+ } else if (slowest.phase.includes("JS Load")) {
87
+ console.log("\x1b[32mTip: JS load time includes loading packages like wasmoon, which might take time due to file system lookups.\x1b[0m");
88
+ }
89
+ }
90
+ }
91
+
3
92
  import child_process from "node:child_process"
4
93
  import { accessSync, constants, existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
5
94
  import { mkdir } from "node:fs/promises";
@@ -512,6 +601,7 @@ function parseArgs(argv) {
512
601
  clean: false,
513
602
  cat: false,
514
603
  docs: false,
604
+ changelog: false,
515
605
  configDir: "",
516
606
  debug: false,
517
607
  profile: false,
@@ -529,9 +619,17 @@ function parseArgs(argv) {
529
619
  else if (arg === "-help" || arg === "--help" || arg === "-h") flags.help = true;
530
620
  else if (arg === "-clean") flags.clean = true;
531
621
  else if (arg === "--cat" || arg === "-cat" || arg === "--ccat" || arg === "-ccat" || arg === "--bat" || arg === "-bat" || arg === "--glow" || arg === "-glow") flags.cat = true;
622
+ else if (arg === "--xxd" || arg === "--hexdump") {
623
+ flags.cat = true;
624
+ flags.settings.set("encoding", "hex3");
625
+ }
626
+ else if (arg === "--hex3") {
627
+ flags.settings.set("encoding", "hex3");
628
+ }
532
629
  else if (arg === "--docs" || arg === "--readme") flags.docs = true;
630
+ else if (arg === "--changelog") flags.changelog = true;
533
631
  else if (arg === "-debug") flags.debug = true;
534
- else if (arg === "-profile") flags.profile = true;
632
+ else if (arg === "-profile" || arg === "--profile") flags.profile = true;
535
633
  else if (arg === "-config-dir") flags.configDir = argv[++i] ?? "";
536
634
  else if (arg === "-plugin") flags.plugin = argv[++i] ?? "";
537
635
  else if (arg.startsWith("--remote-debugging-port=")) {
@@ -578,14 +676,22 @@ function usage() {
578
676
  " Set an option for this session",
579
677
  "-options",
580
678
  " Show option help and exit\n",
679
+ "-profile, --profile",
680
+ " Print startup performance profile and exit\n",
581
681
  "--cat, --ccat, --bat, --glow",
582
682
  " Syntax-highlight file(s) and write to stdout, then exit (.md uses Bun.markdown.ansi)\n",
683
+ "--xxd, --hexdump",
684
+ " Hex3 dump file(s) and write to stdout (same as --cat -encoding hex3)\n",
685
+ "--hex3",
686
+ " Set -encoding hex3 for this session\n",
583
687
  "-help, -h, --help",
584
688
  " Show this help & exit",
585
689
  "-version, -V, --version",
586
690
  " Show version+backend info & exit",
587
691
  "--docs, --readme",
588
692
  ` Show ${pkg.name}'s README.md & exit`,
693
+ "--changelog",
694
+ " Show CHANGELOG.md & exit",
589
695
  "",
590
696
  "--remote-debugging-port=PORT",
591
697
  " Start CDP (Chrome DevTools Protocol) server on PORT at launch",
@@ -6573,8 +6679,16 @@ async function loadBuffers(files, command) {
6573
6679
  } else if (!process.stdin.isTTY) {
6574
6680
  const chunks = [];
6575
6681
  for await (const chunk of process.stdin) chunks.push(chunk);
6576
- const stdinText = Buffer.concat(chunks).toString("utf8");
6577
- const stdinBuf = new BufferModel({ text: stdinText, type: process.stdout.isTTY ? "default" : "stdout", command });
6682
+ const context = loadBuffers.context ?? {};
6683
+ const encoding = context.config?.globalSettings?.encoding ?? DEFAULT_SETTINGS.encoding;
6684
+ const decoded = decodeTextBytesWithEncoding(Buffer.concat(chunks), encoding);
6685
+ const stdinText = decoded.text;
6686
+ const stdinBuf = new BufferModel({
6687
+ text: stdinText,
6688
+ type: process.stdout.isTTY ? "default" : "stdout",
6689
+ command,
6690
+ encoding: decoded.encoding,
6691
+ });
6578
6692
  if (loadBuffers.context) attachSyntax(stdinBuf, loadBuffers.context, "", stdinText);
6579
6693
  buffers.push(stdinBuf);
6580
6694
  } else {
@@ -6593,7 +6707,13 @@ async function printReadmeDocs() {
6593
6707
  process.stdout.write(Bun.markdown.ansi(readme, { hyperlinks: true }));
6594
6708
  }
6595
6709
 
6710
+ async function printChangelogDocs() {
6711
+ const changelog = await Bun.file(join(REPO_ROOT, "CHANGELOG.md")).text();
6712
+ process.stdout.write(Bun.markdown.ansi(changelog, { hyperlinks: true }));
6713
+ }
6714
+
6596
6715
  async function main() {
6716
+ addCheckpoint("Argument Parsing");
6597
6717
  const { flags, files: rawFiles } = parseArgs(process.argv.slice(2));
6598
6718
  if (flags.help) {
6599
6719
  console.log(usage());
@@ -6628,6 +6748,10 @@ async function main() {
6628
6748
  await printReadmeDocs();
6629
6749
  return;
6630
6750
  }
6751
+ if (flags.changelog) {
6752
+ await printChangelogDocs();
6753
+ return;
6754
+ }
6631
6755
  if (flags.options) {
6632
6756
  for (const [key, value] of Object.entries(defaultAllSettings()).sort(([a], [b]) => a.localeCompare(b))) {
6633
6757
  console.log(`-${key} value`);
@@ -6635,13 +6759,16 @@ async function main() {
6635
6759
  }
6636
6760
  return;
6637
6761
  }
6762
+ addCheckpoint("Config Initialization");
6638
6763
  const config = await new Config({ configDir: flags.configDir }).init();
6639
6764
  config.applyCliSettings(flags.settings);
6640
6765
  syncEditorSettings(config);
6641
6766
 
6767
+ addCheckpoint("Runtime Registry Init");
6642
6768
  const runtime = new RuntimeRegistry({ repoRoot: REPO_ROOT, configDir: config.configDir });
6643
6769
  await runtime.init({ user: true });
6644
6770
 
6771
+ addCheckpoint("Colorscheme & Syntax Load");
6645
6772
  const colorscheme = await new Colorscheme(runtime).load(config.getGlobalOption("colorscheme") || "default");
6646
6773
  const syntaxDefinitions = await loadSyntaxDefinitions(runtime);
6647
6774
 
@@ -6650,6 +6777,7 @@ async function main() {
6650
6777
  return;
6651
6778
  }
6652
6779
 
6780
+ addCheckpoint("Lua Plugin Manager Init");
6653
6781
  const plugins = new PluginManager({ config, runtime, repoRoot: REPO_ROOT });
6654
6782
  await plugins.init();
6655
6783
 
@@ -6688,50 +6816,114 @@ async function main() {
6688
6816
  return;
6689
6817
  }
6690
6818
 
6691
- const pluginErr = await plugins.loadAll();
6692
- if (pluginErr) console.error(`Plugin runtime disabled: ${pluginErr.message}`);
6693
- if (!pluginErr) {
6694
- await plugins.run("preinit");
6695
- await plugins.run("init");
6696
- await plugins.run("postinit");
6697
- }
6698
-
6699
- // ── JS plugin system ──────────────────────────────────────────────────────
6700
- const jsPlugins = new JsPluginManager();
6701
6819
  const { files, command } = parseInput(rawFiles);
6820
+ const jsPlugins = new JsPluginManager();
6702
6821
  const context = { colorscheme, syntaxDefinitions, plugins, config, runtime, jsPlugins };
6703
6822
  jsPlugins.setContext(context);
6704
6823
  buildMicroGlobal(jsPlugins); // sets globalThis.micro
6705
6824
 
6706
- const jsDirs = [
6707
- { dir: join(REPO_ROOT, "runtime", "jsplugins"), builtin: true },
6708
- { dir: join(config.configDir, "jsplug"), builtin: false },
6709
- ];
6710
- await jsPlugins.loadFrom(jsDirs);
6711
- // ─────────────────────────────────────────────────────────────────────────
6825
+ addCheckpoint("Parallel Initialization Start");
6826
+
6827
+ const luaPromise = (async () => {
6828
+ // return 0;
6829
+ const start = Bun.nanoseconds();
6830
+ const pluginErr = await plugins.loadAll();
6831
+ if (pluginErr) console.error(`Plugin runtime disabled: ${pluginErr.message}`);
6832
+ if (!pluginErr) {
6833
+ await plugins.run("preinit");
6834
+ await plugins.run("init");
6835
+ await plugins.run("postinit");
6836
+ }
6837
+ const end = Bun.nanoseconds();
6838
+ return { pluginErr, duration: end - start };
6839
+ })();
6840
+
6841
+ const jsPromise = (async () => {
6842
+ const start = Bun.nanoseconds();
6843
+ const jsDirs = [
6844
+ { dir: join(REPO_ROOT, "runtime", "jsplugins"), builtin: true },
6845
+ { dir: join(config.configDir, "jsplug"), builtin: false },
6846
+ ];
6847
+ await jsPlugins.loadFrom(jsDirs);
6848
+ const end = Bun.nanoseconds();
6849
+ return { duration: end - start };
6850
+ })();
6851
+
6852
+ const buffersPromise = (async () => {
6853
+ const start = Bun.nanoseconds();
6854
+ let cursorStates = {};
6855
+ if (DEFAULT_SETTINGS.savecursor) {
6856
+ cursorStates = await loadCursorStates(config.configDir);
6857
+ }
6858
+ // Mix in context properties needed for buffer loading:
6859
+ context.cursorStates = cursorStates;
6860
+ context._openBuffers = new Map();
6861
+ context._termPrompt = process.stdout.isTTY ? termPromptLine : null;
6862
+
6863
+ loadBuffers.context = context;
6864
+ const buffers = await loadBuffers(files.map((file) =>
6865
+ isHttpUrl(file) ? file : resolve(file)
6866
+ ), command);
6867
+
6868
+ let historyPromise = Promise.resolve();
6869
+ if (config.getGlobalOption("savehistory") !== false) {
6870
+ historyPromise = loadHistory(config.configDir);
6871
+ }
6872
+ await historyPromise;
6873
+ const end = Bun.nanoseconds();
6874
+ return { buffers, duration: end - start };
6875
+ })();
6876
+
6877
+ const [luaSettled, jsSettled, buffersSettled] = await Promise.allSettled([
6878
+ luaPromise,
6879
+ jsPromise,
6880
+ buffersPromise
6881
+ ]);
6882
+
6883
+ const luaResult = luaSettled.status === "fulfilled"
6884
+ ? luaSettled.value
6885
+ : { pluginErr: luaSettled.reason, duration: 0 };
6886
+ if (luaSettled.status === "rejected") {
6887
+ console.error(`Lua plugin runtime disabled: ${luaSettled.reason?.message || luaSettled.reason}`);
6888
+ }
6889
+
6890
+ const jsResult = jsSettled.status === "fulfilled"
6891
+ ? jsSettled.value
6892
+ : { duration: 0 };
6893
+ if (jsSettled.status === "rejected") {
6894
+ console.error(`JS plugin runtime disabled: ${jsSettled.reason?.message || jsSettled.reason}`);
6895
+ }
6712
6896
 
6713
- if (DEFAULT_SETTINGS.savecursor) {
6714
- context.cursorStates = await loadCursorStates(config.configDir);
6897
+ const buffersResult = buffersSettled.status === "fulfilled"
6898
+ ? buffersSettled.value
6899
+ : { buffers: [new BufferModel({ command })], duration: 0 };
6900
+ if (buffersSettled.status === "rejected") {
6901
+ console.error(`Buffer load failed: ${buffersSettled.reason?.message || buffersSettled.reason}`);
6715
6902
  }
6716
- // Backup prompt available before App starts (stdin still in cooked mode).
6717
- context._termPrompt = process.stdout.isTTY ? termPromptLine : null;
6718
- loadBuffers.context = context;
6719
- const buffers = await loadBuffers(files.map((file) =>
6720
- isHttpUrl(file) ? file : resolve(file)
6721
- ), command);
6722
6903
 
6723
- if (!process.stdout.isTTY) {
6904
+ addCheckpoint("Parallel Initialization End");
6905
+
6906
+ parallelTimings = {
6907
+ lua: luaResult.duration / 1e6,
6908
+ js: jsResult.duration / 1e6,
6909
+ buffers: buffersResult.duration / 1e6
6910
+ };
6911
+
6912
+ const { pluginErr } = luaResult;
6913
+ const { buffers } = buffersResult;
6914
+
6915
+ if (!process.stdout.isTTY && !flags.profile) {
6724
6916
  console.log(buffers[0].lines.join("\n"));
6725
6917
  return;
6726
6918
  }
6727
- if (config.getGlobalOption("savehistory") !== false) {
6728
- await loadHistory(config.configDir);
6729
- }
6919
+
6920
+ addCheckpoint("App Instantiation");
6730
6921
  const app = new App(buffers, context);
6731
6922
  jsPlugins.setApp(app);
6732
6923
  if (plugins && !pluginErr && app.buffer) plugins.curPaneAdapter = makePaneAdapter(app.buffer, app);
6733
6924
  // Dispatch all JS plugin lifecycle hooks after setApp so TermMessage,
6734
6925
  // CurPane, cmd/action proxies, and buffer APIs all work correctly.
6926
+ addCheckpoint("JS Lifecycle Hooks");
6735
6927
  await jsPlugins.run("preinit");
6736
6928
  await jsPlugins.run("init");
6737
6929
  await jsPlugins.run("postinit");
@@ -6744,6 +6936,26 @@ async function main() {
6744
6936
  if (flags.cdpAddress) cdpArgs.push(`--address=${flags.cdpAddress}`);
6745
6937
  await app.handleCommand(`cdp ${cdpArgs.join(" ")}`);
6746
6938
  }
6939
+
6940
+ if (flags.profile) {
6941
+ addCheckpoint("Clipboard Probing");
6942
+ const clipSetting = config.getGlobalOption("clipboard") ?? "external";
6943
+ const clipboard = new ClipboardManager();
6944
+ if (process.stdin.isTTY && process.stdout.isTTY) {
6945
+ process.stdin.setRawMode?.(true);
6946
+ process.stdin.resume();
6947
+ await clipboard.initFromSetting(clipSetting, process.stdin, process.stdout, 150);
6948
+ process.stdin.setRawMode?.(false);
6949
+ process.stdin.pause();
6950
+ } else {
6951
+ await clipboard.initFromSetting(clipSetting, process.stdin, process.stdout, 150);
6952
+ }
6953
+
6954
+ addCheckpoint("Profile Done");
6955
+ printProfileReport();
6956
+ process.exit(0);
6957
+ }
6958
+
6747
6959
  await app.start();
6748
6960
  }
6749
6961
 
@@ -256,6 +256,9 @@ export function osc52Clipboard(stdout) {
256
256
  }
257
257
 
258
258
  export async function probeOSC52(ttyIn, ttyOut, timeoutMs) {
259
+ return true;
260
+ // Almost no terminal reliably supports probing OSC 52, so treat it as
261
+ // available and let the actual write path fail or work on its own.
259
262
  if (process.env.TMUX) return true;
260
263
  return new Promise((resolve) => {
261
264
  let done = false;