332 lines
9.0 KiB
TypeScript

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<string & keyof this>();
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;
}