[AGS] Bar: BT, Audio, SysInfo, Brightness
This commit is contained in:
		| @@ -3,40 +3,55 @@ import Hyprland from "./modules/Hyprland"; | ||||
| import Calendar from "./modules/Calendar"; | ||||
| import QuickView from "./modules/QuickView"; | ||||
| import SystemInfo from "./modules/SystemInfo"; | ||||
| import { CenterBox } from "astal/gtk4/widget"; | ||||
|  | ||||
| const Bar = (gdkmonitor: Gdk.Monitor) => { | ||||
|     const { TOP, LEFT, RIGHT } = Astal.WindowAnchor; | ||||
|  | ||||
|     return ( | ||||
|         <window gdkmonitor={gdkmonitor} | ||||
|         <window | ||||
|             gdkmonitor={gdkmonitor} | ||||
|             cssClasses={["Bar"]} | ||||
|             exclusivity={Astal.Exclusivity.EXCLUSIVE} | ||||
|             anchor={TOP | LEFT | RIGHT} | ||||
|             visible | ||||
|             application={App} | ||||
|             child={ | ||||
|             <box orientation={Gtk.Orientation.HORIZONTAL}> | ||||
|                 <box hexpand halign={Gtk.Align.START} cssClasses={["BarLeft"]}> | ||||
|                     <Calendar.Time /> | ||||
|                     <SystemInfo.SystemInfo /> | ||||
|                     <Hyprland.Workspace /> | ||||
|                 </box> | ||||
|                 <Hyprland.ActiveWindow /> | ||||
|                 <box hexpand halign={Gtk.Align.END} cssClasses={["BarRight"]}> | ||||
|                     <Hyprland.SysTray /> | ||||
|                     <QuickView.QuickView /> | ||||
|                 </box> | ||||
|             </box> | ||||
|             }> | ||||
|         </window> | ||||
|                 <CenterBox | ||||
|                     orientation={Gtk.Orientation.HORIZONTAL} | ||||
|                     start_widget={ | ||||
|                         <box | ||||
|                             hexpand | ||||
|                             halign={Gtk.Align.START} | ||||
|                             cssClasses={["BarLeft"]} | ||||
|                         > | ||||
|                             <Calendar.Time /> | ||||
|                             <SystemInfo.SystemInfo /> | ||||
|                             <Hyprland.Workspace /> | ||||
|                         </box> | ||||
|                     } | ||||
|                     centerWidget={<Hyprland.ActiveWindow />} | ||||
|                     endWidget={ | ||||
|                         <box | ||||
|                             hexpand | ||||
|                             halign={Gtk.Align.END} | ||||
|                             cssClasses={["BarRight"]} | ||||
|                         > | ||||
|                             <Hyprland.SysTray /> | ||||
|                             <QuickView.QuickView /> | ||||
|                         </box> | ||||
|                     } | ||||
|                 ></CenterBox> | ||||
|             } | ||||
|         ></window> | ||||
|     ); | ||||
| } | ||||
| }; | ||||
|  | ||||
| const cliHandler = ( args: string[] ): string => { | ||||
|     return 'Not implemented'; | ||||
| } | ||||
| const cliHandler = (args: string[]): string => { | ||||
|     return "Not implemented"; | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     Bar, | ||||
|     cliHandler | ||||
|     cliHandler, | ||||
| }; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { Gtk } from "astal/gtk4" | ||||
| import Network from "./modules/Networking/Network"; | ||||
| 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' ] } ); | ||||
| @@ -12,9 +13,11 @@ const QuickActions = () => { | ||||
|  | ||||
|  | ||||
| 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> | ||||
|         <Network></Network> | ||||
|         <Bluetooth.BluetoothModule></Bluetooth.BluetoothModule> | ||||
|         <Audio.AudioModule></Audio.AudioModule> | ||||
|     </box> | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
| }; | ||||
| @@ -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, | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 ]; | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -1,37 +1,13 @@ | ||||
| @import "colors"; | ||||
|  | ||||
| .toggle-row { | ||||
|   background-color: $bg; | ||||
|   border-radius: 12px; | ||||
|   margin: 6px 0; | ||||
|   overflow: hidden; | ||||
|   border: 1px solid $border; | ||||
|  | ||||
|   button { | ||||
|     padding: 10px 16px; | ||||
|     font-size: 14px; | ||||
|     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; | ||||
|   } | ||||
| .title-2 { | ||||
|     font-size: 1.2rem; | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| .bt-conn-list { | ||||
|   margin-bottom: 20px; | ||||
| } | ||||
|  | ||||
| popover>box { | ||||
|   margin: 10px; | ||||
|   border-radius: 50px; | ||||
| } | ||||
|   | ||||
							
								
								
									
										19
									
								
								config/astal/components/bar/ui/bar.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								config/astal/components/bar/ui/bar.scss
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
| } | ||||
| @@ -13,11 +13,14 @@ const STATE = AstalNetwork.DeviceState; | ||||
| const QuickView = () => { | ||||
|     const quickActions = QuickActions.QuickActions(); | ||||
|  | ||||
|  | ||||
|     return <button onClicked={() => quickActions.popup()} child={ | ||||
|         <box> | ||||
|             <BatteryWidget></BatteryWidget> | ||||
|             <Audio></Audio> | ||||
|             <BluetoothWidget></BluetoothWidget> | ||||
|             <NetworkWidget></NetworkWidget> | ||||
|             <BrightnessWidget></BrightnessWidget> | ||||
|             <image iconName={"system-shutdown-symbolic"}></image> | ||||
|             { quickActions } | ||||
|         </box> | ||||
|     }></button> | ||||
| @@ -32,20 +35,18 @@ const NetworkWidget = () => { | ||||
|             if ( state === AstalNetwork.State.CONNECTING ) { | ||||
|                 return 'chronometer-reset-symbolic'; | ||||
|             } else if ( state === AstalNetwork.State.CONNECTED_LOCAL || state === AstalNetwork.State.CONNECTED_SITE || state === AstalNetwork.State.CONNECTED_GLOBAL ) { | ||||
|                 print( 'Wired connected' ); | ||||
|                 return 'network-wired-activated-symbolic'; | ||||
|             } else { | ||||
|                 print( 'Unknown state' ); | ||||
|                 return 'paint-unknown-symbolic'; | ||||
|             } | ||||
|         } )} cssClasses={[ 'network-widget' ]} visible={bind( network.wifi, 'state' ).as( state => state !== STATE.ACTIVATED )}></image> | ||||
|         } )} cssClasses={[ 'network-widget', 'quick-view-symbol' ]} visible={bind( network.wifi, 'state' ).as( state => state !== STATE.ACTIVATED )}></image> | ||||
|         <image iconName={bind( network.wifi, 'state' ).as( state => { | ||||
|             if ( state === STATE.ACTIVATED ) { | ||||
|                 return network.wifi.iconName | ||||
|             } else { | ||||
|                 return ''; | ||||
|             } | ||||
|         } )} cssClasses={[ 'network-widget' ]} visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED )}></image> | ||||
|         } )} cssClasses={[ 'network-widget', 'quick-view-symbol' ]} visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED )}></image> | ||||
|     </box> | ||||
|          | ||||
|  | ||||
| @@ -53,15 +54,34 @@ const NetworkWidget = () => { | ||||
|  | ||||
| const BluetoothWidget = () => { | ||||
|     const bluetooth = AstalBluetooth.get_default(); | ||||
|     const enabled = bind( bluetooth, "isPowered" ); | ||||
|     const enabled = bind( bluetooth.adapter, "powered" ); | ||||
|     const connected = bind( bluetooth, "isConnected" ); | ||||
|  | ||||
|     // For each connected BT device, render status | ||||
|     return <box> | ||||
|         <box visible={enabled.as( e => e )}> | ||||
|             <image iconName={"bluetooth-active-symbolic"} visible={connected.as( c => c )}></image> | ||||
|             <image iconName={"bluetooth-disconnected-symbolic"} visible={connected.as( c => !c )}></image> | ||||
|         </box> | ||||
|         <image iconName={"bluetooth-disabled-symbolic"} visible={enabled.as( e => !e )}></image> | ||||
|         <box> | ||||
|             {bind( bluetooth, 'devices' ).as( devices => { | ||||
|                 return devices.map( device => { | ||||
|                     return <box visible={bind( device, 'connected' ).as( c => c )}> | ||||
|                         <image iconName={bind( device, 'icon' ).as( icon => icon )}></image> | ||||
|                         <label label={bind( device, 'batteryPercentage' ).as( n => { return n + '%' } ) }></label> | ||||
|                     </box> | ||||
|                 } ); | ||||
|             } )} | ||||
|         </box> | ||||
|     </box> | ||||
| } | ||||
|  | ||||
|  | ||||
| const BatteryWidget = () => { | ||||
|     const battery = AstalBattery.get_default(); | ||||
|     if ( battery.get_is_present() ) { | ||||
|         return <image iconName={battery.iconName}></image> | ||||
|         return <image iconName={bind( battery, 'iconName' ).as( icon => icon )} cssClasses={[ 'quick-view-symbol' ]}></image> | ||||
|     } else { | ||||
|         return <box></box> | ||||
|     } | ||||
| @@ -70,31 +90,27 @@ const BatteryWidget = () => { | ||||
|  | ||||
|  | ||||
| const BrightnessWidget = () => { | ||||
|     // TODO: Finish (detect if there is a controllable screen) | ||||
|     const brightness = Brightness.get_default(); | ||||
|     const screen_brightness = bind( brightness, "screen" ); | ||||
|  | ||||
|     return <label label={"🌣" + screen_brightness}></label> | ||||
|     return <label label={"🌣" + screen_brightness} visible={bind( brightness, 'screenAvailable' )} cssClasses={[ 'quick-view-symbol' ]}></label> | ||||
| } | ||||
|  | ||||
|  | ||||
| const Audio = () => { | ||||
|     const wireplumber = AstalWp.get_default(); | ||||
|     if ( wireplumber ) { | ||||
|         const volume_speakers = bind( wireplumber.defaultSpeaker, 'volume' ); | ||||
|  | ||||
|         return <box orientation={Gtk.Orientation.HORIZONTAL}> | ||||
|             <image iconName={wireplumber.defaultSpeaker.volumeIcon}></image> | ||||
|             <image iconName={wireplumber.defaultMicrophone.volumeIcon}></image> | ||||
|             <label label={volume_speakers.as( v => { return "" + v } ) }></label> | ||||
|             <label label={wireplumber.defaultSpeaker.get_name()}></label> | ||||
|             <image iconName={bind(wireplumber.defaultSpeaker, 'volumeIcon').as( icon => icon )} cssClasses={[ 'quick-view-symbol' ]}></image> | ||||
|             <image iconName={bind(wireplumber.defaultMicrophone, 'volumeIcon').as( icon => icon )} cssClasses={[ 'quick-view-symbol' ]}></image> | ||||
|         </box> | ||||
|     } else { | ||||
|         print( '[ WirePlumber ] Could not connect, Audio support in bar will be missing' ); | ||||
|         return <image iconName={"action-unavailable-symbolic"}></image>; | ||||
|     } | ||||
|     return null; | ||||
| } | ||||
|  | ||||
| // cssClasses={[ 'quick-view-symbol' ]} | ||||
|  | ||||
| export default { | ||||
|     QuickView | ||||
|   | ||||
| @@ -1,7 +1,46 @@ | ||||
| import { exec, GLib } from "astal" | ||||
| import { exec, GLib, interval, Variable } from "astal" | ||||
| import { Gtk } from "astal/gtk4"; | ||||
| import AstalBattery from "gi://AstalBattery?version=0.1"; | ||||
|  | ||||
|  | ||||
| const FETCH_INTERVAL = 2000; | ||||
|  | ||||
|  | ||||
| const cpuUtil = Variable( '0%' ); | ||||
| const ramUtil = Variable( '0%' ); | ||||
| const ramUsed = Variable( '0MiB' ); | ||||
| const gpuUtil = Variable( '0%' ); | ||||
| let gpuName = 'card1'; | ||||
| let enabled = false; | ||||
|  | ||||
| const refreshStats = (): Stats => { | ||||
|     gpuName = exec( `/bin/bash -c "ls /sys/class/drm/ | grep '^card[0-9]*$'"` ); | ||||
|     const cpuNameInSensors = 'CPUTIN' | ||||
|     const stats = { | ||||
|         kernel: exec( 'uname -sr' ), | ||||
|         netSpeed: exec( `/bin/bash -c "interface=$(ip route get 8.8.8.8 | awk '{print $5; exit}') && cat \"/sys/class/net/$interface/speed\""` ), | ||||
|         cpuTemp: exec( `/bin/bash -c "sensors | grep -m1 ${cpuNameInSensors} | awk '{print $2}'"` ), | ||||
|         cpuClk: exec( `awk '/cpu MHz/ {sum+=$4; ++n} END {print sum/n " MHz"}' /proc/cpuinfo` ), | ||||
|         gpuTemp: exec( `/bin/bash -c "sensors | grep -E 'edge' | awk '{print $2}'"` ), | ||||
|         gpuClk: exec( `/bin/bash -c "cat /sys/class/drm/${gpuName}/device/pp_dpm_sclk | grep '\\*' | awk '{print $2 $3}'"` ), | ||||
|         vram: Math.round( parseInt( exec( `cat /sys/class/drm/${gpuName}/device/mem_info_vram_used` ) ) / 1024 / 1024 ) + 'MiB', | ||||
|         availableVRAM: Math.round( parseInt( exec( `cat /sys/class/drm/${gpuName}/device/mem_info_vram_total` ) ) / 1024 / 1024 ) + 'MiB', | ||||
|     } | ||||
|  | ||||
|     return stats; | ||||
| } | ||||
|  | ||||
| const systemStats: Variable<Stats> = Variable( refreshStats() ); | ||||
|  | ||||
|  | ||||
| const availableFeatures = { | ||||
|     cpu: true, | ||||
|     ram: true, | ||||
| } | ||||
|  | ||||
|  | ||||
| const featureTest = () => { | ||||
|     // Check if awk is available | ||||
|     // Check if awk & sed are available | ||||
|     try { | ||||
|         exec( 'awk -V' ); | ||||
|         exec( 'sed --version' ); | ||||
| @@ -11,44 +50,86 @@ const featureTest = () => { | ||||
|         enabled = false; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Check if mpstat is available | ||||
|     try { | ||||
|         exec( 'mpstat' ); | ||||
|         exec( 'mpstat -V' ); | ||||
|     } catch ( e ) { | ||||
|         availableFeatures.cpu = false; | ||||
|         printerr( '[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!' ); | ||||
|     } | ||||
|  | ||||
|     // Battery... acpi might be present, but potentially no bat | ||||
| } | ||||
|  | ||||
| let enabled = false; | ||||
|  | ||||
| const availableFeatures = { | ||||
|     cpu: true, | ||||
|     ram: true, | ||||
|     bat: true, | ||||
| const info = () => { | ||||
|     return <box vertical valign={Gtk.Align.START}> | ||||
|         <label label={ramUsed( used => { | ||||
|             return used + `(${ ramUtil.get() }%)`; | ||||
|         } )}></label> | ||||
|         <label label={systemStats( stats => { | ||||
|             return `CPU: ${stats.cpuTemp}, ${stats.cpuClk} | ||||
| GPU: ${stats.gpuTemp}, ${stats.gpuClk} (${stats.vram} / ${stats.availableVRAM}) | ||||
| Network: ${stats.netSpeed} | ||||
| Kernel: ${stats.kernel}` } ) }></label> | ||||
|     </box>; | ||||
| } | ||||
|  | ||||
|  | ||||
| const SystemInformationPanel = () => { | ||||
|     const popover = new Gtk.Popover(); | ||||
|  | ||||
|     popover.set_child( info() ); | ||||
|  | ||||
|     return popover; | ||||
| } | ||||
|  | ||||
|  | ||||
| const sysInfoFetcher = () => { | ||||
|     if ( enabled ) { | ||||
|         if ( availableFeatures.cpu ) { | ||||
|             const cpuUtil = exec( 'mpstat | awk "/all/ {print(100 - $NF)"}' ); | ||||
|             cpuUtil.set( '' + Math.round( parseFloat( exec( `/bin/fish -c cpu-utilization` ) ) ) ); | ||||
|         } | ||||
|         if ( availableFeatures.ram ) { | ||||
|             const ramUtil = exec( `free | awk '/Mem/ { printf("%.2f\\n", ($3/$2)*100) }'` ); | ||||
|         } | ||||
|         if ( availableFeatures.bat ) { | ||||
|             const acpi = exec( `acpi -i | grep 'Battery'` ); | ||||
|             // TODO: Parse acpi output | ||||
|             ramUtil.set( '' + Math.round( parseFloat( exec( `/bin/bash -c "free | awk '/Mem/ { printf(\\"%.2f\\\\n\\", ($3/$2)*100) }'"` ) ) ) ); | ||||
|             ramUsed.set( exec( `/bin/bash -c \"free -h | awk '/^Mem:/ {print $3 \\" used of \\" $2}'\"` ).replaceAll( 'Gi', 'GiB' ).replaceAll( 'Mi', 'MiB' ) ); | ||||
|         } | ||||
|         gpuUtil.set( exec( 'cat /sys/class/drm/card1/device/gpu_busy_percent' ) ); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| const panel = SystemInformationPanel(); | ||||
|  | ||||
|  | ||||
| const SystemInfo = () => { | ||||
|     return <box></box> | ||||
|     featureTest(); | ||||
|  | ||||
|     const openSysInfo = async () => { | ||||
|         panel.popup(); | ||||
|         systemStats.set( refreshStats() ); | ||||
|     } | ||||
|  | ||||
|     if ( enabled ) { | ||||
|         sysInfoFetcher(); | ||||
|         interval( FETCH_INTERVAL, sysInfoFetcher ); | ||||
|  | ||||
|         return <button onClicked={() => openSysInfo() } child={ | ||||
|             <box tooltipText={ ramUsed( v => v ) }> | ||||
|                 <image iconName={"power-profile-performance-symbolic"} marginEnd={1}></image> | ||||
|                 <label label={ cpuUtil( util => util ) } marginEnd={5}></label> | ||||
|                 <image iconName={"histogram-symbolic"}></image> | ||||
|                 <label label={ ramUtil( util => util ) }></label> | ||||
|                 <image iconName={"show-gpu-effects-symbolic"}></image> | ||||
|                 <label label={ gpuUtil( util => util ) }></label> | ||||
|                 { panel } | ||||
|             </box> | ||||
|         }></button> | ||||
|     } else { | ||||
|         return <image iconName={"action-unavailable-symbolic"}></image> | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| export default { | ||||
|     SystemInfo | ||||
|     SystemInfo, | ||||
|     panel | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								config/astal/components/bar/ui/modules/stats.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								config/astal/components/bar/ui/modules/stats.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| interface Stats { | ||||
|     kernel: string; | ||||
|     netSpeed: string; | ||||
|     cpuTemp: string; | ||||
|     cpuClk: string; | ||||
|     gpuTemp: string; | ||||
|     gpuClk: string; | ||||
|     vram: string; | ||||
|     availableVRAM: string; | ||||
| } | ||||
| @@ -12,7 +12,7 @@ export default class Brightness extends GObject.Object { | ||||
|     static get_default() { | ||||
|         if (!this.instance) | ||||
|             this.instance = new Brightness() | ||||
|  | ||||
|          | ||||
|         return this.instance | ||||
|     } | ||||
|  | ||||
| @@ -20,6 +20,10 @@ export default class Brightness extends GObject.Object { | ||||
|     #kbd = get(`--device ${kbd} get`) | ||||
|     #screenMax = get("max") | ||||
|     #screen = get("get") / (get("max") || 1) | ||||
|     #screenAvailable = false | ||||
|  | ||||
|     @property(Boolean) | ||||
|     get screenAvailable() { return this.#screenAvailable } | ||||
|  | ||||
|     @property(Number) | ||||
|     get kbd() { return this.#kbd } | ||||
| @@ -67,5 +71,12 @@ export default class Brightness extends GObject.Object { | ||||
|             this.#kbd = Number(v) / this.#kbdMax | ||||
|             this.notify("kbd") | ||||
|         }) | ||||
|  | ||||
|         // Check if there is a screen available | ||||
|         try { | ||||
|             get( 'g -c backlight' ); | ||||
|         } catch ( _ ) { | ||||
|             this.#screenAvailable = false; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user