auroraeec80fe
switch to aurora shell
its time to get rid of waybar, swaync
+1328 −154
35 files changed
- M .config/hypr/hyprland.lua +2 −2
- M .config/hypr/source/keybinds.lua +6 −3
- R .config/quickshell/wal-control/AudioPage.qml → .config/quickshell/aurora/AudioPage.qml +0 −0
- A .config/quickshell/aurora/Bar.qml +153 −0
- A .config/quickshell/aurora/BarPill.qml +57 −0
- R .config/quickshell/wal-control/BarSlider.qml → .config/quickshell/aurora/BarSlider.qml +0 −0
- R .config/quickshell/wal-control/Bright.qml → .config/quickshell/aurora/Bright.qml +0 −0
- R .config/quickshell/wal-control/BtPage.qml → .config/quickshell/aurora/BtPage.qml +0 −0
- A .config/quickshell/aurora/CatLoop.qml +41 −0
- A .config/quickshell/aurora/ControlPanel.qml +115 −0
- A .config/quickshell/aurora/Gpu.qml +46 −0
- R .config/quickshell/wal-control/HomePage.qml → .config/quickshell/aurora/HomePage.qml +46 −9
- A .config/quickshell/aurora/Hypr.qml +19 −0
- R .config/quickshell/wal-control/IconBtn.qml → .config/quickshell/aurora/IconBtn.qml +0 −0
- R .config/quickshell/wal-control/ListRow.qml → .config/quickshell/aurora/ListRow.qml +0 −0
- R .config/quickshell/wal-control/MediaCard.qml → .config/quickshell/aurora/MediaCard.qml +0 −0
- R .config/quickshell/wal-control/Net.qml → .config/quickshell/aurora/Net.qml +15 −5
- R .config/quickshell/wal-control/NetPage.qml → .config/quickshell/aurora/NetPage.qml +0 −0
- A .config/quickshell/aurora/NotifCard.qml +112 −0
- A .config/quickshell/aurora/NotifPage.qml +65 −0
- A .config/quickshell/aurora/NotifPopups.qml +34 −0
- A .config/quickshell/aurora/Notifs.qml +59 −0
- R .config/quickshell/wal-control/SectionLabel.qml → .config/quickshell/aurora/SectionLabel.qml +0 −0
- R .config/quickshell/wal-control/SliderRow.qml → .config/quickshell/aurora/SliderRow.qml +0 −0
- A .config/quickshell/aurora/Spark.qml +33 −0
- A .config/quickshell/aurora/StatBar.qml +38 −0
- A .config/quickshell/aurora/SysPage.qml +179 −0
- A .config/quickshell/aurora/SysStats.qml +69 −0
- R .config/quickshell/wal-control/Theme.qml → .config/quickshell/aurora/Theme.qml +49 −3
- A .config/quickshell/aurora/Tray.qml +59 −0
- A .config/quickshell/aurora/Workspaces.qml +67 −0
- A .config/quickshell/aurora/shell.qml +62 −0
- D .config/quickshell/wal-control/shell.qml +0 −118
- R .config/quickshell/wal-control/wal-control.service → .config/systemd/user/aurora.service +2 −2
- D .config/systemd/user/wal-control.service +0 −12
| @@ -20,8 +20,8 @@ hl.on("hyprland.start", function() | |||
| 20 | 20 | hl.exec_cmd("wl-paste --type image --watch cliphist store") | |
| 21 | 21 | hl.exec_cmd("hyprpm reload -n") | |
| 22 | 22 | hl.exec_cmd("kanshi") | |
| 23 | − | hl.exec_cmd("waybar") | |
| 24 | − | hl.exec_cmd("swaync") | |
| 23 | + | -- hl.exec_cmd("waybar") | |
| 24 | + | -- hl.exec_cmd("swaync") | |
| 25 | 25 | hl.exec_cmd("gammastep") | |
| 26 | 26 | hl.exec_cmd("hypridle") | |
| 27 | 27 | -- hl.exec_cmd("blueman-applet") | |
| @@ -27,10 +27,15 @@ hl.bind( | |||
| 27 | 27 | ) | |
| 28 | 28 | ||
| 29 | 29 | ||
| 30 | + | ------------------ | |
| 31 | + | -- Aurora Shell | |
| 32 | + | ----------------- | |
| 33 | + | hl.bind(mainMod .. " + c", hl.dsp.exec_cmd("qs -c aurora ipc call shell toggle")) | |
| 34 | + | hl.bind(mainMod .. " + b", hl.dsp.exec_cmd("systemctl --user restart aurora")) | |
| 35 | + | ||
| 30 | 36 | hl.bind(mainMod .. " + R", hl.dsp.exec_cmd(menu)) | |
| 31 | 37 | -- bind = $mainMod SHIFT, P, pseudo, # dwindle | |
| 32 | 38 | hl.bind(mainMod .. " + Tab", hl.dsp.window.cycle_next({ next = false }), { repeating = true }) | |
| 33 | − | hl.bind(mainMod .. " + b", hl.dsp.exec_cmd("killall waybar || waybar")) | |
| 34 | 39 | hl.bind(mainMod .. " + grave", hl.dsp.exec_cmd("fuzzelunicode")) | |
| 35 | 40 | hl.bind(mainMod .. " + escape", hl.dsp.exec_cmd("sysact")) | |
| 36 | 41 | ||
| @@ -145,5 +150,3 @@ hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = tr | |||
| 145 | 150 | hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) | |
| 146 | 151 | hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) | |
| 147 | 152 | ||
| 148 | − | -- Quichshell | |
| 149 | − | hl.bind(mainMod .. "+ C", hl.dsp.exec_cmd("qs -p ~/.config/quickshell/wal-control ipc call control toggle")) | |
[No content changes]
| @@ -0,0 +1,153 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import Quickshell | |
| 3 | + | import Quickshell.Wayland | |
| 4 | + | import Quickshell.Bluetooth | |
| 5 | + | import Quickshell.Services.Pipewire | |
| 6 | + | import Quickshell.Services.UPower | |
| 7 | + | ||
| 8 | + | PanelWindow { | |
| 9 | + | id: bar | |
| 10 | + | required property var modelData | |
| 11 | + | screen: modelData | |
| 12 | + | signal togglePanel(string name) | |
| 13 | + | ||
| 14 | + | anchors { top: true; left: true; right: true } | |
| 15 | + | implicitHeight: Theme.barHeight | |
| 16 | + | color: Theme.barBg | |
| 17 | + | WlrLayershell.namespace: "aurora-bar" | |
| 18 | + | ||
| 19 | + | SystemClock { id: clock; precision: SystemClock.Minutes } | |
| 20 | + | ||
| 21 | + | // ---- state ---- | |
| 22 | + | readonly property var audio: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null | |
| 23 | + | readonly property var mic: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null | |
| 24 | + | readonly property var bat: UPower.displayDevice | |
| 25 | + | readonly property bool hasBat: bat !== null && bat.isPresent | |
| 26 | + | readonly property real batPct: hasBat ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0 | |
| 27 | + | readonly property bool charging: hasBat && bat.state === UPowerDeviceState.Charging | |
| 28 | + | readonly property bool plugged: hasBat && bat.state === UPowerDeviceState.FullyCharged | |
| 29 | + | readonly property bool batLow: hasBat && batPct <= 0.15 && !charging | |
| 30 | + | readonly property var adapter: Bluetooth.defaultAdapter | |
| 31 | + | ||
| 32 | + | function findHeadset() { | |
| 33 | + | var l = Bluetooth.devices.values | |
| 34 | + | for (var i = 0; i < l.length; i++) { | |
| 35 | + | var d = l[i] | |
| 36 | + | if (!d.connected || !d.batteryAvailable) continue | |
| 37 | + | var ic = String(d.icon) | |
| 38 | + | if (ic.indexOf("head") >= 0 || ic.indexOf("audio") >= 0) return d | |
| 39 | + | } | |
| 40 | + | return null | |
| 41 | + | } | |
| 42 | + | function btConnected() { | |
| 43 | + | var l = Bluetooth.devices.values | |
| 44 | + | for (var i = 0; i < l.length; i++) if (l[i].connected) return true | |
| 45 | + | return false | |
| 46 | + | } | |
| 47 | + | readonly property var headset: findHeadset() | |
| 48 | + | readonly property bool btOn: adapter !== null && adapter.enabled | |
| 49 | + | readonly property bool btLink: btOn && btConnected() | |
| 50 | + | ||
| 51 | + | Item { | |
| 52 | + | anchors.fill: parent | |
| 53 | + | ||
| 54 | + | // ---------------- left ---------------- | |
| 55 | + | Row { | |
| 56 | + | anchors.left: parent.left | |
| 57 | + | height: parent.height | |
| 58 | + | spacing: 10 | |
| 59 | + | Workspaces { height: parent.height } | |
| 60 | + | CatLoop { height: parent.height } | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // ---------------- center ---------------- | |
| 64 | + | Text { | |
| 65 | + | anchors.centerIn: parent | |
| 66 | + | width: Math.min(implicitWidth, parent.width * 0.32) | |
| 67 | + | elide: Text.ElideRight | |
| 68 | + | horizontalAlignment: Text.AlignHCenter | |
| 69 | + | readonly property string title: ToplevelManager.activeToplevel ? ToplevelManager.activeToplevel.title : "" | |
| 70 | + | visible: title !== "" | |
| 71 | + | text: Theme.iArch + " " + title | |
| 72 | + | color: Theme.fg | |
| 73 | + | font.family: Theme.font | |
| 74 | + | font.pixelSize: Theme.fontSize | |
| 75 | + | font.bold: true | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | // ---------------- right ---------------- | |
| 79 | + | Row { | |
| 80 | + | anchors.right: parent.right | |
| 81 | + | height: parent.height | |
| 82 | + | spacing: 10 | |
| 83 | + | ||
| 84 | + | Tray {} | |
| 85 | + | ||
| 86 | + | BarPill { | |
| 87 | + | visible: bar.headset !== null | |
| 88 | + | icon: Theme.iHeadphones | |
| 89 | + | text: bar.headset ? Math.round(bar.headset.battery * 100) + "%" : "" | |
| 90 | + | onActivated: bar.togglePanel("bt") | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | BarPill { | |
| 94 | + | visible: bar.audio !== null | |
| 95 | + | icon: Theme.volIcon(bar.audio ? bar.audio.volume : 0, bar.audio ? bar.audio.muted : true) | |
| 96 | + | text: (bar.audio ? Math.round(bar.audio.volume * 100) : 0) + "%" | |
| 97 | + | + (bar.mic && bar.mic.muted ? " " + Theme.iMicOff : "") | |
| 98 | + | onActivated: button => { | |
| 99 | + | if (button === Qt.RightButton && bar.audio) bar.audio.muted = !bar.audio.muted | |
| 100 | + | else if (button === Qt.MiddleButton && bar.mic) bar.mic.muted = !bar.mic.muted | |
| 101 | + | else bar.togglePanel("audio") | |
| 102 | + | } | |
| 103 | + | onScrolled: dir => { | |
| 104 | + | if (bar.audio) bar.audio.volume = Math.max(0, Math.min(1, bar.audio.volume + dir * 0.02)) | |
| 105 | + | } | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | BarPill { | |
| 109 | + | icon: Net.wired && !(Net.wifiEnabled && Net.connected !== "") ? Theme.iEthernet | |
| 110 | + | : (Net.wifiEnabled ? Theme.wifiIcon(Net.connected !== "" ? Net.activeSignal : 0) : Theme.iWifiOff) | |
| 111 | + | tint: (Net.wired || (Net.wifiEnabled && Net.connected !== "")) ? Theme.fg : Theme.dim | |
| 112 | + | onActivated: bar.togglePanel("net") | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | BarPill { | |
| 116 | + | visible: bar.adapter !== null | |
| 117 | + | icon: !bar.btOn ? Theme.iBtOff : (bar.btLink ? Theme.iBtConn : Theme.iBt) | |
| 118 | + | tint: bar.btLink ? Theme.fg : Theme.dim | |
| 119 | + | onActivated: bar.togglePanel("bt") | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | BarPill { | |
| 123 | + | visible: bar.hasBat | |
| 124 | + | icon: Theme.batIcon(bar.batPct, bar.charging, bar.plugged) | |
| 125 | + | text: Math.round(bar.batPct * 100) + "%" | |
| 126 | + | tint: bar.batLow ? Theme.err : Theme.fg | |
| 127 | + | underline: bar.batLow ? Theme.err : Theme.accent | |
| 128 | + | onActivated: bar.togglePanel("sys") | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | BarPill { | |
| 132 | + | icon: Notifs.dnd ? Theme.iBellOff : Theme.iBell | |
| 133 | + | text: Notifs.count > 0 ? String(Notifs.count) : "" | |
| 134 | + | tint: Notifs.dnd ? Theme.dim : Theme.fg | |
| 135 | + | onActivated: button => { | |
| 136 | + | if (button === Qt.RightButton) Notifs.dnd = !Notifs.dnd | |
| 137 | + | else bar.togglePanel("notif") | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | BarPill { | |
| 142 | + | icon: Theme.iCal | |
| 143 | + | text: Qt.formatDateTime(clock.date, "dd MMM yyyy") | |
| 144 | + | onActivated: bar.togglePanel("home") | |
| 145 | + | } | |
| 146 | + | BarPill { | |
| 147 | + | icon: Theme.iClock | |
| 148 | + | text: Qt.formatDateTime(clock.date, "HH:mm") | |
| 149 | + | onActivated: bar.togglePanel("home") | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | } | |
| @@ -0,0 +1,57 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // A waybar-style block: flat, 12px padding, 1px accent underline. | |
| 4 | + | Item { | |
| 5 | + | id: p | |
| 6 | + | property string icon: "" | |
| 7 | + | property string text: "" | |
| 8 | + | property color tint: Theme.fg | |
| 9 | + | property color underline: Theme.accent | |
| 10 | + | signal activated(int button) | |
| 11 | + | signal scrolled(int dir) | |
| 12 | + | ||
| 13 | + | implicitWidth: row.implicitWidth + 24 | |
| 14 | + | implicitHeight: parent ? parent.height : Theme.barHeight | |
| 15 | + | ||
| 16 | + | Rectangle { | |
| 17 | + | anchors.fill: parent | |
| 18 | + | color: m.containsMouse ? Theme.surface : "transparent" | |
| 19 | + | } | |
| 20 | + | Rectangle { | |
| 21 | + | anchors.left: parent.left | |
| 22 | + | anchors.right: parent.right | |
| 23 | + | anchors.bottom: parent.bottom | |
| 24 | + | height: 1 | |
| 25 | + | color: p.underline | |
| 26 | + | } | |
| 27 | + | Row { | |
| 28 | + | id: row | |
| 29 | + | anchors.centerIn: parent | |
| 30 | + | spacing: 6 | |
| 31 | + | Text { | |
| 32 | + | visible: p.icon !== "" | |
| 33 | + | text: p.icon | |
| 34 | + | color: p.tint | |
| 35 | + | font.family: Theme.font | |
| 36 | + | font.pixelSize: Theme.fontSize + 1 | |
| 37 | + | anchors.verticalCenter: parent.verticalCenter | |
| 38 | + | } | |
| 39 | + | Text { | |
| 40 | + | visible: p.text !== "" | |
| 41 | + | text: p.text | |
| 42 | + | color: p.tint | |
| 43 | + | font.family: Theme.font | |
| 44 | + | font.pixelSize: Theme.fontSize | |
| 45 | + | font.bold: true | |
| 46 | + | anchors.verticalCenter: parent.verticalCenter | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | MouseArea { | |
| 50 | + | id: m | |
| 51 | + | anchors.fill: parent | |
| 52 | + | hoverEnabled: true | |
| 53 | + | acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton | |
| 54 | + | onClicked: mouse => p.activated(mouse.button) | |
| 55 | + | onWheel: w => p.scrolled(w.angleDelta.y > 0 ? 1 : -1) | |
| 56 | + | } | |
| 57 | + | } | |
[No content changes]
[No content changes]
[No content changes]
| @@ -0,0 +1,41 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // catloop.sh, natively: frames A-E awake, G-N asleep. Faster with more CPU load, | |
| 4 | + | // falls asleep after 4 idle cycles. Needs the "Waycat" font you already use. | |
| 5 | + | Item { | |
| 6 | + | id: cat | |
| 7 | + | implicitWidth: 40 | |
| 8 | + | ||
| 9 | + | property int idx: 0 | |
| 10 | + | property bool sleeping: false | |
| 11 | + | property int low: 0 | |
| 12 | + | readonly property var awake: ["A", "B", "C", "D", "E"] | |
| 13 | + | readonly property var asleep: ["G", "H", "I", "J", "K", "L", "M", "N"] | |
| 14 | + | readonly property var frames: sleeping ? asleep : awake | |
| 15 | + | ||
| 16 | + | Text { | |
| 17 | + | anchors.centerIn: parent | |
| 18 | + | text: cat.frames[cat.idx] || "" | |
| 19 | + | color: Theme.fg | |
| 20 | + | font.family: "Waycat" | |
| 21 | + | font.pixelSize: 14 | |
| 22 | + | font.bold: true | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | Timer { | |
| 26 | + | id: tick | |
| 27 | + | running: true | |
| 28 | + | repeat: true | |
| 29 | + | interval: 250 | |
| 30 | + | onTriggered: { | |
| 31 | + | var next = cat.idx + 1 | |
| 32 | + | if (next >= cat.frames.length) { | |
| 33 | + | next = 0 | |
| 34 | + | cat.low = SysStats.cpu < 0.02 ? cat.low + 1 : 0 | |
| 35 | + | cat.sleeping = cat.low >= 4 | |
| 36 | + | } | |
| 37 | + | cat.idx = next | |
| 38 | + | tick.interval = Math.round(Math.max(0.03, 1 / (4 + SysStats.cpu * 100)) * 1000) | |
| 39 | + | } | |
| 40 | + | } | |
| 41 | + | } | |
| @@ -0,0 +1,115 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Layouts | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Wayland | |
| 5 | + | import Quickshell.Hyprland | |
| 6 | + | ||
| 7 | + | PanelWindow { | |
| 8 | + | id: win | |
| 9 | + | property bool opened: false | |
| 10 | + | property int tab: 0 | |
| 11 | + | signal tabRequested(int index) | |
| 12 | + | signal closeRequested() | |
| 13 | + | ||
| 14 | + | visible: opened | |
| 15 | + | anchors { top: true; right: true } | |
| 16 | + | margins { top: 6; right: 8 } // sits just under the bar (bar's exclusive zone is respected) | |
| 17 | + | implicitWidth: 440 | |
| 18 | + | implicitHeight: 610 | |
| 19 | + | exclusiveZone: 0 | |
| 20 | + | color: "transparent" | |
| 21 | + | WlrLayershell.layer: WlrLayer.Overlay | |
| 22 | + | WlrLayershell.namespace: "aurora-panel" | |
| 23 | + | WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand | |
| 24 | + | ||
| 25 | + | // click anywhere outside to close | |
| 26 | + | HyprlandFocusGrab { | |
| 27 | + | windows: [win] | |
| 28 | + | active: win.opened | |
| 29 | + | onCleared: win.closeRequested() | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | readonly property var tabs: [ | |
| 33 | + | { icon: Theme.iHome, name: "home" }, | |
| 34 | + | { icon: Theme.iVolHigh, name: "audio" }, | |
| 35 | + | { icon: Theme.iWifi, name: "net" }, | |
| 36 | + | { icon: Theme.iBt, name: "bt" }, | |
| 37 | + | { icon: Theme.iChip, name: "system" }, | |
| 38 | + | { icon: Theme.iBell, name: "notifs" } | |
| 39 | + | ] | |
| 40 | + | ||
| 41 | + | Rectangle { | |
| 42 | + | anchors.fill: parent | |
| 43 | + | color: Theme.bgA | |
| 44 | + | border.width: 2 | |
| 45 | + | border.color: Theme.accent | |
| 46 | + | focus: true | |
| 47 | + | Keys.onEscapePressed: win.closeRequested() | |
| 48 | + | ||
| 49 | + | ColumnLayout { | |
| 50 | + | anchors.fill: parent | |
| 51 | + | anchors.margins: 12 | |
| 52 | + | spacing: 10 | |
| 53 | + | ||
| 54 | + | RowLayout { | |
| 55 | + | Layout.fillWidth: true | |
| 56 | + | spacing: 2 | |
| 57 | + | ||
| 58 | + | Repeater { | |
| 59 | + | model: win.tabs | |
| 60 | + | delegate: Rectangle { | |
| 61 | + | id: tabBtn | |
| 62 | + | required property var modelData | |
| 63 | + | required property int index | |
| 64 | + | readonly property bool current: win.tab === index | |
| 65 | + | implicitWidth: lbl.implicitWidth + 22 | |
| 66 | + | implicitHeight: 28 | |
| 67 | + | color: current ? Theme.accent : (tm.containsMouse ? Theme.surfaceHi : "transparent") | |
| 68 | + | ||
| 69 | + | Text { | |
| 70 | + | id: lbl | |
| 71 | + | anchors.centerIn: parent | |
| 72 | + | text: tabBtn.current ? tabBtn.modelData.icon + " " + tabBtn.modelData.name : tabBtn.modelData.icon | |
| 73 | + | color: tabBtn.current ? Theme.bg : Theme.fg | |
| 74 | + | font.family: Theme.font | |
| 75 | + | font.pixelSize: Theme.fontSize | |
| 76 | + | font.bold: true | |
| 77 | + | } | |
| 78 | + | Rectangle { // unread badge on the notifs tab | |
| 79 | + | visible: tabBtn.modelData.name === "notifs" && Notifs.count > 0 && !tabBtn.current | |
| 80 | + | width: 6; height: 6 | |
| 81 | + | anchors.top: parent.top | |
| 82 | + | anchors.right: parent.right | |
| 83 | + | anchors.margins: 3 | |
| 84 | + | color: Theme.accent | |
| 85 | + | } | |
| 86 | + | MouseArea { | |
| 87 | + | id: tm | |
| 88 | + | anchors.fill: parent | |
| 89 | + | hoverEnabled: true | |
| 90 | + | cursorShape: Qt.PointingHandCursor | |
| 91 | + | onClicked: win.tabRequested(tabBtn.index) | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | Item { Layout.fillWidth: true } | |
| 96 | + | IconBtn { icon: Theme.iClose; implicitHeight: 28; onClicked: win.closeRequested() } | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 1; color: Theme.line } | |
| 100 | + | ||
| 101 | + | StackLayout { | |
| 102 | + | Layout.fillWidth: true | |
| 103 | + | Layout.fillHeight: true | |
| 104 | + | currentIndex: win.tab | |
| 105 | + | ||
| 106 | + | HomePage { onCloseRequested: win.closeRequested() } | |
| 107 | + | AudioPage {} | |
| 108 | + | NetPage { active: win.opened && win.tab === 2 } | |
| 109 | + | BtPage { active: win.opened && win.tab === 3 } | |
| 110 | + | SysPage {} | |
| 111 | + | NotifPage {} | |
| 112 | + | } | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | } | |
| @@ -0,0 +1,46 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Io | |
| 5 | + | ||
| 6 | + | // Mirrors your gpu_mode.sh (envycontrol -q) and can switch modes via pkexec. | |
| 7 | + | Singleton { | |
| 8 | + | id: root | |
| 9 | + | property string mode: "" // "" = envycontrol not available | |
| 10 | + | property string pending: "" // mode queued, applies after reboot | |
| 11 | + | property string message: "" | |
| 12 | + | property bool busy: false | |
| 13 | + | property string target: "" | |
| 14 | + | ||
| 15 | + | function refresh() { query.running = true } | |
| 16 | + | function set(m) { | |
| 17 | + | if (busy) return | |
| 18 | + | busy = true | |
| 19 | + | message = "" | |
| 20 | + | target = m | |
| 21 | + | sw.command = ["pkexec", "envycontrol", "-s", m] | |
| 22 | + | sw.running = true | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | Process { | |
| 26 | + | id: query | |
| 27 | + | command: ["envycontrol", "-q"] | |
| 28 | + | running: true | |
| 29 | + | stdout: StdioCollector { | |
| 30 | + | onStreamFinished: { | |
| 31 | + | var w = text.trim().split(/\s+/) | |
| 32 | + | root.mode = w.length && w[0] !== "" ? w[w.length - 1] : "" | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | } | |
| 36 | + | Process { | |
| 37 | + | id: sw | |
| 38 | + | stderr: StdioCollector { id: swErr } | |
| 39 | + | onExited: (code, status) => { | |
| 40 | + | root.busy = false | |
| 41 | + | if (code === 0) root.pending = root.target | |
| 42 | + | else root.message = "switch failed or cancelled" | |
| 43 | + | root.refresh() | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | } | |
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import QtQuick.Layouts | |
| 3 | 3 | import Quickshell | |
| 4 | + | import Quickshell.Bluetooth | |
| 4 | 5 | import Quickshell.Services.Pipewire | |
| 5 | 6 | ||
| 6 | 7 | ColumnLayout { | |
| @@ -11,10 +12,10 @@ ColumnLayout { | |||
| 11 | 12 | property string armed: "" | |
| 12 | 13 | Timer { id: disarm; interval: 3000; onTriggered: page.armed = "" } | |
| 13 | 14 | ||
| 14 | − | function act(name, cmd, confirm) { | |
| 15 | + | function act(name, fn, confirm) { | |
| 15 | 16 | if (confirm && armed !== name) { armed = name; disarm.restart(); return } | |
| 16 | 17 | armed = "" | |
| 17 | − | Quickshell.execDetached(cmd) | |
| 18 | + | fn() | |
| 18 | 19 | closeRequested() | |
| 19 | 20 | } | |
| 20 | 21 | ||
| @@ -38,7 +39,6 @@ ColumnLayout { | |||
| 38 | 39 | onMoved: v => { if (audio) audio.volume = v } | |
| 39 | 40 | onIconClicked: if (audio) audio.muted = !audio.muted | |
| 40 | 41 | } | |
| 41 | − | ||
| 42 | 42 | SliderRow { | |
| 43 | 43 | Layout.fillWidth: true | |
| 44 | 44 | readonly property var audio: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null | |
| @@ -48,7 +48,6 @@ ColumnLayout { | |||
| 48 | 48 | onMoved: v => { if (audio) audio.volume = v } | |
| 49 | 49 | onIconClicked: if (audio) audio.muted = !audio.muted | |
| 50 | 50 | } | |
| 51 | − | ||
| 52 | 51 | SliderRow { | |
| 53 | 52 | Layout.fillWidth: true | |
| 54 | 53 | visible: Bright.available | |
| @@ -58,6 +57,44 @@ ColumnLayout { | |||
| 58 | 57 | onMoved: v => Bright.setPercent(Math.max(0.01, v) * 100) | |
| 59 | 58 | } | |
| 60 | 59 | ||
| 60 | + | SectionLabel { text: "quick toggles" } | |
| 61 | + | ||
| 62 | + | RowLayout { | |
| 63 | + | Layout.fillWidth: true | |
| 64 | + | spacing: 6 | |
| 65 | + | IconBtn { | |
| 66 | + | Layout.fillWidth: true | |
| 67 | + | icon: Net.wifiEnabled ? Theme.iWifi : Theme.iWifiOff | |
| 68 | + | label: "wifi" | |
| 69 | + | active: Net.wifiEnabled | |
| 70 | + | onClicked: Net.setWifi(!Net.wifiEnabled) | |
| 71 | + | } | |
| 72 | + | IconBtn { | |
| 73 | + | Layout.fillWidth: true | |
| 74 | + | readonly property var ad: Bluetooth.defaultAdapter | |
| 75 | + | visible: ad !== null | |
| 76 | + | icon: ad && ad.enabled ? Theme.iBt : Theme.iBtOff | |
| 77 | + | label: "bt" | |
| 78 | + | active: ad ? ad.enabled : false | |
| 79 | + | onClicked: if (ad) ad.enabled = !ad.enabled | |
| 80 | + | } | |
| 81 | + | IconBtn { | |
| 82 | + | Layout.fillWidth: true | |
| 83 | + | icon: Notifs.dnd ? Theme.iBellOff : Theme.iBell | |
| 84 | + | label: "dnd" | |
| 85 | + | active: Notifs.dnd | |
| 86 | + | onClicked: Notifs.dnd = !Notifs.dnd | |
| 87 | + | } | |
| 88 | + | IconBtn { | |
| 89 | + | Layout.fillWidth: true | |
| 90 | + | readonly property var audio: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null | |
| 91 | + | icon: audio && audio.muted ? Theme.iMicOff : Theme.iMic | |
| 92 | + | label: "mic" | |
| 93 | + | active: audio ? !audio.muted : false | |
| 94 | + | onClicked: if (audio) audio.muted = !audio.muted | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | ||
| 61 | 98 | Item { Layout.fillHeight: true } | |
| 62 | 99 | ||
| 63 | 100 | SectionLabel { text: "session" } | |
| @@ -68,33 +105,33 @@ ColumnLayout { | |||
| 68 | 105 | IconBtn { | |
| 69 | 106 | Layout.fillWidth: true | |
| 70 | 107 | icon: Theme.iLock | |
| 71 | − | onClicked: page.act("lock", ["loginctl", "lock-session"], false) | |
| 108 | + | onClicked: page.act("lock", function () { Quickshell.execDetached(["loginctl", "lock-session"]) }, false) | |
| 72 | 109 | } | |
| 73 | 110 | IconBtn { | |
| 74 | 111 | Layout.fillWidth: true | |
| 75 | 112 | icon: Theme.iSleep | |
| 76 | − | onClicked: page.act("sleep", ["systemctl", "suspend"], false) | |
| 113 | + | onClicked: page.act("sleep", function () { Quickshell.execDetached(["systemctl", "suspend"]) }, false) | |
| 77 | 114 | } | |
| 78 | 115 | IconBtn { | |
| 79 | 116 | Layout.fillWidth: true | |
| 80 | 117 | icon: Theme.iLogout | |
| 81 | 118 | label: page.armed === "logout" ? "sure?" : "" | |
| 82 | 119 | active: page.armed === "logout" | |
| 83 | − | onClicked: page.act("logout", ["hyprctl", "dispatch", "exit"], true) | |
| 120 | + | onClicked: page.act("logout", function () { Hypr.logout() }, true) | |
| 84 | 121 | } | |
| 85 | 122 | IconBtn { | |
| 86 | 123 | Layout.fillWidth: true | |
| 87 | 124 | icon: Theme.iReboot | |
| 88 | 125 | label: page.armed === "reboot" ? "sure?" : "" | |
| 89 | 126 | active: page.armed === "reboot" | |
| 90 | − | onClicked: page.act("reboot", ["systemctl", "reboot"], true) | |
| 127 | + | onClicked: page.act("reboot", function () { Quickshell.execDetached(["systemctl", "reboot"]) }, true) | |
| 91 | 128 | } | |
| 92 | 129 | IconBtn { | |
| 93 | 130 | Layout.fillWidth: true | |
| 94 | 131 | icon: Theme.iPower | |
| 95 | 132 | label: page.armed === "off" ? "sure?" : "" | |
| 96 | 133 | active: page.armed === "off" | |
| 97 | − | onClicked: page.act("off", ["systemctl", "poweroff"], true) | |
| 134 | + | onClicked: page.act("off", function () { Quickshell.execDetached(["systemctl", "poweroff"]) }, true) | |
| 98 | 135 | } | |
| 99 | 136 | } | |
| 100 | 137 | } | |
| @@ -0,0 +1,19 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | ||
| 5 | + | // Compositor actions. Your waybar config uses the new Lua-style dispatchers | |
| 6 | + | // (hl.dsp.focus{...}), so that is the default; flip luaSyntax for older Hyprland. | |
| 7 | + | Singleton { | |
| 8 | + | property bool luaSyntax: true | |
| 9 | + | ||
| 10 | + | function workspace(arg) { | |
| 11 | + | if (luaSyntax) | |
| 12 | + | Quickshell.execDetached(["hyprctl", "dispatch", 'hl.dsp.focus({workspace="' + arg + '"})']) | |
| 13 | + | else | |
| 14 | + | Quickshell.execDetached(["hyprctl", "dispatch", "workspace", String(arg)]) | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // compositor-agnostic logout | |
| 18 | + | function logout() { Quickshell.execDetached(["sh", "-c", 'loginctl terminate-user "$USER"']) } | |
| 19 | + | } | |
[No content changes]
[No content changes]
[No content changes]
| @@ -7,7 +7,8 @@ import Quickshell.Io | |||
| 7 | 7 | Singleton { | |
| 8 | 8 | id: root | |
| 9 | 9 | ||
| 10 | − | property bool active: false // NetPage sets this while visible | |
| 10 | + | property bool active: false // NetPage sets this while visible (faster polling + rescan) | |
| 11 | + | property bool wired: false | |
| 11 | 12 | property bool wifiEnabled: true | |
| 12 | 13 | property string wifiDevice: "" | |
| 13 | 14 | property var raw: [] // [{inUse, ssid, signal, security}] | |
| @@ -21,6 +22,10 @@ Singleton { | |||
| 21 | 22 | property bool lastWasNew: false | |
| 22 | 23 | ||
| 23 | 24 | readonly property var networks: build(raw, savedNames) | |
| 25 | + | readonly property int activeSignal: { | |
| 26 | + | for (var i = 0; i < raw.length; i++) if (raw[i].inUse) return raw[i].signal | |
| 27 | + | return 0 | |
| 28 | + | } | |
| 24 | 29 | readonly property string connected: { | |
| 25 | 30 | for (var i = 0; i < raw.length; i++) if (raw[i].inUse) return raw[i].ssid | |
| 26 | 31 | return "" | |
| @@ -123,10 +128,12 @@ Singleton { | |||
| 123 | 128 | function setWifi(on) { run(["radio", "wifi", on ? "on" : "off"]) } | |
| 124 | 129 | ||
| 125 | 130 | // ---- processes ---------------------------------------------------- | |
| 131 | + | // always on (the bar shows wifi state); quicker while the Net tab is open | |
| 126 | 132 | Timer { | |
| 127 | − | interval: 10000 | |
| 128 | − | running: root.active | |
| 133 | + | interval: root.active ? 10000 : 30000 | |
| 134 | + | running: true | |
| 129 | 135 | repeat: true | |
| 136 | + | triggeredOnStart: true | |
| 130 | 137 | onTriggered: root.refresh() | |
| 131 | 138 | } | |
| 132 | 139 | ||
| @@ -152,11 +159,14 @@ Singleton { | |||
| 152 | 159 | command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] | |
| 153 | 160 | stdout: StdioCollector { | |
| 154 | 161 | onStreamFinished: { | |
| 155 | − | var lines = text.split("\n") | |
| 162 | + | var lines = text.split("\n"), wired = false | |
| 156 | 163 | for (var i = 0; i < lines.length; i++) { | |
| 157 | 164 | var f = root.parseTerse(lines[i]) | |
| 158 | − | if (f.length >= 2 && f[1] === "wifi") { root.wifiDevice = f[0]; return } | |
| 165 | + | if (f.length < 3) continue | |
| 166 | + | if (f[1] === "wifi") root.wifiDevice = f[0] | |
| 167 | + | if (f[1] === "ethernet" && f[2] === "connected") wired = true | |
| 159 | 168 | } | |
| 169 | + | root.wired = wired | |
| 160 | 170 | } | |
| 161 | 171 | } | |
| 162 | 172 | } | |
[No content changes]
| @@ -0,0 +1,112 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Layouts | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Services.Notifications | |
| 5 | + | ||
| 6 | + | Rectangle { | |
| 7 | + | id: card | |
| 8 | + | required property var n // Notification | |
| 9 | + | property bool popup: false | |
| 10 | + | ||
| 11 | + | Layout.fillWidth: true | |
| 12 | + | implicitHeight: body.implicitHeight + 20 | |
| 13 | + | color: popup ? Theme.bg : Theme.surface | |
| 14 | + | border.width: popup ? 2 : 1 | |
| 15 | + | border.color: n.urgency === NotificationUrgency.Critical ? Theme.err : (popup ? Theme.accent : Theme.line) | |
| 16 | + | ||
| 17 | + | readonly property string imgSource: { | |
| 18 | + | if (n.image && n.image !== "") return n.image | |
| 19 | + | if (n.appIcon && n.appIcon !== "") return Quickshell.iconPath(n.appIcon, true) | |
| 20 | + | return "" | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | function close() { | |
| 24 | + | if (popup) Notifs.dropPopup(n) | |
| 25 | + | else n.dismiss() | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | MouseArea { | |
| 29 | + | anchors.fill: parent | |
| 30 | + | onClicked: if (card.popup) Notifs.dropPopup(card.n) | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | RowLayout { | |
| 34 | + | id: body | |
| 35 | + | anchors.fill: parent | |
| 36 | + | anchors.margins: 10 | |
| 37 | + | spacing: 10 | |
| 38 | + | ||
| 39 | + | Image { | |
| 40 | + | visible: card.imgSource !== "" | |
| 41 | + | source: card.imgSource | |
| 42 | + | Layout.preferredWidth: 36 | |
| 43 | + | Layout.preferredHeight: 36 | |
| 44 | + | Layout.alignment: Qt.AlignTop | |
| 45 | + | sourceSize: Qt.size(72, 72) | |
| 46 | + | fillMode: Image.PreserveAspectFit | |
| 47 | + | asynchronous: true | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | ColumnLayout { | |
| 51 | + | Layout.fillWidth: true | |
| 52 | + | spacing: 2 | |
| 53 | + | ||
| 54 | + | RowLayout { | |
| 55 | + | Layout.fillWidth: true | |
| 56 | + | Text { | |
| 57 | + | Layout.fillWidth: true | |
| 58 | + | elide: Text.ElideRight | |
| 59 | + | text: card.n.appName | |
| 60 | + | color: Theme.accent | |
| 61 | + | font.family: Theme.font | |
| 62 | + | font.pixelSize: Theme.fontSize - 3 | |
| 63 | + | font.letterSpacing: 1 | |
| 64 | + | } | |
| 65 | + | IconBtn { | |
| 66 | + | icon: Theme.iClose | |
| 67 | + | iconSize: 12 | |
| 68 | + | implicitWidth: 22 | |
| 69 | + | implicitHeight: 22 | |
| 70 | + | onClicked: card.close() | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | Text { | |
| 74 | + | Layout.fillWidth: true | |
| 75 | + | text: card.n.summary | |
| 76 | + | wrapMode: Text.Wrap | |
| 77 | + | maximumLineCount: 2 | |
| 78 | + | elide: Text.ElideRight | |
| 79 | + | color: Theme.fg | |
| 80 | + | font.family: Theme.font | |
| 81 | + | font.pixelSize: Theme.fontSize | |
| 82 | + | font.bold: true | |
| 83 | + | } | |
| 84 | + | Text { | |
| 85 | + | visible: card.n.body !== "" | |
| 86 | + | Layout.fillWidth: true | |
| 87 | + | text: card.n.body | |
| 88 | + | textFormat: Text.StyledText | |
| 89 | + | wrapMode: Text.Wrap | |
| 90 | + | maximumLineCount: card.popup ? 4 : 6 | |
| 91 | + | elide: Text.ElideRight | |
| 92 | + | color: Theme.dim | |
| 93 | + | font.family: Theme.font | |
| 94 | + | font.pixelSize: Theme.fontSize - 1 | |
| 95 | + | } | |
| 96 | + | Flow { | |
| 97 | + | visible: card.n.actions.length > 0 | |
| 98 | + | Layout.fillWidth: true | |
| 99 | + | spacing: 6 | |
| 100 | + | Repeater { | |
| 101 | + | model: card.n.actions | |
| 102 | + | delegate: IconBtn { | |
| 103 | + | required property var modelData | |
| 104 | + | label: modelData.text | |
| 105 | + | implicitHeight: 24 | |
| 106 | + | onClicked: { modelData.invoke(); if (card.popup) Notifs.dropPopup(card.n) } | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | } | |
| 112 | + | } | |
| @@ -0,0 +1,65 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Layouts | |
| 3 | + | ||
| 4 | + | ColumnLayout { | |
| 5 | + | id: page | |
| 6 | + | spacing: 8 | |
| 7 | + | ||
| 8 | + | RowLayout { | |
| 9 | + | Layout.fillWidth: true | |
| 10 | + | spacing: 6 | |
| 11 | + | Text { | |
| 12 | + | Layout.fillWidth: true | |
| 13 | + | text: Notifs.count + (Notifs.count === 1 ? " notification" : " notifications") | |
| 14 | + | color: Notifs.count > 0 ? Theme.accent : Theme.dim | |
| 15 | + | font.family: Theme.font | |
| 16 | + | font.pixelSize: Theme.fontSize | |
| 17 | + | } | |
| 18 | + | IconBtn { | |
| 19 | + | icon: Notifs.dnd ? Theme.iBellOff : Theme.iBell | |
| 20 | + | label: "dnd" | |
| 21 | + | active: Notifs.dnd | |
| 22 | + | onClicked: Notifs.dnd = !Notifs.dnd | |
| 23 | + | } | |
| 24 | + | IconBtn { | |
| 25 | + | icon: Theme.iTrash | |
| 26 | + | label: "clear" | |
| 27 | + | onClicked: Notifs.clearAll() | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | SectionLabel { text: "history" } | |
| 32 | + | ||
| 33 | + | Text { | |
| 34 | + | visible: Notifs.count === 0 | |
| 35 | + | Layout.alignment: Qt.AlignHCenter | |
| 36 | + | Layout.topMargin: 40 | |
| 37 | + | text: Theme.iBell + " all quiet" | |
| 38 | + | color: Theme.dim | |
| 39 | + | font.family: Theme.font | |
| 40 | + | font.pixelSize: Theme.fontSize + 1 | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | Flickable { | |
| 44 | + | Layout.fillWidth: true | |
| 45 | + | Layout.fillHeight: true | |
| 46 | + | clip: true | |
| 47 | + | contentWidth: width | |
| 48 | + | contentHeight: list.implicitHeight | |
| 49 | + | boundsBehavior: Flickable.StopAtBounds | |
| 50 | + | id: flick | |
| 51 | + | ||
| 52 | + | ColumnLayout { | |
| 53 | + | id: list | |
| 54 | + | width: flick.width | |
| 55 | + | spacing: 6 | |
| 56 | + | Repeater { | |
| 57 | + | model: { Notifs.count; return Notifs.history() } | |
| 58 | + | delegate: NotifCard { | |
| 59 | + | required property var modelData | |
| 60 | + | n: modelData | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | } | |
| @@ -0,0 +1,34 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Layouts | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Wayland | |
| 5 | + | ||
| 6 | + | PanelWindow { | |
| 7 | + | id: win | |
| 8 | + | property bool suppressed: false // hidden while the control panel is open | |
| 9 | + | ||
| 10 | + | visible: Notifs.popups.length > 0 && !suppressed | |
| 11 | + | anchors { top: true; right: true } | |
| 12 | + | margins { top: 6; right: 8 } | |
| 13 | + | implicitWidth: 360 | |
| 14 | + | implicitHeight: col.implicitHeight | |
| 15 | + | exclusiveZone: 0 | |
| 16 | + | color: "transparent" | |
| 17 | + | WlrLayershell.layer: WlrLayer.Overlay | |
| 18 | + | WlrLayershell.namespace: "aurora-notifs" | |
| 19 | + | WlrLayershell.keyboardFocus: WlrKeyboardFocus.None | |
| 20 | + | ||
| 21 | + | ColumnLayout { | |
| 22 | + | id: col | |
| 23 | + | width: 360 | |
| 24 | + | spacing: 6 | |
| 25 | + | Repeater { | |
| 26 | + | model: Notifs.popups | |
| 27 | + | delegate: NotifCard { | |
| 28 | + | required property var modelData | |
| 29 | + | n: modelData.n | |
| 30 | + | popup: true | |
| 31 | + | } | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | } | |
| @@ -0,0 +1,59 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Services.Notifications | |
| 5 | + | ||
| 6 | + | // Native notification daemon: toasts + history + do-not-disturb. | |
| 7 | + | // Stop dunst/mako first, only one daemon can own org.freedesktop.Notifications. | |
| 8 | + | Singleton { | |
| 9 | + | id: root | |
| 10 | + | property bool dnd: false | |
| 11 | + | property var popups: [] // [{n, until}] | |
| 12 | + | readonly property var model: server.trackedNotifications | |
| 13 | + | readonly property int count: server.trackedNotifications.values.length | |
| 14 | + | ||
| 15 | + | function history() { // newest first | |
| 16 | + | var l = server.trackedNotifications.values, out = [] | |
| 17 | + | for (var i = l.length - 1; i >= 0; i--) out.push(l[i]) | |
| 18 | + | return out | |
| 19 | + | } | |
| 20 | + | function clearAll() { | |
| 21 | + | var l = server.trackedNotifications.values, all = [] | |
| 22 | + | for (var i = 0; i < l.length; i++) all.push(l[i]) | |
| 23 | + | for (var j = 0; j < all.length; j++) all[j].dismiss() | |
| 24 | + | popups = [] | |
| 25 | + | } | |
| 26 | + | function dropPopup(n) { | |
| 27 | + | popups = popups.filter(function (p) { return p.n !== n }) | |
| 28 | + | } | |
| 29 | + | function push(n) { | |
| 30 | + | var ms = n.urgency === NotificationUrgency.Critical ? 12000 | |
| 31 | + | : (n.expireTimeout > 0 ? n.expireTimeout * 1000 : 5000) | |
| 32 | + | var next = popups.concat([{ n: n, until: Date.now() + ms }]) | |
| 33 | + | popups = next.slice(-4) | |
| 34 | + | n.closed.connect(function () { root.dropPopup(n) }) | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | NotificationServer { | |
| 38 | + | id: server | |
| 39 | + | keepOnReload: true | |
| 40 | + | actionsSupported: true | |
| 41 | + | bodyMarkupSupported: true | |
| 42 | + | imageSupported: true | |
| 43 | + | onNotification: n => { | |
| 44 | + | n.tracked = true | |
| 45 | + | if (!root.dnd || n.urgency === NotificationUrgency.Critical) root.push(n) | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | Timer { | |
| 50 | + | interval: 500 | |
| 51 | + | running: root.popups.length > 0 | |
| 52 | + | repeat: true | |
| 53 | + | onTriggered: { | |
| 54 | + | var now = Date.now() | |
| 55 | + | var keep = root.popups.filter(function (p) { return p.until > now }) | |
| 56 | + | if (keep.length !== root.popups.length) root.popups = keep | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | } | |
[No content changes]
[No content changes]
| @@ -0,0 +1,33 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | ||
| 3 | + | // Bar-graph history, same flavour as the visualizer bars in your screenshot. | |
| 4 | + | Item { | |
| 5 | + | id: s | |
| 6 | + | property var values: [] | |
| 7 | + | property int slots: 40 | |
| 8 | + | implicitHeight: 44 | |
| 9 | + | ||
| 10 | + | Row { | |
| 11 | + | anchors.fill: parent | |
| 12 | + | spacing: 2 | |
| 13 | + | Repeater { | |
| 14 | + | model: s.slots | |
| 15 | + | delegate: Item { | |
| 16 | + | required property int index | |
| 17 | + | readonly property int vi: s.values.length - s.slots + index | |
| 18 | + | readonly property real v: vi >= 0 && vi < s.values.length ? s.values[vi] : 0 | |
| 19 | + | width: (s.width - (s.slots - 1) * 2) / s.slots | |
| 20 | + | height: s.height | |
| 21 | + | Rectangle { | |
| 22 | + | anchors.bottom: parent.bottom | |
| 23 | + | width: parent.width | |
| 24 | + | height: Math.max(2, parent.height * parent.v) | |
| 25 | + | gradient: Gradient { | |
| 26 | + | GradientStop { position: 0; color: Theme.accent } | |
| 27 | + | GradientStop { position: 1; color: Theme.accentDim } | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | } | |
| 33 | + | } | |
| @@ -0,0 +1,38 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Layouts | |
| 3 | + | ||
| 4 | + | RowLayout { | |
| 5 | + | id: r | |
| 6 | + | property string label: "" | |
| 7 | + | property real frac: 0 | |
| 8 | + | property string valueText: "" | |
| 9 | + | property color fill: Theme.accent | |
| 10 | + | Layout.fillWidth: true | |
| 11 | + | spacing: 10 | |
| 12 | + | ||
| 13 | + | Text { | |
| 14 | + | text: r.label | |
| 15 | + | Layout.preferredWidth: 64 | |
| 16 | + | color: Theme.dim | |
| 17 | + | font.family: Theme.font | |
| 18 | + | font.pixelSize: Theme.fontSize - 1 | |
| 19 | + | } | |
| 20 | + | Rectangle { | |
| 21 | + | Layout.fillWidth: true | |
| 22 | + | Layout.preferredHeight: 6 | |
| 23 | + | color: Theme.surfaceHi | |
| 24 | + | Rectangle { | |
| 25 | + | width: parent.width * Math.max(0, Math.min(1, r.frac)) | |
| 26 | + | height: parent.height | |
| 27 | + | color: r.fill | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | Text { | |
| 31 | + | text: r.valueText | |
| 32 | + | Layout.preferredWidth: 96 | |
| 33 | + | horizontalAlignment: Text.AlignRight | |
| 34 | + | color: Theme.fg | |
| 35 | + | font.family: Theme.font | |
| 36 | + | font.pixelSize: Theme.fontSize - 1 | |
| 37 | + | } | |
| 38 | + | } | |
| @@ -0,0 +1,179 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import QtQuick.Layouts | |
| 3 | + | import Quickshell.Services.UPower | |
| 4 | + | ||
| 5 | + | Flickable { | |
| 6 | + | id: flick | |
| 7 | + | clip: true | |
| 8 | + | contentWidth: width | |
| 9 | + | contentHeight: col.implicitHeight | |
| 10 | + | boundsBehavior: Flickable.StopAtBounds | |
| 11 | + | ||
| 12 | + | readonly property var bat: UPower.displayDevice | |
| 13 | + | readonly property bool hasBat: bat !== null && bat.isPresent | |
| 14 | + | readonly property real batPct: hasBat ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0 | |
| 15 | + | readonly property bool charging: hasBat && bat.state === UPowerDeviceState.Charging | |
| 16 | + | ||
| 17 | + | property string armedGpu: "" | |
| 18 | + | Timer { id: disarm; interval: 3000; onTriggered: flick.armedGpu = "" } | |
| 19 | + | ||
| 20 | + | function gpuClick(m) { | |
| 21 | + | if (Gpu.mode === m || Gpu.busy) return | |
| 22 | + | if (armedGpu !== m) { armedGpu = m; disarm.restart(); return } | |
| 23 | + | armedGpu = "" | |
| 24 | + | Gpu.set(m) | |
| 25 | + | } | |
| 26 | + | function stateText() { | |
| 27 | + | if (!hasBat) return "" | |
| 28 | + | switch (bat.state) { | |
| 29 | + | case UPowerDeviceState.Charging: return "charging" | |
| 30 | + | case UPowerDeviceState.Discharging: return "discharging" | |
| 31 | + | case UPowerDeviceState.FullyCharged: return "full" | |
| 32 | + | default: return "idle" | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | function g(v) { return v.toFixed(1) + "G" } | |
| 36 | + | ||
| 37 | + | ColumnLayout { | |
| 38 | + | id: col | |
| 39 | + | width: flick.width | |
| 40 | + | spacing: 8 | |
| 41 | + | ||
| 42 | + | SectionLabel { text: "cpu" } | |
| 43 | + | RowLayout { | |
| 44 | + | Layout.fillWidth: true | |
| 45 | + | spacing: 10 | |
| 46 | + | Text { | |
| 47 | + | text: Theme.iChip + " " + Math.round(SysStats.cpu * 100) + "%" | |
| 48 | + | Layout.preferredWidth: 80 | |
| 49 | + | color: Theme.fg | |
| 50 | + | font.family: Theme.font | |
| 51 | + | font.pixelSize: Theme.fontSize + 2 | |
| 52 | + | font.bold: true | |
| 53 | + | } | |
| 54 | + | Spark { Layout.fillWidth: true; values: SysStats.history } | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | SectionLabel { text: "memory & disk"; Layout.topMargin: 6 } | |
| 58 | + | StatBar { | |
| 59 | + | label: "memory" | |
| 60 | + | frac: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0 | |
| 61 | + | valueText: flick.g(SysStats.memUsed) + " / " + flick.g(SysStats.memTotal) | |
| 62 | + | } | |
| 63 | + | StatBar { | |
| 64 | + | label: "disk /" | |
| 65 | + | frac: SysStats.diskTotal > 0 ? SysStats.diskUsed / SysStats.diskTotal : 0 | |
| 66 | + | valueText: Math.round(SysStats.diskUsed) + "G / " + Math.round(SysStats.diskTotal) + "G" | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | ColumnLayout { | |
| 70 | + | visible: flick.hasBat | |
| 71 | + | Layout.fillWidth: true | |
| 72 | + | Layout.topMargin: 6 | |
| 73 | + | spacing: 8 | |
| 74 | + | SectionLabel { text: "battery" } | |
| 75 | + | StatBar { | |
| 76 | + | label: "charge" | |
| 77 | + | frac: flick.batPct | |
| 78 | + | fill: flick.batPct <= 0.15 && !flick.charging ? Theme.err : Theme.accent | |
| 79 | + | valueText: Math.round(flick.batPct * 100) + "% " + flick.stateText() | |
| 80 | + | } | |
| 81 | + | Text { | |
| 82 | + | Layout.fillWidth: true | |
| 83 | + | wrapMode: Text.Wrap | |
| 84 | + | color: Theme.dim | |
| 85 | + | font.family: Theme.font | |
| 86 | + | font.pixelSize: Theme.fontSize - 1 | |
| 87 | + | lineHeight: 1.3 | |
| 88 | + | text: { | |
| 89 | + | if (!flick.hasBat) return "" | |
| 90 | + | var lines = [] | |
| 91 | + | var t = flick.charging ? flick.bat.timeToFull : flick.bat.timeToEmpty | |
| 92 | + | lines.push((flick.charging ? "time to full " : "time left ") + Theme.fmtTime(t)) | |
| 93 | + | if (flick.bat.changeRate !== undefined) | |
| 94 | + | lines.push("power draw " + Math.abs(flick.bat.changeRate).toFixed(1) + " W") | |
| 95 | + | if (flick.bat.healthSupported) | |
| 96 | + | lines.push("health " + Math.round(flick.bat.healthPercentage) + "%") | |
| 97 | + | return lines.join("\n") | |
| 98 | + | } | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | ColumnLayout { | |
| 103 | + | visible: PowerProfiles.hasPerformanceProfile !== undefined | |
| 104 | + | Layout.fillWidth: true | |
| 105 | + | Layout.topMargin: 6 | |
| 106 | + | spacing: 8 | |
| 107 | + | SectionLabel { text: "power profile" } | |
| 108 | + | RowLayout { | |
| 109 | + | Layout.fillWidth: true | |
| 110 | + | spacing: 6 | |
| 111 | + | IconBtn { | |
| 112 | + | Layout.fillWidth: true | |
| 113 | + | icon: Theme.iLeaf | |
| 114 | + | label: "saver" | |
| 115 | + | active: PowerProfiles.profile === PowerProfile.PowerSaver | |
| 116 | + | onClicked: PowerProfiles.profile = PowerProfile.PowerSaver | |
| 117 | + | } | |
| 118 | + | IconBtn { | |
| 119 | + | Layout.fillWidth: true | |
| 120 | + | icon: Theme.iBalance | |
| 121 | + | label: "balanced" | |
| 122 | + | active: PowerProfiles.profile === PowerProfile.Balanced | |
| 123 | + | onClicked: PowerProfiles.profile = PowerProfile.Balanced | |
| 124 | + | } | |
| 125 | + | IconBtn { | |
| 126 | + | Layout.fillWidth: true | |
| 127 | + | visible: PowerProfiles.hasPerformanceProfile | |
| 128 | + | icon: Theme.iSpeed | |
| 129 | + | label: "perf" | |
| 130 | + | active: PowerProfiles.profile === PowerProfile.Performance | |
| 131 | + | onClicked: PowerProfiles.profile = PowerProfile.Performance | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | ColumnLayout { | |
| 137 | + | visible: Gpu.mode !== "" | |
| 138 | + | Layout.fillWidth: true | |
| 139 | + | Layout.topMargin: 6 | |
| 140 | + | spacing: 8 | |
| 141 | + | SectionLabel { text: "graphics · envycontrol" } | |
| 142 | + | RowLayout { | |
| 143 | + | Layout.fillWidth: true | |
| 144 | + | spacing: 6 | |
| 145 | + | Repeater { | |
| 146 | + | model: ["integrated", "hybrid", "nvidia"] | |
| 147 | + | delegate: IconBtn { | |
| 148 | + | required property string modelData | |
| 149 | + | Layout.fillWidth: true | |
| 150 | + | icon: Theme.iGpu | |
| 151 | + | label: flick.armedGpu === modelData ? "sure?" : modelData | |
| 152 | + | active: Gpu.mode === modelData | |
| 153 | + | opacity: Gpu.busy ? 0.5 : 1 | |
| 154 | + | onClicked: flick.gpuClick(modelData) | |
| 155 | + | } | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | Text { | |
| 159 | + | visible: Gpu.pending !== "" || Gpu.message !== "" || Gpu.busy | |
| 160 | + | Layout.fillWidth: true | |
| 161 | + | wrapMode: Text.Wrap | |
| 162 | + | color: Gpu.message !== "" ? Theme.err : Theme.accent | |
| 163 | + | font.family: Theme.font | |
| 164 | + | font.pixelSize: Theme.fontSize - 2 | |
| 165 | + | text: Gpu.busy ? "switching…" : (Gpu.message !== "" ? Gpu.message : "queued: " + Gpu.pending + " · reboot to apply") | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | Text { | |
| 170 | + | Layout.topMargin: 10 | |
| 171 | + | Layout.alignment: Qt.AlignRight | |
| 172 | + | text: "up " + Theme.fmtTime(SysStats.uptime) | |
| 173 | + | color: Theme.dim | |
| 174 | + | font.family: Theme.font | |
| 175 | + | font.pixelSize: Theme.fontSize - 2 | |
| 176 | + | } | |
| 177 | + | Item { Layout.preferredHeight: 6 } | |
| 178 | + | } | |
| 179 | + | } | |
| @@ -0,0 +1,69 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Io | |
| 5 | + | ||
| 6 | + | // cpu / memory / disk / uptime. Shared by the cat, the bar and the System tab. | |
| 7 | + | Singleton { | |
| 8 | + | id: root | |
| 9 | + | property real cpu: 0 // 0..1 | |
| 10 | + | property real memUsed: 0 // GiB | |
| 11 | + | property real memTotal: 0 | |
| 12 | + | property real diskUsed: 0 | |
| 13 | + | property real diskTotal: 0 | |
| 14 | + | property real uptime: 0 // seconds | |
| 15 | + | property var history: [] // last 40 cpu samples | |
| 16 | + | property real prevIdle: -1 | |
| 17 | + | property real prevTotal: 0 | |
| 18 | + | ||
| 19 | + | FileView { id: statFile; path: "/proc/stat"; blockLoading: true } | |
| 20 | + | FileView { id: memFile; path: "/proc/meminfo"; blockLoading: true } | |
| 21 | + | FileView { id: upFile; path: "/proc/uptime"; blockLoading: true } | |
| 22 | + | ||
| 23 | + | function sample() { | |
| 24 | + | statFile.reload() | |
| 25 | + | var m = statFile.text().match(/^cpu\s+(.*)$/m) | |
| 26 | + | if (m) { | |
| 27 | + | var f = m[1].trim().split(/\s+/).map(Number) | |
| 28 | + | var idle = f[3] + (f[4] || 0) | |
| 29 | + | var total = 0 | |
| 30 | + | for (var i = 0; i < Math.min(8, f.length); i++) total += f[i] | |
| 31 | + | if (prevIdle >= 0 && total > prevTotal) | |
| 32 | + | cpu = Math.max(0, Math.min(1, 1 - (idle - prevIdle) / (total - prevTotal))) | |
| 33 | + | prevIdle = idle | |
| 34 | + | prevTotal = total | |
| 35 | + | var h = history.slice() | |
| 36 | + | h.push(cpu) | |
| 37 | + | if (h.length > 40) h.shift() | |
| 38 | + | history = h | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | memFile.reload() | |
| 42 | + | var mt = memFile.text() | |
| 43 | + | var t = mt.match(/MemTotal:\s+(\d+)/), a = mt.match(/MemAvailable:\s+(\d+)/) | |
| 44 | + | if (t && a) { | |
| 45 | + | memTotal = parseInt(t[1]) / 1048576 | |
| 46 | + | memUsed = (parseInt(t[1]) - parseInt(a[1])) / 1048576 | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | upFile.reload() | |
| 50 | + | uptime = parseFloat(upFile.text().split(" ")[0]) || 0 | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | Timer { interval: 1000; running: true; repeat: true; triggeredOnStart: true; onTriggered: root.sample() } | |
| 54 | + | ||
| 55 | + | Process { | |
| 56 | + | id: dfProc | |
| 57 | + | command: ["df", "-B1", "--output=used,size", "/"] | |
| 58 | + | stdout: StdioCollector { | |
| 59 | + | onStreamFinished: { | |
| 60 | + | var l = text.trim().split("\n") | |
| 61 | + | if (l.length < 2) return | |
| 62 | + | var p = l[1].trim().split(/\s+/) | |
| 63 | + | root.diskUsed = parseFloat(p[0]) / 1073741824 | |
| 64 | + | root.diskTotal = parseFloat(p[1]) / 1073741824 | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | Timer { interval: 30000; running: true; repeat: true; triggeredOnStart: true; onTriggered: dfProc.running = true } | |
| 69 | + | } | |
| @@ -3,7 +3,7 @@ import QtQuick | |||
| 3 | 3 | import Quickshell | |
| 4 | 4 | import Quickshell.Io | |
| 5 | 5 | ||
| 6 | − | // Reads ~/.cache/wal/colors.json (pywal16) and hot-reloads whenever `wal` runs. | |
| 6 | + | // pywal16 colours (~/.cache/wal/colors.json), hot-reloaded whenever `wal` runs. | |
| 7 | 7 | Singleton { | |
| 8 | 8 | id: root | |
| 9 | 9 | ||
| @@ -11,8 +11,11 @@ Singleton { | |||
| 11 | 11 | property string font: "JetBrainsMono Nerd Font" // any Nerd Font mono | |
| 12 | 12 | property int fontSize: 12 | |
| 13 | 13 | property real panelAlpha: 0.88 | |
| 14 | − | // -1 = auto-pick the most vivid of color1..color6, or force 0..15 | |
| 15 | − | property int accentIndex: -1 | |
| 14 | + | // your waybar uses @color14 for underlines/active workspace, so that is the default. | |
| 15 | + | // set -1 to auto-pick the most vivid of color1..color6 instead | |
| 16 | + | property int accentIndex: 14 | |
| 17 | + | property int barHeight: 18 | |
| 18 | + | property real barAlpha: 1.0 | |
| 16 | 19 | ||
| 17 | 20 | readonly property string walFile: Quickshell.env("HOME") + "/.cache/wal/colors.json" | |
| 18 | 21 | ||
| @@ -79,6 +82,8 @@ Singleton { | |||
| 79 | 82 | readonly property color line: mix(bg, accent, 0.35) | |
| 80 | 83 | readonly property color dim: mix(bg, fg, 0.55) | |
| 81 | 84 | readonly property color err: Qt.color(pal[1]) | |
| 85 | + | readonly property color barBg: Qt.rgba(bg.r, bg.g, bg.b, barAlpha) | |
| 86 | + | readonly property color cursorC: wal.special.cursor | |
| 82 | 87 | readonly property color bgA: Qt.rgba(bg.r, bg.g, bg.b, panelAlpha) | |
| 83 | 88 | ||
| 84 | 89 | // ---- Nerd Font (Material Design) glyphs --------------------------- | |
| @@ -121,6 +126,47 @@ Singleton { | |||
| 121 | 126 | readonly property string iGamepad: cp(0xF0297) | |
| 122 | 127 | readonly property string iSearch: cp(0xF0349) | |
| 123 | 128 | ||
| 129 | + | readonly property string iHeadset: cp(0xF02CE) | |
| 130 | + | readonly property string iBell: cp(0xF009A) | |
| 131 | + | readonly property string iBellOff: cp(0xF009B) | |
| 132 | + | readonly property string iChip: cp(0xF061A) | |
| 133 | + | readonly property string iMem: cp(0xF035B) | |
| 134 | + | readonly property string iDisk: cp(0xF02CA) | |
| 135 | + | readonly property string iGpu: cp(0xF08AE) | |
| 136 | + | readonly property string iLeaf: cp(0xF032A) | |
| 137 | + | readonly property string iBalance: cp(0xF05D1) | |
| 138 | + | readonly property string iSpeed: cp(0xF04C5) | |
| 139 | + | readonly property string iCal: cp(0xF00ED) | |
| 140 | + | readonly property string iClock: cp(0xF0954) | |
| 141 | + | readonly property string iArch: cp(0xF08C7) | |
| 142 | + | readonly property string iEthernet: cp(0xF0200) | |
| 143 | + | readonly property string iPlug: cp(0xF06A5) | |
| 144 | + | readonly property string iBatCharge: cp(0xF0084) | |
| 145 | + | ||
| 146 | + | // workspace icons: the exact Font Awesome glyphs from your waybar config. | |
| 147 | + | // 1 browser, 2 code, 3 book, 4 cog, 5 chat, 6 file, 7 folder, 8 terminal, 9 flask, 10 image | |
| 148 | + | // 11-20 repeat the same ten, like your waybar format-icons did | |
| 149 | + | readonly property var wsIcons: [ | |
| 150 | + | String.fromCharCode(0xf269), String.fromCharCode(0xf121), String.fromCharCode(0xf02d), | |
| 151 | + | String.fromCharCode(0xf085), String.fromCharCode(0xf086), String.fromCharCode(0xf1c9), | |
| 152 | + | String.fromCharCode(0xf07c), String.fromCharCode(0xf120), String.fromCharCode(0xf0c3), | |
| 153 | + | String.fromCharCode(0xf03e) | |
| 154 | + | ] | |
| 155 | + | function wsIcon(id) { return (id >= 1 && id <= 20) ? wsIcons[(id - 1) % 10] : String(id) } | |
| 156 | + | ||
| 157 | + | readonly property var batLevels: [0xF007A, 0xF007B, 0xF007C, 0xF007D, 0xF007E, | |
| 158 | + | 0xF007F, 0xF0080, 0xF0081, 0xF0082, 0xF0079] | |
| 159 | + | function batIcon(p, charging, plugged) { | |
| 160 | + | if (charging) return iBatCharge | |
| 161 | + | if (plugged) return iPlug | |
| 162 | + | return cp(batLevels[Math.max(0, Math.min(9, Math.floor(p * 10)))]) | |
| 163 | + | } | |
| 164 | + | function fmtTime(s) { | |
| 165 | + | if (!s || s <= 0) return "-" | |
| 166 | + | var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60) | |
| 167 | + | return h > 0 ? h + "h " + m + "m" : m + "m" | |
| 168 | + | } | |
| 169 | + | ||
| 124 | 170 | function volIcon(v, muted) { | |
| 125 | 171 | if (muted || v <= 0) return iVolOff | |
| 126 | 172 | return v < 0.34 ? iVolLow : (v < 0.67 ? iVolMed : iVolHigh) | |
| @@ -0,0 +1,59 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import Quickshell | |
| 3 | + | import Quickshell.Services.SystemTray | |
| 4 | + | ||
| 5 | + | Item { | |
| 6 | + | id: tray | |
| 7 | + | visible: SystemTray.items.values.length > 0 | |
| 8 | + | implicitWidth: row.implicitWidth + 24 | |
| 9 | + | implicitHeight: parent ? parent.height : Theme.barHeight | |
| 10 | + | ||
| 11 | + | Rectangle { | |
| 12 | + | anchors.left: parent.left | |
| 13 | + | anchors.right: parent.right | |
| 14 | + | anchors.bottom: parent.bottom | |
| 15 | + | height: 1 | |
| 16 | + | color: Theme.dim | |
| 17 | + | } | |
| 18 | + | Row { | |
| 19 | + | id: row | |
| 20 | + | anchors.centerIn: parent | |
| 21 | + | spacing: 10 | |
| 22 | + | Repeater { | |
| 23 | + | model: SystemTray.items | |
| 24 | + | delegate: Item { | |
| 25 | + | id: it | |
| 26 | + | required property var modelData | |
| 27 | + | width: 14 | |
| 28 | + | height: 14 | |
| 29 | + | anchors.verticalCenter: parent.verticalCenter | |
| 30 | + | ||
| 31 | + | function openMenu() { | |
| 32 | + | var p = it.mapToItem(null, 0, it.height + 4) | |
| 33 | + | modelData.display(QsWindow.window, p.x, p.y) | |
| 34 | + | } | |
| 35 | + | Image { | |
| 36 | + | anchors.fill: parent | |
| 37 | + | source: it.modelData.icon | |
| 38 | + | sourceSize: Qt.size(28, 28) | |
| 39 | + | smooth: true | |
| 40 | + | } | |
| 41 | + | MouseArea { | |
| 42 | + | anchors.fill: parent | |
| 43 | + | acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton | |
| 44 | + | cursorShape: Qt.PointingHandCursor | |
| 45 | + | onClicked: mouse => { | |
| 46 | + | if (mouse.button === Qt.LeftButton) { | |
| 47 | + | if (it.modelData.onlyMenu) it.openMenu() | |
| 48 | + | else it.modelData.activate() | |
| 49 | + | } else if (mouse.button === Qt.MiddleButton) { | |
| 50 | + | it.modelData.secondaryActivate() | |
| 51 | + | } else { | |
| 52 | + | it.openMenu() | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | } | |
| @@ -0,0 +1,67 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import Quickshell.Hyprland | |
| 3 | + | ||
| 4 | + | // Workspaces 1-10 always shown (like a persistent waybar setup), extras appear when used. | |
| 5 | + | Row { | |
| 6 | + | id: ws | |
| 7 | + | spacing: 0 | |
| 8 | + | ||
| 9 | + | function find(id) { | |
| 10 | + | var l = Hyprland.workspaces.values | |
| 11 | + | for (var i = 0; i < l.length; i++) if (l[i].id === id) return l[i] | |
| 12 | + | return null | |
| 13 | + | } | |
| 14 | + | function ids() { | |
| 15 | + | var out = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] | |
| 16 | + | var l = Hyprland.workspaces.values | |
| 17 | + | for (var i = 0; i < l.length; i++) if (l[i].id > 10) out.push(l[i].id) | |
| 18 | + | out.sort(function (a, b) { return a - b }) | |
| 19 | + | return out | |
| 20 | + | } | |
| 21 | + | readonly property var list: ids() | |
| 22 | + | ||
| 23 | + | Repeater { | |
| 24 | + | model: ws.list | |
| 25 | + | delegate: Item { | |
| 26 | + | id: b | |
| 27 | + | required property var modelData | |
| 28 | + | readonly property var obj: ws.find(modelData) | |
| 29 | + | readonly property bool active: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === modelData | |
| 30 | + | readonly property bool occupied: obj !== null && obj.toplevels.values.length > 0 | |
| 31 | + | readonly property bool urgent: obj !== null && obj.urgent | |
| 32 | + | readonly property bool lit: active || m.containsMouse | |
| 33 | + | ||
| 34 | + | width: 30 | |
| 35 | + | height: ws.height | |
| 36 | + | ||
| 37 | + | Rectangle { | |
| 38 | + | anchors.fill: parent | |
| 39 | + | color: b.lit ? Theme.accent : "transparent" | |
| 40 | + | } | |
| 41 | + | Rectangle { // occupied marker, like the inset underline in your css | |
| 42 | + | visible: b.occupied && !b.lit | |
| 43 | + | anchors.left: parent.left | |
| 44 | + | anchors.right: parent.right | |
| 45 | + | anchors.bottom: parent.bottom | |
| 46 | + | height: 1 | |
| 47 | + | color: Theme.cursorC | |
| 48 | + | } | |
| 49 | + | Text { | |
| 50 | + | anchors.centerIn: parent | |
| 51 | + | text: Theme.wsIcon(b.modelData) | |
| 52 | + | color: b.lit ? Theme.bg : (b.urgent ? Theme.err : Theme.fg) | |
| 53 | + | opacity: (b.occupied || b.lit || b.urgent) ? 1 : 0.4 | |
| 54 | + | font.family: Theme.font | |
| 55 | + | font.pixelSize: Theme.fontSize + 1 | |
| 56 | + | } | |
| 57 | + | MouseArea { | |
| 58 | + | id: m | |
| 59 | + | anchors.fill: parent | |
| 60 | + | hoverEnabled: true | |
| 61 | + | cursorShape: Qt.PointingHandCursor | |
| 62 | + | onClicked: Hypr.workspace(b.modelData) | |
| 63 | + | onWheel: w => Hypr.workspace(w.angleDelta.y > 0 ? "e-1" : "e+1") | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | } | |
| @@ -0,0 +1,62 @@ | |||
| 1 | + | import QtQuick | |
| 2 | + | import Quickshell | |
| 3 | + | import Quickshell.Io | |
| 4 | + | import Quickshell.Services.Pipewire | |
| 5 | + | ||
| 6 | + | // aurora: bar + control panel + notifications, themed from pywal16. | |
| 7 | + | ShellRoot { | |
| 8 | + | id: root | |
| 9 | + | ||
| 10 | + | property bool opened: false | |
| 11 | + | property int tab: 0 | |
| 12 | + | property double closedAt: 0 | |
| 13 | + | property string lastTab: "" | |
| 14 | + | readonly property var tabNames: ["home", "audio", "net", "bt", "sys", "notif"] | |
| 15 | + | ||
| 16 | + | function close() { | |
| 17 | + | if (!opened) return | |
| 18 | + | opened = false | |
| 19 | + | closedAt = Date.now() | |
| 20 | + | lastTab = tabNames[tab] | |
| 21 | + | } | |
| 22 | + | function openTab(name) { | |
| 23 | + | var i = tabNames.indexOf(name) | |
| 24 | + | if (i >= 0) tab = i | |
| 25 | + | opened = true | |
| 26 | + | } | |
| 27 | + | // bar buttons: click once to open that tab, again to close. | |
| 28 | + | // (the focus grab already closed the panel on the press, so guard against instant re-open) | |
| 29 | + | function togglePanel(name) { | |
| 30 | + | var i = tabNames.indexOf(name) | |
| 31 | + | if (opened && tab === i) { close(); return } | |
| 32 | + | if (!opened && lastTab === name && Date.now() - closedAt < 250) return | |
| 33 | + | tab = i | |
| 34 | + | opened = true | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // keep every pipewire node bound so volume / mute / properties are readable | |
| 38 | + | PwObjectTracker { objects: Pipewire.nodes.values } | |
| 39 | + | ||
| 40 | + | Variants { | |
| 41 | + | model: Quickshell.screens | |
| 42 | + | Bar { onTogglePanel: name => root.togglePanel(name) } | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | ControlPanel { | |
| 46 | + | opened: root.opened | |
| 47 | + | tab: root.tab | |
| 48 | + | onTabRequested: i => root.tab = i | |
| 49 | + | onCloseRequested: root.close() | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | NotifPopups { suppressed: root.opened } | |
| 53 | + | ||
| 54 | + | IpcHandler { | |
| 55 | + | target: "shell" | |
| 56 | + | function toggle(): void { if (root.opened) root.close(); else root.opened = true } | |
| 57 | + | function open(): void { root.opened = true } | |
| 58 | + | function close(): void { root.close() } | |
| 59 | + | function show(name: string): void { root.openTab(name) } | |
| 60 | + | function dnd(): void { Notifs.dnd = !Notifs.dnd } | |
| 61 | + | } | |
| 62 | + | } | |
| @@ -1,118 +0,0 @@ | |||
| 1 | − | import QtQuick | |
| 2 | − | import QtQuick.Layouts | |
| 3 | − | import Quickshell | |
| 4 | − | import Quickshell.Io | |
| 5 | − | import Quickshell.Wayland | |
| 6 | − | import Quickshell.Hyprland | |
| 7 | − | import Quickshell.Services.Pipewire | |
| 8 | − | ||
| 9 | − | ShellRoot { | |
| 10 | − | id: root | |
| 11 | − | property bool opened: false | |
| 12 | − | property int tab: 0 | |
| 13 | − | readonly property var tabNames: ["home", "audio", "net", "bt"] | |
| 14 | − | ||
| 15 | − | function openTab(name) { | |
| 16 | − | var i = tabNames.indexOf(name) | |
| 17 | − | if (i >= 0) tab = i | |
| 18 | − | opened = true | |
| 19 | − | } | |
| 20 | − | ||
| 21 | − | // keep every pipewire node bound so volume / mute / properties are readable | |
| 22 | − | PwObjectTracker { objects: Pipewire.nodes.values } | |
| 23 | − | ||
| 24 | − | PanelWindow { | |
| 25 | − | id: panel | |
| 26 | − | visible: root.opened | |
| 27 | − | anchors { top: true; right: true } | |
| 28 | − | margins { top: 2 ; right: 2 } | |
| 29 | − | implicitWidth: 430 | |
| 30 | − | implicitHeight: 590 | |
| 31 | − | exclusiveZone: 0 | |
| 32 | − | color: "transparent" | |
| 33 | − | WlrLayershell.layer: WlrLayer.Overlay | |
| 34 | − | WlrLayershell.namespace: "wal-control" | |
| 35 | − | WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand | |
| 36 | − | ||
| 37 | − | Rectangle { | |
| 38 | − | anchors.fill: parent | |
| 39 | − | color: Theme.bgA | |
| 40 | − | border.width: 2 | |
| 41 | − | border.color: Theme.accent | |
| 42 | − | focus: true | |
| 43 | − | Keys.onEscapePressed: root.opened = false | |
| 44 | − | ||
| 45 | − | ColumnLayout { | |
| 46 | − | anchors.fill: parent | |
| 47 | − | anchors.margins: 12 | |
| 48 | − | spacing: 10 | |
| 49 | − | ||
| 50 | − | RowLayout { | |
| 51 | − | Layout.fillWidth: true | |
| 52 | − | spacing: 2 | |
| 53 | − | Repeater { | |
| 54 | − | model: [ | |
| 55 | − | { icon: Theme.iHome, name: "home" }, | |
| 56 | − | { icon: Theme.iVolHigh, name: "audio" }, | |
| 57 | − | { icon: Theme.iWifi, name: "net" }, | |
| 58 | − | { icon: Theme.iBt, name: "bt" } | |
| 59 | − | ] | |
| 60 | − | delegate: Rectangle { | |
| 61 | − | id: tabBtn | |
| 62 | − | required property var modelData | |
| 63 | − | required property int index | |
| 64 | − | implicitWidth: lbl.implicitWidth + 22 | |
| 65 | − | implicitHeight: 28 | |
| 66 | − | color: root.tab === index ? Theme.accent : (tm.containsMouse ? Theme.surfaceHi : "transparent") | |
| 67 | − | Text { | |
| 68 | − | id: lbl | |
| 69 | − | anchors.centerIn: parent | |
| 70 | − | text: tabBtn.modelData.icon + " " + tabBtn.modelData.name | |
| 71 | − | color: root.tab === tabBtn.index ? Theme.bg : Theme.fg | |
| 72 | − | font.family: Theme.font | |
| 73 | − | font.pixelSize: Theme.fontSize | |
| 74 | − | } | |
| 75 | − | MouseArea { | |
| 76 | − | id: tm | |
| 77 | − | anchors.fill: parent | |
| 78 | − | hoverEnabled: true | |
| 79 | − | cursorShape: Qt.PointingHandCursor | |
| 80 | − | onClicked: root.tab = tabBtn.index | |
| 81 | − | } | |
| 82 | − | } | |
| 83 | − | } | |
| 84 | − | Item { Layout.fillWidth: true } | |
| 85 | − | IconBtn { icon: Theme.iClose; height: 28; onClicked: root.opened = false } | |
| 86 | − | } | |
| 87 | − | ||
| 88 | − | Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 1; color: Theme.line } | |
| 89 | − | ||
| 90 | − | StackLayout { | |
| 91 | − | Layout.fillWidth: true | |
| 92 | − | Layout.fillHeight: true | |
| 93 | − | currentIndex: root.tab | |
| 94 | − | ||
| 95 | − | HomePage { onCloseRequested: root.opened = false } | |
| 96 | − | AudioPage {} | |
| 97 | − | NetPage { active: root.opened && root.tab === 2 } | |
| 98 | − | BtPage { active: root.opened && root.tab === 3 } | |
| 99 | − | } | |
| 100 | − | } | |
| 101 | − | } | |
| 102 | − | } | |
| 103 | − | ||
| 104 | − | // clicking anywhere outside the panel closes it | |
| 105 | − | HyprlandFocusGrab { | |
| 106 | − | windows: [panel] | |
| 107 | − | active: root.opened | |
| 108 | − | onCleared: root.opened = false | |
| 109 | − | } | |
| 110 | − | ||
| 111 | − | IpcHandler { | |
| 112 | − | target: "control" | |
| 113 | − | function toggle(): void { root.opened = !root.opened } | |
| 114 | − | function open(): void { root.opened = true } | |
| 115 | − | function close(): void { root.opened = false } | |
| 116 | − | function show(name: string): void { root.openTab(name) } | |
| 117 | − | } | |
| 118 | − | } | |
| @@ -1,10 +1,10 @@ | |||
| 1 | 1 | [Unit] | |
| 2 | − | Description=pywal-themed Quickshell control center | |
| 2 | + | Description=aurora: Quickshell bar, control panel and notifications | |
| 3 | 3 | After=graphical-session.target | |
| 4 | 4 | PartOf=graphical-session.target | |
| 5 | 5 | ||
| 6 | 6 | [Service] | |
| 7 | − | ExecStart=/usr/bin/quickshell -p %h/.config/quickshell/wal-control | |
| 7 | + | ExecStart=/usr/bin/quickshell -p %h/.config/quickshell/aurora | |
| 8 | 8 | Restart=on-failure | |
| 9 | 9 | RestartSec=2 | |
| 10 | 10 | ||
| @@ -1,12 +0,0 @@ | |||
| 1 | − | [Unit] | |
| 2 | − | Description=pywal-themed Quickshell control center | |
| 3 | − | After=graphical-session.target | |
| 4 | − | PartOf=graphical-session.target | |
| 5 | − | ||
| 6 | − | [Service] | |
| 7 | − | ExecStart=/usr/bin/quickshell -p %h/.config/quickshell/wal-control | |
| 8 | − | Restart=on-failure | |
| 9 | − | RestartSec=2 | |
| 10 | − | ||
| 11 | − | [Install] | |
| 12 | − | WantedBy=graphical-session.target | |