cleanup: remove popdown stuff

Popdowns were replaced with the sidebar
This commit is contained in:
2 * r + 2 * t 2025-04-02 15:06:27 +11:00
parent dcc3c1fd4c
commit e9eba2e291
19 changed files with 0 additions and 1612 deletions

View file

@ -1,51 +0,0 @@
@use "sass:color";
@use "../scheme";
@use "../lib";
@use "../font";
$-accent: scheme.$rosewater;
.bluetooth-devices {
@include lib.popdown-window($-accent);
min-width: lib.s(500);
min-height: lib.s(400);
.list {
@include lib.spacing($vertical: true);
color: scheme.$text;
.device {
@include lib.spacing(8);
.icon {
font-size: lib.s(18);
}
button {
@include lib.rounded(5);
@include lib.element-decel;
padding: lib.s(3) lib.s(8);
&:hover,
&:focus {
background-color: scheme.$surface0;
}
&:active {
background-color: scheme.$surface1;
}
&:disabled {
color: scheme.$subtext0;
}
}
&.active {
color: $-accent;
}
}
}
}

View file

@ -1,7 +0,0 @@
@forward "notifications";
@forward "updates";
@forward "bluetoothdevices";
@forward "networks";
@forward "media";
@forward "sideright";
@forward "sideleft";

View file

@ -1,113 +0,0 @@
@use "sass:color";
@use "../scheme";
@use "../lib";
@use "../font";
$-accent: scheme.$lavender;
$-accent2: scheme.$pink;
.media {
@include lib.rounded(8);
@include lib.border($-accent, 0.4, 2);
@include lib.shadow;
@include font.mono;
background-color: scheme.$mantle;
color: $-accent;
padding: lib.s(12);
font-size: lib.s(14);
min-width: lib.s(500);
min-height: lib.s(220);
.icon {
font-size: lib.s(32);
}
.overlay {
@include lib.rounded(5);
background-color: color.change(scheme.$mantle, $alpha: 0.8);
}
.background {
@include lib.rounded(5);
transition: background 300ms ease-in-out;
background-color: scheme.$surface0;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
.player {
padding: lib.s(35) lib.s(12);
font-weight: bold;
@include lib.spacing(10, true);
.title {
font-size: lib.s(18);
}
.artist {
color: scheme.$green;
}
.album {
color: $-accent2;
}
.controls {
margin-top: lib.s(3);
@include lib.spacing(10);
button {
@include lib.rounded(4);
@include lib.element-decel;
@include lib.border(scheme.$base, 0.7);
padding: lib.s(5) lib.s(12);
font-size: lib.s(18);
background-color: $-accent;
color: scheme.$base;
&:disabled {
background-color: scheme.$overlay2;
}
&:hover,
&:focus {
background-color: color.change($-accent, $alpha: 0.8);
}
&:active {
background-color: color.change($-accent, $alpha: 0.6);
}
}
}
// Progress bar
trough {
@include lib.rounded(3);
margin-top: lib.s(10);
background-color: scheme.$surface0;
highlight {
@include lib.rounded(3);
background-color: $-accent2;
padding: lib.s(3) 0;
}
slider {
@include lib.rounded(3);
background-color: scheme.$overlay0;
min-width: lib.s(15);
min-height: lib.s(15);
}
}
}
}

View file

@ -1,51 +0,0 @@
@use "sass:color";
@use "../scheme";
@use "../lib";
@use "../font";
$-accent: scheme.$rosewater;
.networks {
@include lib.popdown-window($-accent);
min-width: lib.s(500);
min-height: lib.s(400);
.list {
@include lib.spacing($vertical: true);
color: scheme.$text;
.network {
@include lib.spacing(8);
.icon {
font-size: lib.s(18);
}
button {
@include lib.rounded(5);
@include lib.element-decel;
padding: lib.s(3) lib.s(8);
&:hover,
&:focus {
background-color: scheme.$surface0;
}
&:active {
background-color: scheme.$surface1;
}
&:disabled {
color: scheme.$subtext0;
}
}
&.active {
color: $-accent;
}
}
}
}

View file

@ -1,58 +0,0 @@
@use "sass:color";
@use "../scheme";
@use "../lib";
@use "../font";
@mixin popup($accent) {
.separator {
background-color: $accent;
}
.image {
@include lib.border($accent, 0.05);
}
}
.notifications {
@include lib.popdown-window(scheme.$mauve);
min-width: lib.s(400);
min-height: lib.s(600);
.notification {
.wrapper {
padding-bottom: lib.s(10);
}
.inner {
background-color: color.mix(scheme.$base, scheme.$surface0, 70%);
&.low {
@include popup(scheme.$overlay0);
}
&.normal {
@include lib.border(scheme.$primary, 0.3);
@include popup(scheme.$primary);
}
&.critical {
@include lib.border(scheme.$error, 0.8);
@include popup(scheme.$error);
}
}
.actions > * {
background-color: scheme.$surface0;
&:hover,
&:focus {
background-color: scheme.$surface1;
}
&:active {
background-color: scheme.$surface2;
}
}
}
}

View file

@ -1,131 +0,0 @@
@use "sass:color";
@use "../scheme";
@use "../lib";
@use "../font";
$-accent: scheme.$yellow;
.sideleft {
@include lib.rounded(8);
@include lib.border($-accent, 0.4, 2);
@include lib.shadow;
@include font.mono;
background-color: scheme.$crust;
color: $-accent;
padding: lib.s(12);
font-size: lib.s(14);
@include lib.spacing(10, true);
& > * {
@include lib.rounded(5);
background-color: scheme.$base;
padding: lib.s(16) lib.s(20);
}
.user {
@include lib.spacing(15);
.face {
@include lib.rounded(1000);
background-position: center;
background-repeat: no-repeat;
background-size: cover;
min-width: lib.s(48);
min-height: lib.s(48);
font-size: lib.s(24);
}
.details {
font-size: lib.s(14);
@include lib.spacing(3, true);
.name {
font-size: lib.s(18);
color: scheme.$text;
}
}
.power {
@include lib.element-decel;
@include font.icon;
font-size: lib.s(24);
color: scheme.$red;
&:hover,
&:focus {
color: color.mix(scheme.$red, scheme.$base, 80%);
}
&:active {
color: color.mix(scheme.$red, scheme.$base, 60%);
}
}
}
.locations {
@include lib.spacing(30);
& > * {
@include lib.spacing(8, true);
}
$-colours: scheme.$pink, scheme.$yellow, scheme.$maroon, scheme.$green, scheme.$mauve, scheme.$lavender;
@for $i from 1 through 6 {
.loc#{$i} {
@include lib.element-decel;
color: nth($-colours, $i);
&:hover,
&:focus {
color: color.mix(nth($-colours, $i), scheme.$base, 80%);
}
&:active {
color: color.mix(nth($-colours, $i), scheme.$base, 60%);
}
}
}
}
.hw-resources {
@include lib.spacing(10, true);
.resource {
font-size: lib.s(13);
.inner {
font-size: lib.s(16);
@include lib.spacing(20);
}
.slider {
@include lib.rounded(5);
@include lib.fluent-decel(1000ms);
min-height: lib.s(10);
background-color: scheme.$surface0;
color: scheme.$red;
}
$-names: gpu, cpu, memory, storage;
$-colours: scheme.$green, scheme.$maroon, scheme.$yellow, scheme.$pink;
@for $i from 1 through length($-names) {
&.#{nth($-names, $i)} {
color: nth($-colours, $i);
.slider {
color: nth($-colours, $i);
}
}
}
}
}
}

View file

@ -1,148 +0,0 @@
@use "sass:color";
@use "../scheme";
@use "../lib";
@use "../font";
$-accent: scheme.$peach;
$-accent2: scheme.$blue;
.sideright {
@include lib.rounded(8);
@include lib.border($-accent, 0.4, 2);
@include lib.shadow;
@include font.mono;
background-color: scheme.$crust;
color: $-accent;
padding: lib.s(12);
font-size: lib.s(14);
@include lib.spacing(10, true);
& > * {
@include lib.rounded(5);
background-color: scheme.$base;
}
.time {
padding: lib.s(12) lib.s(8);
font-size: lib.s(48);
font-weight: bold;
& > * {
@include lib.spacing(3);
}
.ampm {
font-size: lib.s(24);
font-weight: normal;
margin-top: lib.s(18);
color: $-accent2;
}
}
.calendar {
padding: lib.s(5) lib.s(8) 0 lib.s(8);
min-width: lib.s(350);
color: scheme.$text;
calendar {
padding: lib.s(8); // Padding for each day
// Month and year
&.header {
color: $-accent2;
}
// Change month/year buttons
&.button {
&:hover {
color: scheme.$subtext0;
}
&:disabled {
color: color.mix(scheme.$text, scheme.$base, 65%);
}
}
// Weekday labels
&.highlight {
color: $-accent;
}
// Days in other month
&:indeterminate {
color: color.mix(scheme.$text, scheme.$base, 65%);
}
// Current day
&:selected {
$base: color.change(scheme.$base, $alpha: 1);
box-shadow: inset 0 lib.s(-12) 0 0 $-accent, inset 0 lib.s(-9) 0 0 $base,
inset lib.s(-10) lib.s(-3) 0 0 $base, inset lib.s(10) 0 0 lib.s(0.1) $base;
color: $-accent;
}
}
}
.weather {
padding: lib.s(5) lib.s(15);
@include lib.spacing(10, true);
.current {
@include lib.spacing(20);
.status-icon {
font-size: lib.s(64);
padding: lib.s(10) lib.s(20) 0 0;
}
.status {
color: $-accent2;
font-size: lib.s(16);
@include lib.spacing($vertical: true);
.temperature {
font-size: lib.s(28);
font-weight: bold;
color: scheme.$text;
@include lib.spacing;
.temp-icon {
font-size: lib.s(30);
$-temps: freezing, cold, normal, hot, burning;
$-colours: scheme.$sky, scheme.$blue, scheme.$green, scheme.$yellow, scheme.$red;
@for $i from 1 through length($-temps) {
&.#{nth($-temps, $i)} {
color: nth($-colours, $i);
}
}
}
}
}
}
.separator {
background-color: scheme.$surface0;
}
.forecast {
padding: lib.s(10) 0 lib.s(15) 0;
@include lib.spacing(10);
.hour {
@include lib.spacing($vertical: true);
.icon {
font-size: lib.s(32);
}
}
}
}
}

View file

@ -1,50 +0,0 @@
@use "sass:color";
@use "../scheme";
@use "../lib";
@use "../font";
$-accent: scheme.$blue;
.updates {
@include lib.popdown-window($-accent);
min-width: lib.s(550);
min-height: lib.s(800);
.wrapper {
@include lib.element-decel;
&:hover,
&:focus {
color: color.mix($-accent, scheme.$base, 80%);
}
&:active {
color: color.mix($-accent, scheme.$base, 60%);
}
}
.repos {
@include lib.spacing($vertical: true);
.repo {
.header {
font-size: lib.s(16);
}
.list,
.news {
color: scheme.$text;
margin-left: lib.s(12);
}
.list {
@include lib.spacing($vertical: true);
}
.news {
margin-bottom: lib.s(5);
}
}
}
}

View file

@ -102,20 +102,6 @@ export default {
sidebar: {
showOnStartup: false,
},
sideleft: {
directories: {
left: {
top: "󰉍 Downloads",
middle: "󱧶 Documents",
bottom: "󱍙 Music",
},
right: {
top: "󰉏 Pictures",
middle: "󰉏 Videos",
bottom: "󱂵 Home",
},
},
},
// Services
math: {
maxHistory: 100,

View file

@ -54,13 +54,6 @@ export default {
"osds.lock.num.hideDelay": NUM,
// Sidebar
"sidebar.showOnStartup": BOOL,
// Sideleft
"sideleft.directories.left.top": STR,
"sideleft.directories.left.middle": STR,
"sideleft.directories.left.bottom": STR,
"sideleft.directories.right.top": STR,
"sideleft.directories.right.middle": STR,
"sideleft.directories.right.bottom": STR,
// Services
"math.maxHistory": NUM,
"updates.interval": NUM,

View file

@ -1,98 +0,0 @@
import { bind, Variable } from "astal";
import { Astal, Gtk } from "astal/gtk3";
import AstalBluetooth from "gi://AstalBluetooth";
import PopdownWindow from "../../widgets/popdownwindow";
const BluetoothDevice = (device: AstalBluetooth.Device) => (
<box className={bind(device, "connected").as(c => `device ${c ? "active" : ""}`)}>
<icon
className="icon"
icon={bind(device, "icon").as(i =>
Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "bluetooth-symbolic"
)}
/>
<label
truncate
xalign={0}
setup={self => {
const update = () =>
(self.label = `${device.alias}${
device.connected || device.paired ? ` (${device.connected ? "Connected" : "Paired"})` : ""
}`);
self.hook(device, "notify::alias", update);
self.hook(device, "notify::connected", update);
self.hook(device, "notify::paired", update);
update();
}}
/>
<box hexpand />
<button
cursor="pointer"
onClicked={self => {
if (device.connected)
device.disconnect_device((_, res) => {
self.sensitive = true;
device.disconnect_device_finish(res);
});
else
device.connect_device((_, res) => {
self.sensitive = true;
device.connect_device_finish(res);
});
self.sensitive = false;
}}
label={bind(device, "connected").as(c => (c ? "Disconnect" : "Connect"))}
/>
<button
cursor="pointer"
onClicked={() => AstalBluetooth.get_default().adapter.remove_device(device)}
label="Remove"
/>
</box>
);
const List = () => (
<box vertical valign={Gtk.Align.START} className="list">
{bind(AstalBluetooth.get_default(), "devices").as(d => d.map(BluetoothDevice))}
</box>
);
export default () => {
const bluetooth = AstalBluetooth.get_default();
const label = Variable("");
const update = () => {
const devices = bluetooth.get_devices();
const connected = devices.filter(d => d.connected).length;
label.set(`${connected} connected device${connected === 1 ? "" : "s"} (${devices.length} available)`);
};
bluetooth.get_devices().forEach(d => d.connect("notify::connected", update));
bluetooth.connect("device-added", (_, device) => device.connect("notify::connected", update));
bluetooth.connect("notify::devices", update);
update();
return (
<PopdownWindow
name="bluetooth-devices"
count={bind(bluetooth, "devices").as(n => n.length)}
countLabel={bind(label)}
headerButtons={[
{
label: bind(bluetooth, "isPowered").as(p => (p ? "Disable" : "Enable")),
onClicked: () => bluetooth.toggle(),
},
{
label: "Discovery",
onClicked: () => {
if (bluetooth.adapter.discovering) bluetooth.adapter.start_discovery();
else bluetooth.adapter.stop_discovery();
},
enabled: bind(bluetooth.adapter, "discovering"),
},
]}
emptyIcon="bluetooth_disabled"
emptyLabel="No Bluetooth devices"
list={<List />}
/>
);
};

View file

@ -1,19 +0,0 @@
import BluetoothDevices from "./bluetoothdevices";
import Media from "./media";
import Networks from "./networks";
import Notifications from "./notifications";
import SideLeft from "./sideleft";
import SideRight from "./sideright";
import Updates from "./updates";
export default () => {
<Notifications />;
<Updates />;
<BluetoothDevices />;
<Networks />;
<Media />;
<SideRight />;
<SideLeft />;
return null;
};

View file

@ -1,152 +0,0 @@
import Players from "@/services/players";
import { isRealPlayer } from "@/utils/mpris";
import PopupWindow from "@/widgets/popupwindow";
import { bind, Variable } from "astal";
import { Astal, Gtk } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris";
const shuffleToIcon = (s: AstalMpris.Shuffle) =>
`caelestia-${s === AstalMpris.Shuffle.ON ? "shuffle" : "consecutive"}-symbolic`;
const playbackToIcon = (s: AstalMpris.PlaybackStatus) =>
`caelestia-${s === AstalMpris.PlaybackStatus.PLAYING ? "pause" : "play"}-symbolic`;
const loopToIcon = (s: AstalMpris.Loop) => {
if (s === AstalMpris.Loop.PLAYLIST) return "caelestia-repeat-symbolic";
if (s === AstalMpris.Loop.TRACK) return "caelestia-repeat-one-symbolic";
return "caelestia-no-repeat-symbolic";
};
const lengthStr = (length: number) =>
`${Math.floor(length / 60)}:${Math.floor(length % 60)
.toString()
.padStart(2, "0")}`;
const Player = ({ player }: { player: AstalMpris.Player }) => {
const background = (
<box className="background" css={bind(player, "coverArt").as(u => `background-image: url("${u}");`)} />
);
return (
<overlay
overlays={[
<box className="overlay" />,
<box
vertical
className="player"
setup={self =>
self.connect("size-allocate", () =>
background.set_size_request(self.get_preferred_width()[1], self.get_preferred_height()[1])
)
}
>
<label className="title" label={bind(player, "title").as(t => t ?? "")} />
<label className="artist" label={bind(player, "artist").as(a => a ?? "")} />
<label className="album" label={bind(player, "album").as(a => a ?? "")} />
<box className="controls" halign={Gtk.Align.CENTER}>
<button
cursor="pointer"
onClicked={() => player.shuffle()}
sensitive={bind(player, "canControl")}
>
<icon icon={bind(player, "shuffleStatus").as(shuffleToIcon)} />
</button>
<button
cursor="pointer"
onClicked={() => player.previous()}
sensitive={bind(player, "canGoPrevious")}
>
<icon icon="caelestia-skip-previous-symbolic" />
</button>
<button
cursor="pointer"
onClicked={() => player.play_pause()}
sensitive={bind(player, "canControl")}
>
<icon icon={bind(player, "playbackStatus").as(playbackToIcon)} />
</button>
<button cursor="pointer" onClicked={() => player.next()} sensitive={bind(player, "canGoNext")}>
<icon icon="caelestia-skip-next-symbolic" />
</button>
<button cursor="pointer" onClicked={() => player.loop()} sensitive={bind(player, "canControl")}>
<icon icon={bind(player, "loopStatus").as(loopToIcon)} />
</button>
</box>
<slider
hexpand
onDragged={self => player.set_position(self.value * player.length)}
setup={self => {
const update = () => {
self.set_tooltip_text(`${lengthStr(player.position)}/${lengthStr(player.length)}`);
self.set_value(player.position / player.length);
};
self.hook(player, "notify::position", update);
self.hook(player, "notify::length", update);
update();
}}
/>
</box>,
]}
>
{background}
</overlay>
);
};
export default () => {
const shown = Variable("none");
return (
<PopupWindow name="media">
<box className="media">
<eventbox
onClick={(_, event) => {
if (event.button === Astal.MouseButton.SECONDARY) {
const current = Players.get_default().list.find(p => p.busName === shown.get());
if (current) Players.get_default().makeCurrent(current);
}
}}
onScroll={(_, event) => {
const players = AstalMpris.get_default().players.filter(isRealPlayer);
const idx = players.findIndex(p => p.busName === shown.get());
if (idx === -1) return;
if (event.delta_y < 0) {
if (idx > 0) shown.set(players[idx - 1].busName);
} else if (idx < players.length - 1) shown.set(players[idx + 1].busName);
}}
>
<stack
expand
transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
transitionDuration={150}
shown={bind(shown)}
setup={self => {
const players = new Map<string, JSX.Element>();
const addPlayer = (player: AstalMpris.Player) => {
if (!isRealPlayer(player) || players.has(player.busName)) return;
const widget = <Player player={player} />;
self.add_named(widget, player.busName);
if (players.size === 0) shown.set(player.busName);
players.set(player.busName, widget);
};
for (const player of Players.get_default().list) addPlayer(player);
self.hook(AstalMpris.get_default(), "player-added", (_, player) => addPlayer(player));
self.hook(AstalMpris.get_default(), "player-closed", (_, player) => {
players.get(player.busName)?.destroy();
players.delete(player.busName);
if (shown.get() === player.busName)
shown.set(AstalMpris.get_default().players.find(isRealPlayer)?.busName ?? "none");
});
}}
>
<box vertical valign={Gtk.Align.CENTER} name="none">
<label className="icon" label="music_note" />
<label label="No media playing" />
</box>
</stack>
</eventbox>
</box>
</PopupWindow>
);
};

View file

@ -1,101 +0,0 @@
import { bind, execAsync, Variable } from "astal";
import { Gtk } from "astal/gtk3";
import AstalNetwork from "gi://AstalNetwork";
import PopdownWindow from "../../widgets/popdownwindow";
const Network = (accessPoint: AstalNetwork.AccessPoint) => (
<box
className={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(
a => `network ${a === accessPoint ? "active" : ""}`
)}
>
<icon className="icon" icon={bind(accessPoint, "iconName")} />
<label
truncate
xalign={0}
setup={self => {
const update = () =>
(self.label = `${accessPoint.ssid}${` (${accessPoint.frequency > 5000 ? 5 : 2.4}GHz | ${
accessPoint.strength
}/100)`}`);
self.hook(accessPoint, "notify::ssid", update);
self.hook(accessPoint, "notify::frequency", update);
self.hook(accessPoint, "notify::strength", update);
update();
}}
/>
<box hexpand />
<button
cursor="pointer"
onClicked={self => {
const cmd =
AstalNetwork.get_default().wifi.activeAccessPoint === accessPoint ? "c down id" : "d wifi connect";
execAsync(`nmcli ${cmd} '${accessPoint.ssid}'`)
.then(() => (self.sensitive = true))
.catch(console.error);
self.sensitive = false;
}}
label={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(a =>
a === accessPoint ? "Disconnect" : "Connect"
)}
/>
<button
cursor="pointer"
onClicked={() => execAsync(`nmcli c delete id '${accessPoint.ssid}'`).catch(() => {})}
label="Forget"
/>
</box>
);
const List = () => {
const { wifi } = AstalNetwork.get_default();
const children = Variable.derive([bind(wifi, "accessPoints"), bind(wifi, "activeAccessPoint")], (aps, ac) =>
aps
.filter(a => a.ssid)
.sort((a, b) => (a === ac ? -1 : b.strength - a.strength))
.map(Network)
);
return (
<box vertical valign={Gtk.Align.START} className="list" onDestroy={() => children.drop()}>
{bind(children)}
</box>
);
};
export default () => {
const network = AstalNetwork.get_default();
const label = Variable("");
const update = () => {
if (network.primary === AstalNetwork.Primary.WIFI) label.set(network.wifi.ssid ?? "Disconnected");
else if (network.primary === AstalNetwork.Primary.WIRED) label.set(`Ethernet (${network.wired.speed})`);
else label.set("No Wifi");
};
network.connect("notify::primary", update);
network.get_wifi()?.connect("notify::ssid", update);
network.get_wired()?.connect("notify::speed", update);
update();
return (
<PopdownWindow
name="networks"
count={bind(network.wifi, "accessPoints").as(a => a.length)}
countLabel={bind(label)}
headerButtons={[
{
label: bind(network.wifi, "enabled").as(e => (e ? "Disable" : "Enable")),
onClicked: () => (network.wifi.enabled = !network.wifi.enabled),
},
{
label: bind(network.wifi, "scanning").as(s => (s ? "Scanning" : "Scan")),
onClicked: () => network.wifi.scan(),
enabled: bind(network.wifi, "scanning"),
},
]}
emptyIcon="wifi_off"
emptyLabel={bind(network.wifi, "enabled").as(p => (p ? "No available networks" : "Wifi is off"))}
list={<List />}
/>
);
};

View file

@ -1,65 +0,0 @@
import { bind } from "astal";
import { Astal, Gtk } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import Notification from "../../widgets/notification";
import PopdownWindow from "../../widgets/popdownwindow";
const List = () => (
<box
vertical
valign={Gtk.Align.START}
className="list"
setup={self => {
const notifd = AstalNotifd.get_default();
const map = new Map<number, Notification>();
const addNotification = (notification: AstalNotifd.Notification) => {
const notif = (<Notification notification={notification} />) as Notification;
notif.connect("destroy", () => map.get(notification.id) === notif && map.delete(notification.id));
map.get(notification.id)?.destroyWithAnims();
map.set(notification.id, notif);
self.pack_end(
<eventbox
// Dismiss on middle click
onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
>
{notif}
</eventbox>,
false,
false,
0
);
};
notifd
.get_notifications()
.sort((a, b) => a.time - b.time)
.forEach(addNotification);
self.hook(notifd, "notified", (_, id) => addNotification(notifd.get_notification(id)));
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
}}
/>
);
export default () => (
<PopdownWindow
name="notifications"
count={bind(AstalNotifd.get_default(), "notifications").as(n => n.length)}
headerButtons={[
{
label: "Silence",
onClicked: () => (AstalNotifd.get_default().dontDisturb = !AstalNotifd.get_default().dontDisturb),
enabled: bind(AstalNotifd.get_default(), "dontDisturb"),
},
{
label: "Clear",
onClicked: () => AstalNotifd.get_default().notifications.forEach(n => n.dismiss()),
},
]}
emptyIcon="notifications_active"
emptyLabel="All caught up!"
list={<List />}
/>
);

View file

@ -1,205 +0,0 @@
import Cpu from "@/services/cpu";
import Gpu from "@/services/gpu";
import Memory from "@/services/memory";
import Storage from "@/services/storage";
import { osId } from "@/utils/system";
import PopupWindow from "@/widgets/popupwindow";
import { bind, execAsync, GLib, type Binding } from "astal";
import { App, Gtk, type Widget } from "astal/gtk3";
import type cairo from "cairo";
import { sideleft } from "config";
const fmt = (bytes: number, pow: number) => +(bytes / 1024 ** pow).toFixed(2);
const format = ({ total, used }: { total: number; used: number }) => {
if (total >= 1024 ** 4) return `${fmt(used, 4)}/${fmt(total, 4)} TiB`;
if (total >= 1024 ** 3) return `${fmt(used, 3)}/${fmt(total, 3)} GiB`;
if (total >= 1024 ** 2) return `${fmt(used, 2)}/${fmt(total, 2)} MiB`;
if (total >= 1024) return `${fmt(used, 1)}/${fmt(total, 1)} KiB`;
return `${used}/${total} B`;
};
const User = () => (
<box className="user">
<box
valign={Gtk.Align.CENTER}
className="face"
css={`
background-image: url("${HOME}/.face");
`}
>
{!GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS) && (
<label
setup={self => {
const name = GLib.get_real_name();
if (name !== "Unknown")
self.label = name
.split(" ")
.map(s => s[0].toUpperCase())
.join("");
else self.label = "";
}}
/>
)}
</box>
<box vertical hexpand valign={Gtk.Align.CENTER} className="details">
<label xalign={0} className="name" label={GLib.get_user_name()} />
<label xalign={0} label={(GLib.getenv("XDG_CURRENT_DESKTOP") ?? osId).toUpperCase()} />
</box>
<button
valign={Gtk.Align.CENTER}
className="power"
cursor="pointer"
onClicked={() => App.toggle_window("session")}
label="power_settings_new"
/>
</box>
);
const QuickLaunch = () => (
<box className="quick-launch">
<box vertical>
<box>{/* <button */}</box> // TODO
</box>
</box>
);
const Location = ({ label, num }: { label: Binding<string>; num: number }) => (
<button
className={"loc" + num}
cursor="pointer"
onClicked={self => {
self.get_toplevel().hide();
const dir = label.get().split(" ").at(-1);
execAsync(`xdg-open ${HOME}/${dir?.toLowerCase() === "home" ? "" : `${dir}/`}`).catch(console.error);
}}
>
<label xalign={0} label={label} />
</button>
);
const Locations = () => (
<box className="locations">
<box vertical>
<Location label={bind(sideleft.directories.left.top)} num={1} />
<Location label={bind(sideleft.directories.left.middle)} num={2} />
<Location label={bind(sideleft.directories.left.bottom)} num={3} />
</box>
<box vertical>
<Location label={bind(sideleft.directories.right.top)} num={4} />
<Location label={bind(sideleft.directories.right.middle)} num={5} />
<Location label={bind(sideleft.directories.right.bottom)} num={6} />
</box>
</box>
);
const Slider = ({ value }: { value: Binding<number> }) => (
<drawingarea
hexpand
valign={Gtk.Align.CENTER}
className="slider"
css={bind(value).as(v => `font-size: ${v}px;`)}
setup={self => {
const halfPi = Math.PI / 2;
const styleContext = self.get_style_context();
self.set_size_request(-1, styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number);
self.connect("draw", (_, cr: cairo.Context) => {
const styleContext = self.get_style_context();
const width = self.get_allocated_width();
const height = styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number;
self.set_size_request(-1, height);
const progressValue = styleContext.get_property("font-size", Gtk.StateFlags.NORMAL) as number;
let radius = styleContext.get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
// Background
cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
cr.arc(width - radius, radius, radius, -halfPi, 0); // Top right
cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
cr.fill();
// Flatten when near 0
radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
const progressPosition = width * progressValue - radius;
const fg = styleContext.get_color(Gtk.StateFlags.NORMAL);
cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
// Foreground
cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
cr.fill();
});
}}
/>
);
const Resource = ({
icon,
name,
value,
labelSetup,
}: {
icon: string;
name: string;
value: Binding<number>;
labelSetup?: (self: Widget.Label) => void;
}) => (
<box vertical className={`resource ${name}`}>
<box className="inner">
<label label={icon} />
<Slider value={value.as(v => v / 100)} />
</box>
<label halign={Gtk.Align.END} label={labelSetup ? "" : value.as(v => `${+v.toFixed(2)}%`)} setup={labelSetup} />
</box>
);
const HwResources = () => (
<box vertical className="hw-resources">
{Gpu.get_default().available && <Resource icon="󰢮" name="gpu" value={bind(Gpu.get_default(), "usage")} />}
<Resource icon="" name="cpu" value={bind(Cpu.get_default(), "usage")} />
<Resource
icon=""
name="memory"
value={bind(Memory.get_default(), "usage")}
labelSetup={self => {
const mem = Memory.get_default();
const update = () => (self.label = format(mem));
self.hook(mem, "notify::used", update);
self.hook(mem, "notify::total", update);
update();
}}
/>
<Resource
icon="󰋊"
name="storage"
value={bind(Storage.get_default(), "usage")}
labelSetup={self => {
const storage = Storage.get_default();
const update = () => (self.label = format(storage));
self.hook(storage, "notify::used", update);
self.hook(storage, "notify::total", update);
update();
}}
/>
</box>
);
export default () => (
<PopupWindow name="sideleft">
<box vertical className="sideleft">
<User />
{/* <QuickLaunch /> */}
<Locations />
<HwResources />
</box>
</PopupWindow>
);

View file

@ -1,146 +0,0 @@
import SWeather, { type WeatherData } from "@/services/weather";
import { ellipsize } from "@/utils/strings";
import { bindCurrentTime } from "@/utils/system";
import { setupCustomTooltip, Calendar as WCal } from "@/utils/widgets";
import PopupWindow from "@/widgets/popupwindow";
import { bind, timeout } from "astal";
import { Astal, Gtk, type Gdk } from "astal/gtk3";
const getHoursFromUpdate = (data: WeatherData, hours: number) => {
const updateTime = data.location.localtime_epoch;
const updateHourStart = updateTime - (updateTime % 3600);
let nextHour = new Date((updateHourStart + hours * 3600) * 1000).getHours();
if (nextHour >= 24) nextHour -= 24;
return nextHour;
};
const Time = () => (
<box className="time">
<box hexpand halign={Gtk.Align.CENTER}>
<label label={bindCurrentTime("%I:%M:%S")} />
<label className="ampm" label={bindCurrentTime("%p", c => (c.get_hour() < 12 ? "AM" : "PM"))} />
</box>
</box>
);
const Calendar = () => (
<box className="calendar">
<eventbox
setup={self => {
self.connect("button-press-event", (_, event: Gdk.Event) => {
if (event.get_button()[1] === Astal.MouseButton.MIDDLE) {
const now = new Date();
const child = self.get_child() as WCal | null;
if (!child) return;
child.select_month(now.getMonth(), now.getFullYear());
}
});
}}
>
<WCal
hexpand
showDetails={false}
setup={self => {
const update = () =>
timeout(0.1, () => {
const now = new Date();
if (self.month === now.getMonth() && self.year === now.getFullYear())
self.day = now.getDate();
else self.day = 0;
});
self.connect("month-changed", update);
self.connect("next-month", update);
self.connect("prev-month", update);
self.connect("next-year", update);
self.connect("prev-year", update);
update();
}}
/>
</eventbox>
</box>
);
const Weather = () => {
const weather = SWeather.get_default();
return (
<box vertical className="weather">
<centerbox className="current">
<label
halign={Gtk.Align.START}
valign={Gtk.Align.CENTER}
className="status-icon"
label={bind(weather, "icon")}
/>
<box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="status">
<box halign={Gtk.Align.CENTER} className="temperature">
<label label={bind(weather, "temperature")} />
<label
className={bind(weather, "tempColour").as(c => `temp-icon ${c}`)}
label={bind(weather, "tempIcon")}
/>
</box>
<label label={bind(weather, "condition").as(c => ellipsize(c, 16))} />
</box>
<box vertical halign={Gtk.Align.END} valign={Gtk.Align.CENTER} className="other-data">
<label
xalign={0}
label={bind(weather, "wind").as(w => `${w}`)}
setup={self =>
setupCustomTooltip(
self,
bind(weather, "wind").as(w => `${w} wind speed`)
)
}
/>
<label
xalign={0}
label={bind(weather, "rainChance").as(r => `${r}`)}
setup={self =>
setupCustomTooltip(
self,
bind(weather, "rainChance").as(r => `${r} chance of rain`)
)
}
/>
</box>
</centerbox>
<box className="separator" />
<box className="forecast">
{Array.from({ length: 4 }).map((_, i) => (
<box vertical hexpand className="hour">
<label
label={bind(weather, "raw").as(r => {
const hour = getHoursFromUpdate(r, i + 1);
return `${hour % 12 || 12}${hour < 12 ? "AM" : "PM"}`;
})}
/>
<label
className="icon"
label={bind(weather, "raw").as(r =>
weather.getIcon(weather.forecast[getHoursFromUpdate(r, i + 1)].condition.text)
)}
/>
<label
label={bind(weather, "raw").as(r =>
weather.getTemp(weather.forecast[getHoursFromUpdate(r, i + 1)])
)}
/>
</box>
))}
</box>
</box>
);
};
export default () => (
<PopupWindow name="sideright">
<box vertical className="sideright">
<Time />
<Calendar />
<Weather />
</box>
</PopupWindow>
);

View file

@ -1,145 +0,0 @@
import Palette from "@/services/palette";
import Updates, { Repo as IRepo, Update as IUpdate } from "@/services/updates";
import { MenuItem } from "@/utils/widgets";
import PopdownWindow from "@/widgets/popdownwindow";
import { bind, execAsync, GLib, Variable } from "astal";
import { App, Astal, Gtk } from "astal/gtk3";
const constructItem = (label: string, exec: string, quiet = true) =>
new MenuItem({
label,
onActivate() {
App.get_window("updates")?.hide();
execAsync(exec).catch(e => !quiet && console.error(e));
},
});
const Update = (update: IUpdate) => {
const menu = new Gtk.Menu();
menu.append(constructItem("Open info in browser", `xdg-open '${update.url}'`, false));
menu.append(constructItem("Open info in terminal", `uwsm app -- foot -H pacman -Qi ${update.name}`));
menu.append(new Gtk.SeparatorMenuItem({ visible: true }));
menu.append(constructItem("Reinstall", `uwsm app -T -- yay -S ${update.name}`));
menu.append(constructItem("Remove with dependencies", `uwsm app -T -- yay -Rns ${update.name}`));
return (
<button
onClick={(_, event) => event.button === Astal.MouseButton.SECONDARY && menu.popup_at_pointer(null)}
onDestroy={() => menu.destroy()}
>
<label
truncate
useMarkup
xalign={0}
label={bind(Palette.get_default(), "colours").as(
c =>
`${update.name} <span foreground="${c.teal}">(${update.version.old} -> ${
update.version.new
})</span>\n <span foreground="${c.subtext0}">${GLib.markup_escape_text(
update.description,
update.description.length
)}</span>`
)}
/>
</button>
);
};
const Repo = ({ repo, first }: { repo: IRepo; first?: boolean }) => {
const expanded = Variable(first);
return (
<box vertical className="repo">
<button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
<box className="header">
<label className="icon" label={repo.icon} />
<label label={`${repo.name} (${repo.updates.length})`} />
<box hexpand />
<label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
</box>
</button>
<revealer
revealChild={bind(expanded)}
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={200}
>
<box vertical className="list">
{repo.updates.map(Update)}
</box>
</revealer>
</box>
);
};
const News = ({ news }: { news: string }) => {
const expanded = Variable(true);
news = news
.replace(/^([0-9]{4}-[0-9]{2}-[0-9]{2} .+)$/gm, "<b>$1</b>") // Make titles bold
.replaceAll("\n\x1b[0m\n<b>", "\n\n<b>") // Remove unopened \x1b[0m after each piece of news
.slice(0, -5) // Remove last unopened \x1b[0m
.replaceAll("\x1b[0m", "</span>"); // Replace reset code with end span
return (
<box vertical className="repo">
<button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
<box className="header">
<label className="icon" label="newspaper" />
<label label="News" />
<box hexpand />
<label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
</box>
</button>
<revealer
revealChild={bind(expanded)}
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={200}
>
<label
wrap
useMarkup
xalign={0}
className="news"
label={bind(Palette.get_default(), "teal").as(
c => news.replaceAll("\x1b[36m", `<span foreground="${c}">`) // Replace color codes with html spans
)}
/>
</revealer>
</box>
);
};
const List = () => (
<box vertical valign={Gtk.Align.START} className="repos">
{bind(Updates.get_default(), "updateData").as(d =>
d.news
? [<News news={d.news} />, ...d.repos.map(r => <Repo repo={r} />)]
: d.repos.map((r, i) => <Repo repo={r} first={i === 0} />)
)}
</box>
);
export default () => (
<PopdownWindow
name="updates"
count={bind(Updates.get_default(), "numUpdates")}
headerButtons={[
{
label: "Update all",
onClicked: () =>
execAsync("uwsm app -T -- yay")
.then(() => Updates.get_default().getUpdates())
// Ignore errors
.catch(() => {}),
},
{
label: bind(Updates.get_default(), "loading").as(l => (l ? "Loading" : "Reload")),
onClicked: () => Updates.get_default().getUpdates(),
enabled: bind(Updates.get_default(), "loading"),
},
]}
emptyIcon="deployed_code_history"
emptyLabel="All packages up to date!"
list={<List />}
/>
);

View file

@ -1,51 +0,0 @@
import type { Binding } from "astal";
import { Gtk } from "astal/gtk3";
import PopupWindow from "./popupwindow";
export default ({
name,
count,
countLabel = count.as(c => `${c} ${name.slice(0, -1)}${c === 1 ? "" : "s"}`),
headerButtons,
emptyIcon,
emptyLabel,
list,
}: {
name: string;
count: Binding<number>;
countLabel?: Binding<string>;
headerButtons: { label: string | Binding<string>; onClicked: () => void; enabled?: Binding<boolean> }[];
emptyIcon: string;
emptyLabel: string | Binding<string>;
list: JSX.Element;
}) => (
<PopupWindow name={name}>
<box vertical className={name}>
<box className="header">
<label label={countLabel} />
<box hexpand />
{headerButtons.map(({ label, onClicked, enabled }) => (
<button
cursor="pointer"
onClicked={onClicked}
label={label}
className={enabled?.as(d => (d ? "enabled" : ""))}
/>
))}
</box>
<stack
transitionType={Gtk.StackTransitionType.CROSSFADE}
transitionDuration={150}
shown={count.as(c => (c > 0 ? "list" : "empty"))}
>
<box vertical valign={Gtk.Align.CENTER} name="empty">
<label className="icon" label={emptyIcon} />
<label label={emptyLabel} />
</box>
<scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
{list}
</scrollable>
</stack>
</box>
</PopupWindow>
);