diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/modules/bar.tsx | 32 | ||||
| -rw-r--r-- | src/modules/popdowns/index.tsx | 2 | ||||
| -rw-r--r-- | src/modules/popdowns/sideleft.tsx | 203 | ||||
| -rw-r--r-- | src/services/cpu.ts | 45 | ||||
| -rw-r--r-- | src/services/gpu.ts | 55 | ||||
| -rw-r--r-- | src/services/memory.ts | 60 | ||||
| -rw-r--r-- | src/services/storage.ts | 61 |
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()); + } +} |