735 lines
22 KiB
C++
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);
|
|
}
|