theme: write desktop settings through dconf instead of gsettings

fixes #3280
port 1.6
This commit is contained in:
bbedward committed 2026-09-04 10:37:31 -04:00
1 parent d57e32c5cb
commit a665ba8ca5
9 files changed
+185 -80

No files matched your search

+23 -9
View File
@@ -2,6 +2,7 @@ package utils
import (
"fmt"
"os"
"os/exec"
"strings"
)
@@ -10,22 +11,35 @@ func dconfPath(schema, key string) string {
return "/" + strings.ReplaceAll(schema, ".", "/") + "/" + key
}
// GsettingsGet reads a gsettings value, falling back to dconf read.
// gsettings reaches whichever backend its own GLib links, not necessarily the
// dconf store GTK apps read, and it exits 0 either way.
func dconfSelected() bool {
backend := os.Getenv("GSETTINGS_BACKEND")
return backend == "" || backend == "dconf"
}
// GsettingsGet reads a desktop setting from dconf, falling back to gsettings.
func GsettingsGet(schema, key string) (string, error) {
if out, err := exec.Command("gsettings", "get", schema, key).Output(); err == nil {
return strings.TrimSpace(string(out)), nil
if dconfSelected() {
if out, err := exec.Command("dconf", "read", dconfPath(schema, key)).Output(); err == nil {
if value := strings.TrimSpace(string(out)); value != "" {
return value, nil
}
}
}
out, err := exec.Command("dconf", "read", dconfPath(schema, key)).Output()
out, err := exec.Command("gsettings", "get", schema, key).Output()
if err != nil {
return "", fmt.Errorf("gsettings/dconf get failed for %s %s: %w", schema, key, err)
return "", fmt.Errorf("dconf/gsettings get failed for %s %s: %w", schema, key, err)
}
return strings.TrimSpace(string(out)), nil
}
// GsettingsSet writes a gsettings value, falling back to dconf write.
// GsettingsSet writes a desktop setting to dconf, falling back to gsettings.
func GsettingsSet(schema, key, value string) error {
if err := exec.Command("gsettings", "set", schema, key, value).Run(); err == nil {
return nil
if dconfSelected() {
if err := exec.Command("dconf", "write", dconfPath(schema, key), "'"+value+"'").Run(); err == nil {
return nil
}
}
return exec.Command("dconf", "write", dconfPath(schema, key), "'"+value+"'").Run()
return exec.Command("gsettings", "set", schema, key, value).Run()
}
+117
View File
@@ -0,0 +1,117 @@
package utils
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeTool struct {
name string
stdout string
exit int
}
func fakeTools(t *testing.T, tools ...fakeTool) string {
t.Helper()
binDir := t.TempDir()
logPath := filepath.Join(binDir, "calls.log")
for _, tool := range tools {
script := fmt.Sprintf("#!/bin/sh\necho \"%s $*\" >> %q\nprintf '%%s' %q\nexit %d\n", tool.name, logPath, tool.stdout, tool.exit)
require.NoError(t, os.WriteFile(filepath.Join(binDir, tool.name), []byte(script), 0o755))
}
t.Setenv("PATH", binDir)
return logPath
}
func toolCalls(t *testing.T, logPath string) string {
t.Helper()
data, err := os.ReadFile(logPath)
if os.IsNotExist(err) {
return ""
}
require.NoError(t, err)
return string(data)
}
func TestGsettingsSetWritesDconf(t *testing.T) {
t.Setenv("GSETTINGS_BACKEND", "")
logPath := fakeTools(t,
fakeTool{name: "dconf"},
fakeTool{name: "gsettings"},
)
require.NoError(t, GsettingsSet("org.gnome.desktop.interface", "gtk-theme", "adw-gtk3"))
calls := toolCalls(t, logPath)
assert.Contains(t, calls, "dconf write /org/gnome/desktop/interface/gtk-theme 'adw-gtk3'")
assert.NotContains(t, calls, "gsettings")
}
func TestGsettingsSetFallsBackWhenDconfFails(t *testing.T) {
t.Setenv("GSETTINGS_BACKEND", "")
logPath := fakeTools(t,
fakeTool{name: "dconf", exit: 1},
fakeTool{name: "gsettings"},
)
require.NoError(t, GsettingsSet("org.gnome.desktop.interface", "gtk-theme", "adw-gtk3"))
assert.Contains(t, toolCalls(t, logPath), "gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3")
}
func TestGsettingsSetHonorsBackendOverride(t *testing.T) {
t.Setenv("GSETTINGS_BACKEND", "keyfile")
logPath := fakeTools(t,
fakeTool{name: "dconf"},
fakeTool{name: "gsettings"},
)
require.NoError(t, GsettingsSet("org.gnome.desktop.interface", "gtk-theme", "adw-gtk3"))
calls := toolCalls(t, logPath)
assert.Contains(t, calls, "gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3")
assert.NotContains(t, calls, "dconf write")
}
func TestGsettingsGetReadsDconf(t *testing.T) {
t.Setenv("GSETTINGS_BACKEND", "")
fakeTools(t,
fakeTool{name: "dconf", stdout: "'adw-gtk3-dark'"},
fakeTool{name: "gsettings", stdout: "'Adwaita'"},
)
value, err := GsettingsGet("org.gnome.desktop.interface", "gtk-theme")
require.NoError(t, err)
assert.Equal(t, "'adw-gtk3-dark'", value)
}
func TestGsettingsGetFallsBackWhenDconfUnset(t *testing.T) {
t.Setenv("GSETTINGS_BACKEND", "")
fakeTools(t,
fakeTool{name: "dconf"},
fakeTool{name: "gsettings", stdout: "'Adwaita'"},
)
value, err := GsettingsGet("org.gnome.desktop.interface", "gtk-theme")
require.NoError(t, err)
assert.Equal(t, "'Adwaita'", value)
}
func TestGsettingsGetFailsWhenNoToolWorks(t *testing.T) {
t.Setenv("GSETTINGS_BACKEND", "")
fakeTools(t,
fakeTool{name: "dconf", exit: 1},
fakeTool{name: "gsettings", exit: 1},
)
_, err := GsettingsGet("org.gnome.desktop.interface", "gtk-theme")
require.ErrorContains(t, err, "gtk-theme")
}
+19
View File
@@ -0,0 +1,19 @@
.pragma library
function path(schema, key) {
return "/" + schema.replace(/\./g, "/") + "/" + key;
}
// gsettings reaches whichever backend its own GLib links, not necessarily the
// dconf store GTK apps read, and it exits 0 either way.
function backendIsDconf() {
return "[ \"${GSETTINGS_BACKEND:-dconf}\" = dconf ]";
}
function getCmd(schema, key) {
return "{ { " + backendIsDconf() + " && dconf read " + path(schema, key) + " 2>/dev/null | grep .; } || gsettings get " + schema + " " + key + " 2>/dev/null; } | tr -d \"'\"";
}
function setCmd(schema, key, value) {
return "{ " + backendIsDconf() + " && dconf write " + path(schema, key) + " \"'" + value + "'\" 2>/dev/null; } || gsettings set " + schema + " " + key + " '" + value + "'";
}
+6 -17
View File
@@ -8,6 +8,7 @@ import Quickshell.Io
import qs.Common
import qs.Common.settings
import qs.Services
import "GSettings.js" as GSettings
import "settings/SettingsSpec.js" as Spec
import "settings/SettingsStore.js" as Store
@@ -1524,11 +1525,7 @@ Singleton {
return;
if (!SessionData.lastAppliedIconTheme)
return;
const script = `if command -v gsettings >/dev/null 2>&1; then
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
elif command -v dconf >/dev/null 2>&1; then
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
fi`;
const script = GSettings.getCmd("org.gnome.desktop.interface", "icon-theme");
Proc.runCommand("iconThemeDriftCheck", ["sh", "-c", script], (output, exitCode) => {
const platform = (output || "").trim();
@@ -1566,11 +1563,7 @@ Singleton {
const resolved = resolveIconTheme();
let cosmicThemeName = (resolved === "System Default") ? systemDefaultIconTheme : resolved;
if (!cosmicThemeName || cosmicThemeName === "System Default") {
const detectScript = `if command -v gsettings >/dev/null 2>&1; then
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
elif command -v dconf >/dev/null 2>&1; then
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
fi`;
const detectScript = GSettings.getCmd("org.gnome.desktop.interface", "icon-theme");
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
if (exitCode !== 0)
@@ -1632,11 +1625,7 @@ Singleton {
fi
done
if command -v gsettings >/dev/null 2>&1; then
gsettings set org.gnome.desktop.interface icon-theme '${gtkThemeName}' 2>/dev/null || true
elif command -v dconf >/dev/null 2>&1; then
dconf write /org/gnome/desktop/interface/icon-theme "'${gtkThemeName}'" 2>/dev/null || true
fi
${GSettings.setCmd("org.gnome.desktop.interface", "icon-theme", gtkThemeName)} || true
pkill -HUP -f 'gtk' 2>/dev/null || true`;
@@ -2132,7 +2121,7 @@ Singleton {
const pathsArg = iconPaths.join(" ");
const script = `
echo "SYSDEFAULT:$(gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g" || echo '')"
echo "SYSDEFAULT:$(${GSettings.getCmd("org.gnome.desktop.interface", "icon-theme")})"
for dir in ${pathsArg}; do
[ -d "$dir" ] || continue
for theme in "$dir"/*/; do
@@ -2171,7 +2160,7 @@ Singleton {
const pathsArg = cursorPaths.join(" ");
const script = `
echo "SYSDEFAULT:$(gsettings get org.gnome.desktop.interface cursor-theme 2>/dev/null | sed "s/'//g" || echo '')"
echo "SYSDEFAULT:$(${GSettings.getCmd("org.gnome.desktop.interface", "cursor-theme")})"
for dir in ${pathsArg}; do
[ -d "$dir" ] || continue
for theme in "$dir"/*/; do
+5 -15
View File
@@ -10,6 +10,7 @@ import qs.DankCommon.Common as DankCommon
import qs.Services
import qs.Modules.Greetd
import "StockThemes.js" as StockThemes
import "GSettings.js" as GSettings
Singleton {
id: root
@@ -1918,23 +1919,12 @@ Singleton {
const theme = isLight ? "adw-gtk3" : "adw-gtk3-dark";
const schema = "org.gnome.desktop.interface";
const key = "gtk-theme";
const reset = GSettings.setCmd(schema, key, "");
const apply = GSettings.setCmd(schema, key, theme);
const makeCmd = (tool, schema, val) => {
if (tool === "gsettings") {
return `gsettings set ${schema} ${key} '' && gsettings set ${schema} ${key} ${val}`;
} else {
const dconfPath = `/${schema.replace(/\./g, "/")}`;
return `dconf write ${dconfPath}/${key} "''" && dconf write ${dconfPath}/${key} "'${val}'"`;
}
};
Proc.runCommand("gtkRefresher", ["sh", "-c", makeCmd("gsettings", schema, theme)], (output, exitCode) => {
Proc.runCommand("gtkRefresher", ["sh", "-c", `${reset}; ${apply}`], (output, exitCode) => {
if (exitCode !== 0) {
Proc.runCommand("gtkRefreshFallback", ["sh", "-c", makeCmd("dconf", schema, theme)], (output, exitCode) => {
if (exitCode !== 0) {
log.warn("Failed to refresh gtk-theme");
}
});
log.warn("Failed to refresh gtk-theme");
}
});
}
+2 -2
View File
@@ -91,7 +91,7 @@ Item {
tab: "sounds"
tags: ["sound", "theme", "system"]
settingKey: "useSystemSoundTheme"
visible: AudioService.gsettingsAvailable
visible: AudioService.soundThemeSupported
text: I18n.tr("Use System Theme")
description: I18n.tr("Use sound theme from system settings")
checked: SettingsData.useSystemSoundTheme
@@ -123,7 +123,7 @@ Item {
height: 1
color: Theme.outline
opacity: 0.2
visible: AudioService.gsettingsAvailable
visible: AudioService.soundThemeSupported
}
SettingsToggleRow {
+10 -9
View File
@@ -8,6 +8,7 @@ import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import "../Common/GSettings.js" as GSettings
Singleton {
id: root
@@ -18,7 +19,7 @@ Singleton {
readonly property bool soundsAvailable: MultimediaService.available
property bool playersRequested: false
property bool gsettingsAvailable: false
property bool soundThemeSupported: false
property var availableSoundThemes: []
property string currentSoundTheme: ""
property var soundFilePaths: ({})
@@ -545,10 +546,10 @@ EOFCONFIG
}
}
function checkGsettings() {
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
gsettingsAvailable = (exitCode === 0);
if (gsettingsAvailable) {
function checkSoundThemeSupport() {
Proc.runCommand("checkSoundThemeSupport", ["sh", "-c", GSettings.getCmd("org.gnome.desktop.sound", "theme-name")], (output, exitCode) => {
soundThemeSupported = (output || "").trim().length > 0;
if (soundThemeSupported) {
scanSoundThemes();
getCurrentSoundTheme();
}
@@ -581,8 +582,8 @@ EOFCONFIG
}
function getCurrentSoundTheme() {
Proc.runCommand("getCurrentSoundTheme", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null | sed \"s/'//g\""], (output, exitCode) => {
if (exitCode === 0 && output.trim()) {
Proc.runCommand("getCurrentSoundTheme", ["sh", "-c", GSettings.getCmd("org.gnome.desktop.sound", "theme-name")], (output, exitCode) => {
if (output.trim()) {
currentSoundTheme = output.trim();
log.debug("Current system sound theme:", currentSoundTheme);
if (SettingsData.useSystemSoundTheme) {
@@ -600,7 +601,7 @@ EOFCONFIG
return;
}
Proc.runCommand("setSoundTheme", ["sh", "-c", `gsettings set org.gnome.desktop.sound theme-name '${themeName}'`], (output, exitCode) => {
Proc.runCommand("setSoundTheme", ["sh", "-c", GSettings.setCmd("org.gnome.desktop.sound", "theme-name", themeName)], (output, exitCode) => {
if (exitCode === 0) {
currentSoundTheme = themeName;
if (SettingsData.useSystemSoundTheme) {
@@ -1171,7 +1172,7 @@ EOFCONFIG
onSoundsAvailableChanged: {
if (!soundsAvailable)
return;
checkGsettings();
checkSoundThemeSupport();
}
Component.onCompleted: {
+1 -1
View File
@@ -84,7 +84,7 @@ Singleton {
if (Quickshell.env("QS_ICON_THEME"))
return;
const script = `v=$(sed -n 's/^gtk-icon-theme-name *= *//p' "\${XDG_CONFIG_HOME:-$HOME/.config}/gtk-3.0/settings.ini" 2>/dev/null | head -1)
[ -z "$v" ] && command -v gsettings >/dev/null 2>&1 && v=$(gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null)
[ -z "$v" ] && v=$(${GSettings.getCmd("org.gnome.desktop.interface", "icon-theme")})
printf '%s' "$v" | tr -d "'\\""`;
Proc.runCommand("iconThemeProbe", ["sh", "-c", script], (out, code) => {
+2 -27
View File
@@ -6,6 +6,7 @@ import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "../Common/GSettings.js" as GSettings
Singleton {
id: root
@@ -19,7 +20,6 @@ Singleton {
property bool colorSchemeInitialized: false
property bool freedeskAvailable: false
property string colorSchemeCommand: ""
property string pendingProfileImage: ""
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -146,14 +146,7 @@ Singleton {
const preferLight = isLightMode && systemColorScheme === 2;
const targetScheme = isLightMode ? (preferLight ? "prefer-light" : "default") : "prefer-dark";
switch (colorSchemeCommand) {
case "gsettings":
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", targetScheme]);
break;
case "dconf":
Quickshell.execDetached(["dconf", "write", "/org/gnome/desktop/interface/color-scheme", `'${targetScheme}'`]);
break;
}
Quickshell.execDetached(["sh", "-c", GSettings.setCmd("org.gnome.desktop.interface", "color-scheme", targetScheme)]);
}
function setSystemIconTheme(themeName) {
@@ -207,7 +200,6 @@ Singleton {
} else {
log.info("DMS_SOCKET not set");
}
colorSchemeDetector.running = true;
}
Connections {
@@ -348,23 +340,6 @@ Singleton {
}
}
Process {
id: colorSchemeDetector
command: ["bash", "-c", "command -v gsettings || command -v dconf"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const cmd = text.trim();
if (cmd.includes("gsettings")) {
root.colorSchemeCommand = "gsettings";
} else if (cmd.includes("dconf")) {
root.colorSchemeCommand = "dconf";
}
}
}
}
IpcHandler {
target: "profile"