Make Shell a category, the bar legible, and the dock a real dock

Desktop & Dock becomes Shell — Bar, Dock, Control Center, Tiling,
Workspaces — the home for everything Quickshell draws. The settings-
management cluster moves to System as Sync & Backup, Appearance's
Shell tab dissolves, and 24-hour time finally lives on Date & Time,
which always owned it.

The bar gets what it never had: a way to survive the wallpaper. A
second neutral text family (follow theme, or forced light or dark),
a one-layer shadow under every glyph, and a gradient scrim for
wallpapers nothing else survives — all off by default, pixel-identical
until asked. Widgets earn toggles (weather, media, clipboard, calendar
countdown), the vitals cluster stops leaving a dead pill behind, and
Control Center's sections learn to step aside.

The dock graduates from MVP: a context menu with window rows, pin,
unpin, quit and new-window; scroll an icon to cycle its windows; drag
to reorder on the dock itself; hover previews with one-shot captures;
and "Add App to Dock" in the launcher. Three real bugs died en route —
menus that slid away with the autohide, a readonly-property crash on
every menu open, and a drag that drifted half a slot per icon on side
docks. The pinned-apps editor in Settings becomes a drag strip.

166 contracts; the full suite is green except two live display and
switcher tests that cannot run behind a locked session — re-verified
on unlock.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 04:28:20 -04:00
parent 4d7a194300
commit f8f5b25510
77 changed files with 2982 additions and 800 deletions
@@ -36,7 +36,7 @@ Pill {
anchors.verticalCenter: parent.verticalCenter
visible: PrivacyState.activeKinds.length > 1
text: String(PrivacyState.activeKinds.length)
color: Theme.fg
color: Theme.barFg
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
@@ -27,8 +27,8 @@ Pill {
visible: Settings.showAgentUsage && AgentUsage.available
onActivated: ShellState.openSettings("appearance")
onSecondaryActivated: ShellState.openSettings("appearance")
onActivated: ShellState.openSettings("bar")
onSecondaryActivated: ShellState.openSettings("bar")
Row {
spacing: 4
@@ -39,7 +39,7 @@ Pill {
color: {
if (AgentUsage.headline >= 90) return Theme.danger;
if (AgentUsage.headline >= 75) return Theme.warn;
return Theme.fgDim;
return Theme.barFgDim;
}
font.family: Theme.fontMono
font.pixelSize: Theme.fontSize
@@ -48,7 +48,7 @@ Pill {
Text {
anchors.verticalCenter: parent.verticalCenter
text: AgentUsage.headline + "%"
color: Theme.fg
color: Theme.barFg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.features: Theme.tabularFigures
+35 -1
View File
@@ -16,6 +16,7 @@
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Effects
import qs.config
import qs.modules.clipboard
import qs.modules.focus
@@ -62,13 +63,46 @@ PanelWindow {
item: barContent
}
// A scrim for wallpapers the bar cannot win against on tone alone. Drawn
// behind the content rows, fading out downward so the bar still has no
// hard edge of its own — the point is to darken what is under the text,
// not to give the bar a surface. Static: a gradient that never changes
// costs one paint, and this one has nothing to animate.
Rectangle {
anchors.fill: parent
visible: Settings.barBackdrop
gradient: Gradient {
GradientStop {
position: 0.0
color: Theme.alpha("#0c0e18", 0.55)
}
GradientStop {
position: 1.0
color: "transparent"
}
}
}
Item {
id: barContent
anchors.fill: parent
// One shadow for the whole bar rather than one per widget: the content
// is flattened into a single layer and the halo is drawn under it, so
// every glyph and label picks it up and nothing has to opt in. Off by
// default — it only earns its layer on a busy wallpaper.
layer.enabled: Settings.barTextShadow
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: "#0a0c14"
shadowBlur: 0.6
shadowVerticalOffset: 1
}
// ── Left ────────────────────────────────────────────────────────────
Row {
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.itemSpacing
@@ -10,7 +10,7 @@ import qs.widgets
Pill {
id: root
visible: CalendarAgenda.capsuleVisible
visible: Settings.showCalendarCountdown && CalendarAgenda.capsuleVisible
opacity: visible ? 1 : 0
horizontalPadding: 8
onActivated: ShellState.openDateMenu("agenda")
@@ -47,7 +47,7 @@ Pill {
Text {
anchors.verticalCenter: parent.verticalCenter
text: CalendarAgenda.capsuleText
color: Theme.fg
color: Theme.barFg
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
+1 -1
View File
@@ -39,7 +39,7 @@ Pill {
// figures the whole clock shifts sideways on every digit change.
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSize
color: Theme.fg
color: Theme.barFg
}
}
@@ -29,7 +29,7 @@ Pill {
return artist ? artist + " — " + title : title;
}
visible: root.player !== null
visible: Settings.showMediaWidget && root.player !== null
horizontalPadding: 8
onActivated: if (root.player?.canTogglePlaying)
@@ -63,7 +63,7 @@ Pill {
text: root.label
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
color: Theme.fg
color: Theme.barFg
// Titles are unbounded; the bar is not. Elide rather than let one
// podcast episode push the tray off the edge.
@@ -77,10 +77,10 @@ Pill {
color: {
if (root.wiredDevice)
return Theme.fg;
return Theme.barFg;
if (!root.wifiNetwork)
return Theme.fgMuted;
return Networking.connectivity === NetworkConnectivity.Full ? Theme.fg : Theme.warn;
return Theme.barFgMuted;
return Networking.connectivity === NetworkConnectivity.Full ? Theme.barFg : Theme.warn;
}
}
@@ -91,7 +91,7 @@ Pill {
visible: KeyboardLayout.multiple
anchors.verticalCenter: parent.verticalCenter
text: KeyboardLayout.shortLabel
color: Theme.fg
color: Theme.barFg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
@@ -117,7 +117,7 @@ Pill {
return "\u{F0580}"; // md-volume_medium
return "\u{F057E}"; // md-volume_high
}
color: root.muted ? Theme.fgMuted : Theme.fg
color: root.muted ? Theme.barFgMuted : Theme.barFg
}
StatusGlyph {
@@ -125,7 +125,7 @@ Pill {
// simply absent rather than shown crossed out.
visible: root.bluetoothOn
glyph: root.bluetoothConnected ? "\u{F00B1}" : "\u{F00AF}" // md-bluetooth_connect / md-bluetooth
color: root.bluetoothConnected ? Theme.accent : Theme.fg
color: root.bluetoothConnected ? Theme.accent : Theme.barFg
}
StatusGlyph {
@@ -151,7 +151,7 @@ Pill {
if (Battery.critical) return Theme.danger;
if (Battery.low) return Theme.warn;
if (Battery.charging) return Theme.ok;
return Theme.fg;
return Theme.barFg;
}
StatusGlyph {
@@ -16,7 +16,7 @@ Text {
font.pixelSize: Theme.fontSizeLarge
horizontalAlignment: Text.AlignHCenter
width: 18
color: Theme.fg
color: Theme.barFg
Behavior on color {
ColorAnimation {
@@ -20,7 +20,7 @@ Row {
text: root.glyph
font.family: Theme.fontMono
font.pixelSize: Theme.fontSize
color: Theme.fgDim
color: Theme.barFgDim
}
Text {
@@ -41,7 +41,7 @@ Row {
return Theme.danger;
if (root.value >= root.warnAt)
return Theme.warn;
return Theme.fg;
return Theme.barFg;
}
Behavior on color {
@@ -15,7 +15,12 @@ Pill {
// Right-click opens the settings that govern this widget. Which readouts
// appear in the bar, and how often they update.
onSecondaryActivated: ShellState.openSettings("appearance")
onSecondaryActivated: ShellState.openSettings("bar")
// Turning off all three readouts should remove the pill, not leave an empty
// one behind: an invisible child still occupies its Row, so without this the
// padding stays and the bar keeps a gap that reports nothing.
visible: Settings.showCpu || Settings.showMemory || (Settings.showGpu && Vitals.gpuAvailable)
interactive: false
@@ -22,7 +22,7 @@ Pill {
Text {
anchors.verticalCenter: parent.verticalCenter
text: VideoWallpaper.paused ? "\u{F040A}" : "\u{F03E4}" // play / pause
color: VideoWallpaper.paused ? Theme.fgDim : Theme.warn
color: VideoWallpaper.paused ? Theme.barFgDim : Theme.warn
font.family: Theme.fontMono
font.pixelSize: 13
}
@@ -33,7 +33,7 @@ Pill {
? (VideoWallpaper.gamePaused ? "Paused for game"
: (VideoWallpaper.batteryPaused ? "Paused on battery" : "Paused"))
: "Wallpaper"
color: Theme.fgDim
color: Theme.barFgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
@@ -14,7 +14,7 @@ Pill {
onSecondaryActivated: ShellState.openSettings("home")
interactive: false
visible: Weather.available
visible: Settings.showWeatherWidget && Weather.available
Text {
anchors.verticalCenter: parent.verticalCenter
@@ -29,6 +29,6 @@ Pill {
text: Math.round(Weather.temperature) + Weather.unitSuffix
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
color: Theme.fg
color: Theme.barFg
}
}
@@ -132,7 +132,7 @@ Item {
color: {
if (slot.modelData.urgent)
return Theme.urgent;
return slot.isOccupied ? Theme.fg : Theme.alpha(Theme.fg, 0.3);
return slot.isOccupied ? Theme.barFg : Theme.alpha(Theme.barFg, 0.3);
}
Behavior on color {