Each monitor is a row of workspace cards at its own aspect ratio, with live captures of the windows where they sit, including windows on hidden workspaces. Alt+Tab opens it with the previous window picked, further Tabs walk the focus history, and releasing Alt switches. Super+W opens it without the hold. Hover, click, Enter and the workspace digits work too. Group cycling moves from Alt+Tab to Super+Tab and Super+Shift+Tab.
205 lines
8.0 KiB
QML
205 lines
8.0 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
|
|
property bool overviewOpen: false
|
|
// Opened by Alt+Tab, so letting go of Alt picks the window.
|
|
property bool overviewHold: 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: ""
|
|
|
|
// …and for the overview.
|
|
property string overviewScreen: ""
|
|
|
|
// 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)
|
|
signal overviewStepped(int delta)
|
|
signal overviewCommitted()
|
|
|
|
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 toggleOverview() {
|
|
if (!root.overviewOpen) {
|
|
root.overviewHold = false;
|
|
root.overviewScreen = root.focusedScreenName();
|
|
}
|
|
root.overviewOpen = !root.overviewOpen;
|
|
}
|
|
function closeOverview() { root.overviewOpen = false; }
|
|
|
|
// Alt+Tab and Alt+Shift+Tab. The first press opens the overview with the
|
|
// previous window picked; Hyprland keeps the bind while it is open, so
|
|
// later presses arrive here too and walk the focus history.
|
|
function overviewStep(delta: int) {
|
|
if (!root.overviewOpen) {
|
|
root.overviewHold = true;
|
|
root.overviewScreen = root.focusedScreenName();
|
|
root.overviewOpen = true;
|
|
if (delta < 0) root.overviewStepped(-2);
|
|
return;
|
|
}
|
|
root.overviewStepped(delta);
|
|
}
|
|
|
|
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(); }
|
|
function toggleStayAwake(): void { Idle.toggleStayAwake(); }
|
|
function overview(): void { root.toggleOverview(); }
|
|
function overviewNext(): void { root.overviewStep(1); }
|
|
function overviewPrev(): void { root.overviewStep(-1); }
|
|
function overviewCommit(): void { if (root.overviewHold) root.overviewCommitted(); }
|
|
function wallpaper(path: string): void { Wallpaper.set(path); }
|
|
}
|
|
}
|