Compare commits
12
Commits
7979acc2eb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e66c3b249 | ||
|
|
31edff3da4 | ||
|
|
f3d475a0d5 | ||
|
|
278b208258 | ||
|
|
f8b8f9610f | ||
|
|
a81298fb24 | ||
|
|
abfd9de8f6 | ||
|
|
60ca9c78c7 | ||
|
|
2aa6360b3d | ||
|
|
32660ddd1a | ||
|
|
a51ee3339f | ||
|
|
f9e21e0f13 |
No files matched your search
@@ -252,6 +252,10 @@ ifeq ($(ARCH),loongarch64)
|
|||||||
endif
|
endif
|
||||||
rm -rf iso_root
|
rm -rf iso_root
|
||||||
|
|
||||||
|
# Example and regression-test programs that are built into 0:/os for
|
||||||
|
# development but have no place on a release image.
|
||||||
|
RELEASE_EXCLUDE_PROGRAMS := rusthello.elf futextest.elf memtest.elf
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
release: limine/limine kernel ramdisk
|
release: limine/limine kernel ramdisk
|
||||||
rm -rf iso_root .release-tmp
|
rm -rf iso_root .release-tmp
|
||||||
@@ -281,6 +285,7 @@ ifeq ($(RELEASE_WITHOUT_SDK),1)
|
|||||||
rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \
|
rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \
|
||||||
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
|
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
|
||||||
endif
|
endif
|
||||||
|
rm -f $(addprefix .release-tmp/os/,$(RELEASE_EXCLUDE_PROGRAMS))
|
||||||
find .release-tmp -name '*.elf' -exec strip {} +
|
find .release-tmp -name '*.elf' -exec strip {} +
|
||||||
# The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is
|
# The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is
|
||||||
# the image the Installer lays down on the target disk -- and it is not named
|
# the image the Installer lays down on the target disk -- and it is not named
|
||||||
|
|||||||
@@ -1,3 +1,78 @@
|
|||||||
|
MontaukOS 0.2.0 (October 14, 2026)
|
||||||
|
(278b20825815ec04a22fd1c2fcfedbd38e116e6d)
|
||||||
|
|
||||||
|
New
|
||||||
|
====
|
||||||
|
* Capability (application permissions) model added
|
||||||
|
* Consent prompts for missing app permissions
|
||||||
|
* New Capabilities app for reviewing and managing app permissions
|
||||||
|
* Unified system status codes; the shell and Files now print a readable message on failure
|
||||||
|
* Text is now UTF-8 throughout, with Unicode key events and dead keys
|
||||||
|
* Keyboard layout support, with English, Norwegian, Czech, Swedish, and Danish layouts
|
||||||
|
* NumLock support
|
||||||
|
* Battery percentage reporting via ACPI, with a battery indicator in the desktop panel
|
||||||
|
* Display brightness control, including on hybrid graphics laptops, with a slider in the desktop panel
|
||||||
|
* Discrete GPUs are powered down automatically on hybrid graphics laptops
|
||||||
|
* ext2 block cache with readahead, speeding up disk-heavy work such as on-OS compilation
|
||||||
|
* Userspace system logging: klog is now syslog, logs identify the user and program, and system services log through montauk::service_log
|
||||||
|
* The system log is written to disk (0:/state/logs) during shutdown
|
||||||
|
* Desktop sessions now own the processes started in them, and those processes are ended on logout
|
||||||
|
* Rust userspace support (no_std)
|
||||||
|
* Futex syscalls (SYS_FUTEX_WAIT/SYS_FUTEX_WAKE) as groundwork for Rust std and pthreads
|
||||||
|
* Process termination codes; abort() now ends the whole process from any thread
|
||||||
|
* isatty() now reports whether a terminal is actually attached
|
||||||
|
* Settings panel and a Pure Black theme in Terminal (the default for Console sessions)
|
||||||
|
* Syntax highlighting for C++, HTML, CSS, and JavaScript in edit and Text Editor
|
||||||
|
* Expanded SVG renderer
|
||||||
|
* New vol command for listing mounted volumes
|
||||||
|
* Revamped desktop application categories
|
||||||
|
* Fonts, icons, and manual pages moved to 0:/os; persistent state now lives in 0:/state
|
||||||
|
* RTL-SDR driver moved from the kernel to userspace
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
=====
|
||||||
|
* Opening a directory as a file is now rejected on ext2 and FAT32, fixing on-OS gcc on installed systems
|
||||||
|
* Fixed several syscall result and status code comparisons
|
||||||
|
* Fixed a TLS layout bug for binaries whose TLS segment has an alignment below 16
|
||||||
|
* A fault in one thread now ends the whole process instead of leaving its sibling threads running
|
||||||
|
* Fixed an ACPI hardware ID endianness bug
|
||||||
|
* Fixed a race in Wi-Fi event processing and other Wi-Fi bugs
|
||||||
|
* DHCP no longer runs when no network device has an active link
|
||||||
|
* Reduced idle CPU wakeups and deferred-work polling
|
||||||
|
* Redirected consoles are now inherited by processes spawned with capabilities
|
||||||
|
* Fixed numpad navigation
|
||||||
|
* Fixed a timezone formatting bug
|
||||||
|
* Fixed the login wallpaper startup delay
|
||||||
|
* Fixed text overlap in file dialogs
|
||||||
|
* Persisted kernel logs are no longer double-spaced
|
||||||
|
* Files lists now scroll with the arrow keys, and volume labels are consistent
|
||||||
|
* mbrtowc added to libc
|
||||||
|
|
||||||
|
Included in ISO
|
||||||
|
================
|
||||||
|
|
||||||
|
Two x86_64 images are published for this release.
|
||||||
|
montauk-0.2.0-sdk-x86_64.iso additionally carries the
|
||||||
|
Montauk SDK (gcc, binutils, etc.)
|
||||||
|
|
||||||
|
* MontaukOS SMP kernel
|
||||||
|
* Userspace bootstrap (0:/os/init.elf) and other essential system utilities
|
||||||
|
* MontaukOS desktop environment and graphical apps
|
||||||
|
* NetSurf web browser (0:/apps/netsurf)
|
||||||
|
* Manual pages (0:/os/man), readable via man command
|
||||||
|
* Service and system configuration (0:/config: init.toml, capabilities.toml, ssh.toml, ntp.toml)
|
||||||
|
* System CA certificates (0:/os/certs/ca-certificates.crt)
|
||||||
|
* Intel Bluetooth controller and Wi-Fi (iwlwifi) firmware (0:/os/firmware/intel)
|
||||||
|
* Third-party license texts and notices (0:/os/licenses)
|
||||||
|
* Tiny C Compiler (tcc) with MontaukOS headers and libraries (0:/sdk/tcc)
|
||||||
|
* Lua 5.4 interpreter (lua) with headers and static library (0:/sdk/lua)
|
||||||
|
* Montauk SDK - native GCC/G++ and binutils (0:/sdk) - SDK image only
|
||||||
|
* Flat Remix icon pack
|
||||||
|
* JetBrains Mono, Noto Serif, Roboto, and C059 Roman (open Century Schoolbook) TrueType fonts
|
||||||
|
* DOOM engine with SDL_mixer audio support (game data not included)
|
||||||
|
* Experimental HTTP server (httpd) and default page (0:/www/index.html)
|
||||||
|
* Default wallpaper - blossoms by Nikhil Kumar, Unsplash (0:/os/wallpapers)
|
||||||
|
|
||||||
MontaukOS 0.1.9 (August 13, 2026)
|
MontaukOS 0.1.9 (August 13, 2026)
|
||||||
(a28b8ad0b2932203b812e967f35f029f060fd6b5)
|
(a28b8ad0b2932203b812e967f35f029f060fd6b5)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* AcpiWmi.cpp
|
||||||
|
* ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call
|
||||||
|
* its WMxx method
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AcpiWmi.hpp"
|
||||||
|
#include "AcpiDevices.hpp"
|
||||||
|
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Hal {
|
||||||
|
namespace AcpiWmi {
|
||||||
|
|
||||||
|
static constexpr const char* HidWmi = "PNP0C14";
|
||||||
|
static constexpr uint32_t WdgEntrySize = 20; // GUID, id, count, flags
|
||||||
|
|
||||||
|
static int HexDigit(char c) {
|
||||||
|
if (c >= '0' && c <= '9') return c - '0';
|
||||||
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||||
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParseGuid(const char* text, uint8_t out[16]) {
|
||||||
|
// Bytes in text order, then the first three fields byte-swapped.
|
||||||
|
uint8_t bytes[16];
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; text[i]; i++) {
|
||||||
|
if (text[i] == '-') continue;
|
||||||
|
int hi = HexDigit(text[i]);
|
||||||
|
int lo = text[i + 1] ? HexDigit(text[i + 1]) : -1;
|
||||||
|
if (hi < 0 || lo < 0 || count == 16) return false;
|
||||||
|
bytes[count++] = (uint8_t)(hi << 4 | lo);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (count != 16) return false;
|
||||||
|
static constexpr int order[16] = {3, 2, 1, 0, 5, 4, 7, 6,
|
||||||
|
8, 9, 10, 11, 12, 13, 14, 15};
|
||||||
|
for (int i = 0; i < 16; i++) out[i] = bytes[order[i]];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool SearchDevice(int32_t device, const uint8_t guid[16], Block& out) {
|
||||||
|
auto& interp = AML::GetInterpreter();
|
||||||
|
AML::Object wdg{};
|
||||||
|
if (!interp.EvaluateChild(device, "_WDG", nullptr, 0, wdg)
|
||||||
|
|| wdg.Type != AML::ObjectType::Buffer)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// The interpreter caps buffers; a _WDG that fills the cap may have
|
||||||
|
// lost entries past it.
|
||||||
|
if (wdg.Buffer.Length >= (uint32_t)AML::MaxBufferLen) {
|
||||||
|
char path[128];
|
||||||
|
interp.GetNamespace().GetNodePath(device, path, sizeof(path));
|
||||||
|
KernelLogStream(WARNING, "WMI") << path << "._WDG may be truncated ("
|
||||||
|
<< base::dec << (uint64_t)wdg.Buffer.Length << " bytes read)";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t off = 0; off + WdgEntrySize <= wdg.Buffer.Length; off += WdgEntrySize) {
|
||||||
|
const uint8_t* entry = wdg.Buffer.Data + off;
|
||||||
|
if (memcmp(entry, guid, 16) != 0) continue;
|
||||||
|
out.Device = device;
|
||||||
|
out.ObjectId[0] = (char)entry[16];
|
||||||
|
out.ObjectId[1] = (char)entry[17];
|
||||||
|
out.InstanceCount = entry[18];
|
||||||
|
out.Flags = entry[19];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Find(const uint8_t guid[16], Block& out) {
|
||||||
|
auto& interp = AML::GetInterpreter();
|
||||||
|
if (!interp.IsInitialized()) return false;
|
||||||
|
auto& ns = interp.GetNamespace();
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||||
|
[&](int32_t idx, const AML::NamespaceNode*) {
|
||||||
|
if (found) return;
|
||||||
|
char hid[16];
|
||||||
|
if (!AcpiDevices::EvaluateHid(idx, hid, sizeof(hid))) return;
|
||||||
|
if (memcmp(hid, HidWmi, sizeof("PNP0C14")) != 0) return;
|
||||||
|
if (!(AcpiDevices::EvaluateSta(idx) & AcpiDevices::STA_PRESENT)) return;
|
||||||
|
found = SearchDevice(idx, guid, out);
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId,
|
||||||
|
const uint8_t* input, uint32_t inputLength,
|
||||||
|
AML::Object& result) {
|
||||||
|
if (!(block.Flags & FlagMethod) || instance >= block.InstanceCount)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
AML::Object args[3];
|
||||||
|
args[0].Type = AML::ObjectType::Integer;
|
||||||
|
args[0].Integer = instance;
|
||||||
|
args[1].Type = AML::ObjectType::Integer;
|
||||||
|
args[1].Integer = methodId;
|
||||||
|
if (block.Flags & FlagString) {
|
||||||
|
uint32_t n = inputLength;
|
||||||
|
if (n >= (uint32_t)AML::MaxStringLen) n = AML::MaxStringLen - 1;
|
||||||
|
args[2].Type = AML::ObjectType::String;
|
||||||
|
if (n) memcpy(args[2].String.Data, input, n);
|
||||||
|
args[2].String.Data[n] = '\0';
|
||||||
|
args[2].String.Length = (uint16_t)n;
|
||||||
|
} else {
|
||||||
|
uint32_t n = inputLength;
|
||||||
|
if (n > (uint32_t)AML::MaxBufferLen) n = AML::MaxBufferLen;
|
||||||
|
args[2].Type = AML::ObjectType::Buffer;
|
||||||
|
if (n) memcpy(args[2].Buffer.Data, input, n);
|
||||||
|
args[2].Buffer.Length = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
char method[5] = {'W', 'M', block.ObjectId[0], block.ObjectId[1], '\0'};
|
||||||
|
return AML::GetInterpreter().EvaluateChild(block.Device, method, args, 3, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* AcpiWmi.hpp
|
||||||
|
* ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call
|
||||||
|
* its WMxx method
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ACPI/AML/AmlNamespace.hpp>
|
||||||
|
|
||||||
|
namespace Hal {
|
||||||
|
namespace AcpiWmi {
|
||||||
|
|
||||||
|
// _WDG flags (Microsoft "Windows Instrumentation: WMI and ACPI").
|
||||||
|
static constexpr uint8_t FlagExpensive = 0x1;
|
||||||
|
static constexpr uint8_t FlagMethod = 0x2;
|
||||||
|
static constexpr uint8_t FlagString = 0x4;
|
||||||
|
static constexpr uint8_t FlagEvent = 0x8;
|
||||||
|
|
||||||
|
// One _WDG entry, bound to the PNP0C14 device that declared it.
|
||||||
|
struct Block {
|
||||||
|
int32_t Device; // namespace node of the PNP0C14 device
|
||||||
|
char ObjectId[2]; // names the WQxx / WSxx / WMxx methods
|
||||||
|
uint8_t InstanceCount;
|
||||||
|
uint8_t Flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse "603E9613-EF25-4338-A3D0-C46177516DB7" into the byte order
|
||||||
|
// _WDG stores (first three fields little-endian). False if malformed.
|
||||||
|
bool ParseGuid(const char* text, uint8_t out[16]);
|
||||||
|
|
||||||
|
// Search every present PNP0C14 device's _WDG for `guid`. The first
|
||||||
|
// declaration wins, as on Linux.
|
||||||
|
bool Find(const uint8_t guid[16], Block& out);
|
||||||
|
|
||||||
|
// Evaluate WMxx(instance, methodId, input) on a method block. The input
|
||||||
|
// is passed as a String instead of a Buffer when the block says so.
|
||||||
|
bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId,
|
||||||
|
const uint8_t* input, uint32_t inputLength,
|
||||||
|
AML::Object& result);
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* WmiEcBacklight.cpp
|
||||||
|
* Firmware (EC) panel backlight behind the WMI interface NVIDIA specified
|
||||||
|
* for display-mux laptops
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "WmiEcBacklight.hpp"
|
||||||
|
#include "AcpiWmi.hpp"
|
||||||
|
#include "AcpiPower.hpp"
|
||||||
|
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Hal {
|
||||||
|
namespace WmiEcBacklight {
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Interface
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
static constexpr const char* Guid = "603E9613-EF25-4338-A3D0-C46177516DB7";
|
||||||
|
|
||||||
|
// WMxx method IDs.
|
||||||
|
static constexpr uint32_t MethodLevel = 1;
|
||||||
|
static constexpr uint32_t MethodSource = 2;
|
||||||
|
|
||||||
|
// `mode` values in the argument block.
|
||||||
|
static constexpr uint32_t ModeGet = 0;
|
||||||
|
static constexpr uint32_t ModeSet = 1;
|
||||||
|
static constexpr uint32_t ModeGetMaxLevel = 2;
|
||||||
|
|
||||||
|
static constexpr uint64_t SourceEc = 2; // 1 = GPU, 3 = eDP AUX
|
||||||
|
|
||||||
|
// Lowest level driven, so the panel never goes fully dark.
|
||||||
|
static constexpr int MinPercent = 1;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// State
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
static kcp::Mutex g_lock;
|
||||||
|
static bool g_probed = false;
|
||||||
|
static bool g_available = false;
|
||||||
|
static AcpiWmi::Block g_block{};
|
||||||
|
static uint32_t g_maxLevel = 0;
|
||||||
|
|
||||||
|
// Argument block: { u32 mode; u32 val; u32 ret; u32 reserved[3]; }.
|
||||||
|
// The method answers with an Integer. Linux reads it back as the
|
||||||
|
// `ret` field of the same block, which is where ACPICA's copy-out of
|
||||||
|
// an Integer puts the value; a firmware returning the block itself as
|
||||||
|
// a Buffer is read at that same offset.
|
||||||
|
static bool Call(uint32_t method, uint32_t mode, uint32_t value, uint32_t& out) {
|
||||||
|
uint8_t args[24] = {};
|
||||||
|
args[0] = (uint8_t)mode;
|
||||||
|
args[1] = (uint8_t)(mode >> 8);
|
||||||
|
args[2] = (uint8_t)(mode >> 16);
|
||||||
|
args[3] = (uint8_t)(mode >> 24);
|
||||||
|
args[4] = (uint8_t)value;
|
||||||
|
args[5] = (uint8_t)(value >> 8);
|
||||||
|
args[6] = (uint8_t)(value >> 16);
|
||||||
|
args[7] = (uint8_t)(value >> 24);
|
||||||
|
|
||||||
|
AML::Object result{};
|
||||||
|
if (!AcpiWmi::EvaluateMethod(g_block, 0, method, args, sizeof(args), result))
|
||||||
|
return false;
|
||||||
|
if (result.Type == AML::ObjectType::Integer) {
|
||||||
|
out = (uint32_t)result.Integer;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (result.Type == AML::ObjectType::Buffer && result.Buffer.Length >= 12) {
|
||||||
|
const uint8_t* r = result.Buffer.Data + 8;
|
||||||
|
out = r[0] | (uint32_t)r[1] << 8 | (uint32_t)r[2] << 16 | (uint32_t)r[3] << 24;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// A set may legitimately return nothing.
|
||||||
|
out = 0;
|
||||||
|
return mode == ModeSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Probe() {
|
||||||
|
g_probed = true;
|
||||||
|
uint8_t guid[16];
|
||||||
|
if (!AcpiWmi::ParseGuid(Guid, guid) || !AcpiWmi::Find(guid, g_block)) return;
|
||||||
|
if (!(g_block.Flags & AcpiWmi::FlagMethod) || g_block.InstanceCount == 0) return;
|
||||||
|
|
||||||
|
char path[128];
|
||||||
|
AML::GetInterpreter().GetNamespace().GetNodePath(g_block.Device, path, sizeof(path));
|
||||||
|
|
||||||
|
// The interface also exists on machines where the GPU drives the
|
||||||
|
// backlight; there it must be left alone.
|
||||||
|
uint32_t source = 0;
|
||||||
|
if (!Call(MethodSource, ModeGet, 0, source)) {
|
||||||
|
KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path
|
||||||
|
<< ": source query failed";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (source != SourceEc) {
|
||||||
|
KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path
|
||||||
|
<< " reports source " << base::dec << (uint64_t)source << ", not EC";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t maximum = 0;
|
||||||
|
if (!Call(MethodLevel, ModeGetMaxLevel, 0, maximum) || maximum == 0) {
|
||||||
|
KernelLogStream(WARNING, "Backlight") << "WMI EC backlight at " << path
|
||||||
|
<< ": no maximum level";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_maxLevel = maximum;
|
||||||
|
g_available = true;
|
||||||
|
char method[5] = {'W', 'M', g_block.ObjectId[0], g_block.ObjectId[1], '\0'};
|
||||||
|
KernelLogStream(OK, "Backlight") << "Panel brightness via EC (WMI "
|
||||||
|
<< path << "." << method << ", " << base::dec << (uint64_t)maximum
|
||||||
|
<< " levels)";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ToPercent(uint32_t level) {
|
||||||
|
if (level > g_maxLevel) level = g_maxLevel;
|
||||||
|
return (int)(((uint64_t)level * 100 + g_maxLevel / 2) / g_maxLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
int Brightness(int percent) {
|
||||||
|
g_lock.Acquire();
|
||||||
|
if (!g_probed) Probe();
|
||||||
|
int result = -1;
|
||||||
|
if (g_available) {
|
||||||
|
uint32_t level = 0;
|
||||||
|
if (percent >= 0) {
|
||||||
|
if (percent > 100) percent = 100;
|
||||||
|
if (percent < MinPercent) percent = MinPercent;
|
||||||
|
uint32_t target = (uint32_t)(((uint64_t)g_maxLevel * (uint32_t)percent + 50) / 100);
|
||||||
|
if (target == 0) target = 1;
|
||||||
|
uint32_t ignored = 0;
|
||||||
|
if (!Call(MethodLevel, ModeSet, target, ignored)) {
|
||||||
|
result = -2;
|
||||||
|
} else {
|
||||||
|
result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level)
|
||||||
|
: ToPercent(target);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level) : -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_lock.Release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* WmiEcBacklight.hpp
|
||||||
|
* Firmware (EC) panel backlight behind the WMI interface NVIDIA specified
|
||||||
|
* for display-mux laptops
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace Hal {
|
||||||
|
namespace WmiEcBacklight {
|
||||||
|
|
||||||
|
// On a laptop whose panel can be switched between GPUs, firmware may
|
||||||
|
// wire the backlight to the embedded controller instead of either
|
||||||
|
// GPU and expose it through WMI GUID 603E9613-EF25-4338-A3D0-
|
||||||
|
// C46177516DB7. Despite the name the interface is pure firmware: it
|
||||||
|
// works with the discrete GPU powered off. It is used only when the
|
||||||
|
// firmware itself reports the EC as the brightness source, which is
|
||||||
|
// also when GPU PWM writes stop reaching the panel.
|
||||||
|
//
|
||||||
|
// percent < 0 queries. Returns 0..100, -1 when this interface is not
|
||||||
|
// the panel's brightness source, -2 when the firmware call failed.
|
||||||
|
// Discovery runs on first use. May sleep (AML).
|
||||||
|
int Brightness(int percent);
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 287
|
#define MONTAUK_BUILD_NUMBER 1
|
||||||
+21
-11
@@ -12,6 +12,7 @@
|
|||||||
#include <Graphics/Framebuffer.hpp>
|
#include <Graphics/Framebuffer.hpp>
|
||||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||||
#include <ACPI/AcpiVideo.hpp>
|
#include <ACPI/AcpiVideo.hpp>
|
||||||
|
#include <ACPI/WmiEcBacklight.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
|
|
||||||
#include "Syscall.hpp"
|
#include "Syscall.hpp"
|
||||||
@@ -129,29 +130,38 @@ namespace montauk::abi {
|
|||||||
return GPU::Flip((int)index, (flags & 1) != 0, proc->pid);
|
return GPU::Flip((int)index, (flags & 1) != 0, proc->pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The firmware's ACPI brightness methods drive the built-in panel, so
|
// Firmware brightness interfaces (EC via WMI, ACPI _BCM) drive the
|
||||||
// they stand in only when that panel is what is on screen (or no native
|
// built-in panel, so they apply only while that panel is what is on
|
||||||
// driver knows what is): never for an external monitor without DDC/CI.
|
// screen (or no native driver knows what is): never for an external
|
||||||
static bool AcpiBrightnessApplies() {
|
// monitor, where they would silently dim a closed lid instead.
|
||||||
|
static bool PanelIsActiveOutput() {
|
||||||
if (!Drivers::Graphics::IntelGPU::IsInitialized()) return true;
|
if (!Drivers::Graphics::IntelGPU::IsInitialized()) return true;
|
||||||
uint8_t type = Drivers::Graphics::IntelGPU::ActiveConnectorType();
|
uint8_t type = Drivers::Graphics::IntelGPU::ActiveConnectorType();
|
||||||
return type == DISPLAY_CONNECTOR_EDP || type == DISPLAY_CONNECTOR_LVDS;
|
return type == DISPLAY_CONNECTOR_EDP || type == DISPLAY_CONNECTOR_LVDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order follows who actually owns the backlight. A firmware that names
|
||||||
|
// the EC as the source has taken it away from the GPU, whose PWM then
|
||||||
|
// still accepts writes that reach nothing; so the EC interface goes
|
||||||
|
// first, then the GPU's own controls, then ACPI _BCM as a last resort.
|
||||||
static int DisplayBrightness(int percent) {
|
static int DisplayBrightness(int percent) {
|
||||||
|
bool panel = PanelIsActiveOutput();
|
||||||
|
if (panel) {
|
||||||
|
int rc = Hal::WmiEcBacklight::Brightness(percent);
|
||||||
|
if (rc != -1) return rc;
|
||||||
|
}
|
||||||
int rc = Drivers::Graphics::IntelGPU::Brightness(percent);
|
int rc = Drivers::Graphics::IntelGPU::Brightness(percent);
|
||||||
if (rc == -1 && AcpiBrightnessApplies())
|
if (rc == -1 && panel) rc = Hal::AcpiVideo::Brightness(percent);
|
||||||
rc = Hal::AcpiVideo::Brightness(percent);
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int64_t Sys_DisplayInfo(DisplayInfo* out) {
|
static int64_t Sys_DisplayInfo(DisplayInfo* out) {
|
||||||
if (!Drivers::Graphics::IntelGPU::GetDisplayInfo(out)) return NoDevice;
|
if (!Drivers::Graphics::IntelGPU::GetDisplayInfo(out)) return NoDevice;
|
||||||
if (out->brightness < 0 && AcpiBrightnessApplies()) {
|
// The driver only knows its own backends; report what a set would use.
|
||||||
out->brightness = Hal::AcpiVideo::Brightness(-1);
|
int level = DisplayBrightness(-1);
|
||||||
if (out->brightness >= 0) out->capabilities |= DISPLAY_CAP_BRIGHTNESS;
|
out->brightness = level >= 0 ? level : -1;
|
||||||
else out->brightness = -1;
|
if (level >= 0) out->capabilities |= DISPLAY_CAP_BRIGHTNESS;
|
||||||
}
|
else out->capabilities &= ~DISPLAY_CAP_BRIGHTNESS;
|
||||||
return Success;
|
return Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ namespace montauk::abi {
|
|||||||
|
|
||||||
// Copy strings into fixed-size arrays (user-accessible)
|
// Copy strings into fixed-size arrays (user-accessible)
|
||||||
const char* name = "MontaukOS";
|
const char* name = "MontaukOS";
|
||||||
const char* ver = "0.2.0";
|
const char* ver = "0.2.1";
|
||||||
for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i];
|
for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i];
|
||||||
outInfo->osName[9] = '\0';
|
outInfo->osName[9] = '\0';
|
||||||
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
|
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
|
||||||
outInfo->osVersion[5] = '\0';
|
outInfo->osVersion[5] = '\0';
|
||||||
|
|
||||||
outInfo->apiVersion = 11;
|
outInfo->apiVersion = 12;
|
||||||
outInfo->maxProcesses = Sched::MaxProcesses;
|
outInfo->maxProcesses = Sched::MaxProcesses;
|
||||||
outInfo->buildNumber = MONTAUK_BUILD_NUMBER;
|
outInfo->buildNumber = MONTAUK_BUILD_NUMBER;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,7 +220,18 @@ namespace montauk::abi {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flag every thread of the caller's process with TerminatedAborted, then
|
||||||
|
// exit this one. A sibling's ExitProcess ends only itself; the main
|
||||||
|
// thread and the other siblings follow via killPending.
|
||||||
|
static void Sys_Abort() {
|
||||||
|
Sched::KillProcess(Sched::GetCurrentPid(), TerminatedAborted);
|
||||||
|
Sched::ExitProcess();
|
||||||
|
}
|
||||||
|
|
||||||
static int Sys_Kill(int pid) {
|
static int Sys_Kill(int pid) {
|
||||||
|
// A process may always kill itself (kill(getpid(), ...)); waiters
|
||||||
|
// see TerminatedKilled. abort() uses SYS_ABORT instead.
|
||||||
|
if (pid == Sched::GetCurrentPid()) return Sched::KillProcess(pid);
|
||||||
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) {
|
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) {
|
||||||
int ancestor = pid;
|
int ancestor = pid;
|
||||||
bool descendant = false;
|
bool descendant = false;
|
||||||
@@ -302,6 +313,14 @@ namespace montauk::abi {
|
|||||||
return Sched::GetCurrentTid();
|
return Sched::GetCurrentTid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int Sys_FutexWait(uint64_t addr, uint32_t expected, uint64_t timeoutNs) {
|
||||||
|
return Sched::FutexWait(addr, expected, timeoutNs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Sys_FutexWake(uint64_t addr, uint32_t count) {
|
||||||
|
return Sched::FutexWake(addr, count);
|
||||||
|
}
|
||||||
|
|
||||||
static int Sys_Chdir(const char* path) {
|
static int Sys_Chdir(const char* path) {
|
||||||
auto* proc = Sched::GetCurrentProcessPtr();
|
auto* proc = Sched::GetCurrentProcessPtr();
|
||||||
if (proc == nullptr || path == nullptr) return InvalidArgument;
|
if (proc == nullptr || path == nullptr) return InvalidArgument;
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ namespace montauk::abi {
|
|||||||
case SYS_ISKEYAVAILABLE:
|
case SYS_ISKEYAVAILABLE:
|
||||||
return (int64_t)Sys_IsKeyAvailable();
|
return (int64_t)Sys_IsKeyAvailable();
|
||||||
case SYS_GETKEY:
|
case SYS_GETKEY:
|
||||||
if (!UserMemory::Writable<KeyEvent>(frame->arg1)) return BadPointer;
|
if (!UserMemory::Writable<KeyEventV1>(frame->arg1)) return BadPointer;
|
||||||
Sys_GetKey((KeyEvent*)frame->arg1);
|
Sys_GetKey((KeyEventV1*)frame->arg1);
|
||||||
return Success;
|
return Success;
|
||||||
case SYS_GETCHAR:
|
case SYS_GETCHAR:
|
||||||
return (int64_t)Sys_GetChar();
|
return (int64_t)Sys_GetChar();
|
||||||
@@ -365,8 +365,8 @@ namespace montauk::abi {
|
|||||||
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
|
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
|
||||||
return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3);
|
return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3);
|
||||||
case SYS_CHILDIO_WRITEKEY:
|
case SYS_CHILDIO_WRITEKEY:
|
||||||
if (!UserMemory::Readable<KeyEvent>(frame->arg2)) return BadPointer;
|
if (!UserMemory::Readable<KeyEventV1>(frame->arg2)) return BadPointer;
|
||||||
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEvent*)frame->arg2);
|
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEventV1*)frame->arg2);
|
||||||
case SYS_CHILDIO_SETTERMSZ:
|
case SYS_CHILDIO_SETTERMSZ:
|
||||||
return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
|
return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
|
||||||
case SYS_WINCREATE:
|
case SYS_WINCREATE:
|
||||||
@@ -400,6 +400,9 @@ namespace montauk::abi {
|
|||||||
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
|
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
|
||||||
case SYS_KILL:
|
case SYS_KILL:
|
||||||
return (int64_t)Sys_Kill((int)frame->arg1);
|
return (int64_t)Sys_Kill((int)frame->arg1);
|
||||||
|
case SYS_ABORT:
|
||||||
|
Sys_Abort();
|
||||||
|
return Success;
|
||||||
case SYS_SETSESSION:
|
case SYS_SETSESSION:
|
||||||
return (int64_t)Sys_SetSession();
|
return (int64_t)Sys_SetSession();
|
||||||
case SYS_KILLSESSION:
|
case SYS_KILLSESSION:
|
||||||
@@ -527,6 +530,14 @@ namespace montauk::abi {
|
|||||||
return Sched::Consent::Next((ConsentRequest*)frame->arg1);
|
return Sched::Consent::Next((ConsentRequest*)frame->arg1);
|
||||||
case SYS_CONSENT_REPLY:
|
case SYS_CONSENT_REPLY:
|
||||||
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
|
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
|
||||||
|
case SYS_GETKEY_EX:
|
||||||
|
if (frame->arg2 < sizeof(KeyEventV1) || frame->arg2 > 4096) return InvalidArgument;
|
||||||
|
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
|
||||||
|
return Sys_GetKeyEx((void*)frame->arg1, frame->arg2);
|
||||||
|
case SYS_CHILDIO_WRITEKEY_EX:
|
||||||
|
if (frame->arg3 < sizeof(KeyEventV1) || frame->arg3 > 4096) return InvalidArgument;
|
||||||
|
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
|
||||||
|
return (int64_t)Sys_ChildIoWriteKeyEx((int)frame->arg1, (const void*)frame->arg2, frame->arg3);
|
||||||
case SYS_THREAD_SPAWN:
|
case SYS_THREAD_SPAWN:
|
||||||
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
|
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
|
||||||
case SYS_THREAD_EXIT:
|
case SYS_THREAD_EXIT:
|
||||||
@@ -537,6 +548,13 @@ namespace montauk::abi {
|
|||||||
return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2);
|
return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2);
|
||||||
case SYS_THREAD_SELF:
|
case SYS_THREAD_SELF:
|
||||||
return Sys_ThreadSelf();
|
return Sys_ThreadSelf();
|
||||||
|
case SYS_FUTEX_WAIT:
|
||||||
|
// Validation demand-commits the page so the scheduler can
|
||||||
|
// read the word with its lock held without faulting.
|
||||||
|
if (!UserMemory::Readable<uint32_t>(frame->arg1)) return BadPointer;
|
||||||
|
return Sys_FutexWait(frame->arg1, (uint32_t)frame->arg2, frame->arg3);
|
||||||
|
case SYS_FUTEX_WAKE:
|
||||||
|
return Sys_FutexWake(frame->arg1, (uint32_t)frame->arg2);
|
||||||
case SYS_BTSCAN:
|
case SYS_BTSCAN:
|
||||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
|
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
|
||||||
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
|
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
|
||||||
|
|||||||
@@ -358,6 +358,28 @@ namespace montauk::abi {
|
|||||||
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||||
|
|
||||||
|
/* Unicode key events. Size-versioned: the caller passes sizeof(its
|
||||||
|
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
|
||||||
|
rest, so the struct can grow without another syscall. */
|
||||||
|
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
|
||||||
|
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
|
||||||
|
|
||||||
|
/* Futexes: process-private wait queues keyed by a user address. WAIT
|
||||||
|
blocks only if *addr still equals `expected` (checked atomically with
|
||||||
|
enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check
|
||||||
|
only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken,
|
||||||
|
WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the
|
||||||
|
number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */
|
||||||
|
static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status
|
||||||
|
static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status
|
||||||
|
static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL;
|
||||||
|
static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu;
|
||||||
|
|
||||||
|
/* Abnormal termination from any thread: ends the whole process (every
|
||||||
|
thread) with exit code TerminatedAborted (256+SIGABRT). SYS_EXIT from a
|
||||||
|
sibling thread ends only that thread, so abort() needs its own call. */
|
||||||
|
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
|
||||||
|
|
||||||
/* 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 +622,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 +641,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 +704,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;
|
||||||
|
|||||||
@@ -23,5 +23,7 @@ enum StatusCodes {
|
|||||||
NoSuchPath = -11,
|
NoSuchPath = -11,
|
||||||
Busy = -12,
|
Busy = -12,
|
||||||
BufferTooSmall = -13,
|
BufferTooSmall = -13,
|
||||||
OutOfMemory = -14
|
OutOfMemory = -14,
|
||||||
|
TimedOut = -15,
|
||||||
|
WouldBlock = -16
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* TerminationCodes.hpp
|
||||||
|
* MontaukOS process termination codes.
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Exit code recorded when the kernel ends a process, as opposed to the
|
||||||
|
// process calling SYS_EXIT (which is masked to 0..255). Encoded as
|
||||||
|
// 256 + POSIX signal number so libc waitpid can report WTERMSIG.
|
||||||
|
// Mirrored in userspace at programs/include/montauk/statuscodes.h.
|
||||||
|
enum TerminationCodes {
|
||||||
|
TerminationBase = 256,
|
||||||
|
TerminatedIllegalInstruction = TerminationBase + 4, // SIGILL
|
||||||
|
TerminatedBreakpoint = TerminationBase + 5, // SIGTRAP
|
||||||
|
TerminatedAborted = TerminationBase + 6, // SIGABRT
|
||||||
|
TerminatedArithmeticError = TerminationBase + 8, // SIGFPE
|
||||||
|
TerminatedKilled = TerminationBase + 9, // SIGKILL
|
||||||
|
TerminatedAccessViolation = TerminationBase + 11 // SIGSEGV
|
||||||
|
};
|
||||||
|
|
||||||
|
// Termination code for a user-mode CPU exception. The exact vector is
|
||||||
|
// kept in the crash report; this is only the category.
|
||||||
|
inline TerminationCodes TerminationFromVector(uint8_t vector) {
|
||||||
|
switch (vector) {
|
||||||
|
case 0x06: return TerminatedIllegalInstruction; // #UD
|
||||||
|
case 0x01: // #DB
|
||||||
|
case 0x03: return TerminatedBreakpoint; // #BP
|
||||||
|
case 0x00: // #DE
|
||||||
|
case 0x10: // #MF
|
||||||
|
case 0x13: return TerminatedArithmeticError; // #XM
|
||||||
|
default: return TerminatedAccessViolation; // #PF, #GP, #SS, #NP, #AC, #OF, #BR, #CP, ...
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
g_RepeatKey = key;
|
|
||||||
g_RepeatStartMs = now;
|
|
||||||
g_LastRepeatMs = 0;
|
|
||||||
newKeyPressed = true;
|
newKeyPressed = true;
|
||||||
|
// Lock keys toggle once per press; repeating them while
|
||||||
|
// held would flip Caps/Num/Scroll Lock on and off.
|
||||||
|
if (key != 0x39 && key != 0x53 && key != 0x47) {
|
||||||
|
g_RepeatKey = key;
|
||||||
|
g_RepeatStartMs = now;
|
||||||
|
g_LastRepeatMs = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
/*
|
||||||
|
* BlockCache.cpp
|
||||||
|
* Per-mount LRU cache of filesystem blocks
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "BlockCache.hpp"
|
||||||
|
#include <Drivers/Storage/BlockDevice.hpp>
|
||||||
|
#include <Terminal/Terminal.hpp>
|
||||||
|
#include <Libraries/Memory.hpp>
|
||||||
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
|
#include <Memory/Heap.hpp>
|
||||||
|
|
||||||
|
using namespace Kt;
|
||||||
|
|
||||||
|
namespace Fs::BlockCache {
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Sizing
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Capacity is free memory / FreeMemDivisor at creation, clamped to
|
||||||
|
// [MinEntries, MaxEntries]. 32768 4 KiB blocks = 128 MiB, enough to keep
|
||||||
|
// the whole native toolchain (cc1plus, as, ld, headers, libstdc++) hot.
|
||||||
|
static constexpr uint32_t MinEntries = 256;
|
||||||
|
static constexpr uint32_t MaxEntries = 32768;
|
||||||
|
static constexpr uint64_t FreeMemDivisor = 8;
|
||||||
|
|
||||||
|
// Every storage driver accepts at least 128 sectors per command (AHCI and
|
||||||
|
// the NVMe fallback both cap there), so prefetch runs stay within it.
|
||||||
|
static constexpr uint32_t MaxRunSectors = 128;
|
||||||
|
|
||||||
|
static constexpr uint32_t Nil = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Types
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// Entries live in one array and link by index. An entry is in exactly one
|
||||||
|
// of three states: never used (index >= nextFresh, no page), on the free
|
||||||
|
// list (has a page, not hashed), or live (hashed and on the LRU list).
|
||||||
|
struct Entry {
|
||||||
|
uint32_t block;
|
||||||
|
uint32_t hashNext; // bucket chain, or free-list link
|
||||||
|
uint32_t lruPrev;
|
||||||
|
uint32_t lruNext;
|
||||||
|
uint8_t* data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cache {
|
||||||
|
int blockDevIndex;
|
||||||
|
uint64_t partStartLba;
|
||||||
|
uint32_t blockSize;
|
||||||
|
uint32_t sectorsPerBlock;
|
||||||
|
|
||||||
|
Entry* entries;
|
||||||
|
uint32_t* buckets;
|
||||||
|
uint32_t capacity;
|
||||||
|
uint32_t hashShift;
|
||||||
|
int tablePages;
|
||||||
|
|
||||||
|
uint32_t nextFresh;
|
||||||
|
uint32_t freeHead;
|
||||||
|
uint32_t lruHead; // most recently used
|
||||||
|
uint32_t lruTail; // eviction candidate
|
||||||
|
|
||||||
|
// Staging buffer for multi-block prefetch reads (MaxRunSectors long).
|
||||||
|
uint8_t* staging;
|
||||||
|
int stagingPages;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Device I/O
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static bool DevRead(Cache* c, uint32_t block, uint32_t count, void* buf) {
|
||||||
|
auto* dev = Drivers::Storage::GetBlockDevice(c->blockDevIndex);
|
||||||
|
if (!dev) return false;
|
||||||
|
return dev->ReadSectors(dev->Ctx,
|
||||||
|
c->partStartLba + (uint64_t)block * c->sectorsPerBlock,
|
||||||
|
count * c->sectorsPerBlock, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DevWrite(Cache* c, uint32_t block, const void* buf) {
|
||||||
|
auto* dev = Drivers::Storage::GetBlockDevice(c->blockDevIndex);
|
||||||
|
if (!dev) return false;
|
||||||
|
return dev->WriteSectors(dev->Ctx,
|
||||||
|
c->partStartLba + (uint64_t)block * c->sectorsPerBlock,
|
||||||
|
c->sectorsPerBlock, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Hash and LRU bookkeeping
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
static uint32_t Bucket(const Cache* c, uint32_t block) {
|
||||||
|
return (block * 0x9E3779B1u) >> c->hashShift;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t Lookup(Cache* c, uint32_t block) {
|
||||||
|
uint32_t i = c->buckets[Bucket(c, block)];
|
||||||
|
while (i != Nil) {
|
||||||
|
if (c->entries[i].block == block) return i;
|
||||||
|
i = c->entries[i].hashNext;
|
||||||
|
}
|
||||||
|
return Nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void LruUnlink(Cache* c, uint32_t i) {
|
||||||
|
Entry& e = c->entries[i];
|
||||||
|
if (e.lruPrev != Nil) c->entries[e.lruPrev].lruNext = e.lruNext;
|
||||||
|
else c->lruHead = e.lruNext;
|
||||||
|
if (e.lruNext != Nil) c->entries[e.lruNext].lruPrev = e.lruPrev;
|
||||||
|
else c->lruTail = e.lruPrev;
|
||||||
|
e.lruPrev = e.lruNext = Nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void LruPushFront(Cache* c, uint32_t i) {
|
||||||
|
Entry& e = c->entries[i];
|
||||||
|
e.lruPrev = Nil;
|
||||||
|
e.lruNext = c->lruHead;
|
||||||
|
if (c->lruHead != Nil) c->entries[c->lruHead].lruPrev = i;
|
||||||
|
c->lruHead = i;
|
||||||
|
if (c->lruTail == Nil) c->lruTail = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Touch(Cache* c, uint32_t i) {
|
||||||
|
if (c->lruHead == i) return;
|
||||||
|
LruUnlink(c, i);
|
||||||
|
LruPushFront(c, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void HashInsert(Cache* c, uint32_t i) {
|
||||||
|
uint32_t b = Bucket(c, c->entries[i].block);
|
||||||
|
c->entries[i].hashNext = c->buckets[b];
|
||||||
|
c->buckets[b] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void HashRemove(Cache* c, uint32_t i) {
|
||||||
|
uint32_t* link = &c->buckets[Bucket(c, c->entries[i].block)];
|
||||||
|
while (*link != Nil) {
|
||||||
|
if (*link == i) {
|
||||||
|
*link = c->entries[i].hashNext;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
link = &c->entries[*link].hashNext;
|
||||||
|
}
|
||||||
|
c->entries[i].hashNext = Nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take a live entry out of the cache and park it (and its page) on the
|
||||||
|
// free list.
|
||||||
|
static void Drop(Cache* c, uint32_t i) {
|
||||||
|
HashRemove(c, i);
|
||||||
|
LruUnlink(c, i);
|
||||||
|
c->entries[i].block = 0;
|
||||||
|
c->entries[i].hashNext = c->freeHead;
|
||||||
|
c->freeHead = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an entry with a page, not hashed and not on the LRU. Prefers the
|
||||||
|
// free list, then a fresh entry, then evicts the least recently used.
|
||||||
|
// Returns Nil only if the cache is empty and no page can be allocated.
|
||||||
|
static uint32_t Acquire(Cache* c) {
|
||||||
|
if (c->freeHead != Nil) {
|
||||||
|
uint32_t i = c->freeHead;
|
||||||
|
c->freeHead = c->entries[i].hashNext;
|
||||||
|
c->entries[i].hashNext = Nil;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c->nextFresh < c->capacity) {
|
||||||
|
void* page = Memory::g_pfa->Allocate();
|
||||||
|
if (page != nullptr) {
|
||||||
|
uint32_t i = c->nextFresh++;
|
||||||
|
Entry& e = c->entries[i];
|
||||||
|
e.block = 0;
|
||||||
|
e.hashNext = e.lruPrev = e.lruNext = Nil;
|
||||||
|
e.data = (uint8_t*)page;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
// Out of memory: fall through and recycle what we already hold.
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t victim = c->lruTail;
|
||||||
|
if (victim == Nil) return Nil;
|
||||||
|
HashRemove(c, victim);
|
||||||
|
LruUnlink(c, victim);
|
||||||
|
return victim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish a freshly filled entry under `block`.
|
||||||
|
static void Insert(Cache* c, uint32_t i, uint32_t block) {
|
||||||
|
c->entries[i].block = block;
|
||||||
|
HashInsert(c, i);
|
||||||
|
LruPushFront(c, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Public API
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
Cache* Create(int blockDevIndex, uint64_t partStartLba, uint32_t blockSize) {
|
||||||
|
if (blockSize < 512 || blockSize > 0x1000 || (blockSize % 512) != 0) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
montauk::abi::MemStats stats{};
|
||||||
|
Memory::g_pfa->GetStats(&stats);
|
||||||
|
uint64_t want = stats.freeBytes / FreeMemDivisor / blockSize;
|
||||||
|
if (want > MaxEntries) want = MaxEntries;
|
||||||
|
if (want < MinEntries) want = MinEntries;
|
||||||
|
|
||||||
|
// Round down to a power of two so the bucket count can equal capacity.
|
||||||
|
uint32_t capacity = MinEntries;
|
||||||
|
while ((uint64_t)capacity * 2 <= want) capacity *= 2;
|
||||||
|
|
||||||
|
auto* c = (Cache*)Memory::g_heap->Request(sizeof(Cache));
|
||||||
|
if (c == nullptr) return nullptr;
|
||||||
|
memset(c, 0, sizeof(Cache));
|
||||||
|
|
||||||
|
// Entries and buckets share one allocation. Physically consecutive
|
||||||
|
// runs can be scarce on a fragmented PFA, so halve until it fits.
|
||||||
|
while (capacity >= MinEntries) {
|
||||||
|
uint64_t bytes = (uint64_t)capacity * (sizeof(Entry) + sizeof(uint32_t));
|
||||||
|
int pages = (int)((bytes + 0xFFF) / 0x1000);
|
||||||
|
void* table = Memory::g_pfa->ReallocConsecutive(nullptr, pages);
|
||||||
|
if (table != nullptr) {
|
||||||
|
c->entries = (Entry*)table;
|
||||||
|
c->buckets = (uint32_t*)((uint8_t*)table + (uint64_t)capacity * sizeof(Entry));
|
||||||
|
c->tablePages = pages;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
capacity /= 2;
|
||||||
|
}
|
||||||
|
if (c->entries == nullptr) {
|
||||||
|
Memory::g_heap->Free(c);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t log2 = 0;
|
||||||
|
while ((1u << log2) < capacity) log2++;
|
||||||
|
|
||||||
|
c->blockDevIndex = blockDevIndex;
|
||||||
|
c->partStartLba = partStartLba;
|
||||||
|
c->blockSize = blockSize;
|
||||||
|
c->sectorsPerBlock = blockSize / 512;
|
||||||
|
c->capacity = capacity;
|
||||||
|
c->hashShift = 32 - log2;
|
||||||
|
c->nextFresh = 0;
|
||||||
|
c->freeHead = Nil;
|
||||||
|
c->lruHead = Nil;
|
||||||
|
c->lruTail = Nil;
|
||||||
|
for (uint32_t b = 0; b < capacity; b++) c->buckets[b] = Nil;
|
||||||
|
|
||||||
|
// Prefetch is an optimization; without a staging buffer it is a no-op.
|
||||||
|
c->stagingPages = (int)((MaxRunSectors * 512 + 0xFFF) / 0x1000);
|
||||||
|
c->staging = (uint8_t*)Memory::g_pfa->ReallocConsecutive(nullptr, c->stagingPages);
|
||||||
|
if (c->staging == nullptr) c->stagingPages = 0;
|
||||||
|
|
||||||
|
KernelLogStream(OK, "BlockCache") << "Block cache: up to "
|
||||||
|
<< base::dec << (uint64_t)capacity << " blocks ("
|
||||||
|
<< (uint64_t)capacity * blockSize / (1024 * 1024) << " MiB)";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Destroy(Cache* c) {
|
||||||
|
if (c == nullptr) return;
|
||||||
|
for (uint32_t i = 0; i < c->nextFresh; i++) {
|
||||||
|
if (c->entries[i].data != nullptr) Memory::g_pfa->Free(c->entries[i].data);
|
||||||
|
}
|
||||||
|
if (c->staging != nullptr) Memory::g_pfa->Free(c->staging, c->stagingPages);
|
||||||
|
Memory::g_pfa->Free(c->entries, c->tablePages);
|
||||||
|
Memory::g_heap->Free(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Read(Cache* c, uint32_t block, void* buf) {
|
||||||
|
if (block == 0) return false;
|
||||||
|
|
||||||
|
uint32_t i = Lookup(c, block);
|
||||||
|
if (i != Nil) {
|
||||||
|
Touch(c, i);
|
||||||
|
memcpy(buf, c->entries[i].data, c->blockSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
i = Acquire(c);
|
||||||
|
if (i == Nil) return DevRead(c, block, 1, buf);
|
||||||
|
|
||||||
|
if (!DevRead(c, block, 1, c->entries[i].data)) {
|
||||||
|
c->entries[i].hashNext = c->freeHead;
|
||||||
|
c->freeHead = i;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Insert(c, i, block);
|
||||||
|
memcpy(buf, c->entries[i].data, c->blockSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Write(Cache* c, uint32_t block, const void* buf) {
|
||||||
|
if (block == 0) return false;
|
||||||
|
|
||||||
|
uint32_t i = Lookup(c, block);
|
||||||
|
if (!DevWrite(c, block, buf)) {
|
||||||
|
// The on-disk contents are now unknown; don't serve a stale copy.
|
||||||
|
if (i != Nil) Drop(c, i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i != Nil) {
|
||||||
|
Touch(c, i);
|
||||||
|
} else {
|
||||||
|
i = Acquire(c);
|
||||||
|
if (i == Nil) return true;
|
||||||
|
Insert(c, i, block);
|
||||||
|
}
|
||||||
|
memcpy(c->entries[i].data, buf, c->blockSize);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Contains(Cache* c, uint32_t block) {
|
||||||
|
return block != 0 && Lookup(c, block) != Nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t MaxRunBlocks(Cache* c) {
|
||||||
|
return MaxRunSectors / c->sectorsPerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Prefetch(Cache* c, uint32_t first, uint32_t count) {
|
||||||
|
if (c->staging == nullptr || first == 0) return;
|
||||||
|
|
||||||
|
uint32_t maxRun = MaxRunBlocks(c);
|
||||||
|
uint32_t end = first + count;
|
||||||
|
if (end < first) return; // wrapped
|
||||||
|
|
||||||
|
uint32_t b = first;
|
||||||
|
while (b < end) {
|
||||||
|
if (Lookup(c, b) != Nil) { b++; continue; }
|
||||||
|
|
||||||
|
uint32_t run = 1;
|
||||||
|
while (run < maxRun && b + run < end && Lookup(c, b + run) == Nil) run++;
|
||||||
|
|
||||||
|
if (!DevRead(c, b, run, c->staging)) return;
|
||||||
|
|
||||||
|
for (uint32_t k = 0; k < run; k++) {
|
||||||
|
uint32_t i = Acquire(c);
|
||||||
|
if (i == Nil) return;
|
||||||
|
memcpy(c->entries[i].data, c->staging + (uint64_t)k * c->blockSize,
|
||||||
|
c->blockSize);
|
||||||
|
Insert(c, i, b + k);
|
||||||
|
}
|
||||||
|
b += run;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* BlockCache.hpp
|
||||||
|
* Per-mount LRU cache of filesystem blocks
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace Fs::BlockCache {
|
||||||
|
|
||||||
|
// A cache over one partition of a block device, in units of the
|
||||||
|
// filesystem's block size (512..4096 bytes, addressed in 512-byte sectors
|
||||||
|
// from the partition start). Write-through: Write reaches the device
|
||||||
|
// before it returns, so the cache never holds dirty data and there is
|
||||||
|
// nothing to flush on sync or unmount.
|
||||||
|
//
|
||||||
|
// Not internally locked. Callers serialize access; the VFS already holds
|
||||||
|
// vfsLock across every driver call.
|
||||||
|
struct Cache;
|
||||||
|
|
||||||
|
// Create a cache for the partition. Capacity is sized from free memory at
|
||||||
|
// the time of the call; block pages are allocated lazily as the cache
|
||||||
|
// fills and returned on Destroy. Returns nullptr if the bookkeeping
|
||||||
|
// cannot be allocated (callers then do uncached I/O).
|
||||||
|
Cache* Create(int blockDevIndex, uint64_t partStartLba, uint32_t blockSize);
|
||||||
|
void Destroy(Cache* cache);
|
||||||
|
|
||||||
|
// Read one block, from the cache or the device. Block 0 is never cached.
|
||||||
|
bool Read(Cache* cache, uint32_t block, void* buf);
|
||||||
|
|
||||||
|
// Write one block to the device, then update the cached copy.
|
||||||
|
bool Write(Cache* cache, uint32_t block, const void* buf);
|
||||||
|
|
||||||
|
bool Contains(Cache* cache, uint32_t block);
|
||||||
|
|
||||||
|
// Pull `count` physically contiguous blocks starting at `first` into the
|
||||||
|
// cache, in as few device commands as possible. Blocks already cached are
|
||||||
|
// skipped. Best effort: a failed read just leaves those blocks uncached.
|
||||||
|
void Prefetch(Cache* cache, uint32_t first, uint32_t count);
|
||||||
|
|
||||||
|
// Largest run Prefetch reads in one device command.
|
||||||
|
uint32_t MaxRunBlocks(Cache* cache);
|
||||||
|
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "Ext2.hpp"
|
#include "Ext2.hpp"
|
||||||
#include "FsProbe.hpp"
|
#include "FsProbe.hpp"
|
||||||
|
#include "BlockCache.hpp"
|
||||||
#include <Drivers/Storage/BlockDevice.hpp>
|
#include <Drivers/Storage/BlockDevice.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
@@ -171,6 +172,10 @@ namespace Fs::Ext2 {
|
|||||||
uint8_t* blockBuf;
|
uint8_t* blockBuf;
|
||||||
int blockBufPages;
|
int blockBufPages;
|
||||||
|
|
||||||
|
// Write-through cache that every ReadBlock/WriteBlock goes through.
|
||||||
|
// Null if it could not be allocated, in which case I/O is uncached.
|
||||||
|
BlockCache::Cache* cache;
|
||||||
|
|
||||||
// Open file handles
|
// Open file handles
|
||||||
Ext2File files[MaxFilesPerInstance];
|
Ext2File files[MaxFilesPerInstance];
|
||||||
|
|
||||||
@@ -238,12 +243,14 @@ namespace Fs::Ext2 {
|
|||||||
|
|
||||||
static bool ReadBlock(const Ext2Instance& inst, uint32_t blockNum, void* buf) {
|
static bool ReadBlock(const Ext2Instance& inst, uint32_t blockNum, void* buf) {
|
||||||
if (blockNum == 0) return false;
|
if (blockNum == 0) return false;
|
||||||
|
if (inst.cache) return BlockCache::Read(inst.cache, blockNum, buf);
|
||||||
return ReadPartSectors(inst, BlockToPartSector(inst, blockNum),
|
return ReadPartSectors(inst, BlockToPartSector(inst, blockNum),
|
||||||
SectorsPerBlock(inst), buf);
|
SectorsPerBlock(inst), buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool WriteBlock(const Ext2Instance& inst, uint32_t blockNum, const void* buf) {
|
static bool WriteBlock(const Ext2Instance& inst, uint32_t blockNum, const void* buf) {
|
||||||
if (blockNum == 0) return false;
|
if (blockNum == 0) return false;
|
||||||
|
if (inst.cache) return BlockCache::Write(inst.cache, blockNum, buf);
|
||||||
return WritePartSectors(inst, BlockToPartSector(inst, blockNum),
|
return WritePartSectors(inst, BlockToPartSector(inst, blockNum),
|
||||||
SectorsPerBlock(inst), buf);
|
SectorsPerBlock(inst), buf);
|
||||||
}
|
}
|
||||||
@@ -1294,6 +1301,20 @@ namespace Fs::Ext2 {
|
|||||||
uint32_t physBlock = GetPhysicalBlock(self, file.inode, logicalBlock);
|
uint32_t physBlock = GetPhysicalBlock(self, file.inode, logicalBlock);
|
||||||
if (physBlock == 0) break;
|
if (physBlock == 0) break;
|
||||||
|
|
||||||
|
// On a miss, read ahead the physically contiguous run of this
|
||||||
|
// file's following blocks in one device command. Resolving the
|
||||||
|
// run is cheap: the indirect blocks it walks are cached too.
|
||||||
|
if (self.cache && !BlockCache::Contains(self.cache, physBlock)) {
|
||||||
|
uint32_t lastLogical = (fileSize - 1) / blockSize;
|
||||||
|
uint32_t maxRun = BlockCache::MaxRunBlocks(self.cache);
|
||||||
|
uint32_t run = 1;
|
||||||
|
while (run < maxRun && logicalBlock + run <= lastLogical &&
|
||||||
|
GetPhysicalBlock(self, file.inode, logicalBlock + run) == physBlock + run) {
|
||||||
|
run++;
|
||||||
|
}
|
||||||
|
BlockCache::Prefetch(self.cache, physBlock, run);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ReadBlock(self, physBlock, dataBuf)) break;
|
if (!ReadBlock(self, physBlock, dataBuf)) break;
|
||||||
|
|
||||||
uint32_t available = blockSize - blockOff;
|
uint32_t available = blockSize - blockOff;
|
||||||
@@ -1882,6 +1903,7 @@ namespace Fs::Ext2 {
|
|||||||
if (self->dirNames != nullptr && self->dirNamesPages > 0) {
|
if (self->dirNames != nullptr && self->dirNamesPages > 0) {
|
||||||
Memory::g_pfa->Free(self->dirNames, self->dirNamesPages);
|
Memory::g_pfa->Free(self->dirNames, self->dirNamesPages);
|
||||||
}
|
}
|
||||||
|
BlockCache::Destroy(self->cache);
|
||||||
|
|
||||||
self->active = false;
|
self->active = false;
|
||||||
g_instanceSlots[(std::size_t)inst] = nullptr;
|
g_instanceSlots[(std::size_t)inst] = nullptr;
|
||||||
@@ -1916,6 +1938,7 @@ namespace Fs::Ext2 {
|
|||||||
if (self->bgdt != nullptr && self->bgdtPages > 0) {
|
if (self->bgdt != nullptr && self->bgdtPages > 0) {
|
||||||
Memory::g_pfa->Free(self->bgdt, self->bgdtPages);
|
Memory::g_pfa->Free(self->bgdt, self->bgdtPages);
|
||||||
}
|
}
|
||||||
|
BlockCache::Destroy(self->cache);
|
||||||
if (self->driver != nullptr) Memory::g_heap->Free(self->driver);
|
if (self->driver != nullptr) Memory::g_heap->Free(self->driver);
|
||||||
Memory::g_heap->Free(self);
|
Memory::g_heap->Free(self);
|
||||||
}
|
}
|
||||||
@@ -2025,6 +2048,12 @@ namespace Fs::Ext2 {
|
|||||||
return AbortMount(idx);
|
return AbortMount(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inst.cache = BlockCache::Create(blockDevIndex, startLba, blockSize);
|
||||||
|
if (!inst.cache) {
|
||||||
|
KernelLogStream(WARNING, "Ext2") << "No memory for a block cache; "
|
||||||
|
"volume I/O will be uncached";
|
||||||
|
}
|
||||||
|
|
||||||
// Load block group descriptor table
|
// Load block group descriptor table
|
||||||
// BGDT starts at the block after the superblock
|
// BGDT starts at the block after the superblock
|
||||||
uint32_t bgdtStartBlock = inst.firstDataBlock + 1;
|
uint32_t bgdtStartBlock = inst.firstDataBlock + 1;
|
||||||
|
|||||||
+96
-12
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-7
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <Memory/Heap.hpp>
|
#include <Memory/Heap.hpp>
|
||||||
#include <Common/Panic.hpp>
|
#include <Common/Panic.hpp>
|
||||||
|
#include <Common/TerminationCodes.hpp>
|
||||||
#include <Platform/Registers.hpp>
|
#include <Platform/Registers.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
#include <Memory/PageFrameAllocator.hpp>
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
@@ -170,14 +171,21 @@ namespace Hal {
|
|||||||
CrashReport::AddReport(rep);
|
CrashReport::AddReport(rep);
|
||||||
Sched::SpawnCrashPad(proc->pid);
|
Sched::SpawnCrashPad(proc->pid);
|
||||||
|
|
||||||
// Record a killed-by-signal exit code (256+sig) so a parent
|
// Record a termination code (256+sig) so a parent blocked in
|
||||||
// blocked in SYS_WAITPID can tell a crash from a clean exit.
|
// SYS_WAITPID can tell a crash from a clean exit.
|
||||||
{
|
{
|
||||||
int sig = 11; /* SIGSEGV */
|
int code = TerminationFromVector(i);
|
||||||
if (i == 0x00) sig = 8; /* #DE -> SIGFPE */
|
|
||||||
else if (i == 0x06) sig = 4; /* #UD -> SIGILL */
|
// ExitProcess from a sibling thread ends only that thread,
|
||||||
else if (i == 0x10 || i == 0x13) sig = 8; /* x87/SIMD FP */
|
// which would leave the process running with the crash
|
||||||
Sched::SetProcessExitCode(proc->pid, 256 + sig);
|
// swallowed. Flag the whole process (main thread and every
|
||||||
|
// sibling) first, carrying the crash code rather than
|
||||||
|
// KillProcess's default TerminatedKilled, so waiters see the fault.
|
||||||
|
if (Sched::GetCurrentThreadPtr() != proc) {
|
||||||
|
Sched::KillProcess(proc->pid, code);
|
||||||
|
} else {
|
||||||
|
Sched::SetProcessExitCode(proc->pid, code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Sched::ExitProcess();
|
Sched::ExitProcess();
|
||||||
|
|||||||
@@ -536,16 +536,13 @@ namespace Sched {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the main thread's TLS block (x86-64 variant II): the block
|
// Build the main thread's TLS block (x86-64 variant II): the block
|
||||||
// lives below the thread pointer, TP = base + S where
|
// lives below the thread pointer (see ComputeTlsLayout), template
|
||||||
// S = align_up(memsz, align), template data at [base, base+filesz),
|
// data first, .tbss zeroed, and [TP] holds the ABI self-pointer.
|
||||||
// .tbss zeroed, and [TP] holds the ABI-required self-pointer.
|
|
||||||
if (tlsPhdr != nullptr && outTls != nullptr) {
|
if (tlsPhdr != nullptr && outTls != nullptr) {
|
||||||
uint64_t align = tlsPhdr->p_align < 16 ? 16 : tlsPhdr->p_align;
|
TlsLayout layout = ComputeTlsLayout(tlsPhdr->p_memsz, tlsPhdr->p_align);
|
||||||
uint64_t S = (tlsPhdr->p_memsz + align - 1) & ~(align - 1);
|
|
||||||
uint64_t base = (maxVaddrEnd + 0x1FFFULL) & ~0xFFFULL; // page-aligned, 1 guard page gap
|
uint64_t base = (maxVaddrEnd + 0x1FFFULL) & ~0xFFFULL; // page-aligned, 1 guard page gap
|
||||||
uint64_t total = S + 16; // + minimal TCB
|
uint64_t numPages = (layout.totalSize + 0xFFF) / 0x1000;
|
||||||
uint64_t numPages = (total + 0xFFF) / 0x1000;
|
uint64_t tp = base + layout.tpOffset;
|
||||||
uint64_t tp = base + S;
|
|
||||||
|
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
for (uint64_t p = 0; p < numPages && ok; p++) {
|
for (uint64_t p = 0; p < numPages && ok; p++) {
|
||||||
@@ -562,7 +559,7 @@ namespace Sched {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy the .tdata template bytes that land in this page.
|
// Copy the .tdata template bytes that land in this page.
|
||||||
if (!copyPageFromFile(physAddr, virtAddr, base,
|
if (!copyPageFromFile(physAddr, virtAddr, base + layout.templateOffset,
|
||||||
tlsPhdr->p_offset, tlsPhdr->p_filesz)) {
|
tlsPhdr->p_offset, tlsPhdr->p_filesz)) {
|
||||||
ok = false;
|
ok = false;
|
||||||
break;
|
break;
|
||||||
@@ -579,7 +576,7 @@ namespace Sched {
|
|||||||
outTls->templateVaddr = tlsPhdr->p_vaddr;
|
outTls->templateVaddr = tlsPhdr->p_vaddr;
|
||||||
outTls->fileSize = tlsPhdr->p_filesz;
|
outTls->fileSize = tlsPhdr->p_filesz;
|
||||||
outTls->memSize = tlsPhdr->p_memsz;
|
outTls->memSize = tlsPhdr->p_memsz;
|
||||||
outTls->align = align;
|
outTls->align = tlsPhdr->p_align;
|
||||||
} else {
|
} else {
|
||||||
Kt::KernelLogStream(Kt::ERROR, "ELF")
|
Kt::KernelLogStream(Kt::ERROR, "ELF")
|
||||||
<< "PT_TLS setup failed (out of pages?), "
|
<< "PT_TLS setup failed (out of pages?), "
|
||||||
|
|||||||
@@ -60,9 +60,30 @@ namespace Sched {
|
|||||||
uint64_t templateVaddr; // source of .tdata in the loaded image
|
uint64_t templateVaddr; // source of .tdata in the loaded image
|
||||||
uint64_t fileSize; // initialized bytes
|
uint64_t fileSize; // initialized bytes
|
||||||
uint64_t memSize; // total bytes (incl. .tbss)
|
uint64_t memSize; // total bytes (incl. .tbss)
|
||||||
uint64_t align;
|
uint64_t align; // PT_TLS p_align, NOT rounded up
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Geometry of one variant II TLS block, as offsets from its
|
||||||
|
// page-aligned base. The linker addresses TLS variables at
|
||||||
|
// TP - align_up(memSize, p_align) + offset, so the template must start
|
||||||
|
// exactly that far below TP; rounding p_align up first (as an older
|
||||||
|
// version did) shifts every variable away from its initial value.
|
||||||
|
// TP itself is kept at least 16-byte aligned, which the gap between
|
||||||
|
// base and the template absorbs.
|
||||||
|
struct TlsLayout {
|
||||||
|
uint64_t templateOffset; // where .tdata/.tbss start
|
||||||
|
uint64_t tpOffset; // thread pointer; [TP] holds the self-pointer
|
||||||
|
uint64_t totalSize; // bytes to map, incl. the 16-byte TCB
|
||||||
|
};
|
||||||
|
|
||||||
|
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
|
||||||
|
uint64_t align = pAlign ? pAlign : 1;
|
||||||
|
uint64_t block = (memSize + align - 1) & ~(align - 1);
|
||||||
|
uint64_t tpAlign = align < 16 ? 16 : align;
|
||||||
|
uint64_t tp = (block + tpAlign - 1) & ~(tpAlign - 1);
|
||||||
|
return {tp - block, tp, tp + 16};
|
||||||
|
}
|
||||||
|
|
||||||
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls);
|
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls);
|
||||||
|
|
||||||
// Load a shared library into a fixed address range.
|
// Load a shared library into a fixed address range.
|
||||||
|
|||||||
+151
-17
@@ -324,6 +324,8 @@ namespace Sched {
|
|||||||
processTable[i].inTail = 0;
|
processTable[i].inTail = 0;
|
||||||
processTable[i].keyHead = 0;
|
processTable[i].keyHead = 0;
|
||||||
processTable[i].keyTail = 0;
|
processTable[i].keyTail = 0;
|
||||||
|
processTable[i].getcharUtf8Len = 0;
|
||||||
|
processTable[i].getcharUtf8Pos = 0;
|
||||||
processTable[i].termCols = 0;
|
processTable[i].termCols = 0;
|
||||||
processTable[i].termRows = 0;
|
processTable[i].termRows = 0;
|
||||||
processTable[i].ioOutHandle = -1;
|
processTable[i].ioOutHandle = -1;
|
||||||
@@ -512,6 +514,8 @@ namespace Sched {
|
|||||||
proc.joinerSlot = -1;
|
proc.joinerSlot = -1;
|
||||||
proc.exitCode = 0;
|
proc.exitCode = 0;
|
||||||
proc.joinable = false; // main thread is reaped by BSP, not joined
|
proc.joinable = false; // main thread is reaped by BSP, not joined
|
||||||
|
proc.futexAddr = 0;
|
||||||
|
proc.futexWoken = false;
|
||||||
proc.savedRsp = (uint64_t)sp;
|
proc.savedRsp = (uint64_t)sp;
|
||||||
proc.stackBase = (uint64_t)kernelStackBase;
|
proc.stackBase = (uint64_t)kernelStackBase;
|
||||||
proc.entryPoint = entry;
|
proc.entryPoint = entry;
|
||||||
@@ -654,6 +658,8 @@ namespace Sched {
|
|||||||
proc.inTail = 0;
|
proc.inTail = 0;
|
||||||
proc.keyHead = 0;
|
proc.keyHead = 0;
|
||||||
proc.keyTail = 0;
|
proc.keyTail = 0;
|
||||||
|
proc.getcharUtf8Len = 0;
|
||||||
|
proc.getcharUtf8Pos = 0;
|
||||||
proc.termCols = 0;
|
proc.termCols = 0;
|
||||||
proc.termRows = 0;
|
proc.termRows = 0;
|
||||||
proc.ioOutHandle = -1;
|
proc.ioOutHandle = -1;
|
||||||
@@ -714,10 +720,8 @@ namespace Sched {
|
|||||||
uint64_t threadTlsPages = 0;
|
uint64_t threadTlsPages = 0;
|
||||||
Process& primary = processTable[primarySlot_];
|
Process& primary = processTable[primarySlot_];
|
||||||
if (primary.tlsMemSize > 0) {
|
if (primary.tlsMemSize > 0) {
|
||||||
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
|
TlsLayout layout = ComputeTlsLayout(primary.tlsMemSize, primary.tlsAlign);
|
||||||
uint64_t S = (primary.tlsMemSize + align - 1) & ~(align - 1);
|
uint64_t numPages = (layout.totalSize + 0xFFF) / 0x1000;
|
||||||
uint64_t total = S + 16;
|
|
||||||
uint64_t numPages = (total + 0xFFF) / 0x1000;
|
|
||||||
|
|
||||||
uint64_t base = 0;
|
uint64_t base = 0;
|
||||||
if (!ReserveUserHeapRange(primarySlot_, numPages * 0x1000, base)) {
|
if (!ReserveUserHeapRange(primarySlot_, numPages * 0x1000, base)) {
|
||||||
@@ -751,9 +755,9 @@ namespace Sched {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t tp = base + S;
|
uint64_t tp = base + layout.tpOffset;
|
||||||
memcpy((void*)base, (const void*)primary.tlsTemplateVaddr,
|
memcpy((void*)(base + layout.templateOffset),
|
||||||
primary.tlsFileSize);
|
(const void*)primary.tlsTemplateVaddr, primary.tlsFileSize);
|
||||||
*(uint64_t*)tp = tp; /* ABI self-pointer */
|
*(uint64_t*)tp = tp; /* ABI self-pointer */
|
||||||
threadFsBase = tp;
|
threadFsBase = tp;
|
||||||
threadTlsBase = base;
|
threadTlsBase = base;
|
||||||
@@ -820,6 +824,8 @@ namespace Sched {
|
|||||||
thr.exitCode = 0;
|
thr.exitCode = 0;
|
||||||
thr.joinable = true;
|
thr.joinable = true;
|
||||||
thr.threadArg = arg;
|
thr.threadArg = arg;
|
||||||
|
thr.futexAddr = 0;
|
||||||
|
thr.futexWoken = false;
|
||||||
thr.savedRsp = (uint64_t)sp;
|
thr.savedRsp = (uint64_t)sp;
|
||||||
thr.stackBase = (uint64_t)kernelStackBase;
|
thr.stackBase = (uint64_t)kernelStackBase;
|
||||||
thr.entryPoint = entry;
|
thr.entryPoint = entry;
|
||||||
@@ -835,6 +841,7 @@ namespace Sched {
|
|||||||
thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0;
|
thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0;
|
||||||
thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0;
|
thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0;
|
||||||
thr.keyHead = 0; thr.keyTail = 0;
|
thr.keyHead = 0; thr.keyTail = 0;
|
||||||
|
thr.getcharUtf8Len = 0; thr.getcharUtf8Pos = 0;
|
||||||
thr.termCols = 0; thr.termRows = 0;
|
thr.termCols = 0; thr.termRows = 0;
|
||||||
thr.ioOutHandle = -1; thr.ioInHandle = -1;
|
thr.ioOutHandle = -1; thr.ioInHandle = -1;
|
||||||
thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1;
|
thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1;
|
||||||
@@ -936,14 +943,13 @@ namespace Sched {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
|
TlsLayout layout = ComputeTlsLayout(primary.tlsMemSize, primary.tlsAlign);
|
||||||
uint64_t blockSize = (primary.tlsMemSize + align - 1) & ~(align - 1);
|
if (layout.tpOffset > thr.fsBase || layout.totalSize < layout.tpOffset) {
|
||||||
if (blockSize > thr.fsBase || blockSize > UINT64_MAX - 16) {
|
|
||||||
Panic("Invalid sibling TLS metadata during thread teardown", nullptr);
|
Panic("Invalid sibling TLS metadata during thread teardown", nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t base = thr.fsBase - blockSize;
|
uint64_t base = thr.fsBase - layout.tpOffset;
|
||||||
uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000;
|
uint64_t pages = (layout.totalSize + 0xFFF) / 0x1000;
|
||||||
thr.fsBase = 0;
|
thr.fsBase = 0;
|
||||||
Memory::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
|
Memory::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
|
||||||
ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL);
|
ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL);
|
||||||
@@ -1700,6 +1706,8 @@ namespace Sched {
|
|||||||
proc.inTail = 0;
|
proc.inTail = 0;
|
||||||
proc.keyHead = 0;
|
proc.keyHead = 0;
|
||||||
proc.keyTail = 0;
|
proc.keyTail = 0;
|
||||||
|
proc.getcharUtf8Len = 0;
|
||||||
|
proc.getcharUtf8Pos = 0;
|
||||||
proc.termCols = 0;
|
proc.termCols = 0;
|
||||||
proc.termRows = 0;
|
proc.termRows = 0;
|
||||||
proc.ioOutHandle = -1;
|
proc.ioOutHandle = -1;
|
||||||
@@ -1780,10 +1788,13 @@ namespace Sched {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int KillProcess(int pid) {
|
int KillProcess(int pid, int exitCode) {
|
||||||
// Refuse to kill PID 0 (init) or caller's own process
|
// Refuse to kill PID 0 (init). Killing the caller's own process is
|
||||||
|
// allowed: a faulting/aborting sibling thread shares the pid and has
|
||||||
|
// no other way to take the process down. The caller's own slot gets
|
||||||
|
// killPending below and exits at syscall return (ExitIfKilled) or
|
||||||
|
// via the ExitProcess the fault handler calls next.
|
||||||
if (pid == 0) return -1;
|
if (pid == 0) return -1;
|
||||||
if (pid == GetCurrentPid()) return -1;
|
|
||||||
|
|
||||||
schedLock.Acquire();
|
schedLock.Acquire();
|
||||||
|
|
||||||
@@ -1806,7 +1817,7 @@ namespace Sched {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
processTable[primarySlot_].exitCode = 256 + 9; /* killed (SIGKILL) */
|
processTable[primarySlot_].exitCode = exitCode;
|
||||||
|
|
||||||
// Flag the main thread so its next tick (or scheduler dispatch)
|
// Flag the main thread so its next tick (or scheduler dispatch)
|
||||||
// routes through ExitProcess, which sweeps every sibling thread
|
// routes through ExitProcess, which sweeps every sibling thread
|
||||||
@@ -1874,7 +1885,7 @@ namespace Sched {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
primary.exitCode = 256 + 9; /* killed (SIGKILL) */
|
primary.exitCode = TerminatedKilled;
|
||||||
primary.killPending = true;
|
primary.killPending = true;
|
||||||
if (primary.state == ProcessState::Blocked) {
|
if (primary.state == ProcessState::Blocked) {
|
||||||
primary.state = ProcessState::Ready;
|
primary.state = ProcessState::Ready;
|
||||||
@@ -2109,6 +2120,129 @@ namespace Sched {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Monotonic enrollment counter for FIFO futex wakeups. Only touched
|
||||||
|
// under schedLock.
|
||||||
|
static uint64_t futexNextSeq = 1;
|
||||||
|
|
||||||
|
int FutexWait(uint64_t uaddr, uint32_t expected, uint64_t timeoutNs) {
|
||||||
|
if ((uaddr & 3) != 0) return InvalidArgument;
|
||||||
|
|
||||||
|
ExitIfKilled();
|
||||||
|
|
||||||
|
auto* cpu = Smp::GetCurrentCpuData();
|
||||||
|
int slot = cpu ? cpu->currentSlot : -1;
|
||||||
|
if (slot < 0) return GenericFailure;
|
||||||
|
Process& self = processTable[slot];
|
||||||
|
|
||||||
|
// Round up so a short timeout never turns into "check only".
|
||||||
|
uint64_t timeoutMs = 0;
|
||||||
|
if (timeoutNs != FutexWaitForever) {
|
||||||
|
if (timeoutNs == 0) {
|
||||||
|
timeoutMs = 0;
|
||||||
|
} else {
|
||||||
|
timeoutMs = timeoutNs / 1000000ULL + ((timeoutNs % 1000000ULL) ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schedLock.Acquire();
|
||||||
|
|
||||||
|
// The word is read here, with schedLock held and interrupts off, so
|
||||||
|
// it must not fault: the caller validated (and demand-committed) the
|
||||||
|
// page, and the page walk below re-confirms it is still mapped. An
|
||||||
|
// aligned 32-bit word never straddles a page.
|
||||||
|
if (!Memory::VMM::Paging::IsUserRangeAccessible(self.pml4Phys, uaddr, 4, false)) {
|
||||||
|
schedLock.Release();
|
||||||
|
return BadPointer;
|
||||||
|
}
|
||||||
|
uint32_t current = __atomic_load_n((volatile uint32_t*)uaddr, __ATOMIC_SEQ_CST);
|
||||||
|
if (current != expected) {
|
||||||
|
schedLock.Release();
|
||||||
|
return WouldBlock;
|
||||||
|
}
|
||||||
|
if (timeoutNs == 0) {
|
||||||
|
schedLock.Release();
|
||||||
|
return TimedOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = ProcessState::Blocked;
|
||||||
|
self.waitingForPid = -1;
|
||||||
|
self.waitingOnObject = nullptr;
|
||||||
|
self.futexAddr = uaddr;
|
||||||
|
self.futexSeq = futexNextSeq++;
|
||||||
|
self.futexWoken = false;
|
||||||
|
self.sleepUntilTick = (timeoutNs == FutexWaitForever)
|
||||||
|
? 0
|
||||||
|
: Timekeeping::GetTicks() + timeoutMs;
|
||||||
|
self.runningOnCpu = -1;
|
||||||
|
SwitchAwayFromBlockedCurrentLocked();
|
||||||
|
|
||||||
|
// Whatever made this thread runnable (FutexWake, the deadline, or a
|
||||||
|
// kill) it is no longer parked; clear the key before this thread can
|
||||||
|
// block for some other reason and be mistaken for a futex waiter.
|
||||||
|
schedLock.Acquire();
|
||||||
|
bool woken = self.futexWoken;
|
||||||
|
self.futexAddr = 0;
|
||||||
|
self.futexWoken = false;
|
||||||
|
schedLock.Release();
|
||||||
|
|
||||||
|
ExitIfKilled();
|
||||||
|
return woken ? Success : TimedOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FutexWake(uint64_t uaddr, uint32_t count) {
|
||||||
|
if ((uaddr & 3) != 0 || uaddr == 0) return InvalidArgument;
|
||||||
|
if (count == 0) return 0;
|
||||||
|
|
||||||
|
auto* cpu = Smp::GetCurrentCpuData();
|
||||||
|
int slot = cpu ? cpu->currentSlot : -1;
|
||||||
|
if (slot < 0) return GenericFailure;
|
||||||
|
|
||||||
|
schedLock.Acquire();
|
||||||
|
int primary = processTable[slot].primarySlot;
|
||||||
|
if (primary < 0) primary = slot;
|
||||||
|
|
||||||
|
// Gather this process's waiters on uaddr. At most MaxProcesses.
|
||||||
|
int waiters[MaxProcesses];
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < MaxProcesses; i++) {
|
||||||
|
Process& p = processTable[i];
|
||||||
|
if (p.state != ProcessState::Blocked) continue;
|
||||||
|
if (p.futexAddr != uaddr) continue;
|
||||||
|
if (p.primarySlot != primary) continue;
|
||||||
|
waiters[n++] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waking everyone needs no ordering; otherwise pick oldest-first.
|
||||||
|
bool ordered = (uint32_t)n > count;
|
||||||
|
int woken = 0;
|
||||||
|
while (woken < n && (uint32_t)woken < count) {
|
||||||
|
if (ordered) {
|
||||||
|
int best = woken;
|
||||||
|
for (int j = woken + 1; j < n; j++) {
|
||||||
|
if (processTable[waiters[j]].futexSeq < processTable[waiters[best]].futexSeq)
|
||||||
|
best = j;
|
||||||
|
}
|
||||||
|
int tmp = waiters[woken]; waiters[woken] = waiters[best]; waiters[best] = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process& p = processTable[waiters[woken]];
|
||||||
|
p.futexAddr = 0;
|
||||||
|
p.futexWoken = true;
|
||||||
|
p.sleepUntilTick = 0;
|
||||||
|
p.state = ProcessState::Ready;
|
||||||
|
readyCount++;
|
||||||
|
woken++;
|
||||||
|
}
|
||||||
|
schedLock.Release();
|
||||||
|
|
||||||
|
if (woken > 0) {
|
||||||
|
// Re-read: this thread may have migrated since `cpu` was sampled.
|
||||||
|
auto* here = Smp::GetCurrentCpuData();
|
||||||
|
KickOneIdleCpu(here ? here->cpuIndex : -1);
|
||||||
|
}
|
||||||
|
return woken;
|
||||||
|
}
|
||||||
|
|
||||||
bool IsAlive(int pid) {
|
bool IsAlive(int pid) {
|
||||||
for (int i = 0; i < MaxProcesses; i++) {
|
for (int i = 0; i < MaxProcesses; i++) {
|
||||||
if (processTable[i].pid == pid) {
|
if (processTable[i].pid == pid) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <Common/TerminationCodes.hpp>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <Api/Syscall.hpp>
|
#include <Api/Syscall.hpp>
|
||||||
|
|
||||||
@@ -126,6 +127,13 @@ namespace Sched {
|
|||||||
bool joinable = true; // Sibling thread keeps Terminated state for the joiner
|
bool joinable = true; // Sibling thread keeps Terminated state for the joiner
|
||||||
uint64_t threadArg = 0; // Argument passed into entryPoint on first dispatch
|
uint64_t threadArg = 0; // Argument passed into entryPoint on first dispatch
|
||||||
|
|
||||||
|
// Futex wait state (see FutexWait). futexAddr is the user VA this
|
||||||
|
// thread is parked on, 0 when not in a futex wait. futexSeq orders
|
||||||
|
// waiters so FutexWake releases the longest-waiting first.
|
||||||
|
uint64_t futexAddr = 0;
|
||||||
|
uint64_t futexSeq = 0;
|
||||||
|
bool futexWoken = false;
|
||||||
|
|
||||||
// I/O redirection for GUI terminal
|
// I/O redirection for GUI terminal
|
||||||
bool redirected = false;
|
bool redirected = false;
|
||||||
int parentPid = -1;
|
int parentPid = -1;
|
||||||
@@ -135,9 +143,13 @@ namespace Sched {
|
|||||||
uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar)
|
uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar)
|
||||||
volatile uint32_t inHead = 0;
|
volatile uint32_t inHead = 0;
|
||||||
volatile uint32_t inTail = 0;
|
volatile uint32_t inTail = 0;
|
||||||
montauk::abi::KeyEvent keyBuf[64]; // parent injects, child reads (getkey/iskeyavailable)
|
|
||||||
volatile uint32_t keyHead = 0;
|
volatile uint32_t keyHead = 0;
|
||||||
volatile uint32_t keyTail = 0;
|
volatile uint32_t keyTail = 0;
|
||||||
|
// SYS_GETCHAR returns a key's UTF-8 encoding one byte per call; the
|
||||||
|
// bytes not handed out yet wait here.
|
||||||
|
uint8_t getcharUtf8[4] = {};
|
||||||
|
uint8_t getcharUtf8Len = 0;
|
||||||
|
uint8_t getcharUtf8Pos = 0;
|
||||||
static constexpr uint32_t IoBufSize = 4096;
|
static constexpr uint32_t IoBufSize = 4096;
|
||||||
|
|
||||||
// GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE)
|
// GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE)
|
||||||
@@ -266,10 +278,35 @@ namespace Sched {
|
|||||||
// Wake any processes blocked on the given object.
|
// Wake any processes blocked on the given object.
|
||||||
void WakeObjectWaiters(void* object);
|
void WakeObjectWaiters(void* object);
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Futexes
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
// Futexes are private to a process: the key is (primary slot, user VA),
|
||||||
|
// so two processes waiting on the same address never see each other.
|
||||||
|
static constexpr uint64_t FutexWaitForever = ~0ULL;
|
||||||
|
|
||||||
|
// Block the current thread on the 32-bit word at `uaddr` if it still
|
||||||
|
// holds `expected`. The comparison and the enrollment happen atomically
|
||||||
|
// under the scheduler lock, so a FutexWake issued after the waker changed
|
||||||
|
// the word can never be lost. `timeoutNs` is rounded up to the 1 ms tick;
|
||||||
|
// FutexWaitForever waits without a deadline, 0 only performs the check.
|
||||||
|
// Returns Success when woken by FutexWake, WouldBlock when the word did
|
||||||
|
// not hold `expected`, TimedOut when the deadline passed, InvalidArgument
|
||||||
|
// for a misaligned address, BadPointer when the word is not mapped.
|
||||||
|
// Spurious Success returns are not possible, but callers must still
|
||||||
|
// re-check their condition: another thread may have run in between.
|
||||||
|
int FutexWait(uint64_t uaddr, uint32_t expected, uint64_t timeoutNs);
|
||||||
|
|
||||||
|
// Wake up to `count` threads of the current process blocked on `uaddr`,
|
||||||
|
// longest-waiting first. Returns the number woken.
|
||||||
|
int FutexWake(uint64_t uaddr, uint32_t count);
|
||||||
|
|
||||||
// Kill a process by PID. If the process is running on another CPU,
|
// Kill a process by PID. If the process is running on another CPU,
|
||||||
// sets a kill-pending flag checked on the next timer tick.
|
// sets a kill-pending flag checked on the next timer tick. exitCode is
|
||||||
|
// what a waiter sees (a TerminationCodes value).
|
||||||
// Returns 0 on success, -1 on failure.
|
// Returns 0 on success, -1 on failure.
|
||||||
int KillProcess(int pid);
|
int KillProcess(int pid, int exitCode = TerminatedKilled);
|
||||||
|
|
||||||
// Start a new process session for the caller, or terminate every process
|
// Start a new process session for the caller, or terminate every process
|
||||||
// belonging to a session. KillSession returns the number of live members
|
// belonging to a session. KillSession returns the number of live members
|
||||||
|
|||||||
+23
-3
@@ -96,9 +96,9 @@ WPDIR := data/wallpapers
|
|||||||
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
||||||
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
||||||
|
|
||||||
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls
|
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls rust
|
||||||
|
|
||||||
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
|
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad rust $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
|
||||||
|
|
||||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||||
@@ -320,6 +320,26 @@ mandelbrot: libc
|
|||||||
screenshot: libc libjpegwrite libloader dialogs
|
screenshot: libc libjpegwrite libloader dialogs
|
||||||
$(MAKE) -C src/screenshot
|
$(MAKE) -C src/screenshot
|
||||||
|
|
||||||
|
# Rust programs (programs/rust, Phase 1: no_std + alloc via -Zbuild-std).
|
||||||
|
# Optional: skipped with a notice when no rustup-managed cargo with the
|
||||||
|
# pinned nightly is installed, so the default build never requires Rust.
|
||||||
|
# RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading a toolchain itself.
|
||||||
|
# Rust links the toolchain sysroot libc (like ports), not lib/libc directly:
|
||||||
|
# re-run build-montauk-toolchain.sh after libc changes.
|
||||||
|
CARGO ?= $(firstword $(wildcard $(HOME)/.cargo/bin/cargo) $(shell command -v cargo 2>/dev/null))
|
||||||
|
RUST_DIR := rust
|
||||||
|
RUST_OUT := $(RUST_DIR)/target/x86_64-unknown-montauk/release
|
||||||
|
RUST_PROGRAMS := rusthello
|
||||||
|
|
||||||
|
rust:
|
||||||
|
@if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) --version >/dev/null 2>&1); then \
|
||||||
|
echo "rust: no nightly cargo found - skipping Rust programs (see programs/rust/README.md)"; \
|
||||||
|
else \
|
||||||
|
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) build --release) || exit 1; \
|
||||||
|
mkdir -p $(BINDIR)/os; \
|
||||||
|
for p in $(RUST_PROGRAMS); do cp $(RUST_OUT)/$$p $(BINDIR)/os/$$p.elf || exit 1; done; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Verify the TCC C API header (libc/montauk.h) exposes every kernel syscall with
|
# Verify the TCC C API header (libc/montauk.h) exposes every kernel syscall with
|
||||||
# matching numbers. Fails the build on drift; 'make gen-syscalls' refreshes them.
|
# matching numbers. Fails the build on drift; 'make gen-syscalls' refreshes them.
|
||||||
check-syscalls:
|
check-syscalls:
|
||||||
@@ -486,7 +506,7 @@ else
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BINDIR) obj
|
rm -rf $(BINDIR) obj $(RUST_DIR)/target
|
||||||
$(MAKE) -C lib/bearssl clean
|
$(MAKE) -C lib/bearssl clean
|
||||||
$(MAKE) -C lib/libc clean
|
$(MAKE) -C lib/libc clean
|
||||||
$(MAKE) -C lib/libjpeg clean
|
$(MAKE) -C lib/libjpeg clean
|
||||||
|
|||||||
@@ -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,28 @@ namespace montauk::abi {
|
|||||||
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||||
|
|
||||||
|
/* Unicode key events. Size-versioned: the caller passes sizeof(its
|
||||||
|
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
|
||||||
|
rest, so the struct can grow without another syscall. */
|
||||||
|
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
|
||||||
|
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
|
||||||
|
|
||||||
|
/* Futexes: process-private wait queues keyed by a user address. WAIT
|
||||||
|
blocks only if *addr still equals `expected` (checked atomically with
|
||||||
|
enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check
|
||||||
|
only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken,
|
||||||
|
WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the
|
||||||
|
number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */
|
||||||
|
static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status
|
||||||
|
static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status
|
||||||
|
static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL;
|
||||||
|
static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu;
|
||||||
|
|
||||||
|
/* Abnormal termination from any thread: ends the whole process (every
|
||||||
|
thread) with exit code TerminatedAborted (256+SIGABRT). SYS_EXIT from a
|
||||||
|
sibling thread ends only that thread, so abort() needs its own call. */
|
||||||
|
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
|
||||||
|
|
||||||
/* 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 +553,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 +572,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 +635,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.
|
||||||
return TEXT_INPUT_NONE;
|
char typed[montauk::utf8::MAX_BYTES];
|
||||||
|
int typed_len = key.ctrl || key.alt ? 0 : montauk::utf8::key_utf8(key, typed);
|
||||||
|
if (typed_len == 0) return TEXT_INPUT_NONE;
|
||||||
|
for (int i = 0; i < typed_len; i++) {
|
||||||
|
if (!text_input_char_allowed(typed[i], filter, userdata))
|
||||||
|
return TEXT_INPUT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
if (sel_start != sel_end) {
|
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,22 +1267,34 @@ 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;
|
||||||
while (value && value[len] && len < count && len < cap - 1) {
|
if (masked) {
|
||||||
buffer[len] = masked ? '*' : value[len];
|
const char* p = value ? value : "";
|
||||||
len++;
|
while (*p && p - value < count && len < cap - 1) {
|
||||||
|
montauk::utf8::next(&p);
|
||||||
|
buffer[len++] = '*';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (value && value[len] && len < count && len < cap - 1) {
|
||||||
|
buffer[len] = value[len];
|
||||||
|
len++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buffer[len] = '\0';
|
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_len--;
|
text[i] = text[i + n];
|
||||||
cursor--;
|
text_len -= n;
|
||||||
text[text_len] = '\0';
|
cursor = start;
|
||||||
}
|
}
|
||||||
} else if ((unsigned char)key.ascii >= 32
|
} else if (typed_len > 0) {
|
||||||
&& (unsigned char)key.ascii != 127) {
|
// Printable character, inserted as UTF-8
|
||||||
// Printable character
|
if (text_len + typed_len <= 254) {
|
||||||
if (text_len < 254) {
|
for (int i = text_len; i >= cursor; i--)
|
||||||
for (int i = text_len; i > cursor; i--) {
|
text[i + typed_len] = text[i];
|
||||||
text[i] = text[i - 1];
|
for (int i = 0; i < typed_len; i++)
|
||||||
}
|
text[cursor + i] = typed[i];
|
||||||
text[cursor] = key.ascii;
|
cursor += typed_len;
|
||||||
cursor++;
|
text_len += typed_len;
|
||||||
text_len++;
|
|
||||||
text[text_len] = '\0';
|
|
||||||
}
|
}
|
||||||
} else if (key.scancode == 0x4B) {
|
} else if (montauk::nav_scancode(key) == 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+216
-194
@@ -23,200 +23,205 @@ extern "C" {
|
|||||||
Generated from Api/Syscall.hpp by scripts/montauk-syscalls.py.
|
Generated from Api/Syscall.hpp by scripts/montauk-syscalls.py.
|
||||||
Do not edit by hand; run 'make gen-syscalls' to refresh.
|
Do not edit by hand; run 'make gen-syscalls' to refresh.
|
||||||
Wrapper functions below are hand-written. */
|
Wrapper functions below are hand-written. */
|
||||||
#define MTK_SYS_EXIT 0
|
#define MTK_SYS_EXIT 0
|
||||||
#define MTK_SYS_YIELD 1
|
#define MTK_SYS_YIELD 1
|
||||||
#define MTK_SYS_SLEEP_MS 2
|
#define MTK_SYS_SLEEP_MS 2
|
||||||
#define MTK_SYS_GETPID 3
|
#define MTK_SYS_GETPID 3
|
||||||
#define MTK_SYS_PRINT 4
|
#define MTK_SYS_PRINT 4
|
||||||
#define MTK_SYS_PUTCHAR 5
|
#define MTK_SYS_PUTCHAR 5
|
||||||
#define MTK_SYS_OPEN 6
|
#define MTK_SYS_OPEN 6
|
||||||
#define MTK_SYS_READ 7
|
#define MTK_SYS_READ 7
|
||||||
#define MTK_SYS_GETSIZE 8
|
#define MTK_SYS_GETSIZE 8
|
||||||
#define MTK_SYS_CLOSE 9
|
#define MTK_SYS_CLOSE 9
|
||||||
#define MTK_SYS_READDIR 10
|
#define MTK_SYS_READDIR 10
|
||||||
#define MTK_SYS_ALLOC 11
|
#define MTK_SYS_ALLOC 11
|
||||||
#define MTK_SYS_FREE 12
|
#define MTK_SYS_FREE 12
|
||||||
#define MTK_SYS_GETTICKS 13
|
#define MTK_SYS_GETTICKS 13
|
||||||
#define MTK_SYS_GETMILLISECONDS 14
|
#define MTK_SYS_GETMILLISECONDS 14
|
||||||
#define MTK_SYS_GETINFO 15
|
#define MTK_SYS_GETINFO 15
|
||||||
#define MTK_SYS_ISKEYAVAILABLE 16
|
#define MTK_SYS_ISKEYAVAILABLE 16
|
||||||
#define MTK_SYS_GETKEY 17
|
#define MTK_SYS_GETKEY 17
|
||||||
#define MTK_SYS_GETCHAR 18
|
#define MTK_SYS_GETCHAR 18
|
||||||
#define MTK_SYS_PING 19
|
#define MTK_SYS_PING 19
|
||||||
#define MTK_SYS_SPAWN 20
|
#define MTK_SYS_SPAWN 20
|
||||||
#define MTK_SYS_FBINFO 21
|
#define MTK_SYS_FBINFO 21
|
||||||
#define MTK_SYS_FBMAP 22
|
#define MTK_SYS_FBMAP 22
|
||||||
#define MTK_SYS_WAITPID 23
|
#define MTK_SYS_WAITPID 23
|
||||||
#define MTK_SYS_TERMSIZE 24
|
#define MTK_SYS_TERMSIZE 24
|
||||||
#define MTK_SYS_GETARGS 25
|
#define MTK_SYS_GETARGS 25
|
||||||
#define MTK_SYS_RESET 26
|
#define MTK_SYS_RESET 26
|
||||||
#define MTK_SYS_SHUTDOWN 27
|
#define MTK_SYS_SHUTDOWN 27
|
||||||
#define MTK_SYS_GETTIME 28
|
#define MTK_SYS_GETTIME 28
|
||||||
#define MTK_SYS_SOCKET 29
|
#define MTK_SYS_SOCKET 29
|
||||||
#define MTK_SYS_CONNECT 30
|
#define MTK_SYS_CONNECT 30
|
||||||
#define MTK_SYS_BIND 31
|
#define MTK_SYS_BIND 31
|
||||||
#define MTK_SYS_LISTEN 32
|
#define MTK_SYS_LISTEN 32
|
||||||
#define MTK_SYS_ACCEPT 33
|
#define MTK_SYS_ACCEPT 33
|
||||||
#define MTK_SYS_SEND 34
|
#define MTK_SYS_SEND 34
|
||||||
#define MTK_SYS_RECV 35
|
#define MTK_SYS_RECV 35
|
||||||
#define MTK_SYS_CLOSESOCK 36
|
#define MTK_SYS_CLOSESOCK 36
|
||||||
#define MTK_SYS_GETNETCFG 37
|
#define MTK_SYS_GETNETCFG 37
|
||||||
#define MTK_SYS_SETNETCFG 38
|
#define MTK_SYS_SETNETCFG 38
|
||||||
#define MTK_SYS_SENDTO 39
|
#define MTK_SYS_SENDTO 39
|
||||||
#define MTK_SYS_RECVFROM 40
|
#define MTK_SYS_RECVFROM 40
|
||||||
#define MTK_SYS_FWRITE 41
|
#define MTK_SYS_FWRITE 41
|
||||||
#define MTK_SYS_FCREATE 42
|
#define MTK_SYS_FCREATE 42
|
||||||
#define MTK_SYS_TERMSCALE 43
|
#define MTK_SYS_TERMSCALE 43
|
||||||
#define MTK_SYS_RESOLVE 44
|
#define MTK_SYS_RESOLVE 44
|
||||||
#define MTK_SYS_GETRANDOM 45
|
#define MTK_SYS_GETRANDOM 45
|
||||||
#define MTK_SYS_LOG 46
|
#define MTK_SYS_LOG 46
|
||||||
#define MTK_SYS_MOUSESTATE 47
|
#define MTK_SYS_MOUSESTATE 47
|
||||||
#define MTK_SYS_SETMOUSEBOUNDS 48
|
#define MTK_SYS_SETMOUSEBOUNDS 48
|
||||||
#define MTK_SYS_SPAWN_REDIR 49
|
#define MTK_SYS_SPAWN_REDIR 49
|
||||||
#define MTK_SYS_CHILDIO_READ 50
|
#define MTK_SYS_CHILDIO_READ 50
|
||||||
#define MTK_SYS_CHILDIO_WRITE 51
|
#define MTK_SYS_CHILDIO_WRITE 51
|
||||||
#define MTK_SYS_CHILDIO_WRITEKEY 52
|
#define MTK_SYS_CHILDIO_WRITEKEY 52
|
||||||
#define MTK_SYS_CHILDIO_SETTERMSZ 53
|
#define MTK_SYS_CHILDIO_SETTERMSZ 53
|
||||||
#define MTK_SYS_WINCREATE 54
|
#define MTK_SYS_WINCREATE 54
|
||||||
#define MTK_SYS_WINDESTROY 55
|
#define MTK_SYS_WINDESTROY 55
|
||||||
#define MTK_SYS_WINPRESENT 56
|
#define MTK_SYS_WINPRESENT 56
|
||||||
#define MTK_SYS_WINPOLL 57
|
#define MTK_SYS_WINPOLL 57
|
||||||
#define MTK_SYS_WINENUM 58
|
#define MTK_SYS_WINENUM 58
|
||||||
#define MTK_SYS_WINMAP 59
|
#define MTK_SYS_WINMAP 59
|
||||||
#define MTK_SYS_WINSENDEVENT 60
|
#define MTK_SYS_WINSENDEVENT 60
|
||||||
#define MTK_SYS_PROCLIST 61
|
#define MTK_SYS_PROCLIST 61
|
||||||
#define MTK_SYS_KILL 62
|
#define MTK_SYS_KILL 62
|
||||||
#define MTK_SYS_DEVLIST 63
|
#define MTK_SYS_DEVLIST 63
|
||||||
#define MTK_SYS_WINRESIZE 64
|
#define MTK_SYS_WINRESIZE 64
|
||||||
#define MTK_SYS_WINSETSCALE 65
|
#define MTK_SYS_WINSETSCALE 65
|
||||||
#define MTK_SYS_WINGETSCALE 66
|
#define MTK_SYS_WINGETSCALE 66
|
||||||
#define MTK_SYS_MEMSTATS 67
|
#define MTK_SYS_MEMSTATS 67
|
||||||
#define MTK_SYS_WINSETCURSOR 68
|
#define MTK_SYS_WINSETCURSOR 68
|
||||||
#define MTK_SYS_DISKINFO 69
|
#define MTK_SYS_DISKINFO 69
|
||||||
#define MTK_SYS_PARTLIST 70
|
#define MTK_SYS_PARTLIST 70
|
||||||
#define MTK_SYS_DISKREAD 71
|
#define MTK_SYS_DISKREAD 71
|
||||||
#define MTK_SYS_DISKWRITE 72
|
#define MTK_SYS_DISKWRITE 72
|
||||||
#define MTK_SYS_GPTINIT 73
|
#define MTK_SYS_GPTINIT 73
|
||||||
#define MTK_SYS_GPTADD 74
|
#define MTK_SYS_GPTADD 74
|
||||||
#define MTK_SYS_FSMOUNT 75
|
#define MTK_SYS_FSMOUNT 75
|
||||||
#define MTK_SYS_FSFORMAT 76
|
#define MTK_SYS_FSFORMAT 76
|
||||||
#define MTK_SYS_FDELETE 77
|
#define MTK_SYS_FDELETE 77
|
||||||
#define MTK_SYS_FMKDIR 78
|
#define MTK_SYS_FMKDIR 78
|
||||||
#define MTK_SYS_DRIVELIST 79
|
#define MTK_SYS_DRIVELIST 79
|
||||||
#define MTK_SYS_AUDIOOPEN 80
|
#define MTK_SYS_AUDIOOPEN 80
|
||||||
#define MTK_SYS_AUDIOCLOSE 81
|
#define MTK_SYS_AUDIOCLOSE 81
|
||||||
#define MTK_SYS_AUDIOWRITE 82
|
#define MTK_SYS_AUDIOWRITE 82
|
||||||
#define MTK_SYS_AUDIOCTL 83
|
#define MTK_SYS_AUDIOCTL 83
|
||||||
#define MTK_SYS_BTSCAN 84
|
#define MTK_SYS_BTSCAN 84
|
||||||
#define MTK_SYS_BTCONNECT 85
|
#define MTK_SYS_BTCONNECT 85
|
||||||
#define MTK_SYS_BTDISCONNECT 86
|
#define MTK_SYS_BTDISCONNECT 86
|
||||||
#define MTK_SYS_BTLIST 87
|
#define MTK_SYS_BTLIST 87
|
||||||
#define MTK_SYS_BTINFO 88
|
#define MTK_SYS_BTINFO 88
|
||||||
#define MTK_SYS_SUSPEND 89
|
#define MTK_SYS_SUSPEND 89
|
||||||
#define MTK_SYS_SETTZ 90
|
#define MTK_SYS_SETTZ 90
|
||||||
#define MTK_SYS_GETTZ 91
|
#define MTK_SYS_GETTZ 91
|
||||||
#define MTK_SYS_SETUSER 92
|
#define MTK_SYS_SETUSER 92
|
||||||
#define MTK_SYS_GETUSER 93
|
#define MTK_SYS_GETUSER 93
|
||||||
#define MTK_SYS_FRENAME 94
|
#define MTK_SYS_FRENAME 94
|
||||||
#define MTK_SYS_GETCWD 95
|
#define MTK_SYS_GETCWD 95
|
||||||
#define MTK_SYS_CHDIR 96
|
#define MTK_SYS_CHDIR 96
|
||||||
#define MTK_SYS_WINUNMAP 97
|
#define MTK_SYS_WINUNMAP 97
|
||||||
#define MTK_SYS_DUPHANDLE 98
|
#define MTK_SYS_DUPHANDLE 98
|
||||||
#define MTK_SYS_WAIT_HANDLE 99
|
#define MTK_SYS_WAIT_HANDLE 99
|
||||||
#define MTK_SYS_STREAM_CREATE 100
|
#define MTK_SYS_STREAM_CREATE 100
|
||||||
#define MTK_SYS_STREAM_READ 101
|
#define MTK_SYS_STREAM_READ 101
|
||||||
#define MTK_SYS_STREAM_WRITE 102
|
#define MTK_SYS_STREAM_WRITE 102
|
||||||
#define MTK_SYS_MAILBOX_CREATE 103
|
#define MTK_SYS_MAILBOX_CREATE 103
|
||||||
#define MTK_SYS_MAILBOX_SEND 104
|
#define MTK_SYS_MAILBOX_SEND 104
|
||||||
#define MTK_SYS_MAILBOX_RECV 105
|
#define MTK_SYS_MAILBOX_RECV 105
|
||||||
#define MTK_SYS_WAITSET_CREATE 106
|
#define MTK_SYS_WAITSET_CREATE 106
|
||||||
#define MTK_SYS_WAITSET_ADD 107
|
#define MTK_SYS_WAITSET_ADD 107
|
||||||
#define MTK_SYS_WAITSET_REMOVE 108
|
#define MTK_SYS_WAITSET_REMOVE 108
|
||||||
#define MTK_SYS_WAITSET_WAIT 109
|
#define MTK_SYS_WAITSET_WAIT 109
|
||||||
#define MTK_SYS_PROC_OPEN 110
|
#define MTK_SYS_PROC_OPEN 110
|
||||||
#define MTK_SYS_SURFACE_CREATE 111
|
#define MTK_SYS_SURFACE_CREATE 111
|
||||||
#define MTK_SYS_SURFACE_MAP 112
|
#define MTK_SYS_SURFACE_MAP 112
|
||||||
#define MTK_SYS_SURFACE_RESIZE 113
|
#define MTK_SYS_SURFACE_RESIZE 113
|
||||||
#define MTK_SYS_LOAD_LIB 114
|
#define MTK_SYS_LOAD_LIB 114
|
||||||
#define MTK_SYS_UNLOAD_LIB 115
|
#define MTK_SYS_UNLOAD_LIB 115
|
||||||
#define MTK_SYS_DLSYM 116
|
#define MTK_SYS_DLSYM 116
|
||||||
#define MTK_SYS_GETLIBBASE 117
|
#define MTK_SYS_GETLIBBASE 117
|
||||||
#define MTK_SYS_CRASH_REPORT 118
|
#define MTK_SYS_CRASH_REPORT 118
|
||||||
#define MTK_SYS_CLIPBOARD_SET_TEXT 119
|
#define MTK_SYS_CLIPBOARD_SET_TEXT 119
|
||||||
#define MTK_SYS_CLIPBOARD_GET_INFO 120
|
#define MTK_SYS_CLIPBOARD_GET_INFO 120
|
||||||
#define MTK_SYS_CLIPBOARD_GET_TEXT 121
|
#define MTK_SYS_CLIPBOARD_GET_TEXT 121
|
||||||
#define MTK_SYS_CLIPBOARD_CLEAR 122
|
#define MTK_SYS_CLIPBOARD_CLEAR 122
|
||||||
#define MTK_SYS_INPUT_WAIT 123
|
#define MTK_SYS_INPUT_WAIT 123
|
||||||
#define MTK_SYS_DRIVELABEL 124
|
#define MTK_SYS_DRIVELABEL 124
|
||||||
#define MTK_SYS_NETSTATUS 125
|
#define MTK_SYS_NETSTATUS 125
|
||||||
#define MTK_SYS_WINSETFLAGS 126
|
#define MTK_SYS_WINSETFLAGS 126
|
||||||
#define MTK_SYS_DRIVEKIND 127
|
#define MTK_SYS_DRIVEKIND 127
|
||||||
#define MTK_SYS_AUDIOLIST 128
|
#define MTK_SYS_AUDIOLIST 128
|
||||||
#define MTK_SYS_AUDIOWAIT 129
|
#define MTK_SYS_AUDIOWAIT 129
|
||||||
#define MTK_SYS_THREAD_SPAWN 130
|
#define MTK_SYS_THREAD_SPAWN 130
|
||||||
#define MTK_SYS_THREAD_EXIT 131
|
#define MTK_SYS_THREAD_EXIT 131
|
||||||
#define MTK_SYS_THREAD_JOIN 132
|
#define MTK_SYS_THREAD_JOIN 132
|
||||||
#define MTK_SYS_THREAD_SELF 133
|
#define MTK_SYS_THREAD_SELF 133
|
||||||
#define MTK_SYS_FS_SYNC 134
|
#define MTK_SYS_FS_SYNC 134
|
||||||
#define MTK_SYS_POWER_REQUEST 135
|
#define MTK_SYS_POWER_REQUEST 135
|
||||||
#define MTK_SYS_READDIR_AT 136
|
#define MTK_SYS_READDIR_AT 136
|
||||||
#define MTK_SYS_BTSETADDR 137
|
#define MTK_SYS_BTSETADDR 137
|
||||||
#define MTK_SYS_BTBONDS 138
|
#define MTK_SYS_BTBONDS 138
|
||||||
#define MTK_SYS_BTFORGET 139
|
#define MTK_SYS_BTFORGET 139
|
||||||
#define MTK_SYS_RESERVED_140 140
|
#define MTK_SYS_RESERVED_140 140
|
||||||
#define MTK_SYS_RESERVED_141 141
|
#define MTK_SYS_RESERVED_141 141
|
||||||
#define MTK_SYS_RESERVED_142 142
|
#define MTK_SYS_RESERVED_142 142
|
||||||
#define MTK_SYS_RESERVED_143 143
|
#define MTK_SYS_RESERVED_143 143
|
||||||
#define MTK_SYS_RESERVED_144 144
|
#define MTK_SYS_RESERVED_144 144
|
||||||
#define MTK_SYS_RESERVED_145 145
|
#define MTK_SYS_RESERVED_145 145
|
||||||
#define MTK_SYS_RESERVED_146 146
|
#define MTK_SYS_RESERVED_146 146
|
||||||
#define MTK_SYS_RESERVED_147 147
|
#define MTK_SYS_RESERVED_147 147
|
||||||
#define MTK_SYS_RESERVED_148 148
|
#define MTK_SYS_RESERVED_148 148
|
||||||
#define MTK_SYS_POWERINFO 149
|
#define MTK_SYS_POWERINFO 149
|
||||||
#define MTK_SYS_FBFLIP 150
|
#define MTK_SYS_FBFLIP 150
|
||||||
#define MTK_SYS_GETEXECPATH 151
|
#define MTK_SYS_GETEXECPATH 151
|
||||||
#define MTK_SYS_STAT 152
|
#define MTK_SYS_STAT 152
|
||||||
#define MTK_SYS_SETUNIXTIME 153
|
#define MTK_SYS_SETUNIXTIME 153
|
||||||
#define MTK_SYS_DISPLAYINFO 154
|
#define MTK_SYS_DISPLAYINFO 154
|
||||||
#define MTK_SYS_DISPLAYMODES 155
|
#define MTK_SYS_DISPLAYMODES 155
|
||||||
#define MTK_SYS_DISPLAYSETMODE 156
|
#define MTK_SYS_DISPLAYSETMODE 156
|
||||||
#define MTK_SYS_DISPLAYBRIGHTNESS 157
|
#define MTK_SYS_DISPLAYBRIGHTNESS 157
|
||||||
#define MTK_SYS_WIFI_SCAN 158
|
#define MTK_SYS_WIFI_SCAN 158
|
||||||
#define MTK_SYS_WIFI_INFO 159
|
#define MTK_SYS_WIFI_INFO 159
|
||||||
#define MTK_SYS_WIFI_CONNECT 160
|
#define MTK_SYS_WIFI_CONNECT 160
|
||||||
#define MTK_SYS_WIFI_DISCONNECT 161
|
#define MTK_SYS_WIFI_DISCONNECT 161
|
||||||
#define MTK_SYS_WIFI_SCAN_START 162
|
#define MTK_SYS_WIFI_SCAN_START 162
|
||||||
#define MTK_SYS_WIFI_RESULTS 163
|
#define MTK_SYS_WIFI_RESULTS 163
|
||||||
#define MTK_SYS_WIFI_CONNECT_ASYNC 164
|
#define MTK_SYS_WIFI_CONNECT_ASYNC 164
|
||||||
#define MTK_SYS_NETIFS 165
|
#define MTK_SYS_NETIFS 165
|
||||||
#define MTK_SYS_GETCHAR_NB 166
|
#define MTK_SYS_GETCHAR_NB 166
|
||||||
#define MTK_SYS_UTIME 167
|
#define MTK_SYS_UTIME 167
|
||||||
#define MTK_SYS_MMAP_ANON 168
|
#define MTK_SYS_MMAP_ANON 168
|
||||||
#define MTK_SYS_MUNMAP 169
|
#define MTK_SYS_MUNMAP 169
|
||||||
#define MTK_SYS_MPROTECT 170
|
#define MTK_SYS_MPROTECT 170
|
||||||
#define MTK_SYS_GETENVIRON 171
|
#define MTK_SYS_GETENVIRON 171
|
||||||
#define MTK_SYS_SETENVIRON 172
|
#define MTK_SYS_SETENVIRON 172
|
||||||
#define MTK_SYS_SPAWN_ENV 173
|
#define MTK_SYS_SPAWN_ENV 173
|
||||||
#define MTK_SYS_SETSESSION 174
|
#define MTK_SYS_SETSESSION 174
|
||||||
#define MTK_SYS_KILLSESSION 175
|
#define MTK_SYS_KILLSESSION 175
|
||||||
#define MTK_SYS_LOG_WRITE 176
|
#define MTK_SYS_LOG_WRITE 176
|
||||||
#define MTK_SYS_TERMINAL_ATTACHED 177
|
#define MTK_SYS_TERMINAL_ATTACHED 177
|
||||||
#define MTK_SYS_USB_LIST 178
|
#define MTK_SYS_USB_LIST 178
|
||||||
#define MTK_SYS_USB_CLAIM 179
|
#define MTK_SYS_USB_CLAIM 179
|
||||||
#define MTK_SYS_USB_CLOSE 180
|
#define MTK_SYS_USB_CLOSE 180
|
||||||
#define MTK_SYS_USB_CONTROL 181
|
#define MTK_SYS_USB_CONTROL 181
|
||||||
#define MTK_SYS_USB_BULK_IN_START 182
|
#define MTK_SYS_USB_BULK_IN_START 182
|
||||||
#define MTK_SYS_USB_BULK_IN_STOP 183
|
#define MTK_SYS_USB_BULK_IN_STOP 183
|
||||||
#define MTK_SYS_USB_BULK_IN_READ 184
|
#define MTK_SYS_USB_BULK_IN_READ 184
|
||||||
#define MTK_SYS_SPAWN_CAPS 185
|
#define MTK_SYS_SPAWN_CAPS 185
|
||||||
#define MTK_SYS_SPAWN_REDIR_CAPS 186
|
#define MTK_SYS_SPAWN_REDIR_CAPS 186
|
||||||
#define MTK_SYS_ALLOC_EAGER 187
|
#define MTK_SYS_ALLOC_EAGER 187
|
||||||
#define MTK_SYS_BATTERYINFO 188
|
#define MTK_SYS_BATTERYINFO 188
|
||||||
#define MTK_SYS_GETCAPS 189
|
#define MTK_SYS_GETCAPS 189
|
||||||
#define MTK_SYS_VOLINFO 190
|
#define MTK_SYS_VOLINFO 190
|
||||||
#define MTK_SYS_CONSENT_REGISTER 191
|
#define MTK_SYS_CONSENT_REGISTER 191
|
||||||
#define MTK_SYS_CONSENT_NEXT 192
|
#define MTK_SYS_CONSENT_NEXT 192
|
||||||
#define MTK_SYS_CONSENT_REPLY 193
|
#define MTK_SYS_CONSENT_REPLY 193
|
||||||
|
#define MTK_SYS_GETKEY_EX 194
|
||||||
|
#define MTK_SYS_CHILDIO_WRITEKEY_EX 195
|
||||||
|
#define MTK_SYS_FUTEX_WAIT 196
|
||||||
|
#define MTK_SYS_FUTEX_WAKE 197
|
||||||
|
#define MTK_SYS_ABORT 198
|
||||||
/* @SYSCALLS-END */
|
/* @SYSCALLS-END */
|
||||||
|
|
||||||
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
||||||
@@ -238,6 +243,8 @@ extern "C" {
|
|||||||
#define MTK_STATUS_BUSY (-12)
|
#define MTK_STATUS_BUSY (-12)
|
||||||
#define MTK_STATUS_BUFFER_TOO_SMALL (-13)
|
#define MTK_STATUS_BUFFER_TOO_SMALL (-13)
|
||||||
#define MTK_STATUS_OUT_OF_MEMORY (-14)
|
#define MTK_STATUS_OUT_OF_MEMORY (-14)
|
||||||
|
#define MTK_STATUS_TIMED_OUT (-15)
|
||||||
|
#define MTK_STATUS_WOULD_BLOCK (-16)
|
||||||
/* @STATUS-END */
|
/* @STATUS-END */
|
||||||
|
|
||||||
#define MTK_SOCK_TCP 1
|
#define MTK_SOCK_TCP 1
|
||||||
@@ -489,6 +496,21 @@ static inline void mtk_sleep_ms(unsigned long ms) {
|
|||||||
_mtk_syscall1(MTK_SYS_SLEEP_MS, (long)ms);
|
_mtk_syscall1(MTK_SYS_SLEEP_MS, (long)ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Futexes (process-private). mtk_futex_wait blocks while *addr == expected;
|
||||||
|
returns 0 when woken, MTK_STATUS_TIMED_OUT, or MTK_STATUS_WOULD_BLOCK when
|
||||||
|
the word had already changed. timeout_ns: MTK_FUTEX_WAIT_FOREVER for no
|
||||||
|
deadline, 0 to only check. mtk_futex_wake returns the number woken. */
|
||||||
|
#define MTK_FUTEX_WAIT_FOREVER (~0UL)
|
||||||
|
#define MTK_FUTEX_WAKE_ALL 0xFFFFFFFFu
|
||||||
|
|
||||||
|
static inline int mtk_futex_wait(uint32_t *addr, uint32_t expected, unsigned long timeout_ns) {
|
||||||
|
return (int)_mtk_syscall3(MTK_SYS_FUTEX_WAIT, (long)addr, (long)expected, (long)timeout_ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int mtk_futex_wake(uint32_t *addr, uint32_t count) {
|
||||||
|
return (int)_mtk_syscall2(MTK_SYS_FUTEX_WAKE, (long)addr, (long)count);
|
||||||
|
}
|
||||||
|
|
||||||
static inline int mtk_getpid(void) {
|
static inline int mtk_getpid(void) {
|
||||||
return (int)_mtk_syscall0(MTK_SYS_GETPID);
|
return (int)_mtk_syscall0(MTK_SYS_GETPID);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,27 +8,34 @@
|
|||||||
|
|
||||||
#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 {
|
||||||
char id[8];
|
char id[8];
|
||||||
char name[48];
|
char name[48];
|
||||||
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));
|
||||||
|
|
||||||
uint8_t scancodes[MAX_KEYS];
|
build_key(key, sizeof(key), "layouts.", id, ".dead_keys");
|
||||||
uint8_t base[MAX_KEYS];
|
int dead = read_codepoints(doc, key, out->dead_keys, MAX_DEAD_KEYS);
|
||||||
uint8_t shift[MAX_KEYS];
|
if (dead < 0 && doc.get_array(key)) return false; // present but malformed
|
||||||
uint8_t altgr[MAX_KEYS] = {};
|
out->dead_count = dead < 0 ? 0 : dead;
|
||||||
|
|
||||||
|
uint8_t scancodes[MAX_KEYS];
|
||||||
|
uint32_t base[MAX_KEYS];
|
||||||
|
uint32_t shift[MAX_KEYS];
|
||||||
|
uint32_t altgr[MAX_KEYS] = {};
|
||||||
|
|
||||||
build_key(key, sizeof(key), "layouts.", id, ".scancodes");
|
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,57 +329,117 @@ inline int next_enabled(const State& state, int current) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Caps Lock is derived rather than declared per key: it applies only where
|
// 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++) {
|
||||||
|
const KeyMap& mapping = layout->keys[i];
|
||||||
|
if (mapping.scancode != scancode) continue;
|
||||||
|
|
||||||
uint8_t scancode = key->scancode & 0x7F;
|
uint32_t out;
|
||||||
for (int i = 0; i < layout.key_count; i++) {
|
if (key->altgr) {
|
||||||
const KeyMap& mapping = layout.keys[i];
|
out = mapping.altgr;
|
||||||
if (mapping.scancode != scancode) continue;
|
} else {
|
||||||
|
bool upper = key->shift;
|
||||||
|
if (is_letter_pair(mapping.base, mapping.shift))
|
||||||
|
upper = key->shift != key->capslock;
|
||||||
|
out = upper ? mapping.shift : mapping.base;
|
||||||
|
}
|
||||||
|
if (out != 0) {
|
||||||
|
cp = out;
|
||||||
|
// An AltGr key that produced a character is text, not a
|
||||||
|
// shortcut. Apps gate insertion on !alt (and alt is
|
||||||
|
// LeftAlt||RightAlt), so leaving it set would silently swallow
|
||||||
|
// every AltGr character.
|
||||||
|
if (key->altgr) key->alt = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t out;
|
bool dead = layout && !key->extended && is_dead_key(*layout, cp);
|
||||||
if (key->altgr) {
|
bool shortcut = key->ctrl || (key->alt && !key->altgr);
|
||||||
out = mapping.altgr;
|
if (dead && shortcut) dead = false;
|
||||||
} else {
|
|
||||||
bool upper = key->shift;
|
if (key->pressed && !shortcut) {
|
||||||
if (is_letter_pair(mapping.base, mapping.shift))
|
if (dead) {
|
||||||
upper = key->shift != key->capslock;
|
if (state.pending_dead == cp) {
|
||||||
out = upper ? mapping.shift : mapping.base;
|
state.pending_dead = 0; // pressed twice: type the accent
|
||||||
}
|
dead = false;
|
||||||
if (out != 0) {
|
} else {
|
||||||
key->ascii = (char)out;
|
state.pending_dead = cp; // wait for the next character
|
||||||
// An AltGr key that produced a character is text, not a shortcut.
|
}
|
||||||
// Apps gate insertion on !alt (and alt is LeftAlt||RightAlt), so
|
} else if (state.pending_dead) {
|
||||||
// leaving it set would silently swallow every AltGr character.
|
if (cp == ' ') {
|
||||||
if (key->altgr) key->alt = false;
|
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 ----
|
||||||
|
|||||||
@@ -27,9 +27,14 @@ typedef enum {
|
|||||||
NoSuchPath = 11,
|
NoSuchPath = 11,
|
||||||
Busy = 12,
|
Busy = 12,
|
||||||
BufferTooSmall = 13,
|
BufferTooSmall = 13,
|
||||||
OutOfMemory = 14
|
OutOfMemory = 14,
|
||||||
|
TimedOut = 15,
|
||||||
|
WouldBlock = 16
|
||||||
} StatusCode;
|
} StatusCode;
|
||||||
|
|
||||||
|
// Highest defined StatusCode; update when adding a code above.
|
||||||
|
enum { MaxStatusCode = WouldBlock };
|
||||||
|
|
||||||
inline const char* GetStatusText(StatusCode statusCode) {
|
inline const char* GetStatusText(StatusCode statusCode) {
|
||||||
const char* statusText[] = {
|
const char* statusText[] = {
|
||||||
"Success", // 0
|
"Success", // 0
|
||||||
@@ -47,36 +52,48 @@ inline const char* GetStatusText(StatusCode statusCode) {
|
|||||||
"Busy", // 12
|
"Busy", // 12
|
||||||
"Buffer too small", // 13
|
"Buffer too small", // 13
|
||||||
"Out of memory", // 14
|
"Out of memory", // 14
|
||||||
|
"Timed out", // 15
|
||||||
|
"Operation would block", // 16
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statusCode > 0 && statusCode <= 14) {
|
if (statusCode > 0 && statusCode <= MaxStatusCode) {
|
||||||
return statusText[statusCode];
|
return statusText[statusCode];
|
||||||
} else {
|
} else {
|
||||||
return "Unknown error";
|
return "Unknown error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map a raw syscall result to a StatusCode.
|
||||||
|
inline StatusCode StatusFromResult(long long rc) {
|
||||||
|
if (rc >= 0) return Success;
|
||||||
|
if (rc < -(long long)MaxStatusCode) return GenericFailure;
|
||||||
|
return (StatusCode)(-rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared with C (libc.c), so no nullptr.
|
||||||
inline const char* GetStatusExplanationText(StatusCode statusCode) {
|
inline const char* GetStatusExplanationText(StatusCode statusCode) {
|
||||||
const char* explanationText[] = {
|
const char* explanationText[] = {
|
||||||
nullptr, // 0
|
0, // 0
|
||||||
nullptr, // 1
|
0, // 1
|
||||||
nullptr, // 2
|
0, // 2
|
||||||
nullptr, // 3
|
0, // 3
|
||||||
"The path is protected by the system and cannot be modified.", // 4
|
"The path is protected by the system and cannot be modified.", // 4
|
||||||
nullptr, // 5
|
0, // 5
|
||||||
nullptr, // 6
|
0, // 6
|
||||||
nullptr, // 7
|
0, // 7
|
||||||
nullptr, // 8
|
0, // 8
|
||||||
nullptr, // 9
|
0, // 9
|
||||||
nullptr, // 10
|
0, // 10
|
||||||
nullptr, // 11
|
0, // 11
|
||||||
nullptr, // 12
|
0, // 12
|
||||||
"The buffer supplied by the caller is too small.", // 13
|
"The buffer supplied by the caller is too small.", // 13
|
||||||
nullptr, // 14
|
0, // 14
|
||||||
|
0, // 15
|
||||||
|
0, // 16
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statusCode > 0 && statusCode <= 14) {
|
if (statusCode > 0 && statusCode <= MaxStatusCode) {
|
||||||
if (explanationText[statusCode] != nullptr) {
|
if (explanationText[statusCode] != 0) {
|
||||||
return explanationText[statusCode];
|
return explanationText[statusCode];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,6 +101,33 @@ inline const char* GetStatusExplanationText(StatusCode statusCode) {
|
|||||||
return GetStatusText(statusCode);
|
return GetStatusText(statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process termination codes: what waitpid returns when the kernel ended the
|
||||||
|
// process (crash, kill, abort) instead of it calling exit. 256 + POSIX
|
||||||
|
// signal number, so normal exit codes (0..255) can never collide.
|
||||||
|
// Analogous to kernel/src/Common/TerminationCodes.hpp; same values.
|
||||||
|
typedef enum {
|
||||||
|
TerminationBase = 256,
|
||||||
|
TerminatedIllegalInstruction = TerminationBase + 4, // SIGILL
|
||||||
|
TerminatedBreakpoint = TerminationBase + 5, // SIGTRAP
|
||||||
|
TerminatedAborted = TerminationBase + 6, // SIGABRT
|
||||||
|
TerminatedArithmeticError = TerminationBase + 8, // SIGFPE
|
||||||
|
TerminatedKilled = TerminationBase + 9, // SIGKILL
|
||||||
|
TerminatedAccessViolation = TerminationBase + 11 // SIGSEGV
|
||||||
|
} TerminationCode;
|
||||||
|
|
||||||
|
// Shared with C (libc.c), so no nullptr. Returns 0 for an unknown code.
|
||||||
|
inline const char* GetTerminationText(int exitCode) {
|
||||||
|
switch (exitCode) {
|
||||||
|
case TerminatedIllegalInstruction: return "Illegal instruction";
|
||||||
|
case TerminatedBreakpoint: return "Breakpoint";
|
||||||
|
case TerminatedAborted: return "Aborted";
|
||||||
|
case TerminatedArithmeticError: return "Arithmetic error";
|
||||||
|
case TerminatedKilled: return "Killed";
|
||||||
|
case TerminatedAccessViolation: return "Access violation";
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Api/Syscall.hpp>
|
#include <Api/Syscall.hpp>
|
||||||
#include <montauk/statuscodes.h>
|
#include <montauk/statuscodes.h>
|
||||||
|
#include <montauk/utf8.h>
|
||||||
|
|
||||||
namespace montauk {
|
namespace montauk {
|
||||||
|
|
||||||
@@ -222,7 +223,23 @@ namespace montauk {
|
|||||||
|
|
||||||
// Keyboard
|
// Keyboard
|
||||||
inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); }
|
inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); }
|
||||||
inline void getkey(montauk::abi::KeyEvent* out) { syscall1(montauk::abi::SYS_GETKEY, (uint64_t)out); }
|
// Scancode to use when checking for navigation keys (arrows, Home/End,
|
||||||
|
// PgUp/PgDn, Insert/Delete). The keypad shares those scancodes with its
|
||||||
|
// digit and '.' keys, so a keypad key that types a character reports 0
|
||||||
|
// here: numpad 4 is text, not Left. Compare against this, not .scancode.
|
||||||
|
inline uint8_t nav_scancode(const montauk::abi::KeyEvent& key) {
|
||||||
|
uint8_t sc = key.scancode;
|
||||||
|
if (!key.extended && (sc & 0x7F) >= 0x47 && (sc & 0x7F) <= 0x53 &&
|
||||||
|
montauk::utf8::key_codepoint(key) != 0)
|
||||||
|
return 0;
|
||||||
|
return sc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fills the full event, including the Unicode codepoint the key types.
|
||||||
|
inline void getkey(montauk::abi::KeyEvent* out) {
|
||||||
|
syscall2(montauk::abi::SYS_GETKEY_EX, (uint64_t)out, sizeof(*out));
|
||||||
|
}
|
||||||
|
// Text keys arrive as UTF-8, one byte per call.
|
||||||
inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); }
|
inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); }
|
||||||
// Returns 0 rather than blocking when no character is pending.
|
// Returns 0 rather than blocking when no character is pending.
|
||||||
inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); }
|
inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); }
|
||||||
@@ -496,7 +513,8 @@ namespace montauk {
|
|||||||
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITE, (uint64_t)childPid, (uint64_t)data, (uint64_t)len);
|
||||||
}
|
}
|
||||||
inline int childio_writekey(int childPid, const montauk::abi::KeyEvent* key) {
|
inline int childio_writekey(int childPid, const montauk::abi::KeyEvent* key) {
|
||||||
return (int)syscall2(montauk::abi::SYS_CHILDIO_WRITEKEY, (uint64_t)childPid, (uint64_t)key);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_WRITEKEY_EX, (uint64_t)childPid,
|
||||||
|
(uint64_t)key, sizeof(*key));
|
||||||
}
|
}
|
||||||
inline int childio_settermsz(int childPid, int cols, int rows) {
|
inline int childio_settermsz(int childPid, int cols, int rows) {
|
||||||
return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
|
return (int)syscall3(montauk::abi::SYS_CHILDIO_SETTERMSZ, (uint64_t)childPid, (uint64_t)cols, (uint64_t)rows);
|
||||||
@@ -509,6 +527,11 @@ namespace montauk {
|
|||||||
inline int kill(int pid) {
|
inline int kill(int pid) {
|
||||||
return (int)syscall1(montauk::abi::SYS_KILL, (uint64_t)pid);
|
return (int)syscall1(montauk::abi::SYS_KILL, (uint64_t)pid);
|
||||||
}
|
}
|
||||||
|
// End the whole process from any thread (exit code TerminatedAborted).
|
||||||
|
[[noreturn]] inline void abort() {
|
||||||
|
syscall0(montauk::abi::SYS_ABORT);
|
||||||
|
__builtin_unreachable();
|
||||||
|
}
|
||||||
inline int setsession() {
|
inline int setsession() {
|
||||||
return (int)syscall0(montauk::abi::SYS_SETSESSION);
|
return (int)syscall0(montauk::abi::SYS_SETSESSION);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,28 @@ namespace montauk {
|
|||||||
return (int)syscall0(montauk::abi::SYS_THREAD_SELF);
|
return (int)syscall0(montauk::abi::SYS_THREAD_SELF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block while *addr == expected. Returns 0 when woken by futex_wake,
|
||||||
|
// WouldBlock if *addr had already changed, TimedOut when timeout_ns
|
||||||
|
// elapsed (FUTEX_WAIT_FOREVER = no deadline, 0 = check only, otherwise
|
||||||
|
// rounded up to the 1 ms tick). Futexes are private to the process.
|
||||||
|
// A 0 return is never spurious, but always re-check the condition.
|
||||||
|
inline int futex_wait(uint32_t* addr, uint32_t expected,
|
||||||
|
uint64_t timeout_ns = montauk::abi::FUTEX_WAIT_FOREVER) {
|
||||||
|
int64_t r = syscall3(montauk::abi::SYS_FUTEX_WAIT, (uint64_t)addr,
|
||||||
|
(uint64_t)expected, timeout_ns);
|
||||||
|
return r < 0 ? (int)-r : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wake up to `count` waiters on addr, longest-waiting first. Returns the
|
||||||
|
// number woken, or a negative status for a bad address.
|
||||||
|
inline int futex_wake(uint32_t* addr, uint32_t count = 1) {
|
||||||
|
return (int)syscall2(montauk::abi::SYS_FUTEX_WAKE, (uint64_t)addr, (uint64_t)count);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int futex_wake_all(uint32_t* addr) {
|
||||||
|
return futex_wake(addr, montauk::abi::FUTEX_WAKE_ALL);
|
||||||
|
}
|
||||||
|
|
||||||
// Lightweight mutex backed by a single atomic word + the kernel
|
// Lightweight mutex backed by a single atomic word + the kernel
|
||||||
// yield syscall. Adequate for short critical sections; a heavier
|
// yield syscall. Adequate for short critical sections; a heavier
|
||||||
// primitive can wrap mailbox_recv when blocking semantics are needed.
|
// primitive can wrap mailbox_recv when blocking semantics are needed.
|
||||||
|
|||||||
@@ -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
|
||||||
+29
-10
@@ -130,6 +130,7 @@ static inline long _mtk_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
|||||||
#define SYS_GETENVIRON 171
|
#define SYS_GETENVIRON 171
|
||||||
#define SYS_SETENVIRON 172
|
#define SYS_SETENVIRON 172
|
||||||
#define SYS_SPAWN_ENV 173
|
#define SYS_SPAWN_ENV 173
|
||||||
|
#define SYS_ABORT 198
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
errno
|
errno
|
||||||
@@ -1243,9 +1244,9 @@ void exit(int status) {
|
|||||||
__builtin_unreachable();
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ends the whole process from any thread; waitpid sees WTERMSIG == SIGABRT. */
|
||||||
void abort(void) {
|
void abort(void) {
|
||||||
_mtk_syscall1(SYS_PRINT, (long)"abort() called\n");
|
_mtk_syscall0(SYS_ABORT);
|
||||||
_mtk_syscall1(SYS_EXIT, 1);
|
|
||||||
__builtin_unreachable();
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1712,7 +1713,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 +1725,26 @@ static int _stdin_pump(int block) {
|
|||||||
|
|
||||||
if (c == '\b' || c == 127) {
|
if (c == '\b' || c == 127) {
|
||||||
if (_stdin_line_len > 0) {
|
if (_stdin_line_len > 0) {
|
||||||
|
/* Drop the whole UTF-8 character: its continuation bytes,
|
||||||
|
then the lead byte. One terminal cell either way. */
|
||||||
|
while (_stdin_line_len > 1 &&
|
||||||
|
((unsigned char)_stdin_linebuf[_stdin_line_len - 1] & 0xC0) == 0x80)
|
||||||
|
_stdin_line_len--;
|
||||||
_stdin_line_len--;
|
_stdin_line_len--;
|
||||||
_stdin_echo_char('\b');
|
_stdin_echo_char('\b');
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bytes >= 0x80 are UTF-8: the kernel encodes typed characters, and
|
||||||
|
piped input is UTF-8 already. They are stored and echoed as-is. */
|
||||||
|
if (c >= 0x80) {
|
||||||
|
if (_stdin_line_len + 2 >= sizeof(_stdin_linebuf)) continue;
|
||||||
|
_stdin_linebuf[_stdin_line_len++] = (char)c;
|
||||||
|
_stdin_echo_char((char)c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (c == '\n') {
|
if (c == '\n') {
|
||||||
_stdin_linebuf[_stdin_line_len++] = (char)c;
|
_stdin_linebuf[_stdin_line_len++] = (char)c;
|
||||||
_stdin_echo_char((char)c);
|
_stdin_echo_char((char)c);
|
||||||
@@ -2477,7 +2495,7 @@ char *getcwd(char *buf, size_t size) {
|
|||||||
}
|
}
|
||||||
int result = (int)_mtk_syscall2(SYS_GETCWD, (long)buf, (long)size);
|
int result = (int)_mtk_syscall2(SYS_GETCWD, (long)buf, (long)size);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
errno = result == BufferTooSmall ? ERANGE : EIO;
|
errno = result == -BufferTooSmall ? ERANGE : EIO;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
return buf;
|
return buf;
|
||||||
@@ -3567,10 +3585,11 @@ int utime(const char *path, const struct utimbuf *times) {
|
|||||||
times != NULL ? (long)times->modtime : 0,
|
times != NULL ? (long)times->modtime : 0,
|
||||||
times == NULL);
|
times == NULL);
|
||||||
if (result == 0) return 0;
|
if (result == 0) return 0;
|
||||||
if (result == UnsupportedOp) errno = ENOTSUP;
|
/* The kernel returns StatusCodes negated. */
|
||||||
else if (result == InvalidArgument) errno = EOVERFLOW;
|
if (result == -UnsupportedOp) errno = ENOTSUP;
|
||||||
else if (result == IoFailure) errno = EIO;
|
else if (result == -InvalidArgument) errno = EOVERFLOW;
|
||||||
else if (result == SystemIntegrityError) errno = EPERM;
|
else if (result == -IoFailure) errno = EIO;
|
||||||
|
else if (result == -SystemIntegrityError) errno = EPERM;
|
||||||
else errno = ENOENT;
|
else errno = ENOENT;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -4037,8 +4056,8 @@ pid_t waitpid(pid_t pid, int *status, int options) {
|
|||||||
}
|
}
|
||||||
long code = _mtk_syscall1(SYS_WAITPID, (long)pid);
|
long code = _mtk_syscall1(SYS_WAITPID, (long)pid);
|
||||||
if (status != NULL) {
|
if (status != NULL) {
|
||||||
if (code >= 256) {
|
if (code >= TerminationBase) {
|
||||||
*status = (int)((code - 256) & 0x7F); /* signaled */
|
*status = (int)((code - TerminationBase) & 0x7F); /* signaled */
|
||||||
} else {
|
} else {
|
||||||
*status = (int)((code & 0xFF) << 8); /* exited */
|
*status = (int)((code & 0xFF) << 8); /* exited */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Phase 1 Rust support: no_std + alloc on a stock nightly, built with
|
||||||
|
# -Zbuild-std against the custom target spec next to this directory.
|
||||||
|
# Paths below are relative to programs/rust/ (the parent of .cargo/).
|
||||||
|
|
||||||
|
[build]
|
||||||
|
target = "x86_64-unknown-montauk.json"
|
||||||
|
|
||||||
|
[unstable]
|
||||||
|
build-std = ["core", "alloc"]
|
||||||
|
json-target-spec = true
|
||||||
|
|
||||||
|
[target.x86_64-unknown-montauk]
|
||||||
|
# The montauk GCC driver supplies crt1/crti/crtbegin, -lc and libgcc, and
|
||||||
|
# links the static non-PIE ET_EXEC layout the kernel ELF loader expects.
|
||||||
|
linker = "../../toolchain/local/bin/x86_64-montauk-gcc"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
Generated
+14
@@ -0,0 +1,14 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "montauk-rt"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusthello"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"montauk-rt",
|
||||||
|
]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = ["montauk-rt", "rusthello"]
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
panic = "abort"
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
debug = false
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Rust on MontaukOS
|
||||||
|
|
||||||
|
Phase 1: `no_std` + `alloc` programs, cross-compiled from Linux with a
|
||||||
|
stock nightly toolchain and a custom target spec. No compiler fork.
|
||||||
|
|
||||||
|
## Setup (once)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none
|
||||||
|
~/.cargo/bin/rustup toolchain install nightly --component rust-src
|
||||||
|
```
|
||||||
|
|
||||||
|
`rust-toolchain.toml` selects nightly + `rust-src` for this directory.
|
||||||
|
`make programs` builds the Rust programs when that toolchain is present
|
||||||
|
and prints a notice and skips them otherwise. It never downloads a toolchain
|
||||||
|
(`RUSTUP_AUTO_INSTALL=0`).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make -C programs rust # cargo build --release, copies to bin/os/*.elf
|
||||||
|
cd programs/rust && cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `target/x86_64-unknown-montauk/release/<name>`, installed as
|
||||||
|
`programs/bin/os/<name>.elf`. Add new binaries to `RUST_PROGRAMS` in
|
||||||
|
`programs/GNUmakefile` and to the workspace `members`.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `x86_64-unknown-montauk.json` | Target spec: static non-PIE ET_EXEC, no red zone, local-exec TLS, panic=abort |
|
||||||
|
| `.cargo/config.toml` | `-Zbuild-std=core,alloc`, linker = `x86_64-montauk-gcc` |
|
||||||
|
| `montauk-rt/` | Runtime: syscalls, allocator, `print!`, argv, threads, futex `Mutex`, panic handler |
|
||||||
|
| `rusthello/` | Smoke test (`rusthello`, `rusthello panic`, `rusthello panic-thread`) |
|
||||||
|
|
||||||
|
The GCC driver links crt1/crti/crtbegin, the libc and libgcc from
|
||||||
|
`toolchain/sysroot`, the same as ports. After changing `programs/lib/libc`,
|
||||||
|
re-run `toolchain/build-montauk-toolchain.sh` so Rust programs pick it up.
|
||||||
|
|
||||||
|
Syscall numbers and status codes are generated at build time by
|
||||||
|
`montauk-rt/build.rs` from `programs/include/Api/Syscall.hpp` and
|
||||||
|
`kernel/src/Common/StatusCodes.hpp`, so there is nothing to keep in sync by hand.
|
||||||
|
|
||||||
|
## Writing a program
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
montauk_rt::entry!(main);
|
||||||
|
|
||||||
|
fn main() -> i32 {
|
||||||
|
montauk_rt::println!("hello from Rust");
|
||||||
|
0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limits (Phase 1)
|
||||||
|
|
||||||
|
- No `std`. No `std::fs`, `std::net`, `std::process` or `std::thread`; use
|
||||||
|
`montauk_rt` equivalents where they exist.
|
||||||
|
- `panic = "abort"`. A panic on any thread prints the message and kills the
|
||||||
|
whole process.
|
||||||
|
- Thread stacks are owned by the `JoinHandle`. An unjoined thread leaks its stack.
|
||||||
|
|
||||||
|
Phase 2 (a forked rustc at `~/dev/mtk/rust` with a native `target_os =
|
||||||
|
"montauk"` std) replaces the JSON spec with a built-in target.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "montauk-rt"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Minimal no_std runtime for Rust programs on MontaukOS"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Generate syscall numbers and status codes from the C++ headers so Rust
|
||||||
|
// never becomes another hand-maintained copy of the syscall table.
|
||||||
|
//
|
||||||
|
// programs/include/Api/Syscall.hpp -> pub const SYS_*: u64
|
||||||
|
// kernel/src/Common/StatusCodes.hpp -> pub enum Status (positive values,
|
||||||
|
// matching montauk/statuscodes.h)
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let repo = manifest.join("../../..");
|
||||||
|
let syscall_hpp = repo.join("programs/include/Api/Syscall.hpp");
|
||||||
|
let status_hpp = repo.join("kernel/src/Common/StatusCodes.hpp");
|
||||||
|
println!("cargo:rerun-if-changed={}", syscall_hpp.display());
|
||||||
|
println!("cargo:rerun-if-changed={}", status_hpp.display());
|
||||||
|
|
||||||
|
let mut out = String::from("// @generated by montauk-rt/build.rs - do not edit.\n\n");
|
||||||
|
|
||||||
|
// static constexpr uint64_t SYS_NAME = 123; (decimal literals only)
|
||||||
|
let syscalls = fs::read_to_string(&syscall_hpp).expect("read Syscall.hpp");
|
||||||
|
let mut count = 0;
|
||||||
|
for line in syscalls.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
let Some(rest) = line.strip_prefix("static constexpr uint64_t SYS_") else { continue };
|
||||||
|
let Some((name, value)) = rest.split_once('=') else { continue };
|
||||||
|
let value = value.split(';').next().unwrap_or("").trim();
|
||||||
|
if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push_str(&format!("pub const SYS_{}: u64 = {};\n", name.trim(), value));
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
assert!(count > 100, "parsed only {count} syscalls from {}", syscall_hpp.display());
|
||||||
|
|
||||||
|
// enum StatusCodes { Success = 0, GenericFailure = -1, ... };
|
||||||
|
let status = fs::read_to_string(&status_hpp).expect("read StatusCodes.hpp");
|
||||||
|
let body = status
|
||||||
|
.split_once("enum StatusCodes")
|
||||||
|
.and_then(|(_, rest)| rest.split_once('{'))
|
||||||
|
.and_then(|(_, rest)| rest.split_once('}'))
|
||||||
|
.map(|(body, _)| body)
|
||||||
|
.expect("enum StatusCodes not found");
|
||||||
|
out.push_str("\n/// Kernel status codes, stored positive like montauk/statuscodes.h.\n");
|
||||||
|
out.push_str("/// The kernel returns them negated; see [`Status::from_result`].\n");
|
||||||
|
out.push_str("#[derive(Clone, Copy, Debug, PartialEq, Eq)]\n#[repr(i32)]\npub enum Status {\n");
|
||||||
|
let mut variants = Vec::new();
|
||||||
|
for entry in body.split(',') {
|
||||||
|
let entry = entry.split("//").next().unwrap_or("").trim();
|
||||||
|
let Some((name, value)) = entry.split_once('=') else { continue };
|
||||||
|
let value: i32 = value.trim().parse().expect("status value");
|
||||||
|
out.push_str(&format!(" {} = {},\n", name.trim(), -value));
|
||||||
|
variants.push((name.trim().to_string(), -value));
|
||||||
|
}
|
||||||
|
out.push_str("}\n\nimpl Status {\n pub const fn from_code(code: i32) -> Status {\n match code {\n");
|
||||||
|
for (name, value) in &variants {
|
||||||
|
out.push_str(&format!(" {} => Status::{},\n", value, name));
|
||||||
|
}
|
||||||
|
out.push_str(" _ => Status::GenericFailure,\n }\n }\n}\n");
|
||||||
|
|
||||||
|
let dest = PathBuf::from(env::var("OUT_DIR").unwrap()).join("abi.rs");
|
||||||
|
fs::write(dest, out).expect("write abi.rs");
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
//! Command-line arguments, captured from crt1.o's argv by [`crate::entry!`].
|
||||||
|
|
||||||
|
use core::ffi::CStr;
|
||||||
|
use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
static ARGC: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
static ARGV: AtomicPtr<*const u8> = AtomicPtr::new(core::ptr::null_mut());
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// `argv` must point to `argc` NUL-terminated strings that live forever.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub unsafe fn init(argc: i32, argv: *const *const u8) {
|
||||||
|
ARGV.store(argv as *mut *const u8, Ordering::Relaxed);
|
||||||
|
ARGC.store(argc.max(0) as usize, Ordering::Release);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator over the program arguments; argv[0] is the executable path.
|
||||||
|
/// Arguments that are not valid UTF-8 are yielded lossily as `"?"`.
|
||||||
|
pub fn args() -> Args {
|
||||||
|
Args { next: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Args {
|
||||||
|
next: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for Args {
|
||||||
|
type Item = &'static str;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<&'static str> {
|
||||||
|
let argc = ARGC.load(Ordering::Acquire);
|
||||||
|
if self.next >= argc {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let argv = ARGV.load(Ordering::Relaxed);
|
||||||
|
// SAFETY: init() guarantees argc valid, immortal C strings.
|
||||||
|
let s = unsafe { CStr::from_ptr(*argv.add(self.next) as *const core::ffi::c_char) };
|
||||||
|
self.next += 1;
|
||||||
|
Some(s.to_str().unwrap_or("?"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let left = ARGC.load(Ordering::Acquire).saturating_sub(self.next);
|
||||||
|
(left, Some(left))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for Args {}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//! Global allocator over the Montauk libc heap (process-wide locked, so
|
||||||
|
//! safe across threads). libc malloc returns 16-byte aligned blocks; larger
|
||||||
|
//! alignments over-allocate and stash the original pointer just below the
|
||||||
|
//! aligned block.
|
||||||
|
|
||||||
|
use core::alloc::{GlobalAlloc, Layout};
|
||||||
|
use core::ptr;
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn malloc(size: usize) -> *mut u8;
|
||||||
|
fn free(ptr: *mut u8);
|
||||||
|
fn realloc(ptr: *mut u8, size: usize) -> *mut u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MALLOC_ALIGN: usize = 16;
|
||||||
|
|
||||||
|
pub struct LibcAllocator;
|
||||||
|
|
||||||
|
unsafe impl GlobalAlloc for LibcAllocator {
|
||||||
|
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||||
|
unsafe {
|
||||||
|
if layout.align() <= MALLOC_ALIGN {
|
||||||
|
return malloc(layout.size().max(1));
|
||||||
|
}
|
||||||
|
let Some(total) = layout.size().checked_add(layout.align() + size_of::<usize>()) else {
|
||||||
|
return ptr::null_mut();
|
||||||
|
};
|
||||||
|
let raw = malloc(total);
|
||||||
|
if raw.is_null() {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
let start = raw as usize + size_of::<usize>();
|
||||||
|
let aligned = (start + layout.align() - 1) & !(layout.align() - 1);
|
||||||
|
*((aligned - size_of::<usize>()) as *mut usize) = raw as usize;
|
||||||
|
aligned as *mut u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn dealloc(&self, p: *mut u8, layout: Layout) {
|
||||||
|
unsafe {
|
||||||
|
if layout.align() <= MALLOC_ALIGN {
|
||||||
|
free(p);
|
||||||
|
} else {
|
||||||
|
free(*((p as usize - size_of::<usize>()) as *const usize) as *mut u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn realloc(&self, p: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
|
||||||
|
unsafe {
|
||||||
|
if layout.align() <= MALLOC_ALIGN {
|
||||||
|
return realloc(p, new_size.max(1));
|
||||||
|
}
|
||||||
|
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
|
||||||
|
let q = self.alloc(new_layout);
|
||||||
|
if !q.is_null() {
|
||||||
|
ptr::copy_nonoverlapping(p, q, layout.size().min(new_size));
|
||||||
|
self.dealloc(p, layout);
|
||||||
|
}
|
||||||
|
q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOCATOR: LibcAllocator = LibcAllocator;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//! Terminal output. `print!`/`println!` format into a stack buffer and hand
|
||||||
|
//! it to SYS_PRINT in NUL-terminated chunks, so one call is one (or a few)
|
||||||
|
//! syscalls rather than libc write()'s one SYS_PUTCHAR per byte.
|
||||||
|
|
||||||
|
use core::fmt::{self, Write};
|
||||||
|
|
||||||
|
use crate::sys;
|
||||||
|
|
||||||
|
const CHUNK: usize = 256;
|
||||||
|
|
||||||
|
/// Buffered writer to the process's terminal (stdout).
|
||||||
|
pub struct Stdout {
|
||||||
|
buf: [u8; CHUNK],
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stdout {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Stdout { buf: [0; CHUNK], len: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_bytes(&mut self, bytes: &[u8]) {
|
||||||
|
for &b in bytes {
|
||||||
|
if b == 0 {
|
||||||
|
// SYS_PRINT stops at NUL; send it on its own.
|
||||||
|
self.flush();
|
||||||
|
unsafe { sys::syscall1(sys::SYS_PUTCHAR, 0) };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.len == CHUNK - 1 {
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
self.buf[self.len] = b;
|
||||||
|
self.len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(&mut self) {
|
||||||
|
if self.len == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.buf[self.len] = 0;
|
||||||
|
unsafe { sys::syscall1(sys::SYS_PRINT, self.buf.as_ptr() as u64) };
|
||||||
|
self.len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Stdout {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for Stdout {
|
||||||
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||||
|
self.write_bytes(s.as_bytes());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Stdout {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn _print(args: fmt::Arguments) {
|
||||||
|
let mut out = Stdout::new();
|
||||||
|
let _ = out.write_fmt(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! print {
|
||||||
|
($($arg:tt)*) => { $crate::io::_print(format_args!($($arg)*)) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! println {
|
||||||
|
() => { $crate::print!("\n") };
|
||||||
|
($($arg:tt)*) => { $crate::io::_print(format_args!("{}\n", format_args!($($arg)*))) };
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//! Minimal `no_std` runtime for Rust programs on MontaukOS.
|
||||||
|
//!
|
||||||
|
//! Phase 1 of Rust support: `core` + `alloc` on a stock nightly with a
|
||||||
|
//! custom target spec. Programs link against the Montauk libc through the
|
||||||
|
//! x86_64-montauk GCC driver, so crt1.o owns `_start` (argv, environment,
|
||||||
|
//! .init_array) and calls the `main` that [`entry!`] defines.
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! #![no_std]
|
||||||
|
//! #![no_main]
|
||||||
|
//! montauk_rt::entry!(main);
|
||||||
|
//! fn main() -> i32 { montauk_rt::println!("hi"); 0 }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
pub mod sys;
|
||||||
|
pub mod io;
|
||||||
|
pub mod heap;
|
||||||
|
pub mod env;
|
||||||
|
pub mod process;
|
||||||
|
pub mod sync;
|
||||||
|
pub mod thread;
|
||||||
|
pub mod time;
|
||||||
|
|
||||||
|
mod panic;
|
||||||
|
|
||||||
|
pub use sys::Status;
|
||||||
|
|
||||||
|
/// Define the C `main` that crt1.o calls, forwarding to `fn() -> i32`.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! entry {
|
||||||
|
($main:path) => {
|
||||||
|
const _: () = {
|
||||||
|
// Exported as the C symbol `main`; the Rust name stays free so
|
||||||
|
// the program can call its own entry function `main`.
|
||||||
|
#[unsafe(export_name = "main")]
|
||||||
|
extern "C" fn __montauk_rt_main(argc: i32, argv: *const *const u8) -> i32 {
|
||||||
|
// SAFETY: crt1.o passes its static argv table, which lives
|
||||||
|
// for the whole program and is never modified after startup.
|
||||||
|
unsafe { $crate::env::init(argc, argv) };
|
||||||
|
let f: fn() -> i32 = $main;
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use core::fmt::Write;
|
||||||
|
use core::panic::PanicInfo;
|
||||||
|
|
||||||
|
#[panic_handler]
|
||||||
|
fn panic(info: &PanicInfo) -> ! {
|
||||||
|
let mut out = crate::io::Stdout::new();
|
||||||
|
let _ = write!(out, "\nthread {} panicked", crate::thread::current_id());
|
||||||
|
if let Some(loc) = info.location() {
|
||||||
|
let _ = write!(out, " at {}:{}:{}", loc.file(), loc.line(), loc.column());
|
||||||
|
}
|
||||||
|
let _ = writeln!(out, ":\n{}", info.message());
|
||||||
|
out.flush();
|
||||||
|
crate::process::abort()
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! Process control.
|
||||||
|
|
||||||
|
use crate::sys;
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
// libc exit(): runs atexit handlers (and .fini_array), then SYS_EXIT.
|
||||||
|
#[link_name = "exit"]
|
||||||
|
fn libc_exit(code: i32) -> !;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exit the process. From a sibling thread this ends only that thread
|
||||||
|
/// (kernel semantics); use [`abort`] to take the whole process down.
|
||||||
|
pub fn exit(code: i32) -> ! {
|
||||||
|
unsafe { libc_exit(code) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id() -> u32 {
|
||||||
|
unsafe { sys::syscall0(sys::SYS_GETPID) as u32 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminate the whole process from any thread. Waiters see exit code
|
||||||
|
/// TerminatedAborted (256 + SIGABRT).
|
||||||
|
pub fn abort() -> ! {
|
||||||
|
unsafe {
|
||||||
|
sys::syscall0(sys::SYS_ABORT);
|
||||||
|
// Not reached. No panic here: the panic handler calls abort().
|
||||||
|
loop {
|
||||||
|
sys::syscall0(sys::SYS_YIELD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
//! Futex-backed synchronization (SYS_FUTEX_WAIT / SYS_FUTEX_WAKE).
|
||||||
|
|
||||||
|
use core::cell::UnsafeCell;
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
|
use core::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
use crate::sys::{self, Status};
|
||||||
|
|
||||||
|
/// Block while `*word == expected`. `Ok(())` means woken by [`futex_wake`];
|
||||||
|
/// `Err(WouldBlock)` means the word had already changed; `Err(TimedOut)`
|
||||||
|
/// means the deadline passed. Always re-check your condition afterwards.
|
||||||
|
pub fn futex_wait(word: &AtomicU32, expected: u32, timeout: Option<Duration>) -> Result<(), Status> {
|
||||||
|
let ns = match timeout {
|
||||||
|
None => sys::FUTEX_WAIT_FOREVER,
|
||||||
|
Some(d) => d.as_nanos().min((sys::FUTEX_WAIT_FOREVER - 1) as u128) as u64,
|
||||||
|
};
|
||||||
|
let rc = unsafe {
|
||||||
|
sys::syscall3(sys::SYS_FUTEX_WAIT, word.as_ptr() as u64, expected as u64, ns)
|
||||||
|
};
|
||||||
|
Status::from_result(rc).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wake up to `count` waiters on `word`, longest-waiting first.
|
||||||
|
/// Returns how many were woken.
|
||||||
|
pub fn futex_wake(word: &AtomicU32, count: u32) -> u32 {
|
||||||
|
let rc = unsafe { sys::syscall2(sys::SYS_FUTEX_WAKE, word.as_ptr() as u64, count as u64) };
|
||||||
|
Status::from_result(rc).unwrap_or(0) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
// Mutex: Drepper's three-state futex mutex ("Futexes Are Tricky", #3).
|
||||||
|
// 0 = unlocked, 1 = locked, 2 = locked with possible waiters.
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
pub struct Mutex<T: ?Sized> {
|
||||||
|
state: AtomicU32,
|
||||||
|
data: UnsafeCell<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
|
||||||
|
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
|
||||||
|
|
||||||
|
impl<T> Mutex<T> {
|
||||||
|
pub const fn new(value: T) -> Self {
|
||||||
|
Mutex { state: AtomicU32::new(0), data: UnsafeCell::new(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> T {
|
||||||
|
self.data.into_inner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> Mutex<T> {
|
||||||
|
pub fn lock(&self) -> MutexGuard<'_, T> {
|
||||||
|
if self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_err() {
|
||||||
|
self.lock_contended();
|
||||||
|
}
|
||||||
|
MutexGuard { mutex: self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
|
||||||
|
self.state
|
||||||
|
.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
|
||||||
|
.ok()
|
||||||
|
.map(|_| MutexGuard { mutex: self })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cold]
|
||||||
|
fn lock_contended(&self) {
|
||||||
|
// Brief spin first: most critical sections are short.
|
||||||
|
for _ in 0..100 {
|
||||||
|
if self.state.load(Ordering::Relaxed) == 0
|
||||||
|
&& self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_ok()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
core::hint::spin_loop();
|
||||||
|
}
|
||||||
|
while self.state.swap(2, Ordering::Acquire) != 0 {
|
||||||
|
let _ = futex_wait(&self.state, 2, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock(&self) {
|
||||||
|
if self.state.swap(0, Ordering::Release) == 2 {
|
||||||
|
futex_wake(&self.state, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self) -> &mut T {
|
||||||
|
self.data.get_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Default> Default for Mutex<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Mutex::new(T::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MutexGuard<'a, T: ?Sized> {
|
||||||
|
mutex: &'a Mutex<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<T: ?Sized + Sync> Sync for MutexGuard<'_, T> {}
|
||||||
|
|
||||||
|
impl<T: ?Sized> Deref for MutexGuard<'_, T> {
|
||||||
|
type Target = T;
|
||||||
|
fn deref(&self) -> &T {
|
||||||
|
unsafe { &*self.mutex.data.get() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut T {
|
||||||
|
unsafe { &mut *self.mutex.data.get() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.mutex.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! Raw syscall interface. Numbers and status codes are generated from
|
||||||
|
//! programs/include/Api/Syscall.hpp and kernel/src/Common/StatusCodes.hpp.
|
||||||
|
|
||||||
|
use core::arch::asm;
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/abi.rs"));
|
||||||
|
|
||||||
|
/// `SYS_FUTEX_WAIT` timeout meaning "no deadline".
|
||||||
|
pub const FUTEX_WAIT_FOREVER: u64 = !0;
|
||||||
|
/// `SYS_FUTEX_WAKE` count meaning "every waiter".
|
||||||
|
pub const FUTEX_WAKE_ALL: u32 = u32::MAX;
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
/// Convert a raw syscall result: negative values are negated status
|
||||||
|
/// codes, everything else (handles, counts, sizes) is success.
|
||||||
|
pub fn from_result(rc: i64) -> Result<u64, Status> {
|
||||||
|
if rc >= 0 {
|
||||||
|
Ok(rc as u64)
|
||||||
|
} else if rc < i32::MIN as i64 {
|
||||||
|
Err(Status::GenericFailure)
|
||||||
|
} else {
|
||||||
|
Err(Status::from_code(-(rc as i32)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The kernel's SYSCALL path clobbers rcx/r11 (return RIP/RFLAGS) and, like
|
||||||
|
// the C++ wrappers in montauk/syscall.h, is treated as clobbering every
|
||||||
|
// argument register.
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub unsafe fn syscall0(nr: u64) -> i64 {
|
||||||
|
let ret: i64;
|
||||||
|
unsafe {
|
||||||
|
asm!("syscall", inlateout("rax") nr as i64 => ret,
|
||||||
|
out("rcx") _, out("r11") _, out("rdi") _, out("rsi") _, out("rdx") _,
|
||||||
|
out("r8") _, out("r9") _, out("r10") _, options(nostack));
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub unsafe fn syscall1(nr: u64, a1: u64) -> i64 {
|
||||||
|
let ret: i64;
|
||||||
|
unsafe {
|
||||||
|
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
|
||||||
|
out("rcx") _, out("r11") _, out("rsi") _, out("rdx") _,
|
||||||
|
out("r8") _, out("r9") _, out("r10") _, options(nostack));
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub unsafe fn syscall2(nr: u64, a1: u64, a2: u64) -> i64 {
|
||||||
|
let ret: i64;
|
||||||
|
unsafe {
|
||||||
|
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
|
||||||
|
inlateout("rsi") a2 => _,
|
||||||
|
out("rcx") _, out("r11") _, out("rdx") _,
|
||||||
|
out("r8") _, out("r9") _, out("r10") _, options(nostack));
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub unsafe fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {
|
||||||
|
let ret: i64;
|
||||||
|
unsafe {
|
||||||
|
asm!("syscall", inlateout("rax") nr as i64 => ret, inlateout("rdi") a1 => _,
|
||||||
|
inlateout("rsi") a2 => _, inlateout("rdx") a3 => _,
|
||||||
|
out("rcx") _, out("r11") _,
|
||||||
|
out("r8") _, out("r9") _, out("r10") _, options(nostack));
|
||||||
|
}
|
||||||
|
ret
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
//! Threads (SYS_THREAD_SPAWN / SYS_THREAD_JOIN).
|
||||||
|
//!
|
||||||
|
//! The kernel uses the caller's stack verbatim and delivers one argument in
|
||||||
|
//! %rdi. A thread cannot free the stack it runs on, so the [`JoinHandle`]
|
||||||
|
//! owns it and frees it after a successful join; dropping an unjoined
|
||||||
|
//! handle leaks the stack (the thread may still be running on it).
|
||||||
|
|
||||||
|
use alloc::alloc::{alloc, dealloc, Layout};
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
|
||||||
|
use crate::sys::{self, Status};
|
||||||
|
|
||||||
|
pub const DEFAULT_STACK_SIZE: usize = 256 * 1024;
|
||||||
|
const STACK_ALIGN: usize = 16;
|
||||||
|
|
||||||
|
pub struct JoinHandle {
|
||||||
|
tid: u32,
|
||||||
|
stack: *mut u8,
|
||||||
|
stack_layout: Layout,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for JoinHandle {}
|
||||||
|
|
||||||
|
impl JoinHandle {
|
||||||
|
pub fn id(&self) -> u32 {
|
||||||
|
self.tid
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for the thread to finish and return its exit code.
|
||||||
|
pub fn join(self) -> Result<i32, Status> {
|
||||||
|
let mut code: i32 = 0;
|
||||||
|
let rc = unsafe {
|
||||||
|
sys::syscall2(sys::SYS_THREAD_JOIN, self.tid as u64, &mut code as *mut i32 as u64)
|
||||||
|
};
|
||||||
|
// The kernel reports a bad tid as -1 (GenericFailure).
|
||||||
|
Status::from_result(rc)?;
|
||||||
|
unsafe { dealloc(self.stack, self.stack_layout) };
|
||||||
|
Ok(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn a thread running `f`; its return value becomes the exit code.
|
||||||
|
pub fn spawn<F>(f: F) -> Result<JoinHandle, Status>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> i32 + Send + 'static,
|
||||||
|
{
|
||||||
|
spawn_with_stack(DEFAULT_STACK_SIZE, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_with_stack<F>(stack_size: usize, f: F) -> Result<JoinHandle, Status>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> i32 + Send + 'static,
|
||||||
|
{
|
||||||
|
let stack_layout = Layout::from_size_align(stack_size.max(4096), STACK_ALIGN)
|
||||||
|
.map_err(|_| Status::InvalidArgument)?;
|
||||||
|
let stack = unsafe { alloc(stack_layout) };
|
||||||
|
if stack.is_null() {
|
||||||
|
return Err(Status::OutOfMemory);
|
||||||
|
}
|
||||||
|
let closure = Box::into_raw(Box::new(f));
|
||||||
|
let top = (stack as usize + stack_layout.size()) & !(STACK_ALIGN - 1);
|
||||||
|
|
||||||
|
let rc = unsafe {
|
||||||
|
sys::syscall3(sys::SYS_THREAD_SPAWN, trampoline::<F> as *const () as u64, closure as u64, top as u64)
|
||||||
|
};
|
||||||
|
match Status::from_result(rc) {
|
||||||
|
Ok(tid) => Ok(JoinHandle { tid: tid as u32, stack, stack_layout }),
|
||||||
|
Err(e) => unsafe {
|
||||||
|
drop(Box::from_raw(closure));
|
||||||
|
dealloc(stack, stack_layout);
|
||||||
|
Err(e)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn trampoline<F>(arg: *mut F) -> !
|
||||||
|
where
|
||||||
|
F: FnOnce() -> i32,
|
||||||
|
{
|
||||||
|
let f = unsafe { Box::from_raw(arg) };
|
||||||
|
let code = f();
|
||||||
|
unsafe { sys::syscall1(sys::SYS_THREAD_EXIT, code as u32 as u64) };
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The calling thread's id (equals the process id on the main thread).
|
||||||
|
pub fn current_id() -> u32 {
|
||||||
|
unsafe { sys::syscall0(sys::SYS_THREAD_SELF) as u32 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn yield_now() {
|
||||||
|
unsafe { sys::syscall0(sys::SYS_YIELD) };
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//! Time and sleeping.
|
||||||
|
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
use crate::sys;
|
||||||
|
|
||||||
|
/// Milliseconds since boot (monotonic).
|
||||||
|
pub fn uptime_ms() -> u64 {
|
||||||
|
unsafe { sys::syscall0(sys::SYS_GETMILLISECONDS) as u64 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sleep for at least `d`, rounded up to the 1 ms scheduler tick.
|
||||||
|
pub fn sleep(d: Duration) {
|
||||||
|
let ms = d.as_millis().min(u64::MAX as u128) as u64 + u64::from(d.subsec_nanos() % 1_000_000 != 0);
|
||||||
|
if ms > 0 {
|
||||||
|
unsafe { sys::syscall1(sys::SYS_SLEEP_MS, ms) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
|
components = ["rust-src"]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "rusthello"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
montauk-rt = { path = "../montauk-rt" }
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
//! rusthello - Phase 1 Rust smoke test for MontaukOS.
|
||||||
|
//!
|
||||||
|
//! Exercises alloc collections, formatting, argv, a #[thread_local]
|
||||||
|
//! (PT_TLS + per-thread %fs blocks), threads, and the futex mutex.
|
||||||
|
//! `rusthello panic` / `rusthello panic-thread` check that a panic on
|
||||||
|
//! either thread takes the whole process down; `rusthello fault-thread`
|
||||||
|
//! checks the same for a page fault on a worker thread.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
#![feature(thread_local)]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use alloc::collections::BTreeMap;
|
||||||
|
use alloc::format;
|
||||||
|
use alloc::string::String;
|
||||||
|
use alloc::sync::Arc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use core::cell::Cell;
|
||||||
|
use core::time::Duration;
|
||||||
|
|
||||||
|
use montauk_rt::sync::Mutex;
|
||||||
|
use montauk_rt::{entry, println, thread, time};
|
||||||
|
|
||||||
|
entry!(main);
|
||||||
|
|
||||||
|
static mut FAILURES: u32 = 0;
|
||||||
|
|
||||||
|
fn check(ok: bool, name: &str) {
|
||||||
|
println!("{} {}", if ok { "PASS" } else { "FAIL" }, name);
|
||||||
|
if !ok {
|
||||||
|
unsafe { FAILURES += 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[thread_local]
|
||||||
|
static TLS_COUNTER: Cell<u64> = Cell::new(7);
|
||||||
|
|
||||||
|
const THREADS: usize = 4;
|
||||||
|
const ITERS: u64 = 20_000;
|
||||||
|
|
||||||
|
fn main() -> i32 {
|
||||||
|
println!("rusthello: Rust on MontaukOS (no_std + alloc)");
|
||||||
|
|
||||||
|
let args: Vec<&str> = montauk_rt::env::args().collect();
|
||||||
|
println!("argv = {:?}", args);
|
||||||
|
match args.get(1).copied() {
|
||||||
|
Some("panic") => panic!("requested panic on the main thread"),
|
||||||
|
Some("fault-thread") => {
|
||||||
|
// A CPU fault (not a panic) on a worker must still kill the
|
||||||
|
// whole process, with the SIGSEGV exit code (267).
|
||||||
|
let h = thread::spawn(|| {
|
||||||
|
unsafe { core::ptr::write_volatile(8 as *mut u64, 0xdead) };
|
||||||
|
0
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let _ = h.join();
|
||||||
|
thread::yield_now();
|
||||||
|
println!("FAIL process survived a worker-thread fault");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Some("panic-thread") => {
|
||||||
|
let h = thread::spawn(|| panic!("requested panic on a worker thread")).unwrap();
|
||||||
|
let _ = h.join();
|
||||||
|
println!("FAIL process survived a worker-thread panic");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- alloc ---
|
||||||
|
let mut v: Vec<u64> = (1..=100).collect();
|
||||||
|
v.retain(|x| x % 3 == 0);
|
||||||
|
check(v.len() == 33 && v.iter().sum::<u64>() == 1683, "Vec collect/retain/sum");
|
||||||
|
|
||||||
|
let mut s = String::new();
|
||||||
|
for word in ["Montauk", "speaks", "Rust"] {
|
||||||
|
if !s.is_empty() {
|
||||||
|
s.push(' ');
|
||||||
|
}
|
||||||
|
s.push_str(word);
|
||||||
|
}
|
||||||
|
check(s == "Montauk speaks Rust", "String building");
|
||||||
|
check(format!("{:>6.2}|{:#x}|{:?}", 3.14159f64, 255, Some("x")) == " 3.14|0xff|Some(\"x\")",
|
||||||
|
"format! with floats, hex, Debug");
|
||||||
|
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
for (i, w) in s.split(' ').enumerate() {
|
||||||
|
map.insert(w, i);
|
||||||
|
}
|
||||||
|
check(map.get("Rust") == Some(&2) && map.len() == 3, "BTreeMap");
|
||||||
|
|
||||||
|
let big: Vec<u8> = alloc::vec![0xA5; 1 << 20];
|
||||||
|
check(big.iter().all(|&b| b == 0xA5), "1 MiB allocation");
|
||||||
|
|
||||||
|
#[repr(align(4096))]
|
||||||
|
struct PageAligned([u8; 64]);
|
||||||
|
let page = alloc::boxed::Box::new(PageAligned([1; 64]));
|
||||||
|
check((&*page as *const PageAligned as usize) % 4096 == 0 && page.0[63] == 1, "over-aligned Box (4096)");
|
||||||
|
|
||||||
|
// --- thread-local storage ---
|
||||||
|
check(TLS_COUNTER.get() == 7, "#[thread_local] initial value on main thread");
|
||||||
|
TLS_COUNTER.set(1000);
|
||||||
|
let tls_seen = thread::spawn(|| {
|
||||||
|
let initial = TLS_COUNTER.get();
|
||||||
|
TLS_COUNTER.set(55);
|
||||||
|
initial as i32
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.join();
|
||||||
|
check(tls_seen == Ok(7), "worker thread gets a fresh TLS block");
|
||||||
|
check(TLS_COUNTER.get() == 1000, "main thread TLS untouched by worker");
|
||||||
|
|
||||||
|
// --- threads + futex mutex ---
|
||||||
|
let counter = Arc::new(Mutex::new(0u64));
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
for t in 0..THREADS {
|
||||||
|
let counter = counter.clone();
|
||||||
|
handles.push(
|
||||||
|
thread::spawn(move || {
|
||||||
|
for i in 0..ITERS {
|
||||||
|
let mut g = counter.lock();
|
||||||
|
let v = *g;
|
||||||
|
if i % 512 == 0 {
|
||||||
|
thread::yield_now(); // widen the race window under the lock
|
||||||
|
}
|
||||||
|
*g = v + 1;
|
||||||
|
}
|
||||||
|
t as i32 + 10
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let codes: Vec<_> = handles.into_iter().map(|h| h.join()).collect();
|
||||||
|
check(codes.iter().enumerate().all(|(t, c)| *c == Ok(t as i32 + 10)), "thread exit codes via join");
|
||||||
|
check(*counter.lock() == THREADS as u64 * ITERS, "futex Mutex keeps the counter exact");
|
||||||
|
|
||||||
|
// --- time ---
|
||||||
|
let t0 = time::uptime_ms();
|
||||||
|
time::sleep(Duration::from_millis(25));
|
||||||
|
let dt = time::uptime_ms() - t0;
|
||||||
|
check((25..1000).contains(&dt), "sleep(25 ms)");
|
||||||
|
|
||||||
|
let failures = unsafe { FAILURES };
|
||||||
|
println!("rusthello: {}", if failures == 0 { "all passed" } else { "FAILURES" });
|
||||||
|
if failures == 0 { 0 } else { 1 }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"arch": "x86_64",
|
||||||
|
"cpu": "x86-64",
|
||||||
|
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
|
||||||
|
"llvm-target": "x86_64-unknown-none-elf",
|
||||||
|
"os": "montauk",
|
||||||
|
"vendor": "unknown",
|
||||||
|
"target-pointer-width": 64,
|
||||||
|
"max-atomic-width": 64,
|
||||||
|
"executables": true,
|
||||||
|
"linker-flavor": "gnu-cc",
|
||||||
|
"linker": "x86_64-montauk-gcc",
|
||||||
|
"no-default-libraries": false,
|
||||||
|
"crt-objects-fallback": "false",
|
||||||
|
"relocation-model": "static",
|
||||||
|
"code-model": "small",
|
||||||
|
"position-independent-executables": false,
|
||||||
|
"static-position-independent-executables": false,
|
||||||
|
"relro-level": "off",
|
||||||
|
"disable-redzone": true,
|
||||||
|
"has-thread-local": true,
|
||||||
|
"tls-model": "local-exec",
|
||||||
|
"panic-strategy": "abort",
|
||||||
|
"plt-by-default": false,
|
||||||
|
"stack-probes": { "kind": "inline" },
|
||||||
|
"metadata": {
|
||||||
|
"description": "MontaukOS userspace (static ET_EXEC, Montauk libc)",
|
||||||
|
"host_tools": false,
|
||||||
|
"std": false,
|
||||||
|
"tier": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ binary = "audio.elf"
|
|||||||
icon = "pavucontrol.svg"
|
icon = "pavucontrol.svg"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
category = "System"
|
category = "Hardware"
|
||||||
visible = true
|
visible = 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] = '.';
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ binary = "bluetooth.elf"
|
|||||||
icon = "bluetooth.svg"
|
icon = "bluetooth.svg"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
category = "System"
|
category = "Hardware"
|
||||||
visible = true
|
visible = true
|
||||||
@@ -4,7 +4,7 @@ binary = "calculator.elf"
|
|||||||
icon = "accessories-calculator.svg"
|
icon = "accessories-calculator.svg"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
category = "Applications"
|
category = "Utilities"
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
[launch]
|
[launch]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ binary = "charmap.elf"
|
|||||||
icon = "accessories-character-map.svg"
|
icon = "accessories-character-map.svg"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
category = "Applications"
|
category = "Utilities"
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
[launch]
|
[launch]
|
||||||
|
|||||||
@@ -306,12 +306,12 @@ extern "C" void _start() {
|
|||||||
|
|
||||||
if (ev.type == 0 && ev.key.pressed) {
|
if (ev.type == 0 && ev.key.pressed) {
|
||||||
if (ev.key.scancode == 0x01) break; // Escape
|
if (ev.key.scancode == 0x01) break; // Escape
|
||||||
if (ev.key.scancode == 0x51) {
|
if (montauk::nav_scancode(ev.key) == 0x51) {
|
||||||
// Page Down: scroll down
|
// Page Down: scroll down
|
||||||
g_scroll += 60;
|
g_scroll += 60;
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
if (ev.key.scancode == 0x49) {
|
if (montauk::nav_scancode(ev.key) == 0x49) {
|
||||||
// Page Up: scroll up
|
// Page Up: scroll up
|
||||||
g_scroll -= 60;
|
g_scroll -= 60;
|
||||||
if (g_scroll < 0) g_scroll = 0;
|
if (g_scroll < 0) g_scroll = 0;
|
||||||
|
|||||||
@@ -351,36 +351,21 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
} else if (key.scancode == 0x01) {
|
} else if (key.scancode == 0x01) {
|
||||||
filemanager_cancel_pathbar(fm);
|
filemanager_cancel_pathbar(fm);
|
||||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (fm->pathbar_cursor > 0) {
|
montauk::utf8::erase_before(fm->pathbar_buf, &fm->pathbar_len, &fm->pathbar_cursor);
|
||||||
for (int i = fm->pathbar_cursor - 1; i < fm->pathbar_len - 1; i++)
|
} else if (montauk::nav_scancode(key) == 0x53) {
|
||||||
fm->pathbar_buf[i] = fm->pathbar_buf[i + 1];
|
montauk::utf8::erase_at(fm->pathbar_buf, &fm->pathbar_len, fm->pathbar_cursor);
|
||||||
fm->pathbar_len--;
|
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
fm->pathbar_cursor--;
|
fm->pathbar_cursor = montauk::utf8::prev_boundary(fm->pathbar_buf, fm->pathbar_cursor);
|
||||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
}
|
fm->pathbar_cursor = montauk::utf8::next_boundary(fm->pathbar_buf, fm->pathbar_len,
|
||||||
} else if (key.scancode == 0x53) {
|
fm->pathbar_cursor);
|
||||||
if (fm->pathbar_cursor < fm->pathbar_len) {
|
} else if (montauk::nav_scancode(key) == 0x47) {
|
||||||
for (int i = fm->pathbar_cursor; i < fm->pathbar_len - 1; i++)
|
|
||||||
fm->pathbar_buf[i] = fm->pathbar_buf[i + 1];
|
|
||||||
fm->pathbar_len--;
|
|
||||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
|
||||||
}
|
|
||||||
} else if (key.scancode == 0x4B) {
|
|
||||||
if (fm->pathbar_cursor > 0) fm->pathbar_cursor--;
|
|
||||||
} else if (key.scancode == 0x4D) {
|
|
||||||
if (fm->pathbar_cursor < fm->pathbar_len) fm->pathbar_cursor++;
|
|
||||||
} else if (key.scancode == 0x47) {
|
|
||||||
fm->pathbar_cursor = 0;
|
fm->pathbar_cursor = 0;
|
||||||
} else if (key.scancode == 0x4F) {
|
} else if (montauk::nav_scancode(key) == 0x4F) {
|
||||||
fm->pathbar_cursor = fm->pathbar_len;
|
fm->pathbar_cursor = fm->pathbar_len;
|
||||||
} else if ((unsigned char)key.ascii >= 32
|
} else {
|
||||||
&& (unsigned char)key.ascii != 127 && fm->pathbar_len < 254) {
|
montauk::utf8::insert_key(fm->pathbar_buf, &fm->pathbar_len, 255,
|
||||||
for (int i = fm->pathbar_len; i > fm->pathbar_cursor; i--)
|
&fm->pathbar_cursor, key);
|
||||||
fm->pathbar_buf[i] = fm->pathbar_buf[i - 1];
|
|
||||||
fm->pathbar_buf[fm->pathbar_cursor] = key.ascii;
|
|
||||||
fm->pathbar_cursor++;
|
|
||||||
fm->pathbar_len++;
|
|
||||||
fm->pathbar_buf[fm->pathbar_len] = '\0';
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -394,44 +379,27 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
filemanager_cancel_rename(fm);
|
filemanager_cancel_rename(fm);
|
||||||
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
} else if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
// Backspace
|
// Backspace
|
||||||
if (fm->rename_cursor > 0) {
|
montauk::utf8::erase_before(fm->rename_buf, &fm->rename_len, &fm->rename_cursor);
|
||||||
for (int i = fm->rename_cursor - 1; i < fm->rename_len - 1; i++)
|
} else if (montauk::nav_scancode(key) == 0x53) {
|
||||||
fm->rename_buf[i] = fm->rename_buf[i + 1];
|
|
||||||
fm->rename_len--;
|
|
||||||
fm->rename_cursor--;
|
|
||||||
fm->rename_buf[fm->rename_len] = '\0';
|
|
||||||
}
|
|
||||||
} else if (key.scancode == 0x53) {
|
|
||||||
// Delete key
|
// Delete key
|
||||||
if (fm->rename_cursor < fm->rename_len) {
|
montauk::utf8::erase_at(fm->rename_buf, &fm->rename_len, fm->rename_cursor);
|
||||||
for (int i = fm->rename_cursor; i < fm->rename_len - 1; i++)
|
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||||
fm->rename_buf[i] = fm->rename_buf[i + 1];
|
|
||||||
fm->rename_len--;
|
|
||||||
fm->rename_buf[fm->rename_len] = '\0';
|
|
||||||
}
|
|
||||||
} else if (key.scancode == 0x4B) {
|
|
||||||
// Left arrow
|
// Left arrow
|
||||||
if (fm->rename_cursor > 0) fm->rename_cursor--;
|
fm->rename_cursor = montauk::utf8::prev_boundary(fm->rename_buf, fm->rename_cursor);
|
||||||
} else if (key.scancode == 0x4D) {
|
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||||
// Right arrow
|
// Right arrow
|
||||||
if (fm->rename_cursor < fm->rename_len) fm->rename_cursor++;
|
fm->rename_cursor = montauk::utf8::next_boundary(fm->rename_buf, fm->rename_len,
|
||||||
} else if (key.scancode == 0x47) {
|
fm->rename_cursor);
|
||||||
|
} else if (montauk::nav_scancode(key) == 0x47) {
|
||||||
// Home
|
// Home
|
||||||
fm->rename_cursor = 0;
|
fm->rename_cursor = 0;
|
||||||
} else if (key.scancode == 0x4F) {
|
} else if (montauk::nav_scancode(key) == 0x4F) {
|
||||||
// End
|
// End
|
||||||
fm->rename_cursor = fm->rename_len;
|
fm->rename_cursor = fm->rename_len;
|
||||||
} else if ((unsigned char)key.ascii >= 32
|
} else if (key.ascii != '/' && key.ascii != '\\') {
|
||||||
&& (unsigned char)key.ascii != 127) {
|
|
||||||
// Printable character (reject / and other FS-unsafe chars)
|
// Printable character (reject / and other FS-unsafe chars)
|
||||||
if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) {
|
montauk::utf8::insert_key(fm->rename_buf, &fm->rename_len, 63,
|
||||||
for (int i = fm->rename_len; i > fm->rename_cursor; i--)
|
&fm->rename_cursor, key);
|
||||||
fm->rename_buf[i] = fm->rename_buf[i - 1];
|
|
||||||
fm->rename_buf[fm->rename_cursor] = key.ascii;
|
|
||||||
fm->rename_cursor++;
|
|
||||||
fm->rename_len++;
|
|
||||||
fm->rename_buf[fm->rename_len] = '\0';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -466,7 +434,7 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
|
|
||||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
filemanager_go_up(fm);
|
filemanager_go_up(fm);
|
||||||
} else if (key.scancode == 0x48) {
|
} else if (montauk::nav_scancode(key) == 0x48) {
|
||||||
// Up arrow
|
// Up arrow
|
||||||
if (fm->grid_view) {
|
if (fm->grid_view) {
|
||||||
Rect cr_r = win->content_rect();
|
Rect cr_r = win->content_rect();
|
||||||
@@ -477,7 +445,7 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
if (fm->selected > 0) fm->selected--;
|
if (fm->selected > 0) fm->selected--;
|
||||||
}
|
}
|
||||||
filemanager_scroll_to_selected(win, fm);
|
filemanager_scroll_to_selected(win, fm);
|
||||||
} else if (key.scancode == 0x50) {
|
} else if (montauk::nav_scancode(key) == 0x50) {
|
||||||
// Down arrow
|
// Down arrow
|
||||||
if (fm->grid_view) {
|
if (fm->grid_view) {
|
||||||
Rect cr_r = win->content_rect();
|
Rect cr_r = win->content_rect();
|
||||||
@@ -488,22 +456,22 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
|||||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||||
}
|
}
|
||||||
filemanager_scroll_to_selected(win, fm);
|
filemanager_scroll_to_selected(win, fm);
|
||||||
} else if (key.scancode == 0x4B && !key.alt && fm->grid_view) {
|
} else if (montauk::nav_scancode(key) == 0x4B && !key.alt && fm->grid_view) {
|
||||||
// Left arrow (grid view only)
|
// Left arrow (grid view only)
|
||||||
if (fm->selected > 0) fm->selected--;
|
if (fm->selected > 0) fm->selected--;
|
||||||
} else if (key.scancode == 0x4D && !key.alt && fm->grid_view) {
|
} else if (montauk::nav_scancode(key) == 0x4D && !key.alt && fm->grid_view) {
|
||||||
// Right arrow (grid view only)
|
// Right arrow (grid view only)
|
||||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||||
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||||
if (fm->selected >= 0 && fm->selected < fm->entry_count)
|
if (fm->selected >= 0 && fm->selected < fm->entry_count)
|
||||||
filemanager_open_entry(fm, fm->selected);
|
filemanager_open_entry(fm, fm->selected);
|
||||||
} else if (key.scancode == 0x53) {
|
} else if (montauk::nav_scancode(key) == 0x53) {
|
||||||
// Delete key
|
// Delete key
|
||||||
filemanager_delete_selected(fm);
|
filemanager_delete_selected(fm);
|
||||||
} else if (key.alt && key.scancode == 0x4B) {
|
} else if (key.alt && montauk::nav_scancode(key) == 0x4B) {
|
||||||
// Alt+Left: go back
|
// Alt+Left: go back
|
||||||
filemanager_go_back(fm);
|
filemanager_go_back(fm);
|
||||||
} else if (key.alt && key.scancode == 0x4D) {
|
} else if (key.alt && montauk::nav_scancode(key) == 0x4D) {
|
||||||
// Alt+Right: go forward
|
// Alt+Right: go forward
|
||||||
filemanager_go_forward(fm);
|
filemanager_go_forward(fm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,10 +234,7 @@ void filemanager_read_system_configuration(FileManagerState* fm);
|
|||||||
// Map a raw syscall result (kernel StatusCodes are negative) to a userspace
|
// Map a raw syscall result (kernel StatusCodes are negative) to a userspace
|
||||||
// montauk::StatusCodes. Non-negative results (handles, counts) are Success.
|
// montauk::StatusCodes. Non-negative results (handles, counts) are Success.
|
||||||
inline montauk::StatusCode filemanager_status_from_result(int rc) {
|
inline montauk::StatusCode filemanager_status_from_result(int rc) {
|
||||||
if (rc >= 0) return montauk::Success;
|
return montauk::StatusFromResult(rc);
|
||||||
int code = -rc;
|
|
||||||
if (code > montauk::OutOfMemory) return montauk::GenericFailure;
|
|
||||||
return (montauk::StatusCode)code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The path itself may not be modified. Falling back to copy + delete after a
|
// The path itself may not be modified. Falling back to copy + delete after a
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ static void props_fit_text(const char* src, char* out, int out_len, int max_w) {
|
|||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
out[len] = '\0';
|
out[len] = '\0';
|
||||||
if (text_width(out) + ellipsis_w <= max_w) break;
|
if (text_width(out) + ellipsis_w <= max_w) break;
|
||||||
len--;
|
len = montauk::utf8::prev_boundary(out, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > out_len - 4) len = out_len - 4;
|
if (len > out_len - 4) len = montauk::utf8::floor_boundary(out, len, out_len - 4);
|
||||||
if (len < 0) len = 0;
|
if (len < 0) len = 0;
|
||||||
out[len] = '.';
|
out[len] = '.';
|
||||||
out[len + 1] = '.';
|
out[len + 1] = '.';
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ static int fitted_prefix_len(const char* src, int max_w, int max_len) {
|
|||||||
char tmp[128];
|
char tmp[128];
|
||||||
int best = 0;
|
int best = 0;
|
||||||
int limit = max_len < 127 ? max_len : 127;
|
int limit = max_len < 127 ? max_len : 127;
|
||||||
for (int len = 1; len <= limit; len++) {
|
for (int len = montauk::utf8::next_boundary(src, max_len, 0); len <= limit;
|
||||||
|
len = montauk::utf8::next_boundary(src, max_len, len)) {
|
||||||
copy_label_range(tmp, sizeof(tmp), src, 0, len);
|
copy_label_range(tmp, sizeof(tmp), src, 0, len);
|
||||||
if (text_width(tmp) > max_w) break;
|
if (text_width(tmp) > max_w) break;
|
||||||
best = len;
|
best = len;
|
||||||
|
if (len >= max_len) break;
|
||||||
}
|
}
|
||||||
return best > 0 ? best : 1;
|
return best > 0 ? best : montauk::utf8::next_boundary(src, max_len, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int max_w) {
|
static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int max_w) {
|
||||||
@@ -49,10 +51,10 @@ static void fit_label_with_ellipsis(char* dst, int dst_max, const char* src, int
|
|||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
dst[len] = '\0';
|
dst[len] = '\0';
|
||||||
if (text_width(dst) + ellipsis_w <= max_w) break;
|
if (text_width(dst) + ellipsis_w <= max_w) break;
|
||||||
len--;
|
len = montauk::utf8::prev_boundary(dst, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len > dst_max - 4) len = dst_max - 4;
|
if (len > dst_max - 4) len = montauk::utf8::floor_boundary(dst, len, dst_max - 4);
|
||||||
if (len < 0) len = 0;
|
if (len < 0) len = 0;
|
||||||
dst[len] = '.';
|
dst[len] = '.';
|
||||||
dst[len + 1] = '.';
|
dst[len + 1] = '.';
|
||||||
@@ -403,7 +405,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
// Draw rename text (truncated to cell width)
|
// Draw rename text (truncated to cell width)
|
||||||
char display[16];
|
char display[16];
|
||||||
int rlen = fm->rename_len;
|
int rlen = fm->rename_len;
|
||||||
if (rlen > 10) rlen = 10;
|
if (rlen > 10) rlen = montauk::utf8::floor_boundary(fm->rename_buf, fm->rename_len, 10);
|
||||||
for (int k = 0; k < rlen; k++) display[k] = fm->rename_buf[k];
|
for (int k = 0; k < rlen; k++) display[k] = fm->rename_buf[k];
|
||||||
display[rlen] = '\0';
|
display[rlen] = '\0';
|
||||||
c.text(cell_x + 4, ty, display, colors::TEXT_COLOR);
|
c.text(cell_x + 4, ty, display, colors::TEXT_COLOR);
|
||||||
@@ -557,18 +559,19 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
|||||||
int name_max_w = size_col_x - tx - 8;
|
int name_max_w = size_col_x - tx - 8;
|
||||||
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
|
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
|
||||||
char label[64];
|
char label[64];
|
||||||
montauk::strncpy(label, name, 63);
|
montauk::utf8::copy(label, 64, name);
|
||||||
|
|
||||||
if (text_width(label) > name_max_w && montauk::slen(name) > 3) {
|
if (text_width(label) > name_max_w && montauk::slen(name) > 3) {
|
||||||
int ellipsis_w = text_width("...");
|
int ellipsis_w = text_width("...");
|
||||||
int final_len = montauk::slen(name);
|
int final_len = montauk::slen(label);
|
||||||
while (final_len > 1) {
|
while (final_len > 1) {
|
||||||
label[final_len] = '\0';
|
label[final_len] = '\0';
|
||||||
int w = text_width(label);
|
int w = text_width(label);
|
||||||
if (w + ellipsis_w <= name_max_w) break;
|
if (w + ellipsis_w <= name_max_w) break;
|
||||||
final_len--;
|
final_len = montauk::utf8::prev_boundary(label, final_len);
|
||||||
}
|
}
|
||||||
if (final_len > 60) final_len = 60;
|
if (final_len > 60)
|
||||||
|
final_len = montauk::utf8::floor_boundary(label, final_len, 60);
|
||||||
label[final_len] = '.';
|
label[final_len] = '.';
|
||||||
label[final_len + 1] = '.';
|
label[final_len + 1] = '.';
|
||||||
label[final_len + 2] = '.';
|
label[final_len + 2] = '.';
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ void settings_draw_about(Canvas& c, SettingsState* st) {
|
|||||||
mtk::draw_separator(c, x, y, c.w - 2 * x, theme);
|
mtk::draw_separator(c, x, y, c.w - 2 * x, theme);
|
||||||
y += 12;
|
y += 12;
|
||||||
|
|
||||||
c.text(x, y, "Copyright \xa9 2026 Montauk Operating System Project", theme.text_muted);
|
c.text(x, y, "Copyright \u00A9 2025-2026 Montauk Operating System Project", theme.text_muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace settings_app
|
} // namespace settings_app
|
||||||
@@ -205,7 +205,7 @@ void desktop_draw_brightness_popup(DesktopState* ds) {
|
|||||||
fill_rounded_rect(fb, settings.x, settings.y, settings.w, settings.h, 5,
|
fill_rounded_rect(fb, settings.x, settings.y, settings.w, settings.h, 5,
|
||||||
Color::from_rgb(0xF0, 0xF3, 0xF6));
|
Color::from_rgb(0xF0, 0xF3, 0xF6));
|
||||||
draw_text(fb, settings.x + 6, settings.y + (settings.h - system_font_height()) / 2,
|
draw_text(fb, settings.x + 6, settings.y + (settings.h - system_font_height()) / 2,
|
||||||
"Display Settings...", colors::TEXT_COLOR);
|
"Display Settings", colors::TEXT_COLOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true when the event was consumed.
|
// Returns true when the event was consumed.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ static constexpr int MENU_DIV_H = 10;
|
|||||||
|
|
||||||
struct MenuRow {
|
struct MenuRow {
|
||||||
bool is_category;
|
bool is_category;
|
||||||
|
bool expanded; // category rows only: children shown (dividers always true)
|
||||||
char label[48];
|
char label[48];
|
||||||
DesktopBuiltinId app_id; // desktop builtin ID, or DESKTOP_BUILTIN_NONE for categories/dividers
|
DesktopBuiltinId app_id; // desktop builtin ID, or DESKTOP_BUILTIN_NONE for categories/dividers
|
||||||
bool external; // true = spawn binary_path
|
bool external; // true = spawn binary_path
|
||||||
@@ -74,23 +75,15 @@ inline MenuRow* menu_alloc_row() {
|
|||||||
return &menu_rows[menu_row_count++];
|
return &menu_rows[menu_row_count++];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collapsible category state (categories 0-3 are toggleable; divider category always expanded)
|
// Collapsible categories: each category row carries its own expanded flag,
|
||||||
static constexpr int MENU_NUM_CATS = 5;
|
// so the menu can hold any number of manifest-defined categories. Dividers
|
||||||
inline bool menu_cat_expanded[MENU_NUM_CATS] = { false, false, false, false, true };
|
// (empty label) are always expanded.
|
||||||
|
|
||||||
inline int menu_get_cat(int row_idx) {
|
|
||||||
int cat = -1;
|
|
||||||
for (int i = 0; i <= row_idx; i++)
|
|
||||||
if (menu_rows[i].is_category) cat++;
|
|
||||||
return cat;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool menu_row_visible(int row_idx) {
|
inline bool menu_row_visible(int row_idx) {
|
||||||
const MenuRow& row = menu_rows[row_idx];
|
const MenuRow& row = menu_rows[row_idx];
|
||||||
if (row.is_category) return true;
|
if (row.is_category) return true;
|
||||||
int cat = menu_get_cat(row_idx);
|
for (int i = row_idx - 1; i >= 0; i--)
|
||||||
if (cat < 0 || cat >= MENU_NUM_CATS) return true;
|
if (menu_rows[i].is_category) return menu_rows[i].expanded;
|
||||||
return menu_cat_expanded[cat];
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int menu_row_height(const MenuRow& row) {
|
inline int menu_row_height(const MenuRow& row) {
|
||||||
@@ -102,17 +95,11 @@ inline int menu_total_height() {
|
|||||||
// Single-pass walk to avoid O(N^2) from menu_row_visible repeatedly
|
// Single-pass walk to avoid O(N^2) from menu_row_visible repeatedly
|
||||||
// counting categories from the start.
|
// counting categories from the start.
|
||||||
int h = 10; // top + bottom padding
|
int h = 10; // top + bottom padding
|
||||||
int cur_cat = -1;
|
bool cat_expanded = true;
|
||||||
for (int i = 0; i < menu_row_count; i++) {
|
for (int i = 0; i < menu_row_count; i++) {
|
||||||
const MenuRow& row = menu_rows[i];
|
const MenuRow& row = menu_rows[i];
|
||||||
if (row.is_category) cur_cat++;
|
if (row.is_category) cat_expanded = row.expanded;
|
||||||
bool visible = true;
|
if (row.is_category || cat_expanded) h += menu_row_height(row);
|
||||||
if (!row.is_category) {
|
|
||||||
if (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) {
|
|
||||||
visible = menu_cat_expanded[cur_cat];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (visible) h += menu_row_height(row);
|
|
||||||
}
|
}
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
@@ -126,6 +113,7 @@ inline int menu_add_category(const char* label) {
|
|||||||
if (!rp) return -1;
|
if (!rp) return -1;
|
||||||
MenuRow& r = *rp;
|
MenuRow& r = *rp;
|
||||||
r.is_category = true;
|
r.is_category = true;
|
||||||
|
r.expanded = !label[0]; // named categories start collapsed; dividers stay open
|
||||||
montauk::strncpy(r.label, label, sizeof(r.label));
|
montauk::strncpy(r.label, label, sizeof(r.label));
|
||||||
r.app_id = DESKTOP_BUILTIN_NONE;
|
r.app_id = DESKTOP_BUILTIN_NONE;
|
||||||
r.external = false;
|
r.external = false;
|
||||||
@@ -140,6 +128,7 @@ inline int menu_add_embedded(const char* label, DesktopBuiltinId app_id, SvgIcon
|
|||||||
if (!rp) return -1;
|
if (!rp) return -1;
|
||||||
MenuRow& r = *rp;
|
MenuRow& r = *rp;
|
||||||
r.is_category = false;
|
r.is_category = false;
|
||||||
|
r.expanded = false;
|
||||||
montauk::strncpy(r.label, label, sizeof(r.label));
|
montauk::strncpy(r.label, label, sizeof(r.label));
|
||||||
r.app_id = app_id;
|
r.app_id = app_id;
|
||||||
r.external = false;
|
r.external = false;
|
||||||
@@ -154,6 +143,7 @@ inline int menu_add_external(const char* label, const char* binary, SvgIcon* ico
|
|||||||
if (!rp) return -1;
|
if (!rp) return -1;
|
||||||
MenuRow& r = *rp;
|
MenuRow& r = *rp;
|
||||||
r.is_category = false;
|
r.is_category = false;
|
||||||
|
r.expanded = false;
|
||||||
montauk::strncpy(r.label, label, sizeof(r.label));
|
montauk::strncpy(r.label, label, sizeof(r.label));
|
||||||
r.app_id = DESKTOP_BUILTIN_NONE;
|
r.app_id = DESKTOP_BUILTIN_NONE;
|
||||||
r.external = true;
|
r.external = true;
|
||||||
|
|||||||
@@ -98,22 +98,12 @@ static void handle_lock_keyboard(DesktopState* ds, const montauk::abi::KeyEvent&
|
|||||||
|
|
||||||
// Backspace
|
// Backspace
|
||||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||||
if (ds->lock_password_len > 0) {
|
montauk::utf8::erase_last(ds->lock_password, &ds->lock_password_len);
|
||||||
ds->lock_password_len--;
|
|
||||||
ds->lock_password[ds->lock_password_len] = '\0';
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printable characters
|
// Printable characters (stored as UTF-8)
|
||||||
if ((unsigned char)key.ascii >= 0x20
|
montauk::utf8::append_key(ds->lock_password, &ds->lock_password_len, 64, key);
|
||||||
&& (unsigned char)key.ascii != 0x7F) {
|
|
||||||
if (ds->lock_password_len < 63) {
|
|
||||||
ds->lock_password[ds->lock_password_len] = key.ascii;
|
|
||||||
ds->lock_password_len++;
|
|
||||||
ds->lock_password[ds->lock_password_len] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void forward_external_mouse(gui::Window* win,
|
static void forward_external_mouse(gui::Window* win,
|
||||||
@@ -370,16 +360,16 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
if (menu_rect.contains(mx, my)) {
|
if (menu_rect.contains(mx, my)) {
|
||||||
// Walk visible rows to find which one was clicked
|
// Walk visible rows to find which one was clicked
|
||||||
int iy = menu_y + 5;
|
int iy = menu_y + 5;
|
||||||
int cur_cat = -1;
|
bool cat_expanded = true;
|
||||||
for (int i = 0; i < menu_row_count; i++) {
|
for (int i = 0; i < menu_row_count; i++) {
|
||||||
const MenuRow& row = menu_rows[i];
|
MenuRow& row = menu_rows[i];
|
||||||
if (row.is_category) cur_cat++;
|
if (row.is_category) cat_expanded = row.expanded;
|
||||||
if (!menu_row_visible(i)) continue;
|
else if (!cat_expanded) continue;
|
||||||
int row_h = menu_row_height(row);
|
int row_h = menu_row_height(row);
|
||||||
if (my >= iy && my < iy + row_h) {
|
if (my >= iy && my < iy + row_h) {
|
||||||
if (row.is_category && row.label[0] && cur_cat >= 0 && cur_cat < MENU_NUM_CATS - 1) {
|
if (row.is_category && row.label[0]) {
|
||||||
// Toggle category expand/collapse
|
// Toggle category expand/collapse
|
||||||
menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat];
|
row.expanded = !row.expanded;
|
||||||
} else if (!row.is_category) {
|
} else if (!row.is_category) {
|
||||||
if (row.external) {
|
if (row.external) {
|
||||||
if (row.launch_with_home) {
|
if (row.launch_with_home) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,11 @@ void desktop_scan_apps(DesktopState* ds) {
|
|||||||
const char* name = doc.get_string("app.name", "Unknown");
|
const char* name = doc.get_string("app.name", "Unknown");
|
||||||
const char* binary = doc.get_string("app.binary", "");
|
const char* binary = doc.get_string("app.binary", "");
|
||||||
const char* icon_file = doc.get_string("app.icon", "");
|
const char* icon_file = doc.get_string("app.icon", "");
|
||||||
const char* category = doc.get_string("menu.category", "Applications");
|
const char* category = doc.get_string("menu.category", "Productivity");
|
||||||
|
// "Applications" was the old catch-all; keep older manifests out of
|
||||||
|
// a stray custom category.
|
||||||
|
if (!category[0] || montauk::streq(category, "Applications"))
|
||||||
|
category = "Productivity";
|
||||||
bool visible = doc.get_bool("menu.visible", true);
|
bool visible = doc.get_bool("menu.visible", true);
|
||||||
bool launch_with_home = doc.get_bool("launch.pass_home_dir", true);
|
bool launch_with_home = doc.get_bool("launch.pass_home_dir", true);
|
||||||
const char* section_name = doc.get_string("desktop.section", "apps");
|
const char* section_name = doc.get_string("desktop.section", "apps");
|
||||||
@@ -197,18 +201,55 @@ void desktop_scan_apps(DesktopState* ds) {
|
|||||||
// Menu Builder
|
// Menu Builder
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Category definitions and their ordering
|
// Predefined categories and their ordering. Manifests may name any other
|
||||||
static const char* CATEGORY_NAMES[] = { "Applications", "Internet", "System", "Games" };
|
// category; those are appended after these, sorted by name.
|
||||||
static constexpr int NUM_CATEGORIES = 4;
|
// Builtins reference these by index (DesktopBuiltinEntry::launcher_category).
|
||||||
|
static const char* CATEGORY_NAMES[] = {
|
||||||
|
"Productivity", "Media", "Internet",
|
||||||
|
"Utilities", "Games", "Hardware", "System",
|
||||||
|
};
|
||||||
|
static constexpr int NUM_CATEGORIES = sizeof(CATEGORY_NAMES) / sizeof(CATEGORY_NAMES[0]);
|
||||||
|
|
||||||
|
static bool category_eq(const char* a, const char* b) {
|
||||||
|
for (;; a++, b++) {
|
||||||
|
char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
|
||||||
|
char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
|
||||||
|
if (ca != cb) return false;
|
||||||
|
if (!ca) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int category_cmp(const char* a, const char* b) {
|
||||||
|
for (;; a++, b++) {
|
||||||
|
char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
|
||||||
|
char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
|
||||||
|
if (ca != cb || !ca) return (int)(unsigned char)ca - (int)(unsigned char)cb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool menu_item_shown(DesktopState* ds, const DesktopItem* item) {
|
||||||
|
return item->menu_visible && !(item->admin_only && !ds->is_admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void menu_add_manifest_items(DesktopState* ds, const char* category) {
|
||||||
|
for (int x = 0; x < ds->desktop_item_count; x++) {
|
||||||
|
DesktopItem* item = &ds->desktop_items[x];
|
||||||
|
if (!menu_item_shown(ds, item)) continue;
|
||||||
|
if (category_eq(item->category, category)) {
|
||||||
|
menu_add_external(item->name, item->binary_path,
|
||||||
|
&item->icon, item->launch_with_home);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void desktop_build_menu(DesktopState* ds) {
|
void desktop_build_menu(DesktopState* ds) {
|
||||||
menu_row_count = 0;
|
menu_row_count = 0;
|
||||||
int builtin_count = 0;
|
int builtin_count = 0;
|
||||||
const DesktopBuiltinEntry* builtins = desktop_builtin_registry(&builtin_count);
|
const DesktopBuiltinEntry* builtins = desktop_builtin_registry(&builtin_count);
|
||||||
|
|
||||||
// Build each category
|
// Build each predefined category
|
||||||
for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
|
for (int cat = 0; cat < NUM_CATEGORIES; cat++) {
|
||||||
menu_add_category(CATEGORY_NAMES[cat]);
|
int header = menu_add_category(CATEGORY_NAMES[cat]);
|
||||||
|
|
||||||
// Add desktop builtins in this category
|
// Add desktop builtins in this category
|
||||||
for (int b = 0; b < builtin_count; b++) {
|
for (int b = 0; b < builtin_count; b++) {
|
||||||
@@ -218,19 +259,48 @@ void desktop_build_menu(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add manifest-backed items in this category.
|
menu_add_manifest_items(ds, CATEGORY_NAMES[cat]);
|
||||||
|
|
||||||
|
// Drop the header again if nothing landed under it.
|
||||||
|
if (header >= 0 && menu_row_count == header + 1) menu_row_count = header;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom categories: the first item of each distinct, non-predefined
|
||||||
|
// category (case-insensitive) represents it; emit them in name order.
|
||||||
|
const DesktopItem** custom = ds->desktop_item_count > 0
|
||||||
|
? (const DesktopItem**)montauk::malloc(
|
||||||
|
(uint64_t)ds->desktop_item_count * sizeof(DesktopItem*))
|
||||||
|
: nullptr;
|
||||||
|
int custom_count = 0;
|
||||||
|
if (custom) {
|
||||||
for (int x = 0; x < ds->desktop_item_count; x++) {
|
for (int x = 0; x < ds->desktop_item_count; x++) {
|
||||||
DesktopItem* item = &ds->desktop_items[x];
|
const DesktopItem* item = &ds->desktop_items[x];
|
||||||
if (!item->menu_visible || (item->admin_only && !ds->is_admin)) continue;
|
if (!menu_item_shown(ds, item)) continue;
|
||||||
if (montauk::streq(item->category, CATEGORY_NAMES[cat])) {
|
|
||||||
menu_add_external(item->name, item->binary_path,
|
bool known = false;
|
||||||
&item->icon, item->launch_with_home);
|
for (int c = 0; c < NUM_CATEGORIES && !known; c++)
|
||||||
|
known = category_eq(item->category, CATEGORY_NAMES[c]);
|
||||||
|
for (int c = 0; c < custom_count && !known; c++)
|
||||||
|
known = category_eq(item->category, custom[c]->category);
|
||||||
|
if (known) continue;
|
||||||
|
|
||||||
|
int pos = custom_count;
|
||||||
|
while (pos > 0 && category_cmp(custom[pos - 1]->category, item->category) > 0) {
|
||||||
|
custom[pos] = custom[pos - 1];
|
||||||
|
pos--;
|
||||||
}
|
}
|
||||||
|
custom[pos] = item;
|
||||||
|
custom_count++;
|
||||||
}
|
}
|
||||||
|
for (int c = 0; c < custom_count; c++) {
|
||||||
|
menu_add_category(custom[c]->category);
|
||||||
|
menu_add_manifest_items(ds, custom[c]->category);
|
||||||
|
}
|
||||||
|
montauk::mfree(custom);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Divider + always-visible entries
|
// Divider + always-visible entries
|
||||||
menu_add_category(""); // divider (cat 4, always expanded)
|
menu_add_category(""); // divider (always expanded)
|
||||||
for (int b = 0; b < builtin_count; b++) {
|
for (int b = 0; b < builtin_count; b++) {
|
||||||
if (builtins[b].launcher_footer) {
|
if (builtins[b].launcher_footer) {
|
||||||
menu_add_embedded(builtins[b].label, builtins[b].id,
|
menu_add_embedded(builtins[b].label, builtins[b].id,
|
||||||
|
|||||||
@@ -271,13 +271,13 @@ void desktop_draw_app_menu(DesktopState* ds) {
|
|||||||
int mx = ds->mouse.x;
|
int mx = ds->mouse.x;
|
||||||
int my = ds->mouse.y;
|
int my = ds->mouse.y;
|
||||||
int iy = menu_y + 5;
|
int iy = menu_y + 5;
|
||||||
int cur_cat = -1;
|
bool cat_expanded = true;
|
||||||
|
|
||||||
for (int i = 0; i < menu_row_count; i++) {
|
for (int i = 0; i < menu_row_count; i++) {
|
||||||
const MenuRow& row = menu_rows[i];
|
const MenuRow& row = menu_rows[i];
|
||||||
|
|
||||||
if (row.is_category) cur_cat++;
|
if (row.is_category) cat_expanded = row.expanded;
|
||||||
if (!menu_row_visible(i)) continue;
|
else if (!cat_expanded) continue;
|
||||||
|
|
||||||
int row_h = menu_row_height(row);
|
int row_h = menu_row_height(row);
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ void desktop_draw_app_menu(DesktopState* ds) {
|
|||||||
Color::from_rgb(0xE8, 0xE8, 0xE8));
|
Color::from_rgb(0xE8, 0xE8, 0xE8));
|
||||||
|
|
||||||
// Arrow indicator: > for collapsed, v for expanded
|
// Arrow indicator: > for collapsed, v for expanded
|
||||||
bool expanded = (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) && menu_cat_expanded[cur_cat];
|
bool expanded = row.expanded;
|
||||||
int ax = menu_x + 10;
|
int ax = menu_x + 10;
|
||||||
int ay = iy + row_h / 2;
|
int ay = iy + row_h / 2;
|
||||||
Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88);
|
Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ binary = "devexplorer.elf"
|
|||||||
icon = "hardware.svg"
|
icon = "hardware.svg"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
category = "System"
|
category = "Hardware"
|
||||||
visible = true
|
visible = true
|
||||||
@@ -263,7 +263,7 @@ extern "C" void _start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (link == false) {
|
if (link == false) {
|
||||||
montauk::service_log("Unable to locate a network device with an active link.\n");
|
montauk::service_log("Unable to locate a network device with an active link.");
|
||||||
montauk::exit(montauk::StatusCode::NoDevice);
|
montauk::exit(montauk::StatusCode::NoDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ binary = "disks.elf"
|
|||||||
icon = "gparted.svg"
|
icon = "gparted.svg"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
category = "System"
|
category = "Hardware"
|
||||||
visible = true
|
visible = true
|
||||||
@@ -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++] = '.';
|
||||||
@@ -500,7 +501,7 @@ static void apply_changes() {
|
|||||||
if (g_driver_available && g_selected_mode != g_info.currentMode) {
|
if (g_driver_available && g_selected_mode != g_info.currentMode) {
|
||||||
int result = montauk::display_set_mode(g_selected_mode);
|
int result = montauk::display_set_mode(g_selected_mode);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
set_status(result == montauk::UnsupportedOp
|
set_status(montauk::StatusFromResult(result) == montauk::UnsupportedOp
|
||||||
? "Mode needs unsupported display-link training"
|
? "Mode needs unsupported display-link training"
|
||||||
: "Mode change failed; previous mode restored");
|
: "Mode change failed; previous mode restored");
|
||||||
refresh_state(true);
|
refresh_state(true);
|
||||||
|
|||||||
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user