diff options
Diffstat (limited to 'quickshell')
| -rw-r--r-- | quickshell/dot-config/quickshell/Bar.qml | 92 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/Batt.qml | 137 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/ClockWidget.qml | 5 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/CurrentWindow.qml | 49 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/DankSocket.qml | 62 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/Globals.qml | 35 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/Margin.qml | 23 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/Popout.qml | 147 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/QsButton.qml | 125 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/QsStateButton.qml | 77 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/QsTooltip.qml | 45 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/Time.qml | 16 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/Tray.qml | 196 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/services/Popout.qml | 51 | ||||
| -rw-r--r-- | quickshell/dot-config/quickshell/shell.qml | 7 | ||||
| -rw-r--r-- | quickshell/dot-config/systemd/user/quickshell.service | 12 |
16 files changed, 1079 insertions, 0 deletions
diff --git a/quickshell/dot-config/quickshell/Bar.qml b/quickshell/dot-config/quickshell/Bar.qml new file mode 100644 index 0000000..7cc7354 --- /dev/null +++ b/quickshell/dot-config/quickshell/Bar.qml @@ -0,0 +1,92 @@ +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.UPower +import QtQuick +import QtQuick.Layouts + +Scope { + Variants { + model: Quickshell.screens + + PanelWindow { + required property var modelData + screen: modelData + + anchors { + top: true + left: true + right: true + } + + implicitHeight: Globals.controls.barHeight + color: Globals.colors.bg + + + RowLayout { + anchors.fill: parent + spacing: Globals.controls.spacing + + Text { + Layout.alignment: Qt.AlignVCenter + color: Globals.colors.fg + font: Globals.font.regular + text: "Niri" // CurrentWindow.activeWindow + leftPadding: Globals.controls.padding + } + + Item { + Layout.fillWidth: true + } + + Text { + text: Batt.iconRepr + color: Globals.colors.fg + font: Globals.font.regular + } + + Text { + id: profileText + text: "~B~" + color: Globals.colors.fg + font: Globals.font.regular + + Timer { + interval: 2000 + running: true + repeat: true + onTriggered: updatePowerProfile() + } + + Component.onCompleted: updatePowerProfile() + } + + Tray {} + + ClockWidget { + Layout.alignment: Qt.AlignVCenter + color: Globals.colors.fg + font: Globals.font.bold + rightPadding: Globals.controls.padding + } + } + + function updatePowerProfile() { + let profile = PowerProfiles.profile + var color = { + [PowerProfile.Performance]: Globals.colors.urgent, + [PowerProfile.Balanced]: Globals.colors.comment, + [PowerProfile.PowerSaver]: Globals.colors.special, + }[profile] ?? Globals.colors.comment; + + var icon = { + [PowerProfile.Performance]: "[P]", + [PowerProfile.Balanced]: "~B~", + [PowerProfile.PowerSaver]: "\\S/", + }[profile] ?? "~B~"; + + profileText.text = icon; + profileText.color = color; + } + } + } +} diff --git a/quickshell/dot-config/quickshell/Batt.qml b/quickshell/dot-config/quickshell/Batt.qml new file mode 100644 index 0000000..becbd78 --- /dev/null +++ b/quickshell/dot-config/quickshell/Batt.qml @@ -0,0 +1,137 @@ +pragma Singleton + +import Quickshell +import Quickshell.Services.UPower +import QtQuick + +Singleton { + id: root + property UPowerDevice bat: null + property int perc: 0 + property int timeTo: 0 + + readonly property string simpleRepr: { + var info = "" + if (bat !== null) { + const timeToStr = getDurationAsString(timeTo) + switch(bat.state) { + case UPowerDeviceState.Charging: info = `[CHR] (${timeToStr})`; break; + case UPowerDeviceState.Discharging: info = `[DIS] (${timeToStr})`; break; + case UPowerDeviceState.FullyCharged: info = "[Full]"; break; + case UPowerDeviceState.PendingDischarge: info = "[P-DI]"; break; + case UPowerDeviceState.PendingCharge: info = "[P-CH]"; break; + case UPowerDeviceState.Empty: info = "[---]"; break; + case UPowerDeviceState.Unknown: info = "[???]"; break; + } + } + `Bat: ${perc}% ${info}` + } + + readonly property var iconMap: ({ + "nf-md-battery_10": "f007a", + "nf-md-battery_20": "f007b", + "nf-md-battery_30": "f007c", + "nf-md-battery_40": "f007d", + "nf-md-battery_50": "f007e", + "nf-md-battery_60": "f007f", + "nf-md-battery_70": "f0080", + "nf-md-battery_80": "f0081", + "nf-md-battery_90": "f0082", + "nf-md-battery_alert_variant_outline": "f10cd", + "nf-md-battery_charging_10": "f089c", + "nf-md-battery_charging_100": "f0085", + "nf-md-battery_charging_20": "f0086", + "nf-md-battery_charging_30": "f0087", + "nf-md-battery_charging_40": "f0088", + "nf-md-battery_charging_50": "f089d", + "nf-md-battery_charging_60": "f0089", + "nf-md-battery_charging_70": "f089e", + "nf-md-battery_charging_80": "f008a", + "nf-md-battery_charging_90": "f008b", + "nf-md-battery_check": "f17e2", + "nf-md-battery_remove_outline": "f17e9", + "nf-md-battery_unknown": "f0091", + "nf-md-battery": "f0079", + }) + + readonly property string iconRepr: { + if (bat === null) return getIconFromName("nf-md-battery_sync_outline"); + + var perc_str + var info = "" + var perc_round = perc - (perc % 10) + + switch (perc_round) { + case 100: perc_str = "check"; break; + case 0: perc_str = "alert_variant_outline"; break; + default: perc_str = String(perc_round); break; + } + + const timeToStr = getDurationAsString(timeTo) + switch(bat.state) { + case UPowerDeviceState.Charging: info = `(${timeToStr})`; break; + case UPowerDeviceState.Discharging: info = `(${timeToStr})`; break; + case UPowerDeviceState.PendingDischarge: info = "[P-Di]"; break; + case UPowerDeviceState.PendingCharge: info = "[P-Ch]"; break; + case UPowerDeviceState.Empty: perc_str = "alert_variant_outline"; break; + case UPowerDeviceState.Unknown: perc_str = "unknown"; break; + } + + var is_charging = (bat.state === UPowerDeviceState.Charging) + if (is_charging && perc_round === 100) { + is_charging = false + info = "" + } + const iconName = `nf-md-battery${is_charging ? "_charging" : ""}_${perc_str}` + const icon = getIconFromName(iconName) + return `${icon} ${perc === 100 ? "Full" : String(perc) + "%"}${info !== "" ? " ": ""}${info}` + } + + Timer { + interval: 10000 + running: true + repeat: true + onTriggered: root.update() + } + + Component.onCompleted: update() + + function getIconFromName(name: string): string { + return String.fromCodePoint(parseInt(iconMap[name] ?? iconMap["nf-md-battery"], 16)) + } + + function getDurationAsString(seconds: real): string { + if (!Number.isInteger(seconds)) { + throw new Error("seconds must be an integer"); + } + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60 + + if (hours > 0) { + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; + } + else if (minutes > 0) { + return `${minutes}m ${secs}s`; + } + else { + return `${secs}s` + } + // return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; + } + + function update() { + if (bat === null) { + const _bat = UPower.devices.values.filter(dev => dev.isLaptopBattery)[0] ?? null; + if (_bat === null) return; + bat = _bat; + } + perc = Math.floor(bat.percentage * 100) + switch(bat.state) { + case UPowerDeviceState.Charging: timeTo = bat.timeToFull; break; + case UPowerDeviceState.Discharging: timeTo = bat.timeToEmpty; break; + default: timeTo = 0; break; + } + } +} diff --git a/quickshell/dot-config/quickshell/ClockWidget.qml b/quickshell/dot-config/quickshell/ClockWidget.qml new file mode 100644 index 0000000..c8899a0 --- /dev/null +++ b/quickshell/dot-config/quickshell/ClockWidget.qml @@ -0,0 +1,5 @@ +import QtQuick + +Text { + text: Time.time +} diff --git a/quickshell/dot-config/quickshell/CurrentWindow.qml b/quickshell/dot-config/quickshell/CurrentWindow.qml new file mode 100644 index 0000000..e13bf8e --- /dev/null +++ b/quickshell/dot-config/quickshell/CurrentWindow.qml @@ -0,0 +1,49 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + readonly property string socketPath: Quickshell.env("NIRI_SOCKET") + + property var activeWindow: "TODO" + + DankSocket { + id: requestSocket + path: root.socketPath + connected: true + } + + DankSocket { + id: eventStreamSocket + path: root.socketPath + connected: true + + onConnectionStateChanged: { + if (connected) { + if (requestSocket.connected) { + requestSocket.send('"EventStream"') + } + } + } + + parser: SplitParser { + onRead: line => { + try { + const event = JSON.parse(line) + root.handleNiriEvent(event) + } catch (e) { + console.warn("NiriService: Failed to parse event:", line, e) + } + } + } + } + + function handleNiriEvent(event) { + const eventType = Object.keys(event)[0] + console.log(eventType) + } +} diff --git a/quickshell/dot-config/quickshell/DankSocket.qml b/quickshell/dot-config/quickshell/DankSocket.qml new file mode 100644 index 0000000..93471ba --- /dev/null +++ b/quickshell/dot-config/quickshell/DankSocket.qml @@ -0,0 +1,62 @@ +import QtQuick +import Quickshell.Io + +Item { + id: root + + property alias path: socket.path + property alias parser: socket.parser + property bool connected: false + + property int reconnectBaseMs: 400 + property int reconnectMaxMs: 15000 + + property int _reconnectAttempt: 0 + + signal connectionStateChanged() + + onConnectedChanged: { + socket.connected = connected + } + + Socket { + id: socket + + onConnectionStateChanged: { + root.connectionStateChanged() + if (connected) { + root._reconnectAttempt = 0 + return + } + if (root.connected) { + root._scheduleReconnect() + } + } + } + + Timer { + id: reconnectTimer + interval: 0 + repeat: false + onTriggered: { + socket.connected = false + Qt.callLater(() => socket.connected = true) + } + } + + function send(data) { + const json = typeof data === "string" ? data : JSON.stringify(data) + const message = json.endsWith("\n") ? json : json + "\n" + socket.write(message) + socket.flush() + } + + function _scheduleReconnect() { + const pow = Math.min(_reconnectAttempt, 10) + const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs) + const jitter = Math.floor(Math.random() * Math.floor(base / 4)) + reconnectTimer.interval = base + jitter + reconnectTimer.restart() + _reconnectAttempt++ + } +} diff --git a/quickshell/dot-config/quickshell/Globals.qml b/quickshell/dot-config/quickshell/Globals.qml new file mode 100644 index 0000000..83fda27 --- /dev/null +++ b/quickshell/dot-config/quickshell/Globals.qml @@ -0,0 +1,35 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + readonly property var controls: QtObject { + readonly property int padding: 5 + readonly property int spacing: 10 + readonly property int radius: 0 + readonly property int barHeight: 15 + readonly property int iconSize: 13 + readonly property string terminal: "alacritty" + } + readonly property var font: QtObject { + readonly property font regular: Qt.font({ + family: "Dejavu Sans Mono", + pointSize: 10 + }) + readonly property font bold: Qt.font({ + family: "Dejavu Sans Mono", + pointSize: 10, + weight: 800 + }) + } + readonly property var colors: QtObject { + readonly property string bg: "#181818" + readonly property string fg: "#e4e4ef" + readonly property string comment: "#cc8c3c" + readonly property string special: "#96a6c8" + readonly property string urgent: "#c73c3f" + readonly property string shadow: "#101010" + } +} diff --git a/quickshell/dot-config/quickshell/Margin.qml b/quickshell/dot-config/quickshell/Margin.qml new file mode 100644 index 0000000..ca86d70 --- /dev/null +++ b/quickshell/dot-config/quickshell/Margin.qml @@ -0,0 +1,23 @@ +import QtQuick + +Item { + id: root + property bool isVertical + + height: 2 + width: 2 + + Rectangle { + anchors.centerIn: parent + width: parent.width - Globals.controls.padding * 2 + height: 2 + color: Globals.colors.shadow + + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: 1 + color: Globals.colors.fg + } + } +} diff --git a/quickshell/dot-config/quickshell/Popout.qml b/quickshell/dot-config/quickshell/Popout.qml new file mode 100644 index 0000000..92dfbd5 --- /dev/null +++ b/quickshell/dot-config/quickshell/Popout.qml @@ -0,0 +1,147 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Io +import qs.services + +Item { + id: root + required property Item anchor + required property Item body + + readonly property real windowWidth: window.width + + property Item header: null + + property bool isOpen: false + property bool debug + + function open() { + Popout.whosOpen = anchor; + Popout.open(); + isOpen = true; + frame.visible = true; + anim.restart(); + } + function close() { + Popout.whosOpen = null; + isOpen = false; + anim.restart(); + } + function toggle() { + switch (isOpen) { + case true: + root.close(); + break; + case false: + root.open(); + break; + } + } + + // anchor 'frame' popup window to 'root' + anchors.centerIn: anchor + width: Globals.controls.barHeight + height: width + + Connections { + target: Popout + function onOpen() { if (Popout.whosOpen !== anchor && root.isOpen){ + root.isOpen = false; + anim.restart(); + }} + } + + // for dubugging only + Rectangle { + visible: debug + anchors.fill: parent + color: "#8000ff00" + } + + PopupWindow { id: frame + visible: false + mask: Region { + x: window.x + y: window.y + width: window.width + height: window.height + } + anchor { + item: root; + rect { x: root.width /2 -width /2; y: root.height; } // prefer window center horizontally to anchor and bellow bar + } + implicitWidth: window.width +60 + implicitHeight: window.height +60 + color: debug? "#80ff0000" : "transparent" + + // wrapper for all contents in popout + Rectangle { id: window + x: frame.width /2 -width /2 + width: Math.max((header? header.width : 0), body.width, 0) + height: (header? header.height : 0) +body.height + radius: 0 + color: debug? "#800000ff" : Globals.colors.bg + transform: Translate { id: windowTranslate } + + // wrapper for body + Rectangle { id: contentBody + anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; } + width: window.width + height: body.height + color: debug? "#80ff0000" : "transparent" + } + + // draw border around wrapper for header + Rectangle { + visible: header + anchors { horizontalCenter: contentHeader.horizontalCenter; top: contentHeader.top; } + width: contentHeader.width + height: contentHeader.height +2 + // radius: contentHeader.radius + color: Globals.colors.fg + } + + // wrapper for header + Rectangle { id: contentHeader + visible: header + anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; } + width: window.width + height: Math.max(header? header.height : 0, 0) + color: debug? "#8000ff00" : Globals.colors.bg + } + } + } + + SequentialAnimation { id: anim + property real time: 0.25 + + ParallelAnimation { + NumberAnimation { + target: windowTranslate + property: "y" + from: isOpen? -window.height : Globals.controls.spacing + to: isOpen? Globals.controls.spacing : -window.height + duration: anim.time *1000 + easing.type: Easing.OutCirc + } + + NumberAnimation { + target: window + property: "opacity" + from: isOpen? 0.0 : 0.98 + to: isOpen? 0.98 : 0.0 + duration: anim.time *1000 + easing.type: Easing.OutCirc + } + } + + ScriptAction { script: if (!isOpen) frame.visible = false; } + } + + Component.onCompleted: { + body.parent = contentBody; + if (header) header.parent = contentHeader; + } +} + diff --git a/quickshell/dot-config/quickshell/QsButton.qml b/quickshell/dot-config/quickshell/QsButton.qml new file mode 100644 index 0000000..01cc28d --- /dev/null +++ b/quickshell/dot-config/quickshell/QsButton.qml @@ -0,0 +1,125 @@ +import QtQuick +import Quickshell + +Item { + id: root + required property Item content + + readonly property bool containsMouse: mouseArea.containsMouse + readonly property real mouseX: mouseArea.mouseX + readonly property real mouseY: mouseArea.mouseY + + property bool anim: true + property bool shade: true + property bool highlight + property bool fill + property bool isHighlighted: (root.highlight && containsMouse) + property Item tooltip + property bool debug + + signal pressed() + signal clicked() + signal middleClicked() + signal mouseEntered() + signal mouseExited() + signal animMean() + + width: content.width + height: content.height + transform: Translate { id: rootTranslate; } + + // highlight button on hover + Rectangle { id: highlight + anchors.centerIn: parent + visible: isHighlighted || fill + width: parent.width +4 + height: parent.height +4 + color: Globals.colors.comment + opacity: containsMouse? 0.5 : 0.25 + } + + // contentWrapper + Item { id: contentWrapper + anchors.fill: parent + } + + QsTooltip { id: tooltip + anchor: root + content: root.tooltip + + Timer { id: tooltipTimer + running: false + interval: 1500 + onTriggered: parent.isShown = true; + } + } + + MouseArea { id: mouseArea + width: content.width +4 + height: content.height +4 + x: content.x -2 + y: content.y -2 + hoverEnabled: true + onEntered: { root.mouseEntered(); if (root.tooltip) tooltipTimer.restart(); } + onExited: { + root.mouseExited(); + if (root.tooltip) { + tooltipTimer.stop(); + tooltip.isShown = false; + } + } + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + onPressed: (mouse) => { + switch (mouse.button) { + case Qt.RightButton: { + if (root.anim) { + pressedAnim.start(); + } else root.clicked(); + + root.pressed(); + break; + } + case Qt.RightButton: { + if (root.anim) { + pressedAnim.start(); + } else root.clicked(); + + root.pressed(); + break; + } + case Qt.MiddleButton: + root.middleClicked(); + break; + } + } + onReleased: if (anim) releasedAnim.start(); + + Rectangle { + visible: debug + anchors.fill: parent + color: "#4000ff00" + } + } + + PropertyAnimation { id: pressedAnim + target: rootTranslate + properties: "y" + to: 2 + duration: 25 + easing.type: Easing.InCirc; + } + + PropertyAnimation { id: releasedAnim + target: rootTranslate + properties: "y" + to: 0 + duration: 25 + easing.type: Easing.InCirc; + onStarted: root.animMean(); + onFinished: root.clicked(); + } + + Component.onCompleted: { + content.parent = contentWrapper; + } +} diff --git a/quickshell/dot-config/quickshell/QsStateButton.qml b/quickshell/dot-config/quickshell/QsStateButton.qml new file mode 100644 index 0000000..d652bdb --- /dev/null +++ b/quickshell/dot-config/quickshell/QsStateButton.qml @@ -0,0 +1,77 @@ +/*--------------------------------- +--- QsStateButton.qml by andrel --- +---------------------------------*/ + +import QtQuick +import QtQuick.Effects +import Quickshell + +QsButton { + required property var checkState // values can be 'Unchecked', 'PartiallyChecked', or 'Checked' + + property var type: QsMenuButtonType.RadioButton // values can be 'CheckBox', 'RadioButton', or 'None' + + shade: false + content: Item { + width: button.width + height: width + + Rectangle { id: button + anchors.bottom: parent.bottom + + // button background + width: Globals.controls.iconSize + height: width + radius: { + switch (type) { + case QsMenuButtonType.CheckBox: + return 3; + case QsMenuButtonType.RadioButton: + return height /2; + default: + return height /2; + } + } + color: checkState !== Qt.Checked? Globals.colors.comment: Globals.colors.special + } + + // checkmark + Item { + readonly property color color: Globals.colors.fg + + visible: (checkState === Qt.Checked) && (type === QsMenuButtonType.CheckBox) + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: height *0.1 + } + width: height *0.55 + height: parent.height *0.7 + rotation: 45 + + Rectangle { + anchors.right: parent.right + width: parent.height *0.25 + height: parent.height + color: parent.color + } + + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: parent.height *0.25 + color: parent.color + } + } + + // radio circle + Rectangle { + visible: (checkState === Qt.Checked) && (type === QsMenuButtonType.RadioButton) + anchors.centerIn: parent + width: parent.width *0.5 + height: width + radius: height /2 + color: Globals.colors.fg + } + } +} diff --git a/quickshell/dot-config/quickshell/QsTooltip.qml b/quickshell/dot-config/quickshell/QsTooltip.qml new file mode 100644 index 0000000..9725d88 --- /dev/null +++ b/quickshell/dot-config/quickshell/QsTooltip.qml @@ -0,0 +1,45 @@ +import QtQuick +import Quickshell + +Item { id: root + required property Item anchor + required property Item content + + property bool isShown + + anchors.fill: anchor + + Loader { + active: content + sourceComponent: PopupWindow { id: popout; + Connections { + target: root + function onIsShownChanged() { if (isShown) { + popout.anchor.rect.x = root.parent.mouseX; + popout.anchor.rect.y = root.parent.mouseY + Globals.controls.iconSize; + popout.visible = true; + } else popout.visible = false; } + } + visible: false + mask: Region {} + anchor { + item: root + rect { x: root.width /2 -content.width /2 - Globals.controls.padding / 2; y: root.height +6; } + } + implicitWidth: content.width + Globals.controls.padding + implicitHeight: content.height + Globals.controls.padding + color: "transparent" + + Rectangle { id: contentWrapper + anchors.fill: parent + radius: 0 + color: Globals.colors.bg + } + + Component.onCompleted: { + content.parent = contentWrapper; + content.anchors.centerIn = contentWrapper; + } + } + } +} diff --git a/quickshell/dot-config/quickshell/Time.qml b/quickshell/dot-config/quickshell/Time.qml new file mode 100644 index 0000000..7e75e87 --- /dev/null +++ b/quickshell/dot-config/quickshell/Time.qml @@ -0,0 +1,16 @@ +pragma Singleton + +import Quickshell +import QtQuick + +Singleton { + id: root + readonly property string time: { + Qt.formatDateTime(clock.date, "ddd MMM d hh:mm:ss") + } + + SystemClock { + id: clock + precision: SystemClock.Seconds + } +} diff --git a/quickshell/dot-config/quickshell/Tray.qml b/quickshell/dot-config/quickshell/Tray.qml new file mode 100644 index 0000000..03a70e3 --- /dev/null +++ b/quickshell/dot-config/quickshell/Tray.qml @@ -0,0 +1,196 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.SystemTray + + +Row { + id: root + spacing: Globals.controls.spacing + + Repeater { + model: SystemTray.items.values + delegate: Item { + id: systemTrayItem + required property var modelData + + width: Globals.controls.iconSize + height: width + + IconImage { + implicitSize: Globals.controls.iconSize + source: Quickshell.iconPath(modelData.id.toLowerCase(), true) || modelData.icon + } + + MouseArea { + anchors.centerIn: parent + width: parent.width +4 + height: width + hoverEnabled: true + acceptedButtons: Qt.AllButtons + // show/hide tooltip + onEntered: if (modelData.title) tooltipTimer.restart(); + onExited: if (modelData.title) { + tooltipTimer.stop(); + tooltip.isShown = false; + } + onClicked: (mouse) => { + switch (mouse.button) { + // toggle menu + case Qt.RightButton: + if (modelData.hasMenu) popout.toggle(); + break; + // trigger system tray item action + case Qt.LeftButton: + modelData.activate(); + break; + // trigger system tray item secondary action + case Qt.MiddleButton: + modelData.secondaryActivate(); + break; + } + } + // trigger system tray scroll action + onWheel: (wheel) => { + modelData.scroll(wheel.angleDelta.y /120, false); + } + + // tooltip + QsTooltip { id: tooltip + anchor: parent + content: Text { + text: modelData.title + color: Globals.colors.fg + font: Globals.font.regular + } + + Timer { id: tooltipTimer + running: false + interval: 1500 + onTriggered: parent.isShown = true; + } + } + } + + QsMenuAnchor { id: menuAnchor + function toggle() { + if (menuAnchor.visible) menuAnchor.close(); + else menuAnchor.open(); + } + + function refresh() { + menuAnchor.open(); + menuAnchor.close(); + } + + anchor.item: systemTrayItem + menu: modelData.menu + } + + QsMenuOpener { id: menuOpener + readonly property bool hasIcon: children.values.some(e => e.icon) + readonly property bool hasButton: children.values.some(e => e.buttonType !== QsMenuButtonType.None) + + menu: modelData.menu + } + + Popout { + id: popout + anchor: systemTrayItem + body: ColumnLayout { id: bodyLayout + // top padding element + Item { Layout.preferredHeight: 1; } + + // menu entries + Repeater { + model: menuOpener.children.values.filter(e => !e.hasChildren).filter((e, i, arr) => { + const prev = arr[i - 1]; + const next = arr[i + 1]; + + if ((i === 0 || i === arr.length - 1) && e.isSeparator) return false; + if (e.isSeparator && ((prev && prev.isSeparator) || next == null)) return false; + + return true; + }); + delegate: QsButton { id: menuEntry + required property var modelData + required property int index + + // if can interact with meny entry + readonly property bool interactive: modelData.enabled && !modelData.isSeparator + // seperator item + readonly property Item separatorEntry: Margin { anchors.fill: parent; opacity: 0.4; } + // regular menu entry item + readonly property Item textMenuEntry: RowLayout { + anchors.fill: parent + spacing: Globals.controls.spacing + + // left padding element + Item { Layout.preferredWidth: 1; } + + // button + Item { + visible: menuOpener.hasButton + width: Globals.controls.iconSize + height: width + + QsStateButton { + visible: type + type: modelData.buttonType + checkState: modelData.checkState + } + } + + // icon + Item { + visible: menuOpener.hasIcon + width: Globals.controls.iconSize + height: width + + IconImage { + visible: source + implicitSize: Globals.controls.iconSize + source: modelData.icon + } + } + + // text + Text { + Layout.fillWidth: true + text: modelData.text + color: modelData.hasChildren? "red" : Globals.colors.fg + font: Globals.font.regular + } + + // right padding element + Item { Layout.preferredWidth: 1; } + } + + Layout.fillWidth: true + Layout.minimumWidth: content.implicitWidth + shade: false + anim: interactive + highlight: interactive + // update button + onPressed: if (interactive && modelData.buttonType !== QsMenuButtonType.None) { + modelData.triggered(); + menuAnchor.refresh(); + } + // trigger menu entry action + onClicked: if (interactive && modelData.buttonType === QsMenuButtonType.None) { + modelData.triggered(); + menuAnchor.refresh(); + popout.close(); + } + content: modelData.isSeparator? separatorEntry : textMenuEntry + } + } + + // bottom padding element + Item { Layout.preferredHeight: 1; } + } + } + } + } +} diff --git a/quickshell/dot-config/quickshell/services/Popout.qml b/quickshell/dot-config/quickshell/services/Popout.qml new file mode 100644 index 0000000..9118fe7 --- /dev/null +++ b/quickshell/dot-config/quickshell/services/Popout.qml @@ -0,0 +1,51 @@ +pragma Singleton + +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +Singleton { id: root + property var whosOpen: null + + function clear() { + if (root.whosOpen) { + root.whosOpen = null; + root.open(); + } + } + + signal open() + signal accepted() + signal keyPressed(KeyEvent event) + + Loader { + active: whosOpen + sourceComponent: PanelWindow { + anchors { + left: true + right: true + top: true + bottom: true + } + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.layer: WlrLayer.Top + color: "transparent" + + TextInput { id: inputField + focus: true + onAccepted: root.accepted() + color: "transparent" + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) root.clear(); + else root.keyPressed(event) + } + } + + MouseArea { + anchors.fill: parent + onClicked: root.clear(); + } + } + } +} diff --git a/quickshell/dot-config/quickshell/shell.qml b/quickshell/dot-config/quickshell/shell.qml new file mode 100644 index 0000000..e59b2c6 --- /dev/null +++ b/quickshell/dot-config/quickshell/shell.qml @@ -0,0 +1,7 @@ +//@ pragma UseQApplication + +import Quickshell + +Scope { + Bar {} +} diff --git a/quickshell/dot-config/systemd/user/quickshell.service b/quickshell/dot-config/systemd/user/quickshell.service new file mode 100644 index 0000000..b174440 --- /dev/null +++ b/quickshell/dot-config/systemd/user/quickshell.service @@ -0,0 +1,12 @@ +[Unit] +Description=QuickShell for the bar +PartOf=graphical-session.target +After=graphical-session.target + +[Service] +Type=exec +ExecStart=/usr/bin/qs +Restart=on-failure + +[Install] +WantedBy=graphical-session.target |
