[AGS] Bar: Improve QuickActions

This commit is contained in:
Janis Hutz 2025-04-22 17:38:41 +02:00
parent 8a2180e120
commit 69484fc302
12 changed files with 390 additions and 75 deletions

View File

@ -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
};

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

View File

@ -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;

View File

@ -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
}

View File

@ -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);
});
};

View File

@ -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;

View File

@ -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' );