diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index b7550c7a8..b1cb46437 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -245,6 +245,7 @@ Singleton { property JsonObject battery: JsonObject { property int low: 20 property int critical: 5 + property int full: 101 property bool automaticSuspend: true property int suspend: 3 } @@ -418,13 +419,18 @@ Singleton { } } + property JsonObject sounds: JsonObject { + property bool battery: false + property bool pomodoro: false + property string theme: "freedesktop" + } + property JsonObject time: JsonObject { // https://doc.qt.io/qt-6/qtime.html#toString property string format: "hh:mm" property string shortDateFormat: "dd/MM" property string dateFormat: "ddd, dd/MM" property JsonObject pomodoro: JsonObject { - property string alertSound: "" property int breakTime: 300 property int cyclesBeforeLongBreak: 4 property int focus: 1500 diff --git a/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml b/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml index fb4010a01..0953b3d9e 100644 --- a/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/GeneralConfig.qml @@ -113,6 +113,20 @@ ContentPage { } } } + ConfigRow { + uniform: true + ConfigSpinBox { + icon: "charger" + text: Translation.tr("Full warning") + value: Config.options.battery.full + from: 0 + to: 101 + stepSize: 5 + onValueChanged: { + Config.options.battery.full = value; + } + } + } } ContentSection { @@ -241,6 +255,30 @@ ContentPage { } } + ContentSection { + icon: "notification_sound" + title: Translation.tr("Sounds") + ConfigRow { + uniform: true + ConfigSwitch { + buttonIcon: "battery_android_full" + text: Translation.tr("Battery") + checked: Config.options.sounds.battery + onCheckedChanged: { + Config.options.sounds.battery = checked; + } + } + ConfigSwitch { + buttonIcon: "av_timer" + text: Translation.tr("Pomodoro") + checked: Config.options.sounds.pomodoro + onCheckedChanged: { + Config.options.sounds.pomodoro = checked; + } + } + } + } + ContentSection { icon: "nest_clock_farsight_analog" title: Translation.tr("Time") diff --git a/dots/.config/quickshell/ii/services/Audio.qml b/dots/.config/quickshell/ii/services/Audio.qml index 43bc61b4d..4ce6521c6 100644 --- a/dots/.config/quickshell/ii/services/Audio.qml +++ b/dots/.config/quickshell/ii/services/Audio.qml @@ -15,6 +15,7 @@ Singleton { property PwNode sink: Pipewire.defaultAudioSink property PwNode source: Pipewire.defaultAudioSource readonly property real hardMaxValue: 2.00 // People keep joking about setting volume to 5172% so... + property string audioTheme: Config.options.sounds.theme signal sinkProtectionTriggered(string reason); @@ -49,7 +50,28 @@ Singleton { } lastVolume = sink.audio.volume; } - } + function playSystemSound(soundName) { + const ogaPath = `/usr/share/sounds/${root.audioTheme}/stereo/${soundName}.oga`; + const oggPath = `/usr/share/sounds/${root.audioTheme}/stereo/${soundName}.ogg`; + + // Try playing .oga first + let command = [ + "ffplay", + "-nodisp", + "-autoexit", + ogaPath + ]; + Quickshell.execDetached(command); + + // Also try playing .ogg (ffplay will just fail silently if file doesn't exist) + command = [ + "ffplay", + "-nodisp", + "-autoexit", + oggPath + ]; + Quickshell.execDetached(command); + } } diff --git a/dots/.config/quickshell/ii/services/Battery.qml b/dots/.config/quickshell/ii/services/Battery.qml index 0a19b70f1..b07bd5305 100644 --- a/dots/.config/quickshell/ii/services/Battery.qml +++ b/dots/.config/quickshell/ii/services/Battery.qml @@ -8,49 +8,79 @@ import QtQuick import Quickshell.Io Singleton { + id: root property bool available: UPower.displayDevice.isLaptopBattery property var chargeState: UPower.displayDevice.state property bool isCharging: chargeState == UPowerDeviceState.Charging property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge property real percentage: UPower.displayDevice?.percentage ?? 1 readonly property bool allowAutomaticSuspend: Config.options.battery.automaticSuspend + readonly property bool soundEnabled: Config.options.sounds.battery property bool isLow: available && (percentage <= Config.options.battery.low / 100) property bool isCritical: available && (percentage <= Config.options.battery.critical / 100) property bool isSuspending: available && (percentage <= Config.options.battery.suspend / 100) + property bool isFull: available && (percentage >= Config.options.battery.full / 100) property bool isLowAndNotCharging: isLow && !isCharging property bool isCriticalAndNotCharging: isCritical && !isCharging property bool isSuspendingAndNotCharging: allowAutomaticSuspend && isSuspending && !isCharging + property bool isFullAndCharging: isFull && isCharging property real energyRate: UPower.displayDevice.changeRate property real timeToEmpty: UPower.displayDevice.timeToEmpty property real timeToFull: UPower.displayDevice.timeToFull onIsLowAndNotChargingChanged: { - if (available && isLowAndNotCharging) Quickshell.execDetached([ + if (!root.available || !isLowAndNotCharging) return; + Quickshell.execDetached([ "notify-send", Translation.tr("Low battery"), Translation.tr("Consider plugging in your device"), "-u", "critical", "-a", "Shell" ]) + + if (root.soundEnabled) Audio.playSystemSound("dialog-warning"); } onIsCriticalAndNotChargingChanged: { - if (available && isCriticalAndNotCharging) Quickshell.execDetached([ + if (!root.available || !isCriticalAndNotCharging) return; + Quickshell.execDetached([ "notify-send", Translation.tr("Critically low battery"), - Translation.tr("Please charge!\nAutomatic suspend triggers at %1").arg(Config.options.battery.suspend), + Translation.tr("Please charge!\nAutomatic suspend triggers at %1%").arg(Config.options.battery.suspend), "-u", "critical", "-a", "Shell" ]); - + + if (root.soundEnabled) Audio.playSystemSound("suspend-error"); } onIsSuspendingAndNotChargingChanged: { - if (available && isSuspendingAndNotCharging) { + if (root.available && isSuspendingAndNotCharging) { Quickshell.execDetached(["bash", "-c", `systemctl suspend || loginctl suspend`]); } } + + onIsFullAndChargingChanged: { + if (!root.available || !isFullAndCharging) return; + Quickshell.execDetached([ + "notify-send", + Translation.tr("Battery full"), + Translation.tr("Please unplug the charger"), + "-a", "Shell" + ]); + + if (root.soundEnabled) Audio.playSystemSound("complete"); + } + + onIsPluggedInChanged: { + if (!root.available || !root.soundEnabled) return; + if (isPluggedIn) { + Audio.playSystemSound("power-plug") + } else { + Audio.playSystemSound("power-unplug") + } + } } diff --git a/dots/.config/quickshell/ii/services/TimerService.qml b/dots/.config/quickshell/ii/services/TimerService.qml index a75893681..69e5fe1c1 100644 --- a/dots/.config/quickshell/ii/services/TimerService.qml +++ b/dots/.config/quickshell/ii/services/TimerService.qml @@ -1,6 +1,7 @@ pragma Singleton pragma ComponentBehavior: Bound +import qs.services import qs.modules.common import Quickshell @@ -17,7 +18,6 @@ Singleton { property int breakTime: Config.options.time.pomodoro.breakTime property int longBreakTime: Config.options.time.pomodoro.longBreak property int cyclesBeforeLongBreak: Config.options.time.pomodoro.cyclesBeforeLongBreak - property string alertSound: Config.options.time.pomodoro.alertSound property bool pomodoroRunning: Persistent.states.timer.pomodoro.running property bool pomodoroBreak: Persistent.states.timer.pomodoro.isBreak @@ -64,8 +64,9 @@ Singleton { } Quickshell.execDetached(["notify-send", "Pomodoro", notificationMessage, "-a", "Shell"]); - if (alertSound) - Quickshell.execDetached(["ffplay", "-nodisp", "-autoexit", alertSound]); + if (Config.options.sounds.pomodoro) { + Audio.playSystemSound("alarm-clock-elapsed") + } if (!pomodoroBreak) { Persistent.states.timer.pomodoro.cycle = (Persistent.states.timer.pomodoro.cycle + 1) % root.cyclesBeforeLongBreak;