Compare commits

...
13 Commits
174 changed files with 6121 additions and 1373 deletions

No files matched your search

+5
View File
@@ -252,6 +252,10 @@ ifeq ($(ARCH),loongarch64)
endif endif
rm -rf iso_root rm -rf iso_root
# Example and regression-test programs that are built into 0:/os for
# development but have no place on a release image.
RELEASE_EXCLUDE_PROGRAMS := rusthello.elf futextest.elf memtest.elf
.PHONY: release .PHONY: release
release: limine/limine kernel ramdisk release: limine/limine kernel ramdisk
rm -rf iso_root .release-tmp rm -rf iso_root .release-tmp
@@ -281,6 +285,7 @@ ifeq ($(RELEASE_WITHOUT_SDK),1)
rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \ rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk .release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
endif endif
rm -f $(addprefix .release-tmp/os/,$(RELEASE_EXCLUDE_PROGRAMS))
find .release-tmp -name '*.elf' -exec strip {} + find .release-tmp -name '*.elf' -exec strip {} +
# The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is # The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is
# the image the Installer lays down on the target disk -- and it is not named # the image the Installer lays down on the target disk -- and it is not named
+75
View File
@@ -1,3 +1,78 @@
MontaukOS 0.2.0 (October 14, 2026)
(278b20825815ec04a22fd1c2fcfedbd38e116e6d)
New
====
* Capability (application permissions) model added
* Consent prompts for missing app permissions
* New Capabilities app for reviewing and managing app permissions
* Unified system status codes; the shell and Files now print a readable message on failure
* Text is now UTF-8 throughout, with Unicode key events and dead keys
* Keyboard layout support, with English, Norwegian, Czech, Swedish, and Danish layouts
* NumLock support
* Battery percentage reporting via ACPI, with a battery indicator in the desktop panel
* Display brightness control, including on hybrid graphics laptops, with a slider in the desktop panel
* Discrete GPUs are powered down automatically on hybrid graphics laptops
* ext2 block cache with readahead, speeding up disk-heavy work such as on-OS compilation
* Userspace system logging: klog is now syslog, logs identify the user and program, and system services log through montauk::service_log
* The system log is written to disk (0:/state/logs) during shutdown
* Desktop sessions now own the processes started in them, and those processes are ended on logout
* Rust userspace support (no_std)
* Futex syscalls (SYS_FUTEX_WAIT/SYS_FUTEX_WAKE) as groundwork for Rust std and pthreads
* Process termination codes; abort() now ends the whole process from any thread
* isatty() now reports whether a terminal is actually attached
* Settings panel and a Pure Black theme in Terminal (the default for Console sessions)
* Syntax highlighting for C++, HTML, CSS, and JavaScript in edit and Text Editor
* Expanded SVG renderer
* New vol command for listing mounted volumes
* Revamped desktop application categories
* Fonts, icons, and manual pages moved to 0:/os; persistent state now lives in 0:/state
* RTL-SDR driver moved from the kernel to userspace
Fixed
=====
* Opening a directory as a file is now rejected on ext2 and FAT32, fixing on-OS gcc on installed systems
* Fixed several syscall result and status code comparisons
* Fixed a TLS layout bug for binaries whose TLS segment has an alignment below 16
* A fault in one thread now ends the whole process instead of leaving its sibling threads running
* Fixed an ACPI hardware ID endianness bug
* Fixed a race in Wi-Fi event processing and other Wi-Fi bugs
* DHCP no longer runs when no network device has an active link
* Reduced idle CPU wakeups and deferred-work polling
* Redirected consoles are now inherited by processes spawned with capabilities
* Fixed numpad navigation
* Fixed a timezone formatting bug
* Fixed the login wallpaper startup delay
* Fixed text overlap in file dialogs
* Persisted kernel logs are no longer double-spaced
* Files lists now scroll with the arrow keys, and volume labels are consistent
* mbrtowc added to libc
Included in ISO
================
Two x86_64 images are published for this release.
montauk-0.2.0-sdk-x86_64.iso additionally carries the
Montauk SDK (gcc, binutils, etc.)
* MontaukOS SMP kernel
* Userspace bootstrap (0:/os/init.elf) and other essential system utilities
* MontaukOS desktop environment and graphical apps
* NetSurf web browser (0:/apps/netsurf)
* Manual pages (0:/os/man), readable via man command
* Service and system configuration (0:/config: init.toml, capabilities.toml, ssh.toml, ntp.toml)
* System CA certificates (0:/os/certs/ca-certificates.crt)
* Intel Bluetooth controller and Wi-Fi (iwlwifi) firmware (0:/os/firmware/intel)
* Third-party license texts and notices (0:/os/licenses)
* Tiny C Compiler (tcc) with MontaukOS headers and libraries (0:/sdk/tcc)
* Lua 5.4 interpreter (lua) with headers and static library (0:/sdk/lua)
* Montauk SDK - native GCC/G++ and binutils (0:/sdk) - SDK image only
* Flat Remix icon pack
* JetBrains Mono, Noto Serif, Roboto, and C059 Roman (open Century Schoolbook) TrueType fonts
* DOOM engine with SDL_mixer audio support (game data not included)
* Experimental HTTP server (httpd) and default page (0:/www/index.html)
* Default wallpaper - blossoms by Nikhil Kumar, Unsplash (0:/os/wallpapers)
MontaukOS 0.1.9 (August 13, 2026) MontaukOS 0.1.9 (August 13, 2026)
(a28b8ad0b2932203b812e967f35f029f060fd6b5) (a28b8ad0b2932203b812e967f35f029f060fd6b5)
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 288 #define MONTAUK_BUILD_NUMBER 4
+19
View File
@@ -27,6 +27,23 @@ namespace montauk::abi {
return required == 0 || Sched::HasCapability(required); return required == 0 || Sched::HasCapability(required);
} }
// Drivers fail a create under a missing parent with a bare -1; report it
// as NoSuchPath so callers can tell it apart (Rust's create_dir_all only
// creates the missing ancestors when mkdir says NotFound).
static bool ParentDirExists(const char* resolved) {
char parent[256];
int len = Lib::strlen(resolved);
if (len >= (int)sizeof(parent)) return false;
memcpy(parent, resolved, len + 1);
while (len > 0 && parent[len - 1] == '/') len--;
while (len > 0 && parent[len - 1] != '/') len--;
while (len > 0 && parent[len - 1] == '/') len--;
if (len == 0 || parent[len - 1] == ':') return true; // drive root
parent[len] = '\0';
Fs::Vfs::StatInfo info;
return Fs::Vfs::VfsStat(parent, info) == 0 && info.isDir;
}
static int Sys_Open(const char* path) { static int Sys_Open(const char* path) {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
@@ -117,6 +134,7 @@ namespace montauk::abi {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError; if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (!ParentDirExists(resolved)) return NoSuchPath;
return Ipc::CreateFileHandle(resolved); return Ipc::CreateFileHandle(resolved);
} }
@@ -171,6 +189,7 @@ namespace montauk::abi {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError; if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (!ParentDirExists(resolved)) return NoSuchPath;
return Fs::Vfs::VfsMkdir(resolved); return Fs::Vfs::VfsMkdir(resolved);
} }
+2 -2
View File
@@ -17,13 +17,13 @@ namespace montauk::abi {
// Copy strings into fixed-size arrays (user-accessible) // Copy strings into fixed-size arrays (user-accessible)
const char* name = "MontaukOS"; const char* name = "MontaukOS";
const char* ver = "0.2.0"; const char* ver = "0.2.1";
for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i]; for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i];
outInfo->osName[9] = '\0'; outInfo->osName[9] = '\0';
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i]; for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
outInfo->osVersion[5] = '\0'; outInfo->osVersion[5] = '\0';
outInfo->apiVersion = 11; outInfo->apiVersion = 12;
outInfo->maxProcesses = Sched::MaxProcesses; outInfo->maxProcesses = Sched::MaxProcesses;
outInfo->buildNumber = MONTAUK_BUILD_NUMBER; outInfo->buildNumber = MONTAUK_BUILD_NUMBER;
} }
+20 -3
View File
@@ -122,8 +122,9 @@ namespace montauk::abi {
return WriteAllToStream(stream, (const uint8_t*)data, len); return WriteAllToStream(stream, (const uint8_t*)data, len);
} }
static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) { // Queue a key event (raw bytes, `len` of them) on a redirected child's key
if (key == nullptr) return InvalidArgument; // mailbox. Readers accept both the 9-byte legacy and the full format.
static int ChildIoQueueKey(int childPid, const void* key, uint16_t len) {
auto* child = Sched::GetProcessByPid(childPid); auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid()) if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return PermissionDenied; return PermissionDenied;
@@ -133,7 +134,7 @@ namespace montauk::abi {
for (;;) { for (;;) {
uint64_t observedWake = Sched::ObserveObjectWake(mailbox); uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
int rc = Ipc::MailboxSend(mailbox, 0, key, sizeof(KeyEvent)); int rc = Ipc::MailboxSend(mailbox, 0, key, len);
if (rc < 0) return GenericFailure; if (rc < 0) return GenericFailure;
if (rc == 0) { if (rc == 0) {
Sched::BlockOnObjectSince(mailbox, 0, observedWake); Sched::BlockOnObjectSince(mailbox, 0, observedWake);
@@ -143,6 +144,22 @@ namespace montauk::abi {
} }
} }
static int Sys_ChildIoWriteKey(int childPid, const KeyEventV1* key) {
if (key == nullptr) return InvalidArgument;
KeyEventV1 copy = *key;
return ChildIoQueueKey(childPid, &copy, sizeof(copy));
}
// Size-versioned: copies min(size, sizeof(KeyEvent)) bytes (validated
// readable by the dispatcher) and zero-fills the rest.
static int Sys_ChildIoWriteKeyEx(int childPid, const void* key, uint64_t size) {
if (key == nullptr || size < sizeof(KeyEventV1)) return InvalidArgument;
KeyEvent ev;
memset(&ev, 0, sizeof(ev));
memcpy(&ev, key, size < sizeof(ev) ? size : sizeof(ev));
return ChildIoQueueKey(childPid, &ev, sizeof(ev));
}
static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) { static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) {
auto* child = Sched::GetProcessByPid(childPid); auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid()) if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
+137 -22
View File
@@ -12,6 +12,107 @@
#include "Common.hpp" #include "Common.hpp"
namespace montauk::abi { namespace montauk::abi {
// ---- Key text ----------------------------------------------------------
// Keyboard layouts are applied in userspace (desktop, terminal), which send
// KeyEvents carrying the Unicode codepoint. Binaries built before that send
// 9-byte KeyEventV1s whose `ascii` is a Windows-1252 byte; both kinds share
// a redirected child's key mailbox, told apart by message length.
static uint32_t KeyCp1252ToUnicode(uint8_t b) {
static constexpr uint16_t high[32] = {
0x20AC, 0x81, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8D, 0x017D, 0x8F,
0x90, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x9D, 0x017E, 0x0178,
};
return (b >= 0x80 && b <= 0x9F) ? high[b - 0x80] : b;
}
// The character a key event types, or 0 for control/navigation keys.
static uint32_t KeyTextCodepoint(const KeyEvent& ev) {
if (ev.codepoint) return ev.codepoint;
uint8_t b = (uint8_t)ev.ascii;
if (b < 0x20 || b == 0x7F) return 0;
return KeyCp1252ToUnicode(b);
}
static int KeyEncodeUtf8(uint32_t cp, uint8_t* out) {
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = 0xFFFD;
if (cp < 0x80) { out[0] = (uint8_t)cp; return 1; }
if (cp < 0x800) {
out[0] = (uint8_t)(0xC0 | (cp >> 6));
out[1] = (uint8_t)(0x80 | (cp & 0x3F));
return 2;
}
if (cp < 0x10000) {
out[0] = (uint8_t)(0xE0 | (cp >> 12));
out[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[2] = (uint8_t)(0x80 | (cp & 0x3F));
return 3;
}
out[0] = (uint8_t)(0xF0 | (cp >> 18));
out[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[3] = (uint8_t)(0x80 | (cp & 0x3F));
return 4;
}
static void KeyFromDriver(const Drivers::PS2::Keyboard::KeyEvent& k, KeyEvent* out) {
memset(out, 0, sizeof(KeyEvent));
out->scancode = k.Scancode;
out->ascii = k.Ascii;
out->pressed = k.Pressed;
out->shift = k.Shift;
out->ctrl = k.Ctrl;
out->alt = k.Alt;
out->capslock = k.CapsLock;
out->altgr = k.AltGr;
out->extended = k.Extended;
// The kernel's own scancode table is US ASCII.
uint8_t a = (uint8_t)k.Ascii;
out->codepoint = (a >= 0x20 && a < 0x7F) ? a : 0;
}
// Receive one key from a redirected child's mailbox as a full KeyEvent,
// upgrading legacy 9-byte messages. Returns MailboxRecv's result.
static int KeyMailboxRecv(Ipc::Mailbox* mailbox, KeyEvent* out) {
memset(out, 0, sizeof(KeyEvent));
uint16_t len = sizeof(KeyEvent);
int rc = Ipc::MailboxRecv(mailbox, nullptr, out, &len, true);
if (rc > 0 && len < sizeof(KeyEvent)) {
memset((uint8_t*)out + len, 0, sizeof(KeyEvent) - len);
out->codepoint = KeyTextCodepoint(*out);
}
return rc;
}
// Turn a pressed key into the next getchar byte. Characters above ASCII are
// returned as UTF-8, one byte per call; the rest is parked in the process.
static bool KeyTakeChar(Sched::Process* proc, const KeyEvent& ev, char* out) {
if (!ev.pressed) return false;
uint32_t cp = KeyTextCodepoint(ev);
if (cp >= 0x80 && proc != nullptr) {
int n = KeyEncodeUtf8(cp, proc->getcharUtf8);
proc->getcharUtf8Len = (uint8_t)n;
proc->getcharUtf8Pos = 1;
*out = (char)proc->getcharUtf8[0];
return true;
}
if (ev.ascii != 0 && ev.ascii != KEY_ASCII_SUBSTITUTE) {
*out = ev.ascii;
return true;
}
return false;
}
static bool KeyPendingChar(Sched::Process* proc, char* out) {
if (proc == nullptr || proc->getcharUtf8Pos >= proc->getcharUtf8Len) return false;
*out = (char)proc->getcharUtf8[proc->getcharUtf8Pos++];
return true;
}
// ---- Syscalls ------------------------------------------------------------
static bool Sys_IsKeyAvailable() { static bool Sys_IsKeyAvailable() {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc && proc->redirected) { if (proc && proc->redirected) {
@@ -27,8 +128,8 @@ namespace montauk::abi {
return Drivers::PS2::Keyboard::IsKeyAvailable(); return Drivers::PS2::Keyboard::IsKeyAvailable();
} }
static void Sys_GetKey(KeyEvent* outEvent) { // Blocking read of the next key event, in the full format.
if (outEvent == nullptr) return; static void ReadKeyEvent(KeyEvent* outEvent) {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc && proc->redirected) { if (proc && proc->redirected) {
Ipc::HandleSnapshot snapshot; Ipc::HandleSnapshot snapshot;
@@ -36,8 +137,7 @@ namespace montauk::abi {
if (mailbox != nullptr) { if (mailbox != nullptr) {
for (;;) { for (;;) {
uint64_t observedWake = Sched::ObserveObjectWake(mailbox); uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
uint16_t len = sizeof(KeyEvent); int rc = KeyMailboxRecv(mailbox, outEvent);
int rc = Ipc::MailboxRecv(mailbox, nullptr, outEvent, &len, true);
if (rc > 0) return; if (rc > 0) return;
if (rc < 0) { if (rc < 0) {
memset(outEvent, 0, sizeof(KeyEvent)); memset(outEvent, 0, sizeof(KeyEvent));
@@ -47,20 +147,33 @@ namespace montauk::abi {
} }
} }
} }
auto k = Drivers::PS2::Keyboard::GetKey(); KeyFromDriver(Drivers::PS2::Keyboard::GetKey(), outEvent);
outEvent->scancode = k.Scancode; }
outEvent->ascii = k.Ascii;
outEvent->pressed = k.Pressed; // SYS_GETKEY: legacy 9-byte event. A new-format event's first 9 bytes are
outEvent->shift = k.Shift; // already a valid KeyEventV1 (ascii holds the 1252 byte or SUB).
outEvent->ctrl = k.Ctrl; static void Sys_GetKey(KeyEventV1* outEvent) {
outEvent->alt = k.Alt; if (outEvent == nullptr) return;
outEvent->capslock = k.CapsLock; KeyEvent ev;
outEvent->altgr = k.AltGr; ReadKeyEvent(&ev);
outEvent->extended = k.Extended; memcpy(outEvent, &ev, sizeof(KeyEventV1));
}
// SYS_GETKEY_EX: size-versioned. The caller's size was validated by the
// dispatcher (>= sizeof(KeyEventV1), writable).
static int Sys_GetKeyEx(void* outEvent, uint64_t size) {
KeyEvent ev;
ReadKeyEvent(&ev);
uint64_t n = size < sizeof(KeyEvent) ? size : sizeof(KeyEvent);
memcpy(outEvent, &ev, n);
if (size > n) memset((uint8_t*)outEvent + n, 0, size - n);
return Success;
} }
static char Sys_GetChar() { static char Sys_GetChar() {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
char pending;
if (KeyPendingChar(proc, &pending)) return pending;
if (proc && proc->redirected) { if (proc && proc->redirected) {
Ipc::HandleSnapshot inputSnapshot; Ipc::HandleSnapshot inputSnapshot;
Ipc::HandleSnapshot mailboxSnapshot; Ipc::HandleSnapshot mailboxSnapshot;
@@ -78,11 +191,11 @@ namespace montauk::abi {
} }
if (mailbox != nullptr) { if (mailbox != nullptr) {
KeyEvent ev{}; KeyEvent ev;
uint16_t len = sizeof(ev); int rc = KeyMailboxRecv(mailbox, &ev);
int rc = Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true);
if (rc > 0) { if (rc > 0) {
if (ev.pressed && ev.ascii != 0) return ev.ascii; char c;
if (KeyTakeChar(proc, ev, &c)) return c;
continue; continue;
} }
if (rc < 0 && input == nullptr) return 0; if (rc < 0 && input == nullptr) return 0;
@@ -115,6 +228,8 @@ namespace montauk::abi {
*/ */
static char Sys_GetCharNb() { static char Sys_GetCharNb() {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
char pending;
if (KeyPendingChar(proc, &pending)) return pending;
if (proc && proc->redirected) { if (proc && proc->redirected) {
Ipc::HandleSnapshot inputSnapshot; Ipc::HandleSnapshot inputSnapshot;
Ipc::HandleSnapshot mailboxSnapshot; Ipc::HandleSnapshot mailboxSnapshot;
@@ -127,10 +242,10 @@ namespace montauk::abi {
} }
while (mailbox != nullptr) { while (mailbox != nullptr) {
KeyEvent ev{}; KeyEvent ev;
uint16_t len = sizeof(ev); if (KeyMailboxRecv(mailbox, &ev) <= 0) break;
if (Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true) <= 0) break; char c;
if (ev.pressed && ev.ascii != 0) return ev.ascii; if (KeyTakeChar(proc, ev, &c)) return c;
} }
return 0; return 0;
} }
+27
View File
@@ -220,7 +220,26 @@ namespace montauk::abi {
return count; return count;
} }
// Flag every thread of the caller's process with TerminatedAborted, then
// exit this one. A sibling's ExitProcess ends only itself; the main
// thread and the other siblings follow via killPending.
static void Sys_Abort() {
Sched::KillProcess(Sched::GetCurrentPid(), TerminatedAborted);
Sched::ExitProcess();
}
// Like Sys_Abort, but a normal exit with the caller's code: every thread
// goes, and waiters see exitCode & 0xFF, the same as SYS_EXIT on the main
// thread.
static void Sys_ExitGroup(int exitCode) {
Sched::KillProcess(Sched::GetCurrentPid(), exitCode & 0xFF);
Sched::ExitProcess();
}
static int Sys_Kill(int pid) { static int Sys_Kill(int pid) {
// A process may always kill itself (kill(getpid(), ...)); waiters
// see TerminatedKilled. abort() uses SYS_ABORT instead.
if (pid == Sched::GetCurrentPid()) return Sched::KillProcess(pid);
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) { if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) {
int ancestor = pid; int ancestor = pid;
bool descendant = false; bool descendant = false;
@@ -302,6 +321,14 @@ namespace montauk::abi {
return Sched::GetCurrentTid(); return Sched::GetCurrentTid();
} }
static int Sys_FutexWait(uint64_t addr, uint32_t expected, uint64_t timeoutNs) {
return Sched::FutexWait(addr, expected, timeoutNs);
}
static int Sys_FutexWake(uint64_t addr, uint32_t count) {
return Sched::FutexWake(addr, count);
}
static int Sys_Chdir(const char* path) { static int Sys_Chdir(const char* path) {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || path == nullptr) return InvalidArgument; if (proc == nullptr || path == nullptr) return InvalidArgument;
+25 -4
View File
@@ -139,8 +139,8 @@ namespace montauk::abi {
case SYS_ISKEYAVAILABLE: case SYS_ISKEYAVAILABLE:
return (int64_t)Sys_IsKeyAvailable(); return (int64_t)Sys_IsKeyAvailable();
case SYS_GETKEY: case SYS_GETKEY:
if (!UserMemory::Writable<KeyEvent>(frame->arg1)) return BadPointer; if (!UserMemory::Writable<KeyEventV1>(frame->arg1)) return BadPointer;
Sys_GetKey((KeyEvent*)frame->arg1); Sys_GetKey((KeyEventV1*)frame->arg1);
return Success; return Success;
case SYS_GETCHAR: case SYS_GETCHAR:
return (int64_t)Sys_GetChar(); return (int64_t)Sys_GetChar();
@@ -365,8 +365,8 @@ namespace montauk::abi {
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer; if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3); return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3);
case SYS_CHILDIO_WRITEKEY: case SYS_CHILDIO_WRITEKEY:
if (!UserMemory::Readable<KeyEvent>(frame->arg2)) return BadPointer; if (!UserMemory::Readable<KeyEventV1>(frame->arg2)) return BadPointer;
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEvent*)frame->arg2); return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEventV1*)frame->arg2);
case SYS_CHILDIO_SETTERMSZ: case SYS_CHILDIO_SETTERMSZ:
return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3); return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
case SYS_WINCREATE: case SYS_WINCREATE:
@@ -400,6 +400,12 @@ namespace montauk::abi {
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2); return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
case SYS_KILL: case SYS_KILL:
return (int64_t)Sys_Kill((int)frame->arg1); return (int64_t)Sys_Kill((int)frame->arg1);
case SYS_ABORT:
Sys_Abort();
return Success;
case SYS_EXIT_GROUP:
Sys_ExitGroup((int)frame->arg1);
return Success;
case SYS_SETSESSION: case SYS_SETSESSION:
return (int64_t)Sys_SetSession(); return (int64_t)Sys_SetSession();
case SYS_KILLSESSION: case SYS_KILLSESSION:
@@ -527,6 +533,14 @@ namespace montauk::abi {
return Sched::Consent::Next((ConsentRequest*)frame->arg1); return Sched::Consent::Next((ConsentRequest*)frame->arg1);
case SYS_CONSENT_REPLY: case SYS_CONSENT_REPLY:
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2); return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
case SYS_GETKEY_EX:
if (frame->arg2 < sizeof(KeyEventV1) || frame->arg2 > 4096) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return Sys_GetKeyEx((void*)frame->arg1, frame->arg2);
case SYS_CHILDIO_WRITEKEY_EX:
if (frame->arg3 < sizeof(KeyEventV1) || frame->arg3 > 4096) return InvalidArgument;
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
return (int64_t)Sys_ChildIoWriteKeyEx((int)frame->arg1, (const void*)frame->arg2, frame->arg3);
case SYS_THREAD_SPAWN: case SYS_THREAD_SPAWN:
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3); return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
case SYS_THREAD_EXIT: case SYS_THREAD_EXIT:
@@ -537,6 +551,13 @@ namespace montauk::abi {
return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2); return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2);
case SYS_THREAD_SELF: case SYS_THREAD_SELF:
return Sys_ThreadSelf(); return Sys_ThreadSelf();
case SYS_FUTEX_WAIT:
// Validation demand-commits the page so the scheduler can
// read the word with its lock held without faulting.
if (!UserMemory::Readable<uint32_t>(frame->arg1)) return BadPointer;
return Sys_FutexWait(frame->arg1, (uint32_t)frame->arg2, frame->arg3);
case SYS_FUTEX_WAKE:
return Sys_FutexWake(frame->arg1, (uint32_t)frame->arg2);
case SYS_BTSCAN: case SYS_BTSCAN:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied; if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
if ((int64_t)frame->arg2 < 0) return InvalidArgument; if ((int64_t)frame->arg2 < 0) return InvalidArgument;
+60 -2
View File
@@ -358,6 +358,34 @@ namespace montauk::abi {
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
/* Unicode key events. Size-versioned: the caller passes sizeof(its
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
rest, so the struct can grow without another syscall. */
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
/* Futexes: process-private wait queues keyed by a user address. WAIT
blocks only if *addr still equals `expected` (checked atomically with
enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check
only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken,
WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the
number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */
static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status
static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status
static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL;
static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu;
/* Abnormal termination from any thread: ends the whole process (every
thread) with exit code TerminatedAborted (256+SIGABRT). SYS_EXIT from a
sibling thread ends only that thread, so abort() needs its own call. */
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
/* Normal termination of the whole process from any thread, with an exit
code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc
exit() and Rust std::process::exit() use it, so exiting from a sibling
thread ends the process rather than only that thread. */
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
/* Kernel-owned process capabilities. User identities may namespace /* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */ per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
@@ -600,7 +628,10 @@ namespace montauk::abi {
uint64_t txPackets; uint64_t txPackets;
}; };
struct KeyEvent { // Legacy 9-byte key event, read by SYS_GETKEY and written by
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
// `ascii` is a Windows-1252 byte.
struct KeyEventV1 {
uint8_t scancode; uint8_t scancode;
char ascii; char ascii;
bool pressed; bool pressed;
@@ -616,6 +647,32 @@ namespace montauk::abi {
bool extended; bool extended;
}; };
// Key event with the Unicode text it types (SYS_GETKEY_EX,
// SYS_CHILDIO_WRITEKEY_EX, WinEvent). Its first 9 bytes ARE a KeyEventV1, so
// a legacy reader sees a valid event. `codepoint` is the character the key
// produces (0 for control/navigation keys and dead keys). `ascii` is its
// Windows-1252 byte, or KEY_ASCII_SUBSTITUTE when 1252 has none.
struct KeyEvent {
uint8_t scancode;
char ascii;
bool pressed;
bool shift;
bool ctrl;
bool alt;
bool capslock;
bool altgr;
bool extended;
uint8_t _reserved[3];
uint32_t codepoint;
};
static_assert(sizeof(KeyEventV1) == 9, "KeyEventV1 is frozen ABI");
static_assert(sizeof(KeyEvent) == 16, "KeyEvent must fit the WinEvent union");
// ASCII SUB: `ascii` of a text key whose character has no Windows-1252 byte.
// A control character, so legacy code that only accepts ascii >= 0x20 skips
// it, while code testing `ascii == 0` for navigation keys is not confused.
static constexpr char KEY_ASCII_SUBSTITUTE = 0x1A;
struct MouseState { struct MouseState {
int32_t x; int32_t x;
int32_t y; int32_t y;
@@ -653,12 +710,13 @@ namespace montauk::abi {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
uint8_t _pad[3]; uint8_t _pad[3];
union { union {
KeyEvent key; KeyEvent key; // 16 bytes: the union was already that size (mouse)
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
struct { int32_t w, h; } resize; struct { int32_t w, h; } resize;
struct { int32_t scale; } scale; struct { int32_t scale; } scale;
}; };
}; };
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
struct WinInfo { struct WinInfo {
int32_t id; int32_t id;
+3 -1
View File
@@ -23,5 +23,7 @@ enum StatusCodes {
NoSuchPath = -11, NoSuchPath = -11,
Busy = -12, Busy = -12,
BufferTooSmall = -13, BufferTooSmall = -13,
OutOfMemory = -14 OutOfMemory = -14,
TimedOut = -15,
WouldBlock = -16
}; };
+37
View File
@@ -0,0 +1,37 @@
/*
* TerminationCodes.hpp
* MontaukOS process termination codes.
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// Exit code recorded when the kernel ends a process, as opposed to the
// process calling SYS_EXIT (which is masked to 0..255). Encoded as
// 256 + POSIX signal number so libc waitpid can report WTERMSIG.
// Mirrored in userspace at programs/include/montauk/statuscodes.h.
enum TerminationCodes {
TerminationBase = 256,
TerminatedIllegalInstruction = TerminationBase + 4, // SIGILL
TerminatedBreakpoint = TerminationBase + 5, // SIGTRAP
TerminatedAborted = TerminationBase + 6, // SIGABRT
TerminatedArithmeticError = TerminationBase + 8, // SIGFPE
TerminatedKilled = TerminationBase + 9, // SIGKILL
TerminatedAccessViolation = TerminationBase + 11 // SIGSEGV
};
// Termination code for a user-mode CPU exception. The exact vector is
// kept in the crash report; this is only the category.
inline TerminationCodes TerminationFromVector(uint8_t vector) {
switch (vector) {
case 0x06: return TerminatedIllegalInstruction; // #UD
case 0x01: // #DB
case 0x03: return TerminatedBreakpoint; // #BP
case 0x00: // #DE
case 0x10: // #MF
case 0x13: return TerminatedArithmeticError; // #XM
default: return TerminatedAccessViolation; // #PF, #GP, #SS, #NP, #AC, #OF, #BR, #CP, ...
}
}
+14
View File
@@ -139,6 +139,7 @@ namespace Drivers::PS2::Keyboard {
ack = Drivers::PS2::ReadData(); ack = Drivers::PS2::ReadData();
g_Modifiers = {}; g_Modifiers = {};
g_Modifiers.NumLock = true; // keypad types digits, as on most PCs
g_BufferHead = 0; g_BufferHead = 0;
g_BufferTail = 0; g_BufferTail = 0;
g_ExtendedScancode = false; g_ExtendedScancode = false;
@@ -257,6 +258,9 @@ namespace Drivers::PS2::Keyboard {
ascii = g_ScancodeToAscii[keycode]; ascii = g_ScancodeToAscii[keycode];
} }
} }
// Num Lock off: the keypad is navigation keys, which type nothing.
if (!g_Modifiers.NumLock && IsKeypadNavScancode(keycode))
ascii = 0;
KeyEvent event = { KeyEvent event = {
.Scancode = scancode, .Scancode = scancode,
@@ -310,6 +314,12 @@ namespace Drivers::PS2::Keyboard {
KeyEvent stamped = event; KeyEvent stamped = event;
if (stamped.Scancode == ScCapsLock && stamped.Pressed) if (stamped.Scancode == ScCapsLock && stamped.Pressed)
g_Modifiers.CapsLock = !g_Modifiers.CapsLock; g_Modifiers.CapsLock = !g_Modifiers.CapsLock;
// Num Lock likewise. Like the PS/2 path, the lock key itself is not
// delivered to apps.
if (stamped.Scancode == ScNumLock) {
if (stamped.Pressed) g_Modifiers.NumLock = !g_Modifiers.NumLock;
return;
}
stamped.CapsLock = g_Modifiers.CapsLock; stamped.CapsLock = g_Modifiers.CapsLock;
PushAndNotify(stamped); PushAndNotify(stamped);
} }
@@ -318,4 +328,8 @@ namespace Drivers::PS2::Keyboard {
return g_Modifiers.CapsLock; return g_Modifiers.CapsLock;
} }
bool GetNumLock() {
return g_Modifiers.NumLock;
}
}; };
+10
View File
@@ -63,4 +63,14 @@ namespace Drivers::PS2::Keyboard {
// compute ASCII, since the lock is toggled centrally here. // compute ASCII, since the lock is toggled centrally here.
bool GetCapsLock(); bool GetCapsLock();
// Current Num Lock state, owned here like Caps Lock. On (the default), the
// keypad types digits; off, it acts as the navigation keys printed on it.
bool GetNumLock();
// Set-1 scancodes of the keypad keys that are a digit/'.' with Num Lock on
// and Home/arrows/PgUp/End/PgDn/Ins/Del with it off (not '-' and '+').
inline bool IsKeypadNavScancode(uint8_t keycode) {
return keycode >= 0x47 && keycode <= 0x53 && keycode != 0x4A && keycode != 0x4E;
}
}; };
+11 -1
View File
@@ -223,6 +223,12 @@ namespace Drivers::USB::HidKeyboard {
if (caps && base >= 'a' && base <= 'z') upper = !shift; if (caps && base >= 'a' && base <= 'z') upper = !shift;
ascii = upper ? g_ScancodeToAsciiShifted[scancode] : base; ascii = upper ? g_ScancodeToAsciiShifted[scancode] : base;
} }
// Num Lock off: the keypad is navigation keys, which type nothing.
// (Only keypad usages get here with these scancodes: the dedicated
// navigation keys are nonChar already.)
if (!Drivers::PS2::Keyboard::GetNumLock() &&
Drivers::PS2::Keyboard::IsKeypadNavScancode(scancode))
ascii = 0;
Drivers::PS2::Keyboard::KeyEvent event = { Drivers::PS2::Keyboard::KeyEvent event = {
.Scancode = scancode, .Scancode = scancode,
@@ -367,10 +373,14 @@ namespace Drivers::USB::HidKeyboard {
uint8_t scancode = g_HidToScancode[key]; uint8_t scancode = g_HidToScancode[key];
if (scancode != 0) { if (scancode != 0) {
InjectKey(scancode, true, modifiers, IsNonCharKey(key), IsExtendedKey(key)); InjectKey(scancode, true, modifiers, IsNonCharKey(key), IsExtendedKey(key));
newKeyPressed = true;
// Lock keys toggle once per press; repeating them while
// held would flip Caps/Num/Scroll Lock on and off.
if (key != 0x39 && key != 0x53 && key != 0x47) {
g_RepeatKey = key; g_RepeatKey = key;
g_RepeatStartMs = now; g_RepeatStartMs = now;
g_LastRepeatMs = 0; g_LastRepeatMs = 0;
newKeyPressed = true; }
} }
} }
} }
+356
View File
@@ -0,0 +1,356 @@
/*
* BlockCache.cpp
* Per-mount LRU cache of filesystem blocks
* Copyright (c) 2026 Daniel Hammer
*/
#include "BlockCache.hpp"
#include <Drivers/Storage/BlockDevice.hpp>
#include <Terminal/Terminal.hpp>
#include <Libraries/Memory.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/Heap.hpp>
using namespace Kt;
namespace Fs::BlockCache {
// =========================================================================
// Sizing
// =========================================================================
// Capacity is free memory / FreeMemDivisor at creation, clamped to
// [MinEntries, MaxEntries]. 32768 4 KiB blocks = 128 MiB, enough to keep
// the whole native toolchain (cc1plus, as, ld, headers, libstdc++) hot.
static constexpr uint32_t MinEntries = 256;
static constexpr uint32_t MaxEntries = 32768;
static constexpr uint64_t FreeMemDivisor = 8;
// Every storage driver accepts at least 128 sectors per command (AHCI and
// the NVMe fallback both cap there), so prefetch runs stay within it.
static constexpr uint32_t MaxRunSectors = 128;
static constexpr uint32_t Nil = 0xFFFFFFFF;
// =========================================================================
// Types
// =========================================================================
// Entries live in one array and link by index. An entry is in exactly one
// of three states: never used (index >= nextFresh, no page), on the free
// list (has a page, not hashed), or live (hashed and on the LRU list).
struct Entry {
uint32_t block;
uint32_t hashNext; // bucket chain, or free-list link
uint32_t lruPrev;
uint32_t lruNext;
uint8_t* data;
};
struct Cache {
int blockDevIndex;
uint64_t partStartLba;
uint32_t blockSize;
uint32_t sectorsPerBlock;
Entry* entries;
uint32_t* buckets;
uint32_t capacity;
uint32_t hashShift;
int tablePages;
uint32_t nextFresh;
uint32_t freeHead;
uint32_t lruHead; // most recently used
uint32_t lruTail; // eviction candidate
// Staging buffer for multi-block prefetch reads (MaxRunSectors long).
uint8_t* staging;
int stagingPages;
};
// =========================================================================
// Device I/O
// =========================================================================
static bool DevRead(Cache* c, uint32_t block, uint32_t count, void* buf) {
auto* dev = Drivers::Storage::GetBlockDevice(c->blockDevIndex);
if (!dev) return false;
return dev->ReadSectors(dev->Ctx,
c->partStartLba + (uint64_t)block * c->sectorsPerBlock,
count * c->sectorsPerBlock, buf);
}
static bool DevWrite(Cache* c, uint32_t block, const void* buf) {
auto* dev = Drivers::Storage::GetBlockDevice(c->blockDevIndex);
if (!dev) return false;
return dev->WriteSectors(dev->Ctx,
c->partStartLba + (uint64_t)block * c->sectorsPerBlock,
c->sectorsPerBlock, buf);
}
// =========================================================================
// Hash and LRU bookkeeping
// =========================================================================
static uint32_t Bucket(const Cache* c, uint32_t block) {
return (block * 0x9E3779B1u) >> c->hashShift;
}
static uint32_t Lookup(Cache* c, uint32_t block) {
uint32_t i = c->buckets[Bucket(c, block)];
while (i != Nil) {
if (c->entries[i].block == block) return i;
i = c->entries[i].hashNext;
}
return Nil;
}
static void LruUnlink(Cache* c, uint32_t i) {
Entry& e = c->entries[i];
if (e.lruPrev != Nil) c->entries[e.lruPrev].lruNext = e.lruNext;
else c->lruHead = e.lruNext;
if (e.lruNext != Nil) c->entries[e.lruNext].lruPrev = e.lruPrev;
else c->lruTail = e.lruPrev;
e.lruPrev = e.lruNext = Nil;
}
static void LruPushFront(Cache* c, uint32_t i) {
Entry& e = c->entries[i];
e.lruPrev = Nil;
e.lruNext = c->lruHead;
if (c->lruHead != Nil) c->entries[c->lruHead].lruPrev = i;
c->lruHead = i;
if (c->lruTail == Nil) c->lruTail = i;
}
static void Touch(Cache* c, uint32_t i) {
if (c->lruHead == i) return;
LruUnlink(c, i);
LruPushFront(c, i);
}
static void HashInsert(Cache* c, uint32_t i) {
uint32_t b = Bucket(c, c->entries[i].block);
c->entries[i].hashNext = c->buckets[b];
c->buckets[b] = i;
}
static void HashRemove(Cache* c, uint32_t i) {
uint32_t* link = &c->buckets[Bucket(c, c->entries[i].block)];
while (*link != Nil) {
if (*link == i) {
*link = c->entries[i].hashNext;
break;
}
link = &c->entries[*link].hashNext;
}
c->entries[i].hashNext = Nil;
}
// Take a live entry out of the cache and park it (and its page) on the
// free list.
static void Drop(Cache* c, uint32_t i) {
HashRemove(c, i);
LruUnlink(c, i);
c->entries[i].block = 0;
c->entries[i].hashNext = c->freeHead;
c->freeHead = i;
}
// Get an entry with a page, not hashed and not on the LRU. Prefers the
// free list, then a fresh entry, then evicts the least recently used.
// Returns Nil only if the cache is empty and no page can be allocated.
static uint32_t Acquire(Cache* c) {
if (c->freeHead != Nil) {
uint32_t i = c->freeHead;
c->freeHead = c->entries[i].hashNext;
c->entries[i].hashNext = Nil;
return i;
}
if (c->nextFresh < c->capacity) {
void* page = Memory::g_pfa->Allocate();
if (page != nullptr) {
uint32_t i = c->nextFresh++;
Entry& e = c->entries[i];
e.block = 0;
e.hashNext = e.lruPrev = e.lruNext = Nil;
e.data = (uint8_t*)page;
return i;
}
// Out of memory: fall through and recycle what we already hold.
}
uint32_t victim = c->lruTail;
if (victim == Nil) return Nil;
HashRemove(c, victim);
LruUnlink(c, victim);
return victim;
}
// Publish a freshly filled entry under `block`.
static void Insert(Cache* c, uint32_t i, uint32_t block) {
c->entries[i].block = block;
HashInsert(c, i);
LruPushFront(c, i);
}
// =========================================================================
// Public API
// =========================================================================
Cache* Create(int blockDevIndex, uint64_t partStartLba, uint32_t blockSize) {
if (blockSize < 512 || blockSize > 0x1000 || (blockSize % 512) != 0) {
return nullptr;
}
montauk::abi::MemStats stats{};
Memory::g_pfa->GetStats(&stats);
uint64_t want = stats.freeBytes / FreeMemDivisor / blockSize;
if (want > MaxEntries) want = MaxEntries;
if (want < MinEntries) want = MinEntries;
// Round down to a power of two so the bucket count can equal capacity.
uint32_t capacity = MinEntries;
while ((uint64_t)capacity * 2 <= want) capacity *= 2;
auto* c = (Cache*)Memory::g_heap->Request(sizeof(Cache));
if (c == nullptr) return nullptr;
memset(c, 0, sizeof(Cache));
// Entries and buckets share one allocation. Physically consecutive
// runs can be scarce on a fragmented PFA, so halve until it fits.
while (capacity >= MinEntries) {
uint64_t bytes = (uint64_t)capacity * (sizeof(Entry) + sizeof(uint32_t));
int pages = (int)((bytes + 0xFFF) / 0x1000);
void* table = Memory::g_pfa->ReallocConsecutive(nullptr, pages);
if (table != nullptr) {
c->entries = (Entry*)table;
c->buckets = (uint32_t*)((uint8_t*)table + (uint64_t)capacity * sizeof(Entry));
c->tablePages = pages;
break;
}
capacity /= 2;
}
if (c->entries == nullptr) {
Memory::g_heap->Free(c);
return nullptr;
}
uint32_t log2 = 0;
while ((1u << log2) < capacity) log2++;
c->blockDevIndex = blockDevIndex;
c->partStartLba = partStartLba;
c->blockSize = blockSize;
c->sectorsPerBlock = blockSize / 512;
c->capacity = capacity;
c->hashShift = 32 - log2;
c->nextFresh = 0;
c->freeHead = Nil;
c->lruHead = Nil;
c->lruTail = Nil;
for (uint32_t b = 0; b < capacity; b++) c->buckets[b] = Nil;
// Prefetch is an optimization; without a staging buffer it is a no-op.
c->stagingPages = (int)((MaxRunSectors * 512 + 0xFFF) / 0x1000);
c->staging = (uint8_t*)Memory::g_pfa->ReallocConsecutive(nullptr, c->stagingPages);
if (c->staging == nullptr) c->stagingPages = 0;
KernelLogStream(OK, "BlockCache") << "Block cache: up to "
<< base::dec << (uint64_t)capacity << " blocks ("
<< (uint64_t)capacity * blockSize / (1024 * 1024) << " MiB)";
return c;
}
void Destroy(Cache* c) {
if (c == nullptr) return;
for (uint32_t i = 0; i < c->nextFresh; i++) {
if (c->entries[i].data != nullptr) Memory::g_pfa->Free(c->entries[i].data);
}
if (c->staging != nullptr) Memory::g_pfa->Free(c->staging, c->stagingPages);
Memory::g_pfa->Free(c->entries, c->tablePages);
Memory::g_heap->Free(c);
}
bool Read(Cache* c, uint32_t block, void* buf) {
if (block == 0) return false;
uint32_t i = Lookup(c, block);
if (i != Nil) {
Touch(c, i);
memcpy(buf, c->entries[i].data, c->blockSize);
return true;
}
i = Acquire(c);
if (i == Nil) return DevRead(c, block, 1, buf);
if (!DevRead(c, block, 1, c->entries[i].data)) {
c->entries[i].hashNext = c->freeHead;
c->freeHead = i;
return false;
}
Insert(c, i, block);
memcpy(buf, c->entries[i].data, c->blockSize);
return true;
}
bool Write(Cache* c, uint32_t block, const void* buf) {
if (block == 0) return false;
uint32_t i = Lookup(c, block);
if (!DevWrite(c, block, buf)) {
// The on-disk contents are now unknown; don't serve a stale copy.
if (i != Nil) Drop(c, i);
return false;
}
if (i != Nil) {
Touch(c, i);
} else {
i = Acquire(c);
if (i == Nil) return true;
Insert(c, i, block);
}
memcpy(c->entries[i].data, buf, c->blockSize);
return true;
}
bool Contains(Cache* c, uint32_t block) {
return block != 0 && Lookup(c, block) != Nil;
}
uint32_t MaxRunBlocks(Cache* c) {
return MaxRunSectors / c->sectorsPerBlock;
}
void Prefetch(Cache* c, uint32_t first, uint32_t count) {
if (c->staging == nullptr || first == 0) return;
uint32_t maxRun = MaxRunBlocks(c);
uint32_t end = first + count;
if (end < first) return; // wrapped
uint32_t b = first;
while (b < end) {
if (Lookup(c, b) != Nil) { b++; continue; }
uint32_t run = 1;
while (run < maxRun && b + run < end && Lookup(c, b + run) == Nil) run++;
if (!DevRead(c, b, run, c->staging)) return;
for (uint32_t k = 0; k < run; k++) {
uint32_t i = Acquire(c);
if (i == Nil) return;
memcpy(c->entries[i].data, c->staging + (uint64_t)k * c->blockSize,
c->blockSize);
Insert(c, i, b + k);
}
b += run;
}
}
};
+45
View File
@@ -0,0 +1,45 @@
/*
* BlockCache.hpp
* Per-mount LRU cache of filesystem blocks
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Fs::BlockCache {
// A cache over one partition of a block device, in units of the
// filesystem's block size (512..4096 bytes, addressed in 512-byte sectors
// from the partition start). Write-through: Write reaches the device
// before it returns, so the cache never holds dirty data and there is
// nothing to flush on sync or unmount.
//
// Not internally locked. Callers serialize access; the VFS already holds
// vfsLock across every driver call.
struct Cache;
// Create a cache for the partition. Capacity is sized from free memory at
// the time of the call; block pages are allocated lazily as the cache
// fills and returned on Destroy. Returns nullptr if the bookkeeping
// cannot be allocated (callers then do uncached I/O).
Cache* Create(int blockDevIndex, uint64_t partStartLba, uint32_t blockSize);
void Destroy(Cache* cache);
// Read one block, from the cache or the device. Block 0 is never cached.
bool Read(Cache* cache, uint32_t block, void* buf);
// Write one block to the device, then update the cached copy.
bool Write(Cache* cache, uint32_t block, const void* buf);
bool Contains(Cache* cache, uint32_t block);
// Pull `count` physically contiguous blocks starting at `first` into the
// cache, in as few device commands as possible. Blocks already cached are
// skipped. Best effort: a failed read just leaves those blocks uncached.
void Prefetch(Cache* cache, uint32_t first, uint32_t count);
// Largest run Prefetch reads in one device command.
uint32_t MaxRunBlocks(Cache* cache);
};
+29
View File
@@ -6,6 +6,7 @@
#include "Ext2.hpp" #include "Ext2.hpp"
#include "FsProbe.hpp" #include "FsProbe.hpp"
#include "BlockCache.hpp"
#include <Drivers/Storage/BlockDevice.hpp> #include <Drivers/Storage/BlockDevice.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
@@ -171,6 +172,10 @@ namespace Fs::Ext2 {
uint8_t* blockBuf; uint8_t* blockBuf;
int blockBufPages; int blockBufPages;
// Write-through cache that every ReadBlock/WriteBlock goes through.
// Null if it could not be allocated, in which case I/O is uncached.
BlockCache::Cache* cache;
// Open file handles // Open file handles
Ext2File files[MaxFilesPerInstance]; Ext2File files[MaxFilesPerInstance];
@@ -238,12 +243,14 @@ namespace Fs::Ext2 {
static bool ReadBlock(const Ext2Instance& inst, uint32_t blockNum, void* buf) { static bool ReadBlock(const Ext2Instance& inst, uint32_t blockNum, void* buf) {
if (blockNum == 0) return false; if (blockNum == 0) return false;
if (inst.cache) return BlockCache::Read(inst.cache, blockNum, buf);
return ReadPartSectors(inst, BlockToPartSector(inst, blockNum), return ReadPartSectors(inst, BlockToPartSector(inst, blockNum),
SectorsPerBlock(inst), buf); SectorsPerBlock(inst), buf);
} }
static bool WriteBlock(const Ext2Instance& inst, uint32_t blockNum, const void* buf) { static bool WriteBlock(const Ext2Instance& inst, uint32_t blockNum, const void* buf) {
if (blockNum == 0) return false; if (blockNum == 0) return false;
if (inst.cache) return BlockCache::Write(inst.cache, blockNum, buf);
return WritePartSectors(inst, BlockToPartSector(inst, blockNum), return WritePartSectors(inst, BlockToPartSector(inst, blockNum),
SectorsPerBlock(inst), buf); SectorsPerBlock(inst), buf);
} }
@@ -1294,6 +1301,20 @@ namespace Fs::Ext2 {
uint32_t physBlock = GetPhysicalBlock(self, file.inode, logicalBlock); uint32_t physBlock = GetPhysicalBlock(self, file.inode, logicalBlock);
if (physBlock == 0) break; if (physBlock == 0) break;
// On a miss, read ahead the physically contiguous run of this
// file's following blocks in one device command. Resolving the
// run is cheap: the indirect blocks it walks are cached too.
if (self.cache && !BlockCache::Contains(self.cache, physBlock)) {
uint32_t lastLogical = (fileSize - 1) / blockSize;
uint32_t maxRun = BlockCache::MaxRunBlocks(self.cache);
uint32_t run = 1;
while (run < maxRun && logicalBlock + run <= lastLogical &&
GetPhysicalBlock(self, file.inode, logicalBlock + run) == physBlock + run) {
run++;
}
BlockCache::Prefetch(self.cache, physBlock, run);
}
if (!ReadBlock(self, physBlock, dataBuf)) break; if (!ReadBlock(self, physBlock, dataBuf)) break;
uint32_t available = blockSize - blockOff; uint32_t available = blockSize - blockOff;
@@ -1882,6 +1903,7 @@ namespace Fs::Ext2 {
if (self->dirNames != nullptr && self->dirNamesPages > 0) { if (self->dirNames != nullptr && self->dirNamesPages > 0) {
Memory::g_pfa->Free(self->dirNames, self->dirNamesPages); Memory::g_pfa->Free(self->dirNames, self->dirNamesPages);
} }
BlockCache::Destroy(self->cache);
self->active = false; self->active = false;
g_instanceSlots[(std::size_t)inst] = nullptr; g_instanceSlots[(std::size_t)inst] = nullptr;
@@ -1916,6 +1938,7 @@ namespace Fs::Ext2 {
if (self->bgdt != nullptr && self->bgdtPages > 0) { if (self->bgdt != nullptr && self->bgdtPages > 0) {
Memory::g_pfa->Free(self->bgdt, self->bgdtPages); Memory::g_pfa->Free(self->bgdt, self->bgdtPages);
} }
BlockCache::Destroy(self->cache);
if (self->driver != nullptr) Memory::g_heap->Free(self->driver); if (self->driver != nullptr) Memory::g_heap->Free(self->driver);
Memory::g_heap->Free(self); Memory::g_heap->Free(self);
} }
@@ -2025,6 +2048,12 @@ namespace Fs::Ext2 {
return AbortMount(idx); return AbortMount(idx);
} }
inst.cache = BlockCache::Create(blockDevIndex, startLba, blockSize);
if (!inst.cache) {
KernelLogStream(WARNING, "Ext2") << "No memory for a block cache; "
"volume I/O will be uncached";
}
// Load block group descriptor table // Load block group descriptor table
// BGDT starts at the block after the superblock // BGDT starts at the block after the superblock
uint32_t bgdtStartBlock = inst.firstDataBlock + 1; uint32_t bgdtStartBlock = inst.firstDataBlock + 1;
+96 -12
View File
@@ -301,6 +301,53 @@ namespace Fs::Fat32 {
// Short name generation and LFN helpers for Create // Short name generation and LFN helpers for Create
// ========================================================================= // =========================================================================
// =========================================================================
// Name encoding: VFS names are UTF-8, LFN entries are UTF-16
// =========================================================================
// Decode one UTF-8 character at s (NUL-terminated); stores its length.
// A byte that does not start a well-formed sequence is taken as one
// Latin-1 character, so legacy single-byte names still round-trip.
static uint32_t DecodeUtf8(const char* s, int* consumed) {
const uint8_t* u = (const uint8_t*)s;
uint8_t b0 = u[0];
*consumed = 1;
if (b0 < 0x80) return b0;
int n;
uint32_t cp, min;
if (b0 >= 0xC2 && b0 <= 0xDF) { n = 2; cp = b0 & 0x1F; min = 0x80; }
else if (b0 >= 0xE0 && b0 <= 0xEF) { n = 3; cp = b0 & 0x0F; min = 0x800; }
else if (b0 >= 0xF0 && b0 <= 0xF4) { n = 4; cp = b0 & 0x07; min = 0x10000; }
else return b0;
for (int i = 1; i < n; i++) {
if ((u[i] & 0xC0) != 0x80) return b0; // also stops at NUL
cp = (cp << 6) | (u[i] & 0x3F);
}
if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return b0;
*consumed = n;
return cp;
}
static int EncodeUtf8(uint32_t cp, char* out) {
if (cp < 0x80) { out[0] = (char)cp; return 1; }
if (cp < 0x800) {
out[0] = (char)(0xC0 | (cp >> 6));
out[1] = (char)(0x80 | (cp & 0x3F));
return 2;
}
if (cp < 0x10000) {
out[0] = (char)(0xE0 | (cp >> 12));
out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
out[2] = (char)(0x80 | (cp & 0x3F));
return 3;
}
out[0] = (char)(0xF0 | (cp >> 18));
out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
out[3] = (char)(0x80 | (cp & 0x3F));
return 4;
}
static void GenerateShortName(const char* longName, char* shortName) { static void GenerateShortName(const char* longName, char* shortName) {
memset(shortName, ' ', 11); memset(shortName, ' ', 11);
@@ -315,9 +362,15 @@ namespace Fs::Fat32 {
// Base name (up to 8 chars, uppercase, skip invalid chars) // Base name (up to 8 chars, uppercase, skip invalid chars)
int baseEnd = (lastDot >= 0) ? lastDot : nameLen; int baseEnd = (lastDot >= 0) ? lastDot : nameLen;
int j = 0; int j = 0;
// A non-ASCII character becomes one '_' (8.3 names are OEM codepage;
// the real name lives in the LFN entries).
for (int i = 0; i < baseEnd && j < 8; i++) { for (int i = 0; i < baseEnd && j < 8; i++) {
char c = longName[i]; char c = longName[i];
if (c == ' ' || c == '.') continue; if (c == ' ' || c == '.') continue;
if ((uint8_t)c >= 0x80) {
if (((uint8_t)c & 0xC0) == 0x80) continue;
c = '_';
}
if (c >= 'a' && c <= 'z') c -= 32; if (c >= 'a' && c <= 'z') c -= 32;
shortName[j++] = c; shortName[j++] = c;
} }
@@ -327,6 +380,10 @@ namespace Fs::Fat32 {
j = 0; j = 0;
for (int i = lastDot + 1; longName[i] && j < 3; i++) { for (int i = lastDot + 1; longName[i] && j < 3; i++) {
char c = longName[i]; char c = longName[i];
if ((uint8_t)c >= 0x80) {
if (((uint8_t)c & 0xC0) == 0x80) continue;
c = '_';
}
if (c >= 'a' && c <= 'z') c -= 32; if (c >= 'a' && c <= 'z') c -= 32;
shortName[8 + j++] = c; shortName[8 + j++] = c;
} }
@@ -403,12 +460,22 @@ namespace Fs::Fat32 {
// Returns the number of LFN entries. // Returns the number of LFN entries.
static int BuildLfnEntries(const char* longName, uint8_t checksum, static int BuildLfnEntries(const char* longName, uint8_t checksum,
uint8_t* outEntries) { uint8_t* outEntries) {
// Convert to UTF-16 // Convert UTF-8 to UTF-16 (LFN holds at most 255 code units)
uint16_t utf16[MaxNameLen]; uint16_t utf16[MaxNameLen + 13];
int len = 0; int len = 0;
while (longName[len] && len < MaxNameLen - 1) { for (const char* p = longName; *p;) {
utf16[len] = (uint8_t)longName[len]; int n;
len++; uint32_t cp = DecodeUtf8(p, &n);
int units = cp >= 0x10000 ? 2 : 1;
if (len + units > 255) break;
if (units == 2) {
cp -= 0x10000;
utf16[len++] = (uint16_t)(0xD800 | (cp >> 10));
utf16[len++] = (uint16_t)(0xDC00 | (cp & 0x3FF));
} else {
utf16[len++] = (uint16_t)cp;
}
p += n;
} }
utf16[len] = 0; utf16[len] = 0;
@@ -464,9 +531,11 @@ namespace Fs::Fat32 {
// Compare case-insensitively // Compare case-insensitively
if (!StrEqualNoCase(longName, reconstructed)) return true; if (!StrEqualNoCase(longName, reconstructed)) return true;
// Check if long name has lowercase (short names are uppercase) // Check if long name has lowercase (short names are uppercase) or
// characters the short name could not hold
for (int i = 0; longName[i]; i++) { for (int i = 0; longName[i]; i++) {
if (longName[i] >= 'a' && longName[i] <= 'z') return true; if (longName[i] >= 'a' && longName[i] <= 'z') return true;
if ((uint8_t)longName[i] >= 0x80) return true;
} }
return false; return false;
} }
@@ -619,10 +688,24 @@ namespace Fs::Fat32 {
for (int i = 0; i < 2; i++) memcpy(&out[11 + i], entry + 28 + i * 2, 2); for (int i = 0; i < 2; i++) memcpy(&out[11 + i], entry + 28 + i * 2, 2);
} }
static void Utf16ToAscii(const uint16_t* src, int maxLen, char* dst) { // LFN UTF-16 -> UTF-8 name. Unpaired surrogates become U+FFFD; the name is
// cut on a character boundary if it does not fit MaxNameLen bytes.
static void Utf16ToUtf8(const uint16_t* src, int maxLen, char* dst,
int dstCap = MaxNameLen) {
int j = 0; int j = 0;
for (int i = 0; i < maxLen && src[i] != 0 && j < MaxNameLen - 1; i++) { for (int i = 0; i < maxLen && src[i] != 0 && src[i] != 0xFFFF; i++) {
dst[j++] = (src[i] < 128) ? (char)src[i] : '?'; uint32_t cp = src[i];
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < maxLen &&
src[i + 1] >= 0xDC00 && src[i + 1] <= 0xDFFF) {
cp = 0x10000 + ((cp - 0xD800) << 10) + (src[i + 1] - 0xDC00);
i++;
} else if (cp >= 0xD800 && cp <= 0xDFFF) {
cp = 0xFFFD;
}
char enc[4];
int n = EncodeUtf8(cp, enc);
if (j + n > dstCap - 1) break;
for (int k = 0; k < n; k++) dst[j++] = enc[k];
} }
dst[j] = '\0'; dst[j] = '\0';
} }
@@ -685,7 +768,7 @@ namespace Fs::Fat32 {
pe.attributes = attr; pe.attributes = attr;
if (hasLfn) { if (hasLfn) {
Utf16ToAscii(lfnBuf, MaxNameLen, pe.name); Utf16ToUtf8(lfnBuf, MaxNameLen, pe.name);
} else { } else {
ParseShortName(e, pe.name); ParseShortName(e, pe.name);
} }
@@ -752,7 +835,8 @@ namespace Fs::Fat32 {
if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; } if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; }
if (hasLfn) { if (hasLfn) {
Utf16ToAscii(lfnBuf, MaxNameLen, outNames[count]); // Leave room for the '/' appended to directory names
Utf16ToUtf8(lfnBuf, MaxNameLen, outNames[count], MaxNameLen - 1);
} else { } else {
ParseShortName(e, outNames[count]); ParseShortName(e, outNames[count]);
} }
@@ -848,7 +932,7 @@ namespace Fs::Fat32 {
char entryName[MaxNameLen]; char entryName[MaxNameLen];
if (hasLfn) { if (hasLfn) {
Utf16ToAscii(lfnBuf, MaxNameLen, entryName); Utf16ToUtf8(lfnBuf, MaxNameLen, entryName);
} else { } else {
ParseShortName(e, entryName); ParseShortName(e, entryName);
} }
+5 -1
View File
@@ -29,9 +29,13 @@ namespace Fs {
{"/config/ssh.toml", false, montauk::abi::CAP_USER_ADMIN}, {"/config/ssh.toml", false, montauk::abi::CAP_USER_ADMIN},
{"/config/capabilities.toml",false, montauk::abi::CAP_USER_ADMIN}, {"/config/capabilities.toml",false, montauk::abi::CAP_USER_ADMIN},
{"/config/consent.toml", false, montauk::abi::CAP_USER_ADMIN}, {"/config/consent.toml", false, montauk::abi::CAP_USER_ADMIN},
{"/config/wallpaper.cache", false, montauk::abi::CAP_USER_ADMIN},
// Read by the Bluetooth driver at controller bring-up. // Read by the Bluetooth driver at controller bring-up.
{"/config/bluetooth.toml", false, montauk::abi::CAP_DEVICE_ADMIN}, {"/config/bluetooth.toml", false, montauk::abi::CAP_DEVICE_ADMIN},
// Login's wallpaper cache. The directory is listed for the same reason
// as /config: a rename only checks its own path, so an unlisted parent
// could be swapped for one holding a forged cache.
{"/state/cache", false, montauk::abi::CAP_USER_ADMIN},
{"/state/cache/wallpaper.cache", false, montauk::abi::CAP_USER_ADMIN},
{"/apps", true, montauk::abi::CAP_SYSTEM_IMAGE}, {"/apps", true, montauk::abi::CAP_SYSTEM_IMAGE},
{"/os", true, montauk::abi::CAP_SYSTEM_IMAGE}, {"/os", true, montauk::abi::CAP_SYSTEM_IMAGE},
}; };
+18
View File
@@ -89,6 +89,20 @@ namespace Fs::Ramdisk {
return -1; return -1;
} }
// Mkdir/Create must not conjure entries under a missing parent: the
// table is flat, so "a/b/" with no "a/" entry is invisible to ReadDir of
// the parent and makes "a" fail Stat, Delete and recursive removal.
static bool ParentExists(const char* path) {
char normalized[MaxNameLen];
int len = NormalizePath(path, normalized, MaxNameLen);
if (len < 0) return false;
while (len > 0 && normalized[len - 1] != '/') len--;
if (len <= 1) return true; // parent is the root
normalized[len - 1] = '\0';
int idx = FindEntryByPath(normalized);
return idx >= 0 && fileTable[idx].isDirectory;
}
void Initialize(void* moduleData, uint64_t moduleSize) { void Initialize(void* moduleData, uint64_t moduleSize) {
Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)"; Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)";
@@ -442,6 +456,8 @@ namespace Fs::Ramdisk {
} }
} }
if (!ParentExists(path)) return -1;
// Create new file entry // Create new file entry
FileEntry& entry = fileTable[fileCount]; FileEntry& entry = fileTable[fileCount];
@@ -531,6 +547,8 @@ namespace Fs::Ramdisk {
} }
} }
if (!ParentExists(path)) return -1;
// Create directory entry (stored with trailing slash for tar convention) // Create directory entry (stored with trailing slash for tar convention)
FileEntry& entry = fileTable[fileCount]; FileEntry& entry = fileTable[fileCount];
+15 -7
View File
@@ -8,6 +8,7 @@
#include <Memory/Heap.hpp> #include <Memory/Heap.hpp>
#include <Common/Panic.hpp> #include <Common/Panic.hpp>
#include <Common/TerminationCodes.hpp>
#include <Platform/Registers.hpp> #include <Platform/Registers.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
#include <Memory/PageFrameAllocator.hpp> #include <Memory/PageFrameAllocator.hpp>
@@ -170,14 +171,21 @@ namespace Hal {
CrashReport::AddReport(rep); CrashReport::AddReport(rep);
Sched::SpawnCrashPad(proc->pid); Sched::SpawnCrashPad(proc->pid);
// Record a killed-by-signal exit code (256+sig) so a parent // Record a termination code (256+sig) so a parent blocked in
// blocked in SYS_WAITPID can tell a crash from a clean exit. // SYS_WAITPID can tell a crash from a clean exit.
{ {
int sig = 11; /* SIGSEGV */ int code = TerminationFromVector(i);
if (i == 0x00) sig = 8; /* #DE -> SIGFPE */
else if (i == 0x06) sig = 4; /* #UD -> SIGILL */ // ExitProcess from a sibling thread ends only that thread,
else if (i == 0x10 || i == 0x13) sig = 8; /* x87/SIMD FP */ // which would leave the process running with the crash
Sched::SetProcessExitCode(proc->pid, 256 + sig); // swallowed. Flag the whole process (main thread and every
// sibling) first, carrying the crash code rather than
// KillProcess's default TerminatedKilled, so waiters see the fault.
if (Sched::GetCurrentThreadPtr() != proc) {
Sched::KillProcess(proc->pid, code);
} else {
Sched::SetProcessExitCode(proc->pid, code);
}
} }
Sched::ExitProcess(); Sched::ExitProcess();
+7 -10
View File
@@ -536,16 +536,13 @@ namespace Sched {
} }
// Build the main thread's TLS block (x86-64 variant II): the block // Build the main thread's TLS block (x86-64 variant II): the block
// lives below the thread pointer, TP = base + S where // lives below the thread pointer (see ComputeTlsLayout), template
// S = align_up(memsz, align), template data at [base, base+filesz), // data first, .tbss zeroed, and [TP] holds the ABI self-pointer.
// .tbss zeroed, and [TP] holds the ABI-required self-pointer.
if (tlsPhdr != nullptr && outTls != nullptr) { if (tlsPhdr != nullptr && outTls != nullptr) {
uint64_t align = tlsPhdr->p_align < 16 ? 16 : tlsPhdr->p_align; TlsLayout layout = ComputeTlsLayout(tlsPhdr->p_memsz, tlsPhdr->p_align);
uint64_t S = (tlsPhdr->p_memsz + align - 1) & ~(align - 1);
uint64_t base = (maxVaddrEnd + 0x1FFFULL) & ~0xFFFULL; // page-aligned, 1 guard page gap uint64_t base = (maxVaddrEnd + 0x1FFFULL) & ~0xFFFULL; // page-aligned, 1 guard page gap
uint64_t total = S + 16; // + minimal TCB uint64_t numPages = (layout.totalSize + 0xFFF) / 0x1000;
uint64_t numPages = (total + 0xFFF) / 0x1000; uint64_t tp = base + layout.tpOffset;
uint64_t tp = base + S;
bool ok = true; bool ok = true;
for (uint64_t p = 0; p < numPages && ok; p++) { for (uint64_t p = 0; p < numPages && ok; p++) {
@@ -562,7 +559,7 @@ namespace Sched {
} }
// Copy the .tdata template bytes that land in this page. // Copy the .tdata template bytes that land in this page.
if (!copyPageFromFile(physAddr, virtAddr, base, if (!copyPageFromFile(physAddr, virtAddr, base + layout.templateOffset,
tlsPhdr->p_offset, tlsPhdr->p_filesz)) { tlsPhdr->p_offset, tlsPhdr->p_filesz)) {
ok = false; ok = false;
break; break;
@@ -579,7 +576,7 @@ namespace Sched {
outTls->templateVaddr = tlsPhdr->p_vaddr; outTls->templateVaddr = tlsPhdr->p_vaddr;
outTls->fileSize = tlsPhdr->p_filesz; outTls->fileSize = tlsPhdr->p_filesz;
outTls->memSize = tlsPhdr->p_memsz; outTls->memSize = tlsPhdr->p_memsz;
outTls->align = align; outTls->align = tlsPhdr->p_align;
} else { } else {
Kt::KernelLogStream(Kt::ERROR, "ELF") Kt::KernelLogStream(Kt::ERROR, "ELF")
<< "PT_TLS setup failed (out of pages?), " << "PT_TLS setup failed (out of pages?), "
+22 -1
View File
@@ -60,9 +60,30 @@ namespace Sched {
uint64_t templateVaddr; // source of .tdata in the loaded image uint64_t templateVaddr; // source of .tdata in the loaded image
uint64_t fileSize; // initialized bytes uint64_t fileSize; // initialized bytes
uint64_t memSize; // total bytes (incl. .tbss) uint64_t memSize; // total bytes (incl. .tbss)
uint64_t align; uint64_t align; // PT_TLS p_align, NOT rounded up
}; };
// Geometry of one variant II TLS block, as offsets from its
// page-aligned base. The linker addresses TLS variables at
// TP - align_up(memSize, p_align) + offset, so the template must start
// exactly that far below TP; rounding p_align up first (as an older
// version did) shifts every variable away from its initial value.
// TP itself is kept at least 16-byte aligned, which the gap between
// base and the template absorbs.
struct TlsLayout {
uint64_t templateOffset; // where .tdata/.tbss start
uint64_t tpOffset; // thread pointer; [TP] holds the self-pointer
uint64_t totalSize; // bytes to map, incl. the 16-byte TCB
};
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
uint64_t align = pAlign ? pAlign : 1;
uint64_t block = (memSize + align - 1) & ~(align - 1);
uint64_t tpAlign = align < 16 ? 16 : align;
uint64_t tp = (block + tpAlign - 1) & ~(tpAlign - 1);
return {tp - block, tp, tp + 16};
}
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls); uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls);
// Load a shared library into a fixed address range. // Load a shared library into a fixed address range.
+151 -17
View File
@@ -324,6 +324,8 @@ namespace Sched {
processTable[i].inTail = 0; processTable[i].inTail = 0;
processTable[i].keyHead = 0; processTable[i].keyHead = 0;
processTable[i].keyTail = 0; processTable[i].keyTail = 0;
processTable[i].getcharUtf8Len = 0;
processTable[i].getcharUtf8Pos = 0;
processTable[i].termCols = 0; processTable[i].termCols = 0;
processTable[i].termRows = 0; processTable[i].termRows = 0;
processTable[i].ioOutHandle = -1; processTable[i].ioOutHandle = -1;
@@ -512,6 +514,8 @@ namespace Sched {
proc.joinerSlot = -1; proc.joinerSlot = -1;
proc.exitCode = 0; proc.exitCode = 0;
proc.joinable = false; // main thread is reaped by BSP, not joined proc.joinable = false; // main thread is reaped by BSP, not joined
proc.futexAddr = 0;
proc.futexWoken = false;
proc.savedRsp = (uint64_t)sp; proc.savedRsp = (uint64_t)sp;
proc.stackBase = (uint64_t)kernelStackBase; proc.stackBase = (uint64_t)kernelStackBase;
proc.entryPoint = entry; proc.entryPoint = entry;
@@ -654,6 +658,8 @@ namespace Sched {
proc.inTail = 0; proc.inTail = 0;
proc.keyHead = 0; proc.keyHead = 0;
proc.keyTail = 0; proc.keyTail = 0;
proc.getcharUtf8Len = 0;
proc.getcharUtf8Pos = 0;
proc.termCols = 0; proc.termCols = 0;
proc.termRows = 0; proc.termRows = 0;
proc.ioOutHandle = -1; proc.ioOutHandle = -1;
@@ -714,10 +720,8 @@ namespace Sched {
uint64_t threadTlsPages = 0; uint64_t threadTlsPages = 0;
Process& primary = processTable[primarySlot_]; Process& primary = processTable[primarySlot_];
if (primary.tlsMemSize > 0) { if (primary.tlsMemSize > 0) {
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign; TlsLayout layout = ComputeTlsLayout(primary.tlsMemSize, primary.tlsAlign);
uint64_t S = (primary.tlsMemSize + align - 1) & ~(align - 1); uint64_t numPages = (layout.totalSize + 0xFFF) / 0x1000;
uint64_t total = S + 16;
uint64_t numPages = (total + 0xFFF) / 0x1000;
uint64_t base = 0; uint64_t base = 0;
if (!ReserveUserHeapRange(primarySlot_, numPages * 0x1000, base)) { if (!ReserveUserHeapRange(primarySlot_, numPages * 0x1000, base)) {
@@ -751,9 +755,9 @@ namespace Sched {
return -1; return -1;
} }
uint64_t tp = base + S; uint64_t tp = base + layout.tpOffset;
memcpy((void*)base, (const void*)primary.tlsTemplateVaddr, memcpy((void*)(base + layout.templateOffset),
primary.tlsFileSize); (const void*)primary.tlsTemplateVaddr, primary.tlsFileSize);
*(uint64_t*)tp = tp; /* ABI self-pointer */ *(uint64_t*)tp = tp; /* ABI self-pointer */
threadFsBase = tp; threadFsBase = tp;
threadTlsBase = base; threadTlsBase = base;
@@ -820,6 +824,8 @@ namespace Sched {
thr.exitCode = 0; thr.exitCode = 0;
thr.joinable = true; thr.joinable = true;
thr.threadArg = arg; thr.threadArg = arg;
thr.futexAddr = 0;
thr.futexWoken = false;
thr.savedRsp = (uint64_t)sp; thr.savedRsp = (uint64_t)sp;
thr.stackBase = (uint64_t)kernelStackBase; thr.stackBase = (uint64_t)kernelStackBase;
thr.entryPoint = entry; thr.entryPoint = entry;
@@ -835,6 +841,7 @@ namespace Sched {
thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0; thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0;
thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0; thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0;
thr.keyHead = 0; thr.keyTail = 0; thr.keyHead = 0; thr.keyTail = 0;
thr.getcharUtf8Len = 0; thr.getcharUtf8Pos = 0;
thr.termCols = 0; thr.termRows = 0; thr.termCols = 0; thr.termRows = 0;
thr.ioOutHandle = -1; thr.ioInHandle = -1; thr.ioOutHandle = -1; thr.ioInHandle = -1;
thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1; thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1;
@@ -936,14 +943,13 @@ namespace Sched {
return; return;
} }
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign; TlsLayout layout = ComputeTlsLayout(primary.tlsMemSize, primary.tlsAlign);
uint64_t blockSize = (primary.tlsMemSize + align - 1) & ~(align - 1); if (layout.tpOffset > thr.fsBase || layout.totalSize < layout.tpOffset) {
if (blockSize > thr.fsBase || blockSize > UINT64_MAX - 16) {
Panic("Invalid sibling TLS metadata during thread teardown", nullptr); Panic("Invalid sibling TLS metadata during thread teardown", nullptr);
} }
uint64_t base = thr.fsBase - blockSize; uint64_t base = thr.fsBase - layout.tpOffset;
uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000; uint64_t pages = (layout.totalSize + 0xFFF) / 0x1000;
thr.fsBase = 0; thr.fsBase = 0;
Memory::UnmapAndFreeUserRange(primary.pml4Phys, base, pages); Memory::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL); ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL);
@@ -1700,6 +1706,8 @@ namespace Sched {
proc.inTail = 0; proc.inTail = 0;
proc.keyHead = 0; proc.keyHead = 0;
proc.keyTail = 0; proc.keyTail = 0;
proc.getcharUtf8Len = 0;
proc.getcharUtf8Pos = 0;
proc.termCols = 0; proc.termCols = 0;
proc.termRows = 0; proc.termRows = 0;
proc.ioOutHandle = -1; proc.ioOutHandle = -1;
@@ -1780,10 +1788,13 @@ namespace Sched {
} }
} }
int KillProcess(int pid) { int KillProcess(int pid, int exitCode) {
// Refuse to kill PID 0 (init) or caller's own process // Refuse to kill PID 0 (init). Killing the caller's own process is
// allowed: a faulting/aborting sibling thread shares the pid and has
// no other way to take the process down. The caller's own slot gets
// killPending below and exits at syscall return (ExitIfKilled) or
// via the ExitProcess the fault handler calls next.
if (pid == 0) return -1; if (pid == 0) return -1;
if (pid == GetCurrentPid()) return -1;
schedLock.Acquire(); schedLock.Acquire();
@@ -1806,7 +1817,7 @@ namespace Sched {
return -1; return -1;
} }
processTable[primarySlot_].exitCode = 256 + 9; /* killed (SIGKILL) */ processTable[primarySlot_].exitCode = exitCode;
// Flag the main thread so its next tick (or scheduler dispatch) // Flag the main thread so its next tick (or scheduler dispatch)
// routes through ExitProcess, which sweeps every sibling thread // routes through ExitProcess, which sweeps every sibling thread
@@ -1874,7 +1885,7 @@ namespace Sched {
continue; continue;
} }
primary.exitCode = 256 + 9; /* killed (SIGKILL) */ primary.exitCode = TerminatedKilled;
primary.killPending = true; primary.killPending = true;
if (primary.state == ProcessState::Blocked) { if (primary.state == ProcessState::Blocked) {
primary.state = ProcessState::Ready; primary.state = ProcessState::Ready;
@@ -2109,6 +2120,129 @@ namespace Sched {
} }
} }
// Monotonic enrollment counter for FIFO futex wakeups. Only touched
// under schedLock.
static uint64_t futexNextSeq = 1;
int FutexWait(uint64_t uaddr, uint32_t expected, uint64_t timeoutNs) {
if ((uaddr & 3) != 0) return InvalidArgument;
ExitIfKilled();
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu ? cpu->currentSlot : -1;
if (slot < 0) return GenericFailure;
Process& self = processTable[slot];
// Round up so a short timeout never turns into "check only".
uint64_t timeoutMs = 0;
if (timeoutNs != FutexWaitForever) {
if (timeoutNs == 0) {
timeoutMs = 0;
} else {
timeoutMs = timeoutNs / 1000000ULL + ((timeoutNs % 1000000ULL) ? 1 : 0);
}
}
schedLock.Acquire();
// The word is read here, with schedLock held and interrupts off, so
// it must not fault: the caller validated (and demand-committed) the
// page, and the page walk below re-confirms it is still mapped. An
// aligned 32-bit word never straddles a page.
if (!Memory::VMM::Paging::IsUserRangeAccessible(self.pml4Phys, uaddr, 4, false)) {
schedLock.Release();
return BadPointer;
}
uint32_t current = __atomic_load_n((volatile uint32_t*)uaddr, __ATOMIC_SEQ_CST);
if (current != expected) {
schedLock.Release();
return WouldBlock;
}
if (timeoutNs == 0) {
schedLock.Release();
return TimedOut;
}
self.state = ProcessState::Blocked;
self.waitingForPid = -1;
self.waitingOnObject = nullptr;
self.futexAddr = uaddr;
self.futexSeq = futexNextSeq++;
self.futexWoken = false;
self.sleepUntilTick = (timeoutNs == FutexWaitForever)
? 0
: Timekeeping::GetTicks() + timeoutMs;
self.runningOnCpu = -1;
SwitchAwayFromBlockedCurrentLocked();
// Whatever made this thread runnable (FutexWake, the deadline, or a
// kill) it is no longer parked; clear the key before this thread can
// block for some other reason and be mistaken for a futex waiter.
schedLock.Acquire();
bool woken = self.futexWoken;
self.futexAddr = 0;
self.futexWoken = false;
schedLock.Release();
ExitIfKilled();
return woken ? Success : TimedOut;
}
int FutexWake(uint64_t uaddr, uint32_t count) {
if ((uaddr & 3) != 0 || uaddr == 0) return InvalidArgument;
if (count == 0) return 0;
auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu ? cpu->currentSlot : -1;
if (slot < 0) return GenericFailure;
schedLock.Acquire();
int primary = processTable[slot].primarySlot;
if (primary < 0) primary = slot;
// Gather this process's waiters on uaddr. At most MaxProcesses.
int waiters[MaxProcesses];
int n = 0;
for (int i = 0; i < MaxProcesses; i++) {
Process& p = processTable[i];
if (p.state != ProcessState::Blocked) continue;
if (p.futexAddr != uaddr) continue;
if (p.primarySlot != primary) continue;
waiters[n++] = i;
}
// Waking everyone needs no ordering; otherwise pick oldest-first.
bool ordered = (uint32_t)n > count;
int woken = 0;
while (woken < n && (uint32_t)woken < count) {
if (ordered) {
int best = woken;
for (int j = woken + 1; j < n; j++) {
if (processTable[waiters[j]].futexSeq < processTable[waiters[best]].futexSeq)
best = j;
}
int tmp = waiters[woken]; waiters[woken] = waiters[best]; waiters[best] = tmp;
}
Process& p = processTable[waiters[woken]];
p.futexAddr = 0;
p.futexWoken = true;
p.sleepUntilTick = 0;
p.state = ProcessState::Ready;
readyCount++;
woken++;
}
schedLock.Release();
if (woken > 0) {
// Re-read: this thread may have migrated since `cpu` was sampled.
auto* here = Smp::GetCurrentCpuData();
KickOneIdleCpu(here ? here->cpuIndex : -1);
}
return woken;
}
bool IsAlive(int pid) { bool IsAlive(int pid) {
for (int i = 0; i < MaxProcesses; i++) { for (int i = 0; i < MaxProcesses; i++) {
if (processTable[i].pid == pid) { if (processTable[i].pid == pid) {
+40 -3
View File
@@ -6,6 +6,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <Common/TerminationCodes.hpp>
#include <atomic> #include <atomic>
#include <Api/Syscall.hpp> #include <Api/Syscall.hpp>
@@ -126,6 +127,13 @@ namespace Sched {
bool joinable = true; // Sibling thread keeps Terminated state for the joiner bool joinable = true; // Sibling thread keeps Terminated state for the joiner
uint64_t threadArg = 0; // Argument passed into entryPoint on first dispatch uint64_t threadArg = 0; // Argument passed into entryPoint on first dispatch
// Futex wait state (see FutexWait). futexAddr is the user VA this
// thread is parked on, 0 when not in a futex wait. futexSeq orders
// waiters so FutexWake releases the longest-waiting first.
uint64_t futexAddr = 0;
uint64_t futexSeq = 0;
bool futexWoken = false;
// I/O redirection for GUI terminal // I/O redirection for GUI terminal
bool redirected = false; bool redirected = false;
int parentPid = -1; int parentPid = -1;
@@ -135,9 +143,13 @@ namespace Sched {
uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar) uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar)
volatile uint32_t inHead = 0; volatile uint32_t inHead = 0;
volatile uint32_t inTail = 0; volatile uint32_t inTail = 0;
montauk::abi::KeyEvent keyBuf[64]; // parent injects, child reads (getkey/iskeyavailable)
volatile uint32_t keyHead = 0; volatile uint32_t keyHead = 0;
volatile uint32_t keyTail = 0; volatile uint32_t keyTail = 0;
// SYS_GETCHAR returns a key's UTF-8 encoding one byte per call; the
// bytes not handed out yet wait here.
uint8_t getcharUtf8[4] = {};
uint8_t getcharUtf8Len = 0;
uint8_t getcharUtf8Pos = 0;
static constexpr uint32_t IoBufSize = 4096; static constexpr uint32_t IoBufSize = 4096;
// GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE) // GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE)
@@ -266,10 +278,35 @@ namespace Sched {
// Wake any processes blocked on the given object. // Wake any processes blocked on the given object.
void WakeObjectWaiters(void* object); void WakeObjectWaiters(void* object);
// ====================================================================
// Futexes
// ====================================================================
// Futexes are private to a process: the key is (primary slot, user VA),
// so two processes waiting on the same address never see each other.
static constexpr uint64_t FutexWaitForever = ~0ULL;
// Block the current thread on the 32-bit word at `uaddr` if it still
// holds `expected`. The comparison and the enrollment happen atomically
// under the scheduler lock, so a FutexWake issued after the waker changed
// the word can never be lost. `timeoutNs` is rounded up to the 1 ms tick;
// FutexWaitForever waits without a deadline, 0 only performs the check.
// Returns Success when woken by FutexWake, WouldBlock when the word did
// not hold `expected`, TimedOut when the deadline passed, InvalidArgument
// for a misaligned address, BadPointer when the word is not mapped.
// Spurious Success returns are not possible, but callers must still
// re-check their condition: another thread may have run in between.
int FutexWait(uint64_t uaddr, uint32_t expected, uint64_t timeoutNs);
// Wake up to `count` threads of the current process blocked on `uaddr`,
// longest-waiting first. Returns the number woken.
int FutexWake(uint64_t uaddr, uint32_t count);
// Kill a process by PID. If the process is running on another CPU, // Kill a process by PID. If the process is running on another CPU,
// sets a kill-pending flag checked on the next timer tick. // sets a kill-pending flag checked on the next timer tick. exitCode is
// what a waiter sees (a TerminationCodes value).
// Returns 0 on success, -1 on failure. // Returns 0 on success, -1 on failure.
int KillProcess(int pid); int KillProcess(int pid, int exitCode = TerminatedKilled);
// Start a new process session for the caller, or terminate every process // Start a new process session for the caller, or terminate every process
// belonging to a session. KillSession returns the number of live members // belonging to a session. KillSession returns the number of live members
+33 -3
View File
@@ -96,9 +96,9 @@ WPDIR := data/wallpapers
WPSRC := $(wildcard $(WPDIR)/*.jpg) WPSRC := $(wildcard $(WPDIR)/*.jpg)
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC)) WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls .PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls rust
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST) all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad rust $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
# Build BearSSL static library (cross-compiled for freestanding x86_64). # 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) BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
@@ -320,6 +320,36 @@ mandelbrot: libc
screenshot: libc libjpegwrite libloader dialogs screenshot: libc libjpegwrite libloader dialogs
$(MAKE) -C src/screenshot $(MAKE) -C src/screenshot
# Rust programs (programs/rust): std and no_std programs for the built-in
# x86_64-unknown-montauk target of the forked rustc (Phase 2), registered with
# rustup as the 'montauk' toolchain (see programs/rust/README.md). Optional:
# skipped with a notice when that toolchain is missing, so the default build
# never requires Rust. RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading one.
# Rust links the toolchain sysroot libc (like ports), not lib/libc directly:
# re-run build-montauk-toolchain.sh after libc changes. Cargo tracks neither
# that archive nor a rebuilt std in the linked toolchain, so the programs are
# cleaned (and rebuilt) when either is newer than the last build.
CARGO ?= $(firstword $(wildcard $(HOME)/.cargo/bin/cargo) $(shell command -v cargo 2>/dev/null))
RUST_DIR := rust
RUST_OUT := $(RUST_DIR)/target/x86_64-unknown-montauk/release
RUST_PROGRAMS := rusthello ruststd
RUST_LIBC := ../toolchain/sysroot/usr/lib/libc.a
rust:
@if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 rustc --print target-list 2>/dev/null | grep -qx x86_64-unknown-montauk); then \
echo "rust: no 'montauk' rustup toolchain - skipping Rust programs (see programs/rust/README.md)"; \
else \
stdlib=$$(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 rustc --print sysroot)/lib/rustlib/x86_64-unknown-montauk/lib; \
if [ ! -f $(RUST_OUT)/.link-stamp ] || [ $(RUST_LIBC) -nt $(RUST_OUT)/.link-stamp ] || \
[ -n "$$(find $$stdlib -name '*.rlib' -newer $(RUST_OUT)/.link-stamp 2>/dev/null | head -1)" ]; then \
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) clean --release --quiet $(addprefix -p ,$(RUST_PROGRAMS))) || exit 1; \
fi; \
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) build --release) || exit 1; \
touch $(RUST_OUT)/.link-stamp; \
mkdir -p $(BINDIR)/os; \
for p in $(RUST_PROGRAMS); do cp $(RUST_OUT)/$$p $(BINDIR)/os/$$p.elf || exit 1; done; \
fi
# Verify the TCC C API header (libc/montauk.h) exposes every kernel syscall with # Verify the TCC C API header (libc/montauk.h) exposes every kernel syscall with
# matching numbers. Fails the build on drift; 'make gen-syscalls' refreshes them. # matching numbers. Fails the build on drift; 'make gen-syscalls' refreshes them.
check-syscalls: check-syscalls:
@@ -486,7 +516,7 @@ else
endif endif
clean: clean:
rm -rf $(BINDIR) obj rm -rf $(BINDIR) obj $(RUST_DIR)/target
$(MAKE) -C lib/bearssl clean $(MAKE) -C lib/bearssl clean
$(MAKE) -C lib/libc clean $(MAKE) -C lib/libc clean
$(MAKE) -C lib/libjpeg clean $(MAKE) -C lib/libjpeg clean
+44 -11
View File
@@ -15,15 +15,10 @@
# base: character produced unshifted, one per scancode # base: character produced unshifted, one per scancode
# shift: character produced with Shift held, one per scancode # shift: character produced with Shift held, one per scancode
# altgr: character produced with AltGr held, one per scancode # altgr: character produced with AltGr held, one per scancode
# # dead_keys: (optional) characters that are dead keys in this layout
# Characters are Windows-1252 byte values.
#
# A value of 0x00 means "no override" - the key keeps whatever the kernel's
# US scancode table produced. This is how a layout declares only the keys it
# actually changes.
[registry] [registry]
layouts = ["en", "no", "sv", "da"] layouts = ["en", "no", "sv", "da", "cs"]
# ============================================================================= # =============================================================================
# LAYOUTS # LAYOUTS
@@ -54,7 +49,7 @@ shift = [
] ]
altgr = [ altgr = [
0x40, 0xA3, 0x24, 0x00, 0x7B, 0x5B, 0x5D, 0x7D, # @ GBP $ . { [ ] } 0x40, 0xA3, 0x24, 0x00, 0x7B, 0x5B, 0x5D, 0x7D, # @ GBP $ . { [ ] }
0x00, 0xB4, 0x80, 0x00, 0x7E, 0x00, 0x00, 0x00, # . ' EUR . ~ . . . 0x00, 0xB4, 0x20AC, 0x00, 0x7E, 0x00, 0x00, 0x00, # . ' EUR . ~ . . .
0x00, 0x00, 0x00, 0x00, 0x5C, # . . . . backslash 0x00, 0x00, 0x00, 0x00, 0x5C, # . . . . backslash
] ]
@@ -77,8 +72,8 @@ shift = [
0xBD, 0x2A, 0x3B, 0x3A, 0x5F, 0x3E, # 1/2 * ; : _ > 0xBD, 0x2A, 0x3B, 0x3A, 0x5F, 0x3E, # 1/2 * ; : _ >
] ]
altgr = [ altgr = [
0x40, 0xA3, 0x24, 0x80, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ EUR YEN { [ ] 0x40, 0xA3, 0x24, 0x20AC, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ EUR YEN { [ ]
0x7D, 0x5C, 0xB1, 0x80, 0x00, 0x7E, 0xF8, 0xE6, # } bslash +- EUR . ~ o/ ae 0x7D, 0x5C, 0xB1, 0x20AC, 0x00, 0x7E, 0xF8, 0xE6, # } bslash +- EUR . ~ o/ ae
0xB6, 0xB4, 0x00, 0xB7, 0x00, 0x7C, # par ' . mid . | 0xB6, 0xB4, 0x00, 0xB7, 0x00, 0x7C, # par ' . mid . |
] ]
@@ -102,6 +97,44 @@ shift = [
] ]
altgr = [ altgr = [
0x40, 0xA3, 0x24, 0xBD, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ 1/2 YEN { [ ] 0x40, 0xA3, 0x24, 0xBD, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ 1/2 YEN { [ ]
0x7D, 0xB1, 0x7C, 0x80, 0x00, 0x7E, 0xB4, 0x5E, # } +- | EUR . ~ ' ^ 0x7D, 0xB1, 0x7C, 0x20AC, 0x00, 0x7E, 0xB4, 0x5E, # } +- | EUR . ~ ' ^
0xBE, 0x00, 0x00, 0xB7, 0x00, 0x5C, # 3/4 . . mid . backslash 0xBE, 0x00, 0x00, 0xB7, 0x00, 0x5C, # 3/4 . . mid . backslash
] ]
[layouts.cs]
name = "Czech"
short_name = "cs"
# QWERTZ. The number row types accented letters (digits with Shift), and
# AltGr on the number row gives the dead accents.
scancodes = [
0x29, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x10, 0x11, 0x12,
0x15, 0x1a, 0x1b, 0x21, 0x22, 0x25, 0x26, 0x27,
0x28, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31,
0x33, 0x34, 0x35, 0x56,
]
base = [
0x3B, 0x2B, 0x11B, 0x161, 0x10D, 0x159, 0x17E, 0xFD, # ; + e-caron s-caron c-caron r-caron z-caron y-acute
0xE1, 0xED, 0xE9, 0x3D, 0xB4, 0x00, 0x00, 0x00, # a-acute i-acute e-acute = (dead acute) . . .
0x7A, 0xFA, 0x29, 0x00, 0x00, 0x00, 0x00, 0x16F, # z u-acute ) . . . . u-ring
0xA7, 0xA8, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, # section (dead diaeresis) y . . . . .
0x2C, 0x2E, 0x2D, 0x5C, # , . - backslash
]
shift = [
0xB0, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, # (dead ring) 1 2 3 4 5 6 7
0x38, 0x39, 0x30, 0x25, 0x2C7, 0x00, 0x00, 0x00, # 8 9 0 % (dead caron) . . .
0x5A, 0x2F, 0x28, 0x00, 0x00, 0x00, 0x00, 0x22, # Z / ( . . . . "
0x21, 0x27, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, # ! ' Y . . . . .
0x3F, 0x3A, 0x5F, 0x7C, # ? : _ |
]
altgr = [
0x00, 0x7E, 0x2C7, 0x5E, 0x2D8, 0xB0, 0x2DB, 0x60, # . dead: tilde caron circumflex breve ring ogonek grave
0x2D9, 0xB4, 0x2DD, 0xA8, 0xB8, 0x5C, 0x7C, 0x20AC, # dead: dot acute double-acute diaeresis cedilla; backslash | EUR
0x00, 0xF7, 0xD7, 0x5B, 0x5D, 0x142, 0x141, 0x24, # . divide multiply [ ] l-stroke L-stroke $
0xDF, 0xA4, 0x00, 0x23, 0x26, 0x40, 0x7B, 0x7D, # sharp-s currency . # & @ { }
0x3C, 0x3E, 0x2A, 0x00, # < > * .
]
dead_keys = [
0xB4, 0x2C7, 0xA8, 0xB0, 0x7E, 0x5E,
0x2D8, 0x2DB, 0x60, 0x2D9, 0x2DD, 0xB8,
]
+60 -2
View File
@@ -271,6 +271,34 @@ namespace montauk::abi {
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
/* Unicode key events. Size-versioned: the caller passes sizeof(its
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
rest, so the struct can grow without another syscall. */
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
/* Futexes: process-private wait queues keyed by a user address. WAIT
blocks only if *addr still equals `expected` (checked atomically with
enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check
only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken,
WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the
number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */
static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status
static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status
static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL;
static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu;
/* Abnormal termination from any thread: ends the whole process (every
thread) with exit code TerminatedAborted (256+SIGABRT). SYS_EXIT from a
sibling thread ends only that thread, so abort() needs its own call. */
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
/* Normal termination of the whole process from any thread, with an exit
code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc
exit() and Rust std::process::exit() use it, so exiting from a sibling
thread ends the process rather than only that thread. */
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
/* Kernel-owned process capabilities. User identities may namespace /* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */ per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
@@ -531,7 +559,10 @@ namespace montauk::abi {
uint32_t buildNumber; // monotonic kernel build number uint32_t buildNumber; // monotonic kernel build number
}; };
struct KeyEvent { // Legacy 9-byte key event, read by SYS_GETKEY and written by
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
// `ascii` is a Windows-1252 byte.
struct KeyEventV1 {
uint8_t scancode; uint8_t scancode;
char ascii; char ascii;
bool pressed; bool pressed;
@@ -547,6 +578,32 @@ namespace montauk::abi {
bool extended; bool extended;
}; };
// Key event with the Unicode text it types (SYS_GETKEY_EX,
// SYS_CHILDIO_WRITEKEY_EX, WinEvent). Its first 9 bytes ARE a KeyEventV1, so
// a legacy reader sees a valid event. `codepoint` is the character the key
// produces (0 for control/navigation keys and dead keys). `ascii` is its
// Windows-1252 byte, or KEY_ASCII_SUBSTITUTE when 1252 has none.
struct KeyEvent {
uint8_t scancode;
char ascii;
bool pressed;
bool shift;
bool ctrl;
bool alt;
bool capslock;
bool altgr;
bool extended;
uint8_t _reserved[3];
uint32_t codepoint;
};
static_assert(sizeof(KeyEventV1) == 9, "KeyEventV1 is frozen ABI");
static_assert(sizeof(KeyEvent) == 16, "KeyEvent must fit the WinEvent union");
// ASCII SUB: `ascii` of a text key whose character has no Windows-1252 byte.
// A control character, so legacy code that only accepts ascii >= 0x20 skips
// it, while code testing `ascii == 0` for navigation keys is not confused.
static constexpr char KEY_ASCII_SUBSTITUTE = 0x1A;
struct MouseState { struct MouseState {
int32_t x; int32_t x;
int32_t y; int32_t y;
@@ -584,12 +641,13 @@ namespace montauk::abi {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
uint8_t _pad[3]; uint8_t _pad[3];
union { union {
KeyEvent key; KeyEvent key; // 16 bytes: the union was already that size (mouse)
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse; struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
struct { int32_t w, h; } resize; struct { int32_t w, h; } resize;
struct { int32_t scale; } scale; struct { int32_t scale; } scale;
}; };
}; };
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
struct WinInfo { struct WinInfo {
int32_t id; int32_t id;
+2 -2
View File
@@ -165,9 +165,9 @@ struct Canvas {
if (!str || !font_data || scale <= 0) return; if (!str || !font_data || scale <= 0) return;
uint32_t pixel = c.to_pixel(); uint32_t pixel = c.to_pixel();
int cx = x; int cx = x;
for (int i = 0; str[i]; i++, cx += FONT_WIDTH * scale) { for (const char* p = str; *p; cx += FONT_WIDTH * scale) {
const uint8_t* glyph = const uint8_t* glyph =
&font_data[(unsigned char)str[i] * FONT_HEIGHT]; &font_data[cp437_index(montauk::utf8::next(&p)) * FONT_HEIGHT];
for (int row = 0; row < FONT_HEIGHT; row++) { for (int row = 0; row < FONT_HEIGHT; row++) {
uint8_t bits = glyph[row]; uint8_t bits = glyph[row];
if (!bits) continue; if (!bits) continue;
+28
View File
@@ -9,12 +9,32 @@
#include <montauk/syscall.h> #include <montauk/syscall.h>
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/heap.h> #include <montauk/heap.h>
#include <montauk/utf8.h>
namespace gui { namespace gui {
// The clipboard carries UTF-8. Text that is not well-formed (legacy
// Windows-1252 bytes) is converted on the way in and on the way out, so
// every reader can assume valid UTF-8.
inline char* clipboard_utf8_copy(const char* text, int len, int* out_len) {
int cap = len * 3 + 1; // each stray byte grows to at most 3
char* out = (char*)montauk::malloc(cap);
if (!out) return nullptr;
*out_len = montauk::utf8::normalize(text, len, out, cap);
return out;
}
inline bool clipboard_set_text(const char* text, int len) { inline bool clipboard_set_text(const char* text, int len) {
if (len < 0) return false; if (len < 0) return false;
if (len > 0 && text == nullptr) return false; if (len > 0 && text == nullptr) return false;
if (len > 0 && !montauk::utf8::valid(text, len)) {
int fixed_len = 0;
char* fixed = clipboard_utf8_copy(text, len, &fixed_len);
if (!fixed) return false;
bool ok = montauk::clipboard_set_text(fixed, (uint32_t)fixed_len) == 0;
montauk::mfree(fixed);
return ok;
}
return montauk::clipboard_set_text(text, (uint32_t)len) == 0; return montauk::clipboard_set_text(text, (uint32_t)len) == 0;
} }
@@ -53,6 +73,14 @@ namespace gui {
} }
text[actual_len] = '\0'; text[actual_len] = '\0';
if (!montauk::utf8::valid(text, (int)actual_len)) {
int fixed_len = 0;
char* fixed = clipboard_utf8_copy(text, (int)actual_len, &fixed_len);
montauk::mfree(text);
if (!fixed) return nullptr;
text = fixed;
actual_len = (uint32_t)fixed_len;
}
if (out_len) *out_len = (int)actual_len; if (out_len) *out_len = (int)actual_len;
if (out_serial) *out_serial = serial; if (out_serial) *out_serial = serial;
return text; return text;
+40 -7
View File
@@ -19,6 +19,34 @@ static constexpr int FONT_HEIGHT = 16;
// TrueType fonts, while login/desktop can recover when TrueType setup fails. // TrueType fonts, while login/desktop can recover when TrueType setup fails.
extern const uint8_t font_data[256 * 16] __attribute__((weak)); extern const uint8_t font_data[256 * 16] __attribute__((weak));
// The bitmap fallback font is Code Page 437. Map a Unicode codepoint to its
// CP437 cell ('?' when it has none) so UTF-8 text still draws sensibly,
// including the box-drawing characters terminal programs use.
inline uint8_t cp437_index(uint32_t cp) {
if (cp < 0x80) return (uint8_t)cp;
static constexpr uint16_t high[128] = {
0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7,
0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5,
0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9,
0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192,
0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA,
0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB,
0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,
0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510,
0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F,
0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567,
0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B,
0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580,
0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x00B5,0x03C4,
0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229,
0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248,
0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0,
};
for (int i = 0; i < 128; i++)
if (high[i] == cp) return (uint8_t)(0x80 + i);
return '?';
}
// Dynamic font height: TTF line height or 16 (bitmap fallback) // Dynamic font height: TTF line height or 16 (bitmap fallback)
inline int system_font_height() { inline int system_font_height() {
if (fonts::system_font && fonts::system_font->valid) if (fonts::system_font && fonts::system_font->valid)
@@ -74,8 +102,8 @@ inline void draw_text(Framebuffer& fb, int x, int y, const char* text, Color fg)
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE); fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
return; return;
} }
for (int i = 0; text[i]; i++) { for (const char* p = text; *p; x += FONT_WIDTH) {
draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg); draw_char(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg);
} }
} }
@@ -84,18 +112,23 @@ inline void draw_text_bg(Framebuffer& fb, int x, int y, const char* text, Color
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE); fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
return; return;
} }
for (int i = 0; text[i]; i++) { for (const char* p = text; *p; x += FONT_WIDTH) {
draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg); draw_char_bg(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg, bg);
} }
} }
// Width of the first `len` bytes of a UTF-8 string in the UI font.
inline int text_width_n(const char* text, int len) {
if (fonts::system_font && fonts::system_font->valid)
return fonts::system_font->measure_text_n(text, len, fonts::UI_SIZE);
return montauk::utf8::count(text, len) * FONT_WIDTH;
}
inline int text_width(const char* text) { inline int text_width(const char* text) {
if (fonts::system_font && fonts::system_font->valid) { if (fonts::system_font && fonts::system_font->valid) {
return fonts::system_font->measure_text(text, fonts::UI_SIZE); return fonts::system_font->measure_text(text, fonts::UI_SIZE);
} }
int len = 0; return montauk::utf8::count(text) * FONT_WIDTH;
while (text[len]) len++;
return len * FONT_WIDTH;
} }
} // namespace gui } // namespace gui
+63 -35
View File
@@ -296,15 +296,13 @@ inline bool text_input_has_selection(const TextInputState& state, const char* te
return text_input_selection_start(state, len) != text_input_selection_end(state, len); return text_input_selection_start(state, len) != text_input_selection_end(state, len);
} }
// Width of the first `count` bytes (a character boundary). Masked fields
// draw one '*' per character, however many bytes it takes.
inline int text_input_prefix_width(const char* text, int count, bool masked) { inline int text_input_prefix_width(const char* text, int count, bool masked) {
char prefix[512];
int len = 0; int len = 0;
while (text && text[len] && len < count && len < (int)sizeof(prefix) - 1) { while (text && text[len] && len < count) len++;
prefix[len] = masked ? '*' : text[len]; if (!masked) return text_width_n(text, len);
len++; return montauk::utf8::count(text, len) * text_width("*");
}
prefix[len] = '\0';
return text_width(prefix);
} }
inline int text_input_index_from_x(const Rect& bounds, const char* value, inline int text_input_index_from_x(const Rect& bounds, const char* value,
@@ -314,11 +312,13 @@ inline int text_input_index_from_x(const Rect& bounds, const char* value,
int len = text_input_len(value); int len = text_input_len(value);
int prev_w = 0; int prev_w = 0;
for (int i = 0; i < len; i++) { for (int i = 0; i < len;) {
int cur_w = text_input_prefix_width(value, i + 1, masked); int next = montauk::utf8::next_boundary(value, len, i);
int cur_w = text_input_prefix_width(value, next, masked);
int midpoint = prev_w + (cur_w - prev_w) / 2; int midpoint = prev_w + (cur_w - prev_w) / 2;
if (rel_x < midpoint) return i; if (rel_x < midpoint) return i;
prev_w = cur_w; prev_w = cur_w;
i = next;
} }
return len; return len;
} }
@@ -360,8 +360,11 @@ inline int text_input_mouse(TextInputState& state,
return TEXT_INPUT_NONE; return TEXT_INPUT_NONE;
} }
// Bytes >= 0x80 (any non-ASCII character) count as word characters, so word
// jumps never stop inside a multi-byte sequence.
inline bool text_input_word_char(char ch) { inline bool text_input_word_char(char ch) {
return (ch >= 'a' && ch <= 'z') || return (uint8_t)ch >= 0x80 ||
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || (ch >= '0' && ch <= '9') ||
ch == '_'; ch == '_';
@@ -574,8 +577,10 @@ inline int text_input_key(TextInputState& state,
if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE; if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE;
int len = text_input_len(text, cap); int len = text_input_len(text, cap);
state.cursor = text_input_clamp_cursor(state.cursor, len); state.cursor = montauk::utf8::floor_boundary(
state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len); text, len, text_input_clamp_cursor(state.cursor, len));
state.selection_anchor = montauk::utf8::floor_boundary(
text, len, text_input_clamp_cursor(state.selection_anchor, len));
if (state.context.open) { if (state.context.open) {
context_menu_close(state.context); context_menu_close(state.context);
@@ -606,20 +611,22 @@ inline int text_input_key(TextInputState& state,
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0); return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
} }
if (key.scancode == 0x4B || key.scancode == 0x4D || if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x4D ||
key.scancode == 0x47 || key.scancode == 0x4F) { montauk::nav_scancode(key) == 0x47 || montauk::nav_scancode(key) == 0x4F) {
int next = state.cursor; int next = state.cursor;
if (!key.shift && sel_start != sel_end && key.scancode == 0x4B) if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4B)
next = sel_start; next = sel_start;
else if (!key.shift && sel_start != sel_end && key.scancode == 0x4D) else if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4D)
next = sel_end; next = sel_end;
else if (key.scancode == 0x4B) else if (montauk::nav_scancode(key) == 0x4B)
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) : state.cursor - 1; next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor)
else if (key.scancode == 0x4D) : montauk::utf8::prev_boundary(text, state.cursor);
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor) : state.cursor + 1; else if (montauk::nav_scancode(key) == 0x4D)
else if (key.scancode == 0x47) next = key.ctrl ? text_input_next_word_boundary(text, state.cursor)
: montauk::utf8::next_boundary(text, len, state.cursor);
else if (montauk::nav_scancode(key) == 0x47)
next = 0; next = 0;
else if (key.scancode == 0x4F) else if (montauk::nav_scancode(key) == 0x4F)
next = len; next = len;
text_input_set_cursor(state, len, next, key.shift); text_input_set_cursor(state, len, next, key.shift);
@@ -641,15 +648,16 @@ inline int text_input_key(TextInputState& state,
changed = true; changed = true;
} }
} else if (state.cursor > 0) { } else if (state.cursor > 0) {
text_input_delete_range(text, &len, state.cursor - 1, state.cursor); int start = montauk::utf8::prev_boundary(text, state.cursor);
state.cursor--; text_input_delete_range(text, &len, start, state.cursor);
state.cursor = start;
changed = true; changed = true;
} }
state.selection_anchor = state.cursor; state.selection_anchor = state.cursor;
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0); return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
} }
if (key.scancode == 0x53) { if (montauk::nav_scancode(key) == 0x53) {
if (sel_start != sel_end) { if (sel_start != sel_end) {
text_input_delete_range(text, &len, sel_start, sel_end); text_input_delete_range(text, &len, sel_start, sel_end);
state.cursor = sel_start; state.cursor = sel_start;
@@ -661,15 +669,22 @@ inline int text_input_key(TextInputState& state,
changed = true; changed = true;
} }
} else if (state.cursor < len) { } else if (state.cursor < len) {
text_input_delete_range(text, &len, state.cursor, state.cursor + 1); int end = montauk::utf8::next_boundary(text, len, state.cursor);
text_input_delete_range(text, &len, state.cursor, end);
changed = true; changed = true;
} }
state.selection_anchor = state.cursor; state.selection_anchor = state.cursor;
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0); return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
} }
if (key.ctrl || key.alt || !text_input_char_allowed(key.ascii, filter, userdata)) // Typed text is inserted as UTF-8; the filter sees each encoded byte.
char typed[montauk::utf8::MAX_BYTES];
int typed_len = key.ctrl || key.alt ? 0 : montauk::utf8::key_utf8(key, typed);
if (typed_len == 0) return TEXT_INPUT_NONE;
for (int i = 0; i < typed_len; i++) {
if (!text_input_char_allowed(typed[i], filter, userdata))
return TEXT_INPUT_NONE; return TEXT_INPUT_NONE;
}
if (sel_start != sel_end) { if (sel_start != sel_end) {
text_input_delete_range(text, &len, sel_start, sel_end); text_input_delete_range(text, &len, sel_start, sel_end);
@@ -677,11 +692,12 @@ inline int text_input_key(TextInputState& state,
changed = true; changed = true;
} }
if (len < cap - 1) { if (len + typed_len < cap) {
for (int i = len; i >= state.cursor; i--) for (int i = len; i >= state.cursor; i--)
text[i + 1] = text[i]; text[i + typed_len] = text[i];
text[state.cursor++] = key.ascii; for (int i = 0; i < typed_len; i++)
len++; text[state.cursor++] = typed[i];
len += typed_len;
changed = true; changed = true;
} }
@@ -1251,23 +1267,35 @@ inline void draw_badge(Canvas& c,
c.text(x + theme.gap_sm, y + 2, label, fg); c.text(x + theme.gap_sm, y + 2, label, fg);
} }
// One '*' per UTF-8 character.
inline const char* mask_text(const char* value, char* buffer, int cap) { inline const char* mask_text(const char* value, char* buffer, int cap) {
int len = 0; int len = 0;
while (value && value[len] && len < cap - 1) { const char* p = value ? value : "";
buffer[len] = '*'; while (*p && len < cap - 1) {
len++; montauk::utf8::next(&p);
buffer[len++] = '*';
} }
buffer[len] = '\0'; buffer[len] = '\0';
return buffer; return buffer;
} }
// First `count` bytes of value (a character boundary), or one '*' per
// character in them when masked.
inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) { inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) {
if (cap <= 0) return; if (cap <= 0) return;
int len = 0; int len = 0;
if (masked) {
const char* p = value ? value : "";
while (*p && p - value < count && len < cap - 1) {
montauk::utf8::next(&p);
buffer[len++] = '*';
}
} else {
while (value && value[len] && len < count && len < cap - 1) { while (value && value[len] && len < count && len < cap - 1) {
buffer[len] = masked ? '*' : value[len]; buffer[len] = value[len];
len++; len++;
} }
}
buffer[len] = '\0'; buffer[len] = '\0';
} }
+69 -13
View File
@@ -9,13 +9,14 @@
#include "gui/font.hpp" #include "gui/font.hpp"
#include <montauk/syscall.h> #include <montauk/syscall.h>
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/utf8.h>
#include <Api/Syscall.hpp> #include <Api/Syscall.hpp>
#include <montauk/capabilities.h> #include <montauk/capabilities.h>
namespace gui { namespace gui {
struct TermCell { struct TermCell {
char ch; uint32_t ch; // Unicode codepoint (every cell is one column wide)
Color fg; Color fg;
Color bg; Color bg;
}; };
@@ -58,6 +59,11 @@ struct TerminalState {
int csi_params[8]; int csi_params[8];
int csi_param_count; int csi_param_count;
int csi_current_param; int csi_current_param;
// Partial UTF-8 sequence carried across terminal_feed calls (child output
// arrives in arbitrary chunks, so a character can straddle two reads).
char utf8_pending[4];
int utf8_pending_len;
}; };
// ==== Palette ==== // ==== Palette ====
@@ -244,6 +250,7 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
t->csi_private = false; t->csi_private = false;
t->csi_param_count = 0; t->csi_param_count = 0;
t->csi_current_param = 0; t->csi_current_param = 0;
t->utf8_pending_len = 0;
t->child_pid = 0; t->child_pid = 0;
int total_cells = (rows + max_sb) * cols; int total_cells = (rows + max_sb) * cols;
@@ -275,7 +282,7 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
montauk::childio_settermsz(t->child_pid, cols, rows); montauk::childio_settermsz(t->child_pid, cols, rows);
} }
static inline void terminal_put_char(TerminalState* t, char ch) { static inline void terminal_put_char(TerminalState* t, uint32_t ch) {
if (t->cursor_x >= t->cols) { if (t->cursor_x >= t->cols) {
t->cursor_x = 0; t->cursor_x = 0;
t->cursor_y++; t->cursor_y++;
@@ -549,11 +556,61 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
} }
} }
// Emit pending bytes that did not form a valid UTF-8 sequence: each one is a
// Windows-1252 character, matching how the GUI renders legacy text.
static inline void terminal_flush_utf8(TerminalState* t) {
for (int i = 0; i < t->utf8_pending_len; i++)
terminal_put_char(t, montauk::utf8::from_cp1252((uint8_t)t->utf8_pending[i]));
t->utf8_pending_len = 0;
}
static inline int terminal_utf8_expected(uint8_t lead) {
if (lead >= 0xC2 && lead <= 0xDF) return 2;
if (lead >= 0xE0 && lead <= 0xEF) return 3;
if (lead >= 0xF0 && lead <= 0xF4) return 4;
return 0;
}
// Feed one byte >= 0x80 of printable text through the UTF-8 assembler.
static inline void terminal_feed_high(TerminalState* t, uint8_t b) {
if (t->utf8_pending_len > 0) {
if ((b & 0xC0) == 0x80) {
t->utf8_pending[t->utf8_pending_len++] = (char)b;
int want = terminal_utf8_expected((uint8_t)t->utf8_pending[0]);
uint32_t cp = 0;
int n = montauk::utf8::sequence_length(t->utf8_pending,
t->utf8_pending_len, &cp);
if (n == want) {
terminal_put_char(t, cp);
t->utf8_pending_len = 0;
} else if (t->utf8_pending_len >= want) {
terminal_flush_utf8(t); // overlong / surrogate / out of range
}
return;
}
terminal_flush_utf8(t);
}
if (terminal_utf8_expected(b)) {
t->utf8_pending[0] = (char)b;
t->utf8_pending_len = 1;
} else {
terminal_put_char(t, montauk::utf8::from_cp1252(b));
}
}
static inline void terminal_feed(TerminalState* t, const char* data, int len) { static inline void terminal_feed(TerminalState* t, const char* data, int len) {
if (len > 0) t->dirty = true; if (len > 0) t->dirty = true;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
char ch = data[i]; char ch = data[i];
if (t->parse_state == TerminalState::STATE_NORMAL) {
if ((uint8_t)ch >= 0x80) {
terminal_feed_high(t, (uint8_t)ch);
continue;
}
if (t->utf8_pending_len > 0) terminal_flush_utf8(t);
}
switch (t->parse_state) { switch (t->parse_state) {
case TerminalState::STATE_NORMAL: case TerminalState::STATE_NORMAL:
if (ch == '\033') { if (ch == '\033') {
@@ -575,9 +632,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
while (t->cursor_x < next) { while (t->cursor_x < next) {
terminal_put_char(t, ' '); terminal_put_char(t, ' ');
} }
} else if (ch >= 32 || ch < 0) { } else if (ch >= 32) {
// Printable character (also treat high-bit chars as printable) terminal_put_char(t, (uint8_t)ch);
terminal_put_char(t, ch);
} }
break; break;
@@ -641,9 +697,9 @@ static inline void terminal_fill_pixel_rect(uint32_t* pixels, int pw, int ph,
} }
static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph, static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph,
int x, int y, unsigned char ch, int x, int y, uint32_t ch,
uint32_t color) { uint32_t color) {
const uint8_t* glyph = &font_data[ch * FONT_HEIGHT]; const uint8_t* glyph = &font_data[cp437_index(ch) * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT; fy++) { for (int fy = 0; fy < FONT_HEIGHT; fy++) {
int dy = y + fy; int dy = y + fy;
if (dy >= ph) break; if (dy >= ph) break;
@@ -669,14 +725,14 @@ static inline void terminal_draw_cell_pixels(const TermCell& cell,
cell.bg.to_pixel()); cell.bg.to_pixel());
} }
if (cell.ch <= 32 && cell.ch >= 0) return; if (cell.ch <= 32) return;
if (use_ttf) { if (use_ttf) {
int baseline = py + gc->ascent; int baseline = py + gc->ascent;
fonts::mono->draw_char_to_buffer(pixels, pw, ph, fonts::mono->draw_char_to_buffer(pixels, pw, ph,
px, baseline, (unsigned char)cell.ch, cell.fg, gc); px, baseline, (int)cell.ch, cell.fg, gc);
} else { } else {
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
cell.fg.to_pixel()); cell.fg.to_pixel());
} }
} }
@@ -691,14 +747,14 @@ static inline void terminal_draw_cursor_pixels(const TermCell& cell,
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h, terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
g_term_palette.cursor.to_pixel()); g_term_palette.cursor.to_pixel());
if (cell.ch <= 32 && cell.ch >= 0) return; if (cell.ch <= 32) return;
if (use_ttf) { if (use_ttf) {
int baseline = py + gc->ascent; int baseline = py + gc->ascent;
fonts::mono->draw_char_to_buffer(pixels, pw, ph, fonts::mono->draw_char_to_buffer(pixels, pw, ph,
px, baseline, (unsigned char)cell.ch, g_term_palette.bg, gc); px, baseline, (int)cell.ch, g_term_palette.bg, gc);
} else { } else {
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
g_term_palette.bg.to_pixel()); g_term_palette.bg.to_pixel());
} }
} }
+113 -51
View File
@@ -9,6 +9,7 @@
#include <montauk/syscall.h> #include <montauk/syscall.h>
#include <montauk/heap.h> #include <montauk/heap.h>
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/utf8.h>
#include "gui/gui.hpp" #include "gui/gui.hpp"
#include "gui/framebuffer.hpp" #include "gui/framebuffer.hpp"
@@ -41,39 +42,12 @@
namespace gui { namespace gui {
// Windows-1252 byte -> Unicode codepoint, for the remaining code that still
// handles single-byte text (PDF simple-font encodings). UTF-8 strings go
// through measure_text/draw, which decode with montauk::utf8.
inline int decode_single_byte_codepoint(int codepoint) { inline int decode_single_byte_codepoint(int codepoint) {
if (codepoint < 0 || codepoint > 255) return codepoint; if (codepoint < 0 || codepoint > 255) return codepoint;
return (int)montauk::utf8::from_cp1252((uint8_t)codepoint);
switch (codepoint) {
case 0x80: return 0x20AC; // Euro Sign
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
case 0x83: return 0x0192; // Latin Small Letter F With Hook
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
case 0x85: return 0x2026; // Horizontal Ellipsis
case 0x86: return 0x2020; // Dagger
case 0x87: return 0x2021; // Double Dagger
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
case 0x89: return 0x2030; // Per Mille Sign
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
case 0x8C: return 0x0152; // Latin Capital Ligature OE
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
case 0x91: return 0x2018; // Left Single Quotation Mark
case 0x92: return 0x2019; // Right Single Quotation Mark
case 0x93: return 0x201C; // Left Double Quotation Mark
case 0x94: return 0x201D; // Right Double Quotation Mark
case 0x95: return 0x2022; // Bullet
case 0x96: return 0x2013; // En Dash
case 0x97: return 0x2014; // Em Dash
case 0x98: return 0x02DC; // Small Tilde
case 0x99: return 0x2122; // Trade Mark Sign
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
case 0x9C: return 0x0153; // Latin Small Ligature OE
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
default: return codepoint;
}
} }
struct CachedGlyph { struct CachedGlyph {
@@ -84,8 +58,18 @@ struct CachedGlyph {
bool loaded; bool loaded;
}; };
// Glyphs above U+00FF live in a small open-addressed table so CJK, symbols
// and the like cost nothing until they are drawn.
struct ExtGlyph {
uint32_t codepoint; // 0 = empty slot
CachedGlyph glyph;
};
struct GlyphCache { struct GlyphCache {
CachedGlyph glyphs[256]; // Single-byte text with Windows-1252 punctuation mapping CachedGlyph glyphs[256]; // Unicode U+0000-U+00FF, indexed directly
ExtGlyph* ext; // everything above, hashed by codepoint
int ext_cap; // power of two, 0 until first use
int ext_count;
int pixel_size; int pixel_size;
float scale; float scale;
int ascent, descent, line_gap; int ascent, descent, line_gap;
@@ -142,8 +126,21 @@ struct TrueTypeFont {
return true; return true;
} }
void init_cache(GlyphCache* gc, int pixel_size) { static void free_ext(GlyphCache* gc) {
// Free any existing glyph bitmaps if (gc->ext) {
for (int i = 0; i < gc->ext_cap; i++) {
if (gc->ext[i].codepoint && gc->ext[i].glyph.bitmap)
montauk::mfree(gc->ext[i].glyph.bitmap);
}
montauk::mfree(gc->ext);
}
gc->ext = nullptr;
gc->ext_cap = 0;
gc->ext_count = 0;
}
// Release every glyph bitmap in a cache (the cache stays usable).
static void free_cache(GlyphCache* gc) {
for (int i = 0; i < 256; i++) { for (int i = 0; i < 256; i++) {
if (gc->glyphs[i].bitmap) { if (gc->glyphs[i].bitmap) {
montauk::mfree(gc->glyphs[i].bitmap); montauk::mfree(gc->glyphs[i].bitmap);
@@ -151,6 +148,11 @@ struct TrueTypeFont {
gc->glyphs[i].bitmap = nullptr; gc->glyphs[i].bitmap = nullptr;
gc->glyphs[i].loaded = false; gc->glyphs[i].loaded = false;
} }
free_ext(gc);
}
void init_cache(GlyphCache* gc, int pixel_size) {
free_cache(gc);
gc->pixel_size = pixel_size; gc->pixel_size = pixel_size;
gc->scale = em_scaling gc->scale = em_scaling
@@ -179,11 +181,8 @@ struct TrueTypeFont {
return gc; return gc;
} }
// Evict oldest cache (slot 0) — free its glyph bitmaps // Evict oldest cache (slot 0) - free its glyph bitmaps
for (int g = 0; g < 256; g++) { free_cache(&caches[0]);
if (caches[0].glyphs[g].bitmap)
montauk::mfree(caches[0].glyphs[g].bitmap);
}
// Shift remaining caches down // Shift remaining caches down
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache)); montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache));
@@ -192,13 +191,54 @@ struct TrueTypeFont {
return &caches[3]; return &caches[3];
} }
static constexpr int EXT_MAX = 2048; // flush the table beyond this
CachedGlyph* ext_slot(GlyphCache* gc, uint32_t codepoint) {
if (gc->ext_cap == 0 || gc->ext_count * 4 >= gc->ext_cap * 3) {
int new_cap = gc->ext_cap ? gc->ext_cap * 2 : 64;
if (gc->ext_count >= EXT_MAX) {
// A document full of distinct glyphs: start over rather than
// grow without bound. Bitmaps are rebuilt on demand.
free_ext(gc);
new_cap = 64;
}
ExtGlyph* table = (ExtGlyph*)montauk::malloc(sizeof(ExtGlyph) * new_cap);
if (!table) return nullptr;
montauk::memset(table, 0, sizeof(ExtGlyph) * new_cap);
for (int i = 0; i < gc->ext_cap; i++) {
ExtGlyph& e = gc->ext[i];
if (!e.codepoint) continue;
uint32_t h = (e.codepoint * 2654435761u) & (new_cap - 1);
while (table[h].codepoint) h = (h + 1) & (new_cap - 1);
table[h] = e;
}
if (gc->ext) montauk::mfree(gc->ext);
gc->ext = table;
gc->ext_cap = new_cap;
}
uint32_t mask = gc->ext_cap - 1;
uint32_t h = (codepoint * 2654435761u) & mask;
while (gc->ext[h].codepoint && gc->ext[h].codepoint != codepoint)
h = (h + 1) & mask;
if (!gc->ext[h].codepoint) {
gc->ext[h].codepoint = codepoint;
montauk::memset(&gc->ext[h].glyph, 0, sizeof(CachedGlyph));
gc->ext_count++;
}
return &gc->ext[h].glyph;
}
// Glyph for a Unicode codepoint. Codepoints the font lacks render as its
// .notdef box, so missing characters stay visible instead of vanishing.
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) { CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
if (codepoint < 0 || codepoint >= 256) return nullptr; if (codepoint < 0 || codepoint > 0x10FFFF) return nullptr;
CachedGlyph* g = &gc->glyphs[codepoint]; CachedGlyph* g = codepoint < 256 ? &gc->glyphs[codepoint]
: ext_slot(gc, (uint32_t)codepoint);
if (!g) return nullptr;
if (g->loaded) return g; if (g->loaded) return g;
g->loaded = true; g->loaded = true;
int font_codepoint = decode_single_byte_codepoint(codepoint); int font_codepoint = codepoint;
int advance, lsb; int advance, lsb;
stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb); stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb);
g->advance = (int)(advance * gc->scale); g->advance = (int)(advance * gc->scale);
@@ -222,12 +262,34 @@ struct TrueTypeFont {
return g; return g;
} }
// Advance of one codepoint at a size (0 if the font is unusable).
int char_advance(int codepoint, int pixel_size) {
if (!valid) return 0;
CachedGlyph* g = get_glyph(get_cache(pixel_size), codepoint);
return g ? g->advance : 0;
}
// Width of the first `len` bytes of a UTF-8 string.
int measure_text_n(const char* text, int len, int pixel_size) {
if (!valid) return 0;
GlyphCache* gc = get_cache(pixel_size);
int w = 0;
for (int pos = 0; pos < len && text[pos];) {
int n;
uint32_t cp = montauk::utf8::decode(text + pos, len - pos, &n);
CachedGlyph* g = get_glyph(gc, (int)cp);
if (g) w += g->advance;
pos += n;
}
return w;
}
int measure_text(const char* text, int pixel_size) { int measure_text(const char* text, int pixel_size) {
if (!valid) return 0; if (!valid) return 0;
GlyphCache* gc = get_cache(pixel_size); GlyphCache* gc = get_cache(pixel_size);
int w = 0; int w = 0;
for (int i = 0; text[i]; i++) { for (const char* p = text; *p;) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
if (g) w += g->advance; if (g) w += g->advance;
} }
return w; return w;
@@ -253,8 +315,8 @@ struct TrueTypeFont {
uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) | uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) |
((uint32_t)color.g << 8) | color.b; ((uint32_t)color.g << 8) | color.b;
for (int i = 0; text[i]; i++) { for (const char* p = text; *p;) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
if (!g) continue; if (!g) continue;
if (g->bitmap) { if (g->bitmap) {
@@ -325,8 +387,8 @@ struct TrueTypeFont {
int baseline = y + gc->ascent; int baseline = y + gc->ascent;
bool drew_pixel = false; bool drew_pixel = false;
for (int i = 0; text[i]; i++) { for (const char* p = text; *p;) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
if (!g) continue; if (!g) continue;
if (g->bitmap) { if (g->bitmap) {
@@ -378,8 +440,8 @@ struct TrueTypeFont {
int clip_x2 = clip_x + clip_w; int clip_x2 = clip_x + clip_w;
int clip_y2 = clip_y + clip_h; int clip_y2 = clip_y + clip_h;
for (int i = 0; text[i]; i++) { for (const char* p = text; *p;) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
if (!g) continue; if (!g) continue;
if (g->bitmap) { if (g->bitmap) {
@@ -417,7 +479,7 @@ struct TrueTypeFont {
} }
} }
// Draw single character to buffer, returning advance width // Draw one Unicode codepoint to buffer, returning advance width
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h, int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
int x, int baseline, int codepoint, int x, int baseline, int codepoint,
Color color, GlyphCache* gc) { Color color, GlyphCache* gc) {
+24 -27
View File
@@ -212,11 +212,7 @@ struct TextBox {
// Draw cursor if focused // Draw cursor if focused
if (focused) { if (focused) {
// Measure text up to cursor position for proportional fonts int cx = tx + text_width_n(text, cursor);
char prefix[256];
for (int i = 0; i < cursor && i < 255; i++) prefix[i] = text[i];
prefix[cursor < 255 ? cursor : 255] = '\0';
int cx = tx + text_width(prefix);
draw_vline(fb, cx, ty, fh, cursor_color); draw_vline(fb, cx, ty, fh, cursor_color);
} }
} }
@@ -230,34 +226,35 @@ struct TextBox {
void handle_key(const montauk::abi::KeyEvent& key) { void handle_key(const montauk::abi::KeyEvent& key) {
if (!focused || !key.pressed) return; if (!focused || !key.pressed) return;
char typed[montauk::utf8::MAX_BYTES];
int typed_len = montauk::utf8::key_utf8(key, typed);
if (key.ascii == '\b' || key.scancode == 0x0E) { if (key.ascii == '\b' || key.scancode == 0x0E) {
// Backspace // Backspace: remove the whole character before the cursor
if (cursor > 0) { if (cursor > 0) {
for (int i = cursor - 1; i < text_len - 1; i++) { int start = montauk::utf8::prev_boundary(text, cursor);
text[i] = text[i + 1]; int n = cursor - start;
for (int i = start; i <= text_len - n; i++)
text[i] = text[i + n];
text_len -= n;
cursor = start;
} }
text_len--; } else if (typed_len > 0) {
cursor--; // Printable character, inserted as UTF-8
text[text_len] = '\0'; if (text_len + typed_len <= 254) {
for (int i = text_len; i >= cursor; i--)
text[i + typed_len] = text[i];
for (int i = 0; i < typed_len; i++)
text[cursor + i] = typed[i];
cursor += typed_len;
text_len += typed_len;
} }
} else if ((unsigned char)key.ascii >= 32 } else if (montauk::nav_scancode(key) == 0x4B) {
&& (unsigned char)key.ascii != 127) {
// Printable character
if (text_len < 254) {
for (int i = text_len; i > cursor; i--) {
text[i] = text[i - 1];
}
text[cursor] = key.ascii;
cursor++;
text_len++;
text[text_len] = '\0';
}
} else if (key.scancode == 0x4B) {
// Left arrow // Left arrow
if (cursor > 0) cursor--; cursor = montauk::utf8::prev_boundary(text, cursor);
} else if (key.scancode == 0x4D) { } else if (montauk::nav_scancode(key) == 0x4D) {
// Right arrow // Right arrow
if (cursor < text_len) cursor++; cursor = montauk::utf8::next_boundary(text, text_len, cursor);
} }
} }
}; };
+23
View File
@@ -217,6 +217,12 @@ extern "C" {
#define MTK_SYS_CONSENT_REGISTER 191 #define MTK_SYS_CONSENT_REGISTER 191
#define MTK_SYS_CONSENT_NEXT 192 #define MTK_SYS_CONSENT_NEXT 192
#define MTK_SYS_CONSENT_REPLY 193 #define MTK_SYS_CONSENT_REPLY 193
#define MTK_SYS_GETKEY_EX 194
#define MTK_SYS_CHILDIO_WRITEKEY_EX 195
#define MTK_SYS_FUTEX_WAIT 196
#define MTK_SYS_FUTEX_WAKE 197
#define MTK_SYS_ABORT 198
#define MTK_SYS_EXIT_GROUP 199
/* @SYSCALLS-END */ /* @SYSCALLS-END */
/* Syscall status codes (montauk/statuscodes.h for C++). */ /* Syscall status codes (montauk/statuscodes.h for C++). */
@@ -238,6 +244,8 @@ extern "C" {
#define MTK_STATUS_BUSY (-12) #define MTK_STATUS_BUSY (-12)
#define MTK_STATUS_BUFFER_TOO_SMALL (-13) #define MTK_STATUS_BUFFER_TOO_SMALL (-13)
#define MTK_STATUS_OUT_OF_MEMORY (-14) #define MTK_STATUS_OUT_OF_MEMORY (-14)
#define MTK_STATUS_TIMED_OUT (-15)
#define MTK_STATUS_WOULD_BLOCK (-16)
/* @STATUS-END */ /* @STATUS-END */
#define MTK_SOCK_TCP 1 #define MTK_SOCK_TCP 1
@@ -489,6 +497,21 @@ static inline void mtk_sleep_ms(unsigned long ms) {
_mtk_syscall1(MTK_SYS_SLEEP_MS, (long)ms); _mtk_syscall1(MTK_SYS_SLEEP_MS, (long)ms);
} }
/* Futexes (process-private). mtk_futex_wait blocks while *addr == expected;
returns 0 when woken, MTK_STATUS_TIMED_OUT, or MTK_STATUS_WOULD_BLOCK when
the word had already changed. timeout_ns: MTK_FUTEX_WAIT_FOREVER for no
deadline, 0 to only check. mtk_futex_wake returns the number woken. */
#define MTK_FUTEX_WAIT_FOREVER (~0UL)
#define MTK_FUTEX_WAKE_ALL 0xFFFFFFFFu
static inline int mtk_futex_wait(uint32_t *addr, uint32_t expected, unsigned long timeout_ns) {
return (int)_mtk_syscall3(MTK_SYS_FUTEX_WAIT, (long)addr, (long)expected, (long)timeout_ns);
}
static inline int mtk_futex_wake(uint32_t *addr, uint32_t count) {
return (int)_mtk_syscall2(MTK_SYS_FUTEX_WAKE, (long)addr, (long)count);
}
static inline int mtk_getpid(void) { static inline int mtk_getpid(void) {
return (int)_mtk_syscall0(MTK_SYS_GETPID); return (int)_mtk_syscall0(MTK_SYS_GETPID);
} }
+188 -36
View File
@@ -8,19 +8,22 @@
#include <montauk/config.h> #include <montauk/config.h>
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/utf8.h>
namespace montauk::keyboard { namespace montauk::keyboard {
inline constexpr int MAX_LAYOUTS = 8; inline constexpr int MAX_LAYOUTS = 8;
inline constexpr int MAX_KEYS = 64; inline constexpr int MAX_KEYS = 64;
inline constexpr int MAX_DEAD_KEYS = 16;
// One overridden key. Characters are Windows-1252 bytes to match the // One overridden key. Characters are Unicode codepoints; translation stores
// single-byte GUI text stack; 0 means "no override, keep the kernel's value". // them in KeyEvent::codepoint (and the Windows-1252 byte, if any, in ascii).
// 0 means "no override, keep the kernel's value".
struct KeyMap { struct KeyMap {
uint8_t scancode; uint8_t scancode;
uint8_t base; uint32_t base;
uint8_t shift; uint32_t shift;
uint8_t altgr; uint32_t altgr;
}; };
struct Layout { struct Layout {
@@ -29,6 +32,10 @@ struct Layout {
char short_name[8]; char short_name[8];
KeyMap keys[MAX_KEYS]; KeyMap keys[MAX_KEYS];
int key_count; // 0 = passthrough (the kernel's US table) int key_count; // 0 = passthrough (the kernel's US table)
// Characters that act as dead keys in this layout: pressing one types
// nothing and modifies the next character (see compose()).
uint32_t dead_keys[MAX_DEAD_KEYS];
int dead_count;
}; };
struct Registry { struct Registry {
@@ -40,8 +47,71 @@ struct State {
Registry registry; Registry registry;
bool enabled[MAX_LAYOUTS]; bool enabled[MAX_LAYOUTS];
int active; int active;
uint32_t pending_dead; // dead key waiting for the next character, 0 = none
}; };
// ==== Dead-key composition ====
// Keyed by the spacing form of the accent, which is what a layout puts on the
// dead key itself (and what dead key + Space types).
struct ComposeEntry {
uint16_t dead;
uint16_t base;
uint16_t result;
};
inline constexpr ComposeEntry COMPOSE[] = {
// acute
{0x00B4,'a',0xE1},{0x00B4,'A',0xC1},{0x00B4,'e',0xE9},{0x00B4,'E',0xC9},
{0x00B4,'i',0xED},{0x00B4,'I',0xCD},{0x00B4,'o',0xF3},{0x00B4,'O',0xD3},
{0x00B4,'u',0xFA},{0x00B4,'U',0xDA},{0x00B4,'y',0xFD},{0x00B4,'Y',0xDD},
{0x00B4,'c',0x107},{0x00B4,'C',0x106},{0x00B4,'l',0x13A},{0x00B4,'L',0x139},
{0x00B4,'n',0x144},{0x00B4,'N',0x143},{0x00B4,'r',0x155},{0x00B4,'R',0x154},
{0x00B4,'s',0x15B},{0x00B4,'S',0x15A},{0x00B4,'z',0x17A},{0x00B4,'Z',0x179},
// caron
{0x02C7,'c',0x10D},{0x02C7,'C',0x10C},{0x02C7,'d',0x10F},{0x02C7,'D',0x10E},
{0x02C7,'e',0x11B},{0x02C7,'E',0x11A},{0x02C7,'l',0x13E},{0x02C7,'L',0x13D},
{0x02C7,'n',0x148},{0x02C7,'N',0x147},{0x02C7,'r',0x159},{0x02C7,'R',0x158},
{0x02C7,'s',0x161},{0x02C7,'S',0x160},{0x02C7,'t',0x165},{0x02C7,'T',0x164},
{0x02C7,'z',0x17E},{0x02C7,'Z',0x17D},
// diaeresis
{0x00A8,'a',0xE4},{0x00A8,'A',0xC4},{0x00A8,'e',0xEB},{0x00A8,'E',0xCB},
{0x00A8,'i',0xEF},{0x00A8,'I',0xCF},{0x00A8,'o',0xF6},{0x00A8,'O',0xD6},
{0x00A8,'u',0xFC},{0x00A8,'U',0xDC},{0x00A8,'y',0xFF},{0x00A8,'Y',0x178},
// ring above
{0x00B0,'u',0x16F},{0x00B0,'U',0x16E},{0x00B0,'a',0xE5},{0x00B0,'A',0xC5},
// grave
{0x0060,'a',0xE0},{0x0060,'A',0xC0},{0x0060,'e',0xE8},{0x0060,'E',0xC8},
{0x0060,'i',0xEC},{0x0060,'I',0xCC},{0x0060,'o',0xF2},{0x0060,'O',0xD2},
{0x0060,'u',0xF9},{0x0060,'U',0xD9},
// circumflex
{0x005E,'a',0xE2},{0x005E,'A',0xC2},{0x005E,'e',0xEA},{0x005E,'E',0xCA},
{0x005E,'i',0xEE},{0x005E,'I',0xCE},{0x005E,'o',0xF4},{0x005E,'O',0xD4},
{0x005E,'u',0xFB},{0x005E,'U',0xDB},
// tilde
{0x007E,'a',0xE3},{0x007E,'A',0xC3},{0x007E,'n',0xF1},{0x007E,'N',0xD1},
{0x007E,'o',0xF5},{0x007E,'O',0xD5},
// breve
{0x02D8,'a',0x103},{0x02D8,'A',0x102},{0x02D8,'g',0x11F},{0x02D8,'G',0x11E},
// ogonek
{0x02DB,'a',0x105},{0x02DB,'A',0x104},{0x02DB,'e',0x119},{0x02DB,'E',0x118},
{0x02DB,'i',0x12F},{0x02DB,'I',0x12E},{0x02DB,'u',0x173},{0x02DB,'U',0x172},
// dot above
{0x02D9,'z',0x17C},{0x02D9,'Z',0x17B},{0x02D9,'e',0x117},{0x02D9,'E',0x116},
{0x02D9,'I',0x130},
// double acute
{0x02DD,'o',0x151},{0x02DD,'O',0x150},{0x02DD,'u',0x171},{0x02DD,'U',0x170},
// cedilla
{0x00B8,'c',0xE7},{0x00B8,'C',0xC7},{0x00B8,'s',0x15F},{0x00B8,'S',0x15E},
};
// The composed character, or 0 if the accent does not combine with base.
inline uint32_t compose(uint32_t dead, uint32_t base) {
for (const ComposeEntry& e : COMPOSE)
if (e.dead == dead && e.base == base) return e.result;
return 0;
}
inline void build_key(char* out, int cap, const char* prefix, inline void build_key(char* out, int cap, const char* prefix,
const char* id, const char* suffix = nullptr) { const char* id, const char* suffix = nullptr) {
int pos = 0; int pos = 0;
@@ -54,10 +124,9 @@ inline void build_key(char* out, int cap, const char* prefix,
out[pos] = '\0'; out[pos] = '\0';
} }
// Read an array of byte values from the layout table. // Read an array of byte values (scancodes) from the layout table.
// Returns the element count, or -1 if the key is missing or malformed. // Returns the element count, or -1 if the key is missing or malformed.
inline int read_bytes(const toml::Doc& doc, const char* key, inline int read_bytes(const toml::Doc& doc, const char* key, uint8_t* out, int cap) {
uint8_t* out, int cap) {
toml::Value* arr = doc.get_array(key); toml::Value* arr = doc.get_array(key);
if (!arr) return -1; if (!arr) return -1;
if (arr->array.count > cap) return -1; if (arr->array.count > cap) return -1;
@@ -70,6 +139,23 @@ inline int read_bytes(const toml::Doc& doc, const char* key,
return arr->array.count; return arr->array.count;
} }
// Read an array of characters (Unicode codepoints). 0x80-0x9F are C1 control
// codes in Unicode, never key output, so they are read as the Windows-1252
// bytes older layout files used (0x80 = euro sign).
inline int read_codepoints(const toml::Doc& doc, const char* key, uint32_t* out, int cap) {
toml::Value* arr = doc.get_array(key);
if (!arr) return -1;
if (arr->array.count > cap) return -1;
for (int i = 0; i < arr->array.count; i++) {
toml::Value* value = arr->array.items[i];
if (!value || value->type != toml::Type::Int) return -1;
int64_t v = value->ival;
if (v < 0 || v > 0x10FFFF || (v >= 0xD800 && v <= 0xDFFF)) return -1;
out[i] = (v >= 0x80 && v <= 0x9F) ? utf8::from_cp1252((uint8_t)v) : (uint32_t)v;
}
return arr->array.count;
}
// Parse one [layouts.<id>] table. Returns false if the layout declares key // Parse one [layouts.<id>] table. Returns false if the layout declares key
// overrides but they are inconsistent, in which case the caller skips it: a // overrides but they are inconsistent, in which case the caller skips it: a
// malformed table must never produce a half-applied layout. // malformed table must never produce a half-applied layout.
@@ -85,10 +171,15 @@ inline bool load_layout(Layout* out, const char* id, const toml::Doc& doc) {
montauk::strncpy(out->short_name, doc.get_string(key, id), montauk::strncpy(out->short_name, doc.get_string(key, id),
sizeof(out->short_name)); sizeof(out->short_name));
build_key(key, sizeof(key), "layouts.", id, ".dead_keys");
int dead = read_codepoints(doc, key, out->dead_keys, MAX_DEAD_KEYS);
if (dead < 0 && doc.get_array(key)) return false; // present but malformed
out->dead_count = dead < 0 ? 0 : dead;
uint8_t scancodes[MAX_KEYS]; uint8_t scancodes[MAX_KEYS];
uint8_t base[MAX_KEYS]; uint32_t base[MAX_KEYS];
uint8_t shift[MAX_KEYS]; uint32_t shift[MAX_KEYS];
uint8_t altgr[MAX_KEYS] = {}; uint32_t altgr[MAX_KEYS] = {};
build_key(key, sizeof(key), "layouts.", id, ".scancodes"); build_key(key, sizeof(key), "layouts.", id, ".scancodes");
int count = read_bytes(doc, key, scancodes, MAX_KEYS); int count = read_bytes(doc, key, scancodes, MAX_KEYS);
@@ -99,11 +190,11 @@ inline bool load_layout(Layout* out, const char* id, const toml::Doc& doc) {
} }
build_key(key, sizeof(key), "layouts.", id, ".base"); build_key(key, sizeof(key), "layouts.", id, ".base");
if (read_bytes(doc, key, base, MAX_KEYS) != count) return false; if (read_codepoints(doc, key, base, MAX_KEYS) != count) return false;
build_key(key, sizeof(key), "layouts.", id, ".shift"); build_key(key, sizeof(key), "layouts.", id, ".shift");
if (read_bytes(doc, key, shift, MAX_KEYS) != count) return false; if (read_codepoints(doc, key, shift, MAX_KEYS) != count) return false;
build_key(key, sizeof(key), "layouts.", id, ".altgr"); build_key(key, sizeof(key), "layouts.", id, ".altgr");
int altgr_count = read_bytes(doc, key, altgr, MAX_KEYS); int altgr_count = read_codepoints(doc, key, altgr, MAX_KEYS);
if (altgr_count >= 0 && altgr_count != count) return false; if (altgr_count >= 0 && altgr_count != count) return false;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -174,6 +265,7 @@ inline Registry load_registry() {
// it reads one small file instead of re-parsing the whole layout table. // it reads one small file instead of re-parsing the whole layout table.
inline void refresh_selection(State* state, const char* username) { inline void refresh_selection(State* state, const char* username) {
if (!state) return; if (!state) return;
state->pending_dead = 0; // a dead key never carries over a layout switch
for (int i = 0; i < state->registry.count; i++) state->enabled[i] = false; for (int i = 0; i < state->registry.count; i++) state->enabled[i] = false;
toml::Doc doc = config::load_user(username, "keyboard"); toml::Doc doc = config::load_user(username, "keyboard");
@@ -237,40 +329,59 @@ inline int next_enabled(const State& state, int current) {
} }
// Caps Lock is derived rather than declared per key: it applies only where // Caps Lock is derived rather than declared per key: it applies only where
// base and shift are a Windows-1252 lower/upper letter pair. That covers the // base and shift are the lower/upper case of one letter. That covers accented
// accented letters (aa 0xE5 / AA 0xC5) without wrongly upper-casing keys such // letters in any script (aa/AA, e-caron/E-caron) without wrongly upper-casing
// as 2 / " where the shifted value is unrelated punctuation. // keys such as 2 / " where the shifted value is unrelated punctuation.
inline constexpr bool is_letter_pair(uint8_t base, uint8_t shift) { inline constexpr bool is_letter_pair(uint32_t base, uint32_t shift) {
if (base == 0 || shift == 0) return false; if (base == 0 || shift == 0) return false;
if (base < 0x61) return false; return utf8::is_case_pair(base, shift);
if (base > 0x7A && base < 0xE0) return false;
if (base == 0xF7) return false; // division sign sits inside the range
return shift == (uint8_t)(base - 0x20);
} }
static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA
static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE
static_assert(is_letter_pair(0xE6, 0xC6)); // ae / AE static_assert(is_letter_pair(0x11B, 0x11A)); // e-caron
static_assert(is_letter_pair('a', 'A')); static_assert(is_letter_pair('a', 'A'));
static_assert(!is_letter_pair('2', '"')); static_assert(!is_letter_pair('2', '"'));
static_assert(!is_letter_pair(0xF7, 0xD7)); // divide / multiply static_assert(!is_letter_pair(0xF7, 0xD7)); // divide / multiply
static_assert(!is_letter_pair(0x11B, '2')); // Czech number row
inline void translate(const State& state, abi::KeyEvent* key) { inline bool is_dead_key(const Layout& layout, uint32_t cp) {
if (cp == 0) return false;
for (int i = 0; i < layout.dead_count; i++)
if (layout.dead_keys[i] == cp) return true;
return false;
}
// Keys that may be pressed between a dead key and its letter without
// cancelling it (Shift for a capital, AltGr, Caps Lock, ...).
inline constexpr bool is_modifier_scancode(uint8_t sc) {
return sc == 0x2A || sc == 0x36 || sc == 0x1D || sc == 0x38 ||
sc == 0x3A || sc == 0x45 || sc == 0x46 || sc == 0x5B || sc == 0x5C;
}
// Apply the active layout to a raw key event: the character it types goes to
// key->codepoint, its Windows-1252 byte (or KEY_ASCII_SUBSTITUTE) to
// key->ascii. Dead keys type nothing and modify the following character, so
// the state is updated on presses.
inline void translate(State& state, abi::KeyEvent* key) {
if (!key) return; if (!key) return;
if (state.active < 0 || state.active >= state.registry.count) return; const Layout* layout = (state.active >= 0 && state.active < state.registry.count)
const Layout& layout = state.registry.items[state.active]; ? &state.registry.items[state.active] : nullptr;
uint8_t a = (uint8_t)key->ascii;
uint32_t cp = key->codepoint ? key->codepoint
: ((a >= 0x20 && a < 0x7F) ? a : 0);
uint8_t scancode = key->scancode & 0x7F;
// Extended keys carry a main-block scancode with the E0 prefix stripped // Extended keys carry a main-block scancode with the E0 prefix stripped
// (keypad "/" arrives as 0x35, the same as the main "/"), so translating // (keypad "/" arrives as 0x35, the same as the main "/"), so translating
// them would turn keypad "/" into whatever the layout puts on that key. // them would turn keypad "/" into whatever the layout puts on that key.
if (key->extended) return; if (layout && !key->extended) {
for (int i = 0; i < layout->key_count; i++) {
uint8_t scancode = key->scancode & 0x7F; const KeyMap& mapping = layout->keys[i];
for (int i = 0; i < layout.key_count; i++) {
const KeyMap& mapping = layout.keys[i];
if (mapping.scancode != scancode) continue; if (mapping.scancode != scancode) continue;
uint8_t out; uint32_t out;
if (key->altgr) { if (key->altgr) {
out = mapping.altgr; out = mapping.altgr;
} else { } else {
@@ -280,14 +391,55 @@ inline void translate(const State& state, abi::KeyEvent* key) {
out = upper ? mapping.shift : mapping.base; out = upper ? mapping.shift : mapping.base;
} }
if (out != 0) { if (out != 0) {
key->ascii = (char)out; cp = out;
// An AltGr key that produced a character is text, not a shortcut. // An AltGr key that produced a character is text, not a
// Apps gate insertion on !alt (and alt is LeftAlt||RightAlt), so // shortcut. Apps gate insertion on !alt (and alt is
// leaving it set would silently swallow every AltGr character. // LeftAlt||RightAlt), so leaving it set would silently swallow
// every AltGr character.
if (key->altgr) key->alt = false; if (key->altgr) key->alt = false;
} }
break;
}
}
bool dead = layout && !key->extended && is_dead_key(*layout, cp);
bool shortcut = key->ctrl || (key->alt && !key->altgr);
if (dead && shortcut) dead = false;
if (key->pressed && !shortcut) {
if (dead) {
if (state.pending_dead == cp) {
state.pending_dead = 0; // pressed twice: type the accent
dead = false;
} else {
state.pending_dead = cp; // wait for the next character
}
} else if (state.pending_dead) {
if (cp == ' ') {
cp = state.pending_dead; // accent + Space: the accent
state.pending_dead = 0;
} else if (cp) {
// The composed letter, or just the letter when the accent does
// not apply to it (one event can only carry one character).
uint32_t composed = compose(state.pending_dead, cp);
if (composed) cp = composed;
state.pending_dead = 0;
} else if (!is_modifier_scancode(scancode)) {
state.pending_dead = 0; // Enter, arrows, ... cancel it
}
}
}
if (dead) {
key->codepoint = 0;
key->ascii = 0;
return; return;
} }
key->codepoint = cp;
if (cp != 0) {
uint8_t b = utf8::to_cp1252(cp);
key->ascii = b ? (char)b : abi::KEY_ASCII_SUBSTITUTE;
}
} }
// ---- Direct keyboard readers ---- // ---- Direct keyboard readers ----
+61 -17
View File
@@ -27,9 +27,14 @@ typedef enum {
NoSuchPath = 11, NoSuchPath = 11,
Busy = 12, Busy = 12,
BufferTooSmall = 13, BufferTooSmall = 13,
OutOfMemory = 14 OutOfMemory = 14,
TimedOut = 15,
WouldBlock = 16
} StatusCode; } StatusCode;
// Highest defined StatusCode; update when adding a code above.
enum { MaxStatusCode = WouldBlock };
inline const char* GetStatusText(StatusCode statusCode) { inline const char* GetStatusText(StatusCode statusCode) {
const char* statusText[] = { const char* statusText[] = {
"Success", // 0 "Success", // 0
@@ -47,36 +52,48 @@ inline const char* GetStatusText(StatusCode statusCode) {
"Busy", // 12 "Busy", // 12
"Buffer too small", // 13 "Buffer too small", // 13
"Out of memory", // 14 "Out of memory", // 14
"Timed out", // 15
"Operation would block", // 16
}; };
if (statusCode > 0 && statusCode <= 14) { if (statusCode > 0 && statusCode <= MaxStatusCode) {
return statusText[statusCode]; return statusText[statusCode];
} else { } else {
return "Unknown error"; return "Unknown error";
} }
} }
// Map a raw syscall result to a StatusCode.
inline StatusCode StatusFromResult(long long rc) {
if (rc >= 0) return Success;
if (rc < -(long long)MaxStatusCode) return GenericFailure;
return (StatusCode)(-rc);
}
// Shared with C (libc.c), so no nullptr.
inline const char* GetStatusExplanationText(StatusCode statusCode) { inline const char* GetStatusExplanationText(StatusCode statusCode) {
const char* explanationText[] = { const char* explanationText[] = {
nullptr, // 0 0, // 0
nullptr, // 1 0, // 1
nullptr, // 2 0, // 2
nullptr, // 3 0, // 3
"The path is protected by the system and cannot be modified.", // 4 "The path is protected by the system and cannot be modified.", // 4
nullptr, // 5 0, // 5
nullptr, // 6 0, // 6
nullptr, // 7 0, // 7
nullptr, // 8 0, // 8
nullptr, // 9 0, // 9
nullptr, // 10 0, // 10
nullptr, // 11 0, // 11
nullptr, // 12 0, // 12
"The buffer supplied by the caller is too small.", // 13 "The buffer supplied by the caller is too small.", // 13
nullptr, // 14 0, // 14
0, // 15
0, // 16
}; };
if (statusCode > 0 && statusCode <= 14) { if (statusCode > 0 && statusCode <= MaxStatusCode) {
if (explanationText[statusCode] != nullptr) { if (explanationText[statusCode] != 0) {
return explanationText[statusCode]; return explanationText[statusCode];
} }
} }
@@ -84,6 +101,33 @@ inline const char* GetStatusExplanationText(StatusCode statusCode) {
return GetStatusText(statusCode); return GetStatusText(statusCode);
} }
// Process termination codes: what waitpid returns when the kernel ended the
// process (crash, kill, abort) instead of it calling exit. 256 + POSIX
// signal number, so normal exit codes (0..255) can never collide.
// Analogous to kernel/src/Common/TerminationCodes.hpp; same values.
typedef enum {
TerminationBase = 256,
TerminatedIllegalInstruction = TerminationBase + 4, // SIGILL
TerminatedBreakpoint = TerminationBase + 5, // SIGTRAP
TerminatedAborted = TerminationBase + 6, // SIGABRT
TerminatedArithmeticError = TerminationBase + 8, // SIGFPE
TerminatedKilled = TerminationBase + 9, // SIGKILL
TerminatedAccessViolation = TerminationBase + 11 // SIGSEGV
} TerminationCode;
// Shared with C (libc.c), so no nullptr. Returns 0 for an unknown code.
inline const char* GetTerminationText(int exitCode) {
switch (exitCode) {
case TerminatedIllegalInstruction: return "Illegal instruction";
case TerminatedBreakpoint: return "Breakpoint";
case TerminatedAborted: return "Aborted";
case TerminatedArithmeticError: return "Arithmetic error";
case TerminatedKilled: return "Killed";
case TerminatedAccessViolation: return "Access violation";
default: return 0;
}
}
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+25 -2
View File
@@ -7,6 +7,7 @@
#pragma once #pragma once
#include <Api/Syscall.hpp> #include <Api/Syscall.hpp>
#include <montauk/statuscodes.h> #include <montauk/statuscodes.h>
#include <montauk/utf8.h>
namespace montauk { namespace montauk {
@@ -222,7 +223,23 @@ namespace montauk {
// Keyboard // Keyboard
inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); } inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); }
inline void getkey(montauk::abi::KeyEvent* out) { syscall1(montauk::abi::SYS_GETKEY, (uint64_t)out); } // Scancode to use when checking for navigation keys (arrows, Home/End,
// PgUp/PgDn, Insert/Delete). The keypad shares those scancodes with its
// digit and '.' keys, so a keypad key that types a character reports 0
// here: numpad 4 is text, not Left. Compare against this, not .scancode.
inline uint8_t nav_scancode(const montauk::abi::KeyEvent& key) {
uint8_t sc = key.scancode;
if (!key.extended && (sc & 0x7F) >= 0x47 && (sc & 0x7F) <= 0x53 &&
montauk::utf8::key_codepoint(key) != 0)
return 0;
return sc;
}
// Fills the full event, including the Unicode codepoint the key types.
inline void getkey(montauk::abi::KeyEvent* out) {
syscall2(montauk::abi::SYS_GETKEY_EX, (uint64_t)out, sizeof(*out));
}
// Text keys arrive as UTF-8, one byte per call.
inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); } inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); }
// Returns 0 rather than blocking when no character is pending. // Returns 0 rather than blocking when no character is pending.
inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); } inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); }
@@ -496,7 +513,8 @@ namespace montauk {
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len); return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
} }
inline int childio_writekey(int childPid, const montauk::abi::KeyEvent* key) { inline int childio_writekey(int childPid, const montauk::abi::KeyEvent* key) {
return (int)syscall2(montauk::abi::SYS_CHILDIO_WRITEKEY, (uint64_t)childPid, (uint64_t)key); return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITEKEY_EX, (uint64_t)childPid,
(uint64_t)key, sizeof(*key));
} }
inline int childio_settermsz(int childPid, int cols, int rows) { inline int childio_settermsz(int childPid, int cols, int rows) {
return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows); return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
@@ -509,6 +527,11 @@ namespace montauk {
inline int kill(int pid) { inline int kill(int pid) {
return (int)syscall1(montauk::abi::SYS_KILL, (uint64_t)pid); return (int)syscall1(montauk::abi::SYS_KILL, (uint64_t)pid);
} }
// End the whole process from any thread (exit code TerminatedAborted).
[[noreturn]] inline void abort() {
syscall0(montauk::abi::SYS_ABORT);
__builtin_unreachable();
}
inline int setsession() { inline int setsession() {
return (int)syscall0(montauk::abi::SYS_SETSESSION); return (int)syscall0(montauk::abi::SYS_SETSESSION);
} }
+22
View File
@@ -148,6 +148,28 @@ namespace montauk {
return (int)syscall0(montauk::abi::SYS_THREAD_SELF); return (int)syscall0(montauk::abi::SYS_THREAD_SELF);
} }
// Block while *addr == expected. Returns 0 when woken by futex_wake,
// WouldBlock if *addr had already changed, TimedOut when timeout_ns
// elapsed (FUTEX_WAIT_FOREVER = no deadline, 0 = check only, otherwise
// rounded up to the 1 ms tick). Futexes are private to the process.
// A 0 return is never spurious, but always re-check the condition.
inline int futex_wait(uint32_t* addr, uint32_t expected,
uint64_t timeout_ns = montauk::abi::FUTEX_WAIT_FOREVER) {
int64_t r = syscall3(montauk::abi::SYS_FUTEX_WAIT, (uint64_t)addr,
(uint64_t)expected, timeout_ns);
return r < 0 ? (int)-r : 0;
}
// Wake up to `count` waiters on addr, longest-waiting first. Returns the
// number woken, or a negative status for a bad address.
inline int futex_wake(uint32_t* addr, uint32_t count = 1) {
return (int)syscall2(montauk::abi::SYS_FUTEX_WAKE, (uint64_t)addr, (uint64_t)count);
}
inline int futex_wake_all(uint32_t* addr) {
return futex_wake(addr, montauk::abi::FUTEX_WAKE_ALL);
}
// Lightweight mutex backed by a single atomic word + the kernel // Lightweight mutex backed by a single atomic word + the kernel
// yield syscall. Adequate for short critical sections; a heavier // yield syscall. Adequate for short critical sections; a heavier
// primitive can wrap mailbox_recv when blocking semantics are needed. // primitive can wrap mailbox_recv when blocking semantics are needed.
+23 -17
View File
@@ -9,6 +9,7 @@
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/heap.h> #include <montauk/heap.h>
#include <montauk/config.h> #include <montauk/config.h>
#include <montauk/utf8.h>
#include <bearssl_hash.h> #include <bearssl_hash.h>
namespace montauk { namespace montauk {
@@ -107,7 +108,7 @@ namespace user {
bytes_to_hex(hash, 32, out_hex64); bytes_to_hex(hash, 32, out_hex64);
} }
inline bool verify_password(const char* password, const char* salt_hex, const char* stored_hex64) { inline bool verify_password_bytes(const char* password, const char* salt_hex, const char* stored_hex64) {
char computed[65]; char computed[65];
hash_password(password, salt_hex, computed); hash_password(password, salt_hex, computed);
@@ -118,6 +119,26 @@ namespace user {
return diff == 0; return diff == 0;
} }
// Passwords are hashed as UTF-8. Before the switch to UTF-8, password
// fields stored typed characters as Windows-1252 bytes, so a non-ASCII
// password set back then only matches its 1252 form; try that too.
inline bool verify_password(const char* password, const char* salt_hex, const char* stored_hex64) {
if (verify_password_bytes(password, salt_hex, stored_hex64)) return true;
char legacy[256];
int o = 0;
bool non_ascii = false;
for (const char* p = password; *p;) {
uint32_t cp = montauk::utf8::next(&p);
uint8_t b = montauk::utf8::to_cp1252(cp);
if (b == 0 || o >= (int)sizeof(legacy) - 1) return false;
if (b >= 0x80) non_ascii = true;
legacy[o++] = (char)b;
}
legacy[o] = '\0';
return non_ascii && verify_password_bytes(legacy, salt_hex, stored_hex64);
}
// ---- User database I/O ---- // ---- User database I/O ----
inline int load_users(UserInfo* buf, int max) { inline int load_users(UserInfo* buf, int max) {
@@ -307,22 +328,7 @@ namespace user {
return false; return false;
} }
// ---- Session state and current user helpers ---- // ---- Current user helpers ----
// Persist login session state after successful login.
// Current processes should use getuser() for their active username.
inline void set_session(const char* username) {
montauk::toml::Doc doc;
doc.init();
config::set_string(&doc, "session.username", username);
config::save("session", &doc);
doc.destroy();
}
// Clear the session (on logout).
inline void clear_session() {
montauk::fdelete("0:/config/session.toml");
}
// Read the current process username into buf. Returns true if one is set. // Read the current process username into buf. Returns true if one is set.
inline bool get_session_username(char* buf, int bufSz) { inline bool get_session_username(char* buf, int bufSz) {
+399
View File
@@ -0,0 +1,399 @@
/*
* utf8.h
* UTF-8 text encoding helpers with a Windows-1252 fallback
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// MontaukOS text is UTF-8. Before this it was single-byte Windows-1252, so
// older files (and anything else that is not valid UTF-8) still turn up. The
// decoder never fails: a byte that does not start a well-formed sequence is
// read as ONE Windows-1252 character. Valid UTF-8 and plain ASCII are
// unaffected, and legacy text keeps rendering exactly as it used to.
//
// Character boundaries follow the same rule, so cursor movement, deletion and
// truncation all agree with what the renderer draws.
namespace montauk::utf8 {
inline constexpr uint32_t REPLACEMENT = 0xFFFD;
inline constexpr int MAX_BYTES = 4;
// Windows-1252 byte -> Unicode codepoint. 0x80-0x9F carry the typographic
// punctuation; every other byte is identical to Latin-1 / Unicode.
inline constexpr uint32_t from_cp1252(uint8_t b) {
switch (b) {
case 0x80: return 0x20AC; // Euro Sign
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
case 0x83: return 0x0192; // Latin Small Letter F With Hook
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
case 0x85: return 0x2026; // Horizontal Ellipsis
case 0x86: return 0x2020; // Dagger
case 0x87: return 0x2021; // Double Dagger
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
case 0x89: return 0x2030; // Per Mille Sign
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
case 0x8C: return 0x0152; // Latin Capital Ligature OE
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
case 0x91: return 0x2018; // Left Single Quotation Mark
case 0x92: return 0x2019; // Right Single Quotation Mark
case 0x93: return 0x201C; // Left Double Quotation Mark
case 0x94: return 0x201D; // Right Double Quotation Mark
case 0x95: return 0x2022; // Bullet
case 0x96: return 0x2013; // En Dash
case 0x97: return 0x2014; // Em Dash
case 0x98: return 0x02DC; // Small Tilde
case 0x99: return 0x2122; // Trade Mark Sign
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
case 0x9C: return 0x0153; // Latin Small Ligature OE
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
default: return b;
}
}
// Unicode codepoint -> Windows-1252 byte, or 0 if it has none. Used where a
// single byte is still required (PDF WinAnsi output, legacy byte tables).
inline constexpr uint8_t to_cp1252(uint32_t cp) {
if (cp < 0x80 || (cp >= 0xA0 && cp <= 0xFF)) return (uint8_t)cp;
for (int b = 0x80; b <= 0x9F; b++) {
uint32_t m = from_cp1252((uint8_t)b);
if (m != (uint32_t)b && m == cp) return (uint8_t)b;
}
return 0;
}
// Length of the well-formed UTF-8 sequence starting at s (at most `avail`
// bytes, stopping early at a NUL), or 0 if it is not one. Rejects overlong
// forms, surrogates and values above U+10FFFF.
inline int sequence_length(const char* s, int avail, uint32_t* out_cp = nullptr) {
if (avail <= 0) return 0;
const uint8_t* u = (const uint8_t*)s;
uint8_t b0 = u[0];
if (b0 < 0x80) {
if (out_cp) *out_cp = b0;
return 1;
}
int n;
uint32_t cp, min;
if (b0 >= 0xC2 && b0 <= 0xDF) { n = 2; cp = b0 & 0x1F; min = 0x80; }
else if (b0 >= 0xE0 && b0 <= 0xEF) { n = 3; cp = b0 & 0x0F; min = 0x800; }
else if (b0 >= 0xF0 && b0 <= 0xF4) { n = 4; cp = b0 & 0x07; min = 0x10000; }
else return 0;
if (n > avail) return 0;
for (int i = 1; i < n; i++) {
uint8_t c = u[i];
if ((c & 0xC0) != 0x80) return 0; // also catches NUL
cp = (cp << 6) | (c & 0x3F);
}
if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return 0;
if (out_cp) *out_cp = cp;
return n;
}
// Decode one character from a buffer of `avail` bytes. Returns the codepoint
// and stores the number of bytes consumed (always >= 1 when avail > 0).
inline uint32_t decode(const char* s, int avail, int* consumed) {
if (avail <= 0) { if (consumed) *consumed = 0; return 0; }
uint32_t cp;
int n = sequence_length(s, avail, &cp);
if (n == 0) {
n = 1;
cp = from_cp1252((uint8_t)s[0]);
}
if (consumed) *consumed = n;
return cp;
}
// Decode one character from a NUL-terminated string and advance *p past it.
// Returns 0 (without advancing) at the terminator.
inline uint32_t next(const char** p) {
const char* s = *p;
if (!*s) return 0;
int n;
// A sequence never spans the terminator: sequence_length stops at NUL
// because NUL is not a continuation byte.
uint32_t cp = decode(s, MAX_BYTES, &n);
*p = s + n;
return cp;
}
// Encode a codepoint as UTF-8 into out (room for 4 bytes, NOT terminated).
// Returns the byte count. Invalid codepoints encode U+FFFD.
inline int encode(uint32_t cp, char* out) {
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = REPLACEMENT;
uint8_t* o = (uint8_t*)out;
if (cp < 0x80) { o[0] = (uint8_t)cp; return 1; }
if (cp < 0x800) {
o[0] = (uint8_t)(0xC0 | (cp >> 6));
o[1] = (uint8_t)(0x80 | (cp & 0x3F));
return 2;
}
if (cp < 0x10000) {
o[0] = (uint8_t)(0xE0 | (cp >> 12));
o[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
o[2] = (uint8_t)(0x80 | (cp & 0x3F));
return 3;
}
o[0] = (uint8_t)(0xF0 | (cp >> 18));
o[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
o[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
o[3] = (uint8_t)(0x80 | (cp & 0x3F));
return 4;
}
inline int encoded_length(uint32_t cp) {
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return 3; // U+FFFD
return cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
}
// ==== Case mapping ====
// Simple one-to-one case mapping for the scripts MontaukOS keyboard layouts
// cover: ASCII, Latin-1, Latin Extended-A, Greek and Cyrillic. Anything else
// is returned unchanged.
inline constexpr uint32_t to_upper(uint32_t c) {
if (c >= 'a' && c <= 'z') return c - 0x20;
if (c < 0x80) return c;
if (c >= 0xE0 && c <= 0xFE && c != 0xF7) return c - 0x20;
if (c == 0xFF) return 0x178;
if (c == 0xB5) return 0x39C; // micro -> Mu
// Latin Extended-A: pairs alternate upper/lower, except the two ranges
// where the alignment shifts by one (L-stroke..N-apostrophe, Z-acute..).
if (c == 0x131) return 'I'; // dotless i
if (c >= 0x100 && c <= 0x137 && c != 0x130) return (c & 1) ? c - 1 : c;
if (c >= 0x139 && c <= 0x148) return (c & 1) ? c : c - 1;
if (c >= 0x14A && c <= 0x177) return (c & 1) ? c - 1 : c;
if (c >= 0x179 && c <= 0x17E) return (c & 1) ? c : c - 1;
if (c == 0x17F) return 'S'; // long s
// Greek
if (c >= 0x3B1 && c <= 0x3C9 && c != 0x3C2) return c - 0x20;
if (c == 0x3C2) return 0x3A3; // final sigma
if (c == 0x3AC) return 0x386;
if (c >= 0x3AD && c <= 0x3AF) return c - 0x25;
if (c == 0x3CC) return 0x38C;
if (c == 0x3CD || c == 0x3CE) return c - 0x3F;
// Cyrillic
if (c >= 0x430 && c <= 0x44F) return c - 0x20;
if (c >= 0x450 && c <= 0x45F) return c - 0x50;
return c;
}
inline constexpr uint32_t to_lower(uint32_t c) {
if (c >= 'A' && c <= 'Z') return c + 0x20;
if (c < 0x80) return c;
if (c >= 0xC0 && c <= 0xDE && c != 0xD7) return c + 0x20;
if (c == 0x178) return 0xFF;
if (c == 0x130) return 'i'; // dotted I
if (c >= 0x100 && c <= 0x137) return (c & 1) ? c : c + 1;
if (c >= 0x139 && c <= 0x148) return (c & 1) ? c + 1 : c;
if (c >= 0x14A && c <= 0x177) return (c & 1) ? c : c + 1;
if (c >= 0x179 && c <= 0x17E) return (c & 1) ? c + 1 : c;
if (c >= 0x391 && c <= 0x3A9 && c != 0x3A2) return c + 0x20;
if (c == 0x386) return 0x3AC;
if (c >= 0x388 && c <= 0x38A) return c + 0x25;
if (c == 0x38C) return 0x3CC;
if (c == 0x38E || c == 0x38F) return c + 0x3F;
if (c >= 0x410 && c <= 0x42F) return c + 0x20;
if (c >= 0x400 && c <= 0x40F) return c + 0x50;
return c;
}
// True if lower/upper are the two cases of one letter.
inline constexpr bool is_case_pair(uint32_t lower, uint32_t upper) {
return lower != upper && to_upper(lower) == upper && to_lower(upper) == lower;
}
// Text a key event types, as a codepoint (0 for control/non-text keys).
// KeyEvent::codepoint is filled by the kernel and by layout translation;
// events built by hand may leave it 0, so fall back to `ascii` (a
// Windows-1252 byte).
template <typename Key>
inline uint32_t key_codepoint(const Key& key) {
if (key.codepoint) {
uint32_t cp = key.codepoint;
return (cp < 0x20 || cp == 0x7F || (cp >= 0x80 && cp < 0xA0)) ? 0 : cp;
}
uint8_t b = (uint8_t)key.ascii;
if (b < 0x20 || b == 0x7F) return 0;
return from_cp1252(b);
}
// Same, encoded as UTF-8 into out (4 bytes). Returns the length, 0 if none.
template <typename Key>
inline int key_utf8(const Key& key, char* out) {
uint32_t cp = key_codepoint(key);
return cp ? encode(cp, out) : 0;
}
// Byte offset of the character after the one at `pos` (len = buffer length).
inline int next_boundary(const char* s, int len, int pos) {
if (pos >= len) return len;
if (pos < 0) return 0;
int n;
decode(s + pos, len - pos, &n);
return pos + n;
}
// Byte offset of the character before `pos`. A run of continuation bytes is
// one character only if it completes a well-formed sequence ending exactly at
// pos; otherwise the previous byte stands alone (Windows-1252 fallback).
inline int prev_boundary(const char* s, int pos) {
if (pos <= 0) return 0;
for (int back = 2; back <= MAX_BYTES && back <= pos; back++) {
if (((uint8_t)s[pos - back + 1] & 0xC0) != 0x80) break;
int start = pos - back;
if (sequence_length(s + start, back) == back) return start;
}
return pos - 1;
}
// Snap `pos` back onto a character boundary. Use before truncating a buffer
// or splitting at an arbitrary byte offset.
inline int floor_boundary(const char* s, int len, int pos) {
if (pos <= 0) return 0;
if (pos >= len) return len;
if (((uint8_t)s[pos] & 0xC0) != 0x80) return pos;
// pos is a continuation byte: it is inside a character only if a
// well-formed sequence starting up to 3 bytes earlier covers it.
for (int back = 1; back < MAX_BYTES && back <= pos; back++) {
int start = pos - back;
int n = sequence_length(s + start, len - start);
if (n > back) return start;
if (((uint8_t)s[start] & 0xC0) != 0x80) break;
}
return pos;
}
// Number of characters in a NUL-terminated string.
inline int count(const char* s) {
int n = 0;
while (*s) { next(&s); n++; }
return n;
}
// Number of characters in the first `len` bytes.
inline int count(const char* s, int len) {
int n = 0, pos = 0;
while (pos < len) { pos = next_boundary(s, len, pos); n++; }
return n;
}
// Byte offset of the `index`th character (clamped to len).
inline int offset_of(const char* s, int len, int index) {
int pos = 0;
while (index-- > 0 && pos < len) pos = next_boundary(s, len, pos);
return pos;
}
// True if the buffer is entirely well-formed UTF-8.
inline bool valid(const char* s, int len) {
for (int pos = 0; pos < len;) {
int n = sequence_length(s + pos, len - pos);
if (n == 0) return false;
pos += n;
}
return true;
}
// Rewrite a buffer as well-formed UTF-8 (each stray byte becomes the
// character the Windows-1252 fallback would display). Returns the output
// length, truncated on a character boundary to fit cap - 1, NUL-terminated.
inline int normalize(const char* in, int len, char* out, int cap) {
if (cap <= 0) return 0;
int o = 0;
for (int pos = 0; pos < len;) {
int n;
uint32_t cp = decode(in + pos, len - pos, &n);
int need = encoded_length(cp);
if (o + need > cap - 1) break;
o += encode(cp, out + o);
pos += n;
}
out[o] = '\0';
return o;
}
// Copy src into dst (cap bytes incl. NUL) without splitting a character.
inline int copy(char* dst, int cap, const char* src) {
if (cap <= 0) return 0;
int len = 0;
while (src[len]) len++;
int n = len < cap - 1 ? len : floor_boundary(src, len, cap - 1);
for (int i = 0; i < n; i++) dst[i] = src[i];
dst[n] = '\0';
return n;
}
// ==== Editing a NUL-terminated buffer with a byte cursor ====
// `len` is the current length, `cap` the buffer size including the NUL. The
// cursor is a byte offset that is always kept on a character boundary.
// Insert n bytes at *cursor. Returns false (buffer unchanged) if they don't fit.
inline bool insert(char* buf, int* len, int cap, int* cursor,
const char* s, int n) {
if (n <= 0 || *len + n >= cap) return false;
int at = *cursor < 0 ? 0 : (*cursor > *len ? *len : *cursor);
for (int i = *len; i >= at; i--) buf[i + n] = buf[i]; // moves the NUL
for (int i = 0; i < n; i++) buf[at + i] = s[i];
*len += n;
*cursor = at + n;
return true;
}
// Insert the text a key event types. Returns false if it types nothing or
// does not fit.
template <typename Key>
inline bool insert_key(char* buf, int* len, int cap, int* cursor, const Key& key) {
char t[MAX_BYTES];
int n = key_utf8(key, t);
return n > 0 && insert(buf, len, cap, cursor, t, n);
}
// Remove the bytes [start, end).
inline void erase(char* buf, int* len, int start, int end) {
if (start < 0) start = 0;
if (end > *len) end = *len;
if (start >= end) return;
int n = end - start;
for (int i = start; i + n <= *len; i++) buf[i] = buf[i + n]; // moves the NUL
*len -= n;
}
// Backspace: delete the character before the cursor.
inline bool erase_before(char* buf, int* len, int* cursor) {
if (*cursor <= 0) return false;
int start = prev_boundary(buf, *cursor);
erase(buf, len, start, *cursor);
*cursor = start;
return true;
}
// Delete: remove the character at the cursor.
inline bool erase_at(char* buf, int* len, int cursor) {
if (cursor >= *len) return false;
erase(buf, len, cursor, next_boundary(buf, *len, cursor));
return true;
}
// Append typed text at the end (simple line inputs without a cursor).
template <typename Key>
inline bool append_key(char* buf, int* len, int cap, const Key& key) {
int cursor = *len;
return insert_key(buf, len, cap, &cursor, key);
}
// Backspace for append-only inputs: drop the last character.
inline bool erase_last(char* buf, int* len) {
int cursor = *len;
return erase_before(buf, len, &cursor);
}
} // namespace montauk::utf8
+1 -1
View File
@@ -17,7 +17,7 @@
* a raw PSK). Anyone who can read 0:/config can read the keys. * a raw PSK). Anyone who can read 0:/config can read the keys.
* *
* No default copy of this file ships in the image, deliberately. Every other * No default copy of this file ships in the image, deliberately. Every other
* config an app writes (bluetooth.toml, display.toml, session.toml) is * config an app writes (bluetooth.toml, display.toml) is
* created on demand for the same reason: a shipped copy is laid down again * created on demand for the same reason: a shipped copy is laid down again
* by anything that refreshes the system files, and it would overwrite the * by anything that refreshes the system files, and it would overwrite the
* networks the user had saved. * networks the user had saved.
+33 -12
View File
@@ -130,6 +130,8 @@ static inline long _mtk_syscall4(long nr, long a1, long a2, long a3, long a4) {
#define SYS_GETENVIRON 171 #define SYS_GETENVIRON 171
#define SYS_SETENVIRON 172 #define SYS_SETENVIRON 172
#define SYS_SPAWN_ENV 173 #define SYS_SPAWN_ENV 173
#define SYS_ABORT 198
#define SYS_EXIT_GROUP 199
/* ======================================================================== /* ========================================================================
errno errno
@@ -1239,13 +1241,14 @@ int atexit(void (*func)(void)) {
void exit(int status) { void exit(int status) {
for (int i = _atexit_count - 1; i >= 0; i--) for (int i = _atexit_count - 1; i >= 0; i--)
_atexit_funcs[i](); _atexit_funcs[i]();
_mtk_syscall1(SYS_EXIT, (long)status); /* SYS_EXIT from a sibling thread would end only that thread. */
_mtk_syscall1(SYS_EXIT_GROUP, (long)status);
__builtin_unreachable(); __builtin_unreachable();
} }
/* Ends the whole process from any thread; waitpid sees WTERMSIG == SIGABRT. */
void abort(void) { void abort(void) {
_mtk_syscall1(SYS_PRINT, (long)"abort() called\n"); _mtk_syscall0(SYS_ABORT);
_mtk_syscall1(SYS_EXIT, 1);
__builtin_unreachable(); __builtin_unreachable();
} }
@@ -1712,7 +1715,10 @@ static int _stdin_pump(int block) {
for (;;) { for (;;) {
int c = block ? (int)_mtk_syscall0(SYS_GETCHAR) int c = block ? (int)_mtk_syscall0(SYS_GETCHAR)
: (int)_mtk_syscall0(SYS_GETCHAR_NB); : (int)_mtk_syscall0(SYS_GETCHAR_NB);
if (c <= 0) { /* The syscall returns a sign-extended char: bytes >= 0x80 arrive
negative and are real keys, only 0 means "nothing". */
if (c < 0) c &= 0xFF;
if (c == 0) {
if (block) continue; if (block) continue;
return 0; return 0;
} }
@@ -1721,12 +1727,26 @@ static int _stdin_pump(int block) {
if (c == '\b' || c == 127) { if (c == '\b' || c == 127) {
if (_stdin_line_len > 0) { if (_stdin_line_len > 0) {
/* Drop the whole UTF-8 character: its continuation bytes,
then the lead byte. One terminal cell either way. */
while (_stdin_line_len > 1 &&
((unsigned char)_stdin_linebuf[_stdin_line_len - 1] & 0xC0) == 0x80)
_stdin_line_len--;
_stdin_line_len--; _stdin_line_len--;
_stdin_echo_char('\b'); _stdin_echo_char('\b');
} }
continue; continue;
} }
/* Bytes >= 0x80 are UTF-8: the kernel encodes typed characters, and
piped input is UTF-8 already. They are stored and echoed as-is. */
if (c >= 0x80) {
if (_stdin_line_len + 2 >= sizeof(_stdin_linebuf)) continue;
_stdin_linebuf[_stdin_line_len++] = (char)c;
_stdin_echo_char((char)c);
continue;
}
if (c == '\n') { if (c == '\n') {
_stdin_linebuf[_stdin_line_len++] = (char)c; _stdin_linebuf[_stdin_line_len++] = (char)c;
_stdin_echo_char((char)c); _stdin_echo_char((char)c);
@@ -2477,7 +2497,7 @@ char *getcwd(char *buf, size_t size) {
} }
int result = (int)_mtk_syscall2(SYS_GETCWD, (long)buf, (long)size); int result = (int)_mtk_syscall2(SYS_GETCWD, (long)buf, (long)size);
if (result < 0) { if (result < 0) {
errno = result == BufferTooSmall ? ERANGE : EIO; errno = result == -BufferTooSmall ? ERANGE : EIO;
return NULL; return NULL;
} }
return buf; return buf;
@@ -3567,10 +3587,11 @@ int utime(const char *path, const struct utimbuf *times) {
times != NULL ? (long)times->modtime : 0, times != NULL ? (long)times->modtime : 0,
times == NULL); times == NULL);
if (result == 0) return 0; if (result == 0) return 0;
if (result == UnsupportedOp) errno = ENOTSUP; /* The kernel returns StatusCodes negated. */
else if (result == InvalidArgument) errno = EOVERFLOW; if (result == -UnsupportedOp) errno = ENOTSUP;
else if (result == IoFailure) errno = EIO; else if (result == -InvalidArgument) errno = EOVERFLOW;
else if (result == SystemIntegrityError) errno = EPERM; else if (result == -IoFailure) errno = EIO;
else if (result == -SystemIntegrityError) errno = EPERM;
else errno = ENOENT; else errno = ENOENT;
return -1; return -1;
} }
@@ -4037,8 +4058,8 @@ pid_t waitpid(pid_t pid, int *status, int options) {
} }
long code = _mtk_syscall1(SYS_WAITPID, (long)pid); long code = _mtk_syscall1(SYS_WAITPID, (long)pid);
if (status != NULL) { if (status != NULL) {
if (code >= 256) { if (code >= TerminationBase) {
*status = (int)((code - 256) & 0x7F); /* signaled */ *status = (int)((code - TerminationBase) & 0x7F); /* signaled */
} else { } else {
*status = (int)((code & 0xFF) << 8); /* exited */ *status = (int)((code & 0xFF) << 8); /* exited */
} }
@@ -4047,7 +4068,7 @@ pid_t waitpid(pid_t pid, int *status, int options) {
} }
void _exit(int status) { void _exit(int status) {
_mtk_syscall1(SYS_EXIT, (long)status); _mtk_syscall1(SYS_EXIT_GROUP, (long)status);
__builtin_unreachable(); __builtin_unreachable();
} }
+9
View File
@@ -0,0 +1,9 @@
# Paths below are relative to programs/rust/ (the parent of .cargo/).
[build]
target = "x86_64-unknown-montauk"
[target.x86_64-unknown-montauk]
# The montauk GCC driver supplies crt1/crti/crtbegin, -lc and libgcc, and
# links the static non-PIE ET_EXEC layout the kernel ELF loader expects.
linker = "../../toolchain/local/bin/x86_64-montauk-gcc"
+1
View File
@@ -0,0 +1 @@
target/
+18
View File
@@ -0,0 +1,18 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "montauk-rt"
version = "0.1.0"
[[package]]
name = "rusthello"
version = "0.1.0"
dependencies = [
"montauk-rt",
]
[[package]]
name = "ruststd"
version = "0.1.0"
+13
View File
@@ -0,0 +1,13 @@
[workspace]
resolver = "2"
members = ["montauk-rt", "rusthello", "ruststd"]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s"
lto = true
codegen-units = 1
debug = false
+105
View File
@@ -0,0 +1,105 @@
# Rust on MontaukOS
Rust programs are built for `x86_64-unknown-montauk`, a built-in target of a
forked rustc with a native `std` (`target_os = "montauk"`, not a Unix). Both
`std` programs and `no_std` programs (on `montauk-rt`) are supported.
## Setup (once)
```bash
# rustup, plus a nightly whose cargo the linked toolchain borrows
curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none
~/.cargo/bin/rustup toolchain install nightly
# The fork: rust-lang/rust, branch `montauk` (bootstrap.toml is set up)
cd ~/dev/mtk/rust
./x build library --stage 1 --target x86_64-unknown-linux-gnu,x86_64-unknown-montauk
rustup toolchain link montauk ~/dev/mtk/rust/build/host/stage1
```
The stage1 build takes about five minutes (it downloads CI LLVM). After
changing only `library/`, rebuild with
`./x build library --stage 1 --target x86_64-unknown-montauk` (seconds); the
link stays valid.
`rust-toolchain.toml` selects the `montauk` toolchain for this directory. It
has no cargo of its own; rustup falls back to the nightly's cargo.
`make programs` builds the Rust programs when that toolchain is linked, and
prints a notice and skips them otherwise. It never downloads a toolchain
(`RUSTUP_AUTO_INSTALL=0`).
## Building
```bash
make -C programs rust # cargo build --release, copies to bin/os/*.elf
cd programs/rust && cargo build --release
```
Output: `target/x86_64-unknown-montauk/release/<name>`, installed as
`programs/bin/os/<name>.elf`. Add new binaries to `RUST_PROGRAMS` in
`programs/GNUmakefile` and to the workspace `members`.
The GCC driver links crt1/crti/crtbegin, the libc and libgcc from
`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`,
re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up.
`make rust` relinks the programs when the sysroot libc or the toolchain's std
is newer than the last build (cargo tracks neither).
## Layout
| Path | Purpose |
|------|---------|
| `.cargo/config.toml` | target = `x86_64-unknown-montauk`, linker = `x86_64-montauk-gcc` |
| `rust-toolchain.toml` | the linked `montauk` toolchain |
| `ruststd/` | `std` smoke test (`ruststd`, `ruststd net <host> [port]`) |
| `montauk-rt/` | `no_std` runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler |
| `rusthello/` | `no_std` smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`, `rusthello fault-thread`) |
## The std port (in the fork)
| Fork path | What |
|-----------|------|
| `compiler/rustc_target/src/spec/base/montauk.rs`, `targets/x86_64_unknown_montauk.rs` | Target: static non-PIE ET_EXEC, no red zone, local-exec TLS, panic=abort, linker `x86_64-montauk-gcc` |
| `library/std/src/sys/pal/montauk/` | Platform layer: `abi.rs` (syscall numbers, status codes, `SYSCALL` wrappers) |
| `library/std/src/sys/*/montauk.rs` | alloc, env, fs, io errors, net, paths, process, random, stdio, futex, thread, time |
| `library/std/src/os/montauk/` | `std::os::montauk::ffi::{OsStrExt, OsStringExt}` |
std calls the kernel directly. The libc is used only where C code in the same
process must agree: the heap (`malloc`), the environment table (`setenv`),
`exit` (atexit handlers) and line-buffered terminal input (`read(0)`). The
syscall numbers in `abi.rs` are a hand-kept subset of `Api/Syscall.hpp`.
## Platform behaviour
- Errors: the raw OS error of an `io::Error` is the kernel status code
(positive, `montauk/statuscodes.h`); `ErrorKind` is mapped from it.
- Paths: `0:/os/x.elf` is absolute; joining onto it replaces the base. Search
paths (`PATH`, `env::split_paths`) are separated by `;`, not `:`.
- Files: kernel handles have no position, so `File` keeps its own cursor.
`OpenOptions::truncate` deletes and recreates the file; `set_len` can only
grow a file. No links, permissions are read-only, `fsync` is a no-op.
- Threads: stacks are mapped with `SYS_MMAP_ANON` (no guard page). A detached
thread is reaped (joined, stack freed) by the next spawn or join, so its
kernel slot does not leak. `available_parallelism()` is an error (the kernel
does not report a CPU count). `std::process::exit` from any thread ends the
process (libc `exit` -> `SYS_EXIT_GROUP`).
- Sync: `Mutex`, `RwLock`, `Condvar`, `Once` and thread parking use the
generic futex implementations over `SYS_FUTEX_WAIT`/`SYS_FUTEX_WAKE`.
- `thread_local!` uses native `#[thread_local]` (PT_TLS); destructors run when
a spawned thread exits (not on the main thread).
- Time: `Instant` has 1 ms resolution; `SystemTime` comes from the wall clock
(local time minus the timezone offset) with 1 s resolution.
- Processes: `Command` supports arguments, environment, `current_dir` (entered
in the parent for the duration of the spawn), `wait`, `try_wait` and `kill`.
Only inherited stdio is supported; piped/null stdio and `output()` return
`Unsupported`, because a redirected child's output stream disappears with
the child. A bare program name is searched in `PATH`, else `0:/os` and
`0:/games`; `.elf` may be omitted. `ExitStatus` shows abnormal ends as
"terminated by signal N".
- Net: IPv4 only. TCP connect/accept/read/write with read timeouts and
non-blocking mode, UDP send_to/recv_from/connect, DNS via `SYS_RESOLVE`.
No socket options, no `peek`, no `shutdown(Write)`, and accepted
connections do not know the peer's address.
- `panic = "abort"`: a panic on any thread prints the message and kills the
whole process (exit status "signal 6").
- `HashMap`'s random keys come from `SYS_GETRANDOM`, which is not a CSPRNG.
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "montauk-rt"
version = "0.1.0"
edition = "2024"
description = "Minimal no_std runtime for Rust programs on MontaukOS"
build = "build.rs"
[lib]
path = "src/lib.rs"
+65
View File
@@ -0,0 +1,65 @@
// Generate syscall numbers and status codes from the C++ headers so Rust
// never becomes another hand-maintained copy of the syscall table.
//
// programs/include/Api/Syscall.hpp -> pub const SYS_*: u64
// kernel/src/Common/StatusCodes.hpp -> pub enum Status (positive values,
// matching montauk/statuscodes.h)
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let repo = manifest.join("../../..");
let syscall_hpp = repo.join("programs/include/Api/Syscall.hpp");
let status_hpp = repo.join("kernel/src/Common/StatusCodes.hpp");
println!("cargo:rerun-if-changed={}", syscall_hpp.display());
println!("cargo:rerun-if-changed={}", status_hpp.display());
let mut out = String::from("// @generated by montauk-rt/build.rs - do not edit.\n\n");
// static constexpr uint64_t SYS_NAME = 123; (decimal literals only)
let syscalls = fs::read_to_string(&syscall_hpp).expect("read Syscall.hpp");
let mut count = 0;
for line in syscalls.lines() {
let line = line.trim();
let Some(rest) = line.strip_prefix("static constexpr uint64_t SYS_") else { continue };
let Some((name, value)) = rest.split_once('=') else { continue };
let value = value.split(';').next().unwrap_or("").trim();
if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
continue;
}
out.push_str(&format!("pub const SYS_{}: u64 = {};\n", name.trim(), value));
count += 1;
}
assert!(count > 100, "parsed only {count} syscalls from {}", syscall_hpp.display());
// enum StatusCodes { Success = 0, GenericFailure = -1, ... };
let status = fs::read_to_string(&status_hpp).expect("read StatusCodes.hpp");
let body = status
.split_once("enum StatusCodes")
.and_then(|(_, rest)| rest.split_once('{'))
.and_then(|(_, rest)| rest.split_once('}'))
.map(|(body, _)| body)
.expect("enum StatusCodes not found");
out.push_str("\n/// Kernel status codes, stored positive like montauk/statuscodes.h.\n");
out.push_str("/// The kernel returns them negated; see [`Status::from_result`].\n");
out.push_str("#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Status {\n");
let mut variants = Vec::new();
for entry in body.split(',') {
let entry = entry.split("//").next().unwrap_or("").trim();
let Some((name, value)) = entry.split_once('=') else { continue };
let value: i32 = value.trim().parse().expect("status value");
out.push_str(&format!(" {} = {},\n", name.trim(), -value));
variants.push((name.trim().to_string(), -value));
}
out.push_str("}\n\nimpl Status {\n pub const fn from_code(code: i32) -> Status {\n match code {\n");
for (name, value) in &variants {
out.push_str(&format!(" {} => Status::{},\n", value, name));
}
out.push_str(" _ => Status::GenericFailure,\n }\n }\n}\n");
let dest = PathBuf::from(env::var("OUT_DIR").unwrap()).join("abi.rs");
fs::write(dest, out).expect("write abi.rs");
}
+48
View File
@@ -0,0 +1,48 @@
//! Command-line arguments, captured from crt1.o's argv by [`crate::entry!`].
use core::ffi::CStr;
use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
static ARGC: AtomicUsize = AtomicUsize::new(0);
static ARGV: AtomicPtr<*const u8> = AtomicPtr::new(core::ptr::null_mut());
/// # Safety
/// `argv` must point to `argc` NUL-terminated strings that live forever.
#[doc(hidden)]
pub unsafe fn init(argc: i32, argv: *const *const u8) {
ARGV.store(argv as *mut *const u8, Ordering::Relaxed);
ARGC.store(argc.max(0) as usize, Ordering::Release);
}
/// Iterator over the program arguments; argv[0] is the executable path.
/// Arguments that are not valid UTF-8 are yielded lossily as `"?"`.
pub fn args() -> Args {
Args { next: 0 }
}
pub struct Args {
next: usize,
}
impl Iterator for Args {
type Item = &'static str;
fn next(&mut self) -> Option<&'static str> {
let argc = ARGC.load(Ordering::Acquire);
if self.next >= argc {
return None;
}
let argv = ARGV.load(Ordering::Relaxed);
// SAFETY: init() guarantees argc valid, immortal C strings.
let s = unsafe { CStr::from_ptr(*argv.add(self.next) as *const core::ffi::c_char) };
self.next += 1;
Some(s.to_str().unwrap_or("?"))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = ARGC.load(Ordering::Acquire).saturating_sub(self.next);
(left, Some(left))
}
}
impl ExactSizeIterator for Args {}
+66
View File
@@ -0,0 +1,66 @@
//! Global allocator over the Montauk libc heap (process-wide locked, so
//! safe across threads). libc malloc returns 16-byte aligned blocks; larger
//! alignments over-allocate and stash the original pointer just below the
//! aligned block.
use core::alloc::{GlobalAlloc, Layout};
use core::ptr;
unsafe extern "C" {
fn malloc(size: usize) -> *mut u8;
fn free(ptr: *mut u8);
fn realloc(ptr: *mut u8, size: usize) -> *mut u8;
}
const MALLOC_ALIGN: usize = 16;
pub struct LibcAllocator;
unsafe impl GlobalAlloc for LibcAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe {
if layout.align() <= MALLOC_ALIGN {
return malloc(layout.size().max(1));
}
let Some(total) = layout.size().checked_add(layout.align() + size_of::<usize>()) else {
return ptr::null_mut();
};
let raw = malloc(total);
if raw.is_null() {
return raw;
}
let start = raw as usize + size_of::<usize>();
let aligned = (start + layout.align() - 1) & !(layout.align() - 1);
*((aligned - size_of::<usize>()) as *mut usize) = raw as usize;
aligned as *mut u8
}
}
unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
unsafe {
if layout.align() <= MALLOC_ALIGN {
free(p);
} else {
free(*((p as usize - size_of::<usize>()) as *const usize) as *mut u8);
}
}
}
unsafe fn realloc(&self, p: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
unsafe {
if layout.align() <= MALLOC_ALIGN {
return realloc(p, new_size.max(1));
}
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
let q = self.alloc(new_layout);
if !q.is_null() {
ptr::copy_nonoverlapping(p, q, layout.size().min(new_size));
self.dealloc(p, layout);
}
q
}
}
}
#[global_allocator]
static ALLOCATOR: LibcAllocator = LibcAllocator;
+82
View File
@@ -0,0 +1,82 @@
//! Terminal output. `print!`/`println!` format into a stack buffer and hand
//! it to SYS_PRINT in NUL-terminated chunks, so one call is one (or a few)
//! syscalls rather than libc write()'s one SYS_PUTCHAR per byte.
use core::fmt::{self, Write};
use crate::sys;
const CHUNK: usize = 256;
/// Buffered writer to the process's terminal (stdout).
pub struct Stdout {
buf: [u8; CHUNK],
len: usize,
}
impl Stdout {
pub const fn new() -> Self {
Stdout { buf: [0; CHUNK], len: 0 }
}
pub fn write_bytes(&mut self, bytes: &[u8]) {
for &b in bytes {
if b == 0 {
// SYS_PRINT stops at NUL; send it on its own.
self.flush();
unsafe { sys::syscall1(sys::SYS_PUTCHAR, 0) };
continue;
}
if self.len == CHUNK - 1 {
self.flush();
}
self.buf[self.len] = b;
self.len += 1;
}
}
pub fn flush(&mut self) {
if self.len == 0 {
return;
}
self.buf[self.len] = 0;
unsafe { sys::syscall1(sys::SYS_PRINT, self.buf.as_ptr() as u64) };
self.len = 0;
}
}
impl Default for Stdout {
fn default() -> Self {
Self::new()
}
}
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_bytes(s.as_bytes());
Ok(())
}
}
impl Drop for Stdout {
fn drop(&mut self) {
self.flush();
}
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
let mut out = Stdout::new();
let _ = out.write_fmt(args);
}
#[macro_export]
macro_rules! print {
($($arg:tt)*) => { $crate::io::_print(format_args!($($arg)*)) };
}
#[macro_export]
macro_rules! println {
() => { $crate::print!("\n") };
($($arg:tt)*) => { $crate::io::_print(format_args!("{}\n", format_args!($($arg)*))) };
}
+49
View File
@@ -0,0 +1,49 @@
//! Minimal `no_std` runtime for Rust programs on MontaukOS.
//!
//! For programs that use `core` + `alloc` without `std` (most programs can use
//! `std` directly; see ../README.md). Programs link against the Montauk libc
//! through the x86_64-montauk GCC driver, so crt1.o owns `_start` (argv,
//! environment, .init_array) and calls the `main` that [`entry!`] defines.
//!
//! ```ignore
//! #![no_std]
//! #![no_main]
//! montauk_rt::entry!(main);
//! fn main() -> i32 { montauk_rt::println!("hi"); 0 }
//! ```
#![no_std]
extern crate alloc;
pub mod sys;
pub mod io;
pub mod heap;
pub mod env;
pub mod process;
pub mod sync;
pub mod thread;
pub mod time;
mod panic;
pub use sys::Status;
/// Define the C `main` that crt1.o calls, forwarding to `fn() -> i32`.
#[macro_export]
macro_rules! entry {
($main:path) => {
const _: () = {
// Exported as the C symbol `main`; the Rust name stays free so
// the program can call its own entry function `main`.
#[unsafe(export_name = "main")]
extern "C" fn __montauk_rt_main(argc: i32, argv: *const *const u8) -> i32 {
// SAFETY: crt1.o passes its static argv table, which lives
// for the whole program and is never modified after startup.
unsafe { $crate::env::init(argc, argv) };
let f: fn() -> i32 = $main;
f()
}
};
};
}
+14
View File
@@ -0,0 +1,14 @@
use core::fmt::Write;
use core::panic::PanicInfo;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
let mut out = crate::io::Stdout::new();
let _ = write!(out, "\nthread {} panicked", crate::thread::current_id());
if let Some(loc) = info.location() {
let _ = write!(out, " at {}:{}:{}", loc.file(), loc.line(), loc.column());
}
let _ = writeln!(out, ":\n{}", info.message());
out.flush();
crate::process::abort()
}
+31
View File
@@ -0,0 +1,31 @@
//! Process control.
use crate::sys;
unsafe extern "C" {
// libc exit(): runs atexit handlers (and .fini_array), then SYS_EXIT.
#[link_name = "exit"]
fn libc_exit(code: i32) -> !;
}
/// Exit the process. From a sibling thread this ends only that thread
/// (kernel semantics); use [`abort`] to take the whole process down.
pub fn exit(code: i32) -> ! {
unsafe { libc_exit(code) }
}
pub fn id() -> u32 {
unsafe { sys::syscall0(sys::SYS_GETPID) as u32 }
}
/// Terminate the whole process from any thread. Waiters see exit code
/// TerminatedAborted (256 + SIGABRT).
pub fn abort() -> ! {
unsafe {
sys::syscall0(sys::SYS_ABORT);
// Not reached. No panic here: the panic handler calls abort().
loop {
sys::syscall0(sys::SYS_YIELD);
}
}
}
+125
View File
@@ -0,0 +1,125 @@
//! Futex-backed synchronization (SYS_FUTEX_WAIT / SYS_FUTEX_WAKE).
use core::cell::UnsafeCell;
use core::ops::{Deref, DerefMut};
use core::sync::atomic::{AtomicU32, Ordering};
use core::time::Duration;
use crate::sys::{self, Status};
/// Block while `*word == expected`. `Ok(())` means woken by [`futex_wake`];
/// `Err(WouldBlock)` means the word had already changed; `Err(TimedOut)`
/// means the deadline passed. Always re-check your condition afterwards.
pub fn futex_wait(word: &AtomicU32, expected: u32, timeout: Option<Duration>) -> Result<(), Status> {
let ns = match timeout {
None => sys::FUTEX_WAIT_FOREVER,
Some(d) => d.as_nanos().min((sys::FUTEX_WAIT_FOREVER - 1) as u128) as u64,
};
let rc = unsafe {
sys::syscall3(sys::SYS_FUTEX_WAIT, word.as_ptr() as u64, expected as u64, ns)
};
Status::from_result(rc).map(|_| ())
}
/// Wake up to `count` waiters on `word`, longest-waiting first.
/// Returns how many were woken.
pub fn futex_wake(word: &AtomicU32, count: u32) -> u32 {
let rc = unsafe { sys::syscall2(sys::SYS_FUTEX_WAKE, word.as_ptr() as u64, count as u64) };
Status::from_result(rc).unwrap_or(0) as u32
}
// ====================================================================
// Mutex: Drepper's three-state futex mutex ("Futexes Are Tricky", #3).
// 0 = unlocked, 1 = locked, 2 = locked with possible waiters.
// ====================================================================
pub struct Mutex<T: ?Sized> {
state: AtomicU32,
data: UnsafeCell<T>,
}
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
impl<T> Mutex<T> {
pub const fn new(value: T) -> Self {
Mutex { state: AtomicU32::new(0), data: UnsafeCell::new(value) }
}
pub fn into_inner(self) -> T {
self.data.into_inner()
}
}
impl<T: ?Sized> Mutex<T> {
pub fn lock(&self) -> MutexGuard<'_, T> {
if self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_err() {
self.lock_contended();
}
MutexGuard { mutex: self }
}
pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
self.state
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
.ok()
.map(|_| MutexGuard { mutex: self })
}
#[cold]
fn lock_contended(&self) {
// Brief spin first: most critical sections are short.
for _ in 0..100 {
if self.state.load(Ordering::Relaxed) == 0
&& self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_ok()
{
return;
}
core::hint::spin_loop();
}
while self.state.swap(2, Ordering::Acquire) != 0 {
let _ = futex_wait(&self.state, 2, None);
}
}
fn unlock(&self) {
if self.state.swap(0, Ordering::Release) == 2 {
futex_wake(&self.state, 1);
}
}
pub fn get_mut(&mut self) -> &mut T {
self.data.get_mut()
}
}
impl<T: Default> Default for Mutex<T> {
fn default() -> Self {
Mutex::new(T::default())
}
}
pub struct MutexGuard<'a, T: ?Sized> {
mutex: &'a Mutex<T>,
}
unsafe impl<T: ?Sized + Sync> Sync for MutexGuard<'_, T> {}
impl<T: ?Sized> Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.mutex.data.get() }
}
}
impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.mutex.data.get() }
}
}
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
self.mutex.unlock();
}
}
+75
View File
@@ -0,0 +1,75 @@
//! Raw syscall interface. Numbers and status codes are generated from
//! programs/include/Api/Syscall.hpp and kernel/src/Common/StatusCodes.hpp.
use core::arch::asm;
include!(concat!(env!("OUT_DIR"), "/abi.rs"));
/// `SYS_FUTEX_WAIT` timeout meaning "no deadline".
pub const FUTEX_WAIT_FOREVER: u64 = !0;
/// `SYS_FUTEX_WAKE` count meaning "every waiter".
pub const FUTEX_WAKE_ALL: u32 = u32::MAX;
impl Status {
/// Convert a raw syscall result: negative values are negated status
/// codes, everything else (handles, counts, sizes) is success.
pub fn from_result(rc: i64) -> Result<u64, Status> {
if rc >= 0 {
Ok(rc as u64)
} else if rc < i32::MIN as i64 {
Err(Status::GenericFailure)
} else {
Err(Status::from_code(-(rc as i32)))
}
}
}
// The kernel's SYSCALL path clobbers rcx/r11 (return RIP/RFLAGS) and, like
// the C++ wrappers in montauk/syscall.h, is treated as clobbering every
// argument register.
#[inline(always)]
pub unsafe fn syscall0(nr: u64) -> i64 {
let ret: i64;
unsafe {
asm!("syscall", inlateout("rax") nr as i64 => ret,
out("rcx") _, out("r11") _, out("rdi") _, out("rsi") _, out("rdx") _,
out("r8") _, out("r9") _, out("r10") _, options(nostack));
}
ret
}
#[inline(always)]
pub unsafe fn syscall1(nr: u64, a1: u64) -> i64 {
let ret: i64;
unsafe {
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
out("rcx") _, out("r11") _, out("rsi") _, out("rdx") _,
out("r8") _, out("r9") _, out("r10") _, options(nostack));
}
ret
}
#[inline(always)]
pub unsafe fn syscall2(nr: u64, a1: u64, a2: u64) -> i64 {
let ret: i64;
unsafe {
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
inlateout("rsi") a2 => _,
out("rcx") _, out("r11") _, out("rdx") _,
out("r8") _, out("r9") _, out("r10") _, options(nostack));
}
ret
}
#[inline(always)]
pub unsafe fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {
let ret: i64;
unsafe {
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
inlateout("rsi") a2 => _, inlateout("rdx") a3 => _,
out("rcx") _, out("r11") _,
out("r8") _, out("r9") _, out("r10") _, options(nostack));
}
ret
}
+93
View File
@@ -0,0 +1,93 @@
//! Threads (SYS_THREAD_SPAWN / SYS_THREAD_JOIN).
//!
//! The kernel uses the caller's stack verbatim and delivers one argument in
//! %rdi. A thread cannot free the stack it runs on, so the [`JoinHandle`]
//! owns it and frees it after a successful join; dropping an unjoined
//! handle leaks the stack (the thread may still be running on it).
use alloc::alloc::{alloc, dealloc, Layout};
use alloc::boxed::Box;
use crate::sys::{self, Status};
pub const DEFAULT_STACK_SIZE: usize = 256 * 1024;
const STACK_ALIGN: usize = 16;
pub struct JoinHandle {
tid: u32,
stack: *mut u8,
stack_layout: Layout,
}
unsafe impl Send for JoinHandle {}
impl JoinHandle {
pub fn id(&self) -> u32 {
self.tid
}
/// Wait for the thread to finish and return its exit code.
pub fn join(self) -> Result<i32, Status> {
let mut code: i32 = 0;
let rc = unsafe {
sys::syscall2(sys::SYS_THREAD_JOIN, self.tid as u64, &mut code as *mut i32 as u64)
};
// The kernel reports a bad tid as -1 (GenericFailure).
Status::from_result(rc)?;
unsafe { dealloc(self.stack, self.stack_layout) };
Ok(code)
}
}
/// Spawn a thread running `f`; its return value becomes the exit code.
pub fn spawn<F>(f: F) -> Result<JoinHandle, Status>
where
F: FnOnce() -> i32 + Send + 'static,
{
spawn_with_stack(DEFAULT_STACK_SIZE, f)
}
pub fn spawn_with_stack<F>(stack_size: usize, f: F) -> Result<JoinHandle, Status>
where
F: FnOnce() -> i32 + Send + 'static,
{
let stack_layout = Layout::from_size_align(stack_size.max(4096), STACK_ALIGN)
.map_err(|_| Status::InvalidArgument)?;
let stack = unsafe { alloc(stack_layout) };
if stack.is_null() {
return Err(Status::OutOfMemory);
}
let closure = Box::into_raw(Box::new(f));
let top = (stack as usize + stack_layout.size()) & !(STACK_ALIGN - 1);
let rc = unsafe {
sys::syscall3(sys::SYS_THREAD_SPAWN, trampoline::<F> as *const () as u64, closure as u64, top as u64)
};
match Status::from_result(rc) {
Ok(tid) => Ok(JoinHandle { tid: tid as u32, stack, stack_layout }),
Err(e) => unsafe {
drop(Box::from_raw(closure));
dealloc(stack, stack_layout);
Err(e)
},
}
}
extern "C" fn trampoline<F>(arg: *mut F) -> !
where
F: FnOnce() -> i32,
{
let f = unsafe { Box::from_raw(arg) };
let code = f();
unsafe { sys::syscall1(sys::SYS_THREAD_EXIT, code as u32 as u64) };
unreachable!()
}
/// The calling thread's id (equals the process id on the main thread).
pub fn current_id() -> u32 {
unsafe { sys::syscall0(sys::SYS_THREAD_SELF) as u32 }
}
pub fn yield_now() {
unsafe { sys::syscall0(sys::SYS_YIELD) };
}
+18
View File
@@ -0,0 +1,18 @@
//! Time and sleeping.
use core::time::Duration;
use crate::sys;
/// Milliseconds since boot (monotonic).
pub fn uptime_ms() -> u64 {
unsafe { sys::syscall0(sys::SYS_GETMILLISECONDS) as u64 }
}
/// Sleep for at least `d`, rounded up to the 1 ms scheduler tick.
pub fn sleep(d: Duration) {
let ms = d.as_millis().min(u64::MAX as u128) as u64 + u64::from(d.subsec_nanos() % 1_000_000 != 0);
if ms > 0 {
unsafe { sys::syscall1(sys::SYS_SLEEP_MS, ms) };
}
}
+8
View File
@@ -0,0 +1,8 @@
# Phase 2 Rust support: the forked rustc with the built-in
# x86_64-unknown-montauk target and a native std. Build it in ~/dev/mtk/rust,
# then register it with:
# rustup toolchain link montauk ~/dev/mtk/rust/build/host/stage1
# The linked toolchain has no cargo of its own; rustup falls back to the
# nightly toolchain's cargo.
[toolchain]
channel = "montauk"
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "rusthello"
version = "0.1.0"
edition = "2024"
[dependencies]
montauk-rt = { path = "../montauk-rt" }
+148
View File
@@ -0,0 +1,148 @@
//! rusthello - `no_std` Rust smoke test for MontaukOS (montauk-rt).
//!
//! Exercises alloc collections, formatting, argv, a #[thread_local]
//! (PT_TLS + per-thread %fs blocks), threads, and the futex mutex.
//! `rusthello panic` / `rusthello panic-thread` check that a panic on
//! either thread takes the whole process down; `rusthello fault-thread`
//! checks the same for a page fault on a worker thread.
#![no_std]
#![no_main]
#![feature(thread_local)]
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::cell::Cell;
use core::time::Duration;
use montauk_rt::sync::Mutex;
use montauk_rt::{entry, println, thread, time};
entry!(main);
static mut FAILURES: u32 = 0;
fn check(ok: bool, name: &str) {
println!("{} {}", if ok { "PASS" } else { "FAIL" }, name);
if !ok {
unsafe { FAILURES += 1 };
}
}
#[thread_local]
static TLS_COUNTER: Cell<u64> = Cell::new(7);
const THREADS: usize = 4;
const ITERS: u64 = 20_000;
fn main() -> i32 {
println!("rusthello: Rust on MontaukOS (no_std + alloc)");
let args: Vec<&str> = montauk_rt::env::args().collect();
println!("argv = {:?}", args);
match args.get(1).copied() {
Some("panic") => panic!("requested panic on the main thread"),
Some("fault-thread") => {
// A CPU fault (not a panic) on a worker must still kill the
// whole process, with the SIGSEGV exit code (267).
let h = thread::spawn(|| {
unsafe { core::ptr::write_volatile(8 as *mut u64, 0xdead) };
0
})
.unwrap();
let _ = h.join();
thread::yield_now();
println!("FAIL process survived a worker-thread fault");
return 1;
}
Some("panic-thread") => {
let h = thread::spawn(|| panic!("requested panic on a worker thread")).unwrap();
let _ = h.join();
println!("FAIL process survived a worker-thread panic");
return 1;
}
_ => {}
}
// --- alloc ---
let mut v: Vec<u64> = (1..=100).collect();
v.retain(|x| x % 3 == 0);
check(v.len() == 33 && v.iter().sum::<u64>() == 1683, "Vec collect/retain/sum");
let mut s = String::new();
for word in ["Montauk", "speaks", "Rust"] {
if !s.is_empty() {
s.push(' ');
}
s.push_str(word);
}
check(s == "Montauk speaks Rust", "String building");
check(format!("{:>6.2}|{:#x}|{:?}", 3.14159f64, 255, Some("x")) == " 3.14|0xff|Some(\"x\")",
"format! with floats, hex, Debug");
let mut map = BTreeMap::new();
for (i, w) in s.split(' ').enumerate() {
map.insert(w, i);
}
check(map.get("Rust") == Some(&2) && map.len() == 3, "BTreeMap");
let big: Vec<u8> = alloc::vec![0xA5; 1 << 20];
check(big.iter().all(|&b| b == 0xA5), "1 MiB allocation");
#[repr(align(4096))]
struct PageAligned([u8; 64]);
let page = alloc::boxed::Box::new(PageAligned([1; 64]));
check((&*page as *const PageAligned as usize) % 4096 == 0 && page.0[63] == 1, "over-aligned Box (4096)");
// --- thread-local storage ---
check(TLS_COUNTER.get() == 7, "#[thread_local] initial value on main thread");
TLS_COUNTER.set(1000);
let tls_seen = thread::spawn(|| {
let initial = TLS_COUNTER.get();
TLS_COUNTER.set(55);
initial as i32
})
.unwrap()
.join();
check(tls_seen == Ok(7), "worker thread gets a fresh TLS block");
check(TLS_COUNTER.get() == 1000, "main thread TLS untouched by worker");
// --- threads + futex mutex ---
let counter = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for t in 0..THREADS {
let counter = counter.clone();
handles.push(
thread::spawn(move || {
for i in 0..ITERS {
let mut g = counter.lock();
let v = *g;
if i % 512 == 0 {
thread::yield_now(); // widen the race window under the lock
}
*g = v + 1;
}
t as i32 + 10
})
.unwrap(),
);
}
let codes: Vec<_> = handles.into_iter().map(|h| h.join()).collect();
check(codes.iter().enumerate().all(|(t, c)| *c == Ok(t as i32 + 10)), "thread exit codes via join");
check(*counter.lock() == THREADS as u64 * ITERS, "futex Mutex keeps the counter exact");
// --- time ---
let t0 = time::uptime_ms();
time::sleep(Duration::from_millis(25));
let dt = time::uptime_ms() - t0;
check((25..1000).contains(&dt), "sleep(25 ms)");
let failures = unsafe { FAILURES };
println!("rusthello: {}", if failures == 0 { "all passed" } else { "FAILURES" });
if failures == 0 { 0 } else { 1 }
}
+4
View File
@@ -0,0 +1,4 @@
[package]
name = "ruststd"
version = "0.1.0"
edition = "2024"
+477
View File
@@ -0,0 +1,477 @@
//! ruststd - Phase 2 Rust smoke test for MontaukOS: the native `std`.
//!
//! Runs a set of PASS/FAIL checks over the std platform layer: collections and
//! formatting, args and environment, the filesystem, threads (joined, scoped
//! and detached), the futex-backed sync primitives, thread_local! destructors,
//! time, and child processes (exit codes, arguments, environment, working
//! directory, std::process::exit from a worker thread, panics).
//!
//! `ruststd net <host> [port]` additionally fetches `http://<host>/` over TCP.
//! The `child-*` arguments are the child-process halves of the checks.
use std::collections::{BTreeMap, HashMap};
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier, Condvar, Mutex, OnceLock, RwLock, mpsc};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use std::{env, thread};
static FAILURES: AtomicUsize = AtomicUsize::new(0);
fn check(ok: bool, name: &str) {
println!("{} {}", if ok { "PASS" } else { "FAIL" }, name);
if !ok {
FAILURES.fetch_add(1, Ordering::Relaxed);
}
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
match args.get(1).map(String::as_str) {
Some(mode) if mode.starts_with("child-") => return ExitCode::from(child(mode, &args)),
Some("net") => return net(&args),
_ => {}
}
println!("ruststd: Rust std on MontaukOS");
println!("argv = {args:?}");
basics();
environment();
filesystem();
threads();
sync();
thread_locals();
time();
processes(&args);
let failures = FAILURES.load(Ordering::Relaxed);
if failures == 0 {
println!("ruststd: all passed");
ExitCode::SUCCESS
} else {
println!("ruststd: {failures} FAILED");
ExitCode::FAILURE
}
}
// ====================================================================
// Child-process halves
// ====================================================================
fn child(mode: &str, args: &[String]) -> u8 {
match mode {
"child-exit" => 7,
"child-args" => {
let ok = args[2..] == ["with space", "", "it's", "say \"hi\"", "plain"];
if ok { 8 } else { 1 }
}
"child-env" => {
if env::var("RUSTSTD_VAR").as_deref() == Ok("hello there") { 9 } else { 1 }
}
"child-cwd" => {
let cwd = env::current_dir().unwrap_or_default();
if cwd.ends_with("os") { 10 } else { 1 }
}
"child-exit-thread" => {
// std::process::exit from a worker must end the whole process.
let h = thread::spawn(|| std::process::exit(11));
let _ = h.join();
thread::sleep(Duration::from_secs(5));
1
}
"child-panic-thread" => {
let h = thread::spawn(|| panic!("requested panic on a worker thread"));
let _ = h.join();
thread::sleep(Duration::from_secs(5));
1
}
_ => 2,
}
}
// ====================================================================
// Checks
// ====================================================================
fn basics() {
let mut map = HashMap::new();
for (i, w) in "Montauk speaks Rust with std".split(' ').enumerate() {
map.insert(w.to_string(), i);
}
check(map["Rust"] == 2 && map.len() == 5, "HashMap (RandomState keys from SYS_GETRANDOM)");
let sorted: BTreeMap<_, _> = map.iter().map(|(k, v)| (*v, k.clone())).collect();
check(sorted.values().next().map(String::as_str) == Some("Montauk"), "BTreeMap");
check(
format!("{:>8.3}|{:e}|{:#06x}", std::f64::consts::PI, 1234.5f64, 255) == " 3.142|1.2345e3|0x00ff",
"float and integer formatting",
);
let mut out = std::io::stdout().lock();
check(writeln!(out, " (stdout lock + writeln!)").is_ok(), "io::stdout().lock()");
eprintln!(" (eprintln! goes to the terminal too)");
}
fn environment() {
let exe = env::current_exe();
check(exe.as_ref().is_ok_and(|p| p.is_absolute()), "env::current_exe() is absolute");
println!(" current_exe = {exe:?}");
let cwd = env::current_dir();
check(cwd.is_ok(), "env::current_dir()");
unsafe { env::set_var("RUSTSTD_TEST", "value=with=equals") };
check(env::var("RUSTSTD_TEST").as_deref() == Ok("value=with=equals"), "set_var / var");
check(env::vars().any(|(k, v)| k == "RUSTSTD_TEST" && v == "value=with=equals"), "env::vars() lists it");
unsafe { env::remove_var("RUSTSTD_TEST") };
check(env::var_os("RUSTSTD_TEST").is_none(), "remove_var");
let joined = env::join_paths(["0:/os", "0:/games"]).unwrap();
let split: Vec<PathBuf> = env::split_paths(&joined).collect();
check(joined == "0:/os;0:/games" && split.len() == 2, "join_paths / split_paths use ';'");
check(Path::new("0:/os/shell.elf").is_absolute(), "drive paths are absolute");
check(Path::new("0:/os").join("0:/apps") == Path::new("0:/apps"), "joining a drive path replaces");
check(Path::new("0:/os/shell.elf").parent() == Some(Path::new("0:/os")), "Path::parent");
}
fn filesystem() {
let base = env::temp_dir().join(format!("ruststd-{}", std::process::id()));
let _ = fs::remove_dir_all(&base);
check(fs::create_dir_all(base.join("sub/deeper")).is_ok(), "create_dir_all");
check(fs::create_dir(&base).is_err_and(|e| e.kind() == std::io::ErrorKind::AlreadyExists),
"create_dir on an existing dir fails with AlreadyExists");
let file = base.join("hello.txt");
check(fs::write(&file, b"line one\nline two\n").is_ok(), "fs::write");
check(fs::read_to_string(&file).ok().as_deref() == Some("line one\nline two\n"), "fs::read_to_string");
let appended = OpenOptions::new().append(true).open(&file).and_then(|mut f| f.write_all(b"line three\n"));
check(appended.is_ok(), "append");
let lines: Vec<String> = BufReader::new(File::open(&file).unwrap()).lines().map(Result::unwrap).collect();
check(lines == ["line one", "line two", "line three"], "BufReader::lines after append");
let meta = fs::metadata(&file);
check(meta.as_ref().is_ok_and(|m| m.is_file() && m.len() == 29), "metadata: is_file, len");
if let Ok(m) = &meta {
println!(" modified = {:?}", m.modified().ok().and_then(|t| t.duration_since(UNIX_EPOCH).ok()));
}
check(fs::metadata(&base).is_ok_and(|m| m.is_dir()), "metadata: is_dir");
// Seek + overwrite in place.
{
let mut f = OpenOptions::new().read(true).write(true).open(&file).unwrap();
f.seek(SeekFrom::Start(5)).unwrap();
f.write_all(b"ONE").unwrap();
f.seek(SeekFrom::End(-6)).unwrap();
let mut tail = String::new();
f.read_to_string(&mut tail).unwrap();
check(tail == "three\n", "seek from end + read");
}
check(fs::read_to_string(&file).unwrap().starts_with("line ONE\n"), "seek + overwrite");
// Truncate on open.
fs::write(&file, b"short").unwrap();
check(fs::read(&file).ok().as_deref() == Some(&b"short"[..]), "File::create truncates");
check(fs::copy(&file, base.join("sub/copy.txt")).ok() == Some(5), "fs::copy");
check(fs::rename(base.join("sub/copy.txt"), base.join("sub/moved.txt")).is_ok(), "fs::rename");
let mut names: Vec<String> = fs::read_dir(&base)
.map(|rd| rd.filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect())
.unwrap_or_default();
names.sort();
check(names == ["hello.txt", "sub"], "read_dir lists entries");
let sub_types: Vec<(String, bool)> = fs::read_dir(base.join("sub"))
.unwrap()
.map(|e| e.unwrap())
.map(|e| (e.file_name().to_string_lossy().into_owned(), e.file_type().unwrap().is_dir()))
.collect();
check(sub_types.contains(&("deeper".into(), true)) && sub_types.contains(&("moved.txt".into(), false)),
"DirEntry::file_type");
check(File::open(base.join("missing")).is_err_and(|e| e.kind() == std::io::ErrorKind::NotFound),
"open missing file -> NotFound");
check(fs::remove_file(&file).is_ok() && !file.exists(), "remove_file");
check(fs::remove_dir_all(&base).is_ok() && !base.exists(), "remove_dir_all");
}
fn threads() {
let handles: Vec<_> = (0..4).map(|i| thread::spawn(move || i * 10)).collect();
let results: Vec<i32> = handles.into_iter().map(|h| h.join().unwrap()).collect();
check(results == [0, 10, 20, 30], "spawn + join return values");
let named = thread::Builder::new().name("worker".into()).spawn(|| {
thread::current().name().map(str::to_owned)
});
check(named.unwrap().join().unwrap().as_deref() == Some("worker"), "named thread");
let mut data = vec![1, 2, 3, 4];
thread::scope(|s| {
for x in data.iter_mut() {
s.spawn(move || *x *= 2);
}
});
check(data == [2, 4, 6, 8], "thread::scope borrows");
// Detached threads hold a kernel slot until reaped; 300 of them would
// exhaust the 256-slot table if they were not.
let done = Arc::new(AtomicUsize::new(0));
for _ in 0..15 {
for _ in 0..20 {
let done = done.clone();
drop(thread::spawn(move || {
done.fetch_add(1, Ordering::Relaxed);
}));
}
thread::sleep(Duration::from_millis(20));
}
let deadline = Instant::now() + Duration::from_secs(5);
while done.load(Ordering::Relaxed) < 300 && Instant::now() < deadline {
thread::sleep(Duration::from_millis(10));
}
check(done.load(Ordering::Relaxed) == 300, "300 detached threads are reaped");
let big = thread::Builder::new().stack_size(4 << 20).spawn(|| {
let buf = [7u8; 2 << 20];
buf.iter().map(|&b| b as usize).sum::<usize>()
});
check(big.unwrap().join().unwrap() == 7 * (2 << 20), "4 MiB thread stack");
}
fn sync() {
const THREADS: usize = 4;
const ITERS: usize = 10_000;
let counter = Arc::new(Mutex::new(0usize));
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let counter = counter.clone();
thread::spawn(move || {
for i in 0..ITERS {
let mut g = counter.lock().unwrap();
if i % 1000 == 0 {
thread::yield_now();
}
*g += 1;
}
})
})
.collect();
handles.into_iter().for_each(|h| h.join().unwrap());
check(*counter.lock().unwrap() == THREADS * ITERS, "Mutex keeps the counter exact");
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair2 = pair.clone();
let waiter = thread::spawn(move || {
let (lock, cvar) = &*pair2;
let guard = cvar.wait_while(lock.lock().unwrap(), |ready| !*ready).unwrap();
*guard
});
thread::sleep(Duration::from_millis(20));
*pair.0.lock().unwrap() = true;
pair.1.notify_all();
check(waiter.join().unwrap(), "Condvar wait/notify");
let (lock, cvar) = (Mutex::new(()), Condvar::new());
let t0 = Instant::now();
let (_g, res) = cvar.wait_timeout(lock.lock().unwrap(), Duration::from_millis(30)).unwrap();
check(res.timed_out() && t0.elapsed() >= Duration::from_millis(30), "Condvar wait_timeout");
let rw = Arc::new(RwLock::new(vec![1]));
let readers: Vec<_> = (0..3)
.map(|_| {
let rw = rw.clone();
thread::spawn(move || rw.read().unwrap().len())
})
.collect();
rw.write().unwrap().push(2);
let lens: Vec<usize> = readers.into_iter().map(|h| h.join().unwrap()).collect();
check(lens.iter().all(|&l| l == 1 || l == 2) && rw.read().unwrap().len() == 2, "RwLock");
static ONCE: OnceLock<usize> = OnceLock::new();
let inits = Arc::new(AtomicUsize::new(0));
let hs: Vec<_> = (0..4)
.map(|_| {
let inits = inits.clone();
thread::spawn(move || *ONCE.get_or_init(|| inits.fetch_add(1, Ordering::SeqCst) + 42))
})
.collect();
let vals: Vec<usize> = hs.into_iter().map(|h| h.join().unwrap()).collect();
check(vals.iter().all(|&v| v == 42) && inits.load(Ordering::SeqCst) == 1, "OnceLock initializes once");
let (tx, rx) = mpsc::channel();
for t in 0..3 {
let tx = tx.clone();
thread::spawn(move || {
for i in 0..100 {
tx.send(t * 1000 + i).unwrap();
}
});
}
drop(tx);
let received: Vec<i32> = rx.iter().collect();
check(received.len() == 300, "mpsc channel with 3 senders");
let barrier = Arc::new(Barrier::new(3));
let hs: Vec<_> = (0..3)
.map(|_| {
let b = barrier.clone();
thread::spawn(move || b.wait().is_leader())
})
.collect();
let leaders = hs.into_iter().map(|h| h.join().unwrap()).filter(|&l| l).count();
check(leaders == 1, "Barrier");
let parked = thread::spawn(|| {
thread::park();
true
});
thread::sleep(Duration::from_millis(10));
parked.thread().unpark();
check(parked.join().unwrap(), "park / unpark");
}
fn thread_locals() {
static DROPS: AtomicUsize = AtomicUsize::new(0);
struct Tracker(u32);
impl Drop for Tracker {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::SeqCst);
}
}
thread_local! {
static COUNTER: std::cell::Cell<u32> = const { std::cell::Cell::new(5) };
static TRACKER: Tracker = Tracker(1);
}
COUNTER.with(|c| c.set(100));
let seen = thread::spawn(|| {
TRACKER.with(|t| assert_eq!(t.0, 1));
COUNTER.with(|c| c.get())
})
.join()
.unwrap();
check(seen == 5 && COUNTER.with(|c| c.get()) == 100, "thread_local! is per thread");
check(DROPS.load(Ordering::SeqCst) == 1, "thread_local! destructor runs at thread exit");
}
fn time() {
let t0 = Instant::now();
thread::sleep(Duration::from_millis(50));
let dt = t0.elapsed();
check(dt >= Duration::from_millis(50) && dt < Duration::from_secs(2), "Instant + sleep(50 ms)");
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
// 2025-01-01T00:00:00Z
check(now > 1_735_689_600, "SystemTime::now() is after 2025");
println!(" unix time = {now}");
}
fn processes(args: &[String]) {
let Ok(me) = env::current_exe() else {
check(false, "current_exe for child processes");
return;
};
let _ = args;
let run = |cmd: &mut Command| cmd.status().map(|s| s.code());
check(run(Command::new(&me).arg("child-exit")).ok() == Some(Some(7)), "child exit code");
check(
run(Command::new(&me).args(["child-args", "with space", "", "it's", "say \"hi\"", "plain"])).ok() == Some(Some(8)),
"child arguments with spaces and quotes",
);
check(
run(Command::new(&me).arg("child-env").env("RUSTSTD_VAR", "hello there")).ok() == Some(Some(9)),
"child environment",
);
check(
run(Command::new(&me).arg("child-cwd").current_dir("0:/os")).ok() == Some(Some(10)),
"child working directory",
);
check(
run(Command::new(&me).arg("child-exit-thread")).ok() == Some(Some(11)),
"process::exit from a worker thread ends the process",
);
let panicked = Command::new(&me).arg("child-panic-thread").status();
check(
panicked.as_ref().is_ok_and(|s| s.code().is_none() && s.to_string().contains("signal 6")),
"panic on a worker thread aborts the process",
);
if let Ok(s) = panicked {
println!(" panicking child: {s}");
}
let mut child = Command::new(&me).arg("child-exit").spawn().unwrap();
let mut polled = None;
for _ in 0..200 {
if let Ok(Some(s)) = child.try_wait() {
polled = Some(s);
break;
}
thread::sleep(Duration::from_millis(10));
}
check(polled.and_then(|s| s.code()) == Some(7), "Child::try_wait");
check(Command::new("no-such-program").status().is_err(), "spawning a missing program fails");
check(
Command::new(&me).arg("child-exit").output().is_err_and(|e| e.kind() == std::io::ErrorKind::Unsupported),
"output() reports Unsupported (no stdio redirection yet)",
);
}
// ====================================================================
// Network (optional: needs a link)
// ====================================================================
fn net(args: &[String]) -> ExitCode {
use std::net::{TcpStream, ToSocketAddrs};
let Some(host) = args.get(2) else {
println!("usage: ruststd net <host> [port]");
return ExitCode::FAILURE;
};
let port: u16 = args.get(3).and_then(|p| p.parse().ok()).unwrap_or(80);
let addrs: Vec<_> = match (host.as_str(), port).to_socket_addrs() {
Ok(a) => a.collect(),
Err(e) => {
println!("resolve {host}: {e}");
return ExitCode::FAILURE;
}
};
println!("{host} -> {addrs:?}");
let mut stream = match TcpStream::connect((host.as_str(), port)) {
Ok(s) => s,
Err(e) => {
println!("connect: {e}");
return ExitCode::FAILURE;
}
};
stream.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
let request = format!("GET / HTTP/1.0\r\nHost: {host}\r\nConnection: close\r\n\r\n");
if let Err(e) = stream.write_all(request.as_bytes()) {
println!("send: {e}");
return ExitCode::FAILURE;
}
let mut response = Vec::new();
match stream.read_to_end(&mut response) {
Ok(n) => {
let text = String::from_utf8_lossy(&response);
println!("received {n} bytes; status line: {:?}", text.lines().next().unwrap_or(""));
ExitCode::SUCCESS
}
Err(e) => {
println!("recv: {e}");
ExitCode::FAILURE
}
}
}
+4 -4
View File
@@ -466,13 +466,13 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
if (g_game.game_over) if (g_game.game_over)
return false; return false;
if (key.scancode == 0x4B || key.ascii == 'a' || key.ascii == 'A') if (montauk::nav_scancode(key) == 0x4B || key.ascii == 'a' || key.ascii == 'A')
return apply_move(DIR_LEFT); return apply_move(DIR_LEFT);
if (key.scancode == 0x4D || key.ascii == 'd' || key.ascii == 'D') if (montauk::nav_scancode(key) == 0x4D || key.ascii == 'd' || key.ascii == 'D')
return apply_move(DIR_RIGHT); return apply_move(DIR_RIGHT);
if (key.scancode == 0x48 || key.ascii == 'w' || key.ascii == 'W') if (montauk::nav_scancode(key) == 0x48 || key.ascii == 'w' || key.ascii == 'W')
return apply_move(DIR_UP); return apply_move(DIR_UP);
if (key.scancode == 0x50 || key.ascii == 's' || key.ascii == 'S') if (montauk::nav_scancode(key) == 0x50 || key.ascii == 's' || key.ascii == 'S')
return apply_move(DIR_DOWN); return apply_move(DIR_DOWN);
return false; return false;
+2 -2
View File
@@ -469,12 +469,12 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
g_win.closed = true; g_win.closed = true;
return false; return false;
} }
if (key.scancode == 0x4D || key.ascii == '+' || key.ascii == '=') { if (montauk::nav_scancode(key) == 0x4D || key.ascii == '+' || key.ascii == '=') {
if (g_master_muted) toggle_master_mute(); if (g_master_muted) toggle_master_mute();
apply_master_volume(g_master_vol + 5); apply_master_volume(g_master_vol + 5);
return true; return true;
} }
if (key.scancode == 0x4B || key.ascii == '-') { if (montauk::nav_scancode(key) == 0x4B || key.ascii == '-') {
if (g_master_muted) toggle_master_mute(); if (g_master_muted) toggle_master_mute();
apply_master_volume(g_master_vol - 5); apply_master_volume(g_master_vol - 5);
return true; return true;
+1 -1
View File
@@ -4,5 +4,5 @@ binary = "audio.elf"
icon = "pavucontrol.svg" icon = "pavucontrol.svg"
[menu] [menu]
category = "System" category = "Hardware"
visible = true visible = true
+2
View File
@@ -224,6 +224,8 @@ static void draw_text_fit(Canvas& c, int x, int y,
out++; out++;
} }
out = montauk::utf8::floor_boundary(text, montauk::slen(text), out);
if (out <= 0) return; if (out <= 0) return;
buf[out] = '.'; buf[out] = '.';
buf[out + 1] = '.'; buf[out + 1] = '.';
+1 -1
View File
@@ -4,5 +4,5 @@ binary = "bluetooth.elf"
icon = "bluetooth.svg" icon = "bluetooth.svg"
[menu] [menu]
category = "System" category = "Hardware"
visible = true visible = true
+1 -1
View File
@@ -4,7 +4,7 @@ binary = "calculator.elf"
icon = "accessories-calculator.svg" icon = "accessories-calculator.svg"
[menu] [menu]
category = "Applications" category = "Utilities"
visible = true visible = true
[launch] [launch]
+3 -2
View File
@@ -400,6 +400,7 @@ static void fit_text(char* out, int outCap, const char* text, int maxWidth) {
if (text_width(out) + text_width(ellipsis) > maxWidth) break; if (text_width(out) + text_width(ellipsis) > maxWidth) break;
len++; len++;
} }
len = montauk::utf8::floor_boundary(text, montauk::slen(text), len);
out[len++] = '.'; out[len++] = '.';
out[len++] = '.'; out[len++] = '.';
out[len++] = '.'; out[len++] = '.';
@@ -798,8 +799,8 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
return true; return true;
} }
int current = *selected_for_tab(); int current = *selected_for_tab();
if (key.scancode == 0x48) { select_index(current - 1); return true; } // Up if (montauk::nav_scancode(key) == 0x48) { select_index(current - 1); return true; } // Up
if (key.scancode == 0x50) { select_index(current + 1); return true; } // Down if (montauk::nav_scancode(key) == 0x50) { select_index(current + 1); return true; } // Down
if (key.ascii == 'r' || key.ascii == 'R') { if (key.ascii == 'r' || key.ascii == 'R') {
reload(); reload();
return true; return true;
+204 -150
View File
@@ -5,13 +5,9 @@
* Browse curated sets of characters, click (or use the keyboard) to select, * Browse curated sets of characters, click (or use the keyboard) to select,
* and copy to the system clipboard for pasting into other apps. * and copy to the system clipboard for pasting into other apps.
* *
* Character model: MontaukOS's GUI text stack is single-byte (Windows-1252, * Character model: MontaukOS text is UTF-8. Each tile is a Unicode codepoint
* with the 0x80-0x9F block mapped to typographic punctuation in * and Copy puts its UTF-8 encoding on the clipboard. The sets are limited to
* gui::decode_single_byte_codepoint). The glyph cache only holds codepoints * characters the system font (Roboto) actually has a glyph for.
* 0-255, so that byte range is exactly the set of glyphs the system font can
* render. We therefore present that set and copy the *raw byte* to the
* clipboard: it renders identically here and in every other Montauk app
* (copying multi-byte UTF-8 would render as mojibake in the single-byte stack).
* *
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
@@ -37,7 +33,7 @@ using namespace gui;
// ==== Character data ==== // ==== Character data ====
struct Glyph { struct Glyph {
uint8_t byte; // Windows-1252 byte value (what gets copied / rendered) uint32_t cp; // Unicode codepoint (copied as UTF-8)
const char* name; // human-readable description const char* name; // human-readable description
}; };
@@ -49,141 +45,194 @@ struct CharSet {
// Accented Latin letters and ligatures (things you can't easily type). // Accented Latin letters and ligatures (things you can't easily type).
static const Glyph LATIN[] = { static const Glyph LATIN[] = {
{0xC0, "Latin Capital Letter A With Grave"}, {0x00C0, "Latin Capital Letter A With Grave"},
{0xC1, "Latin Capital Letter A With Acute"}, {0x00C1, "Latin Capital Letter A With Acute"},
{0xC2, "Latin Capital Letter A With Circumflex"}, {0x00C2, "Latin Capital Letter A With Circumflex"},
{0xC3, "Latin Capital Letter A With Tilde"}, {0x00C3, "Latin Capital Letter A With Tilde"},
{0xC4, "Latin Capital Letter A With Diaeresis"}, {0x00C4, "Latin Capital Letter A With Diaeresis"},
{0xC5, "Latin Capital Letter A With Ring Above"}, {0x00C5, "Latin Capital Letter A With Ring Above"},
{0xC6, "Latin Capital Letter AE"}, {0x00C6, "Latin Capital Letter AE"},
{0xC7, "Latin Capital Letter C With Cedilla"}, {0x00C7, "Latin Capital Letter C With Cedilla"},
{0xC8, "Latin Capital Letter E With Grave"}, {0x00C8, "Latin Capital Letter E With Grave"},
{0xC9, "Latin Capital Letter E With Acute"}, {0x00C9, "Latin Capital Letter E With Acute"},
{0xCA, "Latin Capital Letter E With Circumflex"}, {0x00CA, "Latin Capital Letter E With Circumflex"},
{0xCB, "Latin Capital Letter E With Diaeresis"}, {0x00CB, "Latin Capital Letter E With Diaeresis"},
{0xCC, "Latin Capital Letter I With Grave"}, {0x00CC, "Latin Capital Letter I With Grave"},
{0xCD, "Latin Capital Letter I With Acute"}, {0x00CD, "Latin Capital Letter I With Acute"},
{0xCE, "Latin Capital Letter I With Circumflex"}, {0x00CE, "Latin Capital Letter I With Circumflex"},
{0xCF, "Latin Capital Letter I With Diaeresis"}, {0x00CF, "Latin Capital Letter I With Diaeresis"},
{0xD0, "Latin Capital Letter Eth"}, {0x00D0, "Latin Capital Letter Eth"},
{0xD1, "Latin Capital Letter N With Tilde"}, {0x00D1, "Latin Capital Letter N With Tilde"},
{0xD2, "Latin Capital Letter O With Grave"}, {0x00D2, "Latin Capital Letter O With Grave"},
{0xD3, "Latin Capital Letter O With Acute"}, {0x00D3, "Latin Capital Letter O With Acute"},
{0xD4, "Latin Capital Letter O With Circumflex"}, {0x00D4, "Latin Capital Letter O With Circumflex"},
{0xD5, "Latin Capital Letter O With Tilde"}, {0x00D5, "Latin Capital Letter O With Tilde"},
{0xD6, "Latin Capital Letter O With Diaeresis"}, {0x00D6, "Latin Capital Letter O With Diaeresis"},
{0xD8, "Latin Capital Letter O With Stroke"}, {0x00D8, "Latin Capital Letter O With Stroke"},
{0xD9, "Latin Capital Letter U With Grave"}, {0x00D9, "Latin Capital Letter U With Grave"},
{0xDA, "Latin Capital Letter U With Acute"}, {0x00DA, "Latin Capital Letter U With Acute"},
{0xDB, "Latin Capital Letter U With Circumflex"}, {0x00DB, "Latin Capital Letter U With Circumflex"},
{0xDC, "Latin Capital Letter U With Diaeresis"}, {0x00DC, "Latin Capital Letter U With Diaeresis"},
{0xDD, "Latin Capital Letter Y With Acute"}, {0x00DD, "Latin Capital Letter Y With Acute"},
{0xDE, "Latin Capital Letter Thorn"}, {0x00DE, "Latin Capital Letter Thorn"},
{0x8A, "Latin Capital Letter S With Caron"}, {0x0160, "Latin Capital Letter S With Caron"},
{0x8C, "Latin Capital Ligature OE"}, {0x0152, "Latin Capital Ligature OE"},
{0x8E, "Latin Capital Letter Z With Caron"}, {0x017D, "Latin Capital Letter Z With Caron"},
{0x9F, "Latin Capital Letter Y With Diaeresis"}, {0x0178, "Latin Capital Letter Y With Diaeresis"},
{0xDF, "Latin Small Letter Sharp S"}, {0x00DF, "Latin Small Letter Sharp S"},
{0xE0, "Latin Small Letter A With Grave"}, {0x00E0, "Latin Small Letter A With Grave"},
{0xE1, "Latin Small Letter A With Acute"}, {0x00E1, "Latin Small Letter A With Acute"},
{0xE2, "Latin Small Letter A With Circumflex"}, {0x00E2, "Latin Small Letter A With Circumflex"},
{0xE3, "Latin Small Letter A With Tilde"}, {0x00E3, "Latin Small Letter A With Tilde"},
{0xE4, "Latin Small Letter A With Diaeresis"}, {0x00E4, "Latin Small Letter A With Diaeresis"},
{0xE5, "Latin Small Letter A With Ring Above"}, {0x00E5, "Latin Small Letter A With Ring Above"},
{0xE6, "Latin Small Letter AE"}, {0x00E6, "Latin Small Letter AE"},
{0xE7, "Latin Small Letter C With Cedilla"}, {0x00E7, "Latin Small Letter C With Cedilla"},
{0xE8, "Latin Small Letter E With Grave"}, {0x00E8, "Latin Small Letter E With Grave"},
{0xE9, "Latin Small Letter E With Acute"}, {0x00E9, "Latin Small Letter E With Acute"},
{0xEA, "Latin Small Letter E With Circumflex"}, {0x00EA, "Latin Small Letter E With Circumflex"},
{0xEB, "Latin Small Letter E With Diaeresis"}, {0x00EB, "Latin Small Letter E With Diaeresis"},
{0xEC, "Latin Small Letter I With Grave"}, {0x00EC, "Latin Small Letter I With Grave"},
{0xED, "Latin Small Letter I With Acute"}, {0x00ED, "Latin Small Letter I With Acute"},
{0xEE, "Latin Small Letter I With Circumflex"}, {0x00EE, "Latin Small Letter I With Circumflex"},
{0xEF, "Latin Small Letter I With Diaeresis"}, {0x00EF, "Latin Small Letter I With Diaeresis"},
{0xF0, "Latin Small Letter Eth"}, {0x00F0, "Latin Small Letter Eth"},
{0xF1, "Latin Small Letter N With Tilde"}, {0x00F1, "Latin Small Letter N With Tilde"},
{0xF2, "Latin Small Letter O With Grave"}, {0x00F2, "Latin Small Letter O With Grave"},
{0xF3, "Latin Small Letter O With Acute"}, {0x00F3, "Latin Small Letter O With Acute"},
{0xF4, "Latin Small Letter O With Circumflex"}, {0x00F4, "Latin Small Letter O With Circumflex"},
{0xF5, "Latin Small Letter O With Tilde"}, {0x00F5, "Latin Small Letter O With Tilde"},
{0xF6, "Latin Small Letter O With Diaeresis"}, {0x00F6, "Latin Small Letter O With Diaeresis"},
{0xF8, "Latin Small Letter O With Stroke"}, {0x00F8, "Latin Small Letter O With Stroke"},
{0xF9, "Latin Small Letter U With Grave"}, {0x00F9, "Latin Small Letter U With Grave"},
{0xFA, "Latin Small Letter U With Acute"}, {0x00FA, "Latin Small Letter U With Acute"},
{0xFB, "Latin Small Letter U With Circumflex"}, {0x00FB, "Latin Small Letter U With Circumflex"},
{0xFC, "Latin Small Letter U With Diaeresis"}, {0x00FC, "Latin Small Letter U With Diaeresis"},
{0xFD, "Latin Small Letter Y With Acute"}, {0x00FD, "Latin Small Letter Y With Acute"},
{0xFE, "Latin Small Letter Thorn"}, {0x00FE, "Latin Small Letter Thorn"},
{0xFF, "Latin Small Letter Y With Diaeresis"}, {0x00FF, "Latin Small Letter Y With Diaeresis"},
{0x9A, "Latin Small Letter S With Caron"}, {0x0161, "Latin Small Letter S With Caron"},
{0x9C, "Latin Small Ligature OE"}, {0x0153, "Latin Small Ligature OE"},
{0x9E, "Latin Small Letter Z With Caron"}, {0x017E, "Latin Small Letter Z With Caron"},
{0x83, "Latin Small Letter F With Hook"}, {0x0192, "Latin Small Letter F With Hook"},
}; };
// Typographic punctuation. // Typographic punctuation.
static const Glyph PUNCT[] = { static const Glyph PUNCT[] = {
{0xA1, "Inverted Exclamation Mark"}, {0x00A1, "Inverted Exclamation Mark"},
{0xBF, "Inverted Question Mark"}, {0x00BF, "Inverted Question Mark"},
{0x91, "Left Single Quotation Mark"}, {0x2018, "Left Single Quotation Mark"},
{0x92, "Right Single Quotation Mark"}, {0x2019, "Right Single Quotation Mark"},
{0x93, "Left Double Quotation Mark"}, {0x201C, "Left Double Quotation Mark"},
{0x94, "Right Double Quotation Mark"}, {0x201D, "Right Double Quotation Mark"},
{0x82, "Single Low-9 Quotation Mark"}, {0x201A, "Single Low-9 Quotation Mark"},
{0x84, "Double Low-9 Quotation Mark"}, {0x201E, "Double Low-9 Quotation Mark"},
{0xAB, "Left-Pointing Double Angle Quotation Mark"}, {0x00AB, "Left-Pointing Double Angle Quotation Mark"},
{0xBB, "Right-Pointing Double Angle Quotation Mark"}, {0x00BB, "Right-Pointing Double Angle Quotation Mark"},
{0x8B, "Single Left-Pointing Angle Quotation Mark"}, {0x2039, "Single Left-Pointing Angle Quotation Mark"},
{0x9B, "Single Right-Pointing Angle Quotation Mark"}, {0x203A, "Single Right-Pointing Angle Quotation Mark"},
{0x96, "En Dash"}, {0x2013, "En Dash"},
{0x97, "Em Dash"}, {0x2014, "Em Dash"},
{0x85, "Horizontal Ellipsis"}, {0x2026, "Horizontal Ellipsis"},
{0x95, "Bullet"}, {0x2022, "Bullet"},
{0xB7, "Middle Dot"}, {0x00B7, "Middle Dot"},
{0x86, "Dagger"}, {0x2020, "Dagger"},
{0x87, "Double Dagger"}, {0x2021, "Double Dagger"},
{0xB6, "Pilcrow Sign"}, {0x00B6, "Pilcrow Sign"},
{0xA7, "Section Sign"}, {0x00A7, "Section Sign"},
{0x89, "Per Mille Sign"}, {0x2030, "Per Mille Sign"},
}; };
// Currency symbols. // Currency symbols.
static const Glyph CURRENCY[] = { static const Glyph CURRENCY[] = {
{0x24, "Dollar Sign"}, {0x0024, "Dollar Sign"},
{0xA2, "Cent Sign"}, {0x00A2, "Cent Sign"},
{0xA3, "Pound Sign"}, {0x00A3, "Pound Sign"},
{0xA5, "Yen Sign"}, {0x00A5, "Yen Sign"},
{0x80, "Euro Sign"}, {0x20AC, "Euro Sign"},
{0xA4, "Currency Sign"}, {0x00A4, "Currency Sign"},
{0x83, "Latin Small Letter F With Hook (Florin)"}, {0x0192, "Latin Small Letter F With Hook (Florin)"},
}; };
// Math, technical, and miscellaneous symbols. // Math, technical, and miscellaneous symbols.
static const Glyph SYMBOLS[] = { static const Glyph SYMBOLS[] = {
{0xA9, "Copyright Sign"}, {0x00A9, "Copyright Sign"},
{0xAE, "Registered Sign"}, {0x00AE, "Registered Sign"},
{0x99, "Trade Mark Sign"}, {0x2122, "Trade Mark Sign"},
{0xB0, "Degree Sign"}, {0x00B0, "Degree Sign"},
{0xB1, "Plus-Minus Sign"}, {0x00B1, "Plus-Minus Sign"},
{0xD7, "Multiplication Sign"}, {0x00D7, "Multiplication Sign"},
{0xF7, "Division Sign"}, {0x00F7, "Division Sign"},
{0xB5, "Micro Sign"}, {0x00B5, "Micro Sign"},
{0xAC, "Not Sign"}, {0x00AC, "Not Sign"},
{0xBC, "Vulgar Fraction One Quarter"}, {0x00BC, "Vulgar Fraction One Quarter"},
{0xBD, "Vulgar Fraction One Half"}, {0x00BD, "Vulgar Fraction One Half"},
{0xBE, "Vulgar Fraction Three Quarters"}, {0x00BE, "Vulgar Fraction Three Quarters"},
{0xB9, "Superscript One"}, {0x00B9, "Superscript One"},
{0xB2, "Superscript Two"}, {0x00B2, "Superscript Two"},
{0xB3, "Superscript Three"}, {0x00B3, "Superscript Three"},
{0xAA, "Feminine Ordinal Indicator"}, {0x00AA, "Feminine Ordinal Indicator"},
{0xBA, "Masculine Ordinal Indicator"}, {0x00BA, "Masculine Ordinal Indicator"},
{0xA6, "Broken Bar"}, {0x00A6, "Broken Bar"},
{0xB4, "Acute Accent"}, {0x00B4, "Acute Accent"},
{0xA8, "Diaeresis"}, {0x00A8, "Diaeresis"},
{0xAF, "Macron"}, {0x00AF, "Macron"},
{0xB8, "Cedilla"}, {0x00B8, "Cedilla"},
{0x88, "Modifier Letter Circumflex Accent"}, {0x02C6, "Modifier Letter Circumflex Accent"},
{0x98, "Small Tilde"}, {0x02DC, "Small Tilde"},
};
// Beyond the old single-byte range: Greek (Roboto covers the whole block).
static const Glyph GREEK[] = {
{0x0391, "Greek Capital Letter Alpha"},
{0x0392, "Greek Capital Letter Beta"},
{0x0393, "Greek Capital Letter Gamma"},
{0x0394, "Greek Capital Letter Delta"},
{0x0395, "Greek Capital Letter Epsilon"},
{0x0396, "Greek Capital Letter Zeta"},
{0x0397, "Greek Capital Letter Eta"},
{0x0398, "Greek Capital Letter Theta"},
{0x0399, "Greek Capital Letter Iota"},
{0x039A, "Greek Capital Letter Kappa"},
{0x039B, "Greek Capital Letter Lamda"},
{0x039C, "Greek Capital Letter Mu"},
{0x039D, "Greek Capital Letter Nu"},
{0x039E, "Greek Capital Letter Xi"},
{0x039F, "Greek Capital Letter Omicron"},
{0x03A0, "Greek Capital Letter Pi"},
{0x03A1, "Greek Capital Letter Rho"},
{0x03A3, "Greek Capital Letter Sigma"},
{0x03A4, "Greek Capital Letter Tau"},
{0x03A5, "Greek Capital Letter Upsilon"},
{0x03A6, "Greek Capital Letter Phi"},
{0x03A7, "Greek Capital Letter Chi"},
{0x03A8, "Greek Capital Letter Psi"},
{0x03A9, "Greek Capital Letter Omega"},
{0x03B1, "Greek Small Letter Alpha"},
{0x03B2, "Greek Small Letter Beta"},
{0x03B3, "Greek Small Letter Gamma"},
{0x03B4, "Greek Small Letter Delta"},
{0x03B5, "Greek Small Letter Epsilon"},
{0x03B6, "Greek Small Letter Zeta"},
{0x03B7, "Greek Small Letter Eta"},
{0x03B8, "Greek Small Letter Theta"},
{0x03B9, "Greek Small Letter Iota"},
{0x03BA, "Greek Small Letter Kappa"},
{0x03BB, "Greek Small Letter Lamda"},
{0x03BC, "Greek Small Letter Mu"},
{0x03BD, "Greek Small Letter Nu"},
{0x03BE, "Greek Small Letter Xi"},
{0x03BF, "Greek Small Letter Omicron"},
{0x03C0, "Greek Small Letter Pi"},
{0x03C1, "Greek Small Letter Rho"},
{0x03C2, "Greek Small Letter Final Sigma"},
{0x03C3, "Greek Small Letter Sigma"},
{0x03C4, "Greek Small Letter Tau"},
{0x03C5, "Greek Small Letter Upsilon"},
{0x03C6, "Greek Small Letter Phi"},
{0x03C7, "Greek Small Letter Chi"},
{0x03C8, "Greek Small Letter Psi"},
{0x03C9, "Greek Small Letter Omega"},
}; };
#define SET(arr, label) { label, arr, (int)(sizeof(arr) / sizeof(arr[0])) } #define SET(arr, label) { label, arr, (int)(sizeof(arr) / sizeof(arr[0])) }
@@ -192,6 +241,7 @@ static const CharSet SETS[] = {
SET(PUNCT, "Punctuation"), SET(PUNCT, "Punctuation"),
SET(CURRENCY, "Currency"), SET(CURRENCY, "Currency"),
SET(SYMBOLS, "Symbols"), SET(SYMBOLS, "Symbols"),
SET(GREEK, "Greek"),
}; };
static constexpr int SET_COUNT = (int)(sizeof(SETS) / sizeof(SETS[0])); static constexpr int SET_COUNT = (int)(sizeof(SETS) / sizeof(SETS[0]));
@@ -319,29 +369,26 @@ static void ensure_visible(const Layout& L) {
// ==== Helpers ==== // ==== Helpers ====
static void glyph_str(uint8_t byte, char out[2]) { static int glyph_str(uint32_t cp, char out[5]) {
out[0] = (char)byte; int n = montauk::utf8::encode(cp, out);
out[1] = '\0'; out[n] = '\0';
} return n;
static int codepoint_of(uint8_t byte) {
return decode_single_byte_codepoint((int)byte);
} }
static void copy_selected() { static void copy_selected() {
const Glyph& g = cur_set().glyphs[g_sel]; const Glyph& g = cur_set().glyphs[g_sel];
char s[2]; char s[5];
glyph_str(g.byte, s); int n = glyph_str(g.cp, s);
gui::clipboard_set_text(s, 1); gui::clipboard_set_text(s, n);
g_copied_until = montauk::get_milliseconds() + COPIED_MS; g_copied_until = montauk::get_milliseconds() + COPIED_MS;
} }
// ==== Rendering ==== // ==== Rendering ====
static void draw_centered_glyph(Canvas& c, const Rect& r, uint8_t byte, static void draw_centered_glyph(Canvas& c, const Rect& r, uint32_t cp,
Color color, int size) { Color color, int size) {
char s[2]; char s[5];
glyph_str(byte, s); glyph_str(cp, s);
int tw = text_width(fonts::system_font, s, size); int tw = text_width(fonts::system_font, s, size);
int th = text_height(fonts::system_font, size); int th = text_height(fonts::system_font, size);
draw_text(c, fonts::system_font, r.x + (r.w - tw) / 2, draw_text(c, fonts::system_font, r.x + (r.w - tw) / 2,
@@ -376,7 +423,7 @@ static void render(Canvas& c) {
c.fill_rounded_rect(r.x, r.y, r.w, r.h, g_theme.radius_md, border); c.fill_rounded_rect(r.x, r.y, r.w, r.h, g_theme.radius_md, border);
c.fill_rounded_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2, c.fill_rounded_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2,
g_theme.radius_md > 0 ? g_theme.radius_md - 1 : 0, fill); g_theme.radius_md > 0 ? g_theme.radius_md - 1 : 0, fill);
draw_centered_glyph(c, r, set.glyphs[i].byte, glyph, CELL_SIZE); draw_centered_glyph(c, r, set.glyphs[i].cp, glyph, CELL_SIZE);
} }
// ---- Scrollbar (shared MTK widget; no-ops when the set fits) ---- // ---- Scrollbar (shared MTK widget; no-ops when the set fits) ----
@@ -407,14 +454,21 @@ static void render(Canvas& c) {
g_theme.radius_md, g_theme.border); g_theme.radius_md, g_theme.border);
c.fill_rounded_rect(preview.x + 1, preview.y + 1, preview.w - 2, preview.h - 2, c.fill_rounded_rect(preview.x + 1, preview.y + 1, preview.w - 2, preview.h - 2,
g_theme.radius_md - 1, colors::WHITE); g_theme.radius_md - 1, colors::WHITE);
draw_centered_glyph(c, preview, sg.byte, g_theme.text, PREVIEW_SIZE); draw_centered_glyph(c, preview, sg.cp, g_theme.text, PREVIEW_SIZE);
// Name + metadata, as a two-line block vertically centered on the preview. // Name + metadata, as a two-line block vertically centered on the preview.
int cp = codepoint_of(sg.byte); int cp = (int)sg.cp;
char enc[5];
int enc_len = glyph_str(sg.cp, enc);
char utf8_hex[16];
int hp = 0;
for (int i = 0; i < enc_len; i++)
hp += snprintf(utf8_hex + hp, sizeof(utf8_hex) - hp, i ? " %02X" : "%02X",
(unsigned)(uint8_t)enc[i]);
int text_x = preview.x + preview.w + 14; int text_x = preview.x + preview.w + 14;
char meta[64]; char meta[64];
snprintf(meta, sizeof(meta), "U+%04X dec %d Alt 0%d", snprintf(meta, sizeof(meta), "U+%04X dec %d UTF-8 %s",
cp, cp, (int)sg.byte); cp, cp, utf8_hex);
int name_h = text_height(title_font, NAME_SIZE); int name_h = text_height(title_font, NAME_SIZE);
int meta_h = text_height(fonts::system_font, META_SIZE); int meta_h = text_height(fonts::system_font, META_SIZE);
@@ -527,7 +581,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key, int w, int h) {
const int count = cur_set().count; const int count = cur_set().count;
bool redraw = false; bool redraw = false;
switch (key.scancode) { switch (montauk::nav_scancode(key)) {
case 0x4B: // Left case 0x4B: // Left
if (g_sel > 0) { g_sel--; ensure_visible(L); redraw = true; } if (g_sel > 0) { g_sel--; ensure_visible(L); redraw = true; }
break; break;
+1 -1
View File
@@ -4,7 +4,7 @@ binary = "charmap.elf"
icon = "accessories-character-map.svg" icon = "accessories-character-map.svg"
[menu] [menu]
category = "Applications" category = "Utilities"
visible = true visible = true
[launch] [launch]
+2 -2
View File
@@ -306,12 +306,12 @@ extern "C" void _start() {
if (ev.type == 0 && ev.key.pressed) { if (ev.type == 0 && ev.key.pressed) {
if (ev.key.scancode == 0x01) break; // Escape if (ev.key.scancode == 0x01) break; // Escape
if (ev.key.scancode == 0x51) { if (montauk::nav_scancode(ev.key) == 0x51) {
// Page Down: scroll down // Page Down: scroll down
g_scroll += 60; g_scroll += 60;
render(); render();
} }
if (ev.key.scancode == 0x49) { if (montauk::nav_scancode(ev.key) == 0x49) {
// Page Up: scroll up // Page Up: scroll up
g_scroll -= 60; g_scroll -= 60;
if (g_scroll < 0) g_scroll = 0; if (g_scroll < 0) g_scroll = 0;
@@ -351,36 +351,21 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
} else if (key.scancode == 0x01) { } else if (key.scancode == 0x01) {
filemanager_cancel_pathbar(fm); filemanager_cancel_pathbar(fm);
} else if (key.ascii == '\b' || key.scancode == 0x0E) { } else if (key.ascii == '\b' || key.scancode == 0x0E) {
if (fm->pathbar_cursor > 0) { montauk::utf8::erase_before(fm->pathbar_buf, &fm->pathbar_len, &fm->pathbar_cursor);
for (int i = fm->pathbar_cursor - 1; i < fm->pathbar_len - 1; i++) } else if (montauk::nav_scancode(key) == 0x53) {
fm->pathbar_buf[i] = fm->pathbar_buf[i + 1]; montauk::utf8::erase_at(fm->pathbar_buf, &fm->pathbar_len, fm->pathbar_cursor);
fm->pathbar_len--; } else if (montauk::nav_scancode(key) == 0x4B) {
fm->pathbar_cursor--; fm->pathbar_cursor = montauk::utf8::prev_boundary(fm->pathbar_buf, fm->pathbar_cursor);
fm->pathbar_buf[fm->pathbar_len] = '\0'; } else if (montauk::nav_scancode(key) == 0x4D) {
} fm->pathbar_cursor = montauk::utf8::next_boundary(fm->pathbar_buf, fm->pathbar_len,
} else if (key.scancode == 0x53) { fm->pathbar_cursor);
if (fm->pathbar_cursor < fm->pathbar_len) { } else if (montauk::nav_scancode(key) == 0x47) {
for (int i = fm->pathbar_cursor; i < fm->pathbar_len - 1; i++)
fm->pathbar_buf[i] = fm->pathbar_buf[i + 1];
fm->pathbar_len--;
fm->pathbar_buf[fm->pathbar_len] = '\0';
}
} else if (key.scancode == 0x4B) {
if (fm->pathbar_cursor > 0) fm->pathbar_cursor--;
} else if (key.scancode == 0x4D) {
if (fm->pathbar_cursor < fm->pathbar_len) fm->pathbar_cursor++;
} else if (key.scancode == 0x47) {
fm->pathbar_cursor = 0; fm->pathbar_cursor = 0;
} else if (key.scancode == 0x4F) { } else if (montauk::nav_scancode(key) == 0x4F) {
fm->pathbar_cursor = fm->pathbar_len; fm->pathbar_cursor = fm->pathbar_len;
} else if ((unsigned char)key.ascii >= 32 } else {
&& (unsigned char)key.ascii != 127 && fm->pathbar_len < 254) { montauk::utf8::insert_key(fm->pathbar_buf, &fm->pathbar_len, 255,
for (int i = fm->pathbar_len; i > fm->pathbar_cursor; i--) &fm->pathbar_cursor, key);
fm->pathbar_buf[i] = fm->pathbar_buf[i - 1];
fm->pathbar_buf[fm->pathbar_cursor] = key.ascii;
fm->pathbar_cursor++;
fm->pathbar_len++;
fm->pathbar_buf[fm->pathbar_len] = '\0';
} }
return; return;
} }
@@ -394,44 +379,27 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
filemanager_cancel_rename(fm); filemanager_cancel_rename(fm);
} else if (key.ascii == '\b' || key.scancode == 0x0E) { } else if (key.ascii == '\b' || key.scancode == 0x0E) {
// Backspace // Backspace
if (fm->rename_cursor > 0) { montauk::utf8::erase_before(fm->rename_buf, &fm->rename_len, &fm->rename_cursor);
for (int i = fm->rename_cursor - 1; i < fm->rename_len - 1; i++) } else if (montauk::nav_scancode(key) == 0x53) {
fm->rename_buf[i] = fm->rename_buf[i + 1];
fm->rename_len--;
fm->rename_cursor--;
fm->rename_buf[fm->rename_len] = '\0';
}
} else if (key.scancode == 0x53) {
// Delete key // Delete key
if (fm->rename_cursor < fm->rename_len) { montauk::utf8::erase_at(fm->rename_buf, &fm->rename_len, fm->rename_cursor);
for (int i = fm->rename_cursor; i < fm->rename_len - 1; i++) } else if (montauk::nav_scancode(key) == 0x4B) {
fm->rename_buf[i] = fm->rename_buf[i + 1];
fm->rename_len--;
fm->rename_buf[fm->rename_len] = '\0';
}
} else if (key.scancode == 0x4B) {
// Left arrow // Left arrow
if (fm->rename_cursor > 0) fm->rename_cursor--; fm->rename_cursor = montauk::utf8::prev_boundary(fm->rename_buf, fm->rename_cursor);
} else if (key.scancode == 0x4D) { } else if (montauk::nav_scancode(key) == 0x4D) {
// Right arrow // Right arrow
if (fm->rename_cursor < fm->rename_len) fm->rename_cursor++; fm->rename_cursor = montauk::utf8::next_boundary(fm->rename_buf, fm->rename_len,
} else if (key.scancode == 0x47) { fm->rename_cursor);
} else if (montauk::nav_scancode(key) == 0x47) {
// Home // Home
fm->rename_cursor = 0; fm->rename_cursor = 0;
} else if (key.scancode == 0x4F) { } else if (montauk::nav_scancode(key) == 0x4F) {
// End // End
fm->rename_cursor = fm->rename_len; fm->rename_cursor = fm->rename_len;
} else if ((unsigned char)key.ascii >= 32 } else if (key.ascii != '/' && key.ascii != '\\') {
&& (unsigned char)key.ascii != 127) {
// Printable character (reject / and other FS-unsafe chars) // Printable character (reject / and other FS-unsafe chars)
if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) { montauk::utf8::insert_key(fm->rename_buf, &fm->rename_len, 63,
for (int i = fm->rename_len; i > fm->rename_cursor; i--) &fm->rename_cursor, key);
fm->rename_buf[i] = fm->rename_buf[i - 1];
fm->rename_buf[fm->rename_cursor] = key.ascii;
fm->rename_cursor++;
fm->rename_len++;
fm->rename_buf[fm->rename_len] = '\0';
}
} }
return; return;
} }
@@ -466,7 +434,7 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
if (key.ascii == '\b' || key.scancode == 0x0E) { if (key.ascii == '\b' || key.scancode == 0x0E) {
filemanager_go_up(fm); filemanager_go_up(fm);
} else if (key.scancode == 0x48) { } else if (montauk::nav_scancode(key) == 0x48) {
// Up arrow // Up arrow
if (fm->grid_view) { if (fm->grid_view) {
Rect cr_r = win->content_rect(); Rect cr_r = win->content_rect();
@@ -477,7 +445,7 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
if (fm->selected > 0) fm->selected--; if (fm->selected > 0) fm->selected--;
} }
filemanager_scroll_to_selected(win, fm); filemanager_scroll_to_selected(win, fm);
} else if (key.scancode == 0x50) { } else if (montauk::nav_scancode(key) == 0x50) {
// Down arrow // Down arrow
if (fm->grid_view) { if (fm->grid_view) {
Rect cr_r = win->content_rect(); Rect cr_r = win->content_rect();
@@ -488,22 +456,22 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
if (fm->selected < fm->entry_count - 1) fm->selected++; if (fm->selected < fm->entry_count - 1) fm->selected++;
} }
filemanager_scroll_to_selected(win, fm); filemanager_scroll_to_selected(win, fm);
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) { } else if (montauk::nav_scancode(key) == 0x4B && !key.alt && fm->grid_view) {
// Left arrow (grid view only) // Left arrow (grid view only)
if (fm->selected > 0) fm->selected--; if (fm->selected > 0) fm->selected--;
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) { } else if (montauk::nav_scancode(key) == 0x4D && !key.alt && fm->grid_view) {
// Right arrow (grid view only) // Right arrow (grid view only)
if (fm->selected < fm->entry_count - 1) fm->selected++; if (fm->selected < fm->entry_count - 1) fm->selected++;
} else if (key.ascii == '\n' || key.ascii == '\r') { } else if (key.ascii == '\n' || key.ascii == '\r') {
if (fm->selected >= 0 && fm->selected < fm->entry_count) if (fm->selected >= 0 && fm->selected < fm->entry_count)
filemanager_open_entry(fm, fm->selected); filemanager_open_entry(fm, fm->selected);
} else if (key.scancode == 0x53) { } else if (montauk::nav_scancode(key) == 0x53) {
// Delete key // Delete key
filemanager_delete_selected(fm); filemanager_delete_selected(fm);
} else if (key.alt && key.scancode == 0x4B) { } else if (key.alt && montauk::nav_scancode(key) == 0x4B) {
// Alt+Left: go back // Alt+Left: go back
filemanager_go_back(fm); filemanager_go_back(fm);
} else if (key.alt && key.scancode == 0x4D) { } else if (key.alt && montauk::nav_scancode(key) == 0x4D) {
// Alt+Right: go forward // Alt+Right: go forward
filemanager_go_forward(fm); filemanager_go_forward(fm);
} }
@@ -234,10 +234,7 @@ void filemanager_read_system_configuration(FileManagerState* fm);
// Map a raw syscall result (kernel StatusCodes are negative) to a userspace // Map a raw syscall result (kernel StatusCodes are negative) to a userspace
// montauk::StatusCodes. Non-negative results (handles, counts) are Success. // montauk::StatusCodes. Non-negative results (handles, counts) are Success.
inline montauk::StatusCode filemanager_status_from_result(int rc) { inline montauk::StatusCode filemanager_status_from_result(int rc) {
if (rc >= 0) return montauk::Success; return montauk::StatusFromResult(rc);
int code = -rc;
if (code > montauk::OutOfMemory) return montauk::GenericFailure;
return (montauk::StatusCode)code;
} }
// The path itself may not be modified. Falling back to copy + delete after a // The path itself may not be modified. Falling back to copy + delete after a
@@ -65,10 +65,10 @@ static void props_fit_text(const char* src, char* out, int out_len, int max_w) {
while (len > 0) { while (len > 0) {
out[len] = '\0'; out[len] = '\0';
if (text_width(out) + ellipsis_w <= max_w) break; if (text_width(out) + ellipsis_w <= max_w) break;
len--; len = montauk::utf8::prev_boundary(out, len);
} }
if (len > out_len - 4) len = out_len - 4; if (len > out_len - 4) len = montauk::utf8::floor_boundary(out, len, out_len - 4);
if (len < 0) len = 0; if (len < 0) len = 0;
out[len] = '.'; out[len] = '.';
out[len + 1] = '.'; out[len + 1] = '.';
@@ -32,12 +32,14 @@ static int fitted_prefix_len(const char* src, int max_w, int max_len) {
char tmp[128]; char tmp[128];
int best = 0; int best = 0;
int limit = max_len < 127 ? max_len : 127; int limit = max_len < 127 ? max_len : 127;
for (int len = 1; len <= limit; len++) { for (int len = montauk::utf8::next_boundary(src, max_len, 0); len <= limit;
len = montauk::utf8::next_boundary(src, max_len, len)) {
copy_label_range(tmp, sizeof(tmp), src, 0, len); copy_label_range(tmp, sizeof(tmp), src, 0, len);
if (text_width(tmp) > max_w) break; if (text_width(tmp) > max_w) break;
best = len; best = len;
if (len >= max_len) break;
} }
return best > 0 ? best : 1; return best > 0 ? best : montauk::utf8::next_boundary(src, max_len, 0);
} }
static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int max_w) { static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int max_w) {
@@ -49,10 +51,10 @@ static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int
while (len > 0) { while (len > 0) {
dst[len] = '\0'; dst[len] = '\0';
if (text_width(dst) + ellipsis_w <= max_w) break; if (text_width(dst) + ellipsis_w <= max_w) break;
len--; len = montauk::utf8::prev_boundary(dst, len);
} }
if (len > dst_max - 4) len = dst_max - 4; if (len > dst_max - 4) len = montauk::utf8::floor_boundary(dst, len, dst_max - 4);
if (len < 0) len = 0; if (len < 0) len = 0;
dst[len] = '.'; dst[len] = '.';
dst[len + 1] = '.'; dst[len + 1] = '.';
@@ -403,7 +405,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
// Draw rename text (truncated to cell width) // Draw rename text (truncated to cell width)
char display[16]; char display[16];
int rlen = fm->rename_len; int rlen = fm->rename_len;
if (rlen > 10) rlen = 10; if (rlen > 10) rlen = montauk::utf8::floor_boundary(fm->rename_buf, fm->rename_len, 10);
for (int k = 0; k < rlen; k++) display[k] = fm->rename_buf[k]; for (int k = 0; k < rlen; k++) display[k] = fm->rename_buf[k];
display[rlen] = '\0'; display[rlen] = '\0';
c.text(cell_x + 4, ty, display, colors::TEXT_COLOR); c.text(cell_x + 4, ty, display, colors::TEXT_COLOR);
@@ -557,18 +559,19 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
int name_max_w = size_col_x - tx - 8; int name_max_w = size_col_x - tx - 8;
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8; if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
char label[64]; char label[64];
montauk::strncpy(label, name, 63); montauk::utf8::copy(label, 64, name);
if (text_width(label) > name_max_w && montauk::slen(name) > 3) { if (text_width(label) > name_max_w && montauk::slen(name) > 3) {
int ellipsis_w = text_width("..."); int ellipsis_w = text_width("...");
int final_len = montauk::slen(name); int final_len = montauk::slen(label);
while (final_len > 1) { while (final_len > 1) {
label[final_len] = '\0'; label[final_len] = '\0';
int w = text_width(label); int w = text_width(label);
if (w + ellipsis_w <= name_max_w) break; if (w + ellipsis_w <= name_max_w) break;
final_len--; final_len = montauk::utf8::prev_boundary(label, final_len);
} }
if (final_len > 60) final_len = 60; if (final_len > 60)
final_len = montauk::utf8::floor_boundary(label, final_len, 60);
label[final_len] = '.'; label[final_len] = '.';
label[final_len + 1] = '.'; label[final_len + 1] = '.';
label[final_len + 2] = '.'; label[final_len + 2] = '.';
+1 -1
View File
@@ -44,7 +44,7 @@ void settings_draw_about(Canvas& c, SettingsState* st) {
mtk::draw_separator(c, x, y, c.w - 2 * x, theme); mtk::draw_separator(c, x, y, c.w - 2 * x, theme);
y += 12; y += 12;
c.text(x, y, "Copyright \xa9 2026 Montauk Operating System Project", theme.text_muted); c.text(x, y, "Copyright \u00A9 2025-2026 Montauk Operating System Project", theme.text_muted);
} }
} // namespace settings_app } // namespace settings_app
+1 -1
View File
@@ -103,7 +103,7 @@ void desktop_draw_lock_screen(DesktopState* ds) {
// Masked password text (clipped to field width) // Masked password text (clipped to field width)
char masked[65]; char masked[65];
int len = ds->lock_password_len; int len = montauk::utf8::count(ds->lock_password); // one '*' per character
if (len > 64) len = 64; if (len > 64) len = 64;
int max_text_w = content_w - 10 - 10 - 2; // left pad, right pad, cursor int max_text_w = content_w - 10 - 10 - 2; // left pad, right pad, cursor
// Show only the trailing portion that fits // Show only the trailing portion that fits
+5 -1
View File
@@ -105,7 +105,11 @@ int wrap_text(const char* text, int max_w, char (*lines)[LINE_CHARS], int max_li
char* last = lines[max_lines - 1]; char* last = lines[max_lines - 1];
int len = montauk::slen(last); int len = montauk::slen(last);
if (len > LINE_CHARS - 4) len = LINE_CHARS - 4; if (len > LINE_CHARS - 4) len = LINE_CHARS - 4;
while (len > 0 && text_width(last) + text_width("...") > max_w) last[--len] = '\0'; last[len] = '\0';
while (len > 0 && text_width(last) + text_width("...") > max_w) {
len = montauk::utf8::prev_boundary(last, len);
last[len] = '\0';
}
last[len] = '.'; last[len + 1] = '.'; last[len + 2] = '.'; last[len + 3] = '\0'; last[len] = '.'; last[len + 1] = '.'; last[len + 2] = '.'; last[len + 3] = '\0';
} }
return count; return count;
+13 -23
View File
@@ -48,6 +48,7 @@ static constexpr int MENU_DIV_H = 10;
struct MenuRow { struct MenuRow {
bool is_category; bool is_category;
bool expanded; // category rows only: children shown (dividers always true)
char label[48]; char label[48];
DesktopBuiltinId app_id; // desktop builtin ID, or DESKTOP_BUILTIN_NONE for categories/dividers DesktopBuiltinId app_id; // desktop builtin ID, or DESKTOP_BUILTIN_NONE for categories/dividers
bool external; // true = spawn binary_path bool external; // true = spawn binary_path
@@ -74,23 +75,15 @@ inline MenuRow* menu_alloc_row() {
return &menu_rows[menu_row_count++]; return &menu_rows[menu_row_count++];
} }
// Collapsible category state (categories 0-3 are toggleable; divider category always expanded) // Collapsible categories: each category row carries its own expanded flag,
static constexpr int MENU_NUM_CATS = 5; // so the menu can hold any number of manifest-defined categories. Dividers
inline bool menu_cat_expanded[MENU_NUM_CATS] = { false, false, false, false, true }; // (empty label) are always expanded.
inline int menu_get_cat(int row_idx) {
int cat = -1;
for (int i = 0; i <= row_idx; i++)
if (menu_rows[i].is_category) cat++;
return cat;
}
inline bool menu_row_visible(int row_idx) { inline bool menu_row_visible(int row_idx) {
const MenuRow& row = menu_rows[row_idx]; const MenuRow& row = menu_rows[row_idx];
if (row.is_category) return true; if (row.is_category) return true;
int cat = menu_get_cat(row_idx); for (int i = row_idx - 1; i >= 0; i--)
if (cat < 0 || cat >= MENU_NUM_CATS) return true; if (menu_rows[i].is_category) return menu_rows[i].expanded;
return menu_cat_expanded[cat]; return true;
} }
inline int menu_row_height(const MenuRow& row) { inline int menu_row_height(const MenuRow& row) {
@@ -102,17 +95,11 @@ inline int menu_total_height() {
// Single-pass walk to avoid O(N^2) from menu_row_visible repeatedly // Single-pass walk to avoid O(N^2) from menu_row_visible repeatedly
// counting categories from the start. // counting categories from the start.
int h = 10; // top + bottom padding int h = 10; // top + bottom padding
int cur_cat = -1; bool cat_expanded = true;
for (int i = 0; i < menu_row_count; i++) { for (int i = 0; i < menu_row_count; i++) {
const MenuRow& row = menu_rows[i]; const MenuRow& row = menu_rows[i];
if (row.is_category) cur_cat++; if (row.is_category) cat_expanded = row.expanded;
bool visible = true; if (row.is_category || cat_expanded) h += menu_row_height(row);
if (!row.is_category) {
if (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) {
visible = menu_cat_expanded[cur_cat];
}
}
if (visible) h += menu_row_height(row);
} }
return h; return h;
} }
@@ -126,6 +113,7 @@ inline int menu_add_category(const char* label) {
if (!rp) return -1; if (!rp) return -1;
MenuRow& r = *rp; MenuRow& r = *rp;
r.is_category = true; r.is_category = true;
r.expanded = !label[0]; // named categories start collapsed; dividers stay open
montauk::strncpy(r.label, label, sizeof(r.label)); montauk::strncpy(r.label, label, sizeof(r.label));
r.app_id = DESKTOP_BUILTIN_NONE; r.app_id = DESKTOP_BUILTIN_NONE;
r.external = false; r.external = false;
@@ -140,6 +128,7 @@ inline int menu_add_embedded(const char* label, DesktopBuiltinId app_id, SvgIcon
if (!rp) return -1; if (!rp) return -1;
MenuRow& r = *rp; MenuRow& r = *rp;
r.is_category = false; r.is_category = false;
r.expanded = false;
montauk::strncpy(r.label, label, sizeof(r.label)); montauk::strncpy(r.label, label, sizeof(r.label));
r.app_id = app_id; r.app_id = app_id;
r.external = false; r.external = false;
@@ -154,6 +143,7 @@ inline int menu_add_external(const char* label, const char* binary, SvgIcon* ico
if (!rp) return -1; if (!rp) return -1;
MenuRow& r = *rp; MenuRow& r = *rp;
r.is_category = false; r.is_category = false;
r.expanded = false;
montauk::strncpy(r.label, label, sizeof(r.label)); montauk::strncpy(r.label, label, sizeof(r.label));
r.app_id = DESKTOP_BUILTIN_NONE; r.app_id = DESKTOP_BUILTIN_NONE;
r.external = true; r.external = true;
+9 -19
View File
@@ -98,22 +98,12 @@ static void handle_lock_keyboard(DesktopState* ds, const montauk::abi::KeyEvent&
// Backspace // Backspace
if (key.ascii == '\b' || key.scancode == 0x0E) { if (key.ascii == '\b' || key.scancode == 0x0E) {
if (ds->lock_password_len > 0) { montauk::utf8::erase_last(ds->lock_password, &ds->lock_password_len);
ds->lock_password_len--;
ds->lock_password[ds->lock_password_len] = '\0';
}
return; return;
} }
// Printable characters // Printable characters (stored as UTF-8)
if ((unsigned char)key.ascii >= 0x20 montauk::utf8::append_key(ds->lock_password, &ds->lock_password_len, 64, key);
&& (unsigned char)key.ascii != 0x7F) {
if (ds->lock_password_len < 63) {
ds->lock_password[ds->lock_password_len] = key.ascii;
ds->lock_password_len++;
ds->lock_password[ds->lock_password_len] = '\0';
}
}
} }
static void forward_external_mouse(gui::Window* win, static void forward_external_mouse(gui::Window* win,
@@ -370,16 +360,16 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
if (menu_rect.contains(mx, my)) { if (menu_rect.contains(mx, my)) {
// Walk visible rows to find which one was clicked // Walk visible rows to find which one was clicked
int iy = menu_y + 5; int iy = menu_y + 5;
int cur_cat = -1; bool cat_expanded = true;
for (int i = 0; i < menu_row_count; i++) { for (int i = 0; i < menu_row_count; i++) {
const MenuRow& row = menu_rows[i]; MenuRow& row = menu_rows[i];
if (row.is_category) cur_cat++; if (row.is_category) cat_expanded = row.expanded;
if (!menu_row_visible(i)) continue; else if (!cat_expanded) continue;
int row_h = menu_row_height(row); int row_h = menu_row_height(row);
if (my >= iy && my < iy + row_h) { if (my >= iy && my < iy + row_h) {
if (row.is_category && row.label[0] && cur_cat >= 0 && cur_cat < MENU_NUM_CATS - 1) { if (row.is_category && row.label[0]) {
// Toggle category expand/collapse // Toggle category expand/collapse
menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat]; row.expanded = !row.expanded;
} else if (!row.is_category) { } else if (!row.is_category) {
if (row.external) { if (row.external) {
if (row.launch_with_home) { if (row.launch_with_home) {
+5 -12
View File
@@ -361,7 +361,7 @@ static void launcher_activate_selected(DesktopState* ds) {
} }
static void launcher_update_super_state(DesktopState* ds, const montauk::abi::KeyEvent& key) { static void launcher_update_super_state(DesktopState* ds, const montauk::abi::KeyEvent& key) {
uint8_t sc = key.scancode & 0x7F; uint8_t sc = montauk::nav_scancode(key) & 0x7F;
if (sc == 0x5B) { if (sc == 0x5B) {
ds->super_left_down = key.pressed; ds->super_left_down = key.pressed;
} else if (sc == 0x5C) { } else if (sc == 0x5C) {
@@ -371,7 +371,7 @@ static void launcher_update_super_state(DesktopState* ds, const montauk::abi::Ke
static const char* fit_trailing_text(const char* text, int max_w) { static const char* fit_trailing_text(const char* text, int max_w) {
const char* visible = text; const char* visible = text;
while (*visible && text_width(visible) > max_w) visible++; while (*visible && text_width(visible) > max_w) montauk::utf8::next(&visible);
return visible; return visible;
} }
@@ -420,7 +420,7 @@ void desktop_close_launcher(DesktopState* ds) {
bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key) { bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key) {
launcher_update_super_state(ds, key); launcher_update_super_state(ds, key);
uint8_t sc = key.scancode & 0x7F; uint8_t sc = montauk::nav_scancode(key) & 0x7F;
if (sc == 0x5B || sc == 0x5C) { if (sc == 0x5B || sc == 0x5C) {
return true; return true;
@@ -456,11 +456,8 @@ bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyE
} }
if (key.ascii == '\b' || sc == 0x0E) { if (key.ascii == '\b' || sc == 0x0E) {
if (ds->launcher_query_len > 0) { if (montauk::utf8::erase_last(ds->launcher_query, &ds->launcher_query_len))
ds->launcher_query_len--;
ds->launcher_query[ds->launcher_query_len] = '\0';
desktop_update_launcher_results(ds); desktop_update_launcher_results(ds);
}
return true; return true;
} }
@@ -495,12 +492,8 @@ bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyE
return true; return true;
} }
if ((unsigned char)key.ascii >= 0x20 if (montauk::utf8::append_key(ds->launcher_query, &ds->launcher_query_len, 64, key))
&& (unsigned char)key.ascii != 0x7F && ds->launcher_query_len < 63) {
ds->launcher_query[ds->launcher_query_len++] = key.ascii;
ds->launcher_query[ds->launcher_query_len] = '\0';
desktop_update_launcher_results(ds); desktop_update_launcher_results(ds);
}
return true; return true;
} }
+83 -13
View File
@@ -121,7 +121,11 @@ void desktop_scan_apps(DesktopState* ds) {
const char* name = doc.get_string("app.name", "Unknown"); const char* name = doc.get_string("app.name", "Unknown");
const char* binary = doc.get_string("app.binary", ""); const char* binary = doc.get_string("app.binary", "");
const char* icon_file = doc.get_string("app.icon", ""); const char* icon_file = doc.get_string("app.icon", "");
const char* category = doc.get_string("menu.category", "Applications"); const char* category = doc.get_string("menu.category", "Productivity");
// "Applications" was the old catch-all; keep older manifests out of
// a stray custom category.
if (!category[0] || montauk::streq(category, "Applications"))
category = "Productivity";
bool visible = doc.get_bool("menu.visible", true); bool visible = doc.get_bool("menu.visible", true);
bool launch_with_home = doc.get_bool("launch.pass_home_dir", true); bool launch_with_home = doc.get_bool("launch.pass_home_dir", true);
const char* section_name = doc.get_string("desktop.section", "apps"); const char* section_name = doc.get_string("desktop.section", "apps");
@@ -197,18 +201,55 @@ void desktop_scan_apps(DesktopState* ds) {
// Menu Builder // Menu Builder
// ============================================================================ // ============================================================================
// Category definitions and their ordering // Predefined categories and their ordering. Manifests may name any other
static const char* CATEGORY_NAMES[] = { "Applications", "Internet", "System", "Games" }; // category; those are appended after these, sorted by name.
static constexpr int NUM_CATEGORIES = 4; // Builtins reference these by index (DesktopBuiltinEntry::launcher_category).
static const char* CATEGORY_NAMES[] = {
"Productivity", "Media", "Internet",
"Utilities", "Games", "Hardware", "System",
};
static constexpr int NUM_CATEGORIES = sizeof(CATEGORY_NAMES) / sizeof(CATEGORY_NAMES[0]);
static bool category_eq(const char* a, const char* b) {
for (;; a++, b++) {
char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
if (ca != cb) return false;
if (!ca) return true;
}
}
static int category_cmp(const char* a, const char* b) {
for (;; a++, b++) {
char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
if (ca != cb || !ca) return (int)(unsigned char)ca - (int)(unsigned char)cb;
}
}
static bool menu_item_shown(DesktopState* ds, const DesktopItem* item) {
return item->menu_visible && !(item->admin_only && !ds->is_admin);
}
static void menu_add_manifest_items(DesktopState* ds, const char* category) {
for (int x = 0; x < ds->desktop_item_count; x++) {
DesktopItem* item = &ds->desktop_items[x];
if (!menu_item_shown(ds, item)) continue;
if (category_eq(item->category, category)) {
menu_add_external(item->name, item->binary_path,
&item->icon, item->launch_with_home);
}
}
}
void desktop_build_menu(DesktopState* ds) { void desktop_build_menu(DesktopState* ds) {
menu_row_count = 0; menu_row_count = 0;
int builtin_count = 0; int builtin_count = 0;
const DesktopBuiltinEntry* builtins = desktop_builtin_registry(&builtin_count); const DesktopBuiltinEntry* builtins = desktop_builtin_registry(&builtin_count);
// Build each category // Build each predefined category
for (int cat = 0; cat < NUM_CATEGORIES; cat++) { for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
menu_add_category(CATEGORY_NAMES[cat]); int header = menu_add_category(CATEGORY_NAMES[cat]);
// Add desktop builtins in this category // Add desktop builtins in this category
for (int b = 0; b < builtin_count; b++) { for (int b = 0; b < builtin_count; b++) {
@@ -218,19 +259,48 @@ void desktop_build_menu(DesktopState* ds) {
} }
} }
// Add manifest-backed items in this category. menu_add_manifest_items(ds, CATEGORY_NAMES[cat]);
// Drop the header again if nothing landed under it.
if (header >= 0 && menu_row_count == header + 1) menu_row_count = header;
}
// Custom categories: the first item of each distinct, non-predefined
// category (case-insensitive) represents it; emit them in name order.
const DesktopItem** custom = ds->desktop_item_count > 0
? (const DesktopItem**)montauk::malloc(
(uint64_t)ds->desktop_item_count * sizeof(DesktopItem*))
: nullptr;
int custom_count = 0;
if (custom) {
for (int x = 0; x < ds->desktop_item_count; x++) { for (int x = 0; x < ds->desktop_item_count; x++) {
DesktopItem* item = &ds->desktop_items[x]; const DesktopItem* item = &ds->desktop_items[x];
if (!item->menu_visible || (item->admin_only && !ds->is_admin)) continue; if (!menu_item_shown(ds, item)) continue;
if (montauk::streq(item->category, CATEGORY_NAMES[cat])) {
menu_add_external(item->name, item->binary_path, bool known = false;
&item->icon, item->launch_with_home); for (int c = 0; c < NUM_CATEGORIES && !known; c++)
known = category_eq(item->category, CATEGORY_NAMES[c]);
for (int c = 0; c < custom_count && !known; c++)
known = category_eq(item->category, custom[c]->category);
if (known) continue;
int pos = custom_count;
while (pos > 0 && category_cmp(custom[pos - 1]->category, item->category) > 0) {
custom[pos] = custom[pos - 1];
pos--;
} }
custom[pos] = item;
custom_count++;
} }
for (int c = 0; c < custom_count; c++) {
menu_add_category(custom[c]->category);
menu_add_manifest_items(ds, custom[c]->category);
}
montauk::mfree(custom);
} }
// Divider + always-visible entries // Divider + always-visible entries
menu_add_category(""); // divider (cat 4, always expanded) menu_add_category(""); // divider (always expanded)
for (int b = 0; b < builtin_count; b++) { for (int b = 0; b < builtin_count; b++) {
if (builtins[b].launcher_footer) { if (builtins[b].launcher_footer) {
menu_add_embedded(builtins[b].label, builtins[b].id, menu_add_embedded(builtins[b].label, builtins[b].id,
+4 -4
View File
@@ -271,13 +271,13 @@ void desktop_draw_app_menu(DesktopState* ds) {
int mx = ds->mouse.x; int mx = ds->mouse.x;
int my = ds->mouse.y; int my = ds->mouse.y;
int iy = menu_y + 5; int iy = menu_y + 5;
int cur_cat = -1; bool cat_expanded = true;
for (int i = 0; i < menu_row_count; i++) { for (int i = 0; i < menu_row_count; i++) {
const MenuRow& row = menu_rows[i]; const MenuRow& row = menu_rows[i];
if (row.is_category) cur_cat++; if (row.is_category) cat_expanded = row.expanded;
if (!menu_row_visible(i)) continue; else if (!cat_expanded) continue;
int row_h = menu_row_height(row); int row_h = menu_row_height(row);
@@ -297,7 +297,7 @@ void desktop_draw_app_menu(DesktopState* ds) {
Color::from_rgb(0xE8, 0xE8, 0xE8)); Color::from_rgb(0xE8, 0xE8, 0xE8));
// Arrow indicator: > for collapsed, v for expanded // Arrow indicator: > for collapsed, v for expanded
bool expanded = (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) && menu_cat_expanded[cur_cat]; bool expanded = row.expanded;
int ax = menu_x + 10; int ax = menu_x + 10;
int ay = iy + row_h / 2; int ay = iy + row_h / 2;
Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88); Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88);
+4 -4
View File
@@ -280,7 +280,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
int row_count = build_display_rows(&de, rows); int row_count = build_display_rows(&de, rows);
if (row_count == 0) return true; if (row_count == 0) return true;
if (key.scancode == 0x48) { // Up if (montauk::nav_scancode(key) == 0x48) { // Up
if (de.selected_row <= 0) de.selected_row = 0; if (de.selected_row <= 0) de.selected_row = 0;
else de.selected_row--; else de.selected_row--;
// Scroll up just enough to bring the selected row's top into view. // Scroll up just enough to bring the selected row's top into view.
@@ -289,7 +289,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
sel_top += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H; sel_top += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
if (sel_top < de.scroll_y) if (sel_top < de.scroll_y)
de.scroll_y = sel_top; de.scroll_y = sel_top;
} else if (key.scancode == 0x50) { // Down } else if (montauk::nav_scancode(key) == 0x50) { // Down
if (de.selected_row < row_count - 1) if (de.selected_row < row_count - 1)
de.selected_row++; de.selected_row++;
// Scroll down just enough to bring the selected row's bottom into view. // Scroll down just enough to bring the selected row's bottom into view.
@@ -300,7 +300,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
int list_h = g_win_h - TOOLBAR_H; int list_h = g_win_h - TOOLBAR_H;
if (sel_top + sel_h > de.scroll_y + list_h) if (sel_top + sel_h > de.scroll_y + list_h)
de.scroll_y = sel_top + sel_h - list_h; de.scroll_y = sel_top + sel_h - list_h;
} else if (key.scancode == 0x4B) { // Left — collapse } else if (montauk::nav_scancode(key) == 0x4B) { // Left — collapse
if (de.selected_row >= 0 && de.selected_row < row_count) { if (de.selected_row >= 0 && de.selected_row < row_count) {
int cat = rows[de.selected_row].category; int cat = rows[de.selected_row].category;
if (!de.collapsed[cat]) { if (!de.collapsed[cat]) {
@@ -318,7 +318,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
} }
} }
} }
} else if (key.scancode == 0x4D) { // Right — expand } else if (montauk::nav_scancode(key) == 0x4D) { // Right — expand
if (de.selected_row >= 0 && de.selected_row < row_count) { if (de.selected_row >= 0 && de.selected_row < row_count) {
int cat = rows[de.selected_row].category; int cat = rows[de.selected_row].category;
de.collapsed[cat] = false; de.collapsed[cat] = false;
+1 -1
View File
@@ -4,5 +4,5 @@ binary = "devexplorer.elf"
icon = "hardware.svg" icon = "hardware.svg"
[menu] [menu]
category = "System" category = "Hardware"
visible = true visible = true
+1 -1
View File
@@ -263,7 +263,7 @@ extern "C" void _start() {
} }
if (link == false) { if (link == false) {
montauk::service_log("Unable to locate a network device with an active link.\n"); montauk::service_log("Unable to locate a network device with an active link.");
montauk::exit(montauk::StatusCode::NoDevice); montauk::exit(montauk::StatusCode::NoDevice);
} }
+6 -5
View File
@@ -261,15 +261,16 @@ void fit_text_start(const char* src, char* out, int out_len, int max_px) {
// Grid labels must fit their cell in pixels. Character-count truncation is // Grid labels must fit their cell in pixels. Character-count truncation is
// insufficient for the proportional system font and lets wide names paint // insufficient for the proportional system font and lets wide names paint
// over the adjacent label. // over the adjacent label.
int keep = montauk::slen(src); int slen = montauk::slen(src);
if (keep > out_len - 3) keep = out_len - 3; int keep = slen;
if (keep > out_len - 3) keep = montauk::utf8::floor_boundary(src, slen, out_len - 3);
while (keep > 0) { while (keep > 0) {
for (int i = 0; i < keep; i++) out[i] = src[i]; for (int i = 0; i < keep; i++) out[i] = src[i];
out[keep] = '.'; out[keep] = '.';
out[keep + 1] = '.'; out[keep + 1] = '.';
out[keep + 2] = '\0'; out[keep + 2] = '\0';
if (text_width(out) <= max_px) return; if (text_width(out) <= max_px) return;
keep--; keep = montauk::utf8::prev_boundary(src, keep);
} }
safe_copy(out, out_len, ".."); safe_copy(out, out_len, "..");
@@ -1216,14 +1217,14 @@ void handle_file_key(const montauk::abi::KeyEvent& key) {
return; return;
} }
if (key.scancode == 0x4B || key.scancode == 0x48) { if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x48) {
if (st->selected > 0) st->selected--; if (st->selected > 0) st->selected--;
else if (st->entry_count > 0) st->selected = 0; else if (st->entry_count > 0) st->selected = 0;
file_ensure_selected_visible(st); file_ensure_selected_visible(st);
return; return;
} }
if (key.scancode == 0x4D || key.scancode == 0x50) { if (montauk::nav_scancode(key) == 0x4D || montauk::nav_scancode(key) == 0x50) {
if (st->selected < st->entry_count - 1) st->selected++; if (st->selected < st->entry_count - 1) st->selected++;
else if (st->entry_count > 0) st->selected = st->entry_count - 1; else if (st->entry_count > 0) st->selected = st->entry_count - 1;
file_ensure_selected_visible(st); file_ensure_selected_visible(st);
+2 -1
View File
@@ -143,7 +143,8 @@ void fit_text_end(const char* src, char* out, int out_len, int max_px) {
gui::dialogs::safe_copy(out, out_len, candidate); gui::dialogs::safe_copy(out, out_len, candidate);
return; return;
} }
keep--; // Drop one whole character from the front of the tail
keep = slen - montauk::utf8::next_boundary(src, slen, slen - keep);
} }
gui::dialogs::safe_copy(out, out_len, "..."); gui::dialogs::safe_copy(out, out_len, "...");
+2 -2
View File
@@ -480,13 +480,13 @@ void handle_print_key(const montauk::abi::KeyEvent& key) {
g_print.error[0] = '\0'; g_print.error[0] = '\0';
return; return;
} }
if ((key.scancode == 0x48 || key.scancode == 0x4B) && g_print.printer_count > 0) { if ((montauk::nav_scancode(key) == 0x48 || montauk::nav_scancode(key) == 0x4B) && g_print.printer_count > 0) {
if (g_print.selected_printer > 0) g_print.selected_printer--; if (g_print.selected_printer > 0) g_print.selected_printer--;
else g_print.selected_printer = 0; else g_print.selected_printer = 0;
g_print.error[0] = '\0'; g_print.error[0] = '\0';
return; return;
} }
if ((key.scancode == 0x50 || key.scancode == 0x4D) && g_print.printer_count > 0) { if ((montauk::nav_scancode(key) == 0x50 || montauk::nav_scancode(key) == 0x4D) && g_print.printer_count > 0) {
if (g_print.selected_printer < g_print.printer_count - 1) g_print.selected_printer++; if (g_print.selected_printer < g_print.printer_count - 1) g_print.selected_printer++;
else g_print.selected_printer = g_print.printer_count - 1; else g_print.selected_printer = g_print.printer_count - 1;
g_print.error[0] = '\0'; g_print.error[0] = '\0';
+6 -6
View File
@@ -339,9 +339,9 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
if (dt.fmt_dlg.open) { if (dt.fmt_dlg.open) {
if (key.scancode == 0x01) { // Escape if (key.scancode == 0x01) { // Escape
close_format_dialog(); close_format_dialog();
} else if (key.scancode == 0x48 && dt.fmt_dlg.selected_fs > 0) { } else if (montauk::nav_scancode(key) == 0x48 && dt.fmt_dlg.selected_fs > 0) {
dt.fmt_dlg.selected_fs--; dt.fmt_dlg.selected_fs--;
} else if (key.scancode == 0x50 && dt.fmt_dlg.selected_fs < NUM_FS_TYPES - 1) { } else if (montauk::nav_scancode(key) == 0x50 && dt.fmt_dlg.selected_fs < NUM_FS_TYPES - 1) {
dt.fmt_dlg.selected_fs++; dt.fmt_dlg.selected_fs++;
} else if (key.ascii == '\n' || key.ascii == '\r') { } else if (key.ascii == '\n' || key.ascii == '\r') {
format_dialog_do_format(); format_dialog_do_format();
@@ -354,14 +354,14 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
if (key.scancode == 0x01) { // Escape if (key.scancode == 0x01) { // Escape
return false; // signal quit return false; // signal quit
} else if (key.scancode == 0x48) { // Up } else if (montauk::nav_scancode(key) == 0x48) { // Up
if (dt.selected_part > 0) dt.selected_part--; if (dt.selected_part > 0) dt.selected_part--;
else if (nparts > 0) dt.selected_part = 0; else if (nparts > 0) dt.selected_part = 0;
} else if (key.scancode == 0x50) { // Down } else if (montauk::nav_scancode(key) == 0x50) { // Down
if (dt.selected_part < nparts - 1) dt.selected_part++; if (dt.selected_part < nparts - 1) dt.selected_part++;
} else if (key.scancode == 0x4B) { // Left } else if (montauk::nav_scancode(key) == 0x4B) { // Left
if (dt.selected_disk > 0) { dt.selected_disk--; dt.selected_part = -1; dt.scroll_y = 0; } if (dt.selected_disk > 0) { dt.selected_disk--; dt.selected_part = -1; dt.scroll_y = 0; }
} else if (key.scancode == 0x4D) { // Right } else if (montauk::nav_scancode(key) == 0x4D) { // Right
if (dt.selected_disk < dt.disk_count - 1) { dt.selected_disk++; dt.selected_part = -1; dt.scroll_y = 0; } if (dt.selected_disk < dt.disk_count - 1) { dt.selected_disk++; dt.selected_part = -1; dt.scroll_y = 0; }
} else if (key.ascii == 'n' || key.ascii == 'N') { } else if (key.ascii == 'n' || key.ascii == 'N') {
do_create_partition(); do_create_partition();
+1 -1
View File
@@ -4,5 +4,5 @@ binary = "disks.elf"
icon = "gparted.svg" icon = "gparted.svg"
[menu] [menu]
category = "System" category = "Hardware"
visible = true visible = true
+2
View File
@@ -53,6 +53,8 @@ static void draw_text_fit(Canvas& c, int x, int y,
out++; out++;
} }
out = montauk::utf8::floor_boundary(text, montauk::slen(text), out);
if (out <= 0) return; if (out <= 0) return;
buf[out] = '.'; buf[out] = '.';
buf[out + 1] = '.'; buf[out + 1] = '.';
+2 -1
View File
@@ -252,6 +252,7 @@ static void fit_text(char* out, int outCap, const char* text, int maxWidth) {
if (text_width(out) + text_width(ellipsis) > maxWidth) break; if (text_width(out) + text_width(ellipsis) > maxWidth) break;
len++; len++;
} }
len = montauk::utf8::floor_boundary(text, montauk::slen(text), len);
out[len++] = '.'; out[len++] = '.';
out[len++] = '.'; out[len++] = '.';
out[len++] = '.'; out[len++] = '.';
@@ -500,7 +501,7 @@ static void apply_changes() {
if (g_driver_available && g_selected_mode != g_info.currentMode) { if (g_driver_available && g_selected_mode != g_info.currentMode) {
int result = montauk::display_set_mode(g_selected_mode); int result = montauk::display_set_mode(g_selected_mode);
if (result < 0) { if (result < 0) {
set_status(result == montauk::UnsupportedOp set_status(montauk::StatusFromResult(result) == montauk::UnsupportedOp
? "Mode needs unsupported display-link training" ? "Mode needs unsupported display-link training"
: "Mode change failed; previous mode restored"); : "Mode change failed; previous mode restored");
refresh_state(true); refresh_state(true);
+75 -36
View File
@@ -7,6 +7,7 @@
#include <montauk/syscall.h> #include <montauk/syscall.h>
#include <montauk/heap.h> #include <montauk/heap.h>
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/utf8.h>
using montauk::slen; using montauk::slen;
using montauk::memcpy; using montauk::memcpy;
@@ -166,6 +167,16 @@ static void line_delete_char(Line* ln, int pos) {
ln->data[ln->len] = '\0'; ln->data[ln->len] = '\0';
} }
// Lines are UTF-8. cursorCol is a byte offset kept on a character boundary;
// the screen works in columns, one per character.
static int line_display_col(const Line* ln, int byteCol) {
return montauk::utf8::count(ln->data, byteCol < ln->len ? byteCol : ln->len);
}
static int line_byte_col(const Line* ln, int displayCol) {
return montauk::utf8::offset_of(ln->data, ln->len, displayCol);
}
static void line_append(Line* ln, const char* s, int slen) { static void line_append(Line* ln, const char* s, int slen) {
line_ensure(ln, ln->len + slen); line_ensure(ln, ln->len + slen);
memcpy(ln->data + ln->len, s, slen); memcpy(ln->data + ln->len, s, slen);
@@ -300,6 +311,21 @@ static void load_file(const char* fname) {
} }
montauk::close(handle); montauk::close(handle);
// Pre-UTF-8 files are Windows-1252: convert once so edits never mix
// encodings and saving writes UTF-8.
bool converted = false;
if (buf && size > 0 && !montauk::utf8::valid((const char*)buf, (int)size)) {
int cap = (int)size * 3 + 1;
uint8_t* conv = (uint8_t*)montauk::malloc(cap);
if (conv) {
size = (uint64_t)montauk::utf8::normalize((const char*)buf, (int)size,
(char*)conv, cap);
montauk::mfree(buf);
buf = conv;
converted = true;
}
}
// Parse into lines // Parse into lines
numLines = 0; numLines = 0;
if (buf && size > 0) { if (buf && size > 0) {
@@ -325,7 +351,7 @@ static void load_file(const char* fname) {
line_init(&lines[0]); line_init(&lines[0]);
} }
set_status("File loaded"); set_status(converted ? "File loaded (converted from Windows-1252 to UTF-8)" : "File loaded");
} }
static bool save_file() { static bool save_file() {
@@ -413,14 +439,13 @@ static int prompt_input(const char* promptStr, char* out, int outMax) {
} }
if (ev.ascii == '\b') { if (ev.ascii == '\b') {
if (pos > 0) { if (montauk::utf8::erase_last(out, &pos)) {
pos--;
putch('\b'); putch(' '); putch('\b'); putch('\b'); putch(' '); putch('\b');
} }
} else if (ev.ascii >= ' ' && pos < outMax - 1) { } else {
out[pos++] = ev.ascii; int before = pos;
out[pos] = '\0'; if (montauk::utf8::append_key(out, &pos, outMax, ev))
putch(ev.ascii); print(out + before);
} }
} }
} }
@@ -506,14 +531,15 @@ static void draw_line(int screenRow, int docLine) {
reset_attrs(); reset_attrs();
// Line content // Line content
// The visible window is leftCol..leftCol+maxChars in columns; convert
// it to a byte range so whole UTF-8 characters are emitted.
Line* ln = &lines[docLine]; Line* ln = &lines[docLine];
int startCol = leftCol; int startCol = line_byte_col(ln, leftCol);
int maxChars = screenCols - gutterWidth; int maxChars = screenCols - gutterWidth;
int visibleLen = 0; int endCol = startCol;
if (startCol < ln->len) { for (int n = 0; n < maxChars && endCol < ln->len; n++)
visibleLen = ln->len - startCol; endCol = montauk::utf8::next_boundary(ln->data, ln->len, endCol);
if (visibleLen > maxChars) visibleLen = maxChars; int visibleLen = endCol - startCol;
}
if (synLang != SYN_LANG_NONE) { if (synLang != SYN_LANG_NONE) {
// Tokenize the WHOLE line, not just the visible window: the // Tokenize the WHOLE line, not just the visible window: the
@@ -577,7 +603,7 @@ static void render() {
// Position cursor // Position cursor
int screenY = cursorRow - topLine + 2; // +2 for status bar int screenY = cursorRow - topLine + 2; // +2 for status bar
int screenX = cursorCol - leftCol + gutterWidth; int screenX = line_display_col(&lines[cursorRow], cursorCol) - leftCol + gutterWidth;
cursor_to(screenY, screenX); cursor_to(screenY, screenX);
show_cursor(); show_cursor();
@@ -598,12 +624,13 @@ static void scroll() {
// Horizontal scrolling // Horizontal scrolling
int textCols = screenCols - gutterWidth; int textCols = screenCols - gutterWidth;
if (cursorCol < leftCol) { int displayCol = line_display_col(&lines[cursorRow], cursorCol);
leftCol = cursorCol; if (displayCol < leftCol) {
leftCol = displayCol;
fullRedraw = true; fullRedraw = true;
} }
if (cursorCol >= leftCol + textCols) { if (displayCol >= leftCol + textCols) {
leftCol = cursorCol - textCols + 1; leftCol = displayCol - textCols + 1;
fullRedraw = true; fullRedraw = true;
} }
} }
@@ -617,6 +644,10 @@ static void insert_char(char c) {
fullRedraw = true; fullRedraw = true;
} }
static void insert_bytes(const char* s, int n) {
for (int i = 0; i < n; i++) insert_char(s[i]);
}
static void insert_tab() { static void insert_tab() {
for (int i = 0; i < TAB_WIDTH; i++) { for (int i = 0; i < TAB_WIDTH; i++) {
insert_char(' '); insert_char(' ');
@@ -647,8 +678,11 @@ static void insert_newline() {
static void delete_char_backspace() { static void delete_char_backspace() {
if (cursorCol > 0) { if (cursorCol > 0) {
int start = montauk::utf8::prev_boundary(lines[cursorRow].data, cursorCol);
while (cursorCol > start) {
line_delete_char(&lines[cursorRow], cursorCol - 1); line_delete_char(&lines[cursorRow], cursorCol - 1);
cursorCol--; cursorCol--;
}
modified = true; modified = true;
fullRedraw = true; fullRedraw = true;
} else if (cursorRow > 0) { } else if (cursorRow > 0) {
@@ -665,7 +699,9 @@ static void delete_char_backspace() {
static void delete_char_forward() { static void delete_char_forward() {
if (cursorCol < lines[cursorRow].len) { if (cursorCol < lines[cursorRow].len) {
line_delete_char(&lines[cursorRow], cursorCol); Line* ln = &lines[cursorRow];
int n = montauk::utf8::next_boundary(ln->data, ln->len, cursorCol) - cursorCol;
while (n-- > 0) line_delete_char(ln, cursorCol);
modified = true; modified = true;
fullRedraw = true; fullRedraw = true;
} else if (cursorRow < numLines - 1) { } else if (cursorRow < numLines - 1) {
@@ -793,26 +829,26 @@ static void handle_key(const montauk::abi::KeyEvent& ev) {
// Non-ASCII keys (scancode-based) // Non-ASCII keys (scancode-based)
if (ev.ascii == 0) { if (ev.ascii == 0) {
switch (ev.scancode) { switch (montauk::nav_scancode(ev)) {
case SC_UP: case SC_UP:
if (cursorRow > 0) { if (cursorRow > 0) {
int col = line_display_col(&lines[cursorRow], cursorCol);
cursorRow--; cursorRow--;
if (cursorCol > lines[cursorRow].len) cursorCol = line_byte_col(&lines[cursorRow], col);
cursorCol = lines[cursorRow].len;
fullRedraw = true; fullRedraw = true;
} }
return; return;
case SC_DOWN: case SC_DOWN:
if (cursorRow < numLines - 1) { if (cursorRow < numLines - 1) {
int col = line_display_col(&lines[cursorRow], cursorCol);
cursorRow++; cursorRow++;
if (cursorCol > lines[cursorRow].len) cursorCol = line_byte_col(&lines[cursorRow], col);
cursorCol = lines[cursorRow].len;
fullRedraw = true; fullRedraw = true;
} }
return; return;
case SC_LEFT: case SC_LEFT:
if (cursorCol > 0) { if (cursorCol > 0) {
cursorCol--; cursorCol = montauk::utf8::prev_boundary(lines[cursorRow].data, cursorCol);
} else if (cursorRow > 0) { } else if (cursorRow > 0) {
cursorRow--; cursorRow--;
cursorCol = lines[cursorRow].len; cursorCol = lines[cursorRow].len;
@@ -821,7 +857,8 @@ static void handle_key(const montauk::abi::KeyEvent& ev) {
return; return;
case SC_RIGHT: case SC_RIGHT:
if (cursorCol < lines[cursorRow].len) { if (cursorCol < lines[cursorRow].len) {
cursorCol++; cursorCol = montauk::utf8::next_boundary(lines[cursorRow].data,
lines[cursorRow].len, cursorCol);
} else if (cursorRow < numLines - 1) { } else if (cursorRow < numLines - 1) {
cursorRow++; cursorRow++;
cursorCol = 0; cursorCol = 0;
@@ -836,20 +873,22 @@ static void handle_key(const montauk::abi::KeyEvent& ev) {
cursorCol = lines[cursorRow].len; cursorCol = lines[cursorRow].len;
fullRedraw = true; fullRedraw = true;
return; return;
case SC_PGUP: case SC_PGUP: {
int col = line_display_col(&lines[cursorRow], cursorCol);
cursorRow -= editorRows; cursorRow -= editorRows;
if (cursorRow < 0) cursorRow = 0; if (cursorRow < 0) cursorRow = 0;
if (cursorCol > lines[cursorRow].len) cursorCol = line_byte_col(&lines[cursorRow], col);
cursorCol = lines[cursorRow].len;
fullRedraw = true; fullRedraw = true;
return; return;
case SC_PGDN: }
case SC_PGDN: {
int col = line_display_col(&lines[cursorRow], cursorCol);
cursorRow += editorRows; cursorRow += editorRows;
if (cursorRow >= numLines) cursorRow = numLines - 1; if (cursorRow >= numLines) cursorRow = numLines - 1;
if (cursorCol > lines[cursorRow].len) cursorCol = line_byte_col(&lines[cursorRow], col);
cursorCol = lines[cursorRow].len;
fullRedraw = true; fullRedraw = true;
return; return;
}
case SC_DELETE: case SC_DELETE:
delete_char_forward(); delete_char_forward();
return; return;
@@ -869,12 +908,12 @@ static void handle_key(const montauk::abi::KeyEvent& ev) {
case '\t': case '\t':
insert_tab(); insert_tab();
break; break;
default: default: {
if (ev.ascii >= ' ') { char typed[montauk::utf8::MAX_BYTES];
insert_char(ev.ascii); insert_bytes(typed, montauk::utf8::key_utf8(ev, typed));
}
break; break;
} }
}
} }
// ---- Entry point ---- // ---- Entry point ----
Loaded 100 of 174 files, more files were not shown because too many files have changed in this diff. Show more