beatrina 0.8.6 → 0.8.7

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/bin/beatrina.mjs CHANGED
@@ -147,10 +147,12 @@ if (args.fromApp) {
147
147
  }
148
148
  // A page already shows the session: the document went there. No page (the
149
149
  // browser was closed): open the session's notebook, which collects it.
150
- if (delivered !== "page" && args.open && live.file) {
151
- // BEATRINA_OPENER substitutes the program that opens it (test/beatrina-sessions.test.mjs).
152
- const opener = process.env.BEATRINA_OPENER ? [process.env.BEATRINA_OPENER, [live.file]]
153
- : process.platform === "darwin" ? ["open", [live.file]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", live.file]] : ["xdg-open", [live.file]];
150
+ if (delivered !== "page" && args.open && live.url) {
151
+ // The page the session SERVES (http://127.0.0.1:<port>/), never the file: macOS `open` drops a
152
+ // file URL's #kernel=…&pair=… fragment and the page then cannot find its kernel. The served
153
+ // origin needs no pairing. BEATRINA_OPENER substitutes the program (test/beatrina-sessions.test.mjs).
154
+ const opener = process.env.BEATRINA_OPENER ? [process.env.BEATRINA_OPENER, [live.url]]
155
+ : process.platform === "darwin" ? ["open", [live.url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", live.url]] : ["xdg-open", [live.url]];
154
156
  spawnSync(opener[0], opener[1]);
155
157
  }
156
158
  if (delivered === "refused") alert(`The running Beatrina session could not open ${file}.`);
@@ -201,7 +203,7 @@ const forced = process.env.BEATRINA_FORCE_FAILSAFE === "1";
201
203
  const deadlineS = Number(process.env.BEATRINA_HOST_DEADLINE || 60);
202
204
  let started = forced
203
205
  ? { announced: false, refused: false, reason: "BEATRINA_FORCE_FAILSAFE=1 asked for it", code: null }
204
- : await runUntilAnnounced({ cmd: process.execPath, args: [path.join(root, "host", "main.mjs"), ...(open ? ["--open"] : [])], env: process.env,
206
+ : await runUntilAnnounced({ cmd: process.execPath, args: [path.join(root, "host", "main.mjs"), ...(open ? ["--open=url"] : [])], env: process.env,
205
207
  deadlineMs: (Number.isFinite(deadlineS) && deadlineS > 0 ? deadlineS : 60) * 1000 });
206
208
  if (started.announced) followChild(started.child);
207
209
  else {
package/bin/shortcut.mjs CHANGED
@@ -22,9 +22,20 @@ import os from "node:os";
22
22
  import path from "node:path";
23
23
 
24
24
  export const BUNDLE_ID = "me.saqr.beatrina";
25
+ // Each type is EXPORTED by Beatrina.app as its own UTI (UTExportedTypeDeclarations) and named in its
26
+ // document types (LSItemContentTypes): Launch Services sets a default handler per UTI, and an app that
27
+ // only lists an extension has no UTI of its own to be the default FOR — the first --default answered
28
+ // -50 (paramErr) for exactly that reason, and .qmd stayed with the app that had declared it (CarmaR).
29
+ // `legacy` is the type CarmaR.app declared for the same extension. A CarmaR bundle anywhere on the
30
+ // disk re-registers itself the moment Finder or Launch Services meets it again, and its declaration
31
+ // then wins the extension back, so a .qmd is typed as CarmaR's type and Beatrina — claiming only its
32
+ // own — is not even a handler of it (2026-09-16: a double-click opened CarmaR out of a dev build
33
+ // folder). Claiming the legacy type too, and being made the default for it, keeps Beatrina the app
34
+ // that opens the file whichever type the system pins on it.
25
35
  export const DOC_TYPES = Object.freeze([
26
- { ext: "qmd", name: "Quarto document", mime: "text/x-quarto-markdown" },
27
- { ext: "Rmd", name: "R Markdown document", mime: "text/x-r-markdown" },
36
+ { ext: "qmd", name: "Quarto document", mime: "text/x-quarto-markdown", uti: "me.saqr.beatrina.qmd", legacy: "me.saqr.carmar.qmd" },
37
+ { ext: "Rmd", name: "R Markdown document", mime: "text/x-r-markdown", uti: "me.saqr.beatrina.rmd", legacy: "me.saqr.carmar.rmd" },
38
+ { ext: "carmd", name: "Beatrina notebook document", mime: "text/markdown", uti: "me.saqr.beatrina.carmd", legacy: "me.saqr.carmar.carmd" },
28
39
  ]);
29
40
 
30
41
  const q = (s) => `'${String(s).replace(/'/g, "'\\''")}'`; // POSIX single-quote
@@ -48,9 +59,19 @@ end open
48
59
  export function documentTypesXml() {
49
60
  return `<array>${DOC_TYPES.map((t) => `<dict><key>CFBundleTypeName</key><string>${t.name}</string>`
50
61
  + `<key>CFBundleTypeRole</key><string>Editor</string><key>LSHandlerRank</key><string>Alternate</string>`
62
+ + `<key>LSItemContentTypes</key><array><string>${t.uti}</string><string>${t.legacy}</string></array>`
51
63
  + `<key>CFBundleTypeExtensions</key><array><string>${t.ext}</string><string>${t.ext.toLowerCase()}</string></array></dict>`).join("")}</array>`;
52
64
  }
53
65
 
66
+ /** The UTIs Beatrina.app exports for those types (UTExportedTypeDeclarations), as plutil-insertable XML. */
67
+ export function exportedTypesXml() {
68
+ return `<array>${DOC_TYPES.map((t) => `<dict><key>UTTypeIdentifier</key><string>${t.uti}</string>`
69
+ + `<key>UTTypeDescription</key><string>${t.name}</string>`
70
+ + `<key>UTTypeConformsTo</key><array><string>public.plain-text</string></array>`
71
+ + `<key>UTTypeTagSpecification</key><dict><key>public.filename-extension</key><array><string>${t.ext}</string><string>${t.ext.toLowerCase()}</string></array>`
72
+ + `<key>public.mime-type</key><array><string>${t.mime}</string></array></dict></dict>`).join("")}</array>`;
73
+ }
74
+
54
75
  /** How to start Beatrina from an icon: this Node, this command, and any baked options. */
55
76
  export function launchCommand({ node = process.execPath, cli, port = "", noOpen = false }) {
56
77
  return `${q(node)} ${q(cli)}${port ? ` --port ${port}` : ""}${noOpen ? " --no-open" : ""}`;
@@ -75,16 +96,23 @@ function macShortcut({ cli, dir, port, makeDefault, noOpen }) {
75
96
  run("plutil", ["-replace", "CFBundleIdentifier", "-string", BUNDLE_ID, plist]);
76
97
  run("plutil", ["-replace", "CFBundleName", "-string", "Beatrina", plist]);
77
98
  run("plutil", ["-replace", "CFBundleDocumentTypes", "-xml", documentTypesXml(), plist]);
99
+ run("plutil", ["-replace", "UTExportedTypeDeclarations", "-xml", exportedTypesXml(), plist]);
78
100
  run("codesign", ["--force", "--sign", "-", app]); // the plist changed after osacompile signed it
79
101
  if (fs.existsSync(LSREGISTER)) run(LSREGISTER, ["-f", app]);
80
102
  const said = [`Beatrina.app is in ${path.dirname(app)}. Double-click it, or right-click a .qmd and choose Open With ▸ Beatrina.`];
81
103
  if (makeDefault) {
104
+ // Beatrina's own UTI for each type, and whatever UTI Launch Services currently resolves the
105
+ // extension to (another app's declaration, or a dynamic one) — both are pointed at Beatrina, so
106
+ // a .qmd opens here whichever identity the system had attached to it. 0 is success.
82
107
  const js = `ObjC.import("CoreServices");
83
- const exts = ${JSON.stringify(DOC_TYPES.map((t) => t.ext))};
84
- exts.map((ext) => { const uti = $.UTTypeCreatePreferredIdentifierForTag($.kUTTagClassFilenameExtension, $(ext), $()).js;
85
- return ext + "=" + $.LSSetDefaultRoleHandlerForContentType($(uti), $.kLSRolesAll, $(${JSON.stringify(BUNDLE_ID)})); }).join(" ");`;
108
+ const types = ${JSON.stringify(DOC_TYPES.map((t) => ({ ext: t.ext, uti: t.uti, legacy: t.legacy })))};
109
+ types.map((t) => { const tagged = $.UTTypeCreatePreferredIdentifierForTag($.kUTTagClassFilenameExtension, $(t.ext), $()).js;
110
+ const utis = [...new Set([t.uti, t.legacy, tagged])];
111
+ return t.ext + "=" + utis.map((u) => $.LSSetDefaultRoleHandlerForContentType($(u), $.kLSRolesAll, $(${JSON.stringify(BUNDLE_ID)}))).join("/"); }).join(" ");`;
86
112
  const out = run("osascript", ["-l", "JavaScript", "-e", js]).trim();
87
- said.push(`Beatrina is now the default app for .qmd and .Rmd (${out}).`);
113
+ const failed = /=(?:[^0 /]|-\d)/.test(out);
114
+ said.push(failed ? `Beatrina could not be made the default for every type (${out}; 0 means done).`
115
+ : `Beatrina is now the default app for ${DOC_TYPES.map((t) => `.${t.ext}`).join(", ")}.`);
88
116
  }
89
117
  return { app, said };
90
118
  }
package/build-info.json CHANGED
@@ -1 +1 @@
1
- {"version":"0.8.6","commit":"eb036e63b7b3f0e0e1f696d357d701d43235ce6c","dirty":false}
1
+ {"version":"0.8.7","commit":"b7aa1255558293d1e2b47a4799a344fde0846b2e","dirty":false}