aboutsummaryrefslogtreecommitdiffstats
path: root/desktop
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-19 09:53:45 +0200
committerDanilo M. <danix@danix.xyz>2026-10-19 09:53:45 +0200
commitfbdfcd4e2d54dbc3f8767ba3ef97667802a7949b (patch)
treea74cd93da69df3957c1b06b241b90155b0edbe91 /desktop
parent96b359b8f9c7dbfea7ac4f2ff367a0385d72622c (diff)
downloadquickshell-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.qml48
-rw-r--r--desktop/modules/notifications/NotificationsPage.qml88
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