summaryrefslogtreecommitdiff
path: root/src/services/monitors.ts
blob: 78a01616f79cd8330b9079f9c0abd43aba87c535 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import { GObject, execAsync, property, register } from "astal";
import AstalHyprland from "gi://AstalHyprland";

@register({ GTypeName: "Monitor" })
export class Monitor extends GObject.Object {
    readonly monitor: AstalHyprland.Monitor;
    readonly width: number;
    readonly height: number;
    readonly id: number;
    readonly serial: string;
    readonly name: string;
    readonly description: string;

    @property(AstalHyprland.Workspace)
    get activeWorkspace() {
        return this.monitor.activeWorkspace;
    }

    isDdc: boolean = false;
    busNum?: string;

    #brightness: number = 0;

    @property(Number)
    get brightness() {
        return this.#brightness;
    }

    set brightness(value) {
        value = Math.min(1, Math.max(0, value));

        this.#brightness = value;
        this.notify("brightness");
        execAsync(
            this.isDdc
                ? `ddcutil -b ${this.busNum} setvcp 10 ${Math.round(value * 100)}`
                : `brightnessctl set ${Math.floor(value * 100)}% -q`
        ).catch(console.error);
    }

    constructor(monitor: AstalHyprland.Monitor) {
        super();

        this.monitor = monitor;
        this.width = monitor.width;
        this.height = monitor.height;
        this.id = monitor.id;
        this.serial = monitor.serial;
        this.name = monitor.name;
        this.description = monitor.description;

        monitor.connect("notify::active-workspace", () => this.notify("active-workspace"));

        execAsync("ddcutil detect --brief")
            .then(out => {
                this.isDdc = out.split("\n\n").some(display => {
                    if (!/^Display \d+/.test(display)) return false;
                    const lines = display.split("\n");
                    if (lines[3].split(":")[3] !== monitor.serial) return false;
                    this.busNum = lines[1].split("/dev/i2c-")[1];
                    return true;
                });
            })
            .catch(() => (this.isDdc = false))
            .finally(async () => {
                if (this.isDdc) {
                    const info = (await execAsync(`ddcutil -b ${this.busNum} getvcp 10 --brief`)).split(" ");
                    this.#brightness = Number(info[3]) / Number(info[4]);
                } else
                    this.#brightness =
                        Number(await execAsync("brightnessctl get")) / Number(await execAsync("brightnessctl max"));
            });
    }
}

@register({ GTypeName: "Monitors" })
export default class Monitors extends GObject.Object {
    static instance: Monitors;
    static get_default() {
        if (!this.instance) this.instance = new Monitors();

        return this.instance;
    }

    readonly #map: Map<number, Monitor> = new Map();

    @property(Object)
    get map() {
        return this.#map;
    }

    @property(Object)
    get list() {
        return Array.from(this.#map.values());
    }

    @property(Monitor)
    get active() {
        return this.#map.get(AstalHyprland.get_default().focusedMonitor.id)!;
    }

    #notify() {
        this.notify("map");
        this.notify("list");
    }

    forEach(fn: (monitor: Monitor) => void) {
        for (const monitor of this.#map.values()) fn(monitor);
    }

    constructor() {
        super();

        const hyprland = AstalHyprland.get_default();

        for (const monitor of hyprland.monitors) this.#map.set(monitor.id, new Monitor(monitor));
        if (this.#map.size > 0) this.#notify();

        hyprland.connect("monitor-added", (_, monitor) => {
            this.#map.set(monitor.id, new Monitor(monitor));
            this.#notify();
        });
        hyprland.connect("monitor-removed", (_, id) => this.#map.delete(id) && this.#notify());

        hyprland.connect("notify::focused-monitor", () => this.notify("active"));
    }
}