A drawer lowered out of the underside of the bar, painted in the hull colour and joined to it by two concave fillets so it reads as more of the hull rather than as a floating card — caelestia's dashboard idea, in the YoRHa language. Four columns divided by hairlines: operator and clock, the month with today as an inverted square, the system meters, and the active player with a hairline timeline and square transport buttons. Opened by clicking the bar's clock (which used to pin the calendar), by SUPER+I, or by `qs ipc call shell dashboard`. Escape or moving focus away closes it through a HyprlandFocusGrab; Actions.dashboardClosedAt keeps the clock click that cleared the grab from reopening it. The clock's calendar hover is suppressed while the drawer is down, since it shows the month. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
6.5 KiB
QML
168 lines
6.5 KiB
QML
pragma Singleton
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Hyprland
|
|
import "root:/services"
|
|
|
|
// Shell-wide UI state and the handful of external commands the shell fires.
|
|
// Also exposes an IPC surface so Hyprland keybinds can drive the same actions
|
|
// (`qs ipc call shell lock`, `qs ipc call shell power`, ...).
|
|
Singleton {
|
|
id: root
|
|
|
|
// ------------------------------------------------------------- UI state
|
|
property bool powerMenuOpen: false
|
|
property bool lockRequested: false
|
|
property bool sidebarOpen: false
|
|
property bool launcherOpen: false
|
|
property bool askOpen: false
|
|
property bool dashboardOpen: false
|
|
|
|
// Seed text for the next launcher open. Lets a keybind or a script hand the
|
|
// launcher a query it did not have to type — `qs ipc call shell prompt
|
|
// "https://…"` is an open-with dialog for anything that can run a command.
|
|
property string launcherSeed: ""
|
|
|
|
// A launcher window exists on every screen, but only one of them is being
|
|
// typed at. Latch the focused monitor at open time so the window appears
|
|
// there alone — latched rather than live, because the launcher itself takes
|
|
// keyboard focus and because a window should not hop mid-query.
|
|
property string launcherScreen: ""
|
|
|
|
// Same story for the power menu: a window per screen, one of them wanted.
|
|
property string powerMenuScreen: ""
|
|
|
|
// …and for the ask panel, which also takes the keyboard.
|
|
property string askScreen: ""
|
|
|
|
// …and for the dashboard drawer.
|
|
property string dashboardScreen: ""
|
|
|
|
// When the dashboard was last closed from outside. A click on the bar's
|
|
// clock first clears the drawer's focus grab, which closes it, and then
|
|
// lands on the clock, which would toggle it straight back open. A toggle
|
|
// that arrives within a moment of a grab-close is that same click.
|
|
property real dashboardClosedAt: 0
|
|
|
|
// Seed text for the next ask open. A seeded open asks straight away, so
|
|
// `qs ipc call shell askAbout "$(wl-paste)"` is a one-shot question.
|
|
property string askSeed: ""
|
|
|
|
signal osdRequested(string kind)
|
|
|
|
function togglePowerMenu() {
|
|
if (!root.powerMenuOpen)
|
|
root.powerMenuScreen = root.focusedScreenName();
|
|
root.powerMenuOpen = !root.powerMenuOpen;
|
|
}
|
|
function closePowerMenu() { root.powerMenuOpen = false; }
|
|
function lock() { root.powerMenuOpen = false; root.lockRequested = true; }
|
|
|
|
function focusedScreenName(): string {
|
|
return Hyprland.focusedMonitor?.name
|
|
?? Quickshell.screens[0]?.name
|
|
?? "";
|
|
}
|
|
|
|
function toggleLauncher() {
|
|
if (!root.launcherOpen) {
|
|
root.launcherSeed = "";
|
|
root.launcherScreen = root.focusedScreenName();
|
|
}
|
|
root.launcherOpen = !root.launcherOpen;
|
|
}
|
|
function closeLauncher() { root.launcherOpen = false; }
|
|
|
|
function toggleDashboard() {
|
|
if (!root.dashboardOpen && Date.now() - root.dashboardClosedAt < 250)
|
|
return;
|
|
if (!root.dashboardOpen)
|
|
root.dashboardScreen = root.focusedScreenName();
|
|
root.dashboardOpen = !root.dashboardOpen;
|
|
}
|
|
function closeDashboard() {
|
|
if (!root.dashboardOpen) return;
|
|
root.dashboardOpen = false;
|
|
root.dashboardClosedAt = Date.now();
|
|
}
|
|
|
|
function toggleAsk() {
|
|
if (!root.askOpen) {
|
|
root.askSeed = "";
|
|
root.askScreen = root.focusedScreenName();
|
|
}
|
|
root.askOpen = !root.askOpen;
|
|
}
|
|
function closeAsk() { root.askOpen = false; }
|
|
|
|
function askAbout(seed: string) {
|
|
root.askSeed = seed;
|
|
root.askScreen = root.focusedScreenName();
|
|
root.askOpen = false;
|
|
root.askOpen = true;
|
|
}
|
|
|
|
// Stand in for `fuzzel --dmenu`. See Apps.beginDmenu for the transport.
|
|
function dmenuLauncher(inPath: string, outPath: string, prompt: string) {
|
|
Apps.beginDmenu(inPath, outPath, prompt);
|
|
root.launcherSeed = "";
|
|
root.launcherScreen = root.focusedScreenName();
|
|
root.launcherOpen = false;
|
|
root.launcherOpen = true;
|
|
}
|
|
|
|
function promptLauncher(seed: string) {
|
|
root.launcherSeed = seed;
|
|
root.launcherScreen = root.focusedScreenName();
|
|
root.launcherOpen = false;
|
|
root.launcherOpen = true;
|
|
}
|
|
|
|
// ------------------------------------------------------------- commands
|
|
// Detached, because nothing here is ever read back. Rotating a fixed set of
|
|
// `Process` slots only moves the collision: `uwsm app` lives as long as the
|
|
// app it started, so the fourth launch reclaims the first slot and kills the
|
|
// app still holding it — and a shell reload kills every one of them at once.
|
|
function run(argv: var) {
|
|
Quickshell.execDetached(argv);
|
|
}
|
|
|
|
function launch(app: string) {
|
|
root.run(["uwsm", "app", "--", app]);
|
|
}
|
|
|
|
// --------------------------------------------------------------- power
|
|
function logout() { root.run(["uwsm", "stop"]); }
|
|
function reboot() { root.run(["systemctl", "reboot"]); }
|
|
function shutdown() { root.run(["systemctl", "poweroff"]); }
|
|
function suspend() { root.lock(); root.run(["systemctl", "suspend"]); }
|
|
function hibernate() { root.lock(); root.run(["systemctl", "hibernate"]); }
|
|
function reloadShell() { Quickshell.reload(true); }
|
|
|
|
// ------------------------------------------------------------- helpers
|
|
function openAudioSettings() { root.launch("pavucontrol"); }
|
|
function openSystemMonitor() { root.run(["alacritty", "-e", "btop"]); }
|
|
function openNetworkSettings() { root.run(["nm-connection-editor"]); }
|
|
function copyText(s: string) { root.run(["wl-copy", "--", s]); }
|
|
|
|
IpcHandler {
|
|
target: "shell"
|
|
|
|
function lock(): void { root.lock(); }
|
|
function power(): void { root.togglePowerMenu(); }
|
|
function launcher(): void { root.toggleLauncher(); }
|
|
function ask(): void { root.toggleAsk(); }
|
|
function dashboard(): void { root.toggleDashboard(); }
|
|
function askAbout(question: string): void { root.askAbout(question); }
|
|
function prompt(seed: string): void { root.promptLauncher(seed); }
|
|
function dmenu(inPath: string, outPath: string, prompt: string): void {
|
|
root.dmenuLauncher(inPath, outPath, prompt);
|
|
}
|
|
function reloadConfig(): void { root.reloadShell(); }
|
|
function toggleDnd(): void { Notifs.toggleDnd(); }
|
|
function clearNotifications(): void { Notifs.clearAll(); }
|
|
}
|
|
}
|