[AGS] Bar: BT, Audio, SysInfo, Brightness

This commit is contained in:
2025-04-23 20:04:48 +02:00
parent 69484fc302
commit e93e051094
26 changed files with 825 additions and 223 deletions

View File

@@ -13,11 +13,14 @@ 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>
@@ -32,20 +35,18 @@ const NetworkWidget = () => {
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>
} )} 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' ]} visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED )}></image>
} )} cssClasses={[ 'network-widget', 'quick-view-symbol' ]} visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED )}></image>
</box>
@@ -53,15 +54,34 @@ const NetworkWidget = () => {
const BluetoothWidget = () => {
const bluetooth = AstalBluetooth.get_default();
const enabled = bind( bluetooth, "isPowered" );
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={battery.iconName}></image>
return <image iconName={bind( battery, 'iconName' ).as( icon => icon )} cssClasses={[ 'quick-view-symbol' ]}></image>
} else {
return <box></box>
}
@@ -70,31 +90,27 @@ const BatteryWidget = () => {
const BrightnessWidget = () => {
// TODO: Finish (detect if there is a controllable screen)
const brightness = Brightness.get_default();
const screen_brightness = bind( brightness, "screen" );
return <label label={"🌣" + screen_brightness}></label>
return <label label={"🌣" + screen_brightness} visible={bind( brightness, 'screenAvailable' )} cssClasses={[ 'quick-view-symbol' ]}></label>
}
const Audio = () => {
const wireplumber = AstalWp.get_default();
if ( wireplumber ) {
const volume_speakers = bind( wireplumber.defaultSpeaker, 'volume' );
return <box orientation={Gtk.Orientation.HORIZONTAL}>
<image iconName={wireplumber.defaultSpeaker.volumeIcon}></image>
<image iconName={wireplumber.defaultMicrophone.volumeIcon}></image>
<label label={volume_speakers.as( v => { return "" + v } ) }></label>
<label label={wireplumber.defaultSpeaker.get_name()}></label>
<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>;
}
return null;
}
// cssClasses={[ 'quick-view-symbol' ]}
export default {
QuickView

View File

@@ -1,7 +1,46 @@
import { exec, GLib } from "astal"
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 is available
// Check if awk & sed are available
try {
exec( 'awk -V' );
exec( 'sed --version' );
@@ -11,44 +50,86 @@ const featureTest = () => {
enabled = false;
return;
}
// Check if mpstat is available
try {
exec( 'mpstat' );
exec( 'mpstat -V' );
} catch ( e ) {
availableFeatures.cpu = false;
printerr( '[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!' );
}
// Battery... acpi might be present, but potentially no bat
}
let enabled = false;
const availableFeatures = {
cpu: true,
ram: true,
bat: true,
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 ) {
const cpuUtil = exec( 'mpstat | awk "/all/ {print(100 - $NF)"}' );
cpuUtil.set( '' + Math.round( parseFloat( exec( `/bin/fish -c cpu-utilization` ) ) ) );
}
if ( availableFeatures.ram ) {
const ramUtil = exec( `free | awk '/Mem/ { printf("%.2f\\n", ($3/$2)*100) }'` );
}
if ( availableFeatures.bat ) {
const acpi = exec( `acpi -i | grep 'Battery'` );
// TODO: Parse acpi output
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 = () => {
return <box></box>
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
SystemInfo,
panel
}

View File

@@ -0,0 +1,10 @@
interface Stats {
kernel: string;
netSpeed: string;
cpuTemp: string;
cpuClk: string;
gpuTemp: string;
gpuClk: string;
vram: string;
availableVRAM: string;
}