A simple yet powerful UI overlay made for Wayland WMs built with Quickshell
wayland qs linux ui ux
at main 219 lines 8.5 kB view raw
1import QtQuick 2import Quickshell.Io 3import Quickshell 4 5QtObject { 6 id: root 7 8 property string cfgPath: "$HOME/.config/quickshell/m4.shell/config.json" 9 property string ctlPath: "$HOME/.config/quickshell/m4.shell/scripts/configctl.sh" 10 11 property var data: ({}) 12 property bool loaded: false 13 14 property bool hydrating: false 15 16 property Timer saveTimer: Timer { 17 interval: 150 18 repeat: false 19 onTriggered: root.flush() 20 } 21 22 function scheduleSave() { 23 if (!loaded || hydrating) return 24 saveTimer.restart() 25 } 26 27 function quoteForShell(s) { 28 return "'" + String(s).replace(/'/g, "'\\''") + "'" 29 } 30 31 property Process loadProc: Process { 32 command: ["sh", "-lc", root.ctlPath + " dump"] 33 stdout: StdioCollector { 34 waitForEnd: true 35 onStreamFinished: { 36 const txt = (this.text || "").trim() 37 if (!txt) return 38 try { 39 const obj = JSON.parse(txt) 40 root.applyLoaded(obj) 41 } catch (e) { 42 console.error("ConfigService: JSON parse failed:", e) 43 } 44 } 45 } 46 stderr: StdioCollector { 47 waitForEnd: true 48 onStreamFinished: { 49 const err = (this.text || "").trim() 50 if (err) console.error("ConfigService: load stderr:", err) 51 } 52 } 53 } 54 55 function reload() { 56 loaded = false 57 hydrating = true 58 loadProc.exec(loadProc.command) 59 } 60 61 property Process saveProc: Process { 62 stderr: StdioCollector { 63 waitForEnd: true 64 onStreamFinished: { 65 const err = (this.text || "").trim() 66 if (err) console.error("ConfigService: save stderr:", err) 67 } 68 } 69 } 70 71 function flush() { 72 if (!loaded || hydrating) return 73 const payload = JSON.stringify(root.data) 74 75 saveProc.command = ["sh", "-lc", root.ctlPath + " write " + quoteForShell(payload)] 76 saveProc.exec(saveProc.command) 77 } 78 79 property Process resetProc: Process { 80 stderr: StdioCollector { 81 waitForEnd: true 82 onStreamFinished: { 83 const err = (this.text || "").trim() 84 if (err) console.error("ConfigService: reset stderr:", err) 85 } 86 } 87 onExited: (code) => { 88 if (code === 0) root.reload() 89 } 90 } 91 92 function restoreDefaults() { 93 resetProc.command = ["sh", "-lc", root.ctlPath + " reset"] 94 resetProc.exec(resetProc.command) 95 } 96 97 function get(pathArr, fallback) { 98 var cur = root.data 99 for (var i = 0; i < pathArr.length; i++) { 100 if (!cur || cur[pathArr[i]] === undefined) return fallback 101 cur = cur[pathArr[i]] 102 } 103 return cur 104 } 105 106 function setPath(pathArr, value) { 107 if (!root.data || typeof root.data !== "object") root.data = ({}) 108 var cur = root.data 109 for (var i = 0; i < pathArr.length - 1; i++) { 110 const k = pathArr[i] 111 if (cur[k] === undefined || cur[k] === null || typeof cur[k] !== "object") 112 cur[k] = {} 113 cur = cur[k] 114 } 115 cur[pathArr[pathArr.length - 1]] = value 116 root.data = root.data 117 scheduleSave() 118 } 119 120 function applyLoaded(obj) { 121 hydrating = true 122 root.data = (obj && typeof obj === "object") ? obj : ({}) 123 124 bg = String(get(["appearance","bg"], "#0B0B0B")) 125 bg2 = String(get(["appearance","bg2"], "#1A1A1A")) 126 fg = String(get(["appearance","fg"], "#E6E6E6")) 127 text = String(get(["appearance","text"], "#E6E6E6")) 128 muted = String(get(["appearance","muted"], "#A8A8A8")) 129 accent = String(get(["appearance","accent"], "#B80000")) 130 borderColor = String(get(["appearance","borderColor"], "#2A2A2A")) 131 opacity = Number(get(["appearance","opacity"], 1.0)) 132 133 barHeight = parseInt(get(["appearance","barHeight"], 35), 10) 134 pad = parseInt(get(["appearance","pad"], 20), 10) 135 fontSize = parseInt(get(["appearance","fontSize"], 12), 10) 136 radius = parseInt(get(["appearance","radius"], 12), 10) 137 animMs = parseInt(get(["appearance","animMs"], 40), 10) 138 139 edge = String(get(["sidebar","edge"], "left")).trim().toLowerCase() 140 edgeWidth = parseInt(get(["sidebar","edgeWidth"], 8), 10) 141 edgeCornerRadius = parseInt(get(["sidebar","edgeCornerRadius"], 20), 10) 142 sidebarWidth = parseInt(get(["sidebar","sidebarWidth"], 300), 10) 143 hoverCloseDelayMs = parseInt(get(["sidebar","hoverCloseDelayMs"], 300), 10) 144 145 const ebs = get(["sidebar","edgeByScreen"], ({})) 146 edgeByScreen = (ebs && typeof ebs === "object") ? ebs : ({}) 147 148 const lm = get(["bar","leftModules"], []) 149 const cm = get(["bar","centerModules"], []) 150 const rm = get(["bar","rightModules"], []) 151 152 leftModules = Array.isArray(lm) ? lm : [] 153 centerModules = Array.isArray(cm) ? cm : [] 154 rightModules = Array.isArray(rm) ? rm : [] 155 156 hydrating = false 157 loaded = true 158 } 159 160 property string bg: "#0B0B0B" 161 property string bg2: "#1A1A1A" 162 property string fg: "#E6E6E6" 163 property string text: "#E6E6E6" 164 property string muted: "#A8A8A8" 165 property string accent: "#B80000" 166 property string borderColor: "#2A2A2A" 167 property real opacity: 1.0 168 169 property int barHeight: 35 170 property int pad: 20 171 property int fontSize: 12 172 property int radius: 12 173 property int animMs: 40 174 175 onBgChanged: if (loaded && !hydrating) setPath(["appearance","bg"], String(bg)) 176 onBg2Changed: if (loaded && !hydrating) setPath(["appearance","bg2"], String(bg2)) 177 onFgChanged: if (loaded && !hydrating) setPath(["appearance","fg"], String(fg)) 178 onTextChanged: if (loaded && !hydrating) setPath(["appearance","text"], String(text)) 179 onMutedChanged: if (loaded && !hydrating) setPath(["appearance","muted"], String(muted)) 180 onAccentChanged: if (loaded && !hydrating) setPath(["appearance","accent"], String(accent)) 181 onBorderColorChanged: if (loaded && !hydrating) setPath(["appearance","borderColor"], String(borderColor)) 182 onOpacityChanged: if (loaded && !hydrating) setPath(["appearance","opacity"], opacity) 183 184 onBarHeightChanged: if (loaded && !hydrating) setPath(["appearance","barHeight"], barHeight) 185 onPadChanged: if (loaded && !hydrating) setPath(["appearance","pad"], pad) 186 onFontSizeChanged: if (loaded && !hydrating) setPath(["appearance","fontSize"], fontSize) 187 onRadiusChanged: if (loaded && !hydrating) setPath(["appearance","radius"], radius) 188 onAnimMsChanged: if (loaded && !hydrating) setPath(["appearance","animMs"], animMs) 189 190 property string edge: "left" 191 property int edgeWidth: 8 192 property int edgeCornerRadius: 20 193 property int sidebarWidth: 300 194 property int hoverCloseDelayMs: 300 195 property var edgeByScreen: ({}) 196 197 onEdgeChanged: if (loaded && !hydrating) setPath(["sidebar","edge"], String(edge).trim().toLowerCase()) 198 onEdgeWidthChanged: if (loaded && !hydrating) setPath(["sidebar","edgeWidth"], edgeWidth) 199 onEdgeCornerRadiusChanged: if (loaded && !hydrating) setPath(["sidebar","edgeCornerRadius"], edgeCornerRadius) 200 onSidebarWidthChanged: if (loaded && !hydrating) setPath(["sidebar","sidebarWidth"], sidebarWidth) 201 onHoverCloseDelayMsChanged: if (loaded && !hydrating) setPath(["sidebar","hoverCloseDelayMs"], hoverCloseDelayMs) 202 onEdgeByScreenChanged: if (loaded && !hydrating) setPath(["sidebar","edgeByScreen"], edgeByScreen) 203 204 property var leftModules: [] 205 property var centerModules: [] 206 property var rightModules: [] 207 208 onLeftModulesChanged: if (loaded && !hydrating) setPath(["bar","leftModules"], leftModules) 209 onCenterModulesChanged: if (loaded && !hydrating) setPath(["bar","centerModules"], centerModules) 210 onRightModulesChanged: if (loaded && !hydrating) setPath(["bar","rightModules"], rightModules) 211 212 function setBarEnabled(section, k, on) { 213 const prop = section + "Modules" 214 const arr = (root[prop] || []) 215 root[prop] = arr.map(e => (e.key === k ? ({ key: e.key, enabled: !!on }) : e)) 216 } 217 218 Component.onCompleted: reload() 219}