@gjsify/devtools 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 +78 -4
- package/lib/esm/index.js +1 -1
- package/lib/esm/pango.js +1 -0
- package/lib/esm/shot.js +1 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/pango.d.ts +38 -0
- package/lib/types/pango.spec.d.ts +2 -0
- package/lib/types/shot.d.ts +60 -0
- package/lib/types/shot.spec.d.ts +2 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -149,14 +149,61 @@ Three of those need their reason stated, because the obvious use is the wrong on
|
|
|
149
149
|
|
|
150
150
|
`opts.extend` adds app-specific methods (§ below); `opts.instance` / `GJSIFY_DEVTOOLS_INSTANCE` scope a multi-instance app.
|
|
151
151
|
|
|
152
|
-
### Calling one by hand
|
|
152
|
+
### Calling one by hand — or from `gjsify devtools`
|
|
153
|
+
|
|
154
|
+
`gdbus` reaches the plane, and it is the right tool for reading a status string. It cannot do the
|
|
155
|
+
two things a script actually needs: `gdbus` cannot write BINARY (so a screenshot comes back as an
|
|
156
|
+
`ay` variant nobody can save), and it speaks only the session bus (which macOS and Windows do not
|
|
157
|
+
have — see above). So both halves live in this package instead of in every consumer's own script:
|
|
153
158
|
|
|
154
159
|
```bash
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
gjsify devtools shot /tmp/app.png --bus-name org.example.App # PNG on disk + its real size
|
|
161
|
+
gjsify devtools find Adw.StatusPage:error --bus-name org.example.App # → a widget path
|
|
162
|
+
gjsify devtools activate /toplevel:0/child:3 --bus-name org.example.App
|
|
163
|
+
gjsify devtools help # every operation and flag
|
|
157
164
|
```
|
|
158
165
|
|
|
159
|
-
|
|
166
|
+
Set `gjsify.devtools.busNameBase` in `package.json` once and the flag stops being needed. Progress
|
|
167
|
+
goes to **stderr**, the result to **stdout**, so `$(gjsify devtools find …)` is the widget path.
|
|
168
|
+
**Exit codes are the contract**: `0` done, `1` the app said no (no match, refused key, no image),
|
|
169
|
+
`2` the request itself was wrong (unknown operation, no `--bus-name`).
|
|
170
|
+
|
|
171
|
+
`gjsify devtools` is the scriptable peer of [`gjsify debug`](../devtools-mcp) (MCP over stdio, for
|
|
172
|
+
an AI agent) — same transport, same client, different front end. `--address` reaches the peer
|
|
173
|
+
socket on a bus-less host, and `--timeout <s>` waits for `GetStatus` to answer rather than sleeping:
|
|
174
|
+
a cold start measured 94 s before the first call was served.
|
|
175
|
+
|
|
176
|
+
### Getting a screenshot onto disk — `captureShot`
|
|
177
|
+
|
|
178
|
+
`gdbus call … Screenshot` returns an `ay` variant, and unpacking one into a file was a 60-line GJS
|
|
179
|
+
script per consumer. `captureShot` is that script, once:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { captureShot } from '@gjsify/devtools/shot';
|
|
183
|
+
|
|
184
|
+
const shot = await captureShot(
|
|
185
|
+
async (scope) => client.control(instance, 'Screenshot', tuple(scope), '(ay)'),
|
|
186
|
+
'/tmp/app.png',
|
|
187
|
+
'window',
|
|
188
|
+
);
|
|
189
|
+
shot.width; // 1100 — read out of the PNG, NOT the size asked for
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The `./shot` entry point is deliberately GTK-free: the client side of a screenshot needs Gio and
|
|
193
|
+
GLib only, so a headless client does not drag this package's app-side GTK adapter in with it.
|
|
194
|
+
|
|
195
|
+
Two invariants ride along, each paid for with a measured incident:
|
|
196
|
+
|
|
197
|
+
- **Empty bytes are a FAILURE, not a picture.** `Screenshot` answers `ay[0]` when there is no active
|
|
198
|
+
window or the window was never realised — a deliberate contract, see `CaptureBlocker`. Writing
|
|
199
|
+
that yields a 0-byte `.png`, a success line and exit 0: a rig that reports success while handing
|
|
200
|
+
on a file nothing can open. `captureShot` retries, then throws `CaptureShotError('empty-answer')`
|
|
201
|
+
**without writing the file**.
|
|
202
|
+
- **The size comes from the PNG, never from the request.** `ResizeWindow` answers with the size it
|
|
203
|
+
was ASKED for, whether or not the window honoured it, and `default-width` reads back that same
|
|
204
|
+
asked-for value. Measured in a consumer: 1280 requested, 1100 in the file, four checks green.
|
|
205
|
+
`pngSize()` reads the IHDR header instead, which is the one number about a screenshot that cannot
|
|
206
|
+
be faked — so `resize` cannot report its own failure and `shot` can.
|
|
160
207
|
|
|
161
208
|
## App-specific methods — a `DevtoolsExtension`
|
|
162
209
|
|
|
@@ -175,6 +222,31 @@ const myExtension: DevtoolsExtension = {
|
|
|
175
222
|
installDevtools(this, { extend: [myExtension] });
|
|
176
223
|
```
|
|
177
224
|
|
|
225
|
+
## The Adwaita markup trap
|
|
226
|
+
|
|
227
|
+
A widget property that Pango parses as markup turns a bare `&` or `<` into a **parse error**, and
|
|
228
|
+
the label renders **blank**. Nothing throws, nothing fails, and the only symptom is a screenshot
|
|
229
|
+
somebody looks at. German UI text is full of `&` ("Kosten & Förderung"), so it is a recurring class.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { hasRawPangoMarkup, isPangoMarkupSink } from '@gjsify/devtools';
|
|
233
|
+
|
|
234
|
+
isPangoMarkupSink('AdwActionRow', 'title'); // true — `use-markup` is @default true
|
|
235
|
+
hasRawPangoMarkup('Kosten & Förderung'); // true — the label would render empty
|
|
236
|
+
hasRawPangoMarkup('Kosten & Förderung'); // false — an entity is markup, and valid
|
|
237
|
+
hasRawPangoMarkup('<b>Kosten</b> & Ertrag'); // false — markup you wrote on purpose
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The interesting half is `PANGO_MARKUP_SINKS`, the list of properties that parse markup at all,
|
|
241
|
+
because the traps cut both ways. `Gtk.Label:label` and `AdwWindowTitle:title` are plain text —
|
|
242
|
+
reporting `&` there teaches an audience to switch the check off. And **`AdwAlertDialog:heading` and
|
|
243
|
+
`:body` are plain text by default**: `heading-use-markup` / `body-use-markup` are both
|
|
244
|
+
`@default false` in `@girs/adw-1`, so a sink list that includes them is wrong, however plausible it
|
|
245
|
+
looks. Every row class inherits markup titles from `AdwPreferencesRow:use-markup` (`@default true`,
|
|
246
|
+
"subclasses may also use it for other labels, such as subtitle") — which is where the trap in most
|
|
247
|
+
apps comes from. Runtime values need `GLib.markup_escape_text()` at the point of use; this checks
|
|
248
|
+
literals, it does not claim a rendered string is clean.
|
|
249
|
+
|
|
178
250
|
## Exports
|
|
179
251
|
|
|
180
252
|
- `installDevtools(app, options)` → returns a `DevtoolsService`, or `null` when the env gate is off **or the transport could not come up**. It never throws: every failure is one stderr line naming the address and the way out. `uninstallDevtools(service)` — opt-in lifecycle; it stops the socket *and* retracts the published address.
|
|
@@ -183,6 +255,8 @@ installDevtools(this, { extend: [myExtension] });
|
|
|
183
255
|
- `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.
|
|
184
256
|
- `captureWidgetPng` (GSK screenshot), `buildVariant` / `variantKindFor` / `VariantKind` (GVariant), `activateAction` / `changeActionState` / `describeActions` (GAction bridge).
|
|
185
257
|
- widget-tree helpers (`dumpTree`, `getWidgetProperty`, `listToplevels`, `resolveWidgetPath`, …), `dumpCss` / `swapCss` / `removeCss`, `dumpGSettings`, `buildDevtoolsIfaceXml`.
|
|
258
|
+
- **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
|
+
- markup-trap helpers (root entry point): `hasRawPangoMarkup(text)`, `rawPangoMarkupIn(text)`, `isPangoMarkupSink(class, property)`, `PANGO_MARKUP_SINKS`.
|
|
186
260
|
- the re-exported `@gjsify/devtools-protocol` contract.
|
|
187
261
|
|
|
188
262
|
> **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.
|
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 h}from"./gsettings.js";import{buildWidgetPath as g,dumpTree as _,getWidgetProperty as v,listToplevels as y,parseWidgetPath as b,pathOfWidget as x,resolveWidgetPath as S,widgetIsA as C,widgetType as w}from"./widget-tree.js";import{DevtoolsService as T}from"./devtools-service.js";import{chooseDevtoolsTransport as E,installDevtools as D,uninstallDevtools as O}from"./install.js";export*from"@gjsify/devtools-protocol";export{l as DevtoolsPeerServerError,T as DevtoolsService,n as activateAction,c as buildDevtoolsIfaceXml,e as buildVariant,g as buildWidgetPath,p as captureWidget,m as captureWidgetPng,r as changeActionState,E as chooseDevtoolsTransport,i as describeActions,a as dumpCss,h as dumpGSettings,_ as dumpTree,v as getWidgetProperty,D as installDevtools,y as listToplevels,b as parseWidgetPath,x as pathOfWidget,o as removeCss,u as removeDevtoolsAddressFile,S as resolveWidgetPath,d as startDevtoolsPeerServer,s as swapCss,O as uninstallDevtools,t as variantKindFor,C as widgetIsA,w as widgetType,f as writeDevtoolsAddressFile};
|
|
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 h}from"./gsettings.js";import{buildWidgetPath as g,dumpTree as _,getWidgetProperty as v,listToplevels as y,parseWidgetPath as b,pathOfWidget as x,resolveWidgetPath as S,widgetIsA as C,widgetType as w}from"./widget-tree.js";import{DevtoolsService as T}from"./devtools-service.js";import{chooseDevtoolsTransport as E,installDevtools as D,uninstallDevtools as O}from"./install.js";import{CaptureShotError as k,captureShot as A,pngSize as j}from"./shot.js";import{PANGO_MARKUP_SINKS as M,hasRawPangoMarkup as N,isPangoMarkupSink as P,rawPangoMarkupIn as F}from"./pango.js";export*from"@gjsify/devtools-protocol";export{k as CaptureShotError,l as DevtoolsPeerServerError,T as DevtoolsService,M as PANGO_MARKUP_SINKS,n as activateAction,c as buildDevtoolsIfaceXml,e as buildVariant,g as buildWidgetPath,A as captureShot,p as captureWidget,m as captureWidgetPng,r as changeActionState,E as chooseDevtoolsTransport,i as describeActions,a as dumpCss,h as dumpGSettings,_ as dumpTree,v as getWidgetProperty,N as hasRawPangoMarkup,D as installDevtools,P as isPangoMarkupSink,y as listToplevels,b as parseWidgetPath,x as pathOfWidget,j as pngSize,F as rawPangoMarkupIn,o as removeCss,u as removeDevtoolsAddressFile,S as resolveWidgetPath,d as startDevtoolsPeerServer,s as swapCss,O as uninstallDevtools,t as variantKindFor,C as widgetIsA,w as widgetType,f as writeDevtoolsAddressFile};
|
package/lib/esm/pango.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";const e={AdwAboutDialog:[`comments`,`license`],AdwAboutWindow:[`comments`,`license`],AdwActionRow:[`title`,`subtitle`],AdwBanner:[`title`],AdwComboRow:[`title`,`subtitle`],AdwEntryRow:[`title`,`subtitle`],AdwExpanderRow:[`title`,`subtitle`],AdwPreferencesGroup:[`title`,`description`],AdwPreferencesPage:[`title`,`description`],AdwSpinRow:[`title`,`subtitle`],AdwStatusPage:[`title`,`description`],AdwSwitchRow:[`title`,`subtitle`],AdwToast:[`title`]},t=`&(?![a-zA-Z][a-zA-Z0-9]*;|#\\d+;|#x[0-9a-fA-F]+;)|<(?![a-zA-Z/!?])`,n=RegExp(t),r=RegExp(t,`g`);function rawPangoMarkupIn(e){let t=new Set;for(let n of e.matchAll(r))t.add(n[0]);return[...t]}function hasRawPangoMarkup(e){return e.length<2?!1:n.test(e)}function isPangoMarkupSink(t,n){let r=e[t];return r?r.includes(n):!1}export{e as PANGO_MARKUP_SINKS,hasRawPangoMarkup,isPangoMarkupSink,rawPangoMarkupIn};
|
package/lib/esm/shot.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import e from"gi://GLib?version=2.0";import t from"gi://Gio?version=2.0";const n=[137,80,78,71,13,10,26,10];var CaptureShotError=class extends Error{constructor(e,t,n,r){super(n),this.scope=e,this.reason=t,this.cause=r,this.name=`CaptureShotError`}};function pngSize(e){if(e.length<24)return null;for(let t=0;t<n.length;t++)if(e[t]!==n[t])return null;let t=new DataView(e.buffer,e.byteOffset,e.byteLength);return{width:t.getUint32(16),height:t.getUint32(20)}}function delay(t){return new Promise(n=>{e.timeout_add(e.PRIORITY_DEFAULT,t,()=>(n(),e.SOURCE_REMOVE))})}function writeBytes(e,n){t.File.new_for_path(e).replace_contents(n,null,!1,t.FileCreateFlags.REPLACE_DESTINATION,null)}async function captureShot(e,t,n=`window`,r={}){let{retries:i=8,retryDelayMs:a=250,settleMs:o=0}=r;o>0&&await delay(o);let s=new Uint8Array;for(let t=0;t<=i&&(s=await e(n),!(s.length>0));t++)t<i&&await delay(a);if(s.length===0)throw new CaptureShotError(n,`empty-answer`,`no image for scope "${n}" after ${i+1} attempts — the window is absent or was never realised`);let c=pngSize(s);if(!c)throw new CaptureShotError(n,`unreadable-png`,`scope "${n}" answered ${s.length} bytes that are not a PNG — refusing to write them out as an image`);try{writeBytes(t,new Uint8Array(s))}catch(e){throw new CaptureShotError(n,`write-failed`,`cannot write ${t}`,e)}return{path:t,byteLength:s.length,width:c.width,height:c.height}}export{CaptureShotError,captureShot,pngSize};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -5,11 +5,14 @@ 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
7
|
export { captureWidget, captureWidgetPng, type CaptureBlocker, type CaptureResult } from './screenshot.js';
|
|
8
|
+
export { CaptureShotError, captureShot, pngSize } from './shot.js';
|
|
9
|
+
export type { CaptureShotOptions, PngSize, PngSource, ShotFailure, ShotResult } from './shot.js';
|
|
8
10
|
export { buildVariant, variantKindFor } from './gvariant.js';
|
|
9
11
|
export type { VariantKind } from './gvariant.js';
|
|
10
12
|
export { activateAction, changeActionState, describeActions } from './actions.js';
|
|
11
13
|
export { buildDevtoolsIfaceXml } from './devtools-iface.js';
|
|
12
14
|
export { buildWidgetPath, dumpTree, getWidgetProperty, listToplevels, parseWidgetPath, pathOfWidget, resolveWidgetPath, widgetIsA, widgetType, } from './widget-tree.js';
|
|
13
15
|
export { dumpCss, removeCss, swapCss } from './css.js';
|
|
16
|
+
export { hasRawPangoMarkup, isPangoMarkupSink, PANGO_MARKUP_SINKS, rawPangoMarkupIn } from './pango.js';
|
|
14
17
|
export { dumpGSettings } from './gsettings.js';
|
|
15
18
|
export * from '@gjsify/devtools-protocol';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adw properties whose value Pango parses as markup, as `ClassName` → the properties it applies to.
|
|
3
|
+
*
|
|
4
|
+
* The `use-markup` / `*-use-markup` properties are the documented switches, and their DEFAULTS are
|
|
5
|
+
* what decide membership. Read out of `@girs/adw-1`:
|
|
6
|
+
*
|
|
7
|
+
* · `AdwPreferencesRow:use-markup` — `@default true`, "whether to use Pango markup for the title
|
|
8
|
+
* label … subclasses may also use it for other labels, such as subtitle". So every
|
|
9
|
+
* `AdwPreferencesRow` subclass has markup `title` AND `subtitle` unless it opts out.
|
|
10
|
+
* · `AdwBanner:use-markup` and `AdwToast:use-markup` — both `@default true`, for the title.
|
|
11
|
+
* · `AdwAlertDialog:heading-use-markup` and `:body-use-markup` — both **`@default false`**, so
|
|
12
|
+
* `heading` and `body` are PLAIN TEXT by default and marking them raw would be a false finding.
|
|
13
|
+
* This is where a sink list copied from a consumer's own script goes wrong.
|
|
14
|
+
* · `AdwMessageDialog` (deprecated since 1.6) is absent for the same practical reason: its heading
|
|
15
|
+
* and body went through markup, but it has no toggle to read a default off and no reason to be
|
|
16
|
+
* written in new code.
|
|
17
|
+
* · `AdwPreferencesGroup`, `AdwPreferencesPage` and `AdwStatusPage` carry no toggle and no GIR note;
|
|
18
|
+
* their titles and descriptions are markup in libadwaita's implementation. Included, and noted
|
|
19
|
+
* here as the one group not backed by GIR documentation, so a future correction has a home.
|
|
20
|
+
* · `AdwAboutDialog`/`AdwAboutWindow` — the GIR documents `comments` ("can also contain links and
|
|
21
|
+
* Pango markup") and `license` ("can contain Pango markup and links") in prose. `debug_info` is
|
|
22
|
+
* in the same class and explicitly cannot, which is why it is not here.
|
|
23
|
+
*
|
|
24
|
+
* `Gtk.Label:label` and `AdwWindowTitle:title` are plain text and are deliberately absent: a rule
|
|
25
|
+
* that reports `&` in those teaches its audience to switch it off.
|
|
26
|
+
*/
|
|
27
|
+
export declare const PANGO_MARKUP_SINKS: Readonly<Record<string, readonly string[]>>;
|
|
28
|
+
/** The raw markup characters in `text`, deduplicated; empty when the string parses as markup. */
|
|
29
|
+
export declare function rawPangoMarkupIn(text: string): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Would `text` render BLANK as an Adw markup property?
|
|
32
|
+
*
|
|
33
|
+
* The single-character cases are answered first because they are the ones that read as deliberate:
|
|
34
|
+
* an author who wrote `"&"` or `"<"` meant them, and flagging it teaches nothing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function hasRawPangoMarkup(text: string): boolean;
|
|
37
|
+
/** Is `property` of `className` (e.g. `'AdwActionRow'`, `'title'`) a markup sink? */
|
|
38
|
+
export declare function isPangoMarkupSink(className: string, property: string): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Fetches PNG bytes for a scope (`''`/`window` for the active window, a widget path for one widget). */
|
|
2
|
+
export type PngSource = (scope: string) => Promise<Uint8Array>;
|
|
3
|
+
/** The pixel dimensions read out of a PNG's own IHDR header. */
|
|
4
|
+
export interface PngSize {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
/** A written screenshot: where it is, how big the file is, and how big the PICTURE is. */
|
|
9
|
+
export interface ShotResult {
|
|
10
|
+
path: string;
|
|
11
|
+
/** Bytes on disk. Never 0 — a capture that produced none throws instead. */
|
|
12
|
+
byteLength: number;
|
|
13
|
+
/** From the PNG header, so it is what the file actually contains. */
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
}
|
|
17
|
+
/** Tunables for {@link captureShot}; the defaults are the measured ones from a consumer rig. */
|
|
18
|
+
export interface CaptureShotOptions {
|
|
19
|
+
/**
|
|
20
|
+
* How many extra attempts after an empty answer. The window is often mid-layout, so one
|
|
21
|
+
* empty reply is normal right after `present()` — 8 × 250 ms was enough for every cold
|
|
22
|
+
* start measured, and 0 is the right answer for a caller that drives its own settling.
|
|
23
|
+
*/
|
|
24
|
+
retries?: number;
|
|
25
|
+
/** Gap between attempts, milliseconds. */
|
|
26
|
+
retryDelayMs?: number;
|
|
27
|
+
/** Pause before the FIRST capture — the counterpart of a rig's `--settle`. */
|
|
28
|
+
settleMs?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Why {@link captureShot} gave up. Named, because "it wrote nothing" is not a diagnosis. */
|
|
31
|
+
export type ShotFailure = 'empty-answer' | 'unreadable-png' | 'write-failed';
|
|
32
|
+
/** The capture that did not happen. Thrown so a caller cannot mistake it for a picture. */
|
|
33
|
+
export declare class CaptureShotError extends Error {
|
|
34
|
+
readonly scope: string;
|
|
35
|
+
readonly reason: ShotFailure;
|
|
36
|
+
readonly cause?: unknown | undefined;
|
|
37
|
+
constructor(scope: string, reason: ShotFailure, message: string, cause?: unknown | undefined);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The pixel size of a PNG, read from its IHDR chunk — the width and height are the first two
|
|
41
|
+
* big-endian `uint32`s after the 8-byte signature and the chunk header.
|
|
42
|
+
*
|
|
43
|
+
* Returns `null` for anything that is not a PNG carrying an IHDR, rather than guessing: a
|
|
44
|
+
* caller printing a size is reporting what the file contains, and a plausible number derived
|
|
45
|
+
* from bytes that are not a header is worse than no number. Zero width or height cannot occur
|
|
46
|
+
* in a valid PNG (both are `>= 1`), so `null` is not reachable for a well-formed file.
|
|
47
|
+
*/
|
|
48
|
+
export declare function pngSize(bytes: Uint8Array): PngSize | null;
|
|
49
|
+
/**
|
|
50
|
+
* Capture a screenshot through `source` and save it to `path`.
|
|
51
|
+
*
|
|
52
|
+
* Retries while the app answers empty bytes (see the file header), then throws
|
|
53
|
+
* {@link CaptureShotError} instead of writing a 0-byte file. The returned size is the PNG's
|
|
54
|
+
* own, not the size that was asked for.
|
|
55
|
+
*
|
|
56
|
+
* @throws {CaptureShotError} `empty-answer` after the retries are spent, `unreadable-png` when
|
|
57
|
+
* the bytes are not a PNG (a control plane that answered with something else is a different
|
|
58
|
+
* bug and must not be written out as an image), `write-failed` when the path is not writable.
|
|
59
|
+
*/
|
|
60
|
+
export declare function captureShot(source: PngSource, path: string, scope?: string, options?: CaptureShotOptions): Promise<ShotResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.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",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./lib/types/index.d.ts",
|
|
11
11
|
"default": "./lib/esm/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./shot": {
|
|
14
|
+
"types": "./lib/types/shot.d.ts",
|
|
15
|
+
"default": "./lib/esm/shot.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
@@ -45,12 +49,12 @@
|
|
|
45
49
|
"@girs/gobject-2.0": "5.4.0",
|
|
46
50
|
"@girs/graphene-1.0": "5.4.0",
|
|
47
51
|
"@girs/gtk-4.0": "5.4.0",
|
|
48
|
-
"@gjsify/devtools-protocol": "^0.
|
|
52
|
+
"@gjsify/devtools-protocol": "^0.54.0"
|
|
49
53
|
},
|
|
50
54
|
"devDependencies": {
|
|
51
|
-
"@gjsify/cli": "^0.
|
|
55
|
+
"@gjsify/cli": "^0.54.0",
|
|
52
56
|
"@gjsify/node-gi": "file:../../node-gi/node-gi",
|
|
53
|
-
"@gjsify/unit": "^0.
|
|
57
|
+
"@gjsify/unit": "^0.54.0",
|
|
54
58
|
"@types/node": "^25.9.2",
|
|
55
59
|
"typescript": "^6.0.3"
|
|
56
60
|
},
|