quickshell fixes

This commit is contained in:
nvrl committed 2026-09-24 13:35:20 +02:00
1 parent d2975650fa
commit 32b43bdbb3
68 files changed
+1461 -1047

No files matched your search

+2 -2
View File
@@ -56,7 +56,7 @@ Item {
renderType: Text.NativeRendering
}
// Crimson duplicate: visible only during the landing transition. The
// Accent duplicate: visible only during the landing transition. The
// small opposing offset gives the title Persona's rough print snap
// without leaving permanent motion in the bar.
Text {
@@ -119,7 +119,7 @@ Item {
landedToplevel = toplevel;
}
// A quick crimson wipe under the title on focus change.
// A quick accent wipe under the title on focus change.
Rectangle {
id: wipe
anchors.bottom: parent.bottom
+1 -1
View File
@@ -126,7 +126,7 @@ PanelWindow {
anchors.centerIn: parent
width: parent.ruleHeight * Theme.skew + parent.ruleWeight
height: parent.ruleHeight
color: Theme.alpha(Theme.outline, 0.9)
color: Theme.alpha(Theme.outline, 0.5)
}
}
}
+3 -3
View File
@@ -5,7 +5,7 @@ import "root:/components"
// Shared chrome for every bar module.
//
// The bar is one continuous slab, so by default a module draws no panel of its
// own — it is content inside the bar's surface, and marks hover with a crimson
// own — it is content inside the bar's surface, and marks hover with an accent
// underline rather than by lighting up a card. That is what stops the bar from
// reading as a row of floating islands.
//
@@ -34,7 +34,7 @@ Item {
//
// The rule used to be drawn into the same box as the content, so anything
// taller than about 32px — the media pill's art, the metric meters — had a
// crimson line struck through its bottom edge on hover. Reserving the band
// accent line struck through its bottom edge on hover. Reserving the band
// and centring content in what is left is what keeps the rule under the
// module instead of across it.
// The band reserved the full rule plus its inset — 7px off a 40px module,
@@ -102,7 +102,7 @@ Item {
anchors.bottomMargin: root.ruleBand
}
// Hover mark for in-bar modules: a crimson rule under the content, wiping
// Hover mark for in-bar modules: an accent rule under the content, wiping
// out from the leading edge. Sheared to match the one angle everything uses.
Item {
visible: !root.standalone && root.interactive
+2 -2
View File
@@ -36,7 +36,8 @@ BarPill {
color: Theme.accent
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2.5
font.weight: Font.Bold
font.letterSpacing: 3
renderType: Text.NativeRendering
}
@@ -98,7 +99,6 @@ BarPill {
text: Qt.formatDateTime(clock.date, "HH:mm")
pixelSize: Theme.fsTitle
horizontalAlignment: Text.AlignHCenter
split: 1.4
}
}
-72
View File
@@ -1,72 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Notification bell with an unread badge. Click opens the history, right click
// toggles do-not-disturb.
BarPill {
id: root
required property var screenRef
accent: Notifs.dnd ? Theme.muted : Theme.primary
padding: Theme.padS
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: {
popout.pinned = !popout.pinned;
Notifs.markRead();
}
onRightClicked: Notifs.toggleDnd()
Icon {
id: bell
anchors.centerIn: parent
text: Notifs.icon
font.pixelSize: Theme.fsLarge
color: {
if (Notifs.dnd) return Theme.muted;
if (Notifs.hasUnread) return Theme.accent;
return root.hovered ? Theme.glow : Theme.primary;
}
}
// Unread badge, chopped like everything else.
Skew {
visible: Notifs.unread > 0 && !Notifs.dnd
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: parent.top
anchors.topMargin: 4
width: Math.max(12, badge.implicitWidth + 5)
height: 12
color: Theme.accent
// The badge count stays upright. It used to inherit the shear from the
// transform on the block below, which at 8px left the digits smeared.
Text {
id: badge
anchors.centerIn: parent
text: Notifs.unread > 9 ? "9+" : Notifs.unread
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: 8
font.weight: Font.Bold
renderType: Text.NativeRendering
}
}
Connections {
target: Notifs
function onPopupRequested() { bell.bump(); }
}
NotifPopout {
id: popout
anchorItem: root
triggerHovered: root.hovered
}
}
-42
View File
@@ -1,42 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Far right. Opens the full-screen power menu.
BarPill {
id: root
required property var screenRef
accent: Theme.accent
slash: true
padding: Theme.padS
active: Actions.powerMenuOpen
implicitWidth: Theme.barHeight - 4 + contentPad
onClicked: Actions.togglePowerMenu()
Icon {
id: glyph
anchors.centerIn: parent
text: "󰐥"
font.pixelSize: Theme.fsLarge
color: root.hovered || Actions.powerMenuOpen ? Theme.accent : Theme.subtext
}
// A crimson halo that swells under the pointer.
Rectangle {
anchors.centerIn: glyph
z: -1
width: root.hovered ? 26 : 0
height: width
radius: width / 2
color: Theme.alpha(Theme.accent, 0.18)
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
}
+6 -6
View File
@@ -94,7 +94,7 @@ PanelWindow {
statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName
hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
alert: Audio.muted
dim: Audio.muted
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
@@ -106,7 +106,7 @@ PanelWindow {
statusText: Audio.micMuted ? "Muted" : Audio.micPercent + "% · " + Audio.sourceName
hint: "Scroll to adjust"
suppressTooltip: audioPopout.detailActive
alert: Audio.micMuted
dim: Audio.micMuted
onClicked: audioPopout.togglePinned()
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
@@ -132,7 +132,7 @@ PanelWindow {
label: "Battery"
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
suppressTooltip: battPopout.detailActive
alert: Battery.critical
alarm: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.togglePinned()
}
@@ -165,7 +165,7 @@ PanelWindow {
: ""
reserveReadout: true
dim: Notifs.dnd
alert: Notifs.hasUnread && !Notifs.dnd
live: Notifs.hasUnread && !Notifs.dnd
suppressTooltip: notifPopout.detailActive
onClicked: {
notifPopout.togglePinned();
@@ -178,7 +178,7 @@ PanelWindow {
icon: "󰐥"
label: "Power"
statusText: "Session actions"
alert: Actions.powerMenuOpen
live: Actions.powerMenuOpen
onClicked: Actions.togglePowerMenu()
}
}
@@ -201,7 +201,7 @@ PanelWindow {
width: parent.ruleWidth
height: parent.ruleWidth * Theme.skew + parent.ruleWeight
vertical: true
color: Theme.alpha(Theme.outline, 0.9)
color: Theme.alpha(Theme.outline, 0.5)
}
}
+27 -13
View File
@@ -3,10 +3,10 @@ import "root:/config"
import "root:/components"
// One slot on the right-hand rail: a glyph with an optional micro readout
// stacked beneath it, and a crimson bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so unlike StatusIcon the readout sits
// under the glyph rather than beside it, and anything longer than a percentage
// belongs in the popout instead.
// stacked beneath it, and an accent bar that wipes down the leading edge on
// hover. The rail is only 48px wide, so the readout sits under the glyph
// rather than beside it, and anything longer than a percentage belongs in the
// popout instead.
Item {
id: root
@@ -17,7 +17,15 @@ Item {
property string hint: ""
property bool suppressTooltip: false
property color tint: Theme.primary
property bool alert: false
// Three states, three meanings. `live` wants attention but nothing is wrong
// (unread notifications, an open power menu): accent. `alarm` is wrong or
// critical (a dying battery): alarm. `dim` is off by choice (muted, radio
// off, do-not-disturb): grey. These used to be one `alert` flag painted in
// the accent, which made a muted speaker look like the most active thing
// on the rail.
property bool live: false
property bool alarm: false
property bool dim: false
property bool pulsing: false
@@ -63,7 +71,7 @@ Item {
id: glyphMetrics
text: root.icon
font.family: Theme.fontIcon
font.pixelSize: Theme.fsLarge
font.pixelSize: Theme.fsRailIcon
}
Item {
@@ -76,9 +84,10 @@ Item {
y: (root.glyphBand - height) / 2
text: root.icon
pulsing: root.pulsing
font.pixelSize: Theme.fsLarge
font.pixelSize: Theme.fsRailIcon
color: {
if (root.alert) return Theme.accent;
if (root.alarm) return Theme.alarm;
if (root.live) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
@@ -96,7 +105,11 @@ Item {
anchors.topMargin: root.glyphBand + 1
visible: root.readout !== ""
text: root.readout
color: root.dim ? Theme.muted : Theme.subtext
color: {
if (root.alarm) return Theme.alarm;
if (root.dim) return Theme.muted;
return Theme.subtext;
}
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.DemiBold
@@ -116,9 +129,9 @@ Item {
// scale then pushed it out past the silhouette onto the desktop. The inset
// has to survive that scale, which is why it is not simply 1 or 2.
//
// It is crimson rather than `tint` because it only ever appears on hover,
// and hover is crimson everywhere else in the shell (Theme.glow). Painting
// it white put a white bar on the rail's edge for no stated reason.
// It is the accent rather than `tint` because it only ever appears on
// hover, and hover is the accent everywhere else in the shell (Theme.glow).
// Painting it white put a white bar on the rail's edge for no stated reason.
Rectangle {
anchors.left: parent.left
anchors.leftMargin: root.wipeInset
@@ -137,7 +150,8 @@ Item {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
onAlarmChanged: if (alarm) glyph.bump()
onLiveChanged: if (live) glyph.bump()
HoverHandler {
id: hover
+2 -2
View File
@@ -63,7 +63,7 @@ BarPill {
anchors.centerIn: parent
width: parent.height * Theme.skew + 1
height: parent.height
color: Theme.alpha(Theme.outline, 0.8)
color: Theme.alpha(Theme.outline, 0.5)
}
}
@@ -106,7 +106,7 @@ BarPill {
id: labelText
anchors.baseline: gauge.baseline
text: metric.label
color: Theme.muted
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
+1 -1
View File
@@ -22,7 +22,7 @@ BarPill {
// The wordmark. Was the Death arcana skull (nf-md-skull, U+F068C) — a
// pictogram in a shell that otherwise says everything in heavy italic
// type. SplitText puts it in the display face with the crimson-and-black
// type. SplitText puts it in the display face with the ink print
// offset every other heading carries, so the house mark is built out of
// the same parts as the rest of the bar.
SplitText {
-117
View File
@@ -1,117 +0,0 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
import "root:/popouts"
// Network, bluetooth, audio, mic, backlight and battery, in one slab. Each
// glyph carries its own popout.
BarPill {
id: root
required property var screenRef
accent: Theme.primary
padding: Theme.padS
interactive: false
implicitWidth: layout.implicitWidth + contentPad * 2
Row {
id: layout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: Theme.padM
StatusIcon {
id: netIcon
icon: Net.icon
label: Sys.netUp ? Sys.rate(Sys.netRx + Sys.netTx) : ""
labelWidth: 62
tint: Theme.primary
dim: !Sys.netUp
onClicked: netPopout.pinned = !netPopout.pinned
onRightClicked: Net.toggleWifi()
onMiddleClicked: Actions.openNetworkSettings()
}
StatusIcon {
id: btIcon
icon: Bt.icon
tint: Bt.connected.length > 0 ? Theme.blue : Theme.primary
dim: !Bt.enabled
onClicked: btPopout.pinned = !btPopout.pinned
onRightClicked: Bt.toggle()
}
StatusIcon {
id: volIcon
icon: Audio.icon
label: Audio.muted ? "" : Audio.volumePercent + "%"
labelWidth: Audio.muted ? 0 : 34
alert: Audio.muted
onClicked: Audio.toggleMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onMiddleClicked: Actions.openAudioSettings()
onScrolled: delta => Audio.changeVolume(delta > 0 ? 0.05 : -0.05)
}
StatusIcon {
id: micIcon
icon: Audio.micIcon
alert: Audio.micMuted
onClicked: Audio.toggleMicMute()
onRightClicked: audioPopout.pinned = !audioPopout.pinned
onScrolled: delta => Audio.setMicVolume(Audio.micVolume + (delta > 0 ? 0.05 : -0.05))
}
StatusIcon {
id: brightIcon
visible: Brightness.available
icon: Brightness.icon
label: Math.round(Brightness.percent) + "%"
labelWidth: 34
// Brightness is never an alarm — it stays white and lets the
// battery be the only thing in the cluster that can go crimson.
tint: Theme.text
onScrolled: delta => Brightness.change(delta > 0 ? 5 : -5)
}
StatusIcon {
id: battIcon
visible: Battery.available
icon: Battery.icon
label: Math.round(Battery.percent) + "%"
labelWidth: 34
tint: Battery.tint
alert: Battery.critical
pulsing: Battery.critical
onClicked: battPopout.pinned = !battPopout.pinned
}
}
NetPopout {
id: netPopout
anchorItem: netIcon
triggerHovered: netIcon.hovered
}
BtPopout {
id: btPopout
anchorItem: btIcon
triggerHovered: btIcon.hovered
}
AudioPopout {
id: audioPopout
anchorItem: volIcon
triggerHovered: volIcon.hovered || micIcon.hovered
}
BatteryPopout {
id: battPopout
anchorItem: battIcon
triggerHovered: battIcon.hovered
visible: Battery.available && shown
}
}
-108
View File
@@ -1,108 +0,0 @@
import QtQuick
import "root:/config"
import "root:/components"
// One indicator inside the status cluster: a glyph, an optional readout, and a
// crimson underline that wipes in on hover.
Item {
id: root
property string icon: ""
property string label: ""
property color tint: Theme.primary
property bool alert: false
property bool dim: false
property bool pulsing: false
// Fixed label width. Readouts that change width constantly (throughput,
// percentages) would otherwise shove the whole cluster around.
property real labelWidth: 0
readonly property bool hovered: hover.hovered
signal clicked()
signal rightClicked()
signal middleClicked()
signal scrolled(real delta)
implicitWidth: layout.implicitWidth
implicitHeight: Theme.barHeight - Theme.padS * 2
Row {
id: layout
anchors.centerIn: parent
spacing: 5
Icon {
id: glyph
anchors.verticalCenter: parent.verticalCenter
text: root.icon
pulsing: root.pulsing
color: {
if (root.alert) return Theme.accent;
if (root.dim) return Theme.muted;
if (root.hovered) return Theme.glow;
return root.tint;
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.label !== ""
text: root.label
width: root.labelWidth > 0 ? root.labelWidth : implicitWidth
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
color: root.dim ? Theme.muted : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
height: 2
width: root.hovered ? layout.implicitWidth : 0
color: root.alert ? Theme.accent : root.tint
Behavior on width {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutExpo }
}
}
scale: root.hovered ? 1.06 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
onAlertChanged: if (alert) glyph.bump()
HoverHandler {
id: hover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: root.clicked()
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: root.rightClicked()
}
TapHandler {
acceptedButtons: Qt.MiddleButton
onTapped: root.middleClicked()
}
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => root.scrolled(event.angleDelta.y)
}
}
+17 -16
View File
@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
@@ -15,10 +14,19 @@ Column {
id: root
spacing: Theme.padS + 2
visible: SystemTray.items.values.length > 0
visible: root.shownItems.length > 0
// Applets whose whole job the rail already does. nm-applet and blueman keep
// running — they are the Wi-Fi secret agent and the pairing agent — but
// their icons were a second network and a second bluetooth glyph a few
// slots below the rail's own.
readonly property var hiddenIds: ["nm-applet", "blueman"]
readonly property var shownItems: SystemTray.items.values
.filter(item => root.hiddenIds.indexOf(item.id) === -1)
Repeater {
model: SystemTray.items
model: root.shownItems
Item {
id: entry
@@ -31,8 +39,8 @@ Column {
// App icons arrive in their own brand colours, which wrecks the
// two-hue palette faster than anything else in the shell. Flatten
// every one of them to a single-colour silhouette: white at rest,
// crimson under the pointer. Detail inside the glyph is lost on
// every one of them to a single-colour silhouette: subtext at rest,
// a notch under the rail's own glyphs, and accent under the pointer. Detail inside the glyph is lost on
// purpose — that flattening *is* the screen-printed look.
IconImage {
id: img
@@ -41,22 +49,15 @@ Column {
asynchronous: true
layer.enabled: true
layer.effect: MultiEffect {
colorization: 1.0
colorizationColor: itemHover.hovered
layer.effect: TintEffect {
tintColor: itemHover.hovered
? Theme.accent
: Theme.text
: Theme.subtext
Behavior on colorizationColor {
Behavior on tintColor {
ColorAnimation { duration: Theme.durFast }
}
}
opacity: itemHover.hovered ? 1.0 : 0.82
Behavior on opacity {
NumberAnimation { duration: Theme.durFast }
}
}
scale: itemHover.hovered ? 1.18 : 1.0
+27 -14
View File
@@ -5,7 +5,7 @@ import "root:/config"
import "root:/components"
// Workspace indicator: leaning ticks that stretch open when focused. Occupied
// workspaces glow teal, urgent ones snap to crimson.
// workspaces fill in, urgent ones snap to the alarm colour.
Item {
id: root
@@ -65,8 +65,12 @@ Item {
// Every tick is the same height and the same shear. Only the
// width changes, and only to say which workspace is focused.
width: focused ? 32 : (occupied ? 16 : 9)
height: 18
//
// Kept well short of the bar's 20px divider rules. At 18px an
// empty outline tick was the same height, lean and weight as a
// divider, and the workspace strip read as a row of rules.
width: focused ? 30 : (occupied ? 16 : 8)
height: 14
Behavior on width {
NumberAnimation {
@@ -80,23 +84,32 @@ Item {
id: tick
anchors.fill: parent
// Focused is a solid crimson block. Occupied is a white
// block at half weight. Empty is an outline. Three states,
// two colours, no ambiguity.
// Focused is a solid accent block. Urgent is a solid alarm
// block — it used to be the accent too, which made an
// urgent workspace indistinguishable from the focused one.
// Occupied is a subtext block. Empty is a flat recessed
// slot, filled rather than outlined so it cannot pass for a
// divider.
color: {
if (pip.focused || pip.urgent) return Theme.accent;
if (pip.focused) return Theme.accent;
if (pip.urgent) return Theme.alarm;
if (pip.hovered) return Theme.text;
if (pip.occupied) return Theme.alpha(Theme.text, 0.5);
return "transparent";
if (pip.occupied) return Theme.subtext;
return Theme.surfaceAlt;
}
borderColor: pip.occupied || pip.focused
? "transparent"
: Theme.alpha(Theme.text, 0.35)
borderWidth: 1
Behavior on color {
ColorAnimation { duration: Theme.durBase }
}
// Urgent is the one pip that has to be seen peripherally.
SequentialAnimation on opacity {
running: pip.urgent && !pip.focused
loops: Animation.Infinite
alwaysRunToEnd: true
NumberAnimation { to: 0.4; duration: 520; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 520; easing.type: Easing.InOutQuad }
}
}
// The focused workspace wears its number.
@@ -107,7 +120,7 @@ Item {
text: pip.wsId
color: Theme.ink
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.pixelSize: Theme.fsSmall
font.weight: Theme.weightDisplay
font.italic: true
renderType: Text.NativeRendering