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:
nvrl committed 2026-10-04 23:10:19 +02:00
1 parent 864420281b
commit c36a74e861
20 files changed
+396 -128

No files matched your search

+2 -1
View File
@@ -10,7 +10,8 @@ hl.on("hyprland.start", function()
hl.exec_cmd("uwsm app -- qs") hl.exec_cmd("uwsm app -- qs")
hl.exec_cmd("uwsm app -- nm-applet --indicator") hl.exec_cmd("uwsm app -- nm-applet --indicator")
hl.exec_cmd("uwsm app -- hyprpaper") -- hyprpaper is not started here: its systemd unit is enabled for
-- graphical-session.target, and a second copy here ran alongside it.
-- clipboard history, text and images -- clipboard history, text and images
hl.exec_cmd("wl-paste --type text --watch cliphist store") hl.exec_cmd("wl-paste --type text --watch cliphist store")
+1
View File
@@ -11,6 +11,7 @@ shell("power", { key = mod .. " + P", description = "power menu" })
shell("ask", { key = mod .. " + A", description = "ask claude" }) shell("ask", { key = mod .. " + A", description = "ask claude" })
shell("dashboard", { key = mod .. " + D", description = "dashboard" }) shell("dashboard", { key = mod .. " + D", description = "dashboard" })
shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" }) shell("toggleDnd", { key = mod .. " + SHIFT + N", description = "toggle do not disturb" })
shell("toggleStayAwake", { key = mod .. " + SHIFT + I", description = "keep the screen awake" })
shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" }) shell("clearNotifications", { key = mod .. " + CTRL + SHIFT + N", description = "clear notifications" })
shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" }) shell("reloadConfig", { key = mod .. " + SHIFT + R", description = "reload shell config" })
+1 -8
View File
@@ -79,9 +79,6 @@ local function reconcile(ignore)
return return
end end
-- First decision of the session, and it agrees with what lua/monitors.lua
-- already applied: nothing actually moved, so no daemon needs poking.
local settled = panel_off == nil and not want_off
panel_off = want_off panel_off = want_off
if want_off then if want_off then
@@ -91,11 +88,7 @@ local function reconcile(ignore)
end end
monitors.apply_external() monitors.apply_external()
-- Quickshell follows monitor hotplug by itself; only the wallpaper daemon -- Quickshell follows monitor hotplug by itself, wallpaper included.
-- needs a kick.
if not settled then
hl.exec_cmd("systemctl restart --user hyprpaper")
end
end end
-- ------------------------------------------------------------------ triggers -- ------------------------------------------------------------------ triggers
+18 -22
View File
@@ -74,6 +74,10 @@ hl.config({
font_family = "FiraCode Nerd Font", font_family = "FiraCode Nerd Font",
background_color = c.base, background_color = c.base,
focus_on_activate = true, focus_on_activate = true,
-- The shell powers displays down after idle (quickshell/services/Idle.qml)
-- and back up when idle ends; these wake them even if it is not running.
key_press_enables_dpms = true,
mouse_move_enables_dpms = true,
}, },
group = { group = {
@@ -82,37 +86,29 @@ hl.config({
border_inactive = c.overlay, border_inactive = c.overlay,
}, },
-- Tabs are solid slabs of the hull, not titles painted over the -- Tabs are thin rounded lines, not titled slabs. The bar already
-- wallpaper. Hyprland paints a tab's fill and its indicator from the -- writes the focused title and its place in the group ("2/3", in
-- same colour, so the current tab is told apart by being lifted a step -- quickshell/bar/ActiveWindow.qml), so titles here only repeated it in
-- and by its title taking the light, not by an accent fill — a light -- a second font. Hyprland paints a tab's indicator in the same colour
-- fill would swallow its own title. -- as its fill, so with no fill the current tab is simply the light one.
groupbar = { groupbar = {
font_family = "Noto Sans", render_titles = false,
font_size = 10, gradients = false,
font_weight_active = "medium",
font_weight_inactive = "normal",
height = 22, indicator_height = 3,
indicator_height = 1,
indicator_gap = 0, indicator_gap = 0,
gradients = true, rounding = 2,
rounding = 6, round_only_edges = false,
gradient_rounding = 6,
round_only_edges = true,
gradient_round_only_edges = true,
keep_upper_gap = false, keep_upper_gap = false,
gaps_in = 2, gaps_in = 4,
gaps_out = 0, gaps_out = 0,
col = { col = {
active = c.overlay, active = c.accent,
inactive = c.surface, inactive = c.overlay,
locked_active = c.alarm, locked_active = c.alarm,
locked_inactive = c.surface, locked_inactive = c.overlay,
}, },
text_color = c.accent,
text_color_inactive = c.muted,
}, },
}, },
+2 -3
View File
@@ -49,10 +49,9 @@ local function mirror_all(panel)
end end
end end
-- The bar and popouts stay on the outputs they were created for, so -- The bar, popouts and wallpaper stay on the outputs they were created for,
-- respawn the shell the same way lua/autostart.lua starts it. -- so respawn the shell the same way lua/autostart.lua starts it.
local function restart_clients() local function restart_clients()
hl.exec_cmd("systemctl restart --user hyprpaper")
hl.exec_cmd("sh -c 'qs kill; exec uwsm app -- qs'") hl.exec_cmd("sh -c 'qs kill; exec uwsm app -- qs'")
end end
+2 -1
View File
@@ -3,6 +3,7 @@ aalib
abseil-cpp abseil-cpp
acl acl
acpi acpi
adw-gtk-theme
adwaita-cursors adwaita-cursors
adwaita-fonts adwaita-fonts
adwaita-icon-theme adwaita-icon-theme
@@ -87,7 +88,6 @@ calibre
cargo-audit cargo-audit
catppuccin-gtk-theme-mocha catppuccin-gtk-theme-mocha
catppuccin-qt5ct-git catppuccin-qt5ct-git
catppuccinifier-cli-git
cbindgen cbindgen
cblas cblas
cdparanoia cdparanoia
@@ -111,6 +111,7 @@ cups-pk-helper
curl curl
cxxbridge cxxbridge
cython cython
darkly-bin
dav1d dav1d
db5.3 db5.3
dbus dbus
+1 -1
View File
@@ -3,7 +3,7 @@ color_scheme_path=/home/narl/.config/qt6ct/colors/yorha.conf
custom_palette=true custom_palette=true
icon_theme=Papirus-Dark icon_theme=Papirus-Dark
standard_dialogs=default standard_dialogs=default
style=Fusion style=Darkly
[Fonts] [Fonts]
fixed="Sans Serif,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1" fixed="Sans Serif,9,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
+42 -4
View File
@@ -66,6 +66,7 @@ bar/ the two surfaces and their modules
popouts/ hover panels hanging off rail and bar modules popouts/ hover panels hanging off rail and bar modules
overlays/ power menu, notification toasts, volume/brightness OSD overlays/ power menu, notification toasts, volume/brightness OSD
lock/ session lock lock/ session lock
backdrop/ the wallpaper, drawn by the shell
assets/ halftone and stripe tiles, generated with ImageMagick assets/ halftone and stripe tiles, generated with ImageMagick
``` ```
@@ -143,6 +144,7 @@ Set in `~/.config/hypr/lua/binds/shell.lua`:
- `Super+Ctrl+L` — lock - `Super+Ctrl+L` — lock
- `Super+P` — power menu - `Super+P` — power menu
- `Super+Shift+N` — toggle do not disturb - `Super+Shift+N` — toggle do not disturb
- `Super+Shift+I` — stay awake (no dim, lock or screen-off until toggled back)
- `Super+Ctrl+Shift+N` — clear notifications - `Super+Ctrl+Shift+N` — clear notifications
- `Super+A` — ask panel - `Super+A` — ask panel
- `Super+Shift+R` — reload the shell - `Super+Shift+R` — reload the shell
@@ -153,6 +155,8 @@ All of these go through Quickshell's IPC, so they work from any script:
qs ipc call shell lock qs ipc call shell lock
qs ipc call shell power qs ipc call shell power
qs ipc call shell toggleDnd qs ipc call shell toggleDnd
qs ipc call shell toggleStayAwake
qs ipc call shell wallpaper ~/.wallpapers/space-1.jpg
qs ipc call shell ask qs ipc call shell ask
qs ipc call shell askAbout "why is the sky blue" qs ipc call shell askAbout "why is the sky blue"
qs ipc call shell clearNotifications qs ipc call shell clearNotifications
@@ -211,6 +215,34 @@ NetworkManager for Wi-Fi, UPower for battery, MPRIS for media, and Quickshell's
own notification server. These are event-driven, so the bar and OSD react the own notification server. These are event-driven, so the bar and OSD react the
instant a volume key is pressed instead of on the next poll. instant a volume key is pressed instead of on the next poll.
## Wallpaper
`backdrop/Backdrop.qml` draws it on the background layer; there is no
hyprpaper. The picture is drawn 8% larger than the screen and drifts with the
workspace: 1 sits at the top of it and each step down moves it a fixed
distance, a little slower than Hyprland's own slide, so the windows arrive
first and the scene behind catches up. It never moves on its own, so an idle
desktop costs the compositor nothing. While the workspace holds windows it
settles a shade darker.
The choice is one line in `quickshell/wallpaper` (`~` allowed), watched live.
`qs ipc call shell wallpaper <path>` rewrites it.
## Idle
`services/Idle.qml`, on Wayland's idle-notify protocol instead of hypridle:
| After | Does |
|---|---|
| 4 min | the screen dims slowly (`overlays/IdleDim.qml`); any input lifts it |
| 5 min | lock |
| 6 min | displays off; any input wakes them |
| 15 min | suspend, on battery only |
Every stage respects idle inhibitors, so a fullscreen video holds them all
off. `Super+Shift+I` does the same by hand and puts a cup on the rail while it
is on.
## Lock screen ## Lock screen
Authenticates through `/etc/pam.d/hyprlock`, which on this box is just Authenticates through `/etc/pam.d/hyprlock`, which on this box is just
@@ -219,7 +251,8 @@ styled in an ordinary window (`qs -p lockpreview.qml`) without locking the
session. session.
It is the wallpaper, barely softened, with the face set in the dark of the It is the wallpaper, barely softened, with the face set in the dark of the
porthole: the mark, the time in light display type, the date, a status log porthole: the mark, the time in light display type, the date, "Take your
time.", a status log
(battery, network, what is playing, unread notices), and a rounded field that (battery, network, what is playing, unread notices), and a rounded field that
fills with dots as you type. The dots breathe while PAM checks; a wrong fills with dots as you type. The dots breathe while PAM checks; a wrong
password turns them and the field's rim to rust and nudges the field once. password turns them and the field's rim to rust and nudges the field once.
@@ -231,6 +264,9 @@ means restarting Hyprland from a TTY.
## What was retired ## What was retired
- `waybar.service` — disabled. Its config is still in `~/.config/waybar`. - `waybar.service` — disabled. Its config is still in `~/.config/waybar`.
- `hyprpaper` — replaced by `backdrop/`. Turn its unit off with
`systemctl --user disable --now hyprpaper`.
- `hypridle` — never installed; `services/Idle.qml` does the job.
- `dunst.service` — masked, so it cannot be D-Bus activated and steal - `dunst.service` — masked, so it cannot be D-Bus activated and steal
`org.freedesktop.Notifications`. Undo with `org.freedesktop.Notifications`. Undo with
`systemctl --user unmask dunst.service`. `systemctl --user unmask dunst.service`.
@@ -311,8 +347,9 @@ network and bluetooth, but keep running as the secret and pairing agents.
the opening's 18px corners at a 10px gap (`Theme.frameRadius - gaps_out`); the opening's 18px corners at a 10px gap (`Theme.frameRadius - gaps_out`);
change one and the others follow. No blur, both opacities at `1.0`, 1px change one and the others follow. No blur, both opacities at `1.0`, 1px
borders running from the light into the suit's grey. The shadow is soft and borders running from the light into the suit's grey. The shadow is soft and
wide, so windows float in the opening. Group tabs are solid hull slabs, the wide, so windows float in the opening. Group tabs are thin rounded lines with
current one lifted a step with its title in the light. Motion no titles, the current one in the light; the bar writes the title once and
adds the window's place in the group (`2/3`). Motion
(`hypr/lua/animations.lua`) glides and settles with no overshoot: windows (`hypr/lua/animations.lua`) glides and settles with no overshoot: windows
resolve in from a hair smaller, workspaces drift a short way vertically and resolve in from a hair smaller, workspaces drift a short way vertically and
fade (sliding behind the hull), the `SUPER+S` scratchpad lowers out from fade (sliding behind the hull), the `SUPER+S` scratchpad lowers out from
@@ -335,7 +372,8 @@ network and bluetooth, but keep running as the secret and pairing agents.
The theme name is also in `xsettingsd/xsettingsd.conf` and has to be set with The theme name is also in `xsettingsd/xsettingsd.conf` and has to be set with
`gsettings` for portal-aware apps. `gsettings` for portal-aware apps.
- **Qt** — qt6ct with `qt6ct/colors/yorha.conf`, the same palette in - **Qt** — qt6ct with `qt6ct/colors/yorha.conf`, the same palette in
`QPalette` roles. The style is Fusion unless Darkly is installed. `QPalette` roles, drawn with the Darkly style (package `darkly-bin`) for
rounded controls. Fusion is the fallback if Darkly is missing.
- **zathura** — `zathura/yorha`, included from `zathurarc`. Pages are - **zathura** — `zathura/yorha`, included from `zathurarc`. Pages are
recoloured onto the hull with text in bone; images keep their colours. recoloured onto the hull with text in bone; images keep their colours.
Search hits are a soft bone wash, and the current hit is sand. Search hits are a soft bone wash, and the current hit is sand.
+90
View File
@@ -0,0 +1,90 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import "root:/config"
import "root:/services"
// The wallpaper, seen through the porthole. It is drawn a little larger than
// the screen and drifts when the workspace changes: windows slide past
// vertically (hypr/lua/animations.lua) and the scene behind them moves a few
// pixels the same way and a little later, the way distant things do out of a
// moving window. Only on switches — it never moves on its own, so an idle
// desktop costs the compositor nothing.
//
// It also settles a shade darker while the workspace holds windows, so the
// band of wallpaper round them reads as the far side of the glass rather than
// as part of the desktop.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.namespace: "takemi-backdrop"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
anchors { top: true; bottom: true; left: true; right: true }
color: Theme.ink
mask: Region {}
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
readonly property HyprlandWorkspace workspace: root.monitor?.activeWorkspace ?? null
// Overscan: how much bigger than the screen the picture is drawn, which is
// the room it has to drift in.
readonly property real overscan: 1.08
readonly property real slack: root.height * (root.overscan - 1)
// Workspaces 1–10 spread across the slack, so 1 sits at the top of the
// picture and each step down moves it a fixed distance. Special and
// named workspaces (negative ids) leave it where it was.
property real drift: 0
readonly property int wsId: root.workspace?.id ?? 1
onWsIdChanged: if (root.wsId > 0) root.drift = Math.min(root.wsId - 1, 9) / 9
readonly property bool occupied: (root.workspace?.toplevels?.values?.length ?? 0) > 0
Image {
id: picture
width: root.width * root.overscan
height: root.height * root.overscan
x: -(width - root.width) / 2
y: -root.drift * root.slack
source: Wallpaper.forMonitor(root.screen?.name ?? "")
sourceSize: Qt.size(width, height)
fillMode: Image.PreserveAspectCrop
asynchronous: true
smooth: true
cache: false
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: Theme.durLazy; easing.type: Easing.OutCubic }
}
// Longer than Hyprland's 550ms workspace slide, on the same curve, so
// the windows arrive first and the scene catches up.
Behavior on y {
NumberAnimation {
duration: 900
easing.type: Easing.BezierSpline
easing.bezierCurve: [0.35, 0.0, 0.15, 1.0, 1.0, 1.0]
}
}
}
Rectangle {
anchors.fill: parent
color: Theme.ink
opacity: root.occupied ? 0.18 : 0
Behavior on opacity {
NumberAnimation { duration: Theme.durLazy; easing.type: Easing.InOutSine }
}
}
Component.onCompleted: root.drift = Math.min(Math.max(root.wsId, 1) - 1, 9) / 9
}
+41 -3
View File
@@ -20,6 +20,31 @@ Item {
|| toplevel.monitor.id === root.monitor.id) || toplevel.monitor.id === root.monitor.id)
readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—" readonly property string incomingTitle: mine && toplevel.title ? toplevel.title : "—"
// Where the focused window sits in its group, as "2/3", or "" when it is
// not grouped. Hyprland's group bar draws only indicator lines (see
// hypr/lua/look.lua), so the bar is the one place the title is written.
readonly property string groupLabel: {
const ipc = root.mine ? root.toplevel.lastIpcObject : null;
const members = ipc?.grouped ?? [];
if (members.length < 2) return "";
const bare = a => String(a).replace(/^0x/, "");
const at = members.findIndex(m => bare(m) === bare(root.toplevel.address));
return (at < 0 ? "?" : at + 1) + "/" + members.length;
}
// lastIpcObject is a snapshot, refreshed only on request. Ask again
// whenever focus or group membership may have changed.
Connections {
target: Hyprland
function onRawEvent(event) {
const n = event.name;
if (n === "activewindowv2" || n === "togglegroup"
|| n === "moveintogroup" || n === "moveoutofgroup"
|| n === "closewindow" || n === "openwindow")
Hyprland.refreshToplevels();
}
}
property string displayedTitle: "—" property string displayedTitle: "—"
// Which toplevel the landing transition last played for. The landing is a // Which toplevel the landing transition last played for. The landing is a
@@ -30,7 +55,8 @@ Item {
// The bar shrinks this when the three clusters would otherwise collide. // The bar shrinks this when the three clusters would otherwise collide.
property real maxWidth: 240 property real maxWidth: 240
implicitWidth: Math.min(maxWidth, Math.max(70, label.implicitWidth + 4)) implicitWidth: Math.min(maxWidth,
Math.max(70, label.implicitWidth + 4 + (groupTag.visible ? groupTag.width + Theme.padS : 0)))
implicitHeight: 20 implicitHeight: 20
Behavior on implicitWidth { Behavior on implicitWidth {
@@ -45,7 +71,7 @@ Item {
Text { Text {
id: label id: label
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width width: parent.width - (groupTag.visible ? groupTag.width + Theme.padS : 0)
text: root.displayedTitle text: root.displayedTitle
color: root.mine ? Theme.subtext : Theme.muted color: root.mine ? Theme.subtext : Theme.muted
elide: Text.ElideRight elide: Text.ElideRight
@@ -61,7 +87,7 @@ Item {
Text { Text {
id: ghost id: ghost
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width width: label.width
text: root.displayedTitle text: root.displayedTitle
color: Theme.accent color: Theme.accent
opacity: 0 opacity: 0
@@ -69,6 +95,18 @@ Item {
font: label.font font: label.font
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Text {
id: groupTag
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: root.groupLabel !== ""
text: root.groupLabel
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
} }
// Adopt the settled caption, and stamp it only when focus actually moved. // Adopt the settled caption, and stamp it only when focus actually moved.
+13
View File
@@ -137,6 +137,19 @@ PanelWindow {
Divider { visible: tray.visible } Divider { visible: tray.visible }
// Only there while the idle chain is held off by hand, so it is
// obvious why the screen never locked.
RailIcon {
id: awakeIcon
visible: Idle.stayAwake
icon: "󰅶"
label: "Stay awake"
statusText: "Screen will not dim or lock"
hint: "Click to allow sleep"
live: true
onClicked: Idle.toggleStayAwake()
}
RailIcon { RailIcon {
id: bellIcon id: bellIcon
icon: Notifs.icon icon: Notifs.icon
+12
View File
@@ -131,6 +131,18 @@ Item {
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
// The one line the face says to you. Nothing here is in a hurry.
Text {
topPadding: Math.round(Theme.padL * surface.uiScale)
leftPadding: Math.round(4 * surface.uiScale)
text: "Take your time."
color: Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Math.round(Theme.fsLarge * surface.uiScale)
font.weight: Font.Light
renderType: Text.NativeRendering
}
Item { width: 1; height: Math.round(Theme.padXL * surface.uiScale) } Item { width: 1; height: Math.round(Theme.padXL * surface.uiScale) }
// ------------------------------------------------------ status log // ------------------------------------------------------ status log
+39
View File
@@ -0,0 +1,39 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "root:/config"
import "root:/services"
// The first stage of idle (services/Idle.qml): the screen dims the way a room
// does when the lights go down, slowly enough that you notice it before the
// lock does. Any input lifts it at once. Input passes straight through.
PanelWindow {
id: root
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "takemi-dim"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
mask: Region {}
visible: Idle.dimmed || shade.opacity > 0
Rectangle {
id: shade
anchors.fill: parent
color: Theme.ink
opacity: Idle.dimmed ? 0.6 : 0
Behavior on opacity {
NumberAnimation {
duration: Idle.dimmed ? 8000 : Theme.durBase
easing.type: Idle.dimmed ? Easing.InOutSine : Easing.OutCubic
}
}
}
}
+3 -3
View File
@@ -100,19 +100,19 @@ PanelWindow {
Text { Text {
topPadding: Theme.padM topPadding: Theme.padM
text: "GLORY TO MANKIND." text: "Take your time."
color: Theme.accent color: Theme.accent
font.family: Theme.fontDisplay font.family: Theme.fontDisplay
font.pixelSize: Theme.fsHuge font.pixelSize: Theme.fsHuge
font.weight: Font.Light font.weight: Font.Light
font.letterSpacing: 6 font.letterSpacing: 1
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
Text { Text {
topPadding: Theme.padS topPadding: Theme.padS
leftPadding: 2 leftPadding: 2
text: ["YORHA", (Quickshell.env("USER") || "user") + "@" + hostFile.text().trim(), text: [(Quickshell.env("USER") || "user") + "@" + hostFile.text().trim(),
Sys.uptime ? "UP " + Sys.uptime : ""].filter(t => t !== "").join(" · ") Sys.uptime ? "UP " + Sys.uptime : ""].filter(t => t !== "").join(" · ")
color: Theme.muted color: Theme.muted
font.family: Theme.fontMono font.family: Theme.fontMono
+2
View File
@@ -163,5 +163,7 @@ Singleton {
function reloadConfig(): void { root.reloadShell(); } function reloadConfig(): void { root.reloadShell(); }
function toggleDnd(): void { Notifs.toggleDnd(); } function toggleDnd(): void { Notifs.toggleDnd(); }
function clearNotifications(): void { Notifs.clearAll(); } function clearNotifications(): void { Notifs.clearAll(); }
function toggleStayAwake(): void { Idle.toggleStayAwake(); }
function wallpaper(path: string): void { Wallpaper.set(path); }
} }
} }
+68
View File
@@ -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()
}
}
+17
View File
@@ -10,6 +10,9 @@ Singleton {
id: root id: root
property string preferredId: "" 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 var players: Mpris.players?.values ?? []
readonly property bool hasPlayer: active !== null 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.dbusName === root.preferredId) return p;
} }
for (const p of players) if (p.isPlaying) 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]; 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 title: active?.trackTitle || ""
readonly property string artist: active?.trackArtist || "" readonly property string artist: active?.trackArtist || ""
readonly property string album: active?.trackAlbum || "" readonly property string album: active?.trackAlbum || ""
+24 -82
View File
@@ -5,24 +5,17 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import "root:/config" 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 // The choice lives in ~/.config/quickshell/wallpaper — one line, a path, `~`
// it showed whatever picture was correct on the day that line was written — // allowed — so it survives restarts and can be edited by hand. Change it live
// takemi_2 while hyprpaper had been switched to takemi_1. Anything that puts // with `qs ipc call shell wallpaper ~/.wallpapers/other.jpg`.
// 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.
Singleton { Singleton {
id: root 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. // The wallpaper to use when no particular monitor is being asked about.
property string path: "" property string path: ""
@@ -30,18 +23,19 @@ Singleton {
? Qt.resolvedUrl("file://" + root.path) ? Qt.resolvedUrl("file://" + root.path)
: Theme.fallbackWallpaper : Theme.fallbackWallpaper
// Path set from hyprpaper.conf, used until (and unless) hyprctl answers.
property string configPath: ""
signal changed() 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 { function forMonitor(name: string): url {
const p = root.byMonitor[name]; return root.source;
return p ? Qt.resolvedUrl("file://" + p) : root.source;
} }
function refresh() { function set(p: string) {
reader.running = true; 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 { function _expand(p: string): string {
@@ -50,71 +44,19 @@ Singleton {
return s; return s;
} }
function _adopt(map: var, fallback: string) { FileView {
root.byMonitor = map; id: store
path: Quickshell.env("HOME") + "/.config/quickshell/wallpaper"
// Prefer the focused monitor's wallpaper, then any monitor's, then the watchChanges: true
// config. Object.values order is insertion order here, which is the printErrors: false
// order hyprpaper listed them in. onFileChanged: reload()
const vals = Object.keys(map).map(k => map[k]).filter(v => v !== ""); onLoaded: {
const next = vals.length > 0 ? vals[0] : fallback; const line = text().split("\n").map(l => l.trim()).find(l => l !== "" && !l.startsWith("#"));
const next = line ? root._expand(line) : "";
if (next !== root.path) { if (next !== root.path) {
root.path = next; root.path = next;
root.changed(); 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"
watchChanges: true
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();
}
} }
} }
+16
View File
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell import Quickshell
import "root:/config" import "root:/config"
import "root:/services" import "root:/services"
import "root:/backdrop"
import "root:/bar" import "root:/bar"
import "root:/launcher" import "root:/launcher"
import "root:/dashboard" import "root:/dashboard"
@@ -12,6 +13,7 @@ import "root:/lock"
// Takemi — a YoRHa-dark Quickshell desktop: NieR:Automata's interface // Takemi — a YoRHa-dark Quickshell desktop: NieR:Automata's interface
// language, lit like the wallpaper. // language, lit like the wallpaper.
// //
// backdrop/ the wallpaper, drifting with the workspace
// bar/ the hull, the top bar, the rail and their modules // bar/ the hull, the top bar, the rail and their modules
// popouts/ hover panels hanging off bar modules // popouts/ hover panels hanging off bar modules
// launcher/ the application launcher // launcher/ the application launcher
@@ -36,6 +38,14 @@ ShellRoot {
void Bt.available; void Bt.available;
void Actions.powerMenuOpen; void Actions.powerMenuOpen;
void Apps.index; void Apps.index;
void Idle.stayAwake;
void Wallpaper.path;
}
Variants {
model: Quickshell.screens
Backdrop {}
} }
Variants { Variants {
@@ -92,5 +102,11 @@ ShellRoot {
Osd {} Osd {}
} }
Variants {
model: Quickshell.screens
IdleDim {}
}
Lock {} Lock {}
} }
+2
View File
@@ -0,0 +1,2 @@
# The wallpaper the shell draws (services/Wallpaper.qml). One path; ~ allowed.
~/.wallpapers/space-1.jpg