[AGS] GTK 4 Migration: Done, Start adding QuickActions
Probably gonna abandon the QuickActions, as that is just way too much effort for what it does. Will be providing keybinds for doing what I wanted to do there in Hyprland
This commit is contained in:
		| @@ -1,4 +1,4 @@ | ||||
| import { Astal, Gdk, Gtk } from "astal/gtk3"; | ||||
| import { App, Astal, Gdk, Gtk } from "astal/gtk4"; | ||||
| import Hyprland from "./modules/Hyprland"; | ||||
| import Calendar from "./modules/Calendar"; | ||||
| import QuickView from "./modules/QuickView"; | ||||
| @@ -11,19 +11,23 @@ const Bar = (gdkmonitor: Gdk.Monitor) => { | ||||
|         <window gdkmonitor={gdkmonitor} | ||||
|             cssClasses={["Bar"]} | ||||
|             exclusivity={Astal.Exclusivity.EXCLUSIVE} | ||||
|             anchor={TOP | LEFT | RIGHT}> | ||||
|             <box orientation={Gtk.Orientation.HORIZONTAL} spacing={10}> | ||||
|                 <box hexpand halign={Gtk.Align.START}> | ||||
|             anchor={TOP | LEFT | RIGHT} | ||||
|             visible | ||||
|             application={App} | ||||
|             child={ | ||||
|             <box orientation={Gtk.Orientation.HORIZONTAL}> | ||||
|                 <box hexpand halign={Gtk.Align.START} cssClasses={["BarLeft"]}> | ||||
|                     <Calendar.Time /> | ||||
|                     <SystemInfo.SystemInfo /> | ||||
|                     <Hyprland.Workspace /> | ||||
|                 </box> | ||||
|                 <Hyprland.ActiveWindow /> | ||||
|                 <box hexpand halign={Gtk.Align.END}> | ||||
|                 <box hexpand halign={Gtk.Align.END} cssClasses={["BarRight"]}> | ||||
|                     <Hyprland.SysTray /> | ||||
|                     <QuickView.QuickView /> | ||||
|                 </box> | ||||
|             </box> | ||||
|             }> | ||||
|         </window> | ||||
|     ); | ||||
| } | ||||
|   | ||||
| @@ -1,76 +0,0 @@ | ||||
| import Gtk from "gi://Gtk"; | ||||
| import AstalNetwork from "gi://AstalNetwork"; | ||||
| import AstalBluetooth from "gi://AstalBluetooth"; | ||||
| import { exec } from "gi://GLib"; | ||||
|  | ||||
| let quickActionsMenu; | ||||
|  | ||||
| // Toggle WiFi connection | ||||
| function toggleWiFi() { | ||||
|     const network = AstalNetwork.get_default(); | ||||
|     const wifi = network.get_wifi()!; | ||||
|     const state = wifi.get_state(); | ||||
|  | ||||
|     if (state === AstalNetwork.WifiState.DISCONNECTED) { | ||||
|         wifi.connect(); | ||||
|     } else { | ||||
|         wifi.disconnect(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Toggle Bluetooth power state | ||||
| function toggleBluetooth() { | ||||
|     const bluetooth = AstalBluetooth.get_default(); | ||||
|     const adapter = bluetooth.get_adapter(); | ||||
|     const powered = adapter?.get_powered(); | ||||
|  | ||||
|     adapter?.set_powered(!powered); | ||||
| } | ||||
|  | ||||
| // Adjust volume or microphone (opens pavucontrol) | ||||
| function adjustVolume() { | ||||
|     exec("pavucontrol"); | ||||
| } | ||||
|  | ||||
| function adjustMic() { | ||||
|     exec("pavucontrol"); | ||||
| } | ||||
|  | ||||
| // Power menu | ||||
| function showPowerMenu() { | ||||
|     exec("power-menu"); | ||||
| } | ||||
|  | ||||
| // Show WiFi network picker | ||||
| function pickWiFi() { | ||||
|     const wifi = AstalNetwork.get_default().get_wifi(); | ||||
|     wifi.show_picker(); | ||||
| } | ||||
|  | ||||
| // Show Bluetooth device picker | ||||
| function pickBluetooth() { | ||||
|     const bluetooth = AstalBluetooth.get_default(); | ||||
|     bluetooth.show_picker(); | ||||
| } | ||||
|  | ||||
| // Create menu using JSX | ||||
| function createQuickActionsMenu() { | ||||
|     quickActionsMenu = ( | ||||
|         <menu> | ||||
|             <item label="📶 WiFi" onActivate={toggleWiFi} /> | ||||
|             <item label="→ Pick Network" onActivate={pickWiFi} /> | ||||
|  | ||||
|             <item label="🔵 Bluetooth" onActivate={toggleBluetooth} /> | ||||
|             <item label="→ Pick Device" onActivate={pickBluetooth} /> | ||||
|  | ||||
|             <item label="🔊 Volume" onActivate={adjustVolume} /> | ||||
|             <item label="🎙️ Microphone" onActivate={adjustMic} /> | ||||
|             <item label="🔌 Power" onActivate={showPowerMenu} /> | ||||
|         </menu> | ||||
|     ); | ||||
|  | ||||
|     return quickActionsMenu; | ||||
| } | ||||
|  | ||||
| // Export the function | ||||
| export { createQuickActionsMenu }; | ||||
							
								
								
									
										31
									
								
								config/astal/components/bar/ui/QuickActions/QuickActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								config/astal/components/bar/ui/QuickActions/QuickActions.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { Astal, App } from "astal/gtk4"; | ||||
| import PowerProfiles from "gi://AstalPowerProfiles"; | ||||
| import { Variable } from "astal"; | ||||
| import { Sliders } from "./modules/Sliders"; | ||||
| import { Toggles } from "./modules/Toggles"; | ||||
| import { PowerProfileBox } from "./modules/PowerProfileBox"; | ||||
| import { BatteryBox } from "./modules/BatteryBox"; | ||||
|  | ||||
| export default function QuickActions() { | ||||
|   const powerprofiles = PowerProfiles.get_default(); | ||||
|   const hasProfiles = powerprofiles?.get_profiles()?.length > 0; | ||||
|   const { TOP, RIGHT } = Astal.WindowAnchor; | ||||
|   const visible = Variable(false); | ||||
|   return ( | ||||
|     <window | ||||
|       name="system-menu" | ||||
|       application={App} | ||||
|       layer={Astal.Layer.OVERLAY} | ||||
|       anchor={TOP | RIGHT} | ||||
|       keymode={Astal.Keymode.ON_DEMAND} | ||||
|       visible={visible()} | ||||
|     > | ||||
|       <box cssClasses={["system-menu"]} vertical> | ||||
|         <Toggles /> | ||||
|         {hasProfiles && <PowerProfileBox />} | ||||
|         <Sliders /> | ||||
|         <BatteryBox /> | ||||
|       </box> | ||||
|     </window> | ||||
|   ); | ||||
| } | ||||
| @@ -0,0 +1,221 @@ | ||||
| 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> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,2 @@ | ||||
| # Source | ||||
| This is a modified version from [MatShell](https://github.com/Neurian/matshell) | ||||
							
								
								
									
										14
									
								
								config/astal/components/bar/ui/QuickActions/modules/Networking/network.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								config/astal/components/bar/ui/QuickActions/modules/Networking/network.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| 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; | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,161 @@ | ||||
| // 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,14 +1,16 @@ | ||||
| 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 <label | ||||
|         className="Time" | ||||
|         onDestroy={() => time.drop()} | ||||
|         label={time()} | ||||
|     /> | ||||
|     return <menubutton cssClasses={["Time"]} hexpand halign={Gtk.Align.CENTER}> | ||||
|             <label onDestroy={() => time.drop()} label={time()}></label> | ||||
|             <popover> | ||||
|                 <Gtk.Calendar /> | ||||
|             </popover> | ||||
|         </menubutton> | ||||
| } | ||||
|  | ||||
| const Calendar = () => { | ||||
|   | ||||
| @@ -1,38 +1,62 @@ | ||||
|  | ||||
| import AstalTray from "gi://AstalTray"; | ||||
| import { bind, Variable } from "astal"; | ||||
| 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(); | ||||
|  | ||||
|     return <box className="SysTray"> | ||||
|         {bind(tray, "items").as( items => items.map( item => ( | ||||
|             <button | ||||
|                 tooltipMarkup={bind(item, "tooltipMarkup")} | ||||
|                 usePopover={false} | ||||
|                 actionGroup={bind(item, "actionGroup").as(ag => ["dbusmenu", ag])} | ||||
|                 menuModel={bind(item, "menuModel")}> | ||||
|                 <icon gicon={bind(item, "gicon")} /> | ||||
|             </button> | ||||
|         ) ) ) } | ||||
|     </box> | ||||
|     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 className={"HyprlandWorkspaces"}> | ||||
|     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 | ||||
|                     className={bind(hypr, "focusedWorkspace").as(fw => | ||||
|                         ws === fw ? "HyprlandFocusedWorkspace" : "")} | ||||
|                     onClicked={() => ws.focus()}> | ||||
|                     {ws.id} | ||||
|                     cssClasses={bind(hypr, "focusedWorkspace").as(fw => | ||||
|                         ws === fw ? ["HyprlandFocusedWorkspace"] : [""])} | ||||
|                     onButtonPressed={() => ws.focus()} child={<label label={String(ws.id)}></label>}> | ||||
|                 </button> | ||||
|             )) | ||||
|         )} | ||||
| @@ -40,24 +64,88 @@ const Workspace = () => { | ||||
| } | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * 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" ); | ||||
|     let visible = Variable( false ); | ||||
|  | ||||
|     const toggleOverlay = () => { | ||||
|         visible.set( !visible.get() ); | ||||
|     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)}> | ||||
|         <Gtk.Button onClicked={toggleOverlay}> | ||||
|         <button onClicked={() => windowPopover.popup()}> | ||||
|             {focused.as( client => ( | ||||
|                 client && <label label={bind( client, "title" ).as( String )} /> | ||||
|             ))} | ||||
|         </Gtk.Button> | ||||
|         <eventbox visible={bind(visible).as( v => v )} name="popover-container"> | ||||
|             <label label="This is a test"></label> | ||||
|         </eventbox> | ||||
|         </button> | ||||
|         { windowPopover } | ||||
|     </box> | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import { bind } from "astal"; | ||||
| import AstalBattery from "gi://AstalBattery?version=0.1"; | ||||
| import AstalBluetooth from "gi://AstalBluetooth?version=0.1"; | ||||
| import AstalNetwork from "gi://AstalNetwork?version=0.1" | ||||
| import AstalWp from "gi://AstalWp?version=0.1"; | ||||
| 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/gtk3"; | ||||
| import { Gtk } from "astal/gtk4"; | ||||
|  | ||||
| const QuickView = () => { | ||||
|     return <box> | ||||
|   | ||||
| @@ -18,7 +18,6 @@ const featureTest = () => { | ||||
|         printerr( '[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!' ); | ||||
|     } | ||||
|  | ||||
|     // Screen brightness... CTL might be available, but no screen controllable | ||||
|     // Battery... acpi might be present, but potentially no bat | ||||
| } | ||||
|  | ||||
| @@ -28,8 +27,6 @@ const availableFeatures = { | ||||
|     cpu: true, | ||||
|     ram: true, | ||||
|     bat: true, | ||||
|     net: false, | ||||
|     brightness: true, | ||||
| } | ||||
|  | ||||
| const sysInfoFetcher = () => { | ||||
| @@ -44,17 +41,10 @@ const sysInfoFetcher = () => { | ||||
|             const acpi = exec( `acpi -i | grep 'Battery'` ); | ||||
|             // TODO: Parse acpi output | ||||
|         } | ||||
|         // if ( availableFeatures.net ) { | ||||
|         //     // Using ifstat or vnstat probably, get current network de | ||||
|         // } | ||||
|         if ( availableFeatures.brightness ) { | ||||
|              | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| const SystemInfo = () => { | ||||
|      | ||||
|     return <box></box> | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user