diff --git a/Bar.qml b/Bar.qml index cf9b126..f628a98 100644 --- a/Bar.qml +++ b/Bar.qml @@ -150,7 +150,7 @@ Variants { Panels { id: panels - screen: bar.modelData + screen: scope.modelData bar: backgroundRect visibilities: visibilities } @@ -175,13 +175,7 @@ Variants { popouts: panels.popouts bar: bar visibilities: visibilities - } - - WindowTitle { - anchors.centerIn: parent - width: Math.min( 300, parent.width * 0.4 ) - height: parent.height - z: 1 + screen: scope.modelData } } } diff --git a/Components/CustomShortcut.qml b/Components/CustomShortcut.qml new file mode 100644 index 0000000..c60cbef --- /dev/null +++ b/Components/CustomShortcut.qml @@ -0,0 +1,5 @@ +import Quickshell.Hyprland + +GlobalShortcut { + appid: "zshell" +} diff --git a/Config/Services.qml b/Config/Services.qml index df76e9a..651f9f2 100644 --- a/Config/Services.qml +++ b/Config/Services.qml @@ -3,4 +3,5 @@ import QtQuick JsonObject { property string weatherLocation: "" + property real brightnessIncrement: 0.1 } diff --git a/Helpers/Brightness.qml b/Helpers/Brightness.qml new file mode 100644 index 0000000..c3c31e5 --- /dev/null +++ b/Helpers/Brightness.qml @@ -0,0 +1,226 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick +import qs.Config +import qs.Components + +Singleton { + id: root + + property list ddcMonitors: [] + readonly property list monitors: variants.instances + property bool appleDisplayPresent: false + + function getMonitorForScreen(screen: ShellScreen): var { + return monitors.find(m => m.modelData === screen); + } + + function getMonitor(query: string): var { + if (query === "active") { + return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); + } + + if (query.startsWith("model:")) { + const model = query.slice(6); + return monitors.find(m => m.modelData.model === model); + } + + if (query.startsWith("serial:")) { + const serial = query.slice(7); + return monitors.find(m => m.modelData.serialNumber === serial); + } + + if (query.startsWith("id:")) { + const id = parseInt(query.slice(3), 10); + return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); + } + + return monitors.find(m => m.modelData.name === query); + } + + function increaseBrightness(): void { + const monitor = getMonitor("active"); + if (monitor) + monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + } + + function decreaseBrightness(): void { + const monitor = getMonitor("active"); + if (monitor) + monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + } + + onMonitorsChanged: { + ddcMonitors = []; + ddcProc.running = true; + } + + Variants { + id: variants + + model: Quickshell.screens + + Monitor {} + } + + Process { + running: true + command: ["sh", "-c", "asdbctl get"] // To avoid warnings if asdbctl is not installed + stdout: StdioCollector { + onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 + } + } + + Process { + id: ddcProc + + command: ["ddcutil", "detect", "--brief"] + stdout: StdioCollector { + onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({ + busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1], + connector: d.match(/DRM connector:\s+(.*)/)[1].replace(/^card\d+-/, "") // strip "card1-" + })) + } + } + + CustomShortcut { + name: "brightnessUp" + description: "Increase brightness" + onPressed: root.increaseBrightness() + } + + CustomShortcut { + name: "brightnessDown" + description: "Decrease brightness" + onPressed: root.decreaseBrightness() + } + + IpcHandler { + target: "brightness" + + function get(): real { + return getFor("active"); + } + + // Allows searching by active/model/serial/id/name + function getFor(query: string): real { + return root.getMonitor(query)?.brightness ?? -1; + } + + function set(value: string): string { + return setFor("active", value); + } + + // Handles brightness value like brightnessctl: 0.1, +0.1, 0.1-, 10%, +10%, 10%- + function setFor(query: string, value: string): string { + const monitor = root.getMonitor(query); + if (!monitor) + return "Invalid monitor: " + query; + + let targetBrightness; + if (value.endsWith("%-")) { + const percent = parseFloat(value.slice(0, -2)); + targetBrightness = monitor.brightness - (percent / 100); + } else if (value.startsWith("+") && value.endsWith("%")) { + const percent = parseFloat(value.slice(1, -1)); + targetBrightness = monitor.brightness + (percent / 100); + } else if (value.endsWith("%")) { + const percent = parseFloat(value.slice(0, -1)); + targetBrightness = percent / 100; + } else if (value.startsWith("+")) { + const increment = parseFloat(value.slice(1)); + targetBrightness = monitor.brightness + increment; + } else if (value.endsWith("-")) { + const decrement = parseFloat(value.slice(0, -1)); + targetBrightness = monitor.brightness - decrement; + } else if (value.includes("%") || value.includes("-") || value.includes("+")) { + return `Invalid brightness format: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`; + } else { + targetBrightness = parseFloat(value); + } + + if (isNaN(targetBrightness)) + return `Failed to parse value: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`; + + monitor.setBrightness(targetBrightness); + + return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; + } + } + + component Monitor: QtObject { + id: monitor + + required property ShellScreen modelData + readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name) + readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? "" + readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") + property real brightness + property real queuedBrightness: NaN + + readonly property Process initProc: Process { + stdout: StdioCollector { + onStreamFinished: { + if (monitor.isAppleDisplay) { + const val = parseInt(text.trim()); + monitor.brightness = val / 101; + } else { + const [, , , cur, max] = text.split(" "); + monitor.brightness = parseInt(cur) / parseInt(max); + } + } + } + } + + readonly property Timer timer: Timer { + interval: 500 + onTriggered: { + if (!isNaN(monitor.queuedBrightness)) { + monitor.setBrightness(monitor.queuedBrightness); + monitor.queuedBrightness = NaN; + } + } + } + + function setBrightness(value: real): void { + value = Math.max(0, Math.min(1, value)); + const rounded = Math.round(value * 100); + if (Math.round(brightness * 100) === rounded) + return; + + if (isDdc && timer.running) { + queuedBrightness = value; + return; + } + + brightness = value; + + if (isAppleDisplay) + Quickshell.execDetached(["asdbctl", "set", rounded]); + else if (isDdc) + Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]); + else + Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]); + + if (isDdc) + timer.restart(); + } + + function initBrightness(): void { + if (isAppleDisplay) + initProc.command = ["asdbctl", "get"]; + else if (isDdc) + initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]; + else + initProc.command = ["sh", "-c", "echo a b c $(brightnessctl g) $(brightnessctl m)"]; + + initProc.running = true; + } + + onBusNumChanged: initBrightness() + Component.onCompleted: initBrightness() + } +} diff --git a/Modules/AudioPopup.qml b/Modules/AudioPopup.qml index a84e14c..e615d50 100644 --- a/Modules/AudioPopup.qml +++ b/Modules/AudioPopup.qml @@ -376,6 +376,13 @@ Item { source: iconPath1 !== "" ? iconPath1 : iconPath2 Layout.alignment: Qt.AlignVCenter implicitSize: 42 + + StateLayer { + radius: 1000 + onClicked: { + appBox.modelData.audio.muted = !appBox.modelData.audio.muted; + } + } } ColumnLayout { diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index 0b8717e..c36f75e 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -18,6 +18,7 @@ RowLayout { required property Wrapper popouts required property PersistentProperties visibilities required property PanelWindow bar + required property ShellScreen screen function checkPopout(x: real): void { const ch = childAt(x, height / 2) as WrappedLoader; @@ -60,7 +61,7 @@ RowLayout { // popouts.currentName = "calendar"; // popouts.currentCenter = Qt.binding( () => item.mapToItem( root, itemWidth / 2, 0 ).x ); // popouts.hasCurrent = true; - } else if ( x > (root.width / 2 + 50) && x < (root.width / 2 - 50) && Config.barConfig.popouts.activeWindow ) { + } else if ( id === "activeWindow" && Config.barConfig.popouts.activeWindow ) { popouts.currentName = "dash"; popouts.currentCenter = root.width / 2; popouts.hasCurrent = true; @@ -153,7 +154,10 @@ RowLayout { DelegateChoice { roleValue: "activeWindow" delegate: WrappedLoader { - sourceComponent: WindowTitle {} + sourceComponent: WindowTitle { + bar: root + monitor: Brightness.getMonitorForScreen(root.screen) + } } } } diff --git a/Modules/Dashboard/Dash.qml b/Modules/Dashboard/Dash.qml index febbcb3..aa09613 100644 --- a/Modules/Dashboard/Dash.qml +++ b/Modules/Dashboard/Dash.qml @@ -30,7 +30,7 @@ GridLayout { Rect { Layout.row: 0 Layout.columnSpan: 2 - Layout.preferredWidth: Config.dashboard.sizes.weatherWidth + Layout.preferredWidth: 250 Layout.fillHeight: true radius: 8 @@ -50,38 +50,6 @@ GridLayout { } } - Rect { - Layout.row: 1 - Layout.column: 1 - Layout.columnSpan: 3 - Layout.fillWidth: true - Layout.preferredHeight: 100 - - radius: 8 - - } - - Rect { - Layout.row: 1 - Layout.column: 4 - Layout.preferredWidth: 100 - Layout.fillHeight: true - - radius: 8 - - } - - Rect { - Layout.row: 0 - Layout.column: 5 - Layout.rowSpan: 2 - Layout.preferredWidth: 100 - Layout.fillHeight: true - - radius: 8 - - } - component Rect: CustomRect { color: DynamicColors.tPalette.m3surfaceContainer } diff --git a/Modules/Dashboard/Dashboard.qml b/Modules/Dashboard/Dashboard.qml index de09385..573a145 100644 --- a/Modules/Dashboard/Dashboard.qml +++ b/Modules/Dashboard/Dashboard.qml @@ -12,32 +12,21 @@ Item { required property var wrapper readonly property PersistentProperties state: PersistentProperties { - property int currentTab + property int currentTab: 0 property date currentDate: new Date() reloadableId: "dashboardState" } readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 - readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2 + readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight - Tabs { - id: tabs - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - - nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 - state: root.state - } - ClippingRectangle { id: viewWrapper - anchors.top: tabs.bottom + anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom diff --git a/Modules/WindowTitle.qml b/Modules/WindowTitle.qml index 4554fe1..ae30bda 100644 --- a/Modules/WindowTitle.qml +++ b/Modules/WindowTitle.qml @@ -1,68 +1,85 @@ +pragma ComponentBehavior: Bound + import QtQuick -import QtQuick.Layouts -import Quickshell.Hyprland -import qs.Helpers -import qs.Config import qs.Components +import qs.Config +import qs.Helpers Item { id: root - property string currentTitle: Hypr.activeName - Layout.fillHeight: true - Layout.preferredWidth: Math.max( titleText1.implicitWidth, titleText2.implicitWidth ) + 10 + + required property var bar + required property Brightness.Monitor monitor + property color colour: DynamicColors.palette.m3primary + + readonly property int maxHeight: { + const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer"); + const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0); + // Length - 2 cause repeater counts as a child + return bar.height - otherHeight - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2; + } + property Title current: text1 + clip: true + implicitWidth: current.implicitWidth + current.anchors.leftMargin + implicitHeight: current.implicitHeight - property bool showFirst: true - property color textColor: Config.useDynamicColors ? DynamicColors.palette.m3primary : "white" - - // Component.onCompleted: { - // Hyprland.rawEvent.connect(( event ) => { - // if (event.name === "activewindow") { - // InitialTitle.getInitialTitle( function( initialTitle ) { - // root.currentTitle = initialTitle - // }) - // } - // }) + // MaterialIcon { + // id: icon + // + // anchors.verticalCenter: parent.verticalCenter + // + // animate: true + // text: Icons.getAppCategoryIcon(Hypr.activeToplevel?.lastIpcObject.class, "desktop_windows") + // color: root.colour // } - onCurrentTitleChanged: { - if (showFirst) { - titleText2.text = currentTitle - showFirst = false - } else { - titleText1.text = currentTitle - showFirst = true + Title { + id: text1 + } + + Title { + id: text2 + } + + TextMetrics { + id: metrics + + text: Hypr.activeToplevel?.title ?? qsTr("Desktop") + font.pointSize: 12 + font.family: "Rubik" + + onTextChanged: { + const next = root.current === text1 ? text2 : text1; + next.text = elidedText; + root.current = next; + } + onElideWidthChanged: root.current.text = elidedText + } + + Behavior on implicitWidth { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects } } - CustomText { - id: titleText1 - anchors.fill: parent - anchors.margins: 5 - text: root.currentTitle - color: root.textColor - elide: Text.ElideRight - font.pixelSize: 16 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - opacity: root.showFirst ? 1 : 0 - Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } - } - } + component Title: CustomText { + id: text + + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 7 + + font.pointSize: metrics.font.pointSize + font.family: metrics.font.family + color: root.colour + opacity: root.current === this ? 1 : 0 + + width: implicitWidth + height: implicitHeight - CustomText { - id: titleText2 - anchors.fill: parent - anchors.margins: 5 - color: root.textColor - elide: Text.ElideRight - font.pixelSize: 16 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - opacity: root.showFirst ? 0 : 1 Behavior on opacity { - NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } + Anim {} } } } diff --git a/Modules/WindowTitleOld.qml b/Modules/WindowTitleOld.qml new file mode 100644 index 0000000..4554fe1 --- /dev/null +++ b/Modules/WindowTitleOld.qml @@ -0,0 +1,68 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Hyprland +import qs.Helpers +import qs.Config +import qs.Components + +Item { + id: root + property string currentTitle: Hypr.activeName + Layout.fillHeight: true + Layout.preferredWidth: Math.max( titleText1.implicitWidth, titleText2.implicitWidth ) + 10 + clip: true + + property bool showFirst: true + property color textColor: Config.useDynamicColors ? DynamicColors.palette.m3primary : "white" + + // Component.onCompleted: { + // Hyprland.rawEvent.connect(( event ) => { + // if (event.name === "activewindow") { + // InitialTitle.getInitialTitle( function( initialTitle ) { + // root.currentTitle = initialTitle + // }) + // } + // }) + // } + + onCurrentTitleChanged: { + if (showFirst) { + titleText2.text = currentTitle + showFirst = false + } else { + titleText1.text = currentTitle + showFirst = true + } + } + + CustomText { + id: titleText1 + anchors.fill: parent + anchors.margins: 5 + text: root.currentTitle + color: root.textColor + elide: Text.ElideRight + font.pixelSize: 16 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: root.showFirst ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } + } + } + + CustomText { + id: titleText2 + anchors.fill: parent + anchors.margins: 5 + color: root.textColor + elide: Text.ElideRight + font.pixelSize: 16 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: root.showFirst ? 0 : 1 + Behavior on opacity { + NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } + } + } +} diff --git a/scripts/update.sh b/scripts/update.sh index e899657..3108bdb 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -1,3 +1,28 @@ -#!/bin/zsh +#!/usr/bin/env bash -exec yay -Sy +OS="arch" +if [[ $(ls ./tmp) ]]; then + exec mkdir ./tmp +fi + +cd ./tmp + +main() { + local OPTARG OPTIND opt + while getopts "arch:nix:" opt; do + case "$opt" in + arch) OS=$OPTARG ;; + nix) OS=$OPTARG ;; + *) fatal 'bad option' ;; + esac + done + + if [[ $OS = "arch" ]]; then + exec yay -Sy + elif [[ $OS = "nix" ]]; then + exec nixos-rebuild build --flake $HOME/Gits/NixOS/#nixos + PKGS=$(exec nix store diff-closures /run/current-system ./result) + fi +} + +main "$@"