| 1 | //@ pragma IconTheme Yaru |
| 2 | // ^ same as gtk-3.0: notify-send -i names (drive-removable-media, network-wireless, ...) resolve through it |
| 3 | import QtQuick |
| 4 | import Quickshell |
| 5 | import Quickshell.Io |
| 6 | import Quickshell.Hyprland |
| 7 | import Quickshell.Services.Pipewire |
| 8 | |
| 9 | // aurora: bar + control panel + notifications, themed from matugen. |
| 10 | ShellRoot { |
| 11 | id: root |
| 12 | |
| 13 | property bool opened: false |
| 14 | property int tab: 0 |
| 15 | property double closedAt: 0 |
| 16 | property string lastTab: "" |
| 17 | property string lastScreen: "" |
| 18 | property var panelScreen: focusedScreen() |
| 19 | readonly property var focusedScr: focusedScreen() |
| 20 | // centered overlay windows (wallpaper picker, clipboard, ...): one at a time, on the focused monitor |
| 21 | property string overlay: "" |
| 22 | property bool barShown: true // `bar` IPC (SUPER+B); not persisted, a restart shows it again |
| 23 | property var overlayScreen: focusedScreen() |
| 24 | // touch singletons that must start with the shell (Idle creates ~/.config/aurora/hypridle.conf) |
| 25 | readonly property string idleFile: Idle.file |
| 26 | readonly property bool batCare: Battery.ready // charge limit + low battery warnings |
| 27 | readonly property bool devWatch: Devices.scanned // removable drive notifications |
| 28 | readonly property var tabNames: ["home", "audio", "net", "bt", "sys", "notif"] |
| 29 | // settings app (a normal window): open state + shown section |
| 30 | property bool settingsOpen: false |
| 31 | property string settingsSection: "network" |
| 32 | |
| 33 | // screen of the monitor that currently has focus (falls back to the first screen) |
| 34 | function focusedScreen() { |
| 35 | var m = Hyprland.focusedMonitor |
| 36 | var l = Quickshell.screens |
| 37 | if (m) for (var i = 0; i < l.length; i++) if (l[i].name === m.name) return l[i] |
| 38 | return l.length ? l[0] : null |
| 39 | } |
| 40 | function close() { |
| 41 | if (!opened) return |
| 42 | opened = false |
| 43 | closedAt = Date.now() |
| 44 | lastTab = tabNames[tab] |
| 45 | lastScreen = panelScreen ? panelScreen.name : "" |
| 46 | } |
| 47 | property double overlayClosedAt: 0 |
| 48 | property string lastOverlay: "" |
| 49 | function closeOverlay() { |
| 50 | if (overlay === "") return |
| 51 | lastOverlay = overlay |
| 52 | overlayClosedAt = Date.now() |
| 53 | overlay = "" |
| 54 | } |
| 55 | // scr: where to open (defaults to the focused monitor). Same instant re-open guard as togglePanel. |
| 56 | function toggleOverlay(name, scr) { |
| 57 | if (overlay === name) { closeOverlay(); return } |
| 58 | if (lastOverlay === name && Date.now() - overlayClosedAt < 250) return |
| 59 | close() |
| 60 | overlayScreen = scr ? scr : focusedScreen() |
| 61 | overlay = name |
| 62 | } |
| 63 | // section "" = toggle the window on its last section; a name opens (or switches) to it |
| 64 | function settings(section) { |
| 65 | if (section === "" || section === undefined) { settingsOpen = !settingsOpen; return } |
| 66 | settingsSection = section |
| 67 | settingsOpen = true |
| 68 | } |
| 69 | function openTab(name) { |
| 70 | if (name === "settings") { settings("appearance"); return } // old panel tab lives in the app now |
| 71 | var i = tabNames.indexOf(name) |
| 72 | if (i >= 0) tab = i |
| 73 | panelScreen = focusedScreen() |
| 74 | opened = true |
| 75 | } |
| 76 | // bar buttons: click once to open that tab, again to close. |
| 77 | // (the focus grab already closed the panel on the press, so guard against instant re-open) |
| 78 | // scr = the screen whose bar was clicked, so the panel opens on that monitor |
| 79 | function togglePanel(name, scr) { |
| 80 | var i = tabNames.indexOf(name) |
| 81 | var sn = scr ? scr.name : "" |
| 82 | if (opened && tab === i && panelScreen && panelScreen.name === sn) { close(); return } |
| 83 | if (!opened && lastTab === name && lastScreen === sn && Date.now() - closedAt < 250) return |
| 84 | tab = i |
| 85 | panelScreen = scr |
| 86 | opened = true |
| 87 | } |
| 88 | |
| 89 | // keep every pipewire node bound so volume / mute / properties are readable |
| 90 | PwObjectTracker { objects: Pipewire.nodes.values } |
| 91 | |
| 92 | Variants { |
| 93 | model: Quickshell.screens |
| 94 | Desktop {} |
| 95 | } |
| 96 | |
| 97 | Variants { |
| 98 | model: Quickshell.screens |
| 99 | Dock {} |
| 100 | } |
| 101 | |
| 102 | Variants { |
| 103 | model: Quickshell.screens |
| 104 | Bar { |
| 105 | visible: root.barShown |
| 106 | onTogglePanel: (name, scr) => root.togglePanel(name, scr) |
| 107 | onToggleOverlay: (name, scr) => root.toggleOverlay(name, scr) |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | ControlPanel { |
| 112 | screen: root.panelScreen |
| 113 | opened: root.opened |
| 114 | tab: root.tab |
| 115 | onTabRequested: i => root.tab = i |
| 116 | onCloseRequested: root.close() |
| 117 | onOverlayRequested: name => { root.close(); root.toggleOverlay(name) } |
| 118 | onSettingsRequested: { root.close(); root.settingsOpen = true } |
| 119 | } |
| 120 | |
| 121 | Overview { |
| 122 | screen: root.overlayScreen |
| 123 | opened: root.overlay === "overview" |
| 124 | onCloseRequested: root.closeOverlay() |
| 125 | } |
| 126 | |
| 127 | CalPopup { |
| 128 | screen: root.overlayScreen |
| 129 | opened: root.overlay === "cal" |
| 130 | onCloseRequested: root.closeOverlay() |
| 131 | } |
| 132 | |
| 133 | ClipWindow { |
| 134 | screen: root.overlayScreen |
| 135 | opened: root.overlay === "clip" |
| 136 | onCloseRequested: root.closeOverlay() |
| 137 | } |
| 138 | |
| 139 | CharPicker { |
| 140 | screen: root.overlayScreen |
| 141 | opened: root.overlay === "chars" |
| 142 | onCloseRequested: root.closeOverlay() |
| 143 | } |
| 144 | |
| 145 | WallPicker { |
| 146 | screen: root.overlayScreen |
| 147 | opened: root.overlay === "wall" |
| 148 | onCloseRequested: root.closeOverlay() |
| 149 | } |
| 150 | |
| 151 | // polkit password prompt (Agents), on the focused monitor |
| 152 | AuthPrompt { screen: root.focusedScr } |
| 153 | |
| 154 | PowerMenu { |
| 155 | screen: root.overlayScreen |
| 156 | opened: root.overlay === "power" |
| 157 | onCloseRequested: root.closeOverlay() |
| 158 | } |
| 159 | |
| 160 | Runner { |
| 161 | id: runner |
| 162 | screen: root.overlayScreen |
| 163 | opened: root.overlay === "runner" |
| 164 | shell: root |
| 165 | onCloseRequested: root.closeOverlay() |
| 166 | onOpenRequested: { |
| 167 | root.close() |
| 168 | if (root.overlay === "runner") return // already open (run mode): it just switched to the list |
| 169 | root.overlayScreen = root.focusedScreen() |
| 170 | root.overlay = "runner" |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | SettingsApp { |
| 175 | opened: root.settingsOpen |
| 176 | section: root.settingsSection |
| 177 | onCloseRequested: root.settingsOpen = false |
| 178 | onSectionRequested: name => root.settingsSection = name |
| 179 | onOverlayRequested: name => root.toggleOverlay(name) |
| 180 | } |
| 181 | |
| 182 | NotifPopups { screen: root.focusedScr; suppressed: root.opened } |
| 183 | |
| 184 | Osd { id: osd; screen: root.focusedScr; suppressed: root.opened } |
| 185 | Connections { |
| 186 | target: Bright |
| 187 | // brightnessctl only knows the laptop panel; external monitors go through ddcutil (no readback) |
| 188 | function onStepped() { |
| 189 | var m = Hyprland.focusedMonitor |
| 190 | if (Bright.available && (!m || m.name.indexOf("eDP") === 0)) |
| 191 | osd.show(Theme.iBright, Bright.percent / 100, false) |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | IpcHandler { |
| 196 | target: "shell" |
| 197 | function toggle(): void { if (root.opened) root.close(); else root.openTab(root.tabNames[root.tab]) } |
| 198 | function open(): void { root.openTab(root.tabNames[root.tab]) } |
| 199 | function close(): void { root.close() } |
| 200 | function show(name: string): void { root.openTab(name) } |
| 201 | // `qs ipc call shell show x` is swallowed by the qs CLI (it has its own `show`), so use this |
| 202 | function tab(name: string): void { root.openTab(name) } |
| 203 | function dnd(): void { Notifs.dnd = !Notifs.dnd } |
| 204 | function shot(mode: string): void { root.close(); Shot.take(mode) } // region | window | output | ocr |
| 205 | function shotsave(): void { Shot.save = !Shot.save } |
| 206 | function overview(): void { root.toggleOverlay("overview") } |
| 207 | function cal(): void { root.toggleOverlay("cal") } |
| 208 | function clip(): void { root.toggleOverlay("clip") } |
| 209 | function chars(): void { root.toggleOverlay("chars") } // glyph picker (SUPER+grave) |
| 210 | function wall(): void { root.toggleOverlay("wall") } |
| 211 | function power(): void { root.toggleOverlay("power") } |
| 212 | function run(): void { root.toggleOverlay("runner") } |
| 213 | function dmenu(dir: string): void { runner.startDmenu(dir) } // used by ~/.local/bin/aurora-run -d |
| 214 | function bar(): void { root.barShown = !root.barShown } |
| 215 | function wallrandom(): void { Wall.random() } |
| 216 | function wallset(path: string): void { Wall.apply(path) } // absolute path (setwall resolves it) |
| 217 | function rec(mode: string): void { root.close(); Rec.toggle(mode === "region" ? "region" : "screen") } // screen (default) | region |
| 218 | function settings(section: string): void { root.settings(section) } // "" = toggle |
| 219 | function caffeine(): void { Toggles.caffeine = !Toggles.caffeine } |
| 220 | function portal(): void { Net.signIn() } // open the captive portal login page (or say we're online) |
| 221 | function bright(dir: string): void { Bright.step(dir === "down" ? -1 : 1) } // up | down |
| 222 | function updates(): void { Updates.finished() } // re-check pending updates (the upgrade terminal calls it) |
| 223 | } |
| 224 | } |