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"));
}
}
|