bunmicro 0.9.21 → 0.9.23

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.23] - 2026-06-10
4
+ - Fixed mouse close prompt cursor move
5
+ - Fixed set filetype doesn't apply instantly
6
+ - Added syntax highlighting fallback if user ~/.config/micro yaml fails
7
+ - Redetect highlighting syntax when unknown filetype saves
8
+ - Warning for dos(CRLF) shell scripts
9
+
10
+ ## [0.9.22] - 2026-06-09
11
+ - Clicking on icons toggles prompts
12
+ - Unsaved star triggers save cmd
13
+ - URLs support save cursor
14
+
3
15
  ## [0.9.21] - 2026-06-09
4
16
  - Prompt mouse click repositions cursor (command and shell prompt)
5
17
  - Clicking > or $ label toggles between command/shell prompt, preserving input
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunmicro",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "Bun JavaScript rewrite of the micro editor originally in Golang",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -47,25 +47,30 @@ export async function loadSyntaxDefinitions(runtime) {
47
47
  const definitions = [];
48
48
  for (const file of runtime.list(1)) {
49
49
  let text = "";
50
+ let activeFile = file;
51
+ let source = null;
50
52
  try {
51
53
  text = await file.text();
54
+ source = Bun.YAML.parse(text);
52
55
  } catch (e) {
53
- console.error("Failed to read syntax yaml:", file.name);
54
- console.error(" Will not highlight this kind of file");
55
- console.error(" @ loadSyntaxDefinitions ");
56
- continue;
56
+ const fallback = file.real ? runtime.fallback?.(1, file.name) : null;
57
+ if (fallback) {
58
+ try {
59
+ text = await fallback.text();
60
+ source = Bun.YAML.parse(text);
61
+ activeFile = fallback;
62
+ console.error("Failed to load user syntax yaml, using built-in fallback:", file.name);
63
+ } catch {}
64
+ }
57
65
  }
58
66
 
59
- let source = null;
60
- try {
61
- source = Bun.YAML.parse(text);
62
- } catch (e) {
63
- console.error("Failed to parse syntax yaml:", file.name);
67
+ if (!source) {
68
+ console.error("Failed to load syntax yaml:", file.name);
64
69
  console.error(" Will not highlight this kind of file");
65
70
  console.error(" @ loadSyntaxDefinitions ");
66
71
  }
67
72
 
68
- const header = headers.get(file.name) ?? (source ? parseHeaderYaml(source) : parseHeaderTextFallback(text, file.name));
73
+ const header = headers.get(activeFile.name) ?? (source ? parseHeaderYaml(source) : parseHeaderTextFallback(text, activeFile.name));
69
74
  definitions.push(new SyntaxDefinition(header, source ?? { rules: [] }, text));
70
75
  }
71
76
  return definitions;
package/src/index.js CHANGED
@@ -1092,6 +1092,7 @@ class BufferModel {
1092
1092
  }
1093
1093
  async save(path = this.path) {
1094
1094
  if (!path) throw new Error("No filename");
1095
+ const detectSyntaxAfterSave = this.filetype === "unknown";
1095
1096
  let text = this.lines.join("\n");
1096
1097
  if (this.encoding === "hex3") {
1097
1098
  await Bun.write(path, decodeBinaryBytes(Buffer.from(text, "latin1")));
@@ -1106,6 +1107,7 @@ class BufferModel {
1106
1107
  this._savedSerial = this._undoSerial ?? 0;
1107
1108
  this.modified = false;
1108
1109
  this.message = `Saved ${path}`;
1110
+ if (detectSyntaxAfterSave && this._syntaxContext) attachSyntax(this, this._syntaxContext, path, text);
1109
1111
  return;
1110
1112
  }
1111
1113
  if ((this.Settings.eofnewline ?? DEFAULT_SETTINGS.eofnewline) && !text.endsWith("\n")) text += "\n";
@@ -1123,6 +1125,7 @@ class BufferModel {
1123
1125
  this._savedSerial = this._undoSerial ?? 0;
1124
1126
  this.modified = false;
1125
1127
  this.message = `Saved ${path}`;
1128
+ if (detectSyntaxAfterSave && this._syntaxContext) attachSyntax(this, this._syntaxContext, path, text);
1126
1129
  }
1127
1130
 
1128
1131
  // --- Autocomplete (BufferComplete) ---
@@ -1788,7 +1791,10 @@ class App {
1788
1791
  const keymenuHeight = this.keymenu ? KEYDISPLAY.length : 0;
1789
1792
  const activeSuggestions = this._activeSuggestions();
1790
1793
  const activeSuggestionIdx = this._activeSuggestionIdx();
1791
- const activeMessage = this.message || this.buffer?.message || "";
1794
+ const formatWarning = this.buffer?.filetype === "shell" && this.buffer?.fileformat === "dos"
1795
+ ? "dos(CRLF fileformat) invalid for shell scripts!"
1796
+ : "";
1797
+ const activeMessage = this.message || this.buffer?.message || formatWarning;
1792
1798
  if (activeSuggestions.length === 0) this._acHScroll = 0;
1793
1799
  const suggestionsHeight = activeSuggestions.length > 1 ? 1 : 0;
1794
1800
  const messageHeight = suggestionsHeight ? 0 : activeMessage ? 1 : 0;
@@ -1884,8 +1890,14 @@ class App {
1884
1890
  let sx = 0, x0;
1885
1891
  // name
1886
1892
  x0 = sx;
1887
- sx = putText(this.screen, sx, statusRow, ` ${name}${dirty} `, isReadonlyBuffer(buf) ? redStatus : baseStatus, this.cols - sx);
1893
+ sx = putText(this.screen, sx, statusRow, ` ${name}`, isReadonlyBuffer(buf) ? redStatus : baseStatus, this.cols - sx);
1888
1894
  markZone("name", x0, sx);
1895
+ if (dirty) {
1896
+ x0 = sx;
1897
+ sx = putText(this.screen, sx, statusRow, dirty, baseStatus, this.cols - sx);
1898
+ markZone("dirty", x0, sx);
1899
+ }
1900
+ sx = putText(this.screen, sx, statusRow, " ", baseStatus, this.cols - sx);
1889
1901
  // (row,col)
1890
1902
  sx = putText(this.screen, sx, statusRow, "(", baseStatus, this.cols - sx);
1891
1903
  x0 = sx;
@@ -3478,6 +3490,12 @@ class App {
3478
3490
  this.nextTab();
3479
3491
  }
3480
3492
  break;
3493
+ case "dirty": {
3494
+ const name = buf?.name ?? "No name";
3495
+ const filename = /^[^\s"'\\]+$/.test(name) ? name : JSON.stringify(name);
3496
+ this.openCommandMode(`save ${filename}`);
3497
+ break;
3498
+ }
3481
3499
  case "row":
3482
3500
  if (isTerm) {
3483
3501
  this.pane.terminal?.write("\x12");
@@ -3505,18 +3523,7 @@ class App {
3505
3523
  }
3506
3524
  break;
3507
3525
  case "ft": {
3508
- const defs = this.context.syntaxDefinitions ?? [];
3509
- const filetypes = defs.map(d => d.filetype).filter(Boolean).sort();
3510
- const ftComplete = (partial) => filetypes.filter(f => f.startsWith(partial));
3511
- this.openPrompt("Set filetype: ", (value) => {
3512
- if (!value || !buf) return;
3513
- buf.filetype = value;
3514
- buf.Settings.filetype = value;
3515
- const def = defs.find(d => d.filetype === value);
3516
- buf.syntaxDefinition = def ?? null;
3517
- buf.highlighter = def ? new Highlighter(def, defs) : null;
3518
- buf._highlightCache = null;
3519
- }, { completer: ftComplete, initial: buf?.filetype ?? "" });
3526
+ this.openCommandMode("set filetype ");
3520
3527
  break;
3521
3528
  }
3522
3529
  case "fmt":
@@ -3540,10 +3547,12 @@ class App {
3540
3547
  await this.addTab();
3541
3548
  break;
3542
3549
  case "cmdmode":
3543
- this.openCommandMode();
3550
+ if (this.prompt?.type === "Command") this._suppressMouseUntilUp = true;
3551
+ await this.togglePromptMode("Command");
3544
3552
  break;
3545
3553
  case "shellmode":
3546
- this.openShellMode();
3554
+ if (this.prompt?.type === "Shell") this._suppressMouseUntilUp = true;
3555
+ await this.togglePromptMode("Shell");
3547
3556
  break;
3548
3557
  }
3549
3558
  return true;
@@ -3753,6 +3762,18 @@ class App {
3753
3762
  this.prompt = new Prompt(label, callback, { yn: true, onCancel });
3754
3763
  }
3755
3764
 
3765
+ async togglePromptMode(type) {
3766
+ if (this.prompt?.type === type) {
3767
+ const prompt = this.prompt;
3768
+ this.prompt = null;
3769
+ await prompt.onCancel?.();
3770
+ return;
3771
+ }
3772
+
3773
+ if (type === "Command") this.openCommandMode();
3774
+ else this.openShellMode();
3775
+ }
3776
+
3756
3777
  async checkExternalReload() {
3757
3778
  if (this.prompt || this.pane?.type !== "editor") return false;
3758
3779
  const buf = this.buffer;
@@ -5125,6 +5146,18 @@ function completeOptionValue(cmd, option, partial, context) {
5125
5146
  const optVal = allSettings[option];
5126
5147
  const suggestions = [];
5127
5148
 
5149
+ if (option === "filetype") {
5150
+ const filetypes = [
5151
+ "off",
5152
+ "unknown",
5153
+ ...(context?.syntaxDefinitions ?? []).map((definition) => definition.filetype),
5154
+ ];
5155
+ return [...new Set(filetypes)]
5156
+ .filter((filetype) => filetype && filetype.startsWith(partial))
5157
+ .sort()
5158
+ .map((filetype) => ({ value: `${cmd} ${option} ${filetype}`, label: filetype }));
5159
+ }
5160
+
5128
5161
  if (typeof optVal === "boolean") {
5129
5162
  if ("on".startsWith(partial)) suggestions.push("on");
5130
5163
  else if ("true".startsWith(partial)) suggestions.push("true");
@@ -5649,17 +5682,18 @@ function commandHasStartupJump(command = {}) {
5649
5682
  }
5650
5683
 
5651
5684
  async function loadBufferForPath(pathOrUrl, context, command = {}) {
5685
+ let buffer;
5652
5686
  if (isHttpUrl(pathOrUrl)) {
5653
5687
  let encoding = context.config?.globalSettings?.encoding ?? DEFAULT_SETTINGS.encoding;
5654
5688
  const decoded = await fetchTextWithEncoding(pathOrUrl, encoding);
5655
5689
  const text = decoded.text;
5656
5690
  encoding = decoded.encoding;
5657
5691
  const urlPath = pathOrUrl.replace(/[?#].*$/, "");
5658
- const buffer = new BufferModel({ path: pathOrUrl, text, command, encoding });
5692
+ buffer = new BufferModel({ path: pathOrUrl, text, command, encoding });
5659
5693
  attachSyntax(buffer, context, urlPath, text);
5660
- return buffer;
5694
+ } else {
5695
+ buffer = await BufferModel.fromFile(pathOrUrl, command, context);
5661
5696
  }
5662
- const buffer = await BufferModel.fromFile(pathOrUrl, command, context);
5663
5697
  if (DEFAULT_SETTINGS.savecursor && !commandHasStartupJump(command) && context?.cursorStates?.[pathOrUrl]) {
5664
5698
  const saved = context.cursorStates[pathOrUrl];
5665
5699
  const y = clamp(saved.y ?? 0, 0, buffer.lines.length - 1);
@@ -6204,9 +6238,14 @@ async function loadBuffers(files, command) {
6204
6238
  if (loadBuffers.context) attachSyntax(stdinBuf, loadBuffers.context, "", stdinText);
6205
6239
  buffers.push(stdinBuf);
6206
6240
  } else {
6207
- buffers.push(new BufferModel({ command }));
6241
+ const buffer = new BufferModel({ command });
6242
+ if (loadBuffers.context) attachSyntax(buffer, loadBuffers.context, "", "");
6243
+ buffers.push(buffer);
6208
6244
  }
6209
- return buffers.length ? buffers : [new BufferModel({ command })];
6245
+ if (buffers.length > 0) return buffers;
6246
+ const buffer = new BufferModel({ command });
6247
+ if (loadBuffers.context) attachSyntax(buffer, loadBuffers.context, "", "");
6248
+ return [buffer];
6210
6249
  }
6211
6250
 
6212
6251
  async function printReadmeDocs() {
@@ -6641,6 +6680,7 @@ function getSelectionText(buf, selection) {
6641
6680
  }
6642
6681
 
6643
6682
  function attachSyntax(buffer, context, path, text) {
6683
+ buffer._syntaxContext = context;
6644
6684
  const def = detectBufferSyntax(context.syntaxDefinitions, path, text);
6645
6685
  buffer.syntaxDefinition = def;
6646
6686
  buffer.filetype = def?.filetype ?? "unknown";
@@ -6648,21 +6688,32 @@ function attachSyntax(buffer, context, path, text) {
6648
6688
  buffer.highlighter = def ? new Highlighter(def, context.syntaxDefinitions ?? []) : null;
6649
6689
  buffer._highlightCache = null;
6650
6690
  buffer._onOptionChange = (option, oldVal, newVal) => {
6691
+ if (option === "filetype") setBufferFiletype(buffer, context, newVal);
6651
6692
  const ba = makeBufferAdapter(buffer);
6652
6693
  context.plugins?.run("onBufferOptionChanged", ba, option, oldVal, newVal);
6653
6694
  context.jsPlugins?.run("onBufferOptionChanged", ba, option, oldVal, newVal);
6654
6695
  };
6655
6696
  }
6656
6697
 
6698
+ function setBufferFiletype(buffer, context, filetype) {
6699
+ const value = String(filetype);
6700
+ const definitions = context?.syntaxDefinitions ?? [];
6701
+ const def = definitions.find((candidate) => candidate.filetype === value) ?? null;
6702
+ buffer.filetype = value;
6703
+ buffer.Settings.filetype = value;
6704
+ buffer.syntaxDefinition = def;
6705
+ buffer.highlighter = def ? new Highlighter(def, definitions) : null;
6706
+ buffer._highlightCache = null;
6707
+ }
6708
+
6657
6709
  function detectBufferSyntax(definitions, path, text) {
6658
6710
  if (!definitions) return null;
6659
- const lines = String(text).split("\n").slice(0, 50);
6711
+ const lines = normalizeBufferText(text).split("\n").slice(0, 50);
6660
6712
  return detectSyntax(definitions, { path, firstLine: lines[0] ?? "", lines });
6661
6713
  }
6662
6714
 
6663
6715
  function detectBufferFiletype(definitions, path, text) {
6664
6716
  if (!definitions) return "unknown";
6665
- const lines = String(text).split("\n").slice(0, 50);
6666
6717
  return detectBufferSyntax(definitions, path, text)?.filetype ?? "unknown";
6667
6718
  }
6668
6719
 
@@ -6736,7 +6787,7 @@ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAUL
6736
6787
  );
6737
6788
  continue;
6738
6789
  }
6739
- const lines = content.split("\n");
6790
+ const lines = normalizeBufferText(content).split("\n");
6740
6791
  const def = detectSyntax(syntaxDefinitions, {
6741
6792
  path: effectivePath ?? "",
6742
6793
  firstLine: lines[0] ?? "",
@@ -14,11 +14,13 @@ export class RuntimeRegistry {
14
14
  this.configDir = configDir;
15
15
  this.files = [[], [], [], [], []];
16
16
  this.realFiles = [[], [], [], [], []];
17
+ this.fallbackFiles = [[], [], [], [], []];
17
18
  }
18
19
 
19
20
  async init({ user = true } = {}) {
20
21
  this.files = [[], [], [], [], []];
21
22
  this.realFiles = [[], [], [], [], []];
23
+ this.fallbackFiles = [[], [], [], [], []];
22
24
  await this.addRuntimeKind(RTColorscheme, "colorschemes", ".micro", user);
23
25
  await this.addRuntimeKind(RTSyntax, "syntax", ".yaml", user);
24
26
  await this.addRuntimeKind(RTSyntaxHeader, "syntax", ".hdr", user);
@@ -36,7 +38,10 @@ export class RuntimeRegistry {
36
38
  for (const entry of entries) {
37
39
  if (entry.isDirectory() || !entry.name.endsWith(extension)) continue;
38
40
  const file = new RuntimeFile(join(dir, entry.name), real);
39
- if (!real && this.realFiles[kind].some((f) => f.name === file.name)) continue;
41
+ if (!real && this.realFiles[kind].some((f) => f.name === file.name)) {
42
+ this.fallbackFiles[kind].push(file);
43
+ continue;
44
+ }
40
45
  this.files[kind].push(file);
41
46
  if (real) this.realFiles[kind].push(file);
42
47
  }
@@ -53,6 +58,10 @@ export class RuntimeRegistry {
53
58
  find(kind, name) {
54
59
  return this.list(kind).find((file) => file.name === name) ?? null;
55
60
  }
61
+
62
+ fallback(kind, name) {
63
+ return this.fallbackFiles[kind]?.find((file) => file.name === name) ?? null;
64
+ }
56
65
  }
57
66
 
58
67
  class RuntimeFile {