aboutsummaryrefslogtreecommitdiff
path: root/quickshell
diff options
context:
space:
mode:
Diffstat (limited to 'quickshell')
-rw-r--r--quickshell/.config/quickshell/Bar.qml60
-rw-r--r--quickshell/.config/quickshell/Theme.qml30
-rw-r--r--quickshell/.config/quickshell/components/BarBlock.qml32
-rw-r--r--quickshell/.config/quickshell/components/BarPopup.qml77
-rw-r--r--quickshell/.config/quickshell/components/Poller.qml28
-rw-r--r--quickshell/.config/quickshell/components/Separator.qml25
-rw-r--r--quickshell/.config/quickshell/modules/Cpu.qml43
-rw-r--r--quickshell/.config/quickshell/modules/DateWidget.qml150
-rw-r--r--quickshell/.config/quickshell/modules/DistroIcon.qml15
-rw-r--r--quickshell/.config/quickshell/modules/Network.qml71
-rw-r--r--quickshell/.config/quickshell/modules/Ram.qml28
-rw-r--r--quickshell/.config/quickshell/modules/TimeWidget.qml113
-rw-r--r--quickshell/.config/quickshell/modules/Tray.qml44
-rw-r--r--quickshell/.config/quickshell/modules/Volume.qml116
-rw-r--r--quickshell/.config/quickshell/modules/WindowTitle.qml29
-rw-r--r--quickshell/.config/quickshell/modules/WorkspaceButton.qml45
-rw-r--r--quickshell/.config/quickshell/modules/Workspaces.qml13
-rw-r--r--quickshell/.config/quickshell/services/Time.qml22
-rw-r--r--quickshell/.config/quickshell/shell.qml1095
19 files changed, 943 insertions, 1093 deletions
diff --git a/quickshell/.config/quickshell/Bar.qml b/quickshell/.config/quickshell/Bar.qml
new file mode 100644
index 0000000..9a9c269
--- /dev/null
+++ b/quickshell/.config/quickshell/Bar.qml
@@ -0,0 +1,60 @@
+import Quickshell
+import QtQuick
+import QtQuick.Layouts
+import qs
+import qs.components
+import qs.modules
+
+PanelWindow {
+ id: root
+ anchors.top: true
+ anchors.left: true
+ anchors.right: true
+ implicitHeight: 25
+ color: "transparent"
+
+ // Center: workspaces
+ Workspaces {
+ id: workspacesRow
+ anchors.centerIn: parent
+ }
+
+ // Left: distro icon -> network -> window title
+ RowLayout {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ spacing: -1
+
+ DistroIcon {}
+ Separator { fillColor: Theme.bg2; backColor: Theme.bg1 }
+ Network {}
+ Separator { fillColor: Theme.bg1; backColor: Theme.bg2 }
+ WindowTitle {
+ id: windowTitle
+ maxChars: Math.max(5, Math.floor((workspacesRow.x - windowTitle.x - 80) / 8))
+ }
+ Separator { fillColor: Theme.bg2 }
+ }
+
+ // Right: tray -> volume -> ram -> cpu -> date -> time
+ RowLayout {
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ spacing: -1
+
+ Separator { pointLeft: true; fillColor: Theme.bg2 }
+ Tray {}
+ Separator { pointLeft: true; fillColor: Theme.bg1; backColor: Theme.bg2 }
+ Volume {}
+ Separator { pointLeft: true; fillColor: Theme.bg2; backColor: Theme.bg1 }
+ Ram {}
+ Separator { pointLeft: true; fillColor: Theme.bg1; backColor: Theme.bg2 }
+ Cpu {}
+ Separator { pointLeft: true; fillColor: Theme.bg2; backColor: Theme.bg1 }
+ DateWidget {}
+ Separator { pointLeft: true; fillColor: Theme.bg1; backColor: Theme.bg2 }
+ TimeWidget {}
+ }
+}
diff --git a/quickshell/.config/quickshell/Theme.qml b/quickshell/.config/quickshell/Theme.qml
new file mode 100644
index 0000000..2c08831
--- /dev/null
+++ b/quickshell/.config/quickshell/Theme.qml
@@ -0,0 +1,30 @@
+pragma Singleton
+import Quickshell
+import QtQuick
+
+Singleton {
+ // Colors
+ readonly property color bg1: "#003636"
+ readonly property color bg2: "#004344"
+ readonly property color fg: "#6ae8eb"
+ readonly property color accent: "#FFC500"
+ readonly property color bright: "#8affff"
+ readonly property color dim: "#4d7f7f"
+ readonly property color muted: "#707880"
+ readonly property color popupBg: Qt.rgba(0, 0.212, 0.212, 1)
+
+ // Fonts
+ readonly property string fontMono: "Source Code Pro"
+ readonly property string fontIcons: "Symbols Nerd Font"
+ readonly property string fontPowerline: "Inconsolata for Powerline"
+ readonly property string fontCjk: "Noto Sans CJK SC"
+ readonly property string fontRunes: "Junicode"
+
+ // Sizes
+ readonly property int textSize: 14
+ readonly property int iconSize: 12
+ readonly property int sepSize: 25
+
+ // Planetary day-of-week symbols (Sun..Sat, Japanese convention)
+ readonly property var daySymbols: ["\u2609", "\u263d", "\u2642", "\u263f", "\u2643", "\u2640", "\u2644"]
+}
diff --git a/quickshell/.config/quickshell/components/BarBlock.qml b/quickshell/.config/quickshell/components/BarBlock.qml
new file mode 100644
index 0000000..a2a3247
--- /dev/null
+++ b/quickshell/.config/quickshell/components/BarBlock.qml
@@ -0,0 +1,32 @@
+import QtQuick
+import QtQuick.Layouts
+import qs
+
+// Colored container for a bar module: fills bar height, centers its
+// children in a RowLayout. Set `clickable: true` and handle `clicked()`
+// for modules that open popups.
+Rectangle {
+ id: block
+
+ property int padding: 16
+ property bool clickable: false
+ signal clicked()
+ property alias spacing: row.spacing
+ default property alias content: row.data
+
+ color: Theme.bg1
+ Layout.fillHeight: true
+ implicitWidth: row.implicitWidth + padding
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: block.clickable
+ onClicked: block.clicked()
+ }
+
+ RowLayout {
+ id: row
+ anchors.centerIn: parent
+ spacing: 4
+ }
+}
diff --git a/quickshell/.config/quickshell/components/BarPopup.qml b/quickshell/.config/quickshell/components/BarPopup.qml
new file mode 100644
index 0000000..5a17148
--- /dev/null
+++ b/quickshell/.config/quickshell/components/BarPopup.qml
@@ -0,0 +1,77 @@
+import Quickshell
+import Quickshell.Hyprland
+import QtQuick
+import qs
+
+// Shared popup for bar modules: anchored below the bar, focus-grabbed
+// (click outside to dismiss), with fade/scale animations.
+PopupWindow {
+ id: popup
+
+ required property Item anchorItem // bar module that owns this popup
+ property real xOffset: 0
+ property int contentMargins: 12
+ property bool showing: false
+ default property alias content: slot.data
+
+ anchor.window: anchorItem.QsWindow.window
+ anchor.rect.y: anchor.window?.height ?? 0
+ visible: false
+ color: "transparent"
+
+ function toggle() {
+ if (visible && showing)
+ close()
+ else
+ open()
+ }
+
+ function open() {
+ anchor.rect.x = anchorItem.mapToItem(null, 0, 0).x + xOffset
+ visible = true
+ showing = true
+ grab.active = true
+ }
+
+ function close() {
+ grab.active = false
+ showing = false
+ hideTimer.start()
+ }
+
+ HyprlandFocusGrab {
+ id: grab
+ windows: [popup]
+ onCleared: popup.close()
+ }
+
+ Timer {
+ id: hideTimer
+ interval: 160
+ onTriggered: popup.visible = false
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: Theme.popupBg
+ border.color: Theme.bright
+ border.width: 1
+
+ opacity: popup.showing ? 1.0 : 0.0
+ scale: popup.showing ? 1.0 : 0.95
+ transformOrigin: Item.Top
+
+ Behavior on opacity {
+ NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
+ }
+ Behavior on scale {
+ NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
+ }
+
+ Item {
+ id: slot
+ anchors.fill: parent
+ anchors.margins: popup.contentMargins
+ }
+ }
+}
diff --git a/quickshell/.config/quickshell/components/Poller.qml b/quickshell/.config/quickshell/components/Poller.qml
new file mode 100644
index 0000000..7fd9764
--- /dev/null
+++ b/quickshell/.config/quickshell/components/Poller.qml
@@ -0,0 +1,28 @@
+import Quickshell
+import Quickshell.Io
+import QtQuick
+
+// Periodically runs a command and emits its trimmed stdout.
+Scope {
+ id: poller
+
+ property alias command: proc.command
+ property alias interval: timer.interval
+ signal output(string text)
+
+ Timer {
+ id: timer
+ interval: 2000
+ running: true
+ repeat: true
+ triggeredOnStart: true
+ onTriggered: proc.running = true
+ }
+
+ Process {
+ id: proc
+ stdout: StdioCollector {
+ onStreamFinished: poller.output(this.text.trim())
+ }
+ }
+}
diff --git a/quickshell/.config/quickshell/components/Separator.qml b/quickshell/.config/quickshell/components/Separator.qml
new file mode 100644
index 0000000..01896ae
--- /dev/null
+++ b/quickshell/.config/quickshell/components/Separator.qml
@@ -0,0 +1,25 @@
+import QtQuick
+import QtQuick.Layouts
+import qs
+
+// Powerline transition glyph between bar modules.
+Text {
+ id: sep
+
+ property color fillColor // triangle (glyph) color
+ property color backColor: "transparent" // color behind the glyph
+ property bool pointLeft: false
+
+ text: pointLeft ? "\ue0b2" : "\ue0b0"
+ color: fillColor
+ font.family: Theme.fontPowerline
+ font.pixelSize: Theme.sepSize
+ Layout.fillHeight: true
+ verticalAlignment: Text.AlignVCenter
+
+ Rectangle {
+ anchors.fill: parent
+ color: sep.backColor
+ z: -1
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/Cpu.qml b/quickshell/.config/quickshell/modules/Cpu.qml
new file mode 100644
index 0000000..f0de5b8
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/Cpu.qml
@@ -0,0 +1,43 @@
+import QtQuick
+import qs
+import qs.components
+
+// CPU load over the last poll interval (delta of /proc/stat counters).
+BarBlock {
+ id: cpu
+ color: Theme.bg1
+
+ property real prevTotal: 0
+ property real prevIdle: 0
+
+ Text {
+ text: "\uf2db"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: Theme.iconSize
+ }
+
+ Text {
+ id: cpuText
+ text: "..."
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ }
+
+ Poller {
+ command: ["cat", "/proc/stat"]
+ interval: 2000
+ onOutput: text => {
+ const f = text.split("\n")[0].trim().split(/\s+/).slice(1).map(Number)
+ const idle = f[3] + f[4]
+ const total = f.reduce((a, b) => a + b, 0)
+ const dt = total - cpu.prevTotal
+ const di = idle - cpu.prevIdle
+ if (cpu.prevTotal > 0 && dt > 0)
+ cpuText.text = Math.round(100 * (dt - di) / dt) + "%"
+ cpu.prevTotal = total
+ cpu.prevIdle = idle
+ }
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/DateWidget.qml b/quickshell/.config/quickshell/modules/DateWidget.qml
new file mode 100644
index 0000000..b0629d2
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/DateWidget.qml
@@ -0,0 +1,150 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import qs
+import qs.components
+import qs.services
+
+// Date: full date in the bar, click for a calendar popup.
+BarBlock {
+ id: dateModule
+ color: Theme.bg2
+ clickable: true
+
+ property int calMonth: new Date().getMonth()
+ property int calYear: new Date().getFullYear()
+
+ onClicked: {
+ const now = new Date()
+ calMonth = now.getMonth()
+ calYear = now.getFullYear()
+ popup.toggle()
+ }
+
+ Text {
+ text: "\uf073"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: Theme.iconSize
+ }
+
+ Text {
+ text: Time.dateString
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ }
+
+ BarPopup {
+ id: popup
+ anchorItem: dateModule
+ xOffset: -40
+ contentMargins: 11
+ implicitWidth: 282
+ implicitHeight: 322
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 8
+
+ // Month/Year header with navigation
+ RowLayout {
+ Layout.fillWidth: true
+
+ Text {
+ text: "<"
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: 16
+ font.bold: true
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (dateModule.calMonth === 0) {
+ dateModule.calMonth = 11
+ dateModule.calYear--
+ } else {
+ dateModule.calMonth--
+ }
+ }
+ }
+ }
+
+ Text {
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignHCenter
+ text: dateModule.calYear + "\u5e74" + String(dateModule.calMonth + 1).padStart(2, '0') + "\u6708"
+ color: Theme.accent
+ font.family: Theme.fontCjk
+ font.pixelSize: 14
+ font.bold: true
+ }
+
+ Text {
+ text: ">"
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: 16
+ font.bold: true
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (dateModule.calMonth === 11) {
+ dateModule.calMonth = 0
+ dateModule.calYear++
+ } else {
+ dateModule.calMonth++
+ }
+ }
+ }
+ }
+ }
+
+ // Day of week headers
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 0
+
+ Repeater {
+ model: Theme.daySymbols
+
+ Text {
+ required property string modelData
+ Layout.fillWidth: true
+ text: modelData
+ color: Theme.accent
+ font.family: Theme.fontMono
+ font.pixelSize: 14
+ font.bold: true
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+ }
+
+ // Calendar grid
+ MonthGrid {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ month: dateModule.calMonth
+ year: dateModule.calYear
+ locale: Qt.locale("ja_JP")
+
+ delegate: Text {
+ required property var model
+ property bool isToday: model.day === Time.now.getDate() &&
+ model.month === Time.now.getMonth() &&
+ model.year === Time.now.getFullYear()
+ property bool isCurrentMonth: model.month === dateModule.calMonth
+
+ text: model.day
+ color: isToday ? Theme.accent : isCurrentMonth ? Theme.fg : Theme.dim
+ font.family: Theme.fontMono
+ font.pixelSize: 14
+ font.bold: isToday
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+ }
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/DistroIcon.qml b/quickshell/.config/quickshell/modules/DistroIcon.qml
new file mode 100644
index 0000000..256cd78
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/DistroIcon.qml
@@ -0,0 +1,15 @@
+import QtQuick
+import qs
+import qs.components
+
+// Distro icon (Arch).
+BarBlock {
+ color: Theme.bg2
+
+ Text {
+ text: "\u{f08c7}"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: 14
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/Network.qml b/quickshell/.config/quickshell/modules/Network.qml
new file mode 100644
index 0000000..bc7856f
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/Network.qml
@@ -0,0 +1,71 @@
+import QtQuick
+import qs
+import qs.components
+
+// Network: WiFi (iface + SSID + IP) or Ethernet (iface + IP), 5s poll.
+BarBlock {
+ id: net
+ color: Theme.bg1
+
+ property string netType: "" // "wifi" | "eth" | "disconnected" | "" (initial)
+ property string iface: ""
+ property string ssid: "..."
+ property string ip: ""
+
+ Text {
+ text: net.netType === "eth" ? "\uef44"
+ : net.netType === "disconnected" ? "\u{f16bc}" : "\uf1eb"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: Theme.iconSize
+ }
+
+ Text {
+ text: net.iface
+ color: Theme.accent
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ visible: text !== ""
+ }
+
+ Text {
+ text: net.netType === "disconnected" ? "DISCONNECTED" : net.ssid
+ color: net.netType === "disconnected" ? Theme.muted : Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ font.bold: net.netType !== "disconnected"
+ visible: text !== ""
+ }
+
+ Text {
+ text: net.ip
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ visible: text !== ""
+ }
+
+ Poller {
+ interval: 5000
+ command: ["sh", "-c", "WIFACE=$(ip -o link show up | grep -oP 'wl[^:]+' | head -1); EIFACE=$(ip -o link show up | grep -oP 'en[^:]+' | head -1); if [ -n \"$WIFACE\" ]; then SSID=$(iwgetid -r 2>/dev/null); if [ -n \"$SSID\" ]; then IP=$(ip -4 -o addr show $WIFACE 2>/dev/null | cut -d' ' -f7 | cut -d/ -f1); echo \"wifi|$WIFACE|$SSID|$IP\"; else echo disconnected; fi; elif [ -n \"$EIFACE\" ]; then IP=$(ip -4 -o addr show $EIFACE 2>/dev/null | cut -d' ' -f7 | cut -d/ -f1); echo \"eth|$EIFACE|$IP\"; else echo disconnected; fi"]
+ onOutput: text => {
+ const parts = text.split("|")
+ if (parts[0] === "wifi") {
+ net.netType = "wifi"
+ net.iface = parts[1] || ""
+ net.ssid = parts[2] || ""
+ net.ip = parts[3] || ""
+ } else if (parts[0] === "eth") {
+ net.netType = "eth"
+ net.iface = parts[1] || ""
+ net.ssid = ""
+ net.ip = parts[2] || ""
+ } else {
+ net.netType = "disconnected"
+ net.iface = ""
+ net.ssid = ""
+ net.ip = ""
+ }
+ }
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/Ram.qml b/quickshell/.config/quickshell/modules/Ram.qml
new file mode 100644
index 0000000..7c58a5c
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/Ram.qml
@@ -0,0 +1,28 @@
+import QtQuick
+import qs
+import qs.components
+
+// RAM usage: used/total in GB, refreshed every 2s.
+BarBlock {
+ color: Theme.bg2
+
+ Text {
+ text: "\uefc5"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: Theme.iconSize
+ }
+
+ Text {
+ id: ramText
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ }
+
+ Poller {
+ command: ["sh", "-c", "free -b | awk '/^Mem:/ {printf \"%.1f/%.1fGB\", $3/1073741824, $2/1073741824}'"]
+ interval: 2000
+ onOutput: text => ramText.text = text
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/TimeWidget.qml b/quickshell/.config/quickshell/modules/TimeWidget.qml
new file mode 100644
index 0000000..34d915c
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/TimeWidget.qml
@@ -0,0 +1,113 @@
+import QtQuick
+import QtQuick.Layouts
+import qs
+import qs.components
+import qs.services
+
+// Time: HH:mm:ss in the bar, click for an analog clock popup.
+BarBlock {
+ id: timeModule
+ color: Theme.bg1
+ clickable: true
+ onClicked: popup.toggle()
+
+ Text {
+ text: "\u{f0954}"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: Theme.iconSize
+ }
+
+ Text {
+ text: Time.hhmmss
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ }
+
+ BarPopup {
+ id: popup
+ anchorItem: timeModule
+ xOffset: -100
+ contentMargins: 15
+ implicitWidth: 220
+ implicitHeight: 240
+
+ Canvas {
+ id: clockCanvas
+ anchors.fill: parent
+
+ property date now: Time.now
+ onNowChanged: if (popup.visible) requestPaint()
+
+ onPaint: {
+ const ctx = getContext("2d")
+ const size = Math.min(width, height)
+ const cx = width / 2
+ const cy = height / 2
+ const r = size / 2 - 5
+ const hours = now.getHours()
+ const minutes = now.getMinutes()
+ const seconds = now.getSeconds()
+
+ ctx.clearRect(0, 0, width, height)
+
+ // Clock face circle
+ ctx.beginPath()
+ ctx.arc(cx, cy, r, 0, 2 * Math.PI)
+ ctx.strokeStyle = Theme.bright
+ ctx.lineWidth = 1.5
+ ctx.stroke()
+
+ // Hour markers
+ for (let i = 0; i < 12; i++) {
+ const angle = (i * 30 - 90) * Math.PI / 180
+ const inner = i % 3 === 0 ? r - 12 : r - 8
+ const outer = r - 3
+ ctx.beginPath()
+ ctx.moveTo(cx + inner * Math.cos(angle), cy + inner * Math.sin(angle))
+ ctx.lineTo(cx + outer * Math.cos(angle), cy + outer * Math.sin(angle))
+ ctx.strokeStyle = i % 3 === 0 ? Theme.accent : Theme.fg
+ ctx.lineWidth = i % 3 === 0 ? 2 : 1
+ ctx.stroke()
+ }
+
+ // Hour hand
+ const hAngle = ((hours % 12) * 30 + minutes * 0.5 - 90) * Math.PI / 180
+ ctx.beginPath()
+ ctx.moveTo(cx, cy)
+ ctx.lineTo(cx + r * 0.5 * Math.cos(hAngle), cy + r * 0.5 * Math.sin(hAngle))
+ ctx.strokeStyle = Theme.accent
+ ctx.lineWidth = 3
+ ctx.lineCap = "round"
+ ctx.stroke()
+
+ // Minute hand
+ const mAngle = (minutes * 6 + seconds * 0.1 - 90) * Math.PI / 180
+ ctx.beginPath()
+ ctx.moveTo(cx, cy)
+ ctx.lineTo(cx + r * 0.7 * Math.cos(mAngle), cy + r * 0.7 * Math.sin(mAngle))
+ ctx.strokeStyle = Theme.fg
+ ctx.lineWidth = 2
+ ctx.lineCap = "round"
+ ctx.stroke()
+
+ // Second hand
+ const sAngle = (seconds * 6 - 90) * Math.PI / 180
+ ctx.beginPath()
+ ctx.moveTo(cx, cy)
+ ctx.lineTo(cx + r * 0.8 * Math.cos(sAngle), cy + r * 0.8 * Math.sin(sAngle))
+ ctx.strokeStyle = Theme.bright
+ ctx.lineWidth = 1
+ ctx.lineCap = "round"
+ ctx.stroke()
+
+ // Center dot
+ ctx.beginPath()
+ ctx.arc(cx, cy, 3, 0, 2 * Math.PI)
+ ctx.fillStyle = Theme.accent
+ ctx.fill()
+ }
+ }
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/Tray.qml b/quickshell/.config/quickshell/modules/Tray.qml
new file mode 100644
index 0000000..cca3936
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/Tray.qml
@@ -0,0 +1,44 @@
+import QtQuick
+import Quickshell
+import Quickshell.Services.SystemTray
+import qs
+import qs.components
+
+// System tray icons; left click activates, right click opens the menu.
+BarBlock {
+ color: Theme.bg2
+ padding: 12
+ spacing: 6
+
+ Text {
+ text: "\uf013"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: Theme.iconSize
+ }
+
+ Repeater {
+ model: SystemTray.items
+
+ Image {
+ id: trayIcon
+ required property SystemTrayItem modelData
+ source: modelData.icon
+ sourceSize.width: 16
+ sourceSize.height: 16
+ width: 16
+ height: 16
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onClicked: mouse => {
+ if (mouse.button === Qt.LeftButton)
+ trayIcon.modelData.activate()
+ else
+ trayIcon.modelData.display(trayIcon.QsWindow.window, mouse.x, mouse.y)
+ }
+ }
+ }
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/Volume.qml b/quickshell/.config/quickshell/modules/Volume.qml
new file mode 100644
index 0000000..a8ff7f3
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/Volume.qml
@@ -0,0 +1,116 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Services.Pipewire
+import qs
+import qs.components
+
+// Volume: shows default sink volume, click for a slider popup (0-300%).
+BarBlock {
+ id: volModule
+ color: Theme.bg1
+ clickable: true
+ onClicked: popup.toggle()
+
+ readonly property var sink: Pipewire.defaultAudioSink
+ readonly property real volPercent: sink && sink.audio ? Math.round(sink.audio.volume * 100) : 0
+ readonly property bool isMuted: sink && sink.audio ? sink.audio.muted : false
+
+ PwObjectTracker { objects: [ Pipewire.defaultAudioSink ] }
+
+ Text {
+ text: volModule.isMuted ? "\u{f0581}" : "\uf028"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: Theme.iconSize
+ }
+
+ Text {
+ text: volModule.isMuted ? "MUTED" : volModule.volPercent + "%"
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ }
+
+ BarPopup {
+ id: popup
+ anchorItem: volModule
+ xOffset: -40
+ implicitWidth: 200
+ implicitHeight: 80
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ RowLayout {
+ Layout.alignment: Qt.AlignHCenter
+ spacing: 6
+
+ Text {
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: 18
+ text: volSlider.value === 0 ? "\uf026" : volSlider.value <= 50 ? "\uf027" : "\uf028"
+ }
+
+ Text {
+ color: Theme.accent
+ font.family: Theme.fontMono
+ font.pixelSize: 17
+ font.bold: true
+ text: Math.round(volSlider.value) + "%"
+ }
+ }
+
+ Item {
+ id: volSlider
+ Layout.fillWidth: true
+ height: 20
+
+ // Show the drag position while pressed, otherwise track the sink.
+ // (Never assign to `value` -- that would break this binding.)
+ property real value: sliderMouse.pressed ? sliderMouse.dragValue : volModule.volPercent
+
+ Rectangle {
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width
+ height: 4
+ radius: 2
+ color: Theme.dim
+
+ Rectangle {
+ width: Math.max(0, Math.min(1, volSlider.value / 300)) * parent.width
+ height: parent.height
+ color: volSlider.value > 100 ? Theme.accent : Theme.bright
+ radius: 2
+ }
+ }
+
+ Rectangle {
+ x: Math.max(0, Math.min(1, volSlider.value / 300)) * (parent.width - width)
+ anchors.verticalCenter: parent.verticalCenter
+ width: 12
+ height: 12
+ radius: 6
+ color: volSlider.value > 100 ? Theme.accent : Theme.fg
+ }
+
+ MouseArea {
+ id: sliderMouse
+ anchors.fill: parent
+ property real dragValue: 0
+
+ onPressed: mouse => updateVol(mouse)
+ onPositionChanged: mouse => { if (pressed) updateVol(mouse) }
+
+ function updateVol(mouse) {
+ const ratio = Math.max(0, Math.min(1, mouse.x / width))
+ dragValue = Math.round(ratio * 300)
+ if (volModule.sink && volModule.sink.audio)
+ volModule.sink.audio.volume = dragValue / 100
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/WindowTitle.qml b/quickshell/.config/quickshell/modules/WindowTitle.qml
new file mode 100644
index 0000000..64f9b79
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/WindowTitle.qml
@@ -0,0 +1,29 @@
+import QtQuick
+import Quickshell.Hyprland
+import qs
+import qs.components
+
+// Active window title, truncated to fit the space left of the workspaces.
+BarBlock {
+ id: windowModule
+ color: Theme.bg2
+
+ property int maxChars: 60
+
+ Text {
+ text: "\uf2d0"
+ color: Theme.fg
+ font.family: Theme.fontIcons
+ font.pixelSize: Theme.iconSize
+ }
+
+ Text {
+ readonly property string fullTitle: Hyprland.activeToplevel?.title ?? ""
+ text: fullTitle.length > windowModule.maxChars
+ ? fullTitle.substring(0, windowModule.maxChars - 3) + "..."
+ : fullTitle
+ color: Theme.fg
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.textSize
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/WorkspaceButton.qml b/quickshell/.config/quickshell/modules/WorkspaceButton.qml
new file mode 100644
index 0000000..5f3aa46
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/WorkspaceButton.qml
@@ -0,0 +1,45 @@
+import QtQuick
+import Quickshell.Hyprland
+import qs
+
+Item {
+ id: button
+
+ required property int index
+ required property string modelData
+ readonly property int wsId: index + 1
+ readonly property string wsStatus: {
+ if ((Hyprland.focusedWorkspace?.id ?? -1) === wsId)
+ return "focused"
+ const ws = Hyprland.workspaces.values
+ for (let i = 0; i < ws.length; i++)
+ if (ws[i].id === wsId)
+ return "occupied"
+ return "empty"
+ }
+
+ width: wsLabel.implicitWidth
+ height: wsLabel.implicitHeight
+
+ Text {
+ id: wsLabel
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.verticalCenterOffset: button.wsId >= 11 ? 4 : 0
+
+ text: button.modelData
+ color: button.wsStatus === "focused" ? Theme.accent
+ : button.wsStatus === "occupied" ? Theme.bright : Theme.dim
+ font.family: button.wsId >= 11 ? Theme.fontRunes : Theme.fontCjk
+ font.pixelSize: button.wsId >= 11 ? 17 : 13
+ font.bold: button.wsStatus === "focused"
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ anchors.margins: -4 // hit target slightly larger than the glyph
+ // This Hyprland build routes dispatches through its Lua config:
+ // the classic "workspace N" string is a Lua syntax error there.
+ onClicked: Hyprland.dispatch("hl.dsp.focus({ workspace = " + button.wsId + " })")
+ }
+}
diff --git a/quickshell/.config/quickshell/modules/Workspaces.qml b/quickshell/.config/quickshell/modules/Workspaces.qml
new file mode 100644
index 0000000..df536e3
--- /dev/null
+++ b/quickshell/.config/quickshell/modules/Workspaces.qml
@@ -0,0 +1,13 @@
+import QtQuick
+import QtQuick.Layouts
+import qs
+
+// Hyprland workspaces 1-12: Chinese numerals + runes. Click to switch.
+RowLayout {
+ spacing: 12
+
+ Repeater {
+ model: ["\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341", "\u16a8", "\u16a0"]
+ WorkspaceButton {}
+ }
+}
diff --git a/quickshell/.config/quickshell/services/Time.qml b/quickshell/.config/quickshell/services/Time.qml
new file mode 100644
index 0000000..f5e07fc
--- /dev/null
+++ b/quickshell/.config/quickshell/services/Time.qml
@@ -0,0 +1,22 @@
+pragma Singleton
+import Quickshell
+import QtQuick
+import qs
+
+Singleton {
+ id: root
+
+ readonly property date now: clock.date
+ readonly property string hhmmss: Qt.formatDateTime(clock.date, "HH:mm:ss")
+ readonly property string dateString: {
+ const d = clock.date
+ const m = String(d.getMonth() + 1).padStart(2, '0')
+ const day = String(d.getDate()).padStart(2, '0')
+ return d.getFullYear() + "\u5e74" + m + "\u6708" + day + "\u65e5 (" + Theme.daySymbols[d.getDay()] + ")"
+ }
+
+ SystemClock {
+ id: clock
+ precision: SystemClock.Seconds
+ }
+}
diff --git a/quickshell/.config/quickshell/shell.qml b/quickshell/.config/quickshell/shell.qml
index 8012d66..ae374aa 100644
--- a/quickshell/.config/quickshell/shell.qml
+++ b/quickshell/.config/quickshell/shell.qml
@@ -17,1099 +17,8 @@
// ║ A Quickshell status bar for Hyprland ║
// ╚══════════════════════════════════════════════════════════════════╝
-// ╔══════════════════════════╗
-// ║ 📦 IMPORTS ║
-// ╚══════════════════════════╝
import Quickshell
-import Quickshell.Hyprland
-import Quickshell.Io
-import QtQuick
-import QtQuick.Layouts
-import QtQuick.Controls
-import Quickshell.Services.SystemTray
-import Quickshell.Services.Pipewire
-PanelWindow {
- id: root
- anchors.top: true
- anchors.left: true
- anchors.right: true
- implicitHeight: 25
- color: "transparent"
-
- // ╔══════════════════════════╗
- // ║ 🎨 THEME ║
- // ╚══════════════════════════╝
- property color bg1: "#003636"
- property color bg2: "#004344"
- property color fg: "#6ae8eb"
-
- // ╔══════════════════════════╗
- // ║ 🗂️ WORKSPACE STATE ║
- // ╚══════════════════════════╝
- property var wsValues: Hyprland.workspaces.values
- property int focusedId: Hyprland.focusedWorkspace?.id ?? -1
-
- function wsState(id) {
- if (focusedId === id) return "focused"
- for (var i = 0; i < wsValues.length; i++) {
- if (wsValues[i].id === id) return "occupied"
- }
- return "empty"
- }
-
- // ╔══════════════════════════════════════════════════════════════╗
- // ║ 🏢 CENTER: WORKSPACES ║
- // ╚══════════════════════════════════════════════════════════════╝
- RowLayout {
- id: workspacesRow
- anchors.centerIn: parent
- spacing: 12
-
- Repeater {
- model: ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "ᚨ", "ᚠ"]
-
- Item {
- required property int index
- required property string modelData
- property int wsId: index + 1
- property string state: wsState(wsId)
-
- width: wsLabel.implicitWidth
- height: wsLabel.implicitHeight
-
- Text {
- id: wsLabel
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.verticalCenter: parent.verticalCenter
- anchors.verticalCenterOffset: parent.wsId >= 11 ? 4 : 0
-
- text: parent.modelData
- color: parent.state === "focused" ? "#FFC500" : parent.state === "occupied" ? "#8affff" : "#4d7f7f"
- font.family: parent.wsId >= 11 ? "Junicode" : "Noto Sans CJK SC"
- font.pixelSize: parent.wsId >= 11 ? 17 : 13
- font.bold: parent.state === "focused"
- }
-
- MouseArea {
- anchors.fill: parent
- onClicked: Hyprland.dispatch("workspace " + wsId)
- }
- }
- }
- }
-
- // ╔══════════════════════════════════════════════════════════════╗
- // ║ ◀️ LEFT MODULES ║
- // ║ Distro Icon → Network → Window Title ║
- // ╚══════════════════════════════════════════════════════════════╝
- RowLayout {
- anchors.left: parent.left
- anchors.top: parent.top
- anchors.bottom: parent.bottom
- spacing: -1
-
- // ── 🐧 Distro Icon ────────────────────────────────────────
- Rectangle {
- id: distroModule
- color: root.bg2
- Layout.fillHeight: true
- implicitWidth: distroIcon.implicitWidth + 16
-
- Text {
- id: distroIcon
- anchors.centerIn: parent
- text: "󰣇"
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 14
- }
-
- }
-
- // Powerline separator: bg2 -> bg1
- Text {
- text: ""
- color: root.bg2
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
-
- Rectangle {
- anchors.fill: parent
- color: root.bg1
- z: -1
- }
- }
-
- // ── 📶 Network (WiFi / Ethernet) ──────────────────────────
- Rectangle {
- color: root.bg1
- Layout.fillHeight: true
- implicitWidth: wlanRow.implicitWidth + 16
-
- RowLayout {
- id: wlanRow
- anchors.centerIn: parent
- spacing: 4
-
- Text {
- text: ""
- id: wlanIcon
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 12
- }
-
- Text {
- id: wlanIface
- text: "wlo1"
- color: "#FFC500"
- font.family: "Source Code Pro"
- font.pixelSize: 14
- visible: text !== ""
- }
-
- Text {
- id: wlanSsid
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 14
- font.bold: true
- text: "..."
- }
-
- Text {
- id: wlanIp
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 14
- visible: text !== ""
- }
- }
-
- Timer {
- interval: 5000
- running: true
- repeat: true
- triggeredOnStart: true
- onTriggered: wlanProc.running = true
- }
-
- Process {
- id: wlanProc
- command: ["sh", "-c", "WIFACE=$(ip -o link show up | grep -oP 'wl[^:]+' | head -1); EIFACE=$(ip -o link show up | grep -oP 'en[^:]+' | head -1); if [ -n \"$WIFACE\" ]; then SSID=$(iwgetid -r 2>/dev/null); if [ -n \"$SSID\" ]; then IP=$(ip -4 -o addr show $WIFACE 2>/dev/null | cut -d' ' -f7 | cut -d/ -f1); echo \"wifi|$WIFACE|$SSID|$IP\"; else echo disconnected; fi; elif [ -n \"$EIFACE\" ]; then IP=$(ip -4 -o addr show $EIFACE 2>/dev/null | cut -d' ' -f7 | cut -d/ -f1); echo \"eth|$EIFACE|$IP\"; else echo disconnected; fi"]
- stdout: StdioCollector {
- onStreamFinished: {
- var parts = this.text.trim().split("|")
- var type = parts[0]
- if (type === "wifi") {
- wlanIcon.text = ""
- wlanIface.text = parts[1] || ""
- wlanSsid.text = parts[2] || ""
- wlanSsid.color = root.fg
- wlanSsid.font.bold = true
- wlanIp.text = parts[3] || ""
- } else if (type === "eth") {
- wlanIcon.text = ""
- wlanIface.text = parts[1] || ""
- wlanSsid.text = ""
- wlanIp.text = parts[2] || ""
- } else {
- wlanIcon.text = "󱚼"
- wlanIface.text = ""
- wlanSsid.text = "DISCONNECTED"
- wlanSsid.color = "#707880"
- wlanSsid.font.bold = false
- wlanIp.text = ""
- }
- }
- }
- }
- }
-
- // Powerline separator: bg1 -> bg2
- Text {
- text: ""
- color: root.bg1
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
-
- Rectangle {
- anchors.fill: parent
- color: root.bg2
- z: -1
- }
- }
-
- // ── 🪟 Window Title ───────────────────────────────────────
- Rectangle {
- id: windowModule
- color: root.bg2
- Layout.fillHeight: true
- implicitWidth: windowRow.implicitWidth + 16
-
- RowLayout {
- id: windowRow
- anchors.centerIn: parent
- spacing: 4
-
- Text {
- text: ""
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 12
- }
-
- Text {
- id: windowTitle
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 14
- property string fullTitle: Hyprland.activeToplevel?.title ?? ""
- property int maxChars: Math.max(5, Math.floor((workspacesRow.x - windowModule.x - 80) / 8))
- text: fullTitle.length > maxChars ? fullTitle.substring(0, maxChars - 3) + "..." : fullTitle
- }
- }
- }
-
- // Powerline separator: bg2 -> transparent
- Text {
- text: ""
- color: root.bg2
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
- }
- }
-
- // ╔══════════════════════════════════════════════════════════════╗
- // ║ ▶️ RIGHT MODULES ║
- // ║ Tray → Volume → RAM → CPU → Date → Time ║
- // ╚══════════════════════════════════════════════════════════════╝
- RowLayout {
- anchors.right: parent.right
- anchors.top: parent.top
- anchors.bottom: parent.bottom
- spacing: -1
-
- // Powerline separator: transparent -> bg2
- Text {
- text: ""
- color: root.bg2
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
- }
-
- // ── 📌 System Tray ────────────────────────────────────────
- Rectangle {
- color: root.bg2
- Layout.fillHeight: true
- implicitWidth: trayRow.implicitWidth + 12
- // visible: SystemTray.items.values.length > 0
-
- RowLayout {
- id: trayRow
- anchors.centerIn: parent
- spacing: 6
-
- Text {
- text: ""
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 12
- }
-
- Repeater {
- model: SystemTray.items
-
- Image {
- required property SystemTrayItem modelData
- source: modelData.icon
- sourceSize.width: 16
- sourceSize.height: 16
- width: 16
- height: 16
-
- MouseArea {
- anchors.fill: parent
- acceptedButtons: Qt.LeftButton | Qt.RightButton
- onClicked: function(mouse) {
- if (mouse.button === Qt.LeftButton) {
- modelData.activate()
- } else {
- modelData.display(root, mouse.x, mouse.y)
- }
- }
- }
- }
- }
- }
- }
-
- // Powerline separator: bg2 -> bg1
- Text {
- text: ""
- color: root.bg1
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
-
- Rectangle {
- anchors.fill: parent
- color: root.bg2
- z: -1
- }
- }
-
- // ── 🔊 Volume ─────────────────────────────────────────────
- Rectangle {
- id: volModule
- color: root.bg1
- Layout.fillHeight: true
- implicitWidth: volRow.implicitWidth + 16
-
- MouseArea {
- anchors.fill: parent
- onClicked: {
- if (!volPopup.visible) {
- var pos = volModule.mapToItem(null, 0, 0)
- volPopup.popupX = pos.x - 40
- volPopup.visible = true
- volPopup.showing = true
- volGrab.active = true
- } else {
- volGrab.active = false
- volPopup.showing = false
- volCloseTimer.start()
- }
- }
- }
-
- Timer {
- id: volCloseTimer
- interval: 160
- onTriggered: volPopup.visible = false
- }
-
- PopupWindow {
- id: volPopup
- anchor.window: root
- property real popupX: 0
- anchor.rect.x: popupX
- anchor.rect.y: root.height
- visible: false
- width: 200
- height: 80
- color: "transparent"
-
- property bool showing: false
- property var sink: Pipewire.defaultAudioSink
- property real currentVol: sink && sink.audio ? Math.round(sink.audio.volume * 100) : 0
- property bool isMuted: sink && sink.audio ? sink.audio.muted : false
-
- HyprlandFocusGrab {
- id: volGrab
- windows: [volPopup]
- onCleared: {
- volPopup.showing = false
- volCloseTimer.start()
- }
- }
-
- Rectangle {
- anchors.fill: parent
- color: Qt.rgba(0, 0.212, 0.212, 1)
- border.color: "#8affff"
- border.width: 1
-
- opacity: volPopup.showing ? 1.0 : 0.0
- scale: volPopup.showing ? 1.0 : 0.95
- transformOrigin: Item.Top
-
- Behavior on opacity {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
- Behavior on scale {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
- }
-
- ColumnLayout {
- anchors.fill: parent
- anchors.margins: 12
- spacing: 0
-
- opacity: volPopup.showing ? 1.0 : 0.0
- Behavior on opacity {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
-
- RowLayout {
- Layout.alignment: Qt.AlignHCenter
- spacing: 6
-
- Text {
- id: volPopupIcon
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 18
- text: volSlider.value === 0 ? "" : volSlider.value <= 50 ? "" : ""
- }
-
- Text {
- id: volSliderLabel
- color: "#FFC500"
- font.family: "Source Code Pro"
- font.pixelSize: 17
- font.bold: true
- text: Math.round(volSlider.value) + "%"
- }
- }
-
- Item {
- id: volSlider
- Layout.fillWidth: true
- height: 20
-
- property real value: volPopup.currentVol
- property bool pressed: sliderMouse.pressed
-
- Rectangle {
- anchors.verticalCenter: parent.verticalCenter
- width: parent.width
- height: 4
- radius: 2
- color: "#4d7f7f"
-
- Rectangle {
- width: Math.max(0, Math.min(1, volSlider.value / 300)) * parent.width
- height: parent.height
- color: volSlider.value > 100 ? "#FFC500" : "#8affff"
- radius: 2
- }
- }
-
- Rectangle {
- x: Math.max(0, Math.min(1, volSlider.value / 300)) * (parent.width - width)
- anchors.verticalCenter: parent.verticalCenter
- width: 12
- height: 12
- radius: 6
- color: volSlider.value > 100 ? "#FFC500" : "#6ae8eb"
- }
-
- MouseArea {
- id: sliderMouse
- anchors.fill: parent
- onPressed: function(mouse) { updateVol(mouse) }
- onPositionChanged: function(mouse) { if (pressed) updateVol(mouse) }
-
- function updateVol(mouse) {
- var ratio = Math.max(0, Math.min(1, mouse.x / width))
- var vol = Math.round(ratio * 300)
- volSlider.value = vol
- if (volPopup.sink && volPopup.sink.audio)
- volPopup.sink.audio.volume = vol / 100
- }
- }
- }
- }
-
- }
-
- RowLayout {
- id: volRow
- anchors.centerIn: parent
- spacing: 4
-
- Text {
- id: volIcon
- text: ""
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 12
- }
-
- Text {
- id: volText
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 14
- }
- }
-
- property var sink: Pipewire.defaultAudioSink
- property real volPercent: sink && sink.audio ? Math.round(sink.audio.volume * 100) : 0
- property bool isMuted: sink && sink.audio ? sink.audio.muted : false
-
- PwObjectTracker {
- objects: [ Pipewire.defaultAudioSink ]
- }
-
- Binding { target: volText; property: "text"; value: volModule.isMuted ? "MUTED" : volModule.volPercent + "%" }
- Binding { target: volIcon; property: "text"; value: volModule.isMuted ? "󰖁" : "" }
- }
-
- // Powerline separator: bg1 -> bg2
- Text {
- text: ""
- color: root.bg2
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
-
- Rectangle {
- anchors.fill: parent
- color: root.bg1
- z: -1
- }
- }
- // ── 🧠 RAM ────────────────────────────────────────────────
- Rectangle {
- color: root.bg2
- Layout.fillHeight: true
- implicitWidth: ramRow.implicitWidth + 16
-
- property string ramOutput: ""
-
- RowLayout {
- id: ramRow
- anchors.centerIn: parent
- spacing: 4
-
- Text {
- text: ""
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 12
- }
-
- Text {
- id: ramText
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 14
- }
- }
-
- Timer {
- interval: 2000
- running: true
- repeat: true
- triggeredOnStart: true
- onTriggered: ramProc.running = true
- }
-
- Process {
- id: ramProc
- command: ["sh", "-c", "free -b | awk '/^Mem:/ {printf \"%.1f/%.1fGB\", $3/1073741824, $2/1073741824}'"]
- stdout: StdioCollector {
- onStreamFinished: ramText.text = this.text.trim()
- }
- }
- }
-
- // Powerline separator: bg2 -> bg1
- Text {
- text: ""
- color: root.bg1
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
-
- Rectangle {
- anchors.fill: parent
- color: root.bg2
- z: -1
- }
- }
-
- // ── 🖥️ CPU ────────────────────────────────────────────────
- Rectangle {
- color: root.bg1
- Layout.fillHeight: true
- implicitWidth: cpuRow.implicitWidth + 16
-
- RowLayout {
- id: cpuRow
- anchors.centerIn: parent
- spacing: 4
-
- Text {
- text: ""
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 12
- }
-
- Text {
- id: cpuText
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 14
- }
- }
-
- Timer {
- interval: 2000
- running: true
- repeat: true
- triggeredOnStart: true
- onTriggered: cpuProc.running = true
- }
-
- Process {
- id: cpuProc
- command: ["sh", "-c", "awk '/^cpu / {u=$2+$4; t=$2+$3+$4+$5+$6+$7+$8; printf \"%.0f%%\", u*100/t}' /proc/stat"]
- stdout: StdioCollector {
- onStreamFinished: cpuText.text = this.text.trim()
- }
- }
- }
-
- // Powerline separator: bg1 -> bg2
- Text {
- text: ""
- color: root.bg2
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
-
- Rectangle {
- anchors.fill: parent
- color: root.bg1
- z: -1
- }
- }
-
- // ── 📅 Date (click for calendar popup) ────────────────────
- Rectangle {
- id: dateModule
- color: root.bg2
- Layout.fillHeight: true
- implicitWidth: dateRow.implicitWidth + 16
-
- property int calMonth: new Date().getMonth()
- property int calYear: new Date().getFullYear()
-
- MouseArea {
- anchors.fill: parent
- onClicked: {
- var now = new Date()
- dateModule.calMonth = now.getMonth()
- dateModule.calYear = now.getFullYear()
- var pos = dateModule.mapToItem(null, 0, 0)
- calendarPopup.popupX = pos.x - 40
- if (!calendarPopup.visible) {
- calendarPopup.visible = true
- calendarPopup.showing = true
- } else {
- calendarPopup.showing = false
- calCloseTimer.start()
- }
- }
- }
-
- Timer {
- id: calCloseTimer
- interval: 160
- onTriggered: calendarPopup.visible = false
- }
-
- PopupWindow {
- id: calendarPopup
- anchor.window: root
- property real popupX: root.width - 400
- anchor.rect.x: popupX
- anchor.rect.y: root.height
- visible: false
- width: 282
- height: 322
- color: "transparent"
-
- property bool showing: false
-
- Rectangle {
- id: calendarBg
- anchors.fill: parent
- color: Qt.rgba(0, 0.212, 0.212, 1)
- border.color: "#8affff"
- border.width: 1
- radius: 0
- clip: true
-
- opacity: calendarPopup.showing ? 1.0 : 0.0
- scale: calendarPopup.showing ? 1.0 : 0.95
- transformOrigin: Item.Top
-
- Behavior on opacity {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
- Behavior on scale {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
- }
-
- ColumnLayout {
- anchors.fill: parent
- anchors.margins: 11
- spacing: 8
-
- opacity: calendarPopup.showing ? 1.0 : 0.0
- Behavior on opacity {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
-
- // Month/Year header with navigation
- RowLayout {
- Layout.fillWidth: true
-
- Text {
- text: "<"
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 16
- font.bold: true
- MouseArea {
- anchors.fill: parent
- onClicked: {
- if (dateModule.calMonth === 0) {
- dateModule.calMonth = 11
- dateModule.calYear--
- } else {
- dateModule.calMonth--
- }
- }
- }
- }
-
- Text {
- Layout.fillWidth: true
- horizontalAlignment: Text.AlignHCenter
- text: dateModule.calYear + "年" + String(dateModule.calMonth + 1).padStart(2, '0') + "月"
- color: "#FFC500"
- font.family: "Noto Sans CJK SC"
- font.pixelSize: 14
- font.bold: true
- }
-
- Text {
- text: ">"
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 16
- font.bold: true
- MouseArea {
- anchors.fill: parent
- onClicked: {
- if (dateModule.calMonth === 11) {
- dateModule.calMonth = 0
- dateModule.calYear++
- } else {
- dateModule.calMonth++
- }
- }
- }
- }
- }
-
- // Day of week headers
- RowLayout {
- Layout.fillWidth: true
- spacing: 0
-
- Repeater {
- model: ["☉", "☽", "♂", "☿", "♃", "♀", "♄"]
-
- Text {
- required property string modelData
- Layout.fillWidth: true
- text: modelData
- color: "#FFC500"
- font.family: "Source Code Pro"
- font.pixelSize: 14
- font.bold: true
- horizontalAlignment: Text.AlignHCenter
- }
- }
- }
-
- // Calendar grid
- MonthGrid {
- Layout.fillWidth: true
- Layout.fillHeight: true
- month: dateModule.calMonth
- year: dateModule.calYear
- locale: Qt.locale("ja_JP")
-
- delegate: Text {
- required property var model
- property bool isToday: model.day === new Date().getDate() &&
- model.month === new Date().getMonth() &&
- model.year === new Date().getFullYear()
- property bool isCurrentMonth: model.month === dateModule.calMonth
-
- text: model.day
- color: isToday ? "#FFC500" : isCurrentMonth ? root.fg : "#4d7f7f"
- font.family: "Source Code Pro"
- font.pixelSize: 14
- font.bold: isToday
- horizontalAlignment: Text.AlignHCenter
- verticalAlignment: Text.AlignVCenter
- }
- }
- }
- }
-
- RowLayout {
- id: dateRow
- anchors.centerIn: parent
- spacing: 4
-
- Text {
- text: ""
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 12
- }
-
- Text {
- id: dateText
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 14
-
- property var daySymbols: ["☉", "☽", "♂", "☿", "♃", "♀", "♄"]
-
- Timer {
- interval: 1000
- running: true
- repeat: true
- triggeredOnStart: true
- onTriggered: {
- var now = new Date()
- var y = now.getFullYear()
- var m = String(now.getMonth() + 1).padStart(2, '0')
- var d = String(now.getDate()).padStart(2, '0')
- dateText.text = y + "年" + m + "月" + d + "日 (" + dateText.daySymbols[now.getDay()] + ")"
- }
- }
- }
- }
- }
-
- // Powerline separator: bg2 -> bg1
- Text {
- text: ""
- color: root.bg1
- font.family: "Inconsolata for Powerline"
- font.pixelSize: 25
- Layout.fillHeight: true
- verticalAlignment: Text.AlignVCenter
-
- Rectangle {
- anchors.fill: parent
- color: root.bg2
- z: -1
- }
- }
-
- // ── 🕐 Time (click for analog clock popup) ────────────────
- Rectangle {
- id: timeModule
- color: root.bg1
- Layout.fillHeight: true
- implicitWidth: timeRow.implicitWidth + 16
-
- MouseArea {
- anchors.fill: parent
- onClicked: {
- if (!clockPopup.visible) {
- var pos = timeModule.mapToItem(null, 0, 0)
- clockPopup.popupX = pos.x - 100
- clockPopup.visible = true
- clockPopup.showing = true
- } else {
- clockPopup.showing = false
- clockCloseTimer.start()
- }
- }
- }
-
- Timer {
- id: clockCloseTimer
- interval: 160
- onTriggered: clockPopup.visible = false
- }
-
- PopupWindow {
- id: clockPopup
- anchor.window: root
- property real popupX: root.width - 250
- anchor.rect.x: popupX
- anchor.rect.y: root.height
- visible: false
- width: 220
- height: 240
- color: "transparent"
-
- property bool showing: false
-
- Rectangle {
- anchors.fill: parent
- color: Qt.rgba(0, 0.212, 0.212, 1)
- border.color: "#8affff"
- border.width: 1
-
- opacity: clockPopup.showing ? 1.0 : 0.0
- scale: clockPopup.showing ? 1.0 : 0.95
- transformOrigin: Item.Top
-
- Behavior on opacity {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
- Behavior on scale {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
- }
-
- Canvas {
- id: clockCanvas
- anchors.fill: parent
- anchors.margins: 15
-
- opacity: clockPopup.showing ? 1.0 : 0.0
- Behavior on opacity {
- NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
- }
-
- property real hours: 0
- property real minutes: 0
- property real seconds: 0
-
- Timer {
- interval: 1000
- running: clockPopup.visible
- repeat: true
- triggeredOnStart: true
- onTriggered: {
- var now = new Date()
- clockCanvas.hours = now.getHours()
- clockCanvas.minutes = now.getMinutes()
- clockCanvas.seconds = now.getSeconds()
- clockCanvas.requestPaint()
- }
- }
-
- onPaint: {
- var ctx = getContext("2d")
- var size = Math.min(width, height)
- var cx = width / 2
- var cy = height / 2
- var r = size / 2 - 5
-
- ctx.clearRect(0, 0, width, height)
-
- // Clock face circle
- ctx.beginPath()
- ctx.arc(cx, cy, r, 0, 2 * Math.PI)
- ctx.strokeStyle = "#8affff"
- ctx.lineWidth = 1.5
- ctx.stroke()
-
- // Hour markers
- for (var i = 0; i < 12; i++) {
- var angle = (i * 30 - 90) * Math.PI / 180
- var inner = i % 3 === 0 ? r - 12 : r - 8
- var outer = r - 3
- ctx.beginPath()
- ctx.moveTo(cx + inner * Math.cos(angle), cy + inner * Math.sin(angle))
- ctx.lineTo(cx + outer * Math.cos(angle), cy + outer * Math.sin(angle))
- ctx.strokeStyle = i % 3 === 0 ? "#FFC500" : "#6ae8eb"
- ctx.lineWidth = i % 3 === 0 ? 2 : 1
- ctx.stroke()
- }
-
- // Hour hand
- var hAngle = ((hours % 12) * 30 + minutes * 0.5 - 90) * Math.PI / 180
- ctx.beginPath()
- ctx.moveTo(cx, cy)
- ctx.lineTo(cx + r * 0.5 * Math.cos(hAngle), cy + r * 0.5 * Math.sin(hAngle))
- ctx.strokeStyle = "#FFC500"
- ctx.lineWidth = 3
- ctx.lineCap = "round"
- ctx.stroke()
-
- // Minute hand
- var mAngle = (minutes * 6 + seconds * 0.1 - 90) * Math.PI / 180
- ctx.beginPath()
- ctx.moveTo(cx, cy)
- ctx.lineTo(cx + r * 0.7 * Math.cos(mAngle), cy + r * 0.7 * Math.sin(mAngle))
- ctx.strokeStyle = "#6ae8eb"
- ctx.lineWidth = 2
- ctx.lineCap = "round"
- ctx.stroke()
-
- // Second hand
- var sAngle = (seconds * 6 - 90) * Math.PI / 180
- ctx.beginPath()
- ctx.moveTo(cx, cy)
- ctx.lineTo(cx + r * 0.8 * Math.cos(sAngle), cy + r * 0.8 * Math.sin(sAngle))
- ctx.strokeStyle = "#8affff"
- ctx.lineWidth = 1
- ctx.lineCap = "round"
- ctx.stroke()
-
- // Center dot
- ctx.beginPath()
- ctx.arc(cx, cy, 3, 0, 2 * Math.PI)
- ctx.fillStyle = "#FFC500"
- ctx.fill()
- }
- }
- }
-
- RowLayout {
- id: timeRow
- anchors.centerIn: parent
- spacing: 4
-
- Text {
- text: "󰥔"
- color: root.fg
- font.family: "Symbols Nerd Font"
- font.pixelSize: 12
- }
-
- Text {
- id: timeText
- color: root.fg
- font.family: "Source Code Pro"
- font.pixelSize: 14
-
- Timer {
- interval: 100
- running: true
- repeat: true
- triggeredOnStart: true
- onTriggered: {
- var now = new Date()
- timeText.text = now.toLocaleTimeString(Qt.locale(), "HH:mm:ss")
- }
- }
- }
- }
- }
- }
+ShellRoot {
+ Bar {}
}