@gjsify/devtools 0.8.0 → 0.12.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 ADDED
@@ -0,0 +1,85 @@
1
+ # @gjsify/devtools
2
+
3
+ The in-app **DBus devtools control plane** for GTK/GJS apps. Opt in with one call and your running app exposes `org.gjsify.Devtools` — a control plane you can drive from `gdbus`, d-feet, GNOME Builder, headless CI, or (via [`@gjsify/devtools-mcp`](../devtools-mcp)) an AI agent.
4
+
5
+ It implements the toolkit-neutral [`@gjsify/devtools-protocol`](../devtools-protocol) contract over DBus and re-exports it, so a consumer needs one import.
6
+
7
+ ## Install it in your app
8
+
9
+ `installDevtools` is a no-op unless `GJSIFY_DEVTOOLS=1` (or `enabled: true`). Call it from `onStartup`/`vfunc_startup`, after the actions and the active window exist.
10
+
11
+ ```ts
12
+ import Adw from '@girs/adw-1';
13
+ import GObject from '@girs/gobject-2.0';
14
+ import { installDevtools } from '@gjsify/devtools';
15
+
16
+ export class MyApplication extends Adw.Application {
17
+ static { GObject.registerClass(MyApplication); }
18
+
19
+ vfunc_startup(): void {
20
+ super.vfunc_startup();
21
+ // … create actions + the main window …
22
+
23
+ installDevtools(this, {
24
+ // Adw.ApplicationWindow does NOT expose its win.* actions as a group —
25
+ // pass one explicitly so win.* commands are bridged.
26
+ winActionGroup: this._window,
27
+ // Optional: contribute app-specific methods (see below).
28
+ extend: [],
29
+ // Optional: gate mutating methods behind your own pause state.
30
+ paused: () => this._externalControlPaused,
31
+ });
32
+ }
33
+ }
34
+ ```
35
+
36
+ Then drive it without any AI:
37
+
38
+ ```bash
39
+ GJSIFY_DEVTOOLS=1 gjsify run dist/index.js &
40
+ gdbus call --session -d org.example.App -o /org/example/App/devtools \
41
+ -m org.gjsify.Devtools.GetStatus
42
+ gdbus call --session -d org.example.App -o /org/example/App/devtools \
43
+ -m org.gjsify.Devtools.Screenshot 'window' # PNG bytes as a GVariant `ay`
44
+ ```
45
+
46
+ ## Generic methods (out of the box)
47
+
48
+ `GetStatus`, `Screenshot` (GSK widget snapshot PNG), `ListActions` / `ActivateAction` / `ChangeActionState` (GAction bridge), `PresentWindow`, `ResizeWindow`, plus full introspection: `ListToplevels`, `DumpTree`, `GetProperty` (by index path), `GetFocused`, `DumpGSettings`, `DumpCss` / `SwapCss` (live CSS hot-swap).
49
+
50
+ GActions are auto-bridged into the command registry (handling the `Adw.ApplicationWindow` non-export gotcha via `winActionGroup`).
51
+
52
+ ## App-specific methods — a `DevtoolsExtension`
53
+
54
+ Add your own methods without forking the core. The `<method>` XML is merged into the interface, handlers attach as DBus methods, and each method declares a kind the pause guard enforces.
55
+
56
+ ```ts
57
+ import type { DevtoolsExtension } from '@gjsify/devtools';
58
+
59
+ const myExtension: DevtoolsExtension = {
60
+ methodsXml: ['<method name="DoThing"><arg type="s" direction="in" name="what"/></method>'],
61
+ handlers: { DoThing: (what: string) => { /* … */ } },
62
+ methodKinds: { DoThing: 'mutating' },
63
+ contributeStatus: () => ({ thing: currentThing }),
64
+ };
65
+
66
+ installDevtools(this, { extend: [myExtension] });
67
+ ```
68
+
69
+ ## Exports
70
+
71
+ - `installDevtools(app, options)` → returns a `DevtoolsService` (or `null` when the env gate is off); `uninstallDevtools(service)` — opt-in lifecycle.
72
+ - `DevtoolsService` — the exported service object `installDevtools` returns and `uninstallDevtools` accepts.
73
+ - `DevtoolsExtension`, `InstallDevtoolsOptions` — the extension + options types.
74
+ - `captureWidgetPng` (GSK screenshot), `buildVariant` / `variantKindFor` / `VariantKind` (GVariant), `activateAction` / `changeActionState` / `describeActions` (GAction bridge).
75
+ - widget-tree helpers (`dumpTree`, `getWidgetProperty`, `listToplevels`, `resolveWidgetPath`, …), `dumpCss` / `swapCss` / `removeCss`, `dumpGSettings`, `buildDevtoolsIfaceXml`.
76
+ - the re-exported `@gjsify/devtools-protocol` contract.
77
+
78
+ > **GJS gotcha:** the long-lived `Gio.DBusExportedObject` is reachable only through a self-cycle, which SpiderMonkey's GC can collect mid-run. `installDevtools` roots the service in a module-level set to defeat this — keep that rooting if you wire the service by hand.
79
+
80
+ ## Build / test
81
+
82
+ ```bash
83
+ gjsify workspace @gjsify/devtools build
84
+ gjsify workspace @gjsify/devtools test
85
+ ```
package/lib/esm/css.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import e from"@girs/gtk-4.0";import t from"@girs/gdk-4.0";const n=new Map;function swapCss(r,i){let a=t.Display.get_default();if(!a)return!1;let o=n.get(r);return o||(o=new e.CssProvider,e.StyleContext.add_provider_for_display(a,o,e.STYLE_PROVIDER_PRIORITY_USER+1),n.set(r,o)),o.load_from_string(i),!0}function removeCss(r){let i=n.get(r);if(!i)return!1;let a=t.Display.get_default();return a&&e.StyleContext.remove_provider_for_display(a,i),n.delete(r),!0}function dumpCss(){return{providers:[...n.keys()]}}export{dumpCss,removeCss,swapCss};
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/gdk-4.0";import t from"@girs/gtk-4.0";const n=new Map;function swapCss(r,i){let a=e.Display.get_default();if(!a)return!1;let o=n.get(r);return o||(o=new t.CssProvider,t.StyleContext.add_provider_for_display(a,o,t.STYLE_PROVIDER_PRIORITY_USER+1),n.set(r,o)),o.load_from_string(i),!0}function removeCss(r){let i=n.get(r);if(!i)return!1;let a=e.Display.get_default();return a&&t.StyleContext.remove_provider_for_display(a,i),n.delete(r),!0}function dumpCss(){return{providers:[...n.keys()]}}export{dumpCss,removeCss,swapCss};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{captureWidgetPng as e}from"./screenshot.js";import{buildDevtoolsIfaceXml as t}from"./devtools-iface.js";import{dumpCss as n,swapCss as r}from"./css.js";import{activateAction as i,changeActionState as a,describeActions as o}from"./actions.js";import{dumpGSettings as s}from"./gsettings.js";import{dumpTree as c,getWidgetProperty as l,listToplevels as u,pathOfWidget as d,resolveWidgetPath as f,widgetType as p}from"./widget-tree.js";import m from"@girs/gtk-4.0";import{GENERIC_METHODS as h,formatDbusErrorMessage as g}from"@gjsify/devtools-protocol";import _ from"@girs/gio-2.0";var DevtoolsService=class{constructor(e,t){this._app=e,this._options=t,this._exported=null,this._kinds=new Map,this._extensions=t.extend??[];for(let[e,t]of Object.entries(h))this._kinds.set(e,t);for(let e of this._extensions){for(let[t,n]of Object.entries(e.methodKinds))this._kinds.set(t,n);for(let[t,n]of Object.entries(e.handlers)){let e=n;this[t]=(...n)=>(this._guard(t),e(...n))}}}export(e,n){if(this._exported)return;let r=t(this._extensions.flatMap(e=>e.methodsXml??[])),i=_.DBusExportedObject.wrapJSObject(r,this);i.export(e,n),this._exported=i}unexport(){this._exported?.unexport(),this._exported=null}GetStatus(){let e=this._app.get_active_window(),t={appId:this._app.get_application_id()??`unknown`,instance:this._options.instance??`default`,activeWindow:e?{id:e.get_name()||`window`,title:e.get_title()??``,mapped:e.get_mapped()}:null,toplevelCount:m.Window.get_toplevels().get_n_items(),focusedWidget:null,paused:this._isPaused()};for(let e of this._extensions){let n=e.contributeStatus?.();n&&Object.assign(t,n)}return JSON.stringify(t)}async Screenshot(t){let n=this._app.get_active_window();return n?e(n)??new Uint8Array:new Uint8Array}ListActions(){let e={app:o(this._actionGroup(`app`)),win:o(this._actionGroup(`win`))};return JSON.stringify(e)}ActivateAction(e,t,n){this._guard(`ActivateAction`);let r=this._scope(e);i(this._requireGroup(r,t),t,this._parseValue(n))}ChangeActionState(e,t,n){this._guard(`ChangeActionState`);let r=this._scope(e);a(this._requireGroup(r,t),t,this._parseValue(n))}PresentWindow(){this._app.get_active_window()?.present()}ResizeWindow(e,t){this._guard(`ResizeWindow`);let n=this._app.get_active_window();if(!n)throw Error(g(`unavailable`,`no active window to resize`));let r=Math.max(1,Math.round(e)),i=Math.max(1,Math.round(t));return n.is_maximized()&&n.unmaximize(),n.is_fullscreen()&&n.unfullscreen(),n.set_default_size(r,i),[r,i]}ListToplevels(){return JSON.stringify(u())}DumpTree(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(g(`not-found`,`no widget at '${e||`active window`}'`));return JSON.stringify(c(n.widget,t>0?t:8,n.path))}GetProperty(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(g(`not-found`,`no widget at '${e}'`));let r=l(n.widget,t);if(r===void 0)throw Error(g(`not-found`,`widget has no property '${t}'`));return JSON.stringify(r)}GetFocused(){let e=this._app.get_active_window(),t=e?e.get_focus():null;return JSON.stringify(t?{path:d(t),name:t.get_name()||null,type:p(t)}:null)}DumpGSettings(e){return JSON.stringify(s(e))}DumpCss(){return JSON.stringify(n())}SwapCss(e,t){return this._guard(`SwapCss`),r(e,t)}_resolveRootWidget(e){if(!e||e===`window`||e===`active`){let e=this._app.get_active_window();return e?{widget:e,path:d(e)??`toplevel:0`}:null}let t=f(e);return t?{widget:t,path:e}:null}_isPaused(){return this._options.paused?.()??!1}_guard(e){let t=this._kinds.get(e);if(!t)throw Error(g(`internal`,`unclassified devtools method '${e}'`));if(t===`mutating`&&this._isPaused())throw Error(g(`paused`,`${e} rejected — external control is paused. Read-only methods keep working.`))}_scope(e){return e===`app`?`app`:`win`}_actionGroup(e){if(e===`app`)return this._app;if(this._options.winActionGroup)return this._options.winActionGroup;let t=this._app.get_active_window();return t instanceof m.ApplicationWindow?t:null}_requireGroup(e,t){let n=this._actionGroup(e);if(!n)throw Error(g(`unavailable`,`no '${e}' action group`));if(!n.has_action(t))throw Error(g(`not-found`,`unknown action ${e}.${t}`));return n}_parseValue(e){if(!e||e===`null`)return null;try{return JSON.parse(e)}catch{throw Error(g(`invalid-params`,`value_json is not valid JSON: ${e}`))}}};export{DevtoolsService};
1
+ import"./_virtual/_rolldown/runtime.js";import{activateAction as e,changeActionState as t,describeActions as n}from"./actions.js";import{dumpCss as r,swapCss as i}from"./css.js";import{buildDevtoolsIfaceXml as a}from"./devtools-iface.js";import{captureWidgetPng as o}from"./screenshot.js";import{dumpGSettings as s}from"./gsettings.js";import{dumpTree as c,getWidgetProperty as l,listToplevels as u,pathOfWidget as d,resolveWidgetPath as f,widgetType as p}from"./widget-tree.js";import m from"@girs/glib-2.0";import h from"@girs/gtk-4.0";import{GENERIC_METHODS as g,formatDbusErrorMessage as _}from"@gjsify/devtools-protocol";import v from"@girs/gio-2.0";function frameDelay(e){return new Promise(t=>{m.timeout_add(m.PRIORITY_DEFAULT,e,()=>(t(),m.SOURCE_REMOVE))})}async function captureWidgetWhenRenderable(e,t=12,n=50){for(let r=0;r<t;r++){let t=o(e);if(t)return t;await frameDelay(n)}return o(e)}var DevtoolsService=class{constructor(e,t){this._app=e,this._options=t,this._exported=null,this._kinds=new Map,this._extensions=t.extend??[];for(let[e,t]of Object.entries(g))this._kinds.set(e,t);for(let e of this._extensions){for(let[t,n]of Object.entries(e.methodKinds))this._kinds.set(t,n);for(let[t,n]of Object.entries(e.handlers)){let e=n;this[t]=(...n)=>(this._guard(t),e(...n))}}}export(e,t){if(this._exported)return;let n=a(this._extensions.flatMap(e=>e.methodsXml??[])),r=v.DBusExportedObject.wrapJSObject(n,this);r.export(e,t),this._exported=r}unexport(){this._exported?.unexport(),this._exported=null}GetStatus(){let e=this._app.get_active_window(),t={appId:this._app.get_application_id()??`unknown`,instance:this._options.instance??`default`,activeWindow:e?{id:e.get_name()||`window`,title:e.get_title()??``,mapped:e.get_mapped()}:null,toplevelCount:h.Window.get_toplevels().get_n_items(),focusedWidget:null,paused:this._isPaused()};for(let e of this._extensions){let n=e.contributeStatus?.();n&&Object.assign(t,n)}return JSON.stringify(t)}async Screenshot(e){let t=this._app.get_active_window();if(!t)return new Uint8Array;let n=o(t);return n||=(t.present(),await captureWidgetWhenRenderable(t)),n??new Uint8Array}ListActions(){let e={app:n(this._actionGroup(`app`)),win:n(this._actionGroup(`win`))};return JSON.stringify(e)}ActivateAction(t,n,r){this._guard(`ActivateAction`);let i=this._scope(t);e(this._requireGroup(i,n),n,this._parseValue(r))}ChangeActionState(e,n,r){this._guard(`ChangeActionState`);let i=this._scope(e);t(this._requireGroup(i,n),n,this._parseValue(r))}PresentWindow(){this._app.get_active_window()?.present()}ResizeWindow(e,t){this._guard(`ResizeWindow`);let n=this._app.get_active_window();if(!n)throw Error(_(`unavailable`,`no active window to resize`));let r=Math.max(1,Math.round(e)),i=Math.max(1,Math.round(t));return n.is_maximized()&&n.unmaximize(),n.is_fullscreen()&&n.unfullscreen(),n.set_default_size(r,i),[r,i]}ListToplevels(){return JSON.stringify(u())}DumpTree(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(_(`not-found`,`no widget at '${e||`active window`}'`));return JSON.stringify(c(n.widget,t>0?t:8,n.path))}GetProperty(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(_(`not-found`,`no widget at '${e}'`));let r=l(n.widget,t);if(r===void 0)throw Error(_(`not-found`,`widget has no property '${t}'`));return JSON.stringify(r)}GetFocused(){let e=this._app.get_active_window(),t=e?e.get_focus():null;return JSON.stringify(t?{path:d(t),name:t.get_name()||null,type:p(t)}:null)}DumpGSettings(e){return JSON.stringify(s(e))}DumpCss(){return JSON.stringify(r())}SwapCss(e,t){return this._guard(`SwapCss`),i(e,t)}_resolveRootWidget(e){if(!e||e===`window`||e===`active`){let e=this._app.get_active_window();return e?{widget:e,path:d(e)??`toplevel:0`}:null}let t=f(e);return t?{widget:t,path:e}:null}_isPaused(){return this._options.paused?.()??!1}_guard(e){let t=this._kinds.get(e);if(!t)throw Error(_(`internal`,`unclassified devtools method '${e}'`));if(t===`mutating`&&this._isPaused())throw Error(_(`paused`,`${e} rejected — external control is paused. Read-only methods keep working.`))}_scope(e){return e===`app`?`app`:`win`}_actionGroup(e){if(e===`app`)return this._app;if(this._options.winActionGroup)return this._options.winActionGroup;let t=this._app.get_active_window();return t instanceof h.ApplicationWindow?t:null}_requireGroup(e,t){let n=this._actionGroup(e);if(!n)throw Error(_(`unavailable`,`no '${e}' action group`));if(!n.has_action(t))throw Error(_(`not-found`,`unknown action ${e}.${t}`));return n}_parseValue(e){if(!e||e===`null`)return null;try{return JSON.parse(e)}catch{throw Error(_(`invalid-params`,`value_json is not valid JSON: ${e}`))}}};export{DevtoolsService};
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{captureWidgetPng as e}from"./screenshot.js";import{buildDevtoolsIfaceXml as t}from"./devtools-iface.js";import{buildVariant as n,variantKindFor as r}from"./gvariant.js";import{dumpCss as i,removeCss as a,swapCss as o}from"./css.js";import{activateAction as s,changeActionState as c,describeActions as l}from"./actions.js";import{dumpGSettings as u}from"./gsettings.js";import{buildWidgetPath as d,dumpTree as f,getWidgetProperty as p,listToplevels as m,parseWidgetPath as h,pathOfWidget as g,resolveWidgetPath as _,widgetType as v}from"./widget-tree.js";import{DevtoolsService as y}from"./devtools-service.js";import{installDevtools as b,uninstallDevtools as x}from"./install.js";export*from"@gjsify/devtools-protocol";export{y as DevtoolsService,s as activateAction,t as buildDevtoolsIfaceXml,n as buildVariant,d as buildWidgetPath,e as captureWidgetPng,c as changeActionState,l as describeActions,i as dumpCss,u as dumpGSettings,f as dumpTree,p as getWidgetProperty,b as installDevtools,m as listToplevels,h as parseWidgetPath,g as pathOfWidget,a as removeCss,_ as resolveWidgetPath,o as swapCss,x as uninstallDevtools,r as variantKindFor,v as widgetType};
1
+ import{buildVariant as e,variantKindFor as t}from"./gvariant.js";import{activateAction as n,changeActionState as r,describeActions as i}from"./actions.js";import{dumpCss as a,removeCss as o,swapCss as s}from"./css.js";import{buildDevtoolsIfaceXml as c}from"./devtools-iface.js";import{captureWidgetPng as l}from"./screenshot.js";import{dumpGSettings as u}from"./gsettings.js";import{buildWidgetPath as d,dumpTree as f,getWidgetProperty as p,listToplevels as m,parseWidgetPath as h,pathOfWidget as g,resolveWidgetPath as _,widgetType as v}from"./widget-tree.js";import{DevtoolsService as y}from"./devtools-service.js";import{installDevtools as b,uninstallDevtools as x}from"./install.js";export*from"@gjsify/devtools-protocol";export{y as DevtoolsService,n as activateAction,c as buildDevtoolsIfaceXml,e as buildVariant,d as buildWidgetPath,l as captureWidgetPng,r as changeActionState,i as describeActions,a as dumpCss,u as dumpGSettings,f as dumpTree,p as getWidgetProperty,b as installDevtools,m as listToplevels,h as parseWidgetPath,g as pathOfWidget,o as removeCss,_ as resolveWidgetPath,s as swapCss,x as uninstallDevtools,t as variantKindFor,v as widgetType};
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import e from"@girs/graphene-1.0";import t from"@girs/gtk-4.0";function captureWidgetPng(n){let r=n.get_native()?.get_renderer();if(!r)return null;let i=n.get_width(),a=n.get_height();if(i<=0||a<=0)return null;let o=t.WidgetPaintable.new(n),s=t.Snapshot.new();o.snapshot(s,i,a);let c=s.to_node();if(!c)return null;let l=new e.Rect;l.init(0,0,i,a);let u=r.render_texture(c,l).save_to_png_bytes().get_data();return u?new Uint8Array(u):null}export{captureWidgetPng};
1
+ import"./_virtual/_rolldown/runtime.js";import e from"@girs/gtk-4.0";import t from"@girs/graphene-1.0";function captureWidgetPng(n){let r=n.get_native()?.get_renderer();if(!r)return null;let i=n.get_width(),a=n.get_height();if(i<=0||a<=0)return null;let o=e.WidgetPaintable.new(n),s=e.Snapshot.new();o.snapshot(s,i,a);let c=s.to_node();if(!c)return null;let l=new t.Rect;l.init(0,0,i,a);let u=r.render_texture(c,l).save_to_png_bytes().get_data();return u?new Uint8Array(u):null}export{captureWidgetPng};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Install or replace a named `Gtk.CssProvider` on the default display at a
3
+ * priority just above USER, so it overrides the app's baked styles. Re-applying
4
+ * is free (no widget re-registration) — this is what makes live CSS iteration
5
+ * (`gjsify serve` Tier 3) work. Returns false when there is no display.
6
+ */
7
+ export declare function swapCss(name: string, css: string): boolean;
8
+ /** Remove a previously-installed named provider. Returns false if unknown. */
9
+ export declare function removeCss(name: string): boolean;
10
+ /** List the devtools-installed CSS provider names. */
11
+ export declare function dumpCss(): {
12
+ providers: string[];
13
+ };
@@ -1,5 +1,3 @@
1
- /** Well-known interface name implemented on each app's own bus name + object path. */
2
- export declare const DEVTOOLS_INTERFACE = "org.gjsify.Devtools";
3
1
  /**
4
2
  * Assemble the full `org.gjsify.Devtools` introspection node XML, merging any
5
3
  * app-specific `<method>…</method>` fragments contributed by extensions into
@@ -34,6 +34,21 @@ export declare class DevtoolsService {
34
34
  PresentWindow(): void;
35
35
  /** `ResizeWindow(w, h) -> (w, h)` — resize the active window; returns the requested size. */
36
36
  ResizeWindow(width: number, height: number): [number, number];
37
+ /** `ListToplevels() -> s` — JSON of live toplevel windows. */
38
+ ListToplevels(): string;
39
+ /** `DumpTree(root, depth) -> s` — JSON widget tree from `root` ('' = active window). */
40
+ DumpTree(root: string, depth: number): string;
41
+ /** `GetProperty(path, prop) -> s` — JSON value of a widget's GObject property. */
42
+ GetProperty(path: string, prop: string): string;
43
+ /** `GetFocused() -> s` — JSON {path,name,type} of the focused widget, or null. */
44
+ GetFocused(): string;
45
+ /** `DumpGSettings(schema_id) -> s` — JSON of a schema's keys + values. */
46
+ DumpGSettings(schemaId: string): string;
47
+ /** `DumpCss() -> s` — JSON of the devtools-installed CSS provider names. */
48
+ DumpCss(): string;
49
+ /** `SwapCss(name, css) -> b` — live-install/replace a named CSS provider. */
50
+ SwapCss(name: string, css: string): boolean;
51
+ private _resolveRootWidget;
37
52
  private _isPaused;
38
53
  private _guard;
39
54
  private _scope;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Dump every key + current value of an installed GSettings schema (read-only).
3
+ * Throws a `not-found`-coded error (the code survives the DBus wire) when the
4
+ * schema id is not installed.
5
+ */
6
+ export declare function dumpGSettings(schemaId: string): Record<string, unknown>;
@@ -5,5 +5,8 @@ export { captureWidgetPng } from './screenshot.js';
5
5
  export { buildVariant, variantKindFor } from './gvariant.js';
6
6
  export type { VariantKind } from './gvariant.js';
7
7
  export { activateAction, changeActionState, describeActions } from './actions.js';
8
- export { buildDevtoolsIfaceXml, DEVTOOLS_INTERFACE } from './devtools-iface.js';
8
+ export { buildDevtoolsIfaceXml } from './devtools-iface.js';
9
+ export { buildWidgetPath, dumpTree, getWidgetProperty, listToplevels, parseWidgetPath, pathOfWidget, resolveWidgetPath, widgetType, } from './widget-tree.js';
10
+ export { dumpCss, removeCss, swapCss } from './css.js';
11
+ export { dumpGSettings } from './gsettings.js';
9
12
  export * from '@gjsify/devtools-protocol';
@@ -0,0 +1,37 @@
1
+ import Gtk from '@girs/gtk-4.0';
2
+ import type { NodeInfo } from '@gjsify/devtools-protocol';
3
+ /** A parsed widget path: a toplevel index + a chain of child indices. */
4
+ export interface ParsedWidgetPath {
5
+ toplevel: number;
6
+ children: number[];
7
+ }
8
+ /**
9
+ * Parse a stable widget path like `toplevel:0/child:2/child:0` into its
10
+ * indices. Pure (no GTK) so it is unit-testable on node + gjs. Returns null
11
+ * for a malformed path.
12
+ */
13
+ export declare function parseWidgetPath(path: string): ParsedWidgetPath | null;
14
+ /** Build a widget path from a toplevel index + child-index chain. */
15
+ export declare function buildWidgetPath(toplevel: number, children: readonly number[]): string;
16
+ /** The registered GType name of a widget instance (best-effort). */
17
+ export declare function widgetType(widget: Gtk.Widget): string;
18
+ /** Enumerate live toplevel windows as `{ path, type, title, mapped, focused }`. */
19
+ export declare function listToplevels(): Array<{
20
+ path: string;
21
+ type: string;
22
+ title: string | null;
23
+ mapped: boolean;
24
+ focused: boolean;
25
+ }>;
26
+ /** Resolve a widget path to its live widget, or null if it no longer exists. */
27
+ export declare function resolveWidgetPath(path: string): Gtk.Widget | null;
28
+ /** Dump a widget subtree to {@link NodeInfo}, bounded by `maxDepth`. */
29
+ export declare function dumpTree(root: Gtk.Widget, maxDepth: number, basePath: string): NodeInfo;
30
+ /**
31
+ * Read a GObject property by name, trying the original, snake_case and
32
+ * camelCase accessor forms GJS exposes. Returns a JSON-safe value (non-scalars
33
+ * become a `[type]` marker).
34
+ */
35
+ export declare function getWidgetProperty(widget: Gtk.Widget, prop: string): unknown;
36
+ /** Compute the stable path of a live widget by walking up to its toplevel. */
37
+ export declare function pathOfWidget(widget: Gtk.Widget): string | null;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/devtools",
3
- "version": "0.8.0",
3
+ "version": "0.12.0",
4
4
  "description": "In-app DBus devtools control plane for GJS/GTK apps — inspect, screenshot, and drive a running app over org.gjsify.Devtools (gdbus or the MCP bridge)",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -43,11 +43,11 @@
43
43
  "@girs/gobject-2.0": "2.88.0-4.0.4",
44
44
  "@girs/graphene-1.0": "1.0.0-4.0.4",
45
45
  "@girs/gtk-4.0": "4.23.0-4.0.4",
46
- "@gjsify/devtools-protocol": "^0.8.0"
46
+ "@gjsify/devtools-protocol": "^0.12.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@gjsify/cli": "^0.8.0",
50
- "@gjsify/unit": "^0.8.0",
49
+ "@gjsify/cli": "^0.12.0",
50
+ "@gjsify/unit": "^0.12.0",
51
51
  "@types/node": "^25.9.2",
52
52
  "typescript": "^6.0.3"
53
53
  },