[AGS] Bar: Improve QuickActions
This commit is contained in:
		| @@ -1,31 +1,25 @@ | ||||
| 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"; | ||||
| import { Gtk } from "astal/gtk4" | ||||
| import Network from "./modules/Networking/Network"; | ||||
| import Power from "./modules/Power"; | ||||
|  | ||||
| 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> | ||||
|   ); | ||||
| const QuickActions = () => { | ||||
|     const popover = new Gtk.Popover( { cssClasses: [ 'quick-actions-popover' ] } ); | ||||
|  | ||||
|     popover.set_child( createQuickActionMenu() ); | ||||
|  | ||||
|     return popover; | ||||
| } | ||||
|  | ||||
|  | ||||
| const createQuickActionMenu = () => { | ||||
|     return <box visible cssClasses={[ 'quick-actions' ]}> | ||||
|         <Power></Power> | ||||
|         <Network></Network> | ||||
|     </box> | ||||
| } | ||||
|  | ||||
|  | ||||
| // TODO: Expose additional functions to be usable through CLI | ||||
| export default { | ||||
|     QuickActions | ||||
| }; | ||||
|   | ||||
							
								
								
									
										14
									
								
								config/astal/components/bar/ui/QuickActions/modules/Networking-old/network.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								config/astal/components/bar/ui/QuickActions/modules/Networking-old/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,90 @@ | ||||
| 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'; | ||||
|                             } | ||||
|                         } )}></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; | ||||
| @@ -0,0 +1,30 @@ | ||||
| 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 = () => { | ||||
|     print( 'Hello World' ); | ||||
|     return 'Hello World'; | ||||
|     // return exec( "ip addr show | grep 'inet ' | awk '{print $2}' | grep -v '127'" ).split( '/' )[ 0 ]; | ||||
| } | ||||
|  | ||||
|  | ||||
| export default { | ||||
|     networkEnabled, | ||||
|     setNetworking, | ||||
|     getIP | ||||
| } | ||||
| @@ -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); | ||||
|         }); | ||||
| }; | ||||
| @@ -0,0 +1,31 @@ | ||||
| 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 <box visible> | ||||
|         <button cssClasses={['PowerMenuButton']} child={<image iconName={"system-shutdown-symbolic"}></image>} onClicked={() => pm.popup()}/> | ||||
|         { pm } | ||||
|     </box> | ||||
| } | ||||
|  | ||||
|  | ||||
| export default Power; | ||||
| @@ -5,24 +5,49 @@ 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 = () => { | ||||
|     return <box> | ||||
|         <Audio></Audio> | ||||
|         <label label="QuickView"></label> | ||||
|     </box> | ||||
|     const quickActions = QuickActions.QuickActions(); | ||||
|  | ||||
|  | ||||
|     return <button onClicked={() => quickActions.popup()} child={ | ||||
|         <box> | ||||
|             <Audio></Audio> | ||||
|             <NetworkWidget></NetworkWidget> | ||||
|             { quickActions } | ||||
|         </box> | ||||
|     }></button> | ||||
| } | ||||
|  | ||||
|  | ||||
| const NetworkWidget = () => { | ||||
|     const network = AstalNetwork.get_default(); | ||||
|     const status = bind( network, "state" ); | ||||
|     const wifiStrength = bind( network.wifi, 'strength' ); | ||||
|     const states = { | ||||
|         "off": "" | ||||
|     } | ||||
|  | ||||
|     return <label label={""}></label> | ||||
|     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 ) { | ||||
|                 print( 'Wired connected' ); | ||||
|                 return 'network-wired-activated-symbolic'; | ||||
|             } else { | ||||
|                 print( 'Unknown state' ); | ||||
|                 return 'paint-unknown-symbolic'; | ||||
|             } | ||||
|         } )} cssClasses={[ 'network-widget' ]} 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' ]} visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED )}></image> | ||||
|     </box> | ||||
|          | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -36,21 +61,9 @@ const BluetoothWidget = () => { | ||||
| const BatteryWidget = () => { | ||||
|     const battery = AstalBattery.get_default(); | ||||
|     if ( battery.get_is_present() ) { | ||||
|         const states = { | ||||
|             "100": "", | ||||
|             "90": "", | ||||
|             "80": "", | ||||
|             "70": "", | ||||
|             "60": "", | ||||
|             "50": "", | ||||
|             "40": "", | ||||
|             "30": "", | ||||
|             "20": "", | ||||
|             "10": "", | ||||
|             "critical": "", | ||||
|             "charging":"", | ||||
|             "plugged": " ", | ||||
|         } | ||||
|         return <image iconName={battery.iconName}></image> | ||||
|     } else { | ||||
|         return <box></box> | ||||
|     } | ||||
|     // Else, no battery available -> Don't show the widget | ||||
| } | ||||
| @@ -68,31 +81,13 @@ const BrightnessWidget = () => { | ||||
| const Audio = () => { | ||||
|     const wireplumber = AstalWp.get_default(); | ||||
|     if ( wireplumber ) { | ||||
|         // With the states, split up the icons according to number of elements available | ||||
|         const speakerMuted = " "; | ||||
|         const speakersStates = [ | ||||
|             "", | ||||
|             "", | ||||
|             "" | ||||
|         ] | ||||
|         const micStates = { | ||||
|             "on": " ", | ||||
|             "muted": " ", | ||||
|         } | ||||
|         const volume_speakers = bind( wireplumber.defaultSpeaker, 'volume' ); | ||||
|         const muted_speakers = bind( wireplumber.defaultSpeaker, 'mute' ); | ||||
|         const muted_mic = bind( wireplumber.defaultMicrophone, 'mute' ); | ||||
|  | ||||
|         return <box orientation={Gtk.Orientation.HORIZONTAL}> | ||||
|             <label label={micStates[ muted_mic ? 'muted' : 'on' ]}></label> | ||||
|             <label label={(muted_speakers ? speakerMuted : volume_speakers.as( v => { | ||||
|                 if ( v === 0 ) return speakerMuted; | ||||
|                 else if ( v <= 30 ) return speakersStates[ 0 ]; | ||||
|                 else if ( v <= 70 ) return speakersStates[ 1 ]; | ||||
|                 else return speakersStates[ 2 ]; | ||||
|             } ) )}></label> | ||||
|             <image iconName={wireplumber.defaultSpeaker.volumeIcon}></image> | ||||
|             <image iconName={wireplumber.defaultMicrophone.volumeIcon}></image> | ||||
|             <label label={volume_speakers.as( v => { return "" + v } ) }></label> | ||||
|             <label label={wireplumber.default_speaker.get_name()}></label> | ||||
|             <label label={wireplumber.defaultSpeaker.get_name()}></label> | ||||
|         </box> | ||||
|     } else { | ||||
|         print( '[ WirePlumber ] Could not connect, Audio support in bar will be missing' ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user