[AGS] Bar: Done (WiFi still missing, will be added at some later point)
This commit is contained in:
		| @@ -1,21 +1,23 @@ | ||||
| import { App } from "astal/gtk4" | ||||
| import style from "./style.scss" | ||||
|  | ||||
| // import notifications from "./components/notifications/handler"; | ||||
| import Bar from "./components/bar/ui/Bar"; | ||||
| import Bar from "./components/bar/Bar"; | ||||
| import AstalHyprland from "gi://AstalHyprland?version=0.1"; | ||||
|  | ||||
| App.start({ | ||||
|     instanceName: "runner", | ||||
|     css: style, | ||||
|     main() { | ||||
|         // notifications.startNotificationHandler( App.get_monitors()[0] ); | ||||
|         // TODO: Monitor handling | ||||
|         Bar.Bar( App.get_monitors()[0] ); | ||||
|         const hypr = AstalHyprland.get_default(); | ||||
|         const monitors = App.get_monitors(); | ||||
|         for (let index = 0; index < monitors.length; index++) { | ||||
|             Bar.Bar( monitors[ index ] ); | ||||
|         } | ||||
|  | ||||
|         // TODO: Handle monitor add | ||||
|     }, | ||||
|     requestHandler(request, res) { | ||||
|         const args = request.trimStart().split( ' ' ); | ||||
|  | ||||
|         // Notifications (TODO: Handle the arguments in the components themselves) | ||||
|         if ( args[ 0 ] === 'notifier' ) { | ||||
|             res( 'Not available here yet, run astal -i notifier ' + args[ 1 ] ); | ||||
|             // 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 ( | ||||
|         <box cssClasses={["audio-box"]} vertical> | ||||
|             <box> | ||||
|             <box hexpand vexpand> | ||||
|                 <button | ||||
|                     onClicked={() => | ||||
|                         wp.defaultSpeaker.set_mute( | ||||
|                             !wp.defaultSpeaker.get_mute(), | ||||
|                         ) | ||||
|                     } | ||||
|                     tooltipText={"Mute audio output"} | ||||
|                     child={ | ||||
|                         <image | ||||
|                             iconName={bind(wp.defaultSpeaker, "volumeIcon")} | ||||
| @@ -33,7 +34,7 @@ const AudioModule = () => { | ||||
|                     } | ||||
|                 ></button> | ||||
|                 <label | ||||
|                     label={bind(wp.defaultMicrophone, "volume").as( | ||||
|                     label={bind(wp.defaultSpeaker, "volume").as( | ||||
|                         v => Math.round(100 * v) + "%", | ||||
|                     )} | ||||
|                 ></label> | ||||
| @@ -42,11 +43,13 @@ const AudioModule = () => { | ||||
|                     max={100} | ||||
|                     min={0} | ||||
|                     step={1} | ||||
|                     widthRequest={100} | ||||
|                     hexpand | ||||
|                     vexpand | ||||
|                     onChangeValue={self => setVolumeSpeaker(self.value)} | ||||
|                 ></slider> | ||||
|                 <button | ||||
|                     cssClasses={["sink-select-button"]} | ||||
|                     tooltipText={"Pick audio output"} | ||||
|                     child={ | ||||
|                         <box> | ||||
|                             <image iconName={"speaker-symbolic"}></image> | ||||
| @@ -56,13 +59,14 @@ const AudioModule = () => { | ||||
|                     onClicked={() => speakerSelector.popup()} | ||||
|                 ></button> | ||||
|             </box> | ||||
|             <box> | ||||
|             <box hexpand vexpand> | ||||
|                 <button | ||||
|                     onClicked={() => | ||||
|                         wp.defaultMicrophone.set_mute( | ||||
|                             !wp.defaultMicrophone.get_mute(), | ||||
|                         ) | ||||
|                     } | ||||
|                     tooltipText={"Mute audio input"} | ||||
|                     child={ | ||||
|                         <image | ||||
|                             iconName={bind(wp.defaultMicrophone, "volumeIcon")} | ||||
| @@ -82,11 +86,13 @@ const AudioModule = () => { | ||||
|                     max={100} | ||||
|                     min={0} | ||||
|                     step={1} | ||||
|                     widthRequest={100} | ||||
|                     hexpand | ||||
|                     vexpand | ||||
|                     onChangeValue={self => setVolumeMicrophone(self.value)} | ||||
|                 ></slider> | ||||
|                 <button | ||||
|                     cssClasses={["sink-select-button"]} | ||||
|                     tooltipText={"Select audio input"} | ||||
|                     child={ | ||||
|                         <box> | ||||
|                             <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 AstalBluetooth from "gi://AstalBluetooth"; | ||||
| import BTDevice from "./Device"; | ||||
| const ALIGN = Gtk.Align; | ||||
| 
 | ||||
| const bt = AstalBluetooth.get_default(); | ||||
| 
 | ||||
| @@ -11,8 +12,8 @@ const BluetoothModule = () => { | ||||
|             <button | ||||
|                 cssClasses={bind(bt.adapter, "powered").as(powered => | ||||
|                     powered | ||||
|                         ? ["bt-toggle-button", "bt-on"] | ||||
|                         : ["bt-toggle-button"], | ||||
|                         ? ["toggle-button", "toggle-on"] | ||||
|                         : ["toggle-button"], | ||||
|                 )} | ||||
|                 onClicked={() => | ||||
|                     bt.adapter.set_powered(!bt.adapter.get_powered()) | ||||
| @@ -20,10 +21,12 @@ const BluetoothModule = () => { | ||||
|                 child={ | ||||
|                     <box vertical> | ||||
|                         <label | ||||
|                             cssClasses={["button-title"]} | ||||
|                             cssClasses={["title-2"]} | ||||
|                             label={"Bluetooth"} | ||||
|                             halign={ALIGN.CENTER} | ||||
|                             valign={ALIGN.CENTER} | ||||
|                         ></label> | ||||
|                         <box> | ||||
|                         <box halign={ALIGN.CENTER} valign={ALIGN.CENTER}> | ||||
|                             <label | ||||
|                                 visible={bind(bt.adapter, "powered").as( | ||||
|                                     p => !p, | ||||
| @@ -48,7 +51,7 @@ const BluetoothModule = () => { | ||||
|                 } | ||||
|             ></button> | ||||
|             <button | ||||
|                 cssClasses={["bt-devices-button"]} | ||||
|                 cssClasses={["actions-button"]} | ||||
|                 visible={bind(bt.adapter, "powered")} | ||||
|                 child={ | ||||
|                     <box> | ||||
| @@ -56,6 +59,7 @@ const BluetoothModule = () => { | ||||
|                         {picker} | ||||
|                     </box> | ||||
|                 } | ||||
|                 tooltipText={"View available devices"} | ||||
|                 onClicked={() => openBTPicker()} | ||||
|             ></button> | ||||
|         </box> | ||||
| @@ -70,15 +74,44 @@ const openBTPicker = () => { | ||||
| }; | ||||
| 
 | ||||
| const BluetoothPickerList = () => { | ||||
|     let btEnableState = readFile("./btconf") === "true" ? true : false; | ||||
|     bt.adapter.set_powered(btEnableState); | ||||
| 
 | ||||
|     const updateState = () => { | ||||
|         btEnableState = !btEnableState; | ||||
|         writeFile("./btconf", "" + btEnableState); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <box vertical onDestroy={() => bt.adapter.stop_discovery()}> | ||||
|             <label label={"Connected devices"} cssClasses={["title-2"]}></label> | ||||
|         <box | ||||
|             vertical | ||||
|             onDestroy={() => bt.adapter.stop_discovery()} | ||||
|             cssClasses={["popover-box"]} | ||||
|         > | ||||
|             <label cssClasses={["title"]} label={"Bluetooth"}></label> | ||||
|             <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 => { | ||||
|                     return devices | ||||
|                         .filter(device => { | ||||
|                             if (device.get_connected()) { | ||||
|                             if (device.get_connected() || device.get_paired()) { | ||||
|                                 return device; | ||||
|                             } | ||||
|                         }) | ||||
| @@ -91,13 +124,13 @@ const BluetoothPickerList = () => { | ||||
|                 visible={bind(bt, "devices").as(devices => { | ||||
|                     return ( | ||||
|                         devices.filter(device => { | ||||
|                             if (device.get_connected()) { | ||||
|                             if (device.get_connected() || device.get_paired()) { | ||||
|                                 return device; | ||||
|                             } | ||||
|                         }).length === 0 | ||||
|                     ); | ||||
|                 })} | ||||
|                 label={"No connected devices"} | ||||
|                 label={"No connected / trusted devices"} | ||||
|                 cssClasses={["bt-no-found", "bt-conn-list"]} | ||||
|             ></label> | ||||
|             <label | ||||
| @@ -109,7 +142,7 @@ const BluetoothPickerList = () => { | ||||
|                 {bind(bt, "devices").as(devices => { | ||||
|                     return devices | ||||
|                         .filter(data => { | ||||
|                             if (!data.get_connected()) { | ||||
|                             if (!data.get_connected() && !data.get_paired()) { | ||||
|                                 return data; | ||||
|                             } | ||||
|                         }) | ||||
| @@ -122,7 +155,10 @@ const BluetoothPickerList = () => { | ||||
|                 visible={bind(bt, "devices").as(devices => { | ||||
|                     return ( | ||||
|                         devices.filter(device => { | ||||
|                             if (!device.get_connected()) { | ||||
|                             if ( | ||||
|                                 !device.get_connected() && | ||||
|                                 !device.get_paired() | ||||
|                             ) { | ||||
|                                 return device; | ||||
|                             } | ||||
|                         }).length === 0 | ||||
| @@ -135,13 +171,11 @@ const BluetoothPickerList = () => { | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| const BluetoothPicker = () => { | ||||
|     const popover = new Gtk.Popover(); | ||||
| 
 | ||||
|     popover.set_child(BluetoothPickerList()); | ||||
|     popover.connect( 'closed', () => bt.adapter.stop_discovery() ); | ||||
|     popover.connect("closed", () => bt.adapter.stop_discovery()); | ||||
| 
 | ||||
|     return popover; | ||||
| }; | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { bind } from "astal"; | ||||
| import AstalBluetooth from "gi://AstalBluetooth"; | ||||
| 
 | ||||
| 
 | ||||
| const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => { | ||||
|     return ( | ||||
|         <button | ||||
| @@ -10,7 +9,11 @@ const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => { | ||||
|                 <centerbox | ||||
|                     startWidget={ | ||||
|                         <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 | ||||
|                                 iconName={bind(device, "icon")} | ||||
|                                 marginEnd={3} | ||||
| @@ -29,24 +32,41 @@ const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => { | ||||
|                                 label={bind(device, "batteryPercentage").as( | ||||
|                                     bat => (bat >= 0 ? bat + "%" : "?%"), | ||||
|                                 )} | ||||
|                                 tooltipText={"Device's battery percentage"} | ||||
|                                 marginEnd={3} | ||||
|                             ></label> | ||||
|                             <image | ||||
|                                 iconName={bind(device, "trusted").as(v => | ||||
|                                     v ? "checkbox" : "paint-unknown-symbolic", | ||||
|                                 iconName={bind(device, "paired").as(v => | ||||
|                                     v ? "network-bluetooth-activated-symbolic" : "bluetooth-disconnected-symbolic", | ||||
|                                 )} | ||||
|                             ></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> | ||||
|                     } | ||||
|                 ></centerbox> | ||||
|             } | ||||
|             onClicked={() => { | ||||
|                 // TODO: Make sure to check if device was previously paired and otherwise do some pairing shenanigans
 | ||||
|                 device.connect_device( () => {} ); | ||||
|                 connectOrPair( device ); | ||||
|             }} | ||||
|         ></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; | ||||
| @@ -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={ | ||||
|                         <image iconName={"system-shutdown-symbolic"}></image> | ||||
|                     } | ||||
|                     onClicked={() => exec("shutdown now")} | ||||
|                     onClicked={() => exec("/bin/sh -c 'shutdown now'")} | ||||
|                 ></button> | ||||
|                 <button | ||||
|                     cssClasses={["power-button"]} | ||||
|                     child={<image iconName={"system-reboot-symbolic"}></image>} | ||||
|                     onClicked={() => exec("reboot")} | ||||
|                     onClicked={() => exec("/bin/sh -c '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")} | ||||
|                     onClicked={() => exec("/bin/sh -c 'systemctl suspend'")} | ||||
|                 ></button> | ||||
|             </box> | ||||
|         ); | ||||
| @@ -48,7 +36,10 @@ const Power = () => { | ||||
|     const pm = PowerMenu(); | ||||
|     return ( | ||||
|         <button | ||||
|             cssClasses={["PowerMenuButton"]} | ||||
|             widthRequest={0} | ||||
|             hexpand={false} | ||||
|             vexpand={false} | ||||
|             cssClasses={["power-menu-button"]} | ||||
|             child={ | ||||
|                 <box> | ||||
|                     <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 | ||||
|                             hexpand | ||||
|                             halign={Gtk.Align.START} | ||||
|                             cssClasses={["BarLeft"]} | ||||
|                         > | ||||
|                             <Calendar.Time /> | ||||
|                             <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/bar/ui/bar.scss'; | ||||
| @use './components/bar/ui/QuickActions/quickactions.scss'; | ||||
| /* @use './components/notifications/notifications.scss'; */ | ||||
| @use "./components/bar/bar.scss"; | ||||
| @use "./components/QuickActions/quickactions.scss"; | ||||
| @use "./util/colours.scss" as *; | ||||
|  | ||||
| * { | ||||
|   font-size: 1rem; | ||||
| @@ -10,3 +11,13 @@ empty { | ||||
|   min-width: 0; | ||||
|   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 ); | ||||
		Reference in New Issue
	
	Block a user