332 lines
9.0 KiB
TypeScript
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;
|
|
}
|