[AGS] Bar: Done (WiFi still missing, will be added at some later point)

This commit is contained in:
2025-04-24 16:58:30 +02:00
parent e93e051094
commit 10136ab9de
48 changed files with 1423 additions and 548 deletions

View File

@@ -23,7 +23,6 @@ const Bar = (gdkmonitor: Gdk.Monitor) => {
<box
hexpand
halign={Gtk.Align.START}
cssClasses={["BarLeft"]}
>
<Calendar.Time />
<SystemInfo.SystemInfo />

View File

@@ -0,0 +1,62 @@
@use "../../util/colours.scss" as *;
window.Bar {
font-family: "Comfortaa, sans-serif";
background: transparent;
color: $fg-color;
font-weight: bold;
/* >centerbox { */
/* background: $bg-color; */
/* border-radius: 10px; */
/* margin: 8px; */
/* } */
.bar-button {
border-radius: 20px;
margin: 2px;
padding-left: 10px;
padding-right: 10px;
background-color: $bg-color;
& button {
background-color: $bg-color;
}
}
.quick-action-button {
border-radius: 20px;
margin: 2px;
padding-left: 10px;
padding-right: 10px;
background-color: $bg-color;
}
button.workspace-button {
border-radius: 20px;
margin: 1px;
&.focused-workspace-button {
color: $accent-color-2;
}
}
.tray-item {
margin: 0;
padding: 0;
& button {
margin: 2px;
box-shadow: none;
}
}
.time {
min-width: 11rem;
padding: 3px;
& button {
box-shadow: none;
padding: 0;
}
}
}

View 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,
};

View 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,
};

View 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,
};

View 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,
};

View File

@@ -1,28 +0,0 @@
import { Gtk } from "astal/gtk4"
import Power from "./modules/Power";
import Audio from "./modules/Audio/Audio";
import Bluetooth from "./modules/Bluetooth/Bluetooth";
const QuickActions = () => {
const popover = new Gtk.Popover( { cssClasses: [ 'quick-actions-popover' ] } );
popover.set_child( createQuickActionMenu() );
return popover;
}
const createQuickActionMenu = () => {
// TODO: For the future add WiFi / Networking back, for the time being remove, as unnecessary effort
return <box visible cssClasses={[ 'quick-actions' ]} vertical>
<Power></Power>
<Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
<Audio.AudioModule></Audio.AudioModule>
</box>
}
// TODO: Expose additional functions to be usable through CLI
export default {
QuickActions
};

View File

@@ -1,178 +0,0 @@
import { bind, Binding } from "astal";
import { Gtk } from "astal/gtk4";
import AstalWp from "gi://AstalWp";
const wp = AstalWp.get_default()!;
const AudioModule = () => {
const setVolumeSpeaker = (volume: number) => {
wp.defaultSpeaker.set_volume(volume / 100);
};
const setVolumeMicrophone = (volume: number) => {
wp.defaultMicrophone.set_volume(volume / 100);
};
const speakerSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_SPEAKER);
const micSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_MICROPHONE);
return (
<box cssClasses={["audio-box"]} vertical>
<box>
<button
onClicked={() =>
wp.defaultSpeaker.set_mute(
!wp.defaultSpeaker.get_mute(),
)
}
child={
<image
iconName={bind(wp.defaultSpeaker, "volumeIcon")}
marginEnd={3}
></image>
}
></button>
<label
label={bind(wp.defaultMicrophone, "volume").as(
v => Math.round(100 * v) + "%",
)}
></label>
<slider
value={bind(wp.defaultSpeaker, "volume").as(v => 100 * v)}
max={100}
min={0}
step={1}
widthRequest={100}
onChangeValue={self => setVolumeSpeaker(self.value)}
></slider>
<button
cssClasses={["sink-select-button"]}
child={
<box>
<image iconName={"speaker-symbolic"}></image>
{speakerSelector}
</box>
}
onClicked={() => speakerSelector.popup()}
></button>
</box>
<box>
<button
onClicked={() =>
wp.defaultMicrophone.set_mute(
!wp.defaultMicrophone.get_mute(),
)
}
child={
<image
iconName={bind(wp.defaultMicrophone, "volumeIcon")}
marginEnd={3}
></image>
}
></button>
<label
label={bind(wp.defaultMicrophone, "volume").as(
v => Math.round(100 * v) + "%",
)}
></label>
<slider
value={bind(wp.defaultMicrophone, "volume").as(
v => 100 * v,
)}
max={100}
min={0}
step={1}
widthRequest={100}
onChangeValue={self => setVolumeMicrophone(self.value)}
></slider>
<button
cssClasses={["sink-select-button"]}
child={
<box>
<image iconName={"microphone"}></image>
{micSelector}
</box>
}
onClicked={() => micSelector.popup()}
></button>
</box>
</box>
);
};
const SinkPicker = (type: AstalWp.MediaClass) => {
const devices = bind(wp, "endpoints");
return (
<box vertical>
<label
label={`Available Audio ${type === AstalWp.MediaClass.AUDIO_SPEAKER ? "Output" : type === AstalWp.MediaClass.AUDIO_MICROPHONE ? "Input" : ""} Devices`}
></label>
<Gtk.Separator marginBottom={5} marginTop={3}></Gtk.Separator>
<box vertical cssClasses={["sink-picker"]}>
{devices.as(d => {
return d.map(device => {
if (device.get_media_class() !== type) {
return <box cssClasses={[ 'empty' ]}></box>;
}
return (
<button
cssClasses={bind(device, "id").as(id => {
if (
id ===
(type ===
AstalWp.MediaClass.AUDIO_SPEAKER
? wp.defaultSpeaker.id
: type ===
AstalWp.MediaClass
.AUDIO_MICROPHONE
? wp.defaultMicrophone.id
: "")
) {
return [
"sink-option",
"currently-selected-sink-option",
];
} else {
return ["sink-option"];
}
})}
child={
<box halign={Gtk.Align.START}>
<image
iconName={bind(device, "icon").as(
icon => icon,
)}
marginEnd={3}
></image>
<label
label={bind(
device,
"description",
).as(t => t ?? "")}
></label>
</box>
}
onClicked={() => {
device.set_is_default(true);
}}
></button>
);
});
})}
</box>
</box>
);
};
const SinkSelectPopover = (type: AstalWp.MediaClass) => {
const popover = new Gtk.Popover();
popover.set_child(SinkPicker(type));
return popover;
};
export default {
AudioModule,
};

View File

@@ -1,153 +0,0 @@
import { bind } from "astal";
import { Gtk } from "astal/gtk4";
import AstalBluetooth from "gi://AstalBluetooth";
import BTDevice from "./Device";
const bt = AstalBluetooth.get_default();
const BluetoothModule = () => {
return (
<box>
<button
cssClasses={bind(bt.adapter, "powered").as(powered =>
powered
? ["bt-toggle-button", "bt-on"]
: ["bt-toggle-button"],
)}
onClicked={() =>
bt.adapter.set_powered(!bt.adapter.get_powered())
}
child={
<box vertical>
<label
cssClasses={["button-title"]}
label={"Bluetooth"}
></label>
<box>
<label
visible={bind(bt.adapter, "powered").as(
p => !p,
)}
label="Disabled"
></label>
<label
visible={bind(bt.adapter, "powered")}
label={bind(bt, "devices").as(devices => {
let count = 0;
devices.forEach(device => {
if (device.connected) {
count++;
}
});
return `On (${count} ${count === 1 ? "client" : "clients"} connected)`;
})}
></label>
</box>
<label></label>
</box>
}
></button>
<button
cssClasses={["bt-devices-button"]}
visible={bind(bt.adapter, "powered")}
child={
<box>
<image iconName={"arrow-right-symbolic"}></image>
{picker}
</box>
}
onClicked={() => openBTPicker()}
></button>
</box>
);
};
const openBTPicker = () => {
picker.popup();
try {
bt.adapter.start_discovery();
} catch (_) {}
};
const BluetoothPickerList = () => {
return (
<box vertical onDestroy={() => bt.adapter.stop_discovery()}>
<label label={"Connected devices"} cssClasses={["title-2"]}></label>
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
<box vertical cssClasses={["bt-conn-list"]}>
{bind(bt, "devices").as(devices => {
return devices
.filter(device => {
if (device.get_connected()) {
return device;
}
})
.map(device => {
return <BTDevice device={device}></BTDevice>;
});
})}
</box>
<label
visible={bind(bt, "devices").as(devices => {
return (
devices.filter(device => {
if (device.get_connected()) {
return device;
}
}).length === 0
);
})}
label={"No connected devices"}
cssClasses={["bt-no-found", "bt-conn-list"]}
></label>
<label
label={"Discovered bluetooth devices"}
cssClasses={["title-2"]}
></label>
<Gtk.Separator marginBottom={5} marginTop={3}></Gtk.Separator>
<box vertical>
{bind(bt, "devices").as(devices => {
return devices
.filter(data => {
if (!data.get_connected()) {
return data;
}
})
.map(device => {
return <BTDevice device={device}></BTDevice>;
});
})}
</box>
<label
visible={bind(bt, "devices").as(devices => {
return (
devices.filter(device => {
if (!device.get_connected()) {
return device;
}
}).length === 0
);
})}
label={"No discovered devices"}
cssClasses={["bt-no-found"]}
></label>
</box>
);
};
const BluetoothPicker = () => {
const popover = new Gtk.Popover();
popover.set_child(BluetoothPickerList());
popover.connect( 'closed', () => bt.adapter.stop_discovery() );
return popover;
};
const picker = BluetoothPicker();
export default {
BluetoothModule,
};

View File

@@ -1,52 +0,0 @@
import { bind } from "astal";
import AstalBluetooth from "gi://AstalBluetooth";
const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => {
return (
<button
visible={bind(device, "name").as(n => n !== null)}
child={
<centerbox
startWidget={
<box>
<image iconName={"chronometer-reset"} visible={bind( device, 'connecting' )}></image>
<image
iconName={bind(device, "icon")}
marginEnd={3}
></image>
</box>
}
centerWidget={
<label
label={bind(device, "name").as(n => n ?? "No name")}
marginEnd={5}
></label>
}
endWidget={
<box>
<label
label={bind(device, "batteryPercentage").as(
bat => (bat >= 0 ? bat + "%" : "?%"),
)}
marginEnd={3}
></label>
<image
iconName={bind(device, "trusted").as(v =>
v ? "checkbox" : "paint-unknown-symbolic",
)}
></image>
</box>
}
></centerbox>
}
onClicked={() => {
// TODO: Make sure to check if device was previously paired and otherwise do some pairing shenanigans
device.connect_device( () => {} );
}}
></button>
);
};
export default BTDevice;

View File

@@ -1,221 +0,0 @@
import { execAsync, bind } from "astal";
import Network from "gi://AstalNetwork";
import { App, Gtk } from "astal/gtk4";
import { NetworkItem } from "./modules/NetworkItem";
import { PasswordDialog } from "./modules/PasswordDialog";
import {
availableNetworks,
savedNetworks,
activeNetwork,
showPasswordDialog,
scanNetworks,
getSavedNetworks,
disconnectNetwork,
forgetNetwork,
isExpanded,
refreshIntervalId,
} from "./networkinghelper";
// Main WiFi Box component
export const WiFiBox = () => {
const network = Network.get_default();
// Initial scan when component is first used
setTimeout(() => {
scanNetworks();
getSavedNetworks();
}, 100);
return (
<box vertical cssClasses={["wifi-menu", "toggle"]}>
{/* WiFi Toggle Header */}
<box cssClasses={["toggle", "wifi-toggle"]}>
<button
onClicked={() => {
if (network.wifi.enabled) {
network.wifi.set_enabled(false);
} else network.wifi.set_enabled(true);
}}
cssClasses={bind(network.wifi, "enabled").as((enabled) =>
enabled ? ["button"] : ["button-disabled"],
)}
>
<image iconName={bind(network.wifi, "icon_name")} />
</button>
<button
hexpand={true}
onClicked={() => {
if (network.wifi.enabled) {
isExpanded.set(!isExpanded.get());
if (isExpanded.get()) {
scanNetworks();
getSavedNetworks();
}
}
}}
>
<box hexpand={true}>
<label
hexpand={true}
xalign={0}
label={bind(network.wifi, "ssid").as(
(ssid) =>
ssid || (network.wifi.enabled ? "Not Connected" : "WiFi Off"),
)}
/>
<image
iconName="pan-end-symbolic"
halign={Gtk.Align.END}
cssClasses={bind(isExpanded).as((expanded) =>
expanded
? ["arrow-indicator", "arrow-down"]
: ["arrow-indicator"],
)}
/>
</box>
</button>
</box>
{/* Networks List Revealer */}
<revealer
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={300}
revealChild={bind(isExpanded)}
setup={() => {
const clearScanInterval = () => {
if (refreshIntervalId.get()) {
clearInterval( parseInt( '' + refreshIntervalId.get() ));
refreshIntervalId.set(null);
}
};
bind(isExpanded).subscribe((expanded) => {
// Clear existing interval
clearScanInterval();
if (expanded) {
// Scan networks
network.wifi?.scan();
// Set up new interval if WiFi is enabled
if (network.wifi?.enabled) {
refreshIntervalId.set(
setInterval(() => {
scanNetworks();
getSavedNetworks();
print("updated");
}, 10000),
);
}
} else {
// Apply revealer bug fix when collapsed
App.toggle_window("system-menu");
App.toggle_window("system-menu");
}
});
// Monitor window toggling
const windowListener = App.connect("window-toggled", (_, window) => {
if (window.name === "system-menu" && isExpanded.get()) {
isExpanded.set(false);
}
});
// Clean up resources when component is destroyed
return () => {
App.disconnect(windowListener);
clearScanInterval();
};
}}
>
<box vertical cssClasses={["network-list"]}>
<box visible={showPasswordDialog( v => v )}>
<PasswordDialog />
</box>
<label label="Available Networks" cssClasses={["section-label"]} />
<label label="No networks found" cssClasses={["empty-label"]} visible={availableNetworks( net => net.length === 0 )}/>
<box visible={availableNetworks( networks => networks.length > 1 )}>
{availableNetworks( networks =>
networks.map( (network) => <NetworkItem network={network} />)
)}
</box>
{savedNetworks((networks) => {
// Filter out networks already shown in available networks
const filteredNetworks = networks.filter(
(ssid) => !availableNetworks.get().some((n) => n.ssid === ssid)
);
// Only render the section if there are filtered networks to show
return filteredNetworks.length > 0 ? (
<box vertical>
<label label="Saved Networks" cssClasses={["section-label"]} />
{filteredNetworks.map((ssid) => (
<box cssClasses={["saved-network"]}>
<label label={ssid} />
<box hexpand={true} />
<button
label="Forget"
cssClasses={["forget-button", "button"]}
onClicked={() => forgetNetwork(ssid)}
/>
</box>
))}
</box>
) : (
<box></box>
);
})}
<box hexpand>
<button
halign={Gtk.Align.START}
cssClasses={["refresh-button"]}
onClicked={() => {
scanNetworks();
getSavedNetworks();
}}
>
<image iconName="view-refresh-symbolic" />
</button>
{/* Connected Network Options */}
<box hexpand>
{activeNetwork((active) =>
active ? (
<box vertical cssClasses={["connected-network"]} hexpand>
<button
label="Disconnect"
cssClasses={["disconnect-button"]}
onClicked={() => disconnectNetwork(active.ssid)}
/>
</box>
) : (
""
),
)}
</box>
<button
cssClasses={["settings-button"]}
halign={Gtk.Align.END}
hexpand={false}
onClicked={() => {
execAsync([
"sh",
"-c",
"XDG_CURRENT_DESKTOP=GNOME gnome-control-center wifi",
]);
isExpanded.set(false);
}}
>
<image iconName={"emblem-system-symbolic"} />
</button>
) : (
""
),
)}
</box>
</box>
</revealer>
</box>
);
};

View File

@@ -1,2 +0,0 @@
# Source
This is a modified version from [MatShell](https://github.com/Neurian/matshell)

View File

@@ -1,14 +0,0 @@
import AstalNetwork from "gi://AstalNetwork?version=0.1";
interface CurrentWiFi {
ssid: string;
strength: number;
secured: boolean;
}
interface WiFiDetails extends CurrentWiFi {
active: boolean;
accessPoint: AstalNetwork.AccessPoint;
iconName: string;
}

View File

@@ -1,161 +0,0 @@
// From https://github.com/Neurarian/matshell/blob/master/utils/wifi.ts
import { execAsync, Variable } from "astal";
import Network from "gi://AstalNetwork";
import { CurrentWiFi, WiFiDetails } from "./network";
// State trackers
export const availableNetworks: Variable<WiFiDetails[]> = Variable([]);
export const savedNetworks: Variable<string[]> = Variable([]);
export const activeNetwork: Variable<CurrentWiFi | null> = Variable(null);
export const isConnecting: Variable<boolean> = Variable(false);
export const showPasswordDialog: Variable<boolean> = Variable(false);
export const errorMessage: Variable<string> = Variable("");
export const isExpanded: Variable<boolean> = Variable(false);
export const passwordInput: Variable<string> = Variable("");
export const selectedNetwork: Variable<null | WiFiDetails> = Variable(null);
export const refreshIntervalId: Variable<
number | null | ReturnType<typeof setTimeout>
> = Variable(null);
// Function to scan for available networks
export const scanNetworks = () => {
const network = Network.get_default();
if (network && network.wifi) {
network.wifi.scan();
// Get available networks from access points
const networks: WiFiDetails[] = network.wifi.accessPoints
.map(ap => ({
ssid: ap.ssid,
strength: ap.strength,
secured: ap.flags !== 0,
active: network.wifi.activeAccessPoint?.ssid === ap.ssid,
accessPoint: ap,
iconName: ap.iconName,
}))
.filter(n => n.ssid);
// Sort by signal strength
networks.sort((a, b) => b.strength - a.strength);
// Remove duplicates (same SSID)
const uniqueNetworks: WiFiDetails[] = [];
const seen = new Set();
networks.forEach(network => {
if (!seen.has(network.ssid)) {
seen.add(network.ssid);
uniqueNetworks.push(network);
}
});
availableNetworks.set(uniqueNetworks);
// Update active network
if (network.wifi.activeAccessPoint) {
activeNetwork.set({
ssid: network.wifi.activeAccessPoint.ssid,
strength: network.wifi.activeAccessPoint.strength,
secured: network.wifi.activeAccessPoint.flags !== 0,
});
} else {
activeNetwork.set(null);
}
}
};
// Function to list saved networks
export const getSavedNetworks = () => {
execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"])
.then(output => {
if (typeof output === "string") {
const savedWifiNetworks = output
.split("\n")
.filter(line => line.includes("802-11-wireless"))
.map(line => line.split(":")[0].trim());
savedNetworks.set(savedWifiNetworks);
}
})
.catch(error => console.error("Error fetching saved networks:", error));
};
// Function to connect to a network
export const connectToNetwork = (
ssid: string,
password: null | string = null,
) => {
isConnecting.set(true);
errorMessage.set("");
const network = Network.get_default();
const currentSsid = network.wifi.ssid;
// Function to perform the actual connection
const performConnection = () => {
let command = "";
if (password) {
// Connect with password
command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`;
} else {
// Connect without password (saved or open network)
command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`;
}
execAsync(["bash", "-c", command])
.then(() => {
showPasswordDialog.set(false);
isConnecting.set(false);
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Connection error:", error);
errorMessage.set("Failed to connect. Check password.");
isConnecting.set(false);
});
};
// If already connected to a network, disconnect first
if (currentSsid && currentSsid !== ssid) {
console.log(
`Disconnecting from ${currentSsid} before connecting to ${ssid}`,
);
execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`])
.then(() => {
// Wait a moment for the disconnection to complete fully
setTimeout(() => {
performConnection();
}, 500); // 500ms delay for clean disconnection
})
.catch(error => {
console.error("Disconnect error:", error);
// Continue with connection attempt even if disconnect fails
performConnection();
});
} else {
// No active connection or connecting to same network (reconnect case)
performConnection();
}
};
// Function to disconnect from a network
export const disconnectNetwork = (ssid: string) => {
execAsync(["bash", "-c", `nmcli connection down "${ssid}"`])
.then(() => {
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Disconnect error:", error);
});
};
// Function to forget a saved network
export const forgetNetwork = (ssid: string) => {
execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`])
.then(() => {
getSavedNetworks(); // Refresh saved networks list
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Forget network error:", error);
});
};

View File

@@ -1,91 +0,0 @@
import { bind } from "astal";
import { Gtk } from "astal/gtk4";
import AstalNetwork from "gi://AstalNetwork";
import networkHelper from "./network-helper";
const net = AstalNetwork.get_default();
const STATE = AstalNetwork.DeviceState;
const WiFiList = () => {
const popover = new Gtk.Popover({ cssClasses: ["WiFiPicker"] });
return popover;
};
const renderWiFiList = () => {
return <box>
<label label="Test"></label>
</box>
}
const Network = () => {
const wifiList = WiFiList();
return (
<box>
<button
cssClasses={networkHelper.networkEnabled(en => {
if (en) return ["network-button", "net-on"];
else return ["network-button"];
})}
onClicked={() =>
networkHelper.setNetworking(
!networkHelper.networkEnabled.get(),
)
}
child={<box vertical>
<label label="Wired" cssClasses={[ 'button-name' ]}></label>
<label label={bind( net.wired, 'state' ).as( state => {
if ( state === STATE.ACTIVATED ) {
return 'Connected. IP: ' + networkHelper.getIP();
} else if ( state === STATE.DISCONNECTED ) {
return 'Disconnected';
} else if ( state === STATE.FAILED ) {
return 'Error';
} else if ( state === STATE.PREPARE || state === STATE.CONFIG || state === STATE.IP_CHECK || state === STATE.IP_CONFIG ) {
return 'Connecting...';
} else {
return 'Unavailable';
}
} )}></label>
</box>}
></button>
<box>
<button
cssClasses={bind(net.wifi, "enabled").as(b => {
const classes = ["network-button"];
if (b) {
classes.push("wifi-on");
}
return classes;
})}
child={<box vertical>
<label label="WiFi" cssClasses={[ 'button-name' ]}></label>
<label label={bind( net.wifi, 'state' ).as( state => {
if ( state === STATE.ACTIVATED ) {
return 'Connected. IP: ' + networkHelper.getIP();
} else if ( state === STATE.DISCONNECTED ) {
return 'Disconnected';
} else if ( state === STATE.FAILED ) {
return 'Error';
} else if ( state === STATE.PREPARE || state === STATE.CONFIG || state === STATE.IP_CHECK || state === STATE.IP_CONFIG ) {
return 'Connecting...';
} else {
return 'Unavailable';
}
} )} visible={bind(net.wifi, 'enabled').as( en => en )}></label>
<label label="Disabled" visible={bind(net.wifi, 'enabled').as( en => !en )}></label>
</box>}
></button>
<button
cssClasses={["network-button-context"]}
visible={bind(net.wifi, "enabled").as(b => b)}
onClicked={() => wifiList.popup()}
></button>
</box>
{wifiList}
</box>
);
};
export default Network;

View File

@@ -1,28 +0,0 @@
import { exec, Variable } from "astal";
import AstalNetwork from "gi://AstalNetwork";
const networkEnabled = Variable( exec( 'nmcli networking connectivity' ) !== 'none' );
const network = AstalNetwork.get_default();
const setNetworking = ( status: boolean ) => {
if ( status === true ) {
exec( 'nmcli networking on' );
networkEnabled.set( true );
} else {
exec( 'nmcli networking off' );
networkEnabled.set( false );
}
}
const getIP = () => {
return exec( `/bin/bash -c "ip addr show | grep 'inet ' | awk '{print $2}' | grep -v '127'"` ).split( '/' )[ 0 ];
}
export default {
networkEnabled,
setNetworking,
getIP
}

View File

@@ -1,14 +0,0 @@
import AstalNetwork from "gi://AstalNetwork?version=0.1";
interface CurrentWiFi {
ssid: string;
strength: number;
secured: boolean;
}
interface WiFiDetails extends CurrentWiFi {
active: boolean;
accessPoint: AstalNetwork.AccessPoint;
iconName: string;
}

View File

@@ -1,161 +0,0 @@
// From https://github.com/Neurarian/matshell/blob/master/utils/wifi.ts
import { execAsync, Variable } from "astal";
import Network from "gi://AstalNetwork";
import { CurrentWiFi, WiFiDetails } from "./network";
// State trackers
export const availableNetworks: Variable<WiFiDetails[]> = Variable([]);
export const savedNetworks: Variable<string[]> = Variable([]);
export const activeNetwork: Variable<CurrentWiFi | null> = Variable(null);
export const isConnecting: Variable<boolean> = Variable(false);
export const showPasswordDialog: Variable<boolean> = Variable(false);
export const errorMessage: Variable<string> = Variable("");
export const isExpanded: Variable<boolean> = Variable(false);
export const passwordInput: Variable<string> = Variable("");
export const selectedNetwork: Variable<null | WiFiDetails> = Variable(null);
export const refreshIntervalId: Variable<
number | null | ReturnType<typeof setTimeout>
> = Variable(null);
// Function to scan for available networks
export const scanNetworks = () => {
const network = Network.get_default();
if (network && network.wifi) {
network.wifi.scan();
// Get available networks from access points
const networks: WiFiDetails[] = network.wifi.accessPoints
.map(ap => ({
ssid: ap.ssid,
strength: ap.strength,
secured: ap.flags !== 0,
active: network.wifi.activeAccessPoint?.ssid === ap.ssid,
accessPoint: ap,
iconName: ap.iconName,
}))
.filter(n => n.ssid);
// Sort by signal strength
networks.sort((a, b) => b.strength - a.strength);
// Remove duplicates (same SSID)
const uniqueNetworks: WiFiDetails[] = [];
const seen = new Set();
networks.forEach(network => {
if (!seen.has(network.ssid)) {
seen.add(network.ssid);
uniqueNetworks.push(network);
}
});
availableNetworks.set(uniqueNetworks);
// Update active network
if (network.wifi.activeAccessPoint) {
activeNetwork.set({
ssid: network.wifi.activeAccessPoint.ssid,
strength: network.wifi.activeAccessPoint.strength,
secured: network.wifi.activeAccessPoint.flags !== 0,
});
} else {
activeNetwork.set(null);
}
}
};
// Function to list saved networks
export const getSavedNetworks = () => {
execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"])
.then(output => {
if (typeof output === "string") {
const savedWifiNetworks = output
.split("\n")
.filter(line => line.includes("802-11-wireless"))
.map(line => line.split(":")[0].trim());
savedNetworks.set(savedWifiNetworks);
}
})
.catch(error => console.error("Error fetching saved networks:", error));
};
// Function to connect to a network
export const connectToNetwork = (
ssid: string,
password: null | string = null,
) => {
isConnecting.set(true);
errorMessage.set("");
const network = Network.get_default();
const currentSsid = network.wifi.ssid;
// Function to perform the actual connection
const performConnection = () => {
let command = "";
if (password) {
// Connect with password
command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`;
} else {
// Connect without password (saved or open network)
command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`;
}
execAsync(["bash", "-c", command])
.then(() => {
showPasswordDialog.set(false);
isConnecting.set(false);
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Connection error:", error);
errorMessage.set("Failed to connect. Check password.");
isConnecting.set(false);
});
};
// If already connected to a network, disconnect first
if (currentSsid && currentSsid !== ssid) {
console.log(
`Disconnecting from ${currentSsid} before connecting to ${ssid}`,
);
execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`])
.then(() => {
// Wait a moment for the disconnection to complete fully
setTimeout(() => {
performConnection();
}, 500); // 500ms delay for clean disconnection
})
.catch(error => {
console.error("Disconnect error:", error);
// Continue with connection attempt even if disconnect fails
performConnection();
});
} else {
// No active connection or connecting to same network (reconnect case)
performConnection();
}
};
// Function to disconnect from a network
export const disconnectNetwork = (ssid: string) => {
execAsync(["bash", "-c", `nmcli connection down "${ssid}"`])
.then(() => {
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Disconnect error:", error);
});
};
// Function to forget a saved network
export const forgetNetwork = (ssid: string) => {
execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`])
.then(() => {
getSavedNetworks(); // Refresh saved networks list
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Forget network error:", error);
});
};

View File

@@ -1,63 +0,0 @@
import { exec } from "astal";
import { Gtk } from "astal/gtk4";
const PowerMenu = (): Gtk.Popover => {
const popover = new Gtk.Popover({ cssClasses: ["PowerMenu"] });
const powerMenuBox = () => {
return (
<box>
<button
cssClasses={["power-button"]}
child={
<image iconName={"system-shutdown-symbolic"}></image>
}
onClicked={() => exec("shutdown now")}
></button>
<button
cssClasses={["power-button"]}
child={<image iconName={"system-reboot-symbolic"}></image>}
onClicked={() => exec("reboot")}
></button>
<button
cssClasses={["power-button"]}
child={<image iconName={"system-suspend-symbolic"}></image>}
onClicked={() => exec("systemctl suspend")}
></button>
<button
cssClasses={["power-button"]}
child={
<image iconName={"system-lock-screen-symbolic"}></image>
}
onClicked={() => exec("hyprlock")}
></button>
<button
cssClasses={["power-button"]}
child={<image iconName={"system-log-out-symbolic"}></image>}
onClicked={() => exec("hyprctl dispatch exit 0")}
></button>
</box>
);
};
popover.set_child(powerMenuBox());
return popover;
};
const Power = () => {
const pm = PowerMenu();
return (
<button
cssClasses={["PowerMenuButton"]}
child={
<box>
<image iconName={"system-shutdown-symbolic"}></image>
{pm}
</box>
}
onClicked={() => pm.popup()}
/>
);
};
export default Power;

View File

@@ -1,13 +0,0 @@
.title-2 {
font-size: 1.2rem;
font-weight: bold;
}
.bt-conn-list {
margin-bottom: 20px;
}
popover>box {
margin: 10px;
border-radius: 50px;
}

View File

@@ -1,19 +0,0 @@
$fg-color: #{"@theme_fg_color"};
$bg-color: #{"@theme_bg_color"};
window.Bar {
background: transparent;
color: $fg-color;
font-weight: bold;
>centerbox {
background: $bg-color;
border-radius: 10px;
margin: 8px;
}
button {
border-radius: 8px;
margin: 2px;
}
}

View File

@@ -1,23 +0,0 @@
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"]} hexpand halign={Gtk.Align.CENTER}>
<label onDestroy={() => time.drop()} label={time()}></label>
<popover>
<Gtk.Calendar />
</popover>
</menubutton>
}
const Calendar = () => {
}
export default {
Time
}

View File

@@ -1,157 +0,0 @@
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();
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 });
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 cssClasses={["HyprlandWorkspaces"]}>
{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 ? ["HyprlandFocusedWorkspace"] : [""])}
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.Box>();
const popover = new Gtk.Popover();
const popoverBox = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL } );
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 );
popoverBox.append( clientBox );
clients.set( client.get_address(), clientBox );
}
// 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 cssName={"HyprlandFocusedClients"} visible={focused.as(Boolean)}>
<button onClicked={() => windowPopover.popup()}>
{focused.as( client => (
client && <label label={bind( client, "title" ).as( String )} />
))}
</button>
{ windowPopover }
</box>
}
export default {
Workspace,
ActiveWindow,
SysTray
}

View File

@@ -1,117 +0,0 @@
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 Brightness from "../../util/brightness";
import { Gtk } from "astal/gtk4";
import QuickActions from "../QuickActions/QuickActions";
const STATE = AstalNetwork.DeviceState;
const QuickView = () => {
const quickActions = QuickActions.QuickActions();
return <button onClicked={() => quickActions.popup()} child={
<box>
<BatteryWidget></BatteryWidget>
<Audio></Audio>
<BluetoothWidget></BluetoothWidget>
<NetworkWidget></NetworkWidget>
<BrightnessWidget></BrightnessWidget>
<image iconName={"system-shutdown-symbolic"}></image>
{ quickActions }
</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
}

View File

@@ -1,135 +0,0 @@
import { exec, 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 valign={Gtk.Align.START}>
<label label={ramUsed( used => {
return 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}
Kernel: ${stats.kernel}` } ) }></label>
</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>
}></button>
} else {
return <image iconName={"action-unavailable-symbolic"}></image>
}
}
export default {
SystemInfo,
panel
}

View File

@@ -1,82 +0,0 @@
import GObject, { register, property } from "astal/gobject"
import { monitorFile, readFileAsync } from "astal/file"
import { exec, execAsync } from "astal/process"
const get = (args: string) => Number(exec(`brightnessctl ${args}`))
const screen = exec(`bash -c "ls -w1 /sys/class/backlight | head -1"`)
const kbd = exec(`bash -c "ls -w1 /sys/class/leds | head -1"`)
@register({ GTypeName: "Brightness" })
export default class Brightness extends GObject.Object {
static instance: Brightness
static get_default() {
if (!this.instance)
this.instance = new Brightness()
return this.instance
}
#kbdMax = get(`--device ${kbd} max`)
#kbd = get(`--device ${kbd} get`)
#screenMax = get("max")
#screen = get("get") / (get("max") || 1)
#screenAvailable = false
@property(Boolean)
get screenAvailable() { return this.#screenAvailable }
@property(Number)
get kbd() { return this.#kbd }
set kbd(value) {
if (value < 0 || value > this.#kbdMax)
return
execAsync(`brightnessctl -d ${kbd} s ${value} -q`).then(() => {
this.#kbd = value
this.notify("kbd")
})
}
@property(Number)
get screen() { return this.#screen }
set screen(percent) {
if (percent < 0)
percent = 0
if (percent > 1)
percent = 1
execAsync(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => {
this.#screen = percent
this.notify("screen")
})
}
constructor() {
super()
const screenPath = `/sys/class/backlight/${screen}/brightness`
const kbdPath = `/sys/class/leds/${kbd}/brightness`
monitorFile(screenPath, async f => {
const v = await readFileAsync(f)
this.#screen = Number(v) / this.#screenMax
this.notify("screen")
})
monitorFile(kbdPath, async f => {
const v = await readFileAsync(f)
this.#kbd = Number(v) / this.#kbdMax
this.notify("kbd")
})
// Check if there is a screen available
try {
get( 'g -c backlight' );
} catch ( _ ) {
this.#screenAvailable = false;
}
}
}