chore!: split-up in components and widgets
The project has been split up in "components" and "widgets". Here's what's meant by both: - Components: big building blocks to coquille shell (i.e. the bar itself, the app launcher...) - Widgets: smaller building blocks, contained in components and composing them (i.e. workspaces widget, wifi status...)
This commit is contained in:
parent
eb7c276914
commit
0bb9279c61
14 changed files with 272 additions and 245 deletions
2
app.ts
2
app.ts
|
@ -1,6 +1,6 @@
|
||||||
import { App } from "astal/gtk4";
|
import { App } from "astal/gtk4";
|
||||||
import style from "./style.scss";
|
import style from "./style.scss";
|
||||||
import Bar from "@/widget/Bar";
|
import Bar from "@/components/Bar";
|
||||||
|
|
||||||
App.start({
|
App.start({
|
||||||
css: style,
|
css: style,
|
||||||
|
|
31
src/components/Bar/index.tsx
Normal file
31
src/components/Bar/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Astal, Gtk, Gdk } from "astal/gtk4";
|
||||||
|
import QuickSettings from "@/components/QuickSettings";
|
||||||
|
import { Workspaces } from "./widgets/workspaces";
|
||||||
|
import { Time } from "./widgets/time";
|
||||||
|
|
||||||
|
export default function Bar(monitor: Gdk.Monitor) {
|
||||||
|
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<window
|
||||||
|
visible
|
||||||
|
namespace={"bar"}
|
||||||
|
cssClasses={["Bar"]}
|
||||||
|
gdkmonitor={monitor}
|
||||||
|
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||||
|
anchor={TOP | LEFT | RIGHT}
|
||||||
|
>
|
||||||
|
<centerbox shrinkCenterLast>
|
||||||
|
<box hexpand halign={Gtk.Align.START}>
|
||||||
|
<Workspaces />
|
||||||
|
</box>
|
||||||
|
<box halign={Gtk.Align.CENTER}>
|
||||||
|
<Time />
|
||||||
|
</box>
|
||||||
|
<box halign={Gtk.Align.END}>
|
||||||
|
<QuickSettings />
|
||||||
|
</box>
|
||||||
|
</centerbox>
|
||||||
|
</window>
|
||||||
|
);
|
||||||
|
}
|
20
src/components/Bar/widgets/time.tsx
Normal file
20
src/components/Bar/widgets/time.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { Variable, GLib } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
|
||||||
|
export function Time({ format = "%H:%M - %A %e." }) {
|
||||||
|
const time = Variable<string>("").poll(
|
||||||
|
1000,
|
||||||
|
() => GLib.DateTime.new_now_local().format(format)!,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box cssClasses={["Time"]}>
|
||||||
|
<menubutton>
|
||||||
|
<label label={time()} />
|
||||||
|
<popover hasArrow={false}>
|
||||||
|
<Gtk.Calendar />
|
||||||
|
</popover>
|
||||||
|
</menubutton>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
25
src/components/Bar/widgets/workspaces.tsx
Normal file
25
src/components/Bar/widgets/workspaces.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { bind } from "astal";
|
||||||
|
import Hyprland from "gi://AstalHyprland";
|
||||||
|
import Gtk from "gi://Gtk";
|
||||||
|
|
||||||
|
export function Workspaces() {
|
||||||
|
const hypr = Hyprland.get_default();
|
||||||
|
return (
|
||||||
|
<box cssClasses={["Workspaces"]}>
|
||||||
|
{bind(hypr, "workspaces").as((wss) =>
|
||||||
|
wss
|
||||||
|
.filter((ws) => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
|
.map((ws) => (
|
||||||
|
<button
|
||||||
|
valign={Gtk.Align.CENTER}
|
||||||
|
cssClasses={bind(hypr, "focusedWorkspace").as((fw) =>
|
||||||
|
ws === fw ? ["focused"] : [""],
|
||||||
|
)}
|
||||||
|
onClicked={() => ws.focus()}
|
||||||
|
></button>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
39
src/components/QuickSettings/index.tsx
Normal file
39
src/components/QuickSettings/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import { WifiStatus, WifiToggle } from "./widgets/wifi";
|
||||||
|
import { AudioStatus, AudioSlider } from "./widgets/audio";
|
||||||
|
import { BatteryStatus } from "./widgets/battery";
|
||||||
|
import { IdleInhibitor } from "./widgets/idle";
|
||||||
|
import { BluetoothToggle } from "./widgets/bluetooth";
|
||||||
|
import { BrightnessSlider } from "./widgets/brightness";
|
||||||
|
|
||||||
|
export default function QuickSettings() {
|
||||||
|
return (
|
||||||
|
<box cssClasses={["QuickSettings"]}>
|
||||||
|
<menubutton>
|
||||||
|
<box>
|
||||||
|
<AudioStatus />
|
||||||
|
<WifiStatus />
|
||||||
|
<BatteryStatus />
|
||||||
|
</box>
|
||||||
|
<popover hasArrow={false}>
|
||||||
|
<box vertical>
|
||||||
|
<box
|
||||||
|
cssClasses={["Toggles"]}
|
||||||
|
spacing={30}
|
||||||
|
halign={Gtk.Align.CENTER}
|
||||||
|
hexpand
|
||||||
|
>
|
||||||
|
<WifiToggle />
|
||||||
|
<BluetoothToggle />
|
||||||
|
<IdleInhibitor />
|
||||||
|
</box>
|
||||||
|
<box vertical>
|
||||||
|
<AudioSlider />
|
||||||
|
<BrightnessSlider />
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</popover>
|
||||||
|
</menubutton>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
28
src/components/QuickSettings/widgets/audio.tsx
Normal file
28
src/components/QuickSettings/widgets/audio.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { bind } from "astal";
|
||||||
|
import Wp from "gi://AstalWp";
|
||||||
|
|
||||||
|
const speaker = Wp.get_default()?.audio.defaultSpeaker!;
|
||||||
|
|
||||||
|
export function AudioStatus() {
|
||||||
|
return (
|
||||||
|
<box cssClasses={["Audio"]}>
|
||||||
|
<image iconName={bind(speaker, "volumeIcon")} />
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AudioSlider() {
|
||||||
|
return (
|
||||||
|
<box cssClasses={["AudioSlider"]}>
|
||||||
|
<image iconName={bind(speaker, "volumeIcon")} />
|
||||||
|
<slider
|
||||||
|
hexpand
|
||||||
|
widthRequest={100}
|
||||||
|
onChangeValue={({ value }) => {
|
||||||
|
speaker.volume = value;
|
||||||
|
}}
|
||||||
|
value={bind(speaker, "volume")}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
15
src/components/QuickSettings/widgets/battery.tsx
Normal file
15
src/components/QuickSettings/widgets/battery.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { bind } from "astal";
|
||||||
|
import Battery from "gi://AstalBattery";
|
||||||
|
|
||||||
|
export function BatteryStatus() {
|
||||||
|
const bat = Battery.get_default();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box cssClasses={["Battery"]} visible={bind(bat, "isPresent")}>
|
||||||
|
<image iconName={bind(bat, "batteryIconName")} />
|
||||||
|
<label
|
||||||
|
label={bind(bat, "percentage").as((p) => `${Math.floor(p * 100)} %`)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
18
src/components/QuickSettings/widgets/bluetooth.tsx
Normal file
18
src/components/QuickSettings/widgets/bluetooth.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import { bind } from "astal";
|
||||||
|
import Bluetooth from "gi://AstalBluetooth";
|
||||||
|
|
||||||
|
const bluetooth = Bluetooth.get_default();
|
||||||
|
|
||||||
|
export function BluetoothToggle() {
|
||||||
|
return (
|
||||||
|
<box visible={bluetooth.adapter != null} halign={Gtk.Align.CENTER}>
|
||||||
|
<button
|
||||||
|
onClicked={() => bluetooth.toggle()}
|
||||||
|
iconName={bind(bluetooth, "is_powered").as((p) =>
|
||||||
|
p ? "bluetooth-symbolic" : "bluetooth-disabled-symbolic",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
20
src/components/QuickSettings/widgets/brightness.tsx
Normal file
20
src/components/QuickSettings/widgets/brightness.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { bind } from "astal";
|
||||||
|
import Brightness from "@/services/brightness";
|
||||||
|
|
||||||
|
const brightness = Brightness.get_default();
|
||||||
|
|
||||||
|
export function BrightnessSlider() {
|
||||||
|
return (
|
||||||
|
<box cssClasses={["BrightnessSlider"]}>
|
||||||
|
<image iconName={"display-brightness-symbolic"} />
|
||||||
|
<slider
|
||||||
|
hexpand
|
||||||
|
widthRequest={100}
|
||||||
|
onChangeValue={({ value }) => {
|
||||||
|
brightness.screen = value;
|
||||||
|
}}
|
||||||
|
value={bind(brightness, "screen")}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
41
src/components/QuickSettings/widgets/idle.tsx
Normal file
41
src/components/QuickSettings/widgets/idle.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { Variable, bind, exec } from "astal";
|
||||||
|
import { dependencies } from "@/lib/utils";
|
||||||
|
|
||||||
|
type IdleState = "active" | "inactive" | "unknown";
|
||||||
|
export function IdleInhibitor() {
|
||||||
|
/*
|
||||||
|
* matcha needs additional checking to ensure the daemon is properly running
|
||||||
|
*/
|
||||||
|
function isDaemonRunning() {
|
||||||
|
try {
|
||||||
|
exec("matcha --status");
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dependencies("matcha") || !isDaemonRunning()) return <></>;
|
||||||
|
|
||||||
|
const state = Variable<IdleState>("unknown");
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
exec("matcha --toggle");
|
||||||
|
const response = exec("matcha --status");
|
||||||
|
const enabled = response.match(/on/g);
|
||||||
|
state.set(enabled ? "active" : "inactive");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box cssName="IdleInhibitor">
|
||||||
|
<button
|
||||||
|
onClicked={() => toggle()}
|
||||||
|
iconName={bind(state).as((s) =>
|
||||||
|
s === "active"
|
||||||
|
? "my-caffeine-on-symbolic"
|
||||||
|
: "my-caffeine-off-symbolic",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
34
src/components/QuickSettings/widgets/wifi.tsx
Normal file
34
src/components/QuickSettings/widgets/wifi.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { bind } from "astal";
|
||||||
|
import Network from "gi://AstalNetwork";
|
||||||
|
|
||||||
|
const network = Network.get_default();
|
||||||
|
const wifi = bind(network, "wifi");
|
||||||
|
|
||||||
|
export function WifiStatus() {
|
||||||
|
return (
|
||||||
|
<box visible={wifi.as(Boolean)}>
|
||||||
|
{wifi.as(
|
||||||
|
(wifi) =>
|
||||||
|
wifi && (
|
||||||
|
<image cssClasses={["Wifi"]} iconName={bind(wifi, "iconName")} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WifiToggle() {
|
||||||
|
return (
|
||||||
|
<box visible={wifi.as(Boolean)} cssClasses={["WifiButton"]}>
|
||||||
|
{wifi.as(
|
||||||
|
(wifi) =>
|
||||||
|
wifi && (
|
||||||
|
<button
|
||||||
|
onClicked={() => (wifi.enabled = !wifi.enabled)}
|
||||||
|
iconName={bind(wifi, "iconName")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,242 +0,0 @@
|
||||||
import { Astal, Gtk, Gdk } from "astal/gtk4";
|
|
||||||
import { Variable, GLib, bind, exec } from "astal";
|
|
||||||
import Battery from "gi://AstalBattery";
|
|
||||||
import Bluetooth from "gi://AstalBluetooth";
|
|
||||||
import Wp from "gi://AstalWp";
|
|
||||||
import Network from "gi://AstalNetwork";
|
|
||||||
import Hyprland from "gi://AstalHyprland";
|
|
||||||
import Brightness from "@/service/brightness";
|
|
||||||
import { dependencies } from "@/lib/utils";
|
|
||||||
|
|
||||||
const network = Network.get_default();
|
|
||||||
const wifi = bind(network, "wifi");
|
|
||||||
const hypr = Hyprland.get_default();
|
|
||||||
const bluetooth = Bluetooth.get_default();
|
|
||||||
|
|
||||||
function Wifi() {
|
|
||||||
return (
|
|
||||||
<box visible={wifi.as(Boolean)}>
|
|
||||||
{wifi.as(
|
|
||||||
(wifi) =>
|
|
||||||
wifi && (
|
|
||||||
<image cssClasses={["Wifi"]} iconName={bind(wifi, "iconName")} />
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Audio() {
|
|
||||||
const speaker = Wp.get_default()?.audio.defaultSpeaker!;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box cssClasses={["Audio"]}>
|
|
||||||
<image iconName={bind(speaker, "volumeIcon")} />
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AudioSlider() {
|
|
||||||
const speaker = Wp.get_default()?.audio.defaultSpeaker!;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box cssClasses={["AudioSlider"]}>
|
|
||||||
<image iconName={bind(speaker, "volumeIcon")} />
|
|
||||||
<slider
|
|
||||||
hexpand
|
|
||||||
widthRequest={100}
|
|
||||||
onChangeValue={({ value }) => {
|
|
||||||
speaker.volume = value;
|
|
||||||
}}
|
|
||||||
value={bind(speaker, "volume")}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BrightnessSlider() {
|
|
||||||
const brightness = Brightness.get_default();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box cssClasses={["BrightnessSlider"]}>
|
|
||||||
<image iconName={"display-brightness-symbolic"} />
|
|
||||||
<slider
|
|
||||||
hexpand
|
|
||||||
widthRequest={100}
|
|
||||||
onChangeValue={({ value }) => {
|
|
||||||
brightness.screen = value;
|
|
||||||
}}
|
|
||||||
value={bind(brightness, "screen")}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BatteryLevel() {
|
|
||||||
const bat = Battery.get_default();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box cssClasses={["Battery"]} visible={bind(bat, "isPresent")}>
|
|
||||||
<image iconName={bind(bat, "batteryIconName")} />
|
|
||||||
<label
|
|
||||||
label={bind(bat, "percentage").as((p) => `${Math.floor(p * 100)} %`)}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Time({ format = "%H:%M - %A %e." }) {
|
|
||||||
const time = Variable<string>("").poll(
|
|
||||||
1000,
|
|
||||||
() => GLib.DateTime.new_now_local().format(format)!,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box cssClasses={["Time"]}>
|
|
||||||
<menubutton>
|
|
||||||
<label label={time()} />
|
|
||||||
<popover hasArrow={false}>
|
|
||||||
<Gtk.Calendar />
|
|
||||||
</popover>
|
|
||||||
</menubutton>
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type IdleState = "active" | "inactive" | "unknown";
|
|
||||||
|
|
||||||
function IdleInhibitor() {
|
|
||||||
/*
|
|
||||||
* matcha needs additional checking to ensure the daemon is properly running
|
|
||||||
*/
|
|
||||||
function isDaemonRunning() {
|
|
||||||
try {
|
|
||||||
exec("matcha --status");
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dependencies("matcha") || !isDaemonRunning()) return <></>;
|
|
||||||
|
|
||||||
const state = Variable<IdleState>("unknown");
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
exec("matcha --toggle");
|
|
||||||
const response = exec("matcha --status");
|
|
||||||
const enabled = response.match(/on/g);
|
|
||||||
state.set(enabled ? "active" : "inactive");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<box cssName="IdleInhibitor">
|
|
||||||
<button
|
|
||||||
onClicked={() => toggle()}
|
|
||||||
iconName={bind(state).as((s) =>
|
|
||||||
s === "active"
|
|
||||||
? "my-caffeine-on-symbolic"
|
|
||||||
: "my-caffeine-off-symbolic",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function QuickSettings() {
|
|
||||||
return (
|
|
||||||
<box cssClasses={["QuickSettings"]}>
|
|
||||||
<menubutton>
|
|
||||||
<box>
|
|
||||||
<Audio />
|
|
||||||
<Wifi />
|
|
||||||
<BatteryLevel />
|
|
||||||
</box>
|
|
||||||
<popover hasArrow={false}>
|
|
||||||
<box vertical>
|
|
||||||
<box
|
|
||||||
cssClasses={["Toggles"]}
|
|
||||||
spacing={30}
|
|
||||||
halign={Gtk.Align.CENTER}
|
|
||||||
hexpand
|
|
||||||
>
|
|
||||||
<box visible={wifi.as(Boolean)} cssClasses={["WifiButton"]}>
|
|
||||||
{wifi.as(
|
|
||||||
(wifi) =>
|
|
||||||
wifi && (
|
|
||||||
<button
|
|
||||||
onClicked={() => (wifi.enabled = !wifi.enabled)}
|
|
||||||
iconName={bind(wifi, "iconName")}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</box>
|
|
||||||
<box
|
|
||||||
visible={bluetooth.adapter != null}
|
|
||||||
halign={Gtk.Align.CENTER}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClicked={() => bluetooth.toggle()}
|
|
||||||
iconName={bind(bluetooth, "is_powered").as((p) =>
|
|
||||||
p ? "bluetooth-symbolic" : "bluetooth-disabled-symbolic",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
<IdleInhibitor />
|
|
||||||
</box>
|
|
||||||
<box vertical>
|
|
||||||
<AudioSlider />
|
|
||||||
<BrightnessSlider />
|
|
||||||
</box>
|
|
||||||
</box>
|
|
||||||
</popover>
|
|
||||||
</menubutton>
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Workspaces() {
|
|
||||||
return (
|
|
||||||
<box cssClasses={["Workspaces"]}>
|
|
||||||
{bind(hypr, "workspaces").as((wss) =>
|
|
||||||
wss
|
|
||||||
.filter((ws) => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
|
||||||
.sort((a, b) => a.id - b.id)
|
|
||||||
.map((ws) => (
|
|
||||||
<button
|
|
||||||
valign={Gtk.Align.CENTER}
|
|
||||||
cssClasses={bind(hypr, "focusedWorkspace").as((fw) =>
|
|
||||||
ws === fw ? ["focused"] : [""],
|
|
||||||
)}
|
|
||||||
onClicked={() => ws.focus()}
|
|
||||||
></button>
|
|
||||||
)),
|
|
||||||
)}
|
|
||||||
</box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default function Bar(monitor: Gdk.Monitor) {
|
|
||||||
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<window
|
|
||||||
visible
|
|
||||||
namespace={"bar"}
|
|
||||||
cssClasses={["Bar"]}
|
|
||||||
gdkmonitor={monitor}
|
|
||||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
|
||||||
anchor={TOP | LEFT | RIGHT}
|
|
||||||
>
|
|
||||||
<centerbox shrinkCenterLast>
|
|
||||||
<box hexpand halign={Gtk.Align.START}>
|
|
||||||
<Workspaces />
|
|
||||||
</box>
|
|
||||||
<box halign={Gtk.Align.CENTER}>
|
|
||||||
<Time />
|
|
||||||
</box>
|
|
||||||
<box halign={Gtk.Align.END}>
|
|
||||||
<QuickSettings />
|
|
||||||
</box>
|
|
||||||
</centerbox>
|
|
||||||
</window>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
import { bind } from "astal";
|
|
||||||
import Hyprland from "gi://AstalHyprland";
|
|
Loading…
Add table
Reference in a new issue