@gjsify/devtools 0.41.0 → 0.42.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.
|
@@ -47,6 +47,15 @@ import"./_virtual/_rolldown/runtime.js";import{DEVTOOLS_INTERFACE as e}from"@gjs
|
|
|
47
47
|
<arg type="s" direction="in" name="path"/>
|
|
48
48
|
<arg type="b" direction="out" name="activated"/>
|
|
49
49
|
</method>
|
|
50
|
+
<method name="FindWidget">
|
|
51
|
+
<arg type="s" direction="in" name="selector"/>
|
|
52
|
+
<arg type="s" direction="out" name="path"/>
|
|
53
|
+
</method>
|
|
54
|
+
<method name="SendKey">
|
|
55
|
+
<arg type="s" direction="in" name="accelerator"/>
|
|
56
|
+
<arg type="s" direction="in" name="path"/>
|
|
57
|
+
<arg type="b" direction="out" name="delivered"/>
|
|
58
|
+
</method>
|
|
50
59
|
<method name="DumpGSettings">
|
|
51
60
|
<arg type="s" direction="in" name="schema_id"/>
|
|
52
61
|
<arg type="s" direction="out" name="settings_json"/>
|
|
@@ -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,
|
|
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,findWidgetPath as d,getWidgetProperty as f,listToplevels as p,parseWidgetSelector as m,pathOfWidget as h,resolveWidgetPath as g,sendKeyToWidget as _,widgetType as v}from"./widget-tree.js";import y from"@girs/glib-2.0";import b from"@girs/gtk-4.0";import{GENERIC_METHODS as x,formatDbusErrorMessage as S}from"@gjsify/devtools-protocol";import C from"@girs/gio-2.0";function isActiveWindowScope(e){return!e||e===`window`||e===`active`}function frameDelay(e){return new Promise(t=>{y.timeout_add(y.PRIORITY_DEFAULT,e,()=>(t(),y.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(x))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=C.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:b.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 y.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(S(`not-found`,`no widget at '${e}'`))}let n=s(t.widget);if(!n){let e=t.widget.get_root();e instanceof b.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(S(`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(p())}DumpTree(e,t){let n=this._resolveRootWidget(e);if(!n)throw Error(S(`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(S(`not-found`,`no widget at '${e}'`));let r=f(n.widget,t);if(r===void 0)throw Error(S(`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:h(t),name:t.get_name()||null,type:v(t)}:null)}ActivateWidget(e){this._guard(`ActivateWidget`);let t=this._resolveRootWidget(e);if(!t)throw Error(S(`not-found`,`no widget at '${e}'`));return l(t.widget)}FindWidget(e){let t=m(e);if(!t)throw Error(S(`invalid-params`,`empty selector '${e}'`));let n=this._resolveRootWidget(``);return n?d(n.widget,t,n.path)??``:``}SendKey(e,t){this._guard(`SendKey`);let[n,r,i]=b.accelerator_parse(e);if(!n)throw Error(S(`invalid-params`,`cannot parse accelerator '${e}'`));let a;if(a=t?this._resolveRootWidget(t)?.widget??null:this._app.get_active_window()?.get_focus()??null,!a)throw Error(S(`not-found`,`no widget at '${t||`focus`}'`));return _(a,r,i??0)}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:h(e)??`toplevel:0`}:null}let t=g(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(S(`internal`,`unclassified devtools method '${e}'`));if(t===`mutating`&&this._isPaused())throw Error(S(`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 b.ApplicationWindow?t:null}_requireGroup(e,t){let n=this._actionGroup(e);if(!n)throw Error(S(`unavailable`,`no '${e}' action group`));if(!n.has_action(t))throw Error(S(`not-found`,`unknown action ${e}.${t}`));return n}_parseValue(e){if(!e||e===`null`)return null;try{return JSON.parse(e)}catch{throw Error(S(`invalid-params`,`value_json is not valid JSON: ${e}`))}}};export{DevtoolsService};
|
package/lib/esm/widget-tree.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import e from"@girs/gtk-4.0";function parseWidgetPath(e){let t=e.split(`/`).filter(e=>e.length>0);if(t.length===0)return null;let n=t[0].match(/^toplevel:(\d+)$/);if(!n)return null;let r=[];for(let e of t.slice(1)){let t=e.match(/^child:(\d+)$/);if(!t)return null;r.push(Number(t[1]))}return{toplevel:Number(n[1]),children:r}}function buildWidgetPath(e,t){return[`toplevel:${e}`,...t.map(e=>`child:${e}`)].join(`/`)}function widgetType(e){let t=e;return typeof t
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import e from"@girs/gtk-4.0";function parseWidgetPath(e){let t=e.split(`/`).filter(e=>e.length>0);if(t.length===0)return null;let n=t[0].match(/^toplevel:(\d+)$/);if(!n)return null;let r=[];for(let e of t.slice(1)){let t=e.match(/^child:(\d+)$/);if(!t)return null;r.push(Number(t[1]))}return{toplevel:Number(n[1]),children:r}}function buildWidgetPath(e,t){return[`toplevel:${e}`,...t.map(e=>`child:${e}`)].join(`/`)}function widgetType(e){return gtypeName(e)??`GtkWidget`}function gtypeName(e){let t=e;return typeof t?.$typeName==`string`&&t.$typeName.length>0?t.$typeName:t?.constructor?.$gtype?.name??null}function activateWidget(e){let t=e;if(typeof t.activate==`function`&&t.activate()===!0)return!0;let n=typeof t.get_parent==`function`?t.get_parent():null;if(n&&widgetType(n)===`GtkListBox`){let t=n,r=!1;return typeof t.select_row==`function`&&(t.select_row(e),r=!0),typeof t.emit==`function`&&(t.emit(`row-activated`,e),r=!0),r}return!1}function sendKeyToWidget(e,t,n){let r=e.observe_controllers(),i=!1;for(let e=0;e<r.get_n_items();e++){let a=r.get_item(e);gtypeName(a)===`GtkEventControllerKey`&&(a.emit(`key-pressed`,t,0,n),i=!0)}return i}function nthChild(e,t){let n=e.get_first_child(),r=0;for(;n&&r<t;)n=n.get_next_sibling(),r++;return r===t?n:null}function listToplevels(){let t=e.Window.get_toplevels(),n=t.get_n_items(),r=[];for(let e=0;e<n;e++){let n=t.get_item(e);n&&r.push({path:`toplevel:${e}`,type:widgetType(n),title:n.get_title?.()??null,mapped:n.get_mapped(),focused:n.is_active??!1})}return r}function resolveWidgetPath(t){let n=parseWidgetPath(t);if(!n)return null;let r=e.Window.get_toplevels().get_item(n.toplevel);if(!r)return null;for(let e of n.children)if(r=nthChild(r,e),!r)return null;return r}function parseWidgetSelector(e){let t=e.indexOf(`:`),n=(t<0?e:e.slice(0,t)).trim(),r=(t<0?``:e.slice(t+1)).trim();return!n&&!r?null:{type:n,cssClass:r}}function findWidgetPath(e,t,n){if(!e.get_visible()||!e.get_mapped())return null;let r=!t.type||widgetType(e)===t.type,i=!t.cssClass||e.get_css_classes().includes(t.cssClass);if(r&&i)return n;let a=e.get_first_child(),o=0;for(;a;){let e=findWidgetPath(a,t,`${n}/child:${o}`);if(e)return e;a=a.get_next_sibling(),o++}return null}function dumpTree(e,t,n){let r={path:n,type:widgetType(e),name:e.get_name()||null,cssClasses:e.get_css_classes(),mapped:e.get_mapped(),visible:e.get_visible(),children:[]};if(t<=0)return r;let i=e.get_first_child(),a=0;for(;i;)r.children.push(dumpTree(i,t-1,`${n}/child:${a}`)),i=i.get_next_sibling(),a++;return r}function jsonSafe(e){let t=typeof e;return e==null||t===`string`||t===`number`||t===`boolean`?e??null:`[${t}]`}function getWidgetProperty(e,t){let n=e,r=t.replace(/-/g,`_`),i=t.replace(/[-_]([a-z])/g,(e,t)=>t.toUpperCase());for(let e of[t,r,i])if(e in n)return jsonSafe(n[e])}function childIndex(e,t){let n=e.get_first_child(),r=0;for(;n;){if(n===t)return r;n=n.get_next_sibling(),r++}return-1}function pathOfWidget(t){let n=[],r=t,i=r.get_parent();for(;i;){let e=childIndex(i,r);if(e<0)return null;n.unshift(e),r=i,i=r.get_parent()}let a=e.Window.get_toplevels(),o=a.get_n_items();for(let e=0;e<o;e++)if(a.get_item(e)===r)return buildWidgetPath(e,n);return null}export{activateWidget,buildWidgetPath,dumpTree,findWidgetPath,getWidgetProperty,listToplevels,parseWidgetPath,parseWidgetSelector,pathOfWidget,resolveWidgetPath,sendKeyToWidget,widgetType};
|
|
@@ -107,6 +107,32 @@ export declare class DevtoolsService {
|
|
|
107
107
|
* counterpart of DumpTree/GetProperty. Throws if the path resolves to no
|
|
108
108
|
* live widget; returns whether it activated. */
|
|
109
109
|
ActivateWidget(path: string): boolean;
|
|
110
|
+
/** `FindWidget(selector) -> s` — path of the first widget matching
|
|
111
|
+
* `Type`, `:css-class` or `Type:css-class`, searched depth-first from the
|
|
112
|
+
* active window; `''` when nothing matches.
|
|
113
|
+
*
|
|
114
|
+
* The lookup every click-driving caller was doing by hand. Widget paths are
|
|
115
|
+
* POSITIONAL, so one written into a script is wrong as soon as a widget is
|
|
116
|
+
* inserted above it — callers therefore dumped the tree and walked the JSON
|
|
117
|
+
* themselves, reimplementing this same walk once per language. Empty string
|
|
118
|
+
* rather than an error for "no match": not finding a widget is an ordinary
|
|
119
|
+
* answer a caller acts on (wait and retry, or report), not a fault.
|
|
120
|
+
*
|
|
121
|
+
* Invisible and unmapped subtrees are skipped — see {@link findWidgetPath}. */
|
|
122
|
+
FindWidget(selector: string): string;
|
|
123
|
+
/** `SendKey(accelerator, path) -> b` — deliver a key to the key controllers of the
|
|
124
|
+
* widget at `path` (empty path = the focused widget, else the active window).
|
|
125
|
+
*
|
|
126
|
+
* The half of headless GUI verification that was missing: buttons could be
|
|
127
|
+
* pressed and actions fired, but nothing could be TYPED, so every
|
|
128
|
+
* `Gtk.EventControllerKey` handler — an editor's arrow keys, Delete in a
|
|
129
|
+
* canvas — was unverifiable. Accelerator syntax is GTK's own
|
|
130
|
+
* (`Delete`, `Left`, `<primary>s`, `<shift>Up`).
|
|
131
|
+
*
|
|
132
|
+
* What it proves is the HANDLER, not GDK's routing — see
|
|
133
|
+
* {@link sendKeyToWidget}. Pair it with `GetProperty(path, "focusable")` when
|
|
134
|
+
* the question is whether a real key would arrive. */
|
|
135
|
+
SendKey(accelerator: string, path: string): boolean;
|
|
110
136
|
/** `DumpGSettings(schema_id) -> s` — JSON of a schema's keys + values. */
|
|
111
137
|
DumpGSettings(schemaId: string): string;
|
|
112
138
|
/** `DumpCss() -> s` — JSON of the devtools-installed CSS provider names. */
|
|
@@ -50,6 +50,25 @@ export declare function widgetType(widget: Gtk.Widget): string;
|
|
|
50
50
|
* so the spec can feed plain mock shapes).
|
|
51
51
|
*/
|
|
52
52
|
export declare function activateWidget(widget: Gtk.Widget): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Deliver a key to a widget's own key controllers — how external tooling proves a KEYBOARD works.
|
|
55
|
+
*
|
|
56
|
+
* The control plane could already press buttons and fire actions, so a GUI could be driven and
|
|
57
|
+
* screenshotted headlessly — but not TYPED INTO. Anything handled by a `Gtk.EventControllerKey`
|
|
58
|
+
* (an editor's arrow keys, Delete in a canvas, Escape in a picker) was therefore unverifiable: the
|
|
59
|
+
* code looked right, the screenshot looked right, and whether a key ever reached the handler was
|
|
60
|
+
* nobody's measurement. A `Gtk.DrawingArea` that is not `focusable` swallows every key silently,
|
|
61
|
+
* which is exactly the shape of bug that survives review.
|
|
62
|
+
*
|
|
63
|
+
* It emits `key-pressed` on the controllers ATTACHED TO THE WIDGET rather than fabricating a
|
|
64
|
+
* `Gdk.Event`: GTK4 made events opaque with no public constructor, so no binding can construct
|
|
65
|
+
* one. Say what that means honestly — this proves the HANDLER runs and what it does, not that GDK
|
|
66
|
+
* would route a real key press here. The two differ exactly when the widget cannot take focus, so
|
|
67
|
+
* a caller checking a keyboard should read `focusable` as well.
|
|
68
|
+
*
|
|
69
|
+
* Returns true if some controller claimed the key (its handler returned true).
|
|
70
|
+
*/
|
|
71
|
+
export declare function sendKeyToWidget(widget: Gtk.Widget, keyval: number, modifiers: number): boolean;
|
|
53
72
|
/** Enumerate live toplevel windows as `{ path, type, title, mapped, focused }`. */
|
|
54
73
|
export declare function listToplevels(): Array<{
|
|
55
74
|
path: string;
|
|
@@ -60,6 +79,36 @@ export declare function listToplevels(): Array<{
|
|
|
60
79
|
}>;
|
|
61
80
|
/** Resolve a widget path to its live widget, or null if it no longer exists. */
|
|
62
81
|
export declare function resolveWidgetPath(path: string): Gtk.Widget | null;
|
|
82
|
+
/**
|
|
83
|
+
* A widget selector: `Type`, `:css-class`, or `Type:css-class`.
|
|
84
|
+
*
|
|
85
|
+
* Widget PATHS are positional (`toplevel:0/child:0/child:3/…`), which is what makes them stable to
|
|
86
|
+
* pass around and useless to write down: inserting one widget above the target renames every path
|
|
87
|
+
* below it. So a script that wants to press "the suggested-action button on the error page" had to
|
|
88
|
+
* dump the whole tree and walk the JSON itself — which is exactly the walk this module already
|
|
89
|
+
* does, reimplemented once per caller in whatever language the caller happens to be.
|
|
90
|
+
*/
|
|
91
|
+
export interface WidgetSelector {
|
|
92
|
+
/** GType name, e.g. `GtkButton`. Empty matches any type. */
|
|
93
|
+
type: string;
|
|
94
|
+
/** CSS class the widget must carry, e.g. `suggested-action`. Empty matches any. */
|
|
95
|
+
cssClass: string;
|
|
96
|
+
}
|
|
97
|
+
/** Parse `Type`, `Type:css-class` or `:css-class`. Returns null when both halves are empty. */
|
|
98
|
+
export declare function parseWidgetSelector(selector: string): WidgetSelector | null;
|
|
99
|
+
/**
|
|
100
|
+
* Find the first widget matching `selector`, depth-first, and return its path — `null` if none.
|
|
101
|
+
*
|
|
102
|
+
* Depth-first means the first hit is the topmost one in reading order, which is what a person
|
|
103
|
+
* describing "the button" means.
|
|
104
|
+
*
|
|
105
|
+
* Invisible and unmapped widgets are SKIPPED, subtree and all. A GTK tree is full of widgets that
|
|
106
|
+
* exist but are not on screen — the other pages of a stack, a hidden row, the collapsed half of a
|
|
107
|
+
* split view — and activating one of those proves nothing about what a user can reach. Skipping the
|
|
108
|
+
* subtree as well as the node matters: the children of an unmapped stack page are unmapped too, but
|
|
109
|
+
* a caller checking only the leaf would happily match one.
|
|
110
|
+
*/
|
|
111
|
+
export declare function findWidgetPath(root: Gtk.Widget, selector: WidgetSelector, basePath: string): string | null;
|
|
63
112
|
/** Dump a widget subtree to {@link NodeInfo}, bounded by `maxDepth`. */
|
|
64
113
|
export declare function dumpTree(root: Gtk.Widget, maxDepth: number, basePath: string): NodeInfo;
|
|
65
114
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.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.
|
|
46
|
+
"@gjsify/devtools-protocol": "^0.42.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@gjsify/cli": "^0.
|
|
50
|
-
"@gjsify/unit": "^0.
|
|
49
|
+
"@gjsify/cli": "^0.42.0",
|
|
50
|
+
"@gjsify/unit": "^0.42.0",
|
|
51
51
|
"@types/node": "^25.9.2",
|
|
52
52
|
"typescript": "^6.0.3"
|
|
53
53
|
},
|