@houwert/conductor 0.33.0 → 0.33.2
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/dist/commands/screenshot.js +71 -2
- package/dist/drivers/devicectl.js +24 -0
- package/dist/drivers/ios-displays.js +51 -0
- package/dist/enum-options.js +11 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/skills/conductor-device-setup/SKILL.md +10 -2
- package/skills/conductor-inspect/SKILL.md +1 -1
|
@@ -4,9 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HELP = void 0;
|
|
7
|
+
exports.captureScreen = captureScreen;
|
|
7
8
|
exports.screenshot = screenshot;
|
|
8
|
-
exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page]
|
|
9
|
+
exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page] [--display <panel>]
|
|
9
10
|
Take screenshot (--full-page: web only, capture entire scrollable page)
|
|
11
|
+
--display <cover|inner|id> Which display to capture (default: whichever panel
|
|
12
|
+
is live). An unknown value lists the device's displays
|
|
10
13
|
<element> Crop to the element matched by text (positional)
|
|
11
14
|
--id <id> Crop to the element matched by accessibility id
|
|
12
15
|
--text <text> Crop to the element matched by text only (not id)
|
|
@@ -21,9 +24,14 @@ exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page]
|
|
|
21
24
|
--left-of <text> Match element left of the given reference
|
|
22
25
|
--right-of <text> Match element right of the given reference`;
|
|
23
26
|
const path_1 = __importDefault(require("path"));
|
|
27
|
+
const os_1 = __importDefault(require("os"));
|
|
24
28
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
29
|
+
const child_process_1 = require("child_process");
|
|
30
|
+
const util_1 = require("util");
|
|
25
31
|
const runner_js_1 = require("../runner.js");
|
|
26
32
|
const output_js_1 = require("../output.js");
|
|
33
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
34
|
+
const ios_displays_js_1 = require("../drivers/ios-displays.js");
|
|
27
35
|
const ios_js_1 = require("../drivers/ios.js");
|
|
28
36
|
const android_js_1 = require("../drivers/android.js");
|
|
29
37
|
const web_js_1 = require("../drivers/web.js");
|
|
@@ -32,7 +40,63 @@ const roku_js_1 = require("../drivers/roku.js");
|
|
|
32
40
|
const wait_js_1 = require("../drivers/wait.js");
|
|
33
41
|
const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
|
|
34
42
|
const png_crop_js_1 = require("../png-crop.js");
|
|
43
|
+
const exec = (0, util_1.promisify)(child_process_1.execFile);
|
|
35
44
|
const DEFAULT_MARGIN_PX = 8;
|
|
45
|
+
/**
|
|
46
|
+
* Grab the screen, pointing at the right panel on a multi-display device.
|
|
47
|
+
*
|
|
48
|
+
* The driver always screenshots `XCUIScreen.main`, which on a foldable is the
|
|
49
|
+
* cover panel — powered off, and so a black image, whenever the device is
|
|
50
|
+
* unfolded. When the device reports more than one integrated panel we capture
|
|
51
|
+
* the live one through simctl instead. Ordinary devices keep the driver path.
|
|
52
|
+
*/
|
|
53
|
+
async function captureScreen(driver, opts, displayOverride) {
|
|
54
|
+
if (!(driver instanceof ios_js_1.IOSDriver)) {
|
|
55
|
+
if (displayOverride) {
|
|
56
|
+
throw new Error('--display is iOS-only; other platforms expose a single screen');
|
|
57
|
+
}
|
|
58
|
+
return { buffer: await driver.screenshot(opts), redirected: false };
|
|
59
|
+
}
|
|
60
|
+
const deviceId = driver.deviceId;
|
|
61
|
+
if (!deviceId) {
|
|
62
|
+
if (displayOverride)
|
|
63
|
+
throw new Error('--display needs a device to query for its displays');
|
|
64
|
+
return { buffer: await driver.screenshot(opts), redirected: false };
|
|
65
|
+
}
|
|
66
|
+
const displays = await (0, devicectl_js_1.listDisplays)(deviceId).catch(() => []);
|
|
67
|
+
if (!displays.length) {
|
|
68
|
+
if (displayOverride)
|
|
69
|
+
throw new Error("could not read this device's displays");
|
|
70
|
+
return { buffer: await driver.screenshot(opts), redirected: false };
|
|
71
|
+
}
|
|
72
|
+
const choice = (0, ios_displays_js_1.pickCaptureDisplay)(displays, displayOverride);
|
|
73
|
+
if (choice.error)
|
|
74
|
+
throw new Error(choice.error);
|
|
75
|
+
const primary = displays.find((d) => d.primary);
|
|
76
|
+
// Nothing to redirect: the driver already captures the primary panel.
|
|
77
|
+
if (choice.displayId === null || (primary && choice.displayId === primary.displayId)) {
|
|
78
|
+
return { buffer: await driver.screenshot(opts), redirected: false };
|
|
79
|
+
}
|
|
80
|
+
const file = path_1.default.join(os_1.default.tmpdir(), `conductor-shot-${Date.now()}.png`);
|
|
81
|
+
try {
|
|
82
|
+
await exec('xcrun', [
|
|
83
|
+
'simctl',
|
|
84
|
+
'io',
|
|
85
|
+
deviceId,
|
|
86
|
+
'screenshot',
|
|
87
|
+
'--display',
|
|
88
|
+
String(choice.displayId),
|
|
89
|
+
file,
|
|
90
|
+
]);
|
|
91
|
+
return { buffer: await promises_1.default.readFile(file), redirected: true };
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
throw new Error(`could not capture display ${choice.displayId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
await promises_1.default.unlink(file).catch(() => { });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
36
100
|
async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPage = false, query = '', flags = {}) {
|
|
37
101
|
const timestamp = Date.now();
|
|
38
102
|
const defaultName = `screenshot-${timestamp}.png`;
|
|
@@ -63,8 +127,13 @@ async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPa
|
|
|
63
127
|
: '';
|
|
64
128
|
const margin = flags.margin ?? DEFAULT_MARGIN_PX;
|
|
65
129
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
66
|
-
const buf = await driver
|
|
130
|
+
const { buffer: buf, redirected } = await captureScreen(driver, { fullPage }, flags.display);
|
|
67
131
|
let out = buf;
|
|
132
|
+
if (sel && redirected) {
|
|
133
|
+
throw new Error('cropping to an element is not supported on this display yet — the panel is ' +
|
|
134
|
+
'rotated relative to the accessibility coordinate space. Re-run with ' +
|
|
135
|
+
'--display cover, or fold the device, to crop against the main panel.');
|
|
136
|
+
}
|
|
68
137
|
if (sel) {
|
|
69
138
|
let el;
|
|
70
139
|
let hierarchyW;
|
|
@@ -17,6 +17,7 @@ exports.terminateApp = terminateApp;
|
|
|
17
17
|
exports.listApps = listApps;
|
|
18
18
|
exports.getOrientation = getOrientation;
|
|
19
19
|
exports.setOrientation = setOrientation;
|
|
20
|
+
exports.listDisplays = listDisplays;
|
|
20
21
|
/**
|
|
21
22
|
* `xcrun devicectl` wrapper — the physical-device counterpart to `simctl`.
|
|
22
23
|
*
|
|
@@ -269,3 +270,26 @@ async function getOrientation(deviceId) {
|
|
|
269
270
|
async function setOrientation(deviceId, orientation) {
|
|
270
271
|
await devicectl(['device', 'orientation', 'set', '--device', deviceId, orientation]);
|
|
271
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* List the device's displays. Foldables report two integrated panels and flag
|
|
275
|
+
* which one is live, which is the only reliable way to know where to point a
|
|
276
|
+
* screenshot: the swap follows the system's own transition logic, not the
|
|
277
|
+
* hinge angle.
|
|
278
|
+
*/
|
|
279
|
+
async function listDisplays(deviceId) {
|
|
280
|
+
const parsed = await devicectlJson(['device', 'info', 'displays', '--device', deviceId], 15000);
|
|
281
|
+
return (parsed.result?.displays ?? [])
|
|
282
|
+
.filter((d) => typeof d.displayId === 'number')
|
|
283
|
+
.map((d) => {
|
|
284
|
+
// `type` is a single-key object, e.g. { integrated: {} } or { carPlay: {} }.
|
|
285
|
+
const kind = Object.keys(d.type ?? {})[0] ?? '';
|
|
286
|
+
return {
|
|
287
|
+
displayId: d.displayId,
|
|
288
|
+
name: d.name ?? '',
|
|
289
|
+
active: d.active === true,
|
|
290
|
+
primary: d.primary === true,
|
|
291
|
+
kind,
|
|
292
|
+
integrated: kind === 'integrated',
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pickCaptureDisplay = pickCaptureDisplay;
|
|
4
|
+
/**
|
|
5
|
+
* Roles, which mean the same thing on any platform that grows a foldable.
|
|
6
|
+
* Everything else is addressed by display id: a device's own class and panel
|
|
7
|
+
* names (integrated/carPlay, LCD-1) are platform vocabulary that wouldn't carry
|
|
8
|
+
* over to Android, and every display it could name already has an id. Unknown
|
|
9
|
+
* values list the device's displays so the ids are discoverable.
|
|
10
|
+
*/
|
|
11
|
+
const ROLE_ALIASES = {
|
|
12
|
+
cover: 'primary',
|
|
13
|
+
main: 'primary',
|
|
14
|
+
primary: 'primary',
|
|
15
|
+
inner: 'secondary',
|
|
16
|
+
secondary: 'secondary',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Pick the display to capture. With no override this is whichever panel is
|
|
20
|
+
* live, which keeps screenshots working across a fold without the caller
|
|
21
|
+
* having to think about it. Returns null for ordinary single-display devices
|
|
22
|
+
* so they keep using the driver path untouched.
|
|
23
|
+
*/
|
|
24
|
+
function pickCaptureDisplay(displays, override) {
|
|
25
|
+
const integrated = displays.filter((d) => d.integrated);
|
|
26
|
+
if (override) {
|
|
27
|
+
const key = override.trim().toLowerCase();
|
|
28
|
+
const id = Number(key);
|
|
29
|
+
if (Number.isInteger(id) && displays.some((d) => d.displayId === id)) {
|
|
30
|
+
return { displayId: id };
|
|
31
|
+
}
|
|
32
|
+
const role = ROLE_ALIASES[key];
|
|
33
|
+
if (role) {
|
|
34
|
+
const match = role === 'primary' ? integrated.find((d) => d.primary) : integrated.find((d) => !d.primary);
|
|
35
|
+
if (match)
|
|
36
|
+
return { displayId: match.displayId };
|
|
37
|
+
}
|
|
38
|
+
const known = displays
|
|
39
|
+
.map((d) => `${d.displayId} (${[d.name, d.kind].filter(Boolean).join(', ')})`)
|
|
40
|
+
.join('; ');
|
|
41
|
+
return {
|
|
42
|
+
displayId: null,
|
|
43
|
+
error: `this device has no '${override}' display — it reports: ${known}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Single-panel devices: nothing to choose, let the driver handle it.
|
|
47
|
+
if (integrated.length < 2)
|
|
48
|
+
return { displayId: null };
|
|
49
|
+
const live = integrated.find((d) => d.active) ?? integrated.find((d) => d.primary);
|
|
50
|
+
return { displayId: live ? live.displayId : null };
|
|
51
|
+
}
|
package/dist/enum-options.js
CHANGED
|
@@ -59,6 +59,17 @@ exports.ENUM_PARAMS = [
|
|
|
59
59
|
{ value: 'faceDown', description: 'iOS only' },
|
|
60
60
|
],
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
command: 'take-screenshot',
|
|
64
|
+
param: '--display',
|
|
65
|
+
description: 'Which display to capture (default: whichever panel is live)',
|
|
66
|
+
// Source: ROLE_ALIASES in drivers/ios-displays.ts. Any display id the
|
|
67
|
+
// device reports is also accepted, so the list is not exhaustive.
|
|
68
|
+
values: [
|
|
69
|
+
{ value: 'cover', description: 'Foldable outer panel (XCUIScreen.main)' },
|
|
70
|
+
{ value: 'inner', description: 'Foldable inner panel, live when unfolded' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
62
73
|
{
|
|
63
74
|
command: 'set-fold',
|
|
64
75
|
param: '<state>',
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -39,8 +39,16 @@ conductor list-apps # installed app ids / package names (--json adds ap
|
|
|
39
39
|
`set-fold` drives the hinge the same way Device Hub's slider does, so the device
|
|
40
40
|
really folds: SpringBoard swaps between the cover and inner displays, and
|
|
41
41
|
`devicectl device motion hinge-angle` reports the new angle. `take-screenshot`
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
follows the fold automatically — it captures whichever panel is live, so an
|
|
43
|
+
unfolded device gives you the inner screen rather than the powered-off cover.
|
|
44
|
+
Pass `--display cover` or `--display inner` to pin it to one panel, or a display
|
|
45
|
+
id for anything else the device has attached (CarPlay, an external screen). An
|
|
46
|
+
unknown value lists that device's displays with their ids.
|
|
47
|
+
|
|
48
|
+
Cropping a screenshot to an element (`take-screenshot <element>`) only works
|
|
49
|
+
against the main panel: the inner panel's framebuffer is rotated relative to the
|
|
50
|
+
accessibility coordinate space, so conductor refuses the crop there instead of
|
|
51
|
+
returning the wrong region. Fold the device or pass `--display cover` to crop.
|
|
44
52
|
|
|
45
53
|
Angles are in degrees, 0 (shut) to 180 (flat). `closed`/`book`/`open` map to
|
|
46
54
|
0/130/180. Named poses always swap the display; an arbitrary mid-way angle sets
|
|
@@ -17,7 +17,7 @@ here. Always observe before you act, and confirm after.
|
|
|
17
17
|
| `conductor inspect [--dump]` | Print the UI hierarchy (`--dump` = raw driver output) |
|
|
18
18
|
| `conductor inspect --at <x,y> [--tappable]` | Topmost view at a screen point |
|
|
19
19
|
| `conductor focused [--poll [ms]]` | Metadata of the focused element. `--poll` watches changes — only with a bounded use, then stop it |
|
|
20
|
-
| `conductor take-screenshot [<element>] [--output <path>] [--full-page]` | Screenshot; crop to a matched element; `--full-page` (web) |
|
|
20
|
+
| `conductor take-screenshot [<element>] [--output <path>] [--full-page] [--display <panel>]` | Screenshot; crop to a matched element; `--full-page` (web); `--display cover\|inner\|<id>` picks a display (default: whichever panel is live) |
|
|
21
21
|
|
|
22
22
|
`capture-ui` is the workhorse: it returns the screen as structured data **and**
|
|
23
23
|
gives each element a ref like `@e3` that `conductor tap-on @e3` taps by cached
|