monitor interface works
This commit is contained in:
parent
c023391702
commit
7218885cd7
7
app.ts
7
app.ts
@ -2,10 +2,11 @@ import { App } from "astal/gtk3";
|
|||||||
import style from "./style.css";
|
import style from "./style.css";
|
||||||
import Bar from "./widget/Bar";
|
import Bar from "./widget/Bar";
|
||||||
import "@/globals";
|
import "@/globals";
|
||||||
import { Sway } from "@services/sway/ipc";
|
import { CompositorConnection, SwayConnection } from "@services/compositor";
|
||||||
import { Tree } from "@services/sway/workspaces";
|
import { OutputService } from "./services/outputs/service";
|
||||||
|
|
||||||
new Tree("/home/ezri/.config/sway/workspaces.json");
|
new SwayConnection();
|
||||||
|
new OutputService();
|
||||||
|
|
||||||
App.start({
|
App.start({
|
||||||
css: style,
|
css: style,
|
||||||
|
|||||||
22
dbus/dev.ezri.voidshell.Monitor.xml
Normal file
22
dbus/dev.ezri.voidshell.Monitor.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE
|
||||||
|
node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||||
|
<node>
|
||||||
|
<interface name="dev.ezri.voidshell.Monitor">
|
||||||
|
<!-- Compositor-supplied values -->
|
||||||
|
<property name="name" type="s" access="read" />
|
||||||
|
<!-- EDID values -->
|
||||||
|
<property name="availableModes" type="a(iiis)" access="read" />
|
||||||
|
<property name="make" type="s" access="read" />
|
||||||
|
<property name="model" type="s" access="read" />
|
||||||
|
<property name="serial" type="s" access="read" />
|
||||||
|
<!-- Configuration values -->
|
||||||
|
<property name="mode" type="(iiis)" access="readwrite" />
|
||||||
|
<property name="active" type="b" access="readwrite" />
|
||||||
|
<property name="powered" type="b" access="readwrite" />
|
||||||
|
<property name="position" type="(ii)" access="readwrite" />
|
||||||
|
<property name="scale" type="d" access="readwrite" />
|
||||||
|
<property name="rect" type="(iiii)" access="read" />
|
||||||
|
<property name="transform" type="s" access="readwrite" />
|
||||||
|
</interface>
|
||||||
|
</node>
|
||||||
16
dbus/dev.ezri.voidshell.Output.xml
Normal file
16
dbus/dev.ezri.voidshell.Output.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE
|
||||||
|
node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||||
|
<node>
|
||||||
|
<interface name="dev.ezri.voidshell.Output">
|
||||||
|
<property name="name" type="s" access="read" />
|
||||||
|
<property name="criteria" type="a{sv}" access="read" />
|
||||||
|
<property name="connected" type="b" access="read" />
|
||||||
|
<property name="matches" type="ao" access="read" />
|
||||||
|
<property name="active" type="o" access="read" />
|
||||||
|
<method name="changeMonitor">
|
||||||
|
<arg type="o" name="monitor" />
|
||||||
|
<arg type="b" direction="out" />
|
||||||
|
</method>
|
||||||
|
</interface>
|
||||||
|
</node>
|
||||||
@ -1,6 +1,13 @@
|
|||||||
import GObject, { register, property, signal } from "astal/gobject";
|
import GObject, { register, property, signal } from "astal/gobject";
|
||||||
import { ConnectionInstantiationError, NotImplementedError } from "./errors";
|
import { ConnectionInstantiationError, NotImplementedError } from "./errors";
|
||||||
import { OutputConfig, VSOutputAdapter, VSWorkspaceAdapter } from "./types";
|
import {
|
||||||
|
OutputAdapter,
|
||||||
|
OutputConfig,
|
||||||
|
VSOutputAdapter,
|
||||||
|
VSOutputEvent,
|
||||||
|
VSWorkspaceAdapter,
|
||||||
|
VSWorkspaceEvent,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mid-level abstract compositor connection providing raw data about the compositor to the rest of VoidShell.
|
* Mid-level abstract compositor connection providing raw data about the compositor to the rest of VoidShell.
|
||||||
@ -91,6 +98,14 @@ export class CompositorConnection extends GObject.Object {
|
|||||||
throw new NotImplementedError();
|
throw new NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of all workspaces
|
||||||
|
* @returns a list of VSWorkspaceAdapter objects representing the workspaces presently known to the compositor
|
||||||
|
*/
|
||||||
|
async getWorkspaces(): Promise<VSWorkspaceAdapter[]> {
|
||||||
|
throw new NotImplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable an output
|
* Disable an output
|
||||||
* @param outputName The name of the output to disable
|
* @param outputName The name of the output to disable
|
||||||
@ -99,18 +114,28 @@ export class CompositorConnection extends GObject.Object {
|
|||||||
throw new NotImplementedError();
|
throw new NotImplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an output property without fully reconfiguring the output
|
||||||
|
*/
|
||||||
|
async setOutputProperty<Prop extends string & keyof OutputConfig>(
|
||||||
|
property: Prop,
|
||||||
|
value: Exclude<OutputConfig[Prop], undefined>,
|
||||||
|
) {
|
||||||
|
throw new NotImplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when a workspace's data changes
|
* Emitted when a workspace's data changes
|
||||||
*/
|
*/
|
||||||
@signal(VSWorkspaceAdapter)
|
@signal(VSWorkspaceEvent)
|
||||||
declare workspaceChange: (event: VSWorkspaceAdapter) => void;
|
declare workspaceChange: (event: VSWorkspaceEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when an output's data changes
|
* Emitted when an output's data changes
|
||||||
* For example, it is enabled or disabled, or its mode changes
|
* For example, it is enabled or disabled, or its mode changes
|
||||||
*/
|
*/
|
||||||
@signal(VSOutputAdapter)
|
@signal(VSOutputEvent)
|
||||||
declare outputChange: (event: VSOutputAdapter) => void;
|
declare outputChange: (event: VSOutputEvent) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the binding mode changes
|
* Emitted when the binding mode changes
|
||||||
|
|||||||
@ -15,7 +15,14 @@ import * as utils from "@/utils";
|
|||||||
import { Mutex } from "@/synchronization";
|
import { Mutex } from "@/synchronization";
|
||||||
import { CompositorConnection } from "../connection";
|
import { CompositorConnection } from "../connection";
|
||||||
import { ActionError } from "../errors";
|
import { ActionError } from "../errors";
|
||||||
import { OutputConfig, VSOutputAdapter, VSRect } from "../types";
|
import {
|
||||||
|
EventType,
|
||||||
|
OutputConfig,
|
||||||
|
VSOutputAdapter,
|
||||||
|
VSOutputEvent,
|
||||||
|
VSWorkspaceAdapter,
|
||||||
|
VSWorkspaceEvent,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
class SwayCommandError
|
class SwayCommandError
|
||||||
extends ActionError
|
extends ActionError
|
||||||
@ -58,9 +65,121 @@ class SwayCommandError
|
|||||||
export class SwayConnection extends CompositorConnection {
|
export class SwayConnection extends CompositorConnection {
|
||||||
private ipc: SwayIPC;
|
private ipc: SwayIPC;
|
||||||
|
|
||||||
|
private lastOutputs = new Map<string, VSOutputAdapter>();
|
||||||
|
private lastWorkspaces = new Map<string, VSWorkspaceAdapter>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.ipc = SwayIPC.instance;
|
this.ipc = SwayIPC.instance;
|
||||||
|
this.onWorkspaceEvent();
|
||||||
|
this.onOutputEvent();
|
||||||
|
this.ipc.connect("workspace", () => {
|
||||||
|
this.onWorkspaceEvent().catch(console.error);
|
||||||
|
});
|
||||||
|
this.ipc.connect("output", () => {
|
||||||
|
this.onOutputEvent().catch(console.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call when we receive a workspace event from Sway
|
||||||
|
*/
|
||||||
|
private async onWorkspaceEvent() {
|
||||||
|
const workspaces = await this.getWorkspaces();
|
||||||
|
// Emit new events for all new outputs. No need to check set, this is the first emission.
|
||||||
|
workspaces
|
||||||
|
.filter((workspace) => !this.lastWorkspaces.has(workspace.id))
|
||||||
|
.forEach((workspace) => {
|
||||||
|
this.workspaceChange(
|
||||||
|
new VSWorkspaceEvent({
|
||||||
|
type: EventType.CREATE,
|
||||||
|
workspace: workspace.id,
|
||||||
|
adapter: workspace,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Emit changed events for all outputs that have changed. No need to check set, as this and previous loops are guaranteed disjoint.
|
||||||
|
workspaces
|
||||||
|
.filter((workspace) => this.lastWorkspaces.has(workspace.id))
|
||||||
|
.filter(
|
||||||
|
(workspace) => !workspace.equals(this.lastWorkspaces.get(workspace.id)),
|
||||||
|
)
|
||||||
|
.forEach((workspace) => {
|
||||||
|
this.workspaceChange(
|
||||||
|
new VSWorkspaceEvent({
|
||||||
|
type: EventType.CHANGE,
|
||||||
|
workspace: workspace.id,
|
||||||
|
adapter: workspace,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Emit destroy events for all outputs that do not now exist.
|
||||||
|
const workspaceNames = new Set(workspaces.map((workspace) => workspace.id));
|
||||||
|
Array.from(this.lastWorkspaces.values())
|
||||||
|
.filter((workspace) => !workspaceNames.has(workspace.id))
|
||||||
|
.forEach((workspace) => {
|
||||||
|
this.workspaceChange(
|
||||||
|
new VSWorkspaceEvent({
|
||||||
|
type: EventType.DESTROY,
|
||||||
|
workspace: workspace.id,
|
||||||
|
adapter: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.lastWorkspaces = new Map();
|
||||||
|
workspaces.forEach((workspace) =>
|
||||||
|
this.lastWorkspaces.set(workspace.id, workspace),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call when we receive an output event from Sway
|
||||||
|
*
|
||||||
|
* NOTE: Currently, Sway does not provide any useful information with this event, so we need to get all outputs
|
||||||
|
* ourselves and determine what changed.
|
||||||
|
*/
|
||||||
|
private async onOutputEvent() {
|
||||||
|
const outputs = await this.getOutputs();
|
||||||
|
// Emit new events for all new outputs. No need to check set, this is the first emission.
|
||||||
|
outputs
|
||||||
|
.filter((output) => !this.lastOutputs.has(output.name))
|
||||||
|
.forEach((output) => {
|
||||||
|
this.outputChange(
|
||||||
|
new VSOutputEvent({
|
||||||
|
type: EventType.CREATE,
|
||||||
|
output: output.name,
|
||||||
|
adapter: output,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Emit changed events for all outputs that have changed. No need to check set, as this and previous loops are guaranteed disjoint.
|
||||||
|
outputs
|
||||||
|
.filter((output) => this.lastOutputs.has(output.name))
|
||||||
|
.filter((output) => !output.equals(this.lastOutputs.get(output.name)))
|
||||||
|
.forEach((output) => {
|
||||||
|
this.outputChange(
|
||||||
|
new VSOutputEvent({
|
||||||
|
type: EventType.CHANGE,
|
||||||
|
output: output.name,
|
||||||
|
adapter: output,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// Emit destroy events for all outputs that do not now exist.
|
||||||
|
const outputNames = new Set(outputs.map((output) => output.name));
|
||||||
|
Array.from(this.lastOutputs.values())
|
||||||
|
.filter((output) => !outputNames.has(output.name))
|
||||||
|
.forEach((output) => {
|
||||||
|
this.outputChange(
|
||||||
|
new VSOutputEvent({
|
||||||
|
type: EventType.DESTROY,
|
||||||
|
output: output.name,
|
||||||
|
adapter: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.lastOutputs = new Map();
|
||||||
|
outputs.forEach((output) => this.lastOutputs.set(output.name, output));
|
||||||
}
|
}
|
||||||
|
|
||||||
async focusWorkspace(workspaceId: string) {
|
async focusWorkspace(workspaceId: string) {
|
||||||
@ -102,21 +221,89 @@ export class SwayConnection extends CompositorConnection {
|
|||||||
return SwayCommandError.assertCommands(`output ${outputName} disable`);
|
return SwayCommandError.assertCommands(`output ${outputName} disable`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setOutputProperty<Prop extends string & keyof OutputConfig>(
|
||||||
|
output: string,
|
||||||
|
property: Prop,
|
||||||
|
value: Exclude<OutputConfig[Prop], undefined>,
|
||||||
|
) {
|
||||||
|
switch (property) {
|
||||||
|
case "modeline":
|
||||||
|
return SwayCommandError.assertCommands(
|
||||||
|
`output ${output} mode ${value}`,
|
||||||
|
);
|
||||||
|
case "scale":
|
||||||
|
return SwayCommandError.assertCommands(
|
||||||
|
`output ${output} scale ${value}`,
|
||||||
|
);
|
||||||
|
case "position":
|
||||||
|
return SwayCommandError.assertCommands(
|
||||||
|
`output ${output} position ${(value as number[])[0]} ${(value as number[])[1]}`,
|
||||||
|
);
|
||||||
|
case "active":
|
||||||
|
if (value) {
|
||||||
|
return SwayCommandError.assertCommands(`output ${output} enable`);
|
||||||
|
} else {
|
||||||
|
return SwayCommandError.assertCommands(`output ${output} disable`);
|
||||||
|
}
|
||||||
|
case "powered":
|
||||||
|
return SwayCommandError.assertCommands(
|
||||||
|
`output ${output} power ${value ? "on" : "off"}`,
|
||||||
|
);
|
||||||
|
case "transform":
|
||||||
|
return SwayCommandError.assertCommands(
|
||||||
|
`output ${output} transform ${value}`,
|
||||||
|
);
|
||||||
|
case "colorProfile":
|
||||||
|
return SwayCommandError.assertCommands(
|
||||||
|
`output ${output} color_profile ${value}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSwayOutput(output: OutputResponse): VSOutputAdapter {
|
||||||
|
return new VSOutputAdapter({
|
||||||
|
name: output.name,
|
||||||
|
active: output.active,
|
||||||
|
powered: output.power,
|
||||||
|
rect: output.rect,
|
||||||
|
scale: output.scale,
|
||||||
|
transform: output.transform,
|
||||||
|
current_workspace: output.current_workspace,
|
||||||
|
make: output.make,
|
||||||
|
model: output.model,
|
||||||
|
serial: output.serial,
|
||||||
|
availableModes: output.modes.map(({ picture_aspect_ratio, ...mode }) => ({
|
||||||
|
aspect_ratio: picture_aspect_ratio,
|
||||||
|
...mode,
|
||||||
|
})),
|
||||||
|
mode: {
|
||||||
|
height: output.current_mode.height,
|
||||||
|
width: output.current_mode.width,
|
||||||
|
refresh: output.current_mode.refresh,
|
||||||
|
aspect_ratio: output.current_mode.picture_aspect_ratio,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSwayWorkspace(workspace: WorkspaceResponse): VSWorkspaceAdapter {
|
||||||
|
return new VSWorkspaceAdapter({
|
||||||
|
id: workspace.name,
|
||||||
|
output: workspace.output,
|
||||||
|
focused: workspace.focused,
|
||||||
|
visible: workspace.visible,
|
||||||
|
urgent: workspace.urgent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getOutputs(): Promise<VSOutputAdapter[]> {
|
async getOutputs(): Promise<VSOutputAdapter[]> {
|
||||||
const result = await this.ipc.getOutputs();
|
return (await this.ipc.getOutputs()).map((swayOutput) =>
|
||||||
return result.map(
|
this.parseSwayOutput(swayOutput),
|
||||||
(swayOutput) =>
|
);
|
||||||
new VSOutputAdapter({
|
}
|
||||||
name: swayOutput.name,
|
|
||||||
active: swayOutput.active,
|
async getWorkspaces(): Promise<VSWorkspaceAdapter[]> {
|
||||||
rect: new VSRect(swayOutput.rect),
|
return (await this.ipc.getWorkspaces()).map((swayWorkspace) =>
|
||||||
scale: swayOutput.scale,
|
this.parseSwayWorkspace(swayWorkspace),
|
||||||
transform: swayOutput.transform,
|
|
||||||
current_workspace: swayOutput.current_workspace,
|
|
||||||
make: swayOutput.make,
|
|
||||||
model: swayOutput.model,
|
|
||||||
serial: swayOutput.serial,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,11 +366,13 @@ class SwayIPC extends GObject.Object {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#connected = false;
|
#connected = false;
|
||||||
|
#onConnected: Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a command on sway
|
* Run a command on sway
|
||||||
*/
|
*/
|
||||||
public async command(command: string): Promise<CommandResponse[]> {
|
public async command(command: string): Promise<CommandResponse[]> {
|
||||||
|
await this.#onConnected;
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
(await this.#sendRaw(
|
(await this.#sendRaw(
|
||||||
this.#commandSocket,
|
this.#commandSocket,
|
||||||
@ -197,6 +386,7 @@ class SwayIPC extends GObject.Object {
|
|||||||
* Get the current sway workspaces
|
* Get the current sway workspaces
|
||||||
*/
|
*/
|
||||||
public async getWorkspaces(): Promise<WorkspaceResponse[]> {
|
public async getWorkspaces(): Promise<WorkspaceResponse[]> {
|
||||||
|
await this.#onConnected;
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
(await this.#sendRaw(
|
(await this.#sendRaw(
|
||||||
this.#commandSocket,
|
this.#commandSocket,
|
||||||
@ -209,6 +399,7 @@ class SwayIPC extends GObject.Object {
|
|||||||
* Get the current sway outputs
|
* Get the current sway outputs
|
||||||
*/
|
*/
|
||||||
public async getOutputs(): Promise<OutputResponse[]> {
|
public async getOutputs(): Promise<OutputResponse[]> {
|
||||||
|
await this.#onConnected;
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
(await this.#sendRaw(
|
(await this.#sendRaw(
|
||||||
this.#commandSocket,
|
this.#commandSocket,
|
||||||
@ -221,6 +412,7 @@ class SwayIPC extends GObject.Object {
|
|||||||
* Get the current sway tree
|
* Get the current sway tree
|
||||||
*/
|
*/
|
||||||
public async getTree(): Promise<TreeNode> {
|
public async getTree(): Promise<TreeNode> {
|
||||||
|
await this.#onConnected;
|
||||||
return JSON.parse(
|
return JSON.parse(
|
||||||
(await this.#sendRaw(
|
(await this.#sendRaw(
|
||||||
this.#commandSocket,
|
this.#commandSocket,
|
||||||
@ -231,6 +423,12 @@ class SwayIPC extends GObject.Object {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.#onConnected = new Promise((resolve) => {
|
||||||
|
const id = this.connect("connected", () => {
|
||||||
|
resolve();
|
||||||
|
this.disconnect(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
this.#connect();
|
this.#connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
services/compositor/index.ts
Normal file
2
services/compositor/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { SwayConnection } from "./connections/sway";
|
||||||
|
export { CompositorConnection } from "./connection";
|
||||||
@ -28,15 +28,151 @@ export const enum EventType {
|
|||||||
DESTROY,
|
DESTROY,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Constructor<T> = { new (...args: any[]): T };
|
export type Constructor<T> = { new (...args: any[]): T };
|
||||||
|
|
||||||
|
class WrappingError extends Error {
|
||||||
|
name = "WrappingError";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic wrapper for TypeScript interfaces that are used in GObject signals.
|
||||||
|
* While this can be used directly, it is recommended to subclass it for each
|
||||||
|
* interface to expose the properties as GObject properties.
|
||||||
|
*/
|
||||||
@register({
|
@register({
|
||||||
GTypeName: "VSWrapper",
|
GTypeName: "VSWrapper",
|
||||||
})
|
})
|
||||||
export class VSWrapper<WrappedInterface extends Object> extends GObject.Object {
|
export class VSWrapper<WrappedInterface extends Object> extends GObject.Object {
|
||||||
|
declare static readonly wrappers: {
|
||||||
|
[key: string]: typeof VSWrapper<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare private keys: string[];
|
||||||
|
|
||||||
|
private getWrapperConstructor<Key extends string & keyof WrappedInterface>(
|
||||||
|
key: Key,
|
||||||
|
): Constructor<VSWrapper<any>> {
|
||||||
|
if ("wrappers" in this.constructor) {
|
||||||
|
if (
|
||||||
|
typeof this.constructor.wrappers !== "object" ||
|
||||||
|
this.constructor.wrappers === null
|
||||||
|
) {
|
||||||
|
throw new WrappingError(
|
||||||
|
`Trying to wrap ${key}: ${this.constructor.name}.wrappers is not an object!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!(key in this.constructor.wrappers)) {
|
||||||
|
throw new WrappingError(
|
||||||
|
`Trying to wrap ${key}: ${this.constructor.name}.wrappers.${key} does not exist!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// @ts-ignore yeah, no good way to type this properly :/
|
||||||
|
return this.constructor.wrappers[key];
|
||||||
|
}
|
||||||
|
throw new WrappingError(
|
||||||
|
`Trying to wrap ${key}: ${this.constructor.name}.wrappers does not exist!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(obj: WrappedInterface) {
|
constructor(obj: WrappedInterface) {
|
||||||
super();
|
const keys = (Object.keys(obj) as (keyof typeof obj)[]).filter(
|
||||||
Object.assign(this, obj);
|
(key) => typeof key === "string",
|
||||||
|
);
|
||||||
|
keys
|
||||||
|
.filter((key) => obj[key] === undefined)
|
||||||
|
.forEach((key) => (obj[key] = null as any));
|
||||||
|
const toWrap = new Set<string & keyof typeof obj>();
|
||||||
|
const toConstruct = Object.entries(obj).reduce(
|
||||||
|
(acc, [key, value]) => {
|
||||||
|
if (typeof key !== "string") return acc;
|
||||||
|
if (
|
||||||
|
["string", "number", "boolean"].some(
|
||||||
|
(type) => typeof value === type,
|
||||||
|
) ||
|
||||||
|
value === null ||
|
||||||
|
value instanceof GObject.Object
|
||||||
|
) {
|
||||||
|
acc[key] = value;
|
||||||
|
} else if (value === undefined) {
|
||||||
|
acc[key] = null;
|
||||||
|
} else {
|
||||||
|
toWrap.add(key as string & keyof typeof obj);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string | number | boolean | null | VSWrapper<any>>,
|
||||||
|
);
|
||||||
|
// @ts-ignore
|
||||||
|
super(toConstruct);
|
||||||
|
toWrap.forEach((key) => {
|
||||||
|
const constructor = this.getWrapperConstructor(key);
|
||||||
|
if (obj[key] instanceof Array) {
|
||||||
|
// @ts-ignore
|
||||||
|
this[key] = obj[key].map((val) => new constructor(val));
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
this[key] = new constructor(obj[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(this, "keys", {
|
||||||
|
value: keys,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconstruct the native JS object that doesn't have any GObject add-ons
|
||||||
|
* This is necessary over the spread operator because GJS defines GObject
|
||||||
|
* properties as non-enumerable for some reason.
|
||||||
|
*/
|
||||||
|
public simplify(): WrappedInterface {
|
||||||
|
return this.keys.reduce((acc, key) => {
|
||||||
|
const val = this[key as keyof this];
|
||||||
|
if (val instanceof VSWrapper) {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: val.simplify(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: val,
|
||||||
|
};
|
||||||
|
}, {} as WrappedInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value-based deep equality check
|
||||||
|
* NOTE: this may be expensive depending on the data structure!
|
||||||
|
* NOTE: only deep-checks VSWrapper instances!
|
||||||
|
*/
|
||||||
|
public equals(other: WrappedInterface | null | undefined): boolean {
|
||||||
|
if (other === null || other === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.keys.every((key) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const thisValue = this[key];
|
||||||
|
// @ts-ignore
|
||||||
|
const otherValue = other[key];
|
||||||
|
return this.checkItemEquality(thisValue, otherValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkItemEquality(thisValue: any, otherValue: any): boolean {
|
||||||
|
if (thisValue === otherValue) return true;
|
||||||
|
if (thisValue instanceof Array && otherValue instanceof Array) {
|
||||||
|
return thisValue.every((val, idx) =>
|
||||||
|
this.checkItemEquality(val, otherValue[idx]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (thisValue instanceof VSWrapper && typeof otherValue === "object") {
|
||||||
|
return thisValue.equals(otherValue);
|
||||||
|
}
|
||||||
|
console.log(`items differ: ${thisValue} !== ${otherValue}`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,10 +181,14 @@ export class VSWrapper<WrappedInterface extends Object> extends GObject.Object {
|
|||||||
public static wrap<
|
public static wrap<
|
||||||
WrappedInterface extends Object,
|
WrappedInterface extends Object,
|
||||||
Wrapper extends VSWrapper<WrappedInterface>,
|
Wrapper extends VSWrapper<WrappedInterface>,
|
||||||
>(this: Constructor<Wrapper>, obj: WrappedInterface): Wrapper {
|
>(
|
||||||
|
this: Constructor<Wrapper>,
|
||||||
|
obj: WrappedInterface,
|
||||||
|
): Wrapper & WrappedInterface {
|
||||||
if (obj instanceof this) {
|
if (obj instanceof this) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore The values of WrappedInterface exist on the object, as guaranteed by the Object.assign() call.
|
||||||
return new this(obj);
|
return new this(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,6 +252,39 @@ export class VSWorkspaceAdapter
|
|||||||
declare readonly urgent: boolean;
|
declare readonly urgent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common interface for compositor backends that represents an output mode.
|
||||||
|
*/
|
||||||
|
export interface OutputMode {
|
||||||
|
/**
|
||||||
|
* The width of the output in pixels
|
||||||
|
*/
|
||||||
|
readonly width: number;
|
||||||
|
/**
|
||||||
|
* The height of the output in pixels
|
||||||
|
*/
|
||||||
|
readonly height: number;
|
||||||
|
/**
|
||||||
|
* The refresh rate of the mode in integer millihertz
|
||||||
|
*/
|
||||||
|
readonly refresh: number;
|
||||||
|
readonly aspect_ratio: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@register({
|
||||||
|
GTypeName: "VSOutputMode",
|
||||||
|
})
|
||||||
|
export class VSOutputMode extends VSWrapper<OutputMode> implements OutputMode {
|
||||||
|
@property(Number)
|
||||||
|
declare readonly width: number;
|
||||||
|
@property(Number)
|
||||||
|
declare readonly height: number;
|
||||||
|
@property(Number)
|
||||||
|
declare readonly refresh: number;
|
||||||
|
@property(String)
|
||||||
|
declare readonly aspect_ratio: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common interface for compositor backends that represents the results of an output query.
|
* Common interface for compositor backends that represents the results of an output query.
|
||||||
* Contains only information that VoidShell needs to know.
|
* Contains only information that VoidShell needs to know.
|
||||||
@ -119,14 +292,16 @@ export class VSWorkspaceAdapter
|
|||||||
export interface OutputAdapter {
|
export interface OutputAdapter {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly active: boolean;
|
readonly active: boolean;
|
||||||
|
readonly powered: boolean;
|
||||||
readonly rect: Rect;
|
readonly rect: Rect;
|
||||||
readonly scale: number;
|
readonly scale: number;
|
||||||
readonly transform: OutputTransform;
|
readonly transform: OutputTransform;
|
||||||
readonly focused: boolean;
|
|
||||||
readonly current_workspace: string;
|
readonly current_workspace: string;
|
||||||
readonly make: string;
|
readonly make: string;
|
||||||
readonly model: string;
|
readonly model: string;
|
||||||
readonly serial: string;
|
readonly serial: string;
|
||||||
|
readonly availableModes: OutputMode[];
|
||||||
|
readonly mode: OutputMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@register({
|
@register({
|
||||||
@ -136,18 +311,24 @@ export class VSOutputAdapter
|
|||||||
extends VSWrapper<OutputAdapter>
|
extends VSWrapper<OutputAdapter>
|
||||||
implements OutputAdapter
|
implements OutputAdapter
|
||||||
{
|
{
|
||||||
|
static readonly wrappers = {
|
||||||
|
rect: VSRect,
|
||||||
|
availableModes: VSOutputMode,
|
||||||
|
mode: VSOutputMode,
|
||||||
|
};
|
||||||
|
|
||||||
@property(String)
|
@property(String)
|
||||||
declare readonly name: string;
|
declare readonly name: string;
|
||||||
@property(Boolean)
|
@property(Boolean)
|
||||||
declare readonly active: boolean;
|
declare readonly active: boolean;
|
||||||
|
@property(Boolean)
|
||||||
|
declare readonly powered: boolean;
|
||||||
@property(VSRect)
|
@property(VSRect)
|
||||||
declare readonly rect: VSRect;
|
declare readonly rect: VSRect;
|
||||||
@property(Number)
|
@property(Number)
|
||||||
declare readonly scale: number;
|
declare readonly scale: number;
|
||||||
@property(String)
|
@property(String)
|
||||||
declare readonly transform: OutputTransform;
|
declare readonly transform: OutputTransform;
|
||||||
@property(Boolean)
|
|
||||||
declare readonly focused: boolean;
|
|
||||||
@property(String)
|
@property(String)
|
||||||
declare readonly current_workspace: string;
|
declare readonly current_workspace: string;
|
||||||
@property(String)
|
@property(String)
|
||||||
@ -156,11 +337,10 @@ export class VSOutputAdapter
|
|||||||
declare readonly model: string;
|
declare readonly model: string;
|
||||||
@property(String)
|
@property(String)
|
||||||
declare readonly serial: string;
|
declare readonly serial: string;
|
||||||
|
@property(Object)
|
||||||
constructor(adapter: OutputAdapter) {
|
declare readonly availableModes: VSOutputMode[];
|
||||||
super(adapter);
|
@property(VSOutputMode)
|
||||||
this.rect = VSRect.wrap(adapter.rect);
|
declare readonly mode: VSOutputMode;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -191,19 +371,16 @@ export class VSWorkspaceEvent
|
|||||||
extends VSWrapper<WorkspaceEvent>
|
extends VSWrapper<WorkspaceEvent>
|
||||||
implements WorkspaceEvent
|
implements WorkspaceEvent
|
||||||
{
|
{
|
||||||
@property(String)
|
static readonly wrappers = {
|
||||||
|
adapter: VSWorkspaceAdapter,
|
||||||
|
};
|
||||||
|
|
||||||
|
@property(Number)
|
||||||
declare readonly type: EventType;
|
declare readonly type: EventType;
|
||||||
@property(String)
|
@property(String)
|
||||||
declare readonly workspace: string;
|
declare readonly workspace: string;
|
||||||
@property(VSWorkspaceAdapter)
|
@property(VSWorkspaceAdapter)
|
||||||
declare readonly adapter: VSWorkspaceAdapter | null;
|
declare readonly adapter: VSWorkspaceAdapter | null;
|
||||||
|
|
||||||
constructor(event: WorkspaceEvent) {
|
|
||||||
super(event);
|
|
||||||
this.adapter = event.adapter
|
|
||||||
? VSWorkspaceAdapter.wrap(event.adapter)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -234,27 +411,24 @@ export class VSOutputEvent
|
|||||||
extends VSWrapper<OutputEvent>
|
extends VSWrapper<OutputEvent>
|
||||||
implements OutputEvent
|
implements OutputEvent
|
||||||
{
|
{
|
||||||
@property(String)
|
static readonly wrappers = {
|
||||||
|
adapter: VSOutputAdapter,
|
||||||
|
};
|
||||||
|
|
||||||
|
@property(Number)
|
||||||
declare readonly type: EventType;
|
declare readonly type: EventType;
|
||||||
@property(String)
|
@property(String)
|
||||||
declare readonly output: string;
|
declare readonly output: string;
|
||||||
@property(VSOutputAdapter)
|
@property(VSOutputAdapter)
|
||||||
declare readonly adapter: VSOutputAdapter | null;
|
declare readonly adapter: VSOutputAdapter | null;
|
||||||
|
|
||||||
constructor(event: OutputEvent) {
|
|
||||||
super(event);
|
|
||||||
this.adapter = event.adapter ? VSOutputAdapter.wrap(event.adapter) : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OutputConfig = {
|
export type OutputConfig = {
|
||||||
modeline: string;
|
modeline: `${number}x${number}${`@${number}Hz` | `@${string}.${string}Hz` | ""}`;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
position: [x: number, y: number];
|
position: [x: number, y: number];
|
||||||
} & {
|
active?: boolean;
|
||||||
/**
|
colorProfile?: string;
|
||||||
* Extra options added to the configuration line. Support for these is compositor-dependent, and currently
|
powered?: boolean;
|
||||||
* they will only be honored under Sway.
|
transform: OutputTransform;
|
||||||
*/
|
|
||||||
[extra_option: string]: string | number | boolean;
|
|
||||||
};
|
};
|
||||||
|
|||||||
111
services/dbus.ts
111
services/dbus.ts
@ -1,6 +1,7 @@
|
|||||||
import Gio from "gi://Gio";
|
import Gio from "gi://Gio";
|
||||||
import GObject, { register } from "astal/gobject";
|
import GObject, { register } from "astal/gobject";
|
||||||
import { getDbusXml } from "@/utils";
|
import { getDbusXml } from "@/utils";
|
||||||
|
import GLib from "gi://GLib";
|
||||||
|
|
||||||
let dbusConnectionResolve: ((connection: any) => void) | null = null;
|
let dbusConnectionResolve: ((connection: any) => void) | null = null;
|
||||||
export const Connection: Promise<any> = new Promise<any>(
|
export const Connection: Promise<any> = new Promise<any>(
|
||||||
@ -28,7 +29,7 @@ export class DBusObject extends GObject.Object {
|
|||||||
this.#objectPath = objectPath;
|
this.#objectPath = objectPath;
|
||||||
getDbusXml(iface).then(async (xml) => {
|
getDbusXml(iface).then(async (xml) => {
|
||||||
try {
|
try {
|
||||||
this.dbusObj = Gio.DBusExportedObject.wrapJSObject(xml, this);
|
this.dbusObj = Gio.DBusExportedObject.wrapJSObject(xml, this.proxify());
|
||||||
this.dbusObj.export(await Connection, objectPath);
|
this.dbusObj.export(await Connection, objectPath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error exporting to D-Bus: ${e}`);
|
console.error(`Error exporting to D-Bus: ${e}`);
|
||||||
@ -36,6 +37,114 @@ export class DBusObject extends GObject.Object {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a proxy of the dbus object that returns object paths of DBusObjects
|
||||||
|
* rather than the objects themselves.
|
||||||
|
* This proxy should be used with the DBus export, and should not be used in any other context.
|
||||||
|
*
|
||||||
|
* Due to the way this works, setting DBus object paths is NOT SUPPORTED! Use a method if you really need to do that.
|
||||||
|
*/
|
||||||
|
private proxify() {
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, property: keyof DBusObject) {
|
||||||
|
const marshall_func = `marshall_${String(property)}` as string &
|
||||||
|
keyof typeof target;
|
||||||
|
if (
|
||||||
|
marshall_func in target &&
|
||||||
|
typeof target[marshall_func] === "function"
|
||||||
|
) {
|
||||||
|
const variant: GLib.Variant = target[marshall_func]();
|
||||||
|
return variant.deepUnpack();
|
||||||
|
}
|
||||||
|
if (!(property in target)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (target[property] instanceof DBusObject) {
|
||||||
|
return target[property].objectPath;
|
||||||
|
}
|
||||||
|
if (target[property] instanceof Array) {
|
||||||
|
return target[property].map((prop) =>
|
||||||
|
prop instanceof DBusObject ? prop.objectPath : prop,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return target[property];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private marshallValue(
|
||||||
|
property: string & keyof this,
|
||||||
|
value: any,
|
||||||
|
): GLib.Variant | null {
|
||||||
|
const marshall_func_name = `marshall_${property}` as string & keyof this;
|
||||||
|
if (
|
||||||
|
marshall_func_name in this &&
|
||||||
|
typeof this[marshall_func_name] === "function"
|
||||||
|
) {
|
||||||
|
console.log(`Calling ${marshall_func_name} to marshall value`);
|
||||||
|
return this[marshall_func_name]();
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Did not find ${marshall_func_name}, falling back to default marshalling`,
|
||||||
|
);
|
||||||
|
switch (typeof value) {
|
||||||
|
case "string":
|
||||||
|
return GLib.Variant.new_string(value);
|
||||||
|
case "number":
|
||||||
|
return GLib.Variant.new_double(value);
|
||||||
|
case "bigint":
|
||||||
|
return GLib.Variant.new_int64(Number(value));
|
||||||
|
case "boolean":
|
||||||
|
return GLib.Variant.new_boolean(value);
|
||||||
|
case "symbol":
|
||||||
|
return null;
|
||||||
|
case "object":
|
||||||
|
if (value instanceof Array) {
|
||||||
|
if (value.every((val) => val instanceof DBusObject)) {
|
||||||
|
return new GLib.Variant(
|
||||||
|
"ao",
|
||||||
|
value.map((val) => val.objectPath),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new GLib.Variant("av", value);
|
||||||
|
} else {
|
||||||
|
return new GLib.Variant("a{sv}", value);
|
||||||
|
}
|
||||||
|
case "undefined":
|
||||||
|
return null;
|
||||||
|
case "function":
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private deepMarshallObject(obj: Object): GLib.Variant {
|
||||||
|
// if (obj instanceof DBusObject) {
|
||||||
|
// return GLib.Variant.new_object_path(obj.objectPath);
|
||||||
|
// }
|
||||||
|
// if (obj instanceof Array) {
|
||||||
|
// return new GLib.Variant(
|
||||||
|
// "av",
|
||||||
|
// obj.map(this.marshallValue.bind(this)).filter((val) => val !== null),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// const newObj = Object.entries(obj).reduce((obj, [key, value]) => {
|
||||||
|
// const toEmit = this.marshallValue(, value);
|
||||||
|
// if (toEmit !== null) {
|
||||||
|
// obj[key] = toEmit;
|
||||||
|
// }
|
||||||
|
// return obj;
|
||||||
|
// }, {} as any);
|
||||||
|
// return new GLib.Variant("a{sv}", newObj);
|
||||||
|
// }
|
||||||
|
|
||||||
|
protected emitDBusProperty<Prop extends keyof this & string>(property: Prop) {
|
||||||
|
const toEmit = this.marshallValue(property, this[property]);
|
||||||
|
if (!toEmit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dbusObj?.emit_property_changed(property, toEmit);
|
||||||
|
}
|
||||||
|
|
||||||
get objectPath(): string {
|
get objectPath(): string {
|
||||||
return this.#objectPath;
|
return this.#objectPath;
|
||||||
}
|
}
|
||||||
|
|||||||
318
services/outputs/monitor.ts
Normal file
318
services/outputs/monitor.ts
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
_active: boolean;
|
||||||
|
_powered: boolean;
|
||||||
|
_rect: Rect;
|
||||||
|
_scale: number;
|
||||||
|
_transform: OutputTransform;
|
||||||
|
|
||||||
|
constructor(adapter: VSOutputAdapter) {
|
||||||
|
super(
|
||||||
|
"dev.ezri.voidshell.Monitor",
|
||||||
|
`/dev/ezri/VoidShell/Monitor/_${VSMonitor._nextId++}`,
|
||||||
|
);
|
||||||
|
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();
|
||||||
|
this._active = adapter.active;
|
||||||
|
this._powered = adapter.powered;
|
||||||
|
this._rect = adapter.rect.simplify();
|
||||||
|
this._scale = adapter.scale;
|
||||||
|
this._transform = adapter.transform;
|
||||||
|
|
||||||
|
const subId = this.connect("destroyed", () => {
|
||||||
|
this.disconnect(subId);
|
||||||
|
this.dbusObj?.unexport();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public sync(adapter: VSOutputAdapter) {
|
||||||
|
// 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();
|
||||||
|
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,
|
||||||
|
this._mode.height,
|
||||||
|
this._mode.refresh,
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when the monitor is "destroyed" (disconnected). Invalidates the object.
|
||||||
|
*/
|
||||||
|
@signal(VSMonitor)
|
||||||
|
declare destroyed: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when any element of the monitor's configuration changes.
|
||||||
|
*/
|
||||||
|
@signal(VSMonitor)
|
||||||
|
declare changed: (monitor: this) => void;
|
||||||
|
}
|
||||||
1
services/outputs/output.ts
Normal file
1
services/outputs/output.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import GObject, { property, register } from "astal/gobject";
|
||||||
67
services/outputs/service.ts
Normal file
67
services/outputs/service.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import GObject, { register, property, signal } from "astal/gobject";
|
||||||
|
import { VSMonitor } from "./monitor";
|
||||||
|
import { VSOutput } from "./output";
|
||||||
|
import { CompositorConnection } from "@services/compositor";
|
||||||
|
import { EventType, VSOutputEvent } from "@services/compositor/types";
|
||||||
|
|
||||||
|
@register({
|
||||||
|
GTypeName: "OutputService",
|
||||||
|
})
|
||||||
|
export class OutputService extends GObject.Object {
|
||||||
|
#monitors: VSMonitor[] = [];
|
||||||
|
#outputs: VSOutput[] = [];
|
||||||
|
|
||||||
|
public get monitors(): readonly VSMonitor[] {
|
||||||
|
return this.#monitors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get outputs(): readonly VSOutput[] {
|
||||||
|
return this.#outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@signal(VSMonitor)
|
||||||
|
declare monitorConnected: (monitor: VSMonitor) => void;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
CompositorConnection.instance.connect(
|
||||||
|
"output-change",
|
||||||
|
(_, event: VSOutputEvent) => {
|
||||||
|
console.log("processing output change event");
|
||||||
|
switch (event.type) {
|
||||||
|
case EventType.CREATE:
|
||||||
|
if (
|
||||||
|
this.#monitors.find(
|
||||||
|
(monitor) => monitor.name === event.output,
|
||||||
|
) === null
|
||||||
|
) {
|
||||||
|
this.#monitors.push(new VSMonitor(event.adapter!));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EventType.CHANGE:
|
||||||
|
const monitor = this.#monitors.find(
|
||||||
|
(monitor) => monitor.name === event.output,
|
||||||
|
);
|
||||||
|
monitor?.sync(event.adapter!);
|
||||||
|
break;
|
||||||
|
case EventType.DESTROY:
|
||||||
|
const idx = this.#monitors.findIndex(
|
||||||
|
(output) => output.name === event.output,
|
||||||
|
);
|
||||||
|
if (idx === -1) break;
|
||||||
|
this.#monitors[idx].destroyed();
|
||||||
|
this.#monitors.splice(idx, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
CompositorConnection.instance
|
||||||
|
.getOutputs()
|
||||||
|
.then((compositorOutputs) => {
|
||||||
|
compositorOutputs.forEach((output) =>
|
||||||
|
this.#monitors.push(new VSMonitor(output)),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user