The Problem
Users need a graphical way to configure their shell — change themes, enable/disable widgets, adjust panel height, rebind shortcuts. Editing QML files directly is intimidating for non-developers.
The Naive Approach
Edit config files in a text editor:
vim ~/.config/quickshell/config.qmlEvery typo breaks the shell. Users must know QML syntax just to change a color.
Think of the settings app as the control panel for your shell's engine. Instead of opening the hood and tweaking components directly, you have a dashboard with knobs and switches. Each knob has a label and range, so you can't accidentally break the engine.
The Idea
Create a FloatingWindow or PopupWindow that reads/writes a JSON config file. Each setting is a form control: toggle switches for booleans, sliders for numbers, color pickers for colors, dropdowns for choices.
Let's Build It
// SettingsApp.qml
FloatingWindow {
id: settings
title: "Shell Settings"
width: 500; height: 600
visible: false
property var config: ({})
property bool dirty: false
function loadConfig() {
var f = Qt.open(Quickshell.configPath("settings.json"), "r")
if (f.valid) {
try { config = JSON.parse(f.readAll()) } catch (e) { config = {} }
}
}
function saveConfig() {
Qt.writeFile(Quickshell.configPath("settings.json"), JSON.stringify(config, null, 2))
dirty = false
}
function setSetting(key, value) {
config[key] = value
dirty = true
}
Component.onCompleted: loadConfig()
Rectangle {
anchors.fill: parent; color: "#1e1e2e"
Column {
anchors { fill: parent; margins: 16 }; spacing: 16
Text { text: "Shell Settings"; font.pixelSize: 18; font.bold: true; color: "#cdd6f4" }
// Theme selector
SettingsSection {
label: "Theme"
Row {
spacing: 8
Repeater {
model: ["catppuccin", "dracula", "nord"]
delegate: Rectangle {
required property string modelData
width: 80; height: 28; radius: 4
color: config["theme"] === modelData ? "#89b4fa" : "#313244"
Text { anchors.centerIn: parent; text: modelData; color: "white"; font.pixelSize: 11 }
MouseArea {
anchors.fill: parent
onClicked: setSetting("theme", modelData)
}
}
}
}
}
// Panel height
SettingsSection {
label: "Panel Height"
Row {
spacing: 12
Slider {
id: heightSlider
width: 200
from: 24; to: 64; stepSize: 2
value: config["panelHeight"] || 36
onValueChanged: config["panelHeight"] = value
}
Text {
text: Math.round(heightSlider.value) + "px"
color: "#a6adc8"; font.pixelSize: 12
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Panel position
SettingsSection {
label: "Panel Position"
Row {
spacing: 8
Repeater {
model: ["top", "bottom"]
delegate: Rectangle {
required property string modelData
width: 80; height: 28; radius: 4
color: config["panelPosition"] === modelData ? "#89b4fa" : "#313244"
Text { anchors.centerIn: parent; text: modelData; color: "white"; font.pixelSize: 11 }
MouseArea {
anchors.fill: parent
onClicked: setSetting("panelPosition", modelData)
}
}
}
}
}
// Widget toggles
SettingsSection {
label: "Widgets"
Column {
spacing: 4
Repeater {
model: ["Clock", "CPU", "RAM", "Battery", "Network", "Volume", "Weather", "System Tray"]
delegate: Rectangle {
required property string modelData
width: 200; height: 28; radius: 4; color: config["show" + modelData.replace(" ", "")] !== false ? "#313244" : "#45475a"
Row {
anchors { fill: parent; leftMargin: 8; rightMargin: 8 }; spacing: 8
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 14; height: 14; radius: 3
color: config["show" + modelData.replace(" ", "")] !== false ? "#a6e3a1" : "#f38ba8"
anchors.verticalCenter: parent.verticalCenter
}
Text { text: modelData; color: "#cdd6f4"; font.pixelSize: 12; anchors.verticalCenter: parent.verticalCenter }
}
MouseArea {
anchors.fill: parent
onClicked: {
var key = "show" + modelData.replace(" ", "")
config[key] = config[key] !== false ? false : true
}
}
}
}
}
}
// Save / Cancel
Row {
width: parent.width; spacing: 8; anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: 100; height: 36; radius: 6; color: "#89b4fa"
Text { anchors.centerIn: parent; text: "Apply"; color: "white"; font.pixelSize: 13; font.bold: true }
MouseArea { anchors.fill: parent; onClicked: { saveConfig(); Quickshell.reload() } }
}
Rectangle {
width: 100; height: 36; radius: 6; color: "#313244"
Text { anchors.centerIn: parent; text: "Cancel"; color: "#cdd6f4"; font.pixelSize: 13 }
MouseArea { anchors.fill: parent; onClicked: settings.visible = false }
}
}
}
}
}Helper component:
// SettingsSection.qml
Column {
property string label: ""
spacing: 8
Text { text: label; font.pixelSize: 13; font.bold: true; color: "#cdd6f4" }
// Children go here
}The settings app reads/writes settings.json. It provides toggles for each widget, sliders for panel height, and theme selection. Changes are applied on "Apply" which triggers Quickshell.reload().
Let's Improve It
Add a color picker for accent colors and a keyboard shortcut rebinding interface:
Rectangle {
width: 200; height: 28; radius: 4; color: config["accentColor"] || "#89b4fa"
MouseArea {
anchors.fill: parent
onClicked: {
// Open a color picker popup that updates config["accentColor"]
}
}
}Not validating input. A slider that allows negative panel height breaks the shell. Always use from, to, and stepSize on Slider. For text input, parse and validate before saving.
Triggering reload unnecessarily. Only call Quickshell.reload() when settings that require it change. Theme, panel height, and widget visibility need a reload. OSD timeout, for example, can be applied dynamically without a reload.
Not providing defaults. The settings app should fall back to hardcoded defaults for any key not present in settings.json. Use config[key] || defaultConfig[key] in your shell components.
Under the Hood
The settings app reads configuration from a JSON file. JSON was chosen over QML for user-facing config because it's easier to edit programmatically (no QML syntax) and safer (can't inject malicious code). The Quickshell.reload() method re-evaluates your shell.qml, preserving state paths but re-creating all QML objects.
Exercises
- Settings app provides graphical control over shell configuration
- Read/write JSON settings file — easy to parse, safe to modify
- Apply triggers Quickshell.reload() for reloadable settings
- Use form controls: sliders, toggles, dropdowns, color pickers