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

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

View File

@ -1,14 +1,14 @@
import { App } from "astal/gtk4" import { App } from "astal/gtk4"
import style from "./style.scss" import style from "./style.scss"
import notifications from "./components/notifications/handler"; // import notifications from "./components/notifications/handler";
import Bar from "./components/bar/ui/Bar"; import Bar from "./components/bar/ui/Bar";
App.start({ App.start({
instanceName: "runner", instanceName: "runner",
css: style, css: style,
main() { main() {
notifications.startNotificationHandler( App.get_monitors()[0] ); // notifications.startNotificationHandler( App.get_monitors()[0] );
// TODO: Monitor handling // TODO: Monitor handling
Bar.Bar( App.get_monitors()[0] ); Bar.Bar( App.get_monitors()[0] );
}, },
@ -17,7 +17,8 @@ App.start({
// Notifications (TODO: Handle the arguments in the components themselves) // Notifications (TODO: Handle the arguments in the components themselves)
if ( args[ 0 ] === 'notifier' ) { if ( args[ 0 ] === 'notifier' ) {
res( notifications.cliHandler( args ) ); res( 'Not available here yet, run astal -i notifier ' + args[ 1 ] );
// res( notifications.cliHandler( args ) );
} else if ( args[ 0 ] === 'bar' ) { } else if ( args[ 0 ] === 'bar' ) {
res( Bar.cliHandler( args ) ); res( Bar.cliHandler( args ) );
} }

View File

@ -3,40 +3,55 @@ import Hyprland from "./modules/Hyprland";
import Calendar from "./modules/Calendar"; import Calendar from "./modules/Calendar";
import QuickView from "./modules/QuickView"; import QuickView from "./modules/QuickView";
import SystemInfo from "./modules/SystemInfo"; import SystemInfo from "./modules/SystemInfo";
import { CenterBox } from "astal/gtk4/widget";
const Bar = (gdkmonitor: Gdk.Monitor) => { const Bar = (gdkmonitor: Gdk.Monitor) => {
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor; const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
return ( return (
<window gdkmonitor={gdkmonitor} <window
gdkmonitor={gdkmonitor}
cssClasses={["Bar"]} cssClasses={["Bar"]}
exclusivity={Astal.Exclusivity.EXCLUSIVE} exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | LEFT | RIGHT} anchor={TOP | LEFT | RIGHT}
visible visible
application={App} application={App}
child={ child={
<box orientation={Gtk.Orientation.HORIZONTAL}> <CenterBox
<box hexpand halign={Gtk.Align.START} cssClasses={["BarLeft"]}> orientation={Gtk.Orientation.HORIZONTAL}
<Calendar.Time /> start_widget={
<SystemInfo.SystemInfo /> <box
<Hyprland.Workspace /> hexpand
</box> halign={Gtk.Align.START}
<Hyprland.ActiveWindow /> cssClasses={["BarLeft"]}
<box hexpand halign={Gtk.Align.END} cssClasses={["BarRight"]}> >
<Hyprland.SysTray /> <Calendar.Time />
<QuickView.QuickView /> <SystemInfo.SystemInfo />
</box> <Hyprland.Workspace />
</box> </box>
}> }
</window> centerWidget={<Hyprland.ActiveWindow />}
endWidget={
<box
hexpand
halign={Gtk.Align.END}
cssClasses={["BarRight"]}
>
<Hyprland.SysTray />
<QuickView.QuickView />
</box>
}
></CenterBox>
}
></window>
); );
} };
const cliHandler = ( args: string[] ): string => { const cliHandler = (args: string[]): string => {
return 'Not implemented'; return "Not implemented";
} };
export default { export default {
Bar, Bar,
cliHandler cliHandler,
}; };

View File

@ -1,6 +1,7 @@
import { Gtk } from "astal/gtk4" import { Gtk } from "astal/gtk4"
import Network from "./modules/Networking/Network";
import Power from "./modules/Power"; import Power from "./modules/Power";
import Audio from "./modules/Audio/Audio";
import Bluetooth from "./modules/Bluetooth/Bluetooth";
const QuickActions = () => { const QuickActions = () => {
const popover = new Gtk.Popover( { cssClasses: [ 'quick-actions-popover' ] } ); const popover = new Gtk.Popover( { cssClasses: [ 'quick-actions-popover' ] } );
@ -12,9 +13,11 @@ const QuickActions = () => {
const createQuickActionMenu = () => { const createQuickActionMenu = () => {
return <box visible cssClasses={[ 'quick-actions' ]}> // 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> <Power></Power>
<Network></Network> <Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
<Audio.AudioModule></Audio.AudioModule>
</box> </box>
} }

View File

@ -0,0 +1,178 @@
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,
};

View File

@ -0,0 +1,153 @@
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,
};

View File

@ -0,0 +1,52 @@
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;

View File

@ -73,7 +73,8 @@ const Network = () => {
} else { } else {
return 'Unavailable'; return 'Unavailable';
} }
} )}></label> } )} visible={bind(net.wifi, 'enabled').as( en => en )}></label>
<label label="Disabled" visible={bind(net.wifi, 'enabled').as( en => !en )}></label>
</box>} </box>}
></button> ></button>
<button <button

View File

@ -17,9 +17,7 @@ const setNetworking = ( status: boolean ) => {
const getIP = () => { const getIP = () => {
print( 'Hello World' ); return exec( `/bin/bash -c "ip addr show | grep 'inet ' | awk '{print $2}' | grep -v '127'"` ).split( '/' )[ 0 ];
return 'Hello World';
// return exec( "ip addr show | grep 'inet ' | awk '{print $2}' | grep -v '127'" ).split( '/' )[ 0 ];
} }

View File

@ -2,30 +2,62 @@ import { exec } from "astal";
import { Gtk } from "astal/gtk4"; import { Gtk } from "astal/gtk4";
const PowerMenu = (): Gtk.Popover => { const PowerMenu = (): Gtk.Popover => {
const popover = new Gtk.Popover( { cssClasses: [ 'PowerMenu' ] } ); const popover = new Gtk.Popover({ cssClasses: ["PowerMenu"] });
const powerMenuBox = () => { const powerMenuBox = () => {
return <box> return (
<button cssClasses={['power-button']} child={<image iconName={"system-shutdown-symbolic"}></image>} onClicked={() => exec( 'shutdown now' )}></button> <box>
<button cssClasses={['power-button']} child={<image iconName={"system-reboot-symbolic"}></image>} onClicked={() => exec( 'reboot' )}></button> <button
<button cssClasses={['power-button']} child={<image iconName={"system-suspend-symbolic"}></image>} onClicked={() => exec( 'systemctl suspend' )}></button> cssClasses={["power-button"]}
<button cssClasses={['power-button']} child={<image iconName={"system-lock-screen-symbolic"}></image>} onClicked={() => exec( 'hyprlock' )}></button> child={
<button cssClasses={['power-button']} child={<image iconName={"system-log-out-symbolic"}></image>} onClicked={() => exec( 'hyprctl dispatch exit 0' )}></button> <image iconName={"system-shutdown-symbolic"}></image>
</box> }
} 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());
popover.set_child( powerMenuBox() );
return popover; return popover;
} };
const Power = () => { const Power = () => {
const pm = PowerMenu(); const pm = PowerMenu();
return <box visible> return (
<button cssClasses={['PowerMenuButton']} child={<image iconName={"system-shutdown-symbolic"}></image>} onClicked={() => pm.popup()}/> <button
{ pm } cssClasses={["PowerMenuButton"]}
</box> child={
} <box>
<image iconName={"system-shutdown-symbolic"}></image>
{pm}
</box>
}
onClicked={() => pm.popup()}
/>
);
};
export default Power; export default Power;

View File

@ -1,37 +1,13 @@
@import "colors"; .title-2 {
font-size: 1.2rem;
.toggle-row { font-weight: bold;
background-color: $bg; }
border-radius: 12px;
margin: 6px 0; .bt-conn-list {
overflow: hidden; margin-bottom: 20px;
border: 1px solid $border; }
button { popover>box {
padding: 10px 16px; margin: 10px;
font-size: 14px; border-radius: 50px;
transition: background 0.2s ease;
border: none;
background: transparent;
&:hover {
background-color: $hover;
}
}
.toggle {
flex: 1;
background-color: transparent;
text-align: left;
&.active {
background-color: $accent;
color: white;
}
}
.arrow {
width: 40px;
background-color: transparent;
text-align: center;
}
} }

View File

@ -0,0 +1,19 @@
$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;
}
}

View File

@ -13,11 +13,14 @@ const STATE = AstalNetwork.DeviceState;
const QuickView = () => { const QuickView = () => {
const quickActions = QuickActions.QuickActions(); const quickActions = QuickActions.QuickActions();
return <button onClicked={() => quickActions.popup()} child={ return <button onClicked={() => quickActions.popup()} child={
<box> <box>
<BatteryWidget></BatteryWidget>
<Audio></Audio> <Audio></Audio>
<BluetoothWidget></BluetoothWidget>
<NetworkWidget></NetworkWidget> <NetworkWidget></NetworkWidget>
<BrightnessWidget></BrightnessWidget>
<image iconName={"system-shutdown-symbolic"}></image>
{ quickActions } { quickActions }
</box> </box>
}></button> }></button>
@ -32,20 +35,18 @@ const NetworkWidget = () => {
if ( state === AstalNetwork.State.CONNECTING ) { if ( state === AstalNetwork.State.CONNECTING ) {
return 'chronometer-reset-symbolic'; return 'chronometer-reset-symbolic';
} else if ( state === AstalNetwork.State.CONNECTED_LOCAL || state === AstalNetwork.State.CONNECTED_SITE || state === AstalNetwork.State.CONNECTED_GLOBAL ) { } 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'; return 'network-wired-activated-symbolic';
} else { } else {
print( 'Unknown state' );
return 'paint-unknown-symbolic'; 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 => { <image iconName={bind( network.wifi, 'state' ).as( state => {
if ( state === STATE.ACTIVATED ) { if ( state === STATE.ACTIVATED ) {
return network.wifi.iconName return network.wifi.iconName
} else { } else {
return ''; 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> </box>
@ -53,15 +54,34 @@ const NetworkWidget = () => {
const BluetoothWidget = () => { const BluetoothWidget = () => {
const bluetooth = AstalBluetooth.get_default(); const bluetooth = AstalBluetooth.get_default();
const enabled = bind( bluetooth, "isPowered" ); const enabled = bind( bluetooth.adapter, "powered" );
const connected = bind( bluetooth, "isConnected" ); 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 BatteryWidget = () => {
const battery = AstalBattery.get_default(); const battery = AstalBattery.get_default();
if ( battery.get_is_present() ) { 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 { } else {
return <box></box> return <box></box>
} }
@ -70,31 +90,27 @@ const BatteryWidget = () => {
const BrightnessWidget = () => { const BrightnessWidget = () => {
// TODO: Finish (detect if there is a controllable screen)
const brightness = Brightness.get_default(); const brightness = Brightness.get_default();
const screen_brightness = bind( brightness, "screen" ); 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 Audio = () => {
const wireplumber = AstalWp.get_default(); const wireplumber = AstalWp.get_default();
if ( wireplumber ) { if ( wireplumber ) {
const volume_speakers = bind( wireplumber.defaultSpeaker, 'volume' );
return <box orientation={Gtk.Orientation.HORIZONTAL}> return <box orientation={Gtk.Orientation.HORIZONTAL}>
<image iconName={wireplumber.defaultSpeaker.volumeIcon}></image> <image iconName={bind(wireplumber.defaultSpeaker, 'volumeIcon').as( icon => icon )} cssClasses={[ 'quick-view-symbol' ]}></image>
<image iconName={wireplumber.defaultMicrophone.volumeIcon}></image> <image iconName={bind(wireplumber.defaultMicrophone, 'volumeIcon').as( icon => icon )} cssClasses={[ 'quick-view-symbol' ]}></image>
<label label={volume_speakers.as( v => { return "" + v } ) }></label>
<label label={wireplumber.defaultSpeaker.get_name()}></label>
</box> </box>
} else { } else {
print( '[ WirePlumber ] Could not connect, Audio support in bar will be missing' ); 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 { export default {
QuickView 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 = () => { const featureTest = () => {
// Check if awk is available // Check if awk & sed are available
try { try {
exec( 'awk -V' ); exec( 'awk -V' );
exec( 'sed --version' ); exec( 'sed --version' );
@ -11,44 +50,86 @@ const featureTest = () => {
enabled = false; enabled = false;
return; return;
} }
// Check if mpstat is available
try { try {
exec( 'mpstat' ); exec( 'mpstat -V' );
} catch ( e ) { } catch ( e ) {
availableFeatures.cpu = false; availableFeatures.cpu = false;
printerr( '[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!' ); 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 info = () => {
return <box vertical valign={Gtk.Align.START}>
const availableFeatures = { <label label={ramUsed( used => {
cpu: true, return used + `(${ ramUtil.get() }%)`;
ram: true, } )}></label>
bat: true, <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 = () => { const sysInfoFetcher = () => {
if ( enabled ) { if ( enabled ) {
if ( availableFeatures.cpu ) { 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 ) { if ( availableFeatures.ram ) {
const ramUtil = exec( `free | awk '/Mem/ { printf("%.2f\\n", ($3/$2)*100) }'` ); 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' ) );
if ( availableFeatures.bat ) {
const acpi = exec( `acpi -i | grep 'Battery'` );
// TODO: Parse acpi output
} }
gpuUtil.set( exec( 'cat /sys/class/drm/card1/device/gpu_busy_percent' ) );
} }
} }
const panel = SystemInformationPanel();
const SystemInfo = () => { 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 { 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;
}

View File

@ -12,7 +12,7 @@ export default class Brightness extends GObject.Object {
static get_default() { static get_default() {
if (!this.instance) if (!this.instance)
this.instance = new Brightness() this.instance = new Brightness()
return this.instance return this.instance
} }
@ -20,6 +20,10 @@ export default class Brightness extends GObject.Object {
#kbd = get(`--device ${kbd} get`) #kbd = get(`--device ${kbd} get`)
#screenMax = get("max") #screenMax = get("max")
#screen = get("get") / (get("max") || 1) #screen = get("get") / (get("max") || 1)
#screenAvailable = false
@property(Boolean)
get screenAvailable() { return this.#screenAvailable }
@property(Number) @property(Number)
get kbd() { return this.#kbd } get kbd() { return this.#kbd }
@ -67,5 +71,12 @@ export default class Brightness extends GObject.Object {
this.#kbd = Number(v) / this.#kbdMax this.#kbd = Number(v) / this.#kbdMax
this.notify("kbd") this.notify("kbd")
}) })
// Check if there is a screen available
try {
get( 'g -c backlight' );
} catch ( _ ) {
this.#screenAvailable = false;
}
} }
} }

View File

@ -91,7 +91,6 @@ const findNotificationByNotificationID = ( id: number ): number => {
* @param id The notifd ID of the notification * @param id The notifd ID of the notification
*/ */
const addNotification = ( id: number ): void => { const addNotification = ( id: number ): void => {
print( '[ Notifications ] Notification with id ' + id + ' added.' );
const currIndex = Notifications.length; const currIndex = Notifications.length;
Notifications.push( { Notifications.push( {
notifdID: id, notifdID: id,

View File

@ -0,0 +1,12 @@
import { Gtk, Gdk } from "astal/gtk4";
import { GLib } from "astal";
export const isIcon = (icon: string) => {
const display = Gdk.Display.get_default();
if (!display) return false;
const iconTheme = Gtk.IconTheme.get_for_display(display);
return iconTheme.has_icon(icon);
};
export const fileExists = (path: string) =>
GLib.file_test(path, GLib.FileTest.EXISTS);

View File

@ -0,0 +1,24 @@
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { fileExists, isIcon } from "./helper";
export function NotificationIcon(notification: Notifd.Notification) {
if ( notification.image || notification.appIcon || notification.desktopEntry) {
const icon = notification.image || notification.appIcon || notification.desktopEntry;
if (fileExists(icon)) {
return (
<box expand={false} valign={Gtk.Align.CENTER}>
<image file={icon} />
</box>
);
} else if (isIcon(icon)) {
return (
<box expand={false} valign={Gtk.Align.CENTER}>
<image iconName={icon} />
</box>
);
}
}
return null;
}

View File

@ -13,7 +13,7 @@ window.NotificationHandler {
all: unset; all: unset;
} }
box.Notification { box.notification {
&:first-child { &:first-child {
margin-top: 1rem; margin-top: 1rem;

View File

@ -1,103 +1,113 @@
// From astal examples // From astal examples
import { GLib } from "astal" import { bind, GLib } from "astal";
import { Gtk } from "astal/gtk4" import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd" import Notifd from "gi://AstalNotifd";
import { NotificationIcon } from "./icon";
// import Pango from "gi://Pango?version=1.0" // import Pango from "gi://Pango?version=1.0"
const fileExists = (path: string) => const fileExists = (path: string) => GLib.file_test(path, GLib.FileTest.EXISTS);
GLib.file_test(path, GLib.FileTest.EXISTS)
const time = (time: number, format = "%H:%M") => GLib.DateTime const time = (time: number, format = "%H:%M") =>
.new_from_unix_local(time) GLib.DateTime.new_from_unix_local(time).format(format)!;
.format(format)!
const urgency = (n: Notifd.Notification) => { const urgency = (n: Notifd.Notification) => {
const { LOW, NORMAL, CRITICAL } = Notifd.Urgency const { LOW, NORMAL, CRITICAL } = Notifd.Urgency;
// match operator when? // match operator when?
switch (n.urgency) { switch (n.urgency) {
case LOW: return "low" case LOW:
case CRITICAL: return "critical" return "low";
case CRITICAL:
return "critical";
case NORMAL: case NORMAL:
default: return "normal" default:
return "normal";
} }
} };
type Props = { type Props = {
delete: ( id: number ) => void; delete: (id: number) => void;
notification: Notifd.Notification notification: Notifd.Notification;
id: number, id: number;
} };
export default function Notification(props: Props) { export default function Notification(props: Props) {
const { notification: n, id: id, delete: del } = props const { notification: n, id: id, delete: del } = props;
const { START, CENTER, END } = Gtk.Align const { START, CENTER, END } = Gtk.Align;
return <box vertical return (
cssClasses={['Notification', `${urgency(n)}`]}> <box vertical cssClasses={["notification", `${urgency(n)}`]}>
<box cssClasses={["header"]}> <box cssClasses={["header"]}>
{(n.appIcon || n.desktopEntry) ? <Gtk.Image {n.appIcon || n.desktopEntry ? (
cssClasses={["app-icon"]} <Gtk.Image
visible={Boolean(n.appIcon || n.desktopEntry)} cssClasses={["app-icon"]}
iconName={n.appIcon || n.desktopEntry} visible={Boolean(n.appIcon || n.desktopEntry)}
/> : <image iconName={'window-close-symbolic'}></image>} iconName={n.appIcon || n.desktopEntry}
<label />
cssClasses={["app-name"]} ) : (
halign={START} <image iconName={"window-close-symbolic"}></image>
// ellipsize={Pango.EllipsizeMode.END} )}
label={n.appName || "Unknown"}
/>
<label
cssClasses={["time"]}
hexpand
halign={END}
label={time(n.time)}
/>
<button onClicked={() => { del( id ) }}>
<image iconName="window-close-symbolic" />
</button>
</box>
<Gtk.Separator visible />
<box cssClasses={["content"]}>
{n.image && fileExists(n.image) ? <box
valign={START}
cssClasses={["image"]}>
<image file={n.image}></image>
</box>
: <box></box>}
{n.image ? <box
expand={false}
valign={START}
cssClasses={["icon-image"]}>
<image iconName={n.image} expand halign={CENTER} valign={CENTER} />
</box>
: <box></box>}
<box vertical>
<label <label
cssClasses={["summary"]} cssClasses={["app-name"]}
halign={START} halign={START}
xalign={0}
label={n.summary}
// ellipsize={Pango.EllipsizeMode.END} // ellipsize={Pango.EllipsizeMode.END}
label={n.appName || "Unknown"}
/> />
{n.body ? <label <label
cssClasses={["body"]} cssClasses={["time"]}
wrap
useMarkup
halign={START}
xalign={0}
label={n.body}
/> : <label></label>}
</box>
</box>
{n.get_actions().length > 0 ? <box cssClasses={["actions"]}>
{n.get_actions().map(({ label, id }) => (
<button
hexpand hexpand
onClicked={() => n.invoke(id)}> halign={END}
<label label={label} halign={CENTER} hexpand /> label={time(n.time)}
</button> />
))} <button
</box> : <box></box>} onClicked={() => {
</box> del(id);
}}
child={<image iconName="window-close-symbolic" />}
></button>
</box>
<Gtk.Separator visible />
<box cssClasses={["content"]}>
<box
cssClasses={["image"]}
visible={Boolean(NotificationIcon(n))}
halign={CENTER}
valign={CENTER}
vexpand={true}
>
{NotificationIcon(n)}
</box>
<box vertical>
<label
cssClasses={["summary"]}
halign={START}
xalign={0}
useMarkup
label={bind(n, "summary")}
// ellipsize={Pango.EllipsizeMode.END}
/>
{n.body && (
<label
cssClasses={["body"]}
valign={CENTER}
wrap={true}
maxWidthChars={50}
label={bind(n, "body")}
/>
)}
</box>
</box>
{n.get_actions().length > 0 ? (
<box cssClasses={["actions"]}>
{n.get_actions().map(({ label, id }) => (
<button hexpand onClicked={() => n.invoke(id)}>
<label label={label} halign={CENTER} hexpand />
</button>
))}
</box>
) : (
<box></box>
)}
</box>
);
} }

View File

@ -1,21 +1,12 @@
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
@use './components/notifications/notifications.scss'; @use './components/notifications/notifications.scss';
$fg-color: #{"@theme_fg_color"}; @use './components/bar/ui/bar.scss';
$bg-color: #{"@theme_bg_color"}; @use './components/bar/ui/QuickActions/quickactions.scss';
window.Bar { * {
background: transparent; font-size: 1rem;
color: $fg-color; }
font-weight: bold;
empty {
>centerbox { min-width: 0;
background: $bg-color; background-color: transparent;
border-radius: 10px;
margin: 8px;
}
button {
border-radius: 8px;
margin: 2px;
}
} }

View File

@ -11,7 +11,7 @@ read -p "Choose the configs to install, Laptop or Desktop (l/D): " platform
yay -S hyprland hypridle hyprfreeze hyprlock plymouth aylurs-gtk-shell-git brightnessctl pulsemixer xdg-desktop-portal-hyprland yay -S hyprland hypridle hyprfreeze hyprlock plymouth aylurs-gtk-shell-git brightnessctl pulsemixer xdg-desktop-portal-hyprland
# Audio, drivers, tools # Audio, drivers, tools
yay -S pipewire pipewire-alsa pipewire-pulse pipewire-jack mesa fish thunar yazi wireplumber grimblast wl-clipboard wget vimiv zoxide trash-cli fzf ouch zathura yay -S pipewire pipewire-alsa pipewire-pulse pipewire-jack mesa fish thunar yazi wireplumber grimblast wl-clipboard wget vimiv zoxide trash-cli fzf ouch zathura sensors radeontop lm-sensors
# Set up yazi # Set up yazi
ya pack -a ndtoan96/ouch ya pack -a ndtoan96/ouch

20
scripts/cpu-utilization Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
# Get first snapshot
read cpu user nice system idle iowait irq softirq steal guest < /proc/stat
total1=$((user+nice+system+idle+iowait+irq+softirq+steal))
idle1=$idle
sleep 0.5
# Get second snapshot
read cpu user nice system idle iowait irq softirq steal guest < /proc/stat
total2=$((user+nice+system+idle+iowait+irq+softirq+steal))
idle2=$idle
# Calculate usage
total_diff=$((total2 - total1))
idle_diff=$((idle2 - idle1))
cpu_usage=$((100 * (total_diff - idle_diff) / total_diff))
echo "$cpu_usage"