diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 9524fa8..dedc3a5 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 289 +#define MONTAUK_BUILD_NUMBER 296 diff --git a/kernel/src/Api/IoRedir.hpp b/kernel/src/Api/IoRedir.hpp index 6974e95..4195b0f 100644 --- a/kernel/src/Api/IoRedir.hpp +++ b/kernel/src/Api/IoRedir.hpp @@ -122,8 +122,9 @@ namespace montauk::abi { return WriteAllToStream(stream, (const uint8_t*)data, len); } - static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) { - if (key == nullptr) return InvalidArgument; + // Queue a key event (raw bytes, `len` of them) on a redirected child's key + // 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); if (child == nullptr || child->parentPid != Sched::GetCurrentPid()) return PermissionDenied; @@ -133,7 +134,7 @@ namespace montauk::abi { for (;;) { 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) { 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, ©, 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) { auto* child = Sched::GetProcessByPid(childPid); if (child == nullptr || child->parentPid != Sched::GetCurrentPid()) diff --git a/kernel/src/Api/Keyboard.hpp b/kernel/src/Api/Keyboard.hpp index 989fbb8..8ad38dc 100644 --- a/kernel/src/Api/Keyboard.hpp +++ b/kernel/src/Api/Keyboard.hpp @@ -12,6 +12,107 @@ #include "Common.hpp" 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() { auto* proc = Sched::GetCurrentProcessPtr(); if (proc && proc->redirected) { @@ -27,8 +128,8 @@ namespace montauk::abi { return Drivers::PS2::Keyboard::IsKeyAvailable(); } - static void Sys_GetKey(KeyEvent* outEvent) { - if (outEvent == nullptr) return; + // Blocking read of the next key event, in the full format. + static void ReadKeyEvent(KeyEvent* outEvent) { auto* proc = Sched::GetCurrentProcessPtr(); if (proc && proc->redirected) { Ipc::HandleSnapshot snapshot; @@ -36,8 +137,7 @@ namespace montauk::abi { if (mailbox != nullptr) { for (;;) { uint64_t observedWake = Sched::ObserveObjectWake(mailbox); - uint16_t len = sizeof(KeyEvent); - int rc = Ipc::MailboxRecv(mailbox, nullptr, outEvent, &len, true); + int rc = KeyMailboxRecv(mailbox, outEvent); if (rc > 0) return; if (rc < 0) { memset(outEvent, 0, sizeof(KeyEvent)); @@ -47,20 +147,33 @@ namespace montauk::abi { } } } - auto k = Drivers::PS2::Keyboard::GetKey(); - outEvent->scancode = k.Scancode; - outEvent->ascii = k.Ascii; - outEvent->pressed = k.Pressed; - outEvent->shift = k.Shift; - outEvent->ctrl = k.Ctrl; - outEvent->alt = k.Alt; - outEvent->capslock = k.CapsLock; - outEvent->altgr = k.AltGr; - outEvent->extended = k.Extended; + KeyFromDriver(Drivers::PS2::Keyboard::GetKey(), outEvent); + } + + // SYS_GETKEY: legacy 9-byte event. A new-format event's first 9 bytes are + // already a valid KeyEventV1 (ascii holds the 1252 byte or SUB). + static void Sys_GetKey(KeyEventV1* outEvent) { + if (outEvent == nullptr) return; + KeyEvent ev; + ReadKeyEvent(&ev); + 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() { auto* proc = Sched::GetCurrentProcessPtr(); + char pending; + if (KeyPendingChar(proc, &pending)) return pending; if (proc && proc->redirected) { Ipc::HandleSnapshot inputSnapshot; Ipc::HandleSnapshot mailboxSnapshot; @@ -78,11 +191,11 @@ namespace montauk::abi { } if (mailbox != nullptr) { - KeyEvent ev{}; - uint16_t len = sizeof(ev); - int rc = Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true); + KeyEvent ev; + int rc = KeyMailboxRecv(mailbox, &ev); if (rc > 0) { - if (ev.pressed && ev.ascii != 0) return ev.ascii; + char c; + if (KeyTakeChar(proc, ev, &c)) return c; continue; } if (rc < 0 && input == nullptr) return 0; @@ -115,6 +228,8 @@ namespace montauk::abi { */ static char Sys_GetCharNb() { auto* proc = Sched::GetCurrentProcessPtr(); + char pending; + if (KeyPendingChar(proc, &pending)) return pending; if (proc && proc->redirected) { Ipc::HandleSnapshot inputSnapshot; Ipc::HandleSnapshot mailboxSnapshot; @@ -127,10 +242,10 @@ namespace montauk::abi { } while (mailbox != nullptr) { - KeyEvent ev{}; - uint16_t len = sizeof(ev); - if (Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true) <= 0) break; - if (ev.pressed && ev.ascii != 0) return ev.ascii; + KeyEvent ev; + if (KeyMailboxRecv(mailbox, &ev) <= 0) break; + char c; + if (KeyTakeChar(proc, ev, &c)) return c; } return 0; } diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index c5433af..581ee2e 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -139,8 +139,8 @@ namespace montauk::abi { case SYS_ISKEYAVAILABLE: return (int64_t)Sys_IsKeyAvailable(); case SYS_GETKEY: - if (!UserMemory::Writable(frame->arg1)) return BadPointer; - Sys_GetKey((KeyEvent*)frame->arg1); + if (!UserMemory::Writable(frame->arg1)) return BadPointer; + Sys_GetKey((KeyEventV1*)frame->arg1); return Success; case 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; return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3); case SYS_CHILDIO_WRITEKEY: - if (!UserMemory::Readable(frame->arg2)) return BadPointer; - return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEvent*)frame->arg2); + if (!UserMemory::Readable(frame->arg2)) return BadPointer; + return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEventV1*)frame->arg2); case SYS_CHILDIO_SETTERMSZ: return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3); case SYS_WINCREATE: @@ -527,6 +527,14 @@ namespace montauk::abi { return Sched::Consent::Next((ConsentRequest*)frame->arg1); case SYS_CONSENT_REPLY: 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: return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3); case SYS_THREAD_EXIT: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 3306415..5cefc5e 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -358,6 +358,12 @@ namespace montauk::abi { 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 + /* 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 + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; @@ -600,7 +606,10 @@ namespace montauk::abi { 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; char ascii; bool pressed; @@ -616,6 +625,32 @@ namespace montauk::abi { 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 { int32_t x; int32_t y; @@ -653,12 +688,13 @@ namespace montauk::abi { uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t _pad[3]; 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 w, h; } resize; struct { int32_t scale; } scale; }; }; + static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries"); struct WinInfo { int32_t id; diff --git a/kernel/src/Drivers/PS2/Keyboard.cpp b/kernel/src/Drivers/PS2/Keyboard.cpp index c17c3c2..64cbe85 100644 --- a/kernel/src/Drivers/PS2/Keyboard.cpp +++ b/kernel/src/Drivers/PS2/Keyboard.cpp @@ -139,6 +139,7 @@ namespace Drivers::PS2::Keyboard { ack = Drivers::PS2::ReadData(); g_Modifiers = {}; + g_Modifiers.NumLock = true; // keypad types digits, as on most PCs g_BufferHead = 0; g_BufferTail = 0; g_ExtendedScancode = false; @@ -257,6 +258,9 @@ namespace Drivers::PS2::Keyboard { 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 = { .Scancode = scancode, @@ -310,6 +314,12 @@ namespace Drivers::PS2::Keyboard { KeyEvent stamped = event; if (stamped.Scancode == ScCapsLock && stamped.Pressed) 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; PushAndNotify(stamped); } @@ -318,4 +328,8 @@ namespace Drivers::PS2::Keyboard { return g_Modifiers.CapsLock; } + bool GetNumLock() { + return g_Modifiers.NumLock; + } + }; diff --git a/kernel/src/Drivers/PS2/Keyboard.hpp b/kernel/src/Drivers/PS2/Keyboard.hpp index b023fed..b05f3bf 100644 --- a/kernel/src/Drivers/PS2/Keyboard.hpp +++ b/kernel/src/Drivers/PS2/Keyboard.hpp @@ -63,4 +63,14 @@ namespace Drivers::PS2::Keyboard { // compute ASCII, since the lock is toggled centrally here. 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; + } + }; diff --git a/kernel/src/Drivers/USB/HidKeyboard.cpp b/kernel/src/Drivers/USB/HidKeyboard.cpp index 1420d85..860ae63 100644 --- a/kernel/src/Drivers/USB/HidKeyboard.cpp +++ b/kernel/src/Drivers/USB/HidKeyboard.cpp @@ -223,6 +223,12 @@ namespace Drivers::USB::HidKeyboard { if (caps && base >= 'a' && base <= 'z') upper = !shift; 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 = { .Scancode = scancode, @@ -367,10 +373,14 @@ namespace Drivers::USB::HidKeyboard { uint8_t scancode = g_HidToScancode[key]; if (scancode != 0) { InjectKey(scancode, true, modifiers, IsNonCharKey(key), IsExtendedKey(key)); - g_RepeatKey = key; - g_RepeatStartMs = now; - g_LastRepeatMs = 0; 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_RepeatStartMs = now; + g_LastRepeatMs = 0; + } } } } diff --git a/kernel/src/Fs/Fat32.cpp b/kernel/src/Fs/Fat32.cpp index d66b565..60d048c 100644 --- a/kernel/src/Fs/Fat32.cpp +++ b/kernel/src/Fs/Fat32.cpp @@ -301,6 +301,53 @@ namespace Fs::Fat32 { // 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) { memset(shortName, ' ', 11); @@ -315,9 +362,15 @@ namespace Fs::Fat32 { // Base name (up to 8 chars, uppercase, skip invalid chars) int baseEnd = (lastDot >= 0) ? lastDot : nameLen; 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++) { char c = longName[i]; if (c == ' ' || c == '.') continue; + if ((uint8_t)c >= 0x80) { + if (((uint8_t)c & 0xC0) == 0x80) continue; + c = '_'; + } if (c >= 'a' && c <= 'z') c -= 32; shortName[j++] = c; } @@ -327,6 +380,10 @@ namespace Fs::Fat32 { j = 0; for (int i = lastDot + 1; longName[i] && j < 3; 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; shortName[8 + j++] = c; } @@ -403,12 +460,22 @@ namespace Fs::Fat32 { // Returns the number of LFN entries. static int BuildLfnEntries(const char* longName, uint8_t checksum, uint8_t* outEntries) { - // Convert to UTF-16 - uint16_t utf16[MaxNameLen]; + // Convert UTF-8 to UTF-16 (LFN holds at most 255 code units) + uint16_t utf16[MaxNameLen + 13]; int len = 0; - while (longName[len] && len < MaxNameLen - 1) { - utf16[len] = (uint8_t)longName[len]; - len++; + for (const char* p = longName; *p;) { + int n; + 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; @@ -464,9 +531,11 @@ namespace Fs::Fat32 { // Compare case-insensitively 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++) { if (longName[i] >= 'a' && longName[i] <= 'z') return true; + if ((uint8_t)longName[i] >= 0x80) return true; } 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); } - 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; - for (int i = 0; i < maxLen && src[i] != 0 && j < MaxNameLen - 1; i++) { - dst[j++] = (src[i] < 128) ? (char)src[i] : '?'; + for (int i = 0; i < maxLen && src[i] != 0 && src[i] != 0xFFFF; 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'; } @@ -685,7 +768,7 @@ namespace Fs::Fat32 { pe.attributes = attr; if (hasLfn) { - Utf16ToAscii(lfnBuf, MaxNameLen, pe.name); + Utf16ToUtf8(lfnBuf, MaxNameLen, pe.name); } else { ParseShortName(e, pe.name); } @@ -752,7 +835,8 @@ namespace Fs::Fat32 { if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; } if (hasLfn) { - Utf16ToAscii(lfnBuf, MaxNameLen, outNames[count]); + // Leave room for the '/' appended to directory names + Utf16ToUtf8(lfnBuf, MaxNameLen, outNames[count], MaxNameLen - 1); } else { ParseShortName(e, outNames[count]); } @@ -848,7 +932,7 @@ namespace Fs::Fat32 { char entryName[MaxNameLen]; if (hasLfn) { - Utf16ToAscii(lfnBuf, MaxNameLen, entryName); + Utf16ToUtf8(lfnBuf, MaxNameLen, entryName); } else { ParseShortName(e, entryName); } diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 2908bba..6e17530 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -324,6 +324,8 @@ namespace Sched { processTable[i].inTail = 0; processTable[i].keyHead = 0; processTable[i].keyTail = 0; + processTable[i].getcharUtf8Len = 0; + processTable[i].getcharUtf8Pos = 0; processTable[i].termCols = 0; processTable[i].termRows = 0; processTable[i].ioOutHandle = -1; @@ -654,6 +656,8 @@ namespace Sched { proc.inTail = 0; proc.keyHead = 0; proc.keyTail = 0; + proc.getcharUtf8Len = 0; + proc.getcharUtf8Pos = 0; proc.termCols = 0; proc.termRows = 0; proc.ioOutHandle = -1; @@ -835,6 +839,7 @@ namespace Sched { thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0; thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0; thr.keyHead = 0; thr.keyTail = 0; + thr.getcharUtf8Len = 0; thr.getcharUtf8Pos = 0; thr.termCols = 0; thr.termRows = 0; thr.ioOutHandle = -1; thr.ioInHandle = -1; thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1; @@ -1700,6 +1705,8 @@ namespace Sched { proc.inTail = 0; proc.keyHead = 0; proc.keyTail = 0; + proc.getcharUtf8Len = 0; + proc.getcharUtf8Pos = 0; proc.termCols = 0; proc.termRows = 0; proc.ioOutHandle = -1; diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index 6259827..f0b7568 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -135,9 +135,13 @@ namespace Sched { uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar) volatile uint32_t inHead = 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 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; // GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE) diff --git a/programs/data/osdata/keyboard-layouts.toml b/programs/data/osdata/keyboard-layouts.toml index 342d815..3b0b5bd 100644 --- a/programs/data/osdata/keyboard-layouts.toml +++ b/programs/data/osdata/keyboard-layouts.toml @@ -15,15 +15,10 @@ # base: character produced unshifted, one per scancode # shift: character produced with Shift held, one per scancode # altgr: character produced with AltGr held, one per scancode -# -# 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. +# dead_keys: (optional) characters that are dead keys in this layout [registry] -layouts = ["en", "no", "sv", "da"] +layouts = ["en", "no", "sv", "da", "cs"] # ============================================================================= # LAYOUTS @@ -54,7 +49,7 @@ shift = [ ] altgr = [ 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 ] @@ -77,8 +72,8 @@ shift = [ 0xBD, 0x2A, 0x3B, 0x3A, 0x5F, 0x3E, # 1/2 * ; : _ > ] altgr = [ - 0x40, 0xA3, 0x24, 0x80, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ EUR YEN { [ ] - 0x7D, 0x5C, 0xB1, 0x80, 0x00, 0x7E, 0xF8, 0xE6, # } bslash +- EUR . ~ o/ ae + 0x40, 0xA3, 0x24, 0x20AC, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ EUR YEN { [ ] + 0x7D, 0x5C, 0xB1, 0x20AC, 0x00, 0x7E, 0xF8, 0xE6, # } bslash +- EUR . ~ o/ ae 0xB6, 0xB4, 0x00, 0xB7, 0x00, 0x7C, # par ' . mid . | ] @@ -102,6 +97,44 @@ shift = [ ] altgr = [ 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 ] + +[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, +] diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index f513f59..a490c84 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -271,6 +271,12 @@ namespace montauk::abi { 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 + /* 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 + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; @@ -531,7 +537,10 @@ namespace montauk::abi { 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; char ascii; bool pressed; @@ -547,6 +556,32 @@ namespace montauk::abi { 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 { int32_t x; int32_t y; @@ -584,12 +619,13 @@ namespace montauk::abi { uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t _pad[3]; 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 w, h; } resize; struct { int32_t scale; } scale; }; }; + static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries"); struct WinInfo { int32_t id; diff --git a/programs/include/gui/canvas.hpp b/programs/include/gui/canvas.hpp index c38c260..6bc125c 100644 --- a/programs/include/gui/canvas.hpp +++ b/programs/include/gui/canvas.hpp @@ -165,9 +165,9 @@ struct Canvas { if (!str || !font_data || scale <= 0) return; uint32_t pixel = c.to_pixel(); 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 = - &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++) { uint8_t bits = glyph[row]; if (!bits) continue; diff --git a/programs/include/gui/clipboard.hpp b/programs/include/gui/clipboard.hpp index 614eaba..0a35535 100644 --- a/programs/include/gui/clipboard.hpp +++ b/programs/include/gui/clipboard.hpp @@ -9,12 +9,32 @@ #include #include #include +#include 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) { if (len < 0) 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; } @@ -53,6 +73,14 @@ namespace gui { } 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_serial) *out_serial = serial; return text; diff --git a/programs/include/gui/font.hpp b/programs/include/gui/font.hpp index 4001c16..8d35da8 100644 --- a/programs/include/gui/font.hpp +++ b/programs/include/gui/font.hpp @@ -19,6 +19,34 @@ static constexpr int FONT_HEIGHT = 16; // TrueType fonts, while login/desktop can recover when TrueType setup fails. 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) inline int system_font_height() { 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); return; } - for (int i = 0; text[i]; i++) { - draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg); + for (const char* p = text; *p; x += FONT_WIDTH) { + 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); return; } - for (int i = 0; text[i]; i++) { - draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg); + for (const char* p = text; *p; x += FONT_WIDTH) { + 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) { if (fonts::system_font && fonts::system_font->valid) { return fonts::system_font->measure_text(text, fonts::UI_SIZE); } - int len = 0; - while (text[len]) len++; - return len * FONT_WIDTH; + return montauk::utf8::count(text) * FONT_WIDTH; } } // namespace gui diff --git a/programs/include/gui/mtk/widgets.hpp b/programs/include/gui/mtk/widgets.hpp index f4ca3b0..07ff5b0 100644 --- a/programs/include/gui/mtk/widgets.hpp +++ b/programs/include/gui/mtk/widgets.hpp @@ -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); } +// 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) { - char prefix[512]; int len = 0; - while (text && text[len] && len < count && len < (int)sizeof(prefix) - 1) { - prefix[len] = masked ? '*' : text[len]; - len++; - } - prefix[len] = '\0'; - return text_width(prefix); + while (text && text[len] && len < count) len++; + if (!masked) return text_width_n(text, len); + return montauk::utf8::count(text, len) * text_width("*"); } 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 prev_w = 0; - for (int i = 0; i < len; i++) { - int cur_w = text_input_prefix_width(value, i + 1, masked); + for (int i = 0; i < len;) { + 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; if (rel_x < midpoint) return i; prev_w = cur_w; + i = next; } return len; } @@ -360,8 +360,11 @@ inline int text_input_mouse(TextInputState& state, 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) { - return (ch >= 'a' && ch <= 'z') || + return (uint8_t)ch >= 0x80 || + (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_'; @@ -574,8 +577,10 @@ inline int text_input_key(TextInputState& state, if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE; int len = text_input_len(text, cap); - state.cursor = text_input_clamp_cursor(state.cursor, len); - state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len); + state.cursor = montauk::utf8::floor_boundary( + 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) { 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); } - if (key.scancode == 0x4B || key.scancode == 0x4D || - key.scancode == 0x47 || key.scancode == 0x4F) { + if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x4D || + montauk::nav_scancode(key) == 0x47 || montauk::nav_scancode(key) == 0x4F) { 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; - 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; - else if (key.scancode == 0x4B) - next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) : state.cursor - 1; - else if (key.scancode == 0x4D) - next = key.ctrl ? text_input_next_word_boundary(text, state.cursor) : state.cursor + 1; - else if (key.scancode == 0x47) + else if (montauk::nav_scancode(key) == 0x4B) + next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) + : montauk::utf8::prev_boundary(text, state.cursor); + else if (montauk::nav_scancode(key) == 0x4D) + 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; - else if (key.scancode == 0x4F) + else if (montauk::nav_scancode(key) == 0x4F) next = len; text_input_set_cursor(state, len, next, key.shift); @@ -641,15 +648,16 @@ inline int text_input_key(TextInputState& state, changed = true; } } else if (state.cursor > 0) { - text_input_delete_range(text, &len, state.cursor - 1, state.cursor); - state.cursor--; + int start = montauk::utf8::prev_boundary(text, state.cursor); + text_input_delete_range(text, &len, start, state.cursor); + state.cursor = start; changed = true; } state.selection_anchor = state.cursor; return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0); } - if (key.scancode == 0x53) { + if (montauk::nav_scancode(key) == 0x53) { if (sel_start != sel_end) { text_input_delete_range(text, &len, sel_start, sel_end); state.cursor = sel_start; @@ -661,15 +669,22 @@ inline int text_input_key(TextInputState& state, changed = true; } } 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; } state.selection_anchor = state.cursor; return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0); } - if (key.ctrl || key.alt || !text_input_char_allowed(key.ascii, filter, userdata)) - return TEXT_INPUT_NONE; + // 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; + } if (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; } - if (len < cap - 1) { + if (len + typed_len < cap) { for (int i = len; i >= state.cursor; i--) - text[i + 1] = text[i]; - text[state.cursor++] = key.ascii; - len++; + text[i + typed_len] = text[i]; + for (int i = 0; i < typed_len; i++) + text[state.cursor++] = typed[i]; + len += typed_len; changed = true; } @@ -1251,22 +1267,34 @@ inline void draw_badge(Canvas& c, 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) { int len = 0; - while (value && value[len] && len < cap - 1) { - buffer[len] = '*'; - len++; + const char* p = value ? value : ""; + while (*p && len < cap - 1) { + montauk::utf8::next(&p); + buffer[len++] = '*'; } buffer[len] = '\0'; 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) { if (cap <= 0) return; int len = 0; - while (value && value[len] && len < count && len < cap - 1) { - buffer[len] = masked ? '*' : value[len]; - len++; + 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) { + buffer[len] = value[len]; + len++; + } } buffer[len] = '\0'; } diff --git a/programs/include/gui/terminal.hpp b/programs/include/gui/terminal.hpp index 6534bcc..23c133b 100644 --- a/programs/include/gui/terminal.hpp +++ b/programs/include/gui/terminal.hpp @@ -9,13 +9,14 @@ #include "gui/font.hpp" #include #include +#include #include #include namespace gui { struct TermCell { - char ch; + uint32_t ch; // Unicode codepoint (every cell is one column wide) Color fg; Color bg; }; @@ -58,6 +59,11 @@ struct TerminalState { int csi_params[8]; int csi_param_count; 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 ==== @@ -244,6 +250,7 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int t->csi_private = false; t->csi_param_count = 0; t->csi_current_param = 0; + t->utf8_pending_len = 0; t->child_pid = 0; 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); } -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) { t->cursor_x = 0; 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) { if (len > 0) t->dirty = true; for (int i = 0; i < len; 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) { case TerminalState::STATE_NORMAL: if (ch == '\033') { @@ -575,9 +632,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) { while (t->cursor_x < next) { terminal_put_char(t, ' '); } - } else if (ch >= 32 || ch < 0) { - // Printable character (also treat high-bit chars as printable) - terminal_put_char(t, ch); + } else if (ch >= 32) { + terminal_put_char(t, (uint8_t)ch); } 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, - int x, int y, unsigned char ch, + int x, int y, uint32_t ch, 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++) { int dy = y + fy; if (dy >= ph) break; @@ -669,14 +725,14 @@ static inline void terminal_draw_cell_pixels(const TermCell& cell, cell.bg.to_pixel()); } - if (cell.ch <= 32 && cell.ch >= 0) return; + if (cell.ch <= 32) return; if (use_ttf) { int baseline = py + gc->ascent; 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 { - 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()); } } @@ -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, g_term_palette.cursor.to_pixel()); - if (cell.ch <= 32 && cell.ch >= 0) return; + if (cell.ch <= 32) return; if (use_ttf) { int baseline = py + gc->ascent; 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 { - 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()); } } diff --git a/programs/include/gui/truetype.hpp b/programs/include/gui/truetype.hpp index f7d877e..361fc04 100644 --- a/programs/include/gui/truetype.hpp +++ b/programs/include/gui/truetype.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "gui/gui.hpp" #include "gui/framebuffer.hpp" @@ -41,39 +42,12 @@ 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) { if (codepoint < 0 || codepoint > 255) return 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; - } + return (int)montauk::utf8::from_cp1252((uint8_t)codepoint); } struct CachedGlyph { @@ -84,8 +58,18 @@ struct CachedGlyph { 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 { - 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; float scale; int ascent, descent, line_gap; @@ -142,8 +126,21 @@ struct TrueTypeFont { return true; } - void init_cache(GlyphCache* gc, int pixel_size) { - // Free any existing glyph bitmaps + static void free_ext(GlyphCache* gc) { + 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++) { if (gc->glyphs[i].bitmap) { montauk::mfree(gc->glyphs[i].bitmap); @@ -151,6 +148,11 @@ struct TrueTypeFont { gc->glyphs[i].bitmap = nullptr; 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->scale = em_scaling @@ -179,11 +181,8 @@ struct TrueTypeFont { return gc; } - // Evict oldest cache (slot 0) — free its glyph bitmaps - for (int g = 0; g < 256; g++) { - if (caches[0].glyphs[g].bitmap) - montauk::mfree(caches[0].glyphs[g].bitmap); - } + // Evict oldest cache (slot 0) - free its glyph bitmaps + free_cache(&caches[0]); // Shift remaining caches down for (int i = 0; i < 3; i++) montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache)); @@ -192,13 +191,54 @@ struct TrueTypeFont { 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) { - if (codepoint < 0 || codepoint >= 256) return nullptr; - CachedGlyph* g = &gc->glyphs[codepoint]; + if (codepoint < 0 || codepoint > 0x10FFFF) return nullptr; + CachedGlyph* g = codepoint < 256 ? &gc->glyphs[codepoint] + : ext_slot(gc, (uint32_t)codepoint); + if (!g) return nullptr; if (g->loaded) return g; g->loaded = true; - int font_codepoint = decode_single_byte_codepoint(codepoint); + int font_codepoint = codepoint; int advance, lsb; stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb); g->advance = (int)(advance * gc->scale); @@ -222,12 +262,34 @@ struct TrueTypeFont { 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) { if (!valid) return 0; GlyphCache* gc = get_cache(pixel_size); int w = 0; - for (int i = 0; text[i]; i++) { - CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p)); if (g) w += g->advance; } return w; @@ -253,8 +315,8 @@ struct TrueTypeFont { uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) | ((uint32_t)color.g << 8) | color.b; - for (int i = 0; text[i]; i++) { - CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p)); if (!g) continue; if (g->bitmap) { @@ -325,8 +387,8 @@ struct TrueTypeFont { int baseline = y + gc->ascent; bool drew_pixel = false; - for (int i = 0; text[i]; i++) { - CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p)); if (!g) continue; if (g->bitmap) { @@ -378,8 +440,8 @@ struct TrueTypeFont { int clip_x2 = clip_x + clip_w; int clip_y2 = clip_y + clip_h; - for (int i = 0; text[i]; i++) { - CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p)); if (!g) continue; 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 x, int baseline, int codepoint, Color color, GlyphCache* gc) { diff --git a/programs/include/gui/widgets.hpp b/programs/include/gui/widgets.hpp index 5aa203d..e2de916 100644 --- a/programs/include/gui/widgets.hpp +++ b/programs/include/gui/widgets.hpp @@ -212,11 +212,7 @@ struct TextBox { // Draw cursor if focused if (focused) { - // Measure text up to cursor position for proportional fonts - 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); + int cx = tx + text_width_n(text, cursor); draw_vline(fb, cx, ty, fh, cursor_color); } } @@ -230,34 +226,35 @@ struct TextBox { void handle_key(const montauk::abi::KeyEvent& key) { 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) { - // Backspace + // Backspace: remove the whole character before the cursor if (cursor > 0) { - for (int i = cursor - 1; i < text_len - 1; i++) { - text[i] = text[i + 1]; - } - text_len--; - cursor--; - text[text_len] = '\0'; + int start = montauk::utf8::prev_boundary(text, cursor); + int n = cursor - start; + for (int i = start; i <= text_len - n; i++) + text[i] = text[i + n]; + text_len -= n; + cursor = start; } - } else if ((unsigned char)key.ascii >= 32 - && (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 (typed_len > 0) { + // Printable character, inserted as UTF-8 + 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 (key.scancode == 0x4B) { + } else if (montauk::nav_scancode(key) == 0x4B) { // Left arrow - if (cursor > 0) cursor--; - } else if (key.scancode == 0x4D) { + cursor = montauk::utf8::prev_boundary(text, cursor); + } else if (montauk::nav_scancode(key) == 0x4D) { // Right arrow - if (cursor < text_len) cursor++; + cursor = montauk::utf8::next_boundary(text, text_len, cursor); } } }; diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index d08f457..7f02ad6 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -23,200 +23,202 @@ extern "C" { Generated from Api/Syscall.hpp by scripts/montauk-syscalls.py. Do not edit by hand; run 'make gen-syscalls' to refresh. Wrapper functions below are hand-written. */ -#define MTK_SYS_EXIT 0 -#define MTK_SYS_YIELD 1 -#define MTK_SYS_SLEEP_MS 2 -#define MTK_SYS_GETPID 3 -#define MTK_SYS_PRINT 4 -#define MTK_SYS_PUTCHAR 5 -#define MTK_SYS_OPEN 6 -#define MTK_SYS_READ 7 -#define MTK_SYS_GETSIZE 8 -#define MTK_SYS_CLOSE 9 -#define MTK_SYS_READDIR 10 -#define MTK_SYS_ALLOC 11 -#define MTK_SYS_FREE 12 -#define MTK_SYS_GETTICKS 13 -#define MTK_SYS_GETMILLISECONDS 14 -#define MTK_SYS_GETINFO 15 -#define MTK_SYS_ISKEYAVAILABLE 16 -#define MTK_SYS_GETKEY 17 -#define MTK_SYS_GETCHAR 18 -#define MTK_SYS_PING 19 -#define MTK_SYS_SPAWN 20 -#define MTK_SYS_FBINFO 21 -#define MTK_SYS_FBMAP 22 -#define MTK_SYS_WAITPID 23 -#define MTK_SYS_TERMSIZE 24 -#define MTK_SYS_GETARGS 25 -#define MTK_SYS_RESET 26 -#define MTK_SYS_SHUTDOWN 27 -#define MTK_SYS_GETTIME 28 -#define MTK_SYS_SOCKET 29 -#define MTK_SYS_CONNECT 30 -#define MTK_SYS_BIND 31 -#define MTK_SYS_LISTEN 32 -#define MTK_SYS_ACCEPT 33 -#define MTK_SYS_SEND 34 -#define MTK_SYS_RECV 35 -#define MTK_SYS_CLOSESOCK 36 -#define MTK_SYS_GETNETCFG 37 -#define MTK_SYS_SETNETCFG 38 -#define MTK_SYS_SENDTO 39 -#define MTK_SYS_RECVFROM 40 -#define MTK_SYS_FWRITE 41 -#define MTK_SYS_FCREATE 42 -#define MTK_SYS_TERMSCALE 43 -#define MTK_SYS_RESOLVE 44 -#define MTK_SYS_GETRANDOM 45 -#define MTK_SYS_LOG 46 -#define MTK_SYS_MOUSESTATE 47 -#define MTK_SYS_SETMOUSEBOUNDS 48 -#define MTK_SYS_SPAWN_REDIR 49 -#define MTK_SYS_CHILDIO_READ 50 -#define MTK_SYS_CHILDIO_WRITE 51 -#define MTK_SYS_CHILDIO_WRITEKEY 52 -#define MTK_SYS_CHILDIO_SETTERMSZ 53 -#define MTK_SYS_WINCREATE 54 -#define MTK_SYS_WINDESTROY 55 -#define MTK_SYS_WINPRESENT 56 -#define MTK_SYS_WINPOLL 57 -#define MTK_SYS_WINENUM 58 -#define MTK_SYS_WINMAP 59 -#define MTK_SYS_WINSENDEVENT 60 -#define MTK_SYS_PROCLIST 61 -#define MTK_SYS_KILL 62 -#define MTK_SYS_DEVLIST 63 -#define MTK_SYS_WINRESIZE 64 -#define MTK_SYS_WINSETSCALE 65 -#define MTK_SYS_WINGETSCALE 66 -#define MTK_SYS_MEMSTATS 67 -#define MTK_SYS_WINSETCURSOR 68 -#define MTK_SYS_DISKINFO 69 -#define MTK_SYS_PARTLIST 70 -#define MTK_SYS_DISKREAD 71 -#define MTK_SYS_DISKWRITE 72 -#define MTK_SYS_GPTINIT 73 -#define MTK_SYS_GPTADD 74 -#define MTK_SYS_FSMOUNT 75 -#define MTK_SYS_FSFORMAT 76 -#define MTK_SYS_FDELETE 77 -#define MTK_SYS_FMKDIR 78 -#define MTK_SYS_DRIVELIST 79 -#define MTK_SYS_AUDIOOPEN 80 -#define MTK_SYS_AUDIOCLOSE 81 -#define MTK_SYS_AUDIOWRITE 82 -#define MTK_SYS_AUDIOCTL 83 -#define MTK_SYS_BTSCAN 84 -#define MTK_SYS_BTCONNECT 85 -#define MTK_SYS_BTDISCONNECT 86 -#define MTK_SYS_BTLIST 87 -#define MTK_SYS_BTINFO 88 -#define MTK_SYS_SUSPEND 89 -#define MTK_SYS_SETTZ 90 -#define MTK_SYS_GETTZ 91 -#define MTK_SYS_SETUSER 92 -#define MTK_SYS_GETUSER 93 -#define MTK_SYS_FRENAME 94 -#define MTK_SYS_GETCWD 95 -#define MTK_SYS_CHDIR 96 -#define MTK_SYS_WINUNMAP 97 -#define MTK_SYS_DUPHANDLE 98 -#define MTK_SYS_WAIT_HANDLE 99 -#define MTK_SYS_STREAM_CREATE 100 -#define MTK_SYS_STREAM_READ 101 -#define MTK_SYS_STREAM_WRITE 102 -#define MTK_SYS_MAILBOX_CREATE 103 -#define MTK_SYS_MAILBOX_SEND 104 -#define MTK_SYS_MAILBOX_RECV 105 -#define MTK_SYS_WAITSET_CREATE 106 -#define MTK_SYS_WAITSET_ADD 107 -#define MTK_SYS_WAITSET_REMOVE 108 -#define MTK_SYS_WAITSET_WAIT 109 -#define MTK_SYS_PROC_OPEN 110 -#define MTK_SYS_SURFACE_CREATE 111 -#define MTK_SYS_SURFACE_MAP 112 -#define MTK_SYS_SURFACE_RESIZE 113 -#define MTK_SYS_LOAD_LIB 114 -#define MTK_SYS_UNLOAD_LIB 115 -#define MTK_SYS_DLSYM 116 -#define MTK_SYS_GETLIBBASE 117 -#define MTK_SYS_CRASH_REPORT 118 -#define MTK_SYS_CLIPBOARD_SET_TEXT 119 -#define MTK_SYS_CLIPBOARD_GET_INFO 120 -#define MTK_SYS_CLIPBOARD_GET_TEXT 121 -#define MTK_SYS_CLIPBOARD_CLEAR 122 -#define MTK_SYS_INPUT_WAIT 123 -#define MTK_SYS_DRIVELABEL 124 -#define MTK_SYS_NETSTATUS 125 -#define MTK_SYS_WINSETFLAGS 126 -#define MTK_SYS_DRIVEKIND 127 -#define MTK_SYS_AUDIOLIST 128 -#define MTK_SYS_AUDIOWAIT 129 -#define MTK_SYS_THREAD_SPAWN 130 -#define MTK_SYS_THREAD_EXIT 131 -#define MTK_SYS_THREAD_JOIN 132 -#define MTK_SYS_THREAD_SELF 133 -#define MTK_SYS_FS_SYNC 134 -#define MTK_SYS_POWER_REQUEST 135 -#define MTK_SYS_READDIR_AT 136 -#define MTK_SYS_BTSETADDR 137 -#define MTK_SYS_BTBONDS 138 -#define MTK_SYS_BTFORGET 139 -#define MTK_SYS_RESERVED_140 140 -#define MTK_SYS_RESERVED_141 141 -#define MTK_SYS_RESERVED_142 142 -#define MTK_SYS_RESERVED_143 143 -#define MTK_SYS_RESERVED_144 144 -#define MTK_SYS_RESERVED_145 145 -#define MTK_SYS_RESERVED_146 146 -#define MTK_SYS_RESERVED_147 147 -#define MTK_SYS_RESERVED_148 148 -#define MTK_SYS_POWERINFO 149 -#define MTK_SYS_FBFLIP 150 -#define MTK_SYS_GETEXECPATH 151 -#define MTK_SYS_STAT 152 -#define MTK_SYS_SETUNIXTIME 153 -#define MTK_SYS_DISPLAYINFO 154 -#define MTK_SYS_DISPLAYMODES 155 -#define MTK_SYS_DISPLAYSETMODE 156 -#define MTK_SYS_DISPLAYBRIGHTNESS 157 -#define MTK_SYS_WIFI_SCAN 158 -#define MTK_SYS_WIFI_INFO 159 -#define MTK_SYS_WIFI_CONNECT 160 -#define MTK_SYS_WIFI_DISCONNECT 161 -#define MTK_SYS_WIFI_SCAN_START 162 -#define MTK_SYS_WIFI_RESULTS 163 -#define MTK_SYS_WIFI_CONNECT_ASYNC 164 -#define MTK_SYS_NETIFS 165 -#define MTK_SYS_GETCHAR_NB 166 -#define MTK_SYS_UTIME 167 -#define MTK_SYS_MMAP_ANON 168 -#define MTK_SYS_MUNMAP 169 -#define MTK_SYS_MPROTECT 170 -#define MTK_SYS_GETENVIRON 171 -#define MTK_SYS_SETENVIRON 172 -#define MTK_SYS_SPAWN_ENV 173 -#define MTK_SYS_SETSESSION 174 -#define MTK_SYS_KILLSESSION 175 -#define MTK_SYS_LOG_WRITE 176 -#define MTK_SYS_TERMINAL_ATTACHED 177 -#define MTK_SYS_USB_LIST 178 -#define MTK_SYS_USB_CLAIM 179 -#define MTK_SYS_USB_CLOSE 180 -#define MTK_SYS_USB_CONTROL 181 -#define MTK_SYS_USB_BULK_IN_START 182 -#define MTK_SYS_USB_BULK_IN_STOP 183 -#define MTK_SYS_USB_BULK_IN_READ 184 -#define MTK_SYS_SPAWN_CAPS 185 -#define MTK_SYS_SPAWN_REDIR_CAPS 186 -#define MTK_SYS_ALLOC_EAGER 187 -#define MTK_SYS_BATTERYINFO 188 -#define MTK_SYS_GETCAPS 189 -#define MTK_SYS_VOLINFO 190 -#define MTK_SYS_CONSENT_REGISTER 191 -#define MTK_SYS_CONSENT_NEXT 192 -#define MTK_SYS_CONSENT_REPLY 193 +#define MTK_SYS_EXIT 0 +#define MTK_SYS_YIELD 1 +#define MTK_SYS_SLEEP_MS 2 +#define MTK_SYS_GETPID 3 +#define MTK_SYS_PRINT 4 +#define MTK_SYS_PUTCHAR 5 +#define MTK_SYS_OPEN 6 +#define MTK_SYS_READ 7 +#define MTK_SYS_GETSIZE 8 +#define MTK_SYS_CLOSE 9 +#define MTK_SYS_READDIR 10 +#define MTK_SYS_ALLOC 11 +#define MTK_SYS_FREE 12 +#define MTK_SYS_GETTICKS 13 +#define MTK_SYS_GETMILLISECONDS 14 +#define MTK_SYS_GETINFO 15 +#define MTK_SYS_ISKEYAVAILABLE 16 +#define MTK_SYS_GETKEY 17 +#define MTK_SYS_GETCHAR 18 +#define MTK_SYS_PING 19 +#define MTK_SYS_SPAWN 20 +#define MTK_SYS_FBINFO 21 +#define MTK_SYS_FBMAP 22 +#define MTK_SYS_WAITPID 23 +#define MTK_SYS_TERMSIZE 24 +#define MTK_SYS_GETARGS 25 +#define MTK_SYS_RESET 26 +#define MTK_SYS_SHUTDOWN 27 +#define MTK_SYS_GETTIME 28 +#define MTK_SYS_SOCKET 29 +#define MTK_SYS_CONNECT 30 +#define MTK_SYS_BIND 31 +#define MTK_SYS_LISTEN 32 +#define MTK_SYS_ACCEPT 33 +#define MTK_SYS_SEND 34 +#define MTK_SYS_RECV 35 +#define MTK_SYS_CLOSESOCK 36 +#define MTK_SYS_GETNETCFG 37 +#define MTK_SYS_SETNETCFG 38 +#define MTK_SYS_SENDTO 39 +#define MTK_SYS_RECVFROM 40 +#define MTK_SYS_FWRITE 41 +#define MTK_SYS_FCREATE 42 +#define MTK_SYS_TERMSCALE 43 +#define MTK_SYS_RESOLVE 44 +#define MTK_SYS_GETRANDOM 45 +#define MTK_SYS_LOG 46 +#define MTK_SYS_MOUSESTATE 47 +#define MTK_SYS_SETMOUSEBOUNDS 48 +#define MTK_SYS_SPAWN_REDIR 49 +#define MTK_SYS_CHILDIO_READ 50 +#define MTK_SYS_CHILDIO_WRITE 51 +#define MTK_SYS_CHILDIO_WRITEKEY 52 +#define MTK_SYS_CHILDIO_SETTERMSZ 53 +#define MTK_SYS_WINCREATE 54 +#define MTK_SYS_WINDESTROY 55 +#define MTK_SYS_WINPRESENT 56 +#define MTK_SYS_WINPOLL 57 +#define MTK_SYS_WINENUM 58 +#define MTK_SYS_WINMAP 59 +#define MTK_SYS_WINSENDEVENT 60 +#define MTK_SYS_PROCLIST 61 +#define MTK_SYS_KILL 62 +#define MTK_SYS_DEVLIST 63 +#define MTK_SYS_WINRESIZE 64 +#define MTK_SYS_WINSETSCALE 65 +#define MTK_SYS_WINGETSCALE 66 +#define MTK_SYS_MEMSTATS 67 +#define MTK_SYS_WINSETCURSOR 68 +#define MTK_SYS_DISKINFO 69 +#define MTK_SYS_PARTLIST 70 +#define MTK_SYS_DISKREAD 71 +#define MTK_SYS_DISKWRITE 72 +#define MTK_SYS_GPTINIT 73 +#define MTK_SYS_GPTADD 74 +#define MTK_SYS_FSMOUNT 75 +#define MTK_SYS_FSFORMAT 76 +#define MTK_SYS_FDELETE 77 +#define MTK_SYS_FMKDIR 78 +#define MTK_SYS_DRIVELIST 79 +#define MTK_SYS_AUDIOOPEN 80 +#define MTK_SYS_AUDIOCLOSE 81 +#define MTK_SYS_AUDIOWRITE 82 +#define MTK_SYS_AUDIOCTL 83 +#define MTK_SYS_BTSCAN 84 +#define MTK_SYS_BTCONNECT 85 +#define MTK_SYS_BTDISCONNECT 86 +#define MTK_SYS_BTLIST 87 +#define MTK_SYS_BTINFO 88 +#define MTK_SYS_SUSPEND 89 +#define MTK_SYS_SETTZ 90 +#define MTK_SYS_GETTZ 91 +#define MTK_SYS_SETUSER 92 +#define MTK_SYS_GETUSER 93 +#define MTK_SYS_FRENAME 94 +#define MTK_SYS_GETCWD 95 +#define MTK_SYS_CHDIR 96 +#define MTK_SYS_WINUNMAP 97 +#define MTK_SYS_DUPHANDLE 98 +#define MTK_SYS_WAIT_HANDLE 99 +#define MTK_SYS_STREAM_CREATE 100 +#define MTK_SYS_STREAM_READ 101 +#define MTK_SYS_STREAM_WRITE 102 +#define MTK_SYS_MAILBOX_CREATE 103 +#define MTK_SYS_MAILBOX_SEND 104 +#define MTK_SYS_MAILBOX_RECV 105 +#define MTK_SYS_WAITSET_CREATE 106 +#define MTK_SYS_WAITSET_ADD 107 +#define MTK_SYS_WAITSET_REMOVE 108 +#define MTK_SYS_WAITSET_WAIT 109 +#define MTK_SYS_PROC_OPEN 110 +#define MTK_SYS_SURFACE_CREATE 111 +#define MTK_SYS_SURFACE_MAP 112 +#define MTK_SYS_SURFACE_RESIZE 113 +#define MTK_SYS_LOAD_LIB 114 +#define MTK_SYS_UNLOAD_LIB 115 +#define MTK_SYS_DLSYM 116 +#define MTK_SYS_GETLIBBASE 117 +#define MTK_SYS_CRASH_REPORT 118 +#define MTK_SYS_CLIPBOARD_SET_TEXT 119 +#define MTK_SYS_CLIPBOARD_GET_INFO 120 +#define MTK_SYS_CLIPBOARD_GET_TEXT 121 +#define MTK_SYS_CLIPBOARD_CLEAR 122 +#define MTK_SYS_INPUT_WAIT 123 +#define MTK_SYS_DRIVELABEL 124 +#define MTK_SYS_NETSTATUS 125 +#define MTK_SYS_WINSETFLAGS 126 +#define MTK_SYS_DRIVEKIND 127 +#define MTK_SYS_AUDIOLIST 128 +#define MTK_SYS_AUDIOWAIT 129 +#define MTK_SYS_THREAD_SPAWN 130 +#define MTK_SYS_THREAD_EXIT 131 +#define MTK_SYS_THREAD_JOIN 132 +#define MTK_SYS_THREAD_SELF 133 +#define MTK_SYS_FS_SYNC 134 +#define MTK_SYS_POWER_REQUEST 135 +#define MTK_SYS_READDIR_AT 136 +#define MTK_SYS_BTSETADDR 137 +#define MTK_SYS_BTBONDS 138 +#define MTK_SYS_BTFORGET 139 +#define MTK_SYS_RESERVED_140 140 +#define MTK_SYS_RESERVED_141 141 +#define MTK_SYS_RESERVED_142 142 +#define MTK_SYS_RESERVED_143 143 +#define MTK_SYS_RESERVED_144 144 +#define MTK_SYS_RESERVED_145 145 +#define MTK_SYS_RESERVED_146 146 +#define MTK_SYS_RESERVED_147 147 +#define MTK_SYS_RESERVED_148 148 +#define MTK_SYS_POWERINFO 149 +#define MTK_SYS_FBFLIP 150 +#define MTK_SYS_GETEXECPATH 151 +#define MTK_SYS_STAT 152 +#define MTK_SYS_SETUNIXTIME 153 +#define MTK_SYS_DISPLAYINFO 154 +#define MTK_SYS_DISPLAYMODES 155 +#define MTK_SYS_DISPLAYSETMODE 156 +#define MTK_SYS_DISPLAYBRIGHTNESS 157 +#define MTK_SYS_WIFI_SCAN 158 +#define MTK_SYS_WIFI_INFO 159 +#define MTK_SYS_WIFI_CONNECT 160 +#define MTK_SYS_WIFI_DISCONNECT 161 +#define MTK_SYS_WIFI_SCAN_START 162 +#define MTK_SYS_WIFI_RESULTS 163 +#define MTK_SYS_WIFI_CONNECT_ASYNC 164 +#define MTK_SYS_NETIFS 165 +#define MTK_SYS_GETCHAR_NB 166 +#define MTK_SYS_UTIME 167 +#define MTK_SYS_MMAP_ANON 168 +#define MTK_SYS_MUNMAP 169 +#define MTK_SYS_MPROTECT 170 +#define MTK_SYS_GETENVIRON 171 +#define MTK_SYS_SETENVIRON 172 +#define MTK_SYS_SPAWN_ENV 173 +#define MTK_SYS_SETSESSION 174 +#define MTK_SYS_KILLSESSION 175 +#define MTK_SYS_LOG_WRITE 176 +#define MTK_SYS_TERMINAL_ATTACHED 177 +#define MTK_SYS_USB_LIST 178 +#define MTK_SYS_USB_CLAIM 179 +#define MTK_SYS_USB_CLOSE 180 +#define MTK_SYS_USB_CONTROL 181 +#define MTK_SYS_USB_BULK_IN_START 182 +#define MTK_SYS_USB_BULK_IN_STOP 183 +#define MTK_SYS_USB_BULK_IN_READ 184 +#define MTK_SYS_SPAWN_CAPS 185 +#define MTK_SYS_SPAWN_REDIR_CAPS 186 +#define MTK_SYS_ALLOC_EAGER 187 +#define MTK_SYS_BATTERYINFO 188 +#define MTK_SYS_GETCAPS 189 +#define MTK_SYS_VOLINFO 190 +#define MTK_SYS_CONSENT_REGISTER 191 +#define MTK_SYS_CONSENT_NEXT 192 +#define MTK_SYS_CONSENT_REPLY 193 +#define MTK_SYS_GETKEY_EX 194 +#define MTK_SYS_CHILDIO_WRITEKEY_EX 195 /* @SYSCALLS-END */ /* Syscall status codes (montauk/statuscodes.h for C++). */ diff --git a/programs/include/montauk/keyboard.h b/programs/include/montauk/keyboard.h index 74bb23a..ef5a1a6 100644 --- a/programs/include/montauk/keyboard.h +++ b/programs/include/montauk/keyboard.h @@ -8,27 +8,34 @@ #include #include +#include namespace montauk::keyboard { -inline constexpr int MAX_LAYOUTS = 8; -inline constexpr int MAX_KEYS = 64; +inline constexpr int MAX_LAYOUTS = 8; +inline constexpr int MAX_KEYS = 64; +inline constexpr int MAX_DEAD_KEYS = 16; -// One overridden key. Characters are Windows-1252 bytes to match the -// single-byte GUI text stack; 0 means "no override, keep the kernel's value". +// One overridden key. Characters are Unicode codepoints; translation stores +// them in KeyEvent::codepoint (and the Windows-1252 byte, if any, in ascii). +// 0 means "no override, keep the kernel's value". struct KeyMap { - uint8_t scancode; - uint8_t base; - uint8_t shift; - uint8_t altgr; + uint8_t scancode; + uint32_t base; + uint32_t shift; + uint32_t altgr; }; struct Layout { - char id[8]; - char name[48]; - char short_name[8]; - KeyMap keys[MAX_KEYS]; - int key_count; // 0 = passthrough (the kernel's US table) + char id[8]; + char name[48]; + char short_name[8]; + KeyMap keys[MAX_KEYS]; + 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 { @@ -40,8 +47,71 @@ struct State { Registry registry; bool enabled[MAX_LAYOUTS]; 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, const char* id, const char* suffix = nullptr) { int pos = 0; @@ -54,10 +124,9 @@ inline void build_key(char* out, int cap, const char* prefix, 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. -inline int read_bytes(const toml::Doc& doc, const char* key, - uint8_t* out, int cap) { +inline int read_bytes(const toml::Doc& doc, const char* key, uint8_t* out, int cap) { toml::Value* arr = doc.get_array(key); if (!arr) 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; } +// 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.] table. Returns false if the layout declares key // overrides but they are inconsistent, in which case the caller skips it: a // 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), sizeof(out->short_name)); - uint8_t scancodes[MAX_KEYS]; - uint8_t base[MAX_KEYS]; - uint8_t shift[MAX_KEYS]; - uint8_t altgr[MAX_KEYS] = {}; + 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]; + uint32_t base[MAX_KEYS]; + uint32_t shift[MAX_KEYS]; + uint32_t altgr[MAX_KEYS] = {}; build_key(key, sizeof(key), "layouts.", id, ".scancodes"); 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"); - 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"); - 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"); - 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; 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. inline void refresh_selection(State* state, const char* username) { 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; toml::Doc doc = config::load_user(username, "keyboard"); @@ -237,57 +329,117 @@ inline int next_enabled(const State& state, int current) { } // 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 -// accented letters (aa 0xE5 / AA 0xC5) without wrongly upper-casing keys such -// as 2 / " where the shifted value is unrelated punctuation. -inline constexpr bool is_letter_pair(uint8_t base, uint8_t shift) { +// base and shift are the lower/upper case of one letter. That covers accented +// letters in any script (aa/AA, e-caron/E-caron) without wrongly upper-casing +// keys such as 2 / " where the shifted value is unrelated punctuation. +inline constexpr bool is_letter_pair(uint32_t base, uint32_t shift) { if (base == 0 || shift == 0) return false; - if (base < 0x61) return false; - if (base > 0x7A && base < 0xE0) return false; - if (base == 0xF7) return false; // division sign sits inside the range - return shift == (uint8_t)(base - 0x20); + return utf8::is_case_pair(base, shift); } -static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA -static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE -static_assert(is_letter_pair(0xE6, 0xC6)); // ae / AE +static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA +static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE +static_assert(is_letter_pair(0x11B, 0x11A)); // e-caron static_assert(is_letter_pair('a', 'A')); 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 (state.active < 0 || state.active >= state.registry.count) return; - const Layout& layout = state.registry.items[state.active]; + const Layout* layout = (state.active >= 0 && state.active < state.registry.count) + ? &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 // (keypad "/" arrives as 0x35, the same as the main "/"), so translating // 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++) { + const KeyMap& mapping = layout->keys[i]; + if (mapping.scancode != scancode) continue; - uint8_t scancode = key->scancode & 0x7F; - for (int i = 0; i < layout.key_count; i++) { - const KeyMap& mapping = layout.keys[i]; - if (mapping.scancode != scancode) continue; + uint32_t out; + if (key->altgr) { + out = mapping.altgr; + } else { + bool upper = key->shift; + if (is_letter_pair(mapping.base, mapping.shift)) + upper = key->shift != key->capslock; + out = upper ? mapping.shift : mapping.base; + } + if (out != 0) { + cp = out; + // An AltGr key that produced a character is text, not a + // shortcut. Apps gate insertion on !alt (and alt is + // LeftAlt||RightAlt), so leaving it set would silently swallow + // every AltGr character. + if (key->altgr) key->alt = false; + } + break; + } + } - uint8_t out; - if (key->altgr) { - out = mapping.altgr; - } else { - bool upper = key->shift; - if (is_letter_pair(mapping.base, mapping.shift)) - upper = key->shift != key->capslock; - out = upper ? mapping.shift : mapping.base; - } - if (out != 0) { - key->ascii = (char)out; - // An AltGr key that produced a character is text, not a shortcut. - // Apps gate insertion on !alt (and alt is LeftAlt||RightAlt), so - // leaving it set would silently swallow every AltGr character. - if (key->altgr) key->alt = false; + 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; } + 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 ---- diff --git a/programs/include/montauk/statuscodes.h b/programs/include/montauk/statuscodes.h index 3c6acbf..e91813a 100644 --- a/programs/include/montauk/statuscodes.h +++ b/programs/include/montauk/statuscodes.h @@ -56,27 +56,28 @@ inline const char* GetStatusText(StatusCode statusCode) { } } +// Shared with C (libc.c), so no nullptr. inline const char* GetStatusExplanationText(StatusCode statusCode) { const char* explanationText[] = { - nullptr, // 0 - nullptr, // 1 - nullptr, // 2 - nullptr, // 3 + 0, // 0 + 0, // 1 + 0, // 2 + 0, // 3 "The path is protected by the system and cannot be modified.", // 4 - nullptr, // 5 - nullptr, // 6 - nullptr, // 7 - nullptr, // 8 - nullptr, // 9 - nullptr, // 10 - nullptr, // 11 - nullptr, // 12 + 0, // 5 + 0, // 6 + 0, // 7 + 0, // 8 + 0, // 9 + 0, // 10 + 0, // 11 + 0, // 12 "The buffer supplied by the caller is too small.", // 13 - nullptr, // 14 + 0, // 14 }; if (statusCode > 0 && statusCode <= 14) { - if (explanationText[statusCode] != nullptr) { + if (explanationText[statusCode] != 0) { return explanationText[statusCode]; } } diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 5c9c188..b2ac9a1 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -7,6 +7,7 @@ #pragma once #include #include +#include namespace montauk { @@ -222,7 +223,23 @@ namespace montauk { // Keyboard 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); } // Returns 0 rather than blocking when no character is pending. 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); } 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) { return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows); diff --git a/programs/include/montauk/user.h b/programs/include/montauk/user.h index efff1e0..855d0a7 100644 --- a/programs/include/montauk/user.h +++ b/programs/include/montauk/user.h @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace montauk { @@ -107,7 +108,7 @@ namespace user { 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]; hash_password(password, salt_hex, computed); @@ -118,6 +119,26 @@ namespace user { 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 ---- inline int load_users(UserInfo* buf, int max) { diff --git a/programs/include/montauk/utf8.h b/programs/include/montauk/utf8.h new file mode 100644 index 0000000..dae0f33 --- /dev/null +++ b/programs/include/montauk/utf8.h @@ -0,0 +1,399 @@ +/* + * utf8.h + * UTF-8 text encoding helpers with a Windows-1252 fallback + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include + +// 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 +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 +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 +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 +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 diff --git a/programs/lib/libc/libc.c b/programs/lib/libc/libc.c index 52b63d8..a3bbc03 100644 --- a/programs/lib/libc/libc.c +++ b/programs/lib/libc/libc.c @@ -1712,7 +1712,10 @@ static int _stdin_pump(int block) { for (;;) { int c = block ? (int)_mtk_syscall0(SYS_GETCHAR) : (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; return 0; } @@ -1721,12 +1724,26 @@ static int _stdin_pump(int block) { if (c == '\b' || c == 127) { 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_echo_char('\b'); } 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') { _stdin_linebuf[_stdin_line_len++] = (char)c; _stdin_echo_char((char)c); diff --git a/programs/src/2048/main.cpp b/programs/src/2048/main.cpp index 26942fb..01c6831 100644 --- a/programs/src/2048/main.cpp +++ b/programs/src/2048/main.cpp @@ -466,13 +466,13 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { if (g_game.game_over) 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); - 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); - 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); - 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 false; diff --git a/programs/src/audio/main.cpp b/programs/src/audio/main.cpp index d13b3e9..9589696 100644 --- a/programs/src/audio/main.cpp +++ b/programs/src/audio/main.cpp @@ -469,12 +469,12 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { g_win.closed = true; 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(); apply_master_volume(g_master_vol + 5); return true; } - if (key.scancode == 0x4B || key.ascii == '-') { + if (montauk::nav_scancode(key) == 0x4B || key.ascii == '-') { if (g_master_muted) toggle_master_mute(); apply_master_volume(g_master_vol - 5); return true; diff --git a/programs/src/bluetooth/main.cpp b/programs/src/bluetooth/main.cpp index 8ee7b5e..c849d6a 100644 --- a/programs/src/bluetooth/main.cpp +++ b/programs/src/bluetooth/main.cpp @@ -224,6 +224,8 @@ static void draw_text_fit(Canvas& c, int x, int y, out++; } + out = montauk::utf8::floor_boundary(text, montauk::slen(text), out); + if (out <= 0) return; buf[out] = '.'; buf[out + 1] = '.'; diff --git a/programs/src/capabilities/main.cpp b/programs/src/capabilities/main.cpp index 722877b..733b855 100644 --- a/programs/src/capabilities/main.cpp +++ b/programs/src/capabilities/main.cpp @@ -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; len++; } + len = montauk::utf8::floor_boundary(text, montauk::slen(text), len); out[len++] = '.'; out[len++] = '.'; out[len++] = '.'; @@ -798,8 +799,8 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { return true; } int current = *selected_for_tab(); - if (key.scancode == 0x48) { select_index(current - 1); return true; } // Up - if (key.scancode == 0x50) { select_index(current + 1); return true; } // Down + if (montauk::nav_scancode(key) == 0x48) { select_index(current - 1); return true; } // Up + if (montauk::nav_scancode(key) == 0x50) { select_index(current + 1); return true; } // Down if (key.ascii == 'r' || key.ascii == 'R') { reload(); return true; diff --git a/programs/src/charmap/main.cpp b/programs/src/charmap/main.cpp index 4ba870d..955b564 100644 --- a/programs/src/charmap/main.cpp +++ b/programs/src/charmap/main.cpp @@ -5,13 +5,9 @@ * Browse curated sets of characters, click (or use the keyboard) to select, * and copy to the system clipboard for pasting into other apps. * - * Character model: MontaukOS's GUI text stack is single-byte (Windows-1252, - * with the 0x80-0x9F block mapped to typographic punctuation in - * gui::decode_single_byte_codepoint). The glyph cache only holds codepoints - * 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). + * Character model: MontaukOS text is UTF-8. Each tile is a Unicode codepoint + * and Copy puts its UTF-8 encoding on the clipboard. The sets are limited to + * characters the system font (Roboto) actually has a glyph for. * * Copyright (c) 2026 Daniel Hammer */ @@ -37,7 +33,7 @@ using namespace gui; // ==== Character data ==== 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 }; @@ -49,141 +45,194 @@ struct CharSet { // Accented Latin letters and ligatures (things you can't easily type). static const Glyph LATIN[] = { - {0xC0, "Latin Capital Letter A With Grave"}, - {0xC1, "Latin Capital Letter A With Acute"}, - {0xC2, "Latin Capital Letter A With Circumflex"}, - {0xC3, "Latin Capital Letter A With Tilde"}, - {0xC4, "Latin Capital Letter A With Diaeresis"}, - {0xC5, "Latin Capital Letter A With Ring Above"}, - {0xC6, "Latin Capital Letter AE"}, - {0xC7, "Latin Capital Letter C With Cedilla"}, - {0xC8, "Latin Capital Letter E With Grave"}, - {0xC9, "Latin Capital Letter E With Acute"}, - {0xCA, "Latin Capital Letter E With Circumflex"}, - {0xCB, "Latin Capital Letter E With Diaeresis"}, - {0xCC, "Latin Capital Letter I With Grave"}, - {0xCD, "Latin Capital Letter I With Acute"}, - {0xCE, "Latin Capital Letter I With Circumflex"}, - {0xCF, "Latin Capital Letter I With Diaeresis"}, - {0xD0, "Latin Capital Letter Eth"}, - {0xD1, "Latin Capital Letter N With Tilde"}, - {0xD2, "Latin Capital Letter O With Grave"}, - {0xD3, "Latin Capital Letter O With Acute"}, - {0xD4, "Latin Capital Letter O With Circumflex"}, - {0xD5, "Latin Capital Letter O With Tilde"}, - {0xD6, "Latin Capital Letter O With Diaeresis"}, - {0xD8, "Latin Capital Letter O With Stroke"}, - {0xD9, "Latin Capital Letter U With Grave"}, - {0xDA, "Latin Capital Letter U With Acute"}, - {0xDB, "Latin Capital Letter U With Circumflex"}, - {0xDC, "Latin Capital Letter U With Diaeresis"}, - {0xDD, "Latin Capital Letter Y With Acute"}, - {0xDE, "Latin Capital Letter Thorn"}, - {0x8A, "Latin Capital Letter S With Caron"}, - {0x8C, "Latin Capital Ligature OE"}, - {0x8E, "Latin Capital Letter Z With Caron"}, - {0x9F, "Latin Capital Letter Y With Diaeresis"}, - {0xDF, "Latin Small Letter Sharp S"}, - {0xE0, "Latin Small Letter A With Grave"}, - {0xE1, "Latin Small Letter A With Acute"}, - {0xE2, "Latin Small Letter A With Circumflex"}, - {0xE3, "Latin Small Letter A With Tilde"}, - {0xE4, "Latin Small Letter A With Diaeresis"}, - {0xE5, "Latin Small Letter A With Ring Above"}, - {0xE6, "Latin Small Letter AE"}, - {0xE7, "Latin Small Letter C With Cedilla"}, - {0xE8, "Latin Small Letter E With Grave"}, - {0xE9, "Latin Small Letter E With Acute"}, - {0xEA, "Latin Small Letter E With Circumflex"}, - {0xEB, "Latin Small Letter E With Diaeresis"}, - {0xEC, "Latin Small Letter I With Grave"}, - {0xED, "Latin Small Letter I With Acute"}, - {0xEE, "Latin Small Letter I With Circumflex"}, - {0xEF, "Latin Small Letter I With Diaeresis"}, - {0xF0, "Latin Small Letter Eth"}, - {0xF1, "Latin Small Letter N With Tilde"}, - {0xF2, "Latin Small Letter O With Grave"}, - {0xF3, "Latin Small Letter O With Acute"}, - {0xF4, "Latin Small Letter O With Circumflex"}, - {0xF5, "Latin Small Letter O With Tilde"}, - {0xF6, "Latin Small Letter O With Diaeresis"}, - {0xF8, "Latin Small Letter O With Stroke"}, - {0xF9, "Latin Small Letter U With Grave"}, - {0xFA, "Latin Small Letter U With Acute"}, - {0xFB, "Latin Small Letter U With Circumflex"}, - {0xFC, "Latin Small Letter U With Diaeresis"}, - {0xFD, "Latin Small Letter Y With Acute"}, - {0xFE, "Latin Small Letter Thorn"}, - {0xFF, "Latin Small Letter Y With Diaeresis"}, - {0x9A, "Latin Small Letter S With Caron"}, - {0x9C, "Latin Small Ligature OE"}, - {0x9E, "Latin Small Letter Z With Caron"}, - {0x83, "Latin Small Letter F With Hook"}, + {0x00C0, "Latin Capital Letter A With Grave"}, + {0x00C1, "Latin Capital Letter A With Acute"}, + {0x00C2, "Latin Capital Letter A With Circumflex"}, + {0x00C3, "Latin Capital Letter A With Tilde"}, + {0x00C4, "Latin Capital Letter A With Diaeresis"}, + {0x00C5, "Latin Capital Letter A With Ring Above"}, + {0x00C6, "Latin Capital Letter AE"}, + {0x00C7, "Latin Capital Letter C With Cedilla"}, + {0x00C8, "Latin Capital Letter E With Grave"}, + {0x00C9, "Latin Capital Letter E With Acute"}, + {0x00CA, "Latin Capital Letter E With Circumflex"}, + {0x00CB, "Latin Capital Letter E With Diaeresis"}, + {0x00CC, "Latin Capital Letter I With Grave"}, + {0x00CD, "Latin Capital Letter I With Acute"}, + {0x00CE, "Latin Capital Letter I With Circumflex"}, + {0x00CF, "Latin Capital Letter I With Diaeresis"}, + {0x00D0, "Latin Capital Letter Eth"}, + {0x00D1, "Latin Capital Letter N With Tilde"}, + {0x00D2, "Latin Capital Letter O With Grave"}, + {0x00D3, "Latin Capital Letter O With Acute"}, + {0x00D4, "Latin Capital Letter O With Circumflex"}, + {0x00D5, "Latin Capital Letter O With Tilde"}, + {0x00D6, "Latin Capital Letter O With Diaeresis"}, + {0x00D8, "Latin Capital Letter O With Stroke"}, + {0x00D9, "Latin Capital Letter U With Grave"}, + {0x00DA, "Latin Capital Letter U With Acute"}, + {0x00DB, "Latin Capital Letter U With Circumflex"}, + {0x00DC, "Latin Capital Letter U With Diaeresis"}, + {0x00DD, "Latin Capital Letter Y With Acute"}, + {0x00DE, "Latin Capital Letter Thorn"}, + {0x0160, "Latin Capital Letter S With Caron"}, + {0x0152, "Latin Capital Ligature OE"}, + {0x017D, "Latin Capital Letter Z With Caron"}, + {0x0178, "Latin Capital Letter Y With Diaeresis"}, + {0x00DF, "Latin Small Letter Sharp S"}, + {0x00E0, "Latin Small Letter A With Grave"}, + {0x00E1, "Latin Small Letter A With Acute"}, + {0x00E2, "Latin Small Letter A With Circumflex"}, + {0x00E3, "Latin Small Letter A With Tilde"}, + {0x00E4, "Latin Small Letter A With Diaeresis"}, + {0x00E5, "Latin Small Letter A With Ring Above"}, + {0x00E6, "Latin Small Letter AE"}, + {0x00E7, "Latin Small Letter C With Cedilla"}, + {0x00E8, "Latin Small Letter E With Grave"}, + {0x00E9, "Latin Small Letter E With Acute"}, + {0x00EA, "Latin Small Letter E With Circumflex"}, + {0x00EB, "Latin Small Letter E With Diaeresis"}, + {0x00EC, "Latin Small Letter I With Grave"}, + {0x00ED, "Latin Small Letter I With Acute"}, + {0x00EE, "Latin Small Letter I With Circumflex"}, + {0x00EF, "Latin Small Letter I With Diaeresis"}, + {0x00F0, "Latin Small Letter Eth"}, + {0x00F1, "Latin Small Letter N With Tilde"}, + {0x00F2, "Latin Small Letter O With Grave"}, + {0x00F3, "Latin Small Letter O With Acute"}, + {0x00F4, "Latin Small Letter O With Circumflex"}, + {0x00F5, "Latin Small Letter O With Tilde"}, + {0x00F6, "Latin Small Letter O With Diaeresis"}, + {0x00F8, "Latin Small Letter O With Stroke"}, + {0x00F9, "Latin Small Letter U With Grave"}, + {0x00FA, "Latin Small Letter U With Acute"}, + {0x00FB, "Latin Small Letter U With Circumflex"}, + {0x00FC, "Latin Small Letter U With Diaeresis"}, + {0x00FD, "Latin Small Letter Y With Acute"}, + {0x00FE, "Latin Small Letter Thorn"}, + {0x00FF, "Latin Small Letter Y With Diaeresis"}, + {0x0161, "Latin Small Letter S With Caron"}, + {0x0153, "Latin Small Ligature OE"}, + {0x017E, "Latin Small Letter Z With Caron"}, + {0x0192, "Latin Small Letter F With Hook"}, }; // Typographic punctuation. static const Glyph PUNCT[] = { - {0xA1, "Inverted Exclamation Mark"}, - {0xBF, "Inverted Question Mark"}, - {0x91, "Left Single Quotation Mark"}, - {0x92, "Right Single Quotation Mark"}, - {0x93, "Left Double Quotation Mark"}, - {0x94, "Right Double Quotation Mark"}, - {0x82, "Single Low-9 Quotation Mark"}, - {0x84, "Double Low-9 Quotation Mark"}, - {0xAB, "Left-Pointing Double Angle Quotation Mark"}, - {0xBB, "Right-Pointing Double Angle Quotation Mark"}, - {0x8B, "Single Left-Pointing Angle Quotation Mark"}, - {0x9B, "Single Right-Pointing Angle Quotation Mark"}, - {0x96, "En Dash"}, - {0x97, "Em Dash"}, - {0x85, "Horizontal Ellipsis"}, - {0x95, "Bullet"}, - {0xB7, "Middle Dot"}, - {0x86, "Dagger"}, - {0x87, "Double Dagger"}, - {0xB6, "Pilcrow Sign"}, - {0xA7, "Section Sign"}, - {0x89, "Per Mille Sign"}, + {0x00A1, "Inverted Exclamation Mark"}, + {0x00BF, "Inverted Question Mark"}, + {0x2018, "Left Single Quotation Mark"}, + {0x2019, "Right Single Quotation Mark"}, + {0x201C, "Left Double Quotation Mark"}, + {0x201D, "Right Double Quotation Mark"}, + {0x201A, "Single Low-9 Quotation Mark"}, + {0x201E, "Double Low-9 Quotation Mark"}, + {0x00AB, "Left-Pointing Double Angle Quotation Mark"}, + {0x00BB, "Right-Pointing Double Angle Quotation Mark"}, + {0x2039, "Single Left-Pointing Angle Quotation Mark"}, + {0x203A, "Single Right-Pointing Angle Quotation Mark"}, + {0x2013, "En Dash"}, + {0x2014, "Em Dash"}, + {0x2026, "Horizontal Ellipsis"}, + {0x2022, "Bullet"}, + {0x00B7, "Middle Dot"}, + {0x2020, "Dagger"}, + {0x2021, "Double Dagger"}, + {0x00B6, "Pilcrow Sign"}, + {0x00A7, "Section Sign"}, + {0x2030, "Per Mille Sign"}, }; // Currency symbols. static const Glyph CURRENCY[] = { - {0x24, "Dollar Sign"}, - {0xA2, "Cent Sign"}, - {0xA3, "Pound Sign"}, - {0xA5, "Yen Sign"}, - {0x80, "Euro Sign"}, - {0xA4, "Currency Sign"}, - {0x83, "Latin Small Letter F With Hook (Florin)"}, + {0x0024, "Dollar Sign"}, + {0x00A2, "Cent Sign"}, + {0x00A3, "Pound Sign"}, + {0x00A5, "Yen Sign"}, + {0x20AC, "Euro Sign"}, + {0x00A4, "Currency Sign"}, + {0x0192, "Latin Small Letter F With Hook (Florin)"}, }; // Math, technical, and miscellaneous symbols. static const Glyph SYMBOLS[] = { - {0xA9, "Copyright Sign"}, - {0xAE, "Registered Sign"}, - {0x99, "Trade Mark Sign"}, - {0xB0, "Degree Sign"}, - {0xB1, "Plus-Minus Sign"}, - {0xD7, "Multiplication Sign"}, - {0xF7, "Division Sign"}, - {0xB5, "Micro Sign"}, - {0xAC, "Not Sign"}, - {0xBC, "Vulgar Fraction One Quarter"}, - {0xBD, "Vulgar Fraction One Half"}, - {0xBE, "Vulgar Fraction Three Quarters"}, - {0xB9, "Superscript One"}, - {0xB2, "Superscript Two"}, - {0xB3, "Superscript Three"}, - {0xAA, "Feminine Ordinal Indicator"}, - {0xBA, "Masculine Ordinal Indicator"}, - {0xA6, "Broken Bar"}, - {0xB4, "Acute Accent"}, - {0xA8, "Diaeresis"}, - {0xAF, "Macron"}, - {0xB8, "Cedilla"}, - {0x88, "Modifier Letter Circumflex Accent"}, - {0x98, "Small Tilde"}, + {0x00A9, "Copyright Sign"}, + {0x00AE, "Registered Sign"}, + {0x2122, "Trade Mark Sign"}, + {0x00B0, "Degree Sign"}, + {0x00B1, "Plus-Minus Sign"}, + {0x00D7, "Multiplication Sign"}, + {0x00F7, "Division Sign"}, + {0x00B5, "Micro Sign"}, + {0x00AC, "Not Sign"}, + {0x00BC, "Vulgar Fraction One Quarter"}, + {0x00BD, "Vulgar Fraction One Half"}, + {0x00BE, "Vulgar Fraction Three Quarters"}, + {0x00B9, "Superscript One"}, + {0x00B2, "Superscript Two"}, + {0x00B3, "Superscript Three"}, + {0x00AA, "Feminine Ordinal Indicator"}, + {0x00BA, "Masculine Ordinal Indicator"}, + {0x00A6, "Broken Bar"}, + {0x00B4, "Acute Accent"}, + {0x00A8, "Diaeresis"}, + {0x00AF, "Macron"}, + {0x00B8, "Cedilla"}, + {0x02C6, "Modifier Letter Circumflex Accent"}, + {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])) } @@ -192,6 +241,7 @@ static const CharSet SETS[] = { SET(PUNCT, "Punctuation"), SET(CURRENCY, "Currency"), SET(SYMBOLS, "Symbols"), + SET(GREEK, "Greek"), }; static constexpr int SET_COUNT = (int)(sizeof(SETS) / sizeof(SETS[0])); @@ -319,29 +369,26 @@ static void ensure_visible(const Layout& L) { // ==== Helpers ==== -static void glyph_str(uint8_t byte, char out[2]) { - out[0] = (char)byte; - out[1] = '\0'; -} - -static int codepoint_of(uint8_t byte) { - return decode_single_byte_codepoint((int)byte); +static int glyph_str(uint32_t cp, char out[5]) { + int n = montauk::utf8::encode(cp, out); + out[n] = '\0'; + return n; } static void copy_selected() { const Glyph& g = cur_set().glyphs[g_sel]; - char s[2]; - glyph_str(g.byte, s); - gui::clipboard_set_text(s, 1); + char s[5]; + int n = glyph_str(g.cp, s); + gui::clipboard_set_text(s, n); g_copied_until = montauk::get_milliseconds() + COPIED_MS; } // ==== 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) { - char s[2]; - glyph_str(byte, s); + char s[5]; + glyph_str(cp, s); int tw = text_width(fonts::system_font, s, size); int th = text_height(fonts::system_font, size); 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 + 1, r.y + 1, r.w - 2, r.h - 2, 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) ---- @@ -407,14 +454,21 @@ static void render(Canvas& c) { g_theme.radius_md, g_theme.border); c.fill_rounded_rect(preview.x + 1, preview.y + 1, preview.w - 2, preview.h - 2, 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. - 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; char meta[64]; - snprintf(meta, sizeof(meta), "U+%04X dec %d Alt 0%d", - cp, cp, (int)sg.byte); + snprintf(meta, sizeof(meta), "U+%04X dec %d UTF-8 %s", + cp, cp, utf8_hex); int name_h = text_height(title_font, NAME_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; bool redraw = false; - switch (key.scancode) { + switch (montauk::nav_scancode(key)) { case 0x4B: // Left if (g_sel > 0) { g_sel--; ensure_visible(L); redraw = true; } break; diff --git a/programs/src/crashpad/main.cpp b/programs/src/crashpad/main.cpp index ad00a64..961e1cc 100644 --- a/programs/src/crashpad/main.cpp +++ b/programs/src/crashpad/main.cpp @@ -306,12 +306,12 @@ extern "C" void _start() { if (ev.type == 0 && ev.key.pressed) { if (ev.key.scancode == 0x01) break; // Escape - if (ev.key.scancode == 0x51) { + if (montauk::nav_scancode(ev.key) == 0x51) { // Page Down: scroll down g_scroll += 60; render(); } - if (ev.key.scancode == 0x49) { + if (montauk::nav_scancode(ev.key) == 0x49) { // Page Up: scroll up g_scroll -= 60; if (g_scroll < 0) g_scroll = 0; diff --git a/programs/src/desktop/apps/filemanager/events.cpp b/programs/src/desktop/apps/filemanager/events.cpp index 046950f..f5f31c7 100644 --- a/programs/src/desktop/apps/filemanager/events.cpp +++ b/programs/src/desktop/apps/filemanager/events.cpp @@ -351,36 +351,21 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) { } else if (key.scancode == 0x01) { filemanager_cancel_pathbar(fm); } else if (key.ascii == '\b' || key.scancode == 0x0E) { - if (fm->pathbar_cursor > 0) { - for (int i = fm->pathbar_cursor - 1; i < fm->pathbar_len - 1; i++) - fm->pathbar_buf[i] = fm->pathbar_buf[i + 1]; - fm->pathbar_len--; - fm->pathbar_cursor--; - fm->pathbar_buf[fm->pathbar_len] = '\0'; - } - } else if (key.scancode == 0x53) { - if (fm->pathbar_cursor < fm->pathbar_len) { - 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) { + montauk::utf8::erase_before(fm->pathbar_buf, &fm->pathbar_len, &fm->pathbar_cursor); + } else if (montauk::nav_scancode(key) == 0x53) { + montauk::utf8::erase_at(fm->pathbar_buf, &fm->pathbar_len, fm->pathbar_cursor); + } else if (montauk::nav_scancode(key) == 0x4B) { + fm->pathbar_cursor = montauk::utf8::prev_boundary(fm->pathbar_buf, fm->pathbar_cursor); + } else if (montauk::nav_scancode(key) == 0x4D) { + fm->pathbar_cursor = montauk::utf8::next_boundary(fm->pathbar_buf, fm->pathbar_len, + fm->pathbar_cursor); + } else if (montauk::nav_scancode(key) == 0x47) { fm->pathbar_cursor = 0; - } else if (key.scancode == 0x4F) { + } else if (montauk::nav_scancode(key) == 0x4F) { fm->pathbar_cursor = fm->pathbar_len; - } else if ((unsigned char)key.ascii >= 32 - && (unsigned char)key.ascii != 127 && fm->pathbar_len < 254) { - for (int i = fm->pathbar_len; i > fm->pathbar_cursor; i--) - 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'; + } else { + montauk::utf8::insert_key(fm->pathbar_buf, &fm->pathbar_len, 255, + &fm->pathbar_cursor, key); } return; } @@ -394,44 +379,27 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) { filemanager_cancel_rename(fm); } else if (key.ascii == '\b' || key.scancode == 0x0E) { // Backspace - if (fm->rename_cursor > 0) { - for (int i = fm->rename_cursor - 1; i < fm->rename_len - 1; i++) - 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) { + montauk::utf8::erase_before(fm->rename_buf, &fm->rename_len, &fm->rename_cursor); + } else if (montauk::nav_scancode(key) == 0x53) { // Delete key - if (fm->rename_cursor < fm->rename_len) { - for (int i = fm->rename_cursor; i < fm->rename_len - 1; i++) - fm->rename_buf[i] = fm->rename_buf[i + 1]; - fm->rename_len--; - fm->rename_buf[fm->rename_len] = '\0'; - } - } else if (key.scancode == 0x4B) { + montauk::utf8::erase_at(fm->rename_buf, &fm->rename_len, fm->rename_cursor); + } else if (montauk::nav_scancode(key) == 0x4B) { // Left arrow - if (fm->rename_cursor > 0) fm->rename_cursor--; - } else if (key.scancode == 0x4D) { + fm->rename_cursor = montauk::utf8::prev_boundary(fm->rename_buf, fm->rename_cursor); + } else if (montauk::nav_scancode(key) == 0x4D) { // Right arrow - if (fm->rename_cursor < fm->rename_len) fm->rename_cursor++; - } else if (key.scancode == 0x47) { + fm->rename_cursor = montauk::utf8::next_boundary(fm->rename_buf, fm->rename_len, + fm->rename_cursor); + } else if (montauk::nav_scancode(key) == 0x47) { // Home fm->rename_cursor = 0; - } else if (key.scancode == 0x4F) { + } else if (montauk::nav_scancode(key) == 0x4F) { // End fm->rename_cursor = fm->rename_len; - } else if ((unsigned char)key.ascii >= 32 - && (unsigned char)key.ascii != 127) { + } else if (key.ascii != '/' && key.ascii != '\\') { // Printable character (reject / and other FS-unsafe chars) - if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) { - for (int i = fm->rename_len; i > fm->rename_cursor; i--) - 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'; - } + montauk::utf8::insert_key(fm->rename_buf, &fm->rename_len, 63, + &fm->rename_cursor, key); } return; } @@ -466,7 +434,7 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) { if (key.ascii == '\b' || key.scancode == 0x0E) { filemanager_go_up(fm); - } else if (key.scancode == 0x48) { + } else if (montauk::nav_scancode(key) == 0x48) { // Up arrow if (fm->grid_view) { 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--; } filemanager_scroll_to_selected(win, fm); - } else if (key.scancode == 0x50) { + } else if (montauk::nav_scancode(key) == 0x50) { // Down arrow if (fm->grid_view) { 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++; } 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) 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) if (fm->selected < fm->entry_count - 1) fm->selected++; } else if (key.ascii == '\n' || key.ascii == '\r') { if (fm->selected >= 0 && fm->selected < fm->entry_count) filemanager_open_entry(fm, fm->selected); - } else if (key.scancode == 0x53) { + } else if (montauk::nav_scancode(key) == 0x53) { // Delete key filemanager_delete_selected(fm); - } else if (key.alt && key.scancode == 0x4B) { + } else if (key.alt && montauk::nav_scancode(key) == 0x4B) { // Alt+Left: go back filemanager_go_back(fm); - } else if (key.alt && key.scancode == 0x4D) { + } else if (key.alt && montauk::nav_scancode(key) == 0x4D) { // Alt+Right: go forward filemanager_go_forward(fm); } diff --git a/programs/src/desktop/apps/filemanager/properties.cpp b/programs/src/desktop/apps/filemanager/properties.cpp index 8c4a28d..3eb3e0f 100644 --- a/programs/src/desktop/apps/filemanager/properties.cpp +++ b/programs/src/desktop/apps/filemanager/properties.cpp @@ -65,10 +65,10 @@ static void props_fit_text(const char* src, char* out, int out_len, int max_w) { while (len > 0) { out[len] = '\0'; 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; out[len] = '.'; out[len + 1] = '.'; diff --git a/programs/src/desktop/apps/filemanager/render.cpp b/programs/src/desktop/apps/filemanager/render.cpp index 402e46b..7922bab 100644 --- a/programs/src/desktop/apps/filemanager/render.cpp +++ b/programs/src/desktop/apps/filemanager/render.cpp @@ -32,12 +32,14 @@ static int fitted_prefix_len(const char* src, int max_w, int max_len) { char tmp[128]; int best = 0; 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); if (text_width(tmp) > max_w) break; 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) { @@ -49,10 +51,10 @@ static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int while (len > 0) { dst[len] = '\0'; 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; dst[len] = '.'; dst[len + 1] = '.'; @@ -403,7 +405,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) { // Draw rename text (truncated to cell width) char display[16]; 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]; display[rlen] = '\0'; 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; if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8; 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) { int ellipsis_w = text_width("..."); - int final_len = montauk::slen(name); + int final_len = montauk::slen(label); while (final_len > 1) { label[final_len] = '\0'; int w = text_width(label); 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 + 1] = '.'; label[final_len + 2] = '.'; diff --git a/programs/src/desktop/apps/settings/about.cpp b/programs/src/desktop/apps/settings/about.cpp index 615ef64..8e18a81 100644 --- a/programs/src/desktop/apps/settings/about.cpp +++ b/programs/src/desktop/apps/settings/about.cpp @@ -44,7 +44,7 @@ void settings_draw_about(Canvas& c, SettingsState* st) { mtk::draw_separator(c, x, y, c.w - 2 * x, theme); y += 12; - c.text(x, y, "Copyright \xa9 2026 Montauk Operating System Project", theme.text_muted); + c.text(x, y, "Copyright \u00A9 2026 Montauk Operating System Project", theme.text_muted); } } // namespace settings_app diff --git a/programs/src/desktop/compose.cpp b/programs/src/desktop/compose.cpp index 3f47102..ac5a23e 100644 --- a/programs/src/desktop/compose.cpp +++ b/programs/src/desktop/compose.cpp @@ -103,7 +103,7 @@ void desktop_draw_lock_screen(DesktopState* ds) { // Masked password text (clipped to field width) 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; int max_text_w = content_w - 10 - 10 - 2; // left pad, right pad, cursor // Show only the trailing portion that fits diff --git a/programs/src/desktop/consent.cpp b/programs/src/desktop/consent.cpp index 854a788..87a58db 100644 --- a/programs/src/desktop/consent.cpp +++ b/programs/src/desktop/consent.cpp @@ -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]; int len = montauk::slen(last); 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'; } return count; diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index cdc1dce..e5923ce 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -98,22 +98,12 @@ static void handle_lock_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& // Backspace if (key.ascii == '\b' || key.scancode == 0x0E) { - if (ds->lock_password_len > 0) { - ds->lock_password_len--; - ds->lock_password[ds->lock_password_len] = '\0'; - } + montauk::utf8::erase_last(ds->lock_password, &ds->lock_password_len); return; } - // Printable characters - if ((unsigned char)key.ascii >= 0x20 - && (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'; - } - } + // Printable characters (stored as UTF-8) + montauk::utf8::append_key(ds->lock_password, &ds->lock_password_len, 64, key); } static void forward_external_mouse(gui::Window* win, diff --git a/programs/src/desktop/launcher.cpp b/programs/src/desktop/launcher.cpp index 69ef62a..cfcf10c 100644 --- a/programs/src/desktop/launcher.cpp +++ b/programs/src/desktop/launcher.cpp @@ -361,7 +361,7 @@ static void launcher_activate_selected(DesktopState* ds) { } 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) { ds->super_left_down = key.pressed; } 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) { 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; } @@ -420,7 +420,7 @@ void desktop_close_launcher(DesktopState* ds) { bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& 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) { return true; @@ -456,11 +456,8 @@ bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyE } if (key.ascii == '\b' || sc == 0x0E) { - if (ds->launcher_query_len > 0) { - ds->launcher_query_len--; - ds->launcher_query[ds->launcher_query_len] = '\0'; + if (montauk::utf8::erase_last(ds->launcher_query, &ds->launcher_query_len)) desktop_update_launcher_results(ds); - } return true; } @@ -495,12 +492,8 @@ bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyE return true; } - if ((unsigned char)key.ascii >= 0x20 - && (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'; + if (montauk::utf8::append_key(ds->launcher_query, &ds->launcher_query_len, 64, key)) desktop_update_launcher_results(ds); - } return true; } diff --git a/programs/src/devexplorer/main.cpp b/programs/src/devexplorer/main.cpp index 5d0681d..66e6b85 100644 --- a/programs/src/devexplorer/main.cpp +++ b/programs/src/devexplorer/main.cpp @@ -280,7 +280,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { int row_count = build_display_rows(&de, rows); 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; else de.selected_row--; // 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; if (sel_top < de.scroll_y) 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) de.selected_row++; // 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; if (sel_top + sel_h > de.scroll_y + 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) { int cat = rows[de.selected_row].category; 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) { int cat = rows[de.selected_row].category; de.collapsed[cat] = false; diff --git a/programs/src/dialogs/filedialog.cpp b/programs/src/dialogs/filedialog.cpp index ed465a5..0b7f21c 100644 --- a/programs/src/dialogs/filedialog.cpp +++ b/programs/src/dialogs/filedialog.cpp @@ -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 // insufficient for the proportional system font and lets wide names paint // over the adjacent label. - int keep = montauk::slen(src); - if (keep > out_len - 3) keep = out_len - 3; + int slen = montauk::slen(src); + int keep = slen; + if (keep > out_len - 3) keep = montauk::utf8::floor_boundary(src, slen, out_len - 3); while (keep > 0) { for (int i = 0; i < keep; i++) out[i] = src[i]; out[keep] = '.'; out[keep + 1] = '.'; out[keep + 2] = '\0'; if (text_width(out) <= max_px) return; - keep--; + keep = montauk::utf8::prev_boundary(src, keep); } safe_copy(out, out_len, ".."); @@ -1216,14 +1217,14 @@ void handle_file_key(const montauk::abi::KeyEvent& key) { 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--; else if (st->entry_count > 0) st->selected = 0; file_ensure_selected_visible(st); 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++; else if (st->entry_count > 0) st->selected = st->entry_count - 1; file_ensure_selected_visible(st); diff --git a/programs/src/dialogs/messagebox.cpp b/programs/src/dialogs/messagebox.cpp index 05de72e..e1eb71e 100644 --- a/programs/src/dialogs/messagebox.cpp +++ b/programs/src/dialogs/messagebox.cpp @@ -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); 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, "..."); diff --git a/programs/src/dialogs/printdialog.cpp b/programs/src/dialogs/printdialog.cpp index 7e7685f..b82ae3c 100644 --- a/programs/src/dialogs/printdialog.cpp +++ b/programs/src/dialogs/printdialog.cpp @@ -480,13 +480,13 @@ void handle_print_key(const montauk::abi::KeyEvent& key) { g_print.error[0] = '\0'; 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--; else g_print.selected_printer = 0; g_print.error[0] = '\0'; 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++; else g_print.selected_printer = g_print.printer_count - 1; g_print.error[0] = '\0'; diff --git a/programs/src/disks/main.cpp b/programs/src/disks/main.cpp index cba8edd..1fd7ec3 100644 --- a/programs/src/disks/main.cpp +++ b/programs/src/disks/main.cpp @@ -339,9 +339,9 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { if (dt.fmt_dlg.open) { if (key.scancode == 0x01) { // Escape 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--; - } 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++; } else if (key.ascii == '\n' || key.ascii == '\r') { format_dialog_do_format(); @@ -354,14 +354,14 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { if (key.scancode == 0x01) { // Escape 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--; 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++; - } 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; } - } 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; } } else if (key.ascii == 'n' || key.ascii == 'N') { do_create_partition(); diff --git a/programs/src/disks/render.cpp b/programs/src/disks/render.cpp index 5b4e7e6..ababe6c 100644 --- a/programs/src/disks/render.cpp +++ b/programs/src/disks/render.cpp @@ -53,6 +53,8 @@ static void draw_text_fit(Canvas& c, int x, int y, out++; } + out = montauk::utf8::floor_boundary(text, montauk::slen(text), out); + if (out <= 0) return; buf[out] = '.'; buf[out + 1] = '.'; diff --git a/programs/src/display/main.cpp b/programs/src/display/main.cpp index 503a134..77455b2 100644 --- a/programs/src/display/main.cpp +++ b/programs/src/display/main.cpp @@ -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; len++; } + len = montauk::utf8::floor_boundary(text, montauk::slen(text), len); out[len++] = '.'; out[len++] = '.'; out[len++] = '.'; diff --git a/programs/src/edit/main.cpp b/programs/src/edit/main.cpp index 1884a0a..9695220 100644 --- a/programs/src/edit/main.cpp +++ b/programs/src/edit/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using montauk::slen; using montauk::memcpy; @@ -166,6 +167,16 @@ static void line_delete_char(Line* ln, int pos) { 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) { line_ensure(ln, ln->len + slen); memcpy(ln->data + ln->len, s, slen); @@ -300,6 +311,21 @@ static void load_file(const char* fname) { } 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 numLines = 0; if (buf && size > 0) { @@ -325,7 +351,7 @@ static void load_file(const char* fname) { 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() { @@ -413,14 +439,13 @@ static int prompt_input(const char* promptStr, char* out, int outMax) { } if (ev.ascii == '\b') { - if (pos > 0) { - pos--; + if (montauk::utf8::erase_last(out, &pos)) { putch('\b'); putch(' '); putch('\b'); } - } else if (ev.ascii >= ' ' && pos < outMax - 1) { - out[pos++] = ev.ascii; - out[pos] = '\0'; - putch(ev.ascii); + } else { + int before = pos; + if (montauk::utf8::append_key(out, &pos, outMax, ev)) + print(out + before); } } } @@ -506,14 +531,15 @@ static void draw_line(int screenRow, int docLine) { reset_attrs(); // 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]; - int startCol = leftCol; + int startCol = line_byte_col(ln, leftCol); int maxChars = screenCols - gutterWidth; - int visibleLen = 0; - if (startCol < ln->len) { - visibleLen = ln->len - startCol; - if (visibleLen > maxChars) visibleLen = maxChars; - } + int endCol = startCol; + for (int n = 0; n < maxChars && endCol < ln->len; n++) + endCol = montauk::utf8::next_boundary(ln->data, ln->len, endCol); + int visibleLen = endCol - startCol; if (synLang != SYN_LANG_NONE) { // Tokenize the WHOLE line, not just the visible window: the @@ -577,7 +603,7 @@ static void render() { // Position cursor 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); show_cursor(); @@ -598,12 +624,13 @@ static void scroll() { // Horizontal scrolling int textCols = screenCols - gutterWidth; - if (cursorCol < leftCol) { - leftCol = cursorCol; + int displayCol = line_display_col(&lines[cursorRow], cursorCol); + if (displayCol < leftCol) { + leftCol = displayCol; fullRedraw = true; } - if (cursorCol >= leftCol + textCols) { - leftCol = cursorCol - textCols + 1; + if (displayCol >= leftCol + textCols) { + leftCol = displayCol - textCols + 1; fullRedraw = true; } } @@ -617,6 +644,10 @@ static void insert_char(char c) { 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() { for (int i = 0; i < TAB_WIDTH; i++) { insert_char(' '); @@ -647,8 +678,11 @@ static void insert_newline() { static void delete_char_backspace() { if (cursorCol > 0) { - line_delete_char(&lines[cursorRow], cursorCol - 1); - cursorCol--; + int start = montauk::utf8::prev_boundary(lines[cursorRow].data, cursorCol); + while (cursorCol > start) { + line_delete_char(&lines[cursorRow], cursorCol - 1); + cursorCol--; + } modified = true; fullRedraw = true; } else if (cursorRow > 0) { @@ -665,7 +699,9 @@ static void delete_char_backspace() { static void delete_char_forward() { 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; fullRedraw = true; } else if (cursorRow < numLines - 1) { @@ -793,26 +829,26 @@ static void handle_key(const montauk::abi::KeyEvent& ev) { // Non-ASCII keys (scancode-based) if (ev.ascii == 0) { - switch (ev.scancode) { + switch (montauk::nav_scancode(ev)) { case SC_UP: if (cursorRow > 0) { + int col = line_display_col(&lines[cursorRow], cursorCol); cursorRow--; - if (cursorCol > lines[cursorRow].len) - cursorCol = lines[cursorRow].len; + cursorCol = line_byte_col(&lines[cursorRow], col); fullRedraw = true; } return; case SC_DOWN: if (cursorRow < numLines - 1) { + int col = line_display_col(&lines[cursorRow], cursorCol); cursorRow++; - if (cursorCol > lines[cursorRow].len) - cursorCol = lines[cursorRow].len; + cursorCol = line_byte_col(&lines[cursorRow], col); fullRedraw = true; } return; case SC_LEFT: if (cursorCol > 0) { - cursorCol--; + cursorCol = montauk::utf8::prev_boundary(lines[cursorRow].data, cursorCol); } else if (cursorRow > 0) { cursorRow--; cursorCol = lines[cursorRow].len; @@ -821,7 +857,8 @@ static void handle_key(const montauk::abi::KeyEvent& ev) { return; case SC_RIGHT: if (cursorCol < lines[cursorRow].len) { - cursorCol++; + cursorCol = montauk::utf8::next_boundary(lines[cursorRow].data, + lines[cursorRow].len, cursorCol); } else if (cursorRow < numLines - 1) { cursorRow++; cursorCol = 0; @@ -836,20 +873,22 @@ static void handle_key(const montauk::abi::KeyEvent& ev) { cursorCol = lines[cursorRow].len; fullRedraw = true; return; - case SC_PGUP: + case SC_PGUP: { + int col = line_display_col(&lines[cursorRow], cursorCol); cursorRow -= editorRows; if (cursorRow < 0) cursorRow = 0; - if (cursorCol > lines[cursorRow].len) - cursorCol = lines[cursorRow].len; + cursorCol = line_byte_col(&lines[cursorRow], col); fullRedraw = true; return; - case SC_PGDN: + } + case SC_PGDN: { + int col = line_display_col(&lines[cursorRow], cursorCol); cursorRow += editorRows; if (cursorRow >= numLines) cursorRow = numLines - 1; - if (cursorCol > lines[cursorRow].len) - cursorCol = lines[cursorRow].len; + cursorCol = line_byte_col(&lines[cursorRow], col); fullRedraw = true; return; + } case SC_DELETE: delete_char_forward(); return; @@ -869,11 +908,11 @@ static void handle_key(const montauk::abi::KeyEvent& ev) { case '\t': insert_tab(); break; - default: - if (ev.ascii >= ' ') { - insert_char(ev.ascii); - } + default: { + char typed[montauk::utf8::MAX_BYTES]; + insert_bytes(typed, montauk::utf8::key_utf8(ev, typed)); break; + } } } diff --git a/programs/src/fontpreview/main.cpp b/programs/src/fontpreview/main.cpp index 8857eb1..1dd1110 100644 --- a/programs/src/fontpreview/main.cpp +++ b/programs/src/fontpreview/main.cpp @@ -402,12 +402,12 @@ extern "C" void _start() { if (ev.key.ascii == 'q' || ev.key.ascii == 'Q' || ev.key.scancode == 0x01) break; - if (ev.key.scancode == 0x48) { g_scroll_y -= SCROLL_STEP; redraw = true; } - if (ev.key.scancode == 0x50) { g_scroll_y += SCROLL_STEP; redraw = true; } - if (ev.key.scancode == 0x47) { g_scroll_y = 0; redraw = true; } - if (ev.key.scancode == 0x4F) { g_scroll_y = g_content_h; redraw = true; } - if (ev.key.scancode == 0x49) { g_scroll_y -= g_win_h; redraw = true; } - if (ev.key.scancode == 0x51) { g_scroll_y += g_win_h; redraw = true; } + if (montauk::nav_scancode(ev.key) == 0x48) { g_scroll_y -= SCROLL_STEP; redraw = true; } + if (montauk::nav_scancode(ev.key) == 0x50) { g_scroll_y += SCROLL_STEP; redraw = true; } + if (montauk::nav_scancode(ev.key) == 0x47) { g_scroll_y = 0; redraw = true; } + if (montauk::nav_scancode(ev.key) == 0x4F) { g_scroll_y = g_content_h; redraw = true; } + if (montauk::nav_scancode(ev.key) == 0x49) { g_scroll_y -= g_win_h; redraw = true; } + if (montauk::nav_scancode(ev.key) == 0x51) { g_scroll_y += g_win_h; redraw = true; } if (redraw) { clamp_scroll(); diff --git a/programs/src/imageviewer/main.cpp b/programs/src/imageviewer/main.cpp index ad6bdb8..34eb331 100644 --- a/programs/src/imageviewer/main.cpp +++ b/programs/src/imageviewer/main.cpp @@ -620,13 +620,13 @@ extern "C" void _start() { else if (key.ctrl && (key.ascii == '-' || key.ascii == 31)) { zoom_out(cx, cy); redraw = true; } // Arrow keys to pan - else if (key.scancode == 0x48) { g_pan_y += PAN_STEP; redraw = true; } - else if (key.scancode == 0x50) { g_pan_y -= PAN_STEP; redraw = true; } - else if (key.scancode == 0x4B) { g_pan_x += PAN_STEP; redraw = true; } - else if (key.scancode == 0x4D) { g_pan_x -= PAN_STEP; redraw = true; } + else if (montauk::nav_scancode(key) == 0x48) { g_pan_y += PAN_STEP; redraw = true; } + else if (montauk::nav_scancode(key) == 0x50) { g_pan_y -= PAN_STEP; redraw = true; } + else if (montauk::nav_scancode(key) == 0x4B) { g_pan_x += PAN_STEP; redraw = true; } + else if (montauk::nav_scancode(key) == 0x4D) { g_pan_x -= PAN_STEP; redraw = true; } // Home to center - else if (key.scancode == 0x47) { center_image(); redraw = true; } + else if (montauk::nav_scancode(key) == 0x47) { center_image(); redraw = true; } if (redraw && g_load_ok) clamp_pan(); } diff --git a/programs/src/installer/main.cpp b/programs/src/installer/main.cpp index 032147d..58b26d2 100644 --- a/programs/src/installer/main.cpp +++ b/programs/src/installer/main.cpp @@ -333,14 +333,14 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { return true; } - if (key.scancode == 0x48) { // Up + if (montauk::nav_scancode(key) == 0x48) { // Up if (st.step == STEP_SELECT_DISK && st.selected_disk > 0) st.selected_disk--; else if (st.step == STEP_UPDATE_SELECT_PART && st.selected_part > 0) st.selected_part--; else if (st.step == STEP_PARTITION_SCHEME && st.partition_scheme > 0) st.partition_scheme--; - } else if (key.scancode == 0x50) { // Down + } else if (montauk::nav_scancode(key) == 0x50) { // Down if (st.step == STEP_SELECT_DISK && st.selected_disk < st.disk_count - 1) st.selected_disk++; else if (st.step == STEP_UPDATE_SELECT_PART && st.selected_part < st.part_count - 1) diff --git a/programs/src/installer/render.cpp b/programs/src/installer/render.cpp index 3f4c717..052bfc3 100644 --- a/programs/src/installer/render.cpp +++ b/programs/src/installer/render.cpp @@ -48,6 +48,8 @@ static void draw_text_fit(Canvas& c, int x, int y, out++; } + out = montauk::utf8::floor_boundary(text, montauk::slen(text), out); + if (out <= 0) return; buf[out] = '.'; buf[out + 1] = '.'; diff --git a/programs/src/irc/main.cpp b/programs/src/irc/main.cpp index 585d1fc..7b22017 100644 --- a/programs/src/irc/main.cpp +++ b/programs/src/irc/main.cpp @@ -7,6 +7,7 @@ #include #include +#include #include using montauk::slen; @@ -621,7 +622,7 @@ static void ui_render() { for (int i = 0; i < input.len; i++) sb_putc(input.buf[i]); // Position cursor at input insertion point - sb_cursor_to(term.rows, input.pos + 3); + sb_cursor_to(term.rows, montauk::utf8::count(input.buf, input.pos) + 3); // Single flush — entire screen in one print call sb_flush(); @@ -892,7 +893,7 @@ extern "C" void _start() { } // Handle special keys by scancode - switch (ev.scancode) { + switch (montauk::nav_scancode(ev)) { case 0x49: // Page Up if (msgBuf.scrollOffset < msgBuf.count - term.msgAreaRows) { msgBuf.scrollOffset += term.msgAreaRows / 2; @@ -922,22 +923,12 @@ extern "C" void _start() { input.len = 0; } } else if (ev.ascii == '\b') { - if (input.pos > 0) { - for (int j = input.pos - 1; j < input.len - 1; j++) { - input.buf[j] = input.buf[j + 1]; - } - input.pos--; - input.len--; - } - } else if (ev.ascii >= ' ' && ev.ascii <= '~') { - if (input.len < 510) { - for (int j = input.len; j > input.pos; j--) { - input.buf[j] = input.buf[j - 1]; - } - input.buf[input.pos] = ev.ascii; - input.pos++; - input.len++; - } + input.buf[input.len] = '\0'; + montauk::utf8::erase_before(input.buf, &input.len, &input.pos); + } else if (montauk::utf8::key_codepoint(ev)) { + // IRC text is UTF-8 + input.buf[input.len] = '\0'; + montauk::utf8::insert_key(input.buf, &input.len, 511, &input.pos, ev); } else { dirty = false; // Unhandled key, no redraw needed } diff --git a/programs/src/keyboard/main.cpp b/programs/src/keyboard/main.cpp index 98c5128..29d336c 100644 --- a/programs/src/keyboard/main.cpp +++ b/programs/src/keyboard/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -176,14 +177,16 @@ static void draw_layouts_tab(Canvas& c, const mtk::Theme& theme) { // Key Map tab: what the active layout actually remaps. Rows are identified by // the character the key produces unshifted, so no copy of the US scancode // table is needed here to name the physical keys. -static void draw_char_cell(Canvas& c, int x, int y, uint8_t byte, +static void draw_char_cell(Canvas& c, int x, int y, uint32_t cp, bool dead, const mtk::Theme& theme) { - char text[2] = {(char)byte, '\0'}; - if (byte == 0) { + if (cp == 0) { c.text(x, y, "-", theme.text_muted); - } else { - c.text(x, y, text, theme.text); + return; } + char text[5]; + text[montauk::utf8::encode(cp, text)] = '\0'; + // Dead keys (accents that combine with the next letter) in the accent color + c.text(x, y, text, dead ? theme.accent : theme.text); } static void draw_keymap_tab(Canvas& c, const mtk::Theme& theme) { @@ -220,9 +223,12 @@ static void draw_keymap_tab(Canvas& c, const mtk::Theme& theme) { if (index >= layout.key_count) break; const montauk::keyboard::KeyMap& key = layout.keys[index]; int y = list_y() + row * MAP_ROW_H; - draw_char_cell(c, gx + col[0], y, key.base, theme); - draw_char_cell(c, gx + col[1], y, key.shift, theme); - draw_char_cell(c, gx + col[2], y, key.altgr, theme); + draw_char_cell(c, gx + col[0], y, key.base, + montauk::keyboard::is_dead_key(layout, key.base), theme); + draw_char_cell(c, gx + col[1], y, key.shift, + montauk::keyboard::is_dead_key(layout, key.shift), theme); + draw_char_cell(c, gx + col[2], y, key.altgr, + montauk::keyboard::is_dead_key(layout, key.altgr), theme); } } diff --git a/programs/src/man/main.cpp b/programs/src/man/main.cpp index 7d9b303..b0659d7 100644 --- a/programs/src/man/main.cpp +++ b/programs/src/man/main.cpp @@ -319,7 +319,7 @@ extern "C" void _start() { break; default: // Handle scancodes for special keys - switch (ev.scancode) { + switch (montauk::nav_scancode(ev)) { case 0x48: // Up arrow if (scroll > 0) scroll--; break; diff --git a/programs/src/music/main.cpp b/programs/src/music/main.cpp index 766ff11..8ad53b1 100644 --- a/programs/src/music/main.cpp +++ b/programs/src/music/main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -415,11 +416,6 @@ static char ascii_lower(char c) { return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; } -static bool is_printable_byte(char c) { - unsigned char value = (unsigned char)c; - return value >= 32 && value != 127; -} - static bool str_contains_case_insensitive(const char* text, const char* needle) { if (!needle || !needle[0]) return true; if (!text || !text[0]) return false; @@ -516,19 +512,16 @@ static void rebuild_filtered_view() { ensure_selected_visible(); } -static void append_filter_char(char c) { +static void append_filter_key(const montauk::abi::KeyEvent& key) { int len = montauk::slen(g.filter_text); - if (len >= MAX_FILTER - 1) return; - g.filter_text[len] = c; - g.filter_text[len + 1] = '\0'; - rebuild_filtered_view(); + if (montauk::utf8::append_key(g.filter_text, &len, MAX_FILTER, key)) + rebuild_filtered_view(); } static void pop_filter_char() { int len = montauk::slen(g.filter_text); - if (len <= 0) return; - g.filter_text[len - 1] = '\0'; - rebuild_filtered_view(); + if (montauk::utf8::erase_last(g.filter_text, &len)) + rebuild_filtered_view(); } static void clear_filter() { @@ -1120,8 +1113,10 @@ static void render(uint32_t* pixels) { int qlen = montauk::slen(query); if (qlen > 15) { montauk::memcpy(filter_query, "...", 3); - montauk::memcpy(filter_query + 3, query + qlen - 15, 15); - filter_query[18] = '\0'; + int start = montauk::utf8::floor_boundary(query, qlen, qlen - 15); + if (start < qlen - 15) start = montauk::utf8::next_boundary(query, qlen, start); + montauk::memcpy(filter_query + 3, query + start, qlen - start); + filter_query[3 + qlen - start] = '\0'; query = filter_query; } @@ -1426,11 +1421,11 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) { return true; } - if (!g.show_visualizer && key.scancode == 0x48) { // Up + if (!g.show_visualizer && montauk::nav_scancode(key) == 0x48) { // Up move_selection(-1); return true; } - if (!g.show_visualizer && key.scancode == 0x50) { // Down + if (!g.show_visualizer && montauk::nav_scancode(key) == 0x50) { // Down move_selection(1); return true; } @@ -1449,8 +1444,8 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) { else g.filter_active = false; return true; } - if (!key.ctrl && !key.alt && is_printable_byte(key.ascii)) { - append_filter_char(key.ascii); + if (!key.ctrl && !key.alt && montauk::utf8::key_codepoint(key)) { + append_filter_key(key); return true; } return false; @@ -1479,11 +1474,11 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) { g.show_visualizer = !g.show_visualizer; return true; } - if (key.scancode == 0x4D) { // Right arrow + if (montauk::nav_scancode(key) == 0x4D) { // Right arrow next_track(false); return true; } - if (key.scancode == 0x4B) { // Left arrow + if (montauk::nav_scancode(key) == 0x4B) { // Left arrow prev_track(); return true; } diff --git a/programs/src/network/main.cpp b/programs/src/network/main.cpp index 2b8fc14..bc4947b 100644 --- a/programs/src/network/main.cpp +++ b/programs/src/network/main.cpp @@ -247,6 +247,7 @@ static void fit_text(char* out, int out_cap, const char* text, int max_w) { break; out_len++; } + out_len = montauk::utf8::floor_boundary(text, montauk::slen(text), out_len); out[out_len] = '.'; out[out_len + 1] = '.'; out[out_len + 2] = '.'; @@ -1216,8 +1217,8 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { wifi_join_selected(); return true; } - if (key.scancode == 0x48 || key.scancode == 0x50) { // up / down - int step = key.scancode == 0x48 ? -1 : 1; + if (montauk::nav_scancode(key) == 0x48 || montauk::nav_scancode(key) == 0x50) { // up / down + int step = montauk::nav_scancode(key) == 0x48 ? -1 : 1; int next = g_wifi_selected < 0 ? 0 : g_wifi_selected + step; if (next < 0) next = 0; if (next >= g_wifi_count) next = g_wifi_count - 1; diff --git a/programs/src/pdfviewer/main.cpp b/programs/src/pdfviewer/main.cpp index ae0d425..36ae05d 100644 --- a/programs/src/pdfviewer/main.cpp +++ b/programs/src/pdfviewer/main.cpp @@ -235,25 +235,13 @@ extern "C" void _start() { } else if (key.scancode == 0x01) { g_pathbar_open = false; } else if (key.ascii == '\b' || key.scancode == 0x0E) { - if (g_pathbar_cursor > 0) { - for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++) - g_pathbar_text[i] = g_pathbar_text[i + 1]; - g_pathbar_len--; - g_pathbar_cursor--; - g_pathbar_text[g_pathbar_len] = '\0'; - } - } else if (key.scancode == 0x4B) { - if (g_pathbar_cursor > 0) g_pathbar_cursor--; - } else if (key.scancode == 0x4D) { - if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++; - } else if ((unsigned char)key.ascii >= 32 - && (unsigned char)key.ascii != 127 && g_pathbar_len < 254) { - for (int i = g_pathbar_len; i > g_pathbar_cursor; i--) - g_pathbar_text[i] = g_pathbar_text[i - 1]; - g_pathbar_text[g_pathbar_cursor] = key.ascii; - g_pathbar_cursor++; - g_pathbar_len++; - g_pathbar_text[g_pathbar_len] = '\0'; + montauk::utf8::erase_before(g_pathbar_text, &g_pathbar_len, &g_pathbar_cursor); + } else if (montauk::nav_scancode(key) == 0x4B) { + g_pathbar_cursor = montauk::utf8::prev_boundary(g_pathbar_text, g_pathbar_cursor); + } else if (montauk::nav_scancode(key) == 0x4D) { + g_pathbar_cursor = montauk::utf8::next_boundary(g_pathbar_text, g_pathbar_len, g_pathbar_cursor); + } else { + montauk::utf8::insert_key(g_pathbar_text, &g_pathbar_len, 255, &g_pathbar_cursor, key); } redraw = true; goto done_keys; @@ -274,9 +262,9 @@ extern "C" void _start() { redraw = true; } // Page Down / Right arrow: next page - else if (key.scancode == 0x51 || key.scancode == 0x4D) { + else if (montauk::nav_scancode(key) == 0x51 || montauk::nav_scancode(key) == 0x4D) { if (g_doc.valid) { - if (key.scancode == 0x4D && g_scroll_y < max_scroll()) { + if (montauk::nav_scancode(key) == 0x4D && g_scroll_y < max_scroll()) { // Right arrow scrolls down within page } else { go_to_page(g_current_page + 1); @@ -285,9 +273,9 @@ extern "C" void _start() { } } // Page Up / Left arrow: previous page - else if (key.scancode == 0x49 || key.scancode == 0x4B) { + else if (montauk::nav_scancode(key) == 0x49 || montauk::nav_scancode(key) == 0x4B) { if (g_doc.valid) { - if (key.scancode == 0x4B && g_scroll_y > 0) { + if (montauk::nav_scancode(key) == 0x4B && g_scroll_y > 0) { // Left arrow scrolls up within page } else { go_to_page(g_current_page - 1); @@ -296,23 +284,23 @@ extern "C" void _start() { } } // Up arrow: scroll up - else if (key.scancode == 0x48) { + else if (montauk::nav_scancode(key) == 0x48) { g_scroll_y -= SCROLL_STEP; clamp_scroll(); redraw = true; } // Down arrow: scroll down - else if (key.scancode == 0x50) { + else if (montauk::nav_scancode(key) == 0x50) { g_scroll_y += SCROLL_STEP; clamp_scroll(); redraw = true; } // Home: first page - else if (key.scancode == 0x47) { + else if (montauk::nav_scancode(key) == 0x47) { if (g_doc.valid) { go_to_page(0); redraw = true; } } // End: last page - else if (key.scancode == 0x4F) { + else if (montauk::nav_scancode(key) == 0x4F) { if (g_doc.valid) { go_to_page(g_doc.page_count - 1); redraw = true; } } // Ctrl++ or Ctrl+=: zoom in diff --git a/programs/src/pdfviewer/render.cpp b/programs/src/pdfviewer/render.cpp index 9b373d1..cd3d18a 100644 --- a/programs/src/pdfviewer/render.cpp +++ b/programs/src/pdfviewer/render.cpp @@ -71,7 +71,7 @@ static void draw_affine_text(uint32_t* pixels, const TextItem& item, by0>-0.0001f&&by0<0.0001f&&by1>-0.0001f&&by1<0.0001f) { float pen=0; for(int i=0;iget_glyph(cache,(uint8_t)text[i]); + CachedGlyph* glyph=font->get_glyph(cache,decode_single_byte_codepoint((uint8_t)text[i])); if(!glyph)continue; if(glyph->bitmap) { int gx=base_x+floor_pixel(pen*g_zoom+0.5f)+glyph->xoff; @@ -101,7 +101,7 @@ static void draw_affine_text(uint32_t* pixels, const TextItem& item, float pen = 0; for (int i = 0; i < text_len; i++) { - CachedGlyph* glyph = font->get_glyph(cache, (uint8_t)text[i]); + CachedGlyph* glyph = font->get_glyph(cache, decode_single_byte_codepoint((uint8_t)text[i])); if (!glyph) continue; if (glyph->bitmap) { float gx0 = (float)(pen + glyph->xoff); diff --git a/programs/src/printd/test_page_jpeg.cpp b/programs/src/printd/test_page_jpeg.cpp index fee9800..1fd40ca 100644 --- a/programs/src/printd/test_page_jpeg.cpp +++ b/programs/src/printd/test_page_jpeg.cpp @@ -143,14 +143,8 @@ static gui::TrueTypeFont* load_page_font(const char* path) { static void free_page_font(gui::TrueTypeFont* font) { if (!font) return; - for (int i = 0; i < font->cache_count; i++) { - for (int g = 0; g < 256; g++) { - if (font->caches[i].glyphs[g].bitmap) { - montauk::mfree(font->caches[i].glyphs[g].bitmap); - font->caches[i].glyphs[g].bitmap = nullptr; - } - } - } + for (int i = 0; i < font->cache_count; i++) + gui::TrueTypeFont::free_cache(&font->caches[i]); if (font->data) { montauk::free(font->data); font->data = nullptr; @@ -209,8 +203,8 @@ static void draw_text(uint8_t* rgb, PageFonts* fonts, bool bold, int cx = x; int baseline = y + gc->ascent; - for (int i = 0; text[i] != '\0'; i++) { - gui::CachedGlyph* glyph = font->get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + gui::CachedGlyph* glyph = font->get_glyph(gc, (int)montauk::utf8::next(&p)); if (!glyph) continue; if (glyph->bitmap) { diff --git a/programs/src/printers/main.cpp b/programs/src/printers/main.cpp index de25350..e1d38ed 100644 --- a/programs/src/printers/main.cpp +++ b/programs/src/printers/main.cpp @@ -230,7 +230,8 @@ static void text_fit(const char* src, char* out, int out_len, int max_w) { int keep = montauk::slen(src); while (keep > 0) { char tmp[MAX_DEBUG_LEN]; - int n = keep < (int)sizeof(tmp) - 4 ? keep : (int)sizeof(tmp) - 4; + int n = keep < (int)sizeof(tmp) - 4 ? keep + : montauk::utf8::floor_boundary(src, keep, (int)sizeof(tmp) - 4); for (int i = 0; i < n; i++) tmp[i] = src[i]; tmp[n] = '\0'; str_append(tmp, ell, (int)sizeof(tmp)); @@ -238,7 +239,7 @@ static void text_fit(const char* src, char* out, int out_len, int max_w) { montauk::strncpy(out, tmp, out_len - 1); return; } - keep--; + keep = montauk::utf8::prev_boundary(src, keep); } montauk::strncpy(out, ell, out_len - 1); } @@ -1162,12 +1163,12 @@ static void handle_key(const montauk::abi::KeyEvent& key) { return; } - if (key.scancode == 0x4B) { + if (montauk::nav_scancode(key) == 0x4B) { cycle_tab(-1); return; } - if (key.scancode == 0x4D) { + if (montauk::nav_scancode(key) == 0x4D) { cycle_tab(1); return; } diff --git a/programs/src/procmgr/main.cpp b/programs/src/procmgr/main.cpp index d88ffcd..841ef8a 100644 --- a/programs/src/procmgr/main.cpp +++ b/programs/src/procmgr/main.cpp @@ -527,6 +527,8 @@ static void draw_text_fit(Canvas& c, int x, int y, out++; } + out = montauk::utf8::floor_boundary(text, montauk::slen(text), out); + if (out <= 0) return; buf[out] = '.'; buf[out + 1] = '.'; @@ -852,24 +854,24 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { return true; } - if (key.scancode == 0x4B) { + if (montauk::nav_scancode(key) == 0x4B) { cycle_tab(-1); return true; } - if (key.scancode == 0x4D) { + if (montauk::nav_scancode(key) == 0x4D) { cycle_tab(1); return true; } - if (key.scancode == 0x53) { + if (montauk::nav_scancode(key) == 0x53) { if (can_kill_selected()) { kill_selected(); return true; } } - if (key.scancode == 0x48) { + if (montauk::nav_scancode(key) == 0x48) { if (g_pm.active_tab == PM_TAB_PROCESSES) { if (g_pm.selected > 0) g_pm.selected--; else if (g_pm.proc_count > 0) g_pm.selected = 0; @@ -884,7 +886,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { } } - if (key.scancode == 0x50) { + if (montauk::nav_scancode(key) == 0x50) { if (g_pm.active_tab == PM_TAB_PROCESSES) { if (g_pm.selected < g_pm.proc_count - 1) g_pm.selected++; else if (g_pm.selected < 0 && g_pm.proc_count > 0) g_pm.selected = 0; diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp index 9da0029..1ef09e5 100644 --- a/programs/src/shell/main.cpp +++ b/programs/src/shell/main.cpp @@ -92,7 +92,9 @@ static void prompt() { // ---- Line editing ---- -static void erase_input(int len) { +// Erase the echoed input: one terminal cell per UTF-8 character. +static void erase_input(const char* line, int bytes) { + int len = montauk::utf8::count(line, bytes); for (int i = 0; i < len; i++) montauk::putchar('\b'); for (int i = 0; i < len; i++) montauk::putchar(' '); for (int i = 0; i < len; i++) montauk::putchar('\b'); @@ -100,9 +102,9 @@ static void erase_input(int len) { static void replace_line(char* line, int* pos, const char* newContent) { int oldLen = *pos; - erase_input(oldLen); + erase_input(line, oldLen); int newLen = slen(newContent); - if (newLen > 255) newLen = 255; + if (newLen > 255) newLen = montauk::utf8::floor_boundary(newContent, newLen, 255); for (int i = 0; i < newLen; i++) { line[i] = newContent[i]; montauk::putchar(newContent[i]); @@ -431,14 +433,14 @@ extern "C" void _start() { // Arrow keys: ascii == 0, check scancode if (ev.ascii == 0) { - if (ev.scancode == SC_UP) { + if (montauk::nav_scancode(ev) == SC_UP) { int next = hist_nav + 1; const char* entry = history_get(next); if (entry) { hist_nav = next; replace_line(line, &pos, entry); } - } else if (ev.scancode == SC_DOWN) { + } else if (montauk::nav_scancode(ev) == SC_DOWN) { if (hist_nav > 0) { hist_nav--; const char* entry = history_get(hist_nav); @@ -447,7 +449,7 @@ extern "C" void _start() { } } else if (hist_nav == 0) { hist_nav = -1; - erase_input(pos); + erase_input(line, pos); pos = 0; line[0] = '\0'; } @@ -496,15 +498,18 @@ extern "C" void _start() { hist_nav = -1; prompt(); } else if (ev.ascii == '\b') { - if (pos > 0) { - pos--; + // One terminal cell per character, however many UTF-8 bytes + line[pos] = '\0'; + if (montauk::utf8::erase_last(line, &pos)) { montauk::putchar('\b'); montauk::putchar(' '); montauk::putchar('\b'); } - } else if (ev.ascii >= ' ' && pos < 255) { - line[pos++] = ev.ascii; - montauk::putchar(ev.ascii); + } else { + line[pos] = '\0'; + int before = pos; + if (montauk::utf8::append_key(line, &pos, 256, ev)) + montauk::print(line + before); } } } diff --git a/programs/src/shell/shell.h b/programs/src/shell/shell.h index 86c52c1..b0350bb 100644 --- a/programs/src/shell/shell.h +++ b/programs/src/shell/shell.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include diff --git a/programs/src/spreadsheet/main.cpp b/programs/src/spreadsheet/main.cpp index 301af2e..18afade 100644 --- a/programs/src/spreadsheet/main.cpp +++ b/programs/src/spreadsheet/main.cpp @@ -415,25 +415,13 @@ extern "C" void _start() { } else if (key.scancode == 0x01) { g_pathbar_open = false; } else if (key.ascii == '\b' || key.scancode == 0x0E) { - if (g_pathbar_cursor > 0) { - for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++) - g_pathbar_text[i] = g_pathbar_text[i + 1]; - g_pathbar_len--; - g_pathbar_cursor--; - g_pathbar_text[g_pathbar_len] = '\0'; - } - } else if (key.scancode == 0x4B) { - if (g_pathbar_cursor > 0) g_pathbar_cursor--; - } else if (key.scancode == 0x4D) { - if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++; - } else if ((unsigned char)key.ascii >= 32 - && (unsigned char)key.ascii != 127 && g_pathbar_len < 254) { - for (int i = g_pathbar_len; i > g_pathbar_cursor; i--) - g_pathbar_text[i] = g_pathbar_text[i - 1]; - g_pathbar_text[g_pathbar_cursor] = key.ascii; - g_pathbar_cursor++; - g_pathbar_len++; - g_pathbar_text[g_pathbar_len] = '\0'; + montauk::utf8::erase_before(g_pathbar_text, &g_pathbar_len, &g_pathbar_cursor); + } else if (montauk::nav_scancode(key) == 0x4B) { + g_pathbar_cursor = montauk::utf8::prev_boundary(g_pathbar_text, g_pathbar_cursor); + } else if (montauk::nav_scancode(key) == 0x4D) { + g_pathbar_cursor = montauk::utf8::next_boundary(g_pathbar_text, g_pathbar_len, g_pathbar_cursor); + } else { + montauk::utf8::insert_key(g_pathbar_text, &g_pathbar_len, 255, &g_pathbar_cursor, key); } redraw = true; goto done_keys; @@ -441,12 +429,12 @@ extern "C" void _start() { // Autocomplete interaction (when editing with autocomplete open) if (g_ac_open && g_editing) { - if (key.scancode == 0x48) { // Up + if (montauk::nav_scancode(key) == 0x48) { // Up g_ac_sel = (g_ac_sel - 1 + g_ac_count) % g_ac_count; redraw = true; goto done_keys; } - if (key.scancode == 0x50) { // Down + if (montauk::nav_scancode(key) == 0x50) { // Down g_ac_sel = (g_ac_sel + 1) % g_ac_count; redraw = true; goto done_keys; @@ -498,7 +486,7 @@ extern "C" void _start() { redraw = true; } // Arrow keys (with Shift = extend selection) - else if (key.scancode == 0x48 && !g_editing) { // Up + else if (montauk::nav_scancode(key) == 0x48 && !g_editing) { // Up if (key.shift) { if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; } if (g_sel_row > 0) g_sel_row--; @@ -509,7 +497,7 @@ extern "C" void _start() { ensure_sel_visible(); redraw = true; } - else if (key.scancode == 0x50 && !g_editing) { // Down + else if (montauk::nav_scancode(key) == 0x50 && !g_editing) { // Down if (key.shift) { if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; } if (g_sel_row < MAX_ROWS - 1) g_sel_row++; @@ -520,7 +508,7 @@ extern "C" void _start() { ensure_sel_visible(); redraw = true; } - else if (key.scancode == 0x4B && !g_editing) { // Left + else if (montauk::nav_scancode(key) == 0x4B && !g_editing) { // Left if (key.shift) { if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; } if (g_sel_col > 0) g_sel_col--; @@ -531,7 +519,7 @@ extern "C" void _start() { ensure_sel_visible(); redraw = true; } - else if (key.scancode == 0x4D && !g_editing) { // Right + else if (montauk::nav_scancode(key) == 0x4D && !g_editing) { // Right if (key.shift) { if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; } if (g_sel_col < MAX_COLS - 1) g_sel_col++; @@ -545,14 +533,8 @@ extern "C" void _start() { // Backspace else if (key.ascii == '\b' || key.scancode == 0x0E) { if (g_editing) { - if (g_edit_cursor > 0) { - for (int i = g_edit_cursor - 1; i < g_edit_len - 1; i++) - g_edit_buf[i] = g_edit_buf[i + 1]; - g_edit_len--; - g_edit_cursor--; - g_edit_buf[g_edit_len] = '\0'; + if (montauk::utf8::erase_before(g_edit_buf, &g_edit_len, &g_edit_cursor)) update_autocomplete(); - } redraw = true; } else { undo_push(); @@ -567,15 +549,10 @@ extern "C" void _start() { } } // Delete key - else if (key.scancode == 0x53) { + else if (montauk::nav_scancode(key) == 0x53) { if (g_editing) { - if (g_edit_cursor < g_edit_len) { - for (int i = g_edit_cursor; i < g_edit_len - 1; i++) - g_edit_buf[i] = g_edit_buf[i + 1]; - g_edit_len--; - g_edit_buf[g_edit_len] = '\0'; + if (montauk::utf8::erase_at(g_edit_buf, &g_edit_len, g_edit_cursor)) update_autocomplete(); - } redraw = true; } else { undo_push(); @@ -590,18 +567,18 @@ extern "C" void _start() { } } // Edit mode arrow keys (cursor movement within formula bar) - else if (key.scancode == 0x4B && g_editing) { - if (g_edit_cursor > 0) g_edit_cursor--; + else if (montauk::nav_scancode(key) == 0x4B && g_editing) { + g_edit_cursor = montauk::utf8::prev_boundary(g_edit_buf, g_edit_cursor); update_autocomplete(); redraw = true; } - else if (key.scancode == 0x4D && g_editing) { - if (g_edit_cursor < g_edit_len) g_edit_cursor++; + else if (montauk::nav_scancode(key) == 0x4D && g_editing) { + g_edit_cursor = montauk::utf8::next_boundary(g_edit_buf, g_edit_len, g_edit_cursor); update_autocomplete(); redraw = true; } // Home key - else if (key.scancode == 0x47) { + else if (montauk::nav_scancode(key) == 0x47) { if (g_editing) { g_edit_cursor = 0; update_autocomplete(); @@ -613,7 +590,7 @@ extern "C" void _start() { redraw = true; } // End key - else if (key.scancode == 0x4F) { + else if (montauk::nav_scancode(key) == 0x4F) { if (g_editing) { g_edit_cursor = g_edit_len; update_autocomplete(); @@ -670,23 +647,16 @@ extern "C" void _start() { redraw = true; } // Printable character: start editing or insert - else if ((unsigned char)key.ascii >= 32 - && (unsigned char)key.ascii != 127 && !key.ctrl) { + else if (montauk::utf8::key_codepoint(key) && !key.ctrl) { if (!g_editing) { clear_selection(); g_editing = true; - g_edit_buf[0] = key.ascii; - g_edit_buf[1] = '\0'; - g_edit_len = 1; - g_edit_cursor = 1; - } else if (g_edit_len < CELL_TEXT_MAX - 1) { - for (int i = g_edit_len; i > g_edit_cursor; i--) - g_edit_buf[i] = g_edit_buf[i - 1]; - g_edit_buf[g_edit_cursor] = key.ascii; - g_edit_len++; - g_edit_cursor++; - g_edit_buf[g_edit_len] = '\0'; + g_edit_buf[0] = '\0'; + g_edit_len = 0; + g_edit_cursor = 0; } + montauk::utf8::insert_key(g_edit_buf, &g_edit_len, CELL_TEXT_MAX, + &g_edit_cursor, key); update_autocomplete(); redraw = true; } diff --git a/programs/src/sshd/main.cpp b/programs/src/sshd/main.cpp index 6144e89..057a7b3 100644 --- a/programs/src/sshd/main.cpp +++ b/programs/src/sshd/main.cpp @@ -7,6 +7,7 @@ #include #include +#include #include extern "C" { @@ -1009,8 +1010,20 @@ static void feed_child(int pid, const uint8_t* data, int size) { /* Remaining C0 codes are the control-modified letters. */ key.ascii = (char)('a' + value - 1); key.ctrl = true; - } else { + } else if (value < 0x80) { key.ascii = (char)value; + key.codepoint = value; + } else { + /* The client sends UTF-8; the key event carries the codepoint plus + its Windows-1252 byte for legacy readers. A keystroke's sequence + arrives in one packet, so splitting across feed_child calls is + ignored. */ + int n; + uint32_t cp = montauk::utf8::decode((const char*)data + i, size - i, &n); + i += n - 1; + uint8_t b = montauk::utf8::to_cp1252(cp); + key.codepoint = cp; + key.ascii = b ? (char)b : montauk::abi::KEY_ASCII_SUBSTITUTE; } montauk::childio_writekey(pid, &key); diff --git a/programs/src/tcpconnect/main.cpp b/programs/src/tcpconnect/main.cpp index 5b6ad53..de69e6c 100644 --- a/programs/src/tcpconnect/main.cpp +++ b/programs/src/tcpconnect/main.cpp @@ -5,6 +5,7 @@ */ #include +#include #include using montauk::skip_spaces; @@ -176,15 +177,18 @@ extern "C" void _start() { montauk::send(fd, sendBuf, sendPos); sendPos = 0; } else if (ev.ascii == '\b') { - if (sendPos > 0) { - sendPos--; + sendBuf[sendPos] = '\0'; + if (montauk::utf8::erase_last(sendBuf, &sendPos)) { montauk::putchar('\b'); montauk::putchar(' '); montauk::putchar('\b'); } - } else if (ev.ascii >= ' ' && sendPos < 254) { - sendBuf[sendPos++] = ev.ascii; - montauk::putchar(ev.ascii); + } else { + // Typed text is sent as UTF-8; leave room for the '\n' + sendBuf[sendPos] = '\0'; + int before = sendPos; + if (montauk::utf8::append_key(sendBuf, &sendPos, 255, ev)) + montauk::print(sendBuf + before); } } else { montauk::wait_handle(fd, montauk::abi::IPC_SIGNAL_READABLE | montauk::abi::IPC_SIGNAL_PEER_CLOSED, 10); diff --git a/programs/src/terminal/settings.cpp b/programs/src/terminal/settings.cpp index 15d3763..5d26d04 100644 --- a/programs/src/terminal/settings.cpp +++ b/programs/src/terminal/settings.cpp @@ -479,9 +479,9 @@ static void handle_key(const montauk::abi::KeyEvent& key) { step_font_size(FONT_SIZE_STEP); } else if (key.ascii == '-') { step_font_size(-FONT_SIZE_STEP); - } else if (key.scancode == 0x48) { // Up + } else if (montauk::nav_scancode(key) == 0x48) { // Up select_theme(g_panel.theme_index - 1); - } else if (key.scancode == 0x50) { // Down + } else if (montauk::nav_scancode(key) == 0x50) { // Down select_theme(g_panel.theme_index + 1); } } diff --git a/programs/src/texteditor/main.cpp b/programs/src/texteditor/main.cpp index 8ccaba0..b6b9271 100644 --- a/programs/src/texteditor/main.cpp +++ b/programs/src/texteditor/main.cpp @@ -94,7 +94,7 @@ int g_line_count = 0; // Cursor int g_cursor_pos = 0; // byte position int g_cursor_line = 0; -int g_cursor_col = 0; +int g_cursor_col = 0; // character (display) column, not bytes int g_scroll_y = 0; // first visible line int g_scroll_x = 0; // horizontal scroll in pixels bool g_scroll_dragging = false; @@ -264,7 +264,8 @@ static void update_cursor_pos() { break; } } - g_cursor_col = g_cursor_pos - g_line_offsets[g_cursor_line]; + int ls = g_line_offsets[g_cursor_line]; + g_cursor_col = montauk::utf8::count(g_buffer + ls, g_cursor_pos - ls); g_cursor_moved = true; } @@ -275,6 +276,12 @@ static int line_length(int line) { return end - start; } +// Byte offset of character column `col` on a line (clamped to its end). +static int line_col_to_pos(int line, int col) { + int start = g_line_offsets[line]; + return start + montauk::utf8::offset_of(g_buffer + start, line_length(line), col); +} + // ============================================================================ // Buffer operations // ============================================================================ @@ -294,17 +301,10 @@ static void delete_range(int start, int count) { g_buf_len -= count; } +static void insert_string(const char* s, int len); + static void insert_char(char c) { - if (g_buf_len >= MAX_CAP - 1) return; - ensure_capacity(1); - for (int i = g_buf_len; i > g_cursor_pos; i--) - g_buffer[i] = g_buffer[i - 1]; - g_buffer[g_cursor_pos] = c; - g_buf_len++; - g_cursor_pos++; - g_modified = true; - recompute_lines(); - update_cursor_pos(); + insert_string(&c, 1); } static void insert_string(const char* s, int len) { @@ -323,8 +323,9 @@ static void insert_string(const char* s, int len) { static void backspace() { if (g_cursor_pos <= 0) return; - g_cursor_pos--; - delete_range(g_cursor_pos, 1); + int start = montauk::utf8::prev_boundary(g_buffer, g_cursor_pos); + delete_range(start, g_cursor_pos - start); + g_cursor_pos = start; g_modified = true; recompute_lines(); update_cursor_pos(); @@ -332,7 +333,8 @@ static void backspace() { static void delete_char() { if (g_cursor_pos >= g_buf_len) return; - delete_range(g_cursor_pos, 1); + delete_range(g_cursor_pos, + montauk::utf8::next_boundary(g_buffer, g_buf_len, g_cursor_pos) - g_cursor_pos); g_modified = true; recompute_lines(); update_cursor_pos(); @@ -344,30 +346,28 @@ static void delete_char() { static void move_up() { if (g_cursor_line <= 0) return; - int col = g_cursor_col; - int prev = g_cursor_line - 1; - int len = line_length(prev); - if (col > len) col = len; - g_cursor_pos = g_line_offsets[prev] + col; + g_cursor_pos = line_col_to_pos(g_cursor_line - 1, g_cursor_col); update_cursor_pos(); } static void move_down() { if (g_cursor_line >= g_line_count - 1) return; - int col = g_cursor_col; - int next = g_cursor_line + 1; - int len = line_length(next); - if (col > len) col = len; - g_cursor_pos = g_line_offsets[next] + col; + g_cursor_pos = line_col_to_pos(g_cursor_line + 1, g_cursor_col); update_cursor_pos(); } static void move_left() { - if (g_cursor_pos > 0) { g_cursor_pos--; update_cursor_pos(); } + if (g_cursor_pos > 0) { + g_cursor_pos = montauk::utf8::prev_boundary(g_buffer, g_cursor_pos); + update_cursor_pos(); + } } static void move_right() { - if (g_cursor_pos < g_buf_len) { g_cursor_pos++; update_cursor_pos(); } + if (g_cursor_pos < g_buf_len) { + g_cursor_pos = montauk::utf8::next_boundary(g_buffer, g_buf_len, g_cursor_pos); + update_cursor_pos(); + } } static void move_home() { @@ -621,6 +621,25 @@ static bool load_file(const char* path) { return false; } + // Files written before MontaukOS moved to UTF-8 are Windows-1252. Convert + // them once on load, so edits (which insert UTF-8) never mix encodings + // and saving writes the whole file back as UTF-8. + bool converted = false; + if (!montauk::utf8::valid(temp, got)) { + int conv_cap = got * 3 + 1; + if (conv_cap > MAX_CAP) conv_cap = MAX_CAP; + char* conv = (char*)montauk::malloc(conv_cap); + if (!conv) { + montauk::mfree(temp); + set_status("Out of memory while opening file"); + return false; + } + got = montauk::utf8::normalize(temp, got, conv, conv_cap); + montauk::mfree(temp); + temp = conv; + converted = true; + } + if (got >= g_buf_cap) { int new_cap = got + 1024; if (new_cap > MAX_CAP) new_cap = MAX_CAP; @@ -649,7 +668,7 @@ static bool load_file(const char* path) { g_syntax_language = syn_detect_language(g_filepath); clear_selection(); - set_status(""); + set_status(converted ? "Converted from Windows-1252 to UTF-8" : ""); recompute_lines(); update_cursor_pos(); @@ -686,10 +705,8 @@ static int hit_test(int mx, int my, int editor_y) { int clicked_col = (mx - LINE_NUM_W - 4 + g_scroll_x + cell_w / 2) / cell_w; if (clicked_col < 0) clicked_col = 0; - int ll = line_length(clicked_line); - if (clicked_col > ll) clicked_col = ll; - return g_line_offsets[clicked_line] + clicked_col; + return line_col_to_pos(clicked_line, clicked_col); } static void draw_context_menu(uint32_t* pixels, int W, int H) { @@ -861,14 +878,19 @@ static void render(uint32_t* pixels) { syn_highlight_line(g_buffer + line_start, line_len, syn_line_buf, SYN_LINE_BUF_CAP, g_syntax_language, syn_state); - for (int ci = 0; ci < line_len; ci++) { - int px_x = text_start_x + ci * cell_w - g_scroll_x; + // One monospace cell per character; ci walks bytes, col walks cells. + for (int ci = 0, col = 0; ci < line_len; col++) { + int ch_bytes; + uint32_t ch = montauk::utf8::decode(g_buffer + line_start + ci, + line_len - ci, &ch_bytes); + int byte_pos = line_start + ci; + int syn_index = ci; + ci += ch_bytes; + + int px_x = text_start_x + col * cell_w - g_scroll_x; if (px_x + cell_w <= LINE_NUM_W + 1) continue; if (px_x >= text_area_w) break; - int byte_pos = line_start + ci; - char ch = g_buffer[byte_pos]; - // Selection highlight if (g_has_selection && byte_pos >= sel_s && byte_pos < sel_e) { int hl_h = cell_h; @@ -878,22 +900,24 @@ static void render(uint32_t* pixels) { } Color char_color = TEXT_COLOR; - if (g_syntax_language != SYN_LANG_NONE && ci < syn_len) - char_color = syn_color(syn_line_buf[ci]); + if (g_syntax_language != SYN_LANG_NONE && syn_index < syn_len) + char_color = syn_color(syn_line_buf[syn_index]); - if (ch >= 32 || ch < 0) { + if (ch >= 32) { if (gc) { g_font_mono->draw_char_to_buffer(pixels, W, H, px_x, py + gc->ascent, - ch, char_color, gc); + (int)ch, char_color, gc); } } } + // Selection highlight for newline at end of line if (g_has_selection && line + 1 < g_line_count) { int nl_pos = line_start + line_len; if (nl_pos >= sel_s && nl_pos < sel_e) { - int px_x = text_start_x + line_len * cell_w - g_scroll_x; + int line_cols = montauk::utf8::count(g_buffer + line_start, line_len); + int px_x = text_start_x + line_cols * cell_w - g_scroll_x; if (px_x > LINE_NUM_W && px_x < text_area_w) { int hl_h = cell_h; if (py + hl_h > editor_y + text_area_h) hl_h = editor_y + text_area_h - py; @@ -1052,25 +1076,12 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) { return true; } if (key.ascii == '\b' || key.scancode == 0x0E) { - if (g_pathbar_cursor > 0) { - for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++) - g_pathbar_text[i] = g_pathbar_text[i + 1]; - g_pathbar_len--; - g_pathbar_cursor--; - g_pathbar_text[g_pathbar_len] = '\0'; - } + montauk::utf8::erase_before(g_pathbar_text, &g_pathbar_len, &g_pathbar_cursor); return true; } - if (key.scancode == 0x4B) { if (g_pathbar_cursor > 0) g_pathbar_cursor--; return true; } - if (key.scancode == 0x4D) { if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++; return true; } - if ((unsigned char)key.ascii >= 32 - && (unsigned char)key.ascii != 127 && g_pathbar_len < 254) { - for (int i = g_pathbar_len; i > g_pathbar_cursor; i--) - g_pathbar_text[i] = g_pathbar_text[i - 1]; - g_pathbar_text[g_pathbar_cursor] = key.ascii; - g_pathbar_cursor++; - g_pathbar_len++; - g_pathbar_text[g_pathbar_len] = '\0'; + if (montauk::nav_scancode(key) == 0x4B) { g_pathbar_cursor = montauk::utf8::prev_boundary(g_pathbar_text, g_pathbar_cursor); return true; } + if (montauk::nav_scancode(key) == 0x4D) { g_pathbar_cursor = montauk::utf8::next_boundary(g_pathbar_text, g_pathbar_len, g_pathbar_cursor); return true; } + if (montauk::utf8::insert_key(g_pathbar_text, &g_pathbar_len, 255, &g_pathbar_cursor, key)) { return true; } return false; @@ -1123,19 +1134,19 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) { } // Arrow keys - if (key.scancode == 0x48) { // Up + if (montauk::nav_scancode(key) == 0x48) { // Up if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); move_up(); if (key.shift) update_selection(); return true; } - if (key.scancode == 0x50) { // Down + if (montauk::nav_scancode(key) == 0x50) { // Down if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); move_down(); if (key.shift) update_selection(); return true; } - if (key.scancode == 0x4B) { // Left + if (montauk::nav_scancode(key) == 0x4B) { // Left if (key.shift) { start_selection(); move_left(); update_selection(); } else if (g_has_selection) { @@ -1144,7 +1155,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) { } else move_left(); return true; } - if (key.scancode == 0x4D) { // Right + if (montauk::nav_scancode(key) == 0x4D) { // Right if (key.shift) { start_selection(); move_right(); update_selection(); } else if (g_has_selection) { @@ -1155,14 +1166,14 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) { } // Home - if (key.scancode == 0x47) { + if (montauk::nav_scancode(key) == 0x47) { if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); move_home(); if (key.shift) update_selection(); return true; } // End - if (key.scancode == 0x4F) { + if (montauk::nav_scancode(key) == 0x4F) { if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); move_end(); if (key.shift) update_selection(); @@ -1170,7 +1181,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) { } // Page Up - if (key.scancode == 0x49) { + if (montauk::nav_scancode(key) == 0x49) { if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); int cell_h = font_cell_h(); int vis = cell_h > 0 ? (g_win_h - TOOLBAR_H - STATUS_H) / cell_h : 20; @@ -1179,7 +1190,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) { return true; } // Page Down - if (key.scancode == 0x51) { + if (montauk::nav_scancode(key) == 0x51) { if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); int cell_h = font_cell_h(); int vis = cell_h > 0 ? (g_win_h - TOOLBAR_H - STATUS_H) / cell_h : 20; @@ -1189,7 +1200,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) { } // Delete - if (key.scancode == 0x53) { + if (montauk::nav_scancode(key) == 0x53) { if (g_has_selection) delete_selection(); else delete_char(); return true; } @@ -1214,10 +1225,12 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) { return true; } - // Printable - if ((unsigned char)key.ascii >= 32 && (unsigned char)key.ascii != 127) { + // Printable (inserted as UTF-8) + char typed[montauk::utf8::MAX_BYTES]; + int typed_len = montauk::utf8::key_utf8(key, typed); + if (typed_len > 0) { if (g_has_selection) delete_selection(); - insert_char(key.ascii); + insert_string(typed, typed_len); return true; } @@ -1404,12 +1417,8 @@ static bool handle_mouse(montauk::abi::WinEvent& ev, int win_id) { if (new_line >= g_scroll_y + visible_lines) new_line = g_scroll_y + visible_lines - 1; if (new_line < 0) new_line = 0; if (new_line >= g_line_count) new_line = g_line_count - 1; - int col = g_cursor_col; - int ll = line_length(new_line); - if (col > ll) col = ll; - g_cursor_pos = g_line_offsets[new_line] + col; - g_cursor_line = new_line; - g_cursor_col = col; + g_cursor_pos = line_col_to_pos(new_line, g_cursor_col); + update_cursor_pos(); } return true; } diff --git a/programs/src/timezone/main.cpp b/programs/src/timezone/main.cpp index 6f8a8f5..2cb79dd 100644 --- a/programs/src/timezone/main.cpp +++ b/programs/src/timezone/main.cpp @@ -455,7 +455,8 @@ static void fit_text(char* out, int cap, const char* text, int max_w) { static constexpr const char* ellipsis = "..."; int len = montauk::slen(out); while (len > 0) { - out[--len] = '\0'; + len = montauk::utf8::prev_boundary(out, len); + out[len] = '\0'; char candidate[160]; snprintf(candidate, sizeof(candidate), "%s%s", out, ellipsis); if (text_width(candidate) <= max_w) { @@ -1051,12 +1052,12 @@ static bool handle_key(const montauk::abi::KeyEvent& key) { int old_country = g_selected_country; - if (key.scancode == 0x4B) g_focus_pane = 0; // Left - else if (key.scancode == 0x4D) g_focus_pane = 1; // Right - else if (key.scancode == 0x48 && *selected > 0) (*selected)--; // Up - else if (key.scancode == 0x50 && *selected < count - 1) (*selected)++; // Down - else if (key.scancode == 0x49) *selected = gui_max(0, *selected - 8); // Page Up - else if (key.scancode == 0x51) *selected = gui_min(count - 1, *selected + 8); // Page Down + if (montauk::nav_scancode(key) == 0x4B) g_focus_pane = 0; // Left + else if (montauk::nav_scancode(key) == 0x4D) g_focus_pane = 1; // Right + else if (montauk::nav_scancode(key) == 0x48 && *selected > 0) (*selected)--; // Up + else if (montauk::nav_scancode(key) == 0x50 && *selected < count - 1) (*selected)++; // Down + else if (montauk::nav_scancode(key) == 0x49) *selected = gui_max(0, *selected - 8); // Page Up + else if (montauk::nav_scancode(key) == 0x51) *selected = gui_min(count - 1, *selected + 8); // Page Down else return false; if (old_country != g_selected_country) { diff --git a/programs/src/video/main.cpp b/programs/src/video/main.cpp index 38f492e..68d9415 100644 --- a/programs/src/video/main.cpp +++ b/programs/src/video/main.cpp @@ -297,7 +297,7 @@ static void copy_text_fit(char* out, int out_cap, const char* text, int max_w, i int len = montauk::slen(out); const char* ellipsis = "..."; while (len > 0) { - len--; + len = montauk::utf8::prev_boundary(out, len); out[len] = '\0'; char trial[160]; montauk::strncpy(trial, out, (int)sizeof(trial)); @@ -1235,14 +1235,14 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) { return true; } - if (key.scancode == 0x4B) { + if (montauk::nav_scancode(key) == 0x4B) { uint64_t cur = current_time_us(); if (cur > 5000000ull) cur -= 5000000ull; else cur = 0; seek_to_time(cur); return true; } - if (key.scancode == 0x4D) { + if (montauk::nav_scancode(key) == 0x4D) { seek_to_time(current_time_us() + 5000000ull); return true; } diff --git a/programs/src/weather/main.cpp b/programs/src/weather/main.cpp index 5cf1327..5e9bd93 100644 --- a/programs/src/weather/main.cpp +++ b/programs/src/weather/main.cpp @@ -251,9 +251,9 @@ static void do_fetch() { extract_array_value(body, bodyLen, "\"areaName\"", area, sizeof(area)); extract_array_value(body, bodyLen, "\"country\"", country, sizeof(country)); - // Degree sign is U+00B0 = 0xB0 in Latin-1 (single byte, within the 256-entry glyph cache) - snprintf(g_temp, sizeof(g_temp), "%s\xb0""C", temp_raw); - snprintf(g_feels, sizeof(g_feels), "Feels like: %s\xb0""C", feels_raw); + // Degree sign U+00B0 (UTF-8) + snprintf(g_temp, sizeof(g_temp), "%s\u00B0C", temp_raw); + snprintf(g_feels, sizeof(g_feels), "Feels like: %s\u00B0C", feels_raw); if (area[0] && country[0]) snprintf(g_location, sizeof(g_location), "%s, %s", area, country); diff --git a/programs/src/wiki/main.cpp b/programs/src/wiki/main.cpp index 13022ea..c142264 100644 --- a/programs/src/wiki/main.cpp +++ b/programs/src/wiki/main.cpp @@ -10,6 +10,7 @@ #include #include +#include #include extern "C" { @@ -177,12 +178,23 @@ static int extract_json_string(const char* buf, int len, const char* key, else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10; } p += 4; - if (val < 128) out[j++] = (char)val; - else if (val == 0x2013 || val == 0x2014) out[j++] = '-'; - else if (val == 0x2018 || val == 0x2019) out[j++] = '\''; - else if (val == 0x201C || val == 0x201D) out[j++] = '"'; - else if (val == 0x2026) { out[j++] = '.'; out[j++] = '.'; out[j++] = '.'; } - else out[j++] = '?'; + // The terminal renders UTF-8; join surrogate pairs + if (val >= 0xD800 && val <= 0xDBFF && p + 6 < len && + buf[p + 1] == '\\' && buf[p + 2] == 'u') { + unsigned lo = 0; + for (int k = 3; k <= 6; k++) { + char h = buf[p + k]; + lo <<= 4; + if (h >= '0' && h <= '9') lo |= h - '0'; + else if (h >= 'a' && h <= 'f') lo |= h - 'a' + 10; + else if (h >= 'A' && h <= 'F') lo |= h - 'A' + 10; + } + if (lo >= 0xDC00 && lo <= 0xDFFF) { + val = 0x10000 + ((val - 0xD800) << 10) + (lo - 0xDC00); + p += 6; + } + } + j += montauk::utf8::encode(val, out + j); } break; } @@ -219,9 +231,12 @@ static int wrap_paragraph(const char* text, int textLen, int cols, const char* lastSpace = nullptr; int col = 0; + // One terminal column per UTF-8 character while (p < end && col < cols) { if (*p == ' ') lastSpace = p; - p++; + int n; + montauk::utf8::decode(p, (int)(end - p), &n); + p += n; col++; } @@ -376,8 +391,7 @@ static void render_pager(WikiLine* lines, int totalLines, int scroll, int maxW = cols; if (ln.type == LINE_SECTION && ln.level > 2) maxW -= (ln.level - 2) * 2; - int printLen = ln.len; - if (printLen > maxW) printLen = maxW; + int printLen = montauk::utf8::offset_of(ln.text, ln.len, maxW < 0 ? 0 : maxW); for (int c = 0; c < printLen; c++) sb_putc(ln.text[c]); @@ -463,7 +477,7 @@ static void run_pager(WikiLine* lines, int totalLines, const char* title, case 'g': scroll = 0; break; case 'G': scroll = maxScroll; break; default: - switch (ev.scancode) { + switch (montauk::nav_scancode(ev)) { case 0x48: if (scroll > 0) scroll--; break; // Up case 0x50: if (scroll < maxScroll) scroll++; break; // Down case 0x49: scroll -= contentRows; if (scroll < 0) scroll = 0; break; // PgUp diff --git a/programs/src/wikipedia/main.cpp b/programs/src/wikipedia/main.cpp index a256773..ad20a80 100644 --- a/programs/src/wikipedia/main.cpp +++ b/programs/src/wikipedia/main.cpp @@ -41,7 +41,7 @@ static bool init_buffers_and_fonts() { static void handle_key_event(const montauk::abi::WinEvent& ev, bool& search_pending) { uint8_t ascii = ev.key.ascii; - uint8_t scan = ev.key.scancode; + uint8_t scan = montauk::nav_scancode(ev.key); if (g_search_focused) { if (ascii == '\n' || ascii == '\r') { diff --git a/programs/src/wikipedia/text.cpp b/programs/src/wikipedia/text.cpp index 70095bb..70e4447 100644 --- a/programs/src/wikipedia/text.cpp +++ b/programs/src/wikipedia/text.cpp @@ -10,64 +10,12 @@ // Codepoint and UTF-8 helpers // ============================================================================ -int encode_single_byte_codepoint(unsigned codepoint) { - if (codepoint < 0x80) return (int)codepoint; - if (codepoint >= 0xA0 && codepoint <= 0xFF) return (int)codepoint; - - switch (codepoint) { - case 0x20AC: return 0x80; - case 0x201A: return 0x82; - case 0x0192: return 0x83; - case 0x201E: return 0x84; - case 0x2026: return 0x85; - case 0x2020: return 0x86; - case 0x2021: return 0x87; - case 0x02C6: return 0x88; - case 0x2030: return 0x89; - case 0x0160: return 0x8A; - case 0x2039: return 0x8B; - case 0x0152: return 0x8C; - case 0x017D: return 0x8E; - case 0x2018: return 0x91; - case 0x2019: return 0x92; - case 0x201C: return 0x93; - case 0x201D: return 0x94; - case 0x2022: return 0x95; - case 0x2013: return 0x96; - case 0x2014: return 0x97; - case 0x02DC: return 0x98; - case 0x2122: return 0x99; - case 0x0161: return 0x9A; - case 0x203A: return 0x9B; - case 0x0153: return 0x9C; - case 0x017E: return 0x9E; - case 0x0178: return 0x9F; - default: return -1; - } -} - +// Article text is kept as UTF-8, which the renderer draws directly. void append_codepoint(char* out, int* j, int maxOut, unsigned codepoint) { - if (!out || !j || *j >= maxOut - 1) return; - - int encoded = encode_single_byte_codepoint(codepoint); - if (encoded >= 0) { - out[(*j)++] = (char)encoded; - return; - } - - switch (codepoint) { - case 0x00A0: - out[(*j)++] = ' '; - return; - case 0x2010: - case 0x2011: - case 0x2212: - out[(*j)++] = '-'; - return; - default: - out[(*j)++] = '?'; - return; - } + if (!out || !j) return; + int n = montauk::utf8::encoded_length(codepoint); + if (*j + n > maxOut - 1) return; + *j += montauk::utf8::encode(codepoint, out + *j); } void underscores_to_spaces(char* text) { @@ -205,14 +153,27 @@ int extract_json_string(const char* buf, int len, const char* key, case '/': out[j++] = '/'; break; case 'u': { if (p + 4 < len) { - unsigned val = 0; - for (int k = 1; k <= 4; k++) { - char h = buf[p + k]; val <<= 4; - if (h>='0'&&h<='9') val |= h-'0'; - else if (h>='a'&&h<='f') val |= h-'a'+10; - else if (h>='A'&&h<='F') val |= h-'A'+10; - } + auto hex4 = [&](int at) { + unsigned val = 0; + for (int k = 0; k < 4; k++) { + char h = buf[at + k]; val <<= 4; + if (h>='0'&&h<='9') val |= h-'0'; + else if (h>='a'&&h<='f') val |= h-'a'+10; + else if (h>='A'&&h<='F') val |= h-'A'+10; + } + return val; + }; + unsigned val = hex4(p + 1); p += 4; + // A character outside the BMP is a \uD8xx\uDCxx pair + if (val >= 0xD800 && val <= 0xDBFF && p + 6 < len && + buf[p + 1] == '\\' && buf[p + 2] == 'u') { + unsigned lo = hex4(p + 3); + if (lo >= 0xDC00 && lo <= 0xDFFF) { + val = 0x10000 + ((val - 0xD800) << 10) + (lo - 0xDC00); + p += 6; + } + } append_codepoint(out, &j, maxOut, val); } break; diff --git a/programs/src/wikipedia/wikipedia.h b/programs/src/wikipedia/wikipedia.h index c13fd4e..a3246a3 100644 --- a/programs/src/wikipedia/wikipedia.h +++ b/programs/src/wikipedia/wikipedia.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -184,7 +185,6 @@ int document_top_pad(); // text.cpp -- encoding, HTTP/URL/JSON parsing // ============================================================================ -int encode_single_byte_codepoint(unsigned codepoint); void append_codepoint(char* out, int* j, int maxOut, unsigned codepoint); void underscores_to_spaces(char* text); bool is_main_page_query(const char* query); diff --git a/programs/src/wordprocessor/document.cpp b/programs/src/wordprocessor/document.cpp index f5e52e2..16c2505 100644 --- a/programs/src/wordprocessor/document.cpp +++ b/programs/src/wordprocessor/document.cpp @@ -263,7 +263,7 @@ static int wp_char_advance_at(WordProcessorState* wp, int abs_pos, int layout_dp wp_font_at(wp, abs_pos, layout_dpi, &font, &gc); if (!font || !gc) return wp_points_to_pixels(6, layout_dpi); - CachedGlyph* g = font->get_glyph(gc, (unsigned char)ch); + CachedGlyph* g = font->get_glyph(gc, decode_single_byte_codepoint((unsigned char)ch)); return g ? g->advance : wp_points_to_pixels(6, layout_dpi); } diff --git a/programs/src/wordprocessor/input.cpp b/programs/src/wordprocessor/input.cpp index d16358b..d2914c1 100644 --- a/programs/src/wordprocessor/input.cpp +++ b/programs/src/wordprocessor/input.cpp @@ -14,17 +14,46 @@ static void wp_set_status(WordProcessorState* wp, const char* msg) { wp->status_msg[sizeof(wp->status_msg) - 1] = '\0'; } +// Documents store text as Windows-1252 (one byte per character, matching the +// WinAnsi fonts used for PDF export); the clipboard is UTF-8. Convert both ways. +static char* wp_cp1252_to_utf8_alloc(const char* text, int len, int* out_len) { + char* out = (char*)montauk::malloc(len * 3 + 1); + if (!out) return nullptr; + int o = 0; + for (int i = 0; i < len; i++) + o += montauk::utf8::encode(montauk::utf8::from_cp1252((uint8_t)text[i]), out + o); + out[o] = '\0'; + *out_len = o; + return out; +} + +// Characters with no Windows-1252 form become '?'. +static int wp_utf8_to_cp1252_inplace(char* text, int len) { + int o = 0; + for (int pos = 0; pos < len;) { + int n; + uint32_t cp = montauk::utf8::decode(text + pos, len - pos, &n); + pos += n; + uint8_t b = cp == 0 ? 0 : montauk::utf8::to_cp1252(cp); + text[o++] = b ? (char)b : '?'; + } + text[o] = '\0'; + return o; +} + static bool wp_copy_selection_to_clipboard(WordProcessorState* wp) { int len = 0; - char* text = wp_selection_plain_text_alloc(wp, &len); - if (text == nullptr || len <= 0) { - if (text) montauk::mfree(text); + char* raw = wp_selection_plain_text_alloc(wp, &len); + if (raw == nullptr || len <= 0) { + if (raw) montauk::mfree(raw); wp_set_status(wp, "Nothing selected"); return false; } - bool ok = gui::clipboard_set_text(text, len); - montauk::mfree(text); + char* text = wp_cp1252_to_utf8_alloc(raw, len, &len); + montauk::mfree(raw); + bool ok = text && gui::clipboard_set_text(text, len); + if (text) montauk::mfree(text); if (!ok) { wp_set_status(wp, "Copy failed"); return false; @@ -52,6 +81,7 @@ static bool wp_paste_from_clipboard(WordProcessorState* wp) { wp_set_status(wp, "Clipboard is empty"); return false; } + len = wp_utf8_to_cp1252_inplace(text, len); bool ok = wp_replace_selection_with_text(wp, text, len); montauk::mfree(text); @@ -263,7 +293,7 @@ static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, in for (int ci = 0; ci < to_check; ci++) { char ch = r->text[ro + ci]; CachedGlyph* g = (ch >= 32 || ch < 0) && ch != '\n' - ? font->get_glyph(gc, (unsigned char)ch) : nullptr; + ? font->get_glyph(gc, decode_single_byte_codepoint((unsigned char)ch)) : nullptr; int char_w = g ? g->advance : 0; if (x + char_w / 2 > click_x) return best_abs; @@ -611,32 +641,18 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) { return; } if (key.ascii == '\b' || key.scancode == 0x0E) { - if (wp->pathbar_cursor > 0) { - for (int i = wp->pathbar_cursor - 1; i < wp->pathbar_len - 1; i++) - wp->pathbar_text[i] = wp->pathbar_text[i + 1]; - wp->pathbar_len--; - wp->pathbar_cursor--; - wp->pathbar_text[wp->pathbar_len] = '\0'; - } + montauk::utf8::erase_before(wp->pathbar_text, &wp->pathbar_len, &wp->pathbar_cursor); return; } - if (key.scancode == 0x4B) { - if (wp->pathbar_cursor > 0) wp->pathbar_cursor--; + if (montauk::nav_scancode(key) == 0x4B) { + wp->pathbar_cursor = montauk::utf8::prev_boundary(wp->pathbar_text, wp->pathbar_cursor); return; } - if (key.scancode == 0x4D) { - if (wp->pathbar_cursor < wp->pathbar_len) wp->pathbar_cursor++; + if (montauk::nav_scancode(key) == 0x4D) { + wp->pathbar_cursor = montauk::utf8::next_boundary(wp->pathbar_text, wp->pathbar_len, wp->pathbar_cursor); return; } - if ((unsigned char)key.ascii >= 32 - && (unsigned char)key.ascii != 127 && wp->pathbar_len < 254) { - for (int i = wp->pathbar_len; i > wp->pathbar_cursor; i--) - wp->pathbar_text[i] = wp->pathbar_text[i - 1]; - wp->pathbar_text[wp->pathbar_cursor] = key.ascii; - wp->pathbar_cursor++; - wp->pathbar_len++; - wp->pathbar_text[wp->pathbar_len] = '\0'; - } + montauk::utf8::insert_key(wp->pathbar_text, &wp->pathbar_len, 255, &wp->pathbar_cursor, key); return; } @@ -736,22 +752,22 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) { return; } - if (key.ctrl && key.alt && key.scancode == 0x48) { + if (key.ctrl && key.alt && montauk::nav_scancode(key) == 0x48) { wp_adjust_paragraph_spacing_before(wp, -WP_SPACE_STEP); wp_history_checkpoint(wp); return; } - if (key.ctrl && key.alt && key.scancode == 0x50) { + if (key.ctrl && key.alt && montauk::nav_scancode(key) == 0x50) { wp_adjust_paragraph_spacing_before(wp, WP_SPACE_STEP); wp_history_checkpoint(wp); return; } - if (key.ctrl && key.shift && key.scancode == 0x48) { + if (key.ctrl && key.shift && montauk::nav_scancode(key) == 0x48) { wp_adjust_paragraph_spacing_after(wp, -WP_SPACE_STEP); wp_history_checkpoint(wp); return; } - if (key.ctrl && key.shift && key.scancode == 0x50) { + if (key.ctrl && key.shift && montauk::nav_scancode(key) == 0x50) { wp_adjust_paragraph_spacing_after(wp, WP_SPACE_STEP); wp_history_checkpoint(wp); return; @@ -783,21 +799,21 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) { return; } - if (key.scancode == 0x48) { + if (montauk::nav_scancode(key) == 0x48) { if (key.shift) wp_start_selection(wp); else wp_clear_selection(wp); wp_cursor_up(wp); if (key.shift) wp_update_selection_to_cursor(wp); return; } - if (key.scancode == 0x50) { + if (montauk::nav_scancode(key) == 0x50) { if (key.shift) wp_start_selection(wp); else wp_clear_selection(wp); wp_cursor_down(wp); if (key.shift) wp_update_selection_to_cursor(wp); return; } - if (key.scancode == 0x4B) { + if (montauk::nav_scancode(key) == 0x4B) { if (key.shift) wp_start_selection(wp); else if (wp->has_selection) { int s, e; @@ -810,7 +826,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) { if (key.shift) wp_update_selection_to_cursor(wp); return; } - if (key.scancode == 0x4D) { + if (montauk::nav_scancode(key) == 0x4D) { if (key.shift) wp_start_selection(wp); else if (wp->has_selection) { int s, e; @@ -824,7 +840,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) { return; } - if (key.scancode == 0x47) { + if (montauk::nav_scancode(key) == 0x47) { if (key.shift) wp_start_selection(wp); else wp_clear_selection(wp); int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset); @@ -834,7 +850,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) { if (key.shift) wp_update_selection_to_cursor(wp); return; } - if (key.scancode == 0x4F) { + if (montauk::nav_scancode(key) == 0x4F) { if (key.shift) wp_start_selection(wp); else wp_clear_selection(wp); int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset); @@ -858,7 +874,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) { return; } - if (key.scancode == 0x53) { + if (montauk::nav_scancode(key) == 0x53) { if (wp->has_selection) wp_delete_selection(wp); else wp_delete_char(wp); wp_history_checkpoint(wp); diff --git a/programs/src/wordprocessor/print_export.cpp b/programs/src/wordprocessor/print_export.cpp index 1253078..64b2bc4 100644 --- a/programs/src/wordprocessor/print_export.cpp +++ b/programs/src/wordprocessor/print_export.cpp @@ -217,7 +217,7 @@ static void wp_draw_print_list_marker(Canvas& c, WordProcessorState* wp, WrapLin if (!font) font = g_ui_font; int th = text_height(font, font_px); int top_y = py + (wl->height - th) / 2; - char bullet[2] = { '\x95', '\0' }; + const char* bullet = "\u2022"; draw_text(c, font, bullet_x, top_y, bullet, colors::BLACK, font_px); return; } @@ -293,7 +293,8 @@ static bool wp_encode_print_page(WordProcessorState* wp, if (ch != '\n' && (ch >= 32 || ch < 0)) { int adv = font->draw_char_to_buffer( - c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, colors::BLACK, gc); + c.pixels, c.w, c.h, x, baseline, + decode_single_byte_codepoint((unsigned char)ch), colors::BLACK, gc); x += adv; } char_idx++; diff --git a/programs/src/wordprocessor/render.cpp b/programs/src/wordprocessor/render.cpp index 8add383..754ea6f 100644 --- a/programs/src/wordprocessor/render.cpp +++ b/programs/src/wordprocessor/render.cpp @@ -111,7 +111,8 @@ static void wp_fit_ui_text(const char* src, char* out, int out_len, int max_w) { if (text_width(g_ui_font, out, fonts::UI_SIZE) <= max_w) return; int slen = montauk::slen(src); - for (int keep = slen; keep > 1; keep--) { + for (int keep = slen; keep > 1; + keep = slen - montauk::utf8::next_boundary(src, slen, slen - keep)) { char candidate[256]; candidate[0] = '.'; candidate[1] = '.'; @@ -138,7 +139,8 @@ static void wp_fit_ui_text_end(const char* src, char* out, int out_len, int max_ out[out_len - 1] = '\0'; if (text_width(g_ui_font, out, fonts::UI_SIZE) <= max_w) return; - for (int keep = montauk::slen(src); keep > 0; keep--) { + for (int keep = montauk::slen(src); keep > 0; + keep = montauk::utf8::prev_boundary(src, keep)) { char candidate[256]; if (keep > (int)sizeof(candidate) - 3) continue; montauk::memcpy(candidate, src, keep); @@ -225,7 +227,7 @@ static void wp_draw_list_marker(Canvas& c, WordProcessorState* wp, WrapLine* wl, if (!font) font = g_ui_font; int th = text_height(font, bullet_size); int top_y = py + (wl->height - th) / 2; - char bullet[2] = { '\x95', '\0' }; + const char* bullet = "\u2022"; draw_text(c, font, bullet_x, top_y, bullet, colors::TEXT_COLOR, bullet_size); return; } @@ -346,7 +348,7 @@ void wp_render() { Color special_bg = wp->special_char_flyout_open ? btn_active : btn_bg; c.fill_rounded_rect(WP_BTN_SECTION_X, 6, 24, 24, 3, special_bg); { - char section[2] = { (char)0xA7, '\0' }; + const char* section = "\u00A7"; TrueTypeFont* font = wp_get_font(wp_default_font_id(), 0); draw_text(c, font ? font : g_ui_font, WP_BTN_SECTION_X + 6, 8, section, colors::TEXT_COLOR, fonts::UI_SIZE); } @@ -355,7 +357,7 @@ void wp_render() { Color pageview_bg = wp->page_view_mode ? btn_active : btn_bg; c.fill_rounded_rect(WP_BTN_PAGEVIEW_X, 6, 24, 24, 3, pageview_bg); - wp_draw_ui_icon_button(c, WP_BTN_PAGEVIEW_X, 6, 24, 24, g_icon_page_view, "\xA7", + wp_draw_ui_icon_button(c, WP_BTN_PAGEVIEW_X, 6, 24, 24, g_icon_page_view, "\u00A7", pageview_bg, colors::TEXT_COLOR); c.hline(0, WP_TOOLBAR_H - 1, c.w, colors::BORDER); @@ -506,7 +508,7 @@ void wp_render() { int char_adv = 0; if (ch != '\n' && (ch >= 32 || ch < 0)) { - CachedGlyph* g = font->get_glyph(gc, (unsigned char)ch); + CachedGlyph* g = font->get_glyph(gc, decode_single_byte_codepoint((unsigned char)ch)); char_adv = g ? g->advance : 8; } @@ -558,7 +560,8 @@ void wp_render() { Color text_col = (wp->has_selection && abs_ch >= sel_s && abs_ch < sel_e) ? Color::from_rgb(0x10, 0x10, 0x10) : colors::TEXT_COLOR; int adv = font->draw_char_to_buffer( - c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, text_col, gc); + c.pixels, c.w, c.h, x, baseline, + decode_single_byte_codepoint((unsigned char)ch), text_col, gc); x += adv; } else { x += char_adv; @@ -717,7 +720,9 @@ void wp_render() { c.rect(dx, dy, dw, dh, colors::BORDER); for (int i = 0; i < WP_SPECIAL_CHAR_OPTION_COUNT; i++) { int iy = dy + 2 + i * WP_SPECIAL_CHAR_ROW_H; - char glyph[2] = { (char)WP_SPECIAL_CHAR_OPTIONS[i].code, '\0' }; + char glyph[5]; + glyph[montauk::utf8::encode(montauk::utf8::from_cp1252( + WP_SPECIAL_CHAR_OPTIONS[i].code), glyph)] = '\0'; c.fill_rect(dx + 2, iy, dw - 4, WP_SPECIAL_CHAR_ROW_H - 2, colors::MENU_BG); draw_text(c, glyph_font ? glyph_font : g_ui_font, dx + 8, iy + (WP_SPECIAL_CHAR_ROW_H - sfh) / 2, diff --git a/programs/src/wordprocessor/wordprocessor.hpp b/programs/src/wordprocessor/wordprocessor.hpp index 7df81dd..66caf4f 100644 --- a/programs/src/wordprocessor/wordprocessor.hpp +++ b/programs/src/wordprocessor/wordprocessor.hpp @@ -193,6 +193,10 @@ struct WPFontTable { bool loaded; }; +// Document text is Windows-1252, one byte per character, unlike the rest of +// MontaukOS (UTF-8). It matches the MWP1 file format and the WinAnsi-encoded +// fonts of PDF export. Convert at the edges: decode_single_byte_codepoint() +// for glyphs, and UTF-8 <-> 1252 for the clipboard (input.cpp). struct StyledRun { char* text; int len; diff --git a/template/sysroot/include/Api/Syscall.hpp b/template/sysroot/include/Api/Syscall.hpp index f513f59..a490c84 100644 --- a/template/sysroot/include/Api/Syscall.hpp +++ b/template/sysroot/include/Api/Syscall.hpp @@ -271,6 +271,12 @@ namespace montauk::abi { 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 + /* 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 + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; @@ -531,7 +537,10 @@ namespace montauk::abi { 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; char ascii; bool pressed; @@ -547,6 +556,32 @@ namespace montauk::abi { 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 { int32_t x; int32_t y; @@ -584,12 +619,13 @@ namespace montauk::abi { uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale uint8_t _pad[3]; 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 w, h; } resize; struct { int32_t scale; } scale; }; }; + static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries"); struct WinInfo { int32_t id; diff --git a/template/sysroot/include/gui/canvas.hpp b/template/sysroot/include/gui/canvas.hpp index c38c260..6bc125c 100644 --- a/template/sysroot/include/gui/canvas.hpp +++ b/template/sysroot/include/gui/canvas.hpp @@ -165,9 +165,9 @@ struct Canvas { if (!str || !font_data || scale <= 0) return; uint32_t pixel = c.to_pixel(); 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 = - &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++) { uint8_t bits = glyph[row]; if (!bits) continue; diff --git a/template/sysroot/include/gui/clipboard.hpp b/template/sysroot/include/gui/clipboard.hpp index 614eaba..0a35535 100644 --- a/template/sysroot/include/gui/clipboard.hpp +++ b/template/sysroot/include/gui/clipboard.hpp @@ -9,12 +9,32 @@ #include #include #include +#include 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) { if (len < 0) 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; } @@ -53,6 +73,14 @@ namespace gui { } 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_serial) *out_serial = serial; return text; diff --git a/template/sysroot/include/gui/desktop.hpp b/template/sysroot/include/gui/desktop.hpp index 54b948c..8f76ee5 100644 --- a/template/sysroot/include/gui/desktop.hpp +++ b/template/sysroot/include/gui/desktop.hpp @@ -227,6 +227,18 @@ struct DesktopState { montauk::abi::BatteryInfo battery_info; uint64_t battery_last_poll; + // Display brightness. Present only while the active output has a + // controllable backlight (laptop panel, or a monitor answering DDC/CI). + SvgIcon icon_brightness; + bool brightness_present; + bool brightness_popup_open; + Rect brightness_icon_rect; + int brightness_level; // 0-100, as shown on the slider + bool brightness_dragging; + int brightness_pending; // drag sample not yet sent, -1 none + uint64_t brightness_last_send; + uint64_t brightness_last_poll; + bool vol_popup_open; Rect vol_icon_rect; int vol_level; // 0-100 diff --git a/template/sysroot/include/gui/font.hpp b/template/sysroot/include/gui/font.hpp index 4001c16..8d35da8 100644 --- a/template/sysroot/include/gui/font.hpp +++ b/template/sysroot/include/gui/font.hpp @@ -19,6 +19,34 @@ static constexpr int FONT_HEIGHT = 16; // TrueType fonts, while login/desktop can recover when TrueType setup fails. 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) inline int system_font_height() { 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); return; } - for (int i = 0; text[i]; i++) { - draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg); + for (const char* p = text; *p; x += FONT_WIDTH) { + 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); return; } - for (int i = 0; text[i]; i++) { - draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg); + for (const char* p = text; *p; x += FONT_WIDTH) { + 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) { if (fonts::system_font && fonts::system_font->valid) { return fonts::system_font->measure_text(text, fonts::UI_SIZE); } - int len = 0; - while (text[len]) len++; - return len * FONT_WIDTH; + return montauk::utf8::count(text) * FONT_WIDTH; } } // namespace gui diff --git a/template/sysroot/include/gui/mtk/widgets.hpp b/template/sysroot/include/gui/mtk/widgets.hpp index f4ca3b0..07ff5b0 100644 --- a/template/sysroot/include/gui/mtk/widgets.hpp +++ b/template/sysroot/include/gui/mtk/widgets.hpp @@ -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); } +// 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) { - char prefix[512]; int len = 0; - while (text && text[len] && len < count && len < (int)sizeof(prefix) - 1) { - prefix[len] = masked ? '*' : text[len]; - len++; - } - prefix[len] = '\0'; - return text_width(prefix); + while (text && text[len] && len < count) len++; + if (!masked) return text_width_n(text, len); + return montauk::utf8::count(text, len) * text_width("*"); } 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 prev_w = 0; - for (int i = 0; i < len; i++) { - int cur_w = text_input_prefix_width(value, i + 1, masked); + for (int i = 0; i < len;) { + 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; if (rel_x < midpoint) return i; prev_w = cur_w; + i = next; } return len; } @@ -360,8 +360,11 @@ inline int text_input_mouse(TextInputState& state, 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) { - return (ch >= 'a' && ch <= 'z') || + return (uint8_t)ch >= 0x80 || + (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_'; @@ -574,8 +577,10 @@ inline int text_input_key(TextInputState& state, if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE; int len = text_input_len(text, cap); - state.cursor = text_input_clamp_cursor(state.cursor, len); - state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len); + state.cursor = montauk::utf8::floor_boundary( + 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) { 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); } - if (key.scancode == 0x4B || key.scancode == 0x4D || - key.scancode == 0x47 || key.scancode == 0x4F) { + if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x4D || + montauk::nav_scancode(key) == 0x47 || montauk::nav_scancode(key) == 0x4F) { 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; - 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; - else if (key.scancode == 0x4B) - next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) : state.cursor - 1; - else if (key.scancode == 0x4D) - next = key.ctrl ? text_input_next_word_boundary(text, state.cursor) : state.cursor + 1; - else if (key.scancode == 0x47) + else if (montauk::nav_scancode(key) == 0x4B) + next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) + : montauk::utf8::prev_boundary(text, state.cursor); + else if (montauk::nav_scancode(key) == 0x4D) + 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; - else if (key.scancode == 0x4F) + else if (montauk::nav_scancode(key) == 0x4F) next = len; text_input_set_cursor(state, len, next, key.shift); @@ -641,15 +648,16 @@ inline int text_input_key(TextInputState& state, changed = true; } } else if (state.cursor > 0) { - text_input_delete_range(text, &len, state.cursor - 1, state.cursor); - state.cursor--; + int start = montauk::utf8::prev_boundary(text, state.cursor); + text_input_delete_range(text, &len, start, state.cursor); + state.cursor = start; changed = true; } state.selection_anchor = state.cursor; return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0); } - if (key.scancode == 0x53) { + if (montauk::nav_scancode(key) == 0x53) { if (sel_start != sel_end) { text_input_delete_range(text, &len, sel_start, sel_end); state.cursor = sel_start; @@ -661,15 +669,22 @@ inline int text_input_key(TextInputState& state, changed = true; } } 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; } state.selection_anchor = state.cursor; return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0); } - if (key.ctrl || key.alt || !text_input_char_allowed(key.ascii, filter, userdata)) - return TEXT_INPUT_NONE; + // 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; + } if (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; } - if (len < cap - 1) { + if (len + typed_len < cap) { for (int i = len; i >= state.cursor; i--) - text[i + 1] = text[i]; - text[state.cursor++] = key.ascii; - len++; + text[i + typed_len] = text[i]; + for (int i = 0; i < typed_len; i++) + text[state.cursor++] = typed[i]; + len += typed_len; changed = true; } @@ -1251,22 +1267,34 @@ inline void draw_badge(Canvas& c, 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) { int len = 0; - while (value && value[len] && len < cap - 1) { - buffer[len] = '*'; - len++; + const char* p = value ? value : ""; + while (*p && len < cap - 1) { + montauk::utf8::next(&p); + buffer[len++] = '*'; } buffer[len] = '\0'; 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) { if (cap <= 0) return; int len = 0; - while (value && value[len] && len < count && len < cap - 1) { - buffer[len] = masked ? '*' : value[len]; - len++; + 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) { + buffer[len] = value[len]; + len++; + } } buffer[len] = '\0'; } diff --git a/template/sysroot/include/gui/terminal.hpp b/template/sysroot/include/gui/terminal.hpp index 6534bcc..23c133b 100644 --- a/template/sysroot/include/gui/terminal.hpp +++ b/template/sysroot/include/gui/terminal.hpp @@ -9,13 +9,14 @@ #include "gui/font.hpp" #include #include +#include #include #include namespace gui { struct TermCell { - char ch; + uint32_t ch; // Unicode codepoint (every cell is one column wide) Color fg; Color bg; }; @@ -58,6 +59,11 @@ struct TerminalState { int csi_params[8]; int csi_param_count; 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 ==== @@ -244,6 +250,7 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int t->csi_private = false; t->csi_param_count = 0; t->csi_current_param = 0; + t->utf8_pending_len = 0; t->child_pid = 0; 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); } -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) { t->cursor_x = 0; 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) { if (len > 0) t->dirty = true; for (int i = 0; i < len; 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) { case TerminalState::STATE_NORMAL: if (ch == '\033') { @@ -575,9 +632,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) { while (t->cursor_x < next) { terminal_put_char(t, ' '); } - } else if (ch >= 32 || ch < 0) { - // Printable character (also treat high-bit chars as printable) - terminal_put_char(t, ch); + } else if (ch >= 32) { + terminal_put_char(t, (uint8_t)ch); } 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, - int x, int y, unsigned char ch, + int x, int y, uint32_t ch, 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++) { int dy = y + fy; if (dy >= ph) break; @@ -669,14 +725,14 @@ static inline void terminal_draw_cell_pixels(const TermCell& cell, cell.bg.to_pixel()); } - if (cell.ch <= 32 && cell.ch >= 0) return; + if (cell.ch <= 32) return; if (use_ttf) { int baseline = py + gc->ascent; 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 { - 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()); } } @@ -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, g_term_palette.cursor.to_pixel()); - if (cell.ch <= 32 && cell.ch >= 0) return; + if (cell.ch <= 32) return; if (use_ttf) { int baseline = py + gc->ascent; 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 { - 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()); } } diff --git a/template/sysroot/include/gui/truetype.hpp b/template/sysroot/include/gui/truetype.hpp index f7d877e..361fc04 100644 --- a/template/sysroot/include/gui/truetype.hpp +++ b/template/sysroot/include/gui/truetype.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "gui/gui.hpp" #include "gui/framebuffer.hpp" @@ -41,39 +42,12 @@ 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) { if (codepoint < 0 || codepoint > 255) return 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; - } + return (int)montauk::utf8::from_cp1252((uint8_t)codepoint); } struct CachedGlyph { @@ -84,8 +58,18 @@ struct CachedGlyph { 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 { - 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; float scale; int ascent, descent, line_gap; @@ -142,8 +126,21 @@ struct TrueTypeFont { return true; } - void init_cache(GlyphCache* gc, int pixel_size) { - // Free any existing glyph bitmaps + static void free_ext(GlyphCache* gc) { + 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++) { if (gc->glyphs[i].bitmap) { montauk::mfree(gc->glyphs[i].bitmap); @@ -151,6 +148,11 @@ struct TrueTypeFont { gc->glyphs[i].bitmap = nullptr; 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->scale = em_scaling @@ -179,11 +181,8 @@ struct TrueTypeFont { return gc; } - // Evict oldest cache (slot 0) — free its glyph bitmaps - for (int g = 0; g < 256; g++) { - if (caches[0].glyphs[g].bitmap) - montauk::mfree(caches[0].glyphs[g].bitmap); - } + // Evict oldest cache (slot 0) - free its glyph bitmaps + free_cache(&caches[0]); // Shift remaining caches down for (int i = 0; i < 3; i++) montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache)); @@ -192,13 +191,54 @@ struct TrueTypeFont { 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) { - if (codepoint < 0 || codepoint >= 256) return nullptr; - CachedGlyph* g = &gc->glyphs[codepoint]; + if (codepoint < 0 || codepoint > 0x10FFFF) return nullptr; + CachedGlyph* g = codepoint < 256 ? &gc->glyphs[codepoint] + : ext_slot(gc, (uint32_t)codepoint); + if (!g) return nullptr; if (g->loaded) return g; g->loaded = true; - int font_codepoint = decode_single_byte_codepoint(codepoint); + int font_codepoint = codepoint; int advance, lsb; stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb); g->advance = (int)(advance * gc->scale); @@ -222,12 +262,34 @@ struct TrueTypeFont { 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) { if (!valid) return 0; GlyphCache* gc = get_cache(pixel_size); int w = 0; - for (int i = 0; text[i]; i++) { - CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p)); if (g) w += g->advance; } return w; @@ -253,8 +315,8 @@ struct TrueTypeFont { uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) | ((uint32_t)color.g << 8) | color.b; - for (int i = 0; text[i]; i++) { - CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p)); if (!g) continue; if (g->bitmap) { @@ -325,8 +387,8 @@ struct TrueTypeFont { int baseline = y + gc->ascent; bool drew_pixel = false; - for (int i = 0; text[i]; i++) { - CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p)); if (!g) continue; if (g->bitmap) { @@ -378,8 +440,8 @@ struct TrueTypeFont { int clip_x2 = clip_x + clip_w; int clip_y2 = clip_y + clip_h; - for (int i = 0; text[i]; i++) { - CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]); + for (const char* p = text; *p;) { + CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p)); if (!g) continue; 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 x, int baseline, int codepoint, Color color, GlyphCache* gc) { diff --git a/template/sysroot/include/gui/widgets.hpp b/template/sysroot/include/gui/widgets.hpp index 5aa203d..e2de916 100644 --- a/template/sysroot/include/gui/widgets.hpp +++ b/template/sysroot/include/gui/widgets.hpp @@ -212,11 +212,7 @@ struct TextBox { // Draw cursor if focused if (focused) { - // Measure text up to cursor position for proportional fonts - 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); + int cx = tx + text_width_n(text, cursor); draw_vline(fb, cx, ty, fh, cursor_color); } } @@ -230,34 +226,35 @@ struct TextBox { void handle_key(const montauk::abi::KeyEvent& key) { 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) { - // Backspace + // Backspace: remove the whole character before the cursor if (cursor > 0) { - for (int i = cursor - 1; i < text_len - 1; i++) { - text[i] = text[i + 1]; - } - text_len--; - cursor--; - text[text_len] = '\0'; + int start = montauk::utf8::prev_boundary(text, cursor); + int n = cursor - start; + for (int i = start; i <= text_len - n; i++) + text[i] = text[i + n]; + text_len -= n; + cursor = start; } - } else if ((unsigned char)key.ascii >= 32 - && (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 (typed_len > 0) { + // Printable character, inserted as UTF-8 + 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 (key.scancode == 0x4B) { + } else if (montauk::nav_scancode(key) == 0x4B) { // Left arrow - if (cursor > 0) cursor--; - } else if (key.scancode == 0x4D) { + cursor = montauk::utf8::prev_boundary(text, cursor); + } else if (montauk::nav_scancode(key) == 0x4D) { // Right arrow - if (cursor < text_len) cursor++; + cursor = montauk::utf8::next_boundary(text, text_len, cursor); } } }; diff --git a/template/sysroot/include/libc/montauk.h b/template/sysroot/include/libc/montauk.h index d08f457..7f02ad6 100644 --- a/template/sysroot/include/libc/montauk.h +++ b/template/sysroot/include/libc/montauk.h @@ -23,200 +23,202 @@ extern "C" { Generated from Api/Syscall.hpp by scripts/montauk-syscalls.py. Do not edit by hand; run 'make gen-syscalls' to refresh. Wrapper functions below are hand-written. */ -#define MTK_SYS_EXIT 0 -#define MTK_SYS_YIELD 1 -#define MTK_SYS_SLEEP_MS 2 -#define MTK_SYS_GETPID 3 -#define MTK_SYS_PRINT 4 -#define MTK_SYS_PUTCHAR 5 -#define MTK_SYS_OPEN 6 -#define MTK_SYS_READ 7 -#define MTK_SYS_GETSIZE 8 -#define MTK_SYS_CLOSE 9 -#define MTK_SYS_READDIR 10 -#define MTK_SYS_ALLOC 11 -#define MTK_SYS_FREE 12 -#define MTK_SYS_GETTICKS 13 -#define MTK_SYS_GETMILLISECONDS 14 -#define MTK_SYS_GETINFO 15 -#define MTK_SYS_ISKEYAVAILABLE 16 -#define MTK_SYS_GETKEY 17 -#define MTK_SYS_GETCHAR 18 -#define MTK_SYS_PING 19 -#define MTK_SYS_SPAWN 20 -#define MTK_SYS_FBINFO 21 -#define MTK_SYS_FBMAP 22 -#define MTK_SYS_WAITPID 23 -#define MTK_SYS_TERMSIZE 24 -#define MTK_SYS_GETARGS 25 -#define MTK_SYS_RESET 26 -#define MTK_SYS_SHUTDOWN 27 -#define MTK_SYS_GETTIME 28 -#define MTK_SYS_SOCKET 29 -#define MTK_SYS_CONNECT 30 -#define MTK_SYS_BIND 31 -#define MTK_SYS_LISTEN 32 -#define MTK_SYS_ACCEPT 33 -#define MTK_SYS_SEND 34 -#define MTK_SYS_RECV 35 -#define MTK_SYS_CLOSESOCK 36 -#define MTK_SYS_GETNETCFG 37 -#define MTK_SYS_SETNETCFG 38 -#define MTK_SYS_SENDTO 39 -#define MTK_SYS_RECVFROM 40 -#define MTK_SYS_FWRITE 41 -#define MTK_SYS_FCREATE 42 -#define MTK_SYS_TERMSCALE 43 -#define MTK_SYS_RESOLVE 44 -#define MTK_SYS_GETRANDOM 45 -#define MTK_SYS_LOG 46 -#define MTK_SYS_MOUSESTATE 47 -#define MTK_SYS_SETMOUSEBOUNDS 48 -#define MTK_SYS_SPAWN_REDIR 49 -#define MTK_SYS_CHILDIO_READ 50 -#define MTK_SYS_CHILDIO_WRITE 51 -#define MTK_SYS_CHILDIO_WRITEKEY 52 -#define MTK_SYS_CHILDIO_SETTERMSZ 53 -#define MTK_SYS_WINCREATE 54 -#define MTK_SYS_WINDESTROY 55 -#define MTK_SYS_WINPRESENT 56 -#define MTK_SYS_WINPOLL 57 -#define MTK_SYS_WINENUM 58 -#define MTK_SYS_WINMAP 59 -#define MTK_SYS_WINSENDEVENT 60 -#define MTK_SYS_PROCLIST 61 -#define MTK_SYS_KILL 62 -#define MTK_SYS_DEVLIST 63 -#define MTK_SYS_WINRESIZE 64 -#define MTK_SYS_WINSETSCALE 65 -#define MTK_SYS_WINGETSCALE 66 -#define MTK_SYS_MEMSTATS 67 -#define MTK_SYS_WINSETCURSOR 68 -#define MTK_SYS_DISKINFO 69 -#define MTK_SYS_PARTLIST 70 -#define MTK_SYS_DISKREAD 71 -#define MTK_SYS_DISKWRITE 72 -#define MTK_SYS_GPTINIT 73 -#define MTK_SYS_GPTADD 74 -#define MTK_SYS_FSMOUNT 75 -#define MTK_SYS_FSFORMAT 76 -#define MTK_SYS_FDELETE 77 -#define MTK_SYS_FMKDIR 78 -#define MTK_SYS_DRIVELIST 79 -#define MTK_SYS_AUDIOOPEN 80 -#define MTK_SYS_AUDIOCLOSE 81 -#define MTK_SYS_AUDIOWRITE 82 -#define MTK_SYS_AUDIOCTL 83 -#define MTK_SYS_BTSCAN 84 -#define MTK_SYS_BTCONNECT 85 -#define MTK_SYS_BTDISCONNECT 86 -#define MTK_SYS_BTLIST 87 -#define MTK_SYS_BTINFO 88 -#define MTK_SYS_SUSPEND 89 -#define MTK_SYS_SETTZ 90 -#define MTK_SYS_GETTZ 91 -#define MTK_SYS_SETUSER 92 -#define MTK_SYS_GETUSER 93 -#define MTK_SYS_FRENAME 94 -#define MTK_SYS_GETCWD 95 -#define MTK_SYS_CHDIR 96 -#define MTK_SYS_WINUNMAP 97 -#define MTK_SYS_DUPHANDLE 98 -#define MTK_SYS_WAIT_HANDLE 99 -#define MTK_SYS_STREAM_CREATE 100 -#define MTK_SYS_STREAM_READ 101 -#define MTK_SYS_STREAM_WRITE 102 -#define MTK_SYS_MAILBOX_CREATE 103 -#define MTK_SYS_MAILBOX_SEND 104 -#define MTK_SYS_MAILBOX_RECV 105 -#define MTK_SYS_WAITSET_CREATE 106 -#define MTK_SYS_WAITSET_ADD 107 -#define MTK_SYS_WAITSET_REMOVE 108 -#define MTK_SYS_WAITSET_WAIT 109 -#define MTK_SYS_PROC_OPEN 110 -#define MTK_SYS_SURFACE_CREATE 111 -#define MTK_SYS_SURFACE_MAP 112 -#define MTK_SYS_SURFACE_RESIZE 113 -#define MTK_SYS_LOAD_LIB 114 -#define MTK_SYS_UNLOAD_LIB 115 -#define MTK_SYS_DLSYM 116 -#define MTK_SYS_GETLIBBASE 117 -#define MTK_SYS_CRASH_REPORT 118 -#define MTK_SYS_CLIPBOARD_SET_TEXT 119 -#define MTK_SYS_CLIPBOARD_GET_INFO 120 -#define MTK_SYS_CLIPBOARD_GET_TEXT 121 -#define MTK_SYS_CLIPBOARD_CLEAR 122 -#define MTK_SYS_INPUT_WAIT 123 -#define MTK_SYS_DRIVELABEL 124 -#define MTK_SYS_NETSTATUS 125 -#define MTK_SYS_WINSETFLAGS 126 -#define MTK_SYS_DRIVEKIND 127 -#define MTK_SYS_AUDIOLIST 128 -#define MTK_SYS_AUDIOWAIT 129 -#define MTK_SYS_THREAD_SPAWN 130 -#define MTK_SYS_THREAD_EXIT 131 -#define MTK_SYS_THREAD_JOIN 132 -#define MTK_SYS_THREAD_SELF 133 -#define MTK_SYS_FS_SYNC 134 -#define MTK_SYS_POWER_REQUEST 135 -#define MTK_SYS_READDIR_AT 136 -#define MTK_SYS_BTSETADDR 137 -#define MTK_SYS_BTBONDS 138 -#define MTK_SYS_BTFORGET 139 -#define MTK_SYS_RESERVED_140 140 -#define MTK_SYS_RESERVED_141 141 -#define MTK_SYS_RESERVED_142 142 -#define MTK_SYS_RESERVED_143 143 -#define MTK_SYS_RESERVED_144 144 -#define MTK_SYS_RESERVED_145 145 -#define MTK_SYS_RESERVED_146 146 -#define MTK_SYS_RESERVED_147 147 -#define MTK_SYS_RESERVED_148 148 -#define MTK_SYS_POWERINFO 149 -#define MTK_SYS_FBFLIP 150 -#define MTK_SYS_GETEXECPATH 151 -#define MTK_SYS_STAT 152 -#define MTK_SYS_SETUNIXTIME 153 -#define MTK_SYS_DISPLAYINFO 154 -#define MTK_SYS_DISPLAYMODES 155 -#define MTK_SYS_DISPLAYSETMODE 156 -#define MTK_SYS_DISPLAYBRIGHTNESS 157 -#define MTK_SYS_WIFI_SCAN 158 -#define MTK_SYS_WIFI_INFO 159 -#define MTK_SYS_WIFI_CONNECT 160 -#define MTK_SYS_WIFI_DISCONNECT 161 -#define MTK_SYS_WIFI_SCAN_START 162 -#define MTK_SYS_WIFI_RESULTS 163 -#define MTK_SYS_WIFI_CONNECT_ASYNC 164 -#define MTK_SYS_NETIFS 165 -#define MTK_SYS_GETCHAR_NB 166 -#define MTK_SYS_UTIME 167 -#define MTK_SYS_MMAP_ANON 168 -#define MTK_SYS_MUNMAP 169 -#define MTK_SYS_MPROTECT 170 -#define MTK_SYS_GETENVIRON 171 -#define MTK_SYS_SETENVIRON 172 -#define MTK_SYS_SPAWN_ENV 173 -#define MTK_SYS_SETSESSION 174 -#define MTK_SYS_KILLSESSION 175 -#define MTK_SYS_LOG_WRITE 176 -#define MTK_SYS_TERMINAL_ATTACHED 177 -#define MTK_SYS_USB_LIST 178 -#define MTK_SYS_USB_CLAIM 179 -#define MTK_SYS_USB_CLOSE 180 -#define MTK_SYS_USB_CONTROL 181 -#define MTK_SYS_USB_BULK_IN_START 182 -#define MTK_SYS_USB_BULK_IN_STOP 183 -#define MTK_SYS_USB_BULK_IN_READ 184 -#define MTK_SYS_SPAWN_CAPS 185 -#define MTK_SYS_SPAWN_REDIR_CAPS 186 -#define MTK_SYS_ALLOC_EAGER 187 -#define MTK_SYS_BATTERYINFO 188 -#define MTK_SYS_GETCAPS 189 -#define MTK_SYS_VOLINFO 190 -#define MTK_SYS_CONSENT_REGISTER 191 -#define MTK_SYS_CONSENT_NEXT 192 -#define MTK_SYS_CONSENT_REPLY 193 +#define MTK_SYS_EXIT 0 +#define MTK_SYS_YIELD 1 +#define MTK_SYS_SLEEP_MS 2 +#define MTK_SYS_GETPID 3 +#define MTK_SYS_PRINT 4 +#define MTK_SYS_PUTCHAR 5 +#define MTK_SYS_OPEN 6 +#define MTK_SYS_READ 7 +#define MTK_SYS_GETSIZE 8 +#define MTK_SYS_CLOSE 9 +#define MTK_SYS_READDIR 10 +#define MTK_SYS_ALLOC 11 +#define MTK_SYS_FREE 12 +#define MTK_SYS_GETTICKS 13 +#define MTK_SYS_GETMILLISECONDS 14 +#define MTK_SYS_GETINFO 15 +#define MTK_SYS_ISKEYAVAILABLE 16 +#define MTK_SYS_GETKEY 17 +#define MTK_SYS_GETCHAR 18 +#define MTK_SYS_PING 19 +#define MTK_SYS_SPAWN 20 +#define MTK_SYS_FBINFO 21 +#define MTK_SYS_FBMAP 22 +#define MTK_SYS_WAITPID 23 +#define MTK_SYS_TERMSIZE 24 +#define MTK_SYS_GETARGS 25 +#define MTK_SYS_RESET 26 +#define MTK_SYS_SHUTDOWN 27 +#define MTK_SYS_GETTIME 28 +#define MTK_SYS_SOCKET 29 +#define MTK_SYS_CONNECT 30 +#define MTK_SYS_BIND 31 +#define MTK_SYS_LISTEN 32 +#define MTK_SYS_ACCEPT 33 +#define MTK_SYS_SEND 34 +#define MTK_SYS_RECV 35 +#define MTK_SYS_CLOSESOCK 36 +#define MTK_SYS_GETNETCFG 37 +#define MTK_SYS_SETNETCFG 38 +#define MTK_SYS_SENDTO 39 +#define MTK_SYS_RECVFROM 40 +#define MTK_SYS_FWRITE 41 +#define MTK_SYS_FCREATE 42 +#define MTK_SYS_TERMSCALE 43 +#define MTK_SYS_RESOLVE 44 +#define MTK_SYS_GETRANDOM 45 +#define MTK_SYS_LOG 46 +#define MTK_SYS_MOUSESTATE 47 +#define MTK_SYS_SETMOUSEBOUNDS 48 +#define MTK_SYS_SPAWN_REDIR 49 +#define MTK_SYS_CHILDIO_READ 50 +#define MTK_SYS_CHILDIO_WRITE 51 +#define MTK_SYS_CHILDIO_WRITEKEY 52 +#define MTK_SYS_CHILDIO_SETTERMSZ 53 +#define MTK_SYS_WINCREATE 54 +#define MTK_SYS_WINDESTROY 55 +#define MTK_SYS_WINPRESENT 56 +#define MTK_SYS_WINPOLL 57 +#define MTK_SYS_WINENUM 58 +#define MTK_SYS_WINMAP 59 +#define MTK_SYS_WINSENDEVENT 60 +#define MTK_SYS_PROCLIST 61 +#define MTK_SYS_KILL 62 +#define MTK_SYS_DEVLIST 63 +#define MTK_SYS_WINRESIZE 64 +#define MTK_SYS_WINSETSCALE 65 +#define MTK_SYS_WINGETSCALE 66 +#define MTK_SYS_MEMSTATS 67 +#define MTK_SYS_WINSETCURSOR 68 +#define MTK_SYS_DISKINFO 69 +#define MTK_SYS_PARTLIST 70 +#define MTK_SYS_DISKREAD 71 +#define MTK_SYS_DISKWRITE 72 +#define MTK_SYS_GPTINIT 73 +#define MTK_SYS_GPTADD 74 +#define MTK_SYS_FSMOUNT 75 +#define MTK_SYS_FSFORMAT 76 +#define MTK_SYS_FDELETE 77 +#define MTK_SYS_FMKDIR 78 +#define MTK_SYS_DRIVELIST 79 +#define MTK_SYS_AUDIOOPEN 80 +#define MTK_SYS_AUDIOCLOSE 81 +#define MTK_SYS_AUDIOWRITE 82 +#define MTK_SYS_AUDIOCTL 83 +#define MTK_SYS_BTSCAN 84 +#define MTK_SYS_BTCONNECT 85 +#define MTK_SYS_BTDISCONNECT 86 +#define MTK_SYS_BTLIST 87 +#define MTK_SYS_BTINFO 88 +#define MTK_SYS_SUSPEND 89 +#define MTK_SYS_SETTZ 90 +#define MTK_SYS_GETTZ 91 +#define MTK_SYS_SETUSER 92 +#define MTK_SYS_GETUSER 93 +#define MTK_SYS_FRENAME 94 +#define MTK_SYS_GETCWD 95 +#define MTK_SYS_CHDIR 96 +#define MTK_SYS_WINUNMAP 97 +#define MTK_SYS_DUPHANDLE 98 +#define MTK_SYS_WAIT_HANDLE 99 +#define MTK_SYS_STREAM_CREATE 100 +#define MTK_SYS_STREAM_READ 101 +#define MTK_SYS_STREAM_WRITE 102 +#define MTK_SYS_MAILBOX_CREATE 103 +#define MTK_SYS_MAILBOX_SEND 104 +#define MTK_SYS_MAILBOX_RECV 105 +#define MTK_SYS_WAITSET_CREATE 106 +#define MTK_SYS_WAITSET_ADD 107 +#define MTK_SYS_WAITSET_REMOVE 108 +#define MTK_SYS_WAITSET_WAIT 109 +#define MTK_SYS_PROC_OPEN 110 +#define MTK_SYS_SURFACE_CREATE 111 +#define MTK_SYS_SURFACE_MAP 112 +#define MTK_SYS_SURFACE_RESIZE 113 +#define MTK_SYS_LOAD_LIB 114 +#define MTK_SYS_UNLOAD_LIB 115 +#define MTK_SYS_DLSYM 116 +#define MTK_SYS_GETLIBBASE 117 +#define MTK_SYS_CRASH_REPORT 118 +#define MTK_SYS_CLIPBOARD_SET_TEXT 119 +#define MTK_SYS_CLIPBOARD_GET_INFO 120 +#define MTK_SYS_CLIPBOARD_GET_TEXT 121 +#define MTK_SYS_CLIPBOARD_CLEAR 122 +#define MTK_SYS_INPUT_WAIT 123 +#define MTK_SYS_DRIVELABEL 124 +#define MTK_SYS_NETSTATUS 125 +#define MTK_SYS_WINSETFLAGS 126 +#define MTK_SYS_DRIVEKIND 127 +#define MTK_SYS_AUDIOLIST 128 +#define MTK_SYS_AUDIOWAIT 129 +#define MTK_SYS_THREAD_SPAWN 130 +#define MTK_SYS_THREAD_EXIT 131 +#define MTK_SYS_THREAD_JOIN 132 +#define MTK_SYS_THREAD_SELF 133 +#define MTK_SYS_FS_SYNC 134 +#define MTK_SYS_POWER_REQUEST 135 +#define MTK_SYS_READDIR_AT 136 +#define MTK_SYS_BTSETADDR 137 +#define MTK_SYS_BTBONDS 138 +#define MTK_SYS_BTFORGET 139 +#define MTK_SYS_RESERVED_140 140 +#define MTK_SYS_RESERVED_141 141 +#define MTK_SYS_RESERVED_142 142 +#define MTK_SYS_RESERVED_143 143 +#define MTK_SYS_RESERVED_144 144 +#define MTK_SYS_RESERVED_145 145 +#define MTK_SYS_RESERVED_146 146 +#define MTK_SYS_RESERVED_147 147 +#define MTK_SYS_RESERVED_148 148 +#define MTK_SYS_POWERINFO 149 +#define MTK_SYS_FBFLIP 150 +#define MTK_SYS_GETEXECPATH 151 +#define MTK_SYS_STAT 152 +#define MTK_SYS_SETUNIXTIME 153 +#define MTK_SYS_DISPLAYINFO 154 +#define MTK_SYS_DISPLAYMODES 155 +#define MTK_SYS_DISPLAYSETMODE 156 +#define MTK_SYS_DISPLAYBRIGHTNESS 157 +#define MTK_SYS_WIFI_SCAN 158 +#define MTK_SYS_WIFI_INFO 159 +#define MTK_SYS_WIFI_CONNECT 160 +#define MTK_SYS_WIFI_DISCONNECT 161 +#define MTK_SYS_WIFI_SCAN_START 162 +#define MTK_SYS_WIFI_RESULTS 163 +#define MTK_SYS_WIFI_CONNECT_ASYNC 164 +#define MTK_SYS_NETIFS 165 +#define MTK_SYS_GETCHAR_NB 166 +#define MTK_SYS_UTIME 167 +#define MTK_SYS_MMAP_ANON 168 +#define MTK_SYS_MUNMAP 169 +#define MTK_SYS_MPROTECT 170 +#define MTK_SYS_GETENVIRON 171 +#define MTK_SYS_SETENVIRON 172 +#define MTK_SYS_SPAWN_ENV 173 +#define MTK_SYS_SETSESSION 174 +#define MTK_SYS_KILLSESSION 175 +#define MTK_SYS_LOG_WRITE 176 +#define MTK_SYS_TERMINAL_ATTACHED 177 +#define MTK_SYS_USB_LIST 178 +#define MTK_SYS_USB_CLAIM 179 +#define MTK_SYS_USB_CLOSE 180 +#define MTK_SYS_USB_CONTROL 181 +#define MTK_SYS_USB_BULK_IN_START 182 +#define MTK_SYS_USB_BULK_IN_STOP 183 +#define MTK_SYS_USB_BULK_IN_READ 184 +#define MTK_SYS_SPAWN_CAPS 185 +#define MTK_SYS_SPAWN_REDIR_CAPS 186 +#define MTK_SYS_ALLOC_EAGER 187 +#define MTK_SYS_BATTERYINFO 188 +#define MTK_SYS_GETCAPS 189 +#define MTK_SYS_VOLINFO 190 +#define MTK_SYS_CONSENT_REGISTER 191 +#define MTK_SYS_CONSENT_NEXT 192 +#define MTK_SYS_CONSENT_REPLY 193 +#define MTK_SYS_GETKEY_EX 194 +#define MTK_SYS_CHILDIO_WRITEKEY_EX 195 /* @SYSCALLS-END */ /* Syscall status codes (montauk/statuscodes.h for C++). */ diff --git a/template/sysroot/include/montauk/keyboard.h b/template/sysroot/include/montauk/keyboard.h index 74bb23a..ef5a1a6 100644 --- a/template/sysroot/include/montauk/keyboard.h +++ b/template/sysroot/include/montauk/keyboard.h @@ -8,27 +8,34 @@ #include #include +#include namespace montauk::keyboard { -inline constexpr int MAX_LAYOUTS = 8; -inline constexpr int MAX_KEYS = 64; +inline constexpr int MAX_LAYOUTS = 8; +inline constexpr int MAX_KEYS = 64; +inline constexpr int MAX_DEAD_KEYS = 16; -// One overridden key. Characters are Windows-1252 bytes to match the -// single-byte GUI text stack; 0 means "no override, keep the kernel's value". +// One overridden key. Characters are Unicode codepoints; translation stores +// them in KeyEvent::codepoint (and the Windows-1252 byte, if any, in ascii). +// 0 means "no override, keep the kernel's value". struct KeyMap { - uint8_t scancode; - uint8_t base; - uint8_t shift; - uint8_t altgr; + uint8_t scancode; + uint32_t base; + uint32_t shift; + uint32_t altgr; }; struct Layout { - char id[8]; - char name[48]; - char short_name[8]; - KeyMap keys[MAX_KEYS]; - int key_count; // 0 = passthrough (the kernel's US table) + char id[8]; + char name[48]; + char short_name[8]; + KeyMap keys[MAX_KEYS]; + 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 { @@ -40,8 +47,71 @@ struct State { Registry registry; bool enabled[MAX_LAYOUTS]; 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, const char* id, const char* suffix = nullptr) { int pos = 0; @@ -54,10 +124,9 @@ inline void build_key(char* out, int cap, const char* prefix, 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. -inline int read_bytes(const toml::Doc& doc, const char* key, - uint8_t* out, int cap) { +inline int read_bytes(const toml::Doc& doc, const char* key, uint8_t* out, int cap) { toml::Value* arr = doc.get_array(key); if (!arr) 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; } +// 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.] table. Returns false if the layout declares key // overrides but they are inconsistent, in which case the caller skips it: a // 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), sizeof(out->short_name)); - uint8_t scancodes[MAX_KEYS]; - uint8_t base[MAX_KEYS]; - uint8_t shift[MAX_KEYS]; - uint8_t altgr[MAX_KEYS] = {}; + 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]; + uint32_t base[MAX_KEYS]; + uint32_t shift[MAX_KEYS]; + uint32_t altgr[MAX_KEYS] = {}; build_key(key, sizeof(key), "layouts.", id, ".scancodes"); 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"); - 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"); - 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"); - 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; 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. inline void refresh_selection(State* state, const char* username) { 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; toml::Doc doc = config::load_user(username, "keyboard"); @@ -237,57 +329,117 @@ inline int next_enabled(const State& state, int current) { } // 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 -// accented letters (aa 0xE5 / AA 0xC5) without wrongly upper-casing keys such -// as 2 / " where the shifted value is unrelated punctuation. -inline constexpr bool is_letter_pair(uint8_t base, uint8_t shift) { +// base and shift are the lower/upper case of one letter. That covers accented +// letters in any script (aa/AA, e-caron/E-caron) without wrongly upper-casing +// keys such as 2 / " where the shifted value is unrelated punctuation. +inline constexpr bool is_letter_pair(uint32_t base, uint32_t shift) { if (base == 0 || shift == 0) return false; - if (base < 0x61) return false; - if (base > 0x7A && base < 0xE0) return false; - if (base == 0xF7) return false; // division sign sits inside the range - return shift == (uint8_t)(base - 0x20); + return utf8::is_case_pair(base, shift); } -static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA -static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE -static_assert(is_letter_pair(0xE6, 0xC6)); // ae / AE +static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA +static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE +static_assert(is_letter_pair(0x11B, 0x11A)); // e-caron static_assert(is_letter_pair('a', 'A')); 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 (state.active < 0 || state.active >= state.registry.count) return; - const Layout& layout = state.registry.items[state.active]; + const Layout* layout = (state.active >= 0 && state.active < state.registry.count) + ? &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 // (keypad "/" arrives as 0x35, the same as the main "/"), so translating // 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++) { + const KeyMap& mapping = layout->keys[i]; + if (mapping.scancode != scancode) continue; - uint8_t scancode = key->scancode & 0x7F; - for (int i = 0; i < layout.key_count; i++) { - const KeyMap& mapping = layout.keys[i]; - if (mapping.scancode != scancode) continue; + uint32_t out; + if (key->altgr) { + out = mapping.altgr; + } else { + bool upper = key->shift; + if (is_letter_pair(mapping.base, mapping.shift)) + upper = key->shift != key->capslock; + out = upper ? mapping.shift : mapping.base; + } + if (out != 0) { + cp = out; + // An AltGr key that produced a character is text, not a + // shortcut. Apps gate insertion on !alt (and alt is + // LeftAlt||RightAlt), so leaving it set would silently swallow + // every AltGr character. + if (key->altgr) key->alt = false; + } + break; + } + } - uint8_t out; - if (key->altgr) { - out = mapping.altgr; - } else { - bool upper = key->shift; - if (is_letter_pair(mapping.base, mapping.shift)) - upper = key->shift != key->capslock; - out = upper ? mapping.shift : mapping.base; - } - if (out != 0) { - key->ascii = (char)out; - // An AltGr key that produced a character is text, not a shortcut. - // Apps gate insertion on !alt (and alt is LeftAlt||RightAlt), so - // leaving it set would silently swallow every AltGr character. - if (key->altgr) key->alt = false; + 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; } + 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 ---- diff --git a/template/sysroot/include/montauk/statuscodes.h b/template/sysroot/include/montauk/statuscodes.h index 1185349..e91813a 100644 --- a/template/sysroot/include/montauk/statuscodes.h +++ b/template/sysroot/include/montauk/statuscodes.h @@ -6,29 +6,84 @@ #pragma once -// Userspace mirror of kernel/src/Common/StatusCodes.hpp. Also usable from C -// (the libc includes it), where the enumerators are unqualified. +// Userspace status codes. +// Analogous to kernel/src/Common/StatusCodes.hpp, but positive numbers are used. #ifdef __cplusplus namespace montauk { #endif -enum StatusCode { +typedef enum { Success = 0, - GenericFailure = -1, - PermissionDenied = -2, - CapabilityDenied = -3, - SystemIntegrityError = -4, - BadPointer = -5, - InvalidArgument = -6, - UnsupportedOp = -7, - NoDevice = -8, - IoFailure = -9, - BadInvocation = -10, - NoSuchPath = -11, - Busy = -12, - BufferTooSmall = -13, - OutOfMemory = -14 -}; + GenericFailure = 1, + PermissionDenied = 2, + CapabilityDenied = 3, + SystemIntegrityError = 4, + BadPointer = 5, + InvalidArgument = 6, + UnsupportedOp = 7, + NoDevice = 8, + IoFailure = 9, + BadInvocation = 10, + NoSuchPath = 11, + Busy = 12, + BufferTooSmall = 13, + OutOfMemory = 14 +} StatusCode; + +inline const char* GetStatusText(StatusCode statusCode) { + const char* statusText[] = { + "Success", // 0 + "Generic failure", // 1 + "Permission denied", // 2 + "Capability denied", // 3 + "System integrity error", // 4 + "Bad pointer", // 5 + "Invalid argument", // 6 + "Unsupported operation", // 7 + "No device", // 8 + "I/O failure", // 9 + "Bad invocation", // 10 + "No such path", // 11 + "Busy", // 12 + "Buffer too small", // 13 + "Out of memory", // 14 + }; + + if (statusCode > 0 && statusCode <= 14) { + return statusText[statusCode]; + } else { + return "Unknown error"; + } +} + +// Shared with C (libc.c), so no nullptr. +inline const char* GetStatusExplanationText(StatusCode statusCode) { + const char* explanationText[] = { + 0, // 0 + 0, // 1 + 0, // 2 + 0, // 3 + "The path is protected by the system and cannot be modified.", // 4 + 0, // 5 + 0, // 6 + 0, // 7 + 0, // 8 + 0, // 9 + 0, // 10 + 0, // 11 + 0, // 12 + "The buffer supplied by the caller is too small.", // 13 + 0, // 14 + }; + + if (statusCode > 0 && statusCode <= 14) { + if (explanationText[statusCode] != 0) { + return explanationText[statusCode]; + } + } + + return GetStatusText(statusCode); +} #ifdef __cplusplus } diff --git a/template/sysroot/include/montauk/syscall.h b/template/sysroot/include/montauk/syscall.h index 5c9c188..b2ac9a1 100644 --- a/template/sysroot/include/montauk/syscall.h +++ b/template/sysroot/include/montauk/syscall.h @@ -7,6 +7,7 @@ #pragma once #include #include +#include namespace montauk { @@ -222,7 +223,23 @@ namespace montauk { // Keyboard 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); } // Returns 0 rather than blocking when no character is pending. 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); } 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) { return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows); diff --git a/template/sysroot/include/montauk/user.h b/template/sysroot/include/montauk/user.h index efff1e0..855d0a7 100644 --- a/template/sysroot/include/montauk/user.h +++ b/template/sysroot/include/montauk/user.h @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace montauk { @@ -107,7 +108,7 @@ namespace user { 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]; hash_password(password, salt_hex, computed); @@ -118,6 +119,26 @@ namespace user { 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 ---- inline int load_users(UserInfo* buf, int max) { diff --git a/template/sysroot/include/montauk/utf8.h b/template/sysroot/include/montauk/utf8.h new file mode 100644 index 0000000..dae0f33 --- /dev/null +++ b/template/sysroot/include/montauk/utf8.h @@ -0,0 +1,399 @@ +/* + * utf8.h + * UTF-8 text encoding helpers with a Windows-1252 fallback + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include + +// 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 +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 +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 +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 +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 diff --git a/template/sysroot/include/rtlsdr/rtlsdr.h b/template/sysroot/include/rtlsdr/rtlsdr.h deleted file mode 100644 index bebc465..0000000 --- a/template/sysroot/include/rtlsdr/rtlsdr.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include -#ifdef __cplusplus -extern "C" { -#endif -typedef struct rtlsdr_device rtlsdr_device; -typedef struct rtlsdr_device_info { - char name[64]; - char tuner[32]; - char serial[32]; - uint64_t freq_min; - uint64_t freq_max; - uint32_t sample_rate_min; - uint32_t sample_rate_max; - uint32_t num_gains; - int32_t gains[32]; -} rtlsdr_device_info; -int rtlsdr_count(void); -int rtlsdr_get_device_info(int index, rtlsdr_device_info* out); -int rtlsdr_open(rtlsdr_device** out, int index); -int rtlsdr_close(rtlsdr_device* dev); -int rtlsdr_set_center_freq(rtlsdr_device* dev, uint64_t hz); -uint64_t rtlsdr_get_center_freq(const rtlsdr_device* dev); -int rtlsdr_set_sample_rate(rtlsdr_device* dev, uint32_t hz); -uint32_t rtlsdr_get_sample_rate(const rtlsdr_device* dev); -int rtlsdr_set_tuner_gain_mode(rtlsdr_device* dev, int manual); -int rtlsdr_set_tuner_gain(rtlsdr_device* dev, int tenths_db); -int rtlsdr_set_freq_correction(rtlsdr_device* dev, int ppm); -int rtlsdr_set_agc_mode(rtlsdr_device* dev, int on); -int rtlsdr_set_direct_sampling(rtlsdr_device* dev, int mode); -int rtlsdr_start(rtlsdr_device* dev); -int rtlsdr_stop(rtlsdr_device* dev); -int rtlsdr_read(rtlsdr_device* dev, void* data, uint32_t length); -#ifdef __cplusplus -} -#endif diff --git a/template/sysroot/lib/libjpeg.a b/template/sysroot/lib/libjpeg.a index 1b4f728..df67214 100644 Binary files a/template/sysroot/lib/libjpeg.a and b/template/sysroot/lib/libjpeg.a differ diff --git a/template/sysroot/lib/liblibc.a b/template/sysroot/lib/liblibc.a index 17fd946..f4b1b69 100644 Binary files a/template/sysroot/lib/liblibc.a and b/template/sysroot/lib/liblibc.a differ diff --git a/template/sysroot/lib/libtls.a b/template/sysroot/lib/libtls.a index b4e9ae8..adbb377 100644 Binary files a/template/sysroot/lib/libtls.a and b/template/sysroot/lib/libtls.a differ