From 68142eac7c2ca97a9bfa79fef82982b89c74f9cb Mon Sep 17 00:00:00 2001 From: rocketcamel Date: Wed, 24 Sep 2025 21:11:28 -0700 Subject: [PATCH] feat: move status-bar to seperate repository, initial commit --- .gitignore | 2 + app.ts | 29 +++++++++ bun.lock | 14 +++++ env.d.ts | 21 +++++++ flake.lock | 70 ++++++++++++++++++++++ flake.nix | 62 +++++++++++++++++++ icons/nixos-2.svg | 1 + icons/nixos-3.svg | 1 + icons/nixos.svg | 1 + package-lock.json | 21 +++++++ package.json | 6 ++ style.scss | 134 ++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 14 +++++ widget/Bar.tsx | 46 +++++++++++++++ widget/audio.tsx | 17 ++++++ widget/battery.tsx | 21 +++++++ widget/cpu-widget.tsx | 19 ++++++ widget/cpu.ts | 58 ++++++++++++++++++ widget/disk.tsx | 10 ++++ widget/network.tsx | 47 +++++++++++++++ widget/ram.tsx | 16 +++++ widget/time.tsx | 15 +++++ widget/title.tsx | 16 +++++ widget/workspaces.tsx | 23 ++++++++ 24 files changed, 664 insertions(+) create mode 100644 .gitignore create mode 100644 app.ts create mode 100644 bun.lock create mode 100644 env.d.ts create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 icons/nixos-2.svg create mode 100644 icons/nixos-3.svg create mode 100644 icons/nixos.svg create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 style.scss create mode 100644 tsconfig.json create mode 100644 widget/Bar.tsx create mode 100644 widget/audio.tsx create mode 100644 widget/battery.tsx create mode 100644 widget/cpu-widget.tsx create mode 100644 widget/cpu.ts create mode 100644 widget/disk.tsx create mode 100644 widget/network.tsx create mode 100644 widget/ram.tsx create mode 100644 widget/time.tsx create mode 100644 widget/title.tsx create mode 100644 widget/workspaces.tsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..298eb4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +@girs/ diff --git a/app.ts b/app.ts new file mode 100644 index 0000000..20076c2 --- /dev/null +++ b/app.ts @@ -0,0 +1,29 @@ +import { App, Gdk } from "astal/gtk3"; +import style from "./style.scss"; +import Bar from "./widget/Bar"; +import { GLib } from "astal"; + +App.start({ + css: style, + icons: "icons", + main() {}, +}); + +let knownMonitors = new Set(); + +function checkMonitors() { + const currentMonitors = App.get_monitors(); + currentMonitors.forEach((monitor) => { + if (!knownMonitors.has(monitor.model)) { + knownMonitors.add(monitor.model); + Bar(monitor); + } + }); +} + +checkMonitors(); + +GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 10, () => { + checkMonitors(); + return true; +}); diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..56a42a2 --- /dev/null +++ b/bun.lock @@ -0,0 +1,14 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "astal-shell", + "dependencies": { + "astal": "/nix/store/pvb3x021mr6xknm91gqq76gy32n96vj0-astal-gjs/share/astal/gjs", + }, + }, + }, + "packages": { + "astal": ["astal@file:../../../../nix/store/pvb3x021mr6xknm91gqq76gy32n96vj0-astal-gjs/share/astal/gjs", {}], + } +} diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..467c0a4 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,21 @@ +declare const SRC: string + +declare module "inline:*" { + const content: string + export default content +} + +declare module "*.scss" { + const content: string + export default content +} + +declare module "*.blp" { + const content: string + export default content +} + +declare module "*.css" { + const content: string + export default content +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..562d628 --- /dev/null +++ b/flake.lock @@ -0,0 +1,70 @@ +{ + "nodes": { + "ags": { + "inputs": { + "astal": "astal", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1744557573, + "narHash": "sha256-XAyj0iDuI51BytJ1PwN53uLpzTDdznPDQFG4RwihlTQ=", + "owner": "aylur", + "repo": "ags", + "rev": "3ed9737bdbc8fc7a7c7ceef2165c9109f336bff6", + "type": "github" + }, + "original": { + "owner": "aylur", + "repo": "ags", + "type": "github" + } + }, + "astal": { + "inputs": { + "nixpkgs": [ + "ags", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1742571008, + "narHash": "sha256-5WgfJAeBpxiKbTR/gJvxrGYfqQRge5aUDcGKmU1YZ1Q=", + "owner": "aylur", + "repo": "astal", + "rev": "dc0e5d37abe9424c53dcbd2506a4886ffee6296e", + "type": "github" + }, + "original": { + "owner": "aylur", + "repo": "astal", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1750365781, + "narHash": "sha256-XE/lFNhz5lsriMm/yjXkvSZz5DfvKJLUjsS6pP8EC50=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "08f22084e6085d19bcfb4be30d1ca76ecb96fe54", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "ags": "ags", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..7554ded --- /dev/null +++ b/flake.nix @@ -0,0 +1,62 @@ +{ + description = "Desktop widgets"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + + ags = { + url = "github:aylur/ags"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + ags, + }: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + + extraPkgs = with ags.packages.${system}; [ + hyprland + battery + wireplumber + network + ]; + in + { + packages.${system} = { + status-bar = ags.lib.bundle { + inherit pkgs; + src = ./.; + name = "status-bar"; + entry = "app.ts"; + + extraPackages = + with pkgs; + extraPkgs + ++ [ + libgtop + ]; + }; + }; + + devShells.${system} = { + default = pkgs.mkShell { + buildInputs = [ + (ags.packages.${system}.default.override { + extraPackages = + with pkgs; + extraPkgs + ++ [ + libgtop + ]; + }) + ]; + }; + }; + }; +} diff --git a/icons/nixos-2.svg b/icons/nixos-2.svg new file mode 100644 index 0000000..40e5de8 --- /dev/null +++ b/icons/nixos-2.svg @@ -0,0 +1 @@ +NixOS \ No newline at end of file diff --git a/icons/nixos-3.svg b/icons/nixos-3.svg new file mode 100644 index 0000000..fb26b80 --- /dev/null +++ b/icons/nixos-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/nixos.svg b/icons/nixos.svg new file mode 100644 index 0000000..63e504c --- /dev/null +++ b/icons/nixos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..635a9c5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "astal-shell", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "astal-shell", + "dependencies": { + "astal": "/nix/store/pvb3x021mr6xknm91gqq76gy32n96vj0-astal-gjs/share/astal/gjs" + } + }, + "../../../../nix/store/pvb3x021mr6xknm91gqq76gy32n96vj0-astal-gjs/share/astal/gjs": { + "name": "astal", + "license": "LGPL-2.1" + }, + "node_modules/astal": { + "resolved": "../../../../nix/store/pvb3x021mr6xknm91gqq76gy32n96vj0-astal-gjs/share/astal/gjs", + "link": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..32f5d43 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "astal-shell", + "dependencies": { + "astal": "/nix/store/pvb3x021mr6xknm91gqq76gy32n96vj0-astal-gjs/share/astal/gjs" + } +} diff --git a/style.scss b/style.scss new file mode 100644 index 0000000..522b9db --- /dev/null +++ b/style.scss @@ -0,0 +1,134 @@ +@use "sass:color"; + +$bg: #212223; +$fg: #f1f1f1; +$accent: #378DF7; +$accent-white: #ffffff; +$radius: 7px; +$bg-color-6: rgb(40, 42, 54); +$inactive-bg-color: rgb(68, 71, 90); + +window.Bar { + border: none; + box-shadow: none; + font-family: Dejavu Sans Mono; + background-color: color.adjust($bg, $alpha: -0.2); + color: $fg; + font-size: 1.1em; + + label { + margin: 0 8px; + } + + .status-box { + background-color: $bg-color-6; + padding: 0 4px; + margin: 0 2px; + font-size: 16px; + } + + .inactive { + background-color: $inactive-bg-color; + } + + .client-title label { + font-weight: bold; + } + + .nix-icon { + font-size: 24px; + padding: 0 2px; + background-color: #003366; + border-top-right-radius: 4px; + border: none; + } + + .workspaces { + font-weight: bold; + + button { + all: unset; + background-color: transparent; + + &:hover label { + background-color: color.adjust($fg, $alpha: -0.84); + border-color: color.adjust($accent, $alpha: -0.8); + } + + &:active label { + background-color: color.adjust($fg, $alpha: -0.8) + } + } + + label { + transition: 200ms; + padding: 3px 8px; + margin: 0px; + border-radius: 0; + border: 3px solid transparent; + } + + .focused label { + background-color: color.adjust($accent-white, $alpha: -0.84); + border-bottom: 3px solid $accent-white; + } + } + + .SysTray { + margin-right: 8px; + + button { + padding: 0 4px; + } + } + + .FocusedClient { + color: $accent; + } + + .Media .Cover { + min-height: 1.2em; + min-width: 1.2em; + border-radius: $radius; + background-position: center; + background-size: contain; + } + + .Battery label { + padding-left: 0; + margin-left: 0; + } + + .AudioSlider { + * { + all: unset; + } + + icon { + margin-right: .6em; + } + + & { + margin: 0 1em; + } + + trough { + background-color: color.adjust($fg, $alpha: -0.8); + border-radius: $radius; + } + + highlight { + background-color: $accent; + min-height: .8em; + border-radius: $radius; + } + + slider { + background-color: $fg; + border-radius: $radius; + min-height: 1em; + min-width: 1em; + margin: -.2em; + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..58e16ed --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "experimentalDecorators": true, + "strict": false, + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + // "checkJs": true, + // "allowJs": true, + "jsx": "react-jsx", + "jsxImportSource": "astal/gtk3", + } +} diff --git a/widget/Bar.tsx b/widget/Bar.tsx new file mode 100644 index 0000000..8b9990c --- /dev/null +++ b/widget/Bar.tsx @@ -0,0 +1,46 @@ +import { App, Astal, Gtk, Gdk } from "astal/gtk3"; +import Workspaces from "./workspaces"; +import Audio from "./audio"; +import NetworkModule from "./network"; +import Cpu from "./cpu-widget"; +import Ram from "./ram"; +import Disk from "./disk"; +import Battery from "./battery"; +import Time from "./time"; +import Title from "./title"; + +export default function Bar(gdkmonitor: Gdk.Monitor) { + const { TOP, LEFT, RIGHT } = Astal.WindowAnchor; + + //@ts-ignore + return ( + + + + + + + + + + + </box> + <box hexpand halign={Gtk.Align.END}> + <Audio /> + <NetworkModule /> + <Cpu /> + <Ram /> + <Disk /> + <Battery /> + <Time /> + </box> + </centerbox> + </window> + ); +} diff --git a/widget/audio.tsx b/widget/audio.tsx new file mode 100644 index 0000000..ec3db8a --- /dev/null +++ b/widget/audio.tsx @@ -0,0 +1,17 @@ +import { bind, Variable } from "astal" +import AstalWp from "gi://AstalWp?version=0.1" + +export default function Audio() { + const speaker = AstalWp.get_default()?.default_speaker! + const derived = Variable.derive([bind(speaker, "volume"), bind(speaker, "mute")], (volume: number, muted: boolean) => { + if (muted) { + return { label: ` (muted)`, muted } + } + return { label: `${Math.floor(volume * 100)}% ` } + }) + + return <box className={derived((v) => ["status-box", v.muted && "inactive"].filter(Boolean).join(" "))}> + <label + label={derived((v) => v.label)} /> + </box> +} diff --git a/widget/battery.tsx b/widget/battery.tsx new file mode 100644 index 0000000..ed08c49 --- /dev/null +++ b/widget/battery.tsx @@ -0,0 +1,21 @@ +import { bind, Variable } from "astal" +import AstalBattery from "gi://AstalBattery?version=0.1" + +export default function Battery() { + const battery = AstalBattery.get_default() + const battery_info = Variable.derive([bind(battery, "percentage"), bind(battery, "charging")], (percentage, charging) => { + const full_percentage = Math.floor(percentage * 100) + if (charging) { + return { label: `${full_percentage == 100 ? "FULL" : "CHR"}: ${full_percentage}%` } + } + return { label: `${full_percentage == 100 ? "FULL" : "BAT"}: ${full_percentage}%` } + }) + + if (!battery.is_battery) { + return <></> + } + return <box className="status-box"> + <label label={battery_info((i) => i.label)} /> + </box> +} + diff --git a/widget/cpu-widget.tsx b/widget/cpu-widget.tsx new file mode 100644 index 0000000..3d8af80 --- /dev/null +++ b/widget/cpu-widget.tsx @@ -0,0 +1,19 @@ +import { bind, GLib, Variable } from "astal"; +import { calc_cpu_usage, get_cpu_snapshot } from "./cpu"; + +let s1 = get_cpu_snapshot(); +let cpu_usage_percent = Variable(0); +GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => { + const s2 = get_cpu_snapshot(); + cpu_usage_percent.set(calc_cpu_usage(s1, s2)); + s1 = s2; + + return true; +}); + +export default function Cpu() { + return <box className="status-box"> + <label label={bind(cpu_usage_percent).as((u) => `${u}% `)} /> + </box> +} + diff --git a/widget/cpu.ts b/widget/cpu.ts new file mode 100644 index 0000000..3863c3a --- /dev/null +++ b/widget/cpu.ts @@ -0,0 +1,58 @@ +import { GLib, Variable } from "astal"; +import GTop from "gi://GTop"; +import Wp05 from "gi://Wp"; + +type Snapshot = { + total: number; + user: number; + sys: number; + idle: number; +}; + +export function get_cpu_snapshot() { + const cpu = new GTop.glibtop_cpu(); + GTop.glibtop_get_cpu(cpu); + return { + total: cpu.total, + user: cpu.user + cpu.nice, + sys: cpu.sys, + idle: cpu.idle, + }; +} + +export function calc_cpu_usage(a: Snapshot, b: Snapshot) { + const total_diff = b.total - a.total; + const active_diff = b.user + b.sys - (a.user + a.sys); + return Math.round(total_diff > 0 ? (100 * active_diff) / total_diff : 0); +} + +export function get_ram_info() { + const mem = new GTop.glibtop_mem(); + GTop.glibtop_get_mem(mem); + return { + total: mem.total, + used: mem.total - mem.free - mem.cached - mem.buffer, + free: mem.free, + }; +} + +function format_bytes(bytes: number) { + let units = ["B", "KiB", "MiB", "GiB", "TiB"]; + let i = 0; + let num: number = bytes; + while (num >= 1024 && i < units.length - 1) { + num /= 1024; + i++; + } + + return `${num.toFixed(2)}${units[i]}`; +} + +export function get_disk_space() { + const usage = new GTop.glibtop_fsusage(); + GTop.glibtop_get_fsusage(usage, "/"); + + const free_bytes = usage.bavail * usage.block_size; + + return format_bytes(free_bytes); +} diff --git a/widget/disk.tsx b/widget/disk.tsx new file mode 100644 index 0000000..590fa44 --- /dev/null +++ b/widget/disk.tsx @@ -0,0 +1,10 @@ +import { bind, Variable } from "astal" +import { get_disk_space } from "./cpu" + +let disk_space = Variable(get_disk_space()).poll(5000, () => get_disk_space()) +export default function Disk() { + return <box className="status-box"> + <label label={bind(disk_space).as((s) => `${s}`)} /> + </box> +} + diff --git a/widget/network.tsx b/widget/network.tsx new file mode 100644 index 0000000..f5c8f0b --- /dev/null +++ b/widget/network.tsx @@ -0,0 +1,47 @@ +import { bind, Variable } from "astal"; +import AstalNetwork from "gi://AstalNetwork?version=0.1"; + +const disconnected_label = { label: `Disconnected ⚠` }; + +function create_label( + primary: AstalNetwork.Primary, + wired: AstalNetwork.Wired, + wifi?: AstalNetwork.Wifi, +) { + if (primary === AstalNetwork.Primary.WIRED) { + return { label: `🖧 Wired ${wired.device.interface}` }; + } + if (!wifi) { + return disconnected_label; + } + if (wifi.active_access_point !== null) { + return { label: `${wifi.ssid} (${wifi.strength}%) ` }; + } + return disconnected_label; +} + +export default function NetworkModule() { + const network = AstalNetwork.get_default(); + const wifi = network.wifi; + const wired = network.wired; + + let derived; + if (!wifi) { + derived = Variable.derive([bind(network, "primary")], (primary) => { + return create_label(primary, wired); + }); + } else { + derived = Variable.derive( + [bind(network, "primary"), bind(wifi, "ssid"), bind(wifi, "strength")], + (primary) => { + return create_label(primary, wired, wifi); + }, + ); + } + + return ( + <box className="status-box"> + <label label={derived((v) => v.label)} /> + </box> + ); +} diff --git a/widget/ram.tsx b/widget/ram.tsx new file mode 100644 index 0000000..8abea28 --- /dev/null +++ b/widget/ram.tsx @@ -0,0 +1,16 @@ +import { bind, GLib, Variable } from "astal" +import { get_ram_info } from "./cpu" + +let info = Variable(get_ram_info()) +GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => { + info.set(get_ram_info()) + + return true +}) + +export default function Ram() { + return <box className="status-box"> + <label label={bind(info).as((i) => `${Math.round((i.used / i.total) * 100)}% `)} /> + </box> +} + diff --git a/widget/time.tsx b/widget/time.tsx new file mode 100644 index 0000000..b453be8 --- /dev/null +++ b/widget/time.tsx @@ -0,0 +1,15 @@ +import { bind, GLib, Variable } from "astal" + +let current_time = Variable(GLib.DateTime.new_now_local().format("%H:%M")) +GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => { + const now = GLib.DateTime.new_now_local() + current_time.set(now.format("%b %e (%a) %H:%M")) + return true +}) +export default function Time() { + + return <box className="status-box"> + <label label={bind(current_time)} /> + </box> +} + diff --git a/widget/title.tsx b/widget/title.tsx new file mode 100644 index 0000000..8d18d12 --- /dev/null +++ b/widget/title.tsx @@ -0,0 +1,16 @@ +import { bind, Variable } from "astal" +import AstalHyprland from "gi://AstalHyprland?version=0.1"; + +const hyprland = AstalHyprland.get_default() + +function get_title() { + return hyprland.focusedClient?.title ?? "" +} + +const title = Variable(get_title()).poll(200, () => get_title()) + +export default function Title() { + return ( + <label label={title(t => t)} /> + ) +} diff --git a/widget/workspaces.tsx b/widget/workspaces.tsx new file mode 100644 index 0000000..0dffed8 --- /dev/null +++ b/widget/workspaces.tsx @@ -0,0 +1,23 @@ +import { bind } from "astal" +import { Gdk } from "astal/gtk3" +import AstalHyprland from "gi://AstalHyprland?version=0.1" + +export default function Workspaces({ monitor }: { monitor: Gdk.Monitor }) { + const hypr = AstalHyprland.get_default() + + return <box className="workspaces"> + {bind(hypr, "workspaces").as(wss => wss.filter(ws => !(ws.id >= -99 && ws.id <= -2)) + .sort((a, b) => a.id - b.id) + .map(ws => { + if (ws.monitor.model !== monitor.model) return <></> + return ( + <button + className={bind(hypr, "focusedWorkspace").as(fw => + ws === fw ? "focused" : "")} + onClicked={() => ws.focus()}> + {ws.id} + </button> + ) + }))} + </box> +}