feat: kernel network statistics reporting, new Network configuration applet
This commit is contained in:
@@ -6,12 +6,12 @@
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/font.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
@@ -157,33 +157,7 @@ static void calc_set_accent(Color accent) {
|
||||
}
|
||||
|
||||
static void calc_load_accent() {
|
||||
calc_set_accent(colors::ACCENT);
|
||||
|
||||
char user[64];
|
||||
montauk::memset(user, 0, sizeof(user));
|
||||
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
|
||||
auto doc = montauk::config::load_user(user, "desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
calc_set_accent(Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF)));
|
||||
doc.destroy();
|
||||
return;
|
||||
}
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
auto doc = montauk::config::load("desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
calc_set_accent(Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF)));
|
||||
}
|
||||
doc.destroy();
|
||||
calc_set_accent(mtk::load_system_accent());
|
||||
}
|
||||
|
||||
static void calc_format_display(CalcState* cs) {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/config.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
|
||||
@@ -199,33 +199,7 @@ static void music_set_accent(Color accent) {
|
||||
}
|
||||
|
||||
static void music_load_accent() {
|
||||
music_set_accent(colors::ACCENT);
|
||||
|
||||
char user[64];
|
||||
montauk::memset(user, 0, sizeof(user));
|
||||
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
|
||||
auto doc = montauk::config::load_user(user, "desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
music_set_accent(Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF)));
|
||||
doc.destroy();
|
||||
return;
|
||||
}
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
auto doc = montauk::config::load("desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
music_set_accent(Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF)));
|
||||
}
|
||||
doc.destroy();
|
||||
music_set_accent(mtk::load_system_accent());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Makefile for network (standalone Network configuration app) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-MMD -MP \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/network/network.elf
|
||||
LIBS := $(LIBDIR)/libc/liblibc.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(BINDIR)/apps/network
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
-include $(OBJS:.o=.d)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,754 @@
|
||||
/*
|
||||
* 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];
|
||||
int cursor;
|
||||
};
|
||||
|
||||
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", {}, 0},
|
||||
{"Subnet Mask", {}, 0},
|
||||
{"Gateway", {}, 0},
|
||||
{"DNS Server", {}, 0},
|
||||
};
|
||||
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 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;
|
||||
field.cursor = 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++) {
|
||||
g_fields[i].cursor = 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 int field_cursor_from_x(int index, int mx) {
|
||||
if (index < 0 || index >= FIELD_COUNT) return 0;
|
||||
|
||||
Rect input = field_input_rect(index);
|
||||
int rel_x = mx - (input.x + 8);
|
||||
if (rel_x <= 0) return 0;
|
||||
|
||||
Field& field = g_fields[index];
|
||||
int len = str_len(field.value);
|
||||
char prefix[FIELD_TEXT_CAP] = {};
|
||||
int prev_w = 0;
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
prefix[i] = field.value[i];
|
||||
prefix[i + 1] = '\0';
|
||||
int cur_w = text_width(prefix);
|
||||
int midpoint = prev_w + (cur_w - prev_w) / 2;
|
||||
if (rel_x < midpoint) return i;
|
||||
prev_w = cur_w;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
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].cursor, g_focus_field == i,
|
||||
false, theme, FIELD_H);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
static bool handle_click(int mx, int my) {
|
||||
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 (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 (field_input_rect(i).contains(mx, my)) {
|
||||
focus_field(i, field_cursor_from_x(i, mx));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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 void delete_before_cursor(Field* field) {
|
||||
if (!field || field->cursor <= 0) return;
|
||||
int len = str_len(field->value);
|
||||
for (int i = field->cursor - 1; i < len; i++) {
|
||||
field->value[i] = field->value[i + 1];
|
||||
}
|
||||
field->cursor--;
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
static void delete_at_cursor(Field* field) {
|
||||
if (!field) return;
|
||||
int len = str_len(field->value);
|
||||
if (field->cursor >= len) return;
|
||||
for (int i = field->cursor; i < len; i++) {
|
||||
field->value[i] = field->value[i + 1];
|
||||
}
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
static void insert_char(Field* field, char ch) {
|
||||
if (!field) return;
|
||||
if (!((ch >= '0' && ch <= '9') || ch == '.')) return;
|
||||
int len = str_len(field->value);
|
||||
if (len >= FIELD_TEXT_CAP - 1) return;
|
||||
for (int i = len; i >= field->cursor; i--) {
|
||||
field->value[i + 1] = field->value[i];
|
||||
}
|
||||
field->value[field->cursor++] = ch;
|
||||
g_dirty = 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 len = str_len(field->value);
|
||||
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
delete_before_cursor(field);
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x53) {
|
||||
delete_at_cursor(field);
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4B) {
|
||||
if (field->cursor > 0) field->cursor--;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4D) {
|
||||
if (field->cursor < len) field->cursor++;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x47) {
|
||||
field->cursor = 0;
|
||||
return true;
|
||||
}
|
||||
if (key.scancode == 0x4F) {
|
||||
field->cursor = len;
|
||||
return true;
|
||||
}
|
||||
if (is_ipv4_input_char(key.ascii)) {
|
||||
insert_char(field, key.ascii);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
if (clicked && handle_click(ev.mouse.x, ev.mouse.y)) {
|
||||
redraw = true;
|
||||
}
|
||||
} else if (ev.type == 0) {
|
||||
redraw = handle_key(ev.key);
|
||||
}
|
||||
|
||||
if (redraw) render();
|
||||
}
|
||||
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[app]
|
||||
name = "Network"
|
||||
binary = "network.elf"
|
||||
icon = "network-wired.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = false
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/svg.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
@@ -166,33 +167,7 @@ static mtk::Theme printers_theme() {
|
||||
}
|
||||
|
||||
static void load_accent() {
|
||||
set_accent(colors::ACCENT);
|
||||
|
||||
char user[64];
|
||||
montauk::memset(user, 0, sizeof(user));
|
||||
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
|
||||
auto doc = montauk::config::load_user(user, "desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
set_accent(Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF)));
|
||||
doc.destroy();
|
||||
return;
|
||||
}
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
auto doc = montauk::config::load("desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
set_accent(Color::from_rgb(
|
||||
(uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF)));
|
||||
}
|
||||
doc.destroy();
|
||||
set_accent(mtk::load_system_accent());
|
||||
}
|
||||
|
||||
static void load_icons() {
|
||||
|
||||
@@ -277,7 +277,7 @@ static int process_command(const char* line) {
|
||||
}
|
||||
|
||||
if (streq(cmd, "exit")) {
|
||||
montauk::print("Goodbye.\n");
|
||||
montauk::print("Have a pleasant day.\n");
|
||||
montauk::exit(last_exit);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
@@ -116,30 +117,7 @@ static void apply_scale(int scale) {
|
||||
}
|
||||
|
||||
static void load_accent() {
|
||||
g_accent = colors::ACCENT;
|
||||
|
||||
char user[64] = {};
|
||||
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
|
||||
auto doc = montauk::config::load_user(user, "desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
g_accent = Color::from_rgb((uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF));
|
||||
doc.destroy();
|
||||
return;
|
||||
}
|
||||
doc.destroy();
|
||||
}
|
||||
|
||||
auto doc = montauk::config::load("desktop");
|
||||
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
||||
if (accent >= 0) {
|
||||
g_accent = Color::from_rgb((uint8_t)((accent >> 16) & 0xFF),
|
||||
(uint8_t)((accent >> 8) & 0xFF),
|
||||
(uint8_t)(accent & 0xFF));
|
||||
}
|
||||
doc.destroy();
|
||||
g_accent = mtk::load_system_accent();
|
||||
}
|
||||
|
||||
static void build_key(char* out, int cap, const char* base, const char* field) {
|
||||
|
||||
Reference in New Issue
Block a user