feat: add functionalities to bar

This is the base of the bar I want to work with. It includes workspaces
for Hyprland, a time/calendar widget, and quick settings including:

- Toggles for wifi, bluetooth and idle inhibition (using matcha as a
dependency)
- Sliders for audio and brightness
This commit is contained in:
Anthony Rodriguez 2025-02-14 19:32:57 +01:00
parent 6f90989dcc
commit 5a918d507a
Signed by: nezia
SSH key fingerprint: SHA256:R/ue1eTzTHUoo77lJD/3fSUsyL4AwvcHImU5BAZai+8
5 changed files with 165 additions and 101 deletions

21
flake.lock generated
View file

@ -42,6 +42,26 @@
"type": "github" "type": "github"
} }
}, },
"matcha": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1738448307,
"narHash": "sha256-jRmfa28gVEDyS7We881BJDpr6L1kla0AgpEWjZ04tZo=",
"ref": "refs/heads/main",
"rev": "90721110060e6b870839df847601d79f9a7a6461",
"revCount": 28,
"type": "git",
"url": "https://codeberg.org/QuincePie/matcha"
},
"original": {
"type": "git",
"url": "https://codeberg.org/QuincePie/matcha"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1739020877, "lastModified": 1739020877,
@ -61,6 +81,7 @@
"root": { "root": {
"inputs": { "inputs": {
"ags": "ags", "ags": "ags",
"matcha": "matcha",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
} }

View file

@ -8,17 +8,24 @@
url = "github:aylur/ags"; url = "github:aylur/ags";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
matcha = {
url = "git+https://codeberg.org/QuincePie/matcha";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { outputs = {
self, self,
nixpkgs, nixpkgs,
ags, ags,
matcha,
}: let }: let
system = "x86_64-linux"; system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
astalPkgs = with ags.packages.${system}; [ astalPkgs = with ags.packages.${system}; [
battery battery
bluetooth
hyprland hyprland
mpris mpris
network network
@ -47,6 +54,7 @@
extraPackages = astalPkgs; extraPackages = astalPkgs;
}) })
pkgs.brightnessctl pkgs.brightnessctl
matcha.packages.${pkgs.system}.default
]; ];
}; };
}; };

View file

@ -34,6 +34,13 @@ $accent: #{"@accent_color"};
} }
} }
.Time {
calendar {
label:selected {
background-color: $accent;
}
}
}
.Battery label { .Battery label {
margin-left: 0; margin-left: 0;
} }
@ -42,11 +49,29 @@ $accent: #{"@accent_color"};
image { image {
-gtk-icon-size: 1.2em; -gtk-icon-size: 1.2em;
} }
.Audio,
menubutton box { .Wifi {
margin-right: 0.5em; margin-right: 0.5em;
&:last-child { }
margin-right: 0;
popover {
min-width: 350px;
.Toggles {
margin: 0.5em 0;
button image {
-gtk-icon-size: 3em;
}
}
trough highlight {
background-color: $accent;
border-radius: 2em;
}
slider {
min-height: 1.7em;
min-width: 1.7em;
margin: -0.2em;
} }
} }
} }
@ -59,6 +84,6 @@ $accent: #{"@accent_color"};
} }
popover { popover {
margin-top: 0.5em; margin-top: 0.8em;
} }
} }

View file

@ -1,14 +1,18 @@
import { Astal, Gtk, Gdk } from "astal/gtk4"; import { Astal, Gtk, Gdk } from "astal/gtk4";
import { Variable, GLib, bind } from "astal"; import { Variable, GLib, bind, execAsync, exec } from "astal";
import Battery from "gi://AstalBattery"; import Battery from "gi://AstalBattery";
import Bluetooth from "gi://AstalBluetooth";
import Wp from "gi://AstalWp"; import Wp from "gi://AstalWp";
import Network from "gi://AstalNetwork"; import Network from "gi://AstalNetwork";
import Hyprland from "gi://AstalHyprland"; import Hyprland from "gi://AstalHyprland";
import Brightness from "../service/brightness";
const network = Network.get_default();
const wifi = bind(network, "wifi");
const hypr = Hyprland.get_default();
const bluetooth = Bluetooth.get_default();
function Wifi() { function Wifi() {
const network = Network.get_default();
const wifi = bind(network, "wifi");
return ( return (
<box visible={wifi.as(Boolean)}> <box visible={wifi.as(Boolean)}>
{wifi.as( {wifi.as(
@ -31,6 +35,42 @@ function Audio() {
); );
} }
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() { function BatteryLevel() {
const bat = Battery.get_default(); const bat = Battery.get_default();
@ -62,6 +102,29 @@ function Time({ format = "%H:%M - %A %e." }) {
); );
} }
function IdleInhibitor() {
const icon = Variable(getIcon());
function getIcon() {
const state = exec("matcha --status");
const enabled = state.match(/on/g);
return enabled ? "my-caffeine-on-symbolic" : "my-caffeine-off-symbolic";
}
function toggle() {
exec("matcha --toggle");
icon.set(getIcon());
}
return (
<box cssName="IdleInhibitor">
<button
onClicked={() => toggle()}
iconName={bind(icon).as((iconName) => iconName)}
/>
</box>
);
}
function QuickSettings() { function QuickSettings() {
return ( return (
<box cssClasses={["QuickSettings"]}> <box cssClasses={["QuickSettings"]}>
@ -71,15 +134,50 @@ function QuickSettings() {
<Wifi /> <Wifi />
<BatteryLevel /> <BatteryLevel />
</box> </box>
<popover hasArrow={false}></popover> <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> </menubutton>
</box> </box>
); );
} }
function Workspaces() { function Workspaces() {
const hypr = Hyprland.get_default();
return ( return (
<box cssClasses={["Workspaces"]}> <box cssClasses={["Workspaces"]}>
{bind(hypr, "workspaces").as((wss) => {bind(hypr, "workspaces").as((wss) =>
@ -118,7 +216,7 @@ export default function Bar(monitor: Gdk.Monitor) {
<box halign={Gtk.Align.CENTER}> <box halign={Gtk.Align.CENTER}>
<Time /> <Time />
</box> </box>
<box hexpand halign={Gtk.Align.END}> <box halign={Gtk.Align.END}>
<QuickSettings /> <QuickSettings />
</box> </box>
</centerbox> </centerbox>

View file

@ -1,88 +0,0 @@
import { Astal, Gdk, Gtk, Widget } from "astal/gtk4";
const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor;
type PopoverProps = Pick<
Widget.WindowProps,
| "name"
| "namespace"
| "className"
| "visible"
| "child"
| "marginBottom"
| "marginTop"
| "marginLeft"
| "marginRight"
| "halign"
| "valign"
> & {
onClose?(self): void;
};
/**
* Full screen window widget where you can space the child widget
* using margins and alignment properties.
*
* NOTE: Child widgets will assume they can span across the full window width
* this means that setting `wrap` or `ellipsize` on labels for example will not work
* without explicitly setting its `max_width_chars` property.
* For a workaround see Popover2.tsx
*/
export default function Popover({
child,
marginBottom,
marginTop,
marginLeft,
marginRight,
halign = Gtk.Align.CENTER,
valign = Gtk.Align.CENTER,
onClose,
...props
}: PopoverProps) {
return (
<window
{...props}
css="background-color: transparent"
keymode={Astal.Keymode.EXCLUSIVE}
anchor={TOP | BOTTOM | LEFT | RIGHT}
exclusivity={Astal.Exclusivity.IGNORE}
onNotifyVisible={(self) => {
if (!self.visible) onClose?.(self);
}}
// close when click occurs otside of child
onButtonPressEvent={(self, event) => {
const [, _x, _y] = event.get_coords();
const { x, y, width, height } = self.get_child()!.get_allocation();
const xOut = _x < x || _x > x + width;
const yOut = _y < y || _y > y + height;
// clicked outside
if (xOut || yOut) {
self.visible = false;
}
}}
// close when hitting Escape
onKeyPressEvent={(self, event: Gdk.Event) => {
if (event.get_keyval()[1] === Gdk.KEY_Escape) {
self.visible = false;
}
}}
>
<box
// make sure click event does not bubble up
onButtonPressEvent={() => true}
// child can be positioned with `halign` `valign` and margins
expand
halign={halign}
valign={valign}
marginBottom={marginBottom}
marginTop={marginTop}
marginStart={marginLeft}
marginEnd={marginRight}
>
{child}
</box>
</window>
);
}