[AGS] Bar: Done (WiFi still missing, will be added at some later point)
This commit is contained in:
26
config/astal/components/bar/modules/Calendar.tsx
Normal file
26
config/astal/components/bar/modules/Calendar.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { GLib, Variable } from "astal";
|
||||
import { Gtk } from "astal/gtk4";
|
||||
|
||||
const Time = ({ format = "%a, %e.%m %H:%M:%S" }) => {
|
||||
const time = Variable<string>("").poll(
|
||||
1000,
|
||||
() => GLib.DateTime.new_now_local().format(format)!,
|
||||
);
|
||||
|
||||
return (
|
||||
<menubutton
|
||||
cssClasses={["time", "bar-button"]}
|
||||
hexpand
|
||||
halign={Gtk.Align.CENTER}
|
||||
>
|
||||
<label onDestroy={() => time.drop()} label={time()} halign={Gtk.Align.CENTER}></label>
|
||||
<popover>
|
||||
<Gtk.Calendar />
|
||||
</popover>
|
||||
</menubutton>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
Time,
|
||||
};
|
197
config/astal/components/bar/modules/Hyprland.tsx
Normal file
197
config/astal/components/bar/modules/Hyprland.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import AstalTray from "gi://AstalTray";
|
||||
import { bind, GObject } from "astal";
|
||||
import AstalHyprland from "gi://AstalHyprland";
|
||||
import { Gtk } from "astal/gtk4";
|
||||
|
||||
const SYNC = GObject.BindingFlags.SYNC_CREATE;
|
||||
|
||||
const SysTray = () => {
|
||||
const trayBox = new Gtk.Box({ cssClasses: ["bar-button"] });
|
||||
const tray = AstalTray.get_default();
|
||||
|
||||
const trayItems = new Map<string, Gtk.MenuButton>();
|
||||
const trayAddedHandler = tray.connect("item-added", (_, id) => {
|
||||
const item = tray.get_item(id);
|
||||
const popover = Gtk.PopoverMenu.new_from_model(item.menu_model);
|
||||
const icon = new Gtk.Image();
|
||||
const button = new Gtk.MenuButton({
|
||||
popover,
|
||||
child: icon,
|
||||
cssClasses: ["tray-item"],
|
||||
});
|
||||
|
||||
item.bind_property("gicon", icon, "gicon", SYNC);
|
||||
popover.insert_action_group("dbusmenu", item.action_group);
|
||||
item.connect("notify::action-group", () => {
|
||||
popover.insert_action_group("dbusmenu", item.action_group);
|
||||
});
|
||||
|
||||
trayItems.set(id, button);
|
||||
trayBox.append(button);
|
||||
});
|
||||
|
||||
const trayRemovedHandler = tray.connect("item-removed", (_, id) => {
|
||||
const button = trayItems.get(id);
|
||||
if (button) {
|
||||
trayBox.remove(button);
|
||||
button.run_dispose();
|
||||
trayItems.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
trayBox.connect("destroy", () => {
|
||||
tray.disconnect(trayAddedHandler);
|
||||
tray.disconnect(trayRemovedHandler);
|
||||
});
|
||||
|
||||
return trayBox;
|
||||
};
|
||||
|
||||
const Workspace = () => {
|
||||
const hypr = AstalHyprland.get_default();
|
||||
|
||||
return (
|
||||
<box>
|
||||
{bind(hypr, "workspaces").as(wss =>
|
||||
wss
|
||||
.filter(ws => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map(ws => (
|
||||
<button
|
||||
cssClasses={bind(hypr, "focusedWorkspace").as(fw =>
|
||||
ws === fw
|
||||
? [
|
||||
"focused-workspace-button",
|
||||
"workspace-button",
|
||||
]
|
||||
: ["workspace-button"],
|
||||
)}
|
||||
onButtonPressed={() => ws.focus()}
|
||||
child={<label label={String(ws.id)}></label>}
|
||||
></button>
|
||||
)),
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays the name of the currently active window and provides a popover for
|
||||
* displaying all available clients
|
||||
*/
|
||||
const ActiveWindow = () => {
|
||||
const hypr = AstalHyprland.get_default();
|
||||
const focused = bind(hypr, "focusedClient");
|
||||
|
||||
const WindowPopover = (): Gtk.Popover => {
|
||||
// Set up boxes + Popover
|
||||
const clients = new Map<string, Gtk.Button>();
|
||||
const popover = new Gtk.Popover();
|
||||
const popoverBox = new Gtk.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
});
|
||||
|
||||
const widgetTitle = new Gtk.Label({
|
||||
cssClasses: ["title-2"],
|
||||
label: "Available Windows",
|
||||
});
|
||||
|
||||
popoverBox.append(widgetTitle);
|
||||
|
||||
const seaparator = new Gtk.Separator({
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
});
|
||||
|
||||
popoverBox.append(seaparator);
|
||||
|
||||
const addClient = (client: AstalHyprland.Client) => {
|
||||
const clientBox = new Gtk.Box();
|
||||
|
||||
// Workspace description
|
||||
const descWS = new Gtk.Label({ label: "(WS " });
|
||||
|
||||
// Workpsace information
|
||||
const workspace = new Gtk.Label();
|
||||
client.workspace.bind_property("name", workspace, "label", SYNC);
|
||||
|
||||
const windowClassDesc = new Gtk.Label({ label: ") [" });
|
||||
|
||||
const windowClass = new Gtk.Label();
|
||||
windowClass.label = client.get_initial_class();
|
||||
|
||||
const titleDesc = new Gtk.Label({ label: "] " });
|
||||
titleDesc.set_margin_end(2);
|
||||
|
||||
const title = new Gtk.Label();
|
||||
client.bind_property("title", title, "label", SYNC);
|
||||
|
||||
clientBox.append(descWS);
|
||||
clientBox.append(workspace);
|
||||
clientBox.append(windowClassDesc);
|
||||
clientBox.append(windowClass);
|
||||
clientBox.append(titleDesc);
|
||||
clientBox.append(title);
|
||||
|
||||
const button = new Gtk.Button();
|
||||
button.connect( 'clicked', () => {
|
||||
client.workspace.focus();
|
||||
} );
|
||||
button.set_child(clientBox);
|
||||
|
||||
popoverBox.append(button);
|
||||
|
||||
clients.set(client.get_address(), button);
|
||||
};
|
||||
|
||||
// Populate with already added clients
|
||||
const c = hypr.get_clients();
|
||||
for (let index = 0; index < c.length; index++) {
|
||||
addClient(c[index]);
|
||||
}
|
||||
|
||||
hypr.connect("client-added", (_, client) => {
|
||||
addClient(client);
|
||||
});
|
||||
|
||||
hypr.connect("client-removed", (_, client) => {
|
||||
const c = clients.get(client);
|
||||
if (c) {
|
||||
popoverBox.remove(c);
|
||||
c.run_dispose();
|
||||
clients.delete(client);
|
||||
}
|
||||
});
|
||||
|
||||
popover.set_child(popoverBox);
|
||||
return popover;
|
||||
};
|
||||
|
||||
const windowPopover = WindowPopover();
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Return fully assembled HyprlandFocusedClient box
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<box visible={focused.as(Boolean)}>
|
||||
<button
|
||||
onClicked={() => windowPopover.popup()}
|
||||
cssClasses={["bar-button"]}
|
||||
>
|
||||
{focused.as(
|
||||
client =>
|
||||
client && (
|
||||
<label label={bind(client, "title").as(String)} />
|
||||
),
|
||||
)}
|
||||
</button>
|
||||
{windowPopover}
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
Workspace,
|
||||
ActiveWindow,
|
||||
SysTray,
|
||||
};
|
186
config/astal/components/bar/modules/QuickView.tsx
Normal file
186
config/astal/components/bar/modules/QuickView.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { bind } from "astal";
|
||||
import AstalBattery from "gi://AstalBattery";
|
||||
import AstalBluetooth from "gi://AstalBluetooth";
|
||||
import AstalNetwork from "gi://AstalNetwork";
|
||||
import AstalWp from "gi://AstalWp";
|
||||
import { Gtk } from "astal/gtk4";
|
||||
import Brightness from "../../../util/brightness";
|
||||
import QuickActions from "../../QuickActions/QuickActions";
|
||||
|
||||
const STATE = AstalNetwork.DeviceState;
|
||||
|
||||
const QuickView = () => {
|
||||
const qa = QuickActions.QuickActions();
|
||||
const showQuickActions = () => {
|
||||
qa.popup();
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClicked={() => showQuickActions()}
|
||||
cssClasses={["quick-action-button"]}
|
||||
child={
|
||||
<box>
|
||||
<BatteryWidget></BatteryWidget>
|
||||
<Audio></Audio>
|
||||
<BluetoothWidget></BluetoothWidget>
|
||||
<NetworkWidget></NetworkWidget>
|
||||
<BrightnessWidget></BrightnessWidget>
|
||||
<image iconName={"system-shutdown-symbolic"}></image>
|
||||
{qa}
|
||||
</box>
|
||||
}
|
||||
></button>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkWidget = () => {
|
||||
const network = AstalNetwork.get_default();
|
||||
|
||||
return (
|
||||
<box>
|
||||
<image
|
||||
iconName={bind(network, "state").as(state => {
|
||||
if (state === AstalNetwork.State.CONNECTING) {
|
||||
return "chronometer-reset-symbolic";
|
||||
} else if (
|
||||
state === AstalNetwork.State.CONNECTED_LOCAL ||
|
||||
state === AstalNetwork.State.CONNECTED_SITE ||
|
||||
state === AstalNetwork.State.CONNECTED_GLOBAL
|
||||
) {
|
||||
return "network-wired-activated-symbolic";
|
||||
} else {
|
||||
return "paint-unknown-symbolic";
|
||||
}
|
||||
})}
|
||||
cssClasses={["network-widget", "quick-view-symbol"]}
|
||||
visible={bind(network.wifi, "state").as(
|
||||
state => state !== STATE.ACTIVATED,
|
||||
)}
|
||||
></image>
|
||||
<image
|
||||
iconName={bind(network.wifi, "state").as(state => {
|
||||
if (state === STATE.ACTIVATED) {
|
||||
return network.wifi.iconName;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
})}
|
||||
cssClasses={["network-widget", "quick-view-symbol"]}
|
||||
visible={bind(network.wifi, "state").as(
|
||||
state => state === STATE.ACTIVATED,
|
||||
)}
|
||||
></image>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
const BluetoothWidget = () => {
|
||||
const bluetooth = AstalBluetooth.get_default();
|
||||
const enabled = bind(bluetooth.adapter, "powered");
|
||||
const connected = bind(bluetooth, "isConnected");
|
||||
|
||||
// For each connected BT device, render status
|
||||
return (
|
||||
<box>
|
||||
<box visible={enabled.as(e => e)}>
|
||||
<image
|
||||
iconName={"bluetooth-active-symbolic"}
|
||||
visible={connected.as(c => c)}
|
||||
></image>
|
||||
<image
|
||||
iconName={"bluetooth-disconnected-symbolic"}
|
||||
visible={connected.as(c => !c)}
|
||||
></image>
|
||||
</box>
|
||||
<image
|
||||
iconName={"bluetooth-disabled-symbolic"}
|
||||
visible={enabled.as(e => !e)}
|
||||
></image>
|
||||
<box>
|
||||
{bind(bluetooth, "devices").as(devices => {
|
||||
return devices.map(device => {
|
||||
return (
|
||||
<box visible={bind(device, "connected").as(c => c)}>
|
||||
<image
|
||||
iconName={bind(device, "icon").as(
|
||||
icon => icon,
|
||||
)}
|
||||
></image>
|
||||
<label
|
||||
label={bind(device, "batteryPercentage").as(
|
||||
n => {
|
||||
return n + "%";
|
||||
},
|
||||
)}
|
||||
></label>
|
||||
</box>
|
||||
);
|
||||
});
|
||||
})}
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
const BatteryWidget = () => {
|
||||
const battery = AstalBattery.get_default();
|
||||
if (battery.get_is_present()) {
|
||||
return (
|
||||
<image
|
||||
iconName={bind(battery, "iconName").as(icon => icon)}
|
||||
cssClasses={["quick-view-symbol"]}
|
||||
></image>
|
||||
);
|
||||
} else {
|
||||
return <box></box>;
|
||||
}
|
||||
// Else, no battery available -> Don't show the widget
|
||||
};
|
||||
|
||||
const BrightnessWidget = () => {
|
||||
const brightness = Brightness.get_default();
|
||||
const screen_brightness = bind(brightness, "screen");
|
||||
|
||||
return (
|
||||
<label
|
||||
label={"🌣" + screen_brightness}
|
||||
visible={bind(brightness, "screenAvailable")}
|
||||
cssClasses={["quick-view-symbol"]}
|
||||
></label>
|
||||
);
|
||||
};
|
||||
|
||||
const Audio = () => {
|
||||
const wireplumber = AstalWp.get_default();
|
||||
if (wireplumber) {
|
||||
return (
|
||||
<box orientation={Gtk.Orientation.HORIZONTAL}>
|
||||
<image
|
||||
iconName={bind(wireplumber.defaultSpeaker, "volumeIcon").as(
|
||||
icon => icon,
|
||||
)}
|
||||
cssClasses={["quick-view-symbol"]}
|
||||
></image>
|
||||
<image
|
||||
iconName={bind(
|
||||
wireplumber.defaultMicrophone,
|
||||
"volumeIcon",
|
||||
).as(icon => icon)}
|
||||
cssClasses={["quick-view-symbol"]}
|
||||
></image>
|
||||
</box>
|
||||
);
|
||||
} else {
|
||||
print(
|
||||
"[ WirePlumber ] Could not connect, Audio support in bar will be missing",
|
||||
);
|
||||
return <image iconName={"action-unavailable-symbolic"}></image>;
|
||||
}
|
||||
};
|
||||
|
||||
// cssClasses={[ 'quick-view-symbol' ]}
|
||||
|
||||
export default {
|
||||
QuickView,
|
||||
};
|
211
config/astal/components/bar/modules/SystemInfo.tsx
Normal file
211
config/astal/components/bar/modules/SystemInfo.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { exec, execAsync, GLib, interval, Variable } from "astal";
|
||||
import { Gtk } from "astal/gtk4";
|
||||
import AstalBattery from "gi://AstalBattery?version=0.1";
|
||||
|
||||
const FETCH_INTERVAL = 2000;
|
||||
|
||||
const cpuUtil = Variable("0%");
|
||||
const ramUtil = Variable("0%");
|
||||
const ramUsed = Variable("0MiB");
|
||||
const gpuUtil = Variable("0%");
|
||||
let gpuName = "card1";
|
||||
let enabled = false;
|
||||
|
||||
const refreshStats = (): Stats => {
|
||||
gpuName = exec(`/bin/bash -c "ls /sys/class/drm/ | grep '^card[0-9]*$'"`);
|
||||
const cpuNameInSensors = "CPUTIN";
|
||||
const stats = {
|
||||
kernel: exec("uname -sr"),
|
||||
netSpeed: exec(
|
||||
`/bin/bash -c "interface=$(ip route get 8.8.8.8 | awk '{print $5; exit}') && cat \"/sys/class/net/$interface/speed\""`,
|
||||
),
|
||||
cpuTemp: exec(
|
||||
`/bin/bash -c "sensors | grep -m1 ${cpuNameInSensors} | awk '{print $2}'"`,
|
||||
),
|
||||
cpuClk: exec(
|
||||
`awk '/cpu MHz/ {sum+=$4; ++n} END {print sum/n " MHz"}' /proc/cpuinfo`,
|
||||
),
|
||||
gpuTemp: exec(
|
||||
`/bin/bash -c "sensors | grep -E 'edge' | awk '{print $2}'"`,
|
||||
),
|
||||
gpuClk: exec(
|
||||
`/bin/bash -c "cat /sys/class/drm/${gpuName}/device/pp_dpm_sclk | grep '\\*' | awk '{print $2 $3}'"`,
|
||||
),
|
||||
vram:
|
||||
Math.round(
|
||||
parseInt(
|
||||
exec(
|
||||
`cat /sys/class/drm/${gpuName}/device/mem_info_vram_used`,
|
||||
),
|
||||
) /
|
||||
1024 /
|
||||
1024,
|
||||
) + "MiB",
|
||||
availableVRAM:
|
||||
Math.round(
|
||||
parseInt(
|
||||
exec(
|
||||
`cat /sys/class/drm/${gpuName}/device/mem_info_vram_total`,
|
||||
),
|
||||
) /
|
||||
1024 /
|
||||
1024,
|
||||
) + "MiB",
|
||||
};
|
||||
|
||||
return stats;
|
||||
};
|
||||
|
||||
const systemStats: Variable<Stats> = Variable(refreshStats());
|
||||
|
||||
const availableFeatures = {
|
||||
cpu: true,
|
||||
ram: true,
|
||||
};
|
||||
|
||||
const featureTest = () => {
|
||||
// Check if awk & sed are available
|
||||
try {
|
||||
exec("awk -V");
|
||||
exec("sed --version");
|
||||
enabled = true;
|
||||
} catch (e) {
|
||||
printerr(
|
||||
"[ SysInfo ] AWK or SED missing! No system info will be available",
|
||||
);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if mpstat is available
|
||||
try {
|
||||
exec("mpstat -V");
|
||||
} catch (e) {
|
||||
availableFeatures.cpu = false;
|
||||
printerr(
|
||||
"[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const info = () => {
|
||||
return (
|
||||
<box vertical>
|
||||
<label
|
||||
label={"System Information"}
|
||||
cssClasses={["title-2"]}
|
||||
></label>
|
||||
<Gtk.Separator marginTop={5} marginBottom={10}></Gtk.Separator>
|
||||
<label
|
||||
vexpand
|
||||
halign={Gtk.Align.START}
|
||||
hexpand
|
||||
label={ramUsed(used => {
|
||||
return "RAM: " + used + ` (${ramUtil.get()}%)`;
|
||||
})}
|
||||
></label>
|
||||
<label
|
||||
label={systemStats(stats => {
|
||||
return `CPU: ${stats.cpuTemp}, ${stats.cpuClk}
|
||||
GPU: ${stats.gpuTemp}, ${stats.gpuClk} (${stats.vram} / ${stats.availableVRAM})
|
||||
Network: ${stats.netSpeed} mb/s
|
||||
Kernel: ${stats.kernel}`;
|
||||
})}
|
||||
></label>
|
||||
<Gtk.Separator marginTop={10}></Gtk.Separator>
|
||||
<button
|
||||
onClicked={() => exec( `/bin/sh -c "kitty --hold fish -c 'fastfetch'"` )}
|
||||
child={
|
||||
<label label={"View FastFetch"}></label>
|
||||
}></button>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
const SystemInformationPanel = () => {
|
||||
const popover = new Gtk.Popover();
|
||||
|
||||
popover.set_child(info());
|
||||
|
||||
return popover;
|
||||
};
|
||||
|
||||
const sysInfoFetcher = () => {
|
||||
if (enabled) {
|
||||
if (availableFeatures.cpu) {
|
||||
cpuUtil.set(
|
||||
"" +
|
||||
Math.round(
|
||||
parseFloat(exec(`/bin/fish -c cpu-utilization`)),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (availableFeatures.ram) {
|
||||
ramUtil.set(
|
||||
"" +
|
||||
Math.round(
|
||||
parseFloat(
|
||||
exec(
|
||||
`/bin/bash -c "free | awk '/Mem/ { printf(\\"%.2f\\\\n\\", ($3/$2)*100) }'"`,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
ramUsed.set(
|
||||
exec(
|
||||
`/bin/bash -c \"free -h | awk '/^Mem:/ {print $3 \\" used of \\" $2}'\"`,
|
||||
)
|
||||
.replaceAll("Gi", "GiB")
|
||||
.replaceAll("Mi", "MiB"),
|
||||
);
|
||||
}
|
||||
gpuUtil.set(exec("cat /sys/class/drm/card1/device/gpu_busy_percent"));
|
||||
}
|
||||
};
|
||||
|
||||
const panel = SystemInformationPanel();
|
||||
|
||||
const SystemInfo = () => {
|
||||
featureTest();
|
||||
|
||||
const openSysInfo = async () => {
|
||||
panel.popup();
|
||||
systemStats.set(refreshStats());
|
||||
};
|
||||
|
||||
if (enabled) {
|
||||
sysInfoFetcher();
|
||||
interval(FETCH_INTERVAL, sysInfoFetcher);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClicked={() => openSysInfo()}
|
||||
child={
|
||||
<box tooltipText={ramUsed(v => v)}>
|
||||
<image
|
||||
iconName={"power-profile-performance-symbolic"}
|
||||
marginEnd={1}
|
||||
></image>
|
||||
<label
|
||||
label={cpuUtil(util => util)}
|
||||
marginEnd={5}
|
||||
></label>
|
||||
<image iconName={"histogram-symbolic"}></image>
|
||||
<label label={ramUtil(util => util)}></label>
|
||||
<image iconName={"show-gpu-effects-symbolic"}></image>
|
||||
<label label={gpuUtil(util => util)}></label>
|
||||
{panel}
|
||||
</box>
|
||||
}
|
||||
cssClasses={["bar-button"]}
|
||||
></button>
|
||||
);
|
||||
} else {
|
||||
return <image iconName={"action-unavailable-symbolic"}></image>;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
SystemInfo,
|
||||
panel,
|
||||
};
|
10
config/astal/components/bar/modules/stats.d.ts
vendored
Normal file
10
config/astal/components/bar/modules/stats.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
interface Stats {
|
||||
kernel: string;
|
||||
netSpeed: string;
|
||||
cpuTemp: string;
|
||||
cpuClk: string;
|
||||
gpuTemp: string;
|
||||
gpuClk: string;
|
||||
vram: string;
|
||||
availableVRAM: string;
|
||||
}
|
Reference in New Issue
Block a user