@gjsify/adwaita-app 0.52.0 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -216,10 +216,63 @@ more helpful and would show a licence line on Windows that Linux does not.
216
216
  - `createNavShell(window, options): NavShell` — builds the
217
217
  `Adw.NavigationSplitView` (sidebar `Gtk.ListBox` + content `Gtk.Stack`) into
218
218
  `window` (which owns the responsive `Adw.Breakpoint`, default `max-width: 720px`).
219
- Returns `{ widget, stack, contentHeader, selectById, selectByIndex }`.
219
+ Returns `{ widget, stack, contentHeader, setItems, selectById, selectByIndex }`.
220
220
  - `resolveInitialNavIndex(items, wantedId?)`, `findNavItem(items, id)` — pure
221
221
  helpers (e.g. to turn a `${PREFIX}_VIEW` dev hook into a start index).
222
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
+
223
276
  ### Async view mounting
224
277
 
225
278
  - `LoadToken` + `loadIntoStack({ stack, token, load, fill, onError? })` — show a
@@ -238,12 +291,65 @@ more helpful and would show a licence line on Windows that Linux does not.
238
291
  - `pickFile(parent, { title?, filters? }): Promise<string | null>` /
239
292
  `saveFile(parent, { title?, filters?, initialName? }): Promise<string | null>`
240
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
+
241
310
  ### Dev hooks
242
311
 
243
312
  - `readAppDevHooks({ prefix, env? }): { view?, file?, debug }` — the
244
313
  `${PREFIX}_VIEW` / `${PREFIX}_FILE` / `${PREFIX}_DEBUG` env pattern (e.g.
245
314
  `MYAPP_VIEW=reports myapp` to open straight to a view in dev).
246
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
+
247
353
  ## See also
248
354
 
249
355
  - [`@gjsify/devtools`](../devtools) — the DBus control plane wired in on startup.
@@ -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};
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{appdataLocale as e,parseAppdata as t}from"./appdata.js";import{applyAppdataFields as n,buildAboutDialogFromAppdata as r,createAboutDialog as i,hasAppdataConstructor as a,licenseTypeFor as o}from"./about-dialog.js";import{BUNDLED_ICON_COUNT as s,BUNDLED_ICON_RESOURCE_PATH as c}from"./icons.generated.js";import{BUNDLED_ICON_THEME_NAME as l,addBundledIconsToTheme as u,installBundledIconTheme as d,registerBundledIconResource as f}from"./icon-theme.js";import{AdwaitaApp as p,runAdwaitaApp as m,runApplication as h}from"./application.js";import{readAppDevHooks as g}from"./dev-hooks.js";import{confirmDialog as _,errorDialog as v}from"./dialogs.js";import{pickFile as y,saveFile as b}from"./file-dialog.js";import{createNavShell as x}from"./nav-shell.js";import{LoadToken as S,loadIntoStack as C}from"./view-loader.js";import{LoadingStack as w}from"./loading-stack.js";import{registerToastOverlay as T,showToast as E}from"./toast.js";import{SYSTEM_LOCALE_DIR as D,resolveLocaleDir as O,systemLocaleDir as k}from"./locale-dir.js";import{initLocale as A}from"./locale.js";import{findNavItem as j,resolveInitialNavIndex as M}from"./nav-model.js";export{p as AdwaitaApp,s as BUNDLED_ICON_COUNT,c as BUNDLED_ICON_RESOURCE_PATH,l as BUNDLED_ICON_THEME_NAME,S as LoadToken,w as LoadingStack,D as SYSTEM_LOCALE_DIR,u as addBundledIconsToTheme,e as appdataLocale,n as applyAppdataFields,r as buildAboutDialogFromAppdata,_ as confirmDialog,i as createAboutDialog,x as createNavShell,v as errorDialog,j as findNavItem,a as hasAppdataConstructor,A as initLocale,d as installBundledIconTheme,o as licenseTypeFor,C as loadIntoStack,t as parseAppdata,y as pickFile,g as readAppDevHooks,f as registerBundledIconResource,T as registerToastOverlay,M as resolveInitialNavIndex,O as resolveLocaleDir,m as runAdwaitaApp,h as runApplication,b as saveFile,E as showToast,k as systemLocaleDir};
1
+ 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://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,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;
@@ -3,7 +3,7 @@ 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';
@@ -20,6 +20,8 @@ export { initLocale } from './locale.js';
20
20
  export type { InitLocaleOptions, Translator } from './locale.js';
21
21
  export { SYSTEM_LOCALE_DIR, resolveLocaleDir, systemLocaleDir } from './locale-dir.js';
22
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';
23
25
  export { readAppDevHooks } from './dev-hooks.js';
24
26
  export type { AppDevHooks, ReadAppDevHooksOptions } from './dev-hooks.js';
25
27
  export { findNavItem, resolveInitialNavIndex } from './nav-model.js';
@@ -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.52.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, and promise dialog/toast/file helpers",
3
+ "version": "0.54.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": [
@@ -49,12 +57,13 @@
49
57
  "@girs/glib-2.0": "5.4.0",
50
58
  "@girs/gobject-2.0": "5.4.0",
51
59
  "@girs/gtk-4.0": "5.4.0",
52
- "@gjsify/devtools": "^0.52.0"
60
+ "@gjsify/adwaita-core": "^0.54.0",
61
+ "@gjsify/devtools": "^0.54.0"
53
62
  },
54
63
  "devDependencies": {
55
- "@gjsify/cli": "^0.52.0",
56
- "@gjsify/storybook": "^0.52.0",
57
- "@gjsify/unit": "^0.52.0",
64
+ "@gjsify/cli": "^0.54.0",
65
+ "@gjsify/storybook": "^0.54.0",
66
+ "@gjsify/unit": "^0.54.0",
58
67
  "@types/node": "^25.9.2",
59
68
  "typescript": "^6.0.3"
60
69
  },
@@ -72,8 +81,8 @@
72
81
  "win32": "partial"
73
82
  },
74
83
  "osNotes": {
75
- "darwin": "`locale.ts` reads `process.platform` to decide whether `initLocale` binds a text domain at all, so this package owes an OS claim (ADR 0018). The DECISION is complete and asserted from any host: the platform is an argument to `systemLocaleDir(platform)`, and `locale-dir.spec.ts` drives darwin, win32, linux, an unmeasured platform and an absent `process.platform` through it from Linux. What no runner observes is the SELECTION — `locale.ts` reading the real `process.platform` on a real Mac and skipping `bindtextdomain`. NO LEG OF THIS PACKAGE'S SUITE HAS EVER RUN ON DARWIN: `gtk-os-suites.yml` runs `@gjsify/gtk-host` (gating) and `@gjsify/react-native` (probe) on the darwin legs and nothing else, and `macos-suites.yml` is a Node-pillar workflow that states outright that `gtk4` is not installed. This is exactly the position `@gjsify/path` records for win32 — semantics exercised from every host, selection observable nowhere — and it takes the same word. Promote from the first run of this package's own suite on a darwin leg.",
76
- "win32": "As darwin for why a claim is owed and why it is not `supported`: `locale-dir.spec.ts` asserts the win32 answer from Linux, and nothing runs this package's suite on a Windows host — `gtk-os-suites.yml`'s win32 leg carries `@gjsify/gtk-host` and `@gjsify/react-native` as probes and nothing else. One thing is BETTER here than on darwin and is worth recording so the promotion is not blocked on the wrong obstacle: there is no GJS host on Windows at all (ADR 0024 § 4), so the route is Node, where `process.platform` comes from the host itself and is authoritative — the unexercised `uname` probe that keeps `@gjsify/process` at win32 `partial` is not on this path. What is unexercised is this package, not the platform read underneath it. Promote from the first run of this package's own suite on a win32 leg."
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."
77
86
  },
78
87
  "storybook": {
79
88
  "applicationId": "org.gjsify.AdwaitaAppStorybook",