[AGS] Bar: Done (WiFi still missing, will be added at some later point)
This commit is contained in:
		| @@ -23,7 +23,6 @@ const Bar = (gdkmonitor: Gdk.Monitor) => { | ||||
|                         <box | ||||
|                             hexpand | ||||
|                             halign={Gtk.Align.START} | ||||
|                             cssClasses={["BarLeft"]} | ||||
|                         > | ||||
|                             <Calendar.Time /> | ||||
|                             <SystemInfo.SystemInfo /> | ||||
							
								
								
									
										62
									
								
								config/astal/components/bar/bar.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								config/astal/components/bar/bar.scss
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										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, | ||||
| }; | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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, | ||||
| }; | ||||
| @@ -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, | ||||
| }; | ||||
| @@ -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; | ||||
| @@ -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> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,2 +0,0 @@ | ||||
| # Source | ||||
| This is a modified version from [MatShell](https://github.com/Neurian/matshell) | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
| @@ -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); | ||||
|         }); | ||||
| }; | ||||
| @@ -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; | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
| @@ -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); | ||||
|         }); | ||||
| }; | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user