diff --git a/Lock.qml b/Lock.qml index e859402..cd8cb27 100644 --- a/Lock.qml +++ b/Lock.qml @@ -15,7 +15,6 @@ WlSessionLock { id: surface color: "transparent" - Component.onCompleted: console.log("done") Rectangle { id: content anchors.fill: parent diff --git a/Panes/Battery.qml b/Panes/Battery.qml index db2af64..70c10a6 100644 --- a/Panes/Battery.qml +++ b/Panes/Battery.qml @@ -10,14 +10,13 @@ Column { width: Math.max(childrenRect.width, 300) height: childrenRect.height anchors.topMargin: 6 - TapHandler {onTapped: console.log(width)} Rectangle { radius: 12 height: 40 anchors.left: parent.left anchors.right: parent.right anchors.margins: 6 - color: "#181818" + color: "#111" Rectangle { color: "white" width: (parent.width - anchors.margins * 2)/3 @@ -116,7 +115,7 @@ Column { id: card required property var data required property var label - color: "#181818" + color: "#111" height: 60 width: 100 radius: 12 diff --git a/Panes/Brightness.qml b/Panes/Brightness.qml new file mode 100644 index 0000000..a8a2642 --- /dev/null +++ b/Panes/Brightness.qml @@ -0,0 +1,154 @@ +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import QtQuick +import qs.Services +import qs.Widgets as Widgets + +Column { + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 6 + } + width: 300 + height: childrenRect.height + anchors.margins * 2 + spacing: 6 + Item { + anchors { + left: parent.left + right: parent.right + margins: 12 + } + width: childrenRect.width + height: children[1].height + Image { + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + verticalCenterOffset: 1 + } + width: 24 + height: width + sourceSize {width: width; height: height} + source: Quickshell.iconPath("brightnesssettings") + } + Text { + anchors { + top: parent.top + left: parent.children[0].right + leftMargin: 6 + } + font.pixelSize: 16 + color: "white" + text: "Laptop display" + } + } + Widgets.Slider { + anchors { + left: parent.left + right: parent.right + margins: 12 + } + height: 24 + value: Brightness.monitors[0].value / Brightness.monitors[0].max + onMoved: { + Brightness.monitors[0].set(Math.round(Brightness.monitors[0].max * position)) + } + } + component ToggleButton: Item { + id: toggleButton + anchors { + left: parent.left + right: parent.right + margins: 12 + } + height: children[0].height + property bool active: false + property var text + Text { + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + height: 24 + font.pixelSize: 14 + color: "white" + text: toggleButton.text + } + Rectangle { + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + rightMargin: (24 - width)/2 + (36-24)/2 + } + radius: height/2 + color: parent.active ? "white" : "#00ffffff" + height: 10 + width: parent.active ? 36 : 24 + border.width: parent.active ? height/2 : 2 + border.color: "white" + Behavior on width {NumberAnimation { duration: 150; easing.type: Easing.OutCubic }} + Behavior on border.width {NumberAnimation { duration: 150; easing.type: Easing.OutCubic }} + } + } + ToggleButton { + id: sunsetToggle + text: "Night Light" + Process { + running: true + command: [ "pgrep", "-x", "sunset" ] + onExited: (exitCode) => {if (!exitCode) sunsetToggle.active = true} + } + TapHandler { + property var idleProc: Process {command: ["hyprsunset"]} + property var idleKill: Process {command: ["pkill", "-x", "hyprsunset"]} + onTapped: { + parent.active = !parent.active + if (parent.active) + idleProc.startDetached() + else + idleKill.startDetached() + } + } + } + ToggleButton { + id: idleToggle + text: "Idle Timer" + Process { + running: true + command: [ "pgrep", "-x", "hypridle" ] + onExited: (exitCode) => {if (!exitCode) idleToggle.active = true} + } + TapHandler { + property var idleProc: Process {command: ["hypridle"]} + property var idleKill: Process {command: ["pkill", "-x", "hypridle"]} + onTapped: { + parent.active = !parent.active + if (parent.active) + idleProc.startDetached() + else + idleKill.startDetached() + } + } + } + ToggleButton { + id: dpmsToggle + text: "Screen on" + active: true + TapHandler { + property var resetTimer: Timer { + interval: 500 + running: false + repeat: false + onTriggered: dpmsToggle.active = true + } + onTapped: { + dpmsToggle.active = false; + Hyprland.dispatch("dpms off"); + resetTimer.start(); + root.close(); + } + } + } +} diff --git a/Panes/Launcher.qml b/Panes/Launcher.qml index 192d776..be7991d 100644 --- a/Panes/Launcher.qml +++ b/Panes/Launcher.qml @@ -38,7 +38,7 @@ Item { margins: 6 } height: 48 - color: "#181818" + color: "#111" radius: 10 TextInput { id: input @@ -95,7 +95,6 @@ Item { property: "x" duration: 300 easing.type: Easing.OutQuint - onStarted: console.log("displaced") } NumberAnimation { property: "opacity" @@ -146,7 +145,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter width: 64 height: width - source: Quickshell.iconPath(modelData.icon) + source: Quickshell.iconPath(modelData.icon, "application-default") } Text { anchors.horizontalCenter: parent.horizontalCenter diff --git a/Panes/NotificationCenter.qml b/Panes/NotificationCenter.qml index b57361c..854c3f6 100644 --- a/Panes/NotificationCenter.qml +++ b/Panes/NotificationCenter.qml @@ -20,7 +20,7 @@ Item { Rectangle { width: 400 height: 300 - color: "#181818" + color: "#111" radius: 10 Column { anchors.centerIn: parent @@ -47,7 +47,6 @@ Item { anchors.rightMargin: anchors.leftMargin clip: true spacing: 4 - interactive: background.index === 3 add: Transition {NumberAnimation { property: "opacity" @@ -69,7 +68,7 @@ Item { header: Item {height: 4; width: 1} model: Array.from(notifServer.trackedNotifications.values) delegate: Widgets.Notification { - color: "#222" + color: "#181818" } } } @@ -98,25 +97,32 @@ Item { } } Column { - anchors.centerIn: parent + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + margins: 6 + } opacity: Mpris.players.values.length Behavior on opacity {NumberAnimation {duration: 150}} Shape { + id: progressShape anchors.horizontalCenter: parent.horizontalCenter - width: 180 - height: 180 + width: 160 + height: width + property var strokeWidth: 6 preferredRendererType: Shape.CurveRenderer ShapePath { capStyle: ShapePath.RoundCap strokeColor: "#444" fillColor: "transparent" - strokeWidth: 4 + strokeWidth: progressShape.strokeWidth PathAngleArc { moveToStart: true - radiusX: 90 - 6 - radiusY: 90 - 6 - centerX: 90 - centerY: 90 + radiusX: (progressShape.width-progressShape.strokeWidth)/2 + radiusY: (progressShape.height-progressShape.strokeWidth)/2 + centerX: progressShape.width/2 + centerY: progressShape.height/2 startAngle: 90 + 20 sweepAngle: 360 - 40 } @@ -125,13 +131,13 @@ Item { capStyle: ShapePath.RoundCap strokeColor: "white" fillColor: "transparent" - strokeWidth: 4 + strokeWidth: 6 PathAngleArc { moveToStart: true - radiusX: 90 - 6 - radiusY: 90 - 6 - centerX: 90 - centerY: 90 + radiusX: (progressShape.width-progressShape.strokeWidth)/2 + radiusY: (progressShape.height-progressShape.strokeWidth)/2 + centerX: progressShape.width/2 + centerY: progressShape.height/2 startAngle: 90 + 20 sweepAngle: (player.modelData?.position/player.modelData.length) * (360 - 40) } @@ -140,9 +146,12 @@ Item { color: "transparent" radius: height/2 anchors.fill: parent - anchors.margins: 12 + anchors.margins: progressShape.strokeWidth*2 Image { anchors.fill: parent + fillMode: Image.PreserveAspectCrop + mipmap: true + smooth: true source: player.modelData?.trackArtUrl ?? source } NumberAnimation on rotation { @@ -159,15 +168,25 @@ Item { } Item {width: 1; height: 10} Text { + anchors { + left: parent.left + right: parent.right + } + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight color: "white" text: player.modelData?.trackTitle ?? "" - anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: 16 } Text { + anchors { + left: parent.left + right: parent.right + } + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight color: "white" text: player.modelData?.trackArtist ?? "" - anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: 12 } Item {width: 1; height: 8} diff --git a/Panes/Notifications.qml b/Panes/Notifications.qml index 2a73caa..1b8af34 100644 --- a/Panes/Notifications.qml +++ b/Panes/Notifications.qml @@ -9,7 +9,7 @@ Item { horizontalCenter: parent.horizontalCenter } width: 350 - height: childrenRect.height + height: childrenRect.height + 12 Connections { target: notifServer function onNotification(notif) { @@ -22,6 +22,7 @@ Item { } Widgets.Notification { id: notifPopup + y: 6 anchors.margins: 6 modelData: notifServer.trackedNotifications.values[0] ?? undefined color: "transparent" @@ -32,4 +33,8 @@ Item { interval: 3000 onTriggered: root.close() } + Connections { + target: background + function onIndexChanged() { timeout.stop() } + } } diff --git a/Panes/Status.qml b/Panes/Status.qml index fc87006..40e3230 100644 --- a/Panes/Status.qml +++ b/Panes/Status.qml @@ -16,6 +16,8 @@ Item { bottom: parent.bottom } Widgets.Battery {} + Widgets.Volume {} + Widgets.Brightness {} } Widgets.Workspaces { height: parent.height @@ -31,6 +33,7 @@ Item { right: parent.right bottom: parent.bottom } + Widgets.Uptime {} Widgets.Time {} } } diff --git a/Panes/Volume.qml b/Panes/Volume.qml new file mode 100644 index 0000000..3da7c0d --- /dev/null +++ b/Panes/Volume.qml @@ -0,0 +1,104 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Controls +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Pipewire +import qs.Widgets as Widgets + +Row { + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + } + padding: 6 + height: 316 + spacing: 6 + + PwObjectTracker { + objects: Pipewire.nodes.values.filter(node => [17, 21].includes(node.type)) + } + + component NodeList: ClippingRectangle { + id: nodeListRect + width: 350 + color: "transparent" + anchors { + top: parent.top + bottom: parent.bottom + bottomMargin: 6 + } + radius: 10 + property var model + clip: true + ListView { + anchors.fill: parent + spacing: 6 + header: Item {width: 1; height: 6} + footer: header + model: nodeListRect.model + delegate: Rectangle { + anchors { + left: parent.left + right: parent.right + } + radius: 12 + color: "#111" + height: children[1].height + Image { + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + leftMargin: 6 + } + sourceSize {width: width; height: height} + source: Quickshell.iconPath((() => { + const description = modelData.description.toLowerCase(); + if (description.search("hdmi") !== -1) + return "display" + if (description.search("speaker") !== -1) + return "audio-speakers" + if (description.search("earphone") !== -1 || description.search("headphone") !== -1) + return "audio-headset" + if (modelData.properties["device.icon-name"]) + return modelData.properties["device.icon-name"] + return modelData.properties["application.name"] + })()?.toLowerCase(), "audio-on") + width: 32 + height: width + } + Column { + anchors { + left: parent.children[0].right + right: parent.right + verticalCenter: parent.verticalCenter + margins: 6 + rightMargin: 12 + } + topPadding: 6 + bottomPadding: 6 + spacing: 2 + Text { + text: { + if (modelData.nickname !== "") return modelData.nickname + if (modelData.description !== "") return description + return modelData.name + } + color: "white" + font.pixelSize: 16 + } + Widgets.Slider { + value: modelData.audio.volume + onMoved: modelData.audio.volume = position + } + } + } + } + } + NodeList { + model: Pipewire.nodes.values.filter(node => node.type === 17) + } + NodeList { + model: Pipewire.nodes.values.filter(node => node.type === 21) + } +} diff --git a/Services/Brightness.qml b/Services/Brightness.qml new file mode 100644 index 0000000..47fbb39 --- /dev/null +++ b/Services/Brightness.qml @@ -0,0 +1,68 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +Singleton { + id: brightness + property var monitors: [] + Process { + id: lsbacklight + running: true + command: ["find", "/sys/class/backlight", "-maxdepth", "1", "-mindepth", "1"] + stdout: StdioCollector { + onStreamFinished: brightness.monitors = this.text.split('\n') + .filter(mon => mon) + .map(mon => brightnessMonitorGen.createObject(brightness, {path: mon})) + } + } + Component { + id: brightnessMonitorGen + BrightnessMonitor {} + } + component BrightnessMonitor: QtObject { + id: monitor + property var path + property var value + property var max + property var get: Process { + running: true + command: ["brightnessctl", "i", "-m"] + stdout: StdioCollector { + onStreamFinished: [,,value,,max] = this.text.split(",") + } + } + property var setInterp: Process { + running: false + command: ["brillo", "-r", "-u", "300000", "-S", value] + } + property var setInstant: Process { + running: false + command: ["brightnessctl", "s", value] + } + function increase() { + value += value * 0.1 + max * 0.01 + value = Math.min(max, value) + setInterp.startDetached() + } + function decrease() { + value -= value * 0.1 + max * 0.01 + value = Math.max(0, value) + setInterp.startDetached() + } + function set(value) { + this.value = Math.max(Math.min(value, max), 0) + setInstant.startDetached() + } + } + GlobalShortcut { + name: "increase_brightness" + onPressed: monitors[0]?.increase() + } + GlobalShortcut { + name: "decrease_brightness" + onPressed: monitors[0]?.decrease() + } +} diff --git a/TopBar.qml b/TopBar.qml index 05ab79b..005c730 100644 --- a/TopBar.qml +++ b/TopBar.qml @@ -36,7 +36,7 @@ PanelWindow { GlobalShortcut { id: shortcut - name: "topbar" + name: "peek_bar" description: "Hold to peek, tap to toggle topbar" onPressed: { background.index = 0 @@ -70,7 +70,7 @@ PanelWindow { id: background anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - color: "#222222" + color: "#181818" radius: 16 width: children[0].width + radius*2 height: children[0].height @@ -107,6 +107,7 @@ PanelWindow { } width: children[0].width height: children[0].height + visible: opacity opacity: 0 property var fadeIn: SequentialAnimation { PauseAnimation {duration: 200} @@ -156,6 +157,8 @@ PanelWindow { Pane { Panes.Battery {} } Pane { Panes.NotificationCenter {} } Pane { Panes.Notifications {} } + Pane { Panes.Volume {} } + Pane { Panes.Brightness {} } property var entry: SequentialAnimation { PauseAnimation {duration: 2} @@ -180,6 +183,9 @@ PanelWindow { launcher.clear() } } - HoverHandler {id: hover} + HoverHandler { + id: hover + onHoveredChanged: { if (!hovered && background.index === 0) root.close() } + } } } diff --git a/Wall.qml b/Wall.qml index 54dbb2b..45c96f6 100644 --- a/Wall.qml +++ b/Wall.qml @@ -15,6 +15,11 @@ Variants { aboveWindows: false property var modelData screen: modelData - color: "#141414" + color: "#111" + Image { + source: Quickshell.shellPath("wallpaper") + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + } } } diff --git a/Widgets/Battery.qml b/Widgets/Battery.qml index 5ca08ad..146e2b5 100644 --- a/Widgets/Battery.qml +++ b/Widgets/Battery.qml @@ -9,11 +9,13 @@ Item { top: parent.top bottom: parent.bottom } - width: children[1].width + 20 + width: children[1].width + 16 Rectangle { anchors.fill: parent anchors.margins: 4 + anchors.rightMargin: 2 color: hover.hovered ? "#11ffffff" : "#00ffffff" + Behavior on color {ColorAnimation {duration: 150}} radius: 8 bottomLeftRadius: 12 } diff --git a/Widgets/Brightness.qml b/Widgets/Brightness.qml new file mode 100644 index 0000000..3b85a5c --- /dev/null +++ b/Widgets/Brightness.qml @@ -0,0 +1,40 @@ +import Quickshell +import QtQuick.Shapes +import qs.Services +import QtQuick + +Item { + anchors { + top: parent.top + bottom: parent.bottom + } + width: children[1].width + 16 + + Rectangle { + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 2 + anchors.rightMargin: 2 + color: hover.hovered ? "#11ffffff" : "#00ffffff" + Behavior on color {ColorAnimation {duration: 150}} + radius: 8 + } + PercentIndicator { + x: 8 + anchors.verticalCenter: parent.verticalCenter + width: 26 + height: width + percent: Brightness.monitors[0].value / Brightness.monitors[0].max + Image { + anchors.centerIn: parent + width: 20 + height: width + sourceSize {width: width; height: height} + source: Quickshell.iconPath("brightnesssettings") + } + } + HoverHandler {id: hover} + TapHandler {onTapped: { + background.index = 6 + }} +} diff --git a/Widgets/PercentIndicator.qml b/Widgets/PercentIndicator.qml new file mode 100644 index 0000000..ae182c1 --- /dev/null +++ b/Widgets/PercentIndicator.qml @@ -0,0 +1,40 @@ +import Quickshell +import QtQuick.Shapes +import Quickshell.Services.Pipewire +import QtQuick + +Shape { + id: root + preferredRendererType: Shape.CurveRenderer + property var percent + ShapePath { + capStyle: ShapePath.RoundCap + fillColor: "transparent" + strokeColor: "#333" + strokeWidth: 2 + PathAngleArc { + moveToStart: true + centerX: root.width/2 + centerY: root.height/2 + radiusX: root.width/2 + radiusY: root.height/2 + startAngle: 90 + 40 + sweepAngle: 360 - 80 + } + } + ShapePath { + capStyle: ShapePath.RoundCap + fillColor: "transparent" + strokeColor: "white" + strokeWidth: 2 + PathAngleArc { + moveToStart: true + centerX: root.width/2 + centerY: root.height/2 + radiusX: root.width/2 - 1 + radiusY: root.height/2 - 1 + startAngle: 90 + 40 + sweepAngle: root.percent * (360 - 80) + } + } +} diff --git a/Widgets/Slider.qml b/Widgets/Slider.qml new file mode 100644 index 0000000..98e1314 --- /dev/null +++ b/Widgets/Slider.qml @@ -0,0 +1,42 @@ +import QtQuick.Controls as QQC +import QtQuick + +QQC.Slider { + anchors { + left: parent.left + right: parent.right + } + height: 24 + orientation: Qt.Horizontal + handle: Rectangle { + x: parent.visualPosition * (parent.availableWidth - width) + y: parent.availableHeight / 2 - height / 2 + height: 16 + width: 4 + radius: width/2 + } + background: Rectangle { + height: 6 + anchors { + verticalCenter: parent.verticalCenter + left: parent.handle.right + right: parent.right + leftMargin: 4 + } + color: "#333" + topRightRadius: height/2 + bottomRightRadius: topRightRadius + } + Rectangle { + height: 6 + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + right: parent.handle.left + rightMargin: 4 + } + color: "white" + topLeftRadius: height/2 + bottomLeftRadius: topLeftRadius + } +} diff --git a/Widgets/Time.qml b/Widgets/Time.qml index 554ab4c..0acb50c 100644 --- a/Widgets/Time.qml +++ b/Widgets/Time.qml @@ -17,7 +17,7 @@ Item { radius: 8 bottomRightRadius: 12 color: hover.hovered ? "#11ffffff" : "#00ffffff" - Behavior on color {ColorAnimation {duration: 100}} + Behavior on color {ColorAnimation {duration: 150}} } Row { spacing: 12 diff --git a/Widgets/Uptime.qml b/Widgets/Uptime.qml new file mode 100644 index 0000000..825993c --- /dev/null +++ b/Widgets/Uptime.qml @@ -0,0 +1,7 @@ +import Quickshell +import QtQuick + +Item { + anchors { + } +} diff --git a/Widgets/Volume.qml b/Widgets/Volume.qml new file mode 100644 index 0000000..afa1d11 --- /dev/null +++ b/Widgets/Volume.qml @@ -0,0 +1,44 @@ +import Quickshell +import QtQuick.Shapes +import Quickshell.Services.Pipewire +import QtQuick + +Item { + anchors { + top: parent.top + bottom: parent.bottom + } + width: children[1].width + 16 + + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + Rectangle { + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 2 + anchors.rightMargin: 2 + color: hover.hovered ? "#11ffffff" : "#00ffffff" + Behavior on color {ColorAnimation {duration: 150}} + radius: 8 + } + PercentIndicator { + x: 8 + anchors.verticalCenter: parent.verticalCenter + width: 26 + height: width + percent: Pipewire.defaultAudioSink.audio.volume + Image { + anchors.centerIn: parent + width: 20 + height: width + sourceSize {width: width; height: height} + source: Quickshell.iconPath("volume-level-high") + } + } + HoverHandler {id: hover} + TapHandler {onTapped: { + background.index = 5 + }} +} diff --git a/Widgets/Workspaces.qml b/Widgets/Workspaces.qml index 3491d5b..969ee0b 100644 --- a/Widgets/Workspaces.qml +++ b/Widgets/Workspaces.qml @@ -18,9 +18,11 @@ Item { width: 24 property var workspace: Hyprland.workspaces.values.find(ws => ws.id === modelData+1) property var icon: { - const appId = workspace ? Array.from(workspace.toplevels.values).sort( - (a, b) => b.lastIpcObject.focusHistoryId - a.lastIpcObject.focusHistoryId - )[0]?.wayland?.appId : undefined + let appId + if (workspace) { + let toplevel = Array.from(workspace.toplevels.values).reduce((prev, curr) => (prev.lastIpcObject.focusHistoryID < curr.lastIpcObject.focusHistoryID) ? prev : curr) + appId = toplevel.wayland?.appId + } return DesktopEntries.applications.values.find( app => app.startupClass === appId || app.id === appId )?.icon; @@ -59,4 +61,8 @@ Item { height: 4 radius: height/2 } + Connections { + target: Hyprland + function onActiveToplevelChanged() {Hyprland.refreshToplevels()} + } } diff --git a/shell.qml b/shell.qml index f064b1c..ecb6994 100644 --- a/shell.qml +++ b/shell.qml @@ -1,3 +1,4 @@ +import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Notifications @@ -9,12 +10,20 @@ ShellRoot { onNotification: (notif) => notif.tracked = true } Shell.Wall {} - Shell.TopBar {} + Shell.TopBar {id: bar} Shell.Boateye {} Shell.Lock { id: lock animate: true } + PanelWindow { + anchors { + top: true + } + implicitHeight: 1 + implicitWidth: 800 + HoverHandler {onHoveredChanged: {if (hovered) bar.open() }} + } IpcHandler { target: "lock" function instalock() {lock.animate = false; lock.locked = true}