diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-19 09:53:45 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-19 09:53:45 +0200 |
| commit | fbdfcd4e2d54dbc3f8767ba3ef97667802a7949b (patch) | |
| tree | a74cd93da69df3957c1b06b241b90155b0edbe91 /desktop | |
| parent | 96b359b8f9c7dbfea7ac4f2ff367a0385d72622c (diff) | |
| download | quickshell-fbdfcd4e2d54dbc3f8767ba3ef97667802a7949b.tar.gz quickshell-fbdfcd4e2d54dbc3f8767ba3ef97667802a7949b.zip | |
desktop: notification sounds on the settings page
A sound per urgency chosen with the widget file dialog, previewed with
pw-play, and the sound theme for apps' sound-name hints. Save writes the
theme to gsettings as well, and the page reads gsettings back on every
open so a change made elsewhere is shown, with a button to adopt it,
rather than overwritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop')
| -rw-r--r-- | desktop/modules/notifications/NotificationsModule.qml | 48 | ||||
| -rw-r--r-- | desktop/modules/notifications/NotificationsPage.qml | 88 |
2 files changed, 129 insertions, 7 deletions
diff --git a/desktop/modules/notifications/NotificationsModule.qml b/desktop/modules/notifications/NotificationsModule.qml index 01eea18..7a113df 100644 --- a/desktop/modules/notifications/NotificationsModule.qml +++ b/desktop/modules/notifications/NotificationsModule.qml @@ -27,6 +27,11 @@ Module { property var draft: Notify.config property string notice: "" + // The installed sound themes (directories with an index.theme), and the + // system theme as gsettings has it, "" when unreadable. Both reread on + // every refresh, so a change made outside the page shows. + property var themes: [] + property string systemTheme: "" dirty: JSON.stringify(nm.draft) !== JSON.stringify(Notify.config) readonly property bool valid: [nm.draft.liveCap, nm.draft.historyCap].every(v => v >= 1 && v <= 100) @@ -35,14 +40,18 @@ Module { // edited. The window calls refresh() on every open, which covers the // value a save leaves behind. Not over unsaved edits. function follow() { nm.draft = Qt.binding(() => Notify.config); } - function refresh() { if (!nm.dirty) nm.follow(); } + function refresh() { + if (!nm.dirty) nm.follow(); + nm.themeLister.running = false; nm.themeLister.running = true; + nm.gsGet.running = false; nm.gsGet.running = true; + } function discard() { nm.follow(); nm.notice = ""; } function edit(patch) { nm.draft = Object.assign({}, nm.draft, patch); } // Unknown keys in the file survive, including inside timeouts and // balloons, which are merged one level down. The text goes on stdin to // cat (the notes pattern: the exit code is the outcome); 1 is the write, - // 2 the reload, so a stopped daemon reads as saved-but-not-applied. + // 3 the gsettings theme, 2 the reload, so a stopped daemon reads as saved-but-not-applied. // Written in onStarted, then stdinEnabled cleared to give cat its EOF: // the sequence notes' bodyWriter uses (NotesModule.qml). function save() { @@ -52,7 +61,9 @@ Module { const out = Object.assign({}, raw, nm.draft, { timeouts: Object.assign({}, obj(raw.timeouts), nm.draft.timeouts), balloons: Object.assign({}, obj(raw.balloons), nm.draft.balloons), + sounds: Object.assign({}, obj(raw.sounds), nm.draft.sounds), }); + nm.saver.theme = nm.draft.soundTheme; nm.saver.data = JSON.stringify(out, null, 2) + "\n"; nm.saver.stdinEnabled = true; nm.saver.running = false; @@ -60,15 +71,38 @@ Module { } property Process saver: Process { property string data: "" - command: ["sh", "-c", 'mkdir -p "$(dirname "$1")" && cat > "$1" || exit 1; notifyctl reload || exit 2', - "sh", Notify.configPath] + property string theme: "" + command: ["sh", "-c", + 'mkdir -p "$(dirname "$1")" && cat > "$1" || exit 1; ' + + 'gsettings set org.gnome.desktop.sound theme-name "$2" || g=3; ' + + 'notifyctl reload || exit 2; exit ${g:-0}', + "sh", Notify.configPath, nm.saver.theme] onStarted: { nm.saver.write(nm.saver.data); nm.saver.stdinEnabled = false; } - onExited: code => nm.notice = code === 0 ? "saved" - : code === 2 ? "saved; notifyd not reachable, it reads the file at its next start" - : "could not write " + Notify.configPath + onExited: code => { + nm.notice = code === 0 ? "saved" + : code === 2 ? "saved; notifyd not reachable, it reads the file at its next start" + : code === 3 ? "saved; the system sound theme could not be set" + : "could not write " + Notify.configPath; + nm.gsGet.running = false; nm.gsGet.running = true; + } + } + property Process themeLister: Process { + command: ["sh", "-c", 'find "$HOME/.local/share/sounds" /usr/share/sounds -mindepth 2 -maxdepth 2 -name index.theme 2>/dev/null'] + stdout: StdioCollector { + onStreamFinished: nm.themes = [...new Set(text.split("\n").filter(l => l) + .map(l => l.split("/").slice(-2)[0]))].sort() + } + } + // gsettings prints the string quoted: 'freedesktop'. + property Process gsGet: Process { + command: ["gsettings", "get", "org.gnome.desktop.sound", "theme-name"] + stdout: StdioCollector { + onStreamFinished: nm.systemTheme = text.trim().replace(/^'(.*)'$/, "$1") + } + onExited: code => { if (code !== 0) nm.systemTheme = ""; } } page: Component { diff --git a/desktop/modules/notifications/NotificationsPage.qml b/desktop/modules/notifications/NotificationsPage.qml index 28bdcf4..ce09a9d 100644 --- a/desktop/modules/notifications/NotificationsPage.qml +++ b/desktop/modules/notifications/NotificationsPage.qml @@ -9,7 +9,9 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell +import Quickshell.Io import QtQuick +import Qt.labs.platform as P import "../.." import "../appearance" import "../qtmaildir" @@ -71,6 +73,92 @@ Column { } } + Heading { text: "Sounds" } + Text { + width: pg.width; wrapMode: Text.Wrap + text: "Played when a balloon pops, never for one muted, held by Do Not Disturb or snoozed. An app's own sound, or its request for silence, wins." + color: Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + Repeater { + model: [["low", "Low"], ["normal", "Normal"], ["critical", "Critical"]] + Row { + required property var modelData + spacing: 10 + readonly property string file: pg.d.sounds[modelData[0]] + Text { + width: 90; anchors.verticalCenter: parent.verticalCenter + text: modelData[1]; color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Text { + width: 180; anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideMiddle + text: parent.file ? parent.file.split("/").pop() : "none" + color: parent.file ? Theme.text : Theme.overlay + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Button { + text: "Choose\u2026" + onClicked: { picker.key = modelData[0]; picker.open(); } + } + Button { + icon: "\uf04b" + enabled: parent.file !== "" + onClicked: { preview.command = ["pw-play", parent.file]; preview.running = false; preview.running = true; } + } + Button { + icon: "\uf00d" + enabled: parent.file !== "" + onClicked: pg.mod.edit({ sounds: Object.assign({}, pg.d.sounds, { [modelData[0]]: "" }) }) + } + } + } + Text { + topPadding: 6 + text: "Theme for sounds apps ask for by name, also set as the system sound theme" + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4; bold: true } + } + Flow { + width: pg.width; spacing: 6 + Repeater { + model: pg.mod.themes + Button { + required property string modelData + text: modelData + border.color: pg.d.soundTheme === modelData ? Theme.accent : Qt.alpha(Theme.text, 0.2) + onClicked: pg.mod.edit({ soundTheme: modelData }) + } + } + } + Row { + spacing: 10 + visible: pg.mod.systemTheme !== "" && pg.mod.systemTheme !== pg.d.soundTheme + Text { + anchors.verticalCenter: parent.verticalCenter + text: "System sound theme is " + pg.mod.systemTheme + ", changed outside this page" + color: Theme.yellow + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + Button { + text: "Use " + pg.mod.systemTheme + onClicked: pg.mod.edit({ soundTheme: pg.mod.systemTheme }) + } + } + + Process { id: preview } + // Qt.labs.platform, not QtQuick.Dialogs: the Quick dialog opens light on + // this desktop (AGENTS.md); this one takes the widget style. + P.FileDialog { + id: picker + property string key: "" + folder: "file://" + Quickshell.env("HOME") + "/.local/share/sounds" + nameFilters: ["Sounds (*.oga *.ogg *.wav)"] + onAccepted: pg.mod.edit({ sounds: Object.assign({}, pg.d.sounds, + { [picker.key]: decodeURIComponent(file.toString().replace(/^file:\/\//, "")) }) }) + } + Heading { text: "Limits" } Text { width: pg.width; wrapMode: Text.Wrap |
