feat: wi-fi - expand support and fix issues, add GUI components

This commit is contained in:
2026-08-07 10:36:53 +02:00
parent bbe1df62fd
commit 9eb21eb3e3
67 changed files with 4573 additions and 245 deletions
+24
View File
@@ -224,6 +224,10 @@ namespace montauk::abi {
static constexpr uint64_t SYS_WIFI_INFO = 159; // (WifiInfo*) -> 0, -1 if absent
static constexpr uint64_t SYS_WIFI_CONNECT = 160; // (ssid, password) -> 0, <0 on error
static constexpr uint64_t SYS_WIFI_DISCONNECT = 161; // () -> 0
static constexpr uint64_t SYS_WIFI_SCAN_START = 162; // (timeoutMs) -> 0 started, 1 busy, -1 no adapter
static constexpr uint64_t SYS_WIFI_RESULTS = 163; // (WifiNetwork*, maxCount) -> count, no radio work
static constexpr uint64_t SYS_WIFI_CONNECT_ASYNC = 164; // (ssid, password) -> 0 accepted, <0 on error
static constexpr uint64_t SYS_NETIFS = 165; // (NetIfInfo*, maxCount) -> count
// Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM).
static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz
@@ -633,6 +637,26 @@ namespace montauk::abi {
uint8_t bssid[6];
uint8_t connected; // 1 once the link can carry IP traffic
uint8_t channel;
int32_t lastError; // WIFI_ERR_* from the last async join, 0 = none
uint32_t scanGeneration; // bumped every time a scan finishes
uint8_t joining; // 1 while an async join is in flight
uint8_t _pad[3];
};
// Link-layer interface kinds reported in NetIfInfo.kind.
static constexpr uint8_t NETIF_KIND_ETHERNET = 0;
static constexpr uint8_t NETIF_KIND_WIRELESS = 1;
// One registered link-layer interface (returned by SYS_NETIFS). The IP
// configuration is global to the stack, so it belongs to whichever
// interface reports active = 1.
struct NetIfInfo {
char name[16]; // "eth0", "wlan0"
uint8_t mac[6];
uint8_t kind; // NETIF_KIND_*
uint8_t linkUp;
uint8_t active; // 1 if this is the interface carrying traffic
uint8_t _pad[3];
};
struct ThermalInfo {
+35
View File
@@ -180,6 +180,41 @@ struct DesktopState {
uint64_t net_cfg_last_poll;
Rect net_icon_rect;
// Registered link-layer interfaces. The IP configuration is global to the
// stack, so the wired and wireless popups use `active` to decide which of
// them owns the address currently on screen.
static constexpr int MAX_NETIFS = 4;
montauk::abi::NetIfInfo netifs[MAX_NETIFS];
int netif_count;
bool eth_present;
// ---- Wi-Fi -------------------------------------------------------------
// The panel entry exists only when a supported adapter is present.
static constexpr int MAX_WIFI_NETWORKS = 32;
static constexpr int WIFI_SSID_CAP = 36;
static constexpr int WIFI_PSK_CAP = 72;
SvgIcon icon_wifi;
bool wifi_present;
bool wifi_popup_open;
Rect wifi_icon_rect;
montauk::abi::WifiInfo wifi_info;
montauk::abi::WifiNetwork wifi_networks[MAX_WIFI_NETWORKS];
int wifi_network_count;
uint64_t wifi_last_poll;
uint32_t wifi_scan_generation;
bool wifi_scanning;
int wifi_scroll; // first visible row of the list
bool wifi_boot_scan_started; // the automatic scan at startup
bool wifi_autoconnect_done; // saved-network join already tried
bool wifi_joining; // a join we started is in flight
bool wifi_dhcp_pending; // ask for a lease once the link is up
bool wifi_dhcp_waiting; // dhcp.elf running, no address yet
char wifi_joining_ssid[WIFI_SSID_CAP];
char wifi_status[96]; // last result line in the popup
uint64_t wifi_status_time;
bool vol_popup_open;
Rect vol_icon_rect;
int vol_level; // 0-100
+4
View File
@@ -185,6 +185,10 @@ extern "C" {
#define MTK_SYS_WIFI_INFO 159
#define MTK_SYS_WIFI_CONNECT 160
#define MTK_SYS_WIFI_DISCONNECT 161
#define MTK_SYS_WIFI_SCAN_START 162
#define MTK_SYS_WIFI_RESULTS 163
#define MTK_SYS_WIFI_CONNECT_ASYNC 164
#define MTK_SYS_NETIFS 165
/* @SYSCALLS-END */
#define MTK_SOCK_TCP 1
+29 -2
View File
@@ -607,8 +607,8 @@ namespace montauk {
inline int wifi_info(montauk::abi::WifiInfo* out) {
return (int)syscall1(montauk::abi::SYS_WIFI_INFO, (uint64_t)out);
}
// Association is groundwork only: open networks bring the firmware
// contexts up, encrypted ones are rejected with -2.
// Join a network. Blocks until the link is up or the attempt fails, and
// returns 0 or a WIFI_ERR_* code.
inline int wifi_connect(const char* ssid, const char* password) {
return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT, (uint64_t)ssid,
(uint64_t)password);
@@ -617,6 +617,33 @@ namespace montauk {
return (int)syscall0(montauk::abi::SYS_WIFI_DISCONNECT);
}
// Non-blocking pair for GUI code, which cannot stall for the seconds a
// sweep or a handshake takes. scan_start() kicks off a sweep (0 started,
// 1 one was already running, -1 no adapter); wifi_info().scanning drops
// back to 0 and .scanGeneration moves on when it finishes, and
// wifi_results() copies out the table without touching the radio.
inline int wifi_scan_start(uint32_t timeoutMs) {
return (int)syscall1(montauk::abi::SYS_WIFI_SCAN_START, (uint64_t)timeoutMs);
}
inline int wifi_results(montauk::abi::WifiNetwork* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_WIFI_RESULTS, (uint64_t)buf,
(uint64_t)maxCount);
}
// Returns 0 once the join is under way, or a WIFI_ERR_* it failed on
// before any frame went out. Watch wifi_info().joining for progress and
// .lastError for the outcome.
inline int wifi_connect_async(const char* ssid, const char* password) {
return (int)syscall2(montauk::abi::SYS_WIFI_CONNECT_ASYNC, (uint64_t)ssid,
(uint64_t)password);
}
// List the registered link-layer interfaces. The IP configuration is
// global to the stack; it belongs to whichever entry has active = 1.
inline int net_interfaces(montauk::abi::NetIfInfo* buf, int maxCount) {
return (int)syscall2(montauk::abi::SYS_NETIFS, (uint64_t)buf,
(uint64_t)maxCount);
}
// Software-defined radio (Rx). Receivers are identified by index [0, count);
// open() returns a handle used by the rest of the calls. Samples are read
// as interleaved 8-bit unsigned I/Q (CU8) from the device's ring buffer.
+261
View File
@@ -0,0 +1,261 @@
/*
* wifi.h
* Saved Wi-Fi networks (0:/config/wifi.toml) and small formatting helpers
* shared by the desktop panel, the Network app and the wifi command.
*
* The file looks like this:
*
* [wifi]
* autoconnect = true
*
* [network.0]
* ssid = "Home"
* psk = "passphrase"
*
* The passphrase is stored as typed, because that is what the join needs:
* the kernel derives the PMK from it (or takes a 64-character hex string as
* a raw PSK). Anyone who can read 0:/config can read the keys.
*
* No default copy of this file ships in the image, deliberately. Every other
* config an app writes (bluetooth.toml, display.toml, session.toml) is
* created on demand for the same reason: a shipped copy is laid down again
* by anything that refreshes the system files, and it would overwrite the
* networks the user had saved.
*
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/config.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
namespace montauk {
namespace wifi {
// The scan table the kernel keeps is 64 entries; saving that many networks
// is already far more than a laptop accumulates.
static constexpr int MAX_SAVED = 32;
static constexpr int SSID_CAP = 36;
static constexpr int PSK_CAP = 72; // 64-character hex PSK plus NUL
struct SavedNetwork {
char ssid[SSID_CAP];
char psk[PSK_CAP];
};
struct SavedList {
SavedNetwork items[MAX_SAVED];
int count;
bool autoconnect;
};
// ---- helpers -----------------------------------------------------------
inline void copy_str(char* dst, int cap, const char* src) {
int i = 0;
for (; src && src[i] && i < cap - 1; i++) dst[i] = src[i];
dst[i] = '\0';
}
// "network.<index>.<field>"
inline void network_key(char* out, int cap, int index, const char* field) {
char idx[8];
int n = 0;
if (index == 0) {
idx[n++] = '0';
} else {
char tmp[8];
int t = 0;
for (int v = index; v > 0 && t < (int)sizeof(tmp); v /= 10)
tmp[t++] = (char)('0' + (v % 10));
while (t > 0) idx[n++] = tmp[--t];
}
idx[n] = '\0';
int p = 0;
const char* prefix = "network.";
while (*prefix && p < cap - 1) out[p++] = *prefix++;
for (int i = 0; i < n && p < cap - 1; i++) out[p++] = idx[i];
if (p < cap - 1) out[p++] = '.';
while (field && *field && p < cap - 1) out[p++] = *field++;
out[p] = '\0';
}
// ---- load / store ------------------------------------------------------
inline void saved_load(SavedList* out) {
if (!out) return;
out->count = 0;
out->autoconnect = true;
auto doc = montauk::config::load("wifi");
out->autoconnect = doc.get_bool("wifi.autoconnect", true);
for (int i = 0; i < MAX_SAVED; i++) {
char key[64];
network_key(key, sizeof(key), i, "ssid");
const char* ssid = doc.get_string(key, nullptr);
if (!ssid || !ssid[0]) break; // entries are written contiguously
network_key(key, sizeof(key), i, "psk");
const char* psk = doc.get_string(key, "");
SavedNetwork& n = out->items[out->count++];
copy_str(n.ssid, SSID_CAP, ssid);
copy_str(n.psk, PSK_CAP, psk);
}
doc.destroy();
}
// Rewrite the file from the list. Returns 0 on success.
inline int saved_store(const SavedList* list) {
if (!list) return -1;
montauk::toml::Doc doc;
doc.init();
montauk::config::set_bool(&doc, "wifi.autoconnect", list->autoconnect);
for (int i = 0; i < list->count && i < MAX_SAVED; i++) {
char key[64];
network_key(key, sizeof(key), i, "ssid");
montauk::config::set_string(&doc, key, list->items[i].ssid);
network_key(key, sizeof(key), i, "psk");
montauk::config::set_string(&doc, key, list->items[i].psk);
}
int rc = montauk::config::save("wifi", &doc);
doc.destroy();
return rc;
}
inline int saved_index_of(const SavedList* list, const char* ssid) {
if (!list || !ssid) return -1;
for (int i = 0; i < list->count; i++) {
if (montauk::streq(list->items[i].ssid, ssid)) return i;
}
return -1;
}
inline const SavedNetwork* saved_find(const SavedList* list, const char* ssid) {
int idx = saved_index_of(list, ssid);
return idx < 0 ? nullptr : &list->items[idx];
}
// Add or update an entry in memory. Returns false when the list is full.
inline bool saved_set(SavedList* list, const char* ssid, const char* psk) {
if (!list || !ssid || !ssid[0]) return false;
int idx = saved_index_of(list, ssid);
if (idx < 0) {
if (list->count >= MAX_SAVED) return false;
idx = list->count++;
copy_str(list->items[idx].ssid, SSID_CAP, ssid);
}
copy_str(list->items[idx].psk, PSK_CAP, psk ? psk : "");
return true;
}
inline bool saved_remove(SavedList* list, const char* ssid) {
int idx = saved_index_of(list, ssid);
if (idx < 0) return false;
for (int i = idx; i < list->count - 1; i++) list->items[i] = list->items[i + 1];
list->count--;
return true;
}
// Convenience wrappers that touch the file directly.
inline bool remember(const char* ssid, const char* psk) {
SavedList list;
saved_load(&list);
if (!saved_set(&list, ssid, psk)) return false;
return saved_store(&list) == 0;
}
inline bool forget(const char* ssid) {
SavedList list;
saved_load(&list);
if (!saved_remove(&list, ssid)) return false;
return saved_store(&list) == 0;
}
// Copy the saved passphrase for `ssid` into out. False when not saved.
inline bool lookup(const char* ssid, char* out, int cap) {
SavedList list;
saved_load(&list);
const SavedNetwork* n = saved_find(&list, ssid);
if (!n) return false;
copy_str(out, cap, n->psk);
return true;
}
// ---- presentation ------------------------------------------------------
inline const char* security_name(uint8_t security) {
switch (security) {
case montauk::abi::WIFI_SEC_OPEN: return "Open";
case montauk::abi::WIFI_SEC_WEP: return "WEP";
case montauk::abi::WIFI_SEC_WPA: return "WPA";
case montauk::abi::WIFI_SEC_WPA2: return "WPA2";
case montauk::abi::WIFI_SEC_WPA3: return "WPA3";
default: return "Unknown";
}
}
inline bool needs_key(uint8_t security) {
return security != montauk::abi::WIFI_SEC_OPEN;
}
// 0-4 bars from an RSSI in dBm.
inline int signal_bars(int8_t rssi) {
if (rssi >= -55) return 4;
if (rssi >= -67) return 3;
if (rssi >= -75) return 2;
if (rssi >= -85) return 1;
return 0;
}
inline const char* state_name(uint8_t state) {
switch (state) {
case montauk::abi::WIFI_STATE_ABSENT: return "No adapter";
case montauk::abi::WIFI_STATE_DETECTED: return "Loading firmware";
case montauk::abi::WIFI_STATE_BOOTING: return "Starting";
case montauk::abi::WIFI_STATE_RUNNING: return "Ready";
case montauk::abi::WIFI_STATE_ERROR: return "Adapter error";
case montauk::abi::WIFI_STATE_RFKILL: return "Radio off";
default: return "Unknown";
}
}
// What the join is doing right now, for a progress line.
inline const char* conn_state_name(uint32_t connState) {
switch (connState) {
case montauk::abi::WIFI_CONN_IDLE: return "Not connected";
case montauk::abi::WIFI_CONN_CONTEXTS_UP: return "Preparing radio...";
case montauk::abi::WIFI_CONN_AUTHENTICATING: return "Authenticating...";
case montauk::abi::WIFI_CONN_AUTHENTICATED: return "Authenticated";
case montauk::abi::WIFI_CONN_ASSOCIATING: return "Associating...";
case montauk::abi::WIFI_CONN_ASSOCIATED: return "Associated";
case montauk::abi::WIFI_CONN_HANDSHAKING: return "Exchanging keys...";
case montauk::abi::WIFI_CONN_CONNECTED: return "Connected";
case montauk::abi::WIFI_CONN_FAILED: return "Connection failed";
default: return "";
}
}
inline const char* error_message(int err) {
switch (err) {
case 0: return "";
case montauk::abi::WIFI_ERR_NO_ADAPTER: return "No Wi-Fi adapter is ready";
case montauk::abi::WIFI_ERR_NOT_FOUND: return "That network is out of range";
case montauk::abi::WIFI_ERR_NEED_KEY: return "This network needs a password";
case montauk::abi::WIFI_ERR_UNSUPPORTED: return "This security type is not supported";
case montauk::abi::WIFI_ERR_AUTH: return "Wrong password";
case montauk::abi::WIFI_ERR_TIMEOUT: return "The network did not respond";
default: return "Could not join the network";
}
}
} // namespace wifi
} // namespace montauk
+34 -5
View File
@@ -7,7 +7,10 @@
wifi info
wifi debug
wifi connect <ssid> [passphrase]
wifi status
wifi disconnect
wifi saved
wifi forget <ssid>
.SH DESCRIPTION
Drives the Intel Wi-Fi adapter. With no arguments, runs a five
@@ -33,13 +36,24 @@
security tallies. Use this when a scan finds nothing.
connect <ssid> [passphrase]
Brings the firmware contexts (PHY, MAC, binding, station) up
for a network from the last scan. The 802.11 authentication
and association exchange is not implemented, so this does not
produce a usable link; encrypted networks are refused outright.
Joins a network from the last scan: open, or WPA2/WPA3-PSK
with CCMP or GCMP. Blocks until the link is up or the attempt
fails, and says which it was. A passphrase given here is
remembered in 0:/config/wifi.toml; given only an SSID, the
remembered one is used. Run dhcp(1) afterwards for an address.
status
The network currently joined, with signal and cipher.
disconnect
Tears those contexts back down.
Leaves the network and tears the firmware contexts down.
saved
Lists the networks in 0:/config/wifi.toml and whether the
desktop rejoins them by itself at startup.
forget <ssid>
Removes a network from 0:/config/wifi.toml.
.SH OUTPUT
SSID SIGNAL RSSI CH BAND SECURITY
@@ -59,6 +73,21 @@
yet)". The PNVM file carries regulatory data; without it the
firmware falls back to conservative built-in limits.
.SH SAVED NETWORKS
0:/config/wifi.toml holds the networks the system may rejoin
without being asked, as [network.<n>] tables of ssid and psk, and
a wifi.autoconnect flag. The desktop writes it when "Remember this
network" is ticked, and reads it after the automatic scan it runs
at startup. Passphrases are stored as typed; anyone who can read
0:/config can read them.
.SH GRAPHICAL USE
The desktop panel carries a Wi-Fi icon whenever a supported
adapter is present, separate from the Ethernet one. Its menu lists
the networks in range, asks for a passphrase when one is needed,
and shows the address once the link is up. The Network app has the
same list under its Wi-Fi tab, along with Forget.
.SH DIAGNOSTICS
no supported Wi-Fi adapter found
No matching device, or its RF type is not GF.
+1 -1
View File
@@ -56,7 +56,7 @@ LDFLAGS := \
# ---- C++ source files ----
CORE_SRCS := main.cpp window.cpp panel.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp font_data.cpp stb_truetype_impl.cpp
CORE_SRCS := main.cpp window.cpp panel.cpp wifi.cpp compose.cpp input.cpp dialogs.cpp launcher.cpp desktop_builtin.cpp desktop_catalog.cpp font_data.cpp stb_truetype_impl.cpp
APP_SRCS := $(sort $(shell find apps -name '*.cpp' -print))
SRCS := $(CORE_SRCS) $(APP_SRCS)
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
+5
View File
@@ -263,6 +263,11 @@ void gui::desktop_compose(DesktopState* ds) {
desktop_draw_net_popup(ds);
}
// Draw Wi-Fi popup if open
if (ds->wifi_popup_open) {
desktop_draw_wifi_popup(ds);
}
// Draw volume popup if open
if (ds->vol_popup_open) {
desktop_draw_vol_popup(ds);
+1
View File
@@ -66,6 +66,7 @@ void desktop_lock_screen(DesktopState* ds) {
desktop_close_launcher(ds);
ds->ctx_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
// Cache display name for lock screen rendering
+10
View File
@@ -158,12 +158,22 @@ gui::CursorStyle cursor_for_edge(gui::ResizeEdge edge);
void desktop_draw_app_menu(gui::DesktopState* ds);
void desktop_draw_net_popup(gui::DesktopState* ds);
void desktop_draw_vol_popup(gui::DesktopState* ds);
const montauk::abi::NetIfInfo* desktop_eth_iface(const gui::DesktopState* ds);
bool desktop_eth_online(const gui::DesktopState* ds);
// wifi.cpp
void desktop_wifi_init(gui::DesktopState* ds);
bool desktop_wifi_poll(gui::DesktopState* ds, uint64_t now);
void desktop_draw_wifi_popup(gui::DesktopState* ds);
bool desktop_wifi_handle_mouse(gui::DesktopState* ds, int mx, int my,
bool left_pressed, int scroll);
// compose.cpp
void desktop_draw_lock_screen(gui::DesktopState* ds);
void desktop_mark_background_dirty(gui::DesktopState* ds);
// main.cpp
void desktop_refresh_netifs(gui::DesktopState* ds);
void desktop_scan_apps(gui::DesktopState* ds);
void desktop_build_menu(gui::DesktopState* ds);
void desktop_open_launcher(gui::DesktopState* ds);
+23
View File
@@ -166,6 +166,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->ctx_menu_open = false;
ds->vol_popup_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
return;
}
@@ -507,6 +508,13 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
}
}
// Wi-Fi popup. The passphrase dialog is an ordinary window and needs
// nothing here.
if (ds->wifi_popup_open) {
if (desktop_wifi_handle_mouse(ds, mx, my, left_pressed, ev.scroll))
return;
}
// Handle net popup clicks
if (ds->net_popup_open && left_pressed) {
int popup_w = 220;
@@ -522,6 +530,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
return;
} else if (!ds->net_icon_rect.contains(mx, my)) {
ds->net_popup_open = false;
ds->wifi_popup_open = false;
}
}
@@ -531,6 +540,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (mx < 36) {
ds->app_menu_open = !ds->app_menu_open;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
ds->ctx_menu_open = false;
return;
@@ -542,6 +552,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->vol_dragging = false;
ds->app_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->ctx_menu_open = false;
return;
}
@@ -551,6 +562,17 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->net_popup_open = !ds->net_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->wifi_popup_open = false;
ds->ctx_menu_open = false;
return;
}
// Wi-Fi icon
if (ds->wifi_icon_rect.w > 0 && ds->wifi_icon_rect.contains(mx, my)) {
ds->wifi_popup_open = !ds->wifi_popup_open;
ds->app_menu_open = false;
ds->vol_popup_open = false;
ds->net_popup_open = false;
ds->ctx_menu_open = false;
return;
}
@@ -775,6 +797,7 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
ds->ctx_menu_y = my;
ds->app_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
}
}
+1
View File
@@ -406,6 +406,7 @@ void desktop_open_launcher(DesktopState* ds) {
ds->app_menu_open = false;
ds->ctx_menu_open = false;
ds->net_popup_open = false;
ds->wifi_popup_open = false;
ds->vol_popup_open = false;
ds->vol_dragging = false;
desktop_update_launcher_results(ds);
+39
View File
@@ -276,6 +276,7 @@ void gui::desktop_init(DesktopState* ds) {
ds->icon_folder = svg_load("0:/icons/folder.svg", 16, 16, defColor);
ds->icon_file = svg_load("0:/icons/text-x-generic.svg", 16, 16, defColor);
ds->icon_network = svg_load("0:/icons/network-wired-symbolic.svg", 16, 16, colors::PANEL_TEXT);
ds->icon_wifi = svg_load("0:/icons/network-wireless-symbolic.svg", 16, 16, colors::PANEL_TEXT);
ds->icon_go_up = svg_load("0:/icons/go-up-symbolic.svg", 16, 16, defColor);
ds->icon_go_back = svg_load("0:/icons/go-previous-symbolic.svg", 16, 16, defColor);
ds->icon_go_forward = svg_load("0:/icons/go-next-symbolic.svg", 16, 16, defColor);
@@ -392,6 +393,9 @@ void gui::desktop_init(DesktopState* ds) {
ds->net_cfg_last_poll = montauk::get_milliseconds();
ds->net_icon_rect = {0, 0, 0, 0};
desktop_refresh_netifs(ds);
desktop_wifi_init(ds);
ds->vol_popup_open = false;
ds->vol_icon_rect = {0, 0, 0, 0};
int vol = montauk::audio_get_master_volume();
@@ -465,6 +469,30 @@ static bool desktop_netcfg_equal(const montauk::abi::NetCfg& a, const montauk::a
return true;
}
// Re-read the interface registry. Cheap, and it is the only way to tell the
// wired and wireless halves of the panel apart.
void desktop_refresh_netifs(DesktopState* ds) {
int n = montauk::net_interfaces(ds->netifs, DesktopState::MAX_NETIFS);
ds->netif_count = n > 0 ? n : 0;
ds->eth_present = false;
for (int i = 0; i < ds->netif_count; i++) {
if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_ETHERNET)
ds->eth_present = true;
}
}
static bool desktop_netifs_equal(const montauk::abi::NetIfInfo* a, int an,
const montauk::abi::NetIfInfo* b, int bn) {
if (an != bn) return false;
for (int i = 0; i < an; i++) {
if (a[i].linkUp != b[i].linkUp || a[i].active != b[i].active ||
a[i].kind != b[i].kind) {
return false;
}
}
return true;
}
static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
if (ds->screen_locked) return false;
@@ -476,9 +504,20 @@ static bool desktop_refresh_panel_state(DesktopState* ds, uint64_t now) {
ds->cached_net_cfg = next;
changed = true;
}
montauk::abi::NetIfInfo ifaces[DesktopState::MAX_NETIFS];
int count = montauk::net_interfaces(ifaces, DesktopState::MAX_NETIFS);
if (count < 0) count = 0;
if (!desktop_netifs_equal(ifaces, count, ds->netifs, ds->netif_count)) {
desktop_refresh_netifs(ds);
changed = true;
}
ds->net_cfg_last_poll = now;
}
if (desktop_wifi_poll(ds, now)) changed = true;
// Event-driven sync: the kernel mixer bumps a serial on every state
// change. Reading it is one cheap syscall; refresh only when the serial
// moved since we last looked. The audio app (and any other client) will
+89 -33
View File
@@ -141,25 +141,48 @@ void gui::desktop_draw_panel(DesktopState* ds) {
}
}
// Network icon (to the left of the volume icon)
int net_icon_x = vol_icon_x - 16 - 10;
int net_icon_y = (PANEL_HEIGHT - 16) / 2;
ds->net_icon_rect = {net_icon_x, net_icon_y, 16, 16};
// Status icons fill in leftwards from the volume icon. Ethernet only
// appears when a wired interface is registered, Wi-Fi only when a
// supported adapter is present, so a machine without either shows neither.
int icon_y = (PANEL_HEIGHT - 16) / 2;
int next_icon_x = vol_icon_x - 16 - 10;
if (ds->icon_network.pixels) {
if (ds->cached_net_cfg.ipAddress == 0) {
uint32_t* src = ds->icon_network.pixels;
int npx = 16 * 16;
uint32_t tinted[256];
for (int p = 0; p < npx; p++) {
uint32_t px = src[p];
uint8_t a = (px >> 24) & 0xFF;
tinted[p] = ((uint32_t)a << 24) | 0x004444CC;
if (ds->eth_present) {
ds->net_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10;
if (ds->icon_network.pixels) {
// Tinted while the cable is doing nothing for us: no link, or a
// link that is not the one carrying traffic.
if (!desktop_eth_online(ds)) {
uint32_t* src = ds->icon_network.pixels;
uint32_t tinted[256];
for (int p = 0; p < 16 * 16; p++) {
uint8_t a = (src[p] >> 24) & 0xFF;
tinted[p] = ((uint32_t)a << 24) | 0x004444CC;
}
fb.blit_alpha(ds->net_icon_rect.x, icon_y, 16, 16, tinted);
} else {
fb.blit_alpha(ds->net_icon_rect.x, icon_y, ds->icon_network.width,
ds->icon_network.height, ds->icon_network.pixels);
}
fb.blit_alpha(net_icon_x, net_icon_y, 16, 16, tinted);
} else {
fb.blit_alpha(net_icon_x, net_icon_y, ds->icon_network.width, ds->icon_network.height, ds->icon_network.pixels);
}
} else {
ds->net_icon_rect = {0, 0, 0, 0};
}
// Wi-Fi stays white whatever the radio is doing: the popup carries the
// state, and a colour-shifting icon next to the clock is just noise.
if (ds->wifi_present) {
ds->wifi_icon_rect = {next_icon_x, icon_y, 16, 16};
next_icon_x -= 16 + 10;
if (ds->icon_wifi.pixels) {
fb.blit_alpha(ds->wifi_icon_rect.x, icon_y, ds->icon_wifi.width,
ds->icon_wifi.height, ds->icon_wifi.pixels);
}
} else {
ds->wifi_icon_rect = {0, 0, 0, 0};
}
}
@@ -265,16 +288,38 @@ void desktop_draw_app_menu(DesktopState* ds) {
// Network Popup
// ============================================================================
// The wired interface, if one is registered.
const montauk::abi::NetIfInfo* desktop_eth_iface(const DesktopState* ds) {
for (int i = 0; i < ds->netif_count; i++) {
if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_ETHERNET)
return &ds->netifs[i];
}
return nullptr;
}
// "Online" for the panel icon means the cable is both up and the interface the
// stack is actually sending through, with an address to send from.
bool desktop_eth_online(const DesktopState* ds) {
const montauk::abi::NetIfInfo* eth = desktop_eth_iface(ds);
return eth && eth->linkUp && eth->active && ds->cached_net_cfg.ipAddress != 0;
}
void desktop_draw_net_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
montauk::abi::NetCfg& nc = ds->cached_net_cfg;
bool connected = nc.ipAddress != 0;
const montauk::abi::NetIfInfo* eth = desktop_eth_iface(ds);
bool link_up = eth && eth->linkUp;
// The IP configuration is global to the stack, so it is only this
// interface's address while this interface is the active one.
bool owns_address = eth && eth->active && nc.ipAddress != 0;
bool connected = link_up && owns_address;
int popup_w = 220;
int fh = system_font_height();
int row_h = fh + 8;
int header_h = row_h + 8; // title + status row + padding
int body_rows = 5; // IP, Subnet, Gateway, DNS, MAC
int body_rows = 6; // Interface, IP, Subnet, Gateway, DNS, MAC
int popup_h = header_h + row_h * body_rows + 12;
int popup_x = ds->net_icon_rect.x + ds->net_icon_rect.w - popup_w;
int popup_y = PANEL_HEIGHT + 2;
@@ -290,11 +335,14 @@ void desktop_draw_net_popup(DesktopState* ds) {
// Header: "Ethernet" + status dot
draw_text(fb, lx, ty, "Ethernet", colors::TEXT_COLOR);
// Status dot + label (right-aligned in header)
// Status dot + label (right-aligned in header). A cable that is plugged
// in but idle (Wi-Fi is carrying the traffic) is not the same as unplugged.
Color dot_color = connected
? Color::from_rgb(0x4C, 0xAF, 0x50) // green
: Color::from_rgb(0xCC, 0x33, 0x33); // red
const char* status_str = connected ? "Connected" : "Disconnected";
: (link_up ? Color::from_rgb(0xD0, 0x9A, 0x2E) // amber
: Color::from_rgb(0xCC, 0x33, 0x33)); // red
const char* status_str = connected ? "Connected"
: (link_up ? "Link up" : "Disconnected");
int sw = text_width(status_str);
int dot_r = 4;
int status_x = popup_x + popup_w - 14 - sw;
@@ -318,23 +366,31 @@ void desktop_draw_net_popup(DesktopState* ds) {
int val_x = popup_x + 76; // fixed column for values
struct NetRow { const char* label; char value[24]; };
NetRow rows[5];
NetRow rows[6];
rows[0].label = "IP";
if (connected) format_ip(rows[0].value, nc.ipAddress);
else montauk::strcpy(rows[0].value, "0.0.0.0");
rows[0].label = "Interface";
montauk::strncpy(rows[0].value, eth ? eth->name : "None", sizeof(rows[0].value));
rows[1].label = "Subnet";
format_ip(rows[1].value, nc.subnetMask);
// Addresses are shown only when this interface owns them; otherwise the
// wireless popup is where they belong.
rows[1].label = "IP";
if (owns_address) format_ip(rows[1].value, nc.ipAddress);
else montauk::strcpy(rows[1].value, link_up ? "Not in use" : "0.0.0.0");
rows[2].label = "Gateway";
format_ip(rows[2].value, nc.gateway);
rows[2].label = "Subnet";
if (owns_address) format_ip(rows[2].value, nc.subnetMask);
else montauk::strcpy(rows[2].value, "-");
rows[3].label = "DNS";
format_ip(rows[3].value, nc.dnsServer);
rows[3].label = "Gateway";
if (owns_address) format_ip(rows[3].value, nc.gateway);
else montauk::strcpy(rows[3].value, "-");
rows[4].label = "MAC";
format_mac(rows[4].value, nc.macAddress);
rows[4].label = "DNS";
if (owns_address) format_ip(rows[4].value, nc.dnsServer);
else montauk::strcpy(rows[4].value, "-");
rows[5].label = "MAC";
format_mac(rows[5].value, eth ? eth->mac : nc.macAddress);
for (int i = 0; i < body_rows; i++) {
draw_text(fb, lx, ty, rows[i].label, dim);
+867
View File
@@ -0,0 +1,867 @@
/*
* wifi.cpp
* Wi-Fi panel entry: the network list popup, the passphrase dialog window,
* and the automatic scan and saved-network join that run at startup.
*
* Everything here goes through the non-blocking Wi-Fi syscalls
* (wifi_scan_start / wifi_results / wifi_connect_async). The blocking pair
* takes seconds, and the compositor cannot stand still for that long.
*
* Copyright (c) 2026 Daniel Hammer
*/
#include "desktop_internal.hpp"
#include <montauk/wifi.h>
#include <gui/mtk.hpp>
using namespace gui;
// ============================================================================
// Geometry
// ============================================================================
static constexpr int WIFI_POPUP_W = 264;
static constexpr int WIFI_ROW_H = 30;
static constexpr int WIFI_VISIBLE = 6; // network rows on screen at once
static constexpr int WIFI_BTN_H = 26;
static int wifi_popup_x(const DesktopState* ds) {
int x = ds->wifi_icon_rect.x + ds->wifi_icon_rect.w - WIFI_POPUP_W;
return x < 4 ? 4 : x;
}
static int wifi_visible_rows(const DesktopState* ds) {
int rows = ds->wifi_network_count;
if (rows > WIFI_VISIBLE) rows = WIFI_VISIBLE;
if (rows < 1) rows = 1; // the "no networks" line
return rows;
}
static int wifi_popup_h(const DesktopState* ds) {
int fh = system_font_height();
int header = fh + 10; // title + status
int detail = ds->wifi_info.connected ? (fh + 6) * 3 + 8 : 0;
int list = wifi_visible_rows(ds) * WIFI_ROW_H;
int footer = WIFI_BTN_H + 14 + fh + 6;
return header + 10 + detail + list + 10 + footer;
}
static Rect wifi_popup_rect(const DesktopState* ds) {
return {wifi_popup_x(ds), PANEL_HEIGHT + 2, WIFI_POPUP_W, wifi_popup_h(ds)};
}
// y of the first network row
static int wifi_list_y(const DesktopState* ds) {
int fh = system_font_height();
int y = PANEL_HEIGHT + 2 + 10 + fh + 10;
if (ds->wifi_info.connected) y += (fh + 6) * 3 + 8;
return y;
}
static Rect wifi_scan_button(const DesktopState* ds) {
Rect popup = wifi_popup_rect(ds);
int fh = system_font_height();
int y = popup.y + popup.h - 10 - fh - 6 - WIFI_BTN_H;
return {popup.x + 12, y, 76, WIFI_BTN_H};
}
static Rect wifi_action_button(const DesktopState* ds) {
Rect scan = wifi_scan_button(ds);
Rect popup = wifi_popup_rect(ds);
int w = 96;
return {popup.x + popup.w - 12 - w, scan.y, w, WIFI_BTN_H};
}
// ============================================================================
// State helpers
// ============================================================================
// The saved-network list lives here rather than on the stack: it is several
// kilobytes, and the compositor's stack is 32 KiB shared with the TrueType
// rasteriser. One copy also saves re-parsing the file on every click.
static montauk::wifi::SavedList g_saved;
static void wifi_reload_saved() {
montauk::wifi::saved_load(&g_saved);
}
static const char* wifi_saved_psk(const char* ssid) {
const montauk::wifi::SavedNetwork* entry =
montauk::wifi::saved_find(&g_saved, ssid);
return (entry && entry->psk[0]) ? entry->psk : nullptr;
}
static void wifi_set_status(DesktopState* ds, const char* msg) {
montauk::strncpy(ds->wifi_status, msg ? msg : "", sizeof(ds->wifi_status));
ds->wifi_status_time = montauk::get_milliseconds();
}
// Sorted strongest-first so the list reads the way a user expects.
static void wifi_sort_results(DesktopState* ds) {
for (int i = 1; i < ds->wifi_network_count; i++) {
montauk::abi::WifiNetwork key = ds->wifi_networks[i];
int j = i - 1;
while (j >= 0 && ds->wifi_networks[j].rssi < key.rssi) {
ds->wifi_networks[j + 1] = ds->wifi_networks[j];
j--;
}
ds->wifi_networks[j + 1] = key;
}
}
static void wifi_refresh_results(DesktopState* ds) {
montauk::abi::WifiNetwork raw[DesktopState::MAX_WIFI_NETWORKS];
int n = montauk::wifi_results(raw, DesktopState::MAX_WIFI_NETWORKS);
if (n < 0) n = 0;
// One row per network rather than one per access point: a house with a
// repeater answers on several BSSIDs under the same name, and the strongest
// is the one worth joining. Hidden networks have no name to join by, so
// they are left out of the list entirely.
ds->wifi_network_count = 0;
for (int i = 0; i < n; i++) {
if (!raw[i].ssid[0]) continue;
int existing = -1;
for (int k = 0; k < ds->wifi_network_count; k++) {
if (montauk::streq(ds->wifi_networks[k].ssid, raw[i].ssid)) {
existing = k;
break;
}
}
if (existing >= 0) {
if (raw[i].rssi > ds->wifi_networks[existing].rssi)
ds->wifi_networks[existing] = raw[i];
continue;
}
ds->wifi_networks[ds->wifi_network_count++] = raw[i];
}
wifi_sort_results(ds);
if (ds->wifi_scroll > ds->wifi_network_count - 1) ds->wifi_scroll = 0;
}
static void wifi_start_scan(DesktopState* ds) {
int rc = montauk::wifi_scan_start(6000);
if (rc < 0) {
wifi_set_status(ds, "The adapter is not ready yet");
return;
}
ds->wifi_scanning = true;
// No status line here: the button reads "Scanning" and the list says so
// too, and three copies of the same word is not progress reporting.
ds->wifi_status[0] = '\0';
}
static void wifi_begin_join(DesktopState* ds, const char* ssid, const char* psk) {
int rc = montauk::wifi_connect_async(ssid, psk);
if (rc < 0) {
wifi_set_status(ds, montauk::wifi::error_message(rc));
ds->wifi_joining = false;
return;
}
ds->wifi_joining = true;
ds->wifi_dhcp_pending = true;
montauk::strncpy(ds->wifi_joining_ssid, ssid, sizeof(ds->wifi_joining_ssid));
char msg[96];
snprintf(msg, sizeof(msg), "Connecting to %s...", ssid);
wifi_set_status(ds, msg);
}
// Defined below: the passphrase dialog is a real desktop window, created the
// same way the reboot and shutdown dialogs are.
static void wifi_open_password_dialog(DesktopState* ds,
const montauk::abi::WifiNetwork& net);
// Join the network under the cursor: straight away when it is open or its
// passphrase is already saved, otherwise ask for the key.
static void wifi_select_network(DesktopState* ds, int index) {
if (index < 0 || index >= ds->wifi_network_count) return;
const montauk::abi::WifiNetwork& net = ds->wifi_networks[index];
if (!montauk::wifi::needs_key(net.security)) {
wifi_begin_join(ds, net.ssid, "");
return;
}
const char* saved = wifi_saved_psk(net.ssid);
if (saved) {
wifi_begin_join(ds, net.ssid, saved);
return;
}
wifi_open_password_dialog(ds, net);
}
// ============================================================================
// Startup and polling
// ============================================================================
void desktop_wifi_init(DesktopState* ds) {
ds->wifi_popup_open = false;
ds->wifi_icon_rect = {0, 0, 0, 0};
ds->wifi_network_count = 0;
ds->wifi_scroll = 0;
ds->wifi_scanning = false;
ds->wifi_boot_scan_started = false;
ds->wifi_autoconnect_done = false;
ds->wifi_joining = false;
ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = false;
ds->wifi_joining_ssid[0] = '\0';
ds->wifi_status[0] = '\0';
ds->wifi_status_time = 0;
ds->wifi_scan_generation = 0;
ds->wifi_last_poll = 0;
wifi_reload_saved();
montauk::memset(&ds->wifi_info, 0, sizeof(ds->wifi_info));
ds->wifi_present = montauk::wifi_info(&ds->wifi_info) == 0 && ds->wifi_info.present;
}
// Try the strongest saved network that is actually in range.
static void wifi_try_autoconnect(DesktopState* ds) {
if (!g_saved.autoconnect || g_saved.count == 0) {
ds->wifi_autoconnect_done = true;
return;
}
for (int i = 0; i < ds->wifi_network_count; i++) { // strongest first
const montauk::abi::WifiNetwork& net = ds->wifi_networks[i];
const montauk::wifi::SavedNetwork* entry =
montauk::wifi::saved_find(&g_saved, net.ssid);
if (!entry) continue;
wifi_begin_join(ds, entry->ssid, entry->psk);
break;
}
ds->wifi_autoconnect_done = true;
}
// Called from the panel refresh. Returns true when something on screen moved.
bool desktop_wifi_poll(DesktopState* ds, uint64_t now) {
// The Network app writes the same file, so pick up its edits whenever the
// popup is opened rather than trusting the copy read at startup.
static bool popup_was_open = false;
if (ds->wifi_popup_open && !popup_was_open) wifi_reload_saved();
popup_was_open = ds->wifi_popup_open;
// The adapter finishes its firmware load well after login, so keep looking
// for it until it shows up rather than deciding once at startup.
if (!ds->wifi_present) {
if (now - ds->wifi_last_poll < 3000) return false;
ds->wifi_last_poll = now;
montauk::abi::WifiInfo info;
if (montauk::wifi_info(&info) != 0 || !info.present) return false;
ds->wifi_info = info;
ds->wifi_present = true;
return true; // the icon appears now
}
// Poll faster while something is in flight so progress text keeps up.
bool busy = ds->wifi_scanning || ds->wifi_joining || ds->wifi_popup_open;
if (now - ds->wifi_last_poll < (busy ? 400u : 3000u)) return false;
ds->wifi_last_poll = now;
montauk::abi::WifiInfo info;
if (montauk::wifi_info(&info) != 0) return false;
bool changed = info.connected != ds->wifi_info.connected
|| info.connState != ds->wifi_info.connState
|| info.scanning != ds->wifi_info.scanning
|| info.scanGeneration != ds->wifi_info.scanGeneration
|| info.state != ds->wifi_info.state;
ds->wifi_info = info;
// The first scan runs by itself once the firmware is up, so the list is
// already populated the first time the user opens the popup.
if (!ds->wifi_boot_scan_started && info.state == montauk::abi::WIFI_STATE_RUNNING) {
ds->wifi_boot_scan_started = true;
wifi_start_scan(ds);
return true;
}
if (info.scanGeneration != ds->wifi_scan_generation) {
ds->wifi_scan_generation = info.scanGeneration;
ds->wifi_scanning = false;
wifi_refresh_results(ds);
changed = true;
char msg[64];
if (ds->wifi_network_count > 0) {
snprintf(msg, sizeof(msg), "Found %d network%s",
ds->wifi_network_count, ds->wifi_network_count == 1 ? "" : "s");
} else {
snprintf(msg, sizeof(msg), "No networks found");
}
wifi_set_status(ds, msg);
// Rejoin whatever was saved as soon as the first sweep lands.
if (!ds->wifi_autoconnect_done && !info.connected && !ds->wifi_joining) {
wifi_reload_saved();
wifi_try_autoconnect(ds);
}
} else if (ds->wifi_scanning && !info.scanning) {
ds->wifi_scanning = false;
changed = true;
}
if (ds->wifi_joining) {
if (info.connected) {
ds->wifi_joining = false;
char msg[96];
snprintf(msg, sizeof(msg), "Connected to %s", info.ssid);
wifi_set_status(ds, msg);
changed = true;
} else if (info.lastError != 0 && !info.joining) {
ds->wifi_joining = false;
ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = false;
wifi_set_status(ds, montauk::wifi::error_message(info.lastError));
changed = true;
}
}
// A wireless link with no address of its own needs a lease; the wired
// interface, if there is one, keeps whatever it already had.
if (ds->wifi_dhcp_pending && info.connected && ds->cached_net_cfg.ipAddress == 0) {
ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = true;
montauk::spawn("0:/os/dhcp.elf");
wifi_set_status(ds, "Requesting an address...");
changed = true;
} else if (ds->wifi_dhcp_pending && info.connected) {
ds->wifi_dhcp_pending = false;
}
// "Requesting an address..." is only true until the lease lands. Leaving it
// up afterwards contradicts the address row further up the popup, so it is
// replaced the moment there is an address (or the link goes away).
if (ds->wifi_dhcp_waiting && (ds->cached_net_cfg.ipAddress != 0 || !info.connected)) {
bool leased = ds->cached_net_cfg.ipAddress != 0;
ds->wifi_dhcp_waiting = false;
if (leased) {
char msg[96];
snprintf(msg, sizeof(msg), "Connected to %s", info.ssid);
wifi_set_status(ds, msg);
} else {
ds->wifi_status[0] = '\0';
}
changed = true;
}
return changed;
}
// ============================================================================
// Drawing
// ============================================================================
static void draw_signal_bars(Framebuffer& fb, int x, int y, int8_t rssi, Color on, Color off) {
int bars = montauk::wifi::signal_bars(rssi);
for (int i = 0; i < 4; i++) {
int h = 3 + i * 3;
fb.fill_rect(x + i * 4, y + 12 - h, 3, h, i < bars ? on : off);
}
}
// A small padlock, so encrypted networks read at a glance.
static void draw_lock(Framebuffer& fb, int x, int y, Color c) {
fb.fill_rect(x + 1, y + 5, 8, 6, c);
fb.fill_rect(x + 2, y + 2, 1, 3, c);
fb.fill_rect(x + 7, y + 2, 1, 3, c);
fb.fill_rect(x + 3, y + 1, 4, 1, c);
}
static void draw_kv_row(Framebuffer& fb, int lx, int vx, int y,
const char* label, const char* value, Color dim, Color text) {
draw_text(fb, lx, y, label, dim);
draw_text(fb, vx, y, value, text);
}
void desktop_draw_wifi_popup(DesktopState* ds) {
Framebuffer& fb = ds->fb;
Rect popup = wifi_popup_rect(ds);
int fh = system_font_height();
Color dim = Color::from_rgb(0x66, 0x66, 0x66);
Color faint = Color::from_rgb(0xCC, 0xCC, 0xCC);
draw_shadow(fb, popup.x, popup.y, popup.w, popup.h, 4, colors::SHADOW);
fill_rounded_rect(fb, popup.x, popup.y, popup.w, popup.h, 8, colors::MENU_BG);
draw_rect(fb, popup.x, popup.y, popup.w, popup.h, colors::BORDER);
int lx = popup.x + 14;
int ty = popup.y + 10;
// Header: "Wi-Fi" + connection state
draw_text(fb, lx, ty, "Wi-Fi", colors::TEXT_COLOR);
const char* status_str;
if (ds->wifi_info.connected) status_str = "Connected";
else if (ds->wifi_joining) status_str = "Connecting";
else if (ds->wifi_info.state == montauk::abi::WIFI_STATE_RFKILL) status_str = "Radio off";
else status_str = "Not connected";
Color dot_color = ds->wifi_info.connected
? Color::from_rgb(0x4C, 0xAF, 0x50)
: (ds->wifi_joining ? Color::from_rgb(0xD0, 0x9A, 0x2E)
: Color::from_rgb(0xCC, 0x33, 0x33));
int sw = text_width(status_str);
int status_x = popup.x + popup.w - 14 - sw;
fill_circle(fb, status_x - 9, ty + fh / 2, 4, dot_color);
draw_text(fb, status_x, ty, status_str, dim);
ty += fh + 10;
// What this interface has, when it has it. The address belongs to the
// wireless interface only while that is the one carrying traffic.
if (ds->wifi_info.connected) {
int vx = popup.x + 76;
char value[40];
montauk::strncpy(value, ds->wifi_info.ssid[0] ? ds->wifi_info.ssid : "-", sizeof(value));
draw_kv_row(fb, lx, vx, ty, "Network", value, dim, colors::TEXT_COLOR);
ty += fh + 6;
bool wireless_active = false;
for (int i = 0; i < ds->netif_count; i++) {
if (ds->netifs[i].kind == montauk::abi::NETIF_KIND_WIRELESS && ds->netifs[i].active)
wireless_active = true;
}
if (wireless_active && ds->cached_net_cfg.ipAddress != 0)
format_ip(value, ds->cached_net_cfg.ipAddress);
else
montauk::strcpy(value, "No address");
draw_kv_row(fb, lx, vx, ty, "IP", value, dim, colors::TEXT_COLOR);
ty += fh + 6;
snprintf(value, sizeof(value), "Channel %d", (int)ds->wifi_info.channel);
draw_kv_row(fb, lx, vx, ty, "Radio", value, dim, colors::TEXT_COLOR);
ty += fh + 6 + 8;
}
// Network list
int list_y = wifi_list_y(ds);
int mx = ds->mouse.x, my = ds->mouse.y;
if (ds->wifi_network_count == 0) {
const char* empty = ds->wifi_scanning ? "Looking for networks..."
: "No networks found";
draw_text(fb, lx, list_y + (WIFI_ROW_H - fh) / 2, empty, dim);
}
int rows = wifi_visible_rows(ds);
for (int r = 0; r < rows && (ds->wifi_scroll + r) < ds->wifi_network_count; r++) {
int idx = ds->wifi_scroll + r;
const montauk::abi::WifiNetwork& net = ds->wifi_networks[idx];
Rect row = {popup.x + 6, list_y + r * WIFI_ROW_H, popup.w - 12, WIFI_ROW_H};
bool current = ds->wifi_info.connected
&& montauk::streq(net.ssid, ds->wifi_info.ssid);
if (row.contains(mx, my))
fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 4,
gui::mtk::accent_hover_tint(ds->settings.accent_color));
else if (current)
fill_rounded_rect(fb, row.x, row.y, row.w, row.h, 4,
Color::from_rgb(0xEC, 0xF2, 0xFB));
draw_signal_bars(fb, row.x + 8, row.y + (WIFI_ROW_H - 12) / 2, net.rssi,
current ? ds->settings.accent_color : Color::from_rgb(0x55, 0x55, 0x55),
faint);
int text_x = row.x + 32;
int text_y = row.y + (WIFI_ROW_H - fh) / 2;
int text_max = row.w - 32 - 26;
char label[40];
montauk::strncpy(label, net.ssid[0] ? net.ssid : "(hidden)", sizeof(label));
while (label[0] && text_width(label) > text_max) {
int n = montauk::slen(label);
label[n - 1] = '\0';
}
draw_text(fb, text_x, text_y, label, colors::TEXT_COLOR);
if (montauk::wifi::needs_key(net.security))
draw_lock(fb, row.x + row.w - 20, row.y + (WIFI_ROW_H - 12) / 2, dim);
}
// A slim scrollbar rather than a line of text: the list scrolls with the
// wheel, and there is no room under it for a hint.
if (ds->wifi_network_count > WIFI_VISIBLE) {
int track_x = popup.x + popup.w - 8;
int track_y = list_y + 2;
int track_h = rows * WIFI_ROW_H - 4;
fb.fill_rect(track_x, track_y, 3, track_h, faint);
int thumb_h = track_h * WIFI_VISIBLE / ds->wifi_network_count;
if (thumb_h < 12) thumb_h = 12;
int span = ds->wifi_network_count - WIFI_VISIBLE;
int thumb_y = track_y + (span > 0 ? (track_h - thumb_h) * ds->wifi_scroll / span : 0);
fb.fill_rect(track_x, thumb_y, 3, thumb_h, Color::from_rgb(0x99, 0x99, 0x99));
}
// Footer: scan / disconnect and the last status line
Rect scan = wifi_scan_button(ds);
Rect action = wifi_action_button(ds);
auto draw_btn = [&](const Rect& r, const char* label, bool primary) {
Color bg = primary ? ds->settings.accent_color : Color::from_rgb(0xE0, 0xE0, 0xE0);
if (r.contains(mx, my))
bg = primary ? gui::mtk::darken(ds->settings.accent_color, 32)
: Color::from_rgb(0xD0, 0xD0, 0xD0);
fill_rounded_rect(fb, r.x, r.y, r.w, r.h, 6, bg);
int tw = text_width(label);
draw_text(fb, r.x + (r.w - tw) / 2, r.y + (r.h - fh) / 2, label,
primary ? colors::WHITE : colors::TEXT_COLOR);
};
draw_btn(scan, ds->wifi_scanning ? "Scanning" : "Scan", false);
draw_btn(action, ds->wifi_info.connected ? "Disconnect" : "Rescan",
ds->wifi_info.connected);
// A result worth reading now, not one left over from ten minutes ago.
bool status_fresh = ds->wifi_status[0]
&& montauk::get_milliseconds() - ds->wifi_status_time < 20000;
const char* line = status_fresh ? ds->wifi_status : "";
if (ds->wifi_joining && ds->wifi_info.connState != montauk::abi::WIFI_CONN_IDLE)
line = montauk::wifi::conn_state_name(ds->wifi_info.connState);
if (line && line[0]) {
char fitted[64];
montauk::strncpy(fitted, line, sizeof(fitted));
while (fitted[0] && text_width(fitted) > popup.w - 28) {
int n = montauk::slen(fitted);
fitted[n - 1] = '\0';
}
draw_text(fb, lx, scan.y + scan.h + 8, fitted, dim);
}
}
// ============================================================================
// Input
// ============================================================================
// Returns true when the click was consumed.
bool desktop_wifi_handle_mouse(DesktopState* ds, int mx, int my,
bool left_pressed, int scroll) {
if (!ds->wifi_popup_open) return false;
Rect popup = wifi_popup_rect(ds);
if (scroll != 0 && popup.contains(mx, my)) {
int max_scroll = ds->wifi_network_count - WIFI_VISIBLE;
if (max_scroll < 0) max_scroll = 0;
ds->wifi_scroll -= scroll;
if (ds->wifi_scroll < 0) ds->wifi_scroll = 0;
if (ds->wifi_scroll > max_scroll) ds->wifi_scroll = max_scroll;
return true;
}
if (!left_pressed) return false;
if (!popup.contains(mx, my)) {
if (!ds->wifi_icon_rect.contains(mx, my)) ds->wifi_popup_open = false;
return false;
}
if (wifi_scan_button(ds).contains(mx, my)) {
if (!ds->wifi_scanning) wifi_start_scan(ds);
return true;
}
if (wifi_action_button(ds).contains(mx, my)) {
if (ds->wifi_info.connected) {
montauk::wifi_disconnect();
ds->wifi_joining = false;
ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = false;
wifi_set_status(ds, "Disconnected");
} else if (!ds->wifi_scanning) {
wifi_start_scan(ds);
}
return true;
}
int list_y = wifi_list_y(ds);
if (my >= list_y && my < list_y + wifi_visible_rows(ds) * WIFI_ROW_H) {
int row = (my - list_y) / WIFI_ROW_H;
wifi_select_network(ds, ds->wifi_scroll + row);
return true;
}
return true; // a click inside the popup never falls through
}
// ============================================================================
// Passphrase dialog
//
// A real desktop window, created and driven exactly like the reboot and
// shutdown dialogs in dialogs.cpp: desktop_create_window() plus the four
// callbacks. It gets a title bar, dragging, a close button and a place in the
// window list for free, and the compositor draws it in the window layer rather
// than in the panel overlay.
// ============================================================================
static constexpr int PWD_W = 400;
static constexpr int PWD_PAD = 20;
static constexpr int PWD_FIELD_H = 32;
static constexpr int PWD_BTN_W = 96;
static constexpr int PWD_BTN_H = 32;
// The vertical rhythm, in one place, so the window height computed at open
// time and the rects drawn into it cannot drift apart.
static constexpr int PWD_TOP = 18; // content top to the heading
static constexpr int PWD_HEAD_GAP = 6; // heading to the network line
static constexpr int PWD_FIELD_GAP = 22; // network line to the field label
static constexpr int PWD_CHECK_GAP = 20; // field to the first checkbox
static constexpr int PWD_ROW_GAP = 6; // between the checkboxes
static constexpr int PWD_BTN_GAP = 30; // last checkbox to the buttons
struct WifiPasswordDialog {
DesktopState* ds;
char ssid[DesktopState::WIFI_SSID_CAP];
char password[DesktopState::WIFI_PSK_CAP];
uint8_t security;
bool reveal;
bool remember;
mtk::TextInputState input;
};
static mtk::Theme wifi_dialog_theme(const DesktopState* ds) {
return mtk::make_theme(ds->settings.accent_color);
}
// Content-relative layout. Everything above the buttons is measured down from
// the top and everything below them up from the bottom, so the two meet in the
// middle wherever the window is sized.
static int pwd_row_h() {
return system_font_height() + 10; // a checkbox row
}
static int pwd_label_y() {
int fh = system_font_height();
return PWD_TOP + fh + PWD_HEAD_GAP + fh + PWD_FIELD_GAP;
}
static Rect pwd_field_rect(int cw, const mtk::Theme& theme) {
return mtk::labeled_text_input_rect(PWD_PAD, pwd_label_y(), cw - PWD_PAD * 2,
theme, PWD_FIELD_H);
}
static Rect pwd_show_rect(int cw, const mtk::Theme& theme) {
Rect field = pwd_field_rect(cw, theme);
return {PWD_PAD, field.y + field.h + PWD_CHECK_GAP, 190, pwd_row_h()};
}
static Rect pwd_remember_rect(int cw, const mtk::Theme& theme) {
Rect show = pwd_show_rect(cw, theme);
return {show.x, show.y + show.h + PWD_ROW_GAP, 240, show.h};
}
// The height the content needs for all of that plus the button row.
static int pwd_content_h(const mtk::Theme& theme) {
Rect remember = pwd_remember_rect(PWD_W, theme);
return remember.y + remember.h + PWD_BTN_GAP + PWD_BTN_H + PWD_PAD;
}
static Rect pwd_join_rect(const Canvas& c) {
return {c.w - PWD_PAD - PWD_BTN_W, c.h - PWD_PAD - PWD_BTN_H,
PWD_BTN_W, PWD_BTN_H};
}
static Rect pwd_cancel_rect(const Canvas& c) {
Rect join = pwd_join_rect(c);
return {join.x - 12 - PWD_BTN_W, join.y, PWD_BTN_W, PWD_BTN_H};
}
static void wifi_dialog_close(WifiPasswordDialog* pd) {
if (!pd) return;
for (int i = 0; i < pd->ds->window_count; i++) {
if (pd->ds->windows[i].app_data == pd) {
desktop_close_window(pd->ds, i);
return;
}
}
}
static void wifi_dialog_submit(WifiPasswordDialog* pd) {
if (!pd->password[0]) return;
// Join either way; a passphrase that cannot be written down still works
// for this session.
bool save_failed = false;
if (pd->remember) {
montauk::wifi::saved_set(&g_saved, pd->ssid, pd->password);
save_failed = montauk::wifi::saved_store(&g_saved) != 0;
}
wifi_begin_join(pd->ds, pd->ssid, pd->password);
if (save_failed)
wifi_set_status(pd->ds, "Connected, but the password could not be saved");
wifi_dialog_close(pd);
}
static void wifi_dialog_on_draw(Window* win, Framebuffer& fb) {
(void)fb;
auto* pd = (WifiPasswordDialog*)win->app_data;
if (!pd) return;
Canvas c(win);
mtk::Theme theme = wifi_dialog_theme(pd->ds);
c.fill(theme.window_bg);
int fh = system_font_height();
int mx = pd->ds->mouse.x - win->content_rect().x;
int my = pd->ds->mouse.y - win->content_rect().y;
char line[96];
snprintf(line, sizeof(line), "Enter the password for %s", pd->ssid);
while (line[0] && text_width(line) > c.w - PWD_PAD * 2) {
int n = montauk::slen(line);
line[n - 1] = '\0';
}
c.text(PWD_PAD, PWD_TOP, line, theme.text);
char sub[64];
snprintf(sub, sizeof(sub), "%s network",
montauk::wifi::security_name(pd->security));
c.text(PWD_PAD, PWD_TOP + fh + PWD_HEAD_GAP, sub, theme.text_subtle);
Rect field = pwd_field_rect(c.w, theme);
mtk::draw_labeled_text_field(c, PWD_PAD, field.y - fh - theme.gap_xs,
c.w - PWD_PAD * 2, "Password", pd->password,
pd->input.cursor, true, !pd->reveal, theme,
PWD_FIELD_H, pd->input.selection_anchor);
Rect show = pwd_show_rect(c.w, theme);
mtk::draw_checkbox(c, show, "Show password",
mtk::check_state(pd->reveal), theme, true,
show.contains(mx, my));
Rect remember = pwd_remember_rect(c.w, theme);
mtk::draw_checkbox(c, remember, "Remember this network",
mtk::check_state(pd->remember), theme, true,
remember.contains(mx, my));
Rect cancel = pwd_cancel_rect(c);
Rect join = pwd_join_rect(c);
mtk::draw_button(c, cancel, "Cancel", mtk::BUTTON_SECONDARY,
mtk::widget_state(false, cancel.contains(mx, my), true), theme);
mtk::draw_button(c, join, "Join", mtk::BUTTON_PRIMARY,
mtk::widget_state(false, join.contains(mx, my),
pd->password[0] != '\0'), theme);
}
static void wifi_dialog_on_mouse(Window* win, MouseEvent& ev) {
auto* pd = (WifiPasswordDialog*)win->app_data;
if (!pd) return;
Canvas c(win);
mtk::Theme theme = wifi_dialog_theme(pd->ds);
Rect cr = win->content_rect();
int mx = ev.x - cr.x;
int my = ev.y - cr.y;
// The field owns the pointer while a drag or its context menu is running.
Rect field = pwd_field_rect(c.w, theme);
if (pd->input.context.open || pd->input.dragging || field.contains(mx, my)) {
mtk::text_input_handle_mouse(pd->input, field, pd->password,
(int)sizeof(pd->password), mx, my,
ev.buttons, ev.prev_buttons, c.w, c.h,
true, !pd->reveal, nullptr);
return;
}
if (!ev.left_pressed()) return;
if (pwd_show_rect(c.w, theme).contains(mx, my)) {
pd->reveal = !pd->reveal;
return;
}
if (pwd_remember_rect(c.w, theme).contains(mx, my)) {
pd->remember = !pd->remember;
return;
}
if (pwd_cancel_rect(c).contains(mx, my)) {
wifi_dialog_close(pd);
return;
}
if (pwd_join_rect(c).contains(mx, my)) {
wifi_dialog_submit(pd);
return;
}
}
static void wifi_dialog_on_key(Window* win, const montauk::abi::KeyEvent& key) {
auto* pd = (WifiPasswordDialog*)win->app_data;
if (!pd || !key.pressed) return;
if (key.scancode == 0x01) { // Escape
wifi_dialog_close(pd);
return;
}
if (key.ascii == '\n' || key.ascii == '\r') {
wifi_dialog_submit(pd);
return;
}
mtk::text_input_key(pd->input, pd->password, (int)sizeof(pd->password),
key, nullptr);
}
static void wifi_dialog_on_close(Window* win) {
if (!win->app_data) return;
// Do not leave the passphrase behind in freed memory.
auto* pd = (WifiPasswordDialog*)win->app_data;
montauk::memset(pd, 0, sizeof(*pd));
montauk::mfree(win->app_data);
win->app_data = nullptr;
}
static void wifi_open_password_dialog(DesktopState* ds,
const montauk::abi::WifiNetwork& net) {
// One dialog at a time: asking twice for the same key helps nobody.
for (int i = 0; i < ds->window_count; i++) {
if (ds->windows[i].on_close == wifi_dialog_on_close
&& ds->windows[i].state != WIN_CLOSED) {
desktop_raise_window(ds, i);
return;
}
}
char title[MAX_TITLE_LEN];
snprintf(title, sizeof(title), "Join %s", net.ssid);
// Height comes from the layout above rather than a constant: at the shipped
// UI size a fixed 250px window put the checkboxes under the buttons.
int pwd_h = pwd_content_h(wifi_dialog_theme(ds))
+ TITLEBAR_HEIGHT + BORDER_WIDTH;
int wx = (ds->screen_w - PWD_W) / 2;
int wy = (ds->screen_h - pwd_h) / 2;
if (wy < PANEL_HEIGHT + 8) wy = PANEL_HEIGHT + 8;
int idx = desktop_create_window(ds, title, wx, wy, PWD_W, pwd_h);
if (idx < 0) return;
auto* pd = (WifiPasswordDialog*)montauk::malloc(sizeof(WifiPasswordDialog));
if (!pd) {
desktop_close_window(ds, idx);
return;
}
montauk::memset(pd, 0, sizeof(*pd));
pd->ds = ds;
montauk::strncpy(pd->ssid, net.ssid, sizeof(pd->ssid));
pd->security = net.security;
pd->remember = true;
mtk::text_input_reset(pd->input, 0);
Window* win = &ds->windows[idx];
win->app_data = pd;
win->on_draw = wifi_dialog_on_draw;
win->on_mouse = wifi_dialog_on_mouse;
win->on_key = wifi_dialog_on_key;
win->on_close = wifi_dialog_on_close;
// The popup has done its job; the dialog is where the interaction is now.
ds->wifi_popup_open = false;
}
+625 -6
View File
@@ -6,6 +6,7 @@
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/wifi.h>
#include <gui/mtk.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/standalone.hpp>
@@ -17,8 +18,8 @@ extern "C" {
using namespace gui;
static constexpr int WIN_W = 560;
static constexpr int WIN_H = 420;
static constexpr int WIN_W = 620;
static constexpr int WIN_H = 480;
static constexpr int TAB_H = 36;
static constexpr int FOOTER_H = 44;
static constexpr int PAD = 16;
@@ -31,8 +32,9 @@ static constexpr int FIELD_H = 32;
enum Tab {
TAB_STATUS = 0,
TAB_CONFIG = 1,
TAB_COUNT = 2,
TAB_WIFI = 1,
TAB_CONFIG = 2,
TAB_COUNT = 3,
};
struct Field {
@@ -43,6 +45,7 @@ struct Field {
static const char* const kTabLabels[TAB_COUNT] = {
"Status",
"Wi-Fi",
"Configure",
};
@@ -59,6 +62,26 @@ static Field g_fields[FIELD_COUNT] = {
{"Gateway", {}, {}},
{"DNS Server", {}, {}},
};
// ---- Wi-Fi tab state -------------------------------------------------------
static constexpr int WIFI_MAX_NETWORKS = 32;
static constexpr int WIFI_ROW_H = 34;
static montauk::abi::WifiInfo g_wifi = {};
static montauk::abi::WifiNetwork g_wifi_nets[WIFI_MAX_NETWORKS];
static int g_wifi_count = 0;
static int g_wifi_selected = -1;
static int g_wifi_scroll = 0; // first visible row
static uint32_t g_wifi_generation = 0;
static bool g_wifi_scanning = false;
static bool g_wifi_joining = false;
static char g_wifi_psk[montauk::wifi::PSK_CAP] = {};
static mtk::TextInputState g_wifi_psk_input = {};
static bool g_wifi_psk_focus = false;
static bool g_wifi_remember = true;
static montauk::wifi::SavedList g_wifi_saved = {};
static bool g_dirty = false;
static char g_status[128] = {};
static uint64_t g_status_time = 0;
@@ -246,12 +269,15 @@ static void copy_cfg_to_fields() {
g_dirty = false;
}
static void wifi_refresh();
static void refresh_state(bool update_fields) {
montauk::get_netcfg(&g_cfg);
if (montauk::net_status(&g_net) < 0) {
montauk::memset(&g_net, 0, sizeof(g_net));
snprintf(g_net.driver, sizeof(g_net.driver), "Unavailable");
}
wifi_refresh();
if (update_fields) copy_cfg_to_fields();
g_last_refresh = montauk::get_milliseconds();
}
@@ -387,6 +413,434 @@ static void draw_status_tab(Canvas& c, const mtk::Theme& theme) {
draw_kv(c, &y, "TX Packets", tx, theme);
}
// ============================================================================
// Wi-Fi tab
//
// Laid out the way the Display app lays out its mode list: borderless rows on
// the window background, a radio for the selection, and state as right-aligned
// text rather than a pill. The block under the list keeps a fixed height so the
// row count does not change as the selection moves between open and encrypted
// networks.
// ============================================================================
static int wifi_status_y() {
return TAB_H + 20;
}
static int wifi_separator_y() {
return wifi_status_y() + system_font_height() * 2 + 26;
}
static int wifi_list_header_y() {
return wifi_separator_y() + 16;
}
static int wifi_list_y() {
return wifi_list_header_y() + system_font_height() + 10;
}
// The passphrase field, the checkbox under it, and the padding around them.
static int wifi_action_h() {
return mtk::labeled_text_height(app_theme(), FIELD_H) + 10
+ system_font_height() + 6;
}
static Rect wifi_action_rect() {
int h = wifi_action_h();
return {PAD, footer_rect().y - 14 - h, g_win.width - PAD * 2, h};
}
static Rect wifi_psk_input_rect() {
Rect action = wifi_action_rect();
return mtk::labeled_text_input_rect(action.x, action.y, action.w,
app_theme(), FIELD_H);
}
static Rect wifi_remember_rect() {
Rect action = wifi_action_rect();
int block = mtk::labeled_text_height(app_theme(), FIELD_H);
return {action.x, action.y + block + 10, 240, system_font_height() + 6};
}
static int wifi_visible_rows() {
int available = wifi_action_rect().y - 14 - wifi_list_y();
int rows = available / WIFI_ROW_H;
if (rows < 1) rows = 1;
if (rows > WIFI_MAX_NETWORKS) rows = WIFI_MAX_NETWORKS;
return rows;
}
static Rect wifi_row_rect(int visibleIndex) {
return {PAD, wifi_list_y() + visibleIndex * WIFI_ROW_H,
g_win.width - PAD * 2, WIFI_ROW_H};
}
static void wifi_meta_text(char* out, size_t cap,
const montauk::abi::WifiNetwork& net) {
snprintf(out, cap, "%s - ch %d - %d dBm",
montauk::wifi::security_name(net.security),
(int)net.channel, (int)net.rssi);
}
static void wifi_clamp_scroll() {
int max_scroll = g_wifi_count - wifi_visible_rows();
if (max_scroll < 0) max_scroll = 0;
if (g_wifi_scroll > max_scroll) g_wifi_scroll = max_scroll;
if (g_wifi_scroll < 0) g_wifi_scroll = 0;
}
static Rect wifi_forget_button() {
Rect refresh = status_refresh_button();
return {refresh.x - (GAP + BUTTON_W) * 2, refresh.y, BUTTON_W, BUTTON_H};
}
static Rect wifi_scan_button() {
Rect refresh = status_refresh_button();
return {refresh.x - GAP - BUTTON_W, refresh.y, BUTTON_W, BUTTON_H};
}
static Rect wifi_connect_button() {
return status_refresh_button();
}
static bool wifi_selection_valid() {
return g_wifi_selected >= 0 && g_wifi_selected < g_wifi_count;
}
static bool wifi_selection_is_current() {
return wifi_selection_valid() && g_wifi.connected
&& montauk::streq(g_wifi_nets[g_wifi_selected].ssid, g_wifi.ssid);
}
// True when joining the selection would need a passphrase typed in: encrypted,
// and nothing remembered for it.
static bool wifi_selection_needs_key() {
if (!wifi_selection_valid()) return false;
const montauk::abi::WifiNetwork& net = g_wifi_nets[g_wifi_selected];
return montauk::wifi::needs_key(net.security);
}
static void wifi_sort() {
for (int i = 1; i < g_wifi_count; i++) {
montauk::abi::WifiNetwork key = g_wifi_nets[i];
int j = i - 1;
while (j >= 0 && g_wifi_nets[j].rssi < key.rssi) {
g_wifi_nets[j + 1] = g_wifi_nets[j];
j--;
}
g_wifi_nets[j + 1] = key;
}
}
static void wifi_load_results() {
char previous[montauk::wifi::SSID_CAP] = {};
if (wifi_selection_valid())
montauk::strncpy(previous, g_wifi_nets[g_wifi_selected].ssid, sizeof(previous));
montauk::abi::WifiNetwork raw[WIFI_MAX_NETWORKS];
int n = montauk::wifi_results(raw, WIFI_MAX_NETWORKS);
if (n < 0) n = 0;
// One row per network, not one per access point: a house with a repeater
// answers on several BSSIDs and the strongest is the one worth joining.
// Hidden networks have no SSID to join by, so they are left out entirely.
g_wifi_count = 0;
for (int i = 0; i < n; i++) {
if (!raw[i].ssid[0]) continue;
int existing = -1;
for (int k = 0; k < g_wifi_count; k++) {
if (montauk::streq(g_wifi_nets[k].ssid, raw[i].ssid)) { existing = k; break; }
}
if (existing >= 0) {
if (raw[i].rssi > g_wifi_nets[existing].rssi)
g_wifi_nets[existing] = raw[i];
continue;
}
g_wifi_nets[g_wifi_count++] = raw[i];
}
wifi_sort();
// Keep the selection on the network it was on, wherever it moved to.
g_wifi_selected = -1;
if (previous[0]) {
for (int i = 0; i < g_wifi_count; i++) {
if (montauk::streq(g_wifi_nets[i].ssid, previous)) { g_wifi_selected = i; break; }
}
}
wifi_clamp_scroll();
}
// Keeps the adapter snapshot, the scan table and the saved list in step.
static void wifi_refresh() {
if (montauk::wifi_info(&g_wifi) != 0) {
montauk::memset(&g_wifi, 0, sizeof(g_wifi));
return;
}
if (g_wifi.scanGeneration != g_wifi_generation) {
g_wifi_generation = g_wifi.scanGeneration;
g_wifi_scanning = false;
wifi_load_results();
char msg[64];
snprintf(msg, sizeof(msg), "Found %d network%s", g_wifi_count,
g_wifi_count == 1 ? "" : "s");
set_status(msg);
} else if (g_wifi_scanning && !g_wifi.scanning) {
g_wifi_scanning = false;
}
if (g_wifi_joining) {
if (g_wifi.connected) {
g_wifi_joining = false;
char msg[96];
snprintf(msg, sizeof(msg), "Connected to %s", g_wifi.ssid);
set_status(msg);
} else if (g_wifi.lastError != 0 && !g_wifi.joining) {
g_wifi_joining = false;
set_status(montauk::wifi::error_message(g_wifi.lastError));
}
}
}
static void wifi_start_scan() {
int rc = montauk::wifi_scan_start(6000);
if (rc < 0) {
set_status("The Wi-Fi adapter is not ready");
return;
}
g_wifi_scanning = true;
set_status("Scanning for networks...");
}
static void wifi_join_selected() {
if (!wifi_selection_valid()) {
set_status("Select a network first");
return;
}
const montauk::abi::WifiNetwork& net = g_wifi_nets[g_wifi_selected];
const char* psk = "";
if (montauk::wifi::needs_key(net.security)) {
if (g_wifi_psk[0]) {
psk = g_wifi_psk;
} else {
const montauk::wifi::SavedNetwork* saved =
montauk::wifi::saved_find(&g_wifi_saved, net.ssid);
if (saved && saved->psk[0]) {
psk = saved->psk;
} else {
g_wifi_psk_focus = true;
set_status("This network needs a password");
return;
}
}
}
bool save_failed = false;
if (montauk::wifi::needs_key(net.security) && g_wifi_remember && psk[0]) {
montauk::wifi::saved_set(&g_wifi_saved, net.ssid, psk);
save_failed = montauk::wifi::saved_store(&g_wifi_saved) != 0;
}
int rc = montauk::wifi_connect_async(net.ssid, psk);
if (rc < 0) {
set_status(montauk::wifi::error_message(rc));
return;
}
g_wifi_joining = true;
char msg[96];
if (save_failed) {
snprintf(msg, sizeof(msg),
"Connecting to %s, but the password could not be saved", net.ssid);
} else {
snprintf(msg, sizeof(msg), "Connecting to %s...", net.ssid);
}
set_status(msg);
}
static void wifi_forget_selected() {
if (!wifi_selection_valid()) return;
const char* ssid = g_wifi_nets[g_wifi_selected].ssid;
if (!montauk::wifi::saved_remove(&g_wifi_saved, ssid)) {
set_status("That network was not saved");
return;
}
bool save_failed = montauk::wifi::saved_store(&g_wifi_saved) != 0;
g_wifi_psk[0] = '\0';
mtk::text_input_reset(g_wifi_psk_input, 0);
char msg[96];
if (save_failed) {
snprintf(msg, sizeof(msg), "Could not update the saved network list");
} else {
snprintf(msg, sizeof(msg), "Forgot %s", ssid);
}
set_status(msg);
}
static void draw_signal(Canvas& c, int x, int y, int8_t rssi, Color on, Color off) {
int bars = montauk::wifi::signal_bars(rssi);
for (int i = 0; i < 4; i++) {
int h = 3 + i * 3;
c.fill_rect(x + i * 4, y + 12 - h, 3, h, i < bars ? on : off);
}
}
static void draw_wifi_tab(Canvas& c, const mtk::Theme& theme) {
int fh = system_font_height();
int y = wifi_status_y();
bool present = g_wifi.present != 0;
bool ready = present && g_wifi.state == montauk::abi::WIFI_STATE_RUNNING;
Color dot = !present ? theme.danger
: (g_wifi.connected ? Color::from_rgb(0x27, 0xA0, 0x58)
: Color::from_rgb(0xD0, 0x9A, 0x2E));
fill_circle(c, PAD + 6, y + fh / 2, 6, dot);
char headline[128];
if (!present) {
snprintf(headline, sizeof(headline), "No Wi-Fi adapter");
} else if (g_wifi.connected) {
snprintf(headline, sizeof(headline), "Connected to %s", g_wifi.ssid);
} else if (g_wifi_joining) {
snprintf(headline, sizeof(headline), "%s",
montauk::wifi::conn_state_name(g_wifi.connState));
} else {
snprintf(headline, sizeof(headline), "%s",
montauk::wifi::state_name(g_wifi.state));
}
draw_text_fit(c, PAD + 22, y, headline, g_win.width - PAD * 2 - 22,
present ? theme.text : theme.danger);
char sub[160];
if (!present) {
snprintf(sub, sizeof(sub),
"Nothing to configure until a supported adapter is fitted.");
} else if (g_wifi.connected) {
snprintf(sub, sizeof(sub), "Channel %d - firmware %s",
(int)g_wifi.channel,
g_wifi.fwVersion[0] ? g_wifi.fwVersion : "unknown");
} else {
snprintf(sub, sizeof(sub), "Firmware %s",
g_wifi.fwVersion[0] ? g_wifi.fwVersion : "not loaded");
}
draw_text_fit(c, PAD + 22, y + fh + 3, sub, g_win.width - PAD * 2 - 22,
theme.text_subtle);
mtk::draw_separator(c, PAD, wifi_separator_y(), g_win.width - PAD * 2, theme);
// List header, with the page indicator where the Display app puts its hint.
int header_y = wifi_list_header_y();
c.text(PAD, header_y, "NETWORKS IN RANGE", theme.text_muted);
int rows = wifi_visible_rows();
char hint[48];
hint[0] = '\0';
if (g_wifi_scanning) {
snprintf(hint, sizeof(hint), "Scanning...");
} else if (g_wifi_count > rows) {
int last = g_wifi_scroll + rows;
if (last > g_wifi_count) last = g_wifi_count;
snprintf(hint, sizeof(hint), "%d-%d of %d (scroll)",
g_wifi_scroll + 1, last, g_wifi_count);
}
if (hint[0])
c.text(g_win.width - PAD - text_width(hint), header_y, hint, theme.text_muted);
if (g_wifi_count == 0) {
const char* empty = !ready ? "The adapter is not ready yet."
: (g_wifi_scanning
? "Looking for networks..."
: "No networks found. Press Scan to look again.");
c.text(PAD + 4, wifi_list_y() + 9, empty, theme.text_subtle);
}
// Fixed right-hand column for the row state, so the signal/channel detail
// beside it can never be drawn over the word "Connected".
static constexpr int STATE_W = 84;
// The detail column starts at one x for every row, sized from the widest
// line in the whole list: right-aligning each row instead would make short
// strings ("ch 1") look indented, and sizing it from the visible rows only
// would make the column jump about while scrolling.
int meta_w = 0;
for (int index = 0; index < g_wifi_count; index++) {
char meta[48];
wifi_meta_text(meta, sizeof(meta), g_wifi_nets[index]);
int w = text_width(meta);
if (w > meta_w) meta_w = w;
}
Rect list_row = wifi_row_rect(0);
int meta_x = list_row.x + list_row.w - STATE_W - 12 - meta_w;
for (int row = 0; row < rows; row++) {
int index = g_wifi_scroll + row;
if (index >= g_wifi_count) break;
const montauk::abi::WifiNetwork& net = g_wifi_nets[index];
Rect option = wifi_row_rect(row);
bool selected = index == g_wifi_selected;
bool current = g_wifi.connected && montauk::streq(net.ssid, g_wifi.ssid);
if (selected)
c.fill_rounded_rect(option.x, option.y + 2, option.w, option.h - 4,
theme.radius_md, theme.accent_soft);
else if (option.contains(g_mouse_x, g_mouse_y))
c.fill_rounded_rect(option.x, option.y + 2, option.w, option.h - 4,
theme.radius_md, theme.surface_hover);
Rect radio = {option.x + 4, option.y, 18, option.h};
mtk::draw_radio(c, radio, "", selected, theme);
int text_y = option.y + (option.h - fh) / 2;
draw_signal(c, option.x + 34, option.y + (option.h - 12) / 2, net.rssi,
current ? theme.accent : theme.text_muted, theme.border);
const char* state = current ? "Connected"
: (montauk::wifi::saved_find(&g_wifi_saved, net.ssid)
? "Saved" : nullptr);
if (state)
c.text(option.x + option.w - STATE_W, text_y, state,
current ? theme.accent : theme.text_muted);
char meta[48];
wifi_meta_text(meta, sizeof(meta), net);
c.text(meta_x, text_y, meta, theme.text_subtle);
int ssid_x = option.x + 60;
draw_text_fit(c, ssid_x, text_y, net.ssid, meta_x - 12 - ssid_x, theme.text);
}
// The block below the list keeps its height whatever is in it: a passphrase
// field when one is wanted, and a line explaining why not when it is not.
Rect action = wifi_action_rect();
if (wifi_selection_needs_key()) {
const montauk::wifi::SavedNetwork* saved =
montauk::wifi::saved_find(&g_wifi_saved,
g_wifi_nets[g_wifi_selected].ssid);
mtk::draw_labeled_text_field(c, action.x, action.y, action.w,
saved ? "Password (saved)" : "Password",
g_wifi_psk, g_wifi_psk_input.cursor,
g_wifi_psk_focus, true, theme, FIELD_H,
g_wifi_psk_input.selection_anchor);
Rect remember = wifi_remember_rect();
mtk::draw_checkbox(c, remember, "Remember this network",
mtk::check_state(g_wifi_remember), theme, true,
remember.contains(g_mouse_x, g_mouse_y));
} else {
const char* line;
if (!wifi_selection_valid())
line = "";
else
line = "Open network - no password needed.";
if (line[0]) c.text(action.x, action.y, line, theme.text_subtle);
}
}
static void draw_config_tab(Canvas& c, const mtk::Theme& theme) {
int y = config_header_y();
c.text(PAD, y, "IPv4 Configuration", theme.text);
@@ -418,10 +872,33 @@ static void draw_footer(Canvas& c, const mtk::Theme& theme) {
const char* msg = status_visible()
? g_status
: (g_dirty ? "Unsaved static IPv4 changes" : "Network settings ready");
int text_right = (g_tab == TAB_STATUS ? status_clear_button().x : config_dhcp_button().x) - GAP;
int text_right = PAD;
if (g_tab == TAB_STATUS) text_right = status_clear_button().x - GAP;
else if (g_tab == TAB_WIFI) text_right = wifi_forget_button().x - GAP;
else text_right = config_dhcp_button().x - GAP;
draw_text_fit(c, PAD, foot.y + (FOOTER_H - system_font_height()) / 2,
msg, text_right - PAD, g_dirty ? theme.text : theme.text_subtle);
if (g_tab == TAB_WIFI) {
Rect forget = wifi_forget_button();
Rect scan = wifi_scan_button();
Rect connect = wifi_connect_button();
bool saved = wifi_selection_valid()
&& montauk::wifi::saved_find(&g_wifi_saved,
g_wifi_nets[g_wifi_selected].ssid) != nullptr;
bool connected = wifi_selection_is_current() || (g_wifi.connected && !wifi_selection_valid());
mtk::draw_button(c, forget, "Forget", mtk::BUTTON_SECONDARY,
button_state(forget, saved), theme);
mtk::draw_button(c, scan, g_wifi_scanning ? "Scanning" : "Scan",
mtk::BUTTON_SECONDARY,
button_state(scan, !g_wifi_scanning && g_wifi.present), theme);
mtk::draw_button(c, connect, connected ? "Disconnect" : "Connect",
mtk::BUTTON_PRIMARY,
button_state(connect, g_wifi.present != 0), theme);
return;
}
if (g_tab == TAB_STATUS) {
Rect clear = status_clear_button();
Rect dhcp = status_dhcp_button();
@@ -455,12 +932,20 @@ static void render() {
if (g_tab == TAB_STATUS) {
draw_status_tab(c, theme);
} else if (g_tab == TAB_WIFI) {
draw_wifi_tab(c, theme);
} else {
draw_config_tab(c, theme);
}
draw_footer(c, theme);
if (g_tab == TAB_CONFIG)
draw_config_context_menus(c, theme);
if (g_tab == TAB_WIFI) {
mtk::draw_text_input_context_menu(
c, g_wifi_psk_input, theme,
mtk::text_input_has_selection(g_wifi_psk_input, g_wifi_psk,
(int)sizeof(g_wifi_psk)));
}
host.present();
}
@@ -523,6 +1008,19 @@ static void set_tab(Tab tab) {
return;
}
g_tab = tab;
if (tab == TAB_WIFI) {
g_focus_field = -1;
g_wifi_psk_focus = false;
montauk::wifi::saved_load(&g_wifi_saved);
wifi_refresh();
wifi_load_results();
// Nothing on screen yet and an adapter that can look: go and find out.
if (g_wifi_count == 0 && !g_wifi_scanning
&& g_wifi.state == montauk::abi::WIFI_STATE_RUNNING) {
wifi_start_scan();
}
return;
}
if (tab == TAB_CONFIG) {
focus_field(0);
} else {
@@ -546,6 +1044,80 @@ static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons)
}
}
if (g_tab == TAB_WIFI) {
// The passphrase field owns the click while a drag or its context menu
// is in progress, the same rule the IPv4 fields follow.
if (g_wifi_psk_input.context.open || g_wifi_psk_input.dragging) {
int result = mtk::text_input_handle_mouse(
g_wifi_psk_input, wifi_psk_input_rect(), g_wifi_psk,
(int)sizeof(g_wifi_psk), mx, my, buttons, prev_buttons,
g_win.width, g_win.height, g_wifi_psk_focus, true, nullptr);
return result != mtk::TEXT_INPUT_NONE;
}
if (!left_pressed && !right_pressed) return false;
if (wifi_selection_needs_key() && wifi_psk_input_rect().contains(mx, my)) {
g_wifi_psk_focus = true;
mtk::text_input_handle_mouse(g_wifi_psk_input, wifi_psk_input_rect(),
g_wifi_psk, (int)sizeof(g_wifi_psk),
mx, my, buttons, prev_buttons,
g_win.width, g_win.height, true, true, nullptr);
return true;
}
if (!left_pressed) return false;
int rows = wifi_visible_rows();
for (int row = 0; row < rows; row++) {
int idx = g_wifi_scroll + row;
if (idx >= g_wifi_count) break;
if (!wifi_row_rect(row).contains(mx, my)) continue;
if (idx != g_wifi_selected) {
g_wifi_selected = idx;
// A different network means a different key; do not carry the
// previous one over, but do offer the remembered one.
g_wifi_psk[0] = '\0';
mtk::text_input_reset(g_wifi_psk_input, 0);
const montauk::wifi::SavedNetwork* saved =
montauk::wifi::saved_find(&g_wifi_saved, g_wifi_nets[idx].ssid);
if (saved) {
montauk::strncpy(g_wifi_psk, saved->psk, sizeof(g_wifi_psk));
mtk::text_input_reset(g_wifi_psk_input, str_len(g_wifi_psk));
}
}
g_wifi_psk_focus = false;
return true;
}
if (wifi_selection_needs_key() && wifi_remember_rect().contains(mx, my)) {
g_wifi_remember = !g_wifi_remember;
return true;
}
if (wifi_scan_button().contains(mx, my)) {
if (!g_wifi_scanning) wifi_start_scan();
return true;
}
if (wifi_forget_button().contains(mx, my)) {
wifi_forget_selected();
return true;
}
if (wifi_connect_button().contains(mx, my)) {
if (wifi_selection_is_current() || (g_wifi.connected && !wifi_selection_valid())) {
montauk::wifi_disconnect();
g_wifi_joining = false;
set_status("Disconnected");
} else {
wifi_join_selected();
}
return true;
}
g_wifi_psk_focus = false;
return true;
}
if (g_tab == TAB_STATUS) {
if (!left_pressed) return false;
if (status_refresh_button().contains(mx, my)) {
@@ -631,6 +1203,42 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
return true;
}
if (g_tab == TAB_WIFI) {
if (key.ascii == '\t') {
set_tab(TAB_CONFIG);
return true;
}
if (key.ascii == '\n' || key.ascii == '\r') {
wifi_join_selected();
return true;
}
if (key.scancode == 0x48 || key.scancode == 0x50) { // up / down
int step = key.scancode == 0x48 ? -1 : 1;
int next = g_wifi_selected < 0 ? 0 : g_wifi_selected + step;
if (next < 0) next = 0;
if (next >= g_wifi_count) next = g_wifi_count - 1;
g_wifi_selected = next;
if (g_wifi_selected >= 0) {
if (g_wifi_selected < g_wifi_scroll)
g_wifi_scroll = g_wifi_selected;
if (g_wifi_selected >= g_wifi_scroll + wifi_visible_rows())
g_wifi_scroll = g_wifi_selected - wifi_visible_rows() + 1;
}
return true;
}
if (g_wifi_psk_focus && wifi_selection_needs_key()) {
int result = mtk::text_input_key(g_wifi_psk_input, g_wifi_psk,
(int)sizeof(g_wifi_psk), key, nullptr);
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
}
if (key.ascii == 's' || key.ascii == 'S') {
if (!g_wifi_scanning) wifi_start_scan();
return true;
}
return false;
}
if (key.ascii == '\t') {
if (g_tab != TAB_CONFIG) {
set_tab(TAB_CONFIG);
@@ -690,7 +1298,9 @@ extern "C" void _start() {
montauk::exit(1);
}
montauk::wifi::saved_load(&g_wifi_saved);
refresh_state(true);
wifi_load_results();
render();
while (g_win.id >= 0 && !g_win.closed) {
@@ -701,7 +1311,10 @@ extern "C" void _start() {
if (r < 0) break;
if (r == 0) {
uint64_t now = montauk::get_milliseconds();
if (now - g_last_refresh >= 3000) {
// The Wi-Fi tab has work in flight worth watching closely; the rest
// of the app is happy with the slower cadence.
bool wifi_busy = g_tab == TAB_WIFI && (g_wifi_scanning || g_wifi_joining);
if (now - g_last_refresh >= (wifi_busy ? 400u : 3000u)) {
refresh_state(!g_dirty);
redraw = true;
}
@@ -718,6 +1331,12 @@ extern "C" void _start() {
g_mouse_x = ev.mouse.x;
g_mouse_y = ev.mouse.y;
redraw = true;
if (ev.mouse.scroll != 0 && g_tab == TAB_WIFI
&& ev.mouse.y >= wifi_list_y()
&& ev.mouse.y < wifi_action_rect().y) {
g_wifi_scroll += ev.mouse.scroll > 0 ? -1 : 1;
wifi_clamp_scroll();
}
if (handle_mouse(ev.mouse.x, ev.mouse.y,
ev.mouse.buttons, ev.mouse.prev_buttons)) {
redraw = true;
+63 -3
View File
@@ -17,6 +17,7 @@
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/wifi.h>
using namespace montauk;
@@ -363,14 +364,30 @@ static int cmd_debug() {
return 0;
}
static int cmd_connect(const char* ssid, const char* password) {
static int cmd_connect(const char* ssid, const char* password, bool remember) {
abi::WifiInfo info;
if (!require_adapter(info)) return 1;
// No passphrase on the command line: fall back to the one the desktop or a
// previous run saved, so "wifi connect Home" works on its own.
char saved[montauk::wifi::PSK_CAP];
if ((!password || !password[0])
&& montauk::wifi::lookup(ssid, saved, sizeof(saved)) && saved[0]) {
print("Using the saved passphrase for \""); print(ssid); print("\".\n");
password = saved;
remember = false; // already stored
}
print("Connecting to \""); print(ssid); print("\"...\n");
int rc = wifi_connect(ssid, password);
if (rc == 0) {
if (remember && password && password[0]) {
if (montauk::wifi::remember(ssid, password))
print("Saved to 0:/config/wifi.toml for next time.\n");
else
print("wifi: could not write 0:/config/wifi.toml.\n");
}
print("Connected.\n");
print("Run \"dhcp\" to get an address, then the network is usable\n");
print("just like a wired connection.\n");
@@ -410,6 +427,36 @@ static int cmd_connect(const char* ssid, const char* password) {
return 1;
}
static int cmd_saved() {
montauk::wifi::SavedList list;
montauk::wifi::saved_load(&list);
if (list.count == 0) {
print("No saved networks.\n");
return 0;
}
print("Saved networks (0:/config/wifi.toml):\n");
for (int i = 0; i < list.count; i++) {
print(" ");
print(list.items[i].ssid);
print("\n");
}
print("\nAutomatic reconnect is ");
print(list.autoconnect ? "on" : "off");
print(".\n");
return 0;
}
static int cmd_forget(const char* ssid) {
if (montauk::wifi::forget(ssid)) {
print("Forgot \""); print(ssid); print("\".\n");
return 0;
}
print("wifi: \""); print(ssid); print("\" was not saved.\n");
return 1;
}
static void usage() {
print("usage: wifi [command]\n\n");
print(" scan [seconds] scan and list nearby networks (default 5)\n");
@@ -417,7 +464,11 @@ static void usage() {
print(" debug diagnostics plus per-BSS scan detail\n");
print(" connect <ssid> [key] join a network (open or WPA2/WPA3-PSK)\n");
print(" status show the network currently joined\n");
print(" disconnect leave the current network\n\n");
print(" disconnect leave the current network\n");
print(" saved list remembered networks\n");
print(" forget <ssid> remove a remembered network\n\n");
print("A passphrase given to connect is remembered, and one that was\n");
print("remembered is used when connect is given only an SSID.\n\n");
print("With no command, runs a 5 second scan.\n");
}
@@ -454,7 +505,16 @@ extern "C" void _start() {
while (p[n] && n < (int)sizeof(pass) - 1) { pass[n] = p[n]; n++; }
while (n > 0 && pass[n - 1] == ' ') n--;
pass[n] = '\0';
exit(cmd_connect(ssid, pass));
exit(cmd_connect(ssid, pass, true));
} else if (streq(cmd, "saved")) {
exit(cmd_saved());
} else if (streq(cmd, "forget")) {
char ssid[40];
if (!next_token(&rest, ssid, sizeof(ssid))) {
print("wifi: forget needs an SSID\n");
exit(1);
}
exit(cmd_forget(ssid));
} else if (streq(cmd, "disconnect")) {
wifi_disconnect();
print("Disconnected.\n");