| 1 | pragma Singleton |
| 2 | import QtQuick |
| 3 | import Quickshell |
| 4 | import Quickshell.Io |
| 5 | |
| 6 | // Wi-Fi backend: thin wrapper over nmcli (NetworkManager). |
| 7 | Singleton { |
| 8 | id: root |
| 9 | |
| 10 | property bool active: false // NetPage sets this while visible |
| 11 | property bool wifiEnabled: true |
| 12 | property string wifiDevice: "" |
| 13 | property var raw: [] // [{inUse, ssid, signal, security}] |
| 14 | property var savedNames: [] |
| 15 | property string askFor: "" // ssid currently asking for a password |
| 16 | property string message: "" |
| 17 | property bool busy: false |
| 18 | property bool scanning: false |
| 19 | |
| 20 | property string lastSsid: "" |
| 21 | property bool lastWasNew: false |
| 22 | |
| 23 | readonly property var networks: build(raw, savedNames) |
| 24 | readonly property string connected: { |
| 25 | for (var i = 0; i < raw.length; i++) if (raw[i].inUse) return raw[i].ssid |
| 26 | return "" |
| 27 | } |
| 28 | |
| 29 | onActiveChanged: if (active) { askFor = ""; message = ""; rescan() } |
| 30 | |
| 31 | // nmcli -t output: ':' separates fields, '\:' and '\\' are escapes |
| 32 | function parseTerse(line) { |
| 33 | var out = [], cur = "" |
| 34 | for (var i = 0; i < line.length; i++) { |
| 35 | var ch = line[i] |
| 36 | if (ch === "\\" && i + 1 < line.length) { cur += line[i + 1]; i++ } |
| 37 | else if (ch === ":") { out.push(cur); cur = "" } |
| 38 | else cur += ch |
| 39 | } |
| 40 | out.push(cur) |
| 41 | return out |
| 42 | } |
| 43 | |
| 44 | function build(list, saved) { |
| 45 | var out = [] |
| 46 | for (var i = 0; i < list.length; i++) { |
| 47 | var n = list[i] |
| 48 | out.push({ |
| 49 | ssid: n.ssid, signal: n.signal, security: n.security, inUse: n.inUse, |
| 50 | saved: saved.indexOf(n.ssid) >= 0 |
| 51 | }) |
| 52 | } |
| 53 | out.sort(function (a, b) { |
| 54 | if (a.inUse !== b.inUse) return a.inUse ? -1 : 1 |
| 55 | return b.signal - a.signal |
| 56 | }) |
| 57 | return out |
| 58 | } |
| 59 | |
| 60 | function refresh() { |
| 61 | radioProc.running = true |
| 62 | devProc.running = true |
| 63 | savedProc.running = true |
| 64 | listProc.running = true |
| 65 | } |
| 66 | |
| 67 | function rescan() { |
| 68 | scanning = true |
| 69 | rescanProc.running = true |
| 70 | refresh() |
| 71 | } |
| 72 | |
| 73 | // ---- actions ------------------------------------------------------ |
| 74 | function run(args) { |
| 75 | if (busy) return |
| 76 | busy = true |
| 77 | runProc.command = ["sh", "-c", |
| 78 | 'out=$(nmcli "$@" 2>&1); rc=$?; printf "%s\\n%d" "$out" "$rc"', "sh"].concat(args) |
| 79 | runProc.running = true |
| 80 | } |
| 81 | |
| 82 | function finish(text) { |
| 83 | busy = false |
| 84 | var lines = text.split("\n") |
| 85 | var rc = parseInt(lines.pop()) |
| 86 | var out = lines.join(" ").trim() |
| 87 | if (rc !== 0) { |
| 88 | message = out |
| 89 | // a failed first-time connect leaves a broken profile behind; drop it |
| 90 | if (lastWasNew && lastSsid !== "") |
| 91 | Quickshell.execDetached(["nmcli", "connection", "delete", "id", lastSsid]) |
| 92 | } else { |
| 93 | message = "" |
| 94 | askFor = "" |
| 95 | } |
| 96 | lastWasNew = false |
| 97 | refresh() |
| 98 | } |
| 99 | |
| 100 | function connectTo(entry) { |
| 101 | if (entry.inUse) return |
| 102 | if (entry.saved || entry.security === "" || entry.security === "--") connect(entry.ssid, "") |
| 103 | else askFor = (askFor === entry.ssid) ? "" : entry.ssid |
| 104 | } |
| 105 | |
| 106 | function connect(ssid, password) { |
| 107 | message = "" |
| 108 | lastSsid = ssid |
| 109 | var isSaved = savedNames.indexOf(ssid) >= 0 |
| 110 | if (isSaved && !password) { |
| 111 | lastWasNew = false |
| 112 | run(["connection", "up", "id", ssid]) |
| 113 | } else { |
| 114 | lastWasNew = !isSaved |
| 115 | var cmd = ["--wait", "20", "device", "wifi", "connect", ssid] |
| 116 | if (password) cmd = cmd.concat(["password", password]) |
| 117 | run(cmd) |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | function disconnect() { if (wifiDevice !== "") run(["device", "disconnect", wifiDevice]) } |
| 122 | function forget(ssid) { run(["connection", "delete", "id", ssid]) } |
| 123 | function setWifi(on) { run(["radio", "wifi", on ? "on" : "off"]) } |
| 124 | |
| 125 | // ---- processes ---------------------------------------------------- |
| 126 | Timer { |
| 127 | interval: 10000 |
| 128 | running: root.active |
| 129 | repeat: true |
| 130 | onTriggered: root.refresh() |
| 131 | } |
| 132 | |
| 133 | Process { |
| 134 | id: runProc |
| 135 | stdout: StdioCollector { onStreamFinished: root.finish(text) } |
| 136 | } |
| 137 | |
| 138 | Process { |
| 139 | id: rescanProc |
| 140 | command: ["nmcli", "device", "wifi", "rescan"] |
| 141 | onExited: { root.scanning = false; listProc.running = true } |
| 142 | } |
| 143 | |
| 144 | Process { |
| 145 | id: radioProc |
| 146 | command: ["nmcli", "radio", "wifi"] |
| 147 | stdout: StdioCollector { onStreamFinished: root.wifiEnabled = text.trim() === "enabled" } |
| 148 | } |
| 149 | |
| 150 | Process { |
| 151 | id: devProc |
| 152 | command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] |
| 153 | stdout: StdioCollector { |
| 154 | onStreamFinished: { |
| 155 | var lines = text.split("\n") |
| 156 | for (var i = 0; i < lines.length; i++) { |
| 157 | var f = root.parseTerse(lines[i]) |
| 158 | if (f.length >= 2 && f[1] === "wifi") { root.wifiDevice = f[0]; return } |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | Process { |
| 165 | id: savedProc |
| 166 | command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"] |
| 167 | stdout: StdioCollector { |
| 168 | onStreamFinished: { |
| 169 | var out = [], lines = text.split("\n") |
| 170 | for (var i = 0; i < lines.length; i++) { |
| 171 | var f = root.parseTerse(lines[i]) |
| 172 | if (f.length >= 2 && f[1].indexOf("wireless") >= 0) out.push(f[0]) |
| 173 | } |
| 174 | root.savedNames = out |
| 175 | } |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | Process { |
| 180 | id: listProc |
| 181 | command: ["nmcli", "-t", "-f", "IN-USE,SSID,SIGNAL,SECURITY", "device", "wifi", "list"] |
| 182 | stdout: StdioCollector { |
| 183 | onStreamFinished: { |
| 184 | var best = {}, lines = text.split("\n") |
| 185 | for (var i = 0; i < lines.length; i++) { |
| 186 | var f = root.parseTerse(lines[i]) |
| 187 | if (f.length < 4 || f[1] === "") continue |
| 188 | var e = { inUse: f[0] === "*", ssid: f[1], signal: parseInt(f[2]) || 0, security: f[3] } |
| 189 | var o = best[e.ssid] |
| 190 | if (!o || e.inUse || (!o.inUse && e.signal > o.signal)) best[e.ssid] = e |
| 191 | } |
| 192 | var out = [] |
| 193 | for (var k in best) out.push(best[k]) |
| 194 | root.raw = out |
| 195 | } |
| 196 | } |
| 197 | } |
| 198 | } |