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:
Anthony Rodriguez 2025-02-16 13:38:16 +01:00
parent eb7c276914
commit 0bb9279c61
Signed by: nezia
SSH key fingerprint: SHA256:R/ue1eTzTHUoo77lJD/3fSUsyL4AwvcHImU5BAZai+8
14 changed files with 272 additions and 245 deletions

2
app.ts
View file

@ -1,6 +1,6 @@
import { App } from "astal/gtk4";
import style from "./style.scss";
import Bar from "@/widget/Bar";
import Bar from "@/components/Bar";
App.start({
css: style,

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

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

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

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

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

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

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

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

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

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

View file

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

View file

@ -1,2 +0,0 @@
import { bind } from "astal";
import Hyprland from "gi://AstalHyprland";