aboutsummaryrefslogtreecommitdiff
path: root/quickshell/dot-config
diff options
context:
space:
mode:
authorMohammad Reza Karimi <m.r.karimi.j@gmail.com>2026-01-09 17:34:31 -0500
committerMohammad Reza Karimi <m.r.karimi.j@gmail.com>2026-01-09 17:34:31 -0500
commit5e83a094476f28eb77508c8b470efe3dfd56de83 (patch)
treef0f39b694fcad8e408c2a41f3d3240dfce44c504 /quickshell/dot-config
parent6c17d2c74ea4daeb9dbf2c2b7aafeb86111b7f65 (diff)
some big changes
Diffstat (limited to 'quickshell/dot-config')
-rw-r--r--quickshell/dot-config/quickshell/Bar.qml92
-rw-r--r--quickshell/dot-config/quickshell/Batt.qml137
-rw-r--r--quickshell/dot-config/quickshell/ClockWidget.qml5
-rw-r--r--quickshell/dot-config/quickshell/CurrentWindow.qml49
-rw-r--r--quickshell/dot-config/quickshell/DankSocket.qml62
-rw-r--r--quickshell/dot-config/quickshell/Globals.qml35
-rw-r--r--quickshell/dot-config/quickshell/Margin.qml23
-rw-r--r--quickshell/dot-config/quickshell/Popout.qml147
-rw-r--r--quickshell/dot-config/quickshell/QsButton.qml125
-rw-r--r--quickshell/dot-config/quickshell/QsStateButton.qml77
-rw-r--r--quickshell/dot-config/quickshell/QsTooltip.qml45
-rw-r--r--quickshell/dot-config/quickshell/Time.qml16
-rw-r--r--quickshell/dot-config/quickshell/Tray.qml196
-rw-r--r--quickshell/dot-config/quickshell/services/Popout.qml51
-rw-r--r--quickshell/dot-config/quickshell/shell.qml7
-rw-r--r--quickshell/dot-config/systemd/user/quickshell.service12
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