import { property, register, signal } from "astal/gobject"; import GLib from "gi://GLib"; import { DBusObject } from "@services/dbus"; import { OutputMode, OutputTransform, Rect, VSOutputAdapter, VSOutputMode, VSRect, VSWrapper, } from "@services/compositor/types"; import { CompositorConnection } from "@services/compositor"; @register({ GTypeName: "VSMonitor", }) export class VSMonitor extends DBusObject { static _nextId: number = 1; private valid: boolean = true; /** * The simplified name of the monitor, such as HDMI-A-1 or DP-3 */ @property(String) public readonly name: string; /** * The list of available modes reported by the monitor's EDID */ @property(Object) public readonly availableModes: VSOutputMode[]; /** * The make of the monitor reported by EDID */ @property(String) public readonly make: string; /** * The model of the monitor reported by EDID */ @property(String) public readonly model: string; /** * The serial of the monitor reported by EDID */ @property(String) public readonly serial: string; _mode: OutputMode | null; _active: boolean; _powered: boolean; _rect: Rect; _scale: number; _transform: OutputTransform; constructor(adapter: VSOutputAdapter) { super( "dev.ezri.voidshell.Monitor", `/dev/ezri/VoidShell/Monitor/_${VSMonitor._nextId++}`, ); console.log("Creating monitor", adapter.name); this.name = adapter.name; this.availableModes = adapter.availableModes; this.make = adapter.make; this.model = adapter.model; this.serial = adapter.serial; this._mode = adapter.mode?.simplify() ?? null; this._active = adapter.active; this._powered = adapter.powered; this._rect = adapter.rect.simplify(); this._scale = adapter.scale; this._transform = adapter.transform; } public sync(adapter: VSOutputAdapter) { if (!this.valid) { console.error( "Cannot sync a destroyed output! You have dangling objects!", ); return; } // EDID data shouldn't ever change, so we'll ignore it. const toEmit = new Set(); if (!adapter.mode?.equals(this._mode)) { toEmit.add("mode"); } if (this._active !== adapter.active) { toEmit.add("active"); } if (this._powered !== adapter.powered) { toEmit.add("powered"); } if (!adapter.rect.equals(this._rect)) { toEmit.add("rect"); if (adapter.rect.x !== this._rect.x || adapter.rect.y !== this._rect.y) { toEmit.add("position"); } } if (this._scale !== adapter.scale) { toEmit.add("scale"); } if (this._transform !== adapter.transform) { toEmit.add("transform"); } this._mode = adapter.mode?.simplify() ?? null; this._active = adapter.active; this._powered = adapter.powered; this._rect = adapter.rect.simplify(); this._scale = adapter.scale; this._transform = adapter.transform; toEmit.forEach((prop) => { let value = this[prop]; if (value instanceof VSWrapper) { value = value.simplify(); } this.notify(prop); this.emitDBusProperty(prop); }); } protected marshall_rect(): GLib.Variant { return new GLib.Variant("(iiii)", [ this._rect.x, this._rect.y, this._rect.width, this._rect.height, ]); } protected marshall_mode(): GLib.Variant { return new GLib.Variant("(iiis)", [ this._mode?.width ?? 0, this._mode?.height ?? 0, this._mode?.refresh ?? 0, this._mode?.aspect_ratio ?? "", ]); } protected marshall_availableModes(): GLib.Variant { return new GLib.Variant( "a(iiis)", this.availableModes.map((mode) => [ mode.width, mode.height, mode.refresh, mode.aspect_ratio, ]), ); } /** * The current "mode" of the monitor, consisting of width, height, and refresh rate. * Setting this will attempt to apply the change to the monitor asynchronously. Changes * will not be immediately reflected. */ @property(VSOutputMode) public get mode(): VSOutputMode | null { if (!this._mode) { return null; } return VSOutputMode.wrap(this._mode); } public set mode(value: OutputMode | [number, number, number, string]) { if (value instanceof Array) { const [width, height, refresh, aspect_ratio] = value; value = { width, height, refresh, aspect_ratio, }; } // Wrap it to get the features of VSWrapper const wrapped = VSOutputMode.wrap(value); if (wrapped.equals(this._mode)) { // Already in this mode return; } console.log("setting mode"); const refreshString = wrapped.refresh.toString(); const modeline = `${wrapped.width}x${wrapped.height}@${refreshString.substring(0, refreshString.length - 3)}.${refreshString.substring(refreshString.length - 3)}Hz` as const; console.log(this.availableModes.map((mode) => mode.simplify())); // Check to ensure that at least one mode matches this if (!this.availableModes.some((mode) => wrapped.equals(mode))) { // Invalid mode, log and return console.warn(`Invalid mode ${modeline} for output ${this.name}`); return; } // Apply the mode CompositorConnection.instance.setOutputProperty( this.name, "modeline", modeline, ); // Don't notify anyone yet, the stored value will be updated asynchronously when we get the output change event } /** * Whether the monitor is active (can have windows, workspaces, etc. on it). * Setting this will attempt to apply the change to the monitor asynchronously. Changes * will not be immediately reflected. */ @property(Boolean) public get active() { return this._active; } public set active(value) { if (value === this._active) { // Do nothing if nothing changed return; } // Set the active state CompositorConnection.instance.setOutputProperty(this.name, "active", value); } /** * Whether the monitor is powered. An unpowered monitor can still be active, but will be placed into * a power-saving mode with the panel turned off. * Setting this will attempt to apply the change to the monitor asynchronously. Changes * will not be immediately reflected. */ @property(Boolean) public get powered() { return this._powered; } public set powered(value) { if (value === this._powered) { // Do nothing if nothing changed return; } // Set the powered state CompositorConnection.instance .setOutputProperty(this.name, "powered", value) .catch(console.error); } /** * The position in the framebuffer the upper-left corner of the monitor is placed at. * Setting this will attempt to apply the change to the monitor asynchronously. Changes * will not be immediately reflected. */ @property(Object) public get position(): [x: number, y: number] { return [this._rect.x, this._rect.y]; } public set position(value) { if (value[0] === this._rect.x && value[1] === this._rect.y) { // Do nothing if nothing changed return; } // Set the position CompositorConnection.instance.setOutputProperty( this.name, "position", value, ); } /** * The scaling factor of the monitor, changing the effective size of the viewport into * the framebuffer. * Setting this will attempt to apply the change to the monitor asynchronously. Changes * will not be immediately reflected. */ @property(Number) public get scale(): number { return this._scale; } public set scale(value) { if (value === this._scale) { return; } CompositorConnection.instance.setOutputProperty(this.name, "scale", value); } /** * The rectangle of the viewport into the framebuffer. The width and height here represent * the _effective_ size of the monitor after scaling. * This property is read-only, and is determined by a combination of mode, position, and scale. */ @property(VSRect) public get rect(): VSRect { return VSRect.wrap(this._rect); } /** * The transform string of the monitor, indicating if and how it is rotated and flipped * Setting this will attempt to apply the change to the monitor asynchronously. Changes * will not be immediately reflected. */ @property(String) public get transform(): OutputTransform { return this._transform; } public set transform(value) { if (this._transform === value) { return; } CompositorConnection.instance.setOutputProperty( this.name, "transform", value, ); } public destroy() { this.dbusObj?.unexport(); this.valid = false; this.emit("destroyed"); } /** * Fired when the monitor is "destroyed" (disconnected). Indicates the object is now invalid. */ @signal() declare destroyed: () => void; /** * Fired when any element of the monitor's configuration changes. */ @signal(VSMonitor) declare changed: (monitor: this) => void; }