@gjsify/devtools 0.63.1-next.235 → 0.63.1-next.236
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 +25 -2
- package/lib/esm/devtools-iface.js +5 -0
- package/lib/esm/devtools-service.js +1 -1
- package/lib/esm/index.js +1 -1
- package/lib/esm/screenshot.js +1 -1
- package/lib/types/devtools-service.d.ts +19 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/screenshot.d.ts +39 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ gdbus call --session -d org.example.App -o /org/example/App/devtools \
|
|
|
41
41
|
-m org.gjsify.Devtools.GetStatus
|
|
42
42
|
gdbus call --session -d org.example.App -o /org/example/App/devtools \
|
|
43
43
|
-m org.gjsify.Devtools.Screenshot 'window' # PNG bytes as a GVariant `ay`
|
|
44
|
+
gdbus call --session -d org.example.App -o /org/example/App/devtools \
|
|
45
|
+
-m org.gjsify.Devtools.ScreenshotScaled 'window' 2.0 # the same, at 2× device pixels
|
|
44
46
|
```
|
|
45
47
|
|
|
46
48
|
## No session bus? (macOS, Windows)
|
|
@@ -126,7 +128,8 @@ A peer call carries no destination bus name at all, so `nameHasOwner` / `listIns
|
|
|
126
128
|
|
|
127
129
|
| method | signature | what it does |
|
|
128
130
|
|---|---|---|
|
|
129
|
-
| `Screenshot` | `(scope: s) -> ay` | PNG bytes. `scope` `''`/`window` shoots the active window; a widget path shoots ONE widget. |
|
|
131
|
+
| `Screenshot` | `(scope: s) -> ay` | PNG bytes at the widget's OWN scale factor, so a HiDPI window captures at the resolution it is drawn at. `scope` `''`/`window` shoots the active window; a widget path shoots ONE widget. |
|
|
132
|
+
| `ScreenshotScaled` | `(scope: s, scale: d) -> ay` | the same capture at an explicit device-pixel scale; `scale <= 0` means the widget's own factor. |
|
|
130
133
|
| `DumpTree` | `-> s` | the widget tree as JSON, with stable `toplevel:N/child:M` paths. |
|
|
131
134
|
| `GetProperty` | `(path, prop) -> s` | read one property off the widget at `path`. |
|
|
132
135
|
| `GetFocused` / `ListToplevels` | `-> s` | the focused widget's path; the toplevel list. |
|
|
@@ -158,6 +161,7 @@ have — see above). So both halves live in this package instead of in every con
|
|
|
158
161
|
|
|
159
162
|
```bash
|
|
160
163
|
gjsify devtools shot /tmp/app.png --bus-name org.example.App # PNG on disk + its real size
|
|
164
|
+
gjsify devtools shot /tmp/app@2x.png --scale 2 --bus-name org.example.App # 2× device pixels
|
|
161
165
|
gjsify devtools find Adw.StatusPage:error --bus-name org.example.App # → a widget path
|
|
162
166
|
gjsify devtools activate /toplevel:0/child:3 --bus-name org.example.App
|
|
163
167
|
gjsify devtools help # every operation and flag
|
|
@@ -192,6 +196,25 @@ shot.width; // 1100 — read out of the PNG, NOT the size asked for
|
|
|
192
196
|
The `./shot` entry point is deliberately GTK-free: the client side of a screenshot needs Gio and
|
|
193
197
|
GLib only, so a headless client does not drag this package's app-side GTK adapter in with it.
|
|
194
198
|
|
|
199
|
+
For a 2× shot, the `source` lambda calls `ScreenshotScaled` instead — `captureShot` itself has
|
|
200
|
+
nothing to say about the scale, because the scope is the only thing it passes on:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
await captureShot(
|
|
204
|
+
async (scope) =>
|
|
205
|
+
client.control(instance, 'ScreenshotScaled', tupleSD(scope, 2), '(ay)'),
|
|
206
|
+
'/tmp/app@2x.png',
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Keep the default on `Screenshot`.** The app and the caller are published separately, so a
|
|
211
|
+
caller that reaches for `ScreenshotScaled` unconditionally fails with `UnknownMethod` against
|
|
212
|
+
every app on `@gjsify/devtools < 0.64` — for a capture the old app could have served. That is
|
|
213
|
+
also why the override is a second method rather than a third argument: an added in-arg fails
|
|
214
|
+
`InvalidArgs`, which a caller cannot tell from a request it got wrong, whereas an unknown NAME is
|
|
215
|
+
detectable and falls back cleanly. `gjsify devtools shot` and the MCP `screenshot` tool both
|
|
216
|
+
switch methods only when a scale is actually asked for.
|
|
217
|
+
|
|
195
218
|
Two invariants ride along, each paid for with a measured incident:
|
|
196
219
|
|
|
197
220
|
- **Empty bytes are a FAILURE, not a picture.** `Screenshot` answers `ay[0]` when there is no active
|
|
@@ -253,7 +276,7 @@ literals, it does not claim a rendered string is clean.
|
|
|
253
276
|
- `DevtoolsService` — the exported service object `installDevtools` returns and `uninstallDevtools` accepts; `service.peerAddress` is the address clients dial in bus-less mode (`null` on a bus), `service.addressFile` is where that address is published (`null` when it is not).
|
|
254
277
|
- `DevtoolsExtension`, `InstallDevtoolsOptions` — the extension + options types.
|
|
255
278
|
- `startDevtoolsPeerServer(service, objectPath, address?)` → `DevtoolsPeerServer` — the bus-less transport by hand; it throws `DevtoolsPeerServerError` (with `address` + `reason`) when the address is unusable, which is what `installDevtools` catches. `chooseDevtoolsTransport(input)` — the precedence table above as a pure function; `writeDevtoolsAddressFile` / `removeDevtoolsAddressFile` — publish/retract a peer address.
|
|
256
|
-
- `captureWidgetPng` (GSK screenshot)
|
|
279
|
+
- `captureWidget(widget, options?)` → `CaptureResult` and `captureWidgetPng(widget, options?)` → bytes-or-`null` (GSK screenshot); `CaptureOptions.scale` is device pixels per logical pixel and `defaultCaptureScale(widget)` is what it defaults to — the widget's own `get_scale_factor()`, clamped to a positive number. `buildVariant` / `variantKindFor` / `VariantKind` (GVariant), `activateAction` / `changeActionState` / `describeActions` (GAction bridge).
|
|
257
280
|
- widget-tree helpers (`dumpTree`, `getWidgetProperty`, `listToplevels`, `resolveWidgetPath`, …), `dumpCss` / `swapCss` / `removeCss`, `dumpGSettings`, `buildDevtoolsIfaceXml`.
|
|
258
281
|
- **client-side, from the GTK-free `./shot` entry point**: `captureShot(source, path, scope?, options?)` → `ShotResult` (with `width`/`height` read from the PNG) or `CaptureShotError`; `pngSize(bytes)` → `{ width, height }` or `null` for bytes that are not a PNG.
|
|
259
282
|
- markup-trap helpers (root entry point): `hasRawPangoMarkup(text)`, `rawPangoMarkupIn(text)`, `isPangoMarkupSink(class, property)`, `PANGO_MARKUP_SINKS`.
|
|
@@ -7,6 +7,11 @@ import"./_virtual/_rolldown/runtime.js";import{DEVTOOLS_INTERFACE as e}from"@gjs
|
|
|
7
7
|
<arg type="s" direction="in" name="scope"/>
|
|
8
8
|
<arg type="ay" direction="out" name="png_bytes"/>
|
|
9
9
|
</method>
|
|
10
|
+
<method name="ScreenshotScaled">
|
|
11
|
+
<arg type="s" direction="in" name="scope"/>
|
|
12
|
+
<arg type="d" direction="in" name="scale"/>
|
|
13
|
+
<arg type="ay" direction="out" name="png_bytes"/>
|
|
14
|
+
</method>
|
|
10
15
|
<method name="ListActions">
|
|
11
16
|
<arg type="s" direction="out" name="actions_json"/>
|
|
12
17
|
</method>
|
|
@@ -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{captureWidget 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"gi://GLib?version=2.0";import b from"gi://Gtk?version=4.0";import{GENERIC_METHODS as x,formatDbusErrorMessage as S}from"@gjsify/devtools-protocol";import C from"gi://Gio?version=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=
|
|
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{captureWidget 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"gi://GLib?version=2.0";import b from"gi://Gtk?version=4.0";import{GENERIC_METHODS as x,formatDbusErrorMessage as S}from"@gjsify/devtools-protocol";import C from"gi://Gio?version=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={},n=12,r=50){let i=null;for(let a=0;a<n;a++){let n=s(e,t);if(n.png)return{png:n.png,blocker:null};i=n.blocker,await frameDelay(r)}let a=s(e,t);return{png:a.png,blocker:a.png?null:a.blocker??i}}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 ScreenshotScaledAsync(e,t){try{let n=typeof e?.[0]==`string`?e[0]:``,r=typeof e?.[1]==`number`?e[1]:0,i=await this._captureScopePng(n,r>0?{scale:r}:{});t.return_value(new y.Variant(`(ay)`,[i]))}catch(e){t.return_dbus_error(`org.gjsify.Devtools.Error.Screenshot`,e instanceof Error?e.message:String(e))}}async _captureScopePng(e,t={}){let n=this._resolveRootWidget(e);if(!n){if(isActiveWindowScope(e))return new Uint8Array;throw Error(S(`not-found`,`no widget at '${e}'`))}let r=s(n.widget,t);if(r.png)return r.png;let i=n.widget.get_root();i instanceof b.Window&&i.present();let a=await captureWidgetWhenRenderable(n.widget,t);return a.png?a.png:(console.warn(`[devtools] Screenshot('${e}') has no image: ${a.blocker??`unknown`} after 12 tries. Empty bytes follow.`),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:40,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/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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{DevtoolsPeerServerError as l,removeDevtoolsAddressFile as u,startDevtoolsPeerServer as d,writeDevtoolsAddressFile as f}from"./peer-transport.js";import{captureWidget as p,captureWidgetPng as m}from"./screenshot.js";import{dumpGSettings as
|
|
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{DevtoolsPeerServerError as l,removeDevtoolsAddressFile as u,startDevtoolsPeerServer as d,writeDevtoolsAddressFile as f}from"./peer-transport.js";import{captureWidget as p,captureWidgetPng as m,defaultCaptureScale as h}from"./screenshot.js";import{dumpGSettings as g}from"./gsettings.js";import{buildWidgetPath as _,dumpTree as v,getWidgetProperty as y,listToplevels as b,parseWidgetPath as x,pathOfWidget as S,resolveWidgetPath as C,widgetIsA as w,widgetType as T}from"./widget-tree.js";import{DevtoolsService as E}from"./devtools-service.js";import{chooseDevtoolsTransport as D,installDevtools as O,uninstallDevtools as k}from"./install.js";import{CaptureShotError as A,captureShot as j,pngSize as M}from"./shot.js";import{PANGO_MARKUP_SINKS as N,hasRawPangoMarkup as P,isPangoMarkupSink as F,rawPangoMarkupIn as I}from"./pango.js";export*from"@gjsify/devtools-protocol";export{A as CaptureShotError,l as DevtoolsPeerServerError,E as DevtoolsService,N as PANGO_MARKUP_SINKS,n as activateAction,c as buildDevtoolsIfaceXml,e as buildVariant,_ as buildWidgetPath,j as captureShot,p as captureWidget,m as captureWidgetPng,r as changeActionState,D as chooseDevtoolsTransport,h as defaultCaptureScale,i as describeActions,a as dumpCss,g as dumpGSettings,v as dumpTree,y as getWidgetProperty,P as hasRawPangoMarkup,O as installDevtools,F as isPangoMarkupSink,b as listToplevels,x as parseWidgetPath,S as pathOfWidget,M as pngSize,I as rawPangoMarkupIn,o as removeCss,u as removeDevtoolsAddressFile,C as resolveWidgetPath,d as startDevtoolsPeerServer,s as swapCss,k as uninstallDevtools,t as variantKindFor,w as widgetIsA,T as widgetType,f as writeDevtoolsAddressFile};
|
package/lib/esm/screenshot.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import e from"gi://Gtk?version=4.0";import t from"gi://Graphene?version=1.0";function captureWidget(n){let
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import e from"gi://Gtk?version=4.0";import t from"gi://Graphene?version=1.0";function defaultCaptureScale(e){let t=e.get_scale_factor?.();return typeof t==`number`&&t>0?t:1}function captureWidget(n,r={}){let i=n.get_native()?.get_renderer();if(!i)return{png:null,blocker:`no-renderer`};let a=n.get_width(),o=n.get_height();if(a<=0||o<=0)return{png:null,blocker:`zero-size`};let s=r.scale&&r.scale>0?r.scale:defaultCaptureScale(n),c=e.WidgetPaintable.new(n),l=e.Snapshot.new();s!==1&&l.scale(s,s),c.snapshot(l,a,o);let u=l.to_node();if(!u)return{png:null,blocker:`empty-snapshot`};let d=new t.Rect;d.init(0,0,a*s,o*s);let f=i.render_texture(u,d).save_to_png_bytes().get_data();return f?{png:new Uint8Array(f)}:{png:null,blocker:`empty-png`}}function captureWidgetPng(e,t={}){return captureWidget(e,t).png}export{captureWidget,captureWidgetPng,defaultCaptureScale};
|
|
@@ -73,8 +73,27 @@ export declare class DevtoolsService {
|
|
|
73
73
|
* implementation returned an incorrect value type`), while a manual reply was
|
|
74
74
|
* unaffected. That no longer reproduces on gjs 1.88.1 — a plain `async Screenshot()`
|
|
75
75
|
* returning `ay` marshals correctly — so this can collapse back into one.
|
|
76
|
+
*
|
|
77
|
+
* The scale is the WIDGET's own `get_scale_factor()` and is not on this signature on
|
|
78
|
+
* purpose — see {@link ScreenshotScaledAsync} for why the override is a second method
|
|
79
|
+
* rather than a third argument here.
|
|
76
80
|
*/
|
|
77
81
|
ScreenshotAsync(params: unknown[], invocation: Gio.DBusMethodInvocation): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* `ScreenshotScaled(scope, scale) -> ay` — the same capture at an EXPLICIT device-pixel
|
|
84
|
+
* scale. `scale <= 0` means "whatever the widget's own `get_scale_factor()` says", which
|
|
85
|
+
* is exactly what `Screenshot` does, so the two methods differ in one thing only.
|
|
86
|
+
*
|
|
87
|
+
* WHY A SECOND METHOD AND NOT A THIRD ARGUMENT ON `Screenshot`. The two ends of this
|
|
88
|
+
* wire are published separately — `@gjsify/devtools` is in the app, `@gjsify/devtools-mcp`
|
|
89
|
+
* and the `gjsify devtools` CLI are in the caller's toolchain — so every mixed-version
|
|
90
|
+
* pair is a pair that really occurs. Adding an in-arg makes a new CLI's `(sd)` call fail
|
|
91
|
+
* `InvalidArgs` against an app on an older devtools, for a request the old app could
|
|
92
|
+
* have served; a new NAME fails `UnknownMethod`, which is a thing a caller can test for
|
|
93
|
+
* and fall back from, and it leaves `Screenshot(s) -> ay` byte-identical for the
|
|
94
|
+
* `gdbus` lines in this package's README and in consumers' scripts.
|
|
95
|
+
*/
|
|
96
|
+
ScreenshotScaledAsync(params: unknown[], invocation: Gio.DBusMethodInvocation): Promise<void>;
|
|
78
97
|
/**
|
|
79
98
|
* Capture `scope` to PNG, warming up across frames: a just-launched or mid-layout
|
|
80
99
|
* window yields a zero-size GSK frame, so the first capture can be empty. Empty
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { DevtoolsService } from './devtools-service.js';
|
|
|
4
4
|
export type { DevtoolsExtension, InstallDevtoolsOptions } from './extension.js';
|
|
5
5
|
export { DevtoolsPeerServerError, removeDevtoolsAddressFile, startDevtoolsPeerServer, writeDevtoolsAddressFile, } from './peer-transport.js';
|
|
6
6
|
export type { DevtoolsPeerServer } from './peer-transport.js';
|
|
7
|
-
export { captureWidget, captureWidgetPng, type CaptureBlocker, type CaptureResult } from './screenshot.js';
|
|
7
|
+
export { captureWidget, captureWidgetPng, defaultCaptureScale, type CaptureBlocker, type CaptureOptions, type CaptureResult, } from './screenshot.js';
|
|
8
8
|
export { CaptureShotError, captureShot, pngSize } from './shot.js';
|
|
9
9
|
export type { CaptureShotOptions, PngSize, PngSource, ShotFailure, ShotResult } from './shot.js';
|
|
10
10
|
export { buildVariant, variantKindFor } from './gvariant.js';
|
|
@@ -29,6 +29,25 @@ export type CaptureResult = {
|
|
|
29
29
|
readonly png: null;
|
|
30
30
|
readonly blocker: CaptureBlocker;
|
|
31
31
|
};
|
|
32
|
+
/** Options for {@link captureWidget}. */
|
|
33
|
+
export interface CaptureOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Device pixels per logical pixel. Defaults to the widget's own
|
|
36
|
+
* `get_scale_factor()`, so a window on a HiDPI monitor captures at the
|
|
37
|
+
* resolution it is actually drawn at.
|
|
38
|
+
*/
|
|
39
|
+
readonly scale?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The scale a capture uses when the caller names none: the widget's own
|
|
43
|
+
* `Gtk.Widget.get_scale_factor()`, clamped to a usable positive number.
|
|
44
|
+
*
|
|
45
|
+
* A widget handed in from a duck-typed test, or one not yet attached to a
|
|
46
|
+
* surface, may answer 0 or nothing at all — that is not a request for a
|
|
47
|
+
* zero-pixel image, it is an absent answer, and 1 is what the capture did
|
|
48
|
+
* before scaling existed.
|
|
49
|
+
*/
|
|
50
|
+
export declare function defaultCaptureScale(widget: Gtk.Widget): number;
|
|
32
51
|
/**
|
|
33
52
|
* Render a GTK widget — typically the top-level window — to PNG bytes, fully
|
|
34
53
|
* in-process via the GSK renderer. No external screenshot tools, no
|
|
@@ -40,7 +59,25 @@ export type CaptureResult = {
|
|
|
40
59
|
* `gtk4paintablesink` paintable in a `Gtk.Picture` captures at 202 645 bytes for the
|
|
41
60
|
* picture alone and 204 865 for its window, both with `videotestsrc` and with a real
|
|
42
61
|
* HLS stream. The renderer downloads the texture like any other.
|
|
62
|
+
*
|
|
63
|
+
* THE SCALE IS A GSK TRANSFORM AND IT GENUINELY RE-RASTERISES — the question every
|
|
64
|
+
* reader of this function asks, measured rather than assumed. `Gtk.Snapshot.scale()`
|
|
65
|
+
* before `paintable.snapshot()` makes the whole subtree draw into a viewport `scale`
|
|
66
|
+
* times larger, and text and symbolic icons come out re-rendered at that resolution,
|
|
67
|
+
* not magnified: Laplacian variance against a bicubic 2× upscale of the same 1×
|
|
68
|
+
* capture is 5–6× for a whole window, 7.35× for a symbolic icon and 7.04× for a
|
|
69
|
+
* full-colour themed icon. The control in the same probe is a RASTER texture at its
|
|
70
|
+
* native size, which scores 0.42× — it cannot gain detail it never had, and its being
|
|
71
|
+
* the only thing that does not prove the metric reads rasterisation rather than
|
|
72
|
+
* magnification. So an app's own PNG assets stay as sharp as the file they came from;
|
|
73
|
+
* everything GTK draws gets sharper.
|
|
74
|
+
*
|
|
75
|
+
* THE MONITOR'S SCALE DOES NOT REACH THIS CAPTURE. Only the transform does: the same
|
|
76
|
+
* window captured under a compositor at scale 1, 1.25 and 2 produces byte-identical
|
|
77
|
+
* PNGs (md5 `1294b43d…` at every monitor scale, `916c5a02…` once `scale: 2` is passed).
|
|
78
|
+
* This is why a HiDPI capture never happened by itself on a HiDPI screen, and why a CI
|
|
79
|
+
* runner under Xvfb at scale 1 can still prove the 2× path exactly.
|
|
43
80
|
*/
|
|
44
|
-
export declare function captureWidget(widget: Gtk.Widget): CaptureResult;
|
|
81
|
+
export declare function captureWidget(widget: Gtk.Widget, options?: CaptureOptions): CaptureResult;
|
|
45
82
|
/** The bytes alone, for callers that have nothing to do with the reason. */
|
|
46
|
-
export declare function captureWidgetPng(widget: Gtk.Widget): Uint8Array | null;
|
|
83
|
+
export declare function captureWidgetPng(widget: Gtk.Widget, options?: CaptureOptions): Uint8Array | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/devtools",
|
|
3
|
-
"version": "0.63.1-next.
|
|
3
|
+
"version": "0.63.1-next.236",
|
|
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",
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"@girs/gobject-2.0": "5.4.0",
|
|
50
50
|
"@girs/graphene-1.0": "5.4.0",
|
|
51
51
|
"@girs/gtk-4.0": "5.4.0",
|
|
52
|
-
"@gjsify/devtools-protocol": "0.63.1-next.
|
|
52
|
+
"@gjsify/devtools-protocol": "0.63.1-next.236"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@gjsify/cli": "0.63.1-next.
|
|
55
|
+
"@gjsify/cli": "0.63.1-next.236",
|
|
56
56
|
"@gjsify/node-gi": "file:../../node-gi/node-gi",
|
|
57
|
-
"@gjsify/unit": "0.63.1-next.
|
|
57
|
+
"@gjsify/unit": "0.63.1-next.236",
|
|
58
58
|
"@types/node": "^25.9.2",
|
|
59
59
|
"typescript": "^6.0.3"
|
|
60
60
|
},
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"url": "https://github.com/gjsify/gjsify/issues"
|
|
83
83
|
},
|
|
84
84
|
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/framework/devtools#readme",
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "d4ae412daabae9cb89f4fa11ee7279a308c9e4be"
|
|
86
86
|
}
|