Add battery and notifications to QuickSettings popup

This commit is contained in:
Joshua Yuen 2026-09-12 15:56:03 -04:00
parent 578797498e
commit f6ddf3d4b7
Signed by: josh
GPG Key ID: 530DA0331EFE99F5
5 changed files with 150 additions and 42 deletions

View File

@ -91,8 +91,7 @@ $module-bg: transparentize($base02, 0.4);
color: $base00; color: $base00;
} }
.ws:hover, .ws:hover {
.battery:hover {
background: transparentize($base05, 0.88); background: transparentize($base05, 0.88);
} }
@ -254,14 +253,23 @@ $module-bg: transparentize($base02, 0.4);
background: transparentize($base05, 0.88); background: transparentize($base05, 0.88);
} }
.battery-percent {
font-size: 1.6em;
font-weight: bold;
}
.battery-level { .battery-level {
font-size: 0.85em; font-size: 0.85em;
margin-left: 3px; }
.battery-section {
margin-top: 10px;
border-top: 1px solid $base02;
padding-top: 8px;
icon {
font-size: 16px;
}
}
.battery-percent {
font-weight: bold;
margin: 0 6px;
} }
.battery-status { .battery-status {
@ -269,6 +277,49 @@ $module-bg: transparentize($base02, 0.4);
opacity: 0.7; opacity: 0.7;
} }
.notif-section {
margin-top: 10px;
border-top: 1px solid $base02;
padding-top: 8px;
}
.notif-toggle {
padding: 4px 6px;
border-radius: 6px;
label {
font-weight: bold;
margin: 0 2px;
}
&:hover {
background: transparentize($base05, 0.88);
}
}
.notif-clear {
padding: 4px 10px;
border-radius: 6px;
font-size: 0.85em;
&:hover {
background: transparentize($base05, 0.88);
}
}
.notif-empty {
font-size: 0.85em;
opacity: 0.7;
}
.quicksettings .notif-count {
font-size: 0.8em;
}
.notif-section .notif {
background: $base02;
}
.notifications { .notifications {
.notif { .notif {
background: $base01; background: $base01;

View File

@ -2,7 +2,6 @@ import { App, Astal, Gdk, Gtk } from "astal/gtk3"
import Workspaces from "./Workspaces" import Workspaces from "./Workspaces"
import Logo from "./Logo" import Logo from "./Logo"
import Clock from "./Clock" import Clock from "./Clock"
import { BatteryWidget } from "./Battery"
import QuickSettings from "./QuickSettings" import QuickSettings from "./QuickSettings"
const RADIUS = 12 const RADIUS = 12
@ -54,16 +53,11 @@ function Corner({ side }: { side: "left" | "right" }) {
export default function Bar(gdkmonitor: Gdk.Monitor) { export default function Bar(gdkmonitor: Gdk.Monitor) {
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
const battery = BatteryWidget(gdkmonitor)
const quicksettings = QuickSettings(gdkmonitor) const quicksettings = QuickSettings(gdkmonitor)
const right: Array<JSX.Element> = [] const right: Array<JSX.Element> = [quicksettings.button]
if (battery.button) right.push(battery.button)
right.push(quicksettings.button)
const popups: Array<JSX.Element> = [] const popups: Array<JSX.Element> = [quicksettings.popup]
if (battery.popup) popups.push(battery.popup)
popups.push(quicksettings.popup)
return [ return [
( (

View File

@ -1,6 +1,6 @@
import Gtk from "gi://Gtk?version=3.0"
import AstalBattery from "gi://AstalBattery" import AstalBattery from "gi://AstalBattery"
import { Variable, bind } from "astal" import { type Binding, Variable, bind } from "astal"
import Dropdown from "./Dropdown"
const battery = AstalBattery.get_default() const battery = AstalBattery.get_default()
@ -31,28 +31,20 @@ const status = Variable.derive(
}, },
) )
export function BatteryWidget(gdkmonitor: Gdk.Monitor) { export const batteryIcon: Binding<string> | null = battery.isBattery
if (!battery.isBattery) return { button: null, popup: null } ? bind(battery, "icon-name")
: null
const open = Variable(false) export const batteryPercent: Binding<string> | null = battery.isBattery
? bind(battery, "percentage").as((p) => `${Math.round(p * 100)}%`)
: null
const button = ( export function BatterySection() {
<button if (!battery.isBattery) return <box />
className="battery"
onClicked={() => open.set(!open.get())}
>
<box>
<icon icon={bind(battery, "icon-name")} />
<label
className="battery-level"
label={bind(battery, "percentage").as((p) => `${Math.round(p * 100)}%`)}
/>
</box>
</button>
)
const popup = ( return (
<Dropdown gdkmonitor={gdkmonitor} target={button} open={open} width={180}> <box className="battery-section">
<icon icon={bind(battery, "icon-name")} />
<label <label
className="battery-percent" className="battery-percent"
label={bind(battery, "percentage").as((p) => `${Math.round(p * 100)}%`)} label={bind(battery, "percentage").as((p) => `${Math.round(p * 100)}%`)}
@ -61,9 +53,9 @@ export function BatteryWidget(gdkmonitor: Gdk.Monitor) {
className="battery-status" className="battery-status"
label={status()} label={status()}
visible={status().as((text) => text !== "")} visible={status().as((text) => text !== "")}
hexpand
halign={Gtk.Align.START}
/> />
</Dropdown> </box>
) )
return { button, popup }
} }

View File

@ -32,7 +32,7 @@ function formatTime(n: Notifd.Notification) {
return GLib.DateTime.new_from_unix_local(n.time).format("%H:%M")! return GLib.DateTime.new_from_unix_local(n.time).format("%H:%M")!
} }
function NotificationCard(n: Notifd.Notification) { export function NotificationCard(n: Notifd.Notification) {
const critical = n.urgency === Notifd.Urgency.CRITICAL const critical = n.urgency === Notifd.Urgency.CRITICAL
return ( return (

View File

@ -1,10 +1,17 @@
import Gtk from "gi://Gtk?version=3.0"
import AstalWp from "gi://AstalWp" import AstalWp from "gi://AstalWp"
import Notifd from "gi://AstalNotifd"
import { execAsync, Variable, bind } from "astal" import { execAsync, Variable, bind } from "astal"
import Dropdown from "./Dropdown" import Dropdown from "./Dropdown"
import { wifiIcon, WifiSection } from "./Wifi" import { wifiIcon, WifiSection } from "./Wifi"
import { batteryIcon, batteryPercent, BatterySection } from "./Battery"
import { NotificationCard } from "./Notifications"
const speaker = AstalWp.get_default().audio.default_speaker! const speaker = AstalWp.get_default().audio.default_speaker!
const mic = AstalWp.get_default().audio.default_microphone const mic = AstalWp.get_default().audio.default_microphone
const notifd = Notifd.get_default()
const notifCount = bind(notifd, "notifications").as((ns) => ns.length)
const speakerIcon = Variable.derive( const speakerIcon = Variable.derive(
[bind(speaker, "volume"), bind(speaker, "mute")], [bind(speaker, "volume"), bind(speaker, "mute")],
@ -34,6 +41,11 @@ const actions = [
export default function QuickSettings(gdkmonitor: Gdk.Monitor) { export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
const open = Variable(false) const open = Variable(false)
const notifOpen = Variable(false)
open.subscribe((v) => {
if (!v) notifOpen.set(false)
})
const button = ( const button = (
<button <button
@ -48,9 +60,23 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
}} }}
> >
<box spacing={6}> <box spacing={6}>
<box className="notif-indicator" spacing={3}>
<icon icon="preferences-system-notifications-symbolic" />
<label
className="notif-count"
label={notifCount.as((n) => `${n}`)}
visible={notifCount.as((n) => n > 0)}
/>
</box>
{wifiIcon && <icon icon={wifiIcon} />}
<icon icon={speakerIcon()} /> <icon icon={speakerIcon()} />
{micIcon && <icon icon={micIcon()} />} {micIcon && <icon icon={micIcon()} />}
{wifiIcon && <icon icon={wifiIcon} />} {batteryIcon && batteryPercent && (
<box spacing={3}>
<icon icon={batteryIcon} />
<label className="battery-level" label={batteryPercent} />
</box>
)}
</box> </box>
</button> </button>
) )
@ -98,6 +124,51 @@ export default function QuickSettings(gdkmonitor: Gdk.Monitor) {
</box> </box>
)} )}
<WifiSection open={open} /> <WifiSection open={open} />
<BatterySection />
<box className="notif-section" vertical spacing={6}>
<box className="notif-header-row" spacing={6}>
<button
className="notif-toggle"
hexpand
halign={Gtk.Align.START}
onClicked={() => notifOpen.set(!notifOpen.get())}
>
<box spacing={6}>
<icon icon="preferences-system-notifications-symbolic" />
<label label="Notifications" />
<icon
icon={notifOpen().as((v) =>
v ? "pan-down-symbolic" : "pan-end-symbolic",
)}
/>
</box>
</button>
<button
className="notif-clear"
onClicked={() => [...notifd.notifications].forEach((n) => n.dismiss())}
>
<label label="Clear" />
</button>
</box>
<revealer
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={180}
revealChild={bind(notifOpen)}
>
<box className="notifications" vertical>
<label
className="notif-empty"
label="no notifications"
visible={notifCount.as((n) => n === 0)}
/>
{bind(notifd, "notifications").as((ns) =>
[...ns]
.sort((a, b) => b.time - a.time)
.map((n) => NotificationCard(n)),
)}
</box>
</revealer>
</box>
</Dropdown> </Dropdown>
) )