aurora5515e5c
aurora: add multi-monitor setup for workspaces
+246 −97
11 files changed
- M .config/quickshell/aurora/Bar.qml +17 −12
- M .config/quickshell/aurora/ControlPanel.qml +1 −1
- M .config/quickshell/aurora/HomePage.qml +39 −1
- A .config/quickshell/aurora/Shot.qml +33 −0
- M .config/quickshell/aurora/StatBar.qml +2 −1
- M .config/quickshell/aurora/SysPage.qml +49 −40
- M .config/quickshell/aurora/SysStats.qml +36 −13
- M .config/quickshell/aurora/Theme.qml +9 −2
- M .config/quickshell/aurora/Tray.qml +2 −1
- M .config/quickshell/aurora/Workspaces.qml +32 −19
- M .config/quickshell/aurora/shell.qml +26 −7
| @@ -2,6 +2,7 @@ import QtQuick | |||
| 2 | 2 | import Quickshell | |
| 3 | 3 | import Quickshell.Wayland | |
| 4 | 4 | import Quickshell.Bluetooth | |
| 5 | + | import Quickshell.Hyprland | |
| 5 | 6 | import Quickshell.Services.Pipewire | |
| 6 | 7 | import Quickshell.Services.UPower | |
| 7 | 8 | ||
| @@ -9,7 +10,7 @@ PanelWindow { | |||
| 9 | 10 | id: bar | |
| 10 | 11 | required property var modelData | |
| 11 | 12 | screen: modelData | |
| 12 | − | signal togglePanel(string name) | |
| 13 | + | signal togglePanel(string name, var scr) | |
| 13 | 14 | ||
| 14 | 15 | anchors { top: true; left: true; right: true } | |
| 15 | 16 | implicitHeight: Theme.barHeight | |
| @@ -29,6 +30,10 @@ PanelWindow { | |||
| 29 | 30 | readonly property bool batLow: hasBat && batPct <= 0.15 && !charging | |
| 30 | 31 | readonly property var adapter: Bluetooth.defaultAdapter | |
| 31 | 32 | ||
| 33 | + | // which Hyprland monitor this bar belongs to, and whether it has focus | |
| 34 | + | readonly property var hmon: Hyprland.monitorFor(modelData) | |
| 35 | + | readonly property bool monFocused: hmon !== null && hmon.focused | |
| 36 | + | ||
| 32 | 37 | function findHeadset() { | |
| 33 | 38 | var l = Bluetooth.devices.values | |
| 34 | 39 | for (var i = 0; i < l.length; i++) { | |
| @@ -56,7 +61,7 @@ PanelWindow { | |||
| 56 | 61 | anchors.left: parent.left | |
| 57 | 62 | height: parent.height | |
| 58 | 63 | spacing: 10 | |
| 59 | − | Workspaces { height: parent.height } | |
| 64 | + | Workspaces { height: parent.height; monitor: bar.hmon } | |
| 60 | 65 | CatLoop { height: parent.height } | |
| 61 | 66 | } | |
| 62 | 67 | ||
| @@ -67,7 +72,7 @@ PanelWindow { | |||
| 67 | 72 | elide: Text.ElideRight | |
| 68 | 73 | horizontalAlignment: Text.AlignHCenter | |
| 69 | 74 | readonly property string title: ToplevelManager.activeToplevel ? ToplevelManager.activeToplevel.title : "" | |
| 70 | − | visible: title !== "" | |
| 75 | + | visible: bar.monFocused && title !== "" // active window is global: show it on the focused monitor only | |
| 71 | 76 | text: Theme.iArch + " " + title | |
| 72 | 77 | color: Theme.fg | |
| 73 | 78 | font.family: Theme.font | |
| @@ -81,13 +86,13 @@ PanelWindow { | |||
| 81 | 86 | height: parent.height | |
| 82 | 87 | spacing: 10 | |
| 83 | 88 | ||
| 84 | − | Tray {} | |
| 89 | + | Tray { show: bar.monFocused } | |
| 85 | 90 | ||
| 86 | 91 | BarPill { | |
| 87 | 92 | visible: bar.headset !== null | |
| 88 | 93 | icon: Theme.iHeadphones | |
| 89 | 94 | text: bar.headset ? Math.round(bar.headset.battery * 100) + "%" : "" | |
| 90 | − | onActivated: bar.togglePanel("bt") | |
| 95 | + | onActivated: bar.togglePanel("bt", bar.modelData) | |
| 91 | 96 | } | |
| 92 | 97 | ||
| 93 | 98 | BarPill { | |
| @@ -98,7 +103,7 @@ PanelWindow { | |||
| 98 | 103 | onActivated: button => { | |
| 99 | 104 | if (button === Qt.RightButton && bar.audio) bar.audio.muted = !bar.audio.muted | |
| 100 | 105 | else if (button === Qt.MiddleButton && bar.mic) bar.mic.muted = !bar.mic.muted | |
| 101 | − | else bar.togglePanel("audio") | |
| 106 | + | else bar.togglePanel("audio", bar.modelData) | |
| 102 | 107 | } | |
| 103 | 108 | onScrolled: dir => { | |
| 104 | 109 | if (bar.audio) bar.audio.volume = Math.max(0, Math.min(1, bar.audio.volume + dir * 0.02)) | |
| @@ -109,14 +114,14 @@ PanelWindow { | |||
| 109 | 114 | icon: Net.wired && !(Net.wifiEnabled && Net.connected !== "") ? Theme.iEthernet | |
| 110 | 115 | : (Net.wifiEnabled ? Theme.wifiIcon(Net.connected !== "" ? Net.activeSignal : 0) : Theme.iWifiOff) | |
| 111 | 116 | tint: (Net.wired || (Net.wifiEnabled && Net.connected !== "")) ? Theme.fg : Theme.dim | |
| 112 | − | onActivated: bar.togglePanel("net") | |
| 117 | + | onActivated: bar.togglePanel("net", bar.modelData) | |
| 113 | 118 | } | |
| 114 | 119 | ||
| 115 | 120 | BarPill { | |
| 116 | 121 | visible: bar.adapter !== null | |
| 117 | 122 | icon: !bar.btOn ? Theme.iBtOff : (bar.btLink ? Theme.iBtConn : Theme.iBt) | |
| 118 | 123 | tint: bar.btLink ? Theme.fg : Theme.dim | |
| 119 | − | onActivated: bar.togglePanel("bt") | |
| 124 | + | onActivated: bar.togglePanel("bt", bar.modelData) | |
| 120 | 125 | } | |
| 121 | 126 | ||
| 122 | 127 | BarPill { | |
| @@ -125,7 +130,7 @@ PanelWindow { | |||
| 125 | 130 | text: Math.round(bar.batPct * 100) + "%" | |
| 126 | 131 | tint: bar.batLow ? Theme.err : Theme.fg | |
| 127 | 132 | underline: bar.batLow ? Theme.err : Theme.accent | |
| 128 | − | onActivated: bar.togglePanel("sys") | |
| 133 | + | onActivated: bar.togglePanel("sys", bar.modelData) | |
| 129 | 134 | } | |
| 130 | 135 | ||
| 131 | 136 | BarPill { | |
| @@ -134,19 +139,19 @@ PanelWindow { | |||
| 134 | 139 | tint: Notifs.dnd ? Theme.dim : Theme.fg | |
| 135 | 140 | onActivated: button => { | |
| 136 | 141 | if (button === Qt.RightButton) Notifs.dnd = !Notifs.dnd | |
| 137 | − | else bar.togglePanel("notif") | |
| 142 | + | else bar.togglePanel("notif", bar.modelData) | |
| 138 | 143 | } | |
| 139 | 144 | } | |
| 140 | 145 | ||
| 141 | 146 | BarPill { | |
| 142 | 147 | icon: Theme.iCal | |
| 143 | 148 | text: Qt.formatDateTime(clock.date, "dd MMM yyyy") | |
| 144 | − | onActivated: bar.togglePanel("home") | |
| 149 | + | onActivated: bar.togglePanel("home", bar.modelData) | |
| 145 | 150 | } | |
| 146 | 151 | BarPill { | |
| 147 | 152 | icon: Theme.iClock | |
| 148 | 153 | text: Qt.formatDateTime(clock.date, "HH:mm") | |
| 149 | − | onActivated: bar.togglePanel("home") | |
| 154 | + | onActivated: bar.togglePanel("home", bar.modelData) | |
| 150 | 155 | } | |
| 151 | 156 | } | |
| 152 | 157 | } | |
| @@ -15,7 +15,7 @@ PanelWindow { | |||
| 15 | 15 | anchors { top: true; right: true } | |
| 16 | 16 | margins { top: 6; right: 8 } // sits just under the bar (bar's exclusive zone is respected) | |
| 17 | 17 | implicitWidth: 440 | |
| 18 | − | implicitHeight: 610 | |
| 18 | + | implicitHeight: 640 | |
| 19 | 19 | exclusiveZone: 0 | |
| 20 | 20 | color: "transparent" | |
| 21 | 21 | WlrLayershell.layer: WlrLayer.Overlay | |
| @@ -6,7 +6,7 @@ import Quickshell.Services.Pipewire | |||
| 6 | 6 | ||
| 7 | 7 | ColumnLayout { | |
| 8 | 8 | id: page | |
| 9 | − | spacing: 12 | |
| 9 | + | spacing: 10 | |
| 10 | 10 | signal closeRequested() | |
| 11 | 11 | ||
| 12 | 12 | property string armed: "" | |
| @@ -95,6 +95,44 @@ ColumnLayout { | |||
| 95 | 95 | } | |
| 96 | 96 | } | |
| 97 | 97 | ||
| 98 | + | SectionLabel { text: "capture" } | |
| 99 | + | ||
| 100 | + | RowLayout { | |
| 101 | + | Layout.fillWidth: true | |
| 102 | + | spacing: 6 | |
| 103 | + | IconBtn { // where the shot goes: clipboard or ~/pix/ss | |
| 104 | + | Layout.fillWidth: true | |
| 105 | + | icon: Shot.save ? Theme.iSave : Theme.iClipboard | |
| 106 | + | label: Shot.save ? "save" : "copy" | |
| 107 | + | active: true | |
| 108 | + | onClicked: Shot.save = !Shot.save | |
| 109 | + | } | |
| 110 | + | IconBtn { | |
| 111 | + | Layout.fillWidth: true | |
| 112 | + | icon: Theme.iCrop | |
| 113 | + | label: "area" | |
| 114 | + | onClicked: { page.closeRequested(); Shot.take("region") } | |
| 115 | + | } | |
| 116 | + | IconBtn { | |
| 117 | + | Layout.fillWidth: true | |
| 118 | + | icon: Theme.iWindow | |
| 119 | + | label: "window" | |
| 120 | + | onClicked: { page.closeRequested(); Shot.take("window") } | |
| 121 | + | } | |
| 122 | + | IconBtn { | |
| 123 | + | Layout.fillWidth: true | |
| 124 | + | icon: Theme.iMonitor | |
| 125 | + | label: "screen" | |
| 126 | + | onClicked: { page.closeRequested(); Shot.take("output") } | |
| 127 | + | } | |
| 128 | + | IconBtn { | |
| 129 | + | Layout.fillWidth: true | |
| 130 | + | icon: Theme.iSearch | |
| 131 | + | label: "ocr" | |
| 132 | + | onClicked: { page.closeRequested(); Shot.take("ocr") } | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 98 | 136 | Item { Layout.fillHeight: true } | |
| 99 | 137 | ||
| 100 | 138 | SectionLabel { text: "session" } | |
| @@ -0,0 +1,33 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | ||
| 5 | + | // Screenshots, ported from your fuzzel script. Needs: hyprshot (+ grim, slurp), and for | |
| 6 | + | // OCR: tesseract + wl-clipboard. Used by the Home tab and by `ipc call shell shot <mode>`. | |
| 7 | + | Singleton { | |
| 8 | + | id: root | |
| 9 | + | ||
| 10 | + | property bool save: false // false = clipboard, true = file | |
| 11 | + | property string dir: Quickshell.env("HOME") + "/pix/ss" // where "save" puts files | |
| 12 | + | property string ocrLang: "eng" | |
| 13 | + | ||
| 14 | + | readonly property var names: ({ region: "selected", window: "window", output: "full" }) | |
| 15 | + | ||
| 16 | + | // mode: "region" | "window" | "output" | "ocr" | |
| 17 | + | function take(mode) { | |
| 18 | + | var script | |
| 19 | + | if (mode === "ocr") { | |
| 20 | + | script = 'slurp | grim -g - - | tesseract stdin stdout -l "$2" | wl-copy' | |
| 21 | + | + ' && notify-send -a screenshot "OCR" "text copied to clipboard"' | |
| 22 | + | } else if (names[mode] === undefined) { | |
| 23 | + | return | |
| 24 | + | } else if (!save) { | |
| 25 | + | script = 'hyprshot -m ' + mode + ' --clipboard-only' | |
| 26 | + | } else { | |
| 27 | + | script = 'mkdir -p "$1"; hyprshot -m ' + mode + ' -o "$1"' | |
| 28 | + | + ' -f "pic-' + names[mode] + '-$(date +%y-%m-%d_%H%M%S).png"' | |
| 29 | + | } | |
| 30 | + | // short delay so the panel is gone before the capture starts | |
| 31 | + | Quickshell.execDetached(["sh", "-c", "sleep 0.35; " + script, "sh", dir, ocrLang]) | |
| 32 | + | } | |
| 33 | + | } | |
| @@ -12,7 +12,8 @@ RowLayout { | |||
| 12 | 12 | ||
| 13 | 13 | Text { | |
| 14 | 14 | text: r.label | |
| 15 | − | Layout.preferredWidth: 64 | |
| 15 | + | Layout.preferredWidth: 72 | |
| 16 | + | elide: Text.ElideRight | |
| 16 | 17 | color: Theme.dim | |
| 17 | 18 | font.family: Theme.font | |
| 18 | 19 | font.pixelSize: Theme.fontSize - 1 | |
| @@ -32,7 +32,11 @@ Flickable { | |||
| 32 | 32 | default: return "idle" | |
| 33 | 33 | } | |
| 34 | 34 | } | |
| 35 | − | function g(v) { return v.toFixed(1) + "G" } | |
| 35 | + | // GiB -> "812G" / "1.8T" | |
| 36 | + | function size(v) { return v >= 1000 ? (v / 1024).toFixed(1) + "T" : (v >= 100 ? Math.round(v) : v.toFixed(1)) + "G" } | |
| 37 | + | ||
| 38 | + | // pick up drives plugged in since the last poll | |
| 39 | + | onVisibleChanged: if (visible) SysStats.refreshDisks() | |
| 36 | 40 | ||
| 37 | 41 | ColumnLayout { | |
| 38 | 42 | id: col | |
| @@ -54,16 +58,21 @@ Flickable { | |||
| 54 | 58 | Spark { Layout.fillWidth: true; values: SysStats.history } | |
| 55 | 59 | } | |
| 56 | 60 | ||
| 57 | − | SectionLabel { text: "memory & disk"; Layout.topMargin: 6 } | |
| 61 | + | SectionLabel { text: "memory & storage"; Layout.topMargin: 6 } | |
| 58 | 62 | StatBar { | |
| 59 | 63 | label: "memory" | |
| 60 | 64 | frac: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0 | |
| 61 | − | valueText: flick.g(SysStats.memUsed) + " / " + flick.g(SysStats.memTotal) | |
| 65 | + | valueText: flick.size(SysStats.memUsed) + " / " + flick.size(SysStats.memTotal) | |
| 62 | 66 | } | |
| 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 | + | Repeater { | |
| 68 | + | model: SysStats.disks | |
| 69 | + | delegate: StatBar { | |
| 70 | + | required property var modelData | |
| 71 | + | label: modelData.label | |
| 72 | + | frac: modelData.used / modelData.size | |
| 73 | + | fill: frac > 0.9 ? Theme.err : Theme.accent | |
| 74 | + | valueText: flick.size(modelData.used) + " / " + flick.size(modelData.size) | |
| 75 | + | } | |
| 67 | 76 | } | |
| 68 | 77 | ||
| 69 | 78 | ColumnLayout { | |
| @@ -99,39 +108,39 @@ Flickable { | |||
| 99 | 108 | } | |
| 100 | 109 | } | |
| 101 | 110 | ||
| 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 | − | // } | |
| 111 | + | ColumnLayout { | |
| 112 | + | visible: PowerProfiles.hasPerformanceProfile !== undefined | |
| 113 | + | Layout.fillWidth: true | |
| 114 | + | Layout.topMargin: 6 | |
| 115 | + | spacing: 8 | |
| 116 | + | SectionLabel { text: "power profile" } | |
| 117 | + | RowLayout { | |
| 118 | + | Layout.fillWidth: true | |
| 119 | + | spacing: 6 | |
| 120 | + | IconBtn { | |
| 121 | + | Layout.fillWidth: true | |
| 122 | + | icon: Theme.iLeaf | |
| 123 | + | label: "saver" | |
| 124 | + | active: PowerProfiles.profile === PowerProfile.PowerSaver | |
| 125 | + | onClicked: PowerProfiles.profile = PowerProfile.PowerSaver | |
| 126 | + | } | |
| 127 | + | IconBtn { | |
| 128 | + | Layout.fillWidth: true | |
| 129 | + | icon: Theme.iBalance | |
| 130 | + | label: "balanced" | |
| 131 | + | active: PowerProfiles.profile === PowerProfile.Balanced | |
| 132 | + | onClicked: PowerProfiles.profile = PowerProfile.Balanced | |
| 133 | + | } | |
| 134 | + | IconBtn { | |
| 135 | + | Layout.fillWidth: true | |
| 136 | + | visible: PowerProfiles.hasPerformanceProfile | |
| 137 | + | icon: Theme.iSpeed | |
| 138 | + | label: "perf" | |
| 139 | + | active: PowerProfiles.profile === PowerProfile.Performance | |
| 140 | + | onClicked: PowerProfiles.profile = PowerProfile.Performance | |
| 141 | + | } | |
| 142 | + | } | |
| 143 | + | } | |
| 135 | 144 | ||
| 136 | 145 | ColumnLayout { | |
| 137 | 146 | visible: Gpu.mode !== "" | |
| @@ -9,8 +9,7 @@ Singleton { | |||
| 9 | 9 | property real cpu: 0 // 0..1 | |
| 10 | 10 | property real memUsed: 0 // GiB | |
| 11 | 11 | property real memTotal: 0 | |
| 12 | − | property real diskUsed: 0 | |
| 13 | − | property real diskTotal: 0 | |
| 12 | + | property var disks: [] // [{source, target, label, used, size}] in GiB, "/" first | |
| 14 | 13 | property real uptime: 0 // seconds | |
| 15 | 14 | property var history: [] // last 40 cpu samples | |
| 16 | 15 | property real prevIdle: -1 | |
| @@ -52,18 +51,42 @@ Singleton { | |||
| 52 | 51 | ||
| 53 | 52 | Timer { interval: 1000; running: true; repeat: true; triggeredOnStart: true; onTriggered: root.sample() } | |
| 54 | 53 | ||
| 54 | + | // every mounted *local* block device (internal nvme, external drives, usb sticks...). | |
| 55 | + | // pseudo filesystems and network mounts are skipped; one entry per physical source | |
| 56 | + | // (so btrfs subvolumes / bind mounts of the same partition don't repeat). | |
| 57 | + | function refreshDisks() { dfProc.running = true } | |
| 58 | + | ||
| 59 | + | function parseDf(text) { | |
| 60 | + | var lines = text.split("\n"), seen = {}, out = [] | |
| 61 | + | for (var i = 1; i < lines.length; i++) { | |
| 62 | + | var m = lines[i].match(/^(\S+)\s+(\d+)\s+(\d+)\s+(.*)$/) | |
| 63 | + | if (!m) continue | |
| 64 | + | var src = m[1], target = m[4].trim() | |
| 65 | + | if (src.indexOf("/dev/") !== 0 || src.indexOf("/dev/loop") === 0) continue | |
| 66 | + | if (target.indexOf("/boot") === 0 || target.indexOf("/efi") === 0 || target.indexOf("/var/lib") === 0) continue | |
| 67 | + | if (seen[src]) continue | |
| 68 | + | seen[src] = true | |
| 69 | + | var size = parseFloat(m[3]) | |
| 70 | + | if (size <= 0) continue | |
| 71 | + | var parts = target.split("/").filter(function (x) { return x !== "" }) | |
| 72 | + | out.push({ | |
| 73 | + | source: src, target: target, | |
| 74 | + | label: parts.length ? parts[parts.length - 1] : "/", | |
| 75 | + | used: parseFloat(m[2]) / 1073741824, size: size / 1073741824 | |
| 76 | + | }) | |
| 77 | + | } | |
| 78 | + | out.sort(function (a, b) { | |
| 79 | + | if (a.target === "/") return -1 | |
| 80 | + | if (b.target === "/") return 1 | |
| 81 | + | return a.target.localeCompare(b.target) | |
| 82 | + | }) | |
| 83 | + | return out | |
| 84 | + | } | |
| 85 | + | ||
| 55 | 86 | Process { | |
| 56 | 87 | 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 | − | } | |
| 88 | + | command: ["df", "-l", "-B1", "--output=source,used,size,target"] | |
| 89 | + | stdout: StdioCollector { onStreamFinished: root.disks = root.parseDf(text) } | |
| 67 | 90 | } | |
| 68 | − | Timer { interval: 30000; running: true; repeat: true; triggeredOnStart: true; onTriggered: dfProc.running = true } | |
| 91 | + | Timer { interval: 30000; running: true; repeat: true; triggeredOnStart: true; onTriggered: root.refreshDisks() } | |
| 69 | 92 | } | |
| @@ -10,11 +10,12 @@ Singleton { | |||
| 10 | 10 | // ---- knobs -------------------------------------------------------- | |
| 11 | 11 | property string font: "JetBrainsMono Nerd Font" // any Nerd Font mono | |
| 12 | 12 | property int fontSize: 12 | |
| 13 | + | property int wsFontSize: 11 // workspace icons only; everything else uses fontSize | |
| 13 | 14 | property real panelAlpha: 0.88 | |
| 14 | 15 | // your waybar uses @color14 for underlines/active workspace, so that is the default. | |
| 15 | 16 | // set -1 to auto-pick the most vivid of color1..color6 instead | |
| 16 | 17 | property int accentIndex: 14 | |
| 17 | − | property int barHeight: 18 | |
| 18 | + | property int barHeight: 22 | |
| 18 | 19 | property real barAlpha: 1.0 | |
| 19 | 20 | ||
| 20 | 21 | readonly property string walFile: Quickshell.env("HOME") + "/.cache/wal/colors.json" | |
| @@ -127,6 +128,12 @@ Singleton { | |||
| 127 | 128 | readonly property string iSearch: cp(0xF0349) | |
| 128 | 129 | ||
| 129 | 130 | readonly property string iHeadset: cp(0xF02CE) | |
| 131 | + | readonly property string iCamera: cp(0xF0100) | |
| 132 | + | readonly property string iCrop: cp(0xF019E) | |
| 133 | + | readonly property string iWindow: cp(0xF05B0) | |
| 134 | + | readonly property string iMonitor: cp(0xF0379) | |
| 135 | + | readonly property string iSave: cp(0xF0193) | |
| 136 | + | readonly property string iClipboard: cp(0xF0147) | |
| 130 | 137 | readonly property string iBell: cp(0xF009A) | |
| 131 | 138 | readonly property string iBellOff: cp(0xF009B) | |
| 132 | 139 | readonly property string iChip: cp(0xF061A) | |
| @@ -152,7 +159,7 @@ Singleton { | |||
| 152 | 159 | String.fromCharCode(0xf07c), String.fromCharCode(0xf120), String.fromCharCode(0xf0c3), | |
| 153 | 160 | String.fromCharCode(0xf03e) | |
| 154 | 161 | ] | |
| 155 | − | function wsIcon(id) { return (id >= 1 && id <= 20) ? wsIcons[(id - 1) % 10] : String(id) } | |
| 162 | + | function wsIcon(id) { return id >= 1 ? wsIcons[(id - 1) % 10] : String(id) } | |
| 156 | 163 | ||
| 157 | 164 | readonly property var batLevels: [0xF007A, 0xF007B, 0xF007C, 0xF007D, 0xF007E, | |
| 158 | 165 | 0xF007F, 0xF0080, 0xF0081, 0xF0082, 0xF0079] | |
| @@ -4,7 +4,8 @@ import Quickshell.Services.SystemTray | |||
| 4 | 4 | ||
| 5 | 5 | Item { | |
| 6 | 6 | id: tray | |
| 7 | − | visible: SystemTray.items.values.length > 0 | |
| 7 | + | property bool show: true | |
| 8 | + | visible: show && SystemTray.items.values.length > 0 | |
| 8 | 9 | implicitWidth: row.implicitWidth + 24 | |
| 9 | 10 | implicitHeight: parent ? parent.height : Theme.barHeight | |
| 10 | 11 | ||
| @@ -1,24 +1,35 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import Quickshell.Hyprland | |
| 3 | 3 | ||
| 4 | − | // Workspaces 1-10 always shown (like a persistent waybar setup), extras appear when used. | |
| 4 | + | // Per-monitor workspaces, dwm / split-monitor-workspaces style. | |
| 5 | + | // Each monitor owns a block of 10 ids (monitor 1: 1-10, monitor 2: 11-20, ...). | |
| 6 | + | // The block is derived from the monitor's own active workspace, so it works with or | |
| 7 | + | // without the plugin. Icons repeat every 10, like your waybar format-icons. | |
| 5 | 8 | Row { | |
| 6 | 9 | id: ws | |
| 7 | 10 | spacing: 0 | |
| 8 | 11 | ||
| 12 | + | property var monitor: null // HyprlandMonitor of the bar this sits in | |
| 13 | + | ||
| 14 | + | readonly property var activeWs: monitor ? monitor.activeWorkspace : null | |
| 15 | + | readonly property int offset: activeWs && activeWs.id > 0 ? Math.floor((activeWs.id - 1) / 10) * 10 : 0 | |
| 16 | + | readonly property bool monFocused: monitor ? monitor.focused : true | |
| 17 | + | readonly property var list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (i) { return offset + i }) | |
| 18 | + | ||
| 9 | 19 | function find(id) { | |
| 10 | 20 | var l = Hyprland.workspaces.values | |
| 11 | 21 | for (var i = 0; i < l.length; i++) if (l[i].id === id) return l[i] | |
| 12 | 22 | return null | |
| 13 | 23 | } | |
| 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 | |
| 24 | + | ||
| 25 | + | // scroll: next/previous *existing* workspace inside this monitor's block (like e+1 / e-1) | |
| 26 | + | function step(dir) { | |
| 27 | + | var cur = activeWs ? activeWs.id : offset + 1 | |
| 28 | + | for (var id = cur + dir; id >= offset + 1 && id <= offset + 10; id += dir) { | |
| 29 | + | var o = find(id) | |
| 30 | + | if (o !== null && o.toplevels.values.length > 0) { Hypr.workspace(id); return } | |
| 31 | + | } | |
| 20 | 32 | } | |
| 21 | − | readonly property var list: ids() | |
| 22 | 33 | ||
| 23 | 34 | Repeater { | |
| 24 | 35 | model: ws.list | |
| @@ -26,33 +37,35 @@ Row { | |||
| 26 | 37 | id: b | |
| 27 | 38 | required property var modelData | |
| 28 | 39 | readonly property var obj: ws.find(modelData) | |
| 29 | − | readonly property bool active: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === modelData | |
| 40 | + | readonly property bool active: ws.activeWs !== null && ws.activeWs.id === modelData | |
| 30 | 41 | readonly property bool occupied: obj !== null && obj.toplevels.values.length > 0 | |
| 31 | 42 | readonly property bool urgent: obj !== null && obj.urgent | |
| 32 | − | readonly property bool lit: active || m.containsMouse | |
| 43 | + | // active on the focused monitor = filled; active on another monitor = accent text + thick underline | |
| 44 | + | readonly property bool lit: (active && ws.monFocused) || m.containsMouse | |
| 45 | + | readonly property bool dimActive: active && !ws.monFocused && !m.containsMouse | |
| 33 | 46 | ||
| 34 | − | width: 32 | |
| 47 | + | width: 30 | |
| 35 | 48 | height: ws.height | |
| 36 | 49 | ||
| 37 | 50 | Rectangle { | |
| 38 | 51 | anchors.fill: parent | |
| 39 | 52 | color: b.lit ? Theme.accent : "transparent" | |
| 40 | 53 | } | |
| 41 | − | Rectangle { // occupied marker, like the inset underline in your css | |
| 42 | − | visible: b.occupied && !b.lit | |
| 54 | + | Rectangle { // occupied marker (1px), or active-on-unfocused-monitor marker (2px) | |
| 55 | + | visible: (b.occupied && !b.lit) || b.dimActive | |
| 43 | 56 | anchors.left: parent.left | |
| 44 | 57 | anchors.right: parent.right | |
| 45 | 58 | anchors.bottom: parent.bottom | |
| 46 | − | height: 1 | |
| 47 | − | color: Theme.cursorC | |
| 59 | + | height: b.dimActive ? 2 : 1 | |
| 60 | + | color: b.dimActive ? Theme.accent : Theme.cursorC | |
| 48 | 61 | } | |
| 49 | 62 | Text { | |
| 50 | 63 | anchors.centerIn: parent | |
| 51 | 64 | 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 | |
| 65 | + | color: b.lit ? Theme.bg : (b.dimActive ? Theme.accent : (b.urgent ? Theme.err : Theme.fg)) | |
| 66 | + | opacity: (b.occupied || b.active || b.lit || b.urgent) ? 1 : 0.4 | |
| 54 | 67 | font.family: Theme.font | |
| 55 | − | font.pixelSize: Theme.fontSize | |
| 68 | + | font.pixelSize: Theme.wsFontSize | |
| 56 | 69 | } | |
| 57 | 70 | MouseArea { | |
| 58 | 71 | id: m | |
| @@ -60,7 +73,7 @@ Row { | |||
| 60 | 73 | hoverEnabled: true | |
| 61 | 74 | cursorShape: Qt.PointingHandCursor | |
| 62 | 75 | onClicked: Hypr.workspace(b.modelData) | |
| 63 | − | onWheel: w => Hypr.workspace(w.angleDelta.y > 0 ? "e-1" : "e+1") | |
| 76 | + | onWheel: w => ws.step(w.angleDelta.y > 0 ? -1 : 1) | |
| 64 | 77 | } | |
| 65 | 78 | } | |
| 66 | 79 | } | |
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | 2 | import Quickshell | |
| 3 | 3 | import Quickshell.Io | |
| 4 | + | import Quickshell.Hyprland | |
| 4 | 5 | import Quickshell.Services.Pipewire | |
| 5 | 6 | ||
| 6 | 7 | // aurora: bar + control panel + notifications, themed from pywal16. | |
| @@ -11,26 +12,41 @@ ShellRoot { | |||
| 11 | 12 | property int tab: 0 | |
| 12 | 13 | property double closedAt: 0 | |
| 13 | 14 | property string lastTab: "" | |
| 15 | + | property string lastScreen: "" | |
| 16 | + | property var panelScreen: focusedScreen() | |
| 17 | + | readonly property var focusedScr: focusedScreen() | |
| 14 | 18 | readonly property var tabNames: ["home", "audio", "net", "bt", "sys", "notif"] | |
| 15 | 19 | ||
| 20 | + | // screen of the monitor that currently has focus (falls back to the first screen) | |
| 21 | + | function focusedScreen() { | |
| 22 | + | var m = Hyprland.focusedMonitor | |
| 23 | + | var l = Quickshell.screens | |
| 24 | + | if (m) for (var i = 0; i < l.length; i++) if (l[i].name === m.name) return l[i] | |
| 25 | + | return l.length ? l[0] : null | |
| 26 | + | } | |
| 16 | 27 | function close() { | |
| 17 | 28 | if (!opened) return | |
| 18 | 29 | opened = false | |
| 19 | 30 | closedAt = Date.now() | |
| 20 | 31 | lastTab = tabNames[tab] | |
| 32 | + | lastScreen = panelScreen ? panelScreen.name : "" | |
| 21 | 33 | } | |
| 22 | 34 | function openTab(name) { | |
| 23 | 35 | var i = tabNames.indexOf(name) | |
| 24 | 36 | if (i >= 0) tab = i | |
| 37 | + | panelScreen = focusedScreen() | |
| 25 | 38 | opened = true | |
| 26 | 39 | } | |
| 27 | 40 | // bar buttons: click once to open that tab, again to close. | |
| 28 | 41 | // (the focus grab already closed the panel on the press, so guard against instant re-open) | |
| 29 | − | function togglePanel(name) { | |
| 42 | + | // scr = the screen whose bar was clicked, so the panel opens on that monitor | |
| 43 | + | function togglePanel(name, scr) { | |
| 30 | 44 | var i = tabNames.indexOf(name) | |
| 31 | − | if (opened && tab === i) { close(); return } | |
| 32 | − | if (!opened && lastTab === name && Date.now() - closedAt < 250) return | |
| 45 | + | var sn = scr ? scr.name : "" | |
| 46 | + | if (opened && tab === i && panelScreen && panelScreen.name === sn) { close(); return } | |
| 47 | + | if (!opened && lastTab === name && lastScreen === sn && Date.now() - closedAt < 250) return | |
| 33 | 48 | tab = i | |
| 49 | + | panelScreen = scr | |
| 34 | 50 | opened = true | |
| 35 | 51 | } | |
| 36 | 52 | ||
| @@ -39,24 +55,27 @@ ShellRoot { | |||
| 39 | 55 | ||
| 40 | 56 | Variants { | |
| 41 | 57 | model: Quickshell.screens | |
| 42 | − | Bar { onTogglePanel: name => root.togglePanel(name) } | |
| 58 | + | Bar { onTogglePanel: (name, scr) => root.togglePanel(name, scr) } | |
| 43 | 59 | } | |
| 44 | 60 | ||
| 45 | 61 | ControlPanel { | |
| 62 | + | screen: root.panelScreen | |
| 46 | 63 | opened: root.opened | |
| 47 | 64 | tab: root.tab | |
| 48 | 65 | onTabRequested: i => root.tab = i | |
| 49 | 66 | onCloseRequested: root.close() | |
| 50 | 67 | } | |
| 51 | 68 | ||
| 52 | − | NotifPopups { suppressed: root.opened } | |
| 69 | + | NotifPopups { screen: root.focusedScr; suppressed: root.opened } | |
| 53 | 70 | ||
| 54 | 71 | IpcHandler { | |
| 55 | 72 | target: "shell" | |
| 56 | − | function toggle(): void { if (root.opened) root.close(); else root.opened = true } | |
| 57 | − | function open(): void { root.opened = true } | |
| 73 | + | function toggle(): void { if (root.opened) root.close(); else root.openTab(root.tabNames[root.tab]) } | |
| 74 | + | function open(): void { root.openTab(root.tabNames[root.tab]) } | |
| 58 | 75 | function close(): void { root.close() } | |
| 59 | 76 | function show(name: string): void { root.openTab(name) } | |
| 60 | 77 | function dnd(): void { Notifs.dnd = !Notifs.dnd } | |
| 78 | + | function shot(mode: string): void { root.close(); Shot.take(mode) } // region | window | output | ocr | |
| 79 | + | function shotsave(): void { Shot.save = !Shot.save } | |
| 61 | 80 | } | |
| 62 | 81 | } | |