Files
MontaukOS/programs/src/network/main.cpp
T

735 lines
22 KiB
C++

/*
* main.cpp
* MontaukOS Network configuration applet
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <gui/mtk.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/standalone.hpp>
extern "C" {
#include <stdio.h>
#include <string.h>
}
using namespace gui;
static constexpr int WIN_W = 560;
static constexpr int WIN_H = 420;
static constexpr int TAB_H = 36;
static constexpr int FOOTER_H = 44;
static constexpr int PAD = 16;
static constexpr int GAP = 12;
static constexpr int BUTTON_H = 30;
static constexpr int BUTTON_W = 90;
static constexpr int FIELD_COUNT = 4;
static constexpr int FIELD_TEXT_CAP = 16;
static constexpr int FIELD_H = 32;
enum Tab {
TAB_STATUS = 0,
TAB_CONFIG = 1,
TAB_COUNT = 2,
};
struct Field {
const char* label;
char value[FIELD_TEXT_CAP];
mtk::TextInputState input;
};
static const char* const kTabLabels[TAB_COUNT] = {
"Status",
"Configure",
};
static WsWindow g_win;
static Tab g_tab = TAB_STATUS;
static int g_mouse_x = -1;
static int g_mouse_y = -1;
static int g_focus_field = -1;
static Montauk::NetCfg g_cfg = {};
static Montauk::NetStatus g_net = {};
static Field g_fields[FIELD_COUNT] = {
{"IP Address", {}, {}},
{"Subnet Mask", {}, {}},
{"Gateway", {}, {}},
{"DNS Server", {}, {}},
};
static bool g_dirty = false;
static char g_status[128] = {};
static uint64_t g_status_time = 0;
static uint64_t g_last_refresh = 0;
static Color g_accent = colors::ACCENT;
static mtk::Theme app_theme() {
mtk::Theme theme = mtk::make_theme(g_accent);
theme.danger_soft = Color::from_rgb(0xF8, 0xE0, 0xE0);
return theme;
}
static void load_accent() {
g_accent = mtk::load_system_accent();
}
static Rect tab_bar_rect() {
return {0, 0, g_win.width, TAB_H};
}
static Rect footer_rect() {
return {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
}
static bool status_visible() {
return g_status[0] && (montauk::get_milliseconds() - g_status_time < 5000);
}
static void set_status(const char* msg) {
snprintf(g_status, sizeof(g_status), "%s", msg ? msg : "");
g_status_time = montauk::get_milliseconds();
}
static bool mouse_in_rect(const Rect& rect) {
return rect.contains(g_mouse_x, g_mouse_y);
}
static mtk::WidgetState button_state(const Rect& rect, bool enabled = true) {
return mtk::widget_state(false, mouse_in_rect(rect), enabled);
}
static int str_len(const char* s) {
return s ? montauk::slen(s) : 0;
}
static bool is_ipv4_input_char(char ch) {
return (ch >= '0' && ch <= '9') || ch == '.';
}
static bool accept_ipv4_input_char(char ch, void* userdata) {
(void)userdata;
return is_ipv4_input_char(ch);
}
static void focus_field(int index, int cursor = -1) {
if (index < 0) index = 0;
if (index >= FIELD_COUNT) index = FIELD_COUNT - 1;
Field& field = g_fields[index];
int len = str_len(field.value);
if (cursor < 0) cursor = len;
if (cursor > len) cursor = len;
g_focus_field = index;
for (int i = 0; i < FIELD_COUNT; i++) {
if (i != index) mtk::context_menu_close(g_fields[i].input.context);
}
mtk::text_input_reset(field.input, cursor);
}
static bool mac_is_zero(const uint8_t* mac) {
for (int i = 0; i < 6; i++) {
if (mac[i] != 0) return false;
}
return true;
}
static void format_ip(char* buf, int size, uint32_t ip) {
snprintf(buf, size, "%u.%u.%u.%u",
(unsigned)(ip & 0xFF),
(unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF),
(unsigned)((ip >> 24) & 0xFF));
}
static void format_mac(char* buf, int size, const uint8_t* mac) {
if (!mac || mac_is_zero(mac)) {
snprintf(buf, size, "Unavailable");
return;
}
snprintf(buf, size, "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
static void format_u64(char* buf, int size, uint64_t value) {
if (size <= 0) return;
char tmp[24];
int n = 0;
if (value == 0) {
tmp[n++] = '0';
} else {
while (value && n < (int)sizeof(tmp)) {
tmp[n++] = (char)('0' + (value % 10));
value /= 10;
}
}
int out = 0;
while (n > 0 && out < size - 1) {
buf[out++] = tmp[--n];
}
buf[out] = '\0';
}
static bool parse_ip(const char* s, uint32_t* out) {
if (!s || !out) return false;
uint32_t octets[4] = {};
int idx = 0;
uint32_t value = 0;
bool has_digit = false;
for (int i = 0; ; i++) {
char c = s[i];
if (c >= '0' && c <= '9') {
value = value * 10 + (uint32_t)(c - '0');
if (value > 255) return false;
has_digit = true;
} else if (c == '.' || c == '\0') {
if (!has_digit || idx >= 4) return false;
octets[idx++] = value;
value = 0;
has_digit = false;
if (c == '\0') break;
} else {
return false;
}
}
if (idx != 4) return false;
*out = octets[0] | (octets[1] << 8) | (octets[2] << 16) | (octets[3] << 24);
return true;
}
static void fit_text(char* out, int out_cap, const char* text, int max_w) {
if (!out || out_cap <= 0) return;
out[0] = '\0';
if (!text || !text[0] || max_w <= 0) return;
if (text_width(text) <= max_w) {
snprintf(out, out_cap, "%s", text);
return;
}
const char* ellipsis = "...";
int ell_w = text_width(ellipsis);
if (ell_w > max_w) return;
int out_len = 0;
while (text[out_len] && out_len < out_cap - 4) {
out[out_len] = text[out_len];
out[out_len + 1] = '\0';
if (text_width(out) + ell_w > max_w)
break;
out_len++;
}
out[out_len] = '.';
out[out_len + 1] = '.';
out[out_len + 2] = '.';
out[out_len + 3] = '\0';
}
static void draw_text_fit(Canvas& c, int x, int y, const char* text, int max_w, Color color) {
char fitted[128];
fit_text(fitted, sizeof(fitted), text, max_w);
c.text(x, y, fitted, color);
}
static void copy_cfg_to_fields() {
format_ip(g_fields[0].value, sizeof(g_fields[0].value), g_cfg.ipAddress);
format_ip(g_fields[1].value, sizeof(g_fields[1].value), g_cfg.subnetMask);
format_ip(g_fields[2].value, sizeof(g_fields[2].value), g_cfg.gateway);
format_ip(g_fields[3].value, sizeof(g_fields[3].value), g_cfg.dnsServer);
for (int i = 0; i < FIELD_COUNT; i++) {
mtk::text_input_reset(g_fields[i].input, str_len(g_fields[i].value));
}
g_dirty = false;
}
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");
}
if (update_fields) copy_cfg_to_fields();
g_last_refresh = montauk::get_milliseconds();
}
static Rect status_refresh_button() {
Rect foot = footer_rect();
return {g_win.width - PAD - BUTTON_W, foot.y + 7, BUTTON_W, BUTTON_H};
}
static Rect status_dhcp_button() {
Rect refresh = status_refresh_button();
return {refresh.x - GAP - BUTTON_W, refresh.y, BUTTON_W, BUTTON_H};
}
static Rect status_clear_button() {
Rect dhcp = status_dhcp_button();
return {dhcp.x - GAP - BUTTON_W, dhcp.y, BUTTON_W, BUTTON_H};
}
static Rect config_apply_button() {
Rect foot = footer_rect();
return {g_win.width - PAD - BUTTON_W, foot.y + 7, BUTTON_W, BUTTON_H};
}
static Rect config_revert_button() {
Rect apply = config_apply_button();
return {apply.x - GAP - BUTTON_W, apply.y, BUTTON_W, BUTTON_H};
}
static Rect config_dhcp_button() {
Rect revert = config_revert_button();
return {revert.x - GAP - BUTTON_W, revert.y, BUTTON_W, BUTTON_H};
}
static bool config_two_columns() {
return g_win.width - PAD * 2 >= 470;
}
static int config_header_y() {
return TAB_H + 18;
}
static int config_field_base_y() {
return config_header_y() + system_font_height() + 18;
}
static int config_field_row_step() {
return system_font_height() + FIELD_H + 16;
}
static Rect field_input_rect(int index) {
int available = g_win.width - PAD * 2;
bool two_cols = config_two_columns();
int col_w = two_cols ? (available - GAP) / 2 : available;
int col = two_cols ? (index % 2) : 0;
int row = two_cols ? (index / 2) : index;
int x = PAD + col * (col_w + GAP);
int y = config_field_base_y() + row * config_field_row_step();
return mtk::labeled_text_input_rect(x, y, col_w, app_theme(), FIELD_H);
}
static int field_label_y(int index) {
int row = config_two_columns() ? (index / 2) : index;
return config_field_base_y() + row * config_field_row_step();
}
static int field_x(int index) {
int available = g_win.width - PAD * 2;
bool two_cols = config_two_columns();
int col_w = two_cols ? (available - GAP) / 2 : available;
int col = two_cols ? (index % 2) : 0;
return PAD + col * (col_w + GAP);
}
static int field_w(int index) {
(void)index;
int available = g_win.width - PAD * 2;
return config_two_columns() ? (available - GAP) / 2 : available;
}
static void draw_kv(Canvas& c, int* y, const char* key, const char* value,
const mtk::Theme& theme) {
int fh = system_font_height();
int label_w = 128;
c.text(PAD, *y, key, theme.text_muted);
draw_text_fit(c, PAD + label_w, *y, value,
g_win.width - PAD * 2 - label_w, theme.text);
*y += fh + 10;
}
static void draw_status_tab(Canvas& c, const mtk::Theme& theme) {
int y = TAB_H + 20;
int fh = system_font_height();
bool adapter = g_net.initialized != 0;
bool online = adapter && g_net.linkUp != 0 && g_cfg.ipAddress != 0;
Color dot = adapter
? (g_net.linkUp ? Color::from_rgb(0x27, 0xA0, 0x58) : Color::from_rgb(0xD0, 0x9A, 0x2E))
: theme.danger;
fill_circle(c, PAD + 8, y + 8, 6, dot);
const char* title = adapter
? (online ? "Online" : (g_net.linkUp ? "Link up, no address" : "Link down"))
: "No network adapter";
c.text(PAD + 24, y, title, adapter ? theme.text : theme.danger);
char driver[80];
snprintf(driver, sizeof(driver), "%s%s",
g_net.driver[0] ? g_net.driver : "Unknown driver",
g_net.polling ? " (polling)" : "");
draw_text_fit(c, PAD + 24, y + fh + 3, driver,
g_win.width - PAD * 2 - 24, theme.text_subtle);
y += fh * 2 + 24;
mtk::draw_separator(c, PAD, y, g_win.width - PAD * 2, theme);
y += 16;
char ip[32], mask[32], gw[32], dns[32], mac[40], rx[32], tx[32];
format_ip(ip, sizeof(ip), g_cfg.ipAddress);
format_ip(mask, sizeof(mask), g_cfg.subnetMask);
format_ip(gw, sizeof(gw), g_cfg.gateway);
format_ip(dns, sizeof(dns), g_cfg.dnsServer);
format_mac(mac, sizeof(mac), g_cfg.macAddress);
format_u64(rx, sizeof(rx), g_net.rxPackets);
format_u64(tx, sizeof(tx), g_net.txPackets);
draw_kv(c, &y, "MAC Address", mac, theme);
draw_kv(c, &y, "IP Address", ip, theme);
draw_kv(c, &y, "Subnet Mask", mask, theme);
draw_kv(c, &y, "Gateway", gw, theme);
draw_kv(c, &y, "DNS Server", dns, theme);
draw_kv(c, &y, "RX Packets", rx, theme);
draw_kv(c, &y, "TX Packets", tx, theme);
}
static void draw_config_tab(Canvas& c, const mtk::Theme& theme) {
int y = config_header_y();
c.text(PAD, y, "IPv4 Configuration", theme.text);
for (int i = 0; i < FIELD_COUNT; i++) {
mtk::draw_labeled_text_field(c, field_x(i), field_label_y(i), field_w(i),
g_fields[i].label, g_fields[i].value,
g_fields[i].input.cursor, g_focus_field == i,
false, theme, FIELD_H,
g_fields[i].input.selection_anchor);
}
}
static void draw_config_context_menus(Canvas& c, const mtk::Theme& theme) {
for (int i = 0; i < FIELD_COUNT; i++) {
mtk::draw_text_input_context_menu(
c, g_fields[i].input, theme,
mtk::text_input_has_selection(g_fields[i].input,
g_fields[i].value,
(int)sizeof(g_fields[i].value)));
}
}
static void draw_footer(Canvas& c, const mtk::Theme& theme) {
Rect foot = footer_rect();
c.fill_rect(foot.x, foot.y, foot.w, foot.h, theme.surface);
mtk::draw_separator(c, 0, foot.y, g_win.width, 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;
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_STATUS) {
Rect clear = status_clear_button();
Rect dhcp = status_dhcp_button();
Rect refresh = status_refresh_button();
mtk::draw_button(c, clear, "Clear", mtk::BUTTON_SECONDARY,
button_state(clear), theme);
mtk::draw_button(c, dhcp, "DHCP", mtk::BUTTON_PRIMARY,
button_state(dhcp), theme);
mtk::draw_button(c, refresh, "Refresh", mtk::BUTTON_SECONDARY,
button_state(refresh), theme);
} else {
Rect dhcp = config_dhcp_button();
Rect revert = config_revert_button();
Rect apply = config_apply_button();
mtk::draw_button(c, dhcp, "DHCP", mtk::BUTTON_SECONDARY,
button_state(dhcp), theme);
mtk::draw_button(c, revert, "Revert", mtk::BUTTON_SECONDARY,
button_state(revert, g_dirty), theme);
mtk::draw_button(c, apply, "Apply", mtk::BUTTON_PRIMARY,
button_state(apply, g_dirty), theme);
}
}
static void render() {
mtk::StandaloneHost host(&g_win);
Canvas c = host.canvas();
mtk::Theme theme = app_theme();
c.fill(theme.window_bg);
mtk::draw_tab_bar(c, tab_bar_rect(), kTabLabels, TAB_COUNT, (int)g_tab, theme);
if (g_tab == TAB_STATUS) {
draw_status_tab(c, theme);
} else {
draw_config_tab(c, theme);
}
draw_footer(c, theme);
if (g_tab == TAB_CONFIG)
draw_config_context_menus(c, theme);
host.present();
}
static void launch_dhcp() {
int pid = montauk::spawn("0:/os/dhcp.elf");
if (pid >= 0) {
set_status("DHCP client started");
} else {
set_status("Could not start DHCP client");
}
}
static void clear_config() {
Montauk::NetCfg cfg = g_cfg;
cfg.ipAddress = 0;
cfg.subnetMask = 0;
cfg.gateway = 0;
cfg.dnsServer = 0;
if (montauk::set_netcfg(&cfg) < 0) {
set_status("Could not clear network configuration");
return;
}
refresh_state(true);
set_status("Network configuration cleared");
}
static bool apply_config() {
uint32_t values[FIELD_COUNT];
for (int i = 0; i < FIELD_COUNT; i++) {
if (!parse_ip(g_fields[i].value, &values[i])) {
char msg[96];
snprintf(msg, sizeof(msg), "Invalid %s", g_fields[i].label);
set_status(msg);
g_focus_field = i;
return false;
}
}
Montauk::NetCfg cfg = g_cfg;
cfg.ipAddress = values[0];
cfg.subnetMask = values[1];
cfg.gateway = values[2];
cfg.dnsServer = values[3];
if (montauk::set_netcfg(&cfg) < 0) {
set_status("Could not apply network configuration");
return false;
}
refresh_state(true);
set_status("Network configuration applied");
return true;
}
static void set_tab(Tab tab) {
if (g_tab == tab) {
if (tab == TAB_CONFIG && (g_focus_field < 0 || g_focus_field >= FIELD_COUNT))
focus_field(0);
return;
}
g_tab = tab;
if (tab == TAB_CONFIG) {
focus_field(0);
} else {
g_focus_field = -1;
for (int i = 0; i < FIELD_COUNT; i++) {
g_fields[i].input.dragging = false;
mtk::context_menu_close(g_fields[i].input.context);
}
}
}
static bool handle_mouse(int mx, int my, uint8_t buttons, uint8_t prev_buttons) {
bool left_pressed = (buttons & 1) && !(prev_buttons & 1);
bool right_pressed = (buttons & 2) && !(prev_buttons & 2);
if (left_pressed) {
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, mx, my);
if (tab >= 0) {
set_tab((Tab)tab);
return true;
}
}
if (g_tab == TAB_STATUS) {
if (!left_pressed) return false;
if (status_refresh_button().contains(mx, my)) {
refresh_state(!g_dirty);
set_status("Network status refreshed");
return true;
}
if (status_dhcp_button().contains(mx, my)) {
launch_dhcp();
return true;
}
if (status_clear_button().contains(mx, my)) {
clear_config();
return true;
}
return false;
}
for (int i = 0; i < FIELD_COUNT; i++) {
if (!g_fields[i].input.context.open && !g_fields[i].input.dragging) continue;
int result = mtk::text_input_handle_mouse(g_fields[i].input,
field_input_rect(i),
g_fields[i].value,
(int)sizeof(g_fields[i].value),
mx, my, buttons, prev_buttons,
g_win.width, g_win.height,
g_focus_field == i,
false, accept_ipv4_input_char);
if (result & mtk::TEXT_INPUT_CHANGED)
g_dirty = true;
return result != mtk::TEXT_INPUT_NONE;
}
if (!left_pressed && !right_pressed) return false;
for (int i = 0; i < FIELD_COUNT; i++) {
Rect input = field_input_rect(i);
if (input.contains(mx, my)) {
g_focus_field = i;
for (int j = 0; j < FIELD_COUNT; j++) {
if (j != i) mtk::context_menu_close(g_fields[j].input.context);
}
int result = mtk::text_input_handle_mouse(g_fields[i].input, input,
g_fields[i].value,
(int)sizeof(g_fields[i].value),
mx, my, buttons, prev_buttons,
g_win.width, g_win.height,
true, false,
accept_ipv4_input_char);
if (result & mtk::TEXT_INPUT_CHANGED)
g_dirty = true;
return true;
}
}
if (!left_pressed) return false;
if (config_dhcp_button().contains(mx, my)) {
launch_dhcp();
return true;
}
if (config_revert_button().contains(mx, my)) {
if (g_dirty) {
copy_cfg_to_fields();
set_status("Changes reverted");
}
return true;
}
if (config_apply_button().contains(mx, my)) {
if (g_dirty) apply_config();
return true;
}
g_focus_field = -1;
return true;
}
static bool handle_key(const Montauk::KeyEvent& key) {
if (!key.pressed) return false;
if (key.scancode == 0x01) {
g_win.closed = true;
return true;
}
if (key.ascii == '\t') {
if (g_tab != TAB_CONFIG) {
set_tab(TAB_CONFIG);
} else {
int next = (g_focus_field >= 0 && g_focus_field < FIELD_COUNT)
? (g_focus_field + 1) % FIELD_COUNT
: 0;
focus_field(next);
}
return true;
}
if (g_tab != TAB_CONFIG) {
if (key.ascii == 'r' || key.ascii == 'R') {
refresh_state(!g_dirty);
set_status("Network status refreshed");
return true;
}
if (key.ascii == 'd' || key.ascii == 'D') {
launch_dhcp();
return true;
}
return false;
}
if (key.ascii == '\n' || key.ascii == '\r') {
if (g_dirty) apply_config();
return true;
}
if (key.ascii == '\033') {
if (g_dirty) copy_cfg_to_fields();
return true;
}
if (g_focus_field < 0 || g_focus_field >= FIELD_COUNT) {
if (!is_ipv4_input_char(key.ascii)) return false;
focus_field(0);
}
Field* field = &g_fields[g_focus_field];
int result = mtk::text_input_key(field->input, field->value,
(int)sizeof(field->value), key,
accept_ipv4_input_char);
if (result & mtk::TEXT_INPUT_CHANGED)
g_dirty = true;
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
}
extern "C" void _start() {
if (!fonts::init()) {
montauk::exit(1);
}
load_accent();
if (!g_win.create("Network", WIN_W, WIN_H)) {
montauk::exit(1);
}
refresh_state(true);
render();
while (g_win.id >= 0 && !g_win.closed) {
Montauk::WinEvent ev;
int r = g_win.poll(&ev);
bool redraw = false;
if (r < 0) break;
if (r == 0) {
uint64_t now = montauk::get_milliseconds();
if (now - g_last_refresh >= 3000) {
refresh_state(!g_dirty);
redraw = true;
}
if (status_visible()) redraw = true;
if (redraw) render();
montauk::sleep_ms(16);
continue;
}
if (ev.type == 3) break;
if (ev.type == 2 || ev.type == 4) {
redraw = true;
} else if (ev.type == 1) {
g_mouse_x = ev.mouse.x;
g_mouse_y = ev.mouse.y;
redraw = true;
if (handle_mouse(ev.mouse.x, ev.mouse.y,
ev.mouse.buttons, ev.mouse.prev_buttons)) {
redraw = true;
}
} else if (ev.type == 0) {
redraw = handle_key(ev.key);
}
if (redraw) render();
}
g_win.destroy();
montauk::exit(0);
}