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
+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");