getobsrv 0.12.0 → 0.14.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/out/cli/args.js +52 -5
- package/out/main/cli.js +43 -9
- package/out/main/index.js +68 -10
- package/out/main/{targetSource-DlsHc-Ri.js → targetSource-W6YddYef.js} +14 -0
- package/out/mcp/lib.js +14 -1
- package/out/mcp/server.js +67 -8
- package/out/renderer/assets/{index-DvwDBeho.css → index-BpvyvKfD.css} +148 -18
- package/out/renderer/assets/{index-gYDbIgqJ.js → index-WM0cwf7d.js} +198 -18
- package/out/renderer/index.html +2 -2
- package/out/shared/calibration.js +35 -0
- package/out/shared/control.js +59 -1
- package/out/shared/ipcPayloads.js +15 -0
- package/out/shared/presets.js +11 -1
- package/out/shared/url.js +15 -0
- package/package.json +1 -1
package/out/cli/args.js
CHANGED
|
@@ -30,6 +30,18 @@ Shared flags:
|
|
|
30
30
|
${presets}
|
|
31
31
|
--width <px> --height <px> [--dsf <factor>] [--diagonal <inches>]
|
|
32
32
|
Custom CSS viewport instead of --preset (dsf defaults to 1).
|
|
33
|
+
--orientation <o> portrait | landscape (default ${presets_1.DEFAULT_ORIENTATION}). This names the
|
|
34
|
+
preset's *stored* orientation, not the shape you get:
|
|
35
|
+
portrait = the preset exactly as the table above lists it
|
|
36
|
+
landscape = that rotated a quarter turn (width and height swap)
|
|
37
|
+
Every mobile preset is stored portrait, so for those the two
|
|
38
|
+
readings agree. The laptop and desktop presets are stored
|
|
39
|
+
landscape-natural, so --orientation landscape turns them into a
|
|
40
|
+
portrait screen — which is how you render a 1080p monitor stood on
|
|
41
|
+
end (1080p-24 becomes 1080x1920). Applies to custom --width/--height
|
|
42
|
+
dims too. The diagonal, raster density and physical size never
|
|
43
|
+
change: it is the same panel turned sideways. Each render's JSON
|
|
44
|
+
and log line name the resulting shape.
|
|
33
45
|
--profile <id> Panel profile: ${profiles} (default reference).
|
|
34
46
|
--wait <ms> Extra settle time after load (default 0).
|
|
35
47
|
--timeout <ms> Per-render budget for load + paint quiescence (default ${exports.DEFAULT_TIMEOUT_MS}).
|
|
@@ -55,7 +67,7 @@ warning naming what was missing. Only a render that painted nothing errors.`;
|
|
|
55
67
|
/** Flags that take no value. */
|
|
56
68
|
const BOOLEAN_FLAGS = new Set(['full-page', 'json']);
|
|
57
69
|
/** Flags that consume the next token. */
|
|
58
|
-
const VALUE_FLAGS = new Set(['preset', 'profile', 'out', 'out-dir', 'wait', 'timeout', 'matrix', 'width', 'height', 'dsf', 'diagonal']);
|
|
70
|
+
const VALUE_FLAGS = new Set(['preset', 'profile', 'orientation', 'out', 'out-dir', 'wait', 'timeout', 'matrix', 'width', 'height', 'dsf', 'diagonal']);
|
|
59
71
|
const SNAP_ONLY = new Set(['out', 'full-page', 'matrix']);
|
|
60
72
|
const DIFF_ONLY = new Set(['out-dir', 'json']);
|
|
61
73
|
function collect(command, argv) {
|
|
@@ -131,9 +143,30 @@ function presetSpec(id) {
|
|
|
131
143
|
cssHeight: preset.height,
|
|
132
144
|
deviceScaleFactor: preset.deviceScaleFactor,
|
|
133
145
|
diagonalInches: preset.diagonalInches,
|
|
146
|
+
orientation: presets_1.DEFAULT_ORIENTATION,
|
|
134
147
|
};
|
|
135
148
|
}
|
|
149
|
+
function resolveOrientation(flags) {
|
|
150
|
+
const raw = flags.get('orientation');
|
|
151
|
+
if (raw === undefined)
|
|
152
|
+
return presets_1.DEFAULT_ORIENTATION;
|
|
153
|
+
if (!(0, presets_1.isOrientation)(raw))
|
|
154
|
+
throw new ArgError(`--orientation: expected portrait or landscape, got "${String(raw)}"`);
|
|
155
|
+
return raw;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Rotation swaps the CSS axes and nothing else — the diagonal and the raster
|
|
159
|
+
* density are orientation-independent, so the render is the same screen turned
|
|
160
|
+
* sideways rather than a different one. Applied here, before the diff bounds
|
|
161
|
+
* are checked, so those check the viewport that will actually be rendered.
|
|
162
|
+
*/
|
|
163
|
+
function orientSpec(spec, orientation) {
|
|
164
|
+
if (orientation !== 'landscape')
|
|
165
|
+
return { ...spec, orientation };
|
|
166
|
+
return { ...spec, orientation, cssWidth: spec.cssHeight, cssHeight: spec.cssWidth };
|
|
167
|
+
}
|
|
136
168
|
function resolveSpecs(flags) {
|
|
169
|
+
const orientation = resolveOrientation(flags);
|
|
137
170
|
const custom = ['width', 'height', 'dsf', 'diagonal'].some(f => flags.has(f));
|
|
138
171
|
if (custom && flags.has('preset'))
|
|
139
172
|
throw new ArgError('--preset and --width/--height are mutually exclusive');
|
|
@@ -153,17 +186,25 @@ function resolveSpecs(flags) {
|
|
|
153
186
|
throw new ArgError(`viewport exceeds the 4096-device-pixel budget: at dsf ${deviceScaleFactor} the CSS limit is ${max}`);
|
|
154
187
|
}
|
|
155
188
|
const diagonal = flags.has('diagonal') ? float(flags, 'diagonal', 0, 0.1) : null;
|
|
156
|
-
|
|
189
|
+
const spec = {
|
|
190
|
+
presetId: 'custom',
|
|
191
|
+
cssWidth,
|
|
192
|
+
cssHeight,
|
|
193
|
+
deviceScaleFactor,
|
|
194
|
+
diagonalInches: diagonal,
|
|
195
|
+
orientation: presets_1.DEFAULT_ORIENTATION,
|
|
196
|
+
};
|
|
197
|
+
return { specs: [orientSpec(spec, orientation)], matrix: false };
|
|
157
198
|
}
|
|
158
199
|
const matrixRaw = flags.get('matrix');
|
|
159
200
|
if (typeof matrixRaw === 'string') {
|
|
160
201
|
const ids = matrixRaw.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
161
202
|
if (ids.length === 0)
|
|
162
203
|
throw new ArgError('--matrix: expected a comma-separated list of preset ids');
|
|
163
|
-
return { specs: ids.map(presetSpec), matrix: true };
|
|
204
|
+
return { specs: ids.map(id => orientSpec(presetSpec(id), orientation)), matrix: true };
|
|
164
205
|
}
|
|
165
206
|
const id = typeof flags.get('preset') === 'string' ? flags.get('preset') : exports.DEFAULT_PRESET;
|
|
166
|
-
return { specs: [presetSpec(id)], matrix: false };
|
|
207
|
+
return { specs: [orientSpec(presetSpec(id), orientation)], matrix: false };
|
|
167
208
|
}
|
|
168
209
|
function resolveProfile(flags) {
|
|
169
210
|
const raw = flags.get('profile');
|
|
@@ -201,8 +242,14 @@ function parseArgs(argv) {
|
|
|
201
242
|
}
|
|
202
243
|
const referenceMax = (0, calibration_1.maxCssViewport)(2);
|
|
203
244
|
if (spec.cssWidth > referenceMax || spec.cssHeight > referenceMax) {
|
|
245
|
+
// Named as rendered, not as stored: the dims here are post-rotation, and
|
|
246
|
+
// attributing them to the bare preset id would print "1440p-27 is
|
|
247
|
+
// 1440×2560" — a shape that id never has. The bound itself is per-axis
|
|
248
|
+
// symmetric, so rotation can never sneak a too-large viewport past it;
|
|
249
|
+
// this is the message telling the truth about which one it measured.
|
|
250
|
+
const as = spec.orientation === 'landscape' ? ' rotated a quarter turn' : '';
|
|
204
251
|
throw new ArgError(`diff renders a 2x reference, so the CSS viewport must fit ${referenceMax}px per axis ` +
|
|
205
|
-
`(4096 device px at 2x) — "${spec.presetId}" is ${spec.cssWidth}×${spec.cssHeight}. ` +
|
|
252
|
+
`(4096 device px at 2x) — "${spec.presetId}"${as} is ${spec.cssWidth}×${spec.cssHeight}. ` +
|
|
206
253
|
`Use \`obsrv snap\` for this preset instead.`);
|
|
207
254
|
}
|
|
208
255
|
const outDir = typeof flags.get('out-dir') === 'string' ? flags.get('out-dir') : null;
|
package/out/main/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ const electron = require("electron");
|
|
|
3
3
|
const node_fs = require("node:fs");
|
|
4
4
|
const node_os = require("node:os");
|
|
5
5
|
const node_path = require("node:path");
|
|
6
|
-
const targetSource = require("./targetSource-
|
|
6
|
+
const targetSource = require("./targetSource-W6YddYef.js");
|
|
7
7
|
function boxDownsample(src, factor) {
|
|
8
8
|
if (!Number.isInteger(factor) || factor < 1) throw new RangeError("factor must be an integer >= 1");
|
|
9
9
|
const width = Math.floor(src.width / factor);
|
|
@@ -61,6 +61,18 @@ Shared flags:
|
|
|
61
61
|
${presets}
|
|
62
62
|
--width <px> --height <px> [--dsf <factor>] [--diagonal <inches>]
|
|
63
63
|
Custom CSS viewport instead of --preset (dsf defaults to 1).
|
|
64
|
+
--orientation <o> portrait | landscape (default ${targetSource.DEFAULT_ORIENTATION}). This names the
|
|
65
|
+
preset's *stored* orientation, not the shape you get:
|
|
66
|
+
portrait = the preset exactly as the table above lists it
|
|
67
|
+
landscape = that rotated a quarter turn (width and height swap)
|
|
68
|
+
Every mobile preset is stored portrait, so for those the two
|
|
69
|
+
readings agree. The laptop and desktop presets are stored
|
|
70
|
+
landscape-natural, so --orientation landscape turns them into a
|
|
71
|
+
portrait screen — which is how you render a 1080p monitor stood on
|
|
72
|
+
end (1080p-24 becomes 1080x1920). Applies to custom --width/--height
|
|
73
|
+
dims too. The diagonal, raster density and physical size never
|
|
74
|
+
change: it is the same panel turned sideways. Each render's JSON
|
|
75
|
+
and log line name the resulting shape.
|
|
64
76
|
--profile <id> Panel profile: ${profiles} (default reference).
|
|
65
77
|
--wait <ms> Extra settle time after load (default 0).
|
|
66
78
|
--timeout <ms> Per-render budget for load + paint quiescence (default ${DEFAULT_TIMEOUT_MS}).
|
|
@@ -84,7 +96,7 @@ painted. False is a rescued capture, not a failure: a page that kept animating
|
|
|
84
96
|
warning naming what was missing. Only a render that painted nothing errors.`;
|
|
85
97
|
}
|
|
86
98
|
const BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["full-page", "json"]);
|
|
87
|
-
const VALUE_FLAGS = /* @__PURE__ */ new Set(["preset", "profile", "out", "out-dir", "wait", "timeout", "matrix", "width", "height", "dsf", "diagonal"]);
|
|
99
|
+
const VALUE_FLAGS = /* @__PURE__ */ new Set(["preset", "profile", "orientation", "out", "out-dir", "wait", "timeout", "matrix", "width", "height", "dsf", "diagonal"]);
|
|
88
100
|
const SNAP_ONLY = /* @__PURE__ */ new Set(["out", "full-page", "matrix"]);
|
|
89
101
|
const DIFF_ONLY = /* @__PURE__ */ new Set(["out-dir", "json"]);
|
|
90
102
|
function collect(command, argv) {
|
|
@@ -148,10 +160,22 @@ function presetSpec(id) {
|
|
|
148
160
|
cssWidth: preset.width,
|
|
149
161
|
cssHeight: preset.height,
|
|
150
162
|
deviceScaleFactor: preset.deviceScaleFactor,
|
|
151
|
-
diagonalInches: preset.diagonalInches
|
|
163
|
+
diagonalInches: preset.diagonalInches,
|
|
164
|
+
orientation: targetSource.DEFAULT_ORIENTATION
|
|
152
165
|
};
|
|
153
166
|
}
|
|
167
|
+
function resolveOrientation(flags) {
|
|
168
|
+
const raw = flags.get("orientation");
|
|
169
|
+
if (raw === void 0) return targetSource.DEFAULT_ORIENTATION;
|
|
170
|
+
if (!targetSource.isOrientation(raw)) throw new ArgError(`--orientation: expected portrait or landscape, got "${String(raw)}"`);
|
|
171
|
+
return raw;
|
|
172
|
+
}
|
|
173
|
+
function orientSpec(spec, orientation) {
|
|
174
|
+
if (orientation !== "landscape") return { ...spec, orientation };
|
|
175
|
+
return { ...spec, orientation, cssWidth: spec.cssHeight, cssHeight: spec.cssWidth };
|
|
176
|
+
}
|
|
154
177
|
function resolveSpecs(flags) {
|
|
178
|
+
const orientation = resolveOrientation(flags);
|
|
155
179
|
const custom = ["width", "height", "dsf", "diagonal"].some((f) => flags.has(f));
|
|
156
180
|
if (custom && flags.has("preset")) throw new ArgError("--preset and --width/--height are mutually exclusive");
|
|
157
181
|
if (custom && flags.has("matrix")) throw new ArgError("--matrix lists presets; it cannot be combined with custom --width/--height dims");
|
|
@@ -168,16 +192,24 @@ function resolveSpecs(flags) {
|
|
|
168
192
|
throw new ArgError(`viewport exceeds the 4096-device-pixel budget: at dsf ${deviceScaleFactor} the CSS limit is ${max}`);
|
|
169
193
|
}
|
|
170
194
|
const diagonal = flags.has("diagonal") ? float(flags, "diagonal", 0, 0.1) : null;
|
|
171
|
-
|
|
195
|
+
const spec = {
|
|
196
|
+
presetId: "custom",
|
|
197
|
+
cssWidth,
|
|
198
|
+
cssHeight,
|
|
199
|
+
deviceScaleFactor,
|
|
200
|
+
diagonalInches: diagonal,
|
|
201
|
+
orientation: targetSource.DEFAULT_ORIENTATION
|
|
202
|
+
};
|
|
203
|
+
return { specs: [orientSpec(spec, orientation)], matrix: false };
|
|
172
204
|
}
|
|
173
205
|
const matrixRaw = flags.get("matrix");
|
|
174
206
|
if (typeof matrixRaw === "string") {
|
|
175
207
|
const ids = matrixRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
176
208
|
if (ids.length === 0) throw new ArgError("--matrix: expected a comma-separated list of preset ids");
|
|
177
|
-
return { specs: ids.map(presetSpec), matrix: true };
|
|
209
|
+
return { specs: ids.map((id2) => orientSpec(presetSpec(id2), orientation)), matrix: true };
|
|
178
210
|
}
|
|
179
211
|
const id = typeof flags.get("preset") === "string" ? flags.get("preset") : DEFAULT_PRESET;
|
|
180
|
-
return { specs: [presetSpec(id)], matrix: false };
|
|
212
|
+
return { specs: [orientSpec(presetSpec(id), orientation)], matrix: false };
|
|
181
213
|
}
|
|
182
214
|
function resolveProfile(flags) {
|
|
183
215
|
const raw = flags.get("profile");
|
|
@@ -215,8 +247,9 @@ ${usage()}`);
|
|
|
215
247
|
}
|
|
216
248
|
const referenceMax = targetSource.maxCssViewport(2);
|
|
217
249
|
if (spec.cssWidth > referenceMax || spec.cssHeight > referenceMax) {
|
|
250
|
+
const as = spec.orientation === "landscape" ? " rotated a quarter turn" : "";
|
|
218
251
|
throw new ArgError(
|
|
219
|
-
`diff renders a 2x reference, so the CSS viewport must fit ${referenceMax}px per axis (4096 device px at 2x) — "${spec.presetId}" is ${spec.cssWidth}×${spec.cssHeight}. Use \`obsrv snap\` for this preset instead.`
|
|
252
|
+
`diff renders a 2x reference, so the CSS viewport must fit ${referenceMax}px per axis (4096 device px at 2x) — "${spec.presetId}"${as} is ${spec.cssWidth}×${spec.cssHeight}. Use \`obsrv snap\` for this preset instead.`
|
|
220
253
|
);
|
|
221
254
|
}
|
|
222
255
|
const outDir = typeof flags.get("out-dir") === "string" ? flags.get("out-dir") : null;
|
|
@@ -559,8 +592,9 @@ async function runSnap(cmd) {
|
|
|
559
592
|
const img = applyPanelProfile(bgraToRgba(r.frame.bgra, r.frame.width, r.frame.height), profile);
|
|
560
593
|
node_fs.mkdirSync(node_path.dirname(out), { recursive: true });
|
|
561
594
|
node_fs.writeFileSync(out, encodePng(img));
|
|
595
|
+
const shape = targetSource.screenShape(r.cssWidth, r.cssHeight);
|
|
562
596
|
human(
|
|
563
|
-
`snap ${cmd.url} → ${out} (${r.frame.width}×${r.frame.height} device px, preset ${spec.presetId}, profile ${profile.id})`
|
|
597
|
+
`snap ${cmd.url} → ${out} (${r.frame.width}×${r.frame.height} device px, ${r.cssWidth}×${r.cssHeight} CSS ${shape}, preset ${spec.presetId}, profile ${profile.id})`
|
|
564
598
|
);
|
|
565
599
|
results.push({
|
|
566
600
|
out,
|
|
@@ -607,7 +641,7 @@ async function runDiff(cmd) {
|
|
|
607
641
|
}
|
|
608
642
|
const pct2 = (v) => `${(v * 100).toFixed(2)}%`;
|
|
609
643
|
human(
|
|
610
|
-
`diff ${cmd.url} @ ${cmd.spec.presetId} (profile ${profile.id}): ink ${pct2(metrics.inkCoverage.target)} vs ${pct2(metrics.inkCoverage.reference)} reference, rows ${metrics.rows.target}/${metrics.rows.reference} (ratio ${metrics.rows.ratio?.toFixed(2) ?? "n/a"}), ${metrics.findings.length} finding(s)${settled ? "" : " — UNSETTLED, deltas are not rendering evidence"}`
|
|
644
|
+
`diff ${cmd.url} @ ${cmd.spec.presetId} (${cmd.spec.cssWidth}×${cmd.spec.cssHeight} CSS ${targetSource.screenShape(cmd.spec.cssWidth, cmd.spec.cssHeight)}, profile ${profile.id}): ink ${pct2(metrics.inkCoverage.target)} vs ${pct2(metrics.inkCoverage.reference)} reference, rows ${metrics.rows.target}/${metrics.rows.reference} (ratio ${metrics.rows.ratio?.toFixed(2) ?? "n/a"}), ${metrics.findings.length} finding(s)${settled ? "" : " — UNSETTLED, deltas are not rendering evidence"}`
|
|
611
645
|
);
|
|
612
646
|
await machine({
|
|
613
647
|
url: cmd.url,
|
package/out/main/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const node_fs = require("node:fs");
|
|
|
4
4
|
const promises = require("node:fs/promises");
|
|
5
5
|
const node_path = require("node:path");
|
|
6
6
|
const node_crypto = require("node:crypto");
|
|
7
|
-
const targetSource = require("./targetSource-
|
|
7
|
+
const targetSource = require("./targetSource-W6YddYef.js");
|
|
8
8
|
const node_http = require("node:http");
|
|
9
9
|
const node_url = require("node:url");
|
|
10
10
|
const MAX_SCROLL_SELECTOR = 512;
|
|
@@ -106,12 +106,15 @@ function parseUiState(raw) {
|
|
|
106
106
|
if (mode !== "url" && mode !== "image") return null;
|
|
107
107
|
const panes = raw.panes ?? "both";
|
|
108
108
|
if (panes !== "both" && panes !== "target") return null;
|
|
109
|
+
const orientation = raw.orientation ?? targetSource.DEFAULT_ORIENTATION;
|
|
110
|
+
if (!targetSource.isOrientation(orientation)) return null;
|
|
109
111
|
return {
|
|
110
112
|
tabId,
|
|
111
113
|
presetId,
|
|
112
114
|
profileId,
|
|
113
115
|
viewMode,
|
|
114
116
|
panes,
|
|
117
|
+
orientation,
|
|
115
118
|
mode,
|
|
116
119
|
targetBounds: parseRect(raw.targetBounds),
|
|
117
120
|
canvasBounds: parseRect(raw.canvasBounds)
|
|
@@ -152,6 +155,7 @@ const CONTROL_COMMANDS = [
|
|
|
152
155
|
"setProfile",
|
|
153
156
|
"setViewMode",
|
|
154
157
|
"setPanes",
|
|
158
|
+
"setOrientation",
|
|
155
159
|
"captureVisible",
|
|
156
160
|
// v0.5 drive controls (spec §14 "Drive controls").
|
|
157
161
|
"scroll",
|
|
@@ -196,6 +200,9 @@ function profileApplyError(id) {
|
|
|
196
200
|
function viewModeApplyError(v) {
|
|
197
201
|
return v === "1:1" || v === "fit" ? null : `setViewMode payload must be { mode: '1:1' | 'fit' }`;
|
|
198
202
|
}
|
|
203
|
+
function orientationApplyError(v) {
|
|
204
|
+
return targetSource.isOrientation(v) ? null : `setOrientation payload must be { orientation: 'portrait' | 'landscape' }`;
|
|
205
|
+
}
|
|
199
206
|
function panesApplyError(v) {
|
|
200
207
|
return v === "both" || v === "target" ? null : `setPanes payload must be { panes: 'both' | 'target' }`;
|
|
201
208
|
}
|
|
@@ -400,7 +407,8 @@ function loadTabs(file, max = Infinity) {
|
|
|
400
407
|
tabs.push({
|
|
401
408
|
url: entry.url,
|
|
402
409
|
presetId: targetSource.SCREEN_PRESETS.some((p) => p.id === entry.presetId) ? entry.presetId : DEFAULT_PRESET,
|
|
403
|
-
profileId: targetSource.PANEL_PROFILES.some((p) => p.id === entry.profileId) ? entry.profileId : DEFAULT_PROFILE
|
|
410
|
+
profileId: targetSource.PANEL_PROFILES.some((p) => p.id === entry.profileId) ? entry.profileId : DEFAULT_PROFILE,
|
|
411
|
+
orientation: targetSource.isOrientation(entry.orientation) ? entry.orientation : targetSource.DEFAULT_ORIENTATION
|
|
404
412
|
});
|
|
405
413
|
}
|
|
406
414
|
const kept = tabs.slice(0, Math.max(0, max));
|
|
@@ -587,6 +595,12 @@ class ControlServer {
|
|
|
587
595
|
const panes = payload.panes;
|
|
588
596
|
return this.applyAndConfirm({ panes }, (s) => s.panes === panes);
|
|
589
597
|
}
|
|
598
|
+
case "setOrientation": {
|
|
599
|
+
const err = orientationApplyError(payload.orientation);
|
|
600
|
+
if (err) return reply(400, { error: err });
|
|
601
|
+
const orientation = payload.orientation;
|
|
602
|
+
return this.applyAndConfirm({ orientation }, (s) => s.orientation === orientation);
|
|
603
|
+
}
|
|
590
604
|
case "captureVisible": {
|
|
591
605
|
const capture = await this.deps.captureVisible();
|
|
592
606
|
return reply(200, { ok: true, ...capture });
|
|
@@ -810,7 +824,7 @@ function registerIpc(ctx) {
|
|
|
810
824
|
}
|
|
811
825
|
});
|
|
812
826
|
let panesShowNative = true;
|
|
813
|
-
tabs.nativeVisible = (s) => s.modeIsLive && panesShowNative;
|
|
827
|
+
tabs.nativeVisible = (s) => s.modeIsLive && panesShowNative && !targetSource.isBlankUrl(s.url);
|
|
814
828
|
tabs.busEnabled = (s) => s.modeIsLive;
|
|
815
829
|
const applyNativeVisibility = () => {
|
|
816
830
|
const s = tab();
|
|
@@ -934,6 +948,12 @@ function registerIpc(ctx) {
|
|
|
934
948
|
set viewMode(v) {
|
|
935
949
|
tab().viewMode = v;
|
|
936
950
|
},
|
|
951
|
+
get orientation() {
|
|
952
|
+
return tab().orientation;
|
|
953
|
+
},
|
|
954
|
+
set orientation(v) {
|
|
955
|
+
tab().orientation = v;
|
|
956
|
+
},
|
|
937
957
|
get mode() {
|
|
938
958
|
return tab().reportedMode;
|
|
939
959
|
},
|
|
@@ -1115,7 +1135,12 @@ function registerIpc(ctx) {
|
|
|
1115
1135
|
const persistTabs = () => {
|
|
1116
1136
|
if (!persistReady) return;
|
|
1117
1137
|
const stored = {
|
|
1118
|
-
tabs: tabs.tabs.map((t) => ({
|
|
1138
|
+
tabs: tabs.tabs.map((t) => ({
|
|
1139
|
+
url: t.url,
|
|
1140
|
+
presetId: t.presetId,
|
|
1141
|
+
profileId: t.profileId,
|
|
1142
|
+
orientation: t.orientation
|
|
1143
|
+
})),
|
|
1119
1144
|
activeIndex: tabs.activeIndex
|
|
1120
1145
|
};
|
|
1121
1146
|
const json = JSON.stringify(stored);
|
|
@@ -1127,7 +1152,10 @@ function registerIpc(ctx) {
|
|
|
1127
1152
|
console.warn("obsrv: could not save tabs", e);
|
|
1128
1153
|
}
|
|
1129
1154
|
};
|
|
1130
|
-
tabs.onTabUrlChanged =
|
|
1155
|
+
tabs.onTabUrlChanged = () => {
|
|
1156
|
+
persistTabs();
|
|
1157
|
+
applyNativeVisibility();
|
|
1158
|
+
};
|
|
1131
1159
|
const restoreTabs = async () => {
|
|
1132
1160
|
const stored = loadTabs(tabsFile, settings.maxTabs);
|
|
1133
1161
|
if (stored.tabs.length === 0) return;
|
|
@@ -1137,6 +1165,7 @@ function registerIpc(ctx) {
|
|
|
1137
1165
|
if (!s) break;
|
|
1138
1166
|
s.presetId = entry.presetId;
|
|
1139
1167
|
s.profileId = entry.profileId;
|
|
1168
|
+
s.orientation = entry.orientation;
|
|
1140
1169
|
sessions.push(s);
|
|
1141
1170
|
}
|
|
1142
1171
|
tabs.activate(sessions[Math.min(stored.activeIndex, sessions.length - 1)].id);
|
|
@@ -1176,14 +1205,30 @@ function registerIpc(ctx) {
|
|
|
1176
1205
|
url = tab().target.webContents.getURL();
|
|
1177
1206
|
} catch {
|
|
1178
1207
|
}
|
|
1179
|
-
|
|
1208
|
+
let css = { width: 0, height: 0 };
|
|
1209
|
+
try {
|
|
1210
|
+
css = tab().target.getViewport();
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
version: appVersion,
|
|
1215
|
+
url,
|
|
1216
|
+
tabId: tabs.activeId,
|
|
1217
|
+
tabIndex: tabs.activeIndex,
|
|
1218
|
+
cssWidth: css.width,
|
|
1219
|
+
cssHeight: css.height,
|
|
1220
|
+
screenShape: targetSource.screenShape(css.width, css.height),
|
|
1221
|
+
...uiState
|
|
1222
|
+
};
|
|
1180
1223
|
},
|
|
1181
1224
|
navigate: navigateBoth,
|
|
1182
1225
|
apply: (patch) => {
|
|
1183
1226
|
if (win.isDestroyed()) return;
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1227
|
+
const s = tab();
|
|
1228
|
+
const resizes = patch.presetId !== void 0 && patch.presetId !== s.presetId || patch.orientation !== void 0 && patch.orientation !== s.orientation;
|
|
1229
|
+
if (resizes) {
|
|
1230
|
+
s.viewportPending = true;
|
|
1231
|
+
s.viewportArrived = false;
|
|
1187
1232
|
}
|
|
1188
1233
|
if (!rendererReported) {
|
|
1189
1234
|
if (pendingApplies.length >= MAX_PENDING_APPLIES) {
|
|
@@ -1605,6 +1650,13 @@ class TabSession {
|
|
|
1605
1650
|
viewportArrived = false;
|
|
1606
1651
|
presetId = "1080p-24";
|
|
1607
1652
|
profileId = "reference";
|
|
1653
|
+
/**
|
|
1654
|
+
* Which way round this tab's screen is held. Per tab like the preset it
|
|
1655
|
+
* rotates, and mirrored here from the renderer's `uiState` for the same
|
|
1656
|
+
* reason: a restored tab has to come back the way it was left, and no
|
|
1657
|
+
* renderer existed to report that when the list came off disk.
|
|
1658
|
+
*/
|
|
1659
|
+
orientation = targetSource.DEFAULT_ORIENTATION;
|
|
1608
1660
|
viewMode = "fit";
|
|
1609
1661
|
/**
|
|
1610
1662
|
* Resolves once both panes have settled on their initial `about:blank`.
|
|
@@ -1824,7 +1876,8 @@ class TabManager {
|
|
|
1824
1876
|
url: t.url,
|
|
1825
1877
|
title: t.title,
|
|
1826
1878
|
presetId: t.presetId,
|
|
1827
|
-
profileId: t.profileId
|
|
1879
|
+
profileId: t.profileId,
|
|
1880
|
+
orientation: t.orientation
|
|
1828
1881
|
})),
|
|
1829
1882
|
activeId: this.id
|
|
1830
1883
|
};
|
|
@@ -1842,7 +1895,12 @@ class TabManager {
|
|
|
1842
1895
|
* writes the tab that is named, not the tab that is showing.
|
|
1843
1896
|
*/
|
|
1844
1897
|
create() {
|
|
1898
|
+
let booted = false;
|
|
1845
1899
|
const s = new TabSession(this.win, (url) => {
|
|
1900
|
+
if (!booted) {
|
|
1901
|
+
booted = true;
|
|
1902
|
+
if (url === "about:blank") return;
|
|
1903
|
+
}
|
|
1846
1904
|
s.url = url;
|
|
1847
1905
|
s.title = "";
|
|
1848
1906
|
this.toRenderer(IPC.urlChanged, { tabId: s.id, url });
|
|
@@ -17,6 +17,10 @@ const DEFAULT_SETTINGS = {
|
|
|
17
17
|
split: 0.5,
|
|
18
18
|
maxTabs: 12
|
|
19
19
|
};
|
|
20
|
+
const DEFAULT_ORIENTATION = "portrait";
|
|
21
|
+
function isOrientation(v) {
|
|
22
|
+
return v === "portrait" || v === "landscape";
|
|
23
|
+
}
|
|
20
24
|
const SCREEN_PRESETS = [
|
|
21
25
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
22
26
|
{ id: "laptop-768", label: '1366×768 15.6"', width: 1366, height: 768, diagonalInches: 15.6, deviceScaleFactor: 1, group: "laptop" },
|
|
@@ -55,6 +59,9 @@ function findProfile(id) {
|
|
|
55
59
|
if (!p) throw new Error(`unknown profile: ${id}`);
|
|
56
60
|
return p;
|
|
57
61
|
}
|
|
62
|
+
function screenShape(width, height) {
|
|
63
|
+
return width > height ? "landscape" : "portrait";
|
|
64
|
+
}
|
|
58
65
|
function maxCssViewport(deviceScaleFactor) {
|
|
59
66
|
const dsf = Number.isFinite(deviceScaleFactor) && deviceScaleFactor > 1 ? deviceScaleFactor : 1;
|
|
60
67
|
return Math.max(1, Math.floor(MAX_VIEWPORT / dsf));
|
|
@@ -102,6 +109,9 @@ function urlSchemeError(url) {
|
|
|
102
109
|
if (/^[a-z0-9.-]+:\d+(\/|$)/i.test(trimmed)) return null;
|
|
103
110
|
return `unsupported URL scheme "${scheme}" — obsrv renders ${ALLOWED_URL_SCHEMES.map((s) => `${s}//`).join(", ")} URLs only (bare hosts like example.com also work; they normalise to http(s)).`;
|
|
104
111
|
}
|
|
112
|
+
function isBlankUrl(url) {
|
|
113
|
+
return url === "";
|
|
114
|
+
}
|
|
105
115
|
const ERR_ABORTED = -3;
|
|
106
116
|
const DEFAULT_FPS = 30;
|
|
107
117
|
const DEFAULT_VIEWPORT = { width: 1920, height: 1080 };
|
|
@@ -410,6 +420,7 @@ class TargetSource extends node_events.EventEmitter {
|
|
|
410
420
|
}
|
|
411
421
|
}
|
|
412
422
|
exports.ALLOWED_URL_SCHEMES = ALLOWED_URL_SCHEMES;
|
|
423
|
+
exports.DEFAULT_ORIENTATION = DEFAULT_ORIENTATION;
|
|
413
424
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
414
425
|
exports.IMAGE_EXTENSIONS = IMAGE_EXTENSIONS;
|
|
415
426
|
exports.MAX_TABS_MAX = MAX_TABS_MAX;
|
|
@@ -422,6 +433,9 @@ exports.TargetSource = TargetSource;
|
|
|
422
433
|
exports.classifyFileNavigation = classifyFileNavigation;
|
|
423
434
|
exports.findPreset = findPreset;
|
|
424
435
|
exports.findProfile = findProfile;
|
|
436
|
+
exports.isBlankUrl = isBlankUrl;
|
|
437
|
+
exports.isOrientation = isOrientation;
|
|
425
438
|
exports.maxCssViewport = maxCssViewport;
|
|
426
439
|
exports.normalizeUrl = normalizeUrl;
|
|
440
|
+
exports.screenShape = screenShape;
|
|
427
441
|
exports.urlSchemeError = urlSchemeError;
|
package/out/mcp/lib.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PANE_CAPTURE_HEADLESS_NOTE = exports.APP_NOT_REACHABLE = exports.urlSchemeError = exports.ALLOWED_URL_SCHEMES = exports.STDERR_TAIL_CHARS = exports.MAX_INLINE_IMAGE_BYTES = exports.UsageError = void 0;
|
|
3
|
+
exports.ORIENTATION_NOTE = exports.PANE_CAPTURE_HEADLESS_NOTE = exports.APP_NOT_REACHABLE = exports.urlSchemeError = exports.ALLOWED_URL_SCHEMES = exports.STDERR_TAIL_CHARS = exports.MAX_INLINE_IMAGE_BYTES = exports.UsageError = void 0;
|
|
4
4
|
exports.buildSnapArgs = buildSnapArgs;
|
|
5
5
|
exports.buildDiffArgs = buildDiffArgs;
|
|
6
6
|
exports.shouldInlineImage = shouldInlineImage;
|
|
@@ -46,6 +46,8 @@ function buildSnapArgs(input, outPath) {
|
|
|
46
46
|
const args = ['snap', input.url];
|
|
47
47
|
if (input.preset !== undefined)
|
|
48
48
|
args.push('--preset', input.preset);
|
|
49
|
+
if (input.orientation !== undefined)
|
|
50
|
+
args.push('--orientation', input.orientation);
|
|
49
51
|
if (custom) {
|
|
50
52
|
args.push('--width', String(input.width), '--height', String(input.height));
|
|
51
53
|
if (input.deviceScaleFactor !== undefined)
|
|
@@ -170,9 +172,20 @@ function extractTrailingJson(stdout) {
|
|
|
170
172
|
}
|
|
171
173
|
return null;
|
|
172
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* What `obsrv_presets` says about rotation. Stated once here rather than
|
|
177
|
+
* repeated per entry: it is true of every preset in the table, and a field
|
|
178
|
+
* saying "rotatable: true" fourteen times would carry less than one sentence.
|
|
179
|
+
*/
|
|
180
|
+
exports.ORIENTATION_NOTE = 'cssWidth/cssHeight are each preset\'s natural orientation — portrait for every mobile preset, ' +
|
|
181
|
+
'landscape for the monitor and laptop ones. Every preset rotates: pass orientation: "landscape" ' +
|
|
182
|
+
'to obsrv_snap or obsrv_drive to swap the two axes a quarter turn. Rotation changes nothing else — ' +
|
|
183
|
+
'the diagonal, deviceScaleFactor, ppi and physical size are all orientation-independent, so a ' +
|
|
184
|
+
'rotated screen is the same panel turned sideways rather than a different one.';
|
|
173
185
|
/** The `obsrv_presets` payload, straight from src/shared/presets.ts — no spawn. */
|
|
174
186
|
function listCatalog() {
|
|
175
187
|
return {
|
|
188
|
+
orientation: exports.ORIENTATION_NOTE,
|
|
176
189
|
presets: presets_1.SCREEN_PRESETS.map(p => ({
|
|
177
190
|
id: p.id,
|
|
178
191
|
label: p.label,
|