@gjsify/devtools 0.38.1 → 0.39.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.
@@ -1 +1 @@
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{removeDevtoolsAddressFile as o}from"./peer-transport.js";import{captureWidgetPng as s}from"./screenshot.js";import{dumpGSettings as c}from"./gsettings.js";import{activateWidget as l,dumpTree as u,getWidgetProperty as d,listToplevels as f,pathOfWidget as p,resolveWidgetPath as m,widgetType as h}from"./widget-tree.js";import g from"@girs/glib-2.0";import _ from"@girs/gtk-4.0";import{GENERIC_METHODS as v,formatDbusErrorMessage as y}from"@gjsify/devtools-protocol";import b from"@girs/gio-2.0";function frameDelay(e){return new Promise(t=>{g.timeout_add(g.PRIORITY_DEFAULT,e,()=>(t(),g.SOURCE_REMOVE))})}async function captureWidgetWhenRenderable(e,t=12,n=50){for(let r=0;r<t;r++){let t=s(e);if(t)return t;await frameDelay(n)}return s(e)}var DevtoolsService=class{constructor(e,t){this._app=e,this._options=t,this._exported=new Map,this._peer=null,this._addressFile=null,this._kinds=new Map,this._extensions=t.extend??[];for(let[e,t]of Object.entries(v))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.has(e))return;let n=a(this._extensions.flatMap(e=>e.methodsXml??[])),r=b.DBusExportedObject.wrapJSObject(n,this);r.export(e,t),this._exported.set(e,r)}unexport(e){if(e){this._exported.get(e)?.unexport(),this._exported.delete(e);return}for(let e of this._exported.values())e.unexport();this._exported.clear(),this._peer?.stop(),this._peer=null,this._addressFile&&=(o(this._addressFile),null)}attachPeerServer(e,t){this._peer=e,this._addressFile=t??null}get peerAddress(){return this._peer?.address??null}get addressFile(){return this._addressFile}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:_.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 ScreenshotAsync(e,t){try{let e=await this._captureActiveWindowPng();t.return_value(new g.Variant(`(ay)`,[e]))}catch(e){t.return_dbus_error(`org.gjsify.Devtools.Error.Screenshot`,e instanceof Error?e.message:String(e))}}async _captureActiveWindowPng(){let e=this._app.get_active_window();if(!e)return new Uint8Array;let t=s(e);return t||=(e.present(),await captureWidgetWhenRenderable(e)),t??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(y(`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(f())}DumpTree(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(y(`not-found`,`no widget at '${e||`active window`}'`));return JSON.stringify(u(n.widget,t>0?t:8,n.path))}GetProperty(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(y(`not-found`,`no widget at '${e}'`));let r=d(n.widget,t);if(r===void 0)throw Error(y(`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:p(t),name:t.get_name()||null,type:h(t)}:null)}ActivateWidget(e){this._guard(`ActivateWidget`);let t=this._resolveRootWidget(e);if(!t)throw Error(y(`not-found`,`no widget at '${e}'`));return l(t.widget)}DumpGSettings(e){return JSON.stringify(c(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:p(e)??`toplevel:0`}:null}let t=m(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(y(`internal`,`unclassified devtools method '${e}'`));if(t===`mutating`&&this._isPaused())throw Error(y(`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 _.ApplicationWindow?t:null}_requireGroup(e,t){let n=this._actionGroup(e);if(!n)throw Error(y(`unavailable`,`no '${e}' action group`));if(!n.has_action(t))throw Error(y(`not-found`,`unknown action ${e}.${t}`));return n}_parseValue(e){if(!e||e===`null`)return null;try{return JSON.parse(e)}catch{throw Error(y(`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{removeDevtoolsAddressFile as o}from"./peer-transport.js";import{captureWidgetPng as s}from"./screenshot.js";import{dumpGSettings as c}from"./gsettings.js";import{activateWidget as l,dumpTree as u,getWidgetProperty as d,listToplevels as f,pathOfWidget as p,resolveWidgetPath as m,widgetType as h}from"./widget-tree.js";import g from"@girs/glib-2.0";import _ from"@girs/gtk-4.0";import{GENERIC_METHODS as v,formatDbusErrorMessage as y}from"@gjsify/devtools-protocol";import b from"@girs/gio-2.0";function isActiveWindowScope(e){return!e||e===`window`||e===`active`}function frameDelay(e){return new Promise(t=>{g.timeout_add(g.PRIORITY_DEFAULT,e,()=>(t(),g.SOURCE_REMOVE))})}async function captureWidgetWhenRenderable(e,t=12,n=50){for(let r=0;r<t;r++){let t=s(e);if(t)return t;await frameDelay(n)}return s(e)}var DevtoolsService=class{constructor(e,t){this._app=e,this._options=t,this._exported=new Map,this._peer=null,this._addressFile=null,this._kinds=new Map,this._extensions=t.extend??[];for(let[e,t]of Object.entries(v))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.has(e))return;let n=a(this._extensions.flatMap(e=>e.methodsXml??[])),r=b.DBusExportedObject.wrapJSObject(n,this);r.export(e,t),this._exported.set(e,r)}unexport(e){if(e){this._exported.get(e)?.unexport(),this._exported.delete(e);return}for(let e of this._exported.values())e.unexport();this._exported.clear(),this._peer?.stop(),this._peer=null,this._addressFile&&=(o(this._addressFile),null)}attachPeerServer(e,t){this._peer=e,this._addressFile=t??null}get peerAddress(){return this._peer?.address??null}get addressFile(){return this._addressFile}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:_.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 ScreenshotAsync(e,t){try{let n=typeof e?.[0]==`string`?e[0]:``,r=await this._captureScopePng(n);t.return_value(new g.Variant(`(ay)`,[r]))}catch(e){t.return_dbus_error(`org.gjsify.Devtools.Error.Screenshot`,e instanceof Error?e.message:String(e))}}async _captureScopePng(e){let t=this._resolveRootWidget(e);if(!t){if(isActiveWindowScope(e))return new Uint8Array;throw Error(y(`not-found`,`no widget at '${e}'`))}let n=s(t.widget);if(!n){let e=t.widget.get_root();e instanceof _.Window&&e.present(),n=await captureWidgetWhenRenderable(t.widget)}return 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(y(`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(f())}DumpTree(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(y(`not-found`,`no widget at '${e||`active window`}'`));return JSON.stringify(u(n.widget,t>0?t:8,n.path))}GetProperty(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(y(`not-found`,`no widget at '${e}'`));let r=d(n.widget,t);if(r===void 0)throw Error(y(`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:p(t),name:t.get_name()||null,type:h(t)}:null)}ActivateWidget(e){this._guard(`ActivateWidget`);let t=this._resolveRootWidget(e);if(!t)throw Error(y(`not-found`,`no widget at '${e}'`));return l(t.widget)}DumpGSettings(e){return JSON.stringify(c(e))}DumpCss(){return JSON.stringify(r())}SwapCss(e,t){return this._guard(`SwapCss`),i(e,t)}_resolveRootWidget(e){if(isActiveWindowScope(e)){let e=this._app.get_active_window();return e?{widget:e,path:p(e)??`toplevel:0`}:null}let t=m(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(y(`internal`,`unclassified devtools method '${e}'`));if(t===`mutating`&&this._isPaused())throw Error(y(`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 _.ApplicationWindow?t:null}_requireGroup(e,t){let n=this._actionGroup(e);if(!n)throw Error(y(`unavailable`,`no '${e}' action group`));if(!n.has_action(t))throw Error(y(`not-found`,`unknown action ${e}.${t}`));return n}_parseValue(e){if(!e||e===`null`)return null;try{return JSON.parse(e)}catch{throw Error(y(`invalid-params`,`value_json is not valid JSON: ${e}`))}}};export{DevtoolsService};
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -48,7 +48,20 @@ export declare class DevtoolsService {
48
48
  /** `GetStatus() -> s` — JSON snapshot of the app's live state. */
49
49
  GetStatus(): string;
50
50
  /**
51
- * `Screenshot(scope) -> ay` — PNG bytes of the active window via the GSK renderer.
51
+ * `Screenshot(scope) -> ay` — PNG bytes of `scope` via the GSK renderer.
52
+ *
53
+ * `scope` is the same widget vocabulary `DumpTree`/`GetProperty`/`ActivateWidget`
54
+ * take: `''`, `window` or `active` for the active window, or a stable
55
+ * `toplevel:N/child:M` path for ONE widget inside it. Capturing a single widget
56
+ * costs nothing extra — `captureWidgetPng` snapshots whatever `Gtk.Widget` it is
57
+ * handed — so the only thing that ever made this window-only was not reading the
58
+ * argument.
59
+ *
60
+ * It went unread for the whole life of the method while being DECLARED in the
61
+ * interface XML and exposed as a user-facing `scope` parameter on the MCP
62
+ * `screenshot` tool, which even defaulted it to `'window'`. Asking for a child
63
+ * widget therefore returned the whole window and said nothing — the failure mode a
64
+ * declared-but-ignored argument always has, since every caller looks correct.
52
65
  *
53
66
  * Uses the `<Name>Async` manual-reply convention: the method takes the raw
54
67
  * `invocation` and calls `return_value()` itself, and `wrapJSObject` finds it because
@@ -61,14 +74,14 @@ export declare class DevtoolsService {
61
74
  * unaffected. That no longer reproduces on gjs 1.88.1 — a plain `async Screenshot()`
62
75
  * returning `ay` marshals correctly — so this can collapse back into one.
63
76
  */
64
- ScreenshotAsync(_params: unknown[], invocation: Gio.DBusMethodInvocation): Promise<void>;
77
+ ScreenshotAsync(params: unknown[], invocation: Gio.DBusMethodInvocation): Promise<void>;
65
78
  /**
66
- * Capture the active window to PNG, warming up across frames: a just-launched or
67
- * mid-layout window yields a zero-size GSK frame, so the first `captureWidgetPng` can
68
- * be empty. Empty bytes remain the genuine-failure signal, for a window that never
69
- * realises at all.
79
+ * Capture `scope` to PNG, warming up across frames: a just-launched or mid-layout
80
+ * window yields a zero-size GSK frame, so the first `captureWidgetPng` can be empty.
81
+ * Empty bytes remain the genuine-failure signal, for a window that never realises at
82
+ * all.
70
83
  */
71
- private _captureActiveWindowPng;
84
+ private _captureScopePng;
72
85
  /** `ListActions() -> s` — JSON of the `app.*` + `win.*` actions. */
73
86
  ListActions(): string;
74
87
  /** `ActivateAction(scope, name, value_json)` — activate a GAction with an optional parameter. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/devtools",
3
- "version": "0.38.1",
3
+ "version": "0.39.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": "^4.1.0",
44
44
  "@girs/graphene-1.0": "^4.1.0",
45
45
  "@girs/gtk-4.0": "^4.1.0",
46
- "@gjsify/devtools-protocol": "^0.38.1"
46
+ "@gjsify/devtools-protocol": "^0.39.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@gjsify/cli": "^0.38.1",
50
- "@gjsify/unit": "^0.38.1",
49
+ "@gjsify/cli": "^0.39.0",
50
+ "@gjsify/unit": "^0.39.0",
51
51
  "@types/node": "^25.9.2",
52
52
  "typescript": "^6.0.3"
53
53
  },