@gjsify/adwaita-app 0.51.1 → 0.53.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.
Files changed (45) hide show
  1. package/README.md +191 -2
  2. package/lib/esm/about-dialog.js +1 -0
  3. package/lib/esm/appdata-xml.js +1 -0
  4. package/lib/esm/appdata.js +2 -0
  5. package/lib/esm/appearance/darwin.js +1 -0
  6. package/lib/esm/appearance/gnome-settings.js +1 -0
  7. package/lib/esm/appearance/index.js +1 -0
  8. package/lib/esm/appearance/mapping.js +1 -0
  9. package/lib/esm/appearance/portal.js +1 -0
  10. package/lib/esm/appearance/reader.js +1 -0
  11. package/lib/esm/appearance/run-tool.js +1 -0
  12. package/lib/esm/appearance/win32.js +1 -0
  13. package/lib/esm/application.js +1 -1
  14. package/lib/esm/icon-theme.js +1 -1
  15. package/lib/esm/index.js +1 -1
  16. package/lib/esm/loading-stack.js +1 -1
  17. package/lib/esm/locale-dir.js +1 -1
  18. package/lib/esm/locale.js +1 -1
  19. package/lib/esm/nav-shell.js +1 -1
  20. package/lib/esm/system-accent.js +1 -0
  21. package/lib/types/about-dialog.d.ts +127 -0
  22. package/lib/types/about-dialog.spec.d.ts +2 -0
  23. package/lib/types/appdata-xml.d.ts +38 -0
  24. package/lib/types/appdata-xml.spec.d.ts +2 -0
  25. package/lib/types/appdata.d.ts +78 -0
  26. package/lib/types/appdata.spec.d.ts +2 -0
  27. package/lib/types/appearance/darwin.d.ts +5 -0
  28. package/lib/types/appearance/gnome-settings.d.ts +23 -0
  29. package/lib/types/appearance/index.d.ts +9 -0
  30. package/lib/types/appearance/mapping.d.ts +102 -0
  31. package/lib/types/appearance/mapping.spec.d.ts +2 -0
  32. package/lib/types/appearance/portal.d.ts +25 -0
  33. package/lib/types/appearance/reader.d.ts +26 -0
  34. package/lib/types/appearance/reader.spec.d.ts +2 -0
  35. package/lib/types/appearance/run-tool.d.ts +6 -0
  36. package/lib/types/appearance/win32.d.ts +5 -0
  37. package/lib/types/application.d.ts +12 -0
  38. package/lib/types/index.d.ts +8 -2
  39. package/lib/types/locale-dir.d.ts +32 -4
  40. package/lib/types/locale.d.ts +7 -2
  41. package/lib/types/nav-shell.d.ts +27 -1
  42. package/lib/types/nav-shell.spec.d.ts +2 -0
  43. package/lib/types/system-accent.d.ts +30 -0
  44. package/lib/types/system-accent.spec.d.ts +2 -0
  45. package/package.json +31 -13
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,15 +129,150 @@ 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
134
217
  `Adw.NavigationSplitView` (sidebar `Gtk.ListBox` + content `Gtk.Stack`) into
135
218
  `window` (which owns the responsive `Adw.Breakpoint`, default `max-width: 720px`).
136
- Returns `{ widget, stack, contentHeader, selectById, selectByIndex }`.
219
+ Returns `{ widget, stack, contentHeader, setItems, selectById, selectByIndex }`.
137
220
  - `resolveInitialNavIndex(items, wantedId?)`, `findNavItem(items, id)` — pure
138
221
  helpers (e.g. to turn a `${PREFIX}_VIEW` dev hook into a start index).
139
222
 
223
+ `NavShellOptions` beyond `items` / `onSelect`: `sidebarTitle`, `sidebarHeaderStart`,
224
+ `sidebarHeaderEnd`, `collapseWidth`, and the three below.
225
+
226
+ #### A sidebar that fills asynchronously
227
+
228
+ `setItems(items)` rebuilds the rows in place and replaces the items behind them, so
229
+ `row-selected` → `onSelect(item, index)` and `selectById` / `selectByIndex` always
230
+ resolve against the items you last passed. A hand-rolled `append` does not: inserting a
231
+ row shifts every row below it, and an index read against the array the shell was built
232
+ with hands `onSelect` the wrong item with no error anywhere.
233
+
234
+ ```ts
235
+ const shell = createNavShell(this, {
236
+ items: [],
237
+ onSelect: (item) => shell.stack.set_visible_child_name(item.id),
238
+ });
239
+
240
+ loadInbox().then((threads) => {
241
+ shell.setItems(threads.map((t) => ({ id: t.id, label: t.subject, icon: 'mail-inbox-symbolic' })));
242
+ shell.selectById(threads[0].id);
243
+ });
244
+ ```
245
+
246
+ The `Gtk.Stack` stays yours: `setItems` does not touch it, so a page outliving its item
247
+ is your call to keep or drop.
248
+
249
+ The selected id survives a `setItems` if the new items still carry it, and `onSelect`
250
+ does **not** fire — a rebuild is not a selection, and re-announcing the same item would
251
+ make `onSelect` fire on every list update. An item that is gone leaves nothing selected;
252
+ `selectById` is how you pick the replacement deliberately.
253
+
254
+ #### Group headers
255
+
256
+ `headerFunc(row, before)` goes straight into `Gtk.ListBox.set_header_func`. `before` is
257
+ the `NavItem` of the row above `row`, or `null` for the first row — a header is a
258
+ comparison against the previous section, so that is what it hands you. Return `null` to
259
+ draw no header. (The `NavItem` is passed, not the row: the row is right there as `row`.)
260
+
261
+ ```ts
262
+ createNavShell(this, {
263
+ items: NAV,
264
+ onSelect: (item) => shell.stack.set_visible_child_name(item.id),
265
+ headerFunc: (_row, before) =>
266
+ before && before.id.startsWith('pinned/') ? null : new Gtk.Label({ cssClasses: ['heading'], label: 'Threads' }),
267
+ });
268
+ ```
269
+
270
+ #### Bottom bars
271
+
272
+ `sidebarBottomBar` and `contentBottomBar` are the respective `Adw.ToolbarView`s'
273
+ `add_bottom_bar` — a control under the sidebar list, and one under the content stack
274
+ rather than inside it.
275
+
140
276
  ### Async view mounting
141
277
 
142
278
  - `LoadToken` + `loadIntoStack({ stack, token, load, fill, onError? })` — show a
@@ -155,12 +291,65 @@ has nothing behind it but GTK's own builtins. Register your own with
155
291
  - `pickFile(parent, { title?, filters? }): Promise<string | null>` /
156
292
  `saveFile(parent, { title?, filters?, initialName? }): Promise<string | null>`
157
293
 
294
+ ### macOS system accent
295
+
296
+ A GTK window needs nothing here: libadwaita ≥ 1.6 follows the macOS accent itself,
297
+ and `Adw.StyleManager:accent-color` is read-only. This is for what has no libadwaita
298
+ in the process — a web view (WebKit resolves CSS `AccentColor` to blue whatever the
299
+ setting) or a headless companion process — and it is GTK-free, so import it from
300
+ the `@gjsify/adwaita-app/system-accent` subpath there.
301
+
302
+ - `readMacosAccentColor(): AdwAccentColorName | null` — runs
303
+ `defaults read -g AppleAccentColor` and maps it with `@gjsify/adwaita-core`'s
304
+ `adwAccentFromAppleAccentColor` (graphite → `slate`, Multicolor → `blue`).
305
+ `null` off macOS or for a value macOS does not define.
306
+ - `onMacosAccentColorChanged(listener, { intervalSeconds? }): () => void` — calls
307
+ `listener` with the new accent when it changes; re-reads every 5 s by default
308
+ (there is no change signal GI can reach). Hold and call the returned unsubscribe.
309
+
158
310
  ### Dev hooks
159
311
 
160
312
  - `readAppDevHooks({ prefix, env? }): { view?, file?, debug }` — the
161
313
  `${PREFIX}_VIEW` / `${PREFIX}_FILE` / `${PREFIX}_DEBUG` env pattern (e.g.
162
314
  `MYAPP_VIEW=reports myapp` to open straight to a view in dev).
163
315
 
316
+ ## Desktop appearance (`@gjsify/adwaita-app/appearance`)
317
+
318
+ The desktop's accent colour and colour-scheme preference, for a process that never
319
+ opens a window: a server behind a web UI, a bridge, a CLI. A GTK app does not need
320
+ it, because `Adw.StyleManager` already applies the same values to its widgets. This
321
+ subpath loads only Gio and GLib ([ADR 0078](../../../docs/adr/0078-the-desktop-appearance-reaches-a-web-page-through-a-handoff.md)).
322
+
323
+ ```ts
324
+ import { readDesktopAppearance, renderAppearanceMeta, watchDesktopAppearance } from '@gjsify/adwaita-app/appearance';
325
+
326
+ const appearance = await readDesktopAppearance();
327
+ // { accent: 'purple', accentRgb: '#9141ac', colorScheme: 'dark' }; absent fields are unknown
328
+
329
+ const head = renderAppearanceMeta(appearance);
330
+ // <meta name="adw-accent" content="purple">
331
+ // <meta name="adw-color-scheme" content="dark">
332
+
333
+ const stop = watchDesktopAppearance((next) => broadcast(JSON.stringify(next))); // hold `stop`
334
+ ```
335
+
336
+ A web server must `listen()` before the module's first top-level await. On GJS a server
337
+ started after one exits as soon as the module settles; see `status/open-todos/README.md`.
338
+ [`examples/node/net-adwaita-appearance`](../../../examples/node/net-adwaita-appearance)
339
+ serves a complete page that way.
340
+
341
+ A reported colour is snapped to one of libadwaita's nine accents with
342
+ `nearestAccent`, as libadwaita does. The reader never throws. Sources per OS:
343
+
344
+ | OS | Reads | Follows changes by | Measured |
345
+ |---|---|---|---|
346
+ | Linux (GNOME, KDE, Flatpak) | XDG Settings portal `org.freedesktop.appearance`, then GSettings `org.gnome.desktop.interface` for what the portal left unknown, only when `XDG_CURRENT_DESKTOP` names GNOME (elsewhere the schema answers its defaults) | `SettingChanged`, `changed::` | yes, in CI, against a fake portal on a peer D-Bus connection |
347
+ | Windows | `reg.exe query`: `Explorer\Accent AccentPalette` (else `DWM AccentColor`), `Personalize AppsUseLightTheme` | polling every 3 s | mapping only; reg.exe not run on Windows |
348
+ | macOS | `defaults read -g AppleAccentColor / AppleInterfaceStyle / AppleInterfaceStyleSwitchesAutomatically` | a debounced monitor on `~/Library/Preferences` | mapping only; not run end to end on a Mac |
349
+
350
+ On macOS with Auto appearance and no `AppleInterfaceStyle` key, `colorScheme` stays
351
+ absent: the system switches by time of day, so a missing key does not prove light.
352
+
164
353
  ## See also
165
354
 
166
355
  - [`@gjsify/devtools`](../devtools) — the DBus control plane wired in on startup.
@@ -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};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import{appearanceFromMacDefaults as e}from"./mapping.js";import{runTool as t}from"./run-tool.js";import n from"gi://Gio?version=2.0";import r from"gi://GLib?version=2.0";const i=`.GlobalPreferences.plist`,a=750;async function readDefault(e){let n=await t([`defaults`,`read`,`-g`,e]);return n===null?null:n.trim()}async function readDarwinAppearance(){let[t,n,r]=await Promise.all([readDefault(`AppleAccentColor`),readDefault(`AppleInterfaceStyle`),readDefault(`AppleInterfaceStyleSwitchesAutomatically`)]);return e({appleAccentColor:t,appleInterfaceStyle:n,appleInterfaceStyleSwitchesAutomatically:r})}function watchDarwinAppearance(e){let t=n.File.new_for_path(r.build_filenamev([r.get_home_dir(),`Library`,`Preferences`])),a;try{a=t.monitor_directory(n.FileMonitorFlags.WATCH_MOVES,null)}catch{return()=>void 0}let o=0,s=a.connect(`changed`,(t,n,a)=>{n.get_basename()!==i&&a?.get_basename()!==i||(o&&r.source_remove(o),o=r.timeout_add(r.PRIORITY_DEFAULT,750,()=>(o=0,e(),r.SOURCE_REMOVE)))});return()=>{o&&r.source_remove(o),a.disconnect(s),a.cancel()}}export{a as DARWIN_DEBOUNCE_MS,readDarwinAppearance,watchDarwinAppearance};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import{appearanceFromGnomeSettings as e}from"./mapping.js";import t from"gi://Gio?version=2.0";const n=`org.gnome.desktop.interface`;function gnomeSettingsSource(){let e=t.SettingsSchemaSource.get_default()?.lookup(n,!0);if(!e)return null;let r=[`accent-color`,`color-scheme`].filter(t=>e.has_key(t));return r.length===0?null:{settings:new t.Settings({settings_schema:e}),keys:r}}function readGnomeSettings(t){let read=e=>t.keys.includes(e)?t.settings.get_string(e):null;return e({accentColor:read(`accent-color`),colorScheme:read(`color-scheme`)})}function watchGnomeSettings(e,t){let n=e.keys.map(n=>e.settings.connect(`changed::${n}`,()=>t()));return()=>{for(let t of n)e.settings.disconnect(t)}}export{n as GNOME_INTERFACE_SCHEMA,gnomeSettingsSource,readGnomeSettings,watchGnomeSettings};
@@ -0,0 +1 @@
1
+ import{MACOS_ACCENT_COLORS as e,appearanceFromGnomeSettings as t,appearanceFromMacDefaults as n,appearanceFromPortal as r,appearanceFromWindowsRegistry as i,isGnomeDesktop as a,mergeAppearance as o,parseRegQuery as s,sameAppearance as c}from"./mapping.js";import{readPortalAppearance as l,watchPortalAppearance as u}from"./portal.js";import{readDesktopAppearance as d,watchDesktopAppearance as f}from"./reader.js";import{parseDesktopAppearance as p,renderAppearanceMeta as m}from"@gjsify/adwaita-core";export{e as MACOS_ACCENT_COLORS,t as appearanceFromGnomeSettings,n as appearanceFromMacDefaults,r as appearanceFromPortal,i as appearanceFromWindowsRegistry,a as isGnomeDesktop,o as mergeAppearance,p as parseDesktopAppearance,s as parseRegQuery,d as readDesktopAppearance,l as readPortalAppearance,m as renderAppearanceMeta,c as sameAppearance,f as watchDesktopAppearance,u as watchPortalAppearance};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import{formatAdwRgb as e,isAdwAccentColorName as t,nearestAccent as n,parseAdwRgb as r}from"@gjsify/adwaita-core";function fromRgb(t){return{accent:n(t),accentRgb:e(t)}}function compact(e){let t={};return e.accent&&(t.accent=e.accent),e.accentRgb&&(t.accentRgb=e.accentRgb),e.colorScheme&&(t.colorScheme=e.colorScheme),t}function mergeAppearance(e,t){let n=e.accent!==void 0;return compact({accent:e.accent??t.accent,accentRgb:n?e.accentRgb:t.accentRgb,colorScheme:e.colorScheme??t.colorScheme})}function sameAppearance(e,t){return e.accent===t.accent&&e.accentRgb===t.accentRgb&&e.colorScheme===t.colorScheme}function appearanceFromPortal(e){let t=e[`color-scheme`],n=t===0?`no-preference`:t===1?`dark`:t===2?`light`:void 0,r=e[`accent-color`],i;return Array.isArray(r)&&r.length===3&&r.every(e=>typeof e==`number`&&e>=0&&e<=1)&&(i={red:r[0],green:r[1],blue:r[2]}),compact({...i?fromRgb(i):{},colorScheme:n})}function isGnomeDesktop(e){return(e??``).split(`:`).some(e=>e.trim().toUpperCase()===`GNOME`)}function appearanceFromGnomeSettings(e){return compact({accent:e.accentColor&&t(e.accentColor)?e.accentColor:void 0,colorScheme:e.colorScheme==="default"?`no-preference`:e.colorScheme===`prefer-dark`?`dark`:e.colorScheme===`prefer-light`?`light`:void 0})}function appearanceFromWindowsRegistry(e){let t,n=e.accentPalette;if(n&&n.length>=16)t={red:n[12]/255,green:n[13]/255,blue:n[14]/255};else if(typeof e.dwmAccentColor==`number`){let n=e.dwmAccentColor;t={red:(n&255)/255,green:(n>>>8&255)/255,blue:(n>>>16&255)/255}}let r=e.appsUseLightTheme,i=r===0?`dark`:r===1?`light`:void 0;return compact({...t?fromRgb(t):{},colorScheme:i})}const i={"-1":`#8c8c8c`,0:`#ff5257`,1:`#f7821a`,2:`#ffc602`,3:`#62ba46`,4:`#017bff`,5:`#a550a7`,6:`#f8509e`};function appearanceFromMacDefaults(e){let t=e.appleAccentColor?.trim(),n=t==null||t===``?i[4]:i[t],a=n?r(n):null,o=e.appleInterfaceStyle?.trim(),s=e.appleInterfaceStyleSwitchesAutomatically?.trim()===`1`,c=o===`Dark`?`dark`:o||s?void 0:`light`;return compact({...a?fromRgb(a):{},colorScheme:c})}function parseRegQuery(e,t){for(let n of e.split(/\r?\n/)){let e=/^\s+(.+?)\s{2,}(REG_\w+)\s{2,}(\S*)\s*$/.exec(n);if(!e||e[1]!==t)continue;let[,,r,i]=e;if(r===`REG_DWORD`){let e=Number.parseInt(i,16);return Number.isFinite(e)?e>>>0:null}if(r===`REG_BINARY`){if(i.length%2!=0||!/^[0-9a-f]*$/i.test(i))return null;let e=new Uint8Array(i.length/2);for(let t=0;t<e.length;t++)e[t]=Number.parseInt(i.slice(t*2,t*2+2),16);return e}return null}return null}export{i as MACOS_ACCENT_COLORS,appearanceFromGnomeSettings,appearanceFromMacDefaults,appearanceFromPortal,appearanceFromWindowsRegistry,isGnomeDesktop,mergeAppearance,parseRegQuery,sameAppearance};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import{appearanceFromPortal as e}from"./mapping.js";import t from"gi://Gio?version=2.0";import n from"gi://GLib?version=2.0";const r=`org.freedesktop.portal.Desktop`,i=`/org/freedesktop/portal/desktop`,a=`org.freedesktop.portal.Settings`,o=`org.freedesktop.appearance`;function readPortalAppearance(r){return new Promise(s=>{r.connection.call(r.busName,i,a,`ReadAll`,new n.Variant(`(as)`,[[o]]),new n.VariantType(`(a{sa{sv}})`),t.DBusCallFlags.NONE,5e3,null,(t,n)=>{try{let[r]=t.call_finish(n).recursiveUnpack();s(e(r[`org.freedesktop.appearance`]??{}))}catch{s(null)}})})}function watchPortalAppearance(e,n){let r=e.connection.signal_subscribe(e.busName,a,`SettingChanged`,i,o,t.DBusSignalFlags.NONE,()=>n());return()=>e.connection.signal_unsubscribe(r)}function sessionPortalEndpoint(){return new Promise(e=>{t.bus_get(t.BusType.SESSION,null,(n,i)=>{try{e({connection:t.bus_get_finish(i),busName:r})}catch{e(null)}})})}export{o as APPEARANCE_NAMESPACE,r as PORTAL_BUS_NAME,i as PORTAL_OBJECT_PATH,a as PORTAL_SETTINGS_INTERFACE,readPortalAppearance,sessionPortalEndpoint,watchPortalAppearance};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import{isGnomeDesktop as e,mergeAppearance as t,sameAppearance as n}from"./mapping.js";import{readDarwinAppearance as r,watchDarwinAppearance as i}from"./darwin.js";import{gnomeSettingsSource as a,readGnomeSettings as o,watchGnomeSettings as s}from"./gnome-settings.js";import{readPortalAppearance as c,sessionPortalEndpoint as l,watchPortalAppearance as u}from"./portal.js";import{readWin32Appearance as d,watchWin32Appearance as f}from"./win32.js";import p from"gi://GLib?version=2.0";function currentPlatform(e){return e.platform?e.platform:typeof process>`u`?void 0:process.platform}function linuxBackend(n){let r,sources=()=>(r??=(async()=>({portal:n.portal===void 0?await l():n.portal,settings:e(n.currentDesktop===void 0?p.getenv(`XDG_CURRENT_DESKTOP`):n.currentDesktop)?n.gnomeSettings===void 0?a():n.gnomeSettings:null}))(),r);return{async read(){let{portal:e,settings:n}=await sources(),r=e?await c(e):null,i=n?o(n):{};return t(r??{},i)},watch(e){let t=[],n=!1;return sources().then(({portal:r,settings:i})=>{n||(r&&t.push(u(r,e)),i&&t.push(s(i,e)))}),()=>{n=!0;for(let e of t)e();t=[]}}}}function backendFor(e){let t=currentPlatform(e);return t===`win32`?{read:d,watch:f}:t===`darwin`?{read:r,watch:i}:linuxBackend(e)}function readDesktopAppearance(e={}){return backendFor(e).read()}function watchDesktopAppearance(e,t={}){let r=backendFor(t),i,a=!1,o=Promise.resolve(),refresh=()=>{o=o.then(async()=>{let t=await r.read();a||i&&n(i,t)||(i=t,e(t))})},s=r.watch(refresh);return refresh(),()=>{a=!0,s()}}export{readDesktopAppearance,watchDesktopAppearance};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import e from"gi://Gio?version=2.0";function runTool(t){return new Promise(n=>{let r;try{r=e.Subprocess.new([...t],e.SubprocessFlags.STDOUT_PIPE|e.SubprocessFlags.STDERR_SILENCE)}catch{n(null);return}r.communicate_utf8_async(null,null,(e,t)=>{try{let[,r]=e.communicate_utf8_finish(t);n(e.get_successful()?r??``:null)}catch{n(null)}})})}export{runTool};
@@ -0,0 +1 @@
1
+ import"../_virtual/_rolldown/runtime.js";import{appearanceFromWindowsRegistry as e,parseRegQuery as t}from"./mapping.js";import{runTool as n}from"./run-tool.js";import r from"gi://GLib?version=2.0";const i=3;async function queryValue(e,r){let i=await n([`reg`,`query`,e,`/v`,r]);return i===null?null:t(i,r)}async function readWin32Appearance(){let[t,n]=await Promise.all([queryValue(`HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Accent`,`AccentPalette`),queryValue(`HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize`,`AppsUseLightTheme`)]),r=t instanceof Uint8Array?t:null,i=r?null:await queryValue(`HKCU\\Software\\Microsoft\\Windows\\DWM`,`AccentColor`);return e({accentPalette:r,dwmAccentColor:typeof i==`number`?i:null,appsUseLightTheme:typeof n==`number`?n:null})}function watchWin32Appearance(e){let t=r.timeout_add_seconds(r.PRIORITY_DEFAULT,3,()=>(e(),r.SOURCE_CONTINUE));return()=>r.source_remove(t)}export{i as WIN32_POLL_INTERVAL_S,readWin32Appearance,watchWin32Appearance};
@@ -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{APPLE_ACCENT_COLOR_ARGV as j,MACOS_ACCENT_POLL_SECONDS as M,onMacosAccentColorChanged as N,readMacosAccentColor as P}from"./system-accent.js";import{findNavItem as F,resolveInitialNavIndex as I}from"./nav-model.js";export{j as APPLE_ACCENT_COLOR_ARGV,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,M as MACOS_ACCENT_POLL_SECONDS,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,F as findNavItem,a as hasAppdataConstructor,A as initLocale,d as installBundledIconTheme,o as licenseTypeFor,C as loadIntoStack,N as onMacosAccentColorChanged,t as parseAppdata,y as pickFile,g as readAppDevHooks,P as readMacosAccentColor,f as registerBundledIconResource,T as registerToastOverlay,I 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};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import e from"gi://Adw?version=1";import t from"gi://Gtk?version=4.0";function buildNavRow(e){let n=new t.ListBoxRow,r=new t.Box({orientation:t.Orientation.HORIZONTAL,spacing:12,marginTop:8,marginBottom:8,marginStart:12,marginEnd:12});e.icon&&r.append(new t.Image({iconName:e.icon}));let i=new t.Box({orientation:t.Orientation.VERTICAL,hexpand:!0});return i.append(new t.Label({label:e.label,xalign:0})),e.subtitle&&i.append(new t.Label({label:e.subtitle,xalign:0,cssClasses:[`dim-label`,`caption`]})),r.append(i),n.set_child(r),n}function createNavShell(n,r){let i=new e.NavigationSplitView,a=new t.Stack({transitionType:t.StackTransitionType.CROSSFADE,hexpand:!0,vexpand:!0}),o=new t.ListBox({cssClasses:[`navigation-sidebar`]}),s=new e.HeaderBar,c=new e.HeaderBar;r.sidebarTitle&&c.set_title_widget(new e.WindowTitle({title:r.sidebarTitle,subtitle:``})),r.sidebarHeaderStart&&c.pack_start(r.sidebarHeaderStart),r.sidebarHeaderEnd&&c.pack_end(r.sidebarHeaderEnd);let l=new e.ToolbarView;l.add_top_bar(c),l.set_content(o),i.set_sidebar(new e.NavigationPage({title:r.sidebarTitle??`Menu`,child:l}));let u=new e.ToolbarView;u.add_top_bar(s),u.set_content(a),i.set_content(new e.NavigationPage({title:r.sidebarTitle??``,child:u}));for(let e of r.items)o.append(buildNavRow(e));o.connect(`row-selected`,(e,t)=>{if(!t)return;let n=t.get_index(),a=r.items[n];a&&(r.onSelect(a,n),i.get_collapsed()&&i.set_show_content(!0))});let d=r.collapseWidth??720,f=e.BreakpointCondition.parse(`max-width: ${d}px`);if(f){let t=new e.Breakpoint({condition:f});t.add_setter(i,`collapsed`,!0),n.add_breakpoint(t)}let selectByIndex=e=>{let t=o.get_row_at_index(e);t&&o.select_row(t)};return{widget:i,stack:a,contentHeader:s,selectByIndex,selectById(e){let t=r.items.findIndex(t=>t.id===e);selectByIndex(t>=0?t:0)}}}export{createNavShell};
1
+ import"./_virtual/_rolldown/runtime.js";import e from"gi://Adw?version=1";import t from"gi://Gtk?version=4.0";function buildNavRow(e){let n=new t.ListBoxRow,r=new t.Box({orientation:t.Orientation.HORIZONTAL,spacing:12,marginTop:8,marginBottom:8,marginStart:12,marginEnd:12});e.icon&&r.append(new t.Image({iconName:e.icon}));let i=new t.Box({orientation:t.Orientation.VERTICAL,hexpand:!0});return i.append(new t.Label({label:e.label,xalign:0})),e.subtitle&&i.append(new t.Label({label:e.subtitle,xalign:0,cssClasses:[`dim-label`,`caption`]})),r.append(i),n.set_child(r),n}function createNavShell(n,r){let i=new e.NavigationSplitView,a=new t.Stack({transitionType:t.StackTransitionType.CROSSFADE,hexpand:!0,vexpand:!0}),o=new t.ListBox({cssClasses:[`navigation-sidebar`]}),s=new e.HeaderBar,c=new WeakMap,l=[],u=[],d=null,f=!1,p=new e.HeaderBar;r.sidebarTitle&&p.set_title_widget(new e.WindowTitle({title:r.sidebarTitle,subtitle:``})),r.sidebarHeaderStart&&p.pack_start(r.sidebarHeaderStart),r.sidebarHeaderEnd&&p.pack_end(r.sidebarHeaderEnd);let m=new e.ToolbarView;m.add_top_bar(p),r.sidebarBottomBar&&m.add_bottom_bar(r.sidebarBottomBar),m.set_content(o),i.set_sidebar(new e.NavigationPage({title:r.sidebarTitle??`Menu`,child:m}));let h=new e.ToolbarView;if(h.add_top_bar(s),r.contentBottomBar&&h.add_bottom_bar(r.contentBottomBar),h.set_content(a),i.set_content(new e.NavigationPage({title:r.sidebarTitle??``,child:h})),o.connect(`row-selected`,(e,t)=>{if(!t){f||(d=null);return}let n=c.get(t);if(!n)return;let a=t.get_index();a<0||(d=n.id,!f&&(r.onSelect(n,a),i.get_collapsed()&&i.set_show_content(!0)))}),r.headerFunc){let e=r.headerFunc;o.set_header_func((t,n)=>e(t,n?c.get(n)??null:null))}let setItems=e=>{let t=d;l=e,f=!0;for(let e of u)o.remove(e);u=l.map(e=>{let t=buildNavRow(e);return c.set(t,e),o.append(t),t});let n=t===null?-1:l.findIndex(e=>e.id===t);n>=0&&o.select_row(u[n]),f=!1};setItems(r.items);let g=r.collapseWidth??720,_=e.BreakpointCondition.parse(`max-width: ${g}px`);if(_){let t=new e.Breakpoint({condition:_});t.add_setter(i,`collapsed`,!0),n.add_breakpoint(t)}let selectByIndex=e=>{let t=o.get_row_at_index(e);t&&o.select_row(t)};return{widget:i,stack:a,contentHeader:s,setItems,selectByIndex,selectById(e){let t=l.findIndex(t=>t.id===e);selectByIndex(t>=0?t:0)}}}export{createNavShell};
@@ -0,0 +1 @@
1
+ import"./_virtual/_rolldown/runtime.js";import e from"gi://Gio?version=2.0";import t from"gi://GLib?version=2.0";import{adwAccentFromAppleAccentColor as n}from"@gjsify/adwaita-core";const r=[`defaults`,`read`,`-g`,`AppleAccentColor`],i=5,a=/Could not find key|does not exist/;function readMacosAccentColor(t={}){let i=[...t.argv??r],o;try{let t=e.Subprocess.new(i,e.SubprocessFlags.STDOUT_PIPE|e.SubprocessFlags.STDERR_PIPE),[,n,r]=t.communicate_utf8(null,null);o={ok:t.get_successful(),stdout:n??``,stderr:r??``}}catch{return null}return o.ok?n(o.stdout):a.test(o.stderr)?n(null):null}function onMacosAccentColorChanged(e,n={}){let r=n.read??(()=>readMacosAccentColor()),i=r(),a=t.timeout_add_seconds(t.PRIORITY_LOW,n.intervalSeconds??5,()=>{let n=r();return n!==i&&(i=n,e(n)),t.SOURCE_CONTINUE});return()=>{a!==null&&(t.source_remove(a),a=null)}}export{r as APPLE_ACCENT_COLOR_ARGV,i as MACOS_ACCENT_POLL_SECONDS,onMacosAccentColorChanged,readMacosAccentColor};
@@ -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;
@@ -0,0 +1,5 @@
1
+ import type { DesktopAppearance } from '@gjsify/adwaita-core';
2
+ /** Quiet time after the last file event before re-reading — cfprefsd writes in bursts. */
3
+ export declare const DARWIN_DEBOUNCE_MS = 750;
4
+ export declare function readDarwinAppearance(): Promise<DesktopAppearance>;
5
+ export declare function watchDarwinAppearance(changed: () => void): () => void;
@@ -0,0 +1,23 @@
1
+ import type { DesktopAppearance } from '@gjsify/adwaita-core';
2
+ export declare const GNOME_INTERFACE_SCHEMA = "org.gnome.desktop.interface";
3
+ /** The slice of `Gio.Settings` this reader touches, so a test can hand in a fake. */
4
+ export interface GnomeSettingsLike {
5
+ get_string(key: string): string;
6
+ connect(signal: string, callback: () => void): number;
7
+ disconnect(id: number): void;
8
+ }
9
+ /** A GSettings source and which of the two keys its installed schema actually has. */
10
+ export interface GnomeSettingsSource {
11
+ readonly settings: GnomeSettingsLike;
12
+ readonly keys: readonly ('accent-color' | 'color-scheme')[];
13
+ }
14
+ /**
15
+ * The installed schema, or `null`. Looked up in the schema source FIRST because
16
+ * `new Gio.Settings` on a missing schema aborts the whole process instead of
17
+ * throwing, and a missing KEY aborts `get_string` the same way — so each key is
18
+ * checked too (`accent-color` exists only since GNOME 47).
19
+ */
20
+ export declare function gnomeSettingsSource(): GnomeSettingsSource | null;
21
+ export declare function readGnomeSettings(source: GnomeSettingsSource): DesktopAppearance;
22
+ /** Call `changed` on a change to either key. Returns the unsubscribe. */
23
+ export declare function watchGnomeSettings(source: GnomeSettingsSource, changed: () => void): () => void;
@@ -0,0 +1,9 @@
1
+ export { readDesktopAppearance, watchDesktopAppearance } from './reader.js';
2
+ export type { DesktopAppearanceOptions } from './reader.js';
3
+ export { appearanceFromGnomeSettings, appearanceFromMacDefaults, appearanceFromPortal, appearanceFromWindowsRegistry, isGnomeDesktop, MACOS_ACCENT_COLORS, mergeAppearance, parseRegQuery, sameAppearance, } from './mapping.js';
4
+ export type { MacDefaultsValues, WindowsRegistryValues } from './mapping.js';
5
+ export { readPortalAppearance, watchPortalAppearance } from './portal.js';
6
+ export type { PortalEndpoint } from './portal.js';
7
+ export type { GnomeSettingsLike, GnomeSettingsSource } from './gnome-settings.js';
8
+ export { parseDesktopAppearance, renderAppearanceMeta } from '@gjsify/adwaita-core';
9
+ export type { AdwSystemColorScheme, DesktopAppearance } from '@gjsify/adwaita-core';
@@ -0,0 +1,102 @@
1
+ import { type DesktopAppearance } from '@gjsify/adwaita-core';
2
+ /** Field by field: `primary` wins, `fallback` fills what it left unknown. */
3
+ export declare function mergeAppearance(primary: DesktopAppearance, fallback: DesktopAppearance): DesktopAppearance;
4
+ /** Whether two appearances say the same thing — what a watcher dedupes on. */
5
+ export declare function sameAppearance(a: DesktopAppearance, b: DesktopAppearance): boolean;
6
+ /**
7
+ * The XDG Settings portal's `org.freedesktop.appearance` namespace, as
8
+ * `ReadAll` returns it unpacked.
9
+ *
10
+ * - `color-scheme` (`u`): 0 no preference, 1 prefer dark, 2 prefer light. Any
11
+ * other number is invalid and dropped (libadwaita warns and uses the default).
12
+ * - `accent-color` (`(ddd)`): sRGB in 0…1. The spec says a value outside that
13
+ * range means "not set" — xdg-desktop-portal-gnome sends `(-1,-1,-1)` for a
14
+ * desktop with no accent — so it is dropped, not clamped.
15
+ *
16
+ * Reference: xdg-desktop-portal `org.freedesktop.impl.portal.Settings` docs
17
+ * Reference: refs/libadwaita/src/adw-settings-impl-portal.c#get_fdo_accent_color
18
+ */
19
+ export declare function appearanceFromPortal(namespace: Readonly<Record<string, unknown>>): DesktopAppearance;
20
+ /**
21
+ * Whether GSettings `org.gnome.desktop.interface` describes THIS desktop, from
22
+ * `XDG_CURRENT_DESKTOP` (a colon-separated list such as `ubuntu:GNOME`).
23
+ *
24
+ * The schema is installed far beyond GNOME: KDE and Xfce ship
25
+ * gsettings-desktop-schemas, and so does Homebrew on macOS. There nobody writes
26
+ * the keys, so `get_string` answers the schema DEFAULT, and a reader that took it
27
+ * would report `blue` for a desktop set to purple (measured on macOS with
28
+ * `AppleAccentColor = 5`, JumpLink/beifahrer#19). Outside GNOME the answer is
29
+ * therefore "unknown", never the default.
30
+ */
31
+ export declare function isGnomeDesktop(currentDesktop: string | null | undefined): boolean;
32
+ /**
33
+ * GSettings `org.gnome.desktop.interface` — the fallback where no portal
34
+ * answers. `accent-color` (GNOME 47+) is already one of the nine NAMES, so no
35
+ * snapping and no colour; `color-scheme` (GNOME 42+) is `default`,
36
+ * `prefer-dark` or `prefer-light`.
37
+ */
38
+ export declare function appearanceFromGnomeSettings(values: {
39
+ readonly accentColor?: string | null;
40
+ readonly colorScheme?: string | null;
41
+ }): DesktopAppearance;
42
+ /** The raw registry values the Windows reader fetches; `null` where the value does not exist. */
43
+ export interface WindowsRegistryValues {
44
+ /** `HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Accent` `AccentPalette` (REG_BINARY, 32 bytes). */
45
+ readonly accentPalette?: Uint8Array | null;
46
+ /** `HKCU\Software\Microsoft\Windows\DWM` `AccentColor` (REG_DWORD, `0xAABBGGRR`). */
47
+ readonly dwmAccentColor?: number | null;
48
+ /** `HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize` `AppsUseLightTheme` (REG_DWORD). */
49
+ readonly appsUseLightTheme?: number | null;
50
+ }
51
+ /**
52
+ * Windows. The accent libadwaita reads is WinRT's `UIColorType_Accent`
53
+ * (`adw-settings-impl-win32.c`), which Explorer persists as entry 3 of the
54
+ * eight RGBA quads in `AccentPalette` (light 3/2/1, ACCENT, dark 1/2/3,
55
+ * unused). Where that value is missing, DWM's `AccentColor` — the title-bar
56
+ * accent, stored `0xAABBGGRR` — is the closest the registry offers.
57
+ *
58
+ * `AppsUseLightTheme` is the "choose your app mode" switch: 0 dark, 1 light.
59
+ * Absent (Windows before 10 1809) leaves the scheme unknown.
60
+ *
61
+ * NOT MEASURED ON WINDOWS: the palette layout is Microsoft's undocumented
62
+ * persistence format, taken from its long-standing public descriptions; the
63
+ * registry paths match JumpLink/beifahrer#18.
64
+ */
65
+ export declare function appearanceFromWindowsRegistry(values: WindowsRegistryValues): DesktopAppearance;
66
+ /**
67
+ * The colour each `AppleAccentColor` index stands for. macOS stores an INDEX,
68
+ * not a colour, so these are the `NSColor.controlAccentColor` values libadwaita's
69
+ * own test uses for its macOS cases (`tests/test-accent-color.c`) — the same
70
+ * colours `adw-settings-impl-macos.c` snaps at runtime. An ABSENT key is
71
+ * "multicolour", whose accent is blue (Firefox 156 on macOS 27 reports CSS
72
+ * `AccentColor` as `rgb(0, 122, 255)` there, which snaps to blue as well).
73
+ */
74
+ export declare const MACOS_ACCENT_COLORS: Readonly<Record<string, string>>;
75
+ /** The raw `defaults read -g <key>` answers; `null` where the key does not exist. */
76
+ export interface MacDefaultsValues {
77
+ readonly appleAccentColor?: string | null;
78
+ readonly appleInterfaceStyle?: string | null;
79
+ readonly appleInterfaceStyleSwitchesAutomatically?: string | null;
80
+ }
81
+ /**
82
+ * macOS global defaults. Measured on macOS 27 (JumpLink/beifahrer#18): with
83
+ * default settings all three keys are ABSENT, and absence is itself a value —
84
+ * multicolour accent, light appearance.
85
+ *
86
+ * One exception makes absence ambiguous: with "Auto" appearance
87
+ * (`AppleInterfaceStyleSwitchesAutomatically = 1`) the system flips between
88
+ * light and dark by time of day, and a missing `AppleInterfaceStyle` no longer
89
+ * proves light. The scheme is then left UNKNOWN, so a page falls back to
90
+ * `prefers-color-scheme`, which the browser tracks live.
91
+ */
92
+ export declare function appearanceFromMacDefaults(values: MacDefaultsValues): DesktopAppearance;
93
+ /**
94
+ * Parse one value out of `reg.exe query <key> /v <name>` output:
95
+ *
96
+ * HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM
97
+ * AccentColor REG_DWORD 0xffd47800
98
+ *
99
+ * Returns a number for `REG_DWORD`, bytes for `REG_BINARY`, `null` when the
100
+ * value is not in the output (reg.exe then exits 1 with an error instead).
101
+ */
102
+ export declare function parseRegQuery(stdout: string, name: string): number | Uint8Array | null;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import Gio from 'gi://Gio?version=2.0';
2
+ import type { DesktopAppearance } from '@gjsify/adwaita-core';
3
+ export declare const PORTAL_BUS_NAME = "org.freedesktop.portal.Desktop";
4
+ export declare const PORTAL_OBJECT_PATH = "/org/freedesktop/portal/desktop";
5
+ export declare const PORTAL_SETTINGS_INTERFACE = "org.freedesktop.portal.Settings";
6
+ export declare const APPEARANCE_NAMESPACE = "org.freedesktop.appearance";
7
+ /** Where to reach the portal. `busName` is `null` on a peer-to-peer connection (the tests). */
8
+ export interface PortalEndpoint {
9
+ readonly connection: Gio.DBusConnection;
10
+ readonly busName: string | null;
11
+ }
12
+ /**
13
+ * The appearance the portal reports, or `null` when no portal answered — the
14
+ * caller then falls back. A portal that answers with neither key yields `{}`,
15
+ * which is an answer ("this desktop says nothing"), not a failure.
16
+ */
17
+ export declare function readPortalAppearance(endpoint: PortalEndpoint): Promise<DesktopAppearance | null>;
18
+ /**
19
+ * Call `changed` whenever the portal announces a change in the appearance
20
+ * namespace. Returns the unsubscribe. The caller re-reads rather than applying
21
+ * the signal's one value, so a burst of changes collapses into one answer.
22
+ */
23
+ export declare function watchPortalAppearance(endpoint: PortalEndpoint, changed: () => void): () => void;
24
+ /** The session bus, or `null` where there is none (no `DBUS_SESSION_BUS_ADDRESS`, a CI container). */
25
+ export declare function sessionPortalEndpoint(): Promise<PortalEndpoint | null>;
@@ -0,0 +1,26 @@
1
+ import type { DesktopAppearance } from '@gjsify/adwaita-core';
2
+ import { type GnomeSettingsSource } from './gnome-settings.js';
3
+ import { type PortalEndpoint } from './portal.js';
4
+ /** Overrides for the sources, mainly so a test can point the reader at a fake. */
5
+ export interface DesktopAppearanceOptions {
6
+ /** `process.platform` by default. Anything other than `win32`/`darwin` takes the portal route. */
7
+ readonly platform?: string;
8
+ /** Linux: the portal to ask. Default: `org.freedesktop.portal.Desktop` on the session bus; `null` skips it. */
9
+ readonly portal?: PortalEndpoint | null;
10
+ /** Linux: the GSettings fallback. Default: `org.gnome.desktop.interface` if installed; `null` skips it. */
11
+ readonly gnomeSettings?: GnomeSettingsSource | null;
12
+ /**
13
+ * Linux: `XDG_CURRENT_DESKTOP`, read from the environment by default. GSettings is
14
+ * consulted only when it names GNOME: elsewhere the schema answers its defaults.
15
+ */
16
+ readonly currentDesktop?: string | null;
17
+ }
18
+ /** The desktop's accent and colour-scheme preference, as far as this OS can tell. Never rejects. */
19
+ export declare function readDesktopAppearance(options?: DesktopAppearanceOptions): Promise<DesktopAppearance>;
20
+ /**
21
+ * Call `callback` with the current appearance, then again whenever it changes
22
+ * (duplicate reports are dropped). Returns the unsubscribe — HOLD it: the
23
+ * returned closure is what keeps the bus subscription, the settings object or
24
+ * the file monitor alive.
25
+ */
26
+ export declare function watchDesktopAppearance(callback: (appearance: DesktopAppearance) => void, options?: DesktopAppearanceOptions): () => void;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stdout of `argv` when it exits 0, `null` otherwise — including when the tool
3
+ * is missing. Both tools exit non-zero for "no such value", which the readers
4
+ * treat as absence, so the two cases need no distinction here.
5
+ */
6
+ export declare function runTool(argv: readonly string[]): Promise<string | null>;
@@ -0,0 +1,5 @@
1
+ import type { DesktopAppearance } from '@gjsify/adwaita-core';
2
+ /** How often a watcher re-reads. Settings change by hand, so seconds are fast enough. */
3
+ export declare const WIN32_POLL_INTERVAL_S = 3;
4
+ export declare function readWin32Appearance(): Promise<DesktopAppearance>;
5
+ export declare function watchWin32Appearance(changed: () => void): () => void;
@@ -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
  /**
@@ -3,10 +3,14 @@ export type { AdwaitaAppOptions } from './application.js';
3
3
  export { BUNDLED_ICON_COUNT, BUNDLED_ICON_RESOURCE_PATH, BUNDLED_ICON_THEME_NAME, addBundledIconsToTheme, installBundledIconTheme, registerBundledIconResource, } from './icon-theme.js';
4
4
  export type { BundledIconPreference, BundledIconThemeOptions } from './icon-theme.js';
5
5
  export { createNavShell } from './nav-shell.js';
6
- export type { NavShell, NavShellOptions } from './nav-shell.js';
6
+ export type { NavShell, NavShellHeaderFunc, 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,8 +18,10 @@ 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';
23
+ export { APPLE_ACCENT_COLOR_ARGV, MACOS_ACCENT_POLL_SECONDS, onMacosAccentColorChanged, readMacosAccentColor, } from './system-accent.js';
24
+ export type { OnMacosAccentColorChangedOptions, ReadMacosAccentColorOptions } from './system-accent.js';
19
25
  export { readAppDevHooks } from './dev-hooks.js';
20
26
  export type { AppDevHooks, ReadAppDevHooksOptions } from './dev-hooks.js';
21
27
  export { findNavItem, resolveInitialNavIndex } from './nav-model.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
  /**
@@ -1,6 +1,14 @@
1
1
  import Adw from 'gi://Adw?version=1';
2
2
  import Gtk from 'gi://Gtk?version=4.0';
3
3
  import type { NavItem } from './types.js';
4
+ /**
5
+ * Per-row section header, wired straight into `Gtk.ListBox.set_header_func`.
6
+ *
7
+ * `before` is the item of the row ABOVE `row`, or `null` for the first row — the
8
+ * consumer's decision input, since a header is a comparison against the previous
9
+ * section. Return `null` to draw no header.
10
+ */
11
+ export type NavShellHeaderFunc = (row: Gtk.ListBoxRow, before: NavItem | null) => Gtk.Widget | null;
4
12
  /** Options for {@link createNavShell}. */
5
13
  export interface NavShellOptions {
6
14
  /** Sidebar rows + stack child ids, in order. */
@@ -15,6 +23,12 @@ export interface NavShellOptions {
15
23
  sidebarHeaderEnd?: Gtk.Widget;
16
24
  /** Collapse the split view below this width (px). Default `720`. */
17
25
  collapseWidth?: number;
26
+ /** Per-row group header, mapped onto `Gtk.ListBox.set_header_func`. */
27
+ headerFunc?: NavShellHeaderFunc;
28
+ /** Widget below the sidebar list (e.g. an account row). */
29
+ sidebarBottomBar?: Gtk.Widget;
30
+ /** Widget below the content stack, under the conversation rather than in it. */
31
+ contentBottomBar?: Gtk.Widget;
18
32
  }
19
33
  /** The pieces a consumer wires into its window + views. */
20
34
  export interface NavShell {
@@ -24,6 +38,18 @@ export interface NavShell {
24
38
  stack: Gtk.Stack;
25
39
  /** Content header bar — add title widgets / buttons. */
26
40
  contentHeader: Adw.HeaderBar;
41
+ /**
42
+ * Replace the sidebar rows, in place. The `Gtk.Stack` is left alone: its pages
43
+ * belong to the consumer (`add_named(view, item.id)`), so a page outliving its
44
+ * item is theirs to keep or drop.
45
+ *
46
+ * The selected id survives if the new items still carry it, and `onSelect` does
47
+ * NOT fire either way — a rebuild is not a user selection, and re-announcing
48
+ * the same item would make `onSelect` fire on every list update. An item that
49
+ * is gone leaves nothing selected; `selectById` is how a consumer picks the
50
+ * replacement deliberately.
51
+ */
52
+ setItems(items: readonly NavItem[]): void;
27
53
  /** Select a nav item by its id (falls back to the first item). */
28
54
  selectById(id: string): void;
29
55
  /** Select a nav item by index. */
@@ -33,6 +59,6 @@ export interface NavShell {
33
59
  * Build the nav shell into `window` (which owns the responsive breakpoint).
34
60
  * Returns the split view to set as content plus the stack + content header to
35
61
  * fill. Row selection drives `onSelect`; a collapsed shell reveals the content
36
- * pane on selection.
62
+ * pane on selection. Items may be replaced afterwards — see {@link NavShell.setItems}.
37
63
  */
38
64
  export declare function createNavShell(window: Adw.ApplicationWindow, options: NavShellOptions): NavShell;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,30 @@
1
+ import { type AdwAccentColorName } from '@gjsify/adwaita-core';
2
+ /** The command that prints the accent: `AppleAccentColor` in the global domain. */
3
+ export declare const APPLE_ACCENT_COLOR_ARGV: readonly string[];
4
+ /** How often {@link onMacosAccentColorChanged} re-reads the preference by default. */
5
+ export declare const MACOS_ACCENT_POLL_SECONDS = 5;
6
+ export interface ReadMacosAccentColorOptions {
7
+ /** The command to run instead of {@link APPLE_ACCENT_COLOR_ARGV}. For tests. */
8
+ readonly argv?: readonly string[];
9
+ }
10
+ /**
11
+ * The macOS system accent as one of Adwaita's nine, or `null` where there is
12
+ * none to follow: not macOS (no `defaults`), or a value macOS does not define.
13
+ * "Multicolor" gives blue — an Adwaita app's own accent, as libadwaita shows it.
14
+ *
15
+ * Synchronous: it waits for one short-lived `defaults` process (~7 ms).
16
+ */
17
+ export declare function readMacosAccentColor(options?: ReadMacosAccentColorOptions): AdwAccentColorName | null;
18
+ export interface OnMacosAccentColorChangedOptions {
19
+ /** Seconds between reads. Defaults to {@link MACOS_ACCENT_POLL_SECONDS}. */
20
+ readonly intervalSeconds?: number;
21
+ /** The reader. Defaults to {@link readMacosAccentColor}; tests pass their own. */
22
+ readonly read?: () => AdwAccentColorName | null;
23
+ }
24
+ /**
25
+ * Call `listener` with the new accent whenever the macOS system accent changes.
26
+ * Reads once now as the baseline (without calling `listener`), then re-reads
27
+ * every `intervalSeconds` on the default main context. Returns the unsubscribe,
28
+ * which the caller must hold and call: the timer lives until then.
29
+ */
30
+ export declare function onMacosAccentColorChanged(listener: (accent: AdwAccentColorName | null) => void, options?: OnMacosAccentColorChangedOptions): () => void;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-app",
3
- "version": "0.51.1",
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",
3
+ "version": "0.53.0",
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, promise dialog/toast/file helpers, and a display-less reader of the desktop accent and colour scheme (`./appearance`)",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -9,6 +9,14 @@
9
9
  ".": {
10
10
  "types": "./lib/types/index.d.ts",
11
11
  "default": "./lib/esm/index.js"
12
+ },
13
+ "./appearance": {
14
+ "types": "./lib/types/appearance/index.d.ts",
15
+ "default": "./lib/esm/appearance/index.js"
16
+ },
17
+ "./system-accent": {
18
+ "types": "./lib/types/system-accent.d.ts",
19
+ "default": "./lib/esm/system-accent.js"
12
20
  }
13
21
  },
14
22
  "files": [
@@ -42,19 +50,20 @@
42
50
  "framework"
43
51
  ],
44
52
  "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.1"
53
+ "@girs/adw-1": "5.4.0",
54
+ "@girs/gdk-4.0": "5.4.0",
55
+ "@girs/gio-2.0": "5.4.0",
56
+ "@girs/gjs": "5.4.0",
57
+ "@girs/glib-2.0": "5.4.0",
58
+ "@girs/gobject-2.0": "5.4.0",
59
+ "@girs/gtk-4.0": "5.4.0",
60
+ "@gjsify/adwaita-core": "^0.53.0",
61
+ "@gjsify/devtools": "^0.53.0"
53
62
  },
54
63
  "devDependencies": {
55
- "@gjsify/cli": "^0.51.1",
56
- "@gjsify/storybook": "^0.51.1",
57
- "@gjsify/unit": "^0.51.1",
64
+ "@gjsify/cli": "^0.53.0",
65
+ "@gjsify/storybook": "^0.53.0",
66
+ "@gjsify/unit": "^0.53.0",
58
67
  "@types/node": "^25.9.2",
59
68
  "typescript": "^6.0.3"
60
69
  },
@@ -66,6 +75,15 @@
66
75
  "nativescript": "none"
67
76
  },
68
77
  "tier": 2,
78
+ "os": {
79
+ "linux": "supported",
80
+ "darwin": "partial",
81
+ "win32": "partial"
82
+ },
83
+ "osNotes": {
84
+ "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. The `./appearance` subpath (ADR 0078) branches on `process.platform` too: its macOS reader runs `defaults read -g` and watches `~/Library/Preferences`. Its mapping (`appearanceFromMacDefaults`) is asserted on every host by `appearance/mapping.spec.ts`; spawning `defaults` and the directory monitor have NOT run on a Mac. Verified on real hardware separately, not in CI.",
85
+ "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. The `./appearance` subpath (ADR 0078) reads the accent and app mode with `reg.exe query` and polls for changes, because `GWin32RegistryKey` returns values through a `gpointer` out-parameter introspection cannot marshal. The parser and mapping (`parseRegQuery`, `appearanceFromWindowsRegistry`) are asserted on every host; spawning reg.exe on Windows has NOT been measured."
86
+ },
69
87
  "storybook": {
70
88
  "applicationId": "org.gjsify.AdwaitaAppStorybook",
71
89
  "title": "Adwaita App",