feat: switch text to UTF-8; add Unicode key events, dead keys & Czech layout; fix numpad navigation; add NumLock support
This commit is contained in:
1 parent
32660ddd1a
commit
2aa6360b3d
101 files changed
+3283
-1189
No files matched your search
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 289
|
#define MONTAUK_BUILD_NUMBER 296
|
||||||
@@ -122,8 +122,9 @@ namespace montauk::abi {
|
|||||||
return WriteAllToStream(stream, (const uint8_t*)data, len);
|
return WriteAllToStream(stream, (const uint8_t*)data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) {
|
// Queue a key event (raw bytes, `len` of them) on a redirected child's key
|
||||||
if (key == nullptr) return InvalidArgument;
|
// mailbox. Readers accept both the 9-byte legacy and the full format.
|
||||||
|
static int ChildIoQueueKey(int childPid, const void* key, uint16_t len) {
|
||||||
auto* child = Sched::GetProcessByPid(childPid);
|
auto* child = Sched::GetProcessByPid(childPid);
|
||||||
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
||||||
return PermissionDenied;
|
return PermissionDenied;
|
||||||
@@ -133,7 +134,7 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
|
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
|
||||||
int rc = Ipc::MailboxSend(mailbox, 0, key, sizeof(KeyEvent));
|
int rc = Ipc::MailboxSend(mailbox, 0, key, len);
|
||||||
if (rc < 0) return GenericFailure;
|
if (rc < 0) return GenericFailure;
|
||||||
if (rc == 0) {
|
if (rc == 0) {
|
||||||
Sched::BlockOnObjectSince(mailbox, 0, observedWake);
|
Sched::BlockOnObjectSince(mailbox, 0, observedWake);
|
||||||
@@ -143,6 +144,22 @@ namespace montauk::abi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int Sys_ChildIoWriteKey(int childPid, const KeyEventV1* key) {
|
||||||
|
if (key == nullptr) return InvalidArgument;
|
||||||
|
KeyEventV1 copy = *key;
|
||||||
|
return ChildIoQueueKey(childPid, ©, sizeof(copy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size-versioned: copies min(size, sizeof(KeyEvent)) bytes (validated
|
||||||
|
// readable by the dispatcher) and zero-fills the rest.
|
||||||
|
static int Sys_ChildIoWriteKeyEx(int childPid, const void* key, uint64_t size) {
|
||||||
|
if (key == nullptr || size < sizeof(KeyEventV1)) return InvalidArgument;
|
||||||
|
KeyEvent ev;
|
||||||
|
memset(&ev, 0, sizeof(ev));
|
||||||
|
memcpy(&ev, key, size < sizeof(ev) ? size : sizeof(ev));
|
||||||
|
return ChildIoQueueKey(childPid, &ev, sizeof(ev));
|
||||||
|
}
|
||||||
|
|
||||||
static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) {
|
static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) {
|
||||||
auto* child = Sched::GetProcessByPid(childPid);
|
auto* child = Sched::GetProcessByPid(childPid);
|
||||||
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
||||||
|
|||||||
+137
-22
@@ -12,6 +12,107 @@
|
|||||||
#include "Common.hpp"
|
#include "Common.hpp"
|
||||||
|
|
||||||
namespace montauk::abi {
|
namespace montauk::abi {
|
||||||
|
// ---- Key text ----------------------------------------------------------
|
||||||
|
// Keyboard layouts are applied in userspace (desktop, terminal), which send
|
||||||
|
// KeyEvents carrying the Unicode codepoint. Binaries built before that send
|
||||||
|
// 9-byte KeyEventV1s whose `ascii` is a Windows-1252 byte; both kinds share
|
||||||
|
// a redirected child's key mailbox, told apart by message length.
|
||||||
|
|
||||||
|
static uint32_t KeyCp1252ToUnicode(uint8_t b) {
|
||||||
|
static constexpr uint16_t high[32] = {
|
||||||
|
0x20AC, 0x81, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
|
||||||
|
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8D, 0x017D, 0x8F,
|
||||||
|
0x90, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
|
||||||
|
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x9D, 0x017E, 0x0178,
|
||||||
|
};
|
||||||
|
return (b >= 0x80 && b <= 0x9F) ? high[b - 0x80] : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The character a key event types, or 0 for control/navigation keys.
|
||||||
|
static uint32_t KeyTextCodepoint(const KeyEvent& ev) {
|
||||||
|
if (ev.codepoint) return ev.codepoint;
|
||||||
|
uint8_t b = (uint8_t)ev.ascii;
|
||||||
|
if (b < 0x20 || b == 0x7F) return 0;
|
||||||
|
return KeyCp1252ToUnicode(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int KeyEncodeUtf8(uint32_t cp, uint8_t* out) {
|
||||||
|
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = 0xFFFD;
|
||||||
|
if (cp < 0x80) { out[0] = (uint8_t)cp; return 1; }
|
||||||
|
if (cp < 0x800) {
|
||||||
|
out[0] = (uint8_t)(0xC0 | (cp >> 6));
|
||||||
|
out[1] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (cp < 0x10000) {
|
||||||
|
out[0] = (uint8_t)(0xE0 | (cp >> 12));
|
||||||
|
out[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
out[2] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
out[0] = (uint8_t)(0xF0 | (cp >> 18));
|
||||||
|
out[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
|
||||||
|
out[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
out[3] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void KeyFromDriver(const Drivers::PS2::Keyboard::KeyEvent& k, KeyEvent* out) {
|
||||||
|
memset(out, 0, sizeof(KeyEvent));
|
||||||
|
out->scancode = k.Scancode;
|
||||||
|
out->ascii = k.Ascii;
|
||||||
|
out->pressed = k.Pressed;
|
||||||
|
out->shift = k.Shift;
|
||||||
|
out->ctrl = k.Ctrl;
|
||||||
|
out->alt = k.Alt;
|
||||||
|
out->capslock = k.CapsLock;
|
||||||
|
out->altgr = k.AltGr;
|
||||||
|
out->extended = k.Extended;
|
||||||
|
// The kernel's own scancode table is US ASCII.
|
||||||
|
uint8_t a = (uint8_t)k.Ascii;
|
||||||
|
out->codepoint = (a >= 0x20 && a < 0x7F) ? a : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive one key from a redirected child's mailbox as a full KeyEvent,
|
||||||
|
// upgrading legacy 9-byte messages. Returns MailboxRecv's result.
|
||||||
|
static int KeyMailboxRecv(Ipc::Mailbox* mailbox, KeyEvent* out) {
|
||||||
|
memset(out, 0, sizeof(KeyEvent));
|
||||||
|
uint16_t len = sizeof(KeyEvent);
|
||||||
|
int rc = Ipc::MailboxRecv(mailbox, nullptr, out, &len, true);
|
||||||
|
if (rc > 0 && len < sizeof(KeyEvent)) {
|
||||||
|
memset((uint8_t*)out + len, 0, sizeof(KeyEvent) - len);
|
||||||
|
out->codepoint = KeyTextCodepoint(*out);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn a pressed key into the next getchar byte. Characters above ASCII are
|
||||||
|
// returned as UTF-8, one byte per call; the rest is parked in the process.
|
||||||
|
static bool KeyTakeChar(Sched::Process* proc, const KeyEvent& ev, char* out) {
|
||||||
|
if (!ev.pressed) return false;
|
||||||
|
uint32_t cp = KeyTextCodepoint(ev);
|
||||||
|
if (cp >= 0x80 && proc != nullptr) {
|
||||||
|
int n = KeyEncodeUtf8(cp, proc->getcharUtf8);
|
||||||
|
proc->getcharUtf8Len = (uint8_t)n;
|
||||||
|
proc->getcharUtf8Pos = 1;
|
||||||
|
*out = (char)proc->getcharUtf8[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (ev.ascii != 0 && ev.ascii != KEY_ASCII_SUBSTITUTE) {
|
||||||
|
*out = ev.ascii;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool KeyPendingChar(Sched::Process* proc, char* out) {
|
||||||
|
if (proc == nullptr || proc->getcharUtf8Pos >= proc->getcharUtf8Len) return false;
|
||||||
|
*out = (char)proc->getcharUtf8[proc->getcharUtf8Pos++];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Syscalls ------------------------------------------------------------
|
||||||
|
|
||||||
static bool Sys_IsKeyAvailable() {
|
static bool Sys_IsKeyAvailable() {
|
||||||
auto* proc = Sched::GetCurrentProcessPtr();
|
auto* proc = Sched::GetCurrentProcessPtr();
|
||||||
if (proc && proc->redirected) {
|
if (proc && proc->redirected) {
|
||||||
@@ -27,8 +128,8 @@ namespace montauk::abi {
|
|||||||
return Drivers::PS2::Keyboard::IsKeyAvailable();
|
return Drivers::PS2::Keyboard::IsKeyAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Sys_GetKey(KeyEvent* outEvent) {
|
// Blocking read of the next key event, in the full format.
|
||||||
if (outEvent == nullptr) return;
|
static void ReadKeyEvent(KeyEvent* outEvent) {
|
||||||
auto* proc = Sched::GetCurrentProcessPtr();
|
auto* proc = Sched::GetCurrentProcessPtr();
|
||||||
if (proc && proc->redirected) {
|
if (proc && proc->redirected) {
|
||||||
Ipc::HandleSnapshot snapshot;
|
Ipc::HandleSnapshot snapshot;
|
||||||
@@ -36,8 +137,7 @@ namespace montauk::abi {
|
|||||||
if (mailbox != nullptr) {
|
if (mailbox != nullptr) {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
|
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
|
||||||
uint16_t len = sizeof(KeyEvent);
|
int rc = KeyMailboxRecv(mailbox, outEvent);
|
||||||
int rc = Ipc::MailboxRecv(mailbox, nullptr, outEvent, &len, true);
|
|
||||||
if (rc > 0) return;
|
if (rc > 0) return;
|
||||||
if (rc < 0) {
|
if (rc < 0) {
|
||||||
memset(outEvent, 0, sizeof(KeyEvent));
|
memset(outEvent, 0, sizeof(KeyEvent));
|
||||||
@@ -47,20 +147,33 @@ namespace montauk::abi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
auto k = Drivers::PS2::Keyboard::GetKey();
|
KeyFromDriver(Drivers::PS2::Keyboard::GetKey(), outEvent);
|
||||||
outEvent->scancode = k.Scancode;
|
}
|
||||||
outEvent->ascii = k.Ascii;
|
|
||||||
outEvent->pressed = k.Pressed;
|
// SYS_GETKEY: legacy 9-byte event. A new-format event's first 9 bytes are
|
||||||
outEvent->shift = k.Shift;
|
// already a valid KeyEventV1 (ascii holds the 1252 byte or SUB).
|
||||||
outEvent->ctrl = k.Ctrl;
|
static void Sys_GetKey(KeyEventV1* outEvent) {
|
||||||
outEvent->alt = k.Alt;
|
if (outEvent == nullptr) return;
|
||||||
outEvent->capslock = k.CapsLock;
|
KeyEvent ev;
|
||||||
outEvent->altgr = k.AltGr;
|
ReadKeyEvent(&ev);
|
||||||
outEvent->extended = k.Extended;
|
memcpy(outEvent, &ev, sizeof(KeyEventV1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// SYS_GETKEY_EX: size-versioned. The caller's size was validated by the
|
||||||
|
// dispatcher (>= sizeof(KeyEventV1), writable).
|
||||||
|
static int Sys_GetKeyEx(void* outEvent, uint64_t size) {
|
||||||
|
KeyEvent ev;
|
||||||
|
ReadKeyEvent(&ev);
|
||||||
|
uint64_t n = size < sizeof(KeyEvent) ? size : sizeof(KeyEvent);
|
||||||
|
memcpy(outEvent, &ev, n);
|
||||||
|
if (size > n) memset((uint8_t*)outEvent + n, 0, size - n);
|
||||||
|
return Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char Sys_GetChar() {
|
static char Sys_GetChar() {
|
||||||
auto* proc = Sched::GetCurrentProcessPtr();
|
auto* proc = Sched::GetCurrentProcessPtr();
|
||||||
|
char pending;
|
||||||
|
if (KeyPendingChar(proc, &pending)) return pending;
|
||||||
if (proc && proc->redirected) {
|
if (proc && proc->redirected) {
|
||||||
Ipc::HandleSnapshot inputSnapshot;
|
Ipc::HandleSnapshot inputSnapshot;
|
||||||
Ipc::HandleSnapshot mailboxSnapshot;
|
Ipc::HandleSnapshot mailboxSnapshot;
|
||||||
@@ -78,11 +191,11 @@ namespace montauk::abi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mailbox != nullptr) {
|
if (mailbox != nullptr) {
|
||||||
KeyEvent ev{};
|
KeyEvent ev;
|
||||||
uint16_t len = sizeof(ev);
|
int rc = KeyMailboxRecv(mailbox, &ev);
|
||||||
int rc = Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true);
|
|
||||||
if (rc > 0) {
|
if (rc > 0) {
|
||||||
if (ev.pressed && ev.ascii != 0) return ev.ascii;
|
char c;
|
||||||
|
if (KeyTakeChar(proc, ev, &c)) return c;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (rc < 0 && input == nullptr) return 0;
|
if (rc < 0 && input == nullptr) return 0;
|
||||||
@@ -115,6 +228,8 @@ namespace montauk::abi {
|
|||||||
*/
|
*/
|
||||||
static char Sys_GetCharNb() {
|
static char Sys_GetCharNb() {
|
||||||
auto* proc = Sched::GetCurrentProcessPtr();
|
auto* proc = Sched::GetCurrentProcessPtr();
|
||||||
|
char pending;
|
||||||
|
if (KeyPendingChar(proc, &pending)) return pending;
|
||||||
if (proc && proc->redirected) {
|
if (proc && proc->redirected) {
|
||||||
Ipc::HandleSnapshot inputSnapshot;
|
Ipc::HandleSnapshot inputSnapshot;
|
||||||
Ipc::HandleSnapshot mailboxSnapshot;
|
Ipc::HandleSnapshot mailboxSnapshot;
|
||||||
@@ -127,10 +242,10 @@ namespace montauk::abi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (mailbox != nullptr) {
|
while (mailbox != nullptr) {
|
||||||
KeyEvent ev{};
|
KeyEvent ev;
|
||||||
uint16_t len = sizeof(ev);
|
if (KeyMailboxRecv(mailbox, &ev) <= 0) break;
|
||||||
if (Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true) <= 0) break;
|
char c;
|
||||||
if (ev.pressed && ev.ascii != 0) return ev.ascii;
|
if (KeyTakeChar(proc, ev, &c)) return c;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ namespace montauk::abi {
|
|||||||
case SYS_ISKEYAVAILABLE:
|
case SYS_ISKEYAVAILABLE:
|
||||||
return (int64_t)Sys_IsKeyAvailable();
|
return (int64_t)Sys_IsKeyAvailable();
|
||||||
case SYS_GETKEY:
|
case SYS_GETKEY:
|
||||||
if (!UserMemory::Writable<KeyEvent>(frame->arg1)) return BadPointer;
|
if (!UserMemory::Writable<KeyEventV1>(frame->arg1)) return BadPointer;
|
||||||
Sys_GetKey((KeyEvent*)frame->arg1);
|
Sys_GetKey((KeyEventV1*)frame->arg1);
|
||||||
return Success;
|
return Success;
|
||||||
case SYS_GETCHAR:
|
case SYS_GETCHAR:
|
||||||
return (int64_t)Sys_GetChar();
|
return (int64_t)Sys_GetChar();
|
||||||
@@ -365,8 +365,8 @@ namespace montauk::abi {
|
|||||||
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
|
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
|
||||||
return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3);
|
return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3);
|
||||||
case SYS_CHILDIO_WRITEKEY:
|
case SYS_CHILDIO_WRITEKEY:
|
||||||
if (!UserMemory::Readable<KeyEvent>(frame->arg2)) return BadPointer;
|
if (!UserMemory::Readable<KeyEventV1>(frame->arg2)) return BadPointer;
|
||||||
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEvent*)frame->arg2);
|
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEventV1*)frame->arg2);
|
||||||
case SYS_CHILDIO_SETTERMSZ:
|
case SYS_CHILDIO_SETTERMSZ:
|
||||||
return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
|
return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
|
||||||
case SYS_WINCREATE:
|
case SYS_WINCREATE:
|
||||||
@@ -527,6 +527,14 @@ namespace montauk::abi {
|
|||||||
return Sched::Consent::Next((ConsentRequest*)frame->arg1);
|
return Sched::Consent::Next((ConsentRequest*)frame->arg1);
|
||||||
case SYS_CONSENT_REPLY:
|
case SYS_CONSENT_REPLY:
|
||||||
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
|
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
|
||||||
|
case SYS_GETKEY_EX:
|
||||||
|
if (frame->arg2 < sizeof(KeyEventV1) || frame->arg2 > 4096) return InvalidArgument;
|
||||||
|
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
|
||||||
|
return Sys_GetKeyEx((void*)frame->arg1, frame->arg2);
|
||||||
|
case SYS_CHILDIO_WRITEKEY_EX:
|
||||||
|
if (frame->arg3 < sizeof(KeyEventV1) || frame->arg3 > 4096) return InvalidArgument;
|
||||||
|
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
|
||||||
|
return (int64_t)Sys_ChildIoWriteKeyEx((int)frame->arg1, (const void*)frame->arg2, frame->arg3);
|
||||||
case SYS_THREAD_SPAWN:
|
case SYS_THREAD_SPAWN:
|
||||||
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
|
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
|
||||||
case SYS_THREAD_EXIT:
|
case SYS_THREAD_EXIT:
|
||||||
|
|||||||
@@ -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_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||||
|
|
||||||
|
/* Unicode key events. Size-versioned: the caller passes sizeof(its
|
||||||
|
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
|
||||||
|
rest, so the struct can grow without another syscall. */
|
||||||
|
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
|
||||||
|
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
|
||||||
|
|
||||||
/* Kernel-owned process capabilities. User identities may namespace
|
/* Kernel-owned process capabilities. User identities may namespace
|
||||||
per-user resources, but never participate in authorization decisions. */
|
per-user resources, but never participate in authorization decisions. */
|
||||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||||
@@ -600,7 +606,10 @@ namespace montauk::abi {
|
|||||||
uint64_t txPackets;
|
uint64_t txPackets;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct KeyEvent {
|
// Legacy 9-byte key event, read by SYS_GETKEY and written by
|
||||||
|
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
|
||||||
|
// `ascii` is a Windows-1252 byte.
|
||||||
|
struct KeyEventV1 {
|
||||||
uint8_t scancode;
|
uint8_t scancode;
|
||||||
char ascii;
|
char ascii;
|
||||||
bool pressed;
|
bool pressed;
|
||||||
@@ -616,6 +625,32 @@ namespace montauk::abi {
|
|||||||
bool extended;
|
bool extended;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Key event with the Unicode text it types (SYS_GETKEY_EX,
|
||||||
|
// SYS_CHILDIO_WRITEKEY_EX, WinEvent). Its first 9 bytes ARE a KeyEventV1, so
|
||||||
|
// a legacy reader sees a valid event. `codepoint` is the character the key
|
||||||
|
// produces (0 for control/navigation keys and dead keys). `ascii` is its
|
||||||
|
// Windows-1252 byte, or KEY_ASCII_SUBSTITUTE when 1252 has none.
|
||||||
|
struct KeyEvent {
|
||||||
|
uint8_t scancode;
|
||||||
|
char ascii;
|
||||||
|
bool pressed;
|
||||||
|
bool shift;
|
||||||
|
bool ctrl;
|
||||||
|
bool alt;
|
||||||
|
bool capslock;
|
||||||
|
bool altgr;
|
||||||
|
bool extended;
|
||||||
|
uint8_t _reserved[3];
|
||||||
|
uint32_t codepoint;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(KeyEventV1) == 9, "KeyEventV1 is frozen ABI");
|
||||||
|
static_assert(sizeof(KeyEvent) == 16, "KeyEvent must fit the WinEvent union");
|
||||||
|
|
||||||
|
// ASCII SUB: `ascii` of a text key whose character has no Windows-1252 byte.
|
||||||
|
// A control character, so legacy code that only accepts ascii >= 0x20 skips
|
||||||
|
// it, while code testing `ascii == 0` for navigation keys is not confused.
|
||||||
|
static constexpr char KEY_ASCII_SUBSTITUTE = 0x1A;
|
||||||
|
|
||||||
struct MouseState {
|
struct MouseState {
|
||||||
int32_t x;
|
int32_t x;
|
||||||
int32_t y;
|
int32_t y;
|
||||||
@@ -653,12 +688,13 @@ namespace montauk::abi {
|
|||||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||||
uint8_t _pad[3];
|
uint8_t _pad[3];
|
||||||
union {
|
union {
|
||||||
KeyEvent key;
|
KeyEvent key; // 16 bytes: the union was already that size (mouse)
|
||||||
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
||||||
struct { int32_t w, h; } resize;
|
struct { int32_t w, h; } resize;
|
||||||
struct { int32_t scale; } scale;
|
struct { int32_t scale; } scale;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
|
||||||
|
|
||||||
struct WinInfo {
|
struct WinInfo {
|
||||||
int32_t id;
|
int32_t id;
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
ack = Drivers::PS2::ReadData();
|
ack = Drivers::PS2::ReadData();
|
||||||
|
|
||||||
g_Modifiers = {};
|
g_Modifiers = {};
|
||||||
|
g_Modifiers.NumLock = true; // keypad types digits, as on most PCs
|
||||||
g_BufferHead = 0;
|
g_BufferHead = 0;
|
||||||
g_BufferTail = 0;
|
g_BufferTail = 0;
|
||||||
g_ExtendedScancode = false;
|
g_ExtendedScancode = false;
|
||||||
@@ -257,6 +258,9 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
ascii = g_ScancodeToAscii[keycode];
|
ascii = g_ScancodeToAscii[keycode];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Num Lock off: the keypad is navigation keys, which type nothing.
|
||||||
|
if (!g_Modifiers.NumLock && IsKeypadNavScancode(keycode))
|
||||||
|
ascii = 0;
|
||||||
|
|
||||||
KeyEvent event = {
|
KeyEvent event = {
|
||||||
.Scancode = scancode,
|
.Scancode = scancode,
|
||||||
@@ -310,6 +314,12 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
KeyEvent stamped = event;
|
KeyEvent stamped = event;
|
||||||
if (stamped.Scancode == ScCapsLock && stamped.Pressed)
|
if (stamped.Scancode == ScCapsLock && stamped.Pressed)
|
||||||
g_Modifiers.CapsLock = !g_Modifiers.CapsLock;
|
g_Modifiers.CapsLock = !g_Modifiers.CapsLock;
|
||||||
|
// Num Lock likewise. Like the PS/2 path, the lock key itself is not
|
||||||
|
// delivered to apps.
|
||||||
|
if (stamped.Scancode == ScNumLock) {
|
||||||
|
if (stamped.Pressed) g_Modifiers.NumLock = !g_Modifiers.NumLock;
|
||||||
|
return;
|
||||||
|
}
|
||||||
stamped.CapsLock = g_Modifiers.CapsLock;
|
stamped.CapsLock = g_Modifiers.CapsLock;
|
||||||
PushAndNotify(stamped);
|
PushAndNotify(stamped);
|
||||||
}
|
}
|
||||||
@@ -318,4 +328,8 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
return g_Modifiers.CapsLock;
|
return g_Modifiers.CapsLock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GetNumLock() {
|
||||||
|
return g_Modifiers.NumLock;
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -63,4 +63,14 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
// compute ASCII, since the lock is toggled centrally here.
|
// compute ASCII, since the lock is toggled centrally here.
|
||||||
bool GetCapsLock();
|
bool GetCapsLock();
|
||||||
|
|
||||||
|
// Current Num Lock state, owned here like Caps Lock. On (the default), the
|
||||||
|
// keypad types digits; off, it acts as the navigation keys printed on it.
|
||||||
|
bool GetNumLock();
|
||||||
|
|
||||||
|
// Set-1 scancodes of the keypad keys that are a digit/'.' with Num Lock on
|
||||||
|
// and Home/arrows/PgUp/End/PgDn/Ins/Del with it off (not '-' and '+').
|
||||||
|
inline bool IsKeypadNavScancode(uint8_t keycode) {
|
||||||
|
return keycode >= 0x47 && keycode <= 0x53 && keycode != 0x4A && keycode != 0x4E;
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -223,6 +223,12 @@ namespace Drivers::USB::HidKeyboard {
|
|||||||
if (caps && base >= 'a' && base <= 'z') upper = !shift;
|
if (caps && base >= 'a' && base <= 'z') upper = !shift;
|
||||||
ascii = upper ? g_ScancodeToAsciiShifted[scancode] : base;
|
ascii = upper ? g_ScancodeToAsciiShifted[scancode] : base;
|
||||||
}
|
}
|
||||||
|
// Num Lock off: the keypad is navigation keys, which type nothing.
|
||||||
|
// (Only keypad usages get here with these scancodes: the dedicated
|
||||||
|
// navigation keys are nonChar already.)
|
||||||
|
if (!Drivers::PS2::Keyboard::GetNumLock() &&
|
||||||
|
Drivers::PS2::Keyboard::IsKeypadNavScancode(scancode))
|
||||||
|
ascii = 0;
|
||||||
|
|
||||||
Drivers::PS2::Keyboard::KeyEvent event = {
|
Drivers::PS2::Keyboard::KeyEvent event = {
|
||||||
.Scancode = scancode,
|
.Scancode = scancode,
|
||||||
@@ -367,10 +373,14 @@ namespace Drivers::USB::HidKeyboard {
|
|||||||
uint8_t scancode = g_HidToScancode[key];
|
uint8_t scancode = g_HidToScancode[key];
|
||||||
if (scancode != 0) {
|
if (scancode != 0) {
|
||||||
InjectKey(scancode, true, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
InjectKey(scancode, true, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
||||||
|
newKeyPressed = true;
|
||||||
|
// Lock keys toggle once per press; repeating them while
|
||||||
|
// held would flip Caps/Num/Scroll Lock on and off.
|
||||||
|
if (key != 0x39 && key != 0x53 && key != 0x47) {
|
||||||
g_RepeatKey = key;
|
g_RepeatKey = key;
|
||||||
g_RepeatStartMs = now;
|
g_RepeatStartMs = now;
|
||||||
g_LastRepeatMs = 0;
|
g_LastRepeatMs = 0;
|
||||||
newKeyPressed = true;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-12
@@ -301,6 +301,53 @@ namespace Fs::Fat32 {
|
|||||||
// Short name generation and LFN helpers for Create
|
// Short name generation and LFN helpers for Create
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Name encoding: VFS names are UTF-8, LFN entries are UTF-16
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Decode one UTF-8 character at s (NUL-terminated); stores its length.
|
||||||
|
// A byte that does not start a well-formed sequence is taken as one
|
||||||
|
// Latin-1 character, so legacy single-byte names still round-trip.
|
||||||
|
static uint32_t DecodeUtf8(const char* s, int* consumed) {
|
||||||
|
const uint8_t* u = (const uint8_t*)s;
|
||||||
|
uint8_t b0 = u[0];
|
||||||
|
*consumed = 1;
|
||||||
|
if (b0 < 0x80) return b0;
|
||||||
|
int n;
|
||||||
|
uint32_t cp, min;
|
||||||
|
if (b0 >= 0xC2 && b0 <= 0xDF) { n = 2; cp = b0 & 0x1F; min = 0x80; }
|
||||||
|
else if (b0 >= 0xE0 && b0 <= 0xEF) { n = 3; cp = b0 & 0x0F; min = 0x800; }
|
||||||
|
else if (b0 >= 0xF0 && b0 <= 0xF4) { n = 4; cp = b0 & 0x07; min = 0x10000; }
|
||||||
|
else return b0;
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
if ((u[i] & 0xC0) != 0x80) return b0; // also stops at NUL
|
||||||
|
cp = (cp << 6) | (u[i] & 0x3F);
|
||||||
|
}
|
||||||
|
if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return b0;
|
||||||
|
*consumed = n;
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int EncodeUtf8(uint32_t cp, char* out) {
|
||||||
|
if (cp < 0x80) { out[0] = (char)cp; return 1; }
|
||||||
|
if (cp < 0x800) {
|
||||||
|
out[0] = (char)(0xC0 | (cp >> 6));
|
||||||
|
out[1] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (cp < 0x10000) {
|
||||||
|
out[0] = (char)(0xE0 | (cp >> 12));
|
||||||
|
out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
out[2] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
out[0] = (char)(0xF0 | (cp >> 18));
|
||||||
|
out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
|
||||||
|
out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
out[3] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
static void GenerateShortName(const char* longName, char* shortName) {
|
static void GenerateShortName(const char* longName, char* shortName) {
|
||||||
memset(shortName, ' ', 11);
|
memset(shortName, ' ', 11);
|
||||||
|
|
||||||
@@ -315,9 +362,15 @@ namespace Fs::Fat32 {
|
|||||||
// Base name (up to 8 chars, uppercase, skip invalid chars)
|
// Base name (up to 8 chars, uppercase, skip invalid chars)
|
||||||
int baseEnd = (lastDot >= 0) ? lastDot : nameLen;
|
int baseEnd = (lastDot >= 0) ? lastDot : nameLen;
|
||||||
int j = 0;
|
int j = 0;
|
||||||
|
// A non-ASCII character becomes one '_' (8.3 names are OEM codepage;
|
||||||
|
// the real name lives in the LFN entries).
|
||||||
for (int i = 0; i < baseEnd && j < 8; i++) {
|
for (int i = 0; i < baseEnd && j < 8; i++) {
|
||||||
char c = longName[i];
|
char c = longName[i];
|
||||||
if (c == ' ' || c == '.') continue;
|
if (c == ' ' || c == '.') continue;
|
||||||
|
if ((uint8_t)c >= 0x80) {
|
||||||
|
if (((uint8_t)c & 0xC0) == 0x80) continue;
|
||||||
|
c = '_';
|
||||||
|
}
|
||||||
if (c >= 'a' && c <= 'z') c -= 32;
|
if (c >= 'a' && c <= 'z') c -= 32;
|
||||||
shortName[j++] = c;
|
shortName[j++] = c;
|
||||||
}
|
}
|
||||||
@@ -327,6 +380,10 @@ namespace Fs::Fat32 {
|
|||||||
j = 0;
|
j = 0;
|
||||||
for (int i = lastDot + 1; longName[i] && j < 3; i++) {
|
for (int i = lastDot + 1; longName[i] && j < 3; i++) {
|
||||||
char c = longName[i];
|
char c = longName[i];
|
||||||
|
if ((uint8_t)c >= 0x80) {
|
||||||
|
if (((uint8_t)c & 0xC0) == 0x80) continue;
|
||||||
|
c = '_';
|
||||||
|
}
|
||||||
if (c >= 'a' && c <= 'z') c -= 32;
|
if (c >= 'a' && c <= 'z') c -= 32;
|
||||||
shortName[8 + j++] = c;
|
shortName[8 + j++] = c;
|
||||||
}
|
}
|
||||||
@@ -403,12 +460,22 @@ namespace Fs::Fat32 {
|
|||||||
// Returns the number of LFN entries.
|
// Returns the number of LFN entries.
|
||||||
static int BuildLfnEntries(const char* longName, uint8_t checksum,
|
static int BuildLfnEntries(const char* longName, uint8_t checksum,
|
||||||
uint8_t* outEntries) {
|
uint8_t* outEntries) {
|
||||||
// Convert to UTF-16
|
// Convert UTF-8 to UTF-16 (LFN holds at most 255 code units)
|
||||||
uint16_t utf16[MaxNameLen];
|
uint16_t utf16[MaxNameLen + 13];
|
||||||
int len = 0;
|
int len = 0;
|
||||||
while (longName[len] && len < MaxNameLen - 1) {
|
for (const char* p = longName; *p;) {
|
||||||
utf16[len] = (uint8_t)longName[len];
|
int n;
|
||||||
len++;
|
uint32_t cp = DecodeUtf8(p, &n);
|
||||||
|
int units = cp >= 0x10000 ? 2 : 1;
|
||||||
|
if (len + units > 255) break;
|
||||||
|
if (units == 2) {
|
||||||
|
cp -= 0x10000;
|
||||||
|
utf16[len++] = (uint16_t)(0xD800 | (cp >> 10));
|
||||||
|
utf16[len++] = (uint16_t)(0xDC00 | (cp & 0x3FF));
|
||||||
|
} else {
|
||||||
|
utf16[len++] = (uint16_t)cp;
|
||||||
|
}
|
||||||
|
p += n;
|
||||||
}
|
}
|
||||||
utf16[len] = 0;
|
utf16[len] = 0;
|
||||||
|
|
||||||
@@ -464,9 +531,11 @@ namespace Fs::Fat32 {
|
|||||||
// Compare case-insensitively
|
// Compare case-insensitively
|
||||||
if (!StrEqualNoCase(longName, reconstructed)) return true;
|
if (!StrEqualNoCase(longName, reconstructed)) return true;
|
||||||
|
|
||||||
// Check if long name has lowercase (short names are uppercase)
|
// Check if long name has lowercase (short names are uppercase) or
|
||||||
|
// characters the short name could not hold
|
||||||
for (int i = 0; longName[i]; i++) {
|
for (int i = 0; longName[i]; i++) {
|
||||||
if (longName[i] >= 'a' && longName[i] <= 'z') return true;
|
if (longName[i] >= 'a' && longName[i] <= 'z') return true;
|
||||||
|
if ((uint8_t)longName[i] >= 0x80) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -619,10 +688,24 @@ namespace Fs::Fat32 {
|
|||||||
for (int i = 0; i < 2; i++) memcpy(&out[11 + i], entry + 28 + i * 2, 2);
|
for (int i = 0; i < 2; i++) memcpy(&out[11 + i], entry + 28 + i * 2, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Utf16ToAscii(const uint16_t* src, int maxLen, char* dst) {
|
// LFN UTF-16 -> UTF-8 name. Unpaired surrogates become U+FFFD; the name is
|
||||||
|
// cut on a character boundary if it does not fit MaxNameLen bytes.
|
||||||
|
static void Utf16ToUtf8(const uint16_t* src, int maxLen, char* dst,
|
||||||
|
int dstCap = MaxNameLen) {
|
||||||
int j = 0;
|
int j = 0;
|
||||||
for (int i = 0; i < maxLen && src[i] != 0 && j < MaxNameLen - 1; i++) {
|
for (int i = 0; i < maxLen && src[i] != 0 && src[i] != 0xFFFF; i++) {
|
||||||
dst[j++] = (src[i] < 128) ? (char)src[i] : '?';
|
uint32_t cp = src[i];
|
||||||
|
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < maxLen &&
|
||||||
|
src[i + 1] >= 0xDC00 && src[i + 1] <= 0xDFFF) {
|
||||||
|
cp = 0x10000 + ((cp - 0xD800) << 10) + (src[i + 1] - 0xDC00);
|
||||||
|
i++;
|
||||||
|
} else if (cp >= 0xD800 && cp <= 0xDFFF) {
|
||||||
|
cp = 0xFFFD;
|
||||||
|
}
|
||||||
|
char enc[4];
|
||||||
|
int n = EncodeUtf8(cp, enc);
|
||||||
|
if (j + n > dstCap - 1) break;
|
||||||
|
for (int k = 0; k < n; k++) dst[j++] = enc[k];
|
||||||
}
|
}
|
||||||
dst[j] = '\0';
|
dst[j] = '\0';
|
||||||
}
|
}
|
||||||
@@ -685,7 +768,7 @@ namespace Fs::Fat32 {
|
|||||||
pe.attributes = attr;
|
pe.attributes = attr;
|
||||||
|
|
||||||
if (hasLfn) {
|
if (hasLfn) {
|
||||||
Utf16ToAscii(lfnBuf, MaxNameLen, pe.name);
|
Utf16ToUtf8(lfnBuf, MaxNameLen, pe.name);
|
||||||
} else {
|
} else {
|
||||||
ParseShortName(e, pe.name);
|
ParseShortName(e, pe.name);
|
||||||
}
|
}
|
||||||
@@ -752,7 +835,8 @@ namespace Fs::Fat32 {
|
|||||||
if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; }
|
if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; }
|
||||||
|
|
||||||
if (hasLfn) {
|
if (hasLfn) {
|
||||||
Utf16ToAscii(lfnBuf, MaxNameLen, outNames[count]);
|
// Leave room for the '/' appended to directory names
|
||||||
|
Utf16ToUtf8(lfnBuf, MaxNameLen, outNames[count], MaxNameLen - 1);
|
||||||
} else {
|
} else {
|
||||||
ParseShortName(e, outNames[count]);
|
ParseShortName(e, outNames[count]);
|
||||||
}
|
}
|
||||||
@@ -848,7 +932,7 @@ namespace Fs::Fat32 {
|
|||||||
|
|
||||||
char entryName[MaxNameLen];
|
char entryName[MaxNameLen];
|
||||||
if (hasLfn) {
|
if (hasLfn) {
|
||||||
Utf16ToAscii(lfnBuf, MaxNameLen, entryName);
|
Utf16ToUtf8(lfnBuf, MaxNameLen, entryName);
|
||||||
} else {
|
} else {
|
||||||
ParseShortName(e, entryName);
|
ParseShortName(e, entryName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,6 +324,8 @@ namespace Sched {
|
|||||||
processTable[i].inTail = 0;
|
processTable[i].inTail = 0;
|
||||||
processTable[i].keyHead = 0;
|
processTable[i].keyHead = 0;
|
||||||
processTable[i].keyTail = 0;
|
processTable[i].keyTail = 0;
|
||||||
|
processTable[i].getcharUtf8Len = 0;
|
||||||
|
processTable[i].getcharUtf8Pos = 0;
|
||||||
processTable[i].termCols = 0;
|
processTable[i].termCols = 0;
|
||||||
processTable[i].termRows = 0;
|
processTable[i].termRows = 0;
|
||||||
processTable[i].ioOutHandle = -1;
|
processTable[i].ioOutHandle = -1;
|
||||||
@@ -654,6 +656,8 @@ namespace Sched {
|
|||||||
proc.inTail = 0;
|
proc.inTail = 0;
|
||||||
proc.keyHead = 0;
|
proc.keyHead = 0;
|
||||||
proc.keyTail = 0;
|
proc.keyTail = 0;
|
||||||
|
proc.getcharUtf8Len = 0;
|
||||||
|
proc.getcharUtf8Pos = 0;
|
||||||
proc.termCols = 0;
|
proc.termCols = 0;
|
||||||
proc.termRows = 0;
|
proc.termRows = 0;
|
||||||
proc.ioOutHandle = -1;
|
proc.ioOutHandle = -1;
|
||||||
@@ -835,6 +839,7 @@ namespace Sched {
|
|||||||
thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0;
|
thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0;
|
||||||
thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0;
|
thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0;
|
||||||
thr.keyHead = 0; thr.keyTail = 0;
|
thr.keyHead = 0; thr.keyTail = 0;
|
||||||
|
thr.getcharUtf8Len = 0; thr.getcharUtf8Pos = 0;
|
||||||
thr.termCols = 0; thr.termRows = 0;
|
thr.termCols = 0; thr.termRows = 0;
|
||||||
thr.ioOutHandle = -1; thr.ioInHandle = -1;
|
thr.ioOutHandle = -1; thr.ioInHandle = -1;
|
||||||
thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1;
|
thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1;
|
||||||
@@ -1700,6 +1705,8 @@ namespace Sched {
|
|||||||
proc.inTail = 0;
|
proc.inTail = 0;
|
||||||
proc.keyHead = 0;
|
proc.keyHead = 0;
|
||||||
proc.keyTail = 0;
|
proc.keyTail = 0;
|
||||||
|
proc.getcharUtf8Len = 0;
|
||||||
|
proc.getcharUtf8Pos = 0;
|
||||||
proc.termCols = 0;
|
proc.termCols = 0;
|
||||||
proc.termRows = 0;
|
proc.termRows = 0;
|
||||||
proc.ioOutHandle = -1;
|
proc.ioOutHandle = -1;
|
||||||
|
|||||||
@@ -135,9 +135,13 @@ namespace Sched {
|
|||||||
uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar)
|
uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar)
|
||||||
volatile uint32_t inHead = 0;
|
volatile uint32_t inHead = 0;
|
||||||
volatile uint32_t inTail = 0;
|
volatile uint32_t inTail = 0;
|
||||||
montauk::abi::KeyEvent keyBuf[64]; // parent injects, child reads (getkey/iskeyavailable)
|
|
||||||
volatile uint32_t keyHead = 0;
|
volatile uint32_t keyHead = 0;
|
||||||
volatile uint32_t keyTail = 0;
|
volatile uint32_t keyTail = 0;
|
||||||
|
// SYS_GETCHAR returns a key's UTF-8 encoding one byte per call; the
|
||||||
|
// bytes not handed out yet wait here.
|
||||||
|
uint8_t getcharUtf8[4] = {};
|
||||||
|
uint8_t getcharUtf8Len = 0;
|
||||||
|
uint8_t getcharUtf8Pos = 0;
|
||||||
static constexpr uint32_t IoBufSize = 4096;
|
static constexpr uint32_t IoBufSize = 4096;
|
||||||
|
|
||||||
// GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE)
|
// GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE)
|
||||||
|
|||||||
@@ -15,15 +15,10 @@
|
|||||||
# base: character produced unshifted, one per scancode
|
# base: character produced unshifted, one per scancode
|
||||||
# shift: character produced with Shift held, one per scancode
|
# shift: character produced with Shift held, one per scancode
|
||||||
# altgr: character produced with AltGr held, one per scancode
|
# altgr: character produced with AltGr held, one per scancode
|
||||||
#
|
# dead_keys: (optional) characters that are dead keys in this layout
|
||||||
# Characters are Windows-1252 byte values.
|
|
||||||
#
|
|
||||||
# A value of 0x00 means "no override" - the key keeps whatever the kernel's
|
|
||||||
# US scancode table produced. This is how a layout declares only the keys it
|
|
||||||
# actually changes.
|
|
||||||
|
|
||||||
[registry]
|
[registry]
|
||||||
layouts = ["en", "no", "sv", "da"]
|
layouts = ["en", "no", "sv", "da", "cs"]
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# LAYOUTS
|
# LAYOUTS
|
||||||
@@ -54,7 +49,7 @@ shift = [
|
|||||||
]
|
]
|
||||||
altgr = [
|
altgr = [
|
||||||
0x40, 0xA3, 0x24, 0x00, 0x7B, 0x5B, 0x5D, 0x7D, # @ GBP $ . { [ ] }
|
0x40, 0xA3, 0x24, 0x00, 0x7B, 0x5B, 0x5D, 0x7D, # @ GBP $ . { [ ] }
|
||||||
0x00, 0xB4, 0x80, 0x00, 0x7E, 0x00, 0x00, 0x00, # . ' EUR . ~ . . .
|
0x00, 0xB4, 0x20AC, 0x00, 0x7E, 0x00, 0x00, 0x00, # . ' EUR . ~ . . .
|
||||||
0x00, 0x00, 0x00, 0x00, 0x5C, # . . . . backslash
|
0x00, 0x00, 0x00, 0x00, 0x5C, # . . . . backslash
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -77,8 +72,8 @@ shift = [
|
|||||||
0xBD, 0x2A, 0x3B, 0x3A, 0x5F, 0x3E, # 1/2 * ; : _ >
|
0xBD, 0x2A, 0x3B, 0x3A, 0x5F, 0x3E, # 1/2 * ; : _ >
|
||||||
]
|
]
|
||||||
altgr = [
|
altgr = [
|
||||||
0x40, 0xA3, 0x24, 0x80, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ EUR YEN { [ ]
|
0x40, 0xA3, 0x24, 0x20AC, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ EUR YEN { [ ]
|
||||||
0x7D, 0x5C, 0xB1, 0x80, 0x00, 0x7E, 0xF8, 0xE6, # } bslash +- EUR . ~ o/ ae
|
0x7D, 0x5C, 0xB1, 0x20AC, 0x00, 0x7E, 0xF8, 0xE6, # } bslash +- EUR . ~ o/ ae
|
||||||
0xB6, 0xB4, 0x00, 0xB7, 0x00, 0x7C, # par ' . mid . |
|
0xB6, 0xB4, 0x00, 0xB7, 0x00, 0x7C, # par ' . mid . |
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -102,6 +97,44 @@ shift = [
|
|||||||
]
|
]
|
||||||
altgr = [
|
altgr = [
|
||||||
0x40, 0xA3, 0x24, 0xBD, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ 1/2 YEN { [ ]
|
0x40, 0xA3, 0x24, 0xBD, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ 1/2 YEN { [ ]
|
||||||
0x7D, 0xB1, 0x7C, 0x80, 0x00, 0x7E, 0xB4, 0x5E, # } +- | EUR . ~ ' ^
|
0x7D, 0xB1, 0x7C, 0x20AC, 0x00, 0x7E, 0xB4, 0x5E, # } +- | EUR . ~ ' ^
|
||||||
0xBE, 0x00, 0x00, 0xB7, 0x00, 0x5C, # 3/4 . . mid . backslash
|
0xBE, 0x00, 0x00, 0xB7, 0x00, 0x5C, # 3/4 . . mid . backslash
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[layouts.cs]
|
||||||
|
name = "Czech"
|
||||||
|
short_name = "cs"
|
||||||
|
# QWERTZ. The number row types accented letters (digits with Shift), and
|
||||||
|
# AltGr on the number row gives the dead accents.
|
||||||
|
scancodes = [
|
||||||
|
0x29, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||||
|
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x10, 0x11, 0x12,
|
||||||
|
0x15, 0x1a, 0x1b, 0x21, 0x22, 0x25, 0x26, 0x27,
|
||||||
|
0x28, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31,
|
||||||
|
0x33, 0x34, 0x35, 0x56,
|
||||||
|
]
|
||||||
|
base = [
|
||||||
|
0x3B, 0x2B, 0x11B, 0x161, 0x10D, 0x159, 0x17E, 0xFD, # ; + e-caron s-caron c-caron r-caron z-caron y-acute
|
||||||
|
0xE1, 0xED, 0xE9, 0x3D, 0xB4, 0x00, 0x00, 0x00, # a-acute i-acute e-acute = (dead acute) . . .
|
||||||
|
0x7A, 0xFA, 0x29, 0x00, 0x00, 0x00, 0x00, 0x16F, # z u-acute ) . . . . u-ring
|
||||||
|
0xA7, 0xA8, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, # section (dead diaeresis) y . . . . .
|
||||||
|
0x2C, 0x2E, 0x2D, 0x5C, # , . - backslash
|
||||||
|
]
|
||||||
|
shift = [
|
||||||
|
0xB0, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, # (dead ring) 1 2 3 4 5 6 7
|
||||||
|
0x38, 0x39, 0x30, 0x25, 0x2C7, 0x00, 0x00, 0x00, # 8 9 0 % (dead caron) . . .
|
||||||
|
0x5A, 0x2F, 0x28, 0x00, 0x00, 0x00, 0x00, 0x22, # Z / ( . . . . "
|
||||||
|
0x21, 0x27, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, # ! ' Y . . . . .
|
||||||
|
0x3F, 0x3A, 0x5F, 0x7C, # ? : _ |
|
||||||
|
]
|
||||||
|
altgr = [
|
||||||
|
0x00, 0x7E, 0x2C7, 0x5E, 0x2D8, 0xB0, 0x2DB, 0x60, # . dead: tilde caron circumflex breve ring ogonek grave
|
||||||
|
0x2D9, 0xB4, 0x2DD, 0xA8, 0xB8, 0x5C, 0x7C, 0x20AC, # dead: dot acute double-acute diaeresis cedilla; backslash | EUR
|
||||||
|
0x00, 0xF7, 0xD7, 0x5B, 0x5D, 0x142, 0x141, 0x24, # . divide multiply [ ] l-stroke L-stroke $
|
||||||
|
0xDF, 0xA4, 0x00, 0x23, 0x26, 0x40, 0x7B, 0x7D, # sharp-s currency . # & @ { }
|
||||||
|
0x3C, 0x3E, 0x2A, 0x00, # < > * .
|
||||||
|
]
|
||||||
|
dead_keys = [
|
||||||
|
0xB4, 0x2C7, 0xA8, 0xB0, 0x7E, 0x5E,
|
||||||
|
0x2D8, 0x2DB, 0x60, 0x2D9, 0x2DD, 0xB8,
|
||||||
|
]
|
||||||
@@ -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_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||||
|
|
||||||
|
/* Unicode key events. Size-versioned: the caller passes sizeof(its
|
||||||
|
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
|
||||||
|
rest, so the struct can grow without another syscall. */
|
||||||
|
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
|
||||||
|
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
|
||||||
|
|
||||||
/* Kernel-owned process capabilities. User identities may namespace
|
/* Kernel-owned process capabilities. User identities may namespace
|
||||||
per-user resources, but never participate in authorization decisions. */
|
per-user resources, but never participate in authorization decisions. */
|
||||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||||
@@ -531,7 +537,10 @@ namespace montauk::abi {
|
|||||||
uint32_t buildNumber; // monotonic kernel build number
|
uint32_t buildNumber; // monotonic kernel build number
|
||||||
};
|
};
|
||||||
|
|
||||||
struct KeyEvent {
|
// Legacy 9-byte key event, read by SYS_GETKEY and written by
|
||||||
|
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
|
||||||
|
// `ascii` is a Windows-1252 byte.
|
||||||
|
struct KeyEventV1 {
|
||||||
uint8_t scancode;
|
uint8_t scancode;
|
||||||
char ascii;
|
char ascii;
|
||||||
bool pressed;
|
bool pressed;
|
||||||
@@ -547,6 +556,32 @@ namespace montauk::abi {
|
|||||||
bool extended;
|
bool extended;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Key event with the Unicode text it types (SYS_GETKEY_EX,
|
||||||
|
// SYS_CHILDIO_WRITEKEY_EX, WinEvent). Its first 9 bytes ARE a KeyEventV1, so
|
||||||
|
// a legacy reader sees a valid event. `codepoint` is the character the key
|
||||||
|
// produces (0 for control/navigation keys and dead keys). `ascii` is its
|
||||||
|
// Windows-1252 byte, or KEY_ASCII_SUBSTITUTE when 1252 has none.
|
||||||
|
struct KeyEvent {
|
||||||
|
uint8_t scancode;
|
||||||
|
char ascii;
|
||||||
|
bool pressed;
|
||||||
|
bool shift;
|
||||||
|
bool ctrl;
|
||||||
|
bool alt;
|
||||||
|
bool capslock;
|
||||||
|
bool altgr;
|
||||||
|
bool extended;
|
||||||
|
uint8_t _reserved[3];
|
||||||
|
uint32_t codepoint;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(KeyEventV1) == 9, "KeyEventV1 is frozen ABI");
|
||||||
|
static_assert(sizeof(KeyEvent) == 16, "KeyEvent must fit the WinEvent union");
|
||||||
|
|
||||||
|
// ASCII SUB: `ascii` of a text key whose character has no Windows-1252 byte.
|
||||||
|
// A control character, so legacy code that only accepts ascii >= 0x20 skips
|
||||||
|
// it, while code testing `ascii == 0` for navigation keys is not confused.
|
||||||
|
static constexpr char KEY_ASCII_SUBSTITUTE = 0x1A;
|
||||||
|
|
||||||
struct MouseState {
|
struct MouseState {
|
||||||
int32_t x;
|
int32_t x;
|
||||||
int32_t y;
|
int32_t y;
|
||||||
@@ -584,12 +619,13 @@ namespace montauk::abi {
|
|||||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||||
uint8_t _pad[3];
|
uint8_t _pad[3];
|
||||||
union {
|
union {
|
||||||
KeyEvent key;
|
KeyEvent key; // 16 bytes: the union was already that size (mouse)
|
||||||
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
||||||
struct { int32_t w, h; } resize;
|
struct { int32_t w, h; } resize;
|
||||||
struct { int32_t scale; } scale;
|
struct { int32_t scale; } scale;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
|
||||||
|
|
||||||
struct WinInfo {
|
struct WinInfo {
|
||||||
int32_t id;
|
int32_t id;
|
||||||
|
|||||||
@@ -165,9 +165,9 @@ struct Canvas {
|
|||||||
if (!str || !font_data || scale <= 0) return;
|
if (!str || !font_data || scale <= 0) return;
|
||||||
uint32_t pixel = c.to_pixel();
|
uint32_t pixel = c.to_pixel();
|
||||||
int cx = x;
|
int cx = x;
|
||||||
for (int i = 0; str[i]; i++, cx += FONT_WIDTH * scale) {
|
for (const char* p = str; *p; cx += FONT_WIDTH * scale) {
|
||||||
const uint8_t* glyph =
|
const uint8_t* glyph =
|
||||||
&font_data[(unsigned char)str[i] * FONT_HEIGHT];
|
&font_data[cp437_index(montauk::utf8::next(&p)) * FONT_HEIGHT];
|
||||||
for (int row = 0; row < FONT_HEIGHT; row++) {
|
for (int row = 0; row < FONT_HEIGHT; row++) {
|
||||||
uint8_t bits = glyph[row];
|
uint8_t bits = glyph[row];
|
||||||
if (!bits) continue;
|
if (!bits) continue;
|
||||||
|
|||||||
@@ -9,12 +9,32 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
|
// The clipboard carries UTF-8. Text that is not well-formed (legacy
|
||||||
|
// Windows-1252 bytes) is converted on the way in and on the way out, so
|
||||||
|
// every reader can assume valid UTF-8.
|
||||||
|
inline char* clipboard_utf8_copy(const char* text, int len, int* out_len) {
|
||||||
|
int cap = len * 3 + 1; // each stray byte grows to at most 3
|
||||||
|
char* out = (char*)montauk::malloc(cap);
|
||||||
|
if (!out) return nullptr;
|
||||||
|
*out_len = montauk::utf8::normalize(text, len, out, cap);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool clipboard_set_text(const char* text, int len) {
|
inline bool clipboard_set_text(const char* text, int len) {
|
||||||
if (len < 0) return false;
|
if (len < 0) return false;
|
||||||
if (len > 0 && text == nullptr) return false;
|
if (len > 0 && text == nullptr) return false;
|
||||||
|
if (len > 0 && !montauk::utf8::valid(text, len)) {
|
||||||
|
int fixed_len = 0;
|
||||||
|
char* fixed = clipboard_utf8_copy(text, len, &fixed_len);
|
||||||
|
if (!fixed) return false;
|
||||||
|
bool ok = montauk::clipboard_set_text(fixed, (uint32_t)fixed_len) == 0;
|
||||||
|
montauk::mfree(fixed);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
return montauk::clipboard_set_text(text, (uint32_t)len) == 0;
|
return montauk::clipboard_set_text(text, (uint32_t)len) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +73,14 @@ namespace gui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
text[actual_len] = '\0';
|
text[actual_len] = '\0';
|
||||||
|
if (!montauk::utf8::valid(text, (int)actual_len)) {
|
||||||
|
int fixed_len = 0;
|
||||||
|
char* fixed = clipboard_utf8_copy(text, (int)actual_len, &fixed_len);
|
||||||
|
montauk::mfree(text);
|
||||||
|
if (!fixed) return nullptr;
|
||||||
|
text = fixed;
|
||||||
|
actual_len = (uint32_t)fixed_len;
|
||||||
|
}
|
||||||
if (out_len) *out_len = (int)actual_len;
|
if (out_len) *out_len = (int)actual_len;
|
||||||
if (out_serial) *out_serial = serial;
|
if (out_serial) *out_serial = serial;
|
||||||
return text;
|
return text;
|
||||||
|
|||||||
@@ -19,6 +19,34 @@ static constexpr int FONT_HEIGHT = 16;
|
|||||||
// TrueType fonts, while login/desktop can recover when TrueType setup fails.
|
// TrueType fonts, while login/desktop can recover when TrueType setup fails.
|
||||||
extern const uint8_t font_data[256 * 16] __attribute__((weak));
|
extern const uint8_t font_data[256 * 16] __attribute__((weak));
|
||||||
|
|
||||||
|
// The bitmap fallback font is Code Page 437. Map a Unicode codepoint to its
|
||||||
|
// CP437 cell ('?' when it has none) so UTF-8 text still draws sensibly,
|
||||||
|
// including the box-drawing characters terminal programs use.
|
||||||
|
inline uint8_t cp437_index(uint32_t cp) {
|
||||||
|
if (cp < 0x80) return (uint8_t)cp;
|
||||||
|
static constexpr uint16_t high[128] = {
|
||||||
|
0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7,
|
||||||
|
0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5,
|
||||||
|
0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9,
|
||||||
|
0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192,
|
||||||
|
0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA,
|
||||||
|
0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB,
|
||||||
|
0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,
|
||||||
|
0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510,
|
||||||
|
0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F,
|
||||||
|
0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567,
|
||||||
|
0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B,
|
||||||
|
0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580,
|
||||||
|
0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x00B5,0x03C4,
|
||||||
|
0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229,
|
||||||
|
0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248,
|
||||||
|
0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0,
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 128; i++)
|
||||||
|
if (high[i] == cp) return (uint8_t)(0x80 + i);
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic font height: TTF line height or 16 (bitmap fallback)
|
// Dynamic font height: TTF line height or 16 (bitmap fallback)
|
||||||
inline int system_font_height() {
|
inline int system_font_height() {
|
||||||
if (fonts::system_font && fonts::system_font->valid)
|
if (fonts::system_font && fonts::system_font->valid)
|
||||||
@@ -74,8 +102,8 @@ inline void draw_text(Framebuffer& fb, int x, int y, const char* text, Color fg)
|
|||||||
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
|
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p; x += FONT_WIDTH) {
|
||||||
draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg);
|
draw_char(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,18 +112,23 @@ inline void draw_text_bg(Framebuffer& fb, int x, int y, const char* text, Color
|
|||||||
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
|
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p; x += FONT_WIDTH) {
|
||||||
draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg);
|
draw_char_bg(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg, bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Width of the first `len` bytes of a UTF-8 string in the UI font.
|
||||||
|
inline int text_width_n(const char* text, int len) {
|
||||||
|
if (fonts::system_font && fonts::system_font->valid)
|
||||||
|
return fonts::system_font->measure_text_n(text, len, fonts::UI_SIZE);
|
||||||
|
return montauk::utf8::count(text, len) * FONT_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
inline int text_width(const char* text) {
|
inline int text_width(const char* text) {
|
||||||
if (fonts::system_font && fonts::system_font->valid) {
|
if (fonts::system_font && fonts::system_font->valid) {
|
||||||
return fonts::system_font->measure_text(text, fonts::UI_SIZE);
|
return fonts::system_font->measure_text(text, fonts::UI_SIZE);
|
||||||
}
|
}
|
||||||
int len = 0;
|
return montauk::utf8::count(text) * FONT_WIDTH;
|
||||||
while (text[len]) len++;
|
|
||||||
return len * FONT_WIDTH;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gui
|
} // namespace gui
|
||||||
@@ -296,15 +296,13 @@ inline bool text_input_has_selection(const TextInputState& state, const char* te
|
|||||||
return text_input_selection_start(state, len) != text_input_selection_end(state, len);
|
return text_input_selection_start(state, len) != text_input_selection_end(state, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Width of the first `count` bytes (a character boundary). Masked fields
|
||||||
|
// draw one '*' per character, however many bytes it takes.
|
||||||
inline int text_input_prefix_width(const char* text, int count, bool masked) {
|
inline int text_input_prefix_width(const char* text, int count, bool masked) {
|
||||||
char prefix[512];
|
|
||||||
int len = 0;
|
int len = 0;
|
||||||
while (text && text[len] && len < count && len < (int)sizeof(prefix) - 1) {
|
while (text && text[len] && len < count) len++;
|
||||||
prefix[len] = masked ? '*' : text[len];
|
if (!masked) return text_width_n(text, len);
|
||||||
len++;
|
return montauk::utf8::count(text, len) * text_width("*");
|
||||||
}
|
|
||||||
prefix[len] = '\0';
|
|
||||||
return text_width(prefix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int text_input_index_from_x(const Rect& bounds, const char* value,
|
inline int text_input_index_from_x(const Rect& bounds, const char* value,
|
||||||
@@ -314,11 +312,13 @@ inline int text_input_index_from_x(const Rect& bounds, const char* value,
|
|||||||
|
|
||||||
int len = text_input_len(value);
|
int len = text_input_len(value);
|
||||||
int prev_w = 0;
|
int prev_w = 0;
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len;) {
|
||||||
int cur_w = text_input_prefix_width(value, i + 1, masked);
|
int next = montauk::utf8::next_boundary(value, len, i);
|
||||||
|
int cur_w = text_input_prefix_width(value, next, masked);
|
||||||
int midpoint = prev_w + (cur_w - prev_w) / 2;
|
int midpoint = prev_w + (cur_w - prev_w) / 2;
|
||||||
if (rel_x < midpoint) return i;
|
if (rel_x < midpoint) return i;
|
||||||
prev_w = cur_w;
|
prev_w = cur_w;
|
||||||
|
i = next;
|
||||||
}
|
}
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
@@ -360,8 +360,11 @@ inline int text_input_mouse(TextInputState& state,
|
|||||||
return TEXT_INPUT_NONE;
|
return TEXT_INPUT_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bytes >= 0x80 (any non-ASCII character) count as word characters, so word
|
||||||
|
// jumps never stop inside a multi-byte sequence.
|
||||||
inline bool text_input_word_char(char ch) {
|
inline bool text_input_word_char(char ch) {
|
||||||
return (ch >= 'a' && ch <= 'z') ||
|
return (uint8_t)ch >= 0x80 ||
|
||||||
|
(ch >= 'a' && ch <= 'z') ||
|
||||||
(ch >= 'A' && ch <= 'Z') ||
|
(ch >= 'A' && ch <= 'Z') ||
|
||||||
(ch >= '0' && ch <= '9') ||
|
(ch >= '0' && ch <= '9') ||
|
||||||
ch == '_';
|
ch == '_';
|
||||||
@@ -574,8 +577,10 @@ inline int text_input_key(TextInputState& state,
|
|||||||
if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE;
|
if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE;
|
||||||
|
|
||||||
int len = text_input_len(text, cap);
|
int len = text_input_len(text, cap);
|
||||||
state.cursor = text_input_clamp_cursor(state.cursor, len);
|
state.cursor = montauk::utf8::floor_boundary(
|
||||||
state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len);
|
text, len, text_input_clamp_cursor(state.cursor, len));
|
||||||
|
state.selection_anchor = montauk::utf8::floor_boundary(
|
||||||
|
text, len, text_input_clamp_cursor(state.selection_anchor, len));
|
||||||
|
|
||||||
if (state.context.open) {
|
if (state.context.open) {
|
||||||
context_menu_close(state.context);
|
context_menu_close(state.context);
|
||||||
@@ -606,20 +611,22 @@ inline int text_input_key(TextInputState& state,
|
|||||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4B || key.scancode == 0x4D ||
|
if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x4D ||
|
||||||
key.scancode == 0x47 || key.scancode == 0x4F) {
|
montauk::nav_scancode(key) == 0x47 || montauk::nav_scancode(key) == 0x4F) {
|
||||||
int next = state.cursor;
|
int next = state.cursor;
|
||||||
if (!key.shift && sel_start != sel_end && key.scancode == 0x4B)
|
if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4B)
|
||||||
next = sel_start;
|
next = sel_start;
|
||||||
else if (!key.shift && sel_start != sel_end && key.scancode == 0x4D)
|
else if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4D)
|
||||||
next = sel_end;
|
next = sel_end;
|
||||||
else if (key.scancode == 0x4B)
|
else if (montauk::nav_scancode(key) == 0x4B)
|
||||||
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) : state.cursor - 1;
|
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor)
|
||||||
else if (key.scancode == 0x4D)
|
: montauk::utf8::prev_boundary(text, state.cursor);
|
||||||
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor) : state.cursor + 1;
|
else if (montauk::nav_scancode(key) == 0x4D)
|
||||||
else if (key.scancode == 0x47)
|
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor)
|
||||||
|
: montauk::utf8::next_boundary(text, len, state.cursor);
|
||||||
|
else if (montauk::nav_scancode(key) == 0x47)
|
||||||
next = 0;
|
next = 0;
|
||||||
else if (key.scancode == 0x4F)
|
else if (montauk::nav_scancode(key) == 0x4F)
|
||||||
next = len;
|
next = len;
|
||||||
|
|
||||||
text_input_set_cursor(state, len, next, key.shift);
|
text_input_set_cursor(state, len, next, key.shift);
|
||||||
@@ -641,15 +648,16 @@ inline int text_input_key(TextInputState& state,
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
} else if (state.cursor > 0) {
|
} else if (state.cursor > 0) {
|
||||||
text_input_delete_range(text, &len, state.cursor - 1, state.cursor);
|
int start = montauk::utf8::prev_boundary(text, state.cursor);
|
||||||
state.cursor--;
|
text_input_delete_range(text, &len, start, state.cursor);
|
||||||
|
state.cursor = start;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
state.selection_anchor = state.cursor;
|
state.selection_anchor = state.cursor;
|
||||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x53) {
|
if (montauk::nav_scancode(key) == 0x53) {
|
||||||
if (sel_start != sel_end) {
|
if (sel_start != sel_end) {
|
||||||
text_input_delete_range(text, &len, sel_start, sel_end);
|
text_input_delete_range(text, &len, sel_start, sel_end);
|
||||||
state.cursor = sel_start;
|
state.cursor = sel_start;
|
||||||
@@ -661,15 +669,22 @@ inline int text_input_key(TextInputState& state,
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
} else if (state.cursor < len) {
|
} else if (state.cursor < len) {
|
||||||
text_input_delete_range(text, &len, state.cursor, state.cursor + 1);
|
int end = montauk::utf8::next_boundary(text, len, state.cursor);
|
||||||
|
text_input_delete_range(text, &len, state.cursor, end);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
state.selection_anchor = state.cursor;
|
state.selection_anchor = state.cursor;
|
||||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.ctrl || key.alt || !text_input_char_allowed(key.ascii, filter, userdata))
|
// Typed text is inserted as UTF-8; the filter sees each encoded byte.
|
||||||
|
char typed[montauk::utf8::MAX_BYTES];
|
||||||
|
int typed_len = key.ctrl || key.alt ? 0 : montauk::utf8::key_utf8(key, typed);
|
||||||
|
if (typed_len == 0) return TEXT_INPUT_NONE;
|
||||||
|
for (int i = 0; i < typed_len; i++) {
|
||||||
|
if (!text_input_char_allowed(typed[i], filter, userdata))
|
||||||
return TEXT_INPUT_NONE;
|
return TEXT_INPUT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
if (sel_start != sel_end) {
|
if (sel_start != sel_end) {
|
||||||
text_input_delete_range(text, &len, sel_start, sel_end);
|
text_input_delete_range(text, &len, sel_start, sel_end);
|
||||||
@@ -677,11 +692,12 @@ inline int text_input_key(TextInputState& state,
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len < cap - 1) {
|
if (len + typed_len < cap) {
|
||||||
for (int i = len; i >= state.cursor; i--)
|
for (int i = len; i >= state.cursor; i--)
|
||||||
text[i + 1] = text[i];
|
text[i + typed_len] = text[i];
|
||||||
text[state.cursor++] = key.ascii;
|
for (int i = 0; i < typed_len; i++)
|
||||||
len++;
|
text[state.cursor++] = typed[i];
|
||||||
|
len += typed_len;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1251,23 +1267,35 @@ inline void draw_badge(Canvas& c,
|
|||||||
c.text(x + theme.gap_sm, y + 2, label, fg);
|
c.text(x + theme.gap_sm, y + 2, label, fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One '*' per UTF-8 character.
|
||||||
inline const char* mask_text(const char* value, char* buffer, int cap) {
|
inline const char* mask_text(const char* value, char* buffer, int cap) {
|
||||||
int len = 0;
|
int len = 0;
|
||||||
while (value && value[len] && len < cap - 1) {
|
const char* p = value ? value : "";
|
||||||
buffer[len] = '*';
|
while (*p && len < cap - 1) {
|
||||||
len++;
|
montauk::utf8::next(&p);
|
||||||
|
buffer[len++] = '*';
|
||||||
}
|
}
|
||||||
buffer[len] = '\0';
|
buffer[len] = '\0';
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First `count` bytes of value (a character boundary), or one '*' per
|
||||||
|
// character in them when masked.
|
||||||
inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) {
|
inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) {
|
||||||
if (cap <= 0) return;
|
if (cap <= 0) return;
|
||||||
int len = 0;
|
int len = 0;
|
||||||
|
if (masked) {
|
||||||
|
const char* p = value ? value : "";
|
||||||
|
while (*p && p - value < count && len < cap - 1) {
|
||||||
|
montauk::utf8::next(&p);
|
||||||
|
buffer[len++] = '*';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
while (value && value[len] && len < count && len < cap - 1) {
|
while (value && value[len] && len < count && len < cap - 1) {
|
||||||
buffer[len] = masked ? '*' : value[len];
|
buffer[len] = value[len];
|
||||||
len++;
|
len++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
buffer[len] = '\0';
|
buffer[len] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,14 @@
|
|||||||
#include "gui/font.hpp"
|
#include "gui/font.hpp"
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <Api/Syscall.hpp>
|
#include <Api/Syscall.hpp>
|
||||||
#include <montauk/capabilities.h>
|
#include <montauk/capabilities.h>
|
||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
struct TermCell {
|
struct TermCell {
|
||||||
char ch;
|
uint32_t ch; // Unicode codepoint (every cell is one column wide)
|
||||||
Color fg;
|
Color fg;
|
||||||
Color bg;
|
Color bg;
|
||||||
};
|
};
|
||||||
@@ -58,6 +59,11 @@ struct TerminalState {
|
|||||||
int csi_params[8];
|
int csi_params[8];
|
||||||
int csi_param_count;
|
int csi_param_count;
|
||||||
int csi_current_param;
|
int csi_current_param;
|
||||||
|
|
||||||
|
// Partial UTF-8 sequence carried across terminal_feed calls (child output
|
||||||
|
// arrives in arbitrary chunks, so a character can straddle two reads).
|
||||||
|
char utf8_pending[4];
|
||||||
|
int utf8_pending_len;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==== Palette ====
|
// ==== Palette ====
|
||||||
@@ -244,6 +250,7 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
|
|||||||
t->csi_private = false;
|
t->csi_private = false;
|
||||||
t->csi_param_count = 0;
|
t->csi_param_count = 0;
|
||||||
t->csi_current_param = 0;
|
t->csi_current_param = 0;
|
||||||
|
t->utf8_pending_len = 0;
|
||||||
t->child_pid = 0;
|
t->child_pid = 0;
|
||||||
|
|
||||||
int total_cells = (rows + max_sb) * cols;
|
int total_cells = (rows + max_sb) * cols;
|
||||||
@@ -275,7 +282,7 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
|
|||||||
montauk::childio_settermsz(t->child_pid, cols, rows);
|
montauk::childio_settermsz(t->child_pid, cols, rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void terminal_put_char(TerminalState* t, char ch) {
|
static inline void terminal_put_char(TerminalState* t, uint32_t ch) {
|
||||||
if (t->cursor_x >= t->cols) {
|
if (t->cursor_x >= t->cols) {
|
||||||
t->cursor_x = 0;
|
t->cursor_x = 0;
|
||||||
t->cursor_y++;
|
t->cursor_y++;
|
||||||
@@ -549,11 +556,61 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit pending bytes that did not form a valid UTF-8 sequence: each one is a
|
||||||
|
// Windows-1252 character, matching how the GUI renders legacy text.
|
||||||
|
static inline void terminal_flush_utf8(TerminalState* t) {
|
||||||
|
for (int i = 0; i < t->utf8_pending_len; i++)
|
||||||
|
terminal_put_char(t, montauk::utf8::from_cp1252((uint8_t)t->utf8_pending[i]));
|
||||||
|
t->utf8_pending_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int terminal_utf8_expected(uint8_t lead) {
|
||||||
|
if (lead >= 0xC2 && lead <= 0xDF) return 2;
|
||||||
|
if (lead >= 0xE0 && lead <= 0xEF) return 3;
|
||||||
|
if (lead >= 0xF0 && lead <= 0xF4) return 4;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed one byte >= 0x80 of printable text through the UTF-8 assembler.
|
||||||
|
static inline void terminal_feed_high(TerminalState* t, uint8_t b) {
|
||||||
|
if (t->utf8_pending_len > 0) {
|
||||||
|
if ((b & 0xC0) == 0x80) {
|
||||||
|
t->utf8_pending[t->utf8_pending_len++] = (char)b;
|
||||||
|
int want = terminal_utf8_expected((uint8_t)t->utf8_pending[0]);
|
||||||
|
uint32_t cp = 0;
|
||||||
|
int n = montauk::utf8::sequence_length(t->utf8_pending,
|
||||||
|
t->utf8_pending_len, &cp);
|
||||||
|
if (n == want) {
|
||||||
|
terminal_put_char(t, cp);
|
||||||
|
t->utf8_pending_len = 0;
|
||||||
|
} else if (t->utf8_pending_len >= want) {
|
||||||
|
terminal_flush_utf8(t); // overlong / surrogate / out of range
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
terminal_flush_utf8(t);
|
||||||
|
}
|
||||||
|
if (terminal_utf8_expected(b)) {
|
||||||
|
t->utf8_pending[0] = (char)b;
|
||||||
|
t->utf8_pending_len = 1;
|
||||||
|
} else {
|
||||||
|
terminal_put_char(t, montauk::utf8::from_cp1252(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
||||||
if (len > 0) t->dirty = true;
|
if (len > 0) t->dirty = true;
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
char ch = data[i];
|
char ch = data[i];
|
||||||
|
|
||||||
|
if (t->parse_state == TerminalState::STATE_NORMAL) {
|
||||||
|
if ((uint8_t)ch >= 0x80) {
|
||||||
|
terminal_feed_high(t, (uint8_t)ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (t->utf8_pending_len > 0) terminal_flush_utf8(t);
|
||||||
|
}
|
||||||
|
|
||||||
switch (t->parse_state) {
|
switch (t->parse_state) {
|
||||||
case TerminalState::STATE_NORMAL:
|
case TerminalState::STATE_NORMAL:
|
||||||
if (ch == '\033') {
|
if (ch == '\033') {
|
||||||
@@ -575,9 +632,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
|||||||
while (t->cursor_x < next) {
|
while (t->cursor_x < next) {
|
||||||
terminal_put_char(t, ' ');
|
terminal_put_char(t, ' ');
|
||||||
}
|
}
|
||||||
} else if (ch >= 32 || ch < 0) {
|
} else if (ch >= 32) {
|
||||||
// Printable character (also treat high-bit chars as printable)
|
terminal_put_char(t, (uint8_t)ch);
|
||||||
terminal_put_char(t, ch);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -641,9 +697,9 @@ static inline void terminal_fill_pixel_rect(uint32_t* pixels, int pw, int ph,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph,
|
static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph,
|
||||||
int x, int y, unsigned char ch,
|
int x, int y, uint32_t ch,
|
||||||
uint32_t color) {
|
uint32_t color) {
|
||||||
const uint8_t* glyph = &font_data[ch * FONT_HEIGHT];
|
const uint8_t* glyph = &font_data[cp437_index(ch) * FONT_HEIGHT];
|
||||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||||
int dy = y + fy;
|
int dy = y + fy;
|
||||||
if (dy >= ph) break;
|
if (dy >= ph) break;
|
||||||
@@ -669,14 +725,14 @@ static inline void terminal_draw_cell_pixels(const TermCell& cell,
|
|||||||
cell.bg.to_pixel());
|
cell.bg.to_pixel());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cell.ch <= 32 && cell.ch >= 0) return;
|
if (cell.ch <= 32) return;
|
||||||
|
|
||||||
if (use_ttf) {
|
if (use_ttf) {
|
||||||
int baseline = py + gc->ascent;
|
int baseline = py + gc->ascent;
|
||||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||||
px, baseline, (unsigned char)cell.ch, cell.fg, gc);
|
px, baseline, (int)cell.ch, cell.fg, gc);
|
||||||
} else {
|
} else {
|
||||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
|
terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
|
||||||
cell.fg.to_pixel());
|
cell.fg.to_pixel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -691,14 +747,14 @@ static inline void terminal_draw_cursor_pixels(const TermCell& cell,
|
|||||||
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
|
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
|
||||||
g_term_palette.cursor.to_pixel());
|
g_term_palette.cursor.to_pixel());
|
||||||
|
|
||||||
if (cell.ch <= 32 && cell.ch >= 0) return;
|
if (cell.ch <= 32) return;
|
||||||
|
|
||||||
if (use_ttf) {
|
if (use_ttf) {
|
||||||
int baseline = py + gc->ascent;
|
int baseline = py + gc->ascent;
|
||||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||||
px, baseline, (unsigned char)cell.ch, g_term_palette.bg, gc);
|
px, baseline, (int)cell.ch, g_term_palette.bg, gc);
|
||||||
} else {
|
} else {
|
||||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
|
terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
|
||||||
g_term_palette.bg.to_pixel());
|
g_term_palette.bg.to_pixel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include "gui/gui.hpp"
|
#include "gui/gui.hpp"
|
||||||
#include "gui/framebuffer.hpp"
|
#include "gui/framebuffer.hpp"
|
||||||
|
|
||||||
@@ -41,39 +42,12 @@
|
|||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
|
// Windows-1252 byte -> Unicode codepoint, for the remaining code that still
|
||||||
|
// handles single-byte text (PDF simple-font encodings). UTF-8 strings go
|
||||||
|
// through measure_text/draw, which decode with montauk::utf8.
|
||||||
inline int decode_single_byte_codepoint(int codepoint) {
|
inline int decode_single_byte_codepoint(int codepoint) {
|
||||||
if (codepoint < 0 || codepoint > 255) return codepoint;
|
if (codepoint < 0 || codepoint > 255) return codepoint;
|
||||||
|
return (int)montauk::utf8::from_cp1252((uint8_t)codepoint);
|
||||||
switch (codepoint) {
|
|
||||||
case 0x80: return 0x20AC; // Euro Sign
|
|
||||||
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
|
|
||||||
case 0x83: return 0x0192; // Latin Small Letter F With Hook
|
|
||||||
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
|
|
||||||
case 0x85: return 0x2026; // Horizontal Ellipsis
|
|
||||||
case 0x86: return 0x2020; // Dagger
|
|
||||||
case 0x87: return 0x2021; // Double Dagger
|
|
||||||
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
|
|
||||||
case 0x89: return 0x2030; // Per Mille Sign
|
|
||||||
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
|
|
||||||
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
|
|
||||||
case 0x8C: return 0x0152; // Latin Capital Ligature OE
|
|
||||||
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
|
|
||||||
case 0x91: return 0x2018; // Left Single Quotation Mark
|
|
||||||
case 0x92: return 0x2019; // Right Single Quotation Mark
|
|
||||||
case 0x93: return 0x201C; // Left Double Quotation Mark
|
|
||||||
case 0x94: return 0x201D; // Right Double Quotation Mark
|
|
||||||
case 0x95: return 0x2022; // Bullet
|
|
||||||
case 0x96: return 0x2013; // En Dash
|
|
||||||
case 0x97: return 0x2014; // Em Dash
|
|
||||||
case 0x98: return 0x02DC; // Small Tilde
|
|
||||||
case 0x99: return 0x2122; // Trade Mark Sign
|
|
||||||
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
|
|
||||||
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
|
|
||||||
case 0x9C: return 0x0153; // Latin Small Ligature OE
|
|
||||||
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
|
|
||||||
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
|
|
||||||
default: return codepoint;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CachedGlyph {
|
struct CachedGlyph {
|
||||||
@@ -84,8 +58,18 @@ struct CachedGlyph {
|
|||||||
bool loaded;
|
bool loaded;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Glyphs above U+00FF live in a small open-addressed table so CJK, symbols
|
||||||
|
// and the like cost nothing until they are drawn.
|
||||||
|
struct ExtGlyph {
|
||||||
|
uint32_t codepoint; // 0 = empty slot
|
||||||
|
CachedGlyph glyph;
|
||||||
|
};
|
||||||
|
|
||||||
struct GlyphCache {
|
struct GlyphCache {
|
||||||
CachedGlyph glyphs[256]; // Single-byte text with Windows-1252 punctuation mapping
|
CachedGlyph glyphs[256]; // Unicode U+0000-U+00FF, indexed directly
|
||||||
|
ExtGlyph* ext; // everything above, hashed by codepoint
|
||||||
|
int ext_cap; // power of two, 0 until first use
|
||||||
|
int ext_count;
|
||||||
int pixel_size;
|
int pixel_size;
|
||||||
float scale;
|
float scale;
|
||||||
int ascent, descent, line_gap;
|
int ascent, descent, line_gap;
|
||||||
@@ -142,8 +126,21 @@ struct TrueTypeFont {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void init_cache(GlyphCache* gc, int pixel_size) {
|
static void free_ext(GlyphCache* gc) {
|
||||||
// Free any existing glyph bitmaps
|
if (gc->ext) {
|
||||||
|
for (int i = 0; i < gc->ext_cap; i++) {
|
||||||
|
if (gc->ext[i].codepoint && gc->ext[i].glyph.bitmap)
|
||||||
|
montauk::mfree(gc->ext[i].glyph.bitmap);
|
||||||
|
}
|
||||||
|
montauk::mfree(gc->ext);
|
||||||
|
}
|
||||||
|
gc->ext = nullptr;
|
||||||
|
gc->ext_cap = 0;
|
||||||
|
gc->ext_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release every glyph bitmap in a cache (the cache stays usable).
|
||||||
|
static void free_cache(GlyphCache* gc) {
|
||||||
for (int i = 0; i < 256; i++) {
|
for (int i = 0; i < 256; i++) {
|
||||||
if (gc->glyphs[i].bitmap) {
|
if (gc->glyphs[i].bitmap) {
|
||||||
montauk::mfree(gc->glyphs[i].bitmap);
|
montauk::mfree(gc->glyphs[i].bitmap);
|
||||||
@@ -151,6 +148,11 @@ struct TrueTypeFont {
|
|||||||
gc->glyphs[i].bitmap = nullptr;
|
gc->glyphs[i].bitmap = nullptr;
|
||||||
gc->glyphs[i].loaded = false;
|
gc->glyphs[i].loaded = false;
|
||||||
}
|
}
|
||||||
|
free_ext(gc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_cache(GlyphCache* gc, int pixel_size) {
|
||||||
|
free_cache(gc);
|
||||||
|
|
||||||
gc->pixel_size = pixel_size;
|
gc->pixel_size = pixel_size;
|
||||||
gc->scale = em_scaling
|
gc->scale = em_scaling
|
||||||
@@ -179,11 +181,8 @@ struct TrueTypeFont {
|
|||||||
return gc;
|
return gc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evict oldest cache (slot 0) — free its glyph bitmaps
|
// Evict oldest cache (slot 0) - free its glyph bitmaps
|
||||||
for (int g = 0; g < 256; g++) {
|
free_cache(&caches[0]);
|
||||||
if (caches[0].glyphs[g].bitmap)
|
|
||||||
montauk::mfree(caches[0].glyphs[g].bitmap);
|
|
||||||
}
|
|
||||||
// Shift remaining caches down
|
// Shift remaining caches down
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache));
|
montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache));
|
||||||
@@ -192,13 +191,54 @@ struct TrueTypeFont {
|
|||||||
return &caches[3];
|
return &caches[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static constexpr int EXT_MAX = 2048; // flush the table beyond this
|
||||||
|
|
||||||
|
CachedGlyph* ext_slot(GlyphCache* gc, uint32_t codepoint) {
|
||||||
|
if (gc->ext_cap == 0 || gc->ext_count * 4 >= gc->ext_cap * 3) {
|
||||||
|
int new_cap = gc->ext_cap ? gc->ext_cap * 2 : 64;
|
||||||
|
if (gc->ext_count >= EXT_MAX) {
|
||||||
|
// A document full of distinct glyphs: start over rather than
|
||||||
|
// grow without bound. Bitmaps are rebuilt on demand.
|
||||||
|
free_ext(gc);
|
||||||
|
new_cap = 64;
|
||||||
|
}
|
||||||
|
ExtGlyph* table = (ExtGlyph*)montauk::malloc(sizeof(ExtGlyph) * new_cap);
|
||||||
|
if (!table) return nullptr;
|
||||||
|
montauk::memset(table, 0, sizeof(ExtGlyph) * new_cap);
|
||||||
|
for (int i = 0; i < gc->ext_cap; i++) {
|
||||||
|
ExtGlyph& e = gc->ext[i];
|
||||||
|
if (!e.codepoint) continue;
|
||||||
|
uint32_t h = (e.codepoint * 2654435761u) & (new_cap - 1);
|
||||||
|
while (table[h].codepoint) h = (h + 1) & (new_cap - 1);
|
||||||
|
table[h] = e;
|
||||||
|
}
|
||||||
|
if (gc->ext) montauk::mfree(gc->ext);
|
||||||
|
gc->ext = table;
|
||||||
|
gc->ext_cap = new_cap;
|
||||||
|
}
|
||||||
|
uint32_t mask = gc->ext_cap - 1;
|
||||||
|
uint32_t h = (codepoint * 2654435761u) & mask;
|
||||||
|
while (gc->ext[h].codepoint && gc->ext[h].codepoint != codepoint)
|
||||||
|
h = (h + 1) & mask;
|
||||||
|
if (!gc->ext[h].codepoint) {
|
||||||
|
gc->ext[h].codepoint = codepoint;
|
||||||
|
montauk::memset(&gc->ext[h].glyph, 0, sizeof(CachedGlyph));
|
||||||
|
gc->ext_count++;
|
||||||
|
}
|
||||||
|
return &gc->ext[h].glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glyph for a Unicode codepoint. Codepoints the font lacks render as its
|
||||||
|
// .notdef box, so missing characters stay visible instead of vanishing.
|
||||||
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
|
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
|
||||||
if (codepoint < 0 || codepoint >= 256) return nullptr;
|
if (codepoint < 0 || codepoint > 0x10FFFF) return nullptr;
|
||||||
CachedGlyph* g = &gc->glyphs[codepoint];
|
CachedGlyph* g = codepoint < 256 ? &gc->glyphs[codepoint]
|
||||||
|
: ext_slot(gc, (uint32_t)codepoint);
|
||||||
|
if (!g) return nullptr;
|
||||||
if (g->loaded) return g;
|
if (g->loaded) return g;
|
||||||
|
|
||||||
g->loaded = true;
|
g->loaded = true;
|
||||||
int font_codepoint = decode_single_byte_codepoint(codepoint);
|
int font_codepoint = codepoint;
|
||||||
int advance, lsb;
|
int advance, lsb;
|
||||||
stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb);
|
stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb);
|
||||||
g->advance = (int)(advance * gc->scale);
|
g->advance = (int)(advance * gc->scale);
|
||||||
@@ -222,12 +262,34 @@ struct TrueTypeFont {
|
|||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advance of one codepoint at a size (0 if the font is unusable).
|
||||||
|
int char_advance(int codepoint, int pixel_size) {
|
||||||
|
if (!valid) return 0;
|
||||||
|
CachedGlyph* g = get_glyph(get_cache(pixel_size), codepoint);
|
||||||
|
return g ? g->advance : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width of the first `len` bytes of a UTF-8 string.
|
||||||
|
int measure_text_n(const char* text, int len, int pixel_size) {
|
||||||
|
if (!valid) return 0;
|
||||||
|
GlyphCache* gc = get_cache(pixel_size);
|
||||||
|
int w = 0;
|
||||||
|
for (int pos = 0; pos < len && text[pos];) {
|
||||||
|
int n;
|
||||||
|
uint32_t cp = montauk::utf8::decode(text + pos, len - pos, &n);
|
||||||
|
CachedGlyph* g = get_glyph(gc, (int)cp);
|
||||||
|
if (g) w += g->advance;
|
||||||
|
pos += n;
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
int measure_text(const char* text, int pixel_size) {
|
int measure_text(const char* text, int pixel_size) {
|
||||||
if (!valid) return 0;
|
if (!valid) return 0;
|
||||||
GlyphCache* gc = get_cache(pixel_size);
|
GlyphCache* gc = get_cache(pixel_size);
|
||||||
int w = 0;
|
int w = 0;
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p;) {
|
||||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (g) w += g->advance;
|
if (g) w += g->advance;
|
||||||
}
|
}
|
||||||
return w;
|
return w;
|
||||||
@@ -253,8 +315,8 @@ struct TrueTypeFont {
|
|||||||
uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) |
|
uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) |
|
||||||
((uint32_t)color.g << 8) | color.b;
|
((uint32_t)color.g << 8) | color.b;
|
||||||
|
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p;) {
|
||||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (!g) continue;
|
if (!g) continue;
|
||||||
|
|
||||||
if (g->bitmap) {
|
if (g->bitmap) {
|
||||||
@@ -325,8 +387,8 @@ struct TrueTypeFont {
|
|||||||
int baseline = y + gc->ascent;
|
int baseline = y + gc->ascent;
|
||||||
bool drew_pixel = false;
|
bool drew_pixel = false;
|
||||||
|
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p;) {
|
||||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (!g) continue;
|
if (!g) continue;
|
||||||
|
|
||||||
if (g->bitmap) {
|
if (g->bitmap) {
|
||||||
@@ -378,8 +440,8 @@ struct TrueTypeFont {
|
|||||||
int clip_x2 = clip_x + clip_w;
|
int clip_x2 = clip_x + clip_w;
|
||||||
int clip_y2 = clip_y + clip_h;
|
int clip_y2 = clip_y + clip_h;
|
||||||
|
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p;) {
|
||||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (!g) continue;
|
if (!g) continue;
|
||||||
|
|
||||||
if (g->bitmap) {
|
if (g->bitmap) {
|
||||||
@@ -417,7 +479,7 @@ struct TrueTypeFont {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw single character to buffer, returning advance width
|
// Draw one Unicode codepoint to buffer, returning advance width
|
||||||
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
|
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
|
||||||
int x, int baseline, int codepoint,
|
int x, int baseline, int codepoint,
|
||||||
Color color, GlyphCache* gc) {
|
Color color, GlyphCache* gc) {
|
||||||
|
|||||||
@@ -212,11 +212,7 @@ struct TextBox {
|
|||||||
|
|
||||||
// Draw cursor if focused
|
// Draw cursor if focused
|
||||||
if (focused) {
|
if (focused) {
|
||||||
// Measure text up to cursor position for proportional fonts
|
int cx = tx + text_width_n(text, cursor);
|
||||||
char prefix[256];
|
|
||||||
for (int i = 0; i < cursor && i < 255; i++) prefix[i] = text[i];
|
|
||||||
prefix[cursor < 255 ? cursor : 255] = '\0';
|
|
||||||
int cx = tx + text_width(prefix);
|
|
||||||
draw_vline(fb, cx, ty, fh, cursor_color);
|
draw_vline(fb, cx, ty, fh, cursor_color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,34 +226,35 @@ struct TextBox {
|
|||||||
void handle_key(const montauk::abi::KeyEvent& key) {
|
void handle_key(const montauk::abi::KeyEvent& key) {
|
||||||
if (!focused || !key.pressed) return;
|
if (!focused || !key.pressed) return;
|
||||||
|
|
||||||
|
char typed[montauk::utf8::MAX_BYTES];
|
||||||
|
int typed_len = montauk::utf8::key_utf8(key, typed);
|
||||||
|
|
||||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
// Backspace
|
// Backspace: remove the whole character before the cursor
|
||||||
if (cursor > 0) {
|
if (cursor > 0) {
|
||||||
for (int i = cursor - 1; i < text_len - 1; i++) {
|
int start = montauk::utf8::prev_boundary(text, cursor);
|
||||||
text[i] = text[i + 1];
|
int n = cursor - start;
|
||||||
|
for (int i = start; i <= text_len - n; i++)
|
||||||
|
text[i] = text[i + n];
|
||||||
|
text_len -= n;
|
||||||
|
cursor = start;
|
||||||
}
|
}
|
||||||
text_len--;
|
} else if (typed_len > 0) {
|
||||||
cursor--;
|
// Printable character, inserted as UTF-8
|
||||||
text[text_len] = '\0';
|
if (text_len + typed_len <= 254) {
|
||||||
|
for (int i = text_len; i >= cursor; i--)
|
||||||
|
text[i + typed_len] = text[i];
|
||||||
|
for (int i = 0; i < typed_len; i++)
|
||||||
|
text[cursor + i] = typed[i];
|
||||||
|
cursor += typed_len;
|
||||||
|
text_len += typed_len;
|
||||||
}
|
}
|
||||||
} else if ((unsigned char)key.ascii >= 32
|
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
&& (unsigned char)key.ascii != 127) {
|
|
||||||
// Printable character
|
|
||||||
if (text_len < 254) {
|
|
||||||
for (int i = text_len; i > cursor; i--) {
|
|
||||||
text[i] = text[i - 1];
|
|
||||||
}
|
|
||||||
text[cursor] = key.ascii;
|
|
||||||
cursor++;
|
|
||||||
text_len++;
|
|
||||||
text[text_len] = '\0';
|
|
||||||
}
|
|
||||||
} else if (key.scancode == 0x4B) {
|
|
||||||
// Left arrow
|
// Left arrow
|
||||||
if (cursor > 0) cursor--;
|
cursor = montauk::utf8::prev_boundary(text, cursor);
|
||||||
} else if (key.scancode == 0x4D) {
|
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
// Right arrow
|
// Right arrow
|
||||||
if (cursor < text_len) cursor++;
|
cursor = montauk::utf8::next_boundary(text, text_len, cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -217,6 +217,8 @@ extern "C" {
|
|||||||
#define MTK_SYS_CONSENT_REGISTER 191
|
#define MTK_SYS_CONSENT_REGISTER 191
|
||||||
#define MTK_SYS_CONSENT_NEXT 192
|
#define MTK_SYS_CONSENT_NEXT 192
|
||||||
#define MTK_SYS_CONSENT_REPLY 193
|
#define MTK_SYS_CONSENT_REPLY 193
|
||||||
|
#define MTK_SYS_GETKEY_EX 194
|
||||||
|
#define MTK_SYS_CHILDIO_WRITEKEY_EX 195
|
||||||
/* @SYSCALLS-END */
|
/* @SYSCALLS-END */
|
||||||
|
|
||||||
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
||||||
|
|||||||
@@ -8,19 +8,22 @@
|
|||||||
|
|
||||||
#include <montauk/config.h>
|
#include <montauk/config.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
|
|
||||||
namespace montauk::keyboard {
|
namespace montauk::keyboard {
|
||||||
|
|
||||||
inline constexpr int MAX_LAYOUTS = 8;
|
inline constexpr int MAX_LAYOUTS = 8;
|
||||||
inline constexpr int MAX_KEYS = 64;
|
inline constexpr int MAX_KEYS = 64;
|
||||||
|
inline constexpr int MAX_DEAD_KEYS = 16;
|
||||||
|
|
||||||
// One overridden key. Characters are Windows-1252 bytes to match the
|
// One overridden key. Characters are Unicode codepoints; translation stores
|
||||||
// single-byte GUI text stack; 0 means "no override, keep the kernel's value".
|
// them in KeyEvent::codepoint (and the Windows-1252 byte, if any, in ascii).
|
||||||
|
// 0 means "no override, keep the kernel's value".
|
||||||
struct KeyMap {
|
struct KeyMap {
|
||||||
uint8_t scancode;
|
uint8_t scancode;
|
||||||
uint8_t base;
|
uint32_t base;
|
||||||
uint8_t shift;
|
uint32_t shift;
|
||||||
uint8_t altgr;
|
uint32_t altgr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Layout {
|
struct Layout {
|
||||||
@@ -29,6 +32,10 @@ struct Layout {
|
|||||||
char short_name[8];
|
char short_name[8];
|
||||||
KeyMap keys[MAX_KEYS];
|
KeyMap keys[MAX_KEYS];
|
||||||
int key_count; // 0 = passthrough (the kernel's US table)
|
int key_count; // 0 = passthrough (the kernel's US table)
|
||||||
|
// Characters that act as dead keys in this layout: pressing one types
|
||||||
|
// nothing and modifies the next character (see compose()).
|
||||||
|
uint32_t dead_keys[MAX_DEAD_KEYS];
|
||||||
|
int dead_count;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Registry {
|
struct Registry {
|
||||||
@@ -40,8 +47,71 @@ struct State {
|
|||||||
Registry registry;
|
Registry registry;
|
||||||
bool enabled[MAX_LAYOUTS];
|
bool enabled[MAX_LAYOUTS];
|
||||||
int active;
|
int active;
|
||||||
|
uint32_t pending_dead; // dead key waiting for the next character, 0 = none
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==== Dead-key composition ====
|
||||||
|
// Keyed by the spacing form of the accent, which is what a layout puts on the
|
||||||
|
// dead key itself (and what dead key + Space types).
|
||||||
|
|
||||||
|
struct ComposeEntry {
|
||||||
|
uint16_t dead;
|
||||||
|
uint16_t base;
|
||||||
|
uint16_t result;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr ComposeEntry COMPOSE[] = {
|
||||||
|
// acute
|
||||||
|
{0x00B4,'a',0xE1},{0x00B4,'A',0xC1},{0x00B4,'e',0xE9},{0x00B4,'E',0xC9},
|
||||||
|
{0x00B4,'i',0xED},{0x00B4,'I',0xCD},{0x00B4,'o',0xF3},{0x00B4,'O',0xD3},
|
||||||
|
{0x00B4,'u',0xFA},{0x00B4,'U',0xDA},{0x00B4,'y',0xFD},{0x00B4,'Y',0xDD},
|
||||||
|
{0x00B4,'c',0x107},{0x00B4,'C',0x106},{0x00B4,'l',0x13A},{0x00B4,'L',0x139},
|
||||||
|
{0x00B4,'n',0x144},{0x00B4,'N',0x143},{0x00B4,'r',0x155},{0x00B4,'R',0x154},
|
||||||
|
{0x00B4,'s',0x15B},{0x00B4,'S',0x15A},{0x00B4,'z',0x17A},{0x00B4,'Z',0x179},
|
||||||
|
// caron
|
||||||
|
{0x02C7,'c',0x10D},{0x02C7,'C',0x10C},{0x02C7,'d',0x10F},{0x02C7,'D',0x10E},
|
||||||
|
{0x02C7,'e',0x11B},{0x02C7,'E',0x11A},{0x02C7,'l',0x13E},{0x02C7,'L',0x13D},
|
||||||
|
{0x02C7,'n',0x148},{0x02C7,'N',0x147},{0x02C7,'r',0x159},{0x02C7,'R',0x158},
|
||||||
|
{0x02C7,'s',0x161},{0x02C7,'S',0x160},{0x02C7,'t',0x165},{0x02C7,'T',0x164},
|
||||||
|
{0x02C7,'z',0x17E},{0x02C7,'Z',0x17D},
|
||||||
|
// diaeresis
|
||||||
|
{0x00A8,'a',0xE4},{0x00A8,'A',0xC4},{0x00A8,'e',0xEB},{0x00A8,'E',0xCB},
|
||||||
|
{0x00A8,'i',0xEF},{0x00A8,'I',0xCF},{0x00A8,'o',0xF6},{0x00A8,'O',0xD6},
|
||||||
|
{0x00A8,'u',0xFC},{0x00A8,'U',0xDC},{0x00A8,'y',0xFF},{0x00A8,'Y',0x178},
|
||||||
|
// ring above
|
||||||
|
{0x00B0,'u',0x16F},{0x00B0,'U',0x16E},{0x00B0,'a',0xE5},{0x00B0,'A',0xC5},
|
||||||
|
// grave
|
||||||
|
{0x0060,'a',0xE0},{0x0060,'A',0xC0},{0x0060,'e',0xE8},{0x0060,'E',0xC8},
|
||||||
|
{0x0060,'i',0xEC},{0x0060,'I',0xCC},{0x0060,'o',0xF2},{0x0060,'O',0xD2},
|
||||||
|
{0x0060,'u',0xF9},{0x0060,'U',0xD9},
|
||||||
|
// circumflex
|
||||||
|
{0x005E,'a',0xE2},{0x005E,'A',0xC2},{0x005E,'e',0xEA},{0x005E,'E',0xCA},
|
||||||
|
{0x005E,'i',0xEE},{0x005E,'I',0xCE},{0x005E,'o',0xF4},{0x005E,'O',0xD4},
|
||||||
|
{0x005E,'u',0xFB},{0x005E,'U',0xDB},
|
||||||
|
// tilde
|
||||||
|
{0x007E,'a',0xE3},{0x007E,'A',0xC3},{0x007E,'n',0xF1},{0x007E,'N',0xD1},
|
||||||
|
{0x007E,'o',0xF5},{0x007E,'O',0xD5},
|
||||||
|
// breve
|
||||||
|
{0x02D8,'a',0x103},{0x02D8,'A',0x102},{0x02D8,'g',0x11F},{0x02D8,'G',0x11E},
|
||||||
|
// ogonek
|
||||||
|
{0x02DB,'a',0x105},{0x02DB,'A',0x104},{0x02DB,'e',0x119},{0x02DB,'E',0x118},
|
||||||
|
{0x02DB,'i',0x12F},{0x02DB,'I',0x12E},{0x02DB,'u',0x173},{0x02DB,'U',0x172},
|
||||||
|
// dot above
|
||||||
|
{0x02D9,'z',0x17C},{0x02D9,'Z',0x17B},{0x02D9,'e',0x117},{0x02D9,'E',0x116},
|
||||||
|
{0x02D9,'I',0x130},
|
||||||
|
// double acute
|
||||||
|
{0x02DD,'o',0x151},{0x02DD,'O',0x150},{0x02DD,'u',0x171},{0x02DD,'U',0x170},
|
||||||
|
// cedilla
|
||||||
|
{0x00B8,'c',0xE7},{0x00B8,'C',0xC7},{0x00B8,'s',0x15F},{0x00B8,'S',0x15E},
|
||||||
|
};
|
||||||
|
|
||||||
|
// The composed character, or 0 if the accent does not combine with base.
|
||||||
|
inline uint32_t compose(uint32_t dead, uint32_t base) {
|
||||||
|
for (const ComposeEntry& e : COMPOSE)
|
||||||
|
if (e.dead == dead && e.base == base) return e.result;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
inline void build_key(char* out, int cap, const char* prefix,
|
inline void build_key(char* out, int cap, const char* prefix,
|
||||||
const char* id, const char* suffix = nullptr) {
|
const char* id, const char* suffix = nullptr) {
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
@@ -54,10 +124,9 @@ inline void build_key(char* out, int cap, const char* prefix,
|
|||||||
out[pos] = '\0';
|
out[pos] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read an array of byte values from the layout table.
|
// Read an array of byte values (scancodes) from the layout table.
|
||||||
// Returns the element count, or -1 if the key is missing or malformed.
|
// Returns the element count, or -1 if the key is missing or malformed.
|
||||||
inline int read_bytes(const toml::Doc& doc, const char* key,
|
inline int read_bytes(const toml::Doc& doc, const char* key, uint8_t* out, int cap) {
|
||||||
uint8_t* out, int cap) {
|
|
||||||
toml::Value* arr = doc.get_array(key);
|
toml::Value* arr = doc.get_array(key);
|
||||||
if (!arr) return -1;
|
if (!arr) return -1;
|
||||||
if (arr->array.count > cap) return -1;
|
if (arr->array.count > cap) return -1;
|
||||||
@@ -70,6 +139,23 @@ inline int read_bytes(const toml::Doc& doc, const char* key,
|
|||||||
return arr->array.count;
|
return arr->array.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read an array of characters (Unicode codepoints). 0x80-0x9F are C1 control
|
||||||
|
// codes in Unicode, never key output, so they are read as the Windows-1252
|
||||||
|
// bytes older layout files used (0x80 = euro sign).
|
||||||
|
inline int read_codepoints(const toml::Doc& doc, const char* key, uint32_t* out, int cap) {
|
||||||
|
toml::Value* arr = doc.get_array(key);
|
||||||
|
if (!arr) return -1;
|
||||||
|
if (arr->array.count > cap) return -1;
|
||||||
|
for (int i = 0; i < arr->array.count; i++) {
|
||||||
|
toml::Value* value = arr->array.items[i];
|
||||||
|
if (!value || value->type != toml::Type::Int) return -1;
|
||||||
|
int64_t v = value->ival;
|
||||||
|
if (v < 0 || v > 0x10FFFF || (v >= 0xD800 && v <= 0xDFFF)) return -1;
|
||||||
|
out[i] = (v >= 0x80 && v <= 0x9F) ? utf8::from_cp1252((uint8_t)v) : (uint32_t)v;
|
||||||
|
}
|
||||||
|
return arr->array.count;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse one [layouts.<id>] table. Returns false if the layout declares key
|
// Parse one [layouts.<id>] table. Returns false if the layout declares key
|
||||||
// overrides but they are inconsistent, in which case the caller skips it: a
|
// overrides but they are inconsistent, in which case the caller skips it: a
|
||||||
// malformed table must never produce a half-applied layout.
|
// malformed table must never produce a half-applied layout.
|
||||||
@@ -85,10 +171,15 @@ inline bool load_layout(Layout* out, const char* id, const toml::Doc& doc) {
|
|||||||
montauk::strncpy(out->short_name, doc.get_string(key, id),
|
montauk::strncpy(out->short_name, doc.get_string(key, id),
|
||||||
sizeof(out->short_name));
|
sizeof(out->short_name));
|
||||||
|
|
||||||
|
build_key(key, sizeof(key), "layouts.", id, ".dead_keys");
|
||||||
|
int dead = read_codepoints(doc, key, out->dead_keys, MAX_DEAD_KEYS);
|
||||||
|
if (dead < 0 && doc.get_array(key)) return false; // present but malformed
|
||||||
|
out->dead_count = dead < 0 ? 0 : dead;
|
||||||
|
|
||||||
uint8_t scancodes[MAX_KEYS];
|
uint8_t scancodes[MAX_KEYS];
|
||||||
uint8_t base[MAX_KEYS];
|
uint32_t base[MAX_KEYS];
|
||||||
uint8_t shift[MAX_KEYS];
|
uint32_t shift[MAX_KEYS];
|
||||||
uint8_t altgr[MAX_KEYS] = {};
|
uint32_t altgr[MAX_KEYS] = {};
|
||||||
|
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".scancodes");
|
build_key(key, sizeof(key), "layouts.", id, ".scancodes");
|
||||||
int count = read_bytes(doc, key, scancodes, MAX_KEYS);
|
int count = read_bytes(doc, key, scancodes, MAX_KEYS);
|
||||||
@@ -99,11 +190,11 @@ inline bool load_layout(Layout* out, const char* id, const toml::Doc& doc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".base");
|
build_key(key, sizeof(key), "layouts.", id, ".base");
|
||||||
if (read_bytes(doc, key, base, MAX_KEYS) != count) return false;
|
if (read_codepoints(doc, key, base, MAX_KEYS) != count) return false;
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".shift");
|
build_key(key, sizeof(key), "layouts.", id, ".shift");
|
||||||
if (read_bytes(doc, key, shift, MAX_KEYS) != count) return false;
|
if (read_codepoints(doc, key, shift, MAX_KEYS) != count) return false;
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".altgr");
|
build_key(key, sizeof(key), "layouts.", id, ".altgr");
|
||||||
int altgr_count = read_bytes(doc, key, altgr, MAX_KEYS);
|
int altgr_count = read_codepoints(doc, key, altgr, MAX_KEYS);
|
||||||
if (altgr_count >= 0 && altgr_count != count) return false;
|
if (altgr_count >= 0 && altgr_count != count) return false;
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
@@ -174,6 +265,7 @@ inline Registry load_registry() {
|
|||||||
// it reads one small file instead of re-parsing the whole layout table.
|
// it reads one small file instead of re-parsing the whole layout table.
|
||||||
inline void refresh_selection(State* state, const char* username) {
|
inline void refresh_selection(State* state, const char* username) {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
state->pending_dead = 0; // a dead key never carries over a layout switch
|
||||||
for (int i = 0; i < state->registry.count; i++) state->enabled[i] = false;
|
for (int i = 0; i < state->registry.count; i++) state->enabled[i] = false;
|
||||||
|
|
||||||
toml::Doc doc = config::load_user(username, "keyboard");
|
toml::Doc doc = config::load_user(username, "keyboard");
|
||||||
@@ -237,40 +329,59 @@ inline int next_enabled(const State& state, int current) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Caps Lock is derived rather than declared per key: it applies only where
|
// Caps Lock is derived rather than declared per key: it applies only where
|
||||||
// base and shift are a Windows-1252 lower/upper letter pair. That covers the
|
// base and shift are the lower/upper case of one letter. That covers accented
|
||||||
// accented letters (aa 0xE5 / AA 0xC5) without wrongly upper-casing keys such
|
// letters in any script (aa/AA, e-caron/E-caron) without wrongly upper-casing
|
||||||
// as 2 / " where the shifted value is unrelated punctuation.
|
// keys such as 2 / " where the shifted value is unrelated punctuation.
|
||||||
inline constexpr bool is_letter_pair(uint8_t base, uint8_t shift) {
|
inline constexpr bool is_letter_pair(uint32_t base, uint32_t shift) {
|
||||||
if (base == 0 || shift == 0) return false;
|
if (base == 0 || shift == 0) return false;
|
||||||
if (base < 0x61) return false;
|
return utf8::is_case_pair(base, shift);
|
||||||
if (base > 0x7A && base < 0xE0) return false;
|
|
||||||
if (base == 0xF7) return false; // division sign sits inside the range
|
|
||||||
return shift == (uint8_t)(base - 0x20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA
|
static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA
|
||||||
static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE
|
static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE
|
||||||
static_assert(is_letter_pair(0xE6, 0xC6)); // ae / AE
|
static_assert(is_letter_pair(0x11B, 0x11A)); // e-caron
|
||||||
static_assert(is_letter_pair('a', 'A'));
|
static_assert(is_letter_pair('a', 'A'));
|
||||||
static_assert(!is_letter_pair('2', '"'));
|
static_assert(!is_letter_pair('2', '"'));
|
||||||
static_assert(!is_letter_pair(0xF7, 0xD7)); // divide / multiply
|
static_assert(!is_letter_pair(0xF7, 0xD7)); // divide / multiply
|
||||||
|
static_assert(!is_letter_pair(0x11B, '2')); // Czech number row
|
||||||
|
|
||||||
inline void translate(const State& state, abi::KeyEvent* key) {
|
inline bool is_dead_key(const Layout& layout, uint32_t cp) {
|
||||||
|
if (cp == 0) return false;
|
||||||
|
for (int i = 0; i < layout.dead_count; i++)
|
||||||
|
if (layout.dead_keys[i] == cp) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys that may be pressed between a dead key and its letter without
|
||||||
|
// cancelling it (Shift for a capital, AltGr, Caps Lock, ...).
|
||||||
|
inline constexpr bool is_modifier_scancode(uint8_t sc) {
|
||||||
|
return sc == 0x2A || sc == 0x36 || sc == 0x1D || sc == 0x38 ||
|
||||||
|
sc == 0x3A || sc == 0x45 || sc == 0x46 || sc == 0x5B || sc == 0x5C;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the active layout to a raw key event: the character it types goes to
|
||||||
|
// key->codepoint, its Windows-1252 byte (or KEY_ASCII_SUBSTITUTE) to
|
||||||
|
// key->ascii. Dead keys type nothing and modify the following character, so
|
||||||
|
// the state is updated on presses.
|
||||||
|
inline void translate(State& state, abi::KeyEvent* key) {
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
if (state.active < 0 || state.active >= state.registry.count) return;
|
const Layout* layout = (state.active >= 0 && state.active < state.registry.count)
|
||||||
const Layout& layout = state.registry.items[state.active];
|
? &state.registry.items[state.active] : nullptr;
|
||||||
|
|
||||||
|
uint8_t a = (uint8_t)key->ascii;
|
||||||
|
uint32_t cp = key->codepoint ? key->codepoint
|
||||||
|
: ((a >= 0x20 && a < 0x7F) ? a : 0);
|
||||||
|
uint8_t scancode = key->scancode & 0x7F;
|
||||||
|
|
||||||
// Extended keys carry a main-block scancode with the E0 prefix stripped
|
// Extended keys carry a main-block scancode with the E0 prefix stripped
|
||||||
// (keypad "/" arrives as 0x35, the same as the main "/"), so translating
|
// (keypad "/" arrives as 0x35, the same as the main "/"), so translating
|
||||||
// them would turn keypad "/" into whatever the layout puts on that key.
|
// them would turn keypad "/" into whatever the layout puts on that key.
|
||||||
if (key->extended) return;
|
if (layout && !key->extended) {
|
||||||
|
for (int i = 0; i < layout->key_count; i++) {
|
||||||
uint8_t scancode = key->scancode & 0x7F;
|
const KeyMap& mapping = layout->keys[i];
|
||||||
for (int i = 0; i < layout.key_count; i++) {
|
|
||||||
const KeyMap& mapping = layout.keys[i];
|
|
||||||
if (mapping.scancode != scancode) continue;
|
if (mapping.scancode != scancode) continue;
|
||||||
|
|
||||||
uint8_t out;
|
uint32_t out;
|
||||||
if (key->altgr) {
|
if (key->altgr) {
|
||||||
out = mapping.altgr;
|
out = mapping.altgr;
|
||||||
} else {
|
} else {
|
||||||
@@ -280,14 +391,55 @@ inline void translate(const State& state, abi::KeyEvent* key) {
|
|||||||
out = upper ? mapping.shift : mapping.base;
|
out = upper ? mapping.shift : mapping.base;
|
||||||
}
|
}
|
||||||
if (out != 0) {
|
if (out != 0) {
|
||||||
key->ascii = (char)out;
|
cp = out;
|
||||||
// An AltGr key that produced a character is text, not a shortcut.
|
// An AltGr key that produced a character is text, not a
|
||||||
// Apps gate insertion on !alt (and alt is LeftAlt||RightAlt), so
|
// shortcut. Apps gate insertion on !alt (and alt is
|
||||||
// leaving it set would silently swallow every AltGr character.
|
// LeftAlt||RightAlt), so leaving it set would silently swallow
|
||||||
|
// every AltGr character.
|
||||||
if (key->altgr) key->alt = false;
|
if (key->altgr) key->alt = false;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dead = layout && !key->extended && is_dead_key(*layout, cp);
|
||||||
|
bool shortcut = key->ctrl || (key->alt && !key->altgr);
|
||||||
|
if (dead && shortcut) dead = false;
|
||||||
|
|
||||||
|
if (key->pressed && !shortcut) {
|
||||||
|
if (dead) {
|
||||||
|
if (state.pending_dead == cp) {
|
||||||
|
state.pending_dead = 0; // pressed twice: type the accent
|
||||||
|
dead = false;
|
||||||
|
} else {
|
||||||
|
state.pending_dead = cp; // wait for the next character
|
||||||
|
}
|
||||||
|
} else if (state.pending_dead) {
|
||||||
|
if (cp == ' ') {
|
||||||
|
cp = state.pending_dead; // accent + Space: the accent
|
||||||
|
state.pending_dead = 0;
|
||||||
|
} else if (cp) {
|
||||||
|
// The composed letter, or just the letter when the accent does
|
||||||
|
// not apply to it (one event can only carry one character).
|
||||||
|
uint32_t composed = compose(state.pending_dead, cp);
|
||||||
|
if (composed) cp = composed;
|
||||||
|
state.pending_dead = 0;
|
||||||
|
} else if (!is_modifier_scancode(scancode)) {
|
||||||
|
state.pending_dead = 0; // Enter, arrows, ... cancel it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dead) {
|
||||||
|
key->codepoint = 0;
|
||||||
|
key->ascii = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
key->codepoint = cp;
|
||||||
|
if (cp != 0) {
|
||||||
|
uint8_t b = utf8::to_cp1252(cp);
|
||||||
|
key->ascii = b ? (char)b : abi::KEY_ASCII_SUBSTITUTE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Direct keyboard readers ----
|
// ---- Direct keyboard readers ----
|
||||||
|
|||||||
@@ -56,27 +56,28 @@ inline const char* GetStatusText(StatusCode statusCode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared with C (libc.c), so no nullptr.
|
||||||
inline const char* GetStatusExplanationText(StatusCode statusCode) {
|
inline const char* GetStatusExplanationText(StatusCode statusCode) {
|
||||||
const char* explanationText[] = {
|
const char* explanationText[] = {
|
||||||
nullptr, // 0
|
0, // 0
|
||||||
nullptr, // 1
|
0, // 1
|
||||||
nullptr, // 2
|
0, // 2
|
||||||
nullptr, // 3
|
0, // 3
|
||||||
"The path is protected by the system and cannot be modified.", // 4
|
"The path is protected by the system and cannot be modified.", // 4
|
||||||
nullptr, // 5
|
0, // 5
|
||||||
nullptr, // 6
|
0, // 6
|
||||||
nullptr, // 7
|
0, // 7
|
||||||
nullptr, // 8
|
0, // 8
|
||||||
nullptr, // 9
|
0, // 9
|
||||||
nullptr, // 10
|
0, // 10
|
||||||
nullptr, // 11
|
0, // 11
|
||||||
nullptr, // 12
|
0, // 12
|
||||||
"The buffer supplied by the caller is too small.", // 13
|
"The buffer supplied by the caller is too small.", // 13
|
||||||
nullptr, // 14
|
0, // 14
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statusCode > 0 && statusCode <= 14) {
|
if (statusCode > 0 && statusCode <= 14) {
|
||||||
if (explanationText[statusCode] != nullptr) {
|
if (explanationText[statusCode] != 0) {
|
||||||
return explanationText[statusCode];
|
return explanationText[statusCode];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Api/Syscall.hpp>
|
#include <Api/Syscall.hpp>
|
||||||
#include <montauk/statuscodes.h>
|
#include <montauk/statuscodes.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
|
|
||||||
namespace montauk {
|
namespace montauk {
|
||||||
|
|
||||||
@@ -222,7 +223,23 @@ namespace montauk {
|
|||||||
|
|
||||||
// Keyboard
|
// Keyboard
|
||||||
inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); }
|
inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); }
|
||||||
inline void getkey(montauk::abi::KeyEvent* out) { syscall1(montauk::abi::SYS_GETKEY, (uint64_t)out); }
|
// Scancode to use when checking for navigation keys (arrows, Home/End,
|
||||||
|
// PgUp/PgDn, Insert/Delete). The keypad shares those scancodes with its
|
||||||
|
// digit and '.' keys, so a keypad key that types a character reports 0
|
||||||
|
// here: numpad 4 is text, not Left. Compare against this, not .scancode.
|
||||||
|
inline uint8_t nav_scancode(const montauk::abi::KeyEvent& key) {
|
||||||
|
uint8_t sc = key.scancode;
|
||||||
|
if (!key.extended && (sc & 0x7F) >= 0x47 && (sc & 0x7F) <= 0x53 &&
|
||||||
|
montauk::utf8::key_codepoint(key) != 0)
|
||||||
|
return 0;
|
||||||
|
return sc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fills the full event, including the Unicode codepoint the key types.
|
||||||
|
inline void getkey(montauk::abi::KeyEvent* out) {
|
||||||
|
syscall2(montauk::abi::SYS_GETKEY_EX, (uint64_t)out, sizeof(*out));
|
||||||
|
}
|
||||||
|
// Text keys arrive as UTF-8, one byte per call.
|
||||||
inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); }
|
inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); }
|
||||||
// Returns 0 rather than blocking when no character is pending.
|
// Returns 0 rather than blocking when no character is pending.
|
||||||
inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); }
|
inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); }
|
||||||
@@ -496,7 +513,8 @@ namespace montauk {
|
|||||||
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
|
||||||
}
|
}
|
||||||
inline int childio_writekey(int childPid, const montauk::abi::KeyEvent* key) {
|
inline int childio_writekey(int childPid, const montauk::abi::KeyEvent* key) {
|
||||||
return (int)syscall2(montauk::abi::SYS_CHILDIO_WRITEKEY, (uint64_t)childPid, (uint64_t)key);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITEKEY_EX, (uint64_t)childPid,
|
||||||
|
(uint64_t)key, sizeof(*key));
|
||||||
}
|
}
|
||||||
inline int childio_settermsz(int childPid, int cols, int rows) {
|
inline int childio_settermsz(int childPid, int cols, int rows) {
|
||||||
return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <montauk/config.h>
|
#include <montauk/config.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <bearssl_hash.h>
|
#include <bearssl_hash.h>
|
||||||
|
|
||||||
namespace montauk {
|
namespace montauk {
|
||||||
@@ -107,7 +108,7 @@ namespace user {
|
|||||||
bytes_to_hex(hash, 32, out_hex64);
|
bytes_to_hex(hash, 32, out_hex64);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool verify_password(const char* password, const char* salt_hex, const char* stored_hex64) {
|
inline bool verify_password_bytes(const char* password, const char* salt_hex, const char* stored_hex64) {
|
||||||
char computed[65];
|
char computed[65];
|
||||||
hash_password(password, salt_hex, computed);
|
hash_password(password, salt_hex, computed);
|
||||||
|
|
||||||
@@ -118,6 +119,26 @@ namespace user {
|
|||||||
return diff == 0;
|
return diff == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Passwords are hashed as UTF-8. Before the switch to UTF-8, password
|
||||||
|
// fields stored typed characters as Windows-1252 bytes, so a non-ASCII
|
||||||
|
// password set back then only matches its 1252 form; try that too.
|
||||||
|
inline bool verify_password(const char* password, const char* salt_hex, const char* stored_hex64) {
|
||||||
|
if (verify_password_bytes(password, salt_hex, stored_hex64)) return true;
|
||||||
|
|
||||||
|
char legacy[256];
|
||||||
|
int o = 0;
|
||||||
|
bool non_ascii = false;
|
||||||
|
for (const char* p = password; *p;) {
|
||||||
|
uint32_t cp = montauk::utf8::next(&p);
|
||||||
|
uint8_t b = montauk::utf8::to_cp1252(cp);
|
||||||
|
if (b == 0 || o >= (int)sizeof(legacy) - 1) return false;
|
||||||
|
if (b >= 0x80) non_ascii = true;
|
||||||
|
legacy[o++] = (char)b;
|
||||||
|
}
|
||||||
|
legacy[o] = '\0';
|
||||||
|
return non_ascii && verify_password_bytes(legacy, salt_hex, stored_hex64);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- User database I/O ----
|
// ---- User database I/O ----
|
||||||
|
|
||||||
inline int load_users(UserInfo* buf, int max) {
|
inline int load_users(UserInfo* buf, int max) {
|
||||||
|
|||||||
@@ -0,0 +1,399 @@
|
|||||||
|
/*
|
||||||
|
* utf8.h
|
||||||
|
* UTF-8 text encoding helpers with a Windows-1252 fallback
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// MontaukOS text is UTF-8. Before this it was single-byte Windows-1252, so
|
||||||
|
// older files (and anything else that is not valid UTF-8) still turn up. The
|
||||||
|
// decoder never fails: a byte that does not start a well-formed sequence is
|
||||||
|
// read as ONE Windows-1252 character. Valid UTF-8 and plain ASCII are
|
||||||
|
// unaffected, and legacy text keeps rendering exactly as it used to.
|
||||||
|
//
|
||||||
|
// Character boundaries follow the same rule, so cursor movement, deletion and
|
||||||
|
// truncation all agree with what the renderer draws.
|
||||||
|
|
||||||
|
namespace montauk::utf8 {
|
||||||
|
|
||||||
|
inline constexpr uint32_t REPLACEMENT = 0xFFFD;
|
||||||
|
inline constexpr int MAX_BYTES = 4;
|
||||||
|
|
||||||
|
// Windows-1252 byte -> Unicode codepoint. 0x80-0x9F carry the typographic
|
||||||
|
// punctuation; every other byte is identical to Latin-1 / Unicode.
|
||||||
|
inline constexpr uint32_t from_cp1252(uint8_t b) {
|
||||||
|
switch (b) {
|
||||||
|
case 0x80: return 0x20AC; // Euro Sign
|
||||||
|
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
|
||||||
|
case 0x83: return 0x0192; // Latin Small Letter F With Hook
|
||||||
|
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
|
||||||
|
case 0x85: return 0x2026; // Horizontal Ellipsis
|
||||||
|
case 0x86: return 0x2020; // Dagger
|
||||||
|
case 0x87: return 0x2021; // Double Dagger
|
||||||
|
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
|
||||||
|
case 0x89: return 0x2030; // Per Mille Sign
|
||||||
|
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
|
||||||
|
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
|
||||||
|
case 0x8C: return 0x0152; // Latin Capital Ligature OE
|
||||||
|
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
|
||||||
|
case 0x91: return 0x2018; // Left Single Quotation Mark
|
||||||
|
case 0x92: return 0x2019; // Right Single Quotation Mark
|
||||||
|
case 0x93: return 0x201C; // Left Double Quotation Mark
|
||||||
|
case 0x94: return 0x201D; // Right Double Quotation Mark
|
||||||
|
case 0x95: return 0x2022; // Bullet
|
||||||
|
case 0x96: return 0x2013; // En Dash
|
||||||
|
case 0x97: return 0x2014; // Em Dash
|
||||||
|
case 0x98: return 0x02DC; // Small Tilde
|
||||||
|
case 0x99: return 0x2122; // Trade Mark Sign
|
||||||
|
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
|
||||||
|
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
|
||||||
|
case 0x9C: return 0x0153; // Latin Small Ligature OE
|
||||||
|
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
|
||||||
|
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
|
||||||
|
default: return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicode codepoint -> Windows-1252 byte, or 0 if it has none. Used where a
|
||||||
|
// single byte is still required (PDF WinAnsi output, legacy byte tables).
|
||||||
|
inline constexpr uint8_t to_cp1252(uint32_t cp) {
|
||||||
|
if (cp < 0x80 || (cp >= 0xA0 && cp <= 0xFF)) return (uint8_t)cp;
|
||||||
|
for (int b = 0x80; b <= 0x9F; b++) {
|
||||||
|
uint32_t m = from_cp1252((uint8_t)b);
|
||||||
|
if (m != (uint32_t)b && m == cp) return (uint8_t)b;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length of the well-formed UTF-8 sequence starting at s (at most `avail`
|
||||||
|
// bytes, stopping early at a NUL), or 0 if it is not one. Rejects overlong
|
||||||
|
// forms, surrogates and values above U+10FFFF.
|
||||||
|
inline int sequence_length(const char* s, int avail, uint32_t* out_cp = nullptr) {
|
||||||
|
if (avail <= 0) return 0;
|
||||||
|
const uint8_t* u = (const uint8_t*)s;
|
||||||
|
uint8_t b0 = u[0];
|
||||||
|
if (b0 < 0x80) {
|
||||||
|
if (out_cp) *out_cp = b0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n;
|
||||||
|
uint32_t cp, min;
|
||||||
|
if (b0 >= 0xC2 && b0 <= 0xDF) { n = 2; cp = b0 & 0x1F; min = 0x80; }
|
||||||
|
else if (b0 >= 0xE0 && b0 <= 0xEF) { n = 3; cp = b0 & 0x0F; min = 0x800; }
|
||||||
|
else if (b0 >= 0xF0 && b0 <= 0xF4) { n = 4; cp = b0 & 0x07; min = 0x10000; }
|
||||||
|
else return 0;
|
||||||
|
|
||||||
|
if (n > avail) return 0;
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
uint8_t c = u[i];
|
||||||
|
if ((c & 0xC0) != 0x80) return 0; // also catches NUL
|
||||||
|
cp = (cp << 6) | (c & 0x3F);
|
||||||
|
}
|
||||||
|
if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return 0;
|
||||||
|
if (out_cp) *out_cp = cp;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode one character from a buffer of `avail` bytes. Returns the codepoint
|
||||||
|
// and stores the number of bytes consumed (always >= 1 when avail > 0).
|
||||||
|
inline uint32_t decode(const char* s, int avail, int* consumed) {
|
||||||
|
if (avail <= 0) { if (consumed) *consumed = 0; return 0; }
|
||||||
|
uint32_t cp;
|
||||||
|
int n = sequence_length(s, avail, &cp);
|
||||||
|
if (n == 0) {
|
||||||
|
n = 1;
|
||||||
|
cp = from_cp1252((uint8_t)s[0]);
|
||||||
|
}
|
||||||
|
if (consumed) *consumed = n;
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode one character from a NUL-terminated string and advance *p past it.
|
||||||
|
// Returns 0 (without advancing) at the terminator.
|
||||||
|
inline uint32_t next(const char** p) {
|
||||||
|
const char* s = *p;
|
||||||
|
if (!*s) return 0;
|
||||||
|
int n;
|
||||||
|
// A sequence never spans the terminator: sequence_length stops at NUL
|
||||||
|
// because NUL is not a continuation byte.
|
||||||
|
uint32_t cp = decode(s, MAX_BYTES, &n);
|
||||||
|
*p = s + n;
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode a codepoint as UTF-8 into out (room for 4 bytes, NOT terminated).
|
||||||
|
// Returns the byte count. Invalid codepoints encode U+FFFD.
|
||||||
|
inline int encode(uint32_t cp, char* out) {
|
||||||
|
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = REPLACEMENT;
|
||||||
|
uint8_t* o = (uint8_t*)out;
|
||||||
|
if (cp < 0x80) { o[0] = (uint8_t)cp; return 1; }
|
||||||
|
if (cp < 0x800) {
|
||||||
|
o[0] = (uint8_t)(0xC0 | (cp >> 6));
|
||||||
|
o[1] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (cp < 0x10000) {
|
||||||
|
o[0] = (uint8_t)(0xE0 | (cp >> 12));
|
||||||
|
o[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
o[2] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
o[0] = (uint8_t)(0xF0 | (cp >> 18));
|
||||||
|
o[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
|
||||||
|
o[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
o[3] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int encoded_length(uint32_t cp) {
|
||||||
|
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return 3; // U+FFFD
|
||||||
|
return cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== Case mapping ====
|
||||||
|
// Simple one-to-one case mapping for the scripts MontaukOS keyboard layouts
|
||||||
|
// cover: ASCII, Latin-1, Latin Extended-A, Greek and Cyrillic. Anything else
|
||||||
|
// is returned unchanged.
|
||||||
|
|
||||||
|
inline constexpr uint32_t to_upper(uint32_t c) {
|
||||||
|
if (c >= 'a' && c <= 'z') return c - 0x20;
|
||||||
|
if (c < 0x80) return c;
|
||||||
|
if (c >= 0xE0 && c <= 0xFE && c != 0xF7) return c - 0x20;
|
||||||
|
if (c == 0xFF) return 0x178;
|
||||||
|
if (c == 0xB5) return 0x39C; // micro -> Mu
|
||||||
|
// Latin Extended-A: pairs alternate upper/lower, except the two ranges
|
||||||
|
// where the alignment shifts by one (L-stroke..N-apostrophe, Z-acute..).
|
||||||
|
if (c == 0x131) return 'I'; // dotless i
|
||||||
|
if (c >= 0x100 && c <= 0x137 && c != 0x130) return (c & 1) ? c - 1 : c;
|
||||||
|
if (c >= 0x139 && c <= 0x148) return (c & 1) ? c : c - 1;
|
||||||
|
if (c >= 0x14A && c <= 0x177) return (c & 1) ? c - 1 : c;
|
||||||
|
if (c >= 0x179 && c <= 0x17E) return (c & 1) ? c : c - 1;
|
||||||
|
if (c == 0x17F) return 'S'; // long s
|
||||||
|
// Greek
|
||||||
|
if (c >= 0x3B1 && c <= 0x3C9 && c != 0x3C2) return c - 0x20;
|
||||||
|
if (c == 0x3C2) return 0x3A3; // final sigma
|
||||||
|
if (c == 0x3AC) return 0x386;
|
||||||
|
if (c >= 0x3AD && c <= 0x3AF) return c - 0x25;
|
||||||
|
if (c == 0x3CC) return 0x38C;
|
||||||
|
if (c == 0x3CD || c == 0x3CE) return c - 0x3F;
|
||||||
|
// Cyrillic
|
||||||
|
if (c >= 0x430 && c <= 0x44F) return c - 0x20;
|
||||||
|
if (c >= 0x450 && c <= 0x45F) return c - 0x50;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr uint32_t to_lower(uint32_t c) {
|
||||||
|
if (c >= 'A' && c <= 'Z') return c + 0x20;
|
||||||
|
if (c < 0x80) return c;
|
||||||
|
if (c >= 0xC0 && c <= 0xDE && c != 0xD7) return c + 0x20;
|
||||||
|
if (c == 0x178) return 0xFF;
|
||||||
|
if (c == 0x130) return 'i'; // dotted I
|
||||||
|
if (c >= 0x100 && c <= 0x137) return (c & 1) ? c : c + 1;
|
||||||
|
if (c >= 0x139 && c <= 0x148) return (c & 1) ? c + 1 : c;
|
||||||
|
if (c >= 0x14A && c <= 0x177) return (c & 1) ? c : c + 1;
|
||||||
|
if (c >= 0x179 && c <= 0x17E) return (c & 1) ? c + 1 : c;
|
||||||
|
if (c >= 0x391 && c <= 0x3A9 && c != 0x3A2) return c + 0x20;
|
||||||
|
if (c == 0x386) return 0x3AC;
|
||||||
|
if (c >= 0x388 && c <= 0x38A) return c + 0x25;
|
||||||
|
if (c == 0x38C) return 0x3CC;
|
||||||
|
if (c == 0x38E || c == 0x38F) return c + 0x3F;
|
||||||
|
if (c >= 0x410 && c <= 0x42F) return c + 0x20;
|
||||||
|
if (c >= 0x400 && c <= 0x40F) return c + 0x50;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if lower/upper are the two cases of one letter.
|
||||||
|
inline constexpr bool is_case_pair(uint32_t lower, uint32_t upper) {
|
||||||
|
return lower != upper && to_upper(lower) == upper && to_lower(upper) == lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text a key event types, as a codepoint (0 for control/non-text keys).
|
||||||
|
// KeyEvent::codepoint is filled by the kernel and by layout translation;
|
||||||
|
// events built by hand may leave it 0, so fall back to `ascii` (a
|
||||||
|
// Windows-1252 byte).
|
||||||
|
template <typename Key>
|
||||||
|
inline uint32_t key_codepoint(const Key& key) {
|
||||||
|
if (key.codepoint) {
|
||||||
|
uint32_t cp = key.codepoint;
|
||||||
|
return (cp < 0x20 || cp == 0x7F || (cp >= 0x80 && cp < 0xA0)) ? 0 : cp;
|
||||||
|
}
|
||||||
|
uint8_t b = (uint8_t)key.ascii;
|
||||||
|
if (b < 0x20 || b == 0x7F) return 0;
|
||||||
|
return from_cp1252(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same, encoded as UTF-8 into out (4 bytes). Returns the length, 0 if none.
|
||||||
|
template <typename Key>
|
||||||
|
inline int key_utf8(const Key& key, char* out) {
|
||||||
|
uint32_t cp = key_codepoint(key);
|
||||||
|
return cp ? encode(cp, out) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte offset of the character after the one at `pos` (len = buffer length).
|
||||||
|
inline int next_boundary(const char* s, int len, int pos) {
|
||||||
|
if (pos >= len) return len;
|
||||||
|
if (pos < 0) return 0;
|
||||||
|
int n;
|
||||||
|
decode(s + pos, len - pos, &n);
|
||||||
|
return pos + n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte offset of the character before `pos`. A run of continuation bytes is
|
||||||
|
// one character only if it completes a well-formed sequence ending exactly at
|
||||||
|
// pos; otherwise the previous byte stands alone (Windows-1252 fallback).
|
||||||
|
inline int prev_boundary(const char* s, int pos) {
|
||||||
|
if (pos <= 0) return 0;
|
||||||
|
for (int back = 2; back <= MAX_BYTES && back <= pos; back++) {
|
||||||
|
if (((uint8_t)s[pos - back + 1] & 0xC0) != 0x80) break;
|
||||||
|
int start = pos - back;
|
||||||
|
if (sequence_length(s + start, back) == back) return start;
|
||||||
|
}
|
||||||
|
return pos - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap `pos` back onto a character boundary. Use before truncating a buffer
|
||||||
|
// or splitting at an arbitrary byte offset.
|
||||||
|
inline int floor_boundary(const char* s, int len, int pos) {
|
||||||
|
if (pos <= 0) return 0;
|
||||||
|
if (pos >= len) return len;
|
||||||
|
if (((uint8_t)s[pos] & 0xC0) != 0x80) return pos;
|
||||||
|
// pos is a continuation byte: it is inside a character only if a
|
||||||
|
// well-formed sequence starting up to 3 bytes earlier covers it.
|
||||||
|
for (int back = 1; back < MAX_BYTES && back <= pos; back++) {
|
||||||
|
int start = pos - back;
|
||||||
|
int n = sequence_length(s + start, len - start);
|
||||||
|
if (n > back) return start;
|
||||||
|
if (((uint8_t)s[start] & 0xC0) != 0x80) break;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of characters in a NUL-terminated string.
|
||||||
|
inline int count(const char* s) {
|
||||||
|
int n = 0;
|
||||||
|
while (*s) { next(&s); n++; }
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of characters in the first `len` bytes.
|
||||||
|
inline int count(const char* s, int len) {
|
||||||
|
int n = 0, pos = 0;
|
||||||
|
while (pos < len) { pos = next_boundary(s, len, pos); n++; }
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte offset of the `index`th character (clamped to len).
|
||||||
|
inline int offset_of(const char* s, int len, int index) {
|
||||||
|
int pos = 0;
|
||||||
|
while (index-- > 0 && pos < len) pos = next_boundary(s, len, pos);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if the buffer is entirely well-formed UTF-8.
|
||||||
|
inline bool valid(const char* s, int len) {
|
||||||
|
for (int pos = 0; pos < len;) {
|
||||||
|
int n = sequence_length(s + pos, len - pos);
|
||||||
|
if (n == 0) return false;
|
||||||
|
pos += n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite a buffer as well-formed UTF-8 (each stray byte becomes the
|
||||||
|
// character the Windows-1252 fallback would display). Returns the output
|
||||||
|
// length, truncated on a character boundary to fit cap - 1, NUL-terminated.
|
||||||
|
inline int normalize(const char* in, int len, char* out, int cap) {
|
||||||
|
if (cap <= 0) return 0;
|
||||||
|
int o = 0;
|
||||||
|
for (int pos = 0; pos < len;) {
|
||||||
|
int n;
|
||||||
|
uint32_t cp = decode(in + pos, len - pos, &n);
|
||||||
|
int need = encoded_length(cp);
|
||||||
|
if (o + need > cap - 1) break;
|
||||||
|
o += encode(cp, out + o);
|
||||||
|
pos += n;
|
||||||
|
}
|
||||||
|
out[o] = '\0';
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy src into dst (cap bytes incl. NUL) without splitting a character.
|
||||||
|
inline int copy(char* dst, int cap, const char* src) {
|
||||||
|
if (cap <= 0) return 0;
|
||||||
|
int len = 0;
|
||||||
|
while (src[len]) len++;
|
||||||
|
int n = len < cap - 1 ? len : floor_boundary(src, len, cap - 1);
|
||||||
|
for (int i = 0; i < n; i++) dst[i] = src[i];
|
||||||
|
dst[n] = '\0';
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== Editing a NUL-terminated buffer with a byte cursor ====
|
||||||
|
// `len` is the current length, `cap` the buffer size including the NUL. The
|
||||||
|
// cursor is a byte offset that is always kept on a character boundary.
|
||||||
|
|
||||||
|
// Insert n bytes at *cursor. Returns false (buffer unchanged) if they don't fit.
|
||||||
|
inline bool insert(char* buf, int* len, int cap, int* cursor,
|
||||||
|
const char* s, int n) {
|
||||||
|
if (n <= 0 || *len + n >= cap) return false;
|
||||||
|
int at = *cursor < 0 ? 0 : (*cursor > *len ? *len : *cursor);
|
||||||
|
for (int i = *len; i >= at; i--) buf[i + n] = buf[i]; // moves the NUL
|
||||||
|
for (int i = 0; i < n; i++) buf[at + i] = s[i];
|
||||||
|
*len += n;
|
||||||
|
*cursor = at + n;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the text a key event types. Returns false if it types nothing or
|
||||||
|
// does not fit.
|
||||||
|
template <typename Key>
|
||||||
|
inline bool insert_key(char* buf, int* len, int cap, int* cursor, const Key& key) {
|
||||||
|
char t[MAX_BYTES];
|
||||||
|
int n = key_utf8(key, t);
|
||||||
|
return n > 0 && insert(buf, len, cap, cursor, t, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the bytes [start, end).
|
||||||
|
inline void erase(char* buf, int* len, int start, int end) {
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
if (end > *len) end = *len;
|
||||||
|
if (start >= end) return;
|
||||||
|
int n = end - start;
|
||||||
|
for (int i = start; i + n <= *len; i++) buf[i] = buf[i + n]; // moves the NUL
|
||||||
|
*len -= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backspace: delete the character before the cursor.
|
||||||
|
inline bool erase_before(char* buf, int* len, int* cursor) {
|
||||||
|
if (*cursor <= 0) return false;
|
||||||
|
int start = prev_boundary(buf, *cursor);
|
||||||
|
erase(buf, len, start, *cursor);
|
||||||
|
*cursor = start;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete: remove the character at the cursor.
|
||||||
|
inline bool erase_at(char* buf, int* len, int cursor) {
|
||||||
|
if (cursor >= *len) return false;
|
||||||
|
erase(buf, len, cursor, next_boundary(buf, *len, cursor));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append typed text at the end (simple line inputs without a cursor).
|
||||||
|
template <typename Key>
|
||||||
|
inline bool append_key(char* buf, int* len, int cap, const Key& key) {
|
||||||
|
int cursor = *len;
|
||||||
|
return insert_key(buf, len, cap, &cursor, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backspace for append-only inputs: drop the last character.
|
||||||
|
inline bool erase_last(char* buf, int* len) {
|
||||||
|
int cursor = *len;
|
||||||
|
return erase_before(buf, len, &cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace montauk::utf8
|
||||||
@@ -1712,7 +1712,10 @@ static int _stdin_pump(int block) {
|
|||||||
for (;;) {
|
for (;;) {
|
||||||
int c = block ? (int)_mtk_syscall0(SYS_GETCHAR)
|
int c = block ? (int)_mtk_syscall0(SYS_GETCHAR)
|
||||||
: (int)_mtk_syscall0(SYS_GETCHAR_NB);
|
: (int)_mtk_syscall0(SYS_GETCHAR_NB);
|
||||||
if (c <= 0) {
|
/* The syscall returns a sign-extended char: bytes >= 0x80 arrive
|
||||||
|
negative and are real keys, only 0 means "nothing". */
|
||||||
|
if (c < 0) c &= 0xFF;
|
||||||
|
if (c == 0) {
|
||||||
if (block) continue;
|
if (block) continue;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -1721,12 +1724,26 @@ static int _stdin_pump(int block) {
|
|||||||
|
|
||||||
if (c == '\b' || c == 127) {
|
if (c == '\b' || c == 127) {
|
||||||
if (_stdin_line_len > 0) {
|
if (_stdin_line_len > 0) {
|
||||||
|
/* Drop the whole UTF-8 character: its continuation bytes,
|
||||||
|
then the lead byte. One terminal cell either way. */
|
||||||
|
while (_stdin_line_len > 1 &&
|
||||||
|
((unsigned char)_stdin_linebuf[_stdin_line_len - 1] & 0xC0) == 0x80)
|
||||||
|
_stdin_line_len--;
|
||||||
_stdin_line_len--;
|
_stdin_line_len--;
|
||||||
_stdin_echo_char('\b');
|
_stdin_echo_char('\b');
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bytes >= 0x80 are UTF-8: the kernel encodes typed characters, and
|
||||||
|
piped input is UTF-8 already. They are stored and echoed as-is. */
|
||||||
|
if (c >= 0x80) {
|
||||||
|
if (_stdin_line_len + 2 >= sizeof(_stdin_linebuf)) continue;
|
||||||
|
_stdin_linebuf[_stdin_line_len++] = (char)c;
|
||||||
|
_stdin_echo_char((char)c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
_stdin_linebuf[_stdin_line_len++] = (char)c;
|
_stdin_linebuf[_stdin_line_len++] = (char)c;
|
||||||
_stdin_echo_char((char)c);
|
_stdin_echo_char((char)c);
|
||||||
|
|||||||
@@ -466,13 +466,13 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
if (g_game.game_over)
|
if (g_game.game_over)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (key.scancode == 0x4B || key.ascii == 'a' || key.ascii == 'A')
|
if (montauk::nav_scancode(key) == 0x4B || key.ascii == 'a' || key.ascii == 'A')
|
||||||
return apply_move(DIR_LEFT);
|
return apply_move(DIR_LEFT);
|
||||||
if (key.scancode == 0x4D || key.ascii == 'd' || key.ascii == 'D')
|
if (montauk::nav_scancode(key) == 0x4D || key.ascii == 'd' || key.ascii == 'D')
|
||||||
return apply_move(DIR_RIGHT);
|
return apply_move(DIR_RIGHT);
|
||||||
if (key.scancode == 0x48 || key.ascii == 'w' || key.ascii == 'W')
|
if (montauk::nav_scancode(key) == 0x48 || key.ascii == 'w' || key.ascii == 'W')
|
||||||
return apply_move(DIR_UP);
|
return apply_move(DIR_UP);
|
||||||
if (key.scancode == 0x50 || key.ascii == 's' || key.ascii == 'S')
|
if (montauk::nav_scancode(key) == 0x50 || key.ascii == 's' || key.ascii == 'S')
|
||||||
return apply_move(DIR_DOWN);
|
return apply_move(DIR_DOWN);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -469,12 +469,12 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
g_win.closed = true;
|
g_win.closed = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4D || key.ascii == '+' || key.ascii == '=') {
|
if (montauk::nav_scancode(key) == 0x4D || key.ascii == '+' || key.ascii == '=') {
|
||||||
if (g_master_muted) toggle_master_mute();
|
if (g_master_muted) toggle_master_mute();
|
||||||
apply_master_volume(g_master_vol + 5);
|
apply_master_volume(g_master_vol + 5);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4B || key.ascii == '-') {
|
if (montauk::nav_scancode(key) == 0x4B || key.ascii == '-') {
|
||||||
if (g_master_muted) toggle_master_mute();
|
if (g_master_muted) toggle_master_mute();
|
||||||
apply_master_volume(g_master_vol - 5);
|
apply_master_volume(g_master_vol - 5);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ static void draw_text_fit(Canvas& c, int x, int y,
|
|||||||
out++;
|
out++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out = montauk::utf8::floor_boundary(text, montauk::slen(text), out);
|
||||||
|
|
||||||
if (out <= 0) return;
|
if (out <= 0) return;
|
||||||
buf[out] = '.';
|
buf[out] = '.';
|
||||||
buf[out + 1] = '.';
|
buf[out + 1] = '.';
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ static void fit_text(char* out, int outCap, const char* text, int maxWidth) {
|
|||||||
if (text_width(out) + text_width(ellipsis) > maxWidth) break;
|
if (text_width(out) + text_width(ellipsis) > maxWidth) break;
|
||||||
len++;
|
len++;
|
||||||
}
|
}
|
||||||
|
len = montauk::utf8::floor_boundary(text, montauk::slen(text), len);
|
||||||
out[len++] = '.';
|
out[len++] = '.';
|
||||||
out[len++] = '.';
|
out[len++] = '.';
|
||||||
out[len++] = '.';
|
out[len++] = '.';
|
||||||
@@ -798,8 +799,8 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
int current = *selected_for_tab();
|
int current = *selected_for_tab();
|
||||||
if (key.scancode == 0x48) { select_index(current - 1); return true; } // Up
|
if (montauk::nav_scancode(key) == 0x48) { select_index(current - 1); return true; } // Up
|
||||||
if (key.scancode == 0x50) { select_index(current + 1); return true; } // Down
|
if (montauk::nav_scancode(key) == 0x50) { select_index(current + 1); return true; } // Down
|
||||||
if (key.ascii == 'r' || key.ascii == 'R') {
|
if (key.ascii == 'r' || key.ascii == 'R') {
|
||||||
reload();
|
reload();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+204
-150
@@ -5,13 +5,9 @@
|
|||||||
* Browse curated sets of characters, click (or use the keyboard) to select,
|
* Browse curated sets of characters, click (or use the keyboard) to select,
|
||||||
* and copy to the system clipboard for pasting into other apps.
|
* and copy to the system clipboard for pasting into other apps.
|
||||||
*
|
*
|
||||||
* Character model: MontaukOS's GUI text stack is single-byte (Windows-1252,
|
* Character model: MontaukOS text is UTF-8. Each tile is a Unicode codepoint
|
||||||
* with the 0x80-0x9F block mapped to typographic punctuation in
|
* and Copy puts its UTF-8 encoding on the clipboard. The sets are limited to
|
||||||
* gui::decode_single_byte_codepoint). The glyph cache only holds codepoints
|
* characters the system font (Roboto) actually has a glyph for.
|
||||||
* 0-255, so that byte range is exactly the set of glyphs the system font can
|
|
||||||
* render. We therefore present that set and copy the *raw byte* to the
|
|
||||||
* clipboard: it renders identically here and in every other Montauk app
|
|
||||||
* (copying multi-byte UTF-8 would render as mojibake in the single-byte stack).
|
|
||||||
*
|
*
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
@@ -37,7 +33,7 @@ using namespace gui;
|
|||||||
// ==== Character data ====
|
// ==== Character data ====
|
||||||
|
|
||||||
struct Glyph {
|
struct Glyph {
|
||||||
uint8_t byte; // Windows-1252 byte value (what gets copied / rendered)
|
uint32_t cp; // Unicode codepoint (copied as UTF-8)
|
||||||
const char* name; // human-readable description
|
const char* name; // human-readable description
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,141 +45,194 @@ struct CharSet {
|
|||||||
|
|
||||||
// Accented Latin letters and ligatures (things you can't easily type).
|
// Accented Latin letters and ligatures (things you can't easily type).
|
||||||
static const Glyph LATIN[] = {
|
static const Glyph LATIN[] = {
|
||||||
{0xC0, "Latin Capital Letter A With Grave"},
|
{0x00C0, "Latin Capital Letter A With Grave"},
|
||||||
{0xC1, "Latin Capital Letter A With Acute"},
|
{0x00C1, "Latin Capital Letter A With Acute"},
|
||||||
{0xC2, "Latin Capital Letter A With Circumflex"},
|
{0x00C2, "Latin Capital Letter A With Circumflex"},
|
||||||
{0xC3, "Latin Capital Letter A With Tilde"},
|
{0x00C3, "Latin Capital Letter A With Tilde"},
|
||||||
{0xC4, "Latin Capital Letter A With Diaeresis"},
|
{0x00C4, "Latin Capital Letter A With Diaeresis"},
|
||||||
{0xC5, "Latin Capital Letter A With Ring Above"},
|
{0x00C5, "Latin Capital Letter A With Ring Above"},
|
||||||
{0xC6, "Latin Capital Letter AE"},
|
{0x00C6, "Latin Capital Letter AE"},
|
||||||
{0xC7, "Latin Capital Letter C With Cedilla"},
|
{0x00C7, "Latin Capital Letter C With Cedilla"},
|
||||||
{0xC8, "Latin Capital Letter E With Grave"},
|
{0x00C8, "Latin Capital Letter E With Grave"},
|
||||||
{0xC9, "Latin Capital Letter E With Acute"},
|
{0x00C9, "Latin Capital Letter E With Acute"},
|
||||||
{0xCA, "Latin Capital Letter E With Circumflex"},
|
{0x00CA, "Latin Capital Letter E With Circumflex"},
|
||||||
{0xCB, "Latin Capital Letter E With Diaeresis"},
|
{0x00CB, "Latin Capital Letter E With Diaeresis"},
|
||||||
{0xCC, "Latin Capital Letter I With Grave"},
|
{0x00CC, "Latin Capital Letter I With Grave"},
|
||||||
{0xCD, "Latin Capital Letter I With Acute"},
|
{0x00CD, "Latin Capital Letter I With Acute"},
|
||||||
{0xCE, "Latin Capital Letter I With Circumflex"},
|
{0x00CE, "Latin Capital Letter I With Circumflex"},
|
||||||
{0xCF, "Latin Capital Letter I With Diaeresis"},
|
{0x00CF, "Latin Capital Letter I With Diaeresis"},
|
||||||
{0xD0, "Latin Capital Letter Eth"},
|
{0x00D0, "Latin Capital Letter Eth"},
|
||||||
{0xD1, "Latin Capital Letter N With Tilde"},
|
{0x00D1, "Latin Capital Letter N With Tilde"},
|
||||||
{0xD2, "Latin Capital Letter O With Grave"},
|
{0x00D2, "Latin Capital Letter O With Grave"},
|
||||||
{0xD3, "Latin Capital Letter O With Acute"},
|
{0x00D3, "Latin Capital Letter O With Acute"},
|
||||||
{0xD4, "Latin Capital Letter O With Circumflex"},
|
{0x00D4, "Latin Capital Letter O With Circumflex"},
|
||||||
{0xD5, "Latin Capital Letter O With Tilde"},
|
{0x00D5, "Latin Capital Letter O With Tilde"},
|
||||||
{0xD6, "Latin Capital Letter O With Diaeresis"},
|
{0x00D6, "Latin Capital Letter O With Diaeresis"},
|
||||||
{0xD8, "Latin Capital Letter O With Stroke"},
|
{0x00D8, "Latin Capital Letter O With Stroke"},
|
||||||
{0xD9, "Latin Capital Letter U With Grave"},
|
{0x00D9, "Latin Capital Letter U With Grave"},
|
||||||
{0xDA, "Latin Capital Letter U With Acute"},
|
{0x00DA, "Latin Capital Letter U With Acute"},
|
||||||
{0xDB, "Latin Capital Letter U With Circumflex"},
|
{0x00DB, "Latin Capital Letter U With Circumflex"},
|
||||||
{0xDC, "Latin Capital Letter U With Diaeresis"},
|
{0x00DC, "Latin Capital Letter U With Diaeresis"},
|
||||||
{0xDD, "Latin Capital Letter Y With Acute"},
|
{0x00DD, "Latin Capital Letter Y With Acute"},
|
||||||
{0xDE, "Latin Capital Letter Thorn"},
|
{0x00DE, "Latin Capital Letter Thorn"},
|
||||||
{0x8A, "Latin Capital Letter S With Caron"},
|
{0x0160, "Latin Capital Letter S With Caron"},
|
||||||
{0x8C, "Latin Capital Ligature OE"},
|
{0x0152, "Latin Capital Ligature OE"},
|
||||||
{0x8E, "Latin Capital Letter Z With Caron"},
|
{0x017D, "Latin Capital Letter Z With Caron"},
|
||||||
{0x9F, "Latin Capital Letter Y With Diaeresis"},
|
{0x0178, "Latin Capital Letter Y With Diaeresis"},
|
||||||
{0xDF, "Latin Small Letter Sharp S"},
|
{0x00DF, "Latin Small Letter Sharp S"},
|
||||||
{0xE0, "Latin Small Letter A With Grave"},
|
{0x00E0, "Latin Small Letter A With Grave"},
|
||||||
{0xE1, "Latin Small Letter A With Acute"},
|
{0x00E1, "Latin Small Letter A With Acute"},
|
||||||
{0xE2, "Latin Small Letter A With Circumflex"},
|
{0x00E2, "Latin Small Letter A With Circumflex"},
|
||||||
{0xE3, "Latin Small Letter A With Tilde"},
|
{0x00E3, "Latin Small Letter A With Tilde"},
|
||||||
{0xE4, "Latin Small Letter A With Diaeresis"},
|
{0x00E4, "Latin Small Letter A With Diaeresis"},
|
||||||
{0xE5, "Latin Small Letter A With Ring Above"},
|
{0x00E5, "Latin Small Letter A With Ring Above"},
|
||||||
{0xE6, "Latin Small Letter AE"},
|
{0x00E6, "Latin Small Letter AE"},
|
||||||
{0xE7, "Latin Small Letter C With Cedilla"},
|
{0x00E7, "Latin Small Letter C With Cedilla"},
|
||||||
{0xE8, "Latin Small Letter E With Grave"},
|
{0x00E8, "Latin Small Letter E With Grave"},
|
||||||
{0xE9, "Latin Small Letter E With Acute"},
|
{0x00E9, "Latin Small Letter E With Acute"},
|
||||||
{0xEA, "Latin Small Letter E With Circumflex"},
|
{0x00EA, "Latin Small Letter E With Circumflex"},
|
||||||
{0xEB, "Latin Small Letter E With Diaeresis"},
|
{0x00EB, "Latin Small Letter E With Diaeresis"},
|
||||||
{0xEC, "Latin Small Letter I With Grave"},
|
{0x00EC, "Latin Small Letter I With Grave"},
|
||||||
{0xED, "Latin Small Letter I With Acute"},
|
{0x00ED, "Latin Small Letter I With Acute"},
|
||||||
{0xEE, "Latin Small Letter I With Circumflex"},
|
{0x00EE, "Latin Small Letter I With Circumflex"},
|
||||||
{0xEF, "Latin Small Letter I With Diaeresis"},
|
{0x00EF, "Latin Small Letter I With Diaeresis"},
|
||||||
{0xF0, "Latin Small Letter Eth"},
|
{0x00F0, "Latin Small Letter Eth"},
|
||||||
{0xF1, "Latin Small Letter N With Tilde"},
|
{0x00F1, "Latin Small Letter N With Tilde"},
|
||||||
{0xF2, "Latin Small Letter O With Grave"},
|
{0x00F2, "Latin Small Letter O With Grave"},
|
||||||
{0xF3, "Latin Small Letter O With Acute"},
|
{0x00F3, "Latin Small Letter O With Acute"},
|
||||||
{0xF4, "Latin Small Letter O With Circumflex"},
|
{0x00F4, "Latin Small Letter O With Circumflex"},
|
||||||
{0xF5, "Latin Small Letter O With Tilde"},
|
{0x00F5, "Latin Small Letter O With Tilde"},
|
||||||
{0xF6, "Latin Small Letter O With Diaeresis"},
|
{0x00F6, "Latin Small Letter O With Diaeresis"},
|
||||||
{0xF8, "Latin Small Letter O With Stroke"},
|
{0x00F8, "Latin Small Letter O With Stroke"},
|
||||||
{0xF9, "Latin Small Letter U With Grave"},
|
{0x00F9, "Latin Small Letter U With Grave"},
|
||||||
{0xFA, "Latin Small Letter U With Acute"},
|
{0x00FA, "Latin Small Letter U With Acute"},
|
||||||
{0xFB, "Latin Small Letter U With Circumflex"},
|
{0x00FB, "Latin Small Letter U With Circumflex"},
|
||||||
{0xFC, "Latin Small Letter U With Diaeresis"},
|
{0x00FC, "Latin Small Letter U With Diaeresis"},
|
||||||
{0xFD, "Latin Small Letter Y With Acute"},
|
{0x00FD, "Latin Small Letter Y With Acute"},
|
||||||
{0xFE, "Latin Small Letter Thorn"},
|
{0x00FE, "Latin Small Letter Thorn"},
|
||||||
{0xFF, "Latin Small Letter Y With Diaeresis"},
|
{0x00FF, "Latin Small Letter Y With Diaeresis"},
|
||||||
{0x9A, "Latin Small Letter S With Caron"},
|
{0x0161, "Latin Small Letter S With Caron"},
|
||||||
{0x9C, "Latin Small Ligature OE"},
|
{0x0153, "Latin Small Ligature OE"},
|
||||||
{0x9E, "Latin Small Letter Z With Caron"},
|
{0x017E, "Latin Small Letter Z With Caron"},
|
||||||
{0x83, "Latin Small Letter F With Hook"},
|
{0x0192, "Latin Small Letter F With Hook"},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typographic punctuation.
|
// Typographic punctuation.
|
||||||
static const Glyph PUNCT[] = {
|
static const Glyph PUNCT[] = {
|
||||||
{0xA1, "Inverted Exclamation Mark"},
|
{0x00A1, "Inverted Exclamation Mark"},
|
||||||
{0xBF, "Inverted Question Mark"},
|
{0x00BF, "Inverted Question Mark"},
|
||||||
{0x91, "Left Single Quotation Mark"},
|
{0x2018, "Left Single Quotation Mark"},
|
||||||
{0x92, "Right Single Quotation Mark"},
|
{0x2019, "Right Single Quotation Mark"},
|
||||||
{0x93, "Left Double Quotation Mark"},
|
{0x201C, "Left Double Quotation Mark"},
|
||||||
{0x94, "Right Double Quotation Mark"},
|
{0x201D, "Right Double Quotation Mark"},
|
||||||
{0x82, "Single Low-9 Quotation Mark"},
|
{0x201A, "Single Low-9 Quotation Mark"},
|
||||||
{0x84, "Double Low-9 Quotation Mark"},
|
{0x201E, "Double Low-9 Quotation Mark"},
|
||||||
{0xAB, "Left-Pointing Double Angle Quotation Mark"},
|
{0x00AB, "Left-Pointing Double Angle Quotation Mark"},
|
||||||
{0xBB, "Right-Pointing Double Angle Quotation Mark"},
|
{0x00BB, "Right-Pointing Double Angle Quotation Mark"},
|
||||||
{0x8B, "Single Left-Pointing Angle Quotation Mark"},
|
{0x2039, "Single Left-Pointing Angle Quotation Mark"},
|
||||||
{0x9B, "Single Right-Pointing Angle Quotation Mark"},
|
{0x203A, "Single Right-Pointing Angle Quotation Mark"},
|
||||||
{0x96, "En Dash"},
|
{0x2013, "En Dash"},
|
||||||
{0x97, "Em Dash"},
|
{0x2014, "Em Dash"},
|
||||||
{0x85, "Horizontal Ellipsis"},
|
{0x2026, "Horizontal Ellipsis"},
|
||||||
{0x95, "Bullet"},
|
{0x2022, "Bullet"},
|
||||||
{0xB7, "Middle Dot"},
|
{0x00B7, "Middle Dot"},
|
||||||
{0x86, "Dagger"},
|
{0x2020, "Dagger"},
|
||||||
{0x87, "Double Dagger"},
|
{0x2021, "Double Dagger"},
|
||||||
{0xB6, "Pilcrow Sign"},
|
{0x00B6, "Pilcrow Sign"},
|
||||||
{0xA7, "Section Sign"},
|
{0x00A7, "Section Sign"},
|
||||||
{0x89, "Per Mille Sign"},
|
{0x2030, "Per Mille Sign"},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Currency symbols.
|
// Currency symbols.
|
||||||
static const Glyph CURRENCY[] = {
|
static const Glyph CURRENCY[] = {
|
||||||
{0x24, "Dollar Sign"},
|
{0x0024, "Dollar Sign"},
|
||||||
{0xA2, "Cent Sign"},
|
{0x00A2, "Cent Sign"},
|
||||||
{0xA3, "Pound Sign"},
|
{0x00A3, "Pound Sign"},
|
||||||
{0xA5, "Yen Sign"},
|
{0x00A5, "Yen Sign"},
|
||||||
{0x80, "Euro Sign"},
|
{0x20AC, "Euro Sign"},
|
||||||
{0xA4, "Currency Sign"},
|
{0x00A4, "Currency Sign"},
|
||||||
{0x83, "Latin Small Letter F With Hook (Florin)"},
|
{0x0192, "Latin Small Letter F With Hook (Florin)"},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Math, technical, and miscellaneous symbols.
|
// Math, technical, and miscellaneous symbols.
|
||||||
static const Glyph SYMBOLS[] = {
|
static const Glyph SYMBOLS[] = {
|
||||||
{0xA9, "Copyright Sign"},
|
{0x00A9, "Copyright Sign"},
|
||||||
{0xAE, "Registered Sign"},
|
{0x00AE, "Registered Sign"},
|
||||||
{0x99, "Trade Mark Sign"},
|
{0x2122, "Trade Mark Sign"},
|
||||||
{0xB0, "Degree Sign"},
|
{0x00B0, "Degree Sign"},
|
||||||
{0xB1, "Plus-Minus Sign"},
|
{0x00B1, "Plus-Minus Sign"},
|
||||||
{0xD7, "Multiplication Sign"},
|
{0x00D7, "Multiplication Sign"},
|
||||||
{0xF7, "Division Sign"},
|
{0x00F7, "Division Sign"},
|
||||||
{0xB5, "Micro Sign"},
|
{0x00B5, "Micro Sign"},
|
||||||
{0xAC, "Not Sign"},
|
{0x00AC, "Not Sign"},
|
||||||
{0xBC, "Vulgar Fraction One Quarter"},
|
{0x00BC, "Vulgar Fraction One Quarter"},
|
||||||
{0xBD, "Vulgar Fraction One Half"},
|
{0x00BD, "Vulgar Fraction One Half"},
|
||||||
{0xBE, "Vulgar Fraction Three Quarters"},
|
{0x00BE, "Vulgar Fraction Three Quarters"},
|
||||||
{0xB9, "Superscript One"},
|
{0x00B9, "Superscript One"},
|
||||||
{0xB2, "Superscript Two"},
|
{0x00B2, "Superscript Two"},
|
||||||
{0xB3, "Superscript Three"},
|
{0x00B3, "Superscript Three"},
|
||||||
{0xAA, "Feminine Ordinal Indicator"},
|
{0x00AA, "Feminine Ordinal Indicator"},
|
||||||
{0xBA, "Masculine Ordinal Indicator"},
|
{0x00BA, "Masculine Ordinal Indicator"},
|
||||||
{0xA6, "Broken Bar"},
|
{0x00A6, "Broken Bar"},
|
||||||
{0xB4, "Acute Accent"},
|
{0x00B4, "Acute Accent"},
|
||||||
{0xA8, "Diaeresis"},
|
{0x00A8, "Diaeresis"},
|
||||||
{0xAF, "Macron"},
|
{0x00AF, "Macron"},
|
||||||
{0xB8, "Cedilla"},
|
{0x00B8, "Cedilla"},
|
||||||
{0x88, "Modifier Letter Circumflex Accent"},
|
{0x02C6, "Modifier Letter Circumflex Accent"},
|
||||||
{0x98, "Small Tilde"},
|
{0x02DC, "Small Tilde"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Beyond the old single-byte range: Greek (Roboto covers the whole block).
|
||||||
|
static const Glyph GREEK[] = {
|
||||||
|
{0x0391, "Greek Capital Letter Alpha"},
|
||||||
|
{0x0392, "Greek Capital Letter Beta"},
|
||||||
|
{0x0393, "Greek Capital Letter Gamma"},
|
||||||
|
{0x0394, "Greek Capital Letter Delta"},
|
||||||
|
{0x0395, "Greek Capital Letter Epsilon"},
|
||||||
|
{0x0396, "Greek Capital Letter Zeta"},
|
||||||
|
{0x0397, "Greek Capital Letter Eta"},
|
||||||
|
{0x0398, "Greek Capital Letter Theta"},
|
||||||
|
{0x0399, "Greek Capital Letter Iota"},
|
||||||
|
{0x039A, "Greek Capital Letter Kappa"},
|
||||||
|
{0x039B, "Greek Capital Letter Lamda"},
|
||||||
|
{0x039C, "Greek Capital Letter Mu"},
|
||||||
|
{0x039D, "Greek Capital Letter Nu"},
|
||||||
|
{0x039E, "Greek Capital Letter Xi"},
|
||||||
|
{0x039F, "Greek Capital Letter Omicron"},
|
||||||
|
{0x03A0, "Greek Capital Letter Pi"},
|
||||||
|
{0x03A1, "Greek Capital Letter Rho"},
|
||||||
|
{0x03A3, "Greek Capital Letter Sigma"},
|
||||||
|
{0x03A4, "Greek Capital Letter Tau"},
|
||||||
|
{0x03A5, "Greek Capital Letter Upsilon"},
|
||||||
|
{0x03A6, "Greek Capital Letter Phi"},
|
||||||
|
{0x03A7, "Greek Capital Letter Chi"},
|
||||||
|
{0x03A8, "Greek Capital Letter Psi"},
|
||||||
|
{0x03A9, "Greek Capital Letter Omega"},
|
||||||
|
{0x03B1, "Greek Small Letter Alpha"},
|
||||||
|
{0x03B2, "Greek Small Letter Beta"},
|
||||||
|
{0x03B3, "Greek Small Letter Gamma"},
|
||||||
|
{0x03B4, "Greek Small Letter Delta"},
|
||||||
|
{0x03B5, "Greek Small Letter Epsilon"},
|
||||||
|
{0x03B6, "Greek Small Letter Zeta"},
|
||||||
|
{0x03B7, "Greek Small Letter Eta"},
|
||||||
|
{0x03B8, "Greek Small Letter Theta"},
|
||||||
|
{0x03B9, "Greek Small Letter Iota"},
|
||||||
|
{0x03BA, "Greek Small Letter Kappa"},
|
||||||
|
{0x03BB, "Greek Small Letter Lamda"},
|
||||||
|
{0x03BC, "Greek Small Letter Mu"},
|
||||||
|
{0x03BD, "Greek Small Letter Nu"},
|
||||||
|
{0x03BE, "Greek Small Letter Xi"},
|
||||||
|
{0x03BF, "Greek Small Letter Omicron"},
|
||||||
|
{0x03C0, "Greek Small Letter Pi"},
|
||||||
|
{0x03C1, "Greek Small Letter Rho"},
|
||||||
|
{0x03C2, "Greek Small Letter Final Sigma"},
|
||||||
|
{0x03C3, "Greek Small Letter Sigma"},
|
||||||
|
{0x03C4, "Greek Small Letter Tau"},
|
||||||
|
{0x03C5, "Greek Small Letter Upsilon"},
|
||||||
|
{0x03C6, "Greek Small Letter Phi"},
|
||||||
|
{0x03C7, "Greek Small Letter Chi"},
|
||||||
|
{0x03C8, "Greek Small Letter Psi"},
|
||||||
|
{0x03C9, "Greek Small Letter Omega"},
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SET(arr, label) { label, arr, (int)(sizeof(arr) / sizeof(arr[0])) }
|
#define SET(arr, label) { label, arr, (int)(sizeof(arr) / sizeof(arr[0])) }
|
||||||
@@ -192,6 +241,7 @@ static const CharSet SETS[] = {
|
|||||||
SET(PUNCT, "Punctuation"),
|
SET(PUNCT, "Punctuation"),
|
||||||
SET(CURRENCY, "Currency"),
|
SET(CURRENCY, "Currency"),
|
||||||
SET(SYMBOLS, "Symbols"),
|
SET(SYMBOLS, "Symbols"),
|
||||||
|
SET(GREEK, "Greek"),
|
||||||
};
|
};
|
||||||
static constexpr int SET_COUNT = (int)(sizeof(SETS) / sizeof(SETS[0]));
|
static constexpr int SET_COUNT = (int)(sizeof(SETS) / sizeof(SETS[0]));
|
||||||
|
|
||||||
@@ -319,29 +369,26 @@ static void ensure_visible(const Layout& L) {
|
|||||||
|
|
||||||
// ==== Helpers ====
|
// ==== Helpers ====
|
||||||
|
|
||||||
static void glyph_str(uint8_t byte, char out[2]) {
|
static int glyph_str(uint32_t cp, char out[5]) {
|
||||||
out[0] = (char)byte;
|
int n = montauk::utf8::encode(cp, out);
|
||||||
out[1] = '\0';
|
out[n] = '\0';
|
||||||
}
|
return n;
|
||||||
|
|
||||||
static int codepoint_of(uint8_t byte) {
|
|
||||||
return decode_single_byte_codepoint((int)byte);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void copy_selected() {
|
static void copy_selected() {
|
||||||
const Glyph& g = cur_set().glyphs[g_sel];
|
const Glyph& g = cur_set().glyphs[g_sel];
|
||||||
char s[2];
|
char s[5];
|
||||||
glyph_str(g.byte, s);
|
int n = glyph_str(g.cp, s);
|
||||||
gui::clipboard_set_text(s, 1);
|
gui::clipboard_set_text(s, n);
|
||||||
g_copied_until = montauk::get_milliseconds() + COPIED_MS;
|
g_copied_until = montauk::get_milliseconds() + COPIED_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==== Rendering ====
|
// ==== Rendering ====
|
||||||
|
|
||||||
static void draw_centered_glyph(Canvas& c, const Rect& r, uint8_t byte,
|
static void draw_centered_glyph(Canvas& c, const Rect& r, uint32_t cp,
|
||||||
Color color, int size) {
|
Color color, int size) {
|
||||||
char s[2];
|
char s[5];
|
||||||
glyph_str(byte, s);
|
glyph_str(cp, s);
|
||||||
int tw = text_width(fonts::system_font, s, size);
|
int tw = text_width(fonts::system_font, s, size);
|
||||||
int th = text_height(fonts::system_font, size);
|
int th = text_height(fonts::system_font, size);
|
||||||
draw_text(c, fonts::system_font, r.x + (r.w - tw) / 2,
|
draw_text(c, fonts::system_font, r.x + (r.w - tw) / 2,
|
||||||
@@ -376,7 +423,7 @@ static void render(Canvas& c) {
|
|||||||
c.fill_rounded_rect(r.x, r.y, r.w, r.h, g_theme.radius_md, border);
|
c.fill_rounded_rect(r.x, r.y, r.w, r.h, g_theme.radius_md, border);
|
||||||
c.fill_rounded_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2,
|
c.fill_rounded_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2,
|
||||||
g_theme.radius_md > 0 ? g_theme.radius_md - 1 : 0, fill);
|
g_theme.radius_md > 0 ? g_theme.radius_md - 1 : 0, fill);
|
||||||
draw_centered_glyph(c, r, set.glyphs[i].byte, glyph, CELL_SIZE);
|
draw_centered_glyph(c, r, set.glyphs[i].cp, glyph, CELL_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Scrollbar (shared MTK widget; no-ops when the set fits) ----
|
// ---- Scrollbar (shared MTK widget; no-ops when the set fits) ----
|
||||||
@@ -407,14 +454,21 @@ static void render(Canvas& c) {
|
|||||||
g_theme.radius_md, g_theme.border);
|
g_theme.radius_md, g_theme.border);
|
||||||
c.fill_rounded_rect(preview.x + 1, preview.y + 1, preview.w - 2, preview.h - 2,
|
c.fill_rounded_rect(preview.x + 1, preview.y + 1, preview.w - 2, preview.h - 2,
|
||||||
g_theme.radius_md - 1, colors::WHITE);
|
g_theme.radius_md - 1, colors::WHITE);
|
||||||
draw_centered_glyph(c, preview, sg.byte, g_theme.text, PREVIEW_SIZE);
|
draw_centered_glyph(c, preview, sg.cp, g_theme.text, PREVIEW_SIZE);
|
||||||
|
|
||||||
// Name + metadata, as a two-line block vertically centered on the preview.
|
// Name + metadata, as a two-line block vertically centered on the preview.
|
||||||
int cp = codepoint_of(sg.byte);
|
int cp = (int)sg.cp;
|
||||||
|
char enc[5];
|
||||||
|
int enc_len = glyph_str(sg.cp, enc);
|
||||||
|
char utf8_hex[16];
|
||||||
|
int hp = 0;
|
||||||
|
for (int i = 0; i < enc_len; i++)
|
||||||
|
hp += snprintf(utf8_hex + hp, sizeof(utf8_hex) - hp, i ? " %02X" : "%02X",
|
||||||
|
(unsigned)(uint8_t)enc[i]);
|
||||||
int text_x = preview.x + preview.w + 14;
|
int text_x = preview.x + preview.w + 14;
|
||||||
char meta[64];
|
char meta[64];
|
||||||
snprintf(meta, sizeof(meta), "U+%04X dec %d Alt 0%d",
|
snprintf(meta, sizeof(meta), "U+%04X dec %d UTF-8 %s",
|
||||||
cp, cp, (int)sg.byte);
|
cp, cp, utf8_hex);
|
||||||
|
|
||||||
int name_h = text_height(title_font, NAME_SIZE);
|
int name_h = text_height(title_font, NAME_SIZE);
|
||||||
int meta_h = text_height(fonts::system_font, META_SIZE);
|
int meta_h = text_height(fonts::system_font, META_SIZE);
|
||||||
@@ -527,7 +581,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key, int w, int h) {
|
|||||||
const int count = cur_set().count;
|
const int count = cur_set().count;
|
||||||
bool redraw = false;
|
bool redraw = false;
|
||||||
|
|
||||||
switch (key.scancode) {
|
switch (montauk::nav_scancode(key)) {
|
||||||
case 0x4B: // Left
|
case 0x4B: // Left
|
||||||
if (g_sel > 0) { g_sel--; ensure_visible(L); redraw = true; }
|
if (g_sel > 0) { g_sel--; ensure_visible(L); redraw = true; }
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -306,12 +306,12 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
if (ev.type == 0 && ev.key.pressed) {
|
if (ev.type == 0 && ev.key.pressed) {
|
||||||
if (ev.key.scancode == 0x01) break; // Escape
|
if (ev.key.scancode == 0x01) break; // Escape
|
||||||
if (ev.key.scancode == 0x51) {
|
if (montauk::nav_scancode(ev.key) == 0x51) {
|
||||||
// Page Down: scroll down
|
// Page Down: scroll down
|
||||||
g_scroll += 60;
|
g_scroll += 60;
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
if (ev.key.scancode == 0x49) {
|
if (montauk::nav_scancode(ev.key) == 0x49) {
|
||||||
// Page Up: scroll up
|
// Page Up: scroll up
|
||||||
g_scroll -= 60;
|
g_scroll -= 60;
|
||||||
if (g_scroll < 0) g_scroll = 0;
|
if (g_scroll < 0) g_scroll = 0;
|
||||||
|
|||||||
@@ -351,36 +351,21 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
} else if (key.scancode == 0x01) {
|
} else if (key.scancode == 0x01) {
|
||||||
filemanager_cancel_pathbar(fm);
|
filemanager_cancel_pathbar(fm);
|
||||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (fm->pathbar_cursor > 0) {
|
montauk::utf8::erase_before(fm->pathbar_buf, &fm->pathbar_len, &fm->pathbar_cursor);
|
||||||
for (int i = fm->pathbar_cursor - 1; i < fm->pathbar_len - 1; i++)
|
} else if (montauk::nav_scancode(key) == 0x53) {
|
||||||
fm->pathbar_buf[i] = fm->pathbar_buf[i + 1];
|
montauk::utf8::erase_at(fm->pathbar_buf, &fm->pathbar_len, fm->pathbar_cursor);
|
||||||
fm->pathbar_len--;
|
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
fm->pathbar_cursor--;
|
fm->pathbar_cursor = montauk::utf8::prev_boundary(fm->pathbar_buf, fm->pathbar_cursor);
|
||||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
}
|
fm->pathbar_cursor = montauk::utf8::next_boundary(fm->pathbar_buf, fm->pathbar_len,
|
||||||
} else if (key.scancode == 0x53) {
|
fm->pathbar_cursor);
|
||||||
if (fm->pathbar_cursor < fm->pathbar_len) {
|
} else if (montauk::nav_scancode(key) == 0x47) {
|
||||||
for (int i = fm->pathbar_cursor; i < fm->pathbar_len - 1; i++)
|
|
||||||
fm->pathbar_buf[i] = fm->pathbar_buf[i + 1];
|
|
||||||
fm->pathbar_len--;
|
|
||||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
|
||||||
}
|
|
||||||
} else if (key.scancode == 0x4B) {
|
|
||||||
if (fm->pathbar_cursor > 0) fm->pathbar_cursor--;
|
|
||||||
} else if (key.scancode == 0x4D) {
|
|
||||||
if (fm->pathbar_cursor < fm->pathbar_len) fm->pathbar_cursor++;
|
|
||||||
} else if (key.scancode == 0x47) {
|
|
||||||
fm->pathbar_cursor = 0;
|
fm->pathbar_cursor = 0;
|
||||||
} else if (key.scancode == 0x4F) {
|
} else if (montauk::nav_scancode(key) == 0x4F) {
|
||||||
fm->pathbar_cursor = fm->pathbar_len;
|
fm->pathbar_cursor = fm->pathbar_len;
|
||||||
} else if ((unsigned char)key.ascii >= 32
|
} else {
|
||||||
&& (unsigned char)key.ascii != 127 && fm->pathbar_len < 254) {
|
montauk::utf8::insert_key(fm->pathbar_buf, &fm->pathbar_len, 255,
|
||||||
for (int i = fm->pathbar_len; i > fm->pathbar_cursor; i--)
|
&fm->pathbar_cursor, key);
|
||||||
fm->pathbar_buf[i] = fm->pathbar_buf[i - 1];
|
|
||||||
fm->pathbar_buf[fm->pathbar_cursor] = key.ascii;
|
|
||||||
fm->pathbar_cursor++;
|
|
||||||
fm->pathbar_len++;
|
|
||||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -394,44 +379,27 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
filemanager_cancel_rename(fm);
|
filemanager_cancel_rename(fm);
|
||||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
// Backspace
|
// Backspace
|
||||||
if (fm->rename_cursor > 0) {
|
montauk::utf8::erase_before(fm->rename_buf, &fm->rename_len, &fm->rename_cursor);
|
||||||
for (int i = fm->rename_cursor - 1; i < fm->rename_len - 1; i++)
|
} else if (montauk::nav_scancode(key) == 0x53) {
|
||||||
fm->rename_buf[i] = fm->rename_buf[i + 1];
|
|
||||||
fm->rename_len--;
|
|
||||||
fm->rename_cursor--;
|
|
||||||
fm->rename_buf[fm->rename_len] = '\0';
|
|
||||||
}
|
|
||||||
} else if (key.scancode == 0x53) {
|
|
||||||
// Delete key
|
// Delete key
|
||||||
if (fm->rename_cursor < fm->rename_len) {
|
montauk::utf8::erase_at(fm->rename_buf, &fm->rename_len, fm->rename_cursor);
|
||||||
for (int i = fm->rename_cursor; i < fm->rename_len - 1; i++)
|
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
fm->rename_buf[i] = fm->rename_buf[i + 1];
|
|
||||||
fm->rename_len--;
|
|
||||||
fm->rename_buf[fm->rename_len] = '\0';
|
|
||||||
}
|
|
||||||
} else if (key.scancode == 0x4B) {
|
|
||||||
// Left arrow
|
// Left arrow
|
||||||
if (fm->rename_cursor > 0) fm->rename_cursor--;
|
fm->rename_cursor = montauk::utf8::prev_boundary(fm->rename_buf, fm->rename_cursor);
|
||||||
} else if (key.scancode == 0x4D) {
|
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
// Right arrow
|
// Right arrow
|
||||||
if (fm->rename_cursor < fm->rename_len) fm->rename_cursor++;
|
fm->rename_cursor = montauk::utf8::next_boundary(fm->rename_buf, fm->rename_len,
|
||||||
} else if (key.scancode == 0x47) {
|
fm->rename_cursor);
|
||||||
|
} else if (montauk::nav_scancode(key) == 0x47) {
|
||||||
// Home
|
// Home
|
||||||
fm->rename_cursor = 0;
|
fm->rename_cursor = 0;
|
||||||
} else if (key.scancode == 0x4F) {
|
} else if (montauk::nav_scancode(key) == 0x4F) {
|
||||||
// End
|
// End
|
||||||
fm->rename_cursor = fm->rename_len;
|
fm->rename_cursor = fm->rename_len;
|
||||||
} else if ((unsigned char)key.ascii >= 32
|
} else if (key.ascii != '/' && key.ascii != '\\') {
|
||||||
&& (unsigned char)key.ascii != 127) {
|
|
||||||
// Printable character (reject / and other FS-unsafe chars)
|
// Printable character (reject / and other FS-unsafe chars)
|
||||||
if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) {
|
montauk::utf8::insert_key(fm->rename_buf, &fm->rename_len, 63,
|
||||||
for (int i = fm->rename_len; i > fm->rename_cursor; i--)
|
&fm->rename_cursor, key);
|
||||||
fm->rename_buf[i] = fm->rename_buf[i - 1];
|
|
||||||
fm->rename_buf[fm->rename_cursor] = key.ascii;
|
|
||||||
fm->rename_cursor++;
|
|
||||||
fm->rename_len++;
|
|
||||||
fm->rename_buf[fm->rename_len] = '\0';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -466,7 +434,7 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
|
|
||||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
filemanager_go_up(fm);
|
filemanager_go_up(fm);
|
||||||
} else if (key.scancode == 0x48) {
|
} else if (montauk::nav_scancode(key) == 0x48) {
|
||||||
// Up arrow
|
// Up arrow
|
||||||
if (fm->grid_view) {
|
if (fm->grid_view) {
|
||||||
Rect cr_r = win->content_rect();
|
Rect cr_r = win->content_rect();
|
||||||
@@ -477,7 +445,7 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
if (fm->selected > 0) fm->selected--;
|
if (fm->selected > 0) fm->selected--;
|
||||||
}
|
}
|
||||||
filemanager_scroll_to_selected(win, fm);
|
filemanager_scroll_to_selected(win, fm);
|
||||||
} else if (key.scancode == 0x50) {
|
} else if (montauk::nav_scancode(key) == 0x50) {
|
||||||
// Down arrow
|
// Down arrow
|
||||||
if (fm->grid_view) {
|
if (fm->grid_view) {
|
||||||
Rect cr_r = win->content_rect();
|
Rect cr_r = win->content_rect();
|
||||||
@@ -488,22 +456,22 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||||
}
|
}
|
||||||
filemanager_scroll_to_selected(win, fm);
|
filemanager_scroll_to_selected(win, fm);
|
||||||
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) {
|
} else if (montauk::nav_scancode(key) == 0x4B && !key.alt && fm->grid_view) {
|
||||||
// Left arrow (grid view only)
|
// Left arrow (grid view only)
|
||||||
if (fm->selected > 0) fm->selected--;
|
if (fm->selected > 0) fm->selected--;
|
||||||
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) {
|
} else if (montauk::nav_scancode(key) == 0x4D && !key.alt && fm->grid_view) {
|
||||||
// Right arrow (grid view only)
|
// Right arrow (grid view only)
|
||||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||||
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||||
if (fm->selected >= 0 && fm->selected < fm->entry_count)
|
if (fm->selected >= 0 && fm->selected < fm->entry_count)
|
||||||
filemanager_open_entry(fm, fm->selected);
|
filemanager_open_entry(fm, fm->selected);
|
||||||
} else if (key.scancode == 0x53) {
|
} else if (montauk::nav_scancode(key) == 0x53) {
|
||||||
// Delete key
|
// Delete key
|
||||||
filemanager_delete_selected(fm);
|
filemanager_delete_selected(fm);
|
||||||
} else if (key.alt && key.scancode == 0x4B) {
|
} else if (key.alt && montauk::nav_scancode(key) == 0x4B) {
|
||||||
// Alt+Left: go back
|
// Alt+Left: go back
|
||||||
filemanager_go_back(fm);
|
filemanager_go_back(fm);
|
||||||
} else if (key.alt && key.scancode == 0x4D) {
|
} else if (key.alt && montauk::nav_scancode(key) == 0x4D) {
|
||||||
// Alt+Right: go forward
|
// Alt+Right: go forward
|
||||||
filemanager_go_forward(fm);
|
filemanager_go_forward(fm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ static void props_fit_text(const char* src, char* out, int out_len, int max_w) {
|
|||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
out[len] = '\0';
|
out[len] = '\0';
|
||||||
if (text_width(out) + ellipsis_w <= max_w) break;
|
if (text_width(out) + ellipsis_w <= max_w) break;
|
||||||
len--;
|
len = montauk::utf8::prev_boundary(out, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > out_len - 4) len = out_len - 4;
|
if (len > out_len - 4) len = montauk::utf8::floor_boundary(out, len, out_len - 4);
|
||||||
if (len < 0) len = 0;
|
if (len < 0) len = 0;
|
||||||
out[len] = '.';
|
out[len] = '.';
|
||||||
out[len + 1] = '.';
|
out[len + 1] = '.';
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ static int fitted_prefix_len(const char* src, int max_w, int max_len) {
|
|||||||
char tmp[128];
|
char tmp[128];
|
||||||
int best = 0;
|
int best = 0;
|
||||||
int limit = max_len < 127 ? max_len : 127;
|
int limit = max_len < 127 ? max_len : 127;
|
||||||
for (int len = 1; len <= limit; len++) {
|
for (int len = montauk::utf8::next_boundary(src, max_len, 0); len <= limit;
|
||||||
|
len = montauk::utf8::next_boundary(src, max_len, len)) {
|
||||||
copy_label_range(tmp, sizeof(tmp), src, 0, len);
|
copy_label_range(tmp, sizeof(tmp), src, 0, len);
|
||||||
if (text_width(tmp) > max_w) break;
|
if (text_width(tmp) > max_w) break;
|
||||||
best = len;
|
best = len;
|
||||||
|
if (len >= max_len) break;
|
||||||
}
|
}
|
||||||
return best > 0 ? best : 1;
|
return best > 0 ? best : montauk::utf8::next_boundary(src, max_len, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int max_w) {
|
static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int max_w) {
|
||||||
@@ -49,10 +51,10 @@ static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int
|
|||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
dst[len] = '\0';
|
dst[len] = '\0';
|
||||||
if (text_width(dst) + ellipsis_w <= max_w) break;
|
if (text_width(dst) + ellipsis_w <= max_w) break;
|
||||||
len--;
|
len = montauk::utf8::prev_boundary(dst, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > dst_max - 4) len = dst_max - 4;
|
if (len > dst_max - 4) len = montauk::utf8::floor_boundary(dst, len, dst_max - 4);
|
||||||
if (len < 0) len = 0;
|
if (len < 0) len = 0;
|
||||||
dst[len] = '.';
|
dst[len] = '.';
|
||||||
dst[len + 1] = '.';
|
dst[len + 1] = '.';
|
||||||
@@ -403,7 +405,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
// Draw rename text (truncated to cell width)
|
// Draw rename text (truncated to cell width)
|
||||||
char display[16];
|
char display[16];
|
||||||
int rlen = fm->rename_len;
|
int rlen = fm->rename_len;
|
||||||
if (rlen > 10) rlen = 10;
|
if (rlen > 10) rlen = montauk::utf8::floor_boundary(fm->rename_buf, fm->rename_len, 10);
|
||||||
for (int k = 0; k < rlen; k++) display[k] = fm->rename_buf[k];
|
for (int k = 0; k < rlen; k++) display[k] = fm->rename_buf[k];
|
||||||
display[rlen] = '\0';
|
display[rlen] = '\0';
|
||||||
c.text(cell_x + 4, ty, display, colors::TEXT_COLOR);
|
c.text(cell_x + 4, ty, display, colors::TEXT_COLOR);
|
||||||
@@ -557,18 +559,19 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
int name_max_w = size_col_x - tx - 8;
|
int name_max_w = size_col_x - tx - 8;
|
||||||
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
|
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
|
||||||
char label[64];
|
char label[64];
|
||||||
montauk::strncpy(label, name, 63);
|
montauk::utf8::copy(label, 64, name);
|
||||||
|
|
||||||
if (text_width(label) > name_max_w && montauk::slen(name) > 3) {
|
if (text_width(label) > name_max_w && montauk::slen(name) > 3) {
|
||||||
int ellipsis_w = text_width("...");
|
int ellipsis_w = text_width("...");
|
||||||
int final_len = montauk::slen(name);
|
int final_len = montauk::slen(label);
|
||||||
while (final_len > 1) {
|
while (final_len > 1) {
|
||||||
label[final_len] = '\0';
|
label[final_len] = '\0';
|
||||||
int w = text_width(label);
|
int w = text_width(label);
|
||||||
if (w + ellipsis_w <= name_max_w) break;
|
if (w + ellipsis_w <= name_max_w) break;
|
||||||
final_len--;
|
final_len = montauk::utf8::prev_boundary(label, final_len);
|
||||||
}
|
}
|
||||||
if (final_len > 60) final_len = 60;
|
if (final_len > 60)
|
||||||
|
final_len = montauk::utf8::floor_boundary(label, final_len, 60);
|
||||||
label[final_len] = '.';
|
label[final_len] = '.';
|
||||||
label[final_len + 1] = '.';
|
label[final_len + 1] = '.';
|
||||||
label[final_len + 2] = '.';
|
label[final_len + 2] = '.';
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ void settings_draw_about(Canvas& c, SettingsState* st) {
|
|||||||
mtk::draw_separator(c, x, y, c.w - 2 * x, theme);
|
mtk::draw_separator(c, x, y, c.w - 2 * x, theme);
|
||||||
y += 12;
|
y += 12;
|
||||||
|
|
||||||
c.text(x, y, "Copyright \xa9 2026 Montauk Operating System Project", theme.text_muted);
|
c.text(x, y, "Copyright \u00A9 2026 Montauk Operating System Project", theme.text_muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace settings_app
|
} // namespace settings_app
|
||||||
@@ -103,7 +103,7 @@ void desktop_draw_lock_screen(DesktopState* ds) {
|
|||||||
|
|
||||||
// Masked password text (clipped to field width)
|
// Masked password text (clipped to field width)
|
||||||
char masked[65];
|
char masked[65];
|
||||||
int len = ds->lock_password_len;
|
int len = montauk::utf8::count(ds->lock_password); // one '*' per character
|
||||||
if (len > 64) len = 64;
|
if (len > 64) len = 64;
|
||||||
int max_text_w = content_w - 10 - 10 - 2; // left pad, right pad, cursor
|
int max_text_w = content_w - 10 - 10 - 2; // left pad, right pad, cursor
|
||||||
// Show only the trailing portion that fits
|
// Show only the trailing portion that fits
|
||||||
|
|||||||
@@ -105,7 +105,11 @@ int wrap_text(const char* text, int max_w, char (*lines)[LINE_CHARS], int max_li
|
|||||||
char* last = lines[max_lines - 1];
|
char* last = lines[max_lines - 1];
|
||||||
int len = montauk::slen(last);
|
int len = montauk::slen(last);
|
||||||
if (len > LINE_CHARS - 4) len = LINE_CHARS - 4;
|
if (len > LINE_CHARS - 4) len = LINE_CHARS - 4;
|
||||||
while (len > 0 && text_width(last) + text_width("...") > max_w) last[--len] = '\0';
|
last[len] = '\0';
|
||||||
|
while (len > 0 && text_width(last) + text_width("...") > max_w) {
|
||||||
|
len = montauk::utf8::prev_boundary(last, len);
|
||||||
|
last[len] = '\0';
|
||||||
|
}
|
||||||
last[len] = '.'; last[len + 1] = '.'; last[len + 2] = '.'; last[len + 3] = '\0';
|
last[len] = '.'; last[len + 1] = '.'; last[len + 2] = '.'; last[len + 3] = '\0';
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
|
|||||||
@@ -98,22 +98,12 @@ static void handle_lock_keyboard(DesktopState* ds, const montauk::abi::KeyEvent&
|
|||||||
|
|
||||||
// Backspace
|
// Backspace
|
||||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (ds->lock_password_len > 0) {
|
montauk::utf8::erase_last(ds->lock_password, &ds->lock_password_len);
|
||||||
ds->lock_password_len--;
|
|
||||||
ds->lock_password[ds->lock_password_len] = '\0';
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printable characters
|
// Printable characters (stored as UTF-8)
|
||||||
if ((unsigned char)key.ascii >= 0x20
|
montauk::utf8::append_key(ds->lock_password, &ds->lock_password_len, 64, key);
|
||||||
&& (unsigned char)key.ascii != 0x7F) {
|
|
||||||
if (ds->lock_password_len < 63) {
|
|
||||||
ds->lock_password[ds->lock_password_len] = key.ascii;
|
|
||||||
ds->lock_password_len++;
|
|
||||||
ds->lock_password[ds->lock_password_len] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void forward_external_mouse(gui::Window* win,
|
static void forward_external_mouse(gui::Window* win,
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ static void launcher_activate_selected(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void launcher_update_super_state(DesktopState* ds, const montauk::abi::KeyEvent& key) {
|
static void launcher_update_super_state(DesktopState* ds, const montauk::abi::KeyEvent& key) {
|
||||||
uint8_t sc = key.scancode & 0x7F;
|
uint8_t sc = montauk::nav_scancode(key) & 0x7F;
|
||||||
if (sc == 0x5B) {
|
if (sc == 0x5B) {
|
||||||
ds->super_left_down = key.pressed;
|
ds->super_left_down = key.pressed;
|
||||||
} else if (sc == 0x5C) {
|
} else if (sc == 0x5C) {
|
||||||
@@ -371,7 +371,7 @@ static void launcher_update_super_state(DesktopState* ds, const montauk::abi::Ke
|
|||||||
|
|
||||||
static const char* fit_trailing_text(const char* text, int max_w) {
|
static const char* fit_trailing_text(const char* text, int max_w) {
|
||||||
const char* visible = text;
|
const char* visible = text;
|
||||||
while (*visible && text_width(visible) > max_w) visible++;
|
while (*visible && text_width(visible) > max_w) montauk::utf8::next(&visible);
|
||||||
return visible;
|
return visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +420,7 @@ void desktop_close_launcher(DesktopState* ds) {
|
|||||||
|
|
||||||
bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key) {
|
bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key) {
|
||||||
launcher_update_super_state(ds, key);
|
launcher_update_super_state(ds, key);
|
||||||
uint8_t sc = key.scancode & 0x7F;
|
uint8_t sc = montauk::nav_scancode(key) & 0x7F;
|
||||||
|
|
||||||
if (sc == 0x5B || sc == 0x5C) {
|
if (sc == 0x5B || sc == 0x5C) {
|
||||||
return true;
|
return true;
|
||||||
@@ -456,11 +456,8 @@ bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyE
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (key.ascii == '\b' || sc == 0x0E) {
|
if (key.ascii == '\b' || sc == 0x0E) {
|
||||||
if (ds->launcher_query_len > 0) {
|
if (montauk::utf8::erase_last(ds->launcher_query, &ds->launcher_query_len))
|
||||||
ds->launcher_query_len--;
|
|
||||||
ds->launcher_query[ds->launcher_query_len] = '\0';
|
|
||||||
desktop_update_launcher_results(ds);
|
desktop_update_launcher_results(ds);
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,12 +492,8 @@ bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyE
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((unsigned char)key.ascii >= 0x20
|
if (montauk::utf8::append_key(ds->launcher_query, &ds->launcher_query_len, 64, key))
|
||||||
&& (unsigned char)key.ascii != 0x7F && ds->launcher_query_len < 63) {
|
|
||||||
ds->launcher_query[ds->launcher_query_len++] = key.ascii;
|
|
||||||
ds->launcher_query[ds->launcher_query_len] = '\0';
|
|
||||||
desktop_update_launcher_results(ds);
|
desktop_update_launcher_results(ds);
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
int row_count = build_display_rows(&de, rows);
|
int row_count = build_display_rows(&de, rows);
|
||||||
if (row_count == 0) return true;
|
if (row_count == 0) return true;
|
||||||
|
|
||||||
if (key.scancode == 0x48) { // Up
|
if (montauk::nav_scancode(key) == 0x48) { // Up
|
||||||
if (de.selected_row <= 0) de.selected_row = 0;
|
if (de.selected_row <= 0) de.selected_row = 0;
|
||||||
else de.selected_row--;
|
else de.selected_row--;
|
||||||
// Scroll up just enough to bring the selected row's top into view.
|
// Scroll up just enough to bring the selected row's top into view.
|
||||||
@@ -289,7 +289,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
sel_top += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
|
sel_top += (rows[i].type == ROW_CATEGORY) ? CAT_H : ITEM_H;
|
||||||
if (sel_top < de.scroll_y)
|
if (sel_top < de.scroll_y)
|
||||||
de.scroll_y = sel_top;
|
de.scroll_y = sel_top;
|
||||||
} else if (key.scancode == 0x50) { // Down
|
} else if (montauk::nav_scancode(key) == 0x50) { // Down
|
||||||
if (de.selected_row < row_count - 1)
|
if (de.selected_row < row_count - 1)
|
||||||
de.selected_row++;
|
de.selected_row++;
|
||||||
// Scroll down just enough to bring the selected row's bottom into view.
|
// Scroll down just enough to bring the selected row's bottom into view.
|
||||||
@@ -300,7 +300,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
int list_h = g_win_h - TOOLBAR_H;
|
int list_h = g_win_h - TOOLBAR_H;
|
||||||
if (sel_top + sel_h > de.scroll_y + list_h)
|
if (sel_top + sel_h > de.scroll_y + list_h)
|
||||||
de.scroll_y = sel_top + sel_h - list_h;
|
de.scroll_y = sel_top + sel_h - list_h;
|
||||||
} else if (key.scancode == 0x4B) { // Left — collapse
|
} else if (montauk::nav_scancode(key) == 0x4B) { // Left — collapse
|
||||||
if (de.selected_row >= 0 && de.selected_row < row_count) {
|
if (de.selected_row >= 0 && de.selected_row < row_count) {
|
||||||
int cat = rows[de.selected_row].category;
|
int cat = rows[de.selected_row].category;
|
||||||
if (!de.collapsed[cat]) {
|
if (!de.collapsed[cat]) {
|
||||||
@@ -318,7 +318,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (key.scancode == 0x4D) { // Right — expand
|
} else if (montauk::nav_scancode(key) == 0x4D) { // Right — expand
|
||||||
if (de.selected_row >= 0 && de.selected_row < row_count) {
|
if (de.selected_row >= 0 && de.selected_row < row_count) {
|
||||||
int cat = rows[de.selected_row].category;
|
int cat = rows[de.selected_row].category;
|
||||||
de.collapsed[cat] = false;
|
de.collapsed[cat] = false;
|
||||||
|
|||||||
@@ -261,15 +261,16 @@ void fit_text_start(const char* src, char* out, int out_len, int max_px) {
|
|||||||
// Grid labels must fit their cell in pixels. Character-count truncation is
|
// Grid labels must fit their cell in pixels. Character-count truncation is
|
||||||
// insufficient for the proportional system font and lets wide names paint
|
// insufficient for the proportional system font and lets wide names paint
|
||||||
// over the adjacent label.
|
// over the adjacent label.
|
||||||
int keep = montauk::slen(src);
|
int slen = montauk::slen(src);
|
||||||
if (keep > out_len - 3) keep = out_len - 3;
|
int keep = slen;
|
||||||
|
if (keep > out_len - 3) keep = montauk::utf8::floor_boundary(src, slen, out_len - 3);
|
||||||
while (keep > 0) {
|
while (keep > 0) {
|
||||||
for (int i = 0; i < keep; i++) out[i] = src[i];
|
for (int i = 0; i < keep; i++) out[i] = src[i];
|
||||||
out[keep] = '.';
|
out[keep] = '.';
|
||||||
out[keep + 1] = '.';
|
out[keep + 1] = '.';
|
||||||
out[keep + 2] = '\0';
|
out[keep + 2] = '\0';
|
||||||
if (text_width(out) <= max_px) return;
|
if (text_width(out) <= max_px) return;
|
||||||
keep--;
|
keep = montauk::utf8::prev_boundary(src, keep);
|
||||||
}
|
}
|
||||||
|
|
||||||
safe_copy(out, out_len, "..");
|
safe_copy(out, out_len, "..");
|
||||||
@@ -1216,14 +1217,14 @@ void handle_file_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4B || key.scancode == 0x48) {
|
if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x48) {
|
||||||
if (st->selected > 0) st->selected--;
|
if (st->selected > 0) st->selected--;
|
||||||
else if (st->entry_count > 0) st->selected = 0;
|
else if (st->entry_count > 0) st->selected = 0;
|
||||||
file_ensure_selected_visible(st);
|
file_ensure_selected_visible(st);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4D || key.scancode == 0x50) {
|
if (montauk::nav_scancode(key) == 0x4D || montauk::nav_scancode(key) == 0x50) {
|
||||||
if (st->selected < st->entry_count - 1) st->selected++;
|
if (st->selected < st->entry_count - 1) st->selected++;
|
||||||
else if (st->entry_count > 0) st->selected = st->entry_count - 1;
|
else if (st->entry_count > 0) st->selected = st->entry_count - 1;
|
||||||
file_ensure_selected_visible(st);
|
file_ensure_selected_visible(st);
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ void fit_text_end(const char* src, char* out, int out_len, int max_px) {
|
|||||||
gui::dialogs::safe_copy(out, out_len, candidate);
|
gui::dialogs::safe_copy(out, out_len, candidate);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
keep--;
|
// Drop one whole character from the front of the tail
|
||||||
|
keep = slen - montauk::utf8::next_boundary(src, slen, slen - keep);
|
||||||
}
|
}
|
||||||
|
|
||||||
gui::dialogs::safe_copy(out, out_len, "...");
|
gui::dialogs::safe_copy(out, out_len, "...");
|
||||||
|
|||||||
@@ -480,13 +480,13 @@ void handle_print_key(const montauk::abi::KeyEvent& key) {
|
|||||||
g_print.error[0] = '\0';
|
g_print.error[0] = '\0';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((key.scancode == 0x48 || key.scancode == 0x4B) && g_print.printer_count > 0) {
|
if ((montauk::nav_scancode(key) == 0x48 || montauk::nav_scancode(key) == 0x4B) && g_print.printer_count > 0) {
|
||||||
if (g_print.selected_printer > 0) g_print.selected_printer--;
|
if (g_print.selected_printer > 0) g_print.selected_printer--;
|
||||||
else g_print.selected_printer = 0;
|
else g_print.selected_printer = 0;
|
||||||
g_print.error[0] = '\0';
|
g_print.error[0] = '\0';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((key.scancode == 0x50 || key.scancode == 0x4D) && g_print.printer_count > 0) {
|
if ((montauk::nav_scancode(key) == 0x50 || montauk::nav_scancode(key) == 0x4D) && g_print.printer_count > 0) {
|
||||||
if (g_print.selected_printer < g_print.printer_count - 1) g_print.selected_printer++;
|
if (g_print.selected_printer < g_print.printer_count - 1) g_print.selected_printer++;
|
||||||
else g_print.selected_printer = g_print.printer_count - 1;
|
else g_print.selected_printer = g_print.printer_count - 1;
|
||||||
g_print.error[0] = '\0';
|
g_print.error[0] = '\0';
|
||||||
|
|||||||
@@ -339,9 +339,9 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
if (dt.fmt_dlg.open) {
|
if (dt.fmt_dlg.open) {
|
||||||
if (key.scancode == 0x01) { // Escape
|
if (key.scancode == 0x01) { // Escape
|
||||||
close_format_dialog();
|
close_format_dialog();
|
||||||
} else if (key.scancode == 0x48 && dt.fmt_dlg.selected_fs > 0) {
|
} else if (montauk::nav_scancode(key) == 0x48 && dt.fmt_dlg.selected_fs > 0) {
|
||||||
dt.fmt_dlg.selected_fs--;
|
dt.fmt_dlg.selected_fs--;
|
||||||
} else if (key.scancode == 0x50 && dt.fmt_dlg.selected_fs < NUM_FS_TYPES - 1) {
|
} else if (montauk::nav_scancode(key) == 0x50 && dt.fmt_dlg.selected_fs < NUM_FS_TYPES - 1) {
|
||||||
dt.fmt_dlg.selected_fs++;
|
dt.fmt_dlg.selected_fs++;
|
||||||
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||||
format_dialog_do_format();
|
format_dialog_do_format();
|
||||||
@@ -354,14 +354,14 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
|
|
||||||
if (key.scancode == 0x01) { // Escape
|
if (key.scancode == 0x01) { // Escape
|
||||||
return false; // signal quit
|
return false; // signal quit
|
||||||
} else if (key.scancode == 0x48) { // Up
|
} else if (montauk::nav_scancode(key) == 0x48) { // Up
|
||||||
if (dt.selected_part > 0) dt.selected_part--;
|
if (dt.selected_part > 0) dt.selected_part--;
|
||||||
else if (nparts > 0) dt.selected_part = 0;
|
else if (nparts > 0) dt.selected_part = 0;
|
||||||
} else if (key.scancode == 0x50) { // Down
|
} else if (montauk::nav_scancode(key) == 0x50) { // Down
|
||||||
if (dt.selected_part < nparts - 1) dt.selected_part++;
|
if (dt.selected_part < nparts - 1) dt.selected_part++;
|
||||||
} else if (key.scancode == 0x4B) { // Left
|
} else if (montauk::nav_scancode(key) == 0x4B) { // Left
|
||||||
if (dt.selected_disk > 0) { dt.selected_disk--; dt.selected_part = -1; dt.scroll_y = 0; }
|
if (dt.selected_disk > 0) { dt.selected_disk--; dt.selected_part = -1; dt.scroll_y = 0; }
|
||||||
} else if (key.scancode == 0x4D) { // Right
|
} else if (montauk::nav_scancode(key) == 0x4D) { // Right
|
||||||
if (dt.selected_disk < dt.disk_count - 1) { dt.selected_disk++; dt.selected_part = -1; dt.scroll_y = 0; }
|
if (dt.selected_disk < dt.disk_count - 1) { dt.selected_disk++; dt.selected_part = -1; dt.scroll_y = 0; }
|
||||||
} else if (key.ascii == 'n' || key.ascii == 'N') {
|
} else if (key.ascii == 'n' || key.ascii == 'N') {
|
||||||
do_create_partition();
|
do_create_partition();
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ static void draw_text_fit(Canvas& c, int x, int y,
|
|||||||
out++;
|
out++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out = montauk::utf8::floor_boundary(text, montauk::slen(text), out);
|
||||||
|
|
||||||
if (out <= 0) return;
|
if (out <= 0) return;
|
||||||
buf[out] = '.';
|
buf[out] = '.';
|
||||||
buf[out + 1] = '.';
|
buf[out + 1] = '.';
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ static void fit_text(char* out, int outCap, const char* text, int maxWidth) {
|
|||||||
if (text_width(out) + text_width(ellipsis) > maxWidth) break;
|
if (text_width(out) + text_width(ellipsis) > maxWidth) break;
|
||||||
len++;
|
len++;
|
||||||
}
|
}
|
||||||
|
len = montauk::utf8::floor_boundary(text, montauk::slen(text), len);
|
||||||
out[len++] = '.';
|
out[len++] = '.';
|
||||||
out[len++] = '.';
|
out[len++] = '.';
|
||||||
out[len++] = '.';
|
out[len++] = '.';
|
||||||
|
|||||||
+75
-36
@@ -7,6 +7,7 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
|
|
||||||
using montauk::slen;
|
using montauk::slen;
|
||||||
using montauk::memcpy;
|
using montauk::memcpy;
|
||||||
@@ -166,6 +167,16 @@ static void line_delete_char(Line* ln, int pos) {
|
|||||||
ln->data[ln->len] = '\0';
|
ln->data[ln->len] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lines are UTF-8. cursorCol is a byte offset kept on a character boundary;
|
||||||
|
// the screen works in columns, one per character.
|
||||||
|
static int line_display_col(const Line* ln, int byteCol) {
|
||||||
|
return montauk::utf8::count(ln->data, byteCol < ln->len ? byteCol : ln->len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int line_byte_col(const Line* ln, int displayCol) {
|
||||||
|
return montauk::utf8::offset_of(ln->data, ln->len, displayCol);
|
||||||
|
}
|
||||||
|
|
||||||
static void line_append(Line* ln, const char* s, int slen) {
|
static void line_append(Line* ln, const char* s, int slen) {
|
||||||
line_ensure(ln, ln->len + slen);
|
line_ensure(ln, ln->len + slen);
|
||||||
memcpy(ln->data + ln->len, s, slen);
|
memcpy(ln->data + ln->len, s, slen);
|
||||||
@@ -300,6 +311,21 @@ static void load_file(const char* fname) {
|
|||||||
}
|
}
|
||||||
montauk::close(handle);
|
montauk::close(handle);
|
||||||
|
|
||||||
|
// Pre-UTF-8 files are Windows-1252: convert once so edits never mix
|
||||||
|
// encodings and saving writes UTF-8.
|
||||||
|
bool converted = false;
|
||||||
|
if (buf && size > 0 && !montauk::utf8::valid((const char*)buf, (int)size)) {
|
||||||
|
int cap = (int)size * 3 + 1;
|
||||||
|
uint8_t* conv = (uint8_t*)montauk::malloc(cap);
|
||||||
|
if (conv) {
|
||||||
|
size = (uint64_t)montauk::utf8::normalize((const char*)buf, (int)size,
|
||||||
|
(char*)conv, cap);
|
||||||
|
montauk::mfree(buf);
|
||||||
|
buf = conv;
|
||||||
|
converted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse into lines
|
// Parse into lines
|
||||||
numLines = 0;
|
numLines = 0;
|
||||||
if (buf && size > 0) {
|
if (buf && size > 0) {
|
||||||
@@ -325,7 +351,7 @@ static void load_file(const char* fname) {
|
|||||||
line_init(&lines[0]);
|
line_init(&lines[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
set_status("File loaded");
|
set_status(converted ? "File loaded (converted from Windows-1252 to UTF-8)" : "File loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool save_file() {
|
static bool save_file() {
|
||||||
@@ -413,14 +439,13 @@ static int prompt_input(const char* promptStr, char* out, int outMax) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ev.ascii == '\b') {
|
if (ev.ascii == '\b') {
|
||||||
if (pos > 0) {
|
if (montauk::utf8::erase_last(out, &pos)) {
|
||||||
pos--;
|
|
||||||
putch('\b'); putch(' '); putch('\b');
|
putch('\b'); putch(' '); putch('\b');
|
||||||
}
|
}
|
||||||
} else if (ev.ascii >= ' ' && pos < outMax - 1) {
|
} else {
|
||||||
out[pos++] = ev.ascii;
|
int before = pos;
|
||||||
out[pos] = '\0';
|
if (montauk::utf8::append_key(out, &pos, outMax, ev))
|
||||||
putch(ev.ascii);
|
print(out + before);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,14 +531,15 @@ static void draw_line(int screenRow, int docLine) {
|
|||||||
reset_attrs();
|
reset_attrs();
|
||||||
|
|
||||||
// Line content
|
// Line content
|
||||||
|
// The visible window is leftCol..leftCol+maxChars in columns; convert
|
||||||
|
// it to a byte range so whole UTF-8 characters are emitted.
|
||||||
Line* ln = &lines[docLine];
|
Line* ln = &lines[docLine];
|
||||||
int startCol = leftCol;
|
int startCol = line_byte_col(ln, leftCol);
|
||||||
int maxChars = screenCols - gutterWidth;
|
int maxChars = screenCols - gutterWidth;
|
||||||
int visibleLen = 0;
|
int endCol = startCol;
|
||||||
if (startCol < ln->len) {
|
for (int n = 0; n < maxChars && endCol < ln->len; n++)
|
||||||
visibleLen = ln->len - startCol;
|
endCol = montauk::utf8::next_boundary(ln->data, ln->len, endCol);
|
||||||
if (visibleLen > maxChars) visibleLen = maxChars;
|
int visibleLen = endCol - startCol;
|
||||||
}
|
|
||||||
|
|
||||||
if (synLang != SYN_LANG_NONE) {
|
if (synLang != SYN_LANG_NONE) {
|
||||||
// Tokenize the WHOLE line, not just the visible window: the
|
// Tokenize the WHOLE line, not just the visible window: the
|
||||||
@@ -577,7 +603,7 @@ static void render() {
|
|||||||
|
|
||||||
// Position cursor
|
// Position cursor
|
||||||
int screenY = cursorRow - topLine + 2; // +2 for status bar
|
int screenY = cursorRow - topLine + 2; // +2 for status bar
|
||||||
int screenX = cursorCol - leftCol + gutterWidth;
|
int screenX = line_display_col(&lines[cursorRow], cursorCol) - leftCol + gutterWidth;
|
||||||
cursor_to(screenY, screenX);
|
cursor_to(screenY, screenX);
|
||||||
|
|
||||||
show_cursor();
|
show_cursor();
|
||||||
@@ -598,12 +624,13 @@ static void scroll() {
|
|||||||
|
|
||||||
// Horizontal scrolling
|
// Horizontal scrolling
|
||||||
int textCols = screenCols - gutterWidth;
|
int textCols = screenCols - gutterWidth;
|
||||||
if (cursorCol < leftCol) {
|
int displayCol = line_display_col(&lines[cursorRow], cursorCol);
|
||||||
leftCol = cursorCol;
|
if (displayCol < leftCol) {
|
||||||
|
leftCol = displayCol;
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
}
|
}
|
||||||
if (cursorCol >= leftCol + textCols) {
|
if (displayCol >= leftCol + textCols) {
|
||||||
leftCol = cursorCol - textCols + 1;
|
leftCol = displayCol - textCols + 1;
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,6 +644,10 @@ static void insert_char(char c) {
|
|||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void insert_bytes(const char* s, int n) {
|
||||||
|
for (int i = 0; i < n; i++) insert_char(s[i]);
|
||||||
|
}
|
||||||
|
|
||||||
static void insert_tab() {
|
static void insert_tab() {
|
||||||
for (int i = 0; i < TAB_WIDTH; i++) {
|
for (int i = 0; i < TAB_WIDTH; i++) {
|
||||||
insert_char(' ');
|
insert_char(' ');
|
||||||
@@ -647,8 +678,11 @@ static void insert_newline() {
|
|||||||
|
|
||||||
static void delete_char_backspace() {
|
static void delete_char_backspace() {
|
||||||
if (cursorCol > 0) {
|
if (cursorCol > 0) {
|
||||||
|
int start = montauk::utf8::prev_boundary(lines[cursorRow].data, cursorCol);
|
||||||
|
while (cursorCol > start) {
|
||||||
line_delete_char(&lines[cursorRow], cursorCol - 1);
|
line_delete_char(&lines[cursorRow], cursorCol - 1);
|
||||||
cursorCol--;
|
cursorCol--;
|
||||||
|
}
|
||||||
modified = true;
|
modified = true;
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
} else if (cursorRow > 0) {
|
} else if (cursorRow > 0) {
|
||||||
@@ -665,7 +699,9 @@ static void delete_char_backspace() {
|
|||||||
|
|
||||||
static void delete_char_forward() {
|
static void delete_char_forward() {
|
||||||
if (cursorCol < lines[cursorRow].len) {
|
if (cursorCol < lines[cursorRow].len) {
|
||||||
line_delete_char(&lines[cursorRow], cursorCol);
|
Line* ln = &lines[cursorRow];
|
||||||
|
int n = montauk::utf8::next_boundary(ln->data, ln->len, cursorCol) - cursorCol;
|
||||||
|
while (n-- > 0) line_delete_char(ln, cursorCol);
|
||||||
modified = true;
|
modified = true;
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
} else if (cursorRow < numLines - 1) {
|
} else if (cursorRow < numLines - 1) {
|
||||||
@@ -793,26 +829,26 @@ static void handle_key(const montauk::abi::KeyEvent& ev) {
|
|||||||
|
|
||||||
// Non-ASCII keys (scancode-based)
|
// Non-ASCII keys (scancode-based)
|
||||||
if (ev.ascii == 0) {
|
if (ev.ascii == 0) {
|
||||||
switch (ev.scancode) {
|
switch (montauk::nav_scancode(ev)) {
|
||||||
case SC_UP:
|
case SC_UP:
|
||||||
if (cursorRow > 0) {
|
if (cursorRow > 0) {
|
||||||
|
int col = line_display_col(&lines[cursorRow], cursorCol);
|
||||||
cursorRow--;
|
cursorRow--;
|
||||||
if (cursorCol > lines[cursorRow].len)
|
cursorCol = line_byte_col(&lines[cursorRow], col);
|
||||||
cursorCol = lines[cursorRow].len;
|
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SC_DOWN:
|
case SC_DOWN:
|
||||||
if (cursorRow < numLines - 1) {
|
if (cursorRow < numLines - 1) {
|
||||||
|
int col = line_display_col(&lines[cursorRow], cursorCol);
|
||||||
cursorRow++;
|
cursorRow++;
|
||||||
if (cursorCol > lines[cursorRow].len)
|
cursorCol = line_byte_col(&lines[cursorRow], col);
|
||||||
cursorCol = lines[cursorRow].len;
|
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SC_LEFT:
|
case SC_LEFT:
|
||||||
if (cursorCol > 0) {
|
if (cursorCol > 0) {
|
||||||
cursorCol--;
|
cursorCol = montauk::utf8::prev_boundary(lines[cursorRow].data, cursorCol);
|
||||||
} else if (cursorRow > 0) {
|
} else if (cursorRow > 0) {
|
||||||
cursorRow--;
|
cursorRow--;
|
||||||
cursorCol = lines[cursorRow].len;
|
cursorCol = lines[cursorRow].len;
|
||||||
@@ -821,7 +857,8 @@ static void handle_key(const montauk::abi::KeyEvent& ev) {
|
|||||||
return;
|
return;
|
||||||
case SC_RIGHT:
|
case SC_RIGHT:
|
||||||
if (cursorCol < lines[cursorRow].len) {
|
if (cursorCol < lines[cursorRow].len) {
|
||||||
cursorCol++;
|
cursorCol = montauk::utf8::next_boundary(lines[cursorRow].data,
|
||||||
|
lines[cursorRow].len, cursorCol);
|
||||||
} else if (cursorRow < numLines - 1) {
|
} else if (cursorRow < numLines - 1) {
|
||||||
cursorRow++;
|
cursorRow++;
|
||||||
cursorCol = 0;
|
cursorCol = 0;
|
||||||
@@ -836,20 +873,22 @@ static void handle_key(const montauk::abi::KeyEvent& ev) {
|
|||||||
cursorCol = lines[cursorRow].len;
|
cursorCol = lines[cursorRow].len;
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
return;
|
return;
|
||||||
case SC_PGUP:
|
case SC_PGUP: {
|
||||||
|
int col = line_display_col(&lines[cursorRow], cursorCol);
|
||||||
cursorRow -= editorRows;
|
cursorRow -= editorRows;
|
||||||
if (cursorRow < 0) cursorRow = 0;
|
if (cursorRow < 0) cursorRow = 0;
|
||||||
if (cursorCol > lines[cursorRow].len)
|
cursorCol = line_byte_col(&lines[cursorRow], col);
|
||||||
cursorCol = lines[cursorRow].len;
|
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
return;
|
return;
|
||||||
case SC_PGDN:
|
}
|
||||||
|
case SC_PGDN: {
|
||||||
|
int col = line_display_col(&lines[cursorRow], cursorCol);
|
||||||
cursorRow += editorRows;
|
cursorRow += editorRows;
|
||||||
if (cursorRow >= numLines) cursorRow = numLines - 1;
|
if (cursorRow >= numLines) cursorRow = numLines - 1;
|
||||||
if (cursorCol > lines[cursorRow].len)
|
cursorCol = line_byte_col(&lines[cursorRow], col);
|
||||||
cursorCol = lines[cursorRow].len;
|
|
||||||
fullRedraw = true;
|
fullRedraw = true;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
case SC_DELETE:
|
case SC_DELETE:
|
||||||
delete_char_forward();
|
delete_char_forward();
|
||||||
return;
|
return;
|
||||||
@@ -869,13 +908,13 @@ static void handle_key(const montauk::abi::KeyEvent& ev) {
|
|||||||
case '\t':
|
case '\t':
|
||||||
insert_tab();
|
insert_tab();
|
||||||
break;
|
break;
|
||||||
default:
|
default: {
|
||||||
if (ev.ascii >= ' ') {
|
char typed[montauk::utf8::MAX_BYTES];
|
||||||
insert_char(ev.ascii);
|
insert_bytes(typed, montauk::utf8::key_utf8(ev, typed));
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Entry point ----
|
// ---- Entry point ----
|
||||||
|
|
||||||
|
|||||||
@@ -402,12 +402,12 @@ extern "C" void _start() {
|
|||||||
if (ev.key.ascii == 'q' || ev.key.ascii == 'Q' || ev.key.scancode == 0x01)
|
if (ev.key.ascii == 'q' || ev.key.ascii == 'Q' || ev.key.scancode == 0x01)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
if (ev.key.scancode == 0x48) { g_scroll_y -= SCROLL_STEP; redraw = true; }
|
if (montauk::nav_scancode(ev.key) == 0x48) { g_scroll_y -= SCROLL_STEP; redraw = true; }
|
||||||
if (ev.key.scancode == 0x50) { g_scroll_y += SCROLL_STEP; redraw = true; }
|
if (montauk::nav_scancode(ev.key) == 0x50) { g_scroll_y += SCROLL_STEP; redraw = true; }
|
||||||
if (ev.key.scancode == 0x47) { g_scroll_y = 0; redraw = true; }
|
if (montauk::nav_scancode(ev.key) == 0x47) { g_scroll_y = 0; redraw = true; }
|
||||||
if (ev.key.scancode == 0x4F) { g_scroll_y = g_content_h; redraw = true; }
|
if (montauk::nav_scancode(ev.key) == 0x4F) { g_scroll_y = g_content_h; redraw = true; }
|
||||||
if (ev.key.scancode == 0x49) { g_scroll_y -= g_win_h; redraw = true; }
|
if (montauk::nav_scancode(ev.key) == 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) == 0x51) { g_scroll_y += g_win_h; redraw = true; }
|
||||||
|
|
||||||
if (redraw) {
|
if (redraw) {
|
||||||
clamp_scroll();
|
clamp_scroll();
|
||||||
|
|||||||
@@ -620,13 +620,13 @@ extern "C" void _start() {
|
|||||||
else if (key.ctrl && (key.ascii == '-' || key.ascii == 31)) { zoom_out(cx, cy); redraw = true; }
|
else if (key.ctrl && (key.ascii == '-' || key.ascii == 31)) { zoom_out(cx, cy); redraw = true; }
|
||||||
|
|
||||||
// Arrow keys to pan
|
// Arrow keys to pan
|
||||||
else if (key.scancode == 0x48) { g_pan_y += PAN_STEP; redraw = true; }
|
else if (montauk::nav_scancode(key) == 0x48) { g_pan_y += PAN_STEP; redraw = true; }
|
||||||
else if (key.scancode == 0x50) { g_pan_y -= PAN_STEP; redraw = true; }
|
else if (montauk::nav_scancode(key) == 0x50) { g_pan_y -= PAN_STEP; redraw = true; }
|
||||||
else if (key.scancode == 0x4B) { g_pan_x += PAN_STEP; redraw = true; }
|
else if (montauk::nav_scancode(key) == 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) == 0x4D) { g_pan_x -= PAN_STEP; redraw = true; }
|
||||||
|
|
||||||
// Home to center
|
// 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();
|
if (redraw && g_load_ok) clamp_pan();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,14 +333,14 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x48) { // Up
|
if (montauk::nav_scancode(key) == 0x48) { // Up
|
||||||
if (st.step == STEP_SELECT_DISK && st.selected_disk > 0)
|
if (st.step == STEP_SELECT_DISK && st.selected_disk > 0)
|
||||||
st.selected_disk--;
|
st.selected_disk--;
|
||||||
else if (st.step == STEP_UPDATE_SELECT_PART && st.selected_part > 0)
|
else if (st.step == STEP_UPDATE_SELECT_PART && st.selected_part > 0)
|
||||||
st.selected_part--;
|
st.selected_part--;
|
||||||
else if (st.step == STEP_PARTITION_SCHEME && st.partition_scheme > 0)
|
else if (st.step == STEP_PARTITION_SCHEME && st.partition_scheme > 0)
|
||||||
st.partition_scheme--;
|
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)
|
if (st.step == STEP_SELECT_DISK && st.selected_disk < st.disk_count - 1)
|
||||||
st.selected_disk++;
|
st.selected_disk++;
|
||||||
else if (st.step == STEP_UPDATE_SELECT_PART && st.selected_part < st.part_count - 1)
|
else if (st.step == STEP_UPDATE_SELECT_PART && st.selected_part < st.part_count - 1)
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ static void draw_text_fit(Canvas& c, int x, int y,
|
|||||||
out++;
|
out++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out = montauk::utf8::floor_boundary(text, montauk::slen(text), out);
|
||||||
|
|
||||||
if (out <= 0) return;
|
if (out <= 0) return;
|
||||||
buf[out] = '.';
|
buf[out] = '.';
|
||||||
buf[out + 1] = '.';
|
buf[out + 1] = '.';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <libc/stdio.h>
|
#include <libc/stdio.h>
|
||||||
|
|
||||||
using montauk::slen;
|
using montauk::slen;
|
||||||
@@ -621,7 +622,7 @@ static void ui_render() {
|
|||||||
for (int i = 0; i < input.len; i++) sb_putc(input.buf[i]);
|
for (int i = 0; i < input.len; i++) sb_putc(input.buf[i]);
|
||||||
|
|
||||||
// Position cursor at input insertion point
|
// 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
|
// Single flush — entire screen in one print call
|
||||||
sb_flush();
|
sb_flush();
|
||||||
@@ -892,7 +893,7 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle special keys by scancode
|
// Handle special keys by scancode
|
||||||
switch (ev.scancode) {
|
switch (montauk::nav_scancode(ev)) {
|
||||||
case 0x49: // Page Up
|
case 0x49: // Page Up
|
||||||
if (msgBuf.scrollOffset < msgBuf.count - term.msgAreaRows) {
|
if (msgBuf.scrollOffset < msgBuf.count - term.msgAreaRows) {
|
||||||
msgBuf.scrollOffset += term.msgAreaRows / 2;
|
msgBuf.scrollOffset += term.msgAreaRows / 2;
|
||||||
@@ -922,22 +923,12 @@ extern "C" void _start() {
|
|||||||
input.len = 0;
|
input.len = 0;
|
||||||
}
|
}
|
||||||
} else if (ev.ascii == '\b') {
|
} else if (ev.ascii == '\b') {
|
||||||
if (input.pos > 0) {
|
input.buf[input.len] = '\0';
|
||||||
for (int j = input.pos - 1; j < input.len - 1; j++) {
|
montauk::utf8::erase_before(input.buf, &input.len, &input.pos);
|
||||||
input.buf[j] = input.buf[j + 1];
|
} else if (montauk::utf8::key_codepoint(ev)) {
|
||||||
}
|
// IRC text is UTF-8
|
||||||
input.pos--;
|
input.buf[input.len] = '\0';
|
||||||
input.len--;
|
montauk::utf8::insert_key(input.buf, &input.len, 511, &input.pos, ev);
|
||||||
}
|
|
||||||
} 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++;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
dirty = false; // Unhandled key, no redraw needed
|
dirty = false; // Unhandled key, no redraw needed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <montauk/keyboard.h>
|
#include <montauk/keyboard.h>
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <gui/mtk.hpp>
|
#include <gui/mtk.hpp>
|
||||||
#include <gui/mtk/settings.hpp>
|
#include <gui/mtk/settings.hpp>
|
||||||
#include <gui/standalone.hpp>
|
#include <gui/standalone.hpp>
|
||||||
@@ -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
|
// 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
|
// the character the key produces unshifted, so no copy of the US scancode
|
||||||
// table is needed here to name the physical keys.
|
// 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) {
|
const mtk::Theme& theme) {
|
||||||
char text[2] = {(char)byte, '\0'};
|
if (cp == 0) {
|
||||||
if (byte == 0) {
|
|
||||||
c.text(x, y, "-", theme.text_muted);
|
c.text(x, y, "-", theme.text_muted);
|
||||||
} else {
|
return;
|
||||||
c.text(x, y, text, theme.text);
|
|
||||||
}
|
}
|
||||||
|
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) {
|
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;
|
if (index >= layout.key_count) break;
|
||||||
const montauk::keyboard::KeyMap& key = layout.keys[index];
|
const montauk::keyboard::KeyMap& key = layout.keys[index];
|
||||||
int y = list_y() + row * MAP_ROW_H;
|
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[0], y, key.base,
|
||||||
draw_char_cell(c, gx + col[1], y, key.shift, theme);
|
montauk::keyboard::is_dead_key(layout, key.base), theme);
|
||||||
draw_char_cell(c, gx + col[2], y, key.altgr, 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ extern "C" void _start() {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Handle scancodes for special keys
|
// Handle scancodes for special keys
|
||||||
switch (ev.scancode) {
|
switch (montauk::nav_scancode(ev)) {
|
||||||
case 0x48: // Up arrow
|
case 0x48: // Up arrow
|
||||||
if (scroll > 0) scroll--;
|
if (scroll > 0) scroll--;
|
||||||
break;
|
break;
|
||||||
|
|||||||
+14
-19
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <gui/gui.hpp>
|
#include <gui/gui.hpp>
|
||||||
#include <gui/mtk/settings.hpp>
|
#include <gui/mtk/settings.hpp>
|
||||||
@@ -415,11 +416,6 @@ static char ascii_lower(char c) {
|
|||||||
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : 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) {
|
static bool str_contains_case_insensitive(const char* text, const char* needle) {
|
||||||
if (!needle || !needle[0]) return true;
|
if (!needle || !needle[0]) return true;
|
||||||
if (!text || !text[0]) return false;
|
if (!text || !text[0]) return false;
|
||||||
@@ -516,18 +512,15 @@ static void rebuild_filtered_view() {
|
|||||||
ensure_selected_visible();
|
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);
|
int len = montauk::slen(g.filter_text);
|
||||||
if (len >= MAX_FILTER - 1) return;
|
if (montauk::utf8::append_key(g.filter_text, &len, MAX_FILTER, key))
|
||||||
g.filter_text[len] = c;
|
|
||||||
g.filter_text[len + 1] = '\0';
|
|
||||||
rebuild_filtered_view();
|
rebuild_filtered_view();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void pop_filter_char() {
|
static void pop_filter_char() {
|
||||||
int len = montauk::slen(g.filter_text);
|
int len = montauk::slen(g.filter_text);
|
||||||
if (len <= 0) return;
|
if (montauk::utf8::erase_last(g.filter_text, &len))
|
||||||
g.filter_text[len - 1] = '\0';
|
|
||||||
rebuild_filtered_view();
|
rebuild_filtered_view();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1120,8 +1113,10 @@ static void render(uint32_t* pixels) {
|
|||||||
int qlen = montauk::slen(query);
|
int qlen = montauk::slen(query);
|
||||||
if (qlen > 15) {
|
if (qlen > 15) {
|
||||||
montauk::memcpy(filter_query, "...", 3);
|
montauk::memcpy(filter_query, "...", 3);
|
||||||
montauk::memcpy(filter_query + 3, query + qlen - 15, 15);
|
int start = montauk::utf8::floor_boundary(query, qlen, qlen - 15);
|
||||||
filter_query[18] = '\0';
|
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;
|
query = filter_query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1426,11 +1421,11 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!g.show_visualizer && key.scancode == 0x48) { // Up
|
if (!g.show_visualizer && montauk::nav_scancode(key) == 0x48) { // Up
|
||||||
move_selection(-1);
|
move_selection(-1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!g.show_visualizer && key.scancode == 0x50) { // Down
|
if (!g.show_visualizer && montauk::nav_scancode(key) == 0x50) { // Down
|
||||||
move_selection(1);
|
move_selection(1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1449,8 +1444,8 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) {
|
|||||||
else g.filter_active = false;
|
else g.filter_active = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!key.ctrl && !key.alt && is_printable_byte(key.ascii)) {
|
if (!key.ctrl && !key.alt && montauk::utf8::key_codepoint(key)) {
|
||||||
append_filter_char(key.ascii);
|
append_filter_key(key);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -1479,11 +1474,11 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) {
|
|||||||
g.show_visualizer = !g.show_visualizer;
|
g.show_visualizer = !g.show_visualizer;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4D) { // Right arrow
|
if (montauk::nav_scancode(key) == 0x4D) { // Right arrow
|
||||||
next_track(false);
|
next_track(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4B) { // Left arrow
|
if (montauk::nav_scancode(key) == 0x4B) { // Left arrow
|
||||||
prev_track();
|
prev_track();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ static void fit_text(char* out, int out_cap, const char* text, int max_w) {
|
|||||||
break;
|
break;
|
||||||
out_len++;
|
out_len++;
|
||||||
}
|
}
|
||||||
|
out_len = montauk::utf8::floor_boundary(text, montauk::slen(text), out_len);
|
||||||
out[out_len] = '.';
|
out[out_len] = '.';
|
||||||
out[out_len + 1] = '.';
|
out[out_len + 1] = '.';
|
||||||
out[out_len + 2] = '.';
|
out[out_len + 2] = '.';
|
||||||
@@ -1216,8 +1217,8 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
wifi_join_selected();
|
wifi_join_selected();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x48 || key.scancode == 0x50) { // up / down
|
if (montauk::nav_scancode(key) == 0x48 || montauk::nav_scancode(key) == 0x50) { // up / down
|
||||||
int step = key.scancode == 0x48 ? -1 : 1;
|
int step = montauk::nav_scancode(key) == 0x48 ? -1 : 1;
|
||||||
int next = g_wifi_selected < 0 ? 0 : g_wifi_selected + step;
|
int next = g_wifi_selected < 0 ? 0 : g_wifi_selected + step;
|
||||||
if (next < 0) next = 0;
|
if (next < 0) next = 0;
|
||||||
if (next >= g_wifi_count) next = g_wifi_count - 1;
|
if (next >= g_wifi_count) next = g_wifi_count - 1;
|
||||||
|
|||||||
@@ -235,25 +235,13 @@ extern "C" void _start() {
|
|||||||
} else if (key.scancode == 0x01) {
|
} else if (key.scancode == 0x01) {
|
||||||
g_pathbar_open = false;
|
g_pathbar_open = false;
|
||||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (g_pathbar_cursor > 0) {
|
montauk::utf8::erase_before(g_pathbar_text, &g_pathbar_len, &g_pathbar_cursor);
|
||||||
for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++)
|
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
g_pathbar_text[i] = g_pathbar_text[i + 1];
|
g_pathbar_cursor = montauk::utf8::prev_boundary(g_pathbar_text, g_pathbar_cursor);
|
||||||
g_pathbar_len--;
|
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
g_pathbar_cursor--;
|
g_pathbar_cursor = montauk::utf8::next_boundary(g_pathbar_text, g_pathbar_len, g_pathbar_cursor);
|
||||||
g_pathbar_text[g_pathbar_len] = '\0';
|
} else {
|
||||||
}
|
montauk::utf8::insert_key(g_pathbar_text, &g_pathbar_len, 255, &g_pathbar_cursor, key);
|
||||||
} 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';
|
|
||||||
}
|
}
|
||||||
redraw = true;
|
redraw = true;
|
||||||
goto done_keys;
|
goto done_keys;
|
||||||
@@ -274,9 +262,9 @@ extern "C" void _start() {
|
|||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
// Page Down / Right arrow: next page
|
// 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 (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
|
// Right arrow scrolls down within page
|
||||||
} else {
|
} else {
|
||||||
go_to_page(g_current_page + 1);
|
go_to_page(g_current_page + 1);
|
||||||
@@ -285,9 +273,9 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Page Up / Left arrow: previous page
|
// 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 (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
|
// Left arrow scrolls up within page
|
||||||
} else {
|
} else {
|
||||||
go_to_page(g_current_page - 1);
|
go_to_page(g_current_page - 1);
|
||||||
@@ -296,23 +284,23 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Up arrow: scroll up
|
// Up arrow: scroll up
|
||||||
else if (key.scancode == 0x48) {
|
else if (montauk::nav_scancode(key) == 0x48) {
|
||||||
g_scroll_y -= SCROLL_STEP;
|
g_scroll_y -= SCROLL_STEP;
|
||||||
clamp_scroll();
|
clamp_scroll();
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
// Down arrow: scroll down
|
// Down arrow: scroll down
|
||||||
else if (key.scancode == 0x50) {
|
else if (montauk::nav_scancode(key) == 0x50) {
|
||||||
g_scroll_y += SCROLL_STEP;
|
g_scroll_y += SCROLL_STEP;
|
||||||
clamp_scroll();
|
clamp_scroll();
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
// Home: first page
|
// 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; }
|
if (g_doc.valid) { go_to_page(0); redraw = true; }
|
||||||
}
|
}
|
||||||
// End: last page
|
// 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; }
|
if (g_doc.valid) { go_to_page(g_doc.page_count - 1); redraw = true; }
|
||||||
}
|
}
|
||||||
// Ctrl++ or Ctrl+=: zoom in
|
// Ctrl++ or Ctrl+=: zoom in
|
||||||
|
|||||||
@@ -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) {
|
by0>-0.0001f&&by0<0.0001f&&by1>-0.0001f&&by1<0.0001f) {
|
||||||
float pen=0;
|
float pen=0;
|
||||||
for(int i=0;i<text_len;i++) {
|
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)continue;
|
||||||
if(glyph->bitmap) {
|
if(glyph->bitmap) {
|
||||||
int gx=base_x+floor_pixel(pen*g_zoom+0.5f)+glyph->xoff;
|
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;
|
float pen = 0;
|
||||||
for (int i = 0; i < text_len; i++) {
|
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) continue;
|
||||||
if (glyph->bitmap) {
|
if (glyph->bitmap) {
|
||||||
float gx0 = (float)(pen + glyph->xoff);
|
float gx0 = (float)(pen + glyph->xoff);
|
||||||
|
|||||||
@@ -143,14 +143,8 @@ static gui::TrueTypeFont* load_page_font(const char* path) {
|
|||||||
|
|
||||||
static void free_page_font(gui::TrueTypeFont* font) {
|
static void free_page_font(gui::TrueTypeFont* font) {
|
||||||
if (!font) return;
|
if (!font) return;
|
||||||
for (int i = 0; i < font->cache_count; i++) {
|
for (int i = 0; i < font->cache_count; i++)
|
||||||
for (int g = 0; g < 256; g++) {
|
gui::TrueTypeFont::free_cache(&font->caches[i]);
|
||||||
if (font->caches[i].glyphs[g].bitmap) {
|
|
||||||
montauk::mfree(font->caches[i].glyphs[g].bitmap);
|
|
||||||
font->caches[i].glyphs[g].bitmap = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (font->data) {
|
if (font->data) {
|
||||||
montauk::free(font->data);
|
montauk::free(font->data);
|
||||||
font->data = nullptr;
|
font->data = nullptr;
|
||||||
@@ -209,8 +203,8 @@ static void draw_text(uint8_t* rgb, PageFonts* fonts, bool bold,
|
|||||||
|
|
||||||
int cx = x;
|
int cx = x;
|
||||||
int baseline = y + gc->ascent;
|
int baseline = y + gc->ascent;
|
||||||
for (int i = 0; text[i] != '\0'; i++) {
|
for (const char* p = text; *p;) {
|
||||||
gui::CachedGlyph* glyph = font->get_glyph(gc, (unsigned char)text[i]);
|
gui::CachedGlyph* glyph = font->get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (!glyph) continue;
|
if (!glyph) continue;
|
||||||
|
|
||||||
if (glyph->bitmap) {
|
if (glyph->bitmap) {
|
||||||
|
|||||||
@@ -230,7 +230,8 @@ static void text_fit(const char* src, char* out, int out_len, int max_w) {
|
|||||||
int keep = montauk::slen(src);
|
int keep = montauk::slen(src);
|
||||||
while (keep > 0) {
|
while (keep > 0) {
|
||||||
char tmp[MAX_DEBUG_LEN];
|
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];
|
for (int i = 0; i < n; i++) tmp[i] = src[i];
|
||||||
tmp[n] = '\0';
|
tmp[n] = '\0';
|
||||||
str_append(tmp, ell, (int)sizeof(tmp));
|
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);
|
montauk::strncpy(out, tmp, out_len - 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
keep--;
|
keep = montauk::utf8::prev_boundary(src, keep);
|
||||||
}
|
}
|
||||||
montauk::strncpy(out, ell, out_len - 1);
|
montauk::strncpy(out, ell, out_len - 1);
|
||||||
}
|
}
|
||||||
@@ -1162,12 +1163,12 @@ static void handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4B) {
|
if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
cycle_tab(-1);
|
cycle_tab(-1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4D) {
|
if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
cycle_tab(1);
|
cycle_tab(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -527,6 +527,8 @@ static void draw_text_fit(Canvas& c, int x, int y,
|
|||||||
out++;
|
out++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out = montauk::utf8::floor_boundary(text, montauk::slen(text), out);
|
||||||
|
|
||||||
if (out <= 0) return;
|
if (out <= 0) return;
|
||||||
buf[out] = '.';
|
buf[out] = '.';
|
||||||
buf[out + 1] = '.';
|
buf[out + 1] = '.';
|
||||||
@@ -852,24 +854,24 @@ static bool handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4B) {
|
if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
cycle_tab(-1);
|
cycle_tab(-1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4D) {
|
if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
cycle_tab(1);
|
cycle_tab(1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x53) {
|
if (montauk::nav_scancode(key) == 0x53) {
|
||||||
if (can_kill_selected()) {
|
if (can_kill_selected()) {
|
||||||
kill_selected();
|
kill_selected();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x48) {
|
if (montauk::nav_scancode(key) == 0x48) {
|
||||||
if (g_pm.active_tab == PM_TAB_PROCESSES) {
|
if (g_pm.active_tab == PM_TAB_PROCESSES) {
|
||||||
if (g_pm.selected > 0) g_pm.selected--;
|
if (g_pm.selected > 0) g_pm.selected--;
|
||||||
else if (g_pm.proc_count > 0) g_pm.selected = 0;
|
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.active_tab == PM_TAB_PROCESSES) {
|
||||||
if (g_pm.selected < g_pm.proc_count - 1) g_pm.selected++;
|
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;
|
else if (g_pm.selected < 0 && g_pm.proc_count > 0) g_pm.selected = 0;
|
||||||
|
|||||||
+16
-11
@@ -92,7 +92,9 @@ static void prompt() {
|
|||||||
|
|
||||||
// ---- Line editing ----
|
// ---- 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('\b');
|
||||||
for (int i = 0; i < len; i++) montauk::putchar(' ');
|
for (int i = 0; i < len; i++) montauk::putchar(' ');
|
||||||
for (int i = 0; i < len; i++) montauk::putchar('\b');
|
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) {
|
static void replace_line(char* line, int* pos, const char* newContent) {
|
||||||
int oldLen = *pos;
|
int oldLen = *pos;
|
||||||
erase_input(oldLen);
|
erase_input(line, oldLen);
|
||||||
int newLen = slen(newContent);
|
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++) {
|
for (int i = 0; i < newLen; i++) {
|
||||||
line[i] = newContent[i];
|
line[i] = newContent[i];
|
||||||
montauk::putchar(newContent[i]);
|
montauk::putchar(newContent[i]);
|
||||||
@@ -431,14 +433,14 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
// Arrow keys: ascii == 0, check scancode
|
// Arrow keys: ascii == 0, check scancode
|
||||||
if (ev.ascii == 0) {
|
if (ev.ascii == 0) {
|
||||||
if (ev.scancode == SC_UP) {
|
if (montauk::nav_scancode(ev) == SC_UP) {
|
||||||
int next = hist_nav + 1;
|
int next = hist_nav + 1;
|
||||||
const char* entry = history_get(next);
|
const char* entry = history_get(next);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
hist_nav = next;
|
hist_nav = next;
|
||||||
replace_line(line, &pos, entry);
|
replace_line(line, &pos, entry);
|
||||||
}
|
}
|
||||||
} else if (ev.scancode == SC_DOWN) {
|
} else if (montauk::nav_scancode(ev) == SC_DOWN) {
|
||||||
if (hist_nav > 0) {
|
if (hist_nav > 0) {
|
||||||
hist_nav--;
|
hist_nav--;
|
||||||
const char* entry = history_get(hist_nav);
|
const char* entry = history_get(hist_nav);
|
||||||
@@ -447,7 +449,7 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
} else if (hist_nav == 0) {
|
} else if (hist_nav == 0) {
|
||||||
hist_nav = -1;
|
hist_nav = -1;
|
||||||
erase_input(pos);
|
erase_input(line, pos);
|
||||||
pos = 0;
|
pos = 0;
|
||||||
line[0] = '\0';
|
line[0] = '\0';
|
||||||
}
|
}
|
||||||
@@ -496,15 +498,18 @@ extern "C" void _start() {
|
|||||||
hist_nav = -1;
|
hist_nav = -1;
|
||||||
prompt();
|
prompt();
|
||||||
} else if (ev.ascii == '\b') {
|
} else if (ev.ascii == '\b') {
|
||||||
if (pos > 0) {
|
// One terminal cell per character, however many UTF-8 bytes
|
||||||
pos--;
|
line[pos] = '\0';
|
||||||
|
if (montauk::utf8::erase_last(line, &pos)) {
|
||||||
montauk::putchar('\b');
|
montauk::putchar('\b');
|
||||||
montauk::putchar(' ');
|
montauk::putchar(' ');
|
||||||
montauk::putchar('\b');
|
montauk::putchar('\b');
|
||||||
}
|
}
|
||||||
} else if (ev.ascii >= ' ' && pos < 255) {
|
} else {
|
||||||
line[pos++] = ev.ascii;
|
line[pos] = '\0';
|
||||||
montauk::putchar(ev.ascii);
|
int before = pos;
|
||||||
|
if (montauk::utf8::append_key(line, &pos, 256, ev))
|
||||||
|
montauk::print(line + before);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <montauk/config.h>
|
#include <montauk/config.h>
|
||||||
#include <montauk/capabilities.h>
|
#include <montauk/capabilities.h>
|
||||||
|
|||||||
@@ -415,25 +415,13 @@ extern "C" void _start() {
|
|||||||
} else if (key.scancode == 0x01) {
|
} else if (key.scancode == 0x01) {
|
||||||
g_pathbar_open = false;
|
g_pathbar_open = false;
|
||||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (g_pathbar_cursor > 0) {
|
montauk::utf8::erase_before(g_pathbar_text, &g_pathbar_len, &g_pathbar_cursor);
|
||||||
for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++)
|
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
g_pathbar_text[i] = g_pathbar_text[i + 1];
|
g_pathbar_cursor = montauk::utf8::prev_boundary(g_pathbar_text, g_pathbar_cursor);
|
||||||
g_pathbar_len--;
|
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
g_pathbar_cursor--;
|
g_pathbar_cursor = montauk::utf8::next_boundary(g_pathbar_text, g_pathbar_len, g_pathbar_cursor);
|
||||||
g_pathbar_text[g_pathbar_len] = '\0';
|
} else {
|
||||||
}
|
montauk::utf8::insert_key(g_pathbar_text, &g_pathbar_len, 255, &g_pathbar_cursor, key);
|
||||||
} 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';
|
|
||||||
}
|
}
|
||||||
redraw = true;
|
redraw = true;
|
||||||
goto done_keys;
|
goto done_keys;
|
||||||
@@ -441,12 +429,12 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
// Autocomplete interaction (when editing with autocomplete open)
|
// Autocomplete interaction (when editing with autocomplete open)
|
||||||
if (g_ac_open && g_editing) {
|
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;
|
g_ac_sel = (g_ac_sel - 1 + g_ac_count) % g_ac_count;
|
||||||
redraw = true;
|
redraw = true;
|
||||||
goto done_keys;
|
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;
|
g_ac_sel = (g_ac_sel + 1) % g_ac_count;
|
||||||
redraw = true;
|
redraw = true;
|
||||||
goto done_keys;
|
goto done_keys;
|
||||||
@@ -498,7 +486,7 @@ extern "C" void _start() {
|
|||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
// Arrow keys (with Shift = extend selection)
|
// 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 (key.shift) {
|
||||||
if (!g_has_selection) { g_anchor_col = g_sel_col; g_anchor_row = g_sel_row; g_has_selection = true; }
|
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--;
|
if (g_sel_row > 0) g_sel_row--;
|
||||||
@@ -509,7 +497,7 @@ extern "C" void _start() {
|
|||||||
ensure_sel_visible();
|
ensure_sel_visible();
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
else if (key.scancode == 0x50 && !g_editing) { // Down
|
else if (montauk::nav_scancode(key) == 0x50 && !g_editing) { // Down
|
||||||
if (key.shift) {
|
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_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++;
|
if (g_sel_row < MAX_ROWS - 1) g_sel_row++;
|
||||||
@@ -520,7 +508,7 @@ extern "C" void _start() {
|
|||||||
ensure_sel_visible();
|
ensure_sel_visible();
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
else if (key.scancode == 0x4B && !g_editing) { // Left
|
else if (montauk::nav_scancode(key) == 0x4B && !g_editing) { // Left
|
||||||
if (key.shift) {
|
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_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--;
|
if (g_sel_col > 0) g_sel_col--;
|
||||||
@@ -531,7 +519,7 @@ extern "C" void _start() {
|
|||||||
ensure_sel_visible();
|
ensure_sel_visible();
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
else if (key.scancode == 0x4D && !g_editing) { // Right
|
else if (montauk::nav_scancode(key) == 0x4D && !g_editing) { // Right
|
||||||
if (key.shift) {
|
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_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++;
|
if (g_sel_col < MAX_COLS - 1) g_sel_col++;
|
||||||
@@ -545,14 +533,8 @@ extern "C" void _start() {
|
|||||||
// Backspace
|
// Backspace
|
||||||
else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (g_editing) {
|
if (g_editing) {
|
||||||
if (g_edit_cursor > 0) {
|
if (montauk::utf8::erase_before(g_edit_buf, &g_edit_len, &g_edit_cursor))
|
||||||
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';
|
|
||||||
update_autocomplete();
|
update_autocomplete();
|
||||||
}
|
|
||||||
redraw = true;
|
redraw = true;
|
||||||
} else {
|
} else {
|
||||||
undo_push();
|
undo_push();
|
||||||
@@ -567,15 +549,10 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Delete key
|
// Delete key
|
||||||
else if (key.scancode == 0x53) {
|
else if (montauk::nav_scancode(key) == 0x53) {
|
||||||
if (g_editing) {
|
if (g_editing) {
|
||||||
if (g_edit_cursor < g_edit_len) {
|
if (montauk::utf8::erase_at(g_edit_buf, &g_edit_len, g_edit_cursor))
|
||||||
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';
|
|
||||||
update_autocomplete();
|
update_autocomplete();
|
||||||
}
|
|
||||||
redraw = true;
|
redraw = true;
|
||||||
} else {
|
} else {
|
||||||
undo_push();
|
undo_push();
|
||||||
@@ -590,18 +567,18 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Edit mode arrow keys (cursor movement within formula bar)
|
// Edit mode arrow keys (cursor movement within formula bar)
|
||||||
else if (key.scancode == 0x4B && g_editing) {
|
else if (montauk::nav_scancode(key) == 0x4B && g_editing) {
|
||||||
if (g_edit_cursor > 0) g_edit_cursor--;
|
g_edit_cursor = montauk::utf8::prev_boundary(g_edit_buf, g_edit_cursor);
|
||||||
update_autocomplete();
|
update_autocomplete();
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
else if (key.scancode == 0x4D && g_editing) {
|
else if (montauk::nav_scancode(key) == 0x4D && g_editing) {
|
||||||
if (g_edit_cursor < g_edit_len) g_edit_cursor++;
|
g_edit_cursor = montauk::utf8::next_boundary(g_edit_buf, g_edit_len, g_edit_cursor);
|
||||||
update_autocomplete();
|
update_autocomplete();
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
// Home key
|
// Home key
|
||||||
else if (key.scancode == 0x47) {
|
else if (montauk::nav_scancode(key) == 0x47) {
|
||||||
if (g_editing) {
|
if (g_editing) {
|
||||||
g_edit_cursor = 0;
|
g_edit_cursor = 0;
|
||||||
update_autocomplete();
|
update_autocomplete();
|
||||||
@@ -613,7 +590,7 @@ extern "C" void _start() {
|
|||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
// End key
|
// End key
|
||||||
else if (key.scancode == 0x4F) {
|
else if (montauk::nav_scancode(key) == 0x4F) {
|
||||||
if (g_editing) {
|
if (g_editing) {
|
||||||
g_edit_cursor = g_edit_len;
|
g_edit_cursor = g_edit_len;
|
||||||
update_autocomplete();
|
update_autocomplete();
|
||||||
@@ -670,23 +647,16 @@ extern "C" void _start() {
|
|||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
// Printable character: start editing or insert
|
// Printable character: start editing or insert
|
||||||
else if ((unsigned char)key.ascii >= 32
|
else if (montauk::utf8::key_codepoint(key) && !key.ctrl) {
|
||||||
&& (unsigned char)key.ascii != 127 && !key.ctrl) {
|
|
||||||
if (!g_editing) {
|
if (!g_editing) {
|
||||||
clear_selection();
|
clear_selection();
|
||||||
g_editing = true;
|
g_editing = true;
|
||||||
g_edit_buf[0] = key.ascii;
|
g_edit_buf[0] = '\0';
|
||||||
g_edit_buf[1] = '\0';
|
g_edit_len = 0;
|
||||||
g_edit_len = 1;
|
g_edit_cursor = 0;
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
montauk::utf8::insert_key(g_edit_buf, &g_edit_len, CELL_TEXT_MAX,
|
||||||
|
&g_edit_cursor, key);
|
||||||
update_autocomplete();
|
update_autocomplete();
|
||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <montauk/service_log.h>
|
#include <montauk/service_log.h>
|
||||||
#include <montauk/ssh.h>
|
#include <montauk/ssh.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <libc/stdio.h>
|
#include <libc/stdio.h>
|
||||||
|
|
||||||
extern "C" {
|
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. */
|
/* Remaining C0 codes are the control-modified letters. */
|
||||||
key.ascii = (char)('a' + value - 1);
|
key.ascii = (char)('a' + value - 1);
|
||||||
key.ctrl = true;
|
key.ctrl = true;
|
||||||
} else {
|
} else if (value < 0x80) {
|
||||||
key.ascii = (char)value;
|
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);
|
montauk::childio_writekey(pid, &key);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
|
||||||
using montauk::skip_spaces;
|
using montauk::skip_spaces;
|
||||||
@@ -176,15 +177,18 @@ extern "C" void _start() {
|
|||||||
montauk::send(fd, sendBuf, sendPos);
|
montauk::send(fd, sendBuf, sendPos);
|
||||||
sendPos = 0;
|
sendPos = 0;
|
||||||
} else if (ev.ascii == '\b') {
|
} else if (ev.ascii == '\b') {
|
||||||
if (sendPos > 0) {
|
sendBuf[sendPos] = '\0';
|
||||||
sendPos--;
|
if (montauk::utf8::erase_last(sendBuf, &sendPos)) {
|
||||||
montauk::putchar('\b');
|
montauk::putchar('\b');
|
||||||
montauk::putchar(' ');
|
montauk::putchar(' ');
|
||||||
montauk::putchar('\b');
|
montauk::putchar('\b');
|
||||||
}
|
}
|
||||||
} else if (ev.ascii >= ' ' && sendPos < 254) {
|
} else {
|
||||||
sendBuf[sendPos++] = ev.ascii;
|
// Typed text is sent as UTF-8; leave room for the '\n'
|
||||||
montauk::putchar(ev.ascii);
|
sendBuf[sendPos] = '\0';
|
||||||
|
int before = sendPos;
|
||||||
|
if (montauk::utf8::append_key(sendBuf, &sendPos, 255, ev))
|
||||||
|
montauk::print(sendBuf + before);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
montauk::wait_handle(fd, montauk::abi::IPC_SIGNAL_READABLE | montauk::abi::IPC_SIGNAL_PEER_CLOSED, 10);
|
montauk::wait_handle(fd, montauk::abi::IPC_SIGNAL_READABLE | montauk::abi::IPC_SIGNAL_PEER_CLOSED, 10);
|
||||||
|
|||||||
@@ -479,9 +479,9 @@ static void handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
step_font_size(FONT_SIZE_STEP);
|
step_font_size(FONT_SIZE_STEP);
|
||||||
} else if (key.ascii == '-') {
|
} else if (key.ascii == '-') {
|
||||||
step_font_size(-FONT_SIZE_STEP);
|
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);
|
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);
|
select_theme(g_panel.theme_index + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ int g_line_count = 0;
|
|||||||
// Cursor
|
// Cursor
|
||||||
int g_cursor_pos = 0; // byte position
|
int g_cursor_pos = 0; // byte position
|
||||||
int g_cursor_line = 0;
|
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_y = 0; // first visible line
|
||||||
int g_scroll_x = 0; // horizontal scroll in pixels
|
int g_scroll_x = 0; // horizontal scroll in pixels
|
||||||
bool g_scroll_dragging = false;
|
bool g_scroll_dragging = false;
|
||||||
@@ -264,7 +264,8 @@ static void update_cursor_pos() {
|
|||||||
break;
|
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;
|
g_cursor_moved = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +276,12 @@ static int line_length(int line) {
|
|||||||
return end - start;
|
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
|
// Buffer operations
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -294,17 +301,10 @@ static void delete_range(int start, int count) {
|
|||||||
g_buf_len -= count;
|
g_buf_len -= count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void insert_string(const char* s, int len);
|
||||||
|
|
||||||
static void insert_char(char c) {
|
static void insert_char(char c) {
|
||||||
if (g_buf_len >= MAX_CAP - 1) return;
|
insert_string(&c, 1);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void insert_string(const char* s, int len) {
|
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() {
|
static void backspace() {
|
||||||
if (g_cursor_pos <= 0) return;
|
if (g_cursor_pos <= 0) return;
|
||||||
g_cursor_pos--;
|
int start = montauk::utf8::prev_boundary(g_buffer, g_cursor_pos);
|
||||||
delete_range(g_cursor_pos, 1);
|
delete_range(start, g_cursor_pos - start);
|
||||||
|
g_cursor_pos = start;
|
||||||
g_modified = true;
|
g_modified = true;
|
||||||
recompute_lines();
|
recompute_lines();
|
||||||
update_cursor_pos();
|
update_cursor_pos();
|
||||||
@@ -332,7 +333,8 @@ static void backspace() {
|
|||||||
|
|
||||||
static void delete_char() {
|
static void delete_char() {
|
||||||
if (g_cursor_pos >= g_buf_len) return;
|
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;
|
g_modified = true;
|
||||||
recompute_lines();
|
recompute_lines();
|
||||||
update_cursor_pos();
|
update_cursor_pos();
|
||||||
@@ -344,30 +346,28 @@ static void delete_char() {
|
|||||||
|
|
||||||
static void move_up() {
|
static void move_up() {
|
||||||
if (g_cursor_line <= 0) return;
|
if (g_cursor_line <= 0) return;
|
||||||
int col = g_cursor_col;
|
g_cursor_pos = line_col_to_pos(g_cursor_line - 1, 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;
|
|
||||||
update_cursor_pos();
|
update_cursor_pos();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void move_down() {
|
static void move_down() {
|
||||||
if (g_cursor_line >= g_line_count - 1) return;
|
if (g_cursor_line >= g_line_count - 1) return;
|
||||||
int col = g_cursor_col;
|
g_cursor_pos = line_col_to_pos(g_cursor_line + 1, 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;
|
|
||||||
update_cursor_pos();
|
update_cursor_pos();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void move_left() {
|
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() {
|
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() {
|
static void move_home() {
|
||||||
@@ -621,6 +621,25 @@ static bool load_file(const char* path) {
|
|||||||
return false;
|
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) {
|
if (got >= g_buf_cap) {
|
||||||
int new_cap = got + 1024;
|
int new_cap = got + 1024;
|
||||||
if (new_cap > MAX_CAP) new_cap = MAX_CAP;
|
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);
|
g_syntax_language = syn_detect_language(g_filepath);
|
||||||
clear_selection();
|
clear_selection();
|
||||||
set_status("");
|
set_status(converted ? "Converted from Windows-1252 to UTF-8" : "");
|
||||||
|
|
||||||
recompute_lines();
|
recompute_lines();
|
||||||
update_cursor_pos();
|
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;
|
int clicked_col = (mx - LINE_NUM_W - 4 + g_scroll_x + cell_w / 2) / cell_w;
|
||||||
if (clicked_col < 0) clicked_col = 0;
|
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) {
|
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_highlight_line(g_buffer + line_start, line_len, syn_line_buf,
|
||||||
SYN_LINE_BUF_CAP, g_syntax_language, syn_state);
|
SYN_LINE_BUF_CAP, g_syntax_language, syn_state);
|
||||||
|
|
||||||
for (int ci = 0; ci < line_len; ci++) {
|
// One monospace cell per character; ci walks bytes, col walks cells.
|
||||||
int px_x = text_start_x + ci * cell_w - g_scroll_x;
|
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 + cell_w <= LINE_NUM_W + 1) continue;
|
||||||
if (px_x >= text_area_w) break;
|
if (px_x >= text_area_w) break;
|
||||||
|
|
||||||
int byte_pos = line_start + ci;
|
|
||||||
char ch = g_buffer[byte_pos];
|
|
||||||
|
|
||||||
// Selection highlight
|
// Selection highlight
|
||||||
if (g_has_selection && byte_pos >= sel_s && byte_pos < sel_e) {
|
if (g_has_selection && byte_pos >= sel_s && byte_pos < sel_e) {
|
||||||
int hl_h = cell_h;
|
int hl_h = cell_h;
|
||||||
@@ -878,22 +900,24 @@ static void render(uint32_t* pixels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color char_color = TEXT_COLOR;
|
Color char_color = TEXT_COLOR;
|
||||||
if (g_syntax_language != SYN_LANG_NONE && ci < syn_len)
|
if (g_syntax_language != SYN_LANG_NONE && syn_index < syn_len)
|
||||||
char_color = syn_color(syn_line_buf[ci]);
|
char_color = syn_color(syn_line_buf[syn_index]);
|
||||||
|
|
||||||
if (ch >= 32 || ch < 0) {
|
if (ch >= 32) {
|
||||||
if (gc) {
|
if (gc) {
|
||||||
g_font_mono->draw_char_to_buffer(pixels, W, H, px_x, py + gc->ascent,
|
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
|
// Selection highlight for newline at end of line
|
||||||
if (g_has_selection && line + 1 < g_line_count) {
|
if (g_has_selection && line + 1 < g_line_count) {
|
||||||
int nl_pos = line_start + line_len;
|
int nl_pos = line_start + line_len;
|
||||||
if (nl_pos >= sel_s && nl_pos < sel_e) {
|
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) {
|
if (px_x > LINE_NUM_W && px_x < text_area_w) {
|
||||||
int hl_h = cell_h;
|
int hl_h = cell_h;
|
||||||
if (py + hl_h > editor_y + text_area_h) hl_h = editor_y + text_area_h - py;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (g_pathbar_cursor > 0) {
|
montauk::utf8::erase_before(g_pathbar_text, &g_pathbar_len, &g_pathbar_cursor);
|
||||||
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';
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4B) { if (g_pathbar_cursor > 0) g_pathbar_cursor--; return true; }
|
if (montauk::nav_scancode(key) == 0x4B) { g_pathbar_cursor = montauk::utf8::prev_boundary(g_pathbar_text, g_pathbar_cursor); return true; }
|
||||||
if (key.scancode == 0x4D) { if (g_pathbar_cursor < g_pathbar_len) 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 ((unsigned char)key.ascii >= 32
|
if (montauk::utf8::insert_key(g_pathbar_text, &g_pathbar_len, 255, &g_pathbar_cursor, key)) {
|
||||||
&& (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';
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -1123,19 +1134,19 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Arrow keys
|
// 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();
|
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
||||||
move_up();
|
move_up();
|
||||||
if (key.shift) update_selection();
|
if (key.shift) update_selection();
|
||||||
return true;
|
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();
|
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
||||||
move_down();
|
move_down();
|
||||||
if (key.shift) update_selection();
|
if (key.shift) update_selection();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4B) { // Left
|
if (montauk::nav_scancode(key) == 0x4B) { // Left
|
||||||
if (key.shift) {
|
if (key.shift) {
|
||||||
start_selection(); move_left(); update_selection();
|
start_selection(); move_left(); update_selection();
|
||||||
} else if (g_has_selection) {
|
} else if (g_has_selection) {
|
||||||
@@ -1144,7 +1155,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
|||||||
} else move_left();
|
} else move_left();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4D) { // Right
|
if (montauk::nav_scancode(key) == 0x4D) { // Right
|
||||||
if (key.shift) {
|
if (key.shift) {
|
||||||
start_selection(); move_right(); update_selection();
|
start_selection(); move_right(); update_selection();
|
||||||
} else if (g_has_selection) {
|
} else if (g_has_selection) {
|
||||||
@@ -1155,14 +1166,14 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Home
|
// Home
|
||||||
if (key.scancode == 0x47) {
|
if (montauk::nav_scancode(key) == 0x47) {
|
||||||
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
||||||
move_home();
|
move_home();
|
||||||
if (key.shift) update_selection();
|
if (key.shift) update_selection();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// End
|
// End
|
||||||
if (key.scancode == 0x4F) {
|
if (montauk::nav_scancode(key) == 0x4F) {
|
||||||
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
||||||
move_end();
|
move_end();
|
||||||
if (key.shift) update_selection();
|
if (key.shift) update_selection();
|
||||||
@@ -1170,7 +1181,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Page Up
|
// Page Up
|
||||||
if (key.scancode == 0x49) {
|
if (montauk::nav_scancode(key) == 0x49) {
|
||||||
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
||||||
int cell_h = font_cell_h();
|
int cell_h = font_cell_h();
|
||||||
int vis = cell_h > 0 ? (g_win_h - TOOLBAR_H - STATUS_H) / cell_h : 20;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
// Page Down
|
// Page Down
|
||||||
if (key.scancode == 0x51) {
|
if (montauk::nav_scancode(key) == 0x51) {
|
||||||
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
if (key.shift) start_selection(); else if (g_has_selection) clear_selection();
|
||||||
int cell_h = font_cell_h();
|
int cell_h = font_cell_h();
|
||||||
int vis = cell_h > 0 ? (g_win_h - TOOLBAR_H - STATUS_H) / cell_h : 20;
|
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
|
// Delete
|
||||||
if (key.scancode == 0x53) {
|
if (montauk::nav_scancode(key) == 0x53) {
|
||||||
if (g_has_selection) delete_selection(); else delete_char();
|
if (g_has_selection) delete_selection(); else delete_char();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1214,10 +1225,12 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printable
|
// Printable (inserted as UTF-8)
|
||||||
if ((unsigned char)key.ascii >= 32 && (unsigned char)key.ascii != 127) {
|
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();
|
if (g_has_selection) delete_selection();
|
||||||
insert_char(key.ascii);
|
insert_string(typed, typed_len);
|
||||||
return true;
|
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 >= g_scroll_y + visible_lines) new_line = g_scroll_y + visible_lines - 1;
|
||||||
if (new_line < 0) new_line = 0;
|
if (new_line < 0) new_line = 0;
|
||||||
if (new_line >= g_line_count) new_line = g_line_count - 1;
|
if (new_line >= g_line_count) new_line = g_line_count - 1;
|
||||||
int col = g_cursor_col;
|
g_cursor_pos = line_col_to_pos(new_line, g_cursor_col);
|
||||||
int ll = line_length(new_line);
|
update_cursor_pos();
|
||||||
if (col > ll) col = ll;
|
|
||||||
g_cursor_pos = g_line_offsets[new_line] + col;
|
|
||||||
g_cursor_line = new_line;
|
|
||||||
g_cursor_col = col;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -455,7 +455,8 @@ static void fit_text(char* out, int cap, const char* text, int max_w) {
|
|||||||
static constexpr const char* ellipsis = "...";
|
static constexpr const char* ellipsis = "...";
|
||||||
int len = montauk::slen(out);
|
int len = montauk::slen(out);
|
||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
out[--len] = '\0';
|
len = montauk::utf8::prev_boundary(out, len);
|
||||||
|
out[len] = '\0';
|
||||||
char candidate[160];
|
char candidate[160];
|
||||||
snprintf(candidate, sizeof(candidate), "%s%s", out, ellipsis);
|
snprintf(candidate, sizeof(candidate), "%s%s", out, ellipsis);
|
||||||
if (text_width(candidate) <= max_w) {
|
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;
|
int old_country = g_selected_country;
|
||||||
|
|
||||||
if (key.scancode == 0x4B) g_focus_pane = 0; // Left
|
if (montauk::nav_scancode(key) == 0x4B) g_focus_pane = 0; // Left
|
||||||
else if (key.scancode == 0x4D) g_focus_pane = 1; // Right
|
else if (montauk::nav_scancode(key) == 0x4D) g_focus_pane = 1; // Right
|
||||||
else if (key.scancode == 0x48 && *selected > 0) (*selected)--; // Up
|
else if (montauk::nav_scancode(key) == 0x48 && *selected > 0) (*selected)--; // Up
|
||||||
else if (key.scancode == 0x50 && *selected < count - 1) (*selected)++; // Down
|
else if (montauk::nav_scancode(key) == 0x50 && *selected < count - 1) (*selected)++; // Down
|
||||||
else if (key.scancode == 0x49) *selected = gui_max(0, *selected - 8); // Page Up
|
else if (montauk::nav_scancode(key) == 0x49) *selected = gui_max(0, *selected - 8); // Page Up
|
||||||
else if (key.scancode == 0x51) *selected = gui_min(count - 1, *selected + 8); // Page Down
|
else if (montauk::nav_scancode(key) == 0x51) *selected = gui_min(count - 1, *selected + 8); // Page Down
|
||||||
else return false;
|
else return false;
|
||||||
|
|
||||||
if (old_country != g_selected_country) {
|
if (old_country != g_selected_country) {
|
||||||
|
|||||||
@@ -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);
|
int len = montauk::slen(out);
|
||||||
const char* ellipsis = "...";
|
const char* ellipsis = "...";
|
||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
len--;
|
len = montauk::utf8::prev_boundary(out, len);
|
||||||
out[len] = '\0';
|
out[len] = '\0';
|
||||||
char trial[160];
|
char trial[160];
|
||||||
montauk::strncpy(trial, out, (int)sizeof(trial));
|
montauk::strncpy(trial, out, (int)sizeof(trial));
|
||||||
@@ -1235,14 +1235,14 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4B) {
|
if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
uint64_t cur = current_time_us();
|
uint64_t cur = current_time_us();
|
||||||
if (cur > 5000000ull) cur -= 5000000ull;
|
if (cur > 5000000ull) cur -= 5000000ull;
|
||||||
else cur = 0;
|
else cur = 0;
|
||||||
seek_to_time(cur);
|
seek_to_time(cur);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4D) {
|
if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
seek_to_time(current_time_us() + 5000000ull);
|
seek_to_time(current_time_us() + 5000000ull);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,9 +251,9 @@ static void do_fetch() {
|
|||||||
extract_array_value(body, bodyLen, "\"areaName\"", area, sizeof(area));
|
extract_array_value(body, bodyLen, "\"areaName\"", area, sizeof(area));
|
||||||
extract_array_value(body, bodyLen, "\"country\"", country, sizeof(country));
|
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)
|
// Degree sign U+00B0 (UTF-8)
|
||||||
snprintf(g_temp, sizeof(g_temp), "%s\xb0""C", temp_raw);
|
snprintf(g_temp, sizeof(g_temp), "%s\u00B0C", temp_raw);
|
||||||
snprintf(g_feels, sizeof(g_feels), "Feels like: %s\xb0""C", feels_raw);
|
snprintf(g_feels, sizeof(g_feels), "Feels like: %s\u00B0C", feels_raw);
|
||||||
|
|
||||||
if (area[0] && country[0])
|
if (area[0] && country[0])
|
||||||
snprintf(g_location, sizeof(g_location), "%s, %s", area, country);
|
snprintf(g_location, sizeof(g_location), "%s, %s", area, country);
|
||||||
|
|||||||
+24
-10
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <http/http.hpp>
|
#include <http/http.hpp>
|
||||||
|
|
||||||
extern "C" {
|
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;
|
else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10;
|
||||||
}
|
}
|
||||||
p += 4;
|
p += 4;
|
||||||
if (val < 128) out[j++] = (char)val;
|
// The terminal renders UTF-8; join surrogate pairs
|
||||||
else if (val == 0x2013 || val == 0x2014) out[j++] = '-';
|
if (val >= 0xD800 && val <= 0xDBFF && p + 6 < len &&
|
||||||
else if (val == 0x2018 || val == 0x2019) out[j++] = '\'';
|
buf[p + 1] == '\\' && buf[p + 2] == 'u') {
|
||||||
else if (val == 0x201C || val == 0x201D) out[j++] = '"';
|
unsigned lo = 0;
|
||||||
else if (val == 0x2026) { out[j++] = '.'; out[j++] = '.'; out[j++] = '.'; }
|
for (int k = 3; k <= 6; k++) {
|
||||||
else out[j++] = '?';
|
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;
|
break;
|
||||||
}
|
}
|
||||||
@@ -219,9 +231,12 @@ static int wrap_paragraph(const char* text, int textLen, int cols,
|
|||||||
const char* lastSpace = nullptr;
|
const char* lastSpace = nullptr;
|
||||||
int col = 0;
|
int col = 0;
|
||||||
|
|
||||||
|
// One terminal column per UTF-8 character
|
||||||
while (p < end && col < cols) {
|
while (p < end && col < cols) {
|
||||||
if (*p == ' ') lastSpace = p;
|
if (*p == ' ') lastSpace = p;
|
||||||
p++;
|
int n;
|
||||||
|
montauk::utf8::decode(p, (int)(end - p), &n);
|
||||||
|
p += n;
|
||||||
col++;
|
col++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,8 +391,7 @@ static void render_pager(WikiLine* lines, int totalLines, int scroll,
|
|||||||
int maxW = cols;
|
int maxW = cols;
|
||||||
if (ln.type == LINE_SECTION && ln.level > 2)
|
if (ln.type == LINE_SECTION && ln.level > 2)
|
||||||
maxW -= (ln.level - 2) * 2;
|
maxW -= (ln.level - 2) * 2;
|
||||||
int printLen = ln.len;
|
int printLen = montauk::utf8::offset_of(ln.text, ln.len, maxW < 0 ? 0 : maxW);
|
||||||
if (printLen > maxW) printLen = maxW;
|
|
||||||
for (int c = 0; c < printLen; c++)
|
for (int c = 0; c < printLen; c++)
|
||||||
sb_putc(ln.text[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 = 0; break;
|
||||||
case 'G': scroll = maxScroll; break;
|
case 'G': scroll = maxScroll; break;
|
||||||
default:
|
default:
|
||||||
switch (ev.scancode) {
|
switch (montauk::nav_scancode(ev)) {
|
||||||
case 0x48: if (scroll > 0) scroll--; break; // Up
|
case 0x48: if (scroll > 0) scroll--; break; // Up
|
||||||
case 0x50: if (scroll < maxScroll) scroll++; break; // Down
|
case 0x50: if (scroll < maxScroll) scroll++; break; // Down
|
||||||
case 0x49: scroll -= contentRows; if (scroll < 0) scroll = 0; break; // PgUp
|
case 0x49: scroll -= contentRows; if (scroll < 0) scroll = 0; break; // PgUp
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ static bool init_buffers_and_fonts() {
|
|||||||
|
|
||||||
static void handle_key_event(const montauk::abi::WinEvent& ev, bool& search_pending) {
|
static void handle_key_event(const montauk::abi::WinEvent& ev, bool& search_pending) {
|
||||||
uint8_t ascii = ev.key.ascii;
|
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 (g_search_focused) {
|
||||||
if (ascii == '\n' || ascii == '\r') {
|
if (ascii == '\n' || ascii == '\r') {
|
||||||
|
|||||||
@@ -10,64 +10,12 @@
|
|||||||
// Codepoint and UTF-8 helpers
|
// Codepoint and UTF-8 helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
int encode_single_byte_codepoint(unsigned codepoint) {
|
// Article text is kept as UTF-8, which the renderer draws directly.
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void append_codepoint(char* out, int* j, int maxOut, unsigned codepoint) {
|
void append_codepoint(char* out, int* j, int maxOut, unsigned codepoint) {
|
||||||
if (!out || !j || *j >= maxOut - 1) return;
|
if (!out || !j) return;
|
||||||
|
int n = montauk::utf8::encoded_length(codepoint);
|
||||||
int encoded = encode_single_byte_codepoint(codepoint);
|
if (*j + n > maxOut - 1) return;
|
||||||
if (encoded >= 0) {
|
*j += montauk::utf8::encode(codepoint, out + *j);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void underscores_to_spaces(char* text) {
|
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 '/': out[j++] = '/'; break;
|
||||||
case 'u': {
|
case 'u': {
|
||||||
if (p + 4 < len) {
|
if (p + 4 < len) {
|
||||||
|
auto hex4 = [&](int at) {
|
||||||
unsigned val = 0;
|
unsigned val = 0;
|
||||||
for (int k = 1; k <= 4; k++) {
|
for (int k = 0; k < 4; k++) {
|
||||||
char h = buf[p + k]; val <<= 4;
|
char h = buf[at + k]; val <<= 4;
|
||||||
if (h>='0'&&h<='9') val |= h-'0';
|
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;
|
||||||
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;
|
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);
|
append_codepoint(out, &j, maxOut, val);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <gui/mtk.hpp>
|
#include <gui/mtk.hpp>
|
||||||
#include <gui/mtk/settings.hpp>
|
#include <gui/mtk/settings.hpp>
|
||||||
@@ -184,7 +185,6 @@ int document_top_pad();
|
|||||||
// text.cpp -- encoding, HTTP/URL/JSON parsing
|
// 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 append_codepoint(char* out, int* j, int maxOut, unsigned codepoint);
|
||||||
void underscores_to_spaces(char* text);
|
void underscores_to_spaces(char* text);
|
||||||
bool is_main_page_query(const char* query);
|
bool is_main_page_query(const char* query);
|
||||||
|
|||||||
@@ -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);
|
wp_font_at(wp, abs_pos, layout_dpi, &font, &gc);
|
||||||
if (!font || !gc) return wp_points_to_pixels(6, layout_dpi);
|
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);
|
return g ? g->advance : wp_points_to_pixels(6, layout_dpi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,17 +14,46 @@ static void wp_set_status(WordProcessorState* wp, const char* msg) {
|
|||||||
wp->status_msg[sizeof(wp->status_msg) - 1] = '\0';
|
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) {
|
static bool wp_copy_selection_to_clipboard(WordProcessorState* wp) {
|
||||||
int len = 0;
|
int len = 0;
|
||||||
char* text = wp_selection_plain_text_alloc(wp, &len);
|
char* raw = wp_selection_plain_text_alloc(wp, &len);
|
||||||
if (text == nullptr || len <= 0) {
|
if (raw == nullptr || len <= 0) {
|
||||||
if (text) montauk::mfree(text);
|
if (raw) montauk::mfree(raw);
|
||||||
wp_set_status(wp, "Nothing selected");
|
wp_set_status(wp, "Nothing selected");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = gui::clipboard_set_text(text, len);
|
char* text = wp_cp1252_to_utf8_alloc(raw, len, &len);
|
||||||
montauk::mfree(text);
|
montauk::mfree(raw);
|
||||||
|
bool ok = text && gui::clipboard_set_text(text, len);
|
||||||
|
if (text) montauk::mfree(text);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
wp_set_status(wp, "Copy failed");
|
wp_set_status(wp, "Copy failed");
|
||||||
return false;
|
return false;
|
||||||
@@ -52,6 +81,7 @@ static bool wp_paste_from_clipboard(WordProcessorState* wp) {
|
|||||||
wp_set_status(wp, "Clipboard is empty");
|
wp_set_status(wp, "Clipboard is empty");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
len = wp_utf8_to_cp1252_inplace(text, len);
|
||||||
|
|
||||||
bool ok = wp_replace_selection_with_text(wp, text, len);
|
bool ok = wp_replace_selection_with_text(wp, text, len);
|
||||||
montauk::mfree(text);
|
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++) {
|
for (int ci = 0; ci < to_check; ci++) {
|
||||||
char ch = r->text[ro + ci];
|
char ch = r->text[ro + ci];
|
||||||
CachedGlyph* g = (ch >= 32 || ch < 0) && ch != '\n'
|
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;
|
int char_w = g ? g->advance : 0;
|
||||||
|
|
||||||
if (x + char_w / 2 > click_x) return best_abs;
|
if (x + char_w / 2 > click_x) return best_abs;
|
||||||
@@ -611,32 +641,18 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (wp->pathbar_cursor > 0) {
|
montauk::utf8::erase_before(wp->pathbar_text, &wp->pathbar_len, &wp->pathbar_cursor);
|
||||||
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';
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4B) {
|
if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
if (wp->pathbar_cursor > 0) wp->pathbar_cursor--;
|
wp->pathbar_cursor = montauk::utf8::prev_boundary(wp->pathbar_text, wp->pathbar_cursor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4D) {
|
if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
if (wp->pathbar_cursor < wp->pathbar_len) wp->pathbar_cursor++;
|
wp->pathbar_cursor = montauk::utf8::next_boundary(wp->pathbar_text, wp->pathbar_len, wp->pathbar_cursor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((unsigned char)key.ascii >= 32
|
montauk::utf8::insert_key(wp->pathbar_text, &wp->pathbar_len, 255, &wp->pathbar_cursor, key);
|
||||||
&& (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';
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,22 +752,22 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return;
|
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_adjust_paragraph_spacing_before(wp, -WP_SPACE_STEP);
|
||||||
wp_history_checkpoint(wp);
|
wp_history_checkpoint(wp);
|
||||||
return;
|
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_adjust_paragraph_spacing_before(wp, WP_SPACE_STEP);
|
||||||
wp_history_checkpoint(wp);
|
wp_history_checkpoint(wp);
|
||||||
return;
|
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_adjust_paragraph_spacing_after(wp, -WP_SPACE_STEP);
|
||||||
wp_history_checkpoint(wp);
|
wp_history_checkpoint(wp);
|
||||||
return;
|
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_adjust_paragraph_spacing_after(wp, WP_SPACE_STEP);
|
||||||
wp_history_checkpoint(wp);
|
wp_history_checkpoint(wp);
|
||||||
return;
|
return;
|
||||||
@@ -783,21 +799,21 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x48) {
|
if (montauk::nav_scancode(key) == 0x48) {
|
||||||
if (key.shift) wp_start_selection(wp);
|
if (key.shift) wp_start_selection(wp);
|
||||||
else wp_clear_selection(wp);
|
else wp_clear_selection(wp);
|
||||||
wp_cursor_up(wp);
|
wp_cursor_up(wp);
|
||||||
if (key.shift) wp_update_selection_to_cursor(wp);
|
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x50) {
|
if (montauk::nav_scancode(key) == 0x50) {
|
||||||
if (key.shift) wp_start_selection(wp);
|
if (key.shift) wp_start_selection(wp);
|
||||||
else wp_clear_selection(wp);
|
else wp_clear_selection(wp);
|
||||||
wp_cursor_down(wp);
|
wp_cursor_down(wp);
|
||||||
if (key.shift) wp_update_selection_to_cursor(wp);
|
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4B) {
|
if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
if (key.shift) wp_start_selection(wp);
|
if (key.shift) wp_start_selection(wp);
|
||||||
else if (wp->has_selection) {
|
else if (wp->has_selection) {
|
||||||
int s, e;
|
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);
|
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4D) {
|
if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
if (key.shift) wp_start_selection(wp);
|
if (key.shift) wp_start_selection(wp);
|
||||||
else if (wp->has_selection) {
|
else if (wp->has_selection) {
|
||||||
int s, e;
|
int s, e;
|
||||||
@@ -824,7 +840,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x47) {
|
if (montauk::nav_scancode(key) == 0x47) {
|
||||||
if (key.shift) wp_start_selection(wp);
|
if (key.shift) wp_start_selection(wp);
|
||||||
else wp_clear_selection(wp);
|
else wp_clear_selection(wp);
|
||||||
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
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);
|
if (key.shift) wp_update_selection_to_cursor(wp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.scancode == 0x4F) {
|
if (montauk::nav_scancode(key) == 0x4F) {
|
||||||
if (key.shift) wp_start_selection(wp);
|
if (key.shift) wp_start_selection(wp);
|
||||||
else wp_clear_selection(wp);
|
else wp_clear_selection(wp);
|
||||||
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x53) {
|
if (montauk::nav_scancode(key) == 0x53) {
|
||||||
if (wp->has_selection) wp_delete_selection(wp);
|
if (wp->has_selection) wp_delete_selection(wp);
|
||||||
else wp_delete_char(wp);
|
else wp_delete_char(wp);
|
||||||
wp_history_checkpoint(wp);
|
wp_history_checkpoint(wp);
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ static void wp_draw_print_list_marker(Canvas& c, WordProcessorState* wp, WrapLin
|
|||||||
if (!font) font = g_ui_font;
|
if (!font) font = g_ui_font;
|
||||||
int th = text_height(font, font_px);
|
int th = text_height(font, font_px);
|
||||||
int top_y = py + (wl->height - th) / 2;
|
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);
|
draw_text(c, font, bullet_x, top_y, bullet, colors::BLACK, font_px);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -293,7 +293,8 @@ static bool wp_encode_print_page(WordProcessorState* wp,
|
|||||||
|
|
||||||
if (ch != '\n' && (ch >= 32 || ch < 0)) {
|
if (ch != '\n' && (ch >= 32 || ch < 0)) {
|
||||||
int adv = font->draw_char_to_buffer(
|
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;
|
x += adv;
|
||||||
}
|
}
|
||||||
char_idx++;
|
char_idx++;
|
||||||
|
|||||||
@@ -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;
|
if (text_width(g_ui_font, out, fonts::UI_SIZE) <= max_w) return;
|
||||||
|
|
||||||
int slen = montauk::slen(src);
|
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];
|
char candidate[256];
|
||||||
candidate[0] = '.';
|
candidate[0] = '.';
|
||||||
candidate[1] = '.';
|
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';
|
out[out_len - 1] = '\0';
|
||||||
if (text_width(g_ui_font, out, fonts::UI_SIZE) <= max_w) return;
|
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];
|
char candidate[256];
|
||||||
if (keep > (int)sizeof(candidate) - 3) continue;
|
if (keep > (int)sizeof(candidate) - 3) continue;
|
||||||
montauk::memcpy(candidate, src, keep);
|
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;
|
if (!font) font = g_ui_font;
|
||||||
int th = text_height(font, bullet_size);
|
int th = text_height(font, bullet_size);
|
||||||
int top_y = py + (wl->height - th) / 2;
|
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);
|
draw_text(c, font, bullet_x, top_y, bullet, colors::TEXT_COLOR, bullet_size);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -346,7 +348,7 @@ void wp_render() {
|
|||||||
Color special_bg = wp->special_char_flyout_open ? btn_active : btn_bg;
|
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);
|
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);
|
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);
|
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;
|
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);
|
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);
|
pageview_bg, colors::TEXT_COLOR);
|
||||||
|
|
||||||
c.hline(0, WP_TOOLBAR_H - 1, c.w, colors::BORDER);
|
c.hline(0, WP_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||||
@@ -506,7 +508,7 @@ void wp_render() {
|
|||||||
|
|
||||||
int char_adv = 0;
|
int char_adv = 0;
|
||||||
if (ch != '\n' && (ch >= 32 || ch < 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;
|
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 text_col = (wp->has_selection && abs_ch >= sel_s && abs_ch < sel_e)
|
||||||
? Color::from_rgb(0x10, 0x10, 0x10) : colors::TEXT_COLOR;
|
? Color::from_rgb(0x10, 0x10, 0x10) : colors::TEXT_COLOR;
|
||||||
int adv = font->draw_char_to_buffer(
|
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;
|
x += adv;
|
||||||
} else {
|
} else {
|
||||||
x += char_adv;
|
x += char_adv;
|
||||||
@@ -717,7 +720,9 @@ void wp_render() {
|
|||||||
c.rect(dx, dy, dw, dh, colors::BORDER);
|
c.rect(dx, dy, dw, dh, colors::BORDER);
|
||||||
for (int i = 0; i < WP_SPECIAL_CHAR_OPTION_COUNT; i++) {
|
for (int i = 0; i < WP_SPECIAL_CHAR_OPTION_COUNT; i++) {
|
||||||
int iy = dy + 2 + i * WP_SPECIAL_CHAR_ROW_H;
|
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);
|
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,
|
draw_text(c, glyph_font ? glyph_font : g_ui_font,
|
||||||
dx + 8, iy + (WP_SPECIAL_CHAR_ROW_H - sfh) / 2,
|
dx + 8, iy + (WP_SPECIAL_CHAR_ROW_H - sfh) / 2,
|
||||||
|
|||||||
@@ -193,6 +193,10 @@ struct WPFontTable {
|
|||||||
bool loaded;
|
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 {
|
struct StyledRun {
|
||||||
char* text;
|
char* text;
|
||||||
int len;
|
int len;
|
||||||
|
|||||||
@@ -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_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||||
|
|
||||||
|
/* Unicode key events. Size-versioned: the caller passes sizeof(its
|
||||||
|
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
|
||||||
|
rest, so the struct can grow without another syscall. */
|
||||||
|
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
|
||||||
|
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
|
||||||
|
|
||||||
/* Kernel-owned process capabilities. User identities may namespace
|
/* Kernel-owned process capabilities. User identities may namespace
|
||||||
per-user resources, but never participate in authorization decisions. */
|
per-user resources, but never participate in authorization decisions. */
|
||||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||||
@@ -531,7 +537,10 @@ namespace montauk::abi {
|
|||||||
uint32_t buildNumber; // monotonic kernel build number
|
uint32_t buildNumber; // monotonic kernel build number
|
||||||
};
|
};
|
||||||
|
|
||||||
struct KeyEvent {
|
// Legacy 9-byte key event, read by SYS_GETKEY and written by
|
||||||
|
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
|
||||||
|
// `ascii` is a Windows-1252 byte.
|
||||||
|
struct KeyEventV1 {
|
||||||
uint8_t scancode;
|
uint8_t scancode;
|
||||||
char ascii;
|
char ascii;
|
||||||
bool pressed;
|
bool pressed;
|
||||||
@@ -547,6 +556,32 @@ namespace montauk::abi {
|
|||||||
bool extended;
|
bool extended;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Key event with the Unicode text it types (SYS_GETKEY_EX,
|
||||||
|
// SYS_CHILDIO_WRITEKEY_EX, WinEvent). Its first 9 bytes ARE a KeyEventV1, so
|
||||||
|
// a legacy reader sees a valid event. `codepoint` is the character the key
|
||||||
|
// produces (0 for control/navigation keys and dead keys). `ascii` is its
|
||||||
|
// Windows-1252 byte, or KEY_ASCII_SUBSTITUTE when 1252 has none.
|
||||||
|
struct KeyEvent {
|
||||||
|
uint8_t scancode;
|
||||||
|
char ascii;
|
||||||
|
bool pressed;
|
||||||
|
bool shift;
|
||||||
|
bool ctrl;
|
||||||
|
bool alt;
|
||||||
|
bool capslock;
|
||||||
|
bool altgr;
|
||||||
|
bool extended;
|
||||||
|
uint8_t _reserved[3];
|
||||||
|
uint32_t codepoint;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(KeyEventV1) == 9, "KeyEventV1 is frozen ABI");
|
||||||
|
static_assert(sizeof(KeyEvent) == 16, "KeyEvent must fit the WinEvent union");
|
||||||
|
|
||||||
|
// ASCII SUB: `ascii` of a text key whose character has no Windows-1252 byte.
|
||||||
|
// A control character, so legacy code that only accepts ascii >= 0x20 skips
|
||||||
|
// it, while code testing `ascii == 0` for navigation keys is not confused.
|
||||||
|
static constexpr char KEY_ASCII_SUBSTITUTE = 0x1A;
|
||||||
|
|
||||||
struct MouseState {
|
struct MouseState {
|
||||||
int32_t x;
|
int32_t x;
|
||||||
int32_t y;
|
int32_t y;
|
||||||
@@ -584,12 +619,13 @@ namespace montauk::abi {
|
|||||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||||
uint8_t _pad[3];
|
uint8_t _pad[3];
|
||||||
union {
|
union {
|
||||||
KeyEvent key;
|
KeyEvent key; // 16 bytes: the union was already that size (mouse)
|
||||||
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
||||||
struct { int32_t w, h; } resize;
|
struct { int32_t w, h; } resize;
|
||||||
struct { int32_t scale; } scale;
|
struct { int32_t scale; } scale;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
|
||||||
|
|
||||||
struct WinInfo {
|
struct WinInfo {
|
||||||
int32_t id;
|
int32_t id;
|
||||||
|
|||||||
@@ -165,9 +165,9 @@ struct Canvas {
|
|||||||
if (!str || !font_data || scale <= 0) return;
|
if (!str || !font_data || scale <= 0) return;
|
||||||
uint32_t pixel = c.to_pixel();
|
uint32_t pixel = c.to_pixel();
|
||||||
int cx = x;
|
int cx = x;
|
||||||
for (int i = 0; str[i]; i++, cx += FONT_WIDTH * scale) {
|
for (const char* p = str; *p; cx += FONT_WIDTH * scale) {
|
||||||
const uint8_t* glyph =
|
const uint8_t* glyph =
|
||||||
&font_data[(unsigned char)str[i] * FONT_HEIGHT];
|
&font_data[cp437_index(montauk::utf8::next(&p)) * FONT_HEIGHT];
|
||||||
for (int row = 0; row < FONT_HEIGHT; row++) {
|
for (int row = 0; row < FONT_HEIGHT; row++) {
|
||||||
uint8_t bits = glyph[row];
|
uint8_t bits = glyph[row];
|
||||||
if (!bits) continue;
|
if (!bits) continue;
|
||||||
|
|||||||
@@ -9,12 +9,32 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
|
// The clipboard carries UTF-8. Text that is not well-formed (legacy
|
||||||
|
// Windows-1252 bytes) is converted on the way in and on the way out, so
|
||||||
|
// every reader can assume valid UTF-8.
|
||||||
|
inline char* clipboard_utf8_copy(const char* text, int len, int* out_len) {
|
||||||
|
int cap = len * 3 + 1; // each stray byte grows to at most 3
|
||||||
|
char* out = (char*)montauk::malloc(cap);
|
||||||
|
if (!out) return nullptr;
|
||||||
|
*out_len = montauk::utf8::normalize(text, len, out, cap);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool clipboard_set_text(const char* text, int len) {
|
inline bool clipboard_set_text(const char* text, int len) {
|
||||||
if (len < 0) return false;
|
if (len < 0) return false;
|
||||||
if (len > 0 && text == nullptr) return false;
|
if (len > 0 && text == nullptr) return false;
|
||||||
|
if (len > 0 && !montauk::utf8::valid(text, len)) {
|
||||||
|
int fixed_len = 0;
|
||||||
|
char* fixed = clipboard_utf8_copy(text, len, &fixed_len);
|
||||||
|
if (!fixed) return false;
|
||||||
|
bool ok = montauk::clipboard_set_text(fixed, (uint32_t)fixed_len) == 0;
|
||||||
|
montauk::mfree(fixed);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
return montauk::clipboard_set_text(text, (uint32_t)len) == 0;
|
return montauk::clipboard_set_text(text, (uint32_t)len) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +73,14 @@ namespace gui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
text[actual_len] = '\0';
|
text[actual_len] = '\0';
|
||||||
|
if (!montauk::utf8::valid(text, (int)actual_len)) {
|
||||||
|
int fixed_len = 0;
|
||||||
|
char* fixed = clipboard_utf8_copy(text, (int)actual_len, &fixed_len);
|
||||||
|
montauk::mfree(text);
|
||||||
|
if (!fixed) return nullptr;
|
||||||
|
text = fixed;
|
||||||
|
actual_len = (uint32_t)fixed_len;
|
||||||
|
}
|
||||||
if (out_len) *out_len = (int)actual_len;
|
if (out_len) *out_len = (int)actual_len;
|
||||||
if (out_serial) *out_serial = serial;
|
if (out_serial) *out_serial = serial;
|
||||||
return text;
|
return text;
|
||||||
|
|||||||
@@ -227,6 +227,18 @@ struct DesktopState {
|
|||||||
montauk::abi::BatteryInfo battery_info;
|
montauk::abi::BatteryInfo battery_info;
|
||||||
uint64_t battery_last_poll;
|
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;
|
bool vol_popup_open;
|
||||||
Rect vol_icon_rect;
|
Rect vol_icon_rect;
|
||||||
int vol_level; // 0-100
|
int vol_level; // 0-100
|
||||||
|
|||||||
@@ -19,6 +19,34 @@ static constexpr int FONT_HEIGHT = 16;
|
|||||||
// TrueType fonts, while login/desktop can recover when TrueType setup fails.
|
// TrueType fonts, while login/desktop can recover when TrueType setup fails.
|
||||||
extern const uint8_t font_data[256 * 16] __attribute__((weak));
|
extern const uint8_t font_data[256 * 16] __attribute__((weak));
|
||||||
|
|
||||||
|
// The bitmap fallback font is Code Page 437. Map a Unicode codepoint to its
|
||||||
|
// CP437 cell ('?' when it has none) so UTF-8 text still draws sensibly,
|
||||||
|
// including the box-drawing characters terminal programs use.
|
||||||
|
inline uint8_t cp437_index(uint32_t cp) {
|
||||||
|
if (cp < 0x80) return (uint8_t)cp;
|
||||||
|
static constexpr uint16_t high[128] = {
|
||||||
|
0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7,
|
||||||
|
0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5,
|
||||||
|
0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9,
|
||||||
|
0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192,
|
||||||
|
0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA,
|
||||||
|
0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB,
|
||||||
|
0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,
|
||||||
|
0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510,
|
||||||
|
0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F,
|
||||||
|
0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567,
|
||||||
|
0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B,
|
||||||
|
0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580,
|
||||||
|
0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x00B5,0x03C4,
|
||||||
|
0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229,
|
||||||
|
0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248,
|
||||||
|
0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0,
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 128; i++)
|
||||||
|
if (high[i] == cp) return (uint8_t)(0x80 + i);
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic font height: TTF line height or 16 (bitmap fallback)
|
// Dynamic font height: TTF line height or 16 (bitmap fallback)
|
||||||
inline int system_font_height() {
|
inline int system_font_height() {
|
||||||
if (fonts::system_font && fonts::system_font->valid)
|
if (fonts::system_font && fonts::system_font->valid)
|
||||||
@@ -74,8 +102,8 @@ inline void draw_text(Framebuffer& fb, int x, int y, const char* text, Color fg)
|
|||||||
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
|
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p; x += FONT_WIDTH) {
|
||||||
draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg);
|
draw_char(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,18 +112,23 @@ inline void draw_text_bg(Framebuffer& fb, int x, int y, const char* text, Color
|
|||||||
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
|
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p; x += FONT_WIDTH) {
|
||||||
draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg);
|
draw_char_bg(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg, bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Width of the first `len` bytes of a UTF-8 string in the UI font.
|
||||||
|
inline int text_width_n(const char* text, int len) {
|
||||||
|
if (fonts::system_font && fonts::system_font->valid)
|
||||||
|
return fonts::system_font->measure_text_n(text, len, fonts::UI_SIZE);
|
||||||
|
return montauk::utf8::count(text, len) * FONT_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
inline int text_width(const char* text) {
|
inline int text_width(const char* text) {
|
||||||
if (fonts::system_font && fonts::system_font->valid) {
|
if (fonts::system_font && fonts::system_font->valid) {
|
||||||
return fonts::system_font->measure_text(text, fonts::UI_SIZE);
|
return fonts::system_font->measure_text(text, fonts::UI_SIZE);
|
||||||
}
|
}
|
||||||
int len = 0;
|
return montauk::utf8::count(text) * FONT_WIDTH;
|
||||||
while (text[len]) len++;
|
|
||||||
return len * FONT_WIDTH;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace gui
|
} // namespace gui
|
||||||
@@ -296,15 +296,13 @@ inline bool text_input_has_selection(const TextInputState& state, const char* te
|
|||||||
return text_input_selection_start(state, len) != text_input_selection_end(state, len);
|
return text_input_selection_start(state, len) != text_input_selection_end(state, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Width of the first `count` bytes (a character boundary). Masked fields
|
||||||
|
// draw one '*' per character, however many bytes it takes.
|
||||||
inline int text_input_prefix_width(const char* text, int count, bool masked) {
|
inline int text_input_prefix_width(const char* text, int count, bool masked) {
|
||||||
char prefix[512];
|
|
||||||
int len = 0;
|
int len = 0;
|
||||||
while (text && text[len] && len < count && len < (int)sizeof(prefix) - 1) {
|
while (text && text[len] && len < count) len++;
|
||||||
prefix[len] = masked ? '*' : text[len];
|
if (!masked) return text_width_n(text, len);
|
||||||
len++;
|
return montauk::utf8::count(text, len) * text_width("*");
|
||||||
}
|
|
||||||
prefix[len] = '\0';
|
|
||||||
return text_width(prefix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int text_input_index_from_x(const Rect& bounds, const char* value,
|
inline int text_input_index_from_x(const Rect& bounds, const char* value,
|
||||||
@@ -314,11 +312,13 @@ inline int text_input_index_from_x(const Rect& bounds, const char* value,
|
|||||||
|
|
||||||
int len = text_input_len(value);
|
int len = text_input_len(value);
|
||||||
int prev_w = 0;
|
int prev_w = 0;
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len;) {
|
||||||
int cur_w = text_input_prefix_width(value, i + 1, masked);
|
int next = montauk::utf8::next_boundary(value, len, i);
|
||||||
|
int cur_w = text_input_prefix_width(value, next, masked);
|
||||||
int midpoint = prev_w + (cur_w - prev_w) / 2;
|
int midpoint = prev_w + (cur_w - prev_w) / 2;
|
||||||
if (rel_x < midpoint) return i;
|
if (rel_x < midpoint) return i;
|
||||||
prev_w = cur_w;
|
prev_w = cur_w;
|
||||||
|
i = next;
|
||||||
}
|
}
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
@@ -360,8 +360,11 @@ inline int text_input_mouse(TextInputState& state,
|
|||||||
return TEXT_INPUT_NONE;
|
return TEXT_INPUT_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bytes >= 0x80 (any non-ASCII character) count as word characters, so word
|
||||||
|
// jumps never stop inside a multi-byte sequence.
|
||||||
inline bool text_input_word_char(char ch) {
|
inline bool text_input_word_char(char ch) {
|
||||||
return (ch >= 'a' && ch <= 'z') ||
|
return (uint8_t)ch >= 0x80 ||
|
||||||
|
(ch >= 'a' && ch <= 'z') ||
|
||||||
(ch >= 'A' && ch <= 'Z') ||
|
(ch >= 'A' && ch <= 'Z') ||
|
||||||
(ch >= '0' && ch <= '9') ||
|
(ch >= '0' && ch <= '9') ||
|
||||||
ch == '_';
|
ch == '_';
|
||||||
@@ -574,8 +577,10 @@ inline int text_input_key(TextInputState& state,
|
|||||||
if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE;
|
if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE;
|
||||||
|
|
||||||
int len = text_input_len(text, cap);
|
int len = text_input_len(text, cap);
|
||||||
state.cursor = text_input_clamp_cursor(state.cursor, len);
|
state.cursor = montauk::utf8::floor_boundary(
|
||||||
state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len);
|
text, len, text_input_clamp_cursor(state.cursor, len));
|
||||||
|
state.selection_anchor = montauk::utf8::floor_boundary(
|
||||||
|
text, len, text_input_clamp_cursor(state.selection_anchor, len));
|
||||||
|
|
||||||
if (state.context.open) {
|
if (state.context.open) {
|
||||||
context_menu_close(state.context);
|
context_menu_close(state.context);
|
||||||
@@ -606,20 +611,22 @@ inline int text_input_key(TextInputState& state,
|
|||||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x4B || key.scancode == 0x4D ||
|
if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x4D ||
|
||||||
key.scancode == 0x47 || key.scancode == 0x4F) {
|
montauk::nav_scancode(key) == 0x47 || montauk::nav_scancode(key) == 0x4F) {
|
||||||
int next = state.cursor;
|
int next = state.cursor;
|
||||||
if (!key.shift && sel_start != sel_end && key.scancode == 0x4B)
|
if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4B)
|
||||||
next = sel_start;
|
next = sel_start;
|
||||||
else if (!key.shift && sel_start != sel_end && key.scancode == 0x4D)
|
else if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4D)
|
||||||
next = sel_end;
|
next = sel_end;
|
||||||
else if (key.scancode == 0x4B)
|
else if (montauk::nav_scancode(key) == 0x4B)
|
||||||
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) : state.cursor - 1;
|
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor)
|
||||||
else if (key.scancode == 0x4D)
|
: montauk::utf8::prev_boundary(text, state.cursor);
|
||||||
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor) : state.cursor + 1;
|
else if (montauk::nav_scancode(key) == 0x4D)
|
||||||
else if (key.scancode == 0x47)
|
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor)
|
||||||
|
: montauk::utf8::next_boundary(text, len, state.cursor);
|
||||||
|
else if (montauk::nav_scancode(key) == 0x47)
|
||||||
next = 0;
|
next = 0;
|
||||||
else if (key.scancode == 0x4F)
|
else if (montauk::nav_scancode(key) == 0x4F)
|
||||||
next = len;
|
next = len;
|
||||||
|
|
||||||
text_input_set_cursor(state, len, next, key.shift);
|
text_input_set_cursor(state, len, next, key.shift);
|
||||||
@@ -641,15 +648,16 @@ inline int text_input_key(TextInputState& state,
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
} else if (state.cursor > 0) {
|
} else if (state.cursor > 0) {
|
||||||
text_input_delete_range(text, &len, state.cursor - 1, state.cursor);
|
int start = montauk::utf8::prev_boundary(text, state.cursor);
|
||||||
state.cursor--;
|
text_input_delete_range(text, &len, start, state.cursor);
|
||||||
|
state.cursor = start;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
state.selection_anchor = state.cursor;
|
state.selection_anchor = state.cursor;
|
||||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.scancode == 0x53) {
|
if (montauk::nav_scancode(key) == 0x53) {
|
||||||
if (sel_start != sel_end) {
|
if (sel_start != sel_end) {
|
||||||
text_input_delete_range(text, &len, sel_start, sel_end);
|
text_input_delete_range(text, &len, sel_start, sel_end);
|
||||||
state.cursor = sel_start;
|
state.cursor = sel_start;
|
||||||
@@ -661,15 +669,22 @@ inline int text_input_key(TextInputState& state,
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
} else if (state.cursor < len) {
|
} else if (state.cursor < len) {
|
||||||
text_input_delete_range(text, &len, state.cursor, state.cursor + 1);
|
int end = montauk::utf8::next_boundary(text, len, state.cursor);
|
||||||
|
text_input_delete_range(text, &len, state.cursor, end);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
state.selection_anchor = state.cursor;
|
state.selection_anchor = state.cursor;
|
||||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.ctrl || key.alt || !text_input_char_allowed(key.ascii, filter, userdata))
|
// Typed text is inserted as UTF-8; the filter sees each encoded byte.
|
||||||
|
char typed[montauk::utf8::MAX_BYTES];
|
||||||
|
int typed_len = key.ctrl || key.alt ? 0 : montauk::utf8::key_utf8(key, typed);
|
||||||
|
if (typed_len == 0) return TEXT_INPUT_NONE;
|
||||||
|
for (int i = 0; i < typed_len; i++) {
|
||||||
|
if (!text_input_char_allowed(typed[i], filter, userdata))
|
||||||
return TEXT_INPUT_NONE;
|
return TEXT_INPUT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
if (sel_start != sel_end) {
|
if (sel_start != sel_end) {
|
||||||
text_input_delete_range(text, &len, sel_start, sel_end);
|
text_input_delete_range(text, &len, sel_start, sel_end);
|
||||||
@@ -677,11 +692,12 @@ inline int text_input_key(TextInputState& state,
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len < cap - 1) {
|
if (len + typed_len < cap) {
|
||||||
for (int i = len; i >= state.cursor; i--)
|
for (int i = len; i >= state.cursor; i--)
|
||||||
text[i + 1] = text[i];
|
text[i + typed_len] = text[i];
|
||||||
text[state.cursor++] = key.ascii;
|
for (int i = 0; i < typed_len; i++)
|
||||||
len++;
|
text[state.cursor++] = typed[i];
|
||||||
|
len += typed_len;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1251,23 +1267,35 @@ inline void draw_badge(Canvas& c,
|
|||||||
c.text(x + theme.gap_sm, y + 2, label, fg);
|
c.text(x + theme.gap_sm, y + 2, label, fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One '*' per UTF-8 character.
|
||||||
inline const char* mask_text(const char* value, char* buffer, int cap) {
|
inline const char* mask_text(const char* value, char* buffer, int cap) {
|
||||||
int len = 0;
|
int len = 0;
|
||||||
while (value && value[len] && len < cap - 1) {
|
const char* p = value ? value : "";
|
||||||
buffer[len] = '*';
|
while (*p && len < cap - 1) {
|
||||||
len++;
|
montauk::utf8::next(&p);
|
||||||
|
buffer[len++] = '*';
|
||||||
}
|
}
|
||||||
buffer[len] = '\0';
|
buffer[len] = '\0';
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First `count` bytes of value (a character boundary), or one '*' per
|
||||||
|
// character in them when masked.
|
||||||
inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) {
|
inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) {
|
||||||
if (cap <= 0) return;
|
if (cap <= 0) return;
|
||||||
int len = 0;
|
int len = 0;
|
||||||
|
if (masked) {
|
||||||
|
const char* p = value ? value : "";
|
||||||
|
while (*p && p - value < count && len < cap - 1) {
|
||||||
|
montauk::utf8::next(&p);
|
||||||
|
buffer[len++] = '*';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
while (value && value[len] && len < count && len < cap - 1) {
|
while (value && value[len] && len < count && len < cap - 1) {
|
||||||
buffer[len] = masked ? '*' : value[len];
|
buffer[len] = value[len];
|
||||||
len++;
|
len++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
buffer[len] = '\0';
|
buffer[len] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,14 @@
|
|||||||
#include "gui/font.hpp"
|
#include "gui/font.hpp"
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <Api/Syscall.hpp>
|
#include <Api/Syscall.hpp>
|
||||||
#include <montauk/capabilities.h>
|
#include <montauk/capabilities.h>
|
||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
struct TermCell {
|
struct TermCell {
|
||||||
char ch;
|
uint32_t ch; // Unicode codepoint (every cell is one column wide)
|
||||||
Color fg;
|
Color fg;
|
||||||
Color bg;
|
Color bg;
|
||||||
};
|
};
|
||||||
@@ -58,6 +59,11 @@ struct TerminalState {
|
|||||||
int csi_params[8];
|
int csi_params[8];
|
||||||
int csi_param_count;
|
int csi_param_count;
|
||||||
int csi_current_param;
|
int csi_current_param;
|
||||||
|
|
||||||
|
// Partial UTF-8 sequence carried across terminal_feed calls (child output
|
||||||
|
// arrives in arbitrary chunks, so a character can straddle two reads).
|
||||||
|
char utf8_pending[4];
|
||||||
|
int utf8_pending_len;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==== Palette ====
|
// ==== Palette ====
|
||||||
@@ -244,6 +250,7 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
|
|||||||
t->csi_private = false;
|
t->csi_private = false;
|
||||||
t->csi_param_count = 0;
|
t->csi_param_count = 0;
|
||||||
t->csi_current_param = 0;
|
t->csi_current_param = 0;
|
||||||
|
t->utf8_pending_len = 0;
|
||||||
t->child_pid = 0;
|
t->child_pid = 0;
|
||||||
|
|
||||||
int total_cells = (rows + max_sb) * cols;
|
int total_cells = (rows + max_sb) * cols;
|
||||||
@@ -275,7 +282,7 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
|
|||||||
montauk::childio_settermsz(t->child_pid, cols, rows);
|
montauk::childio_settermsz(t->child_pid, cols, rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void terminal_put_char(TerminalState* t, char ch) {
|
static inline void terminal_put_char(TerminalState* t, uint32_t ch) {
|
||||||
if (t->cursor_x >= t->cols) {
|
if (t->cursor_x >= t->cols) {
|
||||||
t->cursor_x = 0;
|
t->cursor_x = 0;
|
||||||
t->cursor_y++;
|
t->cursor_y++;
|
||||||
@@ -549,11 +556,61 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit pending bytes that did not form a valid UTF-8 sequence: each one is a
|
||||||
|
// Windows-1252 character, matching how the GUI renders legacy text.
|
||||||
|
static inline void terminal_flush_utf8(TerminalState* t) {
|
||||||
|
for (int i = 0; i < t->utf8_pending_len; i++)
|
||||||
|
terminal_put_char(t, montauk::utf8::from_cp1252((uint8_t)t->utf8_pending[i]));
|
||||||
|
t->utf8_pending_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int terminal_utf8_expected(uint8_t lead) {
|
||||||
|
if (lead >= 0xC2 && lead <= 0xDF) return 2;
|
||||||
|
if (lead >= 0xE0 && lead <= 0xEF) return 3;
|
||||||
|
if (lead >= 0xF0 && lead <= 0xF4) return 4;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed one byte >= 0x80 of printable text through the UTF-8 assembler.
|
||||||
|
static inline void terminal_feed_high(TerminalState* t, uint8_t b) {
|
||||||
|
if (t->utf8_pending_len > 0) {
|
||||||
|
if ((b & 0xC0) == 0x80) {
|
||||||
|
t->utf8_pending[t->utf8_pending_len++] = (char)b;
|
||||||
|
int want = terminal_utf8_expected((uint8_t)t->utf8_pending[0]);
|
||||||
|
uint32_t cp = 0;
|
||||||
|
int n = montauk::utf8::sequence_length(t->utf8_pending,
|
||||||
|
t->utf8_pending_len, &cp);
|
||||||
|
if (n == want) {
|
||||||
|
terminal_put_char(t, cp);
|
||||||
|
t->utf8_pending_len = 0;
|
||||||
|
} else if (t->utf8_pending_len >= want) {
|
||||||
|
terminal_flush_utf8(t); // overlong / surrogate / out of range
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
terminal_flush_utf8(t);
|
||||||
|
}
|
||||||
|
if (terminal_utf8_expected(b)) {
|
||||||
|
t->utf8_pending[0] = (char)b;
|
||||||
|
t->utf8_pending_len = 1;
|
||||||
|
} else {
|
||||||
|
terminal_put_char(t, montauk::utf8::from_cp1252(b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
||||||
if (len > 0) t->dirty = true;
|
if (len > 0) t->dirty = true;
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
char ch = data[i];
|
char ch = data[i];
|
||||||
|
|
||||||
|
if (t->parse_state == TerminalState::STATE_NORMAL) {
|
||||||
|
if ((uint8_t)ch >= 0x80) {
|
||||||
|
terminal_feed_high(t, (uint8_t)ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (t->utf8_pending_len > 0) terminal_flush_utf8(t);
|
||||||
|
}
|
||||||
|
|
||||||
switch (t->parse_state) {
|
switch (t->parse_state) {
|
||||||
case TerminalState::STATE_NORMAL:
|
case TerminalState::STATE_NORMAL:
|
||||||
if (ch == '\033') {
|
if (ch == '\033') {
|
||||||
@@ -575,9 +632,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
|||||||
while (t->cursor_x < next) {
|
while (t->cursor_x < next) {
|
||||||
terminal_put_char(t, ' ');
|
terminal_put_char(t, ' ');
|
||||||
}
|
}
|
||||||
} else if (ch >= 32 || ch < 0) {
|
} else if (ch >= 32) {
|
||||||
// Printable character (also treat high-bit chars as printable)
|
terminal_put_char(t, (uint8_t)ch);
|
||||||
terminal_put_char(t, ch);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -641,9 +697,9 @@ static inline void terminal_fill_pixel_rect(uint32_t* pixels, int pw, int ph,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph,
|
static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph,
|
||||||
int x, int y, unsigned char ch,
|
int x, int y, uint32_t ch,
|
||||||
uint32_t color) {
|
uint32_t color) {
|
||||||
const uint8_t* glyph = &font_data[ch * FONT_HEIGHT];
|
const uint8_t* glyph = &font_data[cp437_index(ch) * FONT_HEIGHT];
|
||||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||||
int dy = y + fy;
|
int dy = y + fy;
|
||||||
if (dy >= ph) break;
|
if (dy >= ph) break;
|
||||||
@@ -669,14 +725,14 @@ static inline void terminal_draw_cell_pixels(const TermCell& cell,
|
|||||||
cell.bg.to_pixel());
|
cell.bg.to_pixel());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cell.ch <= 32 && cell.ch >= 0) return;
|
if (cell.ch <= 32) return;
|
||||||
|
|
||||||
if (use_ttf) {
|
if (use_ttf) {
|
||||||
int baseline = py + gc->ascent;
|
int baseline = py + gc->ascent;
|
||||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||||
px, baseline, (unsigned char)cell.ch, cell.fg, gc);
|
px, baseline, (int)cell.ch, cell.fg, gc);
|
||||||
} else {
|
} else {
|
||||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
|
terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
|
||||||
cell.fg.to_pixel());
|
cell.fg.to_pixel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -691,14 +747,14 @@ static inline void terminal_draw_cursor_pixels(const TermCell& cell,
|
|||||||
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
|
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
|
||||||
g_term_palette.cursor.to_pixel());
|
g_term_palette.cursor.to_pixel());
|
||||||
|
|
||||||
if (cell.ch <= 32 && cell.ch >= 0) return;
|
if (cell.ch <= 32) return;
|
||||||
|
|
||||||
if (use_ttf) {
|
if (use_ttf) {
|
||||||
int baseline = py + gc->ascent;
|
int baseline = py + gc->ascent;
|
||||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||||
px, baseline, (unsigned char)cell.ch, g_term_palette.bg, gc);
|
px, baseline, (int)cell.ch, g_term_palette.bg, gc);
|
||||||
} else {
|
} else {
|
||||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
|
terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
|
||||||
g_term_palette.bg.to_pixel());
|
g_term_palette.bg.to_pixel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include "gui/gui.hpp"
|
#include "gui/gui.hpp"
|
||||||
#include "gui/framebuffer.hpp"
|
#include "gui/framebuffer.hpp"
|
||||||
|
|
||||||
@@ -41,39 +42,12 @@
|
|||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
|
// Windows-1252 byte -> Unicode codepoint, for the remaining code that still
|
||||||
|
// handles single-byte text (PDF simple-font encodings). UTF-8 strings go
|
||||||
|
// through measure_text/draw, which decode with montauk::utf8.
|
||||||
inline int decode_single_byte_codepoint(int codepoint) {
|
inline int decode_single_byte_codepoint(int codepoint) {
|
||||||
if (codepoint < 0 || codepoint > 255) return codepoint;
|
if (codepoint < 0 || codepoint > 255) return codepoint;
|
||||||
|
return (int)montauk::utf8::from_cp1252((uint8_t)codepoint);
|
||||||
switch (codepoint) {
|
|
||||||
case 0x80: return 0x20AC; // Euro Sign
|
|
||||||
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
|
|
||||||
case 0x83: return 0x0192; // Latin Small Letter F With Hook
|
|
||||||
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
|
|
||||||
case 0x85: return 0x2026; // Horizontal Ellipsis
|
|
||||||
case 0x86: return 0x2020; // Dagger
|
|
||||||
case 0x87: return 0x2021; // Double Dagger
|
|
||||||
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
|
|
||||||
case 0x89: return 0x2030; // Per Mille Sign
|
|
||||||
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
|
|
||||||
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
|
|
||||||
case 0x8C: return 0x0152; // Latin Capital Ligature OE
|
|
||||||
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
|
|
||||||
case 0x91: return 0x2018; // Left Single Quotation Mark
|
|
||||||
case 0x92: return 0x2019; // Right Single Quotation Mark
|
|
||||||
case 0x93: return 0x201C; // Left Double Quotation Mark
|
|
||||||
case 0x94: return 0x201D; // Right Double Quotation Mark
|
|
||||||
case 0x95: return 0x2022; // Bullet
|
|
||||||
case 0x96: return 0x2013; // En Dash
|
|
||||||
case 0x97: return 0x2014; // Em Dash
|
|
||||||
case 0x98: return 0x02DC; // Small Tilde
|
|
||||||
case 0x99: return 0x2122; // Trade Mark Sign
|
|
||||||
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
|
|
||||||
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
|
|
||||||
case 0x9C: return 0x0153; // Latin Small Ligature OE
|
|
||||||
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
|
|
||||||
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
|
|
||||||
default: return codepoint;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CachedGlyph {
|
struct CachedGlyph {
|
||||||
@@ -84,8 +58,18 @@ struct CachedGlyph {
|
|||||||
bool loaded;
|
bool loaded;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Glyphs above U+00FF live in a small open-addressed table so CJK, symbols
|
||||||
|
// and the like cost nothing until they are drawn.
|
||||||
|
struct ExtGlyph {
|
||||||
|
uint32_t codepoint; // 0 = empty slot
|
||||||
|
CachedGlyph glyph;
|
||||||
|
};
|
||||||
|
|
||||||
struct GlyphCache {
|
struct GlyphCache {
|
||||||
CachedGlyph glyphs[256]; // Single-byte text with Windows-1252 punctuation mapping
|
CachedGlyph glyphs[256]; // Unicode U+0000-U+00FF, indexed directly
|
||||||
|
ExtGlyph* ext; // everything above, hashed by codepoint
|
||||||
|
int ext_cap; // power of two, 0 until first use
|
||||||
|
int ext_count;
|
||||||
int pixel_size;
|
int pixel_size;
|
||||||
float scale;
|
float scale;
|
||||||
int ascent, descent, line_gap;
|
int ascent, descent, line_gap;
|
||||||
@@ -142,8 +126,21 @@ struct TrueTypeFont {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void init_cache(GlyphCache* gc, int pixel_size) {
|
static void free_ext(GlyphCache* gc) {
|
||||||
// Free any existing glyph bitmaps
|
if (gc->ext) {
|
||||||
|
for (int i = 0; i < gc->ext_cap; i++) {
|
||||||
|
if (gc->ext[i].codepoint && gc->ext[i].glyph.bitmap)
|
||||||
|
montauk::mfree(gc->ext[i].glyph.bitmap);
|
||||||
|
}
|
||||||
|
montauk::mfree(gc->ext);
|
||||||
|
}
|
||||||
|
gc->ext = nullptr;
|
||||||
|
gc->ext_cap = 0;
|
||||||
|
gc->ext_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release every glyph bitmap in a cache (the cache stays usable).
|
||||||
|
static void free_cache(GlyphCache* gc) {
|
||||||
for (int i = 0; i < 256; i++) {
|
for (int i = 0; i < 256; i++) {
|
||||||
if (gc->glyphs[i].bitmap) {
|
if (gc->glyphs[i].bitmap) {
|
||||||
montauk::mfree(gc->glyphs[i].bitmap);
|
montauk::mfree(gc->glyphs[i].bitmap);
|
||||||
@@ -151,6 +148,11 @@ struct TrueTypeFont {
|
|||||||
gc->glyphs[i].bitmap = nullptr;
|
gc->glyphs[i].bitmap = nullptr;
|
||||||
gc->glyphs[i].loaded = false;
|
gc->glyphs[i].loaded = false;
|
||||||
}
|
}
|
||||||
|
free_ext(gc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_cache(GlyphCache* gc, int pixel_size) {
|
||||||
|
free_cache(gc);
|
||||||
|
|
||||||
gc->pixel_size = pixel_size;
|
gc->pixel_size = pixel_size;
|
||||||
gc->scale = em_scaling
|
gc->scale = em_scaling
|
||||||
@@ -179,11 +181,8 @@ struct TrueTypeFont {
|
|||||||
return gc;
|
return gc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evict oldest cache (slot 0) — free its glyph bitmaps
|
// Evict oldest cache (slot 0) - free its glyph bitmaps
|
||||||
for (int g = 0; g < 256; g++) {
|
free_cache(&caches[0]);
|
||||||
if (caches[0].glyphs[g].bitmap)
|
|
||||||
montauk::mfree(caches[0].glyphs[g].bitmap);
|
|
||||||
}
|
|
||||||
// Shift remaining caches down
|
// Shift remaining caches down
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache));
|
montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache));
|
||||||
@@ -192,13 +191,54 @@ struct TrueTypeFont {
|
|||||||
return &caches[3];
|
return &caches[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static constexpr int EXT_MAX = 2048; // flush the table beyond this
|
||||||
|
|
||||||
|
CachedGlyph* ext_slot(GlyphCache* gc, uint32_t codepoint) {
|
||||||
|
if (gc->ext_cap == 0 || gc->ext_count * 4 >= gc->ext_cap * 3) {
|
||||||
|
int new_cap = gc->ext_cap ? gc->ext_cap * 2 : 64;
|
||||||
|
if (gc->ext_count >= EXT_MAX) {
|
||||||
|
// A document full of distinct glyphs: start over rather than
|
||||||
|
// grow without bound. Bitmaps are rebuilt on demand.
|
||||||
|
free_ext(gc);
|
||||||
|
new_cap = 64;
|
||||||
|
}
|
||||||
|
ExtGlyph* table = (ExtGlyph*)montauk::malloc(sizeof(ExtGlyph) * new_cap);
|
||||||
|
if (!table) return nullptr;
|
||||||
|
montauk::memset(table, 0, sizeof(ExtGlyph) * new_cap);
|
||||||
|
for (int i = 0; i < gc->ext_cap; i++) {
|
||||||
|
ExtGlyph& e = gc->ext[i];
|
||||||
|
if (!e.codepoint) continue;
|
||||||
|
uint32_t h = (e.codepoint * 2654435761u) & (new_cap - 1);
|
||||||
|
while (table[h].codepoint) h = (h + 1) & (new_cap - 1);
|
||||||
|
table[h] = e;
|
||||||
|
}
|
||||||
|
if (gc->ext) montauk::mfree(gc->ext);
|
||||||
|
gc->ext = table;
|
||||||
|
gc->ext_cap = new_cap;
|
||||||
|
}
|
||||||
|
uint32_t mask = gc->ext_cap - 1;
|
||||||
|
uint32_t h = (codepoint * 2654435761u) & mask;
|
||||||
|
while (gc->ext[h].codepoint && gc->ext[h].codepoint != codepoint)
|
||||||
|
h = (h + 1) & mask;
|
||||||
|
if (!gc->ext[h].codepoint) {
|
||||||
|
gc->ext[h].codepoint = codepoint;
|
||||||
|
montauk::memset(&gc->ext[h].glyph, 0, sizeof(CachedGlyph));
|
||||||
|
gc->ext_count++;
|
||||||
|
}
|
||||||
|
return &gc->ext[h].glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glyph for a Unicode codepoint. Codepoints the font lacks render as its
|
||||||
|
// .notdef box, so missing characters stay visible instead of vanishing.
|
||||||
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
|
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
|
||||||
if (codepoint < 0 || codepoint >= 256) return nullptr;
|
if (codepoint < 0 || codepoint > 0x10FFFF) return nullptr;
|
||||||
CachedGlyph* g = &gc->glyphs[codepoint];
|
CachedGlyph* g = codepoint < 256 ? &gc->glyphs[codepoint]
|
||||||
|
: ext_slot(gc, (uint32_t)codepoint);
|
||||||
|
if (!g) return nullptr;
|
||||||
if (g->loaded) return g;
|
if (g->loaded) return g;
|
||||||
|
|
||||||
g->loaded = true;
|
g->loaded = true;
|
||||||
int font_codepoint = decode_single_byte_codepoint(codepoint);
|
int font_codepoint = codepoint;
|
||||||
int advance, lsb;
|
int advance, lsb;
|
||||||
stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb);
|
stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb);
|
||||||
g->advance = (int)(advance * gc->scale);
|
g->advance = (int)(advance * gc->scale);
|
||||||
@@ -222,12 +262,34 @@ struct TrueTypeFont {
|
|||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advance of one codepoint at a size (0 if the font is unusable).
|
||||||
|
int char_advance(int codepoint, int pixel_size) {
|
||||||
|
if (!valid) return 0;
|
||||||
|
CachedGlyph* g = get_glyph(get_cache(pixel_size), codepoint);
|
||||||
|
return g ? g->advance : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width of the first `len` bytes of a UTF-8 string.
|
||||||
|
int measure_text_n(const char* text, int len, int pixel_size) {
|
||||||
|
if (!valid) return 0;
|
||||||
|
GlyphCache* gc = get_cache(pixel_size);
|
||||||
|
int w = 0;
|
||||||
|
for (int pos = 0; pos < len && text[pos];) {
|
||||||
|
int n;
|
||||||
|
uint32_t cp = montauk::utf8::decode(text + pos, len - pos, &n);
|
||||||
|
CachedGlyph* g = get_glyph(gc, (int)cp);
|
||||||
|
if (g) w += g->advance;
|
||||||
|
pos += n;
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
int measure_text(const char* text, int pixel_size) {
|
int measure_text(const char* text, int pixel_size) {
|
||||||
if (!valid) return 0;
|
if (!valid) return 0;
|
||||||
GlyphCache* gc = get_cache(pixel_size);
|
GlyphCache* gc = get_cache(pixel_size);
|
||||||
int w = 0;
|
int w = 0;
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p;) {
|
||||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (g) w += g->advance;
|
if (g) w += g->advance;
|
||||||
}
|
}
|
||||||
return w;
|
return w;
|
||||||
@@ -253,8 +315,8 @@ struct TrueTypeFont {
|
|||||||
uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) |
|
uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) |
|
||||||
((uint32_t)color.g << 8) | color.b;
|
((uint32_t)color.g << 8) | color.b;
|
||||||
|
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p;) {
|
||||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (!g) continue;
|
if (!g) continue;
|
||||||
|
|
||||||
if (g->bitmap) {
|
if (g->bitmap) {
|
||||||
@@ -325,8 +387,8 @@ struct TrueTypeFont {
|
|||||||
int baseline = y + gc->ascent;
|
int baseline = y + gc->ascent;
|
||||||
bool drew_pixel = false;
|
bool drew_pixel = false;
|
||||||
|
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p;) {
|
||||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (!g) continue;
|
if (!g) continue;
|
||||||
|
|
||||||
if (g->bitmap) {
|
if (g->bitmap) {
|
||||||
@@ -378,8 +440,8 @@ struct TrueTypeFont {
|
|||||||
int clip_x2 = clip_x + clip_w;
|
int clip_x2 = clip_x + clip_w;
|
||||||
int clip_y2 = clip_y + clip_h;
|
int clip_y2 = clip_y + clip_h;
|
||||||
|
|
||||||
for (int i = 0; text[i]; i++) {
|
for (const char* p = text; *p;) {
|
||||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||||
if (!g) continue;
|
if (!g) continue;
|
||||||
|
|
||||||
if (g->bitmap) {
|
if (g->bitmap) {
|
||||||
@@ -417,7 +479,7 @@ struct TrueTypeFont {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw single character to buffer, returning advance width
|
// Draw one Unicode codepoint to buffer, returning advance width
|
||||||
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
|
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
|
||||||
int x, int baseline, int codepoint,
|
int x, int baseline, int codepoint,
|
||||||
Color color, GlyphCache* gc) {
|
Color color, GlyphCache* gc) {
|
||||||
|
|||||||
@@ -212,11 +212,7 @@ struct TextBox {
|
|||||||
|
|
||||||
// Draw cursor if focused
|
// Draw cursor if focused
|
||||||
if (focused) {
|
if (focused) {
|
||||||
// Measure text up to cursor position for proportional fonts
|
int cx = tx + text_width_n(text, cursor);
|
||||||
char prefix[256];
|
|
||||||
for (int i = 0; i < cursor && i < 255; i++) prefix[i] = text[i];
|
|
||||||
prefix[cursor < 255 ? cursor : 255] = '\0';
|
|
||||||
int cx = tx + text_width(prefix);
|
|
||||||
draw_vline(fb, cx, ty, fh, cursor_color);
|
draw_vline(fb, cx, ty, fh, cursor_color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,34 +226,35 @@ struct TextBox {
|
|||||||
void handle_key(const montauk::abi::KeyEvent& key) {
|
void handle_key(const montauk::abi::KeyEvent& key) {
|
||||||
if (!focused || !key.pressed) return;
|
if (!focused || !key.pressed) return;
|
||||||
|
|
||||||
|
char typed[montauk::utf8::MAX_BYTES];
|
||||||
|
int typed_len = montauk::utf8::key_utf8(key, typed);
|
||||||
|
|
||||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
// Backspace
|
// Backspace: remove the whole character before the cursor
|
||||||
if (cursor > 0) {
|
if (cursor > 0) {
|
||||||
for (int i = cursor - 1; i < text_len - 1; i++) {
|
int start = montauk::utf8::prev_boundary(text, cursor);
|
||||||
text[i] = text[i + 1];
|
int n = cursor - start;
|
||||||
|
for (int i = start; i <= text_len - n; i++)
|
||||||
|
text[i] = text[i + n];
|
||||||
|
text_len -= n;
|
||||||
|
cursor = start;
|
||||||
}
|
}
|
||||||
text_len--;
|
} else if (typed_len > 0) {
|
||||||
cursor--;
|
// Printable character, inserted as UTF-8
|
||||||
text[text_len] = '\0';
|
if (text_len + typed_len <= 254) {
|
||||||
|
for (int i = text_len; i >= cursor; i--)
|
||||||
|
text[i + typed_len] = text[i];
|
||||||
|
for (int i = 0; i < typed_len; i++)
|
||||||
|
text[cursor + i] = typed[i];
|
||||||
|
cursor += typed_len;
|
||||||
|
text_len += typed_len;
|
||||||
}
|
}
|
||||||
} else if ((unsigned char)key.ascii >= 32
|
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
&& (unsigned char)key.ascii != 127) {
|
|
||||||
// Printable character
|
|
||||||
if (text_len < 254) {
|
|
||||||
for (int i = text_len; i > cursor; i--) {
|
|
||||||
text[i] = text[i - 1];
|
|
||||||
}
|
|
||||||
text[cursor] = key.ascii;
|
|
||||||
cursor++;
|
|
||||||
text_len++;
|
|
||||||
text[text_len] = '\0';
|
|
||||||
}
|
|
||||||
} else if (key.scancode == 0x4B) {
|
|
||||||
// Left arrow
|
// Left arrow
|
||||||
if (cursor > 0) cursor--;
|
cursor = montauk::utf8::prev_boundary(text, cursor);
|
||||||
} else if (key.scancode == 0x4D) {
|
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
// Right arrow
|
// Right arrow
|
||||||
if (cursor < text_len) cursor++;
|
cursor = montauk::utf8::next_boundary(text, text_len, cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -217,6 +217,8 @@ extern "C" {
|
|||||||
#define MTK_SYS_CONSENT_REGISTER 191
|
#define MTK_SYS_CONSENT_REGISTER 191
|
||||||
#define MTK_SYS_CONSENT_NEXT 192
|
#define MTK_SYS_CONSENT_NEXT 192
|
||||||
#define MTK_SYS_CONSENT_REPLY 193
|
#define MTK_SYS_CONSENT_REPLY 193
|
||||||
|
#define MTK_SYS_GETKEY_EX 194
|
||||||
|
#define MTK_SYS_CHILDIO_WRITEKEY_EX 195
|
||||||
/* @SYSCALLS-END */
|
/* @SYSCALLS-END */
|
||||||
|
|
||||||
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
||||||
|
|||||||
@@ -8,19 +8,22 @@
|
|||||||
|
|
||||||
#include <montauk/config.h>
|
#include <montauk/config.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
|
|
||||||
namespace montauk::keyboard {
|
namespace montauk::keyboard {
|
||||||
|
|
||||||
inline constexpr int MAX_LAYOUTS = 8;
|
inline constexpr int MAX_LAYOUTS = 8;
|
||||||
inline constexpr int MAX_KEYS = 64;
|
inline constexpr int MAX_KEYS = 64;
|
||||||
|
inline constexpr int MAX_DEAD_KEYS = 16;
|
||||||
|
|
||||||
// One overridden key. Characters are Windows-1252 bytes to match the
|
// One overridden key. Characters are Unicode codepoints; translation stores
|
||||||
// single-byte GUI text stack; 0 means "no override, keep the kernel's value".
|
// them in KeyEvent::codepoint (and the Windows-1252 byte, if any, in ascii).
|
||||||
|
// 0 means "no override, keep the kernel's value".
|
||||||
struct KeyMap {
|
struct KeyMap {
|
||||||
uint8_t scancode;
|
uint8_t scancode;
|
||||||
uint8_t base;
|
uint32_t base;
|
||||||
uint8_t shift;
|
uint32_t shift;
|
||||||
uint8_t altgr;
|
uint32_t altgr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Layout {
|
struct Layout {
|
||||||
@@ -29,6 +32,10 @@ struct Layout {
|
|||||||
char short_name[8];
|
char short_name[8];
|
||||||
KeyMap keys[MAX_KEYS];
|
KeyMap keys[MAX_KEYS];
|
||||||
int key_count; // 0 = passthrough (the kernel's US table)
|
int key_count; // 0 = passthrough (the kernel's US table)
|
||||||
|
// Characters that act as dead keys in this layout: pressing one types
|
||||||
|
// nothing and modifies the next character (see compose()).
|
||||||
|
uint32_t dead_keys[MAX_DEAD_KEYS];
|
||||||
|
int dead_count;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Registry {
|
struct Registry {
|
||||||
@@ -40,8 +47,71 @@ struct State {
|
|||||||
Registry registry;
|
Registry registry;
|
||||||
bool enabled[MAX_LAYOUTS];
|
bool enabled[MAX_LAYOUTS];
|
||||||
int active;
|
int active;
|
||||||
|
uint32_t pending_dead; // dead key waiting for the next character, 0 = none
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==== Dead-key composition ====
|
||||||
|
// Keyed by the spacing form of the accent, which is what a layout puts on the
|
||||||
|
// dead key itself (and what dead key + Space types).
|
||||||
|
|
||||||
|
struct ComposeEntry {
|
||||||
|
uint16_t dead;
|
||||||
|
uint16_t base;
|
||||||
|
uint16_t result;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline constexpr ComposeEntry COMPOSE[] = {
|
||||||
|
// acute
|
||||||
|
{0x00B4,'a',0xE1},{0x00B4,'A',0xC1},{0x00B4,'e',0xE9},{0x00B4,'E',0xC9},
|
||||||
|
{0x00B4,'i',0xED},{0x00B4,'I',0xCD},{0x00B4,'o',0xF3},{0x00B4,'O',0xD3},
|
||||||
|
{0x00B4,'u',0xFA},{0x00B4,'U',0xDA},{0x00B4,'y',0xFD},{0x00B4,'Y',0xDD},
|
||||||
|
{0x00B4,'c',0x107},{0x00B4,'C',0x106},{0x00B4,'l',0x13A},{0x00B4,'L',0x139},
|
||||||
|
{0x00B4,'n',0x144},{0x00B4,'N',0x143},{0x00B4,'r',0x155},{0x00B4,'R',0x154},
|
||||||
|
{0x00B4,'s',0x15B},{0x00B4,'S',0x15A},{0x00B4,'z',0x17A},{0x00B4,'Z',0x179},
|
||||||
|
// caron
|
||||||
|
{0x02C7,'c',0x10D},{0x02C7,'C',0x10C},{0x02C7,'d',0x10F},{0x02C7,'D',0x10E},
|
||||||
|
{0x02C7,'e',0x11B},{0x02C7,'E',0x11A},{0x02C7,'l',0x13E},{0x02C7,'L',0x13D},
|
||||||
|
{0x02C7,'n',0x148},{0x02C7,'N',0x147},{0x02C7,'r',0x159},{0x02C7,'R',0x158},
|
||||||
|
{0x02C7,'s',0x161},{0x02C7,'S',0x160},{0x02C7,'t',0x165},{0x02C7,'T',0x164},
|
||||||
|
{0x02C7,'z',0x17E},{0x02C7,'Z',0x17D},
|
||||||
|
// diaeresis
|
||||||
|
{0x00A8,'a',0xE4},{0x00A8,'A',0xC4},{0x00A8,'e',0xEB},{0x00A8,'E',0xCB},
|
||||||
|
{0x00A8,'i',0xEF},{0x00A8,'I',0xCF},{0x00A8,'o',0xF6},{0x00A8,'O',0xD6},
|
||||||
|
{0x00A8,'u',0xFC},{0x00A8,'U',0xDC},{0x00A8,'y',0xFF},{0x00A8,'Y',0x178},
|
||||||
|
// ring above
|
||||||
|
{0x00B0,'u',0x16F},{0x00B0,'U',0x16E},{0x00B0,'a',0xE5},{0x00B0,'A',0xC5},
|
||||||
|
// grave
|
||||||
|
{0x0060,'a',0xE0},{0x0060,'A',0xC0},{0x0060,'e',0xE8},{0x0060,'E',0xC8},
|
||||||
|
{0x0060,'i',0xEC},{0x0060,'I',0xCC},{0x0060,'o',0xF2},{0x0060,'O',0xD2},
|
||||||
|
{0x0060,'u',0xF9},{0x0060,'U',0xD9},
|
||||||
|
// circumflex
|
||||||
|
{0x005E,'a',0xE2},{0x005E,'A',0xC2},{0x005E,'e',0xEA},{0x005E,'E',0xCA},
|
||||||
|
{0x005E,'i',0xEE},{0x005E,'I',0xCE},{0x005E,'o',0xF4},{0x005E,'O',0xD4},
|
||||||
|
{0x005E,'u',0xFB},{0x005E,'U',0xDB},
|
||||||
|
// tilde
|
||||||
|
{0x007E,'a',0xE3},{0x007E,'A',0xC3},{0x007E,'n',0xF1},{0x007E,'N',0xD1},
|
||||||
|
{0x007E,'o',0xF5},{0x007E,'O',0xD5},
|
||||||
|
// breve
|
||||||
|
{0x02D8,'a',0x103},{0x02D8,'A',0x102},{0x02D8,'g',0x11F},{0x02D8,'G',0x11E},
|
||||||
|
// ogonek
|
||||||
|
{0x02DB,'a',0x105},{0x02DB,'A',0x104},{0x02DB,'e',0x119},{0x02DB,'E',0x118},
|
||||||
|
{0x02DB,'i',0x12F},{0x02DB,'I',0x12E},{0x02DB,'u',0x173},{0x02DB,'U',0x172},
|
||||||
|
// dot above
|
||||||
|
{0x02D9,'z',0x17C},{0x02D9,'Z',0x17B},{0x02D9,'e',0x117},{0x02D9,'E',0x116},
|
||||||
|
{0x02D9,'I',0x130},
|
||||||
|
// double acute
|
||||||
|
{0x02DD,'o',0x151},{0x02DD,'O',0x150},{0x02DD,'u',0x171},{0x02DD,'U',0x170},
|
||||||
|
// cedilla
|
||||||
|
{0x00B8,'c',0xE7},{0x00B8,'C',0xC7},{0x00B8,'s',0x15F},{0x00B8,'S',0x15E},
|
||||||
|
};
|
||||||
|
|
||||||
|
// The composed character, or 0 if the accent does not combine with base.
|
||||||
|
inline uint32_t compose(uint32_t dead, uint32_t base) {
|
||||||
|
for (const ComposeEntry& e : COMPOSE)
|
||||||
|
if (e.dead == dead && e.base == base) return e.result;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
inline void build_key(char* out, int cap, const char* prefix,
|
inline void build_key(char* out, int cap, const char* prefix,
|
||||||
const char* id, const char* suffix = nullptr) {
|
const char* id, const char* suffix = nullptr) {
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
@@ -54,10 +124,9 @@ inline void build_key(char* out, int cap, const char* prefix,
|
|||||||
out[pos] = '\0';
|
out[pos] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read an array of byte values from the layout table.
|
// Read an array of byte values (scancodes) from the layout table.
|
||||||
// Returns the element count, or -1 if the key is missing or malformed.
|
// Returns the element count, or -1 if the key is missing or malformed.
|
||||||
inline int read_bytes(const toml::Doc& doc, const char* key,
|
inline int read_bytes(const toml::Doc& doc, const char* key, uint8_t* out, int cap) {
|
||||||
uint8_t* out, int cap) {
|
|
||||||
toml::Value* arr = doc.get_array(key);
|
toml::Value* arr = doc.get_array(key);
|
||||||
if (!arr) return -1;
|
if (!arr) return -1;
|
||||||
if (arr->array.count > cap) return -1;
|
if (arr->array.count > cap) return -1;
|
||||||
@@ -70,6 +139,23 @@ inline int read_bytes(const toml::Doc& doc, const char* key,
|
|||||||
return arr->array.count;
|
return arr->array.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read an array of characters (Unicode codepoints). 0x80-0x9F are C1 control
|
||||||
|
// codes in Unicode, never key output, so they are read as the Windows-1252
|
||||||
|
// bytes older layout files used (0x80 = euro sign).
|
||||||
|
inline int read_codepoints(const toml::Doc& doc, const char* key, uint32_t* out, int cap) {
|
||||||
|
toml::Value* arr = doc.get_array(key);
|
||||||
|
if (!arr) return -1;
|
||||||
|
if (arr->array.count > cap) return -1;
|
||||||
|
for (int i = 0; i < arr->array.count; i++) {
|
||||||
|
toml::Value* value = arr->array.items[i];
|
||||||
|
if (!value || value->type != toml::Type::Int) return -1;
|
||||||
|
int64_t v = value->ival;
|
||||||
|
if (v < 0 || v > 0x10FFFF || (v >= 0xD800 && v <= 0xDFFF)) return -1;
|
||||||
|
out[i] = (v >= 0x80 && v <= 0x9F) ? utf8::from_cp1252((uint8_t)v) : (uint32_t)v;
|
||||||
|
}
|
||||||
|
return arr->array.count;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse one [layouts.<id>] table. Returns false if the layout declares key
|
// Parse one [layouts.<id>] table. Returns false if the layout declares key
|
||||||
// overrides but they are inconsistent, in which case the caller skips it: a
|
// overrides but they are inconsistent, in which case the caller skips it: a
|
||||||
// malformed table must never produce a half-applied layout.
|
// malformed table must never produce a half-applied layout.
|
||||||
@@ -85,10 +171,15 @@ inline bool load_layout(Layout* out, const char* id, const toml::Doc& doc) {
|
|||||||
montauk::strncpy(out->short_name, doc.get_string(key, id),
|
montauk::strncpy(out->short_name, doc.get_string(key, id),
|
||||||
sizeof(out->short_name));
|
sizeof(out->short_name));
|
||||||
|
|
||||||
|
build_key(key, sizeof(key), "layouts.", id, ".dead_keys");
|
||||||
|
int dead = read_codepoints(doc, key, out->dead_keys, MAX_DEAD_KEYS);
|
||||||
|
if (dead < 0 && doc.get_array(key)) return false; // present but malformed
|
||||||
|
out->dead_count = dead < 0 ? 0 : dead;
|
||||||
|
|
||||||
uint8_t scancodes[MAX_KEYS];
|
uint8_t scancodes[MAX_KEYS];
|
||||||
uint8_t base[MAX_KEYS];
|
uint32_t base[MAX_KEYS];
|
||||||
uint8_t shift[MAX_KEYS];
|
uint32_t shift[MAX_KEYS];
|
||||||
uint8_t altgr[MAX_KEYS] = {};
|
uint32_t altgr[MAX_KEYS] = {};
|
||||||
|
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".scancodes");
|
build_key(key, sizeof(key), "layouts.", id, ".scancodes");
|
||||||
int count = read_bytes(doc, key, scancodes, MAX_KEYS);
|
int count = read_bytes(doc, key, scancodes, MAX_KEYS);
|
||||||
@@ -99,11 +190,11 @@ inline bool load_layout(Layout* out, const char* id, const toml::Doc& doc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".base");
|
build_key(key, sizeof(key), "layouts.", id, ".base");
|
||||||
if (read_bytes(doc, key, base, MAX_KEYS) != count) return false;
|
if (read_codepoints(doc, key, base, MAX_KEYS) != count) return false;
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".shift");
|
build_key(key, sizeof(key), "layouts.", id, ".shift");
|
||||||
if (read_bytes(doc, key, shift, MAX_KEYS) != count) return false;
|
if (read_codepoints(doc, key, shift, MAX_KEYS) != count) return false;
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".altgr");
|
build_key(key, sizeof(key), "layouts.", id, ".altgr");
|
||||||
int altgr_count = read_bytes(doc, key, altgr, MAX_KEYS);
|
int altgr_count = read_codepoints(doc, key, altgr, MAX_KEYS);
|
||||||
if (altgr_count >= 0 && altgr_count != count) return false;
|
if (altgr_count >= 0 && altgr_count != count) return false;
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
@@ -174,6 +265,7 @@ inline Registry load_registry() {
|
|||||||
// it reads one small file instead of re-parsing the whole layout table.
|
// it reads one small file instead of re-parsing the whole layout table.
|
||||||
inline void refresh_selection(State* state, const char* username) {
|
inline void refresh_selection(State* state, const char* username) {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
state->pending_dead = 0; // a dead key never carries over a layout switch
|
||||||
for (int i = 0; i < state->registry.count; i++) state->enabled[i] = false;
|
for (int i = 0; i < state->registry.count; i++) state->enabled[i] = false;
|
||||||
|
|
||||||
toml::Doc doc = config::load_user(username, "keyboard");
|
toml::Doc doc = config::load_user(username, "keyboard");
|
||||||
@@ -237,40 +329,59 @@ inline int next_enabled(const State& state, int current) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Caps Lock is derived rather than declared per key: it applies only where
|
// Caps Lock is derived rather than declared per key: it applies only where
|
||||||
// base and shift are a Windows-1252 lower/upper letter pair. That covers the
|
// base and shift are the lower/upper case of one letter. That covers accented
|
||||||
// accented letters (aa 0xE5 / AA 0xC5) without wrongly upper-casing keys such
|
// letters in any script (aa/AA, e-caron/E-caron) without wrongly upper-casing
|
||||||
// as 2 / " where the shifted value is unrelated punctuation.
|
// keys such as 2 / " where the shifted value is unrelated punctuation.
|
||||||
inline constexpr bool is_letter_pair(uint8_t base, uint8_t shift) {
|
inline constexpr bool is_letter_pair(uint32_t base, uint32_t shift) {
|
||||||
if (base == 0 || shift == 0) return false;
|
if (base == 0 || shift == 0) return false;
|
||||||
if (base < 0x61) return false;
|
return utf8::is_case_pair(base, shift);
|
||||||
if (base > 0x7A && base < 0xE0) return false;
|
|
||||||
if (base == 0xF7) return false; // division sign sits inside the range
|
|
||||||
return shift == (uint8_t)(base - 0x20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA
|
static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA
|
||||||
static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE
|
static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE
|
||||||
static_assert(is_letter_pair(0xE6, 0xC6)); // ae / AE
|
static_assert(is_letter_pair(0x11B, 0x11A)); // e-caron
|
||||||
static_assert(is_letter_pair('a', 'A'));
|
static_assert(is_letter_pair('a', 'A'));
|
||||||
static_assert(!is_letter_pair('2', '"'));
|
static_assert(!is_letter_pair('2', '"'));
|
||||||
static_assert(!is_letter_pair(0xF7, 0xD7)); // divide / multiply
|
static_assert(!is_letter_pair(0xF7, 0xD7)); // divide / multiply
|
||||||
|
static_assert(!is_letter_pair(0x11B, '2')); // Czech number row
|
||||||
|
|
||||||
inline void translate(const State& state, abi::KeyEvent* key) {
|
inline bool is_dead_key(const Layout& layout, uint32_t cp) {
|
||||||
|
if (cp == 0) return false;
|
||||||
|
for (int i = 0; i < layout.dead_count; i++)
|
||||||
|
if (layout.dead_keys[i] == cp) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys that may be pressed between a dead key and its letter without
|
||||||
|
// cancelling it (Shift for a capital, AltGr, Caps Lock, ...).
|
||||||
|
inline constexpr bool is_modifier_scancode(uint8_t sc) {
|
||||||
|
return sc == 0x2A || sc == 0x36 || sc == 0x1D || sc == 0x38 ||
|
||||||
|
sc == 0x3A || sc == 0x45 || sc == 0x46 || sc == 0x5B || sc == 0x5C;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the active layout to a raw key event: the character it types goes to
|
||||||
|
// key->codepoint, its Windows-1252 byte (or KEY_ASCII_SUBSTITUTE) to
|
||||||
|
// key->ascii. Dead keys type nothing and modify the following character, so
|
||||||
|
// the state is updated on presses.
|
||||||
|
inline void translate(State& state, abi::KeyEvent* key) {
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
if (state.active < 0 || state.active >= state.registry.count) return;
|
const Layout* layout = (state.active >= 0 && state.active < state.registry.count)
|
||||||
const Layout& layout = state.registry.items[state.active];
|
? &state.registry.items[state.active] : nullptr;
|
||||||
|
|
||||||
|
uint8_t a = (uint8_t)key->ascii;
|
||||||
|
uint32_t cp = key->codepoint ? key->codepoint
|
||||||
|
: ((a >= 0x20 && a < 0x7F) ? a : 0);
|
||||||
|
uint8_t scancode = key->scancode & 0x7F;
|
||||||
|
|
||||||
// Extended keys carry a main-block scancode with the E0 prefix stripped
|
// Extended keys carry a main-block scancode with the E0 prefix stripped
|
||||||
// (keypad "/" arrives as 0x35, the same as the main "/"), so translating
|
// (keypad "/" arrives as 0x35, the same as the main "/"), so translating
|
||||||
// them would turn keypad "/" into whatever the layout puts on that key.
|
// them would turn keypad "/" into whatever the layout puts on that key.
|
||||||
if (key->extended) return;
|
if (layout && !key->extended) {
|
||||||
|
for (int i = 0; i < layout->key_count; i++) {
|
||||||
uint8_t scancode = key->scancode & 0x7F;
|
const KeyMap& mapping = layout->keys[i];
|
||||||
for (int i = 0; i < layout.key_count; i++) {
|
|
||||||
const KeyMap& mapping = layout.keys[i];
|
|
||||||
if (mapping.scancode != scancode) continue;
|
if (mapping.scancode != scancode) continue;
|
||||||
|
|
||||||
uint8_t out;
|
uint32_t out;
|
||||||
if (key->altgr) {
|
if (key->altgr) {
|
||||||
out = mapping.altgr;
|
out = mapping.altgr;
|
||||||
} else {
|
} else {
|
||||||
@@ -280,14 +391,55 @@ inline void translate(const State& state, abi::KeyEvent* key) {
|
|||||||
out = upper ? mapping.shift : mapping.base;
|
out = upper ? mapping.shift : mapping.base;
|
||||||
}
|
}
|
||||||
if (out != 0) {
|
if (out != 0) {
|
||||||
key->ascii = (char)out;
|
cp = out;
|
||||||
// An AltGr key that produced a character is text, not a shortcut.
|
// An AltGr key that produced a character is text, not a
|
||||||
// Apps gate insertion on !alt (and alt is LeftAlt||RightAlt), so
|
// shortcut. Apps gate insertion on !alt (and alt is
|
||||||
// leaving it set would silently swallow every AltGr character.
|
// LeftAlt||RightAlt), so leaving it set would silently swallow
|
||||||
|
// every AltGr character.
|
||||||
if (key->altgr) key->alt = false;
|
if (key->altgr) key->alt = false;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dead = layout && !key->extended && is_dead_key(*layout, cp);
|
||||||
|
bool shortcut = key->ctrl || (key->alt && !key->altgr);
|
||||||
|
if (dead && shortcut) dead = false;
|
||||||
|
|
||||||
|
if (key->pressed && !shortcut) {
|
||||||
|
if (dead) {
|
||||||
|
if (state.pending_dead == cp) {
|
||||||
|
state.pending_dead = 0; // pressed twice: type the accent
|
||||||
|
dead = false;
|
||||||
|
} else {
|
||||||
|
state.pending_dead = cp; // wait for the next character
|
||||||
|
}
|
||||||
|
} else if (state.pending_dead) {
|
||||||
|
if (cp == ' ') {
|
||||||
|
cp = state.pending_dead; // accent + Space: the accent
|
||||||
|
state.pending_dead = 0;
|
||||||
|
} else if (cp) {
|
||||||
|
// The composed letter, or just the letter when the accent does
|
||||||
|
// not apply to it (one event can only carry one character).
|
||||||
|
uint32_t composed = compose(state.pending_dead, cp);
|
||||||
|
if (composed) cp = composed;
|
||||||
|
state.pending_dead = 0;
|
||||||
|
} else if (!is_modifier_scancode(scancode)) {
|
||||||
|
state.pending_dead = 0; // Enter, arrows, ... cancel it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dead) {
|
||||||
|
key->codepoint = 0;
|
||||||
|
key->ascii = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
key->codepoint = cp;
|
||||||
|
if (cp != 0) {
|
||||||
|
uint8_t b = utf8::to_cp1252(cp);
|
||||||
|
key->ascii = b ? (char)b : abi::KEY_ASCII_SUBSTITUTE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Direct keyboard readers ----
|
// ---- Direct keyboard readers ----
|
||||||
|
|||||||
@@ -6,30 +6,85 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// Userspace mirror of kernel/src/Common/StatusCodes.hpp. Also usable from C
|
// Userspace status codes.
|
||||||
// (the libc includes it), where the enumerators are unqualified.
|
// Analogous to kernel/src/Common/StatusCodes.hpp, but positive numbers are used.
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
namespace montauk {
|
namespace montauk {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
enum StatusCode {
|
typedef enum {
|
||||||
Success = 0,
|
Success = 0,
|
||||||
GenericFailure = -1,
|
GenericFailure = 1,
|
||||||
PermissionDenied = -2,
|
PermissionDenied = 2,
|
||||||
CapabilityDenied = -3,
|
CapabilityDenied = 3,
|
||||||
SystemIntegrityError = -4,
|
SystemIntegrityError = 4,
|
||||||
BadPointer = -5,
|
BadPointer = 5,
|
||||||
InvalidArgument = -6,
|
InvalidArgument = 6,
|
||||||
UnsupportedOp = -7,
|
UnsupportedOp = 7,
|
||||||
NoDevice = -8,
|
NoDevice = 8,
|
||||||
IoFailure = -9,
|
IoFailure = 9,
|
||||||
BadInvocation = -10,
|
BadInvocation = 10,
|
||||||
NoSuchPath = -11,
|
NoSuchPath = 11,
|
||||||
Busy = -12,
|
Busy = 12,
|
||||||
BufferTooSmall = -13,
|
BufferTooSmall = 13,
|
||||||
OutOfMemory = -14
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Api/Syscall.hpp>
|
#include <Api/Syscall.hpp>
|
||||||
#include <montauk/statuscodes.h>
|
#include <montauk/statuscodes.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
|
|
||||||
namespace montauk {
|
namespace montauk {
|
||||||
|
|
||||||
@@ -222,7 +223,23 @@ namespace montauk {
|
|||||||
|
|
||||||
// Keyboard
|
// Keyboard
|
||||||
inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); }
|
inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); }
|
||||||
inline void getkey(montauk::abi::KeyEvent* out) { syscall1(montauk::abi::SYS_GETKEY, (uint64_t)out); }
|
// Scancode to use when checking for navigation keys (arrows, Home/End,
|
||||||
|
// PgUp/PgDn, Insert/Delete). The keypad shares those scancodes with its
|
||||||
|
// digit and '.' keys, so a keypad key that types a character reports 0
|
||||||
|
// here: numpad 4 is text, not Left. Compare against this, not .scancode.
|
||||||
|
inline uint8_t nav_scancode(const montauk::abi::KeyEvent& key) {
|
||||||
|
uint8_t sc = key.scancode;
|
||||||
|
if (!key.extended && (sc & 0x7F) >= 0x47 && (sc & 0x7F) <= 0x53 &&
|
||||||
|
montauk::utf8::key_codepoint(key) != 0)
|
||||||
|
return 0;
|
||||||
|
return sc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fills the full event, including the Unicode codepoint the key types.
|
||||||
|
inline void getkey(montauk::abi::KeyEvent* out) {
|
||||||
|
syscall2(montauk::abi::SYS_GETKEY_EX, (uint64_t)out, sizeof(*out));
|
||||||
|
}
|
||||||
|
// Text keys arrive as UTF-8, one byte per call.
|
||||||
inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); }
|
inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); }
|
||||||
// Returns 0 rather than blocking when no character is pending.
|
// Returns 0 rather than blocking when no character is pending.
|
||||||
inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); }
|
inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); }
|
||||||
@@ -496,7 +513,8 @@ namespace montauk {
|
|||||||
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
|
||||||
}
|
}
|
||||||
inline int childio_writekey(int childPid, const montauk::abi::KeyEvent* key) {
|
inline int childio_writekey(int childPid, const montauk::abi::KeyEvent* key) {
|
||||||
return (int)syscall2(montauk::abi::SYS_CHILDIO_WRITEKEY, (uint64_t)childPid, (uint64_t)key);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITEKEY_EX, (uint64_t)childPid,
|
||||||
|
(uint64_t)key, sizeof(*key));
|
||||||
}
|
}
|
||||||
inline int childio_settermsz(int childPid, int cols, int rows) {
|
inline int childio_settermsz(int childPid, int cols, int rows) {
|
||||||
return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
#include <montauk/config.h>
|
#include <montauk/config.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
#include <bearssl_hash.h>
|
#include <bearssl_hash.h>
|
||||||
|
|
||||||
namespace montauk {
|
namespace montauk {
|
||||||
@@ -107,7 +108,7 @@ namespace user {
|
|||||||
bytes_to_hex(hash, 32, out_hex64);
|
bytes_to_hex(hash, 32, out_hex64);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool verify_password(const char* password, const char* salt_hex, const char* stored_hex64) {
|
inline bool verify_password_bytes(const char* password, const char* salt_hex, const char* stored_hex64) {
|
||||||
char computed[65];
|
char computed[65];
|
||||||
hash_password(password, salt_hex, computed);
|
hash_password(password, salt_hex, computed);
|
||||||
|
|
||||||
@@ -118,6 +119,26 @@ namespace user {
|
|||||||
return diff == 0;
|
return diff == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Passwords are hashed as UTF-8. Before the switch to UTF-8, password
|
||||||
|
// fields stored typed characters as Windows-1252 bytes, so a non-ASCII
|
||||||
|
// password set back then only matches its 1252 form; try that too.
|
||||||
|
inline bool verify_password(const char* password, const char* salt_hex, const char* stored_hex64) {
|
||||||
|
if (verify_password_bytes(password, salt_hex, stored_hex64)) return true;
|
||||||
|
|
||||||
|
char legacy[256];
|
||||||
|
int o = 0;
|
||||||
|
bool non_ascii = false;
|
||||||
|
for (const char* p = password; *p;) {
|
||||||
|
uint32_t cp = montauk::utf8::next(&p);
|
||||||
|
uint8_t b = montauk::utf8::to_cp1252(cp);
|
||||||
|
if (b == 0 || o >= (int)sizeof(legacy) - 1) return false;
|
||||||
|
if (b >= 0x80) non_ascii = true;
|
||||||
|
legacy[o++] = (char)b;
|
||||||
|
}
|
||||||
|
legacy[o] = '\0';
|
||||||
|
return non_ascii && verify_password_bytes(legacy, salt_hex, stored_hex64);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- User database I/O ----
|
// ---- User database I/O ----
|
||||||
|
|
||||||
inline int load_users(UserInfo* buf, int max) {
|
inline int load_users(UserInfo* buf, int max) {
|
||||||
|
|||||||
@@ -0,0 +1,399 @@
|
|||||||
|
/*
|
||||||
|
* utf8.h
|
||||||
|
* UTF-8 text encoding helpers with a Windows-1252 fallback
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// MontaukOS text is UTF-8. Before this it was single-byte Windows-1252, so
|
||||||
|
// older files (and anything else that is not valid UTF-8) still turn up. The
|
||||||
|
// decoder never fails: a byte that does not start a well-formed sequence is
|
||||||
|
// read as ONE Windows-1252 character. Valid UTF-8 and plain ASCII are
|
||||||
|
// unaffected, and legacy text keeps rendering exactly as it used to.
|
||||||
|
//
|
||||||
|
// Character boundaries follow the same rule, so cursor movement, deletion and
|
||||||
|
// truncation all agree with what the renderer draws.
|
||||||
|
|
||||||
|
namespace montauk::utf8 {
|
||||||
|
|
||||||
|
inline constexpr uint32_t REPLACEMENT = 0xFFFD;
|
||||||
|
inline constexpr int MAX_BYTES = 4;
|
||||||
|
|
||||||
|
// Windows-1252 byte -> Unicode codepoint. 0x80-0x9F carry the typographic
|
||||||
|
// punctuation; every other byte is identical to Latin-1 / Unicode.
|
||||||
|
inline constexpr uint32_t from_cp1252(uint8_t b) {
|
||||||
|
switch (b) {
|
||||||
|
case 0x80: return 0x20AC; // Euro Sign
|
||||||
|
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
|
||||||
|
case 0x83: return 0x0192; // Latin Small Letter F With Hook
|
||||||
|
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
|
||||||
|
case 0x85: return 0x2026; // Horizontal Ellipsis
|
||||||
|
case 0x86: return 0x2020; // Dagger
|
||||||
|
case 0x87: return 0x2021; // Double Dagger
|
||||||
|
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
|
||||||
|
case 0x89: return 0x2030; // Per Mille Sign
|
||||||
|
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
|
||||||
|
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
|
||||||
|
case 0x8C: return 0x0152; // Latin Capital Ligature OE
|
||||||
|
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
|
||||||
|
case 0x91: return 0x2018; // Left Single Quotation Mark
|
||||||
|
case 0x92: return 0x2019; // Right Single Quotation Mark
|
||||||
|
case 0x93: return 0x201C; // Left Double Quotation Mark
|
||||||
|
case 0x94: return 0x201D; // Right Double Quotation Mark
|
||||||
|
case 0x95: return 0x2022; // Bullet
|
||||||
|
case 0x96: return 0x2013; // En Dash
|
||||||
|
case 0x97: return 0x2014; // Em Dash
|
||||||
|
case 0x98: return 0x02DC; // Small Tilde
|
||||||
|
case 0x99: return 0x2122; // Trade Mark Sign
|
||||||
|
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
|
||||||
|
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
|
||||||
|
case 0x9C: return 0x0153; // Latin Small Ligature OE
|
||||||
|
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
|
||||||
|
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
|
||||||
|
default: return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicode codepoint -> Windows-1252 byte, or 0 if it has none. Used where a
|
||||||
|
// single byte is still required (PDF WinAnsi output, legacy byte tables).
|
||||||
|
inline constexpr uint8_t to_cp1252(uint32_t cp) {
|
||||||
|
if (cp < 0x80 || (cp >= 0xA0 && cp <= 0xFF)) return (uint8_t)cp;
|
||||||
|
for (int b = 0x80; b <= 0x9F; b++) {
|
||||||
|
uint32_t m = from_cp1252((uint8_t)b);
|
||||||
|
if (m != (uint32_t)b && m == cp) return (uint8_t)b;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length of the well-formed UTF-8 sequence starting at s (at most `avail`
|
||||||
|
// bytes, stopping early at a NUL), or 0 if it is not one. Rejects overlong
|
||||||
|
// forms, surrogates and values above U+10FFFF.
|
||||||
|
inline int sequence_length(const char* s, int avail, uint32_t* out_cp = nullptr) {
|
||||||
|
if (avail <= 0) return 0;
|
||||||
|
const uint8_t* u = (const uint8_t*)s;
|
||||||
|
uint8_t b0 = u[0];
|
||||||
|
if (b0 < 0x80) {
|
||||||
|
if (out_cp) *out_cp = b0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n;
|
||||||
|
uint32_t cp, min;
|
||||||
|
if (b0 >= 0xC2 && b0 <= 0xDF) { n = 2; cp = b0 & 0x1F; min = 0x80; }
|
||||||
|
else if (b0 >= 0xE0 && b0 <= 0xEF) { n = 3; cp = b0 & 0x0F; min = 0x800; }
|
||||||
|
else if (b0 >= 0xF0 && b0 <= 0xF4) { n = 4; cp = b0 & 0x07; min = 0x10000; }
|
||||||
|
else return 0;
|
||||||
|
|
||||||
|
if (n > avail) return 0;
|
||||||
|
for (int i = 1; i < n; i++) {
|
||||||
|
uint8_t c = u[i];
|
||||||
|
if ((c & 0xC0) != 0x80) return 0; // also catches NUL
|
||||||
|
cp = (cp << 6) | (c & 0x3F);
|
||||||
|
}
|
||||||
|
if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return 0;
|
||||||
|
if (out_cp) *out_cp = cp;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode one character from a buffer of `avail` bytes. Returns the codepoint
|
||||||
|
// and stores the number of bytes consumed (always >= 1 when avail > 0).
|
||||||
|
inline uint32_t decode(const char* s, int avail, int* consumed) {
|
||||||
|
if (avail <= 0) { if (consumed) *consumed = 0; return 0; }
|
||||||
|
uint32_t cp;
|
||||||
|
int n = sequence_length(s, avail, &cp);
|
||||||
|
if (n == 0) {
|
||||||
|
n = 1;
|
||||||
|
cp = from_cp1252((uint8_t)s[0]);
|
||||||
|
}
|
||||||
|
if (consumed) *consumed = n;
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode one character from a NUL-terminated string and advance *p past it.
|
||||||
|
// Returns 0 (without advancing) at the terminator.
|
||||||
|
inline uint32_t next(const char** p) {
|
||||||
|
const char* s = *p;
|
||||||
|
if (!*s) return 0;
|
||||||
|
int n;
|
||||||
|
// A sequence never spans the terminator: sequence_length stops at NUL
|
||||||
|
// because NUL is not a continuation byte.
|
||||||
|
uint32_t cp = decode(s, MAX_BYTES, &n);
|
||||||
|
*p = s + n;
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode a codepoint as UTF-8 into out (room for 4 bytes, NOT terminated).
|
||||||
|
// Returns the byte count. Invalid codepoints encode U+FFFD.
|
||||||
|
inline int encode(uint32_t cp, char* out) {
|
||||||
|
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = REPLACEMENT;
|
||||||
|
uint8_t* o = (uint8_t*)out;
|
||||||
|
if (cp < 0x80) { o[0] = (uint8_t)cp; return 1; }
|
||||||
|
if (cp < 0x800) {
|
||||||
|
o[0] = (uint8_t)(0xC0 | (cp >> 6));
|
||||||
|
o[1] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (cp < 0x10000) {
|
||||||
|
o[0] = (uint8_t)(0xE0 | (cp >> 12));
|
||||||
|
o[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
o[2] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
o[0] = (uint8_t)(0xF0 | (cp >> 18));
|
||||||
|
o[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
|
||||||
|
o[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
o[3] = (uint8_t)(0x80 | (cp & 0x3F));
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int encoded_length(uint32_t cp) {
|
||||||
|
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return 3; // U+FFFD
|
||||||
|
return cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== Case mapping ====
|
||||||
|
// Simple one-to-one case mapping for the scripts MontaukOS keyboard layouts
|
||||||
|
// cover: ASCII, Latin-1, Latin Extended-A, Greek and Cyrillic. Anything else
|
||||||
|
// is returned unchanged.
|
||||||
|
|
||||||
|
inline constexpr uint32_t to_upper(uint32_t c) {
|
||||||
|
if (c >= 'a' && c <= 'z') return c - 0x20;
|
||||||
|
if (c < 0x80) return c;
|
||||||
|
if (c >= 0xE0 && c <= 0xFE && c != 0xF7) return c - 0x20;
|
||||||
|
if (c == 0xFF) return 0x178;
|
||||||
|
if (c == 0xB5) return 0x39C; // micro -> Mu
|
||||||
|
// Latin Extended-A: pairs alternate upper/lower, except the two ranges
|
||||||
|
// where the alignment shifts by one (L-stroke..N-apostrophe, Z-acute..).
|
||||||
|
if (c == 0x131) return 'I'; // dotless i
|
||||||
|
if (c >= 0x100 && c <= 0x137 && c != 0x130) return (c & 1) ? c - 1 : c;
|
||||||
|
if (c >= 0x139 && c <= 0x148) return (c & 1) ? c : c - 1;
|
||||||
|
if (c >= 0x14A && c <= 0x177) return (c & 1) ? c - 1 : c;
|
||||||
|
if (c >= 0x179 && c <= 0x17E) return (c & 1) ? c : c - 1;
|
||||||
|
if (c == 0x17F) return 'S'; // long s
|
||||||
|
// Greek
|
||||||
|
if (c >= 0x3B1 && c <= 0x3C9 && c != 0x3C2) return c - 0x20;
|
||||||
|
if (c == 0x3C2) return 0x3A3; // final sigma
|
||||||
|
if (c == 0x3AC) return 0x386;
|
||||||
|
if (c >= 0x3AD && c <= 0x3AF) return c - 0x25;
|
||||||
|
if (c == 0x3CC) return 0x38C;
|
||||||
|
if (c == 0x3CD || c == 0x3CE) return c - 0x3F;
|
||||||
|
// Cyrillic
|
||||||
|
if (c >= 0x430 && c <= 0x44F) return c - 0x20;
|
||||||
|
if (c >= 0x450 && c <= 0x45F) return c - 0x50;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline constexpr uint32_t to_lower(uint32_t c) {
|
||||||
|
if (c >= 'A' && c <= 'Z') return c + 0x20;
|
||||||
|
if (c < 0x80) return c;
|
||||||
|
if (c >= 0xC0 && c <= 0xDE && c != 0xD7) return c + 0x20;
|
||||||
|
if (c == 0x178) return 0xFF;
|
||||||
|
if (c == 0x130) return 'i'; // dotted I
|
||||||
|
if (c >= 0x100 && c <= 0x137) return (c & 1) ? c : c + 1;
|
||||||
|
if (c >= 0x139 && c <= 0x148) return (c & 1) ? c + 1 : c;
|
||||||
|
if (c >= 0x14A && c <= 0x177) return (c & 1) ? c : c + 1;
|
||||||
|
if (c >= 0x179 && c <= 0x17E) return (c & 1) ? c + 1 : c;
|
||||||
|
if (c >= 0x391 && c <= 0x3A9 && c != 0x3A2) return c + 0x20;
|
||||||
|
if (c == 0x386) return 0x3AC;
|
||||||
|
if (c >= 0x388 && c <= 0x38A) return c + 0x25;
|
||||||
|
if (c == 0x38C) return 0x3CC;
|
||||||
|
if (c == 0x38E || c == 0x38F) return c + 0x3F;
|
||||||
|
if (c >= 0x410 && c <= 0x42F) return c + 0x20;
|
||||||
|
if (c >= 0x400 && c <= 0x40F) return c + 0x50;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if lower/upper are the two cases of one letter.
|
||||||
|
inline constexpr bool is_case_pair(uint32_t lower, uint32_t upper) {
|
||||||
|
return lower != upper && to_upper(lower) == upper && to_lower(upper) == lower;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text a key event types, as a codepoint (0 for control/non-text keys).
|
||||||
|
// KeyEvent::codepoint is filled by the kernel and by layout translation;
|
||||||
|
// events built by hand may leave it 0, so fall back to `ascii` (a
|
||||||
|
// Windows-1252 byte).
|
||||||
|
template <typename Key>
|
||||||
|
inline uint32_t key_codepoint(const Key& key) {
|
||||||
|
if (key.codepoint) {
|
||||||
|
uint32_t cp = key.codepoint;
|
||||||
|
return (cp < 0x20 || cp == 0x7F || (cp >= 0x80 && cp < 0xA0)) ? 0 : cp;
|
||||||
|
}
|
||||||
|
uint8_t b = (uint8_t)key.ascii;
|
||||||
|
if (b < 0x20 || b == 0x7F) return 0;
|
||||||
|
return from_cp1252(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same, encoded as UTF-8 into out (4 bytes). Returns the length, 0 if none.
|
||||||
|
template <typename Key>
|
||||||
|
inline int key_utf8(const Key& key, char* out) {
|
||||||
|
uint32_t cp = key_codepoint(key);
|
||||||
|
return cp ? encode(cp, out) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte offset of the character after the one at `pos` (len = buffer length).
|
||||||
|
inline int next_boundary(const char* s, int len, int pos) {
|
||||||
|
if (pos >= len) return len;
|
||||||
|
if (pos < 0) return 0;
|
||||||
|
int n;
|
||||||
|
decode(s + pos, len - pos, &n);
|
||||||
|
return pos + n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte offset of the character before `pos`. A run of continuation bytes is
|
||||||
|
// one character only if it completes a well-formed sequence ending exactly at
|
||||||
|
// pos; otherwise the previous byte stands alone (Windows-1252 fallback).
|
||||||
|
inline int prev_boundary(const char* s, int pos) {
|
||||||
|
if (pos <= 0) return 0;
|
||||||
|
for (int back = 2; back <= MAX_BYTES && back <= pos; back++) {
|
||||||
|
if (((uint8_t)s[pos - back + 1] & 0xC0) != 0x80) break;
|
||||||
|
int start = pos - back;
|
||||||
|
if (sequence_length(s + start, back) == back) return start;
|
||||||
|
}
|
||||||
|
return pos - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snap `pos` back onto a character boundary. Use before truncating a buffer
|
||||||
|
// or splitting at an arbitrary byte offset.
|
||||||
|
inline int floor_boundary(const char* s, int len, int pos) {
|
||||||
|
if (pos <= 0) return 0;
|
||||||
|
if (pos >= len) return len;
|
||||||
|
if (((uint8_t)s[pos] & 0xC0) != 0x80) return pos;
|
||||||
|
// pos is a continuation byte: it is inside a character only if a
|
||||||
|
// well-formed sequence starting up to 3 bytes earlier covers it.
|
||||||
|
for (int back = 1; back < MAX_BYTES && back <= pos; back++) {
|
||||||
|
int start = pos - back;
|
||||||
|
int n = sequence_length(s + start, len - start);
|
||||||
|
if (n > back) return start;
|
||||||
|
if (((uint8_t)s[start] & 0xC0) != 0x80) break;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of characters in a NUL-terminated string.
|
||||||
|
inline int count(const char* s) {
|
||||||
|
int n = 0;
|
||||||
|
while (*s) { next(&s); n++; }
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of characters in the first `len` bytes.
|
||||||
|
inline int count(const char* s, int len) {
|
||||||
|
int n = 0, pos = 0;
|
||||||
|
while (pos < len) { pos = next_boundary(s, len, pos); n++; }
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte offset of the `index`th character (clamped to len).
|
||||||
|
inline int offset_of(const char* s, int len, int index) {
|
||||||
|
int pos = 0;
|
||||||
|
while (index-- > 0 && pos < len) pos = next_boundary(s, len, pos);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if the buffer is entirely well-formed UTF-8.
|
||||||
|
inline bool valid(const char* s, int len) {
|
||||||
|
for (int pos = 0; pos < len;) {
|
||||||
|
int n = sequence_length(s + pos, len - pos);
|
||||||
|
if (n == 0) return false;
|
||||||
|
pos += n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite a buffer as well-formed UTF-8 (each stray byte becomes the
|
||||||
|
// character the Windows-1252 fallback would display). Returns the output
|
||||||
|
// length, truncated on a character boundary to fit cap - 1, NUL-terminated.
|
||||||
|
inline int normalize(const char* in, int len, char* out, int cap) {
|
||||||
|
if (cap <= 0) return 0;
|
||||||
|
int o = 0;
|
||||||
|
for (int pos = 0; pos < len;) {
|
||||||
|
int n;
|
||||||
|
uint32_t cp = decode(in + pos, len - pos, &n);
|
||||||
|
int need = encoded_length(cp);
|
||||||
|
if (o + need > cap - 1) break;
|
||||||
|
o += encode(cp, out + o);
|
||||||
|
pos += n;
|
||||||
|
}
|
||||||
|
out[o] = '\0';
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy src into dst (cap bytes incl. NUL) without splitting a character.
|
||||||
|
inline int copy(char* dst, int cap, const char* src) {
|
||||||
|
if (cap <= 0) return 0;
|
||||||
|
int len = 0;
|
||||||
|
while (src[len]) len++;
|
||||||
|
int n = len < cap - 1 ? len : floor_boundary(src, len, cap - 1);
|
||||||
|
for (int i = 0; i < n; i++) dst[i] = src[i];
|
||||||
|
dst[n] = '\0';
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==== Editing a NUL-terminated buffer with a byte cursor ====
|
||||||
|
// `len` is the current length, `cap` the buffer size including the NUL. The
|
||||||
|
// cursor is a byte offset that is always kept on a character boundary.
|
||||||
|
|
||||||
|
// Insert n bytes at *cursor. Returns false (buffer unchanged) if they don't fit.
|
||||||
|
inline bool insert(char* buf, int* len, int cap, int* cursor,
|
||||||
|
const char* s, int n) {
|
||||||
|
if (n <= 0 || *len + n >= cap) return false;
|
||||||
|
int at = *cursor < 0 ? 0 : (*cursor > *len ? *len : *cursor);
|
||||||
|
for (int i = *len; i >= at; i--) buf[i + n] = buf[i]; // moves the NUL
|
||||||
|
for (int i = 0; i < n; i++) buf[at + i] = s[i];
|
||||||
|
*len += n;
|
||||||
|
*cursor = at + n;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the text a key event types. Returns false if it types nothing or
|
||||||
|
// does not fit.
|
||||||
|
template <typename Key>
|
||||||
|
inline bool insert_key(char* buf, int* len, int cap, int* cursor, const Key& key) {
|
||||||
|
char t[MAX_BYTES];
|
||||||
|
int n = key_utf8(key, t);
|
||||||
|
return n > 0 && insert(buf, len, cap, cursor, t, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the bytes [start, end).
|
||||||
|
inline void erase(char* buf, int* len, int start, int end) {
|
||||||
|
if (start < 0) start = 0;
|
||||||
|
if (end > *len) end = *len;
|
||||||
|
if (start >= end) return;
|
||||||
|
int n = end - start;
|
||||||
|
for (int i = start; i + n <= *len; i++) buf[i] = buf[i + n]; // moves the NUL
|
||||||
|
*len -= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backspace: delete the character before the cursor.
|
||||||
|
inline bool erase_before(char* buf, int* len, int* cursor) {
|
||||||
|
if (*cursor <= 0) return false;
|
||||||
|
int start = prev_boundary(buf, *cursor);
|
||||||
|
erase(buf, len, start, *cursor);
|
||||||
|
*cursor = start;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete: remove the character at the cursor.
|
||||||
|
inline bool erase_at(char* buf, int* len, int cursor) {
|
||||||
|
if (cursor >= *len) return false;
|
||||||
|
erase(buf, len, cursor, next_boundary(buf, *len, cursor));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append typed text at the end (simple line inputs without a cursor).
|
||||||
|
template <typename Key>
|
||||||
|
inline bool append_key(char* buf, int* len, int cap, const Key& key) {
|
||||||
|
int cursor = *len;
|
||||||
|
return insert_key(buf, len, cap, &cursor, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backspace for append-only inputs: drop the last character.
|
||||||
|
inline bool erase_last(char* buf, int* len) {
|
||||||
|
int cursor = *len;
|
||||||
|
return erase_before(buf, len, &cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace montauk::utf8
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <stdint.h>
|
|
||||||
#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
|
|
||||||
Binary file not shown.
Binary file not shown.
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user