From 2daa0998eda6e3a340b40f3177c0a52f421f5211 Mon Sep 17 00:00:00 2001 From: bh Date: Thu, 24 Sep 2026 21:45:49 +0800 Subject: quickshell: stop it from being a monolithic mess --- quickshell/.config/quickshell/Bar.qml | 60 ++ quickshell/.config/quickshell/Theme.qml | 30 + .../.config/quickshell/components/BarBlock.qml | 32 + .../.config/quickshell/components/BarPopup.qml | 77 ++ .../.config/quickshell/components/Poller.qml | 28 + .../.config/quickshell/components/Separator.qml | 25 + quickshell/.config/quickshell/modules/Cpu.qml | 43 + .../.config/quickshell/modules/DateWidget.qml | 150 +++ .../.config/quickshell/modules/DistroIcon.qml | 15 + quickshell/.config/quickshell/modules/Network.qml | 71 ++ quickshell/.config/quickshell/modules/Ram.qml | 28 + .../.config/quickshell/modules/TimeWidget.qml | 113 ++ quickshell/.config/quickshell/modules/Tray.qml | 44 + quickshell/.config/quickshell/modules/Volume.qml | 116 +++ .../.config/quickshell/modules/WindowTitle.qml | 29 + .../.config/quickshell/modules/WorkspaceButton.qml | 45 + .../.config/quickshell/modules/Workspaces.qml | 13 + quickshell/.config/quickshell/services/Time.qml | 22 + quickshell/.config/quickshell/shell.qml | 1095 +------------------- 19 files changed, 943 insertions(+), 1093 deletions(-) create mode 100644 quickshell/.config/quickshell/Bar.qml create mode 100644 quickshell/.config/quickshell/Theme.qml create mode 100644 quickshell/.config/quickshell/components/BarBlock.qml create mode 100644 quickshell/.config/quickshell/components/BarPopup.qml create mode 100644 quickshell/.config/quickshell/components/Poller.qml create mode 100644 quickshell/.config/quickshell/components/Separator.qml create mode 100644 quickshell/.config/quickshell/modules/Cpu.qml create mode 100644 quickshell/.config/quickshell/modules/DateWidget.qml create mode 100644 quickshell/.config/quickshell/modules/DistroIcon.qml create mode 100644 quickshell/.config/quickshell/modules/Network.qml create mode 100644 quickshell/.config/quickshell/modules/Ram.qml create mode 100644 quickshell/.config/quickshell/modules/TimeWidget.qml create mode 100644 quickshell/.config/quickshell/modules/Tray.qml create mode 100644 quickshell/.config/quickshell/modules/Volume.qml create mode 100644 quickshell/.config/quickshell/modules/WindowTitle.qml create mode 100644 quickshell/.config/quickshell/modules/WorkspaceButton.qml create mode 100644 quickshell/.config/quickshell/modules/Workspaces.qml create mode 100644 quickshell/.config/quickshell/services/Time.qml (limited to 'quickshell/.config') 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 {} } -- cgit v1.2.3