@zenbu-labs/pixel 0.0.15 → 0.0.16

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/index.d.ts CHANGED
@@ -11,7 +11,8 @@ export type { DevtoolsDock } from "./web/types";
11
11
  export type { ZoomDirection } from "./web/zoom";
12
12
  export type { TerminalTheme } from "./web/session";
13
13
  export { QUIT_URL, defaultOpenWindow } from "./web/host";
14
- export { Box, Text, Input, Image, MarkedText, Path, Markdown, appLog, useTerminalColors, setClipboard, setPointerShape, requestClipboardImage, highlight, diff, parseMarkdown, HIGHLIGHT_CAPTURES, Surface, SurfaceCapture, captureFilmstrip, encodeRecording, } from "./react";
14
+ export { Box, Text, Input, Image, MarkedText, Path, Markdown, appLog, useTerminalColors, setClipboard, setPointerShape, requestClipboardImage, highlight, diff, parseMarkdown, HIGHLIGHT_CAPTURES, Surface, SurfaceCapture, captureFilmstrip, encodeRecording, engineLogs, useStore, } from "./react";
15
+ export type { LogRow, LogBuffer } from "./react";
15
16
  export type { BoxProps, TextProps, TextSpan, InputProps, InputGutter, ImageProps, ImageAdvancedProps, MarkedTextProps, MarkRef, PathProps, ShapeStroke, ClickEvent, DragEvent, EventMods, MouseMoveEvent, ScrollEvent, WheelEvent, PointerEvent, PastedImage, PasteSource, CaretInfo, ChangeInfo, ChangeSource, ContainerSelection, SelectionPart, NodeHandle, Color, Edges, InsetEdges, InsetValue, ScrollbarStyle, Style, EngineInfo, EngineKeyEvent, KeyMods, Rgba, TerminalColors, SurfaceFrame, SurfaceTexture, CaptureStats, CaptureFrameMeta, CaptureIndex, MarkdownProps, MarkdownTheme, MarkdownBlock, HighlightSpan, DiffRow, } from "./react";
16
17
  export { makeTheme } from "./theme";
17
18
  export type { Theme } from "./theme";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.makeTheme = exports.encodeRecording = exports.captureFilmstrip = exports.SurfaceCapture = exports.Surface = exports.HIGHLIGHT_CAPTURES = exports.parseMarkdown = exports.diff = exports.highlight = exports.requestClipboardImage = exports.setPointerShape = exports.setClipboard = exports.useTerminalColors = exports.appLog = exports.Markdown = exports.Path = exports.MarkedText = exports.Image = exports.Input = exports.Text = exports.Box = exports.defaultOpenWindow = exports.QUIT_URL = exports.DevTools = exports.WebView = exports.listInstances = exports.findOwner = exports.createRoot = void 0;
3
+ exports.makeTheme = exports.useStore = exports.engineLogs = exports.encodeRecording = exports.captureFilmstrip = exports.SurfaceCapture = exports.Surface = exports.HIGHLIGHT_CAPTURES = exports.parseMarkdown = exports.diff = exports.highlight = exports.requestClipboardImage = exports.setPointerShape = exports.setClipboard = exports.useTerminalColors = exports.appLog = exports.Markdown = exports.Path = exports.MarkedText = exports.Image = exports.Input = exports.Text = exports.Box = exports.defaultOpenWindow = exports.QUIT_URL = exports.DevTools = exports.WebView = exports.listInstances = exports.findOwner = exports.createRoot = void 0;
4
4
  /// <reference path="../electron/electron.d.ts" preserve="true" />
5
5
  var root_1 = require("./root");
6
6
  Object.defineProperty(exports, "createRoot", { enumerable: true, get: function () { return root_1.createRoot; } });
@@ -35,5 +35,7 @@ Object.defineProperty(exports, "Surface", { enumerable: true, get: function () {
35
35
  Object.defineProperty(exports, "SurfaceCapture", { enumerable: true, get: function () { return react_1.SurfaceCapture; } });
36
36
  Object.defineProperty(exports, "captureFilmstrip", { enumerable: true, get: function () { return react_1.captureFilmstrip; } });
37
37
  Object.defineProperty(exports, "encodeRecording", { enumerable: true, get: function () { return react_1.encodeRecording; } });
38
+ Object.defineProperty(exports, "engineLogs", { enumerable: true, get: function () { return react_1.engineLogs; } });
39
+ Object.defineProperty(exports, "useStore", { enumerable: true, get: function () { return react_1.useStore; } });
38
40
  var theme_1 = require("./theme");
39
41
  Object.defineProperty(exports, "makeTheme", { enumerable: true, get: function () { return theme_1.makeTheme; } });
@@ -13,6 +13,8 @@ export declare function setHighlight(id: number | null, area?: HighlightArea): v
13
13
  export declare function setPicking(on: boolean): void;
14
14
  export declare function toggleExpanded(id: number): void;
15
15
  export declare function setCpuThrottle(rate: number): void;
16
+ export declare function setMaxFps(fps: number): void;
17
+ export declare function setHighlightTransmits(on: boolean): void;
16
18
  export declare function requestLayout(): void;
17
19
  export declare function startRecording(): void;
18
20
  export declare function stopRecording(): void;
@@ -13,6 +13,8 @@ exports.setHighlight = setHighlight;
13
13
  exports.setPicking = setPicking;
14
14
  exports.toggleExpanded = toggleExpanded;
15
15
  exports.setCpuThrottle = setCpuThrottle;
16
+ exports.setMaxFps = setMaxFps;
17
+ exports.setHighlightTransmits = setHighlightTransmits;
16
18
  exports.requestLayout = requestLayout;
17
19
  exports.startRecording = startRecording;
18
20
  exports.stopRecording = stopRecording;
@@ -55,11 +57,14 @@ function openDevtools(selectId) {
55
57
  stores_1.devtoolsStore.update((s) => ({ ...s, open: true }));
56
58
  if (selectId != null)
57
59
  selectNode(selectId, true);
60
+ engineOp({ op: "setLogCapture", on: true });
58
61
  engineOp({ op: "setSplit", fraction: exports.DEFAULT_SPLIT });
59
62
  }
60
63
  function closeDevtools() {
61
64
  stores_1.devtoolsStore.update((s) => ({ ...s, open: false }));
62
65
  setHighlight(null);
66
+ engineOp({ op: "setLogCapture", on: false });
67
+ stores_1.engineLogs.clear();
63
68
  engineOp({ op: "setSplit", fraction: null });
64
69
  }
65
70
  function toggleDevtools() {
@@ -132,6 +137,14 @@ function setCpuThrottle(rate) {
132
137
  stores_1.devtoolsStore.update((s) => ({ ...s, cpuRate: rate }));
133
138
  engineOp({ op: "setCpuThrottle", rate });
134
139
  }
140
+ function setMaxFps(fps) {
141
+ stores_1.devtoolsStore.update((s) => ({ ...s, maxFps: fps }));
142
+ engineOp({ op: "setRender", maxFps: fps });
143
+ }
144
+ function setHighlightTransmits(on) {
145
+ stores_1.devtoolsStore.update((s) => ({ ...s, highlightTransmits: on }));
146
+ engineOp({ op: "setRender", highlightTransmits: on });
147
+ }
135
148
  function requestLayout() {
136
149
  const b = (0, reconciler_config_1.devtoolsBridge)();
137
150
  b.push(reconciler_config_1.APP_VIEW, { op: "queryLayout" });
@@ -84,7 +84,21 @@ const GLOSSARY = {
84
84
  aggregates_note: "paint.* spans are per-frame aggregates laid out back-to-back under tree.paint: " +
85
85
  "durations and counts are real, start times are not the true instants.",
86
86
  counters: "Point samples: 'bytes' = escape-sequence bytes written per frame; 'paint.nodes' / " +
87
- "'paint.glyphs' = nodes visited / glyphs drawn per paint.",
87
+ "'paint.glyphs' = nodes visited / glyphs drawn per paint; 'present.patches' = damaged " +
88
+ "rects sent as separate images that frame, 'present.pixels' = pixels handed to the " +
89
+ "terminal (the whole frame when a full image went out), 'present.whole_frame' = a frame " +
90
+ "that fell back to one full image because too much changed or the patch pool was full. " +
91
+ "Per-stage pixel counts show amplification along the pipeline: 'surface.damage_px' = area " +
92
+ "of the dirty rects the browser reported, 'surface.changed_px' = area that actually changed " +
93
+ "after comparing, 'paint.px' = area repainted in the view, 'compose.px' = area copied into " +
94
+ "the frame, 'present.pixels' = area handed to the terminal. Each should be close to the one " +
95
+ "before it; a jump marks work done on pixels that did not change. 'present.compactions' " +
96
+ "= live patches folded into one image, re-sending their union, to free image ids or " +
97
+ "placements; 'present.absorbed' = a mostly hidden patch deleted as a new one grew over it; " +
98
+ "'present.tiles' = tiles outside the opaque zones re-sent because their pixels changed. " +
99
+ "'cpu.frame_thread_us' / 'cpu.frame_process_us' = CPU time the engine thread / the whole " +
100
+ "process burned during that frame (spans record wall time on one thread and miss worker " +
101
+ "threads and kernel time); 'cpu.convert_thread_us' = the same for one surface compare.",
88
102
  cpuThrottle_note: "meta.cpuThrottle > 1 means the engine and JS threads were duty-cycle suspended to " +
89
103
  "simulate a CPU that many times slower; durations are inflated accordingly. Throttle " +
90
104
  "changes mid-recording appear as 'cpu throttle Nx' interactions.",
@@ -364,6 +364,21 @@ function ProfilerPanel(props) {
364
364
  const rates = [1, 2, 4, 10];
365
365
  const next = rates[(rates.indexOf(devtools.cpuRate) + 1) % rates.length];
366
366
  (0, controller_1.setCpuThrottle)(next);
367
+ } }), (0, jsx_runtime_1.jsx)(ui_1.Button, { rem: rem, label: "outline sends", active: devtools.highlightTransmits, onClick: () => (0, controller_1.setHighlightTransmits)(!devtools.highlightTransmits) }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: { color: theme_1.theme.faint, fontSize: rem * 0.66, wrap: false }, children: "fps" }), (0, jsx_runtime_1.jsx)(components_1.Input, { style: {
368
+ width: rem * 2.6,
369
+ height: rem * 1.3,
370
+ fontSize: rem * 0.7,
371
+ color: theme_1.theme.text,
372
+ font: theme_1.MONO,
373
+ wrap: false,
374
+ padding: { left: rem * 0.3, right: rem * 0.3 },
375
+ border: { width: 1, color: theme_1.theme.border },
376
+ cornerRadius: rem * 0.2,
377
+ }, defaultValue: devtools.maxFps === 0 ? "off" : String(devtools.maxFps), onSubmit: (text) => {
378
+ const value = text.trim().toLowerCase();
379
+ const fps = value === "off" || value === "0" ? 0 : Number(value);
380
+ if (Number.isFinite(fps) && fps >= 0)
381
+ (0, controller_1.setMaxFps)(fps);
367
382
  } }), session && !exportedTo && (0, jsx_runtime_1.jsx)(Summary, { session: session, rem: rem }), exportedTo && ((0, jsx_runtime_1.jsx)(components_1.Text, { style: { color: theme_1.theme.ok, fontSize: rem * 0.66, font: theme_1.MONO, wrap: false }, children: exportedTo })), (0, jsx_runtime_1.jsx)(components_1.Box, { style: { flexGrow: 1 } }), (0, jsx_runtime_1.jsx)(components_1.Text, { style: { color: theme_1.theme.faint, fontSize: rem * 0.62, wrap: false }, children: "f=fit" })] }), (0, jsx_runtime_1.jsx)(ui_1.Divider, {}), state.recording ? ((0, jsx_runtime_1.jsx)(ui_1.Empty, { rem: rem, text: "Recording\u2026" })) : state.pendingStop ? ((0, jsx_runtime_1.jsx)(ui_1.Empty, { rem: rem, text: "Waiting for profile\u2026" })) : !session || !viewport ? ((0, jsx_runtime_1.jsx)(ui_1.Empty, { rem: rem, text: "" })) : ((0, jsx_runtime_1.jsxs)(components_1.Box, { style: {
368
383
  flexDirection: "column",
369
384
  flexGrow: 1,
@@ -80,13 +80,16 @@ export interface ProfilerState {
80
80
  export declare const profilerStore: import("./store").Store<ProfilerState>;
81
81
  export declare const pendingSpans: TimeSpan[];
82
82
  export declare function recordSpan(span: TimeSpan): void;
83
+ export type DevtoolsTab = "elements" | "console" | "profiler";
83
84
  export interface DevtoolsState {
84
85
  open: boolean;
85
- tab: "elements" | "console" | "profiler";
86
+ tab: DevtoolsTab;
86
87
  width: number;
87
88
  height: number;
88
89
  basePx: number;
89
90
  cpuRate: number;
91
+ highlightTransmits: boolean;
92
+ maxFps: number;
90
93
  background: Rgba | null;
91
94
  }
92
95
  export declare const devtoolsStore: import("./store").Store<DevtoolsState>;
@@ -37,5 +37,7 @@ exports.devtoolsStore = (0, store_1.createStore)({
37
37
  height: 0,
38
38
  basePx: 16,
39
39
  cpuRate: 1,
40
+ highlightTransmits: false,
41
+ maxFps: 0,
40
42
  background: null,
41
43
  });
@@ -4,14 +4,16 @@ import type { EngineInfo, HostOptions, TerminalColors } from "./native";
4
4
  import { Surface } from "./surface";
5
5
  import { closeDevtools, engineOp, openDevtools, requestLayout, toggleDevtools } from "./devtools/controller";
6
6
  import type { Rgba } from "./native";
7
- import { inspectorStore, LayoutSnapshot } from "./devtools/stores";
7
+ import { DevtoolsTab, inspectorStore, LayoutSnapshot } from "./devtools/stores";
8
8
  import type { LogLevel } from "./devtools/store";
9
9
  declare function appLog(level: LogLevel, target: string, text: string): void;
10
10
  export { Box, Text, Input, Image, MarkedText, Path, } from "./components";
11
11
  export type { NodeHandle } from "./components";
12
12
  export { appLog };
13
- export { layoutStore, profilerStore } from "./devtools/stores";
14
- export type { LayoutSnapshot, ProfileSession } from "./devtools/stores";
13
+ export { layoutStore, profilerStore, engineLogs } from "./devtools/stores";
14
+ export { useStore } from "./devtools/store";
15
+ export type { LogRow, LogBuffer } from "./devtools/store";
16
+ export type { DevtoolsTab, LayoutSnapshot, ProfileSession } from "./devtools/stores";
15
17
  export type { BoxProps, TextProps, TextSpan, InputGutter, InputProps, MarkRef, PasteSource, PastedImage, CaretInfo, ChangeInfo, ChangeSource, ImageProps, ImageAdvancedProps, MarkedTextProps, ClickEvent, ContainerSelection, DragEvent, EventMods, MouseMoveEvent, ScrollEvent, SelectionPart, WheelEvent, PointerEvent, ShapeStroke, PathProps, } from "./reconciler-config";
16
18
  export type { Color, Edges, InsetEdges, InsetValue, ScrollbarStyle, Style } from "./styles";
17
19
  export type { DiffEmphasis, DiffRow, EngineInfo, HighlightSpan, HostOptions, MarkdownBlock, MarkdownCell, MarkdownRow, MarkdownSpan, Rgba, SurfaceShm, TerminalColors, } from "./native";
@@ -74,6 +76,13 @@ export interface RootOptions {
74
76
  wrapper?: "tmux";
75
77
  sessionEnv?: NodeJS.ProcessEnv;
76
78
  }
79
+ export interface RenderSettings {
80
+ maxFps?: number;
81
+ frameBudgetMbps?: number;
82
+ highlightTransmits?: boolean;
83
+ compareFrames?: boolean;
84
+ frameEvents?: boolean;
85
+ }
77
86
  export interface PixelRoot {
78
87
  info: EngineInfo;
79
88
  sharedTextures: boolean;
@@ -83,8 +92,13 @@ export interface PixelRoot {
83
92
  createSurface(): Surface;
84
93
  surfaceStats(): SurfaceStats;
85
94
  stop(): void;
86
- openDevtools(): void;
95
+ openDevtools(tab?: DevtoolsTab): void;
87
96
  closeDevtools(): void;
97
+ startProfile(): void;
98
+ stopProfile(): Promise<string | null>;
99
+ setRender(settings: RenderSettings): void;
100
+ setLogFile(path: string | null): void;
101
+ highlightTransmits(): boolean;
88
102
  nudgeResize(): void;
89
103
  queryLayout(): void;
90
104
  setPointerShape(shape: string): void;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.inspectorStore = exports.engineOp = exports.requestLayout = exports.toggleDevtools = exports.closeDevtools = exports.openDevtools = exports.Markdown = exports.SurfaceCapture = exports.Surface = exports.useTerminalColors = exports.parseMarkdown = exports.highlight = exports.encodeRecording = exports.diff = exports.captureFilmstrip = exports.HIGHLIGHT_CAPTURES = exports.profilerStore = exports.layoutStore = exports.Path = exports.MarkedText = exports.Image = exports.Input = exports.Text = exports.Box = void 0;
3
+ exports.inspectorStore = exports.engineOp = exports.requestLayout = exports.toggleDevtools = exports.closeDevtools = exports.openDevtools = exports.Markdown = exports.SurfaceCapture = exports.Surface = exports.useTerminalColors = exports.parseMarkdown = exports.highlight = exports.encodeRecording = exports.diff = exports.captureFilmstrip = exports.HIGHLIGHT_CAPTURES = exports.useStore = exports.engineLogs = exports.profilerStore = exports.layoutStore = exports.Path = exports.MarkedText = exports.Image = exports.Input = exports.Text = exports.Box = void 0;
4
4
  exports.appLog = appLog;
5
5
  exports.setKeyCapture = setKeyCapture;
6
6
  exports.setPointerShape = setPointerShape;
@@ -11,6 +11,7 @@ const react_1 = require("react");
11
11
  const constants_1 = require("react-reconciler/constants");
12
12
  const reconciler_config_1 = require("./reconciler-config");
13
13
  const surface_1 = require("./surface");
14
+ const frame_rate_1 = require("../web/frame-rate");
14
15
  const app_1 = require("./devtools/app");
15
16
  const console_capture_1 = require("./devtools/console-capture");
16
17
  const controller_1 = require("./devtools/controller");
@@ -38,6 +39,9 @@ Object.defineProperty(exports, "Path", { enumerable: true, get: function () { re
38
39
  var stores_2 = require("./devtools/stores");
39
40
  Object.defineProperty(exports, "layoutStore", { enumerable: true, get: function () { return stores_2.layoutStore; } });
40
41
  Object.defineProperty(exports, "profilerStore", { enumerable: true, get: function () { return stores_2.profilerStore; } });
42
+ Object.defineProperty(exports, "engineLogs", { enumerable: true, get: function () { return stores_2.engineLogs; } });
43
+ var store_1 = require("./devtools/store");
44
+ Object.defineProperty(exports, "useStore", { enumerable: true, get: function () { return store_1.useStore; } });
41
45
  var native_1 = require("./native");
42
46
  Object.defineProperty(exports, "HIGHLIGHT_CAPTURES", { enumerable: true, get: function () { return native_1.HIGHLIGHT_CAPTURES; } });
43
47
  Object.defineProperty(exports, "captureFilmstrip", { enumerable: true, get: function () { return native_1.captureFilmstrip; } });
@@ -129,7 +133,7 @@ function createRoot(options = {}) {
129
133
  stores_1.engineLogs.push("error", "react", String(error));
130
134
  }, null);
131
135
  if (devtoolsEnabled)
132
- enableDevtools();
136
+ (0, console_capture_1.installConsoleCapture)();
133
137
  const fontIds = new Map();
134
138
  const fontRequests = new Map();
135
139
  const retargets = [];
@@ -278,6 +282,8 @@ function createRoot(options = {}) {
278
282
  if (view === reconciler_config_1.APP_VIEW) {
279
283
  info.width = size.width;
280
284
  info.height = size.height;
285
+ info.cellWidth = event.cellWidth;
286
+ info.cellHeight = event.cellHeight;
281
287
  info.basePx = size.basePx;
282
288
  options.onResize?.(size);
283
289
  }
@@ -489,11 +495,54 @@ function createRoot(options = {}) {
489
495
  process.stdout.off("resize", forwardResize);
490
496
  process.off("exit", restore);
491
497
  },
492
- openDevtools() {
498
+ openDevtools(tab) {
493
499
  enableDevtools();
494
500
  (0, controller_1.attachDevtools)(bridge);
501
+ if (tab)
502
+ stores_1.devtoolsStore.update((s) => ({ ...s, tab }));
495
503
  (0, controller_1.openDevtools)();
496
504
  },
505
+ startProfile() {
506
+ enableDevtools();
507
+ (0, controller_1.attachDevtools)(bridge);
508
+ (0, controller_1.startRecording)();
509
+ },
510
+ stopProfile() {
511
+ return new Promise((resolve) => {
512
+ const now = stores_1.profilerStore.get();
513
+ if (!now.recording && !now.pendingStop) {
514
+ resolve((0, export_profile_1.exportProfile)());
515
+ return;
516
+ }
517
+ const unsubscribe = stores_1.profilerStore.subscribe(() => {
518
+ const state = stores_1.profilerStore.get();
519
+ if (state.recording || state.pendingStop)
520
+ return;
521
+ unsubscribe();
522
+ resolve((0, export_profile_1.exportProfile)());
523
+ });
524
+ (0, controller_1.stopRecording)();
525
+ });
526
+ },
527
+ setRender(settings) {
528
+ // The browser should not bother producing frames the engine would only drop.
529
+ if (settings.maxFps !== undefined)
530
+ (0, frame_rate_1.requestFrameRate)(settings.maxFps);
531
+ stores_1.devtoolsStore.update((s) => ({
532
+ ...s,
533
+ maxFps: settings.maxFps ?? s.maxFps,
534
+ highlightTransmits: settings.highlightTransmits ?? s.highlightTransmits,
535
+ }));
536
+ bridge.push(reconciler_config_1.APP_VIEW, { op: "setRender", ...settings });
537
+ bridge.flush();
538
+ },
539
+ setLogFile(path) {
540
+ bridge.push(reconciler_config_1.APP_VIEW, { op: "setLogFile", path });
541
+ bridge.flush();
542
+ },
543
+ highlightTransmits() {
544
+ return stores_1.devtoolsStore.get().highlightTransmits;
545
+ },
497
546
  closeDevtools() {
498
547
  if ((0, reconciler_config_1.devtoolsBridge)() === bridge)
499
548
  (0, controller_1.closeDevtools)();
@@ -15,7 +15,7 @@ export interface NativeEngine {
15
15
  info(): string;
16
16
  applyOps(ops: string): void;
17
17
  updateSurface(id: number, bgra: Buffer, width: number, height: number, damage?: DamageRect): void;
18
- updateSurfaceTexture?(id: number, handle: Buffer, damage?: DamageRect): void;
18
+ updateSurfaceTexture?(id: number, handle: number, damageX: number, damageY: number, damageWidth: number, damageHeight: number): void;
19
19
  updateSurfaceShm?(id: number, shm: SurfaceShm, damage?: DamageRect, released?: (...args: unknown[]) => void): void;
20
20
  removeSurface(id: number): void;
21
21
  surfaceStats(): string;
@@ -72,6 +72,7 @@ export interface Style {
72
72
  font?: number;
73
73
  hoverBackground?: Color;
74
74
  hoverColor?: Color;
75
+ opaque?: boolean;
75
76
  scrollbar?: ScrollbarStyle;
76
77
  wrap?: boolean;
77
78
  ellipsis?: boolean;
@@ -70,6 +70,7 @@ function serializeStyle(style) {
70
70
  wrap: style.wrap,
71
71
  ellipsis: style.ellipsis,
72
72
  selectable: style.selectable,
73
+ opaque: style.opaque,
73
74
  selectionColor: parseColor(style.selectionColor),
74
75
  selectionMode: style.selectionMode,
75
76
  scrollbar: style.scrollbar && {
@@ -20,7 +20,9 @@ class Surface {
20
20
  const submit = this.engine.updateSurfaceTexture;
21
21
  if (!submit)
22
22
  throw new Error("IOSurface frames are not supported on this platform");
23
- submit.call(this.engine, this.id, frame.ioSurface, frame.damage);
23
+ const address = frame.ioSurface.readUIntLE(0, 6);
24
+ const d = frame.damage;
25
+ submit.call(this.engine, this.id, address, d?.x ?? 0, d?.y ?? 0, d?.width ?? 0, d?.height ?? 0);
24
26
  }
25
27
  else if ("shm" in frame) {
26
28
  const submit = this.engine.updateSurfaceShm;
package/dist/root.js CHANGED
@@ -338,6 +338,12 @@ function createRoot(options = {}) {
338
338
  shutdown(code);
339
339
  },
340
340
  };
341
+ try {
342
+ const hz = electron_1.screen.getPrimaryDisplay().displayFrequency;
343
+ if (hz > 0)
344
+ engineRoot.setRender({ maxFps: hz });
345
+ }
346
+ catch { }
341
347
  liveRoots.add(root);
342
348
  return root;
343
349
  }
package/dist/scale.js CHANGED
@@ -14,16 +14,16 @@ function hostDisplayScale(terminal, env) {
14
14
  class CellZoomFollower {
15
15
  last = null;
16
16
  ratio(info) {
17
- const { height, basePx } = info;
17
+ const { height, basePx, cellHeight } = info;
18
+ const rows = cellHeight > 0 ? Math.round(height / cellHeight) : 0;
18
19
  const prev = this.last;
19
- this.last = { height, basePx };
20
- if (!prev || !prev.basePx || !prev.height)
20
+ this.last = { rows, basePx };
21
+ if (!prev || !prev.basePx || !prev.rows || !rows)
21
22
  return null;
22
23
  const ratio = basePx / prev.basePx;
23
24
  if (!Number.isFinite(ratio) || ratio <= 0 || Math.abs(ratio - 1) < 0.01)
24
25
  return null;
25
- const paneRatio = height / prev.height;
26
- if (Math.abs(paneRatio - ratio) < 0.04 * ratio)
26
+ if (rows === prev.rows)
27
27
  return null;
28
28
  return ratio;
29
29
  }
@@ -17,7 +17,7 @@ const DIRECTION_CODES = {
17
17
  down: "GSdn",
18
18
  up: "GSup",
19
19
  };
20
- const GHOSTTY_BINARY = /\/Ghostty\.app\/Contents\/MacOS\/ghostty(\s|$)/;
20
+ const GHOSTTY_BINARY = /\.app\/Contents\/MacOS\/ghostty(\s|$)/;
21
21
  function ghosttyAbove(start, processes) {
22
22
  for (let pid = start, hops = 0; hops < 64 && processes.has(pid); hops++) {
23
23
  const { parent, command } = processes.get(pid);
@@ -1 +1,3 @@
1
+ export declare function requestFrameRate(fps: number): void;
2
+ export declare function onFrameRateChange(listener: () => void): () => void;
1
3
  export declare function frameRate(): number;
@@ -1,11 +1,35 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestFrameRate = requestFrameRate;
4
+ exports.onFrameRateChange = onFrameRateChange;
3
5
  exports.frameRate = frameRate;
4
6
  const electron_1 = require("electron");
7
+ let requested = 0;
8
+ const listeners = new Set();
9
+ function clamp(fps) {
10
+ return Math.max(1, Math.min(240, Math.round(fps)));
11
+ }
12
+ function requestFrameRate(fps) {
13
+ const next = Number.isFinite(fps) && fps > 0 ? clamp(fps) : 0;
14
+ if (next === requested)
15
+ return;
16
+ requested = next;
17
+ for (const listener of listeners)
18
+ listener();
19
+ }
20
+ function onFrameRateChange(listener) {
21
+ listeners.add(listener);
22
+ return () => {
23
+ listeners.delete(listener);
24
+ };
25
+ }
5
26
  function frameRate() {
6
27
  const configured = Number(process.env.PIXEL_FPS);
7
28
  if (Number.isFinite(configured) && configured > 0) {
8
- return Math.max(1, Math.min(240, Math.round(configured)));
29
+ return clamp(configured);
30
+ }
31
+ if (requested > 0) {
32
+ return requested;
9
33
  }
10
34
  const fastest = Math.max(0, ...electron_1.screen.getAllDisplays().map((display) => display.displayFrequency));
11
35
  return fastest > 0 ? Math.min(240, Math.round(fastest)) : 60;
@@ -32,6 +32,7 @@ export declare class PageHost {
32
32
  private layout;
33
33
  private state;
34
34
  private stopped;
35
+ private stopWatchingFrameRate?;
35
36
  private contentFocused;
36
37
  private readonly input;
37
38
  private readonly clipboardRead;
package/dist/web/host.js CHANGED
@@ -62,6 +62,7 @@ class PageHost {
62
62
  layout;
63
63
  state;
64
64
  stopped = false;
65
+ stopWatchingFrameRate;
65
66
  contentFocused = false;
66
67
  input;
67
68
  clipboardRead;
@@ -133,6 +134,7 @@ class PageHost {
133
134
  if (this.quitLink(url))
134
135
  event.preventDefault();
135
136
  });
137
+ this.stopWatchingFrameRate = (0, frame_rate_1.onFrameRateChange)(this.onDisplayChange);
136
138
  electron_1.screen.on("display-added", this.onDisplayChange);
137
139
  electron_1.screen.on("display-removed", this.onDisplayChange);
138
140
  electron_1.screen.on("display-metrics-changed", this.onDisplayChange);
@@ -424,6 +426,8 @@ class PageHost {
424
426
  for (const popup of [...this.popups])
425
427
  popup.close();
426
428
  this.devtools?.close();
429
+ this.stopWatchingFrameRate?.();
430
+ this.stopWatchingFrameRate = undefined;
427
431
  electron_1.screen.off("display-added", this.onDisplayChange);
428
432
  electron_1.screen.off("display-removed", this.onDisplayChange);
429
433
  electron_1.screen.off("display-metrics-changed", this.onDisplayChange);
package/dist/webview.js CHANGED
@@ -431,6 +431,7 @@ exports.WebView = (0, react_1.forwardRef)(function WebView(props, ref) {
431
431
  height: page.height,
432
432
  cornerRadius: props.style?.cornerRadius,
433
433
  background: theme.bg,
434
+ opaque: !props.browserWindowOptions?.transparent,
434
435
  }, onPointer: (event) => {
435
436
  if (menuRef.current) {
436
437
  if (event.kind === "down")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenbu-labs/pixel",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "[placeholder copy: Electron apps that render into your terminal pane]",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -58,10 +58,10 @@
58
58
  "react": "^18.3.1"
59
59
  },
60
60
  "optionalDependencies": {
61
- "@zenbu-labs/pixel-native-darwin-arm64": "0.0.15",
62
- "@zenbu-labs/pixel-native-darwin-x64": "0.0.15",
63
- "@zenbu-labs/pixel-native-linux-arm64": "0.0.15",
64
- "@zenbu-labs/pixel-native-linux-x64": "0.0.15"
61
+ "@zenbu-labs/pixel-native-darwin-arm64": "0.0.16",
62
+ "@zenbu-labs/pixel-native-darwin-x64": "0.0.16",
63
+ "@zenbu-labs/pixel-native-linux-arm64": "0.0.16",
64
+ "@zenbu-labs/pixel-native-linux-x64": "0.0.16"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^22.0.0",