@gjsify/adwaita-app 0.51.0 → 0.52.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/README.md CHANGED
@@ -64,7 +64,8 @@ promise-job queue under `run()`).
64
64
 
65
65
  - `runAdwaitaApp(options): Promise<number>` — construct + run, resolve with the
66
66
  exit code. `AdwaitaAppOptions`: `applicationId`, `createWindow`, optional
67
- `flags`, `css`, `about` (`AboutInfo`), `quitAction` (default on, `<primary>q`),
67
+ `flags`, `css`, `about` (`AboutInfo`) or `aboutAppdata`
68
+ (`CreateAboutDialogOptions`, see below), `quitAction` (default on, `<primary>q`),
68
69
  `devtools` (`true` | `InstallDevtoolsOptions` | omitted = env-gated),
69
70
  `bundledIcons` (default on — see below), `onStartup`.
70
71
  - `AdwaitaApp` — the configured `Adw.Application` subclass, if you need the
@@ -128,6 +129,88 @@ has nothing behind it but GTK's own builtins. Register your own with
128
129
  > the guarantee is built on the icon THEME rather than on handing widgets an
129
130
  > imported SVG: for most of them there is no property to hand it to.
130
131
 
132
+ ### About dialog from AppStream metainfo
133
+
134
+ `Adw.AboutDialog.new_from_appdata()` builds the whole dialog out of the metainfo
135
+ file an application already ships — name, developer, licence, urls, version,
136
+ release notes, translated. **It does not exist in the Windows GTK runtime.**
137
+ gvsbuild applies `patches/libadwaita/0001-remove-appstream-dependency.patch`,
138
+ which wraps every `*_from_appdata` entry point in `#ifndef G_OS_WIN32`, because
139
+ libadwaita 1.9.x parses AppStream through the heavyweight `appstream` library
140
+ and gvsbuild defines no project for it. Homebrew's formula `depends_on
141
+ "appstream"`, so **both darwin bundles have it and win32-x64 does not** —
142
+ measured symbol by symbol out of each published bundle's own `Adw-1.typelib`
143
+ ([gjsify/gjsify#1662](https://github.com/gjsify/gjsify/issues/1662)). On Windows
144
+ 11 the ordinary call site reads `no static method 'new_from_appdata'` and the
145
+ About dialog does not open at all.
146
+
147
+ `createAboutDialog` is the drop-in that works on all three:
148
+
149
+ ```ts
150
+ import { createAboutDialog } from '@gjsify/adwaita-app';
151
+
152
+ createAboutDialog({
153
+ appdataResource: '/org/example/App/metainfo/org.example.App.metainfo.xml',
154
+ releaseNotesVersion: PACKAGE_VERSION,
155
+ version: PACKAGE_VERSION,
156
+ }).present(window);
157
+ ```
158
+
159
+ Or let the shell wire the `app.about` action to it:
160
+
161
+ ```ts
162
+ runAdwaitaApp({
163
+ applicationId: 'org.example.App',
164
+ createWindow,
165
+ aboutAppdata: { appdataResource: '/org/example/App/metainfo/org.example.App.metainfo.xml' },
166
+ });
167
+ ```
168
+
169
+ Where the constructor exists it is used, unchanged. Where it does not, the same
170
+ dialog is assembled from the same document by `parseAppdata` — which **ports
171
+ `ministream`'s selection rules rule for rule** (the small parser libadwaita 1.10
172
+ replaced the `appstream` dependency with): BCP-47 segment scoring for
173
+ translations, the `+1024` shift that ranks `<developer><name>` over the legacy
174
+ `<developer_name>`, the `.desktop` id rule, per-language release-note buffers.
175
+ Picking fields by our own taste would show a *different* About dialog on Windows
176
+ than on the two platforms that run the real constructor, which is the whole
177
+ failure this closes.
178
+
179
+ It sets exactly what upstream's `populate_from_appdata` sets and nothing else —
180
+ application icon (from `<id>`), application name, developer name, version,
181
+ website, support url, issue url, licence **type**, and the release notes of one
182
+ named release. No `comments`, no `copyright`, no `translator-credits`, no
183
+ `developers`: AppStream's `<summary>` is not `Adw.AboutDialog:comments` and
184
+ upstream never treats it as one. Set those on the returned dialog yourself.
185
+
186
+ Three things it does that upstream does not, each for a measured reason:
187
+
188
+ | | why |
189
+ |---|---|
190
+ | checks the resource with `Gio.resources_get_info` **before** calling the constructor | libadwaita reports an unreadable resource with `g_error()`, which **aborts the process** — a mistyped resource path would kill the app the moment the user opens About, with no exception for any `catch` to see |
191
+ | accepts `appdataPath`, a plain file | a `gjsify ship` artifact may carry the metainfo beside the bundle rather than inside a GResource; upstream's constructor cannot read that at all, so such an app would have no About dialog on *any* platform |
192
+ | `forceParsedAppdata: true` | the parsed path is what every Windows user sees and what no Linux or macOS run would otherwise execute — this is how you look at it before shipping, and how the suite covers it |
193
+
194
+ **Two divergences, both named.** Releases are read in document order rather than
195
+ sorted by ministream's dpkg-style version comparison (AppStream requires
196
+ newest-first, and `appstreamcli validate` reports `releases-not-in-order`
197
+ otherwise) — pass `version` to remove the question on every platform at once. And
198
+ an SPDX id GTK does not know becomes `Gtk.License.CUSTOM` with **no** licence
199
+ text, exactly as upstream leaves it; filling the text with the raw id would be
200
+ more helpful and would show a licence line on Windows that Linux does not.
201
+
202
+ - `createAboutDialog(options?): Adw.AboutDialog` — `appdataResource`,
203
+ `appdataPath`, `releaseNotesVersion`, `version`, `applicationIcon`,
204
+ `forceParsedAppdata`.
205
+ - `buildAboutDialogFromAppdata(xml, { releaseNotesVersion?, locale? }): Adw.AboutDialog`
206
+ — the parsed path on its own, given the document.
207
+ - `applyAppdataFields(dialog, fields, releaseNotesVersion?)` — for an app that
208
+ already has a dialog.
209
+ - `parseAppdata(xml, { locale?, releaseNotesVersion? }): AppdataFields` and
210
+ `appdataLocale(GLib.get_language_names(), { LANG })` — the reading half, with
211
+ **no GI imports**, so it runs (and is tested) under Node as well as GJS.
212
+ - `licenseTypeFor(spdxId): Gtk.License`, `hasAppdataConstructor(namespace?): boolean`.
213
+
131
214
  ### Navigation shell
132
215
 
133
216
  - `createNavShell(window, options): NavShell` — builds the
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{appdataLocale as e,parseAppdata as t}from"./appdata.js";import n from"gi://Adw?version=1";import r from"gi://Gio?version=2.0";import i from"gi://GLib?version=2.0";import a from"gi://Gtk?version=4.0";const o={"GPL-2.0-or-later":a.License.GPL_2_0,"GPL-3.0-or-later":a.License.GPL_3_0,"LGPL-2.1-or-later":a.License.LGPL_2_1,"LGPL-3.0-or-later":a.License.LGPL_3_0,"BSD-2-Clause":a.License.BSD,MIT:a.License.MIT_X11,"Artistic-2.0":a.License.ARTISTIC,"GPL-2.0-only":a.License.GPL_2_0_ONLY,"GPL-3.0-only":a.License.GPL_3_0_ONLY,"LGPL-2.1-only":a.License.LGPL_2_1_ONLY,"LGPL-3.0-only":a.License.LGPL_3_0_ONLY,"AGPL-3.0-or-later":a.License.AGPL_3_0,"AGPL-3.0-only":a.License.AGPL_3_0_ONLY,"BSD-3-Clause":a.License.BSD_3,"Apache-2.0":a.License.APACHE_2_0,"MPL-2.0":a.License.MPL_2_0,"0BSD":a.License[`0BSD`],"GPL-2.0":a.License.GPL_2_0_ONLY,"GPL-3.0":a.License.GPL_3_0_ONLY};function licenseTypeFor(e){return o[e]??a.License.CUSTOM}function hasAppdataConstructor(e=n){try{return typeof e?.AboutDialog?.new_from_appdata==`function`}catch{return!1}}function applyAppdataFields(e,t,n){return t.applicationId!==null&&(e.applicationIcon=t.applicationId),t.applicationName!==null&&(e.applicationName=t.applicationName),t.developerName!==null&&(e.developerName=t.developerName),t.website!==null&&(e.website=t.website),t.supportUrl!==null&&(e.supportUrl=t.supportUrl),t.issueUrl!==null&&(e.issueUrl=t.issueUrl),t.license!==null&&(e.licenseType=licenseTypeFor(t.license)),n!==void 0&&n!==``&&(e.releaseNotesVersion=n),t.releaseNotes!==null&&(e.releaseNotes=t.releaseNotes),t.version!==null&&(e.version=t.version),e}function buildAboutDialogFromAppdata(r,a={}){let o=t(r,{locale:a.locale??e(i.get_language_names(),{LANG:i.getenv(`LANG`)??void 0}),releaseNotesVersion:a.releaseNotesVersion});return applyAppdataFields(new n.AboutDialog,o,a.releaseNotesVersion)}function resourceExists(e){try{return r.resources_get_info(e,r.ResourceLookupFlags.NONE),!0}catch{return!1}}function readResource(e){try{let t=r.resources_lookup_data(e,r.ResourceLookupFlags.NONE);return new TextDecoder().decode(t.toArray())}catch{return null}}function readFile(e){try{let[t,n]=i.file_get_contents(e);return t?new TextDecoder().decode(n):null}catch{return null}}let s=!1;function createAboutDialog(e={}){let{appdataResource:t,appdataPath:r,releaseNotesVersion:i}=e,a=t!==void 0&&resourceExists(t),o;if(!e.forceParsedAppdata&&a&&hasAppdataConstructor())o=n.AboutDialog.new_from_appdata(t,i??``);else{let c=(a?readResource(t):null)??(r===void 0?null:readFile(r));c===null?(console.error(`@gjsify/adwaita-app: no AppStream metainfo found for the About dialog; looked at resource ${t??`(none given)`} and file ${r??`(none given)`}`),o=new n.AboutDialog):(!s&&!e.forceParsedAppdata&&a&&(s=!0,console.log(`@gjsify/adwaita-app: Adw.AboutDialog.new_from_appdata is not in this GTK runtime (expected on Windows — gjsify/gjsify#1662); building the About dialog from the AppStream metainfo directly.`)),o=buildAboutDialogFromAppdata(c,{releaseNotesVersion:i}))}return e.applicationIcon!==void 0&&(o.applicationIcon=e.applicationIcon),e.version!==void 0&&(o.version=e.version),o}export{applyAppdataFields,buildAboutDialogFromAppdata,createAboutDialog,hasAppdataConstructor,licenseTypeFor};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";const e={amp:`&`,lt:`<`,gt:`>`,quot:`"`,apos:`'`};function decodeXmlEntities(t){return t.includes(`&`)?t.replace(/&(#[Xx][0-9A-Fa-f]+|#[0-9]+|[A-Za-z][A-Za-z0-9]*);/g,(t,n)=>{if(n.charAt(0)!==`#`)return e[n]??t;let r=n.charAt(1)===`x`||n.charAt(1)===`X`,i=Number.parseInt(r?n.slice(2):n.slice(1),r?16:10);return!Number.isFinite(i)||i<1||i>1114111||i>=55296&&i<=57343?t:String.fromCodePoint(i)}):t}function findTagEnd(e,t){let n=``;for(let r=t+1;r<e.length;r++){let t=e.charAt(r);if(n)t===n&&(n=``);else if(t===`"`||t===`'`)n=t;else if(t===`>`)return r}return-1}const t=/([^\s=/<>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;function parseAttributes(e){let n={};t.lastIndex=0;let r=t.exec(e);for(;r!==null;)r[1]in n||(n[r[1]]=decodeXmlEntities(r[2]??r[3]??``)),r=t.exec(e);return n}function scanXml(e){let t=[],pushText=e=>{t.push({kind:`text`,text:decodeXmlEntities(e)})},n=0;for(;n<e.length;){let r=e.indexOf(`<`,n);if(r<0){pushText(e.slice(n));break}if(r>n&&pushText(e.slice(n,r)),e.startsWith(`<!--`,r)){let t=e.indexOf(`-->`,r+4);n=t<0?e.length:t+3;continue}if(e.startsWith(`<![CDATA[`,r)){let i=e.indexOf(`]]>`,r+9);t.push({kind:`text`,text:e.slice(r+9,i<0?e.length:i)}),n=i<0?e.length:i+3;continue}if(e.startsWith(`<?`,r)){let t=e.indexOf(`?>`,r+2);n=t<0?e.length:t+2;continue}if(e.startsWith(`<!`,r)){let t=e.indexOf(`>`,r+2);n=t<0?e.length:t+1;continue}let i=findTagEnd(e,r);if(i<0){pushText(e.slice(r));break}let a=e.slice(r+1,i);if(n=i+1,a.charAt(0)===`/`){let e=a.slice(1).trim();e!==``&&t.push({kind:`close`,name:e});continue}let o=a,s=!1;o.endsWith(`/`)&&(s=!0,o=o.slice(0,-1));let c=/^([^\s/<>]+)/.exec(o);if(c===null)continue;let l=c[1];t.push({kind:`open`,name:l,attributes:parseAttributes(o.slice(l.length)),selfClosing:s})}return t}export{decodeXmlEntities,scanXml};
@@ -0,0 +1,2 @@
1
+ import"./_virtual/_rolldown/runtime.js";import{scanXml as e}from"./appdata-xml.js";const t={applicationId:null,applicationName:null,developerName:null,version:null,website:null,supportUrl:null,issueUrl:null,license:null,releaseNotes:null};function localeScore(e,t){let n=0;for(let r=0;;r++){let i=r<e.length?e.charAt(r):`\0`,a=r<t.length?t.charAt(r):`\0`;if((i===`-`||i===`\0`)&&(a===`-`||a===`\0`)&&n++,i===`\0`||a===`\0`)break;if(i!==a)return 0}return n}function appdataLocale(e,t={}){let n,r=e[0];r!==void 0&&!r.includes(`_`)&&t.LANG!==void 0&&t.LANG.includes(`_`)&&(n=t.LANG),n=n??r??`C`,n.endsWith(`.UTF-8`)&&(n=n.slice(0,-6));let i=``,a=n.indexOf(`@`);if(a>=0){let e=n.slice(a);e.startsWith(`@cyrillic`)?i=`-Cyrl`:e.startsWith(`@devanagari`)?i=`-Deva`:e.startsWith(`@latin`)?i=`-Latn`:e.startsWith(`@shaw`)?i=`-Shaw`:e.startsWith(`@euro`)||(i=`-euro`),n=n.slice(0,a)}return n.replace(/_/g,`-`)+i}function childElements(e,t,n){let r=[],i=t;for(;i<n;){let t=e[i];if(t.kind!==`open`){i+=1;continue}let a=t;if(a.selfClosing){r.push({name:a.name,attributes:a.attributes,start:i+1,end:i+1}),i+=1;continue}let o=1,s=i+1;for(;s<n&&o>0;){let t=e[s];t.kind===`open`?t.selfClosing||(o+=1):t.kind===`close`&&--o,s+=1}let c=o===0?s-1:n;r.push({name:a.name,attributes:a.attributes,start:i+1,end:c}),i=o===0?s:n}return r}function elementText(e,t){let n=0,r=``;for(let i=t.start;i<t.end;i++){let t=e[i];t.kind===`open`?t.selfClosing||(n+=1):t.kind===`close`?--n:n===0&&(r+=t.text)}return r.trim()}const lang=e=>e.attributes[`xml:lang`]??`C`;function considerTranslatable(e,t,n,r){(e.value===null&&lang(t)===`C`||r>e.score)&&(e.score=r,e.value=n)}function escapeMarkup(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/'/g,`&#39;`).replace(/"/g,`&quot;`)}function inlineMarkup(e,t){let n=``,r=!0,i=0,a=0;for(let o=t.start;o<t.end;o++){let t=e[o];if(t.kind===`open`){let e=t,r=!e.selfClosing;if(r&&(a+=1),i>0)continue;e.name===`em`||e.name===`code`?n+=`<${e.name}>`:r&&(i=a)}else if(t.kind===`close`)i>0&&a===i?i=0:i===0&&(t.name===`em`||t.name===`code`)&&(n+=`</${t.name}>`),--a;else if(i===0){let e=``;for(let n of t.text){if(n===`
2
+ `||n===` `||n===` `){if(r)continue;e+=` `,r=!0;continue}e+=n,r=!1}n+=escapeMarkup(e)}}return n}function parseDescription(e,t,n){let r=new Map,bucketFor=(e,t)=>{if(localeScore(n,e)===0&&e!==`C`)return null;let i=r.get(e);return i===void 0&&(i=[],r.set(e,i),t!==void 0&&i.push(`<${t}>\n`)),i};for(let n of childElements(e,t.start,t.end))if(n.name===`p`)bucketFor(lang(n))?.push(`<p>${inlineMarkup(e,n)}</p>\n`);else if(n.name===`ul`||n.name===`ol`){for(let e of r.values())e.push(`<${n.name}>\n`);for(let t of childElements(e,n.start,n.end))t.name===`li`&&bucketFor(lang(t),n.name)?.push(`<li>${inlineMarkup(e,t)}</li>\n`);for(let e of r.values())e.push(`</${n.name}>\n`)}let i=r.get(`C`),a=0;for(let[e,t]of r){let r=localeScore(n,e);r>a&&(a=r,i=t)}return i===void 0?null:i.join(``)}function parseAppdata(n,r={}){let i=r.locale??`C`,a=e(n),o=childElements(a,0,a.length).find(e=>e.name===`component`||e.name===`application`);if(o===void 0)return{...t};let s={...t},c={value:null,score:0},l={value:null,score:0},u=[],d=null;for(let e of childElements(a,o.start,o.end))switch(e.name){case`id`:s.applicationId===null&&(s.applicationId=elementText(a,e));break;case`name`:considerTranslatable(c,e,elementText(a,e),localeScore(i,lang(e)));break;case`developer`:for(let t of childElements(a,e.start,e.end)){if(t.name!==`name`)continue;let e=localeScore(i,lang(t))+1024;e>l.score&&(lang(t)===`C`||e>1024)&&(l.score=e,l.value=elementText(a,t))}break;case`developer_name`:considerTranslatable(l,e,elementText(a,e),localeScore(i,lang(e)));break;case`url`:{let t=elementText(a,e);e.attributes.type===`homepage`?s.website=t:e.attributes.type===`bugtracker`?s.issueUrl=t:e.attributes.type===`help`&&(s.supportUrl=t);break}case`launchable`:e.attributes.type===`desktop-id`&&u.push(elementText(a,e));break;case`project_license`:s.license===null&&(s.license=elementText(a,e));break;case`releases`:d===null&&(d=e);break;default:break}if(s.applicationName=c.value,s.developerName=l.value,d!==null){let e=childElements(a,d.start,d.end).filter(e=>e.name===`release`);s.version=e[0]?.attributes.version??null;let t=r.releaseNotesVersion;if(t!==void 0&&t!==``){let n=e.find(e=>e.attributes.version===t),r=n&&childElements(a,n.start,n.end).find(e=>e.name===`description`);r&&(s.releaseNotes=parseDescription(a,r,i))}}s.applicationId!==null&&s.applicationId.endsWith(`.desktop`)&&(u.includes(`${s.applicationId}.desktop`)||(s.applicationId=s.applicationId.slice(0,-8)));for(let e of Object.keys(s))s[e]===``&&(s[e]=null);return s}export{appdataLocale,localeScore,parseAppdata};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{installBundledIconTheme as e}from"./icon-theme.js";import t from"gi://Adw?version=1";import n from"gi://Gdk?version=4.0";import r from"gi://Gio?version=2.0";import i from"gi://GObject?version=2.0";import a from"gi://Gtk?version=4.0";import o from"system";import{installDevtools as s}from"@gjsify/devtools";var c=class AdwaitaApp extends t.Application{static{i.registerClass({GTypeName:`GjsifyAdwaitaApp`},AdwaitaApp)}constructor(e){super({application_id:e.applicationId,flags:e.flags??r.ApplicationFlags.DEFAULT_FLAGS}),this._window=null,this._options=e,this._initActions(),this.connect(`startup`,()=>this._onStartup()),this.connect(`activate`,()=>this._onActivate())}_initActions(){if(this._options.quitAction!==!1){let e=new r.SimpleAction({name:`quit`});e.connect(`activate`,()=>this.quit()),this.add_action(e),this.set_accels_for_action(`app.quit`,[`<primary>q`])}if(this._options.about){let e=new r.SimpleAction({name:`about`});e.connect(`activate`,()=>this._showAbout()),this.add_action(e)}}_onStartup(){this._installBundledIcons(),this._options.css&&this._loadCss(this._options.css),this._installDevtools(),this._options.onStartup?.(this)}_installBundledIcons(){let t=this._options.bundledIcons;t!==!1&&e(typeof t==`object`?t:{})}_loadCss(e){let t=n.Display.get_default();if(!t){console.error(`@gjsify/adwaita-app: no default display; CSS not applied`);return}let r=new a.CssProvider;r.load_from_string(e),a.StyleContext.add_provider_for_display(t,r,a.STYLE_PROVIDER_PRIORITY_APPLICATION)}_installDevtools(){let e=this._options.devtools;if(e===!1)return;let t=typeof e==`object`?e:{enabled:e||void 0};s(this,t)}_onActivate(){this._window||=this._options.createWindow(this),this._window.present()}_showAbout(){let e=this._options.about;if(!e)return;let n=new t.AboutDialog({application_name:e.applicationName,application_icon:e.applicationIcon??this._options.applicationId});e.version&&(n.version=e.version),e.developerName&&(n.developer_name=e.developerName),e.website&&(n.website=e.website),e.issueUrl&&(n.issue_url=e.issueUrl),e.license&&(n.license=e.license),e.comments&&(n.comments=e.comments),e.copyright&&(n.copyright=e.copyright),e.developers?.length&&n.set_developers(e.developers),n.present(this.get_active_window())}};async function runAdwaitaApp(e){return runApplication(new c(e),[o.programInvocationName,...o.programArgs])}async function runApplication(e,t){let n=!1;try{e.register(null),n=e.get_is_remote()}catch{n=!1}let r=await e.runAsync(t);return n&&console.error(`${e.applicationId??`This application`} is already running — brought the existing instance to the front instead of opening a second window.`),r}i.type_ensure(c.$gtype);export{c as AdwaitaApp,runAdwaitaApp,runApplication};
1
+ import"./_virtual/_rolldown/runtime.js";import{createAboutDialog as e}from"./about-dialog.js";import{installBundledIconTheme as t}from"./icon-theme.js";import n from"gi://Adw?version=1";import r from"gi://Gio?version=2.0";import i from"gi://Gtk?version=4.0";import a from"gi://Gdk?version=4.0";import o from"gi://GObject?version=2.0";import s from"system";import{installDevtools as c}from"@gjsify/devtools";var l=class AdwaitaApp extends n.Application{static{o.registerClass({GTypeName:`GjsifyAdwaitaApp`},AdwaitaApp)}constructor(e){super({application_id:e.applicationId,flags:e.flags??r.ApplicationFlags.DEFAULT_FLAGS}),this._window=null,this._options=e,this._initActions(),this.connect(`startup`,()=>this._onStartup()),this.connect(`activate`,()=>this._onActivate())}_initActions(){if(this._options.quitAction!==!1){let e=new r.SimpleAction({name:`quit`});e.connect(`activate`,()=>this.quit()),this.add_action(e),this.set_accels_for_action(`app.quit`,[`<primary>q`])}if(this._options.about||this._options.aboutAppdata){let e=new r.SimpleAction({name:`about`});e.connect(`activate`,()=>this._showAbout()),this.add_action(e)}}_onStartup(){this._installBundledIcons(),this._options.css&&this._loadCss(this._options.css),this._installDevtools(),this._options.onStartup?.(this)}_installBundledIcons(){let e=this._options.bundledIcons;e!==!1&&t(typeof e==`object`?e:{})}_loadCss(e){let t=a.Display.get_default();if(!t){console.error(`@gjsify/adwaita-app: no default display; CSS not applied`);return}let n=new i.CssProvider;n.load_from_string(e),i.StyleContext.add_provider_for_display(t,n,i.STYLE_PROVIDER_PRIORITY_APPLICATION)}_installDevtools(){let e=this._options.devtools;if(e===!1)return;let t=typeof e==`object`?e:{enabled:e||void 0};c(this,t)}_onActivate(){this._window||=this._options.createWindow(this),this._window.present()}_showAbout(){let t=this._options.aboutAppdata;if(t){e(t).present(this.get_active_window());return}let r=this._options.about;if(!r)return;let i=new n.AboutDialog({application_name:r.applicationName,application_icon:r.applicationIcon??this._options.applicationId});r.version&&(i.version=r.version),r.developerName&&(i.developer_name=r.developerName),r.website&&(i.website=r.website),r.issueUrl&&(i.issue_url=r.issueUrl),r.license&&(i.license=r.license),r.comments&&(i.comments=r.comments),r.copyright&&(i.copyright=r.copyright),r.developers?.length&&i.set_developers(r.developers),i.present(this.get_active_window())}};async function runAdwaitaApp(e){return runApplication(new l(e),[s.programInvocationName,...s.programArgs])}async function runApplication(e,t){let n=!1;try{e.register(null),n=e.get_is_remote()}catch{n=!1}let r=await e.runAsync(t);return n&&console.error(`${e.applicationId??`This application`} is already running — brought the existing instance to the front instead of opening a second window.`),r}o.type_ensure(l.$gtype);export{l as AdwaitaApp,runAdwaitaApp,runApplication};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{BUNDLED_ICON_COUNT as e,BUNDLED_ICON_GRESOURCE_BASE64 as t,BUNDLED_ICON_RESOURCE_PATH as n}from"./icons.generated.js";import r from"gi://Gdk?version=4.0";import i from"gi://Gio?version=2.0";import a from"gi://Gtk?version=4.0";import o from"gi://GLib?version=2.0";const s=`gjsify-adwaita-bundled`;let c=null;function registerBundledIconResource(){if(c)return c;let e=o.base64_decode(t);return c=i.Resource.new_from_data(new o.Bytes(e)),i.resources_register(c),c}function addBundledIconsToTheme(e){registerBundledIconResource(),e.add_resource_path(n)}function installBundledIconTheme(e={}){let t=e.prefer??`fallback`;if(t===`host`)return!1;let n=e.display===void 0?r.Display.get_default():e.display;return n?(addBundledIconsToTheme(a.IconTheme.get_for_display(n)),t===`bundled`&&a.Settings.get_for_display(n).set_property(`gtk-icon-theme-name`,s),!0):!1}export{e as BUNDLED_ICON_COUNT,n as BUNDLED_ICON_RESOURCE_PATH,s as BUNDLED_ICON_THEME_NAME,addBundledIconsToTheme,installBundledIconTheme,registerBundledIconResource};
1
+ import"./_virtual/_rolldown/runtime.js";import{BUNDLED_ICON_COUNT as e,BUNDLED_ICON_GRESOURCE_BASE64 as t,BUNDLED_ICON_RESOURCE_PATH as n}from"./icons.generated.js";import r from"gi://Gio?version=2.0";import i from"gi://GLib?version=2.0";import a from"gi://Gtk?version=4.0";import o from"gi://Gdk?version=4.0";const s=`gjsify-adwaita-bundled`;let c=null;function registerBundledIconResource(){if(c)return c;let e=i.base64_decode(t);return c=r.Resource.new_from_data(new i.Bytes(e)),r.resources_register(c),c}function addBundledIconsToTheme(e){registerBundledIconResource(),e.add_resource_path(n)}function installBundledIconTheme(e={}){let t=e.prefer??`fallback`;if(t===`host`)return!1;let n=e.display===void 0?o.Display.get_default():e.display;return n?(addBundledIconsToTheme(a.IconTheme.get_for_display(n)),t===`bundled`&&a.Settings.get_for_display(n).set_property(`gtk-icon-theme-name`,s),!0):!1}export{e as BUNDLED_ICON_COUNT,n as BUNDLED_ICON_RESOURCE_PATH,s as BUNDLED_ICON_THEME_NAME,addBundledIconsToTheme,installBundledIconTheme,registerBundledIconResource};
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{BUNDLED_ICON_COUNT as e,BUNDLED_ICON_RESOURCE_PATH as t}from"./icons.generated.js";import{BUNDLED_ICON_THEME_NAME as n,addBundledIconsToTheme as r,installBundledIconTheme as i,registerBundledIconResource as a}from"./icon-theme.js";import{AdwaitaApp as o,runAdwaitaApp as s,runApplication as c}from"./application.js";import{readAppDevHooks as l}from"./dev-hooks.js";import{confirmDialog as u,errorDialog as d}from"./dialogs.js";import{pickFile as f,saveFile as p}from"./file-dialog.js";import{createNavShell as m}from"./nav-shell.js";import{LoadToken as h,loadIntoStack as g}from"./view-loader.js";import{LoadingStack as _}from"./loading-stack.js";import{registerToastOverlay as v,showToast as y}from"./toast.js";import{SYSTEM_LOCALE_DIR as b,resolveLocaleDir as x}from"./locale-dir.js";import{initLocale as S}from"./locale.js";import{findNavItem as C,resolveInitialNavIndex as w}from"./nav-model.js";export{o as AdwaitaApp,e as BUNDLED_ICON_COUNT,t as BUNDLED_ICON_RESOURCE_PATH,n as BUNDLED_ICON_THEME_NAME,h as LoadToken,_ as LoadingStack,b as SYSTEM_LOCALE_DIR,r as addBundledIconsToTheme,u as confirmDialog,m as createNavShell,d as errorDialog,C as findNavItem,S as initLocale,i as installBundledIconTheme,g as loadIntoStack,f as pickFile,l as readAppDevHooks,a as registerBundledIconResource,v as registerToastOverlay,w as resolveInitialNavIndex,x as resolveLocaleDir,s as runAdwaitaApp,c as runApplication,p as saveFile,y as showToast};
1
+ import{appdataLocale as e,parseAppdata as t}from"./appdata.js";import{applyAppdataFields as n,buildAboutDialogFromAppdata as r,createAboutDialog as i,hasAppdataConstructor as a,licenseTypeFor as o}from"./about-dialog.js";import{BUNDLED_ICON_COUNT as s,BUNDLED_ICON_RESOURCE_PATH as c}from"./icons.generated.js";import{BUNDLED_ICON_THEME_NAME as l,addBundledIconsToTheme as u,installBundledIconTheme as d,registerBundledIconResource as f}from"./icon-theme.js";import{AdwaitaApp as p,runAdwaitaApp as m,runApplication as h}from"./application.js";import{readAppDevHooks as g}from"./dev-hooks.js";import{confirmDialog as _,errorDialog as v}from"./dialogs.js";import{pickFile as y,saveFile as b}from"./file-dialog.js";import{createNavShell as x}from"./nav-shell.js";import{LoadToken as S,loadIntoStack as C}from"./view-loader.js";import{LoadingStack as w}from"./loading-stack.js";import{registerToastOverlay as T,showToast as E}from"./toast.js";import{SYSTEM_LOCALE_DIR as D,resolveLocaleDir as O,systemLocaleDir as k}from"./locale-dir.js";import{initLocale as A}from"./locale.js";import{findNavItem as j,resolveInitialNavIndex as M}from"./nav-model.js";export{p as AdwaitaApp,s as BUNDLED_ICON_COUNT,c as BUNDLED_ICON_RESOURCE_PATH,l as BUNDLED_ICON_THEME_NAME,S as LoadToken,w as LoadingStack,D as SYSTEM_LOCALE_DIR,u as addBundledIconsToTheme,e as appdataLocale,n as applyAppdataFields,r as buildAboutDialogFromAppdata,_ as confirmDialog,i as createAboutDialog,x as createNavShell,v as errorDialog,j as findNavItem,a as hasAppdataConstructor,A as initLocale,d as installBundledIconTheme,o as licenseTypeFor,C as loadIntoStack,t as parseAppdata,y as pickFile,g as readAppDevHooks,f as registerBundledIconResource,T as registerToastOverlay,M as resolveInitialNavIndex,O as resolveLocaleDir,m as runAdwaitaApp,h as runApplication,b as saveFile,E as showToast,k as systemLocaleDir};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import e from"gi://Adw?version=1";import t from"gi://GObject?version=2.0";import n from"gi://Gtk?version=4.0";var r=class LoadingStack extends n.Stack{static{t.registerClass({GTypeName:`GjsifyLoadingStack`},LoadingStack)}constructor(t={}){super(t);let r=new n.Box({orientation:n.Orientation.VERTICAL,halign:n.Align.CENTER,valign:n.Align.CENTER,hexpand:!0,vexpand:!0});r.append(new e.Spinner),this.add_named(r,`loading`),this._content=new e.Bin({hexpand:!0,vexpand:!0}),this.add_named(this._content,`content`),this._error=new e.StatusPage({iconName:`dialog-error-symbolic`,title:`Something went wrong`}),this.add_named(this._error,`error`),this.set_visible_child_name(`loading`)}setContent(e){this._content.set_child(e)}setError(e,t){this._error.set_title(e),this._error.set_description(t??``)}showLoading(){this.set_visible_child_name(`loading`)}showContent(){this.set_visible_child_name(`content`)}showError(){this.set_visible_child_name(`error`)}};t.type_ensure(r.$gtype);export{r as LoadingStack};
1
+ import"./_virtual/_rolldown/runtime.js";import e from"gi://Adw?version=1";import t from"gi://Gtk?version=4.0";import n from"gi://GObject?version=2.0";var r=class LoadingStack extends t.Stack{static{n.registerClass({GTypeName:`GjsifyLoadingStack`},LoadingStack)}constructor(n={}){super(n);let r=new t.Box({orientation:t.Orientation.VERTICAL,halign:t.Align.CENTER,valign:t.Align.CENTER,hexpand:!0,vexpand:!0});r.append(new e.Spinner),this.add_named(r,`loading`),this._content=new e.Bin({hexpand:!0,vexpand:!0}),this.add_named(this._content,`content`),this._error=new e.StatusPage({iconName:`dialog-error-symbolic`,title:`Something went wrong`}),this.add_named(this._error,`error`),this.set_visible_child_name(`loading`)}setContent(e){this._content.set_child(e)}setError(e,t){this._error.set_title(e),this._error.set_description(t??``)}showLoading(){this.set_visible_child_name(`loading`)}showContent(){this.set_visible_child_name(`content`)}showError(){this.set_visible_child_name(`error`)}};n.type_ensure(r.$gtype);export{r as LoadingStack};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";const e=`/usr/share/locale`;function resolveLocaleDir(e={}){return nonEmpty(e.localeDir)??nonEmpty(e.env?.GJSIFY_LOCALE_DIR)??nonEmpty(e.fallbackDir)??`/usr/share/locale`}function nonEmpty(e){if(e===void 0)return;let t=e.trim();return t===``?void 0:t}export{e as SYSTEM_LOCALE_DIR,resolveLocaleDir};
1
+ import"./_virtual/_rolldown/runtime.js";const e=`/usr/share/locale`,t=[`darwin`,`win32`];function systemLocaleDir(n){return n!==void 0&&t.includes(n)?void 0:e}function resolveLocaleDir(e={}){return nonEmpty(e.localeDir)??nonEmpty(e.env?.GJSIFY_LOCALE_DIR)??nonEmpty(e.fallbackDir)??systemLocaleDir(e.platform)}function nonEmpty(e){if(e===void 0)return;let t=e.trim();return t===``?void 0:t}export{e as SYSTEM_LOCALE_DIR,resolveLocaleDir,systemLocaleDir};
package/lib/esm/locale.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{resolveLocaleDir as e}from"./locale-dir.js";import t from"gi://GLib?version=2.0";import n from"gettext";function initLocale(r,i={}){let a=e({...i,env:{GJSIFY_LOCALE_DIR:t.getenv(`GJSIFY_LOCALE_DIR`)??void 0}});return n.setlocale(n.LocaleCategory.ALL,``),n.bindtextdomain(r,a),n.textdomain(r),Object.assign(e=>n.dgettext(r,e),{plural:(e,t,i)=>n.dngettext(r,e,t,i),context:(e,t)=>n.dpgettext(r,e,t),localeDir:a,domain:r})}export{initLocale};
1
+ import"./_virtual/_rolldown/runtime.js";import{resolveLocaleDir as e}from"./locale-dir.js";import t from"gi://GLib?version=2.0";import n from"gettext";function initLocale(r,i={}){let a=e({...i,env:{GJSIFY_LOCALE_DIR:t.getenv(`GJSIFY_LOCALE_DIR`)??void 0},platform:typeof process>`u`?void 0:process.platform});return n.setlocale(n.LocaleCategory.ALL,``),a!==void 0&&n.bindtextdomain(r,a),n.textdomain(r),Object.assign(e=>n.dgettext(r,e),{plural:(e,t,i)=>n.dngettext(r,e,t,i),context:(e,t)=>n.dpgettext(r,e,t),localeDir:a,domain:r})}export{initLocale};
@@ -0,0 +1,127 @@
1
+ import Adw from 'gi://Adw?version=1';
2
+ import Gtk from 'gi://Gtk?version=4.0';
3
+ import { type AppdataFields } from './appdata.js';
4
+ /**
5
+ * The `Gtk.License` an SPDX id names, or `Gtk.License.CUSTOM` for one GTK does not know.
6
+ *
7
+ * CUSTOM is returned WITHOUT also setting a licence text, which is what upstream does. Putting
8
+ * the raw SPDX id in `Adw.AboutDialog:license` instead would be more informative and would
9
+ * therefore show a licence line on Windows that Linux does not show — the divergence this
10
+ * whole module exists to prevent.
11
+ */
12
+ export declare function licenseTypeFor(spdxId: string): Gtk.License;
13
+ /**
14
+ * Whether this GTK runtime has `Adw.AboutDialog.new_from_appdata()`.
15
+ *
16
+ * Asked as a QUESTION rather than discovered by calling and catching, for two reasons. A
17
+ * `try` around the call would also swallow a genuine failure — a metainfo file the parser
18
+ * rejects would silently become "the constructor is missing" and the app would ship a
19
+ * quietly degraded dialog forever. And it would not work anyway: libadwaita reports an
20
+ * unreadable or unparseable resource with `g_error()`, which ABORTS the process, and no
21
+ * catch runs after that.
22
+ *
23
+ * The `try` that IS here wraps the LOOKUP, because the absence has two shapes and only one
24
+ * of them is a plain `undefined`: node-gi reports an entry point missing from the typelib by
25
+ * throwing `no static method 'new_from_appdata'` at property access, which is exactly how the
26
+ * Windows 11 measurement in #1662 reads.
27
+ *
28
+ * `namespace` defaults to the real `Adw` and is a parameter so both shapes can be held
29
+ * against the real question on a platform that has neither. It substitutes a namespace and
30
+ * never means "no namespace": passing `undefined` is the default parameter and therefore asks
31
+ * about `Adw`.
32
+ */
33
+ export declare function hasAppdataConstructor(namespace?: unknown): boolean;
34
+ /** Where {@link createAboutDialog} may read the metainfo document from. */
35
+ export interface AppdataSource {
36
+ /**
37
+ * GResource path of the metainfo file, e.g.
38
+ * `/org/example/App/metainfo/org.example.App.metainfo.xml`.
39
+ *
40
+ * This is the only form the upstream constructor accepts, so an app that registers its
41
+ * metainfo as a resource takes the upstream path wherever it exists.
42
+ */
43
+ appdataResource?: string;
44
+ /**
45
+ * Filesystem path of the metainfo file, consulted when no resource answers.
46
+ *
47
+ * Upstream has no equivalent — and that is the point. A `gjsify ship` artifact may carry
48
+ * the metainfo beside the bundle rather than inside a GResource (a `.app` is dragged
49
+ * wherever the user likes, a Windows program directory is unzipped to an arbitrary path),
50
+ * and such an app would otherwise have no About dialog on ANY platform. Where the
51
+ * directory is remains the caller's question: this module takes a path, exactly as
52
+ * `locale-dir.ts` takes a directory rather than going looking for one.
53
+ */
54
+ appdataPath?: string;
55
+ }
56
+ /** Options for {@link createAboutDialog}. */
57
+ export interface CreateAboutDialogOptions extends AppdataSource {
58
+ /**
59
+ * Show the release notes of this exact version, matched by string equality — the second
60
+ * argument of `Adw.AboutDialog.new_from_appdata()`, with the same semantics.
61
+ */
62
+ releaseNotesVersion?: string;
63
+ /**
64
+ * Version shown in the dialog, overriding the metainfo's newest release.
65
+ *
66
+ * Worth passing. The metainfo answers this question only as well as it is maintained,
67
+ * and it is the one field where this module's reading and ministream's can disagree —
68
+ * ministream sorts releases by a dpkg-style version comparison, `appdata.ts` takes the
69
+ * first one written. An explicit version removes the question on every platform at once.
70
+ */
71
+ version?: string;
72
+ /** Icon name, overriding the one derived from `<id>`. */
73
+ applicationIcon?: string;
74
+ /**
75
+ * Build the dialog from parsed AppStream even where the upstream constructor exists.
76
+ *
77
+ * Not a test hook — or not only one. The parsed path is what every Windows user sees and
78
+ * what NO Linux or macOS run would otherwise execute, so a developer who wants to check
79
+ * their About dialog before shipping has no other way to look at it. The suite beside
80
+ * this file uses it for the same reason.
81
+ */
82
+ forceParsedAppdata?: boolean;
83
+ }
84
+ /**
85
+ * Set on `dialog` exactly what `populate_from_appdata` sets, and nothing else.
86
+ *
87
+ * The upstream list, read off `adw-about-dialog.c` rather than guessed: application icon
88
+ * (from `<id>`), application name, developer name, version, website, support url, issue url,
89
+ * licence TYPE, and the release notes of one named release. It sets no `comments`, no
90
+ * `copyright`, no `translator-credits` and no `developers` — AppStream's `<summary>` is not
91
+ * `Adw.AboutDialog:comments` and upstream never treats it as one — so neither does this.
92
+ * An app that wants those sets them on the returned dialog itself.
93
+ */
94
+ export declare function applyAppdataFields(dialog: Adw.AboutDialog, fields: AppdataFields, releaseNotesVersion?: string): Adw.AboutDialog;
95
+ /**
96
+ * Build the dialog from a metainfo document, without consulting libadwaita's constructor.
97
+ *
98
+ * This is the Windows path, exported so it is callable on a platform that has the constructor:
99
+ * a fallback only one OS can reach is a fallback no suite covers, and the class of defect that
100
+ * hides there is a restructured metainfo file that empties the About dialog on Windows while
101
+ * every Linux run stays green.
102
+ */
103
+ export declare function buildAboutDialogFromAppdata(xml: string, options?: {
104
+ releaseNotesVersion?: string;
105
+ locale?: string;
106
+ }): Adw.AboutDialog;
107
+ /**
108
+ * Create an `Adw.AboutDialog` from the application's AppStream metainfo.
109
+ *
110
+ * A drop-in for `Adw.AboutDialog.new_from_appdata(resource, version)` that works on Windows:
111
+ *
112
+ * ```ts
113
+ * const dialog = createAboutDialog({
114
+ * appdataResource: '/org/example/App/metainfo/org.example.App.metainfo.xml',
115
+ * releaseNotesVersion: PACKAGE_VERSION,
116
+ * version: PACKAGE_VERSION,
117
+ * });
118
+ * dialog.present(window);
119
+ * ```
120
+ *
121
+ * THE RESOURCE IS CHECKED BEFORE THE CONSTRUCTOR IS CALLED, and that is not a tidiness step.
122
+ * `populate_from_appdata` reports a resource it cannot read with `g_error()`, which aborts the
123
+ * process — a typo in a resource path would kill the application when the user opens About,
124
+ * with no exception for anyone to catch. Asking `Gio.resources_get_info` first turns that into
125
+ * a dialog assembled from whatever source did answer.
126
+ */
127
+ export declare function createAboutDialog(options?: CreateAboutDialogOptions): Adw.AboutDialog;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,38 @@
1
+ /** An opening (or self-closing) element. */
2
+ export interface XmlOpenTag {
3
+ kind: 'open';
4
+ /** Element name exactly as written, prefix included (`xml:lang` is an attribute, not this). */
5
+ name: string;
6
+ /** Attribute values with entities already decoded. */
7
+ attributes: Record<string, string>;
8
+ /** `true` for `<foo/>`, which emits no matching {@link XmlCloseTag}. */
9
+ selfClosing: boolean;
10
+ }
11
+ /** A closing element. */
12
+ export interface XmlCloseTag {
13
+ kind: 'close';
14
+ name: string;
15
+ }
16
+ /** Character data between elements, with entities already decoded. */
17
+ export interface XmlText {
18
+ kind: 'text';
19
+ text: string;
20
+ }
21
+ export type XmlToken = XmlOpenTag | XmlCloseTag | XmlText;
22
+ /**
23
+ * Resolve the entity references a metainfo file may contain.
24
+ *
25
+ * An UNKNOWN reference is returned verbatim rather than dropped. A metainfo file is
26
+ * hand-written and `&` is the character most often left unescaped in one; turning
27
+ * `Tom & Jerry` into `Tom ` would silently truncate a developer name, which is exactly
28
+ * the kind of Windows-only difference this module exists to prevent.
29
+ */
30
+ export declare function decodeXmlEntities(text: string): string;
31
+ /**
32
+ * Tokenise an XML document.
33
+ *
34
+ * Returns an array rather than a generator on purpose: a metainfo file is a few kilobytes,
35
+ * three bundler targets (`gjs`, `node`, library) have to agree on the output, and an array
36
+ * is the form that needs no iteration-protocol support from any of them.
37
+ */
38
+ export declare function scanXml(xml: string): XmlToken[];
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,78 @@
1
+ /** Fields {@link parseAppdata} reads. `null` means the document does not carry the field. */
2
+ export interface AppdataFields {
3
+ /** `<id>`, with the `.desktop` suffix resolved the way libadwaita resolves it. */
4
+ applicationId: string | null;
5
+ /** `<name>`, resolved against the locale. */
6
+ applicationName: string | null;
7
+ /** `<name>` inside `<developer>`, or the legacy `<developer_name>`. */
8
+ developerName: string | null;
9
+ /** `version` of the first `<release>` — see {@link parseAppdata} on ordering. */
10
+ version: string | null;
11
+ /** `<url type="homepage">`. */
12
+ website: string | null;
13
+ /** `<url type="help">`. */
14
+ supportUrl: string | null;
15
+ /** `<url type="bugtracker">`. */
16
+ issueUrl: string | null;
17
+ /** `<project_license>`, an SPDX id. */
18
+ license: string | null;
19
+ /**
20
+ * The `<description>` of the release named by `releaseNotesVersion`, as the Pango-ish
21
+ * markup `Adw.AboutDialog:release-notes` parses. `null` when no version was asked for,
22
+ * or when no release matches it.
23
+ */
24
+ releaseNotes: string | null;
25
+ }
26
+ /** Options for {@link parseAppdata}. */
27
+ export interface ParseAppdataOptions {
28
+ /**
29
+ * BCP-47 locale that translatable elements are resolved against. Default `C`, the
30
+ * untranslated source string. {@link appdataLocale} derives it from the platform.
31
+ */
32
+ locale?: string;
33
+ /**
34
+ * Which release's notes to extract, matched by EXACT string equality — the same
35
+ * `g_strcmp0` libadwaita uses, so no version-comparison semantics are implied and
36
+ * `1.0` never matches `1.0.0`.
37
+ */
38
+ releaseNotesVersion?: string;
39
+ }
40
+ /**
41
+ * How well `lang` answers for `locale`, as a count of matching BCP-47 segments; `0` is no
42
+ * match at all.
43
+ *
44
+ * Ported from `ms_locale_score`, including the property that is easy to get wrong: a
45
+ * mismatch INSIDE a segment scores 0 rather than crediting the segments before it, so
46
+ * `de-DE` against `de-AT` is 0 and against `de` is 1. Anything looser would let a Swiss
47
+ * German string win over the untranslated one for an Austrian user.
48
+ */
49
+ export declare function localeScore(locale: string, lang: string): number;
50
+ /**
51
+ * The BCP-47 locale AppStream matching uses, from `GLib.get_language_names()` and the
52
+ * environment.
53
+ *
54
+ * Ported from `ms_get_current_locale`, quirks included: the most specific name wins, `LANG`
55
+ * is consulted when that name carries no region (GLib answers `de` for `LANG=de_DE` in some
56
+ * configurations, and a `de-DE` translation must still win there), `.UTF-8` is dropped
57
+ * because AppStream assumes it, and an `@variant` becomes a BCP-47 script subtag.
58
+ */
59
+ export declare function appdataLocale(languageNames: readonly string[], env?: {
60
+ LANG?: string;
61
+ }): string;
62
+ /**
63
+ * Read an AppStream metainfo document.
64
+ *
65
+ * Mirrors `adw_about_dialog_new_from_appdata()`'s `populate_from_appdata`, whose whole field
66
+ * list is: application icon (from `<id>`), application name, developer name, version,
67
+ * website, support url, issue url, licence type, and the release notes of ONE named release.
68
+ * It sets nothing else — no `comments`, no `copyright`, no `translator-credits`, no
69
+ * `developers` — so neither does this.
70
+ *
71
+ * RELEASE ORDER IS THE ONE DIVERGENCE. ministream sorts `<releases>` by a dpkg-style version
72
+ * comparison and reports the highest; this takes the first `<release>` in document order and
73
+ * does not re-implement that comparison. AppStream requires releases to be written
74
+ * newest-first (`appstreamcli validate` reports `releases-not-in-order` otherwise), so the
75
+ * two agree on every valid document — and a caller that will not rely on that passes its own
76
+ * version to `createAboutDialog`, which overrides this on every platform alike.
77
+ */
78
+ export declare function parseAppdata(xml: string, options?: ParseAppdataOptions): AppdataFields;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -2,6 +2,7 @@ import Adw from 'gi://Adw?version=1';
2
2
  import Gio from 'gi://Gio?version=2.0';
3
3
  import Gtk from 'gi://Gtk?version=4.0';
4
4
  import { type InstallDevtoolsOptions } from '@gjsify/devtools';
5
+ import { type CreateAboutDialogOptions } from './about-dialog.js';
5
6
  import { type BundledIconThemeOptions } from './icon-theme.js';
6
7
  import type { AboutInfo } from './types.js';
7
8
  /** Options for {@link AdwaitaApp} / {@link runAdwaitaApp}. */
@@ -31,6 +32,17 @@ export interface AdwaitaAppOptions {
31
32
  bundledIcons?: boolean | BundledIconThemeOptions;
32
33
  /** When set, wires an `app.about` action opening an `Adw.AboutDialog`. */
33
34
  about?: AboutInfo;
35
+ /**
36
+ * Build the `app.about` dialog from the application's AppStream metainfo instead of from
37
+ * literal fields. Wins over {@link AdwaitaAppOptions.about} when both are given.
38
+ *
39
+ * Prefer it: the metainfo is already written, already translated and already installed, so
40
+ * restating its name, developer, licence and urls in `about` is a second copy that drifts
41
+ * — and the drift is invisible until somebody opens the dialog. See
42
+ * {@link CreateAboutDialogOptions}; it works where `Adw.AboutDialog.new_from_appdata()`
43
+ * does not exist.
44
+ */
45
+ aboutAppdata?: CreateAboutDialogOptions;
34
46
  /** Wire `app.quit` (`<primary>q`). Default `true`. */
35
47
  quitAction?: boolean;
36
48
  /**
@@ -7,6 +7,10 @@ export type { NavShell, NavShellOptions } from './nav-shell.js';
7
7
  export { LoadToken, loadIntoStack } from './view-loader.js';
8
8
  export type { LoadIntoStackOptions } from './view-loader.js';
9
9
  export { LoadingStack } from './loading-stack.js';
10
+ export { applyAppdataFields, buildAboutDialogFromAppdata, createAboutDialog, hasAppdataConstructor, licenseTypeFor, } from './about-dialog.js';
11
+ export type { AppdataSource, CreateAboutDialogOptions } from './about-dialog.js';
12
+ export { appdataLocale, parseAppdata } from './appdata.js';
13
+ export type { AppdataFields, ParseAppdataOptions } from './appdata.js';
10
14
  export { confirmDialog, errorDialog } from './dialogs.js';
11
15
  export type { ConfirmOptions } from './dialogs.js';
12
16
  export { registerToastOverlay, showToast } from './toast.js';
@@ -14,7 +18,7 @@ export { pickFile, saveFile } from './file-dialog.js';
14
18
  export type { FileFilterSpec, PickFileOptions } from './file-dialog.js';
15
19
  export { initLocale } from './locale.js';
16
20
  export type { InitLocaleOptions, Translator } from './locale.js';
17
- export { SYSTEM_LOCALE_DIR, resolveLocaleDir } from './locale-dir.js';
21
+ export { SYSTEM_LOCALE_DIR, resolveLocaleDir, systemLocaleDir } from './locale-dir.js';
18
22
  export type { ResolveLocaleDirOptions } from './locale-dir.js';
19
23
  export { readAppDevHooks } from './dev-hooks.js';
20
24
  export type { AppDevHooks, ReadAppDevHooksOptions } from './dev-hooks.js';
@@ -1,5 +1,26 @@
1
- /** `bindtextdomain`'s directory for a system install, when nothing else names one. */
1
+ /**
2
+ * `bindtextdomain`'s directory for a system install — **on Linux**, where it exists.
3
+ *
4
+ * Deliberately not a cross-platform answer: see {@link systemLocaleDir}.
5
+ */
2
6
  export declare const SYSTEM_LOCALE_DIR = "/usr/share/locale";
7
+ /**
8
+ * The system catalogue directory for `platform`, or `undefined` where there is none.
9
+ *
10
+ * `undefined` for **darwin** and **win32**, and the darwin half is the reason this function
11
+ * exists rather than a constant. `/usr/share/locale` does not exist on Windows, so naming it
12
+ * there was merely useless — but it DOES exist on macOS, holding Apple's own locale data and
13
+ * never an application's catalogues. So the old unconditional fallback bound a real directory
14
+ * that could not resolve a single msgid: no error, no translation, and `Translator.localeDir`
15
+ * reporting a plausible path as if the lookup had somewhere to go. A directory that is absent
16
+ * is the better answer of the two, because it is the true one.
17
+ *
18
+ * Any OTHER platform keeps {@link SYSTEM_LOCALE_DIR}. `process.platform` has more values than
19
+ * the three desktops this project targets — `freebsd`, `openbsd`, `sunos`, … — and it is also
20
+ * `undefined` in a GJS bundle built with `--globals none`. Answering those with "no directory"
21
+ * would change behaviour nobody measured; answering them as today changes nothing.
22
+ */
23
+ export declare function systemLocaleDir(platform?: string): string | undefined;
3
24
  export interface ResolveLocaleDirOptions {
4
25
  /** Wins over everything — a dev tree (`dist/locale`), or a test fixture. */
5
26
  localeDir?: string;
@@ -7,12 +28,19 @@ export interface ResolveLocaleDirOptions {
7
28
  env?: Record<string, string | undefined>;
8
29
  /** Used when neither the option nor the environment names a directory. */
9
30
  fallbackDir?: string;
31
+ /** `process.platform`, for the last step only. Omitted keeps the Linux answer. */
32
+ platform?: string;
10
33
  }
11
34
  /**
12
- * Resolve the directory to bind a text domain to.
35
+ * Resolve the directory to bind a text domain to, or `undefined` when there is none to bind.
13
36
  *
14
37
  * Precedence: explicit option, then `GJSIFY_LOCALE_DIR` (exported by the `gjsify ship` launcher,
15
38
  * which is the only party that knows whether the payload became `/usr`, a `--prefix` tree or
16
- * `/app`), then the caller's fallback, then the system directory.
39
+ * `/app`), then the caller's fallback, then the platform's system directory — if it has one.
40
+ *
41
+ * A shipped app reaches an answer at step two on every OS, so `undefined` means the app carries
42
+ * no catalogues AND the platform has no system ones. `locale.ts` then skips `bindtextdomain`
43
+ * rather than binding somewhere arbitrary, and every lookup returns its msgid — which is what an
44
+ * untranslated application is supposed to do.
17
45
  */
18
- export declare function resolveLocaleDir(options?: ResolveLocaleDirOptions): string;
46
+ export declare function resolveLocaleDir(options?: ResolveLocaleDirOptions): string | undefined;
@@ -7,8 +7,13 @@ export interface Translator {
7
7
  plural(singular: string, plural: string, n: number): string;
8
8
  /** Disambiguating lookup, for a msgid whose meaning depends on where it appears. */
9
9
  context(context: string, msgid: string): string;
10
- /** The bound directory and domain — for a diagnostic line, never for a lookup. */
11
- readonly localeDir: string;
10
+ /**
11
+ * The bound directory and domain — for a diagnostic line, never for a lookup.
12
+ *
13
+ * `undefined` when nothing was bound: no catalogues were staged and the platform has no
14
+ * system directory either (macOS, Windows). Every lookup then returns its msgid.
15
+ */
16
+ readonly localeDir: string | undefined;
12
17
  readonly domain: string;
13
18
  }
14
19
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-app",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Native Adwaita application shell for GJS/GTK apps — Adw.Application lifecycle (runAsync + devtools + CSS), a data-driven NavigationSplitView nav shell, an async-view mounter, and promise dialog/toast/file helpers",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -42,19 +42,19 @@
42
42
  "framework"
43
43
  ],
44
44
  "dependencies": {
45
- "@girs/adw-1": "5.0.0",
46
- "@girs/gdk-4.0": "5.0.0",
47
- "@girs/gio-2.0": "5.0.0",
48
- "@girs/gjs": "5.0.0",
49
- "@girs/glib-2.0": "5.0.0",
50
- "@girs/gobject-2.0": "5.0.0",
51
- "@girs/gtk-4.0": "5.0.0",
52
- "@gjsify/devtools": "^0.51.0"
45
+ "@girs/adw-1": "5.4.0",
46
+ "@girs/gdk-4.0": "5.4.0",
47
+ "@girs/gio-2.0": "5.4.0",
48
+ "@girs/gjs": "5.4.0",
49
+ "@girs/glib-2.0": "5.4.0",
50
+ "@girs/gobject-2.0": "5.4.0",
51
+ "@girs/gtk-4.0": "5.4.0",
52
+ "@gjsify/devtools": "^0.52.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@gjsify/cli": "^0.51.0",
56
- "@gjsify/storybook": "^0.51.0",
57
- "@gjsify/unit": "^0.51.0",
55
+ "@gjsify/cli": "^0.52.0",
56
+ "@gjsify/storybook": "^0.52.0",
57
+ "@gjsify/unit": "^0.52.0",
58
58
  "@types/node": "^25.9.2",
59
59
  "typescript": "^6.0.3"
60
60
  },
@@ -66,6 +66,15 @@
66
66
  "nativescript": "none"
67
67
  },
68
68
  "tier": 2,
69
+ "os": {
70
+ "linux": "supported",
71
+ "darwin": "partial",
72
+ "win32": "partial"
73
+ },
74
+ "osNotes": {
75
+ "darwin": "`locale.ts` reads `process.platform` to decide whether `initLocale` binds a text domain at all, so this package owes an OS claim (ADR 0018). The DECISION is complete and asserted from any host: the platform is an argument to `systemLocaleDir(platform)`, and `locale-dir.spec.ts` drives darwin, win32, linux, an unmeasured platform and an absent `process.platform` through it from Linux. What no runner observes is the SELECTION — `locale.ts` reading the real `process.platform` on a real Mac and skipping `bindtextdomain`. NO LEG OF THIS PACKAGE'S SUITE HAS EVER RUN ON DARWIN: `gtk-os-suites.yml` runs `@gjsify/gtk-host` (gating) and `@gjsify/react-native` (probe) on the darwin legs and nothing else, and `macos-suites.yml` is a Node-pillar workflow that states outright that `gtk4` is not installed. This is exactly the position `@gjsify/path` records for win32 — semantics exercised from every host, selection observable nowhere — and it takes the same word. Promote from the first run of this package's own suite on a darwin leg.",
76
+ "win32": "As darwin for why a claim is owed and why it is not `supported`: `locale-dir.spec.ts` asserts the win32 answer from Linux, and nothing runs this package's suite on a Windows host — `gtk-os-suites.yml`'s win32 leg carries `@gjsify/gtk-host` and `@gjsify/react-native` as probes and nothing else. One thing is BETTER here than on darwin and is worth recording so the promotion is not blocked on the wrong obstacle: there is no GJS host on Windows at all (ADR 0024 § 4), so the route is Node, where `process.platform` comes from the host itself and is authoritative — the unexercised `uname` probe that keeps `@gjsify/process` at win32 `partial` is not on this path. What is unexercised is this package, not the platform read underneath it. Promote from the first run of this package's own suite on a win32 leg."
77
+ },
69
78
  "storybook": {
70
79
  "applicationId": "org.gjsify.AdwaitaAppStorybook",
71
80
  "title": "Adwaita App",