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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
import { App, Astal, Gtk, Gdk } from "astal/gtk4"
import { bind, exec, GLib, Variable } from "astal"
import AstalTray from "gi://AstalTray?version=0.1";
import AstalWp from "gi://AstalWp?version=0.1";
type NiriWorkspace = {
id: number,
idx: number,
name: string | null,
output: string,
is_active: boolean,
is_focused: boolean,
active_window_id: number | null,
};
// Niri Event Stream
// This is used to dynamically update variables that should not be polling and
// reacting as fast as it can be possible
// NOTE: this works only in non-systemd environment on NixOS
// TODO: I should better write a module for Astal that communicate with socket
const niri = Variable("").watch("niri msg event-stream", (out, _prev) => out);
function getWorkspaces(): NiriWorkspace[] {
// NOTE: this works only in non-systemd environment on NixOS
// TODO: try to use Niri socket if it is documented
return JSON.parse(exec("niri msg -j workspaces"));
}
function getWorkspacesByOutput(output: string): NiriWorkspace[] {
return getWorkspaces().filter(workspace => workspace.output == output).sort((a, b) => a.idx - b.idx);
}
function focusWorkspace(idx: number) {
// NOTE: this works only in non-systemd environment on NixOS
// TODO: try to use Niri socket if it is documented
exec(`niri msg action focus-workspace ${idx}`);
}
type WorkspaceButtonArguments = {
idx: number,
workspaces: Variable<NiriWorkspace[]>,
};
function WorkspaceButton(args: WorkspaceButtonArguments) {
const classes: Variable<string[]> = Variable.derive([args.workspaces], workspaces => {
const classes: string[] = [];
const workspace = workspaces.find(v => v.idx == args.idx);
workspace !== undefined && classes.push("used");
workspace?.is_active && classes.push("active");
workspace?.is_focused && classes.push("focused");
return classes;
});
// TODO: this solution make me to have 9 workspace buttons no matter what
// I'd like to dynamically hide/show some of them, but I guess this will cause rerenders
// Maybe I just need to transform them in CSS
return <button cssClasses={classes()} onClicked={() => focusWorkspace(args.idx)} />
}
type WorkspacesArguments = {
connector: string,
};
function Workspaces(args: WorkspacesArguments) {
const workspaces: Variable<NiriWorkspace[]> = Variable.derive([niri], (_event) => {
return getWorkspacesByOutput(args.connector);
});
const workspaceIndices = [...Array(10).keys()];
return <box cssClasses={["Workspaces"]}>
{workspaceIndices.map(i => <WorkspaceButton idx={i + 1} workspaces={workspaces} />)}
</box>
}
function AudioVolume() {
const wireplumber = AstalWp.get_default()!;
const speaker = wireplumber.audio.get_default_speaker()!;
return <box cssClasses={["AudioVolume"]}>
<image iconName={bind(speaker, "volumeIcon")} />
<slider
hexpand
onScroll={(_self, dx, dy) => speaker.volume = Math.min(1, Math.max(0, speaker.volume + (dx + dy) * -0.05))}
onChangeValue={self => { speaker.volume = self.value; }}
value={bind(speaker, "volume")}
/>
</box>;
}
function Tray() {
// TODO: rewrite this using more elements, as this is really restricted design
const tray = AstalTray.get_default();
return <box cssClasses={["Tray"]}>
{bind(tray, "items").as(items =>
// NOTE: this fixes bug with fantom icon, but I don't think that this is a good idea
items.filter(item => item.gicon !== null).map(item =>
<menubutton
setup={self => self.insert_action_group("dbusmenu", item.actionGroup)}
tooltipText={bind(item, "tooltipMarkup")}
>
<image gicon={bind(item, "gicon")} />
{Gtk.PopoverMenu.new_from_model(item.menuModel)}
</menubutton>
)
)}
</box>
}
type TimeArguments = {
format: string,
};
function Time(args: TimeArguments) {
const time = Variable<GLib.DateTime>(GLib.DateTime.new_now_local()).poll(1000, () => GLib.DateTime.new_now_local())
return <box>
{time(v => v.format(args.format))}
</box>
}
export default function Bar(gdkmonitor: Gdk.Monitor) {
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
return <window
visible
name={"Bar"}
cssClasses={["Bar"]}
gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | LEFT | RIGHT}
application={App}>
<centerbox cssClasses={["bar-container"]}>
<box halign={Gtk.Align.START}>
<Workspaces connector={gdkmonitor.get_connector()!} />
</box>
<box halign={Gtk.Align.CENTER}>
</box>
<box halign={Gtk.Align.END}>
<AudioVolume />
<Tray />
<Time format="%I:%M:%S %p %Z" />
</box>
</centerbox>
</window>
}
|