[AGS] Bar: Done (WiFi still missing, will be added at some later point)
This commit is contained in:
parent
e93e051094
commit
10136ab9de
@ -1,21 +1,23 @@
|
|||||||
import { App } from "astal/gtk4"
|
import { App } from "astal/gtk4"
|
||||||
import style from "./style.scss"
|
import style from "./style.scss"
|
||||||
|
import Bar from "./components/bar/Bar";
|
||||||
// import notifications from "./components/notifications/handler";
|
import AstalHyprland from "gi://AstalHyprland?version=0.1";
|
||||||
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] );
|
const hypr = AstalHyprland.get_default();
|
||||||
// TODO: Monitor handling
|
const monitors = App.get_monitors();
|
||||||
Bar.Bar( App.get_monitors()[0] );
|
for (let index = 0; index < monitors.length; index++) {
|
||||||
|
Bar.Bar( monitors[ index ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle monitor add
|
||||||
},
|
},
|
||||||
requestHandler(request, res) {
|
requestHandler(request, res) {
|
||||||
const args = request.trimStart().split( ' ' );
|
const args = request.trimStart().split( ' ' );
|
||||||
|
|
||||||
// Notifications (TODO: Handle the arguments in the components themselves)
|
|
||||||
if ( args[ 0 ] === 'notifier' ) {
|
if ( args[ 0 ] === 'notifier' ) {
|
||||||
res( 'Not available here yet, run astal -i notifier ' + args[ 1 ] );
|
res( 'Not available here yet, run astal -i notifier ' + args[ 1 ] );
|
||||||
// res( notifications.cliHandler( args ) );
|
// res( notifications.cliHandler( args ) );
|
||||||
|
1
config/astal/btconf
Normal file
1
config/astal/btconf
Normal file
@ -0,0 +1 @@
|
|||||||
|
false
|
74
config/astal/components/QuickActions/QuickActions.tsx
Normal file
74
config/astal/components/QuickActions/QuickActions.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Power from "./modules/Power";
|
||||||
|
import Audio from "./modules/Audio/Audio";
|
||||||
|
import Bluetooth from "./modules/Bluetooth/Bluetooth";
|
||||||
|
import Brightness from "./modules/Brightness/Brightness";
|
||||||
|
import Player from "./modules/Player/Player";
|
||||||
|
import { BatteryBox } from "./modules/Battery";
|
||||||
|
import { exec } from "astal";
|
||||||
|
import Network from "./modules/Networking/Network";
|
||||||
|
|
||||||
|
const QuickActions = () => {
|
||||||
|
const popover = new Gtk.Popover({ cssClasses: ["quick-actions-wrapper"] });
|
||||||
|
popover.set_child(renderQuickActions());
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderQuickActions = () => {
|
||||||
|
const user = exec("/bin/sh -c whoami");
|
||||||
|
const profile = exec("/bin/fish -c get-profile-picture");
|
||||||
|
const cwd = exec("pwd");
|
||||||
|
const um = Power.UserMenu();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box visible cssClasses={["quick-actions", "popover-box"]} vertical>
|
||||||
|
<centerbox
|
||||||
|
startWidget={
|
||||||
|
<button
|
||||||
|
onClicked={() => um.popup()}
|
||||||
|
cssClasses={["stealthy-button"]}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
{um}
|
||||||
|
<Gtk.Frame
|
||||||
|
cssClasses={["avatar-icon"]}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
file={
|
||||||
|
profile !== ""
|
||||||
|
? profile
|
||||||
|
: cwd +
|
||||||
|
"/no-avatar-icon.jpg"
|
||||||
|
}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
></Gtk.Frame>
|
||||||
|
<label label={user}></label>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
}
|
||||||
|
endWidget={
|
||||||
|
<box>
|
||||||
|
<BatteryBox></BatteryBox>
|
||||||
|
<Power.Power></Power.Power>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></centerbox>
|
||||||
|
<Gtk.Separator marginTop={10} marginBottom={20}></Gtk.Separator>
|
||||||
|
<box>
|
||||||
|
<Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
|
||||||
|
<Network.Network></Network.Network>
|
||||||
|
</box>
|
||||||
|
<Gtk.Separator marginTop={10} marginBottom={10}></Gtk.Separator>
|
||||||
|
<Brightness.BrightnessModule></Brightness.BrightnessModule>
|
||||||
|
<Audio.AudioModule></Audio.AudioModule>
|
||||||
|
<Player.PlayerModule></Player.PlayerModule>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Expose additional functions to be usable through CLI
|
||||||
|
export default {
|
||||||
|
QuickActions,
|
||||||
|
};
|
35
config/astal/components/QuickActions/dump
Normal file
35
config/astal/components/QuickActions/dump
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Power from "./modules/Power";
|
||||||
|
import Audio from "./modules/Audio/Audio";
|
||||||
|
import Bluetooth from "./modules/Bluetooth/Bluetooth";
|
||||||
|
import Brightness from "./modules/Brightness/Brightness";
|
||||||
|
import Player from "./modules/Player/Player";
|
||||||
|
import { BatteryBox } from "./modules/Battery";
|
||||||
|
|
||||||
|
const QuickActions = () => {
|
||||||
|
const popover = new Gtk.Overlay( { cssClasses: [ 'quick-actions-wrapper' ] } );
|
||||||
|
popover.set_child(renderQuickActions());
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderQuickActions = () => {
|
||||||
|
return (
|
||||||
|
<box visible cssClasses={["quick-actions", "popover-box"]} vertical setup={ self }>
|
||||||
|
<box halign={Gtk.Align.END}>
|
||||||
|
<BatteryBox></BatteryBox>
|
||||||
|
<Power></Power>
|
||||||
|
</box>
|
||||||
|
<Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
|
||||||
|
<Gtk.Separator marginTop={10} marginBottom={10}></Gtk.Separator>
|
||||||
|
<Brightness.BrightnessModule></Brightness.BrightnessModule>
|
||||||
|
<Audio.AudioModule></Audio.AudioModule>
|
||||||
|
<Gtk.Separator marginTop={20} marginBottom={10}></Gtk.Separator>
|
||||||
|
<Player.PlayerModule></Player.PlayerModule>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Expose additional functions to be usable through CLI
|
||||||
|
export default {
|
||||||
|
QuickActions,
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
.audio-box {
|
||||||
|
min-width: 320px;
|
||||||
|
}
|
@ -18,13 +18,14 @@ const AudioModule = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box cssClasses={["audio-box"]} vertical>
|
<box cssClasses={["audio-box"]} vertical>
|
||||||
<box>
|
<box hexpand vexpand>
|
||||||
<button
|
<button
|
||||||
onClicked={() =>
|
onClicked={() =>
|
||||||
wp.defaultSpeaker.set_mute(
|
wp.defaultSpeaker.set_mute(
|
||||||
!wp.defaultSpeaker.get_mute(),
|
!wp.defaultSpeaker.get_mute(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
tooltipText={"Mute audio output"}
|
||||||
child={
|
child={
|
||||||
<image
|
<image
|
||||||
iconName={bind(wp.defaultSpeaker, "volumeIcon")}
|
iconName={bind(wp.defaultSpeaker, "volumeIcon")}
|
||||||
@ -33,7 +34,7 @@ const AudioModule = () => {
|
|||||||
}
|
}
|
||||||
></button>
|
></button>
|
||||||
<label
|
<label
|
||||||
label={bind(wp.defaultMicrophone, "volume").as(
|
label={bind(wp.defaultSpeaker, "volume").as(
|
||||||
v => Math.round(100 * v) + "%",
|
v => Math.round(100 * v) + "%",
|
||||||
)}
|
)}
|
||||||
></label>
|
></label>
|
||||||
@ -42,11 +43,13 @@ const AudioModule = () => {
|
|||||||
max={100}
|
max={100}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
widthRequest={100}
|
hexpand
|
||||||
|
vexpand
|
||||||
onChangeValue={self => setVolumeSpeaker(self.value)}
|
onChangeValue={self => setVolumeSpeaker(self.value)}
|
||||||
></slider>
|
></slider>
|
||||||
<button
|
<button
|
||||||
cssClasses={["sink-select-button"]}
|
cssClasses={["sink-select-button"]}
|
||||||
|
tooltipText={"Pick audio output"}
|
||||||
child={
|
child={
|
||||||
<box>
|
<box>
|
||||||
<image iconName={"speaker-symbolic"}></image>
|
<image iconName={"speaker-symbolic"}></image>
|
||||||
@ -56,13 +59,14 @@ const AudioModule = () => {
|
|||||||
onClicked={() => speakerSelector.popup()}
|
onClicked={() => speakerSelector.popup()}
|
||||||
></button>
|
></button>
|
||||||
</box>
|
</box>
|
||||||
<box>
|
<box hexpand vexpand>
|
||||||
<button
|
<button
|
||||||
onClicked={() =>
|
onClicked={() =>
|
||||||
wp.defaultMicrophone.set_mute(
|
wp.defaultMicrophone.set_mute(
|
||||||
!wp.defaultMicrophone.get_mute(),
|
!wp.defaultMicrophone.get_mute(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
tooltipText={"Mute audio input"}
|
||||||
child={
|
child={
|
||||||
<image
|
<image
|
||||||
iconName={bind(wp.defaultMicrophone, "volumeIcon")}
|
iconName={bind(wp.defaultMicrophone, "volumeIcon")}
|
||||||
@ -82,11 +86,13 @@ const AudioModule = () => {
|
|||||||
max={100}
|
max={100}
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={1}
|
||||||
widthRequest={100}
|
hexpand
|
||||||
|
vexpand
|
||||||
onChangeValue={self => setVolumeMicrophone(self.value)}
|
onChangeValue={self => setVolumeMicrophone(self.value)}
|
||||||
></slider>
|
></slider>
|
||||||
<button
|
<button
|
||||||
cssClasses={["sink-select-button"]}
|
cssClasses={["sink-select-button"]}
|
||||||
|
tooltipText={"Select audio input"}
|
||||||
child={
|
child={
|
||||||
<box>
|
<box>
|
||||||
<image iconName={"microphone"}></image>
|
<image iconName={"microphone"}></image>
|
52
config/astal/components/QuickActions/modules/Battery.tsx
Normal file
52
config/astal/components/QuickActions/modules/Battery.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import Battery from "gi://AstalBattery";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
|
||||||
|
export const BatteryBox = () => {
|
||||||
|
const battery = Battery.get_default();
|
||||||
|
const batteryEnergy = (energyRate: number) => {
|
||||||
|
return energyRate > 0.1 ? `${Math.round(energyRate * 10) / 10} W ` : "";
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
cssClasses={["battery-info"]}
|
||||||
|
visible={bind(battery, "isBattery")}
|
||||||
|
>
|
||||||
|
<box cssClasses={["battery-box"]}>
|
||||||
|
<image
|
||||||
|
iconName={bind(battery, "batteryIconName")}
|
||||||
|
tooltipText={bind(battery, "energyRate").as(er =>
|
||||||
|
batteryEnergy(er),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
label={bind(battery, "percentage").as(
|
||||||
|
p => ` ${Math.round(p * 100)}%`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
cssClasses={["time"]}
|
||||||
|
hexpand={true}
|
||||||
|
halign={Gtk.Align.END}
|
||||||
|
visible={bind(battery, "charging").as(c => !c)}
|
||||||
|
label={bind(battery, "timeToEmpty").as(t => toTime(t))}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTime = (time: number) => {
|
||||||
|
const MINUTE = 60;
|
||||||
|
const HOUR = MINUTE * 60;
|
||||||
|
|
||||||
|
if (time > 24 * HOUR) return "";
|
||||||
|
|
||||||
|
const hours = Math.round(time / HOUR);
|
||||||
|
const minutes = Math.round((time - hours * HOUR) / MINUTE);
|
||||||
|
|
||||||
|
const hoursDisplay = hours > 0 ? `${hours}h ` : "";
|
||||||
|
const minutesDisplay = minutes > 0 ? `${minutes}m ` : "";
|
||||||
|
|
||||||
|
return `${hoursDisplay}${minutesDisplay}`;
|
||||||
|
};
|
@ -1,7 +1,8 @@
|
|||||||
import { bind } from "astal";
|
import { bind, readFile, Variable, writeFile } from "astal";
|
||||||
import { Gtk } from "astal/gtk4";
|
import { Gtk } from "astal/gtk4";
|
||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
import BTDevice from "./Device";
|
import BTDevice from "./Device";
|
||||||
|
const ALIGN = Gtk.Align;
|
||||||
|
|
||||||
const bt = AstalBluetooth.get_default();
|
const bt = AstalBluetooth.get_default();
|
||||||
|
|
||||||
@ -11,8 +12,8 @@ const BluetoothModule = () => {
|
|||||||
<button
|
<button
|
||||||
cssClasses={bind(bt.adapter, "powered").as(powered =>
|
cssClasses={bind(bt.adapter, "powered").as(powered =>
|
||||||
powered
|
powered
|
||||||
? ["bt-toggle-button", "bt-on"]
|
? ["toggle-button", "toggle-on"]
|
||||||
: ["bt-toggle-button"],
|
: ["toggle-button"],
|
||||||
)}
|
)}
|
||||||
onClicked={() =>
|
onClicked={() =>
|
||||||
bt.adapter.set_powered(!bt.adapter.get_powered())
|
bt.adapter.set_powered(!bt.adapter.get_powered())
|
||||||
@ -20,10 +21,12 @@ const BluetoothModule = () => {
|
|||||||
child={
|
child={
|
||||||
<box vertical>
|
<box vertical>
|
||||||
<label
|
<label
|
||||||
cssClasses={["button-title"]}
|
cssClasses={["title-2"]}
|
||||||
label={"Bluetooth"}
|
label={"Bluetooth"}
|
||||||
|
halign={ALIGN.CENTER}
|
||||||
|
valign={ALIGN.CENTER}
|
||||||
></label>
|
></label>
|
||||||
<box>
|
<box halign={ALIGN.CENTER} valign={ALIGN.CENTER}>
|
||||||
<label
|
<label
|
||||||
visible={bind(bt.adapter, "powered").as(
|
visible={bind(bt.adapter, "powered").as(
|
||||||
p => !p,
|
p => !p,
|
||||||
@ -48,7 +51,7 @@ const BluetoothModule = () => {
|
|||||||
}
|
}
|
||||||
></button>
|
></button>
|
||||||
<button
|
<button
|
||||||
cssClasses={["bt-devices-button"]}
|
cssClasses={["actions-button"]}
|
||||||
visible={bind(bt.adapter, "powered")}
|
visible={bind(bt.adapter, "powered")}
|
||||||
child={
|
child={
|
||||||
<box>
|
<box>
|
||||||
@ -56,6 +59,7 @@ const BluetoothModule = () => {
|
|||||||
{picker}
|
{picker}
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
|
tooltipText={"View available devices"}
|
||||||
onClicked={() => openBTPicker()}
|
onClicked={() => openBTPicker()}
|
||||||
></button>
|
></button>
|
||||||
</box>
|
</box>
|
||||||
@ -70,15 +74,44 @@ const openBTPicker = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BluetoothPickerList = () => {
|
const BluetoothPickerList = () => {
|
||||||
|
let btEnableState = readFile("./btconf") === "true" ? true : false;
|
||||||
|
bt.adapter.set_powered(btEnableState);
|
||||||
|
|
||||||
|
const updateState = () => {
|
||||||
|
btEnableState = !btEnableState;
|
||||||
|
writeFile("./btconf", "" + btEnableState);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box vertical onDestroy={() => bt.adapter.stop_discovery()}>
|
<box
|
||||||
<label label={"Connected devices"} cssClasses={["title-2"]}></label>
|
vertical
|
||||||
|
onDestroy={() => bt.adapter.stop_discovery()}
|
||||||
|
cssClasses={["popover-box"]}
|
||||||
|
>
|
||||||
|
<label cssClasses={["title"]} label={"Bluetooth"}></label>
|
||||||
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
|
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
|
||||||
<box vertical cssClasses={["bt-conn-list"]}>
|
<centerbox
|
||||||
|
startWidget={<label label={"Turn on at startup"}></label>}
|
||||||
|
endWidget={
|
||||||
|
<switch
|
||||||
|
valign={ALIGN.END}
|
||||||
|
halign={ALIGN.END}
|
||||||
|
active={btEnableState}
|
||||||
|
onButtonPressed={() => updateState()}
|
||||||
|
></switch>
|
||||||
|
}
|
||||||
|
></centerbox>
|
||||||
|
<label
|
||||||
|
marginTop={10}
|
||||||
|
label={"Connected & Trusted devices"}
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
></label>
|
||||||
|
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
|
||||||
|
<box vertical cssClasses={["devices-list"]}>
|
||||||
{bind(bt, "devices").as(devices => {
|
{bind(bt, "devices").as(devices => {
|
||||||
return devices
|
return devices
|
||||||
.filter(device => {
|
.filter(device => {
|
||||||
if (device.get_connected()) {
|
if (device.get_connected() || device.get_paired()) {
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -91,13 +124,13 @@ const BluetoothPickerList = () => {
|
|||||||
visible={bind(bt, "devices").as(devices => {
|
visible={bind(bt, "devices").as(devices => {
|
||||||
return (
|
return (
|
||||||
devices.filter(device => {
|
devices.filter(device => {
|
||||||
if (device.get_connected()) {
|
if (device.get_connected() || device.get_paired()) {
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
}).length === 0
|
}).length === 0
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
label={"No connected devices"}
|
label={"No connected / trusted devices"}
|
||||||
cssClasses={["bt-no-found", "bt-conn-list"]}
|
cssClasses={["bt-no-found", "bt-conn-list"]}
|
||||||
></label>
|
></label>
|
||||||
<label
|
<label
|
||||||
@ -109,7 +142,7 @@ const BluetoothPickerList = () => {
|
|||||||
{bind(bt, "devices").as(devices => {
|
{bind(bt, "devices").as(devices => {
|
||||||
return devices
|
return devices
|
||||||
.filter(data => {
|
.filter(data => {
|
||||||
if (!data.get_connected()) {
|
if (!data.get_connected() && !data.get_paired()) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -122,7 +155,10 @@ const BluetoothPickerList = () => {
|
|||||||
visible={bind(bt, "devices").as(devices => {
|
visible={bind(bt, "devices").as(devices => {
|
||||||
return (
|
return (
|
||||||
devices.filter(device => {
|
devices.filter(device => {
|
||||||
if (!device.get_connected()) {
|
if (
|
||||||
|
!device.get_connected() &&
|
||||||
|
!device.get_paired()
|
||||||
|
) {
|
||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
}).length === 0
|
}).length === 0
|
||||||
@ -135,13 +171,11 @@ const BluetoothPickerList = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const BluetoothPicker = () => {
|
const BluetoothPicker = () => {
|
||||||
const popover = new Gtk.Popover();
|
const popover = new Gtk.Popover();
|
||||||
|
|
||||||
popover.set_child(BluetoothPickerList());
|
popover.set_child(BluetoothPickerList());
|
||||||
popover.connect( 'closed', () => bt.adapter.stop_discovery() );
|
popover.connect("closed", () => bt.adapter.stop_discovery());
|
||||||
|
|
||||||
return popover;
|
return popover;
|
||||||
};
|
};
|
@ -1,7 +1,6 @@
|
|||||||
import { bind } from "astal";
|
import { bind } from "astal";
|
||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
|
||||||
|
|
||||||
const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => {
|
const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -10,7 +9,11 @@ const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => {
|
|||||||
<centerbox
|
<centerbox
|
||||||
startWidget={
|
startWidget={
|
||||||
<box>
|
<box>
|
||||||
<image iconName={"chronometer-reset"} visible={bind( device, 'connecting' )}></image>
|
<image
|
||||||
|
iconName={"chronometer-reset"}
|
||||||
|
tooltipText={"Device is currently connecting"}
|
||||||
|
visible={bind(device, "connecting")}
|
||||||
|
></image>
|
||||||
<image
|
<image
|
||||||
iconName={bind(device, "icon")}
|
iconName={bind(device, "icon")}
|
||||||
marginEnd={3}
|
marginEnd={3}
|
||||||
@ -29,24 +32,41 @@ const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => {
|
|||||||
label={bind(device, "batteryPercentage").as(
|
label={bind(device, "batteryPercentage").as(
|
||||||
bat => (bat >= 0 ? bat + "%" : "?%"),
|
bat => (bat >= 0 ? bat + "%" : "?%"),
|
||||||
)}
|
)}
|
||||||
|
tooltipText={"Device's battery percentage"}
|
||||||
marginEnd={3}
|
marginEnd={3}
|
||||||
></label>
|
></label>
|
||||||
<image
|
<image
|
||||||
iconName={bind(device, "trusted").as(v =>
|
iconName={bind(device, "paired").as(v =>
|
||||||
v ? "checkbox" : "paint-unknown-symbolic",
|
v ? "network-bluetooth-activated-symbolic" : "bluetooth-disconnected-symbolic",
|
||||||
)}
|
)}
|
||||||
></image>
|
></image>
|
||||||
|
<button tooltipText={"Device trusted status"} child={
|
||||||
|
<image
|
||||||
|
iconName={bind(device, "trusted").as(v =>
|
||||||
|
v ? "checkbox" : "window-close-symbolic",
|
||||||
|
)}
|
||||||
|
></image>
|
||||||
|
} onClicked={() => device.set_trusted( !device.get_trusted() )}
|
||||||
|
cssClasses={[ 'button-no-margin' ]}
|
||||||
|
></button>
|
||||||
</box>
|
</box>
|
||||||
}
|
}
|
||||||
></centerbox>
|
></centerbox>
|
||||||
}
|
}
|
||||||
onClicked={() => {
|
onClicked={() => {
|
||||||
// TODO: Make sure to check if device was previously paired and otherwise do some pairing shenanigans
|
connectOrPair( device );
|
||||||
device.connect_device( () => {} );
|
|
||||||
}}
|
}}
|
||||||
></button>
|
></button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connectOrPair = (device: AstalBluetooth.Device) => {
|
||||||
|
if ( device.get_paired() ) {
|
||||||
|
device.connect_device(() => { });
|
||||||
|
// Show failed message if tried to connect and failed
|
||||||
|
} else {
|
||||||
|
device.pair();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default BTDevice;
|
export default BTDevice;
|
@ -0,0 +1,18 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import Brightness from "../../../../util/brightness";
|
||||||
|
|
||||||
|
const brightness = Brightness.get_default();
|
||||||
|
|
||||||
|
const BrightnessModule = () => {
|
||||||
|
return (
|
||||||
|
<box visible={bind(brightness, 'screenAvailable')}>
|
||||||
|
<image iconName={"brightness-high-symbolic"}></image>
|
||||||
|
<label label={bind(brightness, "screen").as(b => b + "%")}></label>
|
||||||
|
<slider></slider>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
BrightnessModule
|
||||||
|
};
|
@ -0,0 +1,97 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
import networkHelper from "./network-helper";
|
||||||
|
import NetworkMenu from "./NetworkMenu";
|
||||||
|
|
||||||
|
const net = AstalNetwork.get_default();
|
||||||
|
const STATE = AstalNetwork.DeviceState;
|
||||||
|
|
||||||
|
const Network = () => {
|
||||||
|
const netMenu = NetworkMenu.NetworkMenu();
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
cssClasses={networkHelper.networkEnabled(en => {
|
||||||
|
if (en) return ["toggle-button", "toggle-on"];
|
||||||
|
else return ["toggle-button"];
|
||||||
|
})}
|
||||||
|
onClicked={() =>
|
||||||
|
networkHelper.setNetworking(
|
||||||
|
!networkHelper.networkEnabled.get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
child={
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
label={bind(net.wifi, "enabled").as(
|
||||||
|
stat => `Network (${stat ? "WiFi" : "Wired"})`,
|
||||||
|
)}
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
label={bind(net.wired, "state").as(state => {
|
||||||
|
if (state === STATE.ACTIVATED) {
|
||||||
|
return (
|
||||||
|
"Wired. 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";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
visible={bind(net.wifi, "enabled").as(v => !v)}
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
label={bind(net.wifi, "state").as(state => {
|
||||||
|
if (state === STATE.ACTIVATED) {
|
||||||
|
return `${net.wifi.get_ssid()} (${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";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
visible={bind(net.wifi, "enabled")}
|
||||||
|
></label>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
cssClasses={["actions-button"]}
|
||||||
|
visible={networkHelper.networkEnabled()}
|
||||||
|
onClicked={() => netMenu.popup()}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<image iconName={"arrow-right-symbolic"}></image>
|
||||||
|
{ netMenu }
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
tooltipText={"View available devices"}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Network,
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
|
||||||
|
const NetworkMenu = () => {
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
popover.set_child( renderMenu() );
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMenu = () => {
|
||||||
|
return <box vertical>
|
||||||
|
<image iconName={"appointment-soon-symbolic"} iconSize={Gtk.IconSize.LARGE}></image>
|
||||||
|
<label label={"Coming later"}></label>
|
||||||
|
</box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
NetworkMenu,
|
||||||
|
};
|
@ -0,0 +1,56 @@
|
|||||||
|
$fg-color: #{"@theme_fg_color"};
|
||||||
|
$bg-color: #{"@theme_bg_color"};
|
||||||
|
|
||||||
|
box.players-box {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.player {
|
||||||
|
padding: 0.6rem;
|
||||||
|
|
||||||
|
.cover-art {
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
border-radius: 9px;
|
||||||
|
margin-right: 0.6rem;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
scale {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.4rem 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
|
||||||
|
trough {
|
||||||
|
min-height: 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight {
|
||||||
|
background-color: $fg-color;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
slider {
|
||||||
|
all: unset;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
centerbox.actions {
|
||||||
|
min-width: 220px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0.4rem;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
config/astal/components/QuickActions/modules/Player/Player.tsx
Normal file
154
config/astal/components/QuickActions/modules/Player/Player.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import AstalMpris from "gi://AstalMpris";
|
||||||
|
import Pango from "gi://Pango?version=1.0";
|
||||||
|
const ALIGN = Gtk.Align;
|
||||||
|
|
||||||
|
const mpris = AstalMpris.get_default();
|
||||||
|
mpris.connect("player-added", p => {
|
||||||
|
print("Player added:", p);
|
||||||
|
});
|
||||||
|
|
||||||
|
const PlayerModule = () => {
|
||||||
|
return (
|
||||||
|
<box vertical cssClasses={ [ 'players-box' ] }>
|
||||||
|
<label label={"Music Players"} halign={ALIGN.CENTER} cssClasses={[ 'title-2' ]}></label>
|
||||||
|
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
|
||||||
|
<box cssClasses={["players"]}>
|
||||||
|
{bind(mpris, "players").as(players => {
|
||||||
|
return players.map(player => {
|
||||||
|
return <PlayerItem player={player}></PlayerItem>;
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
<label label={"No playback active"} visible={bind(mpris, "players").as( players => players.length === 0 )}></label>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Update widths
|
||||||
|
const pbStatus = AstalMpris.PlaybackStatus;
|
||||||
|
const PlayerItem = ({ player }: { player: AstalMpris.Player }) => {
|
||||||
|
return (
|
||||||
|
<box cssClasses={["player"]}>
|
||||||
|
<image
|
||||||
|
cssClasses={["cover-art"]}
|
||||||
|
file={bind(player, "coverArt")}
|
||||||
|
hexpand
|
||||||
|
vexpand
|
||||||
|
></image>
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
label={bind(player, "title").as(
|
||||||
|
title => title ?? "Unknown title",
|
||||||
|
)}
|
||||||
|
cssClasses={["title"]}
|
||||||
|
halign={ALIGN.START}
|
||||||
|
valign={ALIGN.START}
|
||||||
|
maxWidthChars={30}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
></label>
|
||||||
|
<label
|
||||||
|
label={bind(player, "artist").as(
|
||||||
|
artist => artist ?? "Unknown artist",
|
||||||
|
)}
|
||||||
|
halign={ALIGN.START}
|
||||||
|
valign={ALIGN.START}
|
||||||
|
maxWidthChars={30}
|
||||||
|
ellipsize={Pango.EllipsizeMode.END}
|
||||||
|
></label>
|
||||||
|
<slider
|
||||||
|
visible={bind(player, "length").as(l => l > 0)}
|
||||||
|
value={bind(player, "position")}
|
||||||
|
min={0}
|
||||||
|
max={bind(player, "length")}
|
||||||
|
onChangeValue={v =>
|
||||||
|
player.set_position(v.get_value())
|
||||||
|
}
|
||||||
|
></slider>
|
||||||
|
<centerbox
|
||||||
|
cssClasses={["actions"]}
|
||||||
|
startWidget={
|
||||||
|
<label
|
||||||
|
label={bind(player, "position").as(v =>
|
||||||
|
secondsToFriendlyTime(v),
|
||||||
|
)}
|
||||||
|
hexpand
|
||||||
|
cssClasses={["position"]}
|
||||||
|
></label>
|
||||||
|
}
|
||||||
|
centerWidget={
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
visible={bind(player, "canGoPrevious")}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
iconName={
|
||||||
|
"media-skip-backward-symbolic"
|
||||||
|
}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
onClicked={() => player.previous()}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
visible={bind(player, "canControl")}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
iconName={bind(
|
||||||
|
player,
|
||||||
|
"playbackStatus",
|
||||||
|
).as(status => {
|
||||||
|
if (status === pbStatus.PLAYING) {
|
||||||
|
return "media-playback-pause-symbolic";
|
||||||
|
} else {
|
||||||
|
return "media-playback-start-symbolic";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
onClicked={() => player.play_pause()}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
visible={bind(player, "canGoNext")}
|
||||||
|
child={
|
||||||
|
<image
|
||||||
|
iconName={"media-skip-forward-symbolic"}
|
||||||
|
></image>
|
||||||
|
}
|
||||||
|
onClicked={() => player.next()}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
endWidget={
|
||||||
|
<label
|
||||||
|
cssClasses={["length"]}
|
||||||
|
hexpand
|
||||||
|
label={bind(player, "length").as(v =>
|
||||||
|
secondsToFriendlyTime(v),
|
||||||
|
)}
|
||||||
|
></label>
|
||||||
|
}
|
||||||
|
></centerbox>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondsToFriendlyTime = (time: number) => {
|
||||||
|
const minutes = Math.floor(time / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const seconds = Math.floor(time % 60);
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}:${expandTime(minutes)}:${expandTime(seconds)}`;
|
||||||
|
} else {
|
||||||
|
return `${minutes}:${expandTime(seconds)}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandTime = (time: number): string => {
|
||||||
|
return time < 10 ? `0${time}` : "" + time;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
PlayerModule,
|
||||||
|
};
|
@ -12,29 +12,17 @@ const PowerMenu = (): Gtk.Popover => {
|
|||||||
child={
|
child={
|
||||||
<image iconName={"system-shutdown-symbolic"}></image>
|
<image iconName={"system-shutdown-symbolic"}></image>
|
||||||
}
|
}
|
||||||
onClicked={() => exec("shutdown now")}
|
onClicked={() => exec("/bin/sh -c 'shutdown now'")}
|
||||||
></button>
|
></button>
|
||||||
<button
|
<button
|
||||||
cssClasses={["power-button"]}
|
cssClasses={["power-button"]}
|
||||||
child={<image iconName={"system-reboot-symbolic"}></image>}
|
child={<image iconName={"system-reboot-symbolic"}></image>}
|
||||||
onClicked={() => exec("reboot")}
|
onClicked={() => exec("/bin/sh -c 'reboot'")}
|
||||||
></button>
|
></button>
|
||||||
<button
|
<button
|
||||||
cssClasses={["power-button"]}
|
cssClasses={["power-button"]}
|
||||||
child={<image iconName={"system-suspend-symbolic"}></image>}
|
child={<image iconName={"system-suspend-symbolic"}></image>}
|
||||||
onClicked={() => exec("systemctl suspend")}
|
onClicked={() => exec("/bin/sh -c '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>
|
></button>
|
||||||
</box>
|
</box>
|
||||||
);
|
);
|
||||||
@ -48,7 +36,10 @@ const Power = () => {
|
|||||||
const pm = PowerMenu();
|
const pm = PowerMenu();
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
cssClasses={["PowerMenuButton"]}
|
widthRequest={0}
|
||||||
|
hexpand={false}
|
||||||
|
vexpand={false}
|
||||||
|
cssClasses={["power-menu-button"]}
|
||||||
child={
|
child={
|
||||||
<box>
|
<box>
|
||||||
<image iconName={"system-shutdown-symbolic"}></image>
|
<image iconName={"system-shutdown-symbolic"}></image>
|
||||||
@ -60,4 +51,35 @@ const Power = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Power;
|
const UserMenu = (): Gtk.Popover => {
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
|
||||||
|
const powerMenuBox = () => {
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
<button
|
||||||
|
cssClasses={["power-button"]}
|
||||||
|
child={
|
||||||
|
<image iconName={"system-lock-screen-symbolic"}></image>
|
||||||
|
}
|
||||||
|
onClicked={() => exec("/bin/sh -c 'hyprlock'")}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
cssClasses={["power-button"]}
|
||||||
|
child={<image iconName={"system-log-out-symbolic"}></image>}
|
||||||
|
onClicked={() =>
|
||||||
|
exec("/bin/sh -c 'hyprctl dispatch exit 0'")
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
popover.set_child(powerMenuBox());
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Power,
|
||||||
|
UserMenu
|
||||||
|
};
|
56
config/astal/components/QuickActions/quickactions.scss
Normal file
56
config/astal/components/QuickActions/quickactions.scss
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
@use "./modules/Player/Player.scss";
|
||||||
|
@use "./modules/Audio/Audio.scss";
|
||||||
|
@use "../../util/colours.scss" as *;
|
||||||
|
|
||||||
|
.quick-actions-wrapper {
|
||||||
|
min-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
box.quick-actions {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
popover * {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-no-margin {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-list {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.toggle-button {
|
||||||
|
min-width: 220px;
|
||||||
|
border-radius: 50px;
|
||||||
|
|
||||||
|
&.toggle-on {
|
||||||
|
min-width: 190px;
|
||||||
|
margin-right: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background-color: $accent-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.actions-button {
|
||||||
|
margin-left: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: $accent-color;
|
||||||
|
border-top-right-radius: 50px;
|
||||||
|
border-bottom-right-radius: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-icon {
|
||||||
|
border-radius: 100px;
|
||||||
|
min-height: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
@ -23,7 +23,6 @@ const Bar = (gdkmonitor: Gdk.Monitor) => {
|
|||||||
<box
|
<box
|
||||||
hexpand
|
hexpand
|
||||||
halign={Gtk.Align.START}
|
halign={Gtk.Align.START}
|
||||||
cssClasses={["BarLeft"]}
|
|
||||||
>
|
>
|
||||||
<Calendar.Time />
|
<Calendar.Time />
|
||||||
<SystemInfo.SystemInfo />
|
<SystemInfo.SystemInfo />
|
62
config/astal/components/bar/bar.scss
Normal file
62
config/astal/components/bar/bar.scss
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
@use "../../util/colours.scss" as *;
|
||||||
|
|
||||||
|
window.Bar {
|
||||||
|
font-family: "Comfortaa, sans-serif";
|
||||||
|
background: transparent;
|
||||||
|
color: $fg-color;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
/* >centerbox { */
|
||||||
|
/* background: $bg-color; */
|
||||||
|
/* border-radius: 10px; */
|
||||||
|
/* margin: 8px; */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
.bar-button {
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 2px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
background-color: $bg-color;
|
||||||
|
|
||||||
|
& button {
|
||||||
|
background-color: $bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-button {
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 2px;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
background-color: $bg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.workspace-button {
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 1px;
|
||||||
|
|
||||||
|
&.focused-workspace-button {
|
||||||
|
color: $accent-color-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tray-item {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& button {
|
||||||
|
margin: 2px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
min-width: 11rem;
|
||||||
|
padding: 3px;
|
||||||
|
& button {
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
config/astal/components/bar/modules/Calendar.tsx
Normal file
26
config/astal/components/bar/modules/Calendar.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { GLib, Variable } from "astal";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
|
||||||
|
const Time = ({ format = "%a, %e.%m %H:%M:%S" }) => {
|
||||||
|
const time = Variable<string>("").poll(
|
||||||
|
1000,
|
||||||
|
() => GLib.DateTime.new_now_local().format(format)!,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<menubutton
|
||||||
|
cssClasses={["time", "bar-button"]}
|
||||||
|
hexpand
|
||||||
|
halign={Gtk.Align.CENTER}
|
||||||
|
>
|
||||||
|
<label onDestroy={() => time.drop()} label={time()} halign={Gtk.Align.CENTER}></label>
|
||||||
|
<popover>
|
||||||
|
<Gtk.Calendar />
|
||||||
|
</popover>
|
||||||
|
</menubutton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Time,
|
||||||
|
};
|
197
config/astal/components/bar/modules/Hyprland.tsx
Normal file
197
config/astal/components/bar/modules/Hyprland.tsx
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import AstalTray from "gi://AstalTray";
|
||||||
|
import { bind, GObject } from "astal";
|
||||||
|
import AstalHyprland from "gi://AstalHyprland";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
|
||||||
|
const SYNC = GObject.BindingFlags.SYNC_CREATE;
|
||||||
|
|
||||||
|
const SysTray = () => {
|
||||||
|
const trayBox = new Gtk.Box({ cssClasses: ["bar-button"] });
|
||||||
|
const tray = AstalTray.get_default();
|
||||||
|
|
||||||
|
const trayItems = new Map<string, Gtk.MenuButton>();
|
||||||
|
const trayAddedHandler = tray.connect("item-added", (_, id) => {
|
||||||
|
const item = tray.get_item(id);
|
||||||
|
const popover = Gtk.PopoverMenu.new_from_model(item.menu_model);
|
||||||
|
const icon = new Gtk.Image();
|
||||||
|
const button = new Gtk.MenuButton({
|
||||||
|
popover,
|
||||||
|
child: icon,
|
||||||
|
cssClasses: ["tray-item"],
|
||||||
|
});
|
||||||
|
|
||||||
|
item.bind_property("gicon", icon, "gicon", SYNC);
|
||||||
|
popover.insert_action_group("dbusmenu", item.action_group);
|
||||||
|
item.connect("notify::action-group", () => {
|
||||||
|
popover.insert_action_group("dbusmenu", item.action_group);
|
||||||
|
});
|
||||||
|
|
||||||
|
trayItems.set(id, button);
|
||||||
|
trayBox.append(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
const trayRemovedHandler = tray.connect("item-removed", (_, id) => {
|
||||||
|
const button = trayItems.get(id);
|
||||||
|
if (button) {
|
||||||
|
trayBox.remove(button);
|
||||||
|
button.run_dispose();
|
||||||
|
trayItems.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
trayBox.connect("destroy", () => {
|
||||||
|
tray.disconnect(trayAddedHandler);
|
||||||
|
tray.disconnect(trayRemovedHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
return trayBox;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Workspace = () => {
|
||||||
|
const hypr = AstalHyprland.get_default();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box>
|
||||||
|
{bind(hypr, "workspaces").as(wss =>
|
||||||
|
wss
|
||||||
|
.filter(ws => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
||||||
|
.sort((a, b) => a.id - b.id)
|
||||||
|
.map(ws => (
|
||||||
|
<button
|
||||||
|
cssClasses={bind(hypr, "focusedWorkspace").as(fw =>
|
||||||
|
ws === fw
|
||||||
|
? [
|
||||||
|
"focused-workspace-button",
|
||||||
|
"workspace-button",
|
||||||
|
]
|
||||||
|
: ["workspace-button"],
|
||||||
|
)}
|
||||||
|
onButtonPressed={() => ws.focus()}
|
||||||
|
child={<label label={String(ws.id)}></label>}
|
||||||
|
></button>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the name of the currently active window and provides a popover for
|
||||||
|
* displaying all available clients
|
||||||
|
*/
|
||||||
|
const ActiveWindow = () => {
|
||||||
|
const hypr = AstalHyprland.get_default();
|
||||||
|
const focused = bind(hypr, "focusedClient");
|
||||||
|
|
||||||
|
const WindowPopover = (): Gtk.Popover => {
|
||||||
|
// Set up boxes + Popover
|
||||||
|
const clients = new Map<string, Gtk.Button>();
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
const popoverBox = new Gtk.Box({
|
||||||
|
orientation: Gtk.Orientation.VERTICAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const widgetTitle = new Gtk.Label({
|
||||||
|
cssClasses: ["title-2"],
|
||||||
|
label: "Available Windows",
|
||||||
|
});
|
||||||
|
|
||||||
|
popoverBox.append(widgetTitle);
|
||||||
|
|
||||||
|
const seaparator = new Gtk.Separator({
|
||||||
|
marginTop: 5,
|
||||||
|
marginBottom: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
popoverBox.append(seaparator);
|
||||||
|
|
||||||
|
const addClient = (client: AstalHyprland.Client) => {
|
||||||
|
const clientBox = new Gtk.Box();
|
||||||
|
|
||||||
|
// Workspace description
|
||||||
|
const descWS = new Gtk.Label({ label: "(WS " });
|
||||||
|
|
||||||
|
// Workpsace information
|
||||||
|
const workspace = new Gtk.Label();
|
||||||
|
client.workspace.bind_property("name", workspace, "label", SYNC);
|
||||||
|
|
||||||
|
const windowClassDesc = new Gtk.Label({ label: ") [" });
|
||||||
|
|
||||||
|
const windowClass = new Gtk.Label();
|
||||||
|
windowClass.label = client.get_initial_class();
|
||||||
|
|
||||||
|
const titleDesc = new Gtk.Label({ label: "] " });
|
||||||
|
titleDesc.set_margin_end(2);
|
||||||
|
|
||||||
|
const title = new Gtk.Label();
|
||||||
|
client.bind_property("title", title, "label", SYNC);
|
||||||
|
|
||||||
|
clientBox.append(descWS);
|
||||||
|
clientBox.append(workspace);
|
||||||
|
clientBox.append(windowClassDesc);
|
||||||
|
clientBox.append(windowClass);
|
||||||
|
clientBox.append(titleDesc);
|
||||||
|
clientBox.append(title);
|
||||||
|
|
||||||
|
const button = new Gtk.Button();
|
||||||
|
button.connect( 'clicked', () => {
|
||||||
|
client.workspace.focus();
|
||||||
|
} );
|
||||||
|
button.set_child(clientBox);
|
||||||
|
|
||||||
|
popoverBox.append(button);
|
||||||
|
|
||||||
|
clients.set(client.get_address(), button);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Populate with already added clients
|
||||||
|
const c = hypr.get_clients();
|
||||||
|
for (let index = 0; index < c.length; index++) {
|
||||||
|
addClient(c[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
hypr.connect("client-added", (_, client) => {
|
||||||
|
addClient(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
hypr.connect("client-removed", (_, client) => {
|
||||||
|
const c = clients.get(client);
|
||||||
|
if (c) {
|
||||||
|
popoverBox.remove(c);
|
||||||
|
c.run_dispose();
|
||||||
|
clients.delete(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
popover.set_child(popoverBox);
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const windowPopover = WindowPopover();
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
// Return fully assembled HyprlandFocusedClient box
|
||||||
|
// ───────────────────────────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
<box visible={focused.as(Boolean)}>
|
||||||
|
<button
|
||||||
|
onClicked={() => windowPopover.popup()}
|
||||||
|
cssClasses={["bar-button"]}
|
||||||
|
>
|
||||||
|
{focused.as(
|
||||||
|
client =>
|
||||||
|
client && (
|
||||||
|
<label label={bind(client, "title").as(String)} />
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{windowPopover}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Workspace,
|
||||||
|
ActiveWindow,
|
||||||
|
SysTray,
|
||||||
|
};
|
186
config/astal/components/bar/modules/QuickView.tsx
Normal file
186
config/astal/components/bar/modules/QuickView.tsx
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import { bind } from "astal";
|
||||||
|
import AstalBattery from "gi://AstalBattery";
|
||||||
|
import AstalBluetooth from "gi://AstalBluetooth";
|
||||||
|
import AstalNetwork from "gi://AstalNetwork";
|
||||||
|
import AstalWp from "gi://AstalWp";
|
||||||
|
import { Gtk } from "astal/gtk4";
|
||||||
|
import Brightness from "../../../util/brightness";
|
||||||
|
import QuickActions from "../../QuickActions/QuickActions";
|
||||||
|
|
||||||
|
const STATE = AstalNetwork.DeviceState;
|
||||||
|
|
||||||
|
const QuickView = () => {
|
||||||
|
const qa = QuickActions.QuickActions();
|
||||||
|
const showQuickActions = () => {
|
||||||
|
qa.popup();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClicked={() => showQuickActions()}
|
||||||
|
cssClasses={["quick-action-button"]}
|
||||||
|
child={
|
||||||
|
<box>
|
||||||
|
<BatteryWidget></BatteryWidget>
|
||||||
|
<Audio></Audio>
|
||||||
|
<BluetoothWidget></BluetoothWidget>
|
||||||
|
<NetworkWidget></NetworkWidget>
|
||||||
|
<BrightnessWidget></BrightnessWidget>
|
||||||
|
<image iconName={"system-shutdown-symbolic"}></image>
|
||||||
|
{qa}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
></button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NetworkWidget = () => {
|
||||||
|
const network = AstalNetwork.get_default();
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
return "network-wired-activated-symbolic";
|
||||||
|
} else {
|
||||||
|
return "paint-unknown-symbolic";
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
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", "quick-view-symbol"]}
|
||||||
|
visible={bind(network.wifi, "state").as(
|
||||||
|
state => state === STATE.ACTIVATED,
|
||||||
|
)}
|
||||||
|
></image>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BluetoothWidget = () => {
|
||||||
|
const bluetooth = AstalBluetooth.get_default();
|
||||||
|
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={bind(battery, "iconName").as(icon => icon)}
|
||||||
|
cssClasses={["quick-view-symbol"]}
|
||||||
|
></image>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <box></box>;
|
||||||
|
}
|
||||||
|
// Else, no battery available -> Don't show the widget
|
||||||
|
};
|
||||||
|
|
||||||
|
const BrightnessWidget = () => {
|
||||||
|
const brightness = Brightness.get_default();
|
||||||
|
const screen_brightness = bind(brightness, "screen");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
label={"🌣" + screen_brightness}
|
||||||
|
visible={bind(brightness, "screenAvailable")}
|
||||||
|
cssClasses={["quick-view-symbol"]}
|
||||||
|
></label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Audio = () => {
|
||||||
|
const wireplumber = AstalWp.get_default();
|
||||||
|
if (wireplumber) {
|
||||||
|
return (
|
||||||
|
<box orientation={Gtk.Orientation.HORIZONTAL}>
|
||||||
|
<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>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// cssClasses={[ 'quick-view-symbol' ]}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
QuickView,
|
||||||
|
};
|
211
config/astal/components/bar/modules/SystemInfo.tsx
Normal file
211
config/astal/components/bar/modules/SystemInfo.tsx
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import { exec, execAsync, 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 & sed are available
|
||||||
|
try {
|
||||||
|
exec("awk -V");
|
||||||
|
exec("sed --version");
|
||||||
|
enabled = true;
|
||||||
|
} catch (e) {
|
||||||
|
printerr(
|
||||||
|
"[ SysInfo ] AWK or SED missing! No system info will be available",
|
||||||
|
);
|
||||||
|
enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if mpstat is available
|
||||||
|
try {
|
||||||
|
exec("mpstat -V");
|
||||||
|
} catch (e) {
|
||||||
|
availableFeatures.cpu = false;
|
||||||
|
printerr(
|
||||||
|
"[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = () => {
|
||||||
|
return (
|
||||||
|
<box vertical>
|
||||||
|
<label
|
||||||
|
label={"System Information"}
|
||||||
|
cssClasses={["title-2"]}
|
||||||
|
></label>
|
||||||
|
<Gtk.Separator marginTop={5} marginBottom={10}></Gtk.Separator>
|
||||||
|
<label
|
||||||
|
vexpand
|
||||||
|
halign={Gtk.Align.START}
|
||||||
|
hexpand
|
||||||
|
label={ramUsed(used => {
|
||||||
|
return "RAM: " + 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} mb/s
|
||||||
|
Kernel: ${stats.kernel}`;
|
||||||
|
})}
|
||||||
|
></label>
|
||||||
|
<Gtk.Separator marginTop={10}></Gtk.Separator>
|
||||||
|
<button
|
||||||
|
onClicked={() => exec( `/bin/sh -c "kitty --hold fish -c 'fastfetch'"` )}
|
||||||
|
child={
|
||||||
|
<label label={"View FastFetch"}></label>
|
||||||
|
}></button>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SystemInformationPanel = () => {
|
||||||
|
const popover = new Gtk.Popover();
|
||||||
|
|
||||||
|
popover.set_child(info());
|
||||||
|
|
||||||
|
return popover;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sysInfoFetcher = () => {
|
||||||
|
if (enabled) {
|
||||||
|
if (availableFeatures.cpu) {
|
||||||
|
cpuUtil.set(
|
||||||
|
"" +
|
||||||
|
Math.round(
|
||||||
|
parseFloat(exec(`/bin/fish -c cpu-utilization`)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (availableFeatures.ram) {
|
||||||
|
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 = () => {
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
cssClasses={["bar-button"]}
|
||||||
|
></button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <image iconName={"action-unavailable-symbolic"}></image>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
SystemInfo,
|
||||||
|
panel,
|
||||||
|
};
|
@ -1,28 +0,0 @@
|
|||||||
import { Gtk } from "astal/gtk4"
|
|
||||||
import Power from "./modules/Power";
|
|
||||||
import Audio from "./modules/Audio/Audio";
|
|
||||||
import Bluetooth from "./modules/Bluetooth/Bluetooth";
|
|
||||||
|
|
||||||
const QuickActions = () => {
|
|
||||||
const popover = new Gtk.Popover( { cssClasses: [ 'quick-actions-popover' ] } );
|
|
||||||
|
|
||||||
popover.set_child( createQuickActionMenu() );
|
|
||||||
|
|
||||||
return popover;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const createQuickActionMenu = () => {
|
|
||||||
// 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>
|
|
||||||
<Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
|
|
||||||
<Audio.AudioModule></Audio.AudioModule>
|
|
||||||
</box>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: Expose additional functions to be usable through CLI
|
|
||||||
export default {
|
|
||||||
QuickActions
|
|
||||||
};
|
|
@ -1,13 +0,0 @@
|
|||||||
.title-2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bt-conn-list {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
popover>box {
|
|
||||||
margin: 10px;
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import { GLib, Variable } from "astal"
|
|
||||||
import { Gtk } from "astal/gtk4"
|
|
||||||
|
|
||||||
const Time = ({ format = "%a, %e.%m %H:%M:%S" }) => {
|
|
||||||
const time = Variable<string>("").poll(1000, () =>
|
|
||||||
GLib.DateTime.new_now_local().format(format)!)
|
|
||||||
|
|
||||||
return <menubutton cssClasses={["Time"]} hexpand halign={Gtk.Align.CENTER}>
|
|
||||||
<label onDestroy={() => time.drop()} label={time()}></label>
|
|
||||||
<popover>
|
|
||||||
<Gtk.Calendar />
|
|
||||||
</popover>
|
|
||||||
</menubutton>
|
|
||||||
}
|
|
||||||
|
|
||||||
const Calendar = () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
|
||||||
Time
|
|
||||||
}
|
|
@ -1,157 +0,0 @@
|
|||||||
|
|
||||||
import AstalTray from "gi://AstalTray";
|
|
||||||
import { bind, GObject } from "astal";
|
|
||||||
import AstalHyprland from "gi://AstalHyprland";
|
|
||||||
import { Gtk } from "astal/gtk4";
|
|
||||||
|
|
||||||
const SYNC = GObject.BindingFlags.SYNC_CREATE;
|
|
||||||
|
|
||||||
const SysTray = () => {
|
|
||||||
const trayBox = new Gtk.Box();
|
|
||||||
const tray = AstalTray.get_default();
|
|
||||||
|
|
||||||
const trayItems = new Map<string, Gtk.MenuButton>();
|
|
||||||
const trayAddedHandler = tray.connect("item-added", (_, id) => {
|
|
||||||
const item = tray.get_item(id);
|
|
||||||
const popover = Gtk.PopoverMenu.new_from_model(item.menu_model);
|
|
||||||
const icon = new Gtk.Image();
|
|
||||||
const button = new Gtk.MenuButton({ popover, child: icon });
|
|
||||||
|
|
||||||
item.bind_property("gicon", icon, "gicon", SYNC);
|
|
||||||
popover.insert_action_group("dbusmenu", item.action_group);
|
|
||||||
item.connect("notify::action-group", () => {
|
|
||||||
popover.insert_action_group("dbusmenu", item.action_group);
|
|
||||||
});
|
|
||||||
|
|
||||||
trayItems.set(id, button);
|
|
||||||
trayBox.append(button);
|
|
||||||
})
|
|
||||||
|
|
||||||
const trayRemovedHandler = tray.connect("item-removed", (_, id) => {
|
|
||||||
const button = trayItems.get(id);
|
|
||||||
if (button) {
|
|
||||||
trayBox.remove(button);
|
|
||||||
button.run_dispose();
|
|
||||||
trayItems.delete(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
trayBox.connect("destroy", () => {
|
|
||||||
tray.disconnect(trayAddedHandler);
|
|
||||||
tray.disconnect(trayRemovedHandler);
|
|
||||||
});
|
|
||||||
|
|
||||||
return trayBox;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const Workspace = () => {
|
|
||||||
const hypr = AstalHyprland.get_default()
|
|
||||||
|
|
||||||
return <box cssClasses={["HyprlandWorkspaces"]}>
|
|
||||||
{bind(hypr, "workspaces").as(wss => wss
|
|
||||||
.filter(ws => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
|
|
||||||
.sort((a, b) => a.id - b.id)
|
|
||||||
.map(ws => (
|
|
||||||
<button
|
|
||||||
cssClasses={bind(hypr, "focusedWorkspace").as(fw =>
|
|
||||||
ws === fw ? ["HyprlandFocusedWorkspace"] : [""])}
|
|
||||||
onButtonPressed={() => ws.focus()} child={<label label={String(ws.id)}></label>}>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</box>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the name of the currently active window and provides a popover for
|
|
||||||
* displaying all available clients
|
|
||||||
*/
|
|
||||||
const ActiveWindow = () => {
|
|
||||||
const hypr = AstalHyprland.get_default();
|
|
||||||
const focused = bind( hypr, "focusedClient" );
|
|
||||||
|
|
||||||
const WindowPopover = (): Gtk.Popover => {
|
|
||||||
// Set up boxes + Popover
|
|
||||||
const clients = new Map<string, Gtk.Box>();
|
|
||||||
const popover = new Gtk.Popover();
|
|
||||||
const popoverBox = new Gtk.Box( { orientation: Gtk.Orientation.VERTICAL } );
|
|
||||||
|
|
||||||
const addClient = ( client: AstalHyprland.Client ) => {
|
|
||||||
const clientBox = new Gtk.Box();
|
|
||||||
|
|
||||||
// Workspace description
|
|
||||||
const descWS = new Gtk.Label( { label: '(WS ' } );
|
|
||||||
|
|
||||||
// Workpsace information
|
|
||||||
const workspace = new Gtk.Label();
|
|
||||||
client.workspace.bind_property( 'name', workspace, 'label', SYNC );
|
|
||||||
|
|
||||||
const windowClassDesc = new Gtk.Label( { label: ') [' } );
|
|
||||||
|
|
||||||
const windowClass = new Gtk.Label();
|
|
||||||
windowClass.label = client.get_initial_class();
|
|
||||||
|
|
||||||
const titleDesc = new Gtk.Label( { label: '] ' } );
|
|
||||||
titleDesc.set_margin_end( 2 );
|
|
||||||
|
|
||||||
const title = new Gtk.Label();
|
|
||||||
client.bind_property( 'title', title, 'label', SYNC );
|
|
||||||
|
|
||||||
clientBox.append( descWS );
|
|
||||||
clientBox.append( workspace );
|
|
||||||
clientBox.append( windowClassDesc );
|
|
||||||
clientBox.append( windowClass );
|
|
||||||
clientBox.append( titleDesc );
|
|
||||||
clientBox.append( title );
|
|
||||||
|
|
||||||
popoverBox.append( clientBox );
|
|
||||||
|
|
||||||
clients.set( client.get_address(), clientBox );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate with already added clients
|
|
||||||
const c = hypr.get_clients();
|
|
||||||
for ( let index = 0; index < c.length; index++ ) {
|
|
||||||
addClient( c[ index ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
hypr.connect( 'client-added', ( _, client ) => {
|
|
||||||
addClient( client );
|
|
||||||
} );
|
|
||||||
|
|
||||||
hypr.connect( 'client-removed', ( _, client ) => {
|
|
||||||
const c = clients.get( client );
|
|
||||||
if ( c ) {
|
|
||||||
popoverBox.remove( c );
|
|
||||||
c.run_dispose();
|
|
||||||
clients.delete( client );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
popover.set_child( popoverBox );
|
|
||||||
return popover;
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowPopover = WindowPopover();
|
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────
|
|
||||||
// Return fully assembled HyprlandFocusedClient box
|
|
||||||
// ───────────────────────────────────────────────────────────────────
|
|
||||||
return <box cssName={"HyprlandFocusedClients"} visible={focused.as(Boolean)}>
|
|
||||||
<button onClicked={() => windowPopover.popup()}>
|
|
||||||
{focused.as( client => (
|
|
||||||
client && <label label={bind( client, "title" ).as( String )} />
|
|
||||||
))}
|
|
||||||
</button>
|
|
||||||
{ windowPopover }
|
|
||||||
</box>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
|
||||||
Workspace,
|
|
||||||
ActiveWindow,
|
|
||||||
SysTray
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
import { bind } from "astal";
|
|
||||||
import AstalBattery from "gi://AstalBattery";
|
|
||||||
import AstalBluetooth from "gi://AstalBluetooth";
|
|
||||||
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 = () => {
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const NetworkWidget = () => {
|
|
||||||
const network = AstalNetwork.get_default();
|
|
||||||
|
|
||||||
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 ) {
|
|
||||||
return 'network-wired-activated-symbolic';
|
|
||||||
} else {
|
|
||||||
return 'paint-unknown-symbolic';
|
|
||||||
}
|
|
||||||
} )} 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', 'quick-view-symbol' ]} visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED )}></image>
|
|
||||||
</box>
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const BluetoothWidget = () => {
|
|
||||||
const bluetooth = AstalBluetooth.get_default();
|
|
||||||
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={bind( battery, 'iconName' ).as( icon => icon )} cssClasses={[ 'quick-view-symbol' ]}></image>
|
|
||||||
} else {
|
|
||||||
return <box></box>
|
|
||||||
}
|
|
||||||
// Else, no battery available -> Don't show the widget
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const BrightnessWidget = () => {
|
|
||||||
const brightness = Brightness.get_default();
|
|
||||||
const screen_brightness = bind( brightness, "screen" );
|
|
||||||
|
|
||||||
return <label label={"🌣" + screen_brightness} visible={bind( brightness, 'screenAvailable' )} cssClasses={[ 'quick-view-symbol' ]}></label>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const Audio = () => {
|
|
||||||
const wireplumber = AstalWp.get_default();
|
|
||||||
if ( wireplumber ) {
|
|
||||||
return <box orientation={Gtk.Orientation.HORIZONTAL}>
|
|
||||||
<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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cssClasses={[ 'quick-view-symbol' ]}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
QuickView
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
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 & sed are available
|
|
||||||
try {
|
|
||||||
exec( 'awk -V' );
|
|
||||||
exec( 'sed --version' );
|
|
||||||
enabled = true;
|
|
||||||
} catch ( e ) {
|
|
||||||
printerr( '[ SysInfo ] AWK or SED missing! No system info will be available' );
|
|
||||||
enabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if mpstat is available
|
|
||||||
try {
|
|
||||||
exec( 'mpstat -V' );
|
|
||||||
} catch ( e ) {
|
|
||||||
availableFeatures.cpu = false;
|
|
||||||
printerr( '[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ) {
|
|
||||||
cpuUtil.set( '' + Math.round( parseFloat( exec( `/bin/fish -c cpu-utilization` ) ) ) );
|
|
||||||
}
|
|
||||||
if ( availableFeatures.ram ) {
|
|
||||||
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 = () => {
|
|
||||||
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,
|
|
||||||
panel
|
|
||||||
}
|
|
BIN
config/astal/no-avatar-icon.jpg
Normal file
BIN
config/astal/no-avatar-icon.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
@ -1,6 +1,7 @@
|
|||||||
@use './components/notifications/notifications.scss';
|
/* @use './components/notifications/notifications.scss'; */
|
||||||
@use './components/bar/ui/bar.scss';
|
@use "./components/bar/bar.scss";
|
||||||
@use './components/bar/ui/QuickActions/quickactions.scss';
|
@use "./components/QuickActions/quickactions.scss";
|
||||||
|
@use "./util/colours.scss" as *;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@ -10,3 +11,13 @@ empty {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
4
config/astal/util/colours.scss
Normal file
4
config/astal/util/colours.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
$fg-color: #{"@theme_fg_color"};
|
||||||
|
$bg-color: #{"@theme_bg_color"};
|
||||||
|
$accent-color: #202050;
|
||||||
|
$accent-color-2: #202080;
|
3
config/astal/util/state.ts
Normal file
3
config/astal/util/state.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Variable } from "astal";
|
||||||
|
|
||||||
|
export const quickActionsState = Variable( false );
|
20
scripts/get-profile-picture
Executable file
20
scripts/get-profile-picture
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
USER_NAME=$(whoami)
|
||||||
|
|
||||||
|
# Common paths to check
|
||||||
|
paths=(
|
||||||
|
"/var/lib/AccountsService/icons/$USER_NAME"
|
||||||
|
"$HOME/.face"
|
||||||
|
"$HOME/.face.icon"
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in "${paths[@]}"; do
|
||||||
|
if [ -f "$path" ]; then
|
||||||
|
echo "$path"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Nothing found — exit silently or optionally with an error code
|
||||||
|
exit 0
|
Loading…
x
Reference in New Issue
Block a user