| 1 | import QtQuick |
| 2 | import Quickshell |
| 3 | import Quickshell.Wayland |
| 4 | import Quickshell.Bluetooth |
| 5 | import Quickshell.Hyprland |
| 6 | import Quickshell.Services.Pipewire |
| 7 | import Quickshell.Services.UPower |
| 8 | |
| 9 | PanelWindow { |
| 10 | id: bar |
| 11 | required property var modelData |
| 12 | screen: modelData |
| 13 | signal togglePanel(string name, var scr) |
| 14 | signal toggleOverlay(string name, var scr) |
| 15 | |
| 16 | anchors { top: true; left: true; right: true } |
| 17 | implicitHeight: Theme.barHeight |
| 18 | color: Theme.barBg |
| 19 | WlrLayershell.namespace: "aurora-bar" |
| 20 | |
| 21 | SystemClock { id: clock; precision: SystemClock.Minutes } |
| 22 | |
| 23 | // caffeine: one inhibitor is enough, keep it on the first screen's bar |
| 24 | IdleInhibitor { |
| 25 | window: bar |
| 26 | enabled: Toggles.caffeine && bar.modelData === Quickshell.screens[0] |
| 27 | } |
| 28 | |
| 29 | // ---- state ---- |
| 30 | readonly property var audio: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null |
| 31 | readonly property var mic: Pipewire.defaultAudioSource ? Pipewire.defaultAudioSource.audio : null |
| 32 | readonly property var bat: UPower.displayDevice |
| 33 | readonly property bool hasBat: bat !== null && bat.isPresent |
| 34 | readonly property real batPct: hasBat ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0 |
| 35 | readonly property bool charging: hasBat && bat.state === UPowerDeviceState.Charging |
| 36 | readonly property bool plugged: hasBat && bat.state === UPowerDeviceState.FullyCharged |
| 37 | readonly property bool batLow: hasBat && batPct <= 0.15 && !charging |
| 38 | readonly property var adapter: Bluetooth.defaultAdapter |
| 39 | |
| 40 | // which Hyprland monitor this bar belongs to, and whether it has focus |
| 41 | readonly property var hmon: Hyprland.monitorFor(modelData) |
| 42 | readonly property bool monFocused: hmon !== null && hmon.focused |
| 43 | |
| 44 | function findHeadset() { |
| 45 | var l = Bluetooth.devices.values |
| 46 | for (var i = 0; i < l.length; i++) { |
| 47 | var d = l[i] |
| 48 | if (!d.connected || !d.batteryAvailable) continue |
| 49 | var ic = String(d.icon) |
| 50 | if (ic.indexOf("head") >= 0 || ic.indexOf("audio") >= 0) return d |
| 51 | } |
| 52 | return null |
| 53 | } |
| 54 | function btConnected() { |
| 55 | var l = Bluetooth.devices.values |
| 56 | for (var i = 0; i < l.length; i++) if (l[i].connected) return true |
| 57 | return false |
| 58 | } |
| 59 | readonly property var headset: findHeadset() |
| 60 | readonly property bool btOn: adapter !== null && adapter.enabled |
| 61 | readonly property bool btLink: btOn && btConnected() |
| 62 | |
| 63 | Item { |
| 64 | anchors.fill: parent |
| 65 | |
| 66 | // ---------------- left ---------------- |
| 67 | Row { |
| 68 | anchors.left: parent.left |
| 69 | height: parent.height |
| 70 | spacing: 10 |
| 71 | Workspaces { height: parent.height; monitor: bar.hmon } |
| 72 | CatLoop { height: parent.height } |
| 73 | } |
| 74 | |
| 75 | // ---------------- center ---------------- |
| 76 | Text { |
| 77 | anchors.centerIn: parent |
| 78 | width: Math.min(implicitWidth, parent.width * 0.32) |
| 79 | elide: Text.ElideRight |
| 80 | horizontalAlignment: Text.AlignHCenter |
| 81 | readonly property string title: ToplevelManager.activeToplevel ? ToplevelManager.activeToplevel.title : "" |
| 82 | visible: bar.monFocused && title !== "" // active window is global: show it on the focused monitor only |
| 83 | text: Theme.iArch + " " + title |
| 84 | color: Theme.fg |
| 85 | font.family: Theme.font |
| 86 | font.pixelSize: Theme.fontSize |
| 87 | font.bold: true |
| 88 | } |
| 89 | |
| 90 | // ---------------- right ---------------- |
| 91 | Row { |
| 92 | anchors.right: parent.right |
| 93 | height: parent.height |
| 94 | spacing: 10 |
| 95 | |
| 96 | Tray { show: bar.monFocused } |
| 97 | |
| 98 | BarPill { // click to stop recording / cancel the area selection |
| 99 | visible: Rec.busy |
| 100 | icon: Theme.iStop |
| 101 | text: Rec.running ? "rec " + Theme.fmtClock(Rec.elapsed) : "select area" |
| 102 | tint: Theme.err |
| 103 | underline: Theme.err |
| 104 | onActivated: Rec.stop() |
| 105 | } |
| 106 | |
| 107 | BarPill { |
| 108 | visible: Toggles.caffeine || Toggles.airplane |
| 109 | icon: (Toggles.caffeine ? Theme.iCoffee : "") + (Toggles.caffeine && Toggles.airplane ? " " : "") |
| 110 | + (Toggles.airplane ? Theme.iPlane : "") |
| 111 | onActivated: bar.togglePanel("home", bar.modelData) |
| 112 | } |
| 113 | |
| 114 | BarPill { |
| 115 | visible: bar.headset !== null |
| 116 | icon: Theme.iHeadphones |
| 117 | text: bar.headset ? Math.round(bar.headset.battery * 100) + "%" : "" |
| 118 | onActivated: bar.togglePanel("bt", bar.modelData) |
| 119 | } |
| 120 | |
| 121 | BarPill { |
| 122 | visible: bar.audio !== null |
| 123 | icon: Theme.volIcon(bar.audio ? bar.audio.volume : 0, bar.audio ? bar.audio.muted : true) |
| 124 | text: (bar.audio ? Math.round(bar.audio.volume * 100) : 0) + "%" |
| 125 | + (bar.mic && bar.mic.muted ? " " + Theme.iMicOff : "") |
| 126 | onActivated: button => { |
| 127 | if (button === Qt.RightButton && bar.audio) bar.audio.muted = !bar.audio.muted |
| 128 | else if (button === Qt.MiddleButton && bar.mic) bar.mic.muted = !bar.mic.muted |
| 129 | else bar.togglePanel("audio", bar.modelData) |
| 130 | } |
| 131 | onScrolled: dir => { |
| 132 | if (bar.audio) bar.audio.volume = Math.max(0, Math.min(1, bar.audio.volume + dir * 0.02)) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | BarPill { |
| 137 | icon: Net.wired && !(Net.wifiEnabled && Net.connected !== "") ? Net.wiredIcon |
| 138 | : (Net.wifiEnabled ? Theme.wifiIcon(Net.connected !== "" ? Net.activeSignal : 0) : Theme.iWifiOff) |
| 139 | tint: (Net.wired || (Net.wifiEnabled && Net.connected !== "")) ? Theme.fg : Theme.dim |
| 140 | onActivated: bar.togglePanel("net", bar.modelData) |
| 141 | } |
| 142 | |
| 143 | BarPill { |
| 144 | visible: bar.adapter !== null |
| 145 | icon: !bar.btOn ? Theme.iBtOff : (bar.btLink ? Theme.iBtConn : Theme.iBt) |
| 146 | tint: bar.btLink ? Theme.fg : Theme.dim |
| 147 | onActivated: bar.togglePanel("bt", bar.modelData) |
| 148 | } |
| 149 | |
| 150 | BarPill { |
| 151 | visible: bar.hasBat |
| 152 | icon: Theme.batIcon(bar.batPct, bar.charging, bar.plugged) |
| 153 | text: Math.round(bar.batPct * 100) + "%" |
| 154 | tint: bar.batLow ? Theme.err : Theme.fg |
| 155 | underline: bar.batLow ? Theme.err : Theme.accent |
| 156 | onActivated: bar.togglePanel("sys", bar.modelData) |
| 157 | } |
| 158 | |
| 159 | BarPill { |
| 160 | icon: Notifs.dnd ? Theme.iBellOff : Theme.iBell |
| 161 | text: Notifs.count > 0 ? String(Notifs.count) : "" |
| 162 | tint: Notifs.dnd ? Theme.dim : Theme.fg |
| 163 | onActivated: button => { |
| 164 | if (button === Qt.RightButton) Notifs.dnd = !Notifs.dnd |
| 165 | else bar.togglePanel("notif", bar.modelData) |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | BarPill { |
| 170 | icon: Theme.iCal |
| 171 | text: Qt.formatDateTime(clock.date, "dd MMM yyyy") |
| 172 | onActivated: bar.toggleOverlay("cal", bar.modelData) |
| 173 | } |
| 174 | BarPill { |
| 175 | icon: Theme.iClock |
| 176 | text: Qt.formatDateTime(clock.date, "HH:mm") |
| 177 | onActivated: bar.toggleOverlay("cal", bar.modelData) |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | } |