diff --git a/kernel/src/Api/Net.hpp b/kernel/src/Api/Net.hpp index c4b94bd..7ab6973 100644 --- a/kernel/src/Api/Net.hpp +++ b/kernel/src/Api/Net.hpp @@ -107,6 +107,43 @@ namespace Montauk { out->dnsServer = Net::GetDnsServer(); } + static void CopyNetStatusDriver(char* dst, const char* src) { + static constexpr uint64_t DriverNameBytes = 32; + if (!dst) return; + uint64_t i = 0; + for (; i < DriverNameBytes - 1 && src && src[i]; i++) { + dst[i] = src[i]; + } + dst[i] = '\0'; + } + + static void Sys_NetStatus(NetStatus* out) { + if (out == nullptr) return; + + out->initialized = 0; + out->linkUp = 0; + out->polling = 0; + out->_pad0 = 0; + out->rxPackets = 0; + out->txPackets = 0; + CopyNetStatusDriver(out->driver, "No adapter"); + + if (Drivers::Net::E1000::IsInitialized()) { + out->initialized = 1; + out->linkUp = Drivers::Net::E1000::IsLinkUp() ? 1 : 0; + out->rxPackets = Drivers::Net::E1000::GetRxPacketCount(); + out->txPackets = Drivers::Net::E1000::GetTxPacketCount(); + CopyNetStatusDriver(out->driver, "Intel 82540EM"); + } else if (Drivers::Net::E1000E::IsInitialized()) { + out->initialized = 1; + out->linkUp = Drivers::Net::E1000E::IsLinkUp() ? 1 : 0; + out->polling = Drivers::Net::E1000E::RequiresPolling() ? 1 : 0; + out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount(); + out->txPackets = Drivers::Net::E1000E::GetTxPacketCount(); + CopyNetStatusDriver(out->driver, "Intel e1000e"); + } + } + static int Sys_SetNetCfg(const NetCfg* in) { if (in == nullptr) return -1; Net::SetIpAddress(in->ipAddress); diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 7b19f15..6ef2377 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -21,7 +21,7 @@ #include "Keyboard.hpp" // SYS_ISKEYAVAILABLE, SYS_GETKEY, SYS_GETCHAR #include "Info.hpp" // SYS_GETINFO #include "Graphics.hpp" // SYS_FBINFO, SYS_FBMAP, SYS_TERMSIZE, SYS_TERMSCALE -#include "Net.hpp" // SYS_PING, SYS_SOCKET, SYS_CONNECT, SYS_BIND, SYS_LISTEN, SYS_ACCEPT, SYS_SEND, SYS_RECV, SYS_CLOSESOCK, SYS_SENDTO, SYS_RECVFROM, SYS_GETNETCFG, SYS_SETNETCFG, SYS_RESOLVE +#include "Net.hpp" // SYS_PING, SYS_SOCKET, SYS_CONNECT, SYS_BIND, SYS_LISTEN, SYS_ACCEPT, SYS_SEND, SYS_RECV, SYS_CLOSESOCK, SYS_SENDTO, SYS_RECVFROM, SYS_GETNETCFG, SYS_SETNETCFG, SYS_RESOLVE, SYS_NETSTATUS #include "Power.hpp" // SYS_RESET, SYS_SHUTDOWN, SYS_SUSPEND #include "Mouse.hpp" // SYS_MOUSESTATE, SYS_SETMOUSEBOUNDS #include "IoRedir.hpp" // SYS_SPAWN_REDIR, SYS_CHILDIO_READ, SYS_CHILDIO_WRITE, SYS_CHILDIO_WRITEKEY, SYS_CHILDIO_SETTERMSZ @@ -175,6 +175,10 @@ namespace Montauk { case SYS_SETNETCFG: if (!UserMemory::Readable(frame->arg1)) return -1; return (int64_t)Sys_SetNetCfg((const NetCfg*)frame->arg1); + case SYS_NETSTATUS: + if (!UserMemory::Writable(frame->arg1)) return -1; + Sys_NetStatus((NetStatus*)frame->arg1); + return 0; case SYS_SENDTO: if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return -1; return (int64_t)Sys_SendTo((int)frame->arg1, (const uint8_t*)frame->arg2, diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 0e35615..bb05dbe 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -225,6 +225,9 @@ namespace Montauk { /* Filesystem.hpp */ static constexpr uint64_t SYS_DRIVELABEL = 124; + /* Net.hpp */ + static constexpr uint64_t SYS_NETSTATUS = 125; + static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024; static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0; @@ -269,6 +272,16 @@ namespace Montauk { uint32_t dnsServer; // network byte order }; + struct NetStatus { + uint8_t initialized; + uint8_t linkUp; + uint8_t polling; + uint8_t _pad0; + char driver[32]; + uint64_t rxPackets; + uint64_t txPackets; + }; + struct KeyEvent { uint8_t scancode; char ascii; diff --git a/kernel/src/Drivers/Net/E1000.cpp b/kernel/src/Drivers/Net/E1000.cpp index 6fa7ab0..c79e48b 100644 --- a/kernel/src/Drivers/Net/E1000.cpp +++ b/kernel/src/Drivers/Net/E1000.cpp @@ -502,8 +502,21 @@ namespace Drivers::Net::E1000 { return g_initialized; } + bool IsLinkUp() { + if (!g_initialized || g_mmioBase == nullptr) return false; + return (ReadReg(REG_STATUS) & (1 << 1)) != 0; + } + + uint64_t GetRxPacketCount() { + return g_rxPacketCount; + } + + uint64_t GetTxPacketCount() { + return g_txPacketCount; + } + void SetRxCallback(RxCallback callback) { g_rxCallback = callback; } -}; \ No newline at end of file +}; diff --git a/kernel/src/Drivers/Net/E1000.hpp b/kernel/src/Drivers/Net/E1000.hpp index 3f67a1e..3e21332 100644 --- a/kernel/src/Drivers/Net/E1000.hpp +++ b/kernel/src/Drivers/Net/E1000.hpp @@ -115,10 +115,17 @@ namespace Drivers::Net::E1000 { // Check if the device was found and initialized bool IsInitialized(); + // Read current carrier/link state from the device status register + bool IsLinkUp(); + + // Packet counters maintained by the driver + uint64_t GetRxPacketCount(); + uint64_t GetTxPacketCount(); + // RX callback type: called with (packet data, length) using RxCallback = void(*)(const uint8_t* data, uint16_t length); // Register a callback for received packets void SetRxCallback(RxCallback callback); -}; \ No newline at end of file +}; diff --git a/kernel/src/Drivers/Net/E1000E.cpp b/kernel/src/Drivers/Net/E1000E.cpp index 9377853..c9c1d27 100644 --- a/kernel/src/Drivers/Net/E1000E.cpp +++ b/kernel/src/Drivers/Net/E1000E.cpp @@ -740,10 +740,23 @@ namespace Drivers::Net::E1000E { return g_initialized; } + bool IsLinkUp() { + if (!g_initialized || g_mmioBase == nullptr) return false; + return (ReadReg(REG_STATUS) & (1 << 1)) != 0; + } + bool RequiresPolling() { return g_initialized && g_pollingMode; } + uint64_t GetRxPacketCount() { + return g_rxPacketCount; + } + + uint64_t GetTxPacketCount() { + return g_txPacketCount; + } + void SetRxCallback(RxCallback callback) { g_rxCallback = callback; } diff --git a/kernel/src/Drivers/Net/E1000E.hpp b/kernel/src/Drivers/Net/E1000E.hpp index 32ad7cb..9275d1c 100644 --- a/kernel/src/Drivers/Net/E1000E.hpp +++ b/kernel/src/Drivers/Net/E1000E.hpp @@ -149,9 +149,16 @@ namespace Drivers::Net::E1000E { // Check if the device was found and initialized bool IsInitialized(); + // Read current carrier/link state from the device status register + bool IsLinkUp(); + // Returns true only when the driver had to fall back to timer polling. bool RequiresPolling(); + // Packet counters maintained by the driver + uint64_t GetRxPacketCount(); + uint64_t GetTxPacketCount(); + // RX callback type: called with (packet data, length) using RxCallback = void(*)(const uint8_t* data, uint16_t length); diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 7f27e03..cca346e 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs test_dialogs test_dl libloader libhello crashpad +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth network terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs test_dialogs test_dl libloader libhello crashpad SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, apps go to bin/apps//. @@ -86,9 +86,9 @@ CONFIGDST := $(patsubst $(CONFIGDIR)/%,$(BINDIR)/config/%,$(CONFIGSRC)) # Common shared assets (wallpapers, etc.) COMMONKEEP := $(BINDIR)/common/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth network terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad -all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(COMMONKEEP) +all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth network terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(COMMONKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -116,6 +116,7 @@ libloader: libc # Build shared libraries (libmath.lib etc). libs: libloader $(MAKE) -C libs/libmath + $(MAKE) -C libs/libnetworkapplet $(MAKE) -C libs/libprintersapplet $(MAKE) -C libs/libtimezonesapplet @@ -195,6 +196,10 @@ video: libc bluetooth: libc $(MAKE) -C src/bluetooth +# Build network manager standalone GUI tool (depends on libc). +network: libc + $(MAKE) -C src/network + # Build terminal standalone GUI tool (depends on libc). terminal: libc $(MAKE) -C src/terminal @@ -304,7 +309,7 @@ test_dialogs: libc libloader dialogs $(MAKE) -C src/test_dialogs # Install app bundles (manifests, icons, data files) into bin/apps//. -install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot printers timezone crashpad +install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth network terminal klog procmgr calculator screenshot texteditor mandelbrot printers timezone crashpad ../scripts/install_apps.sh # Copy man pages into bin/man/ so mkramdisk.sh picks them up. @@ -364,6 +369,7 @@ clean: $(MAKE) -C src/music clean $(MAKE) -C src/video clean $(MAKE) -C src/bluetooth clean + $(MAKE) -C src/network clean $(MAKE) -C src/terminal clean $(MAKE) -C src/klog clean $(MAKE) -C src/procmgr clean @@ -382,6 +388,7 @@ clean: $(MAKE) -C src/libloader clean $(MAKE) -C libs/libmath clean $(MAKE) -C libs/libhello clean + $(MAKE) -C libs/libnetworkapplet clean $(MAKE) -C libs/libprintersapplet clean $(MAKE) -C libs/libtimezonesapplet clean $(MAKE) -C src/test_dialogs clean diff --git a/programs/data/config/desktop-applets.toml b/programs/data/config/desktop-applets.toml index ce64cb6..416c4ff 100644 --- a/programs/data/config/desktop-applets.toml +++ b/programs/data/config/desktop-applets.toml @@ -4,6 +4,12 @@ library = "0:/os/libprintersapplet.lib" icon = "0:/icons/printer.svg" enabled = true +[applets.network] +label = "Network" +library = "0:/os/libnetworkapplet.lib" +icon = "0:/icons/network-wired.svg" +enabled = true + [applets.timezone] label = "Time Zone" library = "0:/os/libtimezonesapplet.lib" diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index c5d7237..0692b72 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -158,6 +158,7 @@ namespace Montauk { static constexpr uint64_t SYS_CLIPBOARD_CLEAR = 122; static constexpr uint64_t SYS_INPUT_WAIT = 123; static constexpr uint64_t SYS_DRIVELABEL = 124; + static constexpr uint64_t SYS_NETSTATUS = 125; static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024; @@ -188,6 +189,16 @@ namespace Montauk { uint32_t dnsServer; // network byte order }; + struct NetStatus { + uint8_t initialized; + uint8_t linkUp; + uint8_t polling; + uint8_t _pad0; + char driver[32]; + uint64_t rxPackets; + uint64_t txPackets; + }; + struct DateTime { uint16_t Year; uint8_t Month; diff --git a/programs/include/gui/mtk/settings.hpp b/programs/include/gui/mtk/settings.hpp new file mode 100644 index 0000000..bc11cb8 --- /dev/null +++ b/programs/include/gui/mtk/settings.hpp @@ -0,0 +1,43 @@ +/* + * settings.hpp + * Montauk Toolkit helpers for loading desktop appearance settings + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once + +#include +#include +#include +#include "gui/gui.hpp" + +namespace gui::mtk { + +inline Color color_from_rgb_int(int64_t rgb, Color fallback = colors::ACCENT) { + if (rgb < 0) return fallback; + return Color::from_rgb((uint8_t)((rgb >> 16) & 0xFF), + (uint8_t)((rgb >> 8) & 0xFF), + (uint8_t)(rgb & 0xFF)); +} + +inline Color load_system_accent(Color fallback = 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) { + Color color = color_from_rgb_int(accent, fallback); + doc.destroy(); + return color; + } + doc.destroy(); + } + + auto doc = montauk::config::load("desktop"); + int64_t accent = doc.get_int("appearance.accent_color", -1); + Color color = color_from_rgb_int(accent, fallback); + doc.destroy(); + return color; +} + +} // namespace gui::mtk diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 383ac8a..7007f3f 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -197,6 +197,7 @@ namespace montauk { // Network configuration inline void get_netcfg(Montauk::NetCfg* out) { syscall1(Montauk::SYS_GETNETCFG, (uint64_t)out); } inline int set_netcfg(const Montauk::NetCfg* cfg) { return (int)syscall1(Montauk::SYS_SETNETCFG, (uint64_t)cfg); } + inline int net_status(Montauk::NetStatus* out) { return (int)syscall1(Montauk::SYS_NETSTATUS, (uint64_t)out); } // Sockets inline int socket(int type) { diff --git a/programs/libs/libnetworkapplet/Makefile b/programs/libs/libnetworkapplet/Makefile new file mode 100644 index 0000000..d760213 --- /dev/null +++ b/programs/libs/libnetworkapplet/Makefile @@ -0,0 +1,69 @@ +# Makefile for the Network shared desktop applet +# 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++ + LD := $(TOOLCHAIN_PREFIX)ld +else + CXX := g++ + LD := ld +endif + +OUTDIR := ../../bin/os +OBJDIR := obj +TARGET := $(OUTDIR)/libnetworkapplet.lib + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fPIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -mno-red-zone \ + -mcmodel=small \ + -MMD -MP \ + -I ../../include \ + -isystem ../../include/libc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +LDFLAGS := \ + -shared \ + --build-id=none \ + --hash-style=sysv \ + -m elf_x86_64 \ + -z max-page-size=0x1000 + +SRCS := src/libnetworkapplet.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) Makefile + mkdir -p $(OUTDIR) + $(LD) $(LDFLAGS) $(OBJS) -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(dir $@) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +-include $(OBJS:.o=.d) + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/libs/libnetworkapplet/src/libnetworkapplet.cpp b/programs/libs/libnetworkapplet/src/libnetworkapplet.cpp new file mode 100644 index 0000000..e0ff8de --- /dev/null +++ b/programs/libs/libnetworkapplet/src/libnetworkapplet.cpp @@ -0,0 +1,28 @@ +/* + * libnetworkapplet.cpp + * Shared desktop applet that launches the Network configuration tool + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include + +using gui::desktop_applet::Descriptor; +using gui::desktop_applet::Host; + +static constexpr Descriptor kAppletDescriptor = { + gui::desktop_applet::ABI_VERSION, + gui::desktop_applet::CAP_SYSTEM_CONFIGURATION, + "network", + "Network", + "0:/icons/network-wired.svg", +}; + +extern "C" const Descriptor* desktop_applet_query_v1() { + return &kAppletDescriptor; +} + +extern "C" bool desktop_applet_open_v1(const Host* host) { + (void)host; + return montauk::spawn("0:/apps/network/network.elf") >= 0; +} diff --git a/programs/man/syscalls.2 b/programs/man/syscalls.2 index 0695d0e..3036909 100644 --- a/programs/man/syscalls.2 +++ b/programs/man/syscalls.2 @@ -129,6 +129,11 @@ Set the network configuration (IP, mask, gateway, DNS server). int montauk::set_netcfg(const Montauk::NetCfg* cfg); +.B SYS_NETSTATUS (125) + Get adapter status including driver name, link state, polling mode, + and RX/TX packet counters. + int montauk::net_status(Montauk::NetStatus* out); + .B SYS_RESOLVE (44) Resolve a hostname to an IPv4 address via DNS. Sends a UDP query to the configured DNS server and waits up to 5 seconds diff --git a/programs/src/calculator/main.cpp b/programs/src/calculator/main.cpp index 88e17ec..ad7806e 100644 --- a/programs/src/calculator/main.cpp +++ b/programs/src/calculator/main.cpp @@ -6,12 +6,12 @@ */ #include -#include #include #include #include #include #include +#include #include #include @@ -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) { diff --git a/programs/src/music/main.cpp b/programs/src/music/main.cpp index 2cb67b8..c2385ea 100644 --- a/programs/src/music/main.cpp +++ b/programs/src/music/main.cpp @@ -7,8 +7,8 @@ #include #include #include -#include #include +#include #include #include @@ -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()); } // ============================================================================ diff --git a/programs/src/network/Makefile b/programs/src/network/Makefile new file mode 100644 index 0000000..2260239 --- /dev/null +++ b/programs/src/network/Makefile @@ -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) diff --git a/programs/src/network/main.cpp b/programs/src/network/main.cpp new file mode 100644 index 0000000..98a0173 --- /dev/null +++ b/programs/src/network/main.cpp @@ -0,0 +1,754 @@ +/* + * 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]; + 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); +} diff --git a/programs/src/network/manifest.toml b/programs/src/network/manifest.toml new file mode 100644 index 0000000..2b8747b --- /dev/null +++ b/programs/src/network/manifest.toml @@ -0,0 +1,8 @@ +[app] +name = "Network" +binary = "network.elf" +icon = "network-wired.svg" + +[menu] +category = "System" +visible = false diff --git a/programs/src/network/stb_truetype_impl.cpp b/programs/src/network/stb_truetype_impl.cpp new file mode 100644 index 0000000..dd91785 --- /dev/null +++ b/programs/src/network/stb_truetype_impl.cpp @@ -0,0 +1,33 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +#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 diff --git a/programs/src/printers/main.cpp b/programs/src/printers/main.cpp index a662be3..1169782 100644 --- a/programs/src/printers/main.cpp +++ b/programs/src/printers/main.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -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() { diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp index 913207d..b1b8b55 100644 --- a/programs/src/shell/main.cpp +++ b/programs/src/shell/main.cpp @@ -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); } diff --git a/programs/src/timezone/main.cpp b/programs/src/timezone/main.cpp index 3661b8e..93ab7ec 100644 --- a/programs/src/timezone/main.cpp +++ b/programs/src/timezone/main.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -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) { diff --git a/scripts/install_apps.sh b/scripts/install_apps.sh index 1804f1c..23091e4 100755 --- a/scripts/install_apps.sh +++ b/scripts/install_apps.sh @@ -30,6 +30,7 @@ APPS=( "music|apps/scalable/audio-player.svg" "video|apps/scalable/video-player.svg" "bluetooth|apps/scalable/bluetooth.svg" + "network|devices/scalable/network-wired.svg" "terminal|apps/scalable/utilities-terminal.svg" "klog|apps/scalable/utilities-terminal.svg" "procmgr|apps/scalable/system-monitor.svg" diff --git a/template/docs/syscalls.md b/template/docs/syscalls.md index 7b688d2..3e7dcef 100644 --- a/template/docs/syscalls.md +++ b/template/docs/syscalls.md @@ -274,6 +274,7 @@ int32_t ping(uint32_t ip, uint32_t timeoutMs); // Ping, returns RTT in ms (-1 uint32_t resolve(const char* hostname); // DNS lookup, returns IPv4 (0 on failure) void get_netcfg(NetCfg* out); // Get network config int set_netcfg(const NetCfg* cfg); // Set network config +int net_status(NetStatus* out); // Get link/driver/counter status ``` ### Sockets diff --git a/template/sysroot/include/Api/Syscall.hpp b/template/sysroot/include/Api/Syscall.hpp index a941755..581d864 100644 --- a/template/sysroot/include/Api/Syscall.hpp +++ b/template/sysroot/include/Api/Syscall.hpp @@ -58,6 +58,7 @@ namespace Montauk { static constexpr uint64_t SYS_FMKDIR = 78; static constexpr uint64_t SYS_DRIVELIST = 79; static constexpr uint64_t SYS_DRIVELABEL = 124; + static constexpr uint64_t SYS_NETSTATUS = 125; static constexpr uint64_t SYS_TERMSCALE = 43; static constexpr uint64_t SYS_RESOLVE = 44; static constexpr uint64_t SYS_GETRANDOM = 45; @@ -142,6 +143,16 @@ namespace Montauk { uint32_t dnsServer; // network byte order }; + struct NetStatus { + uint8_t initialized; + uint8_t linkUp; + uint8_t polling; + uint8_t _pad0; + char driver[32]; + uint64_t rxPackets; + uint64_t txPackets; + }; + struct DateTime { uint16_t Year; uint8_t Month; diff --git a/template/sysroot/include/montauk/syscall.h b/template/sysroot/include/montauk/syscall.h index 614afd1..eb0588a 100644 --- a/template/sysroot/include/montauk/syscall.h +++ b/template/sysroot/include/montauk/syscall.h @@ -185,6 +185,7 @@ namespace montauk { // Network configuration inline void get_netcfg(Montauk::NetCfg* out) { syscall1(Montauk::SYS_GETNETCFG, (uint64_t)out); } inline int set_netcfg(const Montauk::NetCfg* cfg) { return (int)syscall1(Montauk::SYS_SETNETCFG, (uint64_t)cfg); } + inline int net_status(Montauk::NetStatus* out) { return (int)syscall1(Montauk::SYS_NETSTATUS, (uint64_t)out); } // Sockets inline int socket(int type) {