summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/modules/bar.tsx32
-rw-r--r--src/modules/popdowns/index.tsx2
-rw-r--r--src/modules/popdowns/sideleft.tsx203
-rw-r--r--src/services/cpu.ts45
-rw-r--r--src/services/gpu.ts55
-rw-r--r--src/services/memory.ts60
-rw-r--r--src/services/storage.ts61
7 files changed, 447 insertions, 11 deletions
diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx
index eb42d0b..16c505a 100644
--- a/src/modules/bar.tsx
+++ b/src/modules/bar.tsx
@@ -1,21 +1,21 @@
+import type { Monitor } from "@/services/monitors";
+import Players from "@/services/players";
+import Updates from "@/services/updates";
+import { getAppCategoryIcon } from "@/utils/icons";
+import { ellipsize } from "@/utils/strings";
+import { bindCurrentTime, osIcon } from "@/utils/system";
+import { setupCustomTooltip } from "@/utils/widgets";
+import type PopupWindow from "@/widgets/popupwindow";
import { execAsync, register, Variable } from "astal";
import { bind, kebabify } from "astal/binding";
import { App, Astal, astalify, Gdk, Gtk, type ConstructProps } from "astal/gtk3";
+import { bar as config } from "config";
import AstalBluetooth from "gi://AstalBluetooth";
import AstalHyprland from "gi://AstalHyprland";
import AstalNetwork from "gi://AstalNetwork";
import AstalNotifd from "gi://AstalNotifd";
import AstalTray from "gi://AstalTray";
import AstalWp01 from "gi://AstalWp";
-import { bar as config } from "../../config";
-import type { Monitor } from "../services/monitors";
-import Players from "../services/players";
-import Updates from "../services/updates";
-import { getAppCategoryIcon } from "../utils/icons";
-import { ellipsize } from "../utils/strings";
-import { bindCurrentTime, osIcon } from "../utils/system";
-import { setupCustomTooltip } from "../utils/widgets";
-import type PopupWindow from "../widgets/popupwindow";
const hyprland = AstalHyprland.get_default();
@@ -44,7 +44,13 @@ const togglePopup = (self: JSX.Element, event: Astal.ClickEvent, name: string) =
}
};
-const OSIcon = () => <label className="module os-icon" label={osIcon} />;
+const OSIcon = () => (
+ <button
+ className="module os-icon"
+ label={osIcon}
+ onClick={(self, event) => event.button === Astal.MouseButton.PRIMARY && togglePopup(self, event, "sideleft")}
+ />
+);
const ActiveWindow = () => (
<box
@@ -453,7 +459,11 @@ const DateTime = () => (
);
const Power = () => (
- <button className="module power" label="power_settings_new" onClicked={() => App.toggle_window("session")} />
+ <button
+ className="module power"
+ label="power_settings_new"
+ onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && App.toggle_window("session")}
+ />
);
export default ({ monitor }: { monitor: Monitor }) => (
diff --git a/src/modules/popdowns/index.tsx b/src/modules/popdowns/index.tsx
index c4f4664..fb9abf7 100644
--- a/src/modules/popdowns/index.tsx
+++ b/src/modules/popdowns/index.tsx
@@ -2,6 +2,7 @@ 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";
@@ -12,6 +13,7 @@ export default () => {
<Networks />;
<Media />;
<SideRight />;
+ <SideLeft />;
return null;
};
diff --git a/src/modules/popdowns/sideleft.tsx b/src/modules/popdowns/sideleft.tsx
new file mode 100644
index 0000000..fdf3e4f
--- /dev/null
+++ b/src/modules/popdowns/sideleft.tsx
@@ -0,0 +1,203 @@
+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";
+
+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 = ({ home, label, num }: { home?: boolean; label: string; num: number }) => (
+ <button
+ className={"loc" + num}
+ cursor="pointer"
+ onClicked={self => {
+ self.get_toplevel().hide();
+ execAsync(`xdg-open ${HOME}/${home ? "" : label.split(" ").at(-1) + "/"}`).catch(console.error);
+ }}
+ >
+ <label xalign={0} label={label} />
+ </button>
+);
+
+const Locations = () => (
+ <box className="locations">
+ <box vertical>
+ <Location label="󰉍 Downloads" num={1} />
+ <Location label="󱧶 Documents" num={2} />
+ <Location label="󱍙 Music" num={3} />
+ </box>
+ <box vertical>
+ <Location label="󰉏 Pictures" num={4} />
+ <Location label="󰉏 Videos" num={5} />
+ <Location label="󱂵 Home" num={6} home />
+ </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>
+);
diff --git a/src/services/cpu.ts b/src/services/cpu.ts
new file mode 100644
index 0000000..f1699f7
--- /dev/null
+++ b/src/services/cpu.ts
@@ -0,0 +1,45 @@
+import { GObject, interval, property, register } from "astal";
+import { cpu as config } from "config";
+import GTop from "gi://GTop";
+
+@register({ GTypeName: "Cpu" })
+export default class Cpu extends GObject.Object {
+ static instance: Cpu;
+ static get_default() {
+ if (!this.instance) this.instance = new Cpu();
+
+ return this.instance;
+ }
+
+ #previous: GTop.glibtop_cpu = new GTop.glibtop_cpu();
+ #usage: number = 0;
+
+ @property(Number)
+ get usage() {
+ return this.#usage;
+ }
+
+ calculateUsage() {
+ const current = new GTop.glibtop_cpu();
+ GTop.glibtop_get_cpu(current);
+
+ // Calculate the differences from the previous to current data
+ const total = current.total - this.#previous.total;
+ const idle = current.idle - this.#previous.idle;
+
+ this.#previous = current;
+
+ return total > 0 ? ((total - idle) / total) * 100 : 0;
+ }
+
+ update() {
+ this.#usage = this.calculateUsage();
+ this.notify("usage");
+ }
+
+ constructor() {
+ super();
+
+ interval(config.interval, () => this.update());
+ }
+}
diff --git a/src/services/gpu.ts b/src/services/gpu.ts
new file mode 100644
index 0000000..916a2bc
--- /dev/null
+++ b/src/services/gpu.ts
@@ -0,0 +1,55 @@
+import { execAsync, Gio, GObject, interval, property, register } from "astal";
+import { gpu as config } from "config";
+
+@register({ GTypeName: "Gpu" })
+export default class Gpu extends GObject.Object {
+ static instance: Gpu;
+ static get_default() {
+ if (!this.instance) this.instance = new Gpu();
+
+ return this.instance;
+ }
+
+ readonly available: boolean = false;
+ #usage: number = 0;
+
+ @property(Number)
+ get usage() {
+ return this.#usage;
+ }
+
+ async calculateUsage() {
+ const percs = (await execAsync("fish -c 'cat /sys/class/drm/card*/device/gpu_busy_percent'")).split("\n");
+ return percs.reduce((a, b) => a + parseFloat(b), 0) / percs.length;
+ }
+
+ update() {
+ this.calculateUsage().then(usage => {
+ this.#usage = usage;
+ this.notify("usage");
+ });
+ }
+
+ constructor() {
+ super();
+
+ let enumerator = null;
+ try {
+ enumerator = Gio.File.new_for_path("/sys/class/drm").enumerate_children(
+ Gio.FILE_ATTRIBUTE_STANDARD_NAME,
+ Gio.FileQueryInfoFlags.NONE,
+ null
+ );
+ } catch {}
+
+ let info: Gio.FileInfo | undefined | null;
+ while ((info = enumerator?.next_file(null))) {
+ if (/card[0-9]+/.test(info.get_name())) {
+ this.available = true;
+ break;
+ }
+ }
+
+ if (this.available) interval(config.interval, () => this.update());
+ }
+}
diff --git a/src/services/memory.ts b/src/services/memory.ts
new file mode 100644
index 0000000..74fa228
--- /dev/null
+++ b/src/services/memory.ts
@@ -0,0 +1,60 @@
+import { GObject, interval, property, readFileAsync, register } from "astal";
+import { memory as config } from "config";
+
+@register({ GTypeName: "Memory" })
+export default class Memory extends GObject.Object {
+ static instance: Memory;
+ static get_default() {
+ if (!this.instance) this.instance = new Memory();
+
+ return this.instance;
+ }
+
+ #total: number = 0;
+ #free: number = 0;
+ #used: number = 0;
+ #usage: number = 0;
+
+ @property(Number)
+ get total() {
+ return this.#total;
+ }
+
+ @property(Number)
+ get free() {
+ return this.#free;
+ }
+
+ @property(Number)
+ get used() {
+ return this.#used;
+ }
+
+ @property(Number)
+ get usage() {
+ return this.#usage;
+ }
+
+ async update() {
+ const info = await readFileAsync("/proc/meminfo");
+ this.#total = parseInt(info.match(/MemTotal:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
+ this.#free = parseInt(info.match(/MemAvailable:\s+(\d+)/)?.[1] ?? "0", 10) * 1024;
+
+ if (isNaN(this.#total)) this.#total = 0;
+ if (isNaN(this.#free)) this.#free = 0;
+
+ this.#used = this.#total - this.#free;
+ this.#usage = this.#total > 0 ? (this.#used / this.#total) * 100 : 0;
+
+ this.notify("total");
+ this.notify("free");
+ this.notify("used");
+ this.notify("usage");
+ }
+
+ constructor() {
+ super();
+
+ interval(config.interval, () => this.update().catch(console.error));
+ }
+}
diff --git a/src/services/storage.ts b/src/services/storage.ts
new file mode 100644
index 0000000..e1e1c55
--- /dev/null
+++ b/src/services/storage.ts
@@ -0,0 +1,61 @@
+import { GObject, interval, property, register } from "astal";
+import { storage as config } from "config";
+import GTop from "gi://GTop";
+
+@register({ GTypeName: "Storage" })
+export default class Storage extends GObject.Object {
+ static instance: Storage;
+ static get_default() {
+ if (!this.instance) this.instance = new Storage();
+
+ return this.instance;
+ }
+
+ #total: number = 0;
+ #free: number = 0;
+ #used: number = 0;
+ #usage: number = 0;
+
+ @property(Number)
+ get total() {
+ return this.#total;
+ }
+
+ @property(Number)
+ get free() {
+ return this.#free;
+ }
+
+ @property(Number)
+ get used() {
+ return this.#used;
+ }
+
+ @property(Number)
+ get usage() {
+ return this.#usage;
+ }
+
+ update() {
+ const root = new GTop.glibtop_fsusage();
+ GTop.glibtop_get_fsusage(root, "/");
+ const home = new GTop.glibtop_fsusage();
+ GTop.glibtop_get_fsusage(home, "/home");
+
+ this.#total = root.blocks * root.block_size + home.blocks * home.block_size;
+ this.#free = root.bavail * root.block_size + home.bavail * home.block_size;
+ this.#used = this.#total - this.#free;
+ this.#usage = this.#total > 0 ? (this.#used / this.#total) * 100 : 0;
+
+ this.notify("total");
+ this.notify("free");
+ this.notify("used");
+ this.notify("usage");
+ }
+
+ constructor() {
+ super();
+
+ interval(config.interval, () => this.update());
+ }
+}