aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/network/NetworkModule.qml
blob: 031047502075dded6b429216042d3fa6eed308b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

import Quickshell
import Quickshell.Networking
import Quickshell.Io
import QtQuick
import "../.."

// Always active: the backends push and there is no poll to gate, so the
// state the tile shows is live from launch. Referencing Networking here is
// also what instantiates the singleton at shell start.
Module {
    id: mod

    name: "network"
    label: "Network"
    alwaysActive: true

    // .values on the device ObjectModel; empty until the backend is ready,
    // roughly two seconds in. Bindings repaint when it arrives.
    readonly property var devices: Networking.devices ? Networking.devices.values : []
    readonly property var wiredDevices: devices.filter(d => d.type === DeviceType.Wired)
    readonly property var wifiDevice: devices.find(d => d.type === DeviceType.Wifi) ?? null

    readonly property bool wiredUp: wiredDevices.some(d => d.connected)
    readonly property bool wifiUp: wifiDevice ? wifiDevice.connected : false
    readonly property bool wifiOn: Networking.wifiEnabled
    readonly property var wifiNetworks: wifiDevice && wifiDevice.networks ? wifiDevice.networks.values : []

    readonly property string wifiSsid: {
        const n = wifiNetworks.find(x => x.connected);
        return n ? n.name : "";
    }
    readonly property string wiredName: {
        const d = wiredDevices.find(x => x.connected);
        return d ? d.name : "";
    }

    // Local IPs, keyed by interface. The Networking API exposes only the MAC
    // (NetworkDevice.address), so this is the module's one shell-out. It is
    // refreshed when a link comes up and on a slow timer while one is up, to
    // catch a DHCP renew. `ip -j` is JSON, so no text parsing.
    property var ipByIface: ({})
    function ipFor(iface) { return ipByIface[iface] ?? ""; }
    function refreshIps() {
        ipProc.running = false;
        ipProc.running = true;
    }

    // Connected first, then known, then strongest. Kept here so both the
    // tile and the page read the same order.
    readonly property var wifiNetworksSorted: {
        const arr = wifiNetworks.slice();
        arr.sort((a, b) =>
            ((b.connected ? 1 : 0) - (a.connected ? 1 : 0)) ||
            ((b.known ? 1 : 0) - (a.known ? 1 : 0)) ||
            ((b.signalStrength ?? 0) - (a.signalStrength ?? 0)));
        return arr;
    }

    // The tile is a globe: the wired/wifi distinction belongs to the page's
    // section headers, not the tile.
    icon: "\uf0ac"

    // Active while any link is up.
    active: mod.wiredUp || mod.wifiUp

    // The network a connect is pending on, so connectionFailed can be caught.
    property var pendingNetwork: null
    property string error: ""

    function connect(net) {
        mod.pendingNetwork = net;
        mod.error = "";
        net.connect();
    }

    function connectWithPsk(net, psk) {
        mod.pendingNetwork = net;
        mod.error = "";
        net.connectWithPsk(psk);
    }

    function disconnectNetwork(net) { mod.error = ""; if (net) net.disconnect(); }
    function forget(net) { mod.error = ""; if (net) net.forget(); }

    // A failure after the drawer closed would otherwise go unseen, so it also
    // raises a notification. Same cached-Process shape as VmModule.
    function notify(title, body) {
        notifyProc.command = ["notify-send", "--app-name=network",
                              "--urgency=critical", "--icon=error", title, body];
        notifyProc.running = false;
        notifyProc.running = true;
    }

    property Process notifyProc: Process {}

    property Process ipProc: Process {
        command: ["ip", "-j", "-4", "addr", "show"]
        stdout: StdioCollector {
            onStreamFinished: {
                const map = {};
                try {
                    for (const iface of JSON.parse(text)) {
                        const a = (iface.addr_info ?? []).find(x => x.family === "inet");
                        if (a) map[iface.ifname] = a.local;
                    }
                } catch (e) {}
                mod.ipByIface = map;
            }
        }
    }

    // Only poll while something is actually up.
    property Timer ipTimer: Timer {
        interval: 15000
        repeat: true
        running: mod.wiredUp || mod.wifiUp
        onTriggered: mod.refreshIps()
    }

    property Connections conn: Connections {
        target: mod.pendingNetwork
        function onConnectionFailed(reason) {
            mod.error = "Could not connect to " + (mod.pendingNetwork?.name ?? "network");
            mod.notify("Network", mod.error);
            mod.pendingNetwork = null;
        }
        function onConnectedChanged() {
            if (mod.pendingNetwork && mod.pendingNetwork.connected)
                mod.pendingNetwork = null;
        }
    }

    onWiredUpChanged: mod.refreshIps()
    onWifiUpChanged: mod.refreshIps()
    Component.onCompleted: mod.refreshIps()

    tileContent: Component { NetworkTile { net: mod } }

    page: Component {
        Page {
            title: "Network"
            NetworkPage { width: parent.width; net: mod }
        }
    }
}