feat(quickshell): idle chain, drifting wallpaper, and a few fixes
- Idle is handled in the shell instead of hypridle: the screen dims slowly at 4 minutes, locks at 5, powers down at 6, and suspends at 15 on battery. Idle inhibitors hold every stage off; Super+Shift+I does the same by hand and puts a cup on the rail. - The shell draws the wallpaper (backdrop/), so hyprpaper is gone. The picture is drawn a little larger than the screen and drifts with the workspace a beat after the windows, and dims a shade while windows are open. The path lives in quickshell/wallpaper. - The lock screen and power menu say "Take your time." instead of the NieR slogan. - hyprpaper is no longer launched twice, the media module keeps the player that played last instead of an empty browser tab, and grouped windows show thin untitled tabs with their place in the group (2/3) in the bar title. - qt6ct uses Darkly; the package list gains adw-gtk-theme and darkly-bin.
This commit is contained in:
1 parent
864420281b
commit
c36a74e861
20 files changed
+396
-128
No files matched your search
@@ -163,5 +163,7 @@ Singleton {
|
||||
function reloadConfig(): void { root.reloadShell(); }
|
||||
function toggleDnd(): void { Notifs.toggleDnd(); }
|
||||
function clearNotifications(): void { Notifs.clearAll(); }
|
||||
function toggleStayAwake(): void { Idle.toggleStayAwake(); }
|
||||
function wallpaper(path: string): void { Wallpaper.set(path); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Wayland
|
||||
import "root:/services"
|
||||
|
||||
// What happens when nobody is at the machine. There is no hypridle: the shell
|
||||
// already owns the lock screen, so it owns the clock that leads up to it.
|
||||
//
|
||||
// dim the screen slowly darkens, so the lock does not arrive unannounced
|
||||
// lock the session lock (lock/Lock.qml)
|
||||
// off the displays power down; any input wakes them
|
||||
// suspend only on battery, and only after a long while
|
||||
//
|
||||
// Every stage respects idle inhibitors, so a fullscreen video or a call that
|
||||
// asks the compositor to stay awake holds all of them off. `stayAwake` is the
|
||||
// same thing by hand (`qs ipc call shell toggleStayAwake`).
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Seconds of no input before each stage.
|
||||
readonly property int dimAfter: 240
|
||||
readonly property int lockAfter: 300
|
||||
readonly property int offAfter: 360
|
||||
readonly property int suspendAfter: 900
|
||||
|
||||
property bool stayAwake: false
|
||||
|
||||
readonly property bool dimmed: dimMon.isIdle && !Actions.lockRequested
|
||||
|
||||
function toggleStayAwake() { root.stayAwake = !root.stayAwake; }
|
||||
|
||||
IdleMonitor {
|
||||
id: dimMon
|
||||
enabled: !root.stayAwake
|
||||
respectInhibitors: true
|
||||
timeout: root.dimAfter
|
||||
}
|
||||
|
||||
IdleMonitor {
|
||||
id: lockMon
|
||||
enabled: !root.stayAwake
|
||||
respectInhibitors: true
|
||||
timeout: root.lockAfter
|
||||
onIsIdleChanged: if (isIdle) Actions.lock()
|
||||
}
|
||||
|
||||
IdleMonitor {
|
||||
id: offMon
|
||||
enabled: !root.stayAwake
|
||||
respectInhibitors: true
|
||||
timeout: root.offAfter
|
||||
// Input ends the idle period, so the displays come back on the same
|
||||
// event that would have woken them anyway.
|
||||
onIsIdleChanged: Hyprland.dispatch(
|
||||
"hl.dsp.dpms({ action = \"" + (isIdle ? "off" : "on") + "\" })")
|
||||
}
|
||||
|
||||
IdleMonitor {
|
||||
id: suspendMon
|
||||
enabled: !root.stayAwake && Battery.onBattery
|
||||
respectInhibitors: true
|
||||
timeout: root.suspendAfter
|
||||
onIsIdleChanged: if (isIdle) Actions.suspend()
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property string preferredId: ""
|
||||
// The player that was playing most recently, so pausing it keeps it in
|
||||
// the bar instead of handing the slot to whichever player is listed first.
|
||||
property string lastPlayingId: ""
|
||||
|
||||
readonly property var players: Mpris.players?.values ?? []
|
||||
readonly property bool hasPlayer: active !== null
|
||||
@@ -22,9 +25,23 @@ Singleton {
|
||||
for (const p of players) if (p.dbusName === root.preferredId) return p;
|
||||
}
|
||||
for (const p of players) if (p.isPlaying) return p;
|
||||
if (root.lastPlayingId !== "") {
|
||||
for (const p of players) if (p.dbusName === root.lastPlayingId) return p;
|
||||
}
|
||||
// Browsers register a player for every tab with a media element, most
|
||||
// of them with no track at all. Prefer one that has something to show.
|
||||
for (const p of players) if (p.trackTitle) return p;
|
||||
return players[0];
|
||||
}
|
||||
|
||||
// Remember whichever player was playing last. Bound through the active
|
||||
// player, so it updates whenever playback starts anywhere.
|
||||
readonly property string playingId: {
|
||||
for (const p of players) if (p.isPlaying) return p.dbusName;
|
||||
return "";
|
||||
}
|
||||
onPlayingIdChanged: if (root.playingId !== "") root.lastPlayingId = root.playingId
|
||||
|
||||
readonly property string title: active?.trackTitle || ""
|
||||
readonly property string artist: active?.trackArtist || ""
|
||||
readonly property string album: active?.trackAlbum || ""
|
||||
|
||||
@@ -5,24 +5,17 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import "root:/config"
|
||||
|
||||
// The wallpaper currently on screen, per monitor.
|
||||
// The wallpaper, owned by the shell. backdrop/Backdrop.qml draws it and the lock
|
||||
// screen fades in from the same picture, so there is one answer to "what is
|
||||
// on screen" and nothing to ask a separate daemon (this used to poll
|
||||
// hyprpaper every ten seconds).
|
||||
//
|
||||
// The lock screen used to point at a hardcoded path in Theme.qml, which meant
|
||||
// it showed whatever picture was correct on the day that line was written —
|
||||
// takemi_2 while hyprpaper had been switched to takemi_1. Anything that puts
|
||||
// the wallpaper on screen should ask hyprpaper what it is actually displaying,
|
||||
// which is what this does.
|
||||
//
|
||||
// hyprpaper is the source of truth at runtime; hyprpaper.conf is the fallback
|
||||
// for the window between the shell starting and hyprpaper answering, and for
|
||||
// the case where hyprpaper is not running at all.
|
||||
// The choice lives in ~/.config/quickshell/wallpaper — one line, a path, `~`
|
||||
// allowed — so it survives restarts and can be edited by hand. Change it live
|
||||
// with `qs ipc call shell wallpaper ~/.wallpapers/other.jpg`.
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// monitor name -> file path. Monitors hyprpaper set with a blank `monitor =`
|
||||
// come back under the empty-string key.
|
||||
property var byMonitor: ({})
|
||||
|
||||
// The wallpaper to use when no particular monitor is being asked about.
|
||||
property string path: ""
|
||||
|
||||
@@ -30,18 +23,19 @@ Singleton {
|
||||
? Qt.resolvedUrl("file://" + root.path)
|
||||
: Theme.fallbackWallpaper
|
||||
|
||||
// Path set from hyprpaper.conf, used until (and unless) hyprctl answers.
|
||||
property string configPath: ""
|
||||
|
||||
signal changed()
|
||||
|
||||
// Every monitor shows the same picture. Kept as a per-monitor lookup so
|
||||
// callers do not have to change if that stops being true.
|
||||
function forMonitor(name: string): url {
|
||||
const p = root.byMonitor[name];
|
||||
return p ? Qt.resolvedUrl("file://" + p) : root.source;
|
||||
return root.source;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
reader.running = true;
|
||||
function set(p: string) {
|
||||
const next = root._expand(p);
|
||||
if (next === "") return;
|
||||
store.setText("# The wallpaper the shell draws (services/Wallpaper.qml). One path; ~ allowed.\n"
|
||||
+ next + "\n");
|
||||
}
|
||||
|
||||
function _expand(p: string): string {
|
||||
@@ -50,71 +44,19 @@ Singleton {
|
||||
return s;
|
||||
}
|
||||
|
||||
function _adopt(map: var, fallback: string) {
|
||||
root.byMonitor = map;
|
||||
|
||||
// Prefer the focused monitor's wallpaper, then any monitor's, then the
|
||||
// config. Object.values order is insertion order here, which is the
|
||||
// order hyprpaper listed them in.
|
||||
const vals = Object.keys(map).map(k => map[k]).filter(v => v !== "");
|
||||
const next = vals.length > 0 ? vals[0] : fallback;
|
||||
|
||||
if (next !== root.path) {
|
||||
root.path = next;
|
||||
root.changed();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: reader
|
||||
command: ["hyprctl", "hyprpaper", "listactive"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const map = {};
|
||||
for (const line of text.split("\n")) {
|
||||
// "eDP-1: /home/narl/.wallpapers/takemi_1.jpg". Split on the
|
||||
// first colon only — the path may contain one.
|
||||
const at = line.indexOf(":");
|
||||
if (at === -1) continue;
|
||||
const mon = line.slice(0, at).trim();
|
||||
const file = line.slice(at + 1).trim();
|
||||
if (file === "" || file.startsWith("invalid")) continue;
|
||||
map[mon] = file;
|
||||
}
|
||||
root._adopt(map, root.configPath);
|
||||
}
|
||||
}
|
||||
|
||||
// hyprpaper down, or hyprctl missing. Fall back to the config file.
|
||||
onExited: code => {
|
||||
if (code !== 0 && root.path === "") root._adopt({}, root.configPath);
|
||||
}
|
||||
}
|
||||
|
||||
// hyprpaper has no change signal to subscribe to, so poll — but slowly. A
|
||||
// wallpaper changes when a person decides to change it, not continuously.
|
||||
Timer {
|
||||
interval: 10000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
// Editing the config is the other way the wallpaper changes here, and that
|
||||
// one we can watch properly.
|
||||
FileView {
|
||||
id: conf
|
||||
path: Quickshell.env("HOME") + "/.config/hypr/hyprpaper.conf"
|
||||
id: store
|
||||
path: Quickshell.env("HOME") + "/.config/quickshell/wallpaper"
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
const m = /^\s*path\s*=\s*(.+)$/m.exec(text());
|
||||
if (m) root.configPath = root._expand(m[1]);
|
||||
// A config edit is usually followed by a hyprpaper reload, so go
|
||||
// ask what actually took effect rather than trusting the file.
|
||||
root.refresh();
|
||||
const line = text().split("\n").map(l => l.trim()).find(l => l !== "" && !l.startsWith("#"));
|
||||
const next = line ? root._expand(line) : "";
|
||||
if (next !== root.path) {
|
||||
root.path = next;
|
||||
root.changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user