/* * main.cpp * MontaukOS Network configuration applet * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include extern "C" { #include #include } 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); }