[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

@@ -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 {
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>}
></button>
<button

View File

@@ -17,9 +17,7 @@ const setNetworking = ( status: boolean ) => {
const getIP = () => {
print( 'Hello World' );
return 'Hello World';
// return exec( "ip addr show | grep 'inet ' | awk '{print $2}' | grep -v '127'" ).split( '/' )[ 0 ];
return exec( `/bin/bash -c "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";
const PowerMenu = (): Gtk.Popover => {
const popover = new Gtk.Popover( { cssClasses: [ 'PowerMenu' ] } );
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>
}
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() );
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>
}
return (
<button
cssClasses={["PowerMenuButton"]}
child={
<box>
<image iconName={"system-shutdown-symbolic"}></image>
{pm}
</box>
}
onClicked={() => pm.popup()}
/>
);
};
export default Power;