added quickshell

This commit is contained in:
2026-08-10 23:57:26 +02:00
parent cbaf55cfc5
commit 47ff5233bd
69 changed files with 6945 additions and 80 deletions
+275
View File
@@ -0,0 +1,275 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import "root:/config"
import "root:/components"
import "root:/services"
// Output and input levels, the sink picker, and a per-app mixer.
Popout {
id: root
contentWidth: 360
contentHeight: surface.implicitContentHeight + Theme.padM * 2
PopoutSurface {
id: surface
anchors.fill: parent
tone: Theme.primary
readonly property real implicitContentHeight: header.height + column.implicitHeight + Theme.padM * 2
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Audio"
icon: Audio.icon
accent: Theme.primary
subtitle: Audio.sinkName
}
Column {
id: column
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
// ------------------------------------------------------- output
LevelRow {
width: parent.width
icon: Audio.icon
label: "Output"
readout: Audio.muted ? "MUTED" : Audio.volumePercent + "%"
value: Audio.volume
muted: Audio.muted
accent: Theme.primary
onMoved: v => Audio.setVolume(v)
onToggled: Audio.toggleMute()
}
// -------------------------------------------------------- input
LevelRow {
width: parent.width
icon: Audio.micIcon
label: "Input"
readout: Audio.micMuted ? "MUTED" : Audio.micPercent + "%"
value: Audio.micVolume
muted: Audio.micMuted
accent: Theme.blue
onMoved: v => Audio.setMicVolume(v)
onToggled: Audio.toggleMicMute()
}
SectionRule {
width: parent.width
text: "Outputs"
visible: Audio.sinks.length > 1
}
Repeater {
model: Audio.sinks.length > 1 ? Audio.sinks : []
PickRow {
required property var modelData
width: column.width
label: modelData.nickname || modelData.description || modelData.name
icon: "󰓃"
selected: Audio.sink === modelData
onClicked: Audio.setSink(modelData)
}
}
SectionRule {
width: parent.width
text: "Applications"
visible: Audio.streams.length > 0
}
Repeater {
model: Audio.streams
LevelRow {
required property var modelData
width: column.width
icon: "󰝚"
label: Audio.streamName(modelData)
readout: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : ""
value: modelData.audio?.volume ?? 0
muted: modelData.audio?.muted ?? false
accent: Theme.glow
onMoved: v => { if (modelData.audio) modelData.audio.volume = v; }
onToggled: { if (modelData.audio) modelData.audio.muted = !modelData.audio.muted; }
}
}
P5Button {
text: "Mixer"
icon: "󰕾"
onClicked: Actions.openAudioSettings()
}
}
}
// ------------------------------------------------------------- pieces
component LevelRow: Item {
id: levelRow
property string icon: ""
property string label: ""
property string readout: ""
property real value: 0
property bool muted: false
property color accent: Theme.primary
signal moved(real value)
signal toggled()
implicitHeight: 42
Row {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padS
Icon {
id: levelIcon
anchors.verticalCenter: parent.verticalCenter
text: levelRow.icon
color: levelRow.muted ? Theme.accent : levelRow.accent
font.pixelSize: Theme.fsLarge
TapHandler { onTapped: levelRow.toggled() }
HoverHandler { cursorShape: Qt.PointingHandCursor }
}
Text {
anchors.verticalCenter: parent.verticalCenter
width: levelRow.width - levelIcon.width - readoutText.width - Theme.padS * 3
text: levelRow.label
elide: Text.ElideRight
color: Theme.text
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
font.italic: true
renderType: Text.NativeRendering
}
Text {
id: readoutText
anchors.verticalCenter: parent.verticalCenter
text: levelRow.readout
color: levelRow.muted ? Theme.accent : Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.weight: Font.Bold
renderType: Text.NativeRendering
}
}
P5Slider {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
value: levelRow.value
accent: levelRow.accent
enabledControl: !levelRow.muted
onMoved: v => levelRow.moved(v)
}
}
component SectionRule: Item {
property string text: ""
implicitHeight: visible ? 18 : 0
Text {
id: sectionLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: parent.text.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
Rectangle {
anchors.left: sectionLabel.right
anchors.leftMargin: Theme.padS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 1
color: Theme.alpha(Theme.outline, 0.6)
}
}
component PickRow: Item {
id: pickRow
property string label: ""
property string icon: ""
property bool selected: false
signal clicked()
implicitHeight: 26
Rectangle {
anchors.fill: parent
color: pickHover.hovered
? Theme.alpha(Theme.primary, 0.14)
: (pickRow.selected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: pickRow.selected ? 18 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.icon
color: pickRow.selected ? Theme.primary : Theme.muted
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: pickRow.label
color: pickRow.selected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
renderType: Text.NativeRendering
}
}
HoverHandler {
id: pickHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: pickRow.clicked()
}
}
}
+119
View File
@@ -0,0 +1,119 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Charge level, time estimate, draw rate and health.
Popout {
id: root
contentWidth: 310
contentHeight: 296
PopoutSurface {
anchors.fill: parent
tone: Battery.tint
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Power"
icon: Battery.icon
accent: Battery.tint
subtitle: Battery.charging ? "CHARGING" : (Battery.onBattery ? "ON BATTERY" : "AC")
}
Row {
id: readout
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
Gauge {
width: 92; height: 92
value: Battery.percent
label: "CHARGE"
arcColor: Battery.tint
readout: Math.round(Battery.percent) + "%"
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
SplitText {
text: Battery.timeLabel
pixelSize: Theme.fsBody
split: 1.1
splitOpacity: 0.55
}
Text {
text: Battery.changeRate > 0
? Battery.changeRate.toFixed(1) + " W " + (Battery.charging ? "in" : "draw")
: "idle"
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
renderType: Text.NativeRendering
}
Text {
visible: Battery.health > 0
text: "Health " + Math.round(Battery.health) + "%"
color: Battery.health < 70 ? Theme.warn : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
MeterBar {
id: bar
anchors.top: readout.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
height: 12
segments: 20
segmentWidth: (width - 19 * 3) / 20
spacing: 3
value: Battery.percent
activeColor: Battery.tint
}
P5Button {
id: suspendButton
anchors.bottom: parent.bottom
anchors.left: parent.left
text: "Suspend"
icon: "󰤄"
onClicked: {
root.pinned = false;
Actions.suspend();
}
}
Text {
anchors.top: bar.bottom
anchors.topMargin: Theme.padS
anchors.bottom: suspendButton.top
anchors.bottomMargin: Theme.padS
anchors.left: parent.left
anchors.right: parent.right
text: Battery.detail
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignTop
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}
+192
View File
@@ -0,0 +1,192 @@
import QtQuick
import Quickshell
import Quickshell.Bluetooth
import "root:/config"
import "root:/components"
import "root:/services"
// Adapter toggle, scan, and the device list with battery levels where BlueZ
// reports them.
Popout {
id: root
contentWidth: 340
contentHeight: 280
// Discovery is expensive; run it only while the panel is open.
onShownChanged: {
if (!shown && Bt.discovering && Bt.adapter) Bt.adapter.discovering = false;
}
PopoutSurface {
anchors.fill: parent
tone: Bt.enabled ? Theme.blue : Theme.muted
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Bluetooth"
icon: Bt.icon
accent: Theme.blue
subtitle: Bt.adapter ? Bt.adapter.name : "NO ADAPTER"
}
Row {
id: controls
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
spacing: Theme.padS
P5Button {
text: Bt.enabled ? "On" : "Off"
icon: Bt.enabled ? "󰂯" : "󰂲"
accent: Bt.enabled ? Theme.blue : Theme.muted
onClicked: Bt.toggle()
}
P5Button {
text: Bt.discovering ? "Scanning" : "Scan"
icon: "󰐷"
accent: Bt.discovering ? Theme.accent : Theme.primary
onClicked: Bt.toggleScan()
}
}
Text {
id: listLabel
anchors.top: controls.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
text: Bt.devices.length > 0 ? "DEVICES" : "NO DEVICES"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
ListView {
anchors.top: listLabel.bottom
anchors.topMargin: Theme.padS
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
spacing: 2
model: Bt.devices
delegate: Item {
id: devRow
required property BluetoothDevice modelData
width: ListView.view.width
height: 34
Rectangle {
anchors.fill: parent
color: devHover.hovered
? Theme.alpha(Theme.blue, 0.16)
: (devRow.modelData.connected ? Theme.alpha(Theme.blue, 0.08) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: devRow.modelData.connected ? 22 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Bt.deviceIcon(devRow.modelData)
color: devRow.modelData.connected ? Theme.blue : Theme.subtext
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -2
Text {
text: Bt.deviceLabel(devRow.modelData)
color: devRow.modelData.connected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: devRow.modelData.connected ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Text {
text: {
if (devRow.modelData.pairing) return "pairing…";
if (devRow.modelData.connected) return "connected";
if (devRow.modelData.paired || devRow.modelData.bonded) return "paired";
return devRow.modelData.address;
}
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
renderType: Text.NativeRendering
}
}
}
// Device battery, where BlueZ exposes it.
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
spacing: 3
visible: devRow.modelData.batteryAvailable
Icon {
anchors.verticalCenter: parent.verticalCenter
text: "󰥉"
color: devRow.modelData.battery > 0.25 ? Theme.ok : Theme.warn
font.pixelSize: Theme.fsSmall
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: Math.round(devRow.modelData.battery * 100) + "%"
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
HoverHandler {
id: devHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: Bt.toggleDevice(devRow.modelData)
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: if (devRow.modelData.paired) devRow.modelData.forget()
}
}
}
}
}
+260
View File
@@ -0,0 +1,260 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Month grid with the current day slashed in crimson, plus uptime and a couple
// of world clocks.
Popout {
id: root
property date clockDate: new Date()
contentWidth: 320
contentHeight: 352
// Month currently on display, as an offset from the real one.
property int monthOffset: 0
onShownChanged: if (!shown) monthOffset = 0
readonly property date viewDate: {
const d = new Date(root.clockDate);
d.setDate(1);
d.setMonth(d.getMonth() + root.monthOffset);
return d;
}
readonly property int viewYear: viewDate.getFullYear()
readonly property int viewMonth: viewDate.getMonth()
// Monday-first grid, 6 rows of 7.
readonly property var cells: {
const first = new Date(root.viewYear, root.viewMonth, 1);
const lead = (first.getDay() + 6) % 7;
const start = new Date(root.viewYear, root.viewMonth, 1 - lead);
const out = [];
for (let i = 0; i < 42; i++) {
const d = new Date(start);
d.setDate(start.getDate() + i);
out.push({
day: d.getDate(),
inMonth: d.getMonth() === root.viewMonth,
today: d.toDateString() === root.clockDate.toDateString()
});
}
return out;
}
PopoutSurface {
anchors.fill: parent
tone: Theme.accent
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: Qt.formatDateTime(root.viewDate, "MMMM")
icon: "󰃭"
accent: Theme.accent
subtitle: Qt.formatDateTime(root.viewDate, "yyyy")
}
// Month stepper.
Row {
anchors.top: header.bottom
anchors.topMargin: Theme.padS
anchors.right: parent.right
spacing: Theme.padS
z: 2
StepButton {
glyph: "󰅁"
onClicked: root.monthOffset--
}
StepButton {
glyph: "󰅂"
onClicked: root.monthOffset++
}
}
// Weekday header, clear of the month stepper above it.
Row {
id: dayNames
anchors.top: header.bottom
anchors.topMargin: 36
anchors.left: parent.left
anchors.right: parent.right
Repeater {
model: ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]
Text {
required property var modelData
required property int index
width: dayNames.width / 7
horizontalAlignment: Text.AlignHCenter
text: modelData
color: index >= 5 ? Theme.alpha(Theme.accent, 0.8) : Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1
renderType: Text.NativeRendering
}
}
}
Grid {
id: grid
anchors.top: dayNames.bottom
anchors.topMargin: Theme.padS
anchors.left: parent.left
anchors.right: parent.right
columns: 7
Repeater {
model: root.cells
Item {
id: cell
required property var modelData
width: grid.width / 7
height: 30
// Today gets a leaning crimson slab.
Rectangle {
anchors.centerIn: parent
width: 26
height: 24
visible: cell.modelData.today
color: Theme.accent
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 24 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Text {
anchors.centerIn: parent
text: cell.modelData.day
color: {
if (cell.modelData.today) return Theme.text;
if (!cell.modelData.inMonth) return Theme.alpha(Theme.muted, 0.45);
return Theme.subtext;
}
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: cell.modelData.today ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
}
}
}
// Footer: uptime and a second time zone.
Row {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
Column {
width: parent.width / 2
spacing: -2
Text {
text: "UPTIME"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
text: Sys.uptime
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
}
Column {
width: parent.width / 2
spacing: -2
Text {
anchors.right: parent.right
text: "TOKYO"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
anchors.right: parent.right
text: {
// Tokyo is UTC+9 year-round.
const utc = root.clockDate.getTime()
+ root.clockDate.getTimezoneOffset() * 60000;
const tokyo = new Date(utc + 9 * 3600000);
return Qt.formatDateTime(tokyo, "HH:mm");
}
color: Theme.primary
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
}
}
}
component StepButton: Item {
id: step
property string glyph: ""
signal clicked()
implicitWidth: 22
implicitHeight: 22
Rectangle {
anchors.fill: parent
color: stepHover.hovered ? Theme.alpha(Theme.accent, 0.25) : "transparent"
border.width: 1
border.color: Theme.alpha(Theme.outline, 0.7)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Icon {
anchors.centerIn: parent
text: step.glyph
color: stepHover.hovered ? Theme.text : Theme.subtext
font.pixelSize: Theme.fsBody
}
HoverHandler {
id: stepHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: step.clicked()
}
}
}
+337
View File
@@ -0,0 +1,337 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// Full player: cover art, transport, seek bar, and a picker when more than one
// player is running.
Popout {
id: root
contentWidth: 400
contentHeight: 200 + (Media.players.length > 1 ? 34 : 0)
PopoutSurface {
id: surface
anchors.fill: parent
tone: Theme.primary
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Now Playing"
icon: Media.sourceIcon
subtitle: Media.identity
}
// Cover art, chopped.
Item {
id: artFrame
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
width: 108
height: 108
Shape {
id: artMask
anchors.fill: parent
visible: false
layer.enabled: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "white"
strokeColor: "transparent"
PathPolyline {
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
}
}
}
Image {
id: art
anchors.fill: parent
source: Media.artUrl
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: status === Image.Ready
layer.enabled: true
layer.effect: MaskedArt { maskItem: artMask }
}
// Placeholder when the player exposes no artwork.
Shape {
anchors.fill: parent
visible: art.status !== Image.Ready
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: Theme.surface
strokeColor: Theme.alpha(Theme.primary, 0.5)
strokeWidth: 2
PathPolyline {
path: Geom.chamfer(Geom.parallelogram(108, 108, 0.1), 18, [1, 3])
}
}
}
Icon {
anchors.centerIn: parent
visible: art.status !== Image.Ready
text: Media.sourceIcon
font.pixelSize: 42
color: Theme.alpha(Theme.primary, 0.7)
}
// Spinning rule around the art while playing.
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
width: 30
height: 3
color: Theme.accent
visible: Media.playing
}
}
Column {
id: info
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: artFrame.right
anchors.leftMargin: Theme.padM
anchors.right: parent.right
spacing: 2
Marquee {
width: parent.width
height: 24
text: Media.title || "Nothing playing"
color: Theme.text
pixelSize: Theme.fsLarge
family: Theme.fontDisplay
weight: Font.Black
italic: true
}
Marquee {
width: parent.width
height: 16
text: Media.artist
color: Theme.primary
pixelSize: Theme.fsSmall
family: Theme.fontDisplay
italic: true
weight: Font.DemiBold
}
Marquee {
width: parent.width
height: 14
text: Media.album
color: Theme.muted
pixelSize: Theme.fsMicro
family: Theme.fontMono
}
Item { width: 1; height: Theme.padS }
// Transport
Row {
spacing: Theme.padM
TransportButton {
glyph: "󰒮"
enabledControl: Media.canGoPrevious
onClicked: Media.previous()
}
TransportButton {
glyph: Media.playing ? "󰏤" : "󰐊"
primary: true
enabledControl: Media.hasPlayer
onClicked: Media.playPause()
}
TransportButton {
glyph: "󰒭"
enabledControl: Media.canGoNext
onClicked: Media.next()
}
TransportButton {
glyph: "󰐒"
enabledControl: Media.hasPlayer
onClicked: Media.raise()
}
}
}
// Seek bar across the bottom.
Item {
id: seekArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: picker.visible ? picker.top : parent.bottom
anchors.bottomMargin: Theme.padS
height: 26
visible: Media.hasPlayer
Text {
id: elapsed
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: Media.timeString(Media.position)
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
Text {
id: total
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: Media.timeString(Media.length)
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
P5Slider {
anchors.left: elapsed.right
anchors.right: total.left
anchors.leftMargin: Theme.padS
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
value: Media.progress
accent: Theme.accent
enabledControl: Media.canSeek
onMoved: v => Media.seekTo(v)
}
}
// Player picker, only when it matters.
Row {
id: picker
anchors.left: parent.left
anchors.bottom: parent.bottom
spacing: Theme.padS
visible: Media.players.length > 1
Repeater {
model: Media.players
Item {
id: chip
required property var modelData
readonly property bool current: Media.active === modelData
implicitWidth: chipLabel.implicitWidth + Theme.padM
implicitHeight: 22
Rectangle {
anchors.fill: parent
color: chip.current
? Theme.alpha(Theme.primary, 0.22)
: (chipHover.hovered ? Theme.alpha(Theme.primary, 0.1) : "transparent")
border.width: 1
border.color: chip.current ? Theme.primary : Theme.alpha(Theme.outline, 0.7)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, 22 * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Text {
id: chipLabel
anchors.centerIn: parent
text: chip.modelData.identity
color: chip.current ? Theme.text : Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
HoverHandler {
id: chipHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: Media.select(chip.modelData)
}
}
}
}
}
component TransportButton: Item {
id: tb
property string glyph: ""
property bool primary: false
property bool enabledControl: true
signal clicked()
implicitWidth: primary ? 38 : 30
implicitHeight: primary ? 38 : 30
Rectangle {
anchors.fill: parent
color: {
if (!tb.enabledControl) return "transparent";
if (tbHover.hovered) return tb.primary ? Theme.accent : Theme.alpha(Theme.primary, 0.22);
return tb.primary ? Theme.alpha(Theme.primary, 0.18) : "transparent";
}
border.width: tb.primary ? 2 : 1
border.color: tb.enabledControl
? (tbHover.hovered ? Theme.text : Theme.alpha(Theme.primary, 0.6))
: Theme.alpha(Theme.outline, 0.4)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, tb.height * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
Behavior on color {
ColorAnimation { duration: Theme.durFast }
}
}
Icon {
anchors.centerIn: parent
text: tb.glyph
font.pixelSize: tb.primary ? Theme.fsTitle : Theme.fsLarge
color: {
if (!tb.enabledControl) return Theme.alpha(Theme.muted, 0.5);
if (tbHover.hovered && tb.primary) return Theme.text;
return Theme.primary;
}
}
scale: tbHover.hovered && tb.enabledControl ? 1.12 : 1.0
Behavior on scale {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
HoverHandler {
id: tbHover
enabled: tb.enabledControl
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: tb.enabledControl
onTapped: tb.clicked()
}
}
}
+252
View File
@@ -0,0 +1,252 @@
import QtQuick
import Quickshell
import Quickshell.Networking
import "root:/config"
import "root:/components"
import "root:/services"
// Link state, throughput, and the Wi-Fi picker.
Popout {
id: root
contentWidth: 360
contentHeight: 300
// Scan only while the popout is actually on screen.
onShownChanged: Net.setScanning(shown)
PopoutSurface {
anchors.fill: parent
tone: Sys.netUp ? Theme.primary : Theme.muted
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Network"
icon: Net.icon
subtitle: Sys.netUp ? Sys.netIp : "OFFLINE"
}
// Throughput
Row {
id: rates
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.padM
RateBlock {
width: (parent.width - Theme.padM) / 2
glyph: "󰇚"
label: "Down"
value: Sys.rate(Sys.netRx)
tone: Theme.primary
}
RateBlock {
width: (parent.width - Theme.padM) / 2
glyph: "󰕒"
label: "Up"
value: Sys.rate(Sys.netTx)
tone: Theme.accent
}
}
Row {
id: controls
anchors.top: rates.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
spacing: Theme.padS
P5Button {
text: Net.wifiEnabled ? "Wi-Fi On" : "Wi-Fi Off"
icon: Net.wifiEnabled ? "󰖩" : "󰖪"
accent: Net.wifiEnabled ? Theme.primary : Theme.muted
onClicked: Net.toggleWifi()
}
P5Button {
text: "Settings"
icon: "󰒓"
onClicked: Actions.openNetworkSettings()
}
}
Text {
id: apsLabel
anchors.top: controls.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
text: Net.scanning ? "SCANNING…" : "NETWORKS"
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 2
renderType: Text.NativeRendering
}
ListView {
anchors.top: apsLabel.bottom
anchors.topMargin: Theme.padS
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
spacing: 2
model: Net.wifiNetworks
delegate: Item {
id: apRow
required property var modelData
width: ListView.view.width
height: 30
Rectangle {
anchors.fill: parent
color: apHover.hovered
? Theme.alpha(Theme.primary, 0.14)
: (apRow.modelData.connected ? Theme.alpha(Theme.primary, 0.07) : "transparent")
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 3
height: apRow.modelData.connected ? 20 : 0
color: Theme.accent
Behavior on height {
NumberAnimation { duration: Theme.durBase; easing.type: Easing.OutBack }
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: Net.signalIcon(apRow.modelData.signalStrength ?? 0)
color: apRow.modelData.connected ? Theme.primary : Theme.subtext
font.pixelSize: Theme.fsBody
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.name
color: apRow.modelData.connected ? Theme.text : Theme.subtext
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsSmall
font.italic: true
font.weight: apRow.modelData.connected ? Font.Black : Font.Medium
renderType: Text.NativeRendering
}
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: Net.secured(apRow.modelData)
text: "󰌾"
color: Theme.muted
font.pixelSize: Theme.fsMicro
}
}
Text {
anchors.right: parent.right
anchors.rightMargin: Theme.padS
anchors.verticalCenter: parent.verticalCenter
text: apRow.modelData.known ? "SAVED" : ""
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1
renderType: Text.NativeRendering
}
HoverHandler {
id: apHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
if (apRow.modelData.connected) {
apRow.modelData.disconnect();
} else {
// Saved networks connect directly; new secured ones
// need a passphrase, which NetworkManager's own
// agent will prompt for.
apRow.modelData.connect();
}
}
}
}
}
}
component RateBlock: Item {
id: rateBlock
property string glyph: ""
property string label: ""
property string value: ""
property color tone: Theme.primary
implicitHeight: 40
Rectangle {
anchors.fill: parent
color: Theme.alpha(rateBlock.tone, 0.08)
border.width: 1
border.color: Theme.alpha(rateBlock.tone, 0.35)
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -Theme.skew, 0, rateBlock.height * Theme.skew,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
Row {
anchors.centerIn: parent
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
text: rateBlock.glyph
color: rateBlock.tone
font.pixelSize: Theme.fsLarge
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: -1
Text {
text: rateBlock.label.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: 8
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
text: rateBlock.value
color: Theme.text
font.family: Theme.fontMono
font.pixelSize: Theme.fsSmall
font.weight: Font.DemiBold
renderType: Text.NativeRendering
}
}
}
}
}
+113
View File
@@ -0,0 +1,113 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "root:/config"
import "root:/components"
import "root:/services"
// Notification history and the do-not-disturb switch.
Popout {
id: root
contentWidth: 400
contentHeight: 380
PopoutSurface {
anchors.fill: parent
tone: Notifs.dnd ? Theme.muted : Theme.primary
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "Notifications"
icon: Notifs.icon
accent: Notifs.dnd ? Theme.muted : Theme.primary
subtitle: Notifs.count + " HELD"
}
Row {
id: controls
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
spacing: Theme.padS
P5Button {
text: Notifs.dnd ? "Silenced" : "Silence"
icon: Notifs.dnd ? "󰂛" : "󰂚"
accent: Notifs.dnd ? Theme.accent : Theme.primary
onClicked: Notifs.toggleDnd()
}
P5Button {
text: "Clear"
icon: "󰎟"
destructive: true
onClicked: Notifs.clearAll()
}
}
Text {
anchors.centerIn: parent
visible: Notifs.count === 0
text: "NOTHING TO REPORT"
color: Theme.muted
font.family: Theme.fontDisplay
font.pixelSize: Theme.fsBody
font.italic: true
font.weight: Font.Black
font.letterSpacing: 3
renderType: Text.NativeRendering
}
ListView {
anchors.top: controls.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
spacing: Theme.padS
model: Notifs.history
delegate: NotifCard {
required property var modelData
width: ListView.view.width
notif: modelData
onDismissed: Notifs.dismiss(modelData)
}
add: Transition {
NumberAnimation {
properties: "x"
from: 40
duration: Theme.durSlow
easing.type: Easing.OutBack
}
NumberAnimation {
property: "opacity"
from: 0; to: 1
duration: Theme.durBase
}
}
remove: Transition {
NumberAnimation {
property: "opacity"
to: 0
duration: Theme.durFast
}
}
displaced: Transition {
NumberAnimation {
properties: "y"
duration: Theme.durBase
easing.type: Easing.OutExpo
}
}
}
}
}
+73
View File
@@ -0,0 +1,73 @@
import QtQuick
import "root:/config"
import "root:/components"
// Title bar for a popout: glyph, split-ink heading, right-aligned status text,
// and a crimson rule with a chopped tail.
Item {
id: root
property string title: ""
property string subtitle: ""
property string icon: ""
property color accent: Theme.primary
implicitHeight: 34
Row {
anchors.left: parent.left
anchors.top: parent.top
spacing: Theme.padS
Icon {
anchors.verticalCenter: parent.verticalCenter
visible: root.icon !== ""
text: root.icon
color: root.accent
font.pixelSize: Theme.fsTitle
}
SplitText {
anchors.verticalCenter: parent.verticalCenter
text: root.title.toUpperCase()
pixelSize: Theme.fsLarge
letterSpacing: 1.5
split: 1.3
splitOpacity: 0.7
}
}
Text {
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 6
visible: root.subtitle !== ""
text: root.subtitle
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1
renderType: Text.NativeRendering
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 22
height: 2
color: Theme.alpha(root.accent, 0.8)
}
Rectangle {
anchors.bottom: parent.bottom
anchors.right: parent.right
width: 16
height: 2
color: Theme.accent
transform: Matrix4x4 {
matrix: Qt.matrix4x4(1, -1.4, 0, 2.8,
0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import QtQuick
import "root:/config"
import "root:/components"
// The slab every popout sits on. Pure preset — it declares no children of its
// own, so P5Panel's default content property stays usable by callers.
P5Panel {
property color tone: Theme.primary
fill: Theme.mantle
fillOpacity: 0.97
border: Theme.alpha(tone, 0.75)
borderWidth: Theme.stroke
halftone: true
halftoneColor: tone
halftoneOpacity: 0.06
sheen: true
slash: true
slashColor: tone
slashWidth: 5
lean: 0.06
chop: 16
padding: Theme.padM
}
+142
View File
@@ -0,0 +1,142 @@
import QtQuick
import Quickshell
import "root:/config"
import "root:/components"
import "root:/services"
// The resource dashboard: four gauges, then the details that do not fit on one.
Popout {
id: root
contentWidth: 420
contentHeight: 268
PopoutSurface {
anchors.fill: parent
tone: Theme.heat(Math.max(Sys.cpuUsage, Sys.memPercent))
PopoutHeader {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
title: "System"
icon: "󰍛"
accent: Theme.primary
subtitle: "UP " + Sys.uptime + " · " + Sys.procs + " PROCS"
}
Row {
id: gauges
anchors.top: header.bottom
anchors.topMargin: Theme.padM
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.padM
Gauge {
width: 88; height: 88
value: Sys.cpuUsage
label: "CPU"
sub: Math.round(Sys.cpuTemp) + "°C"
}
Gauge {
width: 88; height: 88
value: Sys.memPercent
label: "MEM"
sub: Sys.gb(Sys.memUsed) + "/" + Sys.gb(Sys.memTotal)
}
Gauge {
width: 88; height: 88
visible: Sys.gpuAvailable
value: Sys.gpuUsage
label: "GPU"
sub: Math.round(Sys.gpuTemp) + "°C"
}
Gauge {
width: 88; height: 88
value: Sys.diskPercent
label: "DISK"
sub: Sys.diskFree + " free"
}
}
Column {
anchors.top: gauges.bottom
anchors.topMargin: Theme.padM
anchors.left: parent.left
anchors.right: parent.right
spacing: 3
InfoRow {
width: parent.width
key: "Processor"
value: Sys.cpuModel
}
InfoRow {
width: parent.width
key: "Load"
value: Sys.load1.toFixed(2) + " " + Sys.load5.toFixed(2) + " " + Sys.load15.toFixed(2)
}
InfoRow {
width: parent.width
visible: Sys.gpuAvailable
key: "VRAM"
value: Sys.gb(Sys.gpuVramUsed) + " / " + Sys.gb(Sys.gpuVramTotal)
}
InfoRow {
width: parent.width
key: "Root"
value: Sys.gb(Sys.diskUsed) + " of " + Sys.gb(Sys.diskTotal) + " used"
}
InfoRow {
width: parent.width
key: "Network"
value: Sys.netUp
? Sys.netInterface + " ↓ " + Sys.rate(Sys.netRx) + " ↑ " + Sys.rate(Sys.netTx)
: "offline"
}
}
}
component InfoRow: Item {
id: infoRow
property string key: ""
property string value: ""
implicitHeight: visible ? 17 : 0
Text {
id: keyLabel
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: infoRow.key.toUpperCase()
color: Theme.muted
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
font.letterSpacing: 1.5
renderType: Text.NativeRendering
}
Text {
anchors.right: parent.right
anchors.left: keyLabel.right
anchors.leftMargin: Theme.padM
anchors.verticalCenter: parent.verticalCenter
text: infoRow.value
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
color: Theme.subtext
font.family: Theme.fontMono
font.pixelSize: Theme.fsMicro
renderType: Text.NativeRendering
}
}
}