diff --git a/roles/quickshell/files/bar/Bar.qml b/roles/quickshell/files/bar/Bar.qml new file mode 100644 index 0000000..1ab5ff9 --- /dev/null +++ b/roles/quickshell/files/bar/Bar.qml @@ -0,0 +1,84 @@ +import Quickshell +import QtQuick +import "../components" + +PanelWindow { + id: root + + property var modelData + screen: modelData + + anchors { + top: true + left: true + right: true + } + implicitHeight: 40 + color: Theme.bg + + readonly property string screenName: modelData.name + + // Layout — three sections anchored independently for true centering + Item { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + anchors.topMargin: 2 + anchors.bottomMargin: 2 + + // Left + Enclosure { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + child: Workspaces { + screenName: root.screenName + } + } + + // Center — truly centered regardless of left/right content width + Enclosure { + anchors.centerIn: parent + child: MusicPlayer { + id: musicChip + onClicked: musicControls.visible = !musicControls.visible + } + } + + // Right — status chips + Enclosure { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + child: Row { + spacing: 8 + + SysTray { + anchors.verticalCenter: parent.verticalCenter + barWindow: root + } + + NetworkStatus { + anchors.verticalCenter: parent.verticalCenter + } + + VolumeControl { + anchors.verticalCenter: parent.verticalCenter + } + + Clock { + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + + // Popup declared after the layout so musicChip is already initialised. + // Centered horizontally within the bar window, placed just below it. + MusicPlayerControls { + id: musicControls + visible: false + anchor.window: root + anchor.rect.x: Math.round((root.width - musicControls.width) / 2) + anchor.rect.y: root.height + player: musicChip.player + } +} diff --git a/roles/quickshell/files/bar/Clock.qml b/roles/quickshell/files/bar/Clock.qml new file mode 100644 index 0000000..66522da --- /dev/null +++ b/roles/quickshell/files/bar/Clock.qml @@ -0,0 +1,196 @@ +import Quickshell +import QtQuick +import "../components" + +Item { + id: root + implicitWidth: timeLabel.implicitWidth + 10 + implicitHeight: 24 + + property var now: new Date() + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: root.now = new Date() + } + + readonly property string display: { + const d = now + const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + const h = String(d.getHours()).padStart(2, "0") + const m = String(d.getMinutes()).padStart(2, "0") + const s = String(d.getSeconds()).padStart(2, "0") + return days[d.getDay()] + " " + h + ":" + m + ":" + s + } + + Text { + id: timeLabel + anchors.centerIn: parent + text: root.display + color: Theme.text + font.pixelSize: 12 + font.family: "JetBrainsMono Nerd Font Mono" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: calPopup.visible = !calPopup.visible + } + + // --------------- Calendar popup --------------- + PopupWindow { + id: calPopup + visible: false + anchor.item: root + anchor.edges: Edges.Bottom + anchor.gravity: Edges.Bottom + + // calendar state + property int viewYear: root.now.getFullYear() + property int viewMonth: root.now.getMonth() // 0-based + + readonly property var monthNames: [ + "January","February","March","April","May","June", + "July","August","September","October","November","December" + ] + readonly property int daysInMonth: new Date(viewYear, viewMonth + 1, 0).getDate() + // first weekday of month: Mon=0 … Sun=6 + readonly property int startOffset: (new Date(viewYear, viewMonth, 1).getDay() + 6) % 7 + readonly property int totalCells: Math.ceil((startOffset + daysInMonth) / 7) * 7 + + readonly property int todayYear: root.now.getFullYear() + readonly property int todayMonth: root.now.getMonth() + readonly property int todayDay: root.now.getDate() + + implicitWidth: calBg.implicitWidth + implicitHeight: calBg.implicitHeight + + Rectangle { + id: calBg + anchors.fill: parent + color: Theme.bg + border.color: Theme.border + border.width: Theme.borderWidth + radius: Theme.radius + implicitWidth: calLayout.implicitWidth + 24 + implicitHeight: calLayout.implicitHeight + 24 + + Column { + id: calLayout + anchors { fill: parent; margins: 12 } + spacing: 6 + + // Month navigation + Row { + width: parent.width + spacing: 0 + + Text { + text: "‹" + color: Theme.text + font.pixelSize: 16 + width: 20 + horizontalAlignment: Text.AlignHCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (calPopup.viewMonth === 0) { + calPopup.viewMonth = 11 + calPopup.viewYear-- + } else { + calPopup.viewMonth-- + } + } + } + } + + Text { + text: calPopup.monthNames[calPopup.viewMonth] + " " + calPopup.viewYear + color: Theme.text + font.pixelSize: 13 + font.bold: true + // fill remaining space between arrows + width: parent.width - 40 + horizontalAlignment: Text.AlignHCenter + } + + Text { + text: "›" + color: Theme.text + font.pixelSize: 16 + width: 20 + horizontalAlignment: Text.AlignHCenter + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (calPopup.viewMonth === 11) { + calPopup.viewMonth = 0 + calPopup.viewYear++ + } else { + calPopup.viewMonth++ + } + } + } + } + } + + // Weekday headers + Row { + spacing: 2 + Repeater { + model: ["Mo","Tu","We","Th","Fr","Sa","Su"] + Text { + text: modelData + color: Theme.textDim + font.pixelSize: 10 + width: 28 + horizontalAlignment: Text.AlignHCenter + } + } + } + + // Day grid + Grid { + columns: 7 + spacing: 2 + Repeater { + model: calPopup.totalCells + delegate: Item { + width: 28 + height: 22 + readonly property int dayNum: index - calPopup.startOffset + 1 + readonly property bool valid: + index >= calPopup.startOffset && + index < calPopup.startOffset + calPopup.daysInMonth + readonly property bool isToday: + valid && + calPopup.viewYear === calPopup.todayYear && + calPopup.viewMonth === calPopup.todayMonth && + dayNum === calPopup.todayDay + + Rectangle { + anchors.fill: parent + radius: 3 + color: isToday ? Theme.accent : "transparent" + visible: isToday + } + + Text { + anchors.centerIn: parent + text: parent.valid ? String(parent.dayNum) : "" + color: parent.isToday ? Theme.text : Theme.textDim + font.pixelSize: 11 + font.bold: parent.isToday + } + } + } + } + } + } + } +} diff --git a/roles/quickshell/files/bar/MusicPlayer.qml b/roles/quickshell/files/bar/MusicPlayer.qml new file mode 100644 index 0000000..f8781df --- /dev/null +++ b/roles/quickshell/files/bar/MusicPlayer.qml @@ -0,0 +1,39 @@ +import Quickshell +import Quickshell.Services.Mpris +import QtQuick +import "../components" + +Item { + id: root + + signal clicked + + // Pick the first playing player, fall back to first available + readonly property var player: { + const vals = Mpris.players.values + for (const p of vals) { + if (p.isPlaying) return p + } + return vals.length > 0 ? vals[0] : null + } + + visible: player !== null + implicitWidth: player ? Math.min(240, titleLabel.implicitWidth + 20) : 0 + implicitHeight: 24 + + Text { + id: titleLabel + anchors.centerIn: parent + width: parent.width - 20 + text: root.player?.trackTitle ?? "" + color: Theme.text + font.pixelSize: 12 + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + } + + MouseArea { + anchors.fill: parent + onClicked: root.clicked() + } +} diff --git a/roles/quickshell/files/bar/MusicPlayerControls.qml b/roles/quickshell/files/bar/MusicPlayerControls.qml new file mode 100644 index 0000000..f6e44b9 --- /dev/null +++ b/roles/quickshell/files/bar/MusicPlayerControls.qml @@ -0,0 +1,166 @@ +import Quickshell +import Quickshell.Services.Mpris +import QtQuick +import QtQuick.Layouts +import "../components" + +PopupWindow { + id: root + required property var player + + implicitWidth: 280 + implicitHeight: bg.implicitHeight + + // Tickle positionChanged every frame while playing so the player.position + // binding re-evaluates. Paused while the user is dragging the seek bar. + FrameAnimation { + running: (root.player?.isPlaying ?? false) && !progressArea.pressed + onTriggered: root.player.positionChanged() + } + + Rectangle { + id: bg + anchors.fill: parent + color: Theme.bg + border.color: Theme.border + border.width: Theme.borderWidth + radius: Theme.radius + implicitHeight: layout.implicitHeight + 28 + + ColumnLayout { + id: layout + anchors { fill: parent; margins: 14 } + spacing: 8 + + // Track info + Text { + text: root.player?.trackTitle ?? "" + color: Theme.text + font.pixelSize: 13 + font.bold: true + Layout.fillWidth: true + elide: Text.ElideRight + } + + Text { + text: root.player?.trackArtist ?? "" + color: Theme.textDim + font.pixelSize: 11 + Layout.fillWidth: true + elide: Text.ElideRight + visible: (root.player?.trackArtist ?? "") !== "" + Layout.topMargin: -4 + } + + // Progress bar + Item { + visible: root.player?.positionSupported && root.player?.lengthSupported + && (root.player?.length ?? 0) > 0 + Layout.fillWidth: true + height: 12 // visual track is centred inside; extra height = easier to hit + + // Track + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: 3 + radius: 2 + color: Theme.progressTrack + + // Filled portion + Rectangle { + width: root.player?.length > 0 + ? parent.width * (root.player.position / root.player.length) + : 0 + height: parent.height + radius: parent.radius + color: Theme.accent + } + } + + MouseArea { + id: progressArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + function seek(mx) { + if (!(root.player?.canSeek)) return + const ratio = Math.max(0, Math.min(1, mx / width)) + root.player.position = ratio * (root.player?.length ?? 0) + } + + onClicked: seek(mouseX) + onPositionChanged: if (pressed) seek(mouseX) + } + } + + // Media controls + RowLayout { + Layout.fillWidth: true + spacing: 0 + + Item { Layout.fillWidth: true } + + ControlButton { + icon: "⏮" + iconSize: 18 + enabled: root.player?.canGoPrevious ?? false + onActivated: root.player.previous() + } + + Item { width: 8 } + + ControlButton { + icon: root.player?.isPlaying ? "⏸" : "▶" + iconSize: 22 + enabled: true + onActivated: root.player?.togglePlaying() + } + + Item { width: 8 } + + ControlButton { + icon: "⏭" + iconSize: 18 + enabled: root.player?.canGoNext ?? false + onActivated: root.player.next() + } + + Item { Layout.fillWidth: true } + } + } + } + + // Inline helper — a button that highlights on hover + component ControlButton: Rectangle { + id: btn + required property string icon + required property int iconSize + required property bool enabled + signal activated + + implicitWidth: label.implicitWidth + 16 + implicitHeight: label.implicitHeight + 10 + radius: Theme.radius + color: btnArea.containsMouse ? Qt.rgba(1, 1, 1, 0.08) : 'transparent' + + Behavior on color { ColorAnimation { duration: 80 } } + + Text { + id: label + anchors.centerIn: parent + text: btn.icon + font.pixelSize: btn.iconSize + color: btn.enabled ? Theme.text : Theme.textDisabled + } + + MouseArea { + id: btnArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: if (btn.enabled) btn.activated() + } + } +} diff --git a/roles/quickshell/files/bar/NetworkStatus.qml b/roles/quickshell/files/bar/NetworkStatus.qml new file mode 100644 index 0000000..8133401 --- /dev/null +++ b/roles/quickshell/files/bar/NetworkStatus.qml @@ -0,0 +1,78 @@ +import Quickshell +import Quickshell.Io +import QtQuick +import "../components" + +Item { + id: root + implicitWidth: row.implicitWidth + 10 + implicitHeight: 24 + + property string connType: "none" // "wifi", "ethernet", "none" + property string connName: "" + + // \uf1eb = FA wifi, \uf0e8 = FA sitemap (wired proxy), \uf127 = FA chain-broken + readonly property string netIcon: + connType === "wifi" ? "\uf1eb" : + connType === "ethernet" ? "\uf0e8" : "\uf127" + + function parseLine(line) { + const idx1 = line.indexOf(":") + if (idx1 < 0) return + const idx2 = line.indexOf(":", idx1 + 1) + if (idx2 < 0) return + const type = line.substring(0, idx1) + const state = line.substring(idx1 + 1, idx2) + const conn = line.substring(idx2 + 1).trim() + if (state === "connected" && (type === "wifi" || type === "ethernet")) { + if (root.connType === "none" || type === "wifi") { + root.connType = type + root.connName = conn + } + } + } + + Timer { + interval: 5000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: { + root.connType = "none" + root.connName = "" + netProc.running = true + } + } + + Process { + id: netProc + command: ["nmcli", "-t", "-f", "TYPE,STATE,CONNECTION", "dev"] + stdout: SplitParser { + splitMarker: "\n" + onRead: data => root.parseLine(data) + } + onExited: running = false + } + + Row { + id: row + anchors.centerIn: parent + spacing: 5 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: root.netIcon + font.family: "JetBrainsMono Nerd Font Mono" + font.pixelSize: 14 + color: root.connType !== "none" ? Theme.text : Theme.textDim + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: root.connName.length > 16 ? root.connName.substring(0, 15) + "…" : root.connName + font.pixelSize: 11 + color: Theme.textDim + visible: root.connName !== "" + } + } +} diff --git a/roles/quickshell/files/bar/SysTray.qml b/roles/quickshell/files/bar/SysTray.qml new file mode 100644 index 0000000..fb71585 --- /dev/null +++ b/roles/quickshell/files/bar/SysTray.qml @@ -0,0 +1,85 @@ +import Quickshell +import Quickshell.Services.SystemTray +import Quickshell.Widgets +import QtQuick +import "../components" + +Item { + id: root + required property var barWindow + implicitWidth: trayRow.width + implicitHeight: 24 + + Row { + id: trayRow + anchors.verticalCenter: parent.verticalCenter + spacing: 3 + + Repeater { + model: SystemTray.items + + delegate: Item { + id: trayDelegate + required property var modelData + + width: 24 + height: 24 + + // Hover highlight + Rectangle { + anchors.fill: parent + radius: Theme.radius + color: hoverArea.containsMouse ? Qt.rgba(1,1,1,0.08) : "transparent" + Behavior on color { ColorAnimation { duration: 80 } } + } + + IconImage { + id: iconImg + anchors.centerIn: parent + implicitSize: 16 + visible: status === Image.Ready + source: { + const icon = trayDelegate.modelData.icon + if (!icon || icon === "") return "" + if (icon.startsWith("/") || icon.startsWith("file://") || icon.startsWith("image://")) return icon + const path = Quickshell.iconPath(icon, "") + if (path !== "") return "file://" + path + return "image://icon/" + icon + } + mipmap: true + } + + // Letter fallback when icon fails to load + Text { + anchors.centerIn: parent + visible: iconImg.status !== Image.Ready + text: (trayDelegate.modelData.title ?? trayDelegate.modelData.id ?? "?").charAt(0).toUpperCase() + color: Theme.textDim + font.pixelSize: 11 + font.bold: true + } + + MouseArea { + id: hoverArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: mouse => { + const item = trayDelegate.modelData + const wantsMenu = mouse.button === Qt.RightButton || item.onlyMenu + if (wantsMenu && item.hasMenu) { + // display() needs the quickshell PanelWindow (not QQuickWindow). + // mapToItem(null) gives scene/window-local coordinates. + const pos = trayDelegate.mapToItem(null, 0, trayDelegate.height) + item.display(root.barWindow, Math.round(pos.x), Math.round(pos.y)) + } else if (!item.onlyMenu) { + item.activate() + } + } + } + } + } + } +} diff --git a/roles/quickshell/files/bar/VolumeControl.qml b/roles/quickshell/files/bar/VolumeControl.qml new file mode 100644 index 0000000..b80bce5 --- /dev/null +++ b/roles/quickshell/files/bar/VolumeControl.qml @@ -0,0 +1,222 @@ +import Quickshell +import Quickshell.Services.Pipewire +import QtQuick +import QtQuick.Layouts +import "../components" + +Item { + id: root + implicitWidth: chip.implicitWidth + 10 + implicitHeight: 24 + + // Track all PipeWire nodes so their properties are populated. + // Without this, nodes in Pipewire.nodes are "unbound" and have null audio/type. + PwObjectTracker { + objects: Pipewire.nodes.values + } + + // ── helpers ──────────────────────────────────────────────────────── + function safeVolume(node) { + if (!node || !node.ready || !node.audio) return 0 + const v = node.audio.volume + return (v !== undefined && !isNaN(v)) ? v : 0 + } + function setVolume(node, v) { + if (!node || !node.ready || !node.audio) return + node.audio.volume = Math.max(0, Math.min(1, v)) + } + function volIcon(vol, muted) { + if (muted) return "\uf6a9" // fa-volume-mute + if (vol > 0.6) return "\uf028" // fa-volume-up + if (vol > 0.2) return "\uf027" // fa-volume-down + return "\uf026" // fa-volume-off + } + + // ── bar chip (default sink) ───────────────────────────────────────── + readonly property var defaultSink: Pipewire.defaultAudioSink + readonly property real defaultVolume: safeVolume(defaultSink) + readonly property bool defaultMuted: defaultSink?.audio?.muted ?? false + + Row { + id: chip + anchors.centerIn: parent + spacing: 5 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: root.volIcon(root.defaultVolume, root.defaultMuted) + font.family: "JetBrainsMono Nerd Font Mono" + font.pixelSize: 14 + color: root.defaultMuted ? Theme.textDim : Theme.text + } + Text { + anchors.verticalCenter: parent.verticalCenter + text: root.defaultMuted ? "mute" : Math.round(root.defaultVolume * 100) + "%" + font.pixelSize: 11 + color: Theme.textDim + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + if (root.defaultSink?.ready && root.defaultSink?.audio) + root.defaultSink.audio.muted = !root.defaultSink.audio.muted + } else { + mixerPopup.visible = !mixerPopup.visible + } + } + onWheel: wheel => { + if (!root.defaultSink?.ready) return + root.setVolume(root.defaultSink, root.defaultVolume + wheel.angleDelta.y / 120 * 0.05) + } + } + + // ── mixer popup ───────────────────────────────────────────────────── + PopupWindow { + id: mixerPopup + visible: false + anchor.item: root + anchor.edges: Edges.Bottom + anchor.gravity: Edges.Bottom + + implicitWidth: mixerBg.implicitWidth + implicitHeight: mixerBg.implicitHeight + + Rectangle { + id: mixerBg + anchors.fill: parent + color: Theme.bg + border.color: Theme.border + border.width: Theme.borderWidth + radius: Theme.radius + implicitWidth: 300 + implicitHeight: mixerCol.implicitHeight + 24 + + ColumnLayout { + id: mixerCol + anchors { fill: parent; margins: 12 } + spacing: 4 + + // ── Output devices ────────────────────────────────────── + Text { + text: "Output Devices" + color: Theme.textDim + font.pixelSize: 10 + Layout.fillWidth: true + Layout.bottomMargin: 2 + } + + Repeater { + model: Pipewire.nodes + delegate: NodeRow { + required property var modelData + Layout.fillWidth: true + node: modelData + visible: modelData.isSink && !modelData.isStream && modelData.ready + } + } + + // ── Application streams ──────────────────────────────── + Text { + text: "Applications" + color: Theme.textDim + font.pixelSize: 10 + Layout.fillWidth: true + Layout.topMargin: 6 + Layout.bottomMargin: 2 + } + + Repeater { + model: Pipewire.nodes + delegate: NodeRow { + required property var modelData + Layout.fillWidth: true + node: modelData + // exclude monitor sources; include only audio output streams + visible: modelData.isStream && !modelData.isSink && modelData.ready + && !(modelData.description ?? "").toLowerCase().includes("monitor") + } + } + } + } + } + + // ── per-node row component ────────────────────────────────────────── + component NodeRow: RowLayout { + id: row + required property var node + spacing: 8 + implicitHeight: 28 + + // mute toggle + Text { + id: nodeIcon + text: root.volIcon(root.safeVolume(row.node), row.node.audio?.muted ?? false) + font.family: "JetBrainsMono Nerd Font Mono" + font.pixelSize: 13 + color: (row.node.audio?.muted ?? false) ? Theme.textDim : Theme.text + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (row.node.ready && row.node.audio) + row.node.audio.muted = !row.node.audio.muted + } + } + } + + // name + Text { + text: row.node.description || row.node.nickname || row.node.name || "?" + font.pixelSize: 11 + color: Theme.textDim + Layout.preferredWidth: 90 + elide: Text.ElideRight + } + + // horizontal slider + Item { + Layout.fillWidth: true + implicitHeight: 16 + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: 3 + radius: 2 + color: Theme.progressTrack + + Rectangle { + width: parent.width * Math.min(1, root.safeVolume(row.node)) + height: parent.height + radius: parent.radius + color: Theme.accent + } + } + + MouseArea { + anchors { fill: parent; topMargin: -4; bottomMargin: -4 } + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + function seek(mx) { + root.setVolume(row.node, mx / width) + } + onClicked: seek(mouseX) + onPositionChanged: if (pressed) seek(mouseX) + } + } + + // percentage + Text { + text: Math.round(root.safeVolume(row.node) * 100) + "%" + font.pixelSize: 10 + color: Theme.textDim + Layout.preferredWidth: 30 + horizontalAlignment: Text.AlignRight + } + } +} diff --git a/roles/quickshell/files/bar/WorkspaceButton.qml b/roles/quickshell/files/bar/WorkspaceButton.qml new file mode 100644 index 0000000..e4ec3e9 --- /dev/null +++ b/roles/quickshell/files/bar/WorkspaceButton.qml @@ -0,0 +1,58 @@ +import Quickshell +import Quickshell.Hyprland +import Quickshell.Widgets +import QtQuick +import "../components" + +Rectangle { + id: root + required property var workspace + required property string screenName + + visible: workspace?.lastIpcObject?.monitor === screenName + + implicitWidth: Math.max(32, iconsRow.implicitWidth + 14) + implicitHeight: 18 + radius: 6 + color: workspace?.active ? Theme.accent : 'transparent' + + Row { + id: iconsRow + anchors.centerIn: parent + spacing: 3 + + Repeater { + model: root.workspace?.toplevels + + delegate: Item { + id: iconItem + required property var modelData + property string appClass: modelData.lastIpcObject["class"] ?? "" + property var entry: appClass !== "" ? DesktopEntries.heuristicLookup(appClass) : null + + width: 16 + height: 16 + + IconImage { + anchors.fill: parent + source: iconItem.entry && iconItem.entry.icon !== "" + ? "image://icon/" + iconItem.entry.icon + : "" + } + } + } + + // Workspace number shown when no windows are open + Text { + visible: (root.workspace?.toplevels?.values?.length ?? 0) === 0 + text: root.workspace?.id ?? "" + color: root.workspace?.active ? Theme.text : Theme.textDim + font.pixelSize: 11 + } + } + + MouseArea { + anchors.fill: parent + onClicked: Hyprland.dispatch("workspace " + root.workspace.id) + } +} diff --git a/roles/quickshell/files/bar/Workspaces.qml b/roles/quickshell/files/bar/Workspaces.qml new file mode 100644 index 0000000..fba3c53 --- /dev/null +++ b/roles/quickshell/files/bar/Workspaces.qml @@ -0,0 +1,20 @@ +import Quickshell.Hyprland +import QtQuick + +Row { + id: root + required property string screenName + spacing: 4 + + Repeater { + id: workspacesRepeater + model: Hyprland.workspaces + property string screenName: root.screenName + + delegate: WorkspaceButton { + required property var modelData + workspace: modelData + screenName: workspacesRepeater.screenName + } + } +} diff --git a/roles/quickshell/files/bar/qmldir b/roles/quickshell/files/bar/qmldir new file mode 100644 index 0000000..37727eb --- /dev/null +++ b/roles/quickshell/files/bar/qmldir @@ -0,0 +1,9 @@ +Bar 1.0 Bar.qml +Clock 1.0 Clock.qml +MusicPlayer 1.0 MusicPlayer.qml +MusicPlayerControls 1.0 MusicPlayerControls.qml +NetworkStatus 1.0 NetworkStatus.qml +SysTray 1.0 SysTray.qml +VolumeControl 1.0 VolumeControl.qml +Workspaces 1.0 Workspaces.qml +WorkspaceButton 1.0 WorkspaceButton.qml diff --git a/roles/quickshell/files/components/Enclosure.qml b/roles/quickshell/files/components/Enclosure.qml new file mode 100644 index 0000000..244c813 --- /dev/null +++ b/roles/quickshell/files/components/Enclosure.qml @@ -0,0 +1,11 @@ +import QtQuick +import Quickshell.Widgets +import "." + +WrapperRectangle { + radius: Theme.radius + color: 'transparent' + border.color: Theme.border + border.width: Theme.borderWidth + margin: Theme.enclosureMargin +} diff --git a/roles/quickshell/files/components/Theme.qml b/roles/quickshell/files/components/Theme.qml new file mode 100644 index 0000000..c9faea2 --- /dev/null +++ b/roles/quickshell/files/components/Theme.qml @@ -0,0 +1,18 @@ +pragma Singleton +import QtQuick + +QtObject { + // Core palette + readonly property color bg: '#000000' + readonly property color accent: '#9B1A1A' + readonly property color border: '#FFFFFF' + readonly property color text: '#FFFFFF' + readonly property color textDim: '#888888' + readonly property color textDisabled: '#444444' + readonly property color progressTrack: '#333333' + + // Shape / sizing + readonly property int radius: 4 + readonly property int borderWidth: 2 + readonly property int enclosureMargin: 3 +} diff --git a/roles/quickshell/files/components/qmldir b/roles/quickshell/files/components/qmldir new file mode 100644 index 0000000..0400b7e --- /dev/null +++ b/roles/quickshell/files/components/qmldir @@ -0,0 +1,2 @@ +singleton Theme 1.0 Theme.qml +Enclosure 1.0 Enclosure.qml diff --git a/roles/quickshell/files/shell.qml b/roles/quickshell/files/shell.qml new file mode 100644 index 0000000..e4af72e --- /dev/null +++ b/roles/quickshell/files/shell.qml @@ -0,0 +1,13 @@ +//@ pragma UseQApplication +import Quickshell +import "bar" + +ShellRoot { + Variants { + model: Quickshell.screens + + Bar { + // modelData injected by Variants; Bar.qml binds screen: modelData internally + } + } +} diff --git a/roles/quickshell/tasks/main.yml b/roles/quickshell/tasks/main.yml new file mode 100644 index 0000000..8b4490e --- /dev/null +++ b/roles/quickshell/tasks/main.yml @@ -0,0 +1,42 @@ +--- +- name: Check if quickshell path is a symlink + stat: + path: "{{ config_dir }}/quickshell" + register: quickshell_stat + +- name: Remove existing quickshell symlink if present + file: + path: "{{ config_dir }}/quickshell" + state: absent + when: quickshell_stat.stat.exists and quickshell_stat.stat.islnk + +- name: Create quickshell config directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ config_dir }}/quickshell" + - "{{ config_dir }}/quickshell/bar" + - "{{ config_dir }}/quickshell/components" + +- name: Symlink quickshell config files + file: + src: "{{ role_path }}/files/{{ item.src }}" + dest: "{{ config_dir }}/quickshell/{{ item.dest }}" + state: link + force: true + loop: + # Root + - { src: shell.qml, dest: shell.qml } + # Shared components module + - { src: components/Theme.qml, dest: components/Theme.qml } + - { src: components/Enclosure.qml, dest: components/Enclosure.qml } + - { src: components/qmldir, dest: components/qmldir } + # Bar module (imported as "bar" in shell.qml) + - { src: bar/Bar.qml, dest: bar/Bar.qml } + - { src: bar/MusicPlayer.qml, dest: bar/MusicPlayer.qml } + - { src: bar/MusicPlayerControls.qml, dest: bar/MusicPlayerControls.qml } + - { src: bar/Workspaces.qml, dest: bar/Workspaces.qml } + - { src: bar/WorkspaceButton.qml, dest: bar/WorkspaceButton.qml } + - { src: bar/qmldir, dest: bar/qmldir }