diff --git a/config/ags/bar/dump b/config/ags/bar/dump new file mode 100644 index 0000000..0d85b51 --- /dev/null +++ b/config/ags/bar/dump @@ -0,0 +1,886 @@ + +import GLib from "gi://GLib"; +import Astal from "gi://Astal"; +import AstalHyprland from "gi://AstalHyprland"; +import Gio from "gi://Gio"; +import Gtk from "gi://Gtk"; +import Gdk from "gi://Gdk"; +import { exec } from "gi://GLib"; + +function getSystemStats() { + const cpu = exec("sh -c 'top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | awk \"{print 100 - $1}\"'"); + const memory = exec("free | grep Mem | awk '{print $3/$2 * 100.0}'"); + const brightness = exec("brightnessctl g"); + const battery = exec("cat /sys/class/power_supply/BAT0/capacity"); + return { cpu, memory, brightness, battery }; +} + +function getWorkspaceNumber() { + return AstalHyprland.get_default().get_active_workspace().get_id(); +} + +function getWindowTitle() { + return exec("xdotool getactivewindow getwindowname"); +} + +function getDateTime() { + return GLib.DateTime.new_now_local().format("%a %b %d, %Y %H:%M:%S"); +} + +function createQuickActionMenu() { + const menu = new Gtk.Menu(); + const volumeItem = new Gtk.MenuItem({ label: "🔊 Volume" }); + const micItem = new Gtk.MenuItem({ label: "🎙️ Microphone" }); + const wifiItem = new Gtk.MenuItem({ label: "📶 WiFi" }); + const powerItem = new Gtk.MenuItem({ label: "🔌 Power" }); + + menu.append(volumeItem); + menu.append(micItem); + menu.append(wifiItem); + menu.append(powerItem); + + volumeItem.show(); + micItem.show(); + wifiItem.show(); + powerItem.show(); + + return menu; +} + +function Bar(gdkmonitor) { + const { cpu, memory, brightness, battery } = getSystemStats(); + const workspaceNumber = getWorkspaceNumber(); + const windowTitle = getWindowTitle(); + const dateTime = getDateTime(); + const quickActionMenu = createQuickActionMenu(); + + return ( + + + + + + + + + + + + + + + + + ); +} + +function createQuickActionsMenu() { + const wifi = AstalNetwork.get_default().get_wifi(); + const bt = AstalBluetooth.get_default().get_adapter(); + + const wifiSub = new Gtk.Menu(); + const btSub = new Gtk.Menu(); + + function refreshWifiMenu() { + wifiSub.foreach(c => wifiSub.remove(c)); + for (const ap of wifi.get_access_points()) { + const item = new Gtk.MenuItem({ label: `${ap.get_ssid()} (${ap.get_strength()}%)` }); + item.connect("activate", () => wifi.connect_access_point(ap)); + item.show(); + wifiSub.append(item); + } + } + + function refreshBtMenu() { + btSub.foreach(c => btSub.remove(c)); + for (const dev of AstalBluetooth.get_default().get_devices()) { + const name = dev.get_name(); + const label = dev.get_connected() ? `✔ ${name}` : name; + const item = new Gtk.MenuItem({ label }); + item.connect("activate", () => { + dev.get_connected() ? dev.disconnect_device() : dev.connect_device(); + }); + item.show(); + btSub.append(item); + } + } + + return ( + + + + {createToggleRow({ + label: "📶 WiFi", + active: wifi.get_state() !== AstalNetwork.WifiState.DISCONNECTED, + onToggle: () => { + wifi.get_state() === AstalNetwork.WifiState.DISCONNECTED ? wifi.connect() : wifi.disconnect(); + }, + onExpand: () => { + refreshWifiMenu(); + wifiSub.popup(); + } + })} + + + + {createToggleRow({ + label: "🔵 Bluetooth", + active: bt.get_powered(), + onToggle: () => bt.set_powered(!bt.get_powered()), + onExpand: () => { + refreshBtMenu(); + btSub.popup(); + } + })} + + + exec("pavucontrol")} /> + exec("pavucontrol")} /> + exec("power-menu")} /> + + ); +} + +export { createQuickActionsMenu }; + + + + + +@import "colors"; + +.toggle-row { + background-color: $bg; + border-radius: 12px; + margin: 6px 0; + overflow: hidden; + border: 1px solid $border; + + button { + padding: 10px 16px; + font-size: 14px; + transition: background 0.2s ease; + border: none; + background: transparent; + + &:hover { + background-color: $hover; + } + } + + .toggle { + flex: 1; + background-color: transparent; + text-align: left; + &.active { + background-color: $accent; + color: white; + } + } + + .arrow { + width: 40px; + background-color: transparent; + text-align: center; + } +} + + +.audio-control { + display: flex; + align-items: center; + padding: 8px 12px; + gap: 8px; + + label { + min-width: 60px; + color: $fg; + } + + scale { + flex: 1; + } + + button { + background-color: transparent; + padding: 6px; + border-radius: 6px; + + &:hover { + background-color: $hover; + } + } +} + +.media-control { + padding: 12px; + text-align: center; + + label { + display: block; + margin-bottom: 6px; + } + + button { + margin: 0 4px; + padding: 6px 10px; + border-radius: 6px; + + &:hover { + background-color: $hover; + } + } +} + + + + + + +import Gtk from "gi://Gtk"; +import AstalNetwork from "gi://AstalNetwork"; +import AstalBluetooth from "gi://AstalBluetooth"; +import AstalAudio from "gi://AstalAudio"; +import AstalMedia from "gi://AstalMedia"; +import { exec } from "gi://GLib"; + +// Helpers +function createToggleRow({ label, active, onToggle, onExpand }) { + return ( + + + + + ); +} + +function createPowerRow({ onExpand }) { + return ( + + + + + ); +} + +function createQuickActionsMenu() { + const wifi = AstalNetwork.get_default().get_wifi(); + const bt = AstalBluetooth.get_default().get_adapter(); + const audio = AstalAudio.get_default(); + const media = AstalMedia.get_default(); + + // Submenus + const wifiSub = new Gtk.Menu(); + const btSub = new Gtk.Menu(); + const powerSub = new Gtk.Menu(); + const outputSub = new Gtk.Menu(); + const inputSub = new Gtk.Menu(); + + function refreshWifiMenu() { + wifiSub.foreach(c => wifiSub.remove(c)); + for (const ap of wifi.get_access_points()) { + const item = new Gtk.MenuItem({ label: `${ap.get_ssid()} (${ap.get_strength()}%)` }); + item.connect("activate", () => wifi.connect_access_point(ap)); + item.show(); + wifiSub.append(item); + } + } + + function refreshBtMenu() { + btSub.foreach(c => btSub.remove(c)); + for (const dev of AstalBluetooth.get_default().get_devices()) { + const label = dev.get_connected() ? `✔ ${dev.get_name()}` : dev.get_name(); + const item = new Gtk.MenuItem({ label }); + item.connect("activate", () => + dev.get_connected() ? dev.disconnect_device() : dev.connect_device() + ); + item.show(); + btSub.append(item); + } + } + + function refreshPowerMenu() { + const entries = [ + { label: "⏻ Shutdown", cmd: "systemctl poweroff" }, + { label: "🔁 Reboot", cmd: "systemctl reboot" }, + { label: "🔒 Lock", cmd: "loginctl lock-session" }, + { label: "🚪 Logout", cmd: "hyprctl dispatch exit" }, + ]; + powerSub.foreach(c => powerSub.remove(c)); + for (const entry of entries) { + const item = new Gtk.MenuItem({ label: entry.label }); + item.connect("activate", () => exec(entry.cmd)); + item.show(); + powerSub.append(item); + } + } + + function refreshOutputDevices() { + outputSub.foreach(c => outputSub.remove(c)); + for (const device of audio.get_outputs()) { + const label = device.is_default() ? `✔ ${device.get_description()}` : device.get_description(); + const item = new Gtk.MenuItem({ label }); + item.connect("activate", () => audio.set_default_output(device)); + item.show(); + outputSub.append(item); + } + } + + function refreshInputDevices() { + inputSub.foreach(c => inputSub.remove(c)); + for (const device of audio.get_inputs()) { + const label = device.is_default() ? `✔ ${device.get_description()}` : device.get_description(); + const item = new Gtk.MenuItem({ label }); + item.connect("activate", () => audio.set_default_input(device)); + item.show(); + inputSub.append(item); + } + } + + return ( + + + {/* WiFi */} + + {createToggleRow({ + label: "📶 WiFi", + active: wifi.get_state() !== AstalNetwork.WifiState.DISCONNECTED, + onToggle: () => + wifi.get_state() === AstalNetwork.WifiState.DISCONNECTED + ? wifi.connect() + : wifi.disconnect(), + onExpand: () => { + refreshWifiMenu(); + wifiSub.popup(); + }, + })} + + + {/* Bluetooth */} + + {createToggleRow({ + label: "🔵 Bluetooth", + active: bt.get_powered(), + onToggle: () => bt.set_powered(!bt.get_powered()), + onExpand: () => { + refreshBtMenu(); + btSub.popup(); + }, + })} + + + {/* Output Volume */} + + + audio.set_output_volume(v)} + drawValue={false} + min={0} + max={100} + /> + + + + {/* Input Volume */} + + + audio.set_input_volume(v)} + drawValue={false} + min={0} + max={100} + /> + + + + {/* Media Controls */} + {media && ( + + + + + + + + + )} + + {/* Power */} + + {createPowerRow({ + onExpand: () => { + refreshPowerMenu(); + powerSub.popup(); + }, + })} + + + ); +} + +export { createQuickActionsMenu }; diff --git a/config/ags/bar/tsconfig.json b/config/ags/bar/tsconfig.json index a92bc43..9471e35 100644 --- a/config/ags/bar/tsconfig.json +++ b/config/ags/bar/tsconfig.json @@ -9,6 +9,6 @@ // "checkJs": true, // "allowJs": true, "jsx": "react-jsx", - "jsxImportSource": "astal/gtk4", + "jsxImportSource": "astal/gtk3", } } diff --git a/config/ags/bar/ui/Bar.tsx b/config/ags/bar/ui/Bar.tsx index c2db8c5..97be98e 100644 --- a/config/ags/bar/ui/Bar.tsx +++ b/config/ags/bar/ui/Bar.tsx @@ -1,36 +1,23 @@ -import { App, Astal, Gtk, Gdk } from "astal/gtk4" -import { Variable } from "astal" +import { createQuickActionsMenu } from "./QuickActions"; +import { GLib } from "astal"; +import { Astal, Gdk, Gtk } from "astal/gtk3"; -const time = Variable("").poll(1000, "date") +const Bar = (gdkmonitor: Gdk.Monitor) => { -export default function Bar(gdkmonitor: Gdk.Monitor) { - const { TOP, LEFT, RIGHT } = Astal.WindowAnchor - - return - - - - - - - + return ( + + + + + + + + + ) ) ) } + +} + + +const HyprlandWorkspace = () => { + const hypr = AstalHyprland.get_default() + + return + {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 => ( + + )) + )} + +} + + +const HyprlandActiveWindow = () => { + const hypr = AstalHyprland.get_default(); + const focused = bind( hypr, "focusedClient" ); + + return + {focused.as( client => ( + client && +} + +export default { + HyprlandWorkspace, + HyprlandActiveWindow, + SysTray +} diff --git a/config/ags/bar/ui/widgets/HyprlandActiveWindow.tsx b/config/ags/bar/ui/modules/QuickView.tsx similarity index 100% rename from config/ags/bar/ui/widgets/HyprlandActiveWindow.tsx rename to config/ags/bar/ui/modules/QuickView.tsx diff --git a/config/ags/bar/ui/widgets/HyprlandWorkspace.tsx b/config/ags/bar/ui/modules/SystemInfo.tsx similarity index 100% rename from config/ags/bar/ui/widgets/HyprlandWorkspace.tsx rename to config/ags/bar/ui/modules/SystemInfo.tsx diff --git a/config/ags/bar/ui/quickactions.scss b/config/ags/bar/ui/quickactions.scss new file mode 100644 index 0000000..dc4a40e --- /dev/null +++ b/config/ags/bar/ui/quickactions.scss @@ -0,0 +1,37 @@ +@import "colors"; + +.toggle-row { + background-color: $bg; + border-radius: 12px; + margin: 6px 0; + overflow: hidden; + border: 1px solid $border; + + button { + padding: 10px 16px; + font-size: 14px; + transition: background 0.2s ease; + border: none; + background: transparent; + + &:hover { + background-color: $hover; + } + } + + .toggle { + flex: 1; + background-color: transparent; + text-align: left; + &.active { + background-color: $accent; + color: white; + } + } + + .arrow { + width: 40px; + background-color: transparent; + text-align: center; + } +} diff --git a/config/ags/bar/ui/widgets/SystemResources.tsx b/config/ags/bar/ui/widgets/SystemResources.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/config/ags/bar/ui/widgets/SystemTray.tsx b/config/ags/bar/ui/widgets/SystemTray.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/config/ags/bar/ui/widgets/resources.tsx b/config/ags/bar/ui/widgets/resources.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/config/ags/bar/util/hyprland.ts b/config/ags/bar/util/hyprland.ts new file mode 100644 index 0000000..47a1481 --- /dev/null +++ b/config/ags/bar/util/hyprland.ts @@ -0,0 +1,68 @@ +import AstalHyprland from "gi://AstalHyprland"; + +const getAvailableWorkspaces = (): number[] => { + const workspaces: number[] = []; + AstalHyprland.get_default().get_workspaces().forEach( val => { + workspaces.push( val.get_id() ); + } ) + return workspaces; +} + +const getCurrentWorkspaceID = (): number => { + return AstalHyprland.get_default().get_focused_workspace().get_id(); +} + +const getCurrentWindowTitle = (): string => { + return AstalHyprland.get_default().get_focused_client().get_title(); +} + +/** + * Get the workspace ID of a window by its address + * @param address - The address of the window + * @returns The workspace ID + */ +const getWorkspaceIDOfWindowByAddress = ( address: string ): number => { + AstalHyprland.get_default().get_clients().forEach( client => { + if ( client.get_address() === address ) { + return client.get_workspace().get_id(); + } + } ); + + return -1; +} + + +interface HyprlandSubscriptions { + [key: string]: ( data: string ) => void; +} +const hooks: HyprlandSubscriptions = {}; + +/** + * Add an event listener for Hyprland events. + * @param event - A hyprland IPC event. See https://wiki.hyprland.org/IPC/. Useful events include: urgent, windowtitlev2, workspace, createworkspacev2, destroyworkspacev2, activewindowv2 + * @param cb - The callback function + */ +const subscribeToUpdates = ( event: string, cb: ( data: string ) => void ): void => { + hooks[ event ] = cb; +} + +/** + * Listen to events. Must be called at some point if events are to be listened for + */ +const listen = () => { + AstalHyprland.get_default().connect( "event", ( name: string, data: string ) => { + if ( hooks[ name ] ) { + hooks[ name ]( data ); + } + } ); +} + + +export default { + getAvailableWorkspaces, + getCurrentWorkspaceID, + getCurrentWindowTitle, + getWorkspaceIDOfWindowByAddress, + subscribeToUpdates, + listen +} diff --git a/config/ags/bar/ui/widgets/QuickActions.tsx b/config/ags/bar/util/sysinfo.ts similarity index 100% rename from config/ags/bar/ui/widgets/QuickActions.tsx rename to config/ags/bar/util/sysinfo.ts diff --git a/config/ags/quickactions/.gitignore b/config/ags/quickactions/.gitignore deleted file mode 100644 index 298eb4d..0000000 --- a/config/ags/quickactions/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -@girs/ diff --git a/config/ags/quickactions/app.ts b/config/ags/quickactions/app.ts deleted file mode 100644 index 7e8cc7c..0000000 --- a/config/ags/quickactions/app.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { App } from "astal/gtk4" -import style from "./style.scss" -import Bar from "./widget/Bar" - -App.start({ - css: style, - main() { - App.get_monitors().map(Bar) - }, -}) diff --git a/config/ags/quickactions/env.d.ts b/config/ags/quickactions/env.d.ts deleted file mode 100644 index 467c0a4..0000000 --- a/config/ags/quickactions/env.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -declare const SRC: string - -declare module "inline:*" { - const content: string - export default content -} - -declare module "*.scss" { - const content: string - export default content -} - -declare module "*.blp" { - const content: string - export default content -} - -declare module "*.css" { - const content: string - export default content -} diff --git a/config/ags/quickactions/package.json b/config/ags/quickactions/package.json deleted file mode 100644 index 44226f2..0000000 --- a/config/ags/quickactions/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "astal-shell", - "dependencies": { - "astal": "/usr/share/astal/gjs" - } -} diff --git a/config/ags/quickactions/style.scss b/config/ags/quickactions/style.scss deleted file mode 100644 index 1d0d3a9..0000000 --- a/config/ags/quickactions/style.scss +++ /dev/null @@ -1,20 +0,0 @@ -// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss -$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; - } -} diff --git a/config/ags/quickactions/tsconfig.json b/config/ags/quickactions/tsconfig.json deleted file mode 100644 index a92bc43..0000000 --- a/config/ags/quickactions/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "experimentalDecorators": true, - "strict": true, - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Bundler", - // "checkJs": true, - // "allowJs": true, - "jsx": "react-jsx", - "jsxImportSource": "astal/gtk4", - } -} diff --git a/config/ags/quickactions/ui/QuickActions.tsx b/config/ags/quickactions/ui/QuickActions.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/config/ags/quickactions/ui/widgets/ContextMenu.tsx b/config/ags/quickactions/ui/widgets/ContextMenu.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/config/ags/quickactions/ui/widgets/MediaControls.tsx b/config/ags/quickactions/ui/widgets/MediaControls.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/config/ags/quickactions/ui/widgets/toggle.tsx b/config/ags/quickactions/ui/widgets/toggle.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/prompts.md b/prompts.md new file mode 100644 index 0000000..5fb8586 --- /dev/null +++ b/prompts.md @@ -0,0 +1,27 @@ +# Bar +## Attempt 1 +Use Astal (https://aylur.github.io/astal) to write me a new status bar for hyprland. You may use JSX (be aware, it's not react, but gjs based and has therefore some limitations), but I prefer using the other syntax that is similar to what it was for AGS (the old version of astal), which more closely resembles GTK syntax. The bar should include the following, from left to right: +Left aligned +- Date & Time (with seconds, preferably) +- System stats (i.e. CPU, Memory util, Screen Brightness & Battery status, if available) +- Workspace number +Centered +- The window name +Right aligned (still left to right) +- System tray +- QuickAction menu in GNOME QuickAction menu style, but only the closed version showing icons for volume, mic, WiFi / Ethernet and a Power icon. For the icons (all throughout the bar) use the fluentui-icons (so the Windows 11 icons). If you can't provide them here, tell me what icon should go there and I will put it there manually + +The actual QuickAction Menu (which you could also write in a separate file) will provide options where I can pick WiFi, Bluetooth (also turn it on and off), change volume of mic and output, pick the mic and output, have media controls and have a power menu). + +For the QuickAction menu, provide a function that is exposed from the file to open and close it, as for all the features (like enabling BT, WiFi, etc). + + +### Followups +To the WiFi and Bluetooth menus, add the option to pick a WiFi Network / Bluetooth device. Please also fully extract the QuickActions menu to a separate file. + + +Now, can you also provide scss for the bar, such that: +- I have an easy way to customize colours (which I will be doing using one of my scripts, so having a separate colour config file will be a great option) +- it is very rounded (the corners) +- it has a very sleek, but modern design, with only a single accent colour +- Hovering over something clickable uses a hover colour