Add idle timeouts to the Quick Settings General tab

This commit is contained in:
SirBlob committed 2026-09-24 12:49:45 -04:00
1 parent 1f86bfd151
commit 4f2d319a35
4 files changed
+182 -16

No files matched your search

+9 -2
View File
@@ -9,7 +9,7 @@ autostarted.
| Was | Is | Note |
| --- | --- | --- |
| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 13 files, none over 350 lines, with four tabs |
| `QuickSettings.tsx` (523 lines) | `shell/plugins/panels/quick-settings/` | split across 15 files, none over 350 lines, with four tabs |
| `SysMonitor.tsx` (142) | `shell/plugins/panels/sysmon/` | CPU, memory, disk, temperature; the probes and bars are `Ui/SystemStats.qml` and `Ui/MetricBar.qml`, shared with the Performance tab |
| `Notifications.tsx` (160) | `shell/plugins/notification-center/` | reads the notification service's own history files |
| `WallPicker.tsx` (106) | the existing image picker | `blob-wallpaper-set --menu` now calls `blob-menu-images`, so wallpapers get the image grid instead of a text list |
@@ -41,13 +41,20 @@ running `pamixer` and `playerctl` every second.
| Tab | Holds |
| --- | --- |
| General | the calendar and the brightness slider |
| General | the calendar, the brightness slider, and the screensaver and lock timeouts |
| Sound & Music | output and microphone volume (click the icon to mute), the media row with its controls, and buttons for the audio panel (picking devices) and the pavucontrol mixer |
| Performance | CPU, memory, disk and temperature bars, the load average, the power profile (Power saver, Balanced, Performance, whichever the machine offers), and a button that opens btop |
| Quick Settings | the toggle tiles: Wi-Fi, Bluetooth, Wallpaper, Silence, Night Light, Record, Stay Awake, Pick Color, Theme, Tablet Lock |
The weather card beside it is not part of the tabs and stays visible.
The two timeouts on the General tab are the `idle.screensaver` and `idle.lock`
values in `~/.config/blob/shell.json`, in seconds. The minus and plus buttons
step through 1, 2, 5, 10, 15, 30 and 60 minutes (and 2 hours for the lock),
starting from whatever the file holds, and the idle service picks the new value
up straight away. A value set by hand that is not a preset is shown as it is.
While Stay Awake is on, the tab says so, since neither timeout runs then.
## Opening them
| Panel | Opened by |
@@ -0,0 +1,59 @@
import QtQuick
import qs.Commons
Column {
id: root
property bool polling: false
property int brightness: 0
property int screensaverSeconds: 0
property int lockSeconds: 0
property bool stayAwake: false
signal brightnessRequested(int value)
signal timeoutRequested(string key, int seconds)
spacing: Style.spaceReal(12)
Calendar {
width: parent.width
polling: root.polling
}
SliderRow {
width: parent.width
icon: ""
value: root.brightness
onValueRequested: function(next) { root.brightnessRequested(next) }
}
TimeoutRow {
width: parent.width
icon: ""
label: "Screensaver after"
seconds: root.screensaverSeconds
presetMinutes: [1, 2, 5, 10, 15, 30, 60]
onSecondsRequested: function(seconds) { root.timeoutRequested("screensaver", seconds) }
}
TimeoutRow {
width: parent.width
icon: ""
label: "Lock after"
seconds: root.lockSeconds
presetMinutes: [1, 2, 5, 10, 15, 30, 60, 120]
onSecondsRequested: function(seconds) { root.timeoutRequested("lock", seconds) }
}
Text {
width: parent.width
visible: root.stayAwake
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
text: "Stay Awake is on, so neither starts until it is turned off."
textFormat: Text.PlainText
color: Util.alpha(Color.foreground, 0.6)
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
}
@@ -27,6 +27,11 @@ Item {
readonly property bool performanceVisible: opened && currentTab === "performance"
readonly property bool quickVisible: opened && currentTab === "quick"
readonly property var idleConfig: root.shell && root.shell.shellConfig && root.shell.shellConfig.idle
? root.shell.shellConfig.idle : ({})
readonly property int screensaverSeconds: root.timeoutSeconds(idleConfig.screensaver, 150)
readonly property int lockSeconds: root.timeoutSeconds(idleConfig.lock, 300)
readonly property int barInset: {
var bar = root.shell ? root.shell.bar : null
if (!bar || bar.barHidden === true) return 0
@@ -78,6 +83,19 @@ Item {
mediaValue.refresh()
}
function timeoutSeconds(value, fallback) {
var seconds = Number(value)
return isFinite(seconds) && seconds >= 0 ? Math.floor(seconds) : fallback
}
function setIdleTimeout(key, seconds) {
if (!root.shell || typeof root.shell.mutateShellConfig !== "function") return
root.shell.mutateShellConfig(function(config) {
if (!config.idle || typeof config.idle !== "object") config.idle = {}
config.idle[key] = seconds
})
}
function toggleAndRefresh(command) {
root.run(command)
stateSettleTimer.restart()
@@ -109,7 +127,7 @@ Item {
PolledFlag {
id: stayAwakeFlag
polling: root.quickVisible
polling: root.quickVisible || root.generalVisible
command: "blob-toggle-idle status 2>/dev/null | grep -q '\"enabled\":true' && echo on || echo off"
}
@@ -256,25 +274,19 @@ Item {
onTabRequested: function(tabId) { root.currentTab = tabId }
}
Column {
GeneralTab {
width: parent.width
visible: root.currentTab === "general"
spacing: Style.spaceReal(12)
Calendar {
width: parent.width
polling: root.generalVisible
}
SliderRow {
width: parent.width
icon: "\uf185"
value: Number(brightnessValue.value) || 0
onValueRequested: function(next) {
brightness: Number(brightnessValue.value) || 0
screensaverSeconds: root.screensaverSeconds
lockSeconds: root.lockSeconds
stayAwake: stayAwakeFlag.value
onBrightnessRequested: function(next) {
root.run("brightnessctl set " + next + "%")
brightnessValue.value = String(next)
}
}
onTimeoutRequested: function(key, seconds) { root.setIdleTimeout(key, seconds) }
}
SoundTab {
@@ -0,0 +1,88 @@
import QtQuick
import qs.Commons
import qs.Ui
Item {
id: root
property string icon: ""
property string label: ""
property int seconds: 0
property var presetMinutes: []
signal secondsRequested(int seconds)
implicitHeight: Math.max(labelText.implicitHeight, controls.implicitHeight)
function formatSeconds(value) {
if (value >= 3600 && value % 3600 === 0) return (value / 3600) + " h"
if (value % 60 === 0) return (value / 60) + " min"
return (Math.round(value / 6) / 10) + " min"
}
function step(delta) {
var minutes = root.seconds / 60
var presets = root.presetMinutes
var next = -1
if (delta > 0) {
for (var i = 0; i < presets.length; i++)
if (presets[i] > minutes) { next = presets[i]; break }
} else {
for (var j = presets.length - 1; j >= 0; j--)
if (presets[j] < minutes) { next = presets[j]; break }
}
if (next > 0) root.secondsRequested(next * 60)
}
Text {
id: iconText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Style.spaceReal(22)
text: root.icon
textFormat: Text.PlainText
color: Color.magenta
font.family: Style.font.family
font.pixelSize: Style.font.body
}
Text {
id: labelText
anchors.left: iconText.right
anchors.leftMargin: Style.spaceReal(10)
anchors.verticalCenter: parent.verticalCenter
text: root.label
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
Row {
id: controls
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Style.spaceReal(8)
CardIconButton {
icon: "-"
onActivated: root.step(-1)
}
Text {
anchors.verticalCenter: parent.verticalCenter
width: Style.spaceReal(52)
horizontalAlignment: Text.AlignHCenter
text: root.formatSeconds(root.seconds)
textFormat: Text.PlainText
color: Color.foreground
font.family: Style.font.family
font.pixelSize: Style.font.bodySmall
}
CardIconButton {
icon: "+"
onActivated: root.step(1)
}
}
}