init
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var popups: null
|
||||
readonly property bool expanded: popups ? popups.current === "network" : false
|
||||
property bool wifiEnabled: true
|
||||
property var networks: []
|
||||
property string activeSsid: ""
|
||||
property int activeSignal: 0
|
||||
property var knownSsids: ({})
|
||||
property string statusMsg: ""
|
||||
property string pendingSsid: ""
|
||||
|
||||
implicitWidth: icon.implicitWidth
|
||||
implicitHeight: 30
|
||||
|
||||
function secured(sec) {
|
||||
return !!sec && sec.trim().length > 0;
|
||||
}
|
||||
|
||||
function signalGlyph(s) {
|
||||
if (s >= 80) return "\u{F0928}";
|
||||
if (s >= 55) return "\u{F0925}";
|
||||
if (s >= 30) return "\u{F0922}";
|
||||
return "\u{F091F}";
|
||||
}
|
||||
|
||||
function wifiGlyph() {
|
||||
if (!wifiEnabled) return "\u{F05AA}";
|
||||
if (activeSsid === "") return "\u{F05A9}";
|
||||
return signalGlyph(activeSignal);
|
||||
}
|
||||
|
||||
// nmcli -t escapes ':' as '\:' and '\' as '\\'; split on unescaped colons.
|
||||
function splitFields(line) {
|
||||
const out = [];
|
||||
let cur = "";
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const c = line[i];
|
||||
if (c === "\\" && i + 1 < line.length) {
|
||||
cur += line[i + 1];
|
||||
i++;
|
||||
} else if (c === ":") {
|
||||
out.push(cur);
|
||||
cur = "";
|
||||
} else {
|
||||
cur += c;
|
||||
}
|
||||
}
|
||||
out.push(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
statusProc.running = true;
|
||||
listProc.running = true;
|
||||
knownProc.running = true;
|
||||
}
|
||||
|
||||
function setWifi(on) {
|
||||
radioProc.command = ["nmcli", "radio", "wifi", on ? "on" : "off"];
|
||||
radioProc.running = true;
|
||||
root.wifiEnabled = on;
|
||||
Qt.callLater(root.refresh);
|
||||
}
|
||||
|
||||
function rescan() {
|
||||
root.statusMsg = "Scanning…";
|
||||
rescanProc.running = true;
|
||||
}
|
||||
|
||||
function doConnect(ssid, password) {
|
||||
root.statusMsg = "Connecting to " + ssid + "…";
|
||||
const cmd = ["nmcli", "device", "wifi", "connect", ssid];
|
||||
if (password && password.length > 0)
|
||||
cmd.push("password", password);
|
||||
connectProc.command = cmd;
|
||||
connectProc.running = true;
|
||||
root.pendingSsid = "";
|
||||
}
|
||||
|
||||
function disconnect(ssid) {
|
||||
root.statusMsg = "Disconnecting…";
|
||||
actionProc.command = ["nmcli", "connection", "down", "id", ssid];
|
||||
actionProc.running = true;
|
||||
}
|
||||
|
||||
function onNetworkClicked(net) {
|
||||
if (net.active) {
|
||||
root.disconnect(net.ssid);
|
||||
return;
|
||||
}
|
||||
if (root.secured(net.security) && !root.knownSsids[net.ssid]) {
|
||||
root.pendingSsid = (root.pendingSsid === net.ssid) ? "" : net.ssid;
|
||||
return;
|
||||
}
|
||||
root.doConnect(net.ssid, "");
|
||||
}
|
||||
|
||||
Process {
|
||||
id: statusProc
|
||||
command: ["nmcli", "-t", "-f", "WIFI", "radio"]
|
||||
stdout: StdioCollector {
|
||||
id: statusOut
|
||||
onStreamFinished: root.wifiEnabled = statusOut.text.trim() === "enabled"
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: knownProc
|
||||
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
|
||||
stdout: StdioCollector {
|
||||
id: knownOut
|
||||
onStreamFinished: {
|
||||
const map = ({});
|
||||
const lines = knownOut.text.split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
const f = root.splitFields(line);
|
||||
if (f[1] && f[1].indexOf("wireless") >= 0)
|
||||
map[f[0]] = true;
|
||||
}
|
||||
root.knownSsids = map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: listProc
|
||||
command: ["nmcli", "-t", "-f", "ACTIVE,SSID,SIGNAL,SECURITY", "device", "wifi", "list"]
|
||||
stdout: StdioCollector {
|
||||
id: listOut
|
||||
onStreamFinished: {
|
||||
const seen = ({});
|
||||
const list = [];
|
||||
let actSsid = "";
|
||||
let actSig = 0;
|
||||
const lines = listOut.text.split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
const f = root.splitFields(line);
|
||||
const active = f[0] === "yes";
|
||||
const ssid = f[1] || "";
|
||||
const sig = parseInt(f[2] || "0") || 0;
|
||||
const sec = f[3] || "";
|
||||
if (ssid === "") continue;
|
||||
if (active) {
|
||||
actSsid = ssid;
|
||||
actSig = sig;
|
||||
}
|
||||
if (seen[ssid] !== undefined) {
|
||||
const ex = list[seen[ssid]];
|
||||
if (sig > ex.signal) ex.signal = sig;
|
||||
if (active) ex.active = true;
|
||||
continue;
|
||||
}
|
||||
seen[ssid] = list.length;
|
||||
list.push({ ssid: ssid, signal: sig, security: sec, active: active });
|
||||
}
|
||||
list.sort((a, b) => (b.active - a.active) || (b.signal - a.signal));
|
||||
root.networks = list;
|
||||
root.activeSsid = actSsid;
|
||||
root.activeSignal = actSig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: radioProc }
|
||||
|
||||
Process {
|
||||
id: rescanProc
|
||||
command: ["nmcli", "device", "wifi", "rescan"]
|
||||
onExited: {
|
||||
root.statusMsg = "";
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: connectProc
|
||||
stdout: StdioCollector {
|
||||
id: connectOut
|
||||
onStreamFinished: {
|
||||
root.statusMsg = "";
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
id: connectErr
|
||||
onStreamFinished: {
|
||||
const t = connectErr.text.trim();
|
||||
if (t !== "")
|
||||
root.statusMsg = t.replace(/^Error:\s*/, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: actionProc
|
||||
onExited: root.refresh()
|
||||
}
|
||||
|
||||
onExpandedChanged: if (expanded) refresh()
|
||||
|
||||
Timer {
|
||||
interval: 8000
|
||||
running: root.expanded
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Text {
|
||||
id: icon
|
||||
anchors.centerIn: parent
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 17
|
||||
color: root.wifiEnabled ? (root.activeSsid !== "" ? "#89b4fa" : "#cdd6f4") : "#6c7086"
|
||||
text: root.wifiGlyph()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if (root.popups) root.popups.toggle("network")
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.expanded
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
focusable: true
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
margins.top: 6
|
||||
margins.right: 8
|
||||
implicitWidth: 360
|
||||
implicitHeight: card.implicitHeight
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: true
|
||||
windows: [win]
|
||||
onCleared: if (root.popups) root.popups.current = ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: parent.width
|
||||
implicitHeight: col.implicitHeight + 24
|
||||
radius: 16
|
||||
color: "#f21e1e2e"
|
||||
border.color: "#45475a"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 16
|
||||
color: "#cdd6f4"
|
||||
text: "\u{F05A9} Wi-Fi"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Pill {
|
||||
visible: root.wifiEnabled
|
||||
text: "Rescan"
|
||||
onClicked: root.rescan()
|
||||
}
|
||||
|
||||
Pill {
|
||||
active: root.wifiEnabled
|
||||
text: root.wifiEnabled ? "On" : "Off"
|
||||
onClicked: root.setWifi(!root.wifiEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 1
|
||||
color: "#313244"
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.wifiEnabled
|
||||
color: "#6c7086"
|
||||
font.pixelSize: 13
|
||||
text: "Wi-Fi is off"
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.statusMsg !== ""
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
color: "#f9e2af"
|
||||
font.pixelSize: 11
|
||||
text: root.statusMsg
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.wifiEnabled ? root.networks : []
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: nrow
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 15
|
||||
color: nrow.modelData.active ? "#a6e3a1" : "#cdd6f4"
|
||||
text: root.signalGlyph(nrow.modelData.signal)
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
color: nrow.modelData.active ? "#a6e3a1" : "#cdd6f4"
|
||||
font.pixelSize: 13
|
||||
text: nrow.modelData.ssid
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.secured(nrow.modelData.security)
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 12
|
||||
color: "#6c7086"
|
||||
text: "\u{F033E}"
|
||||
}
|
||||
|
||||
Pill {
|
||||
active: nrow.modelData.active
|
||||
text: nrow.modelData.active ? "Disconnect" : "Connect"
|
||||
onClicked: root.onNetworkClicked(nrow.modelData)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: pwRow
|
||||
visible: root.pendingSsid === nrow.modelData.ssid
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
onVisibleChanged: if (visible) pwField.forceActiveFocus()
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
radius: 8
|
||||
color: "#181825"
|
||||
border.color: pwField.activeFocus ? "#89b4fa" : "#45475a"
|
||||
border.width: 1
|
||||
|
||||
TextField {
|
||||
id: pwField
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
echoMode: TextInput.Password
|
||||
placeholderText: "Password"
|
||||
color: "#cdd6f4"
|
||||
placeholderTextColor: "#6c7086"
|
||||
font.pixelSize: 12
|
||||
background: null
|
||||
onAccepted: root.doConnect(nrow.modelData.ssid, text)
|
||||
}
|
||||
}
|
||||
|
||||
Pill {
|
||||
active: true
|
||||
text: "Join"
|
||||
onClicked: root.doConnect(nrow.modelData.ssid, pwField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Pill: Rectangle {
|
||||
id: pill
|
||||
|
||||
property string text: ""
|
||||
property bool active: false
|
||||
signal clicked
|
||||
|
||||
implicitWidth: pillLabel.implicitWidth + 18
|
||||
implicitHeight: pillLabel.implicitHeight + 8
|
||||
radius: height / 2
|
||||
color: pill.active ? "#89b4fa" : "#313244"
|
||||
|
||||
Text {
|
||||
id: pillLabel
|
||||
anchors.centerIn: parent
|
||||
text: pill.text
|
||||
color: pill.active ? "#11111b" : "#cdd6f4"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: pill.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user