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 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
- return { specs: [{ presetId: 'custom', cssWidth, cssHeight, deviceScaleFactor, diagonalInches: diagonal }], matrix: false };
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-DlsHc-Ri.js");
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
- return { specs: [{ presetId: "custom", cssWidth, cssHeight, deviceScaleFactor, diagonalInches: diagonal }], matrix: false };
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-DlsHc-Ri.js");
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) => ({ url: t.url, presetId: t.presetId, profileId: t.profileId })),
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 = persistTabs;
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
- return { version: appVersion, url, tabId: tabs.activeId, tabIndex: tabs.activeIndex, ...uiState };
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
- if (patch.presetId !== void 0) {
1185
- tab().viewportPending = true;
1186
- tab().viewportArrived = false;
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,