Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
821543238d | ||
|
|
86e3ebfe33 |
+14
@@ -270,8 +270,22 @@ ifneq ($(RELEASE_WITH_PORTS),1)
|
||||
done
|
||||
else
|
||||
@echo "release: RELEASE_WITH_PORTS=1 - including out-of-tree port bundles"
|
||||
endif
|
||||
# The Montauk SDK (native GCC + binutils and their target sysroot under
|
||||
# 0:/sdk) dominates the image size. Pass RELEASE_WITHOUT_SDK=1 to cut a slim
|
||||
# ISO without it. Only the devkit-owned subtrees go: sdk/tcc and sdk/lua are
|
||||
# installed by their own targets and stay, as does 0:/tmp, which the libc
|
||||
# uses for tmpnam() and not just for compiler intermediates.
|
||||
ifeq ($(RELEASE_WITHOUT_SDK),1)
|
||||
@echo "release: RELEASE_WITHOUT_SDK=1 - excluding the Montauk SDK (0:/sdk dev tools)"
|
||||
rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \
|
||||
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
|
||||
endif
|
||||
find .release-tmp -name '*.elf' -exec strip {} +
|
||||
# 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
|
||||
# *.elf, so the find above misses it. Unstripped it is ~6 MB against ~0.7 MB.
|
||||
strip .release-tmp/boot/kernel
|
||||
./scripts/mkramdisk.sh .release-tmp iso_root/boot/ramdisk.tar
|
||||
rm -rf .release-tmp
|
||||
mkdir -p iso_root/boot/limine
|
||||
|
||||
@@ -1,3 +1,70 @@
|
||||
MontaukOS 0.1.9 (August 13, 2026)
|
||||
(a28b8ad0b2932203b812e967f35f029f060fd6b5)
|
||||
|
||||
New
|
||||
====
|
||||
* Intel Wi-Fi (AX210/AX211) support
|
||||
* Wi-Fi support in the desktop panel and the Network settings app
|
||||
* wifi command-line tool
|
||||
* NetSurf web browser ported as a native MontaukOS app
|
||||
* SSH server (sshd) with an SSH Server app for managing it
|
||||
* NTP client (configured via 0:/config/ntp.toml and Time applet)
|
||||
* init now discovers services from 0:/config/init.toml rather than a hardcoded sequence
|
||||
* New Display app for managing monitors, modes, and layout
|
||||
* Expanded Intel GPU display management in the kernel
|
||||
* Audio stack now mixes concurrent streams and switches output devices live
|
||||
* New audio device switching UI in Audio app and the desktop panel
|
||||
* Filesystems can be mounted and unmounted at runtime
|
||||
* File metadata across all filesystems, exposed through a new stat syscall
|
||||
* ext2 now saves timestamps and supports utime
|
||||
* New mkdir, rmdir, and memtest commands
|
||||
* Expanded support in the PDF Viewer app
|
||||
* Files shows modified dates in the Properties dialog
|
||||
* Desktop UI consistency improvements
|
||||
* Crash reports now capture a stack snapshot
|
||||
* Userspace heap and virtual memory overhauled
|
||||
* pread, pwrite, strndup, scandir, alphasort, usleep, ftruncate, M_PI, and a non-blocking stdin readiness probe added to libc
|
||||
* DOOM moved out of the OS tree into the mtkports project
|
||||
|
||||
Fixed
|
||||
=====
|
||||
* Numerous kernel concurrency, interrupt-context, and user virtual-address fixes
|
||||
* Hardened the TCP/IP stack and unified the HTTP clients; fixed several networking bugs and regressions
|
||||
* Improved Bluetooth reliability
|
||||
* Corrected Intel GGTT setup and DP AUX EDID reads
|
||||
* IPC handle slots are no longer reused while an object is still being torn down
|
||||
* The ramdisk no longer packs symlinks as zero-byte files
|
||||
* Fixed a ramdisk readdir inconsistency
|
||||
* Processes now inherit their parent's environment
|
||||
* getcwd reports a truncated buffer as ERANGE instead of silently truncating
|
||||
* Improved stdio and argument handling
|
||||
* Clarified Intel iwlwifi firmware licensing
|
||||
|
||||
Included in ISO
|
||||
================
|
||||
|
||||
Two x86_64 images are published for this release.
|
||||
montauk-0.1.9-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:/man), readable via man command
|
||||
* Service and system configuration (0:/config: init.toml, ssh.toml, ntp.toml, timezonedata.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.8 (July 18, 2026)
|
||||
(71c66f803516558012be13ee38cb02a9a0ba476a)
|
||||
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 109
|
||||
#define MONTAUK_BUILD_NUMBER 116
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace montauk::abi {
|
||||
|
||||
// Copy strings into fixed-size arrays (user-accessible)
|
||||
const char* name = "MontaukOS";
|
||||
const char* ver = "0.1.9";
|
||||
const char* ver = "0.2.0";
|
||||
for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i];
|
||||
outInfo->osName[9] = '\0';
|
||||
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
|
||||
|
||||
@@ -54,6 +54,9 @@ namespace montauk::abi {
|
||||
outEvent->shift = k.Shift;
|
||||
outEvent->ctrl = k.Ctrl;
|
||||
outEvent->alt = k.Alt;
|
||||
outEvent->capslock = k.CapsLock;
|
||||
outEvent->altgr = k.AltGr;
|
||||
outEvent->extended = k.Extended;
|
||||
}
|
||||
|
||||
static char Sys_GetChar() {
|
||||
|
||||
@@ -458,6 +458,13 @@ namespace montauk::abi {
|
||||
bool shift;
|
||||
bool ctrl;
|
||||
bool alt;
|
||||
bool capslock;
|
||||
// Right Alt held, reported separately from alt so keyboard layouts can
|
||||
// use it as AltGr without swallowing left-Alt shortcuts.
|
||||
bool altgr;
|
||||
// Key came from an E0-prefixed scancode (keypad "/" and Enter share a
|
||||
// scancode with the main-block keys). Layout translation must skip it.
|
||||
bool extended;
|
||||
};
|
||||
|
||||
struct MouseState {
|
||||
|
||||
@@ -179,7 +179,9 @@ namespace Drivers::PS2::Keyboard {
|
||||
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
||||
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
||||
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
||||
.CapsLock = g_Modifiers.CapsLock
|
||||
.CapsLock = g_Modifiers.CapsLock,
|
||||
.AltGr = g_Modifiers.RightAlt,
|
||||
.Extended = true
|
||||
};
|
||||
PushAndNotify(event);
|
||||
|
||||
@@ -237,7 +239,9 @@ namespace Drivers::PS2::Keyboard {
|
||||
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
||||
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
||||
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
||||
.CapsLock = g_Modifiers.CapsLock
|
||||
.CapsLock = g_Modifiers.CapsLock,
|
||||
.AltGr = g_Modifiers.RightAlt,
|
||||
.Extended = false
|
||||
};
|
||||
PushAndNotify(event);
|
||||
return;
|
||||
@@ -261,7 +265,9 @@ namespace Drivers::PS2::Keyboard {
|
||||
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
||||
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
||||
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
||||
.CapsLock = g_Modifiers.CapsLock
|
||||
.CapsLock = g_Modifiers.CapsLock,
|
||||
.AltGr = g_Modifiers.RightAlt,
|
||||
.Extended = false
|
||||
};
|
||||
|
||||
PushAndNotify(event);
|
||||
@@ -298,7 +304,18 @@ namespace Drivers::PS2::Keyboard {
|
||||
}
|
||||
|
||||
void InjectKeyEvent(const KeyEvent& event) {
|
||||
PushAndNotify(event);
|
||||
// Caps Lock is owned here rather than by each injecting driver: USB HID
|
||||
// reports the key as an ordinary usage and carries no lock state, so
|
||||
// without this the lock would never toggle on a USB keyboard.
|
||||
KeyEvent stamped = event;
|
||||
if (stamped.Scancode == ScCapsLock && stamped.Pressed)
|
||||
g_Modifiers.CapsLock = !g_Modifiers.CapsLock;
|
||||
stamped.CapsLock = g_Modifiers.CapsLock;
|
||||
PushAndNotify(stamped);
|
||||
}
|
||||
|
||||
bool GetCapsLock() {
|
||||
return g_Modifiers.CapsLock;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -18,6 +18,13 @@ namespace Drivers::PS2::Keyboard {
|
||||
bool Ctrl;
|
||||
bool Alt;
|
||||
bool CapsLock;
|
||||
// Right Alt held. Reported separately from Alt so keyboard layouts can
|
||||
// use it as AltGr without swallowing left-Alt shortcuts.
|
||||
bool AltGr;
|
||||
// Key came from an E0-prefixed (extended) scancode. The prefix is not
|
||||
// part of Scancode, so without this flag keypad "/" is indistinguishable
|
||||
// from the main "/" key and layout translation would rewrite it.
|
||||
bool Extended;
|
||||
};
|
||||
|
||||
// Modifier key state
|
||||
@@ -52,4 +59,8 @@ namespace Drivers::PS2::Keyboard {
|
||||
// Inject a key event from an external source (e.g., USB HID keyboard)
|
||||
void InjectKeyEvent(const KeyEvent& event);
|
||||
|
||||
// Current Caps Lock state. Injecting drivers (USB HID) need this to
|
||||
// compute ASCII, since the lock is toggled centrally here.
|
||||
bool GetCapsLock();
|
||||
|
||||
};
|
||||
|
||||
@@ -200,20 +200,28 @@ namespace Drivers::USB::HidKeyboard {
|
||||
return (hidUsage >= 0x39 && hidUsage <= 0x53);
|
||||
}
|
||||
|
||||
// Keys that arrive E0-prefixed on a PS/2 keyboard. They share a scancode
|
||||
// with a main-block key (keypad "/" with "/", keypad Enter with Enter), so
|
||||
// they must be flagged or a keyboard layout would rewrite them.
|
||||
static bool IsExtendedKey(uint8_t hidUsage) {
|
||||
return hidUsage == 0x54 || hidUsage == 0x58; // KP /, KP Enter
|
||||
}
|
||||
|
||||
// Build and inject a KeyEvent into the PS/2 keyboard buffer
|
||||
static void InjectKey(uint8_t scancode, bool pressed, uint8_t modifiers,
|
||||
bool nonChar = false) {
|
||||
bool nonChar = false, bool extended = false) {
|
||||
bool shift = (modifiers & (MOD_LEFT_SHIFT | MOD_RIGHT_SHIFT)) != 0;
|
||||
bool ctrl = (modifiers & (MOD_LEFT_CTRL | MOD_RIGHT_CTRL)) != 0;
|
||||
bool alt = (modifiers & (MOD_LEFT_ALT | MOD_RIGHT_ALT)) != 0;
|
||||
bool caps = Drivers::PS2::Keyboard::GetCapsLock();
|
||||
|
||||
char ascii = 0;
|
||||
if (!nonChar && scancode < 128 && pressed) {
|
||||
if (shift) {
|
||||
ascii = g_ScancodeToAsciiShifted[scancode];
|
||||
} else {
|
||||
ascii = g_ScancodeToAscii[scancode];
|
||||
}
|
||||
// Caps Lock inverts shift for letters only, matching the PS/2 path.
|
||||
char base = g_ScancodeToAscii[scancode];
|
||||
bool upper = shift;
|
||||
if (caps && base >= 'a' && base <= 'z') upper = !shift;
|
||||
ascii = upper ? g_ScancodeToAsciiShifted[scancode] : base;
|
||||
}
|
||||
|
||||
Drivers::PS2::Keyboard::KeyEvent event = {
|
||||
@@ -223,7 +231,9 @@ namespace Drivers::USB::HidKeyboard {
|
||||
.Shift = shift,
|
||||
.Ctrl = ctrl,
|
||||
.Alt = alt,
|
||||
.CapsLock = false
|
||||
.CapsLock = caps, // re-stamped by InjectKeyEvent, which owns the lock
|
||||
.AltGr = (modifiers & MOD_RIGHT_ALT) != 0,
|
||||
.Extended = extended
|
||||
};
|
||||
|
||||
Drivers::PS2::Keyboard::InjectKeyEvent(event);
|
||||
@@ -238,7 +248,9 @@ namespace Drivers::USB::HidKeyboard {
|
||||
.Shift = (modifiers & (MOD_LEFT_SHIFT | MOD_RIGHT_SHIFT)) != 0,
|
||||
.Ctrl = (modifiers & (MOD_LEFT_CTRL | MOD_RIGHT_CTRL)) != 0,
|
||||
.Alt = (modifiers & (MOD_LEFT_ALT | MOD_RIGHT_ALT)) != 0,
|
||||
.CapsLock = false
|
||||
.CapsLock = Drivers::PS2::Keyboard::GetCapsLock(),
|
||||
.AltGr = (modifiers & MOD_RIGHT_ALT) != 0,
|
||||
.Extended = false
|
||||
};
|
||||
|
||||
Drivers::PS2::Keyboard::InjectKeyEvent(event);
|
||||
@@ -271,7 +283,7 @@ namespace Drivers::USB::HidKeyboard {
|
||||
|
||||
uint8_t scancode = g_HidToScancode[key];
|
||||
if (scancode != 0) {
|
||||
InjectKey(scancode, false, modifiers, IsNonCharKey(key));
|
||||
InjectKey(scancode, false, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
||||
}
|
||||
g_PrevKeys[i] = 0;
|
||||
}
|
||||
@@ -354,7 +366,7 @@ namespace Drivers::USB::HidKeyboard {
|
||||
if (!KeyInArray(key, g_PrevKeys)) {
|
||||
uint8_t scancode = g_HidToScancode[key];
|
||||
if (scancode != 0) {
|
||||
InjectKey(scancode, true, modifiers, IsNonCharKey(key));
|
||||
InjectKey(scancode, true, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
||||
g_RepeatKey = key;
|
||||
g_RepeatStartMs = now;
|
||||
g_LastRepeatMs = 0;
|
||||
@@ -373,7 +385,7 @@ namespace Drivers::USB::HidKeyboard {
|
||||
if (!KeyInArray(key, keys)) {
|
||||
uint8_t scancode = g_HidToScancode[key];
|
||||
if (scancode != 0) {
|
||||
InjectKey(scancode, false, modifiers, IsNonCharKey(key));
|
||||
InjectKey(scancode, false, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
||||
}
|
||||
if (key == g_RepeatKey) {
|
||||
g_RepeatKey = 0;
|
||||
@@ -405,7 +417,7 @@ namespace Drivers::USB::HidKeyboard {
|
||||
(now - g_LastRepeatMs) >= TYPEMATIC_PERIOD_MS) {
|
||||
uint8_t scancode = g_HidToScancode[g_RepeatKey];
|
||||
if (scancode != 0) {
|
||||
InjectKey(scancode, true, g_PrevModifiers, IsNonCharKey(g_RepeatKey));
|
||||
InjectKey(scancode, true, g_PrevModifiers, IsNonCharKey(g_RepeatKey), IsExtendedKey(g_RepeatKey));
|
||||
}
|
||||
g_LastRepeatMs = now;
|
||||
}
|
||||
|
||||
@@ -82,13 +82,17 @@ License: MIT License
|
||||
|
||||
|
||||
================================================================================
|
||||
3. stb_image (JPEG image decoding)
|
||||
3. stb_image and stb_truetype (image decoding and font rasterisation)
|
||||
================================================================================
|
||||
Used as the single-header image decoder behind MontaukOS image/JPEG support
|
||||
(programs/include/gui/stb_image.h). This is the "libjpeg" component referenced in
|
||||
the build; it is in fact stb_image by Sean Barrett.
|
||||
|
||||
stb_image is dual-licensed (MIT OR public domain). MontaukOS relies on the MIT
|
||||
stb_truetype, by the same author, is the TrueType rasteriser linked into the
|
||||
graphical applications and into the NetSurf browser port, where it renders the
|
||||
system fonts.
|
||||
|
||||
Both are dual-licensed (MIT OR public domain). MontaukOS relies on the MIT
|
||||
option, reproduced here:
|
||||
|
||||
License: MIT License
|
||||
@@ -498,6 +502,159 @@ notices and license terms; those notices remain applicable.
|
||||
including MontaukOS's GCC and Binutils port patches, is available in the
|
||||
MontaukOS source tree.
|
||||
|
||||
|
||||
================================================================================
|
||||
19. NetSurf (web browser)
|
||||
================================================================================
|
||||
Shipped as the bundled "netsurf" application (0:/apps/netsurf/netsurf.elf), built
|
||||
from NetSurf 3.11 (git 39da3c3a4). The MontaukOS build reuses NetSurf's monkey
|
||||
frontend glue with a native MontaukOS window frontend, replaces the libcurl and
|
||||
OpenSSL fetchers with a BearSSL-backed one, and builds without JavaScript. The
|
||||
port's patches, compatibility shims, and frontend sources are in the mtkports
|
||||
tree under www-client/netsurf/.
|
||||
|
||||
The resource tree installed at 0:/apps/netsurf/res/ (Messages, default.css,
|
||||
quirks.css, and the bundled artwork) is part of NetSurf and is covered by this
|
||||
notice.
|
||||
|
||||
Copyright (C) the NetSurf Browser Project and its respective contributors.
|
||||
Upstream: https://www.netsurf-browser.org/
|
||||
https://github.com/netsurf-browser/netsurf
|
||||
|
||||
License: GNU General Public License, version 2 (GPLv2), for the source code,
|
||||
documentation, translatable message files, and UI definitions; MIT License for
|
||||
the visual artwork.
|
||||
|
||||
NetSurf is free software; you can redistribute it and/or modify it under the
|
||||
terms of the GNU General Public License as published by the Free Software
|
||||
Foundation; version 2 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
Upstream NetSurf grants a special exception permitting the code to be linked
|
||||
against the OpenSSL project's "OpenSSL" library. The MontaukOS port does not
|
||||
link OpenSSL -- HTTPS is provided by BearSSL (section 2) -- so that exception
|
||||
is not relied upon here.
|
||||
|
||||
IMPORTANT: The NetSurf application (netsurf.elf) is licensed under GPLv2,
|
||||
separately from MontaukOS itself. The MontaukOS Software License does NOT apply
|
||||
to netsurf.elf or to the NetSurf source. The full GPLv2 license text is
|
||||
included on the MontaukOS ISO at 0:/os/licenses/GPL-2.0.txt.
|
||||
|
||||
WRITTEN OFFER FOR SOURCE: The complete corresponding source code for the
|
||||
NetSurf application, including the MontaukOS port's patches and frontend
|
||||
sources, under the terms of GPLv2, is available on request and is provided
|
||||
alongside MontaukOS distributions. Contact: [email protected].
|
||||
|
||||
|
||||
================================================================================
|
||||
20. NetSurf project libraries
|
||||
================================================================================
|
||||
Linked into the NetSurf application: libcss (CSS parsing and selection), libdom
|
||||
(DOM), libhubbub (HTML5 parsing), libparserutils (parser building blocks),
|
||||
libwapcaplet (interned strings), libnsutils (time and base64 helpers), libnslog
|
||||
(filtered logging), libnspsl (public suffix list), libnsgif (GIF decoding), and
|
||||
libnsbmp (BMP/ICO decoding).
|
||||
|
||||
Copyright (C) 2007-2008 J-M Bell (libparserutils, libhubbub, libcss, libdom)
|
||||
Copyright (C) 2009 The NetSurf Browser Project (libwapcaplet)
|
||||
Copyright (C) 2014, 2016 Vincent Sanders (libnsutils, libnslog, libnspsl)
|
||||
Copyright (C) 2004, 2006 Richard Wilson (libnsgif, libnsbmp)
|
||||
Copyright (C) 2008 Sean Fox (libnsgif, libnsbmp)
|
||||
Copyright (C) 2013-2021 Michael Drake (libnsgif)
|
||||
Copyright (C) the respective NetSurf Browser Project contributors.
|
||||
Upstream: https://www.netsurf-browser.org/projects/
|
||||
|
||||
License: MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================================
|
||||
21. utf8proc
|
||||
================================================================================
|
||||
Linked into the NetSurf application for Unicode normalisation and case folding,
|
||||
via the NetSurf project's libutf8proc fork.
|
||||
|
||||
Copyright (C) 2014-2019 Steven G. Johnson, Jiahao Chen, Tony Kelman, Jonas
|
||||
Fonseca, and other contributors listed in the git history.
|
||||
Copyright (C) 2006-2013 Jan Behrens and the Public Software Group e. V.,
|
||||
Berlin, Germany (original utf8proc).
|
||||
Upstream: https://juliastrings.github.io/utf8proc/
|
||||
|
||||
License: MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
This library also incorporates Unicode Character Database data, which is
|
||||
distributed under the Unicode, Inc. license agreement for data files and
|
||||
software; that notice is reproduced in the upstream LICENSE.md.
|
||||
|
||||
|
||||
================================================================================
|
||||
22. zlib (compression)
|
||||
================================================================================
|
||||
Linked into the NetSurf application for HTTP content decoding and PNG/gzip
|
||||
stream decompression.
|
||||
|
||||
Copyright (C) 1995-2022 Jean-loup Gailly and Mark Adler
|
||||
Upstream: https://zlib.net/
|
||||
|
||||
License: zlib License
|
||||
|
||||
This software is provided 'as-is', without any express or implied warranty.
|
||||
In no event will the authors be held liable for any damages arising from the
|
||||
use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
Jean-loup Gailly Mark Adler
|
||||
[email protected] [email protected]
|
||||
|
||||
================================================================================
|
||||
For licensing inquiries regarding MontaukOS itself, contact: [email protected]
|
||||
================================================================================
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
<hr>
|
||||
<ul>
|
||||
<li><a href="#latest">Latest Release</a></li>
|
||||
<li><a href="#v019">v0.1.9</a></li>
|
||||
<li><a href="#v018">v0.1.8</a></li>
|
||||
<li><a href="#v017">v0.1.7</a></li>
|
||||
<li><a href="#v016">v0.1.6</a></li>
|
||||
@@ -139,6 +140,89 @@
|
||||
<hr>
|
||||
|
||||
<h2 id="latest">Latest Release</h2>
|
||||
<div class="box release-card" id="v019">
|
||||
<h3>MontaukOS 0.1.9</h3>
|
||||
<p class="release-meta">August 13, 2026</p>
|
||||
<p>It is recommended that users run MontaukOS in a virtual machine rather than on real hardware.</p>
|
||||
<h4>Links</h4>
|
||||
<ul>
|
||||
<li><a href="downloads/montauk-0.1.9-x86_64.iso">Download ISO</a> (29 MB) - recommended</li>
|
||||
<li><a href="downloads/montauk-0.1.9-sdk-x86_64.iso">Download ISO with Montauk SDK</a> (194 MB) - includes gcc/g++/binutils toolchain</li>
|
||||
<li><a href="license.html">License</a></li>
|
||||
<li><a href="docs/usersmanual/index.html">Tutorials</a></li>
|
||||
<li><a href="docs/man/index.html">Manual pages</a></li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
For questions or comments, please mail to [email protected].
|
||||
</blockquote>
|
||||
<h4>New</h4>
|
||||
<ul>
|
||||
<li>Intel Wi-Fi (AX210/AX211) support</li>
|
||||
<li>Wi-Fi support in the desktop panel and the Network settings app</li>
|
||||
<li><code>wifi</code> command-line tool</li>
|
||||
<li>NetSurf web browser ported as a native MontaukOS app</li>
|
||||
<li>SSH server (<code>sshd</code>) with an SSH Server app for managing it</li>
|
||||
<li>NTP client (configured via <code>0:/config/ntp.toml</code> and Time applet)</li>
|
||||
<li><code>init</code> now discovers services from <code>0:/config/init.toml</code> rather than a hardcoded sequence</li>
|
||||
<li>New Display app for managing monitors, modes, and layout</li>
|
||||
<li>Expanded Intel GPU display management in the kernel</li>
|
||||
<li>Audio stack now mixes concurrent streams and switches output devices live</li>
|
||||
<li>New audio device switching UI in Audio app and the desktop panel</li>
|
||||
<li>Filesystems can be mounted and unmounted at runtime</li>
|
||||
<li>File metadata across all filesystems, exposed through a new <code>stat</code> syscall</li>
|
||||
<li>ext2 now saves timestamps and supports <code>utime</code></li>
|
||||
<li>New <code>mkdir</code>, <code>rmdir</code>, and <code>memtest</code> commands</li>
|
||||
<li>Expanded support in the PDF Viewer app</li>
|
||||
<li>Files shows modified dates in the Properties dialog</li>
|
||||
<li>Desktop UI consistency improvements</li>
|
||||
<li>Crash reports now capture a stack snapshot</li>
|
||||
<li>Userspace heap and virtual memory overhauled</li>
|
||||
<li><code>pread</code>, <code>pwrite</code>, <code>strndup</code>, <code>scandir</code>, <code>alphasort</code>, <code>usleep</code>, <code>ftruncate</code>, <code>M_PI</code>, and a non-blocking stdin readiness probe added to libc</li>
|
||||
<li>DOOM moved out of the OS tree into the mtkports project</li>
|
||||
</ul>
|
||||
<h4>Fixed</h4>
|
||||
<ul>
|
||||
<li>Numerous kernel concurrency, interrupt-context, and user virtual-address fixes</li>
|
||||
<li>Hardened the TCP/IP stack and unified the HTTP clients; fixed several networking bugs and regressions</li>
|
||||
<li>Improved Bluetooth reliability</li>
|
||||
<li>Corrected Intel GGTT setup and DP AUX EDID reads</li>
|
||||
<li>IPC handle slots are no longer reused while an object is still being torn down</li>
|
||||
<li>The ramdisk no longer packs symlinks as zero-byte files</li>
|
||||
<li>Fixed a ramdisk readdir inconsistency</li>
|
||||
<li>Processes now inherit their parent's environment</li>
|
||||
<li><code>getcwd</code> reports a truncated buffer as <code>ERANGE</code> instead of silently truncating</li>
|
||||
<li>Improved stdio and argument handling</li>
|
||||
<li>Clarified Intel iwlwifi firmware licensing</li>
|
||||
</ul>
|
||||
<h4>Known issues</h4>
|
||||
<ul>
|
||||
<li>Intel GPU may struggle to wake up from S3 sleep, causing a black screen requiring a reset. S3 sleep is highly experimental and users are discouraged from relying on it.</li>
|
||||
<li>Due to licensing restrictions, the proprietary DOOM WAD cannot be shipped; the DOOM engine warns at launch when no compatible WAD file is present, and game content is unavailable until the user supplies one.</li>
|
||||
</ul>
|
||||
<h4>Included in ISO</h4>
|
||||
<p>Two x86_64 images are published for this release. <code>montauk-0.1.9-sdk-x86_64.iso</code> additionally carries the Montauk SDK (gcc, binutils, etc.)</p>
|
||||
<ul>
|
||||
<li>MontaukOS SMP kernel</li>
|
||||
<li>Userspace bootstrap (<code>0:/os/init.elf</code>) and other essential system utilities</li>
|
||||
<li>MontaukOS desktop environment and graphical apps</li>
|
||||
<li>NetSurf web browser (<code>0:/apps/netsurf</code>)</li>
|
||||
<li>Manual pages (<code>0:/man</code>), readable via <code>man</code> command</li>
|
||||
<li>Service and system configuration (<code>0:/config</code>: init.toml, ssh.toml, ntp.toml, timezonedata.toml)</li>
|
||||
<li>System CA certificates (<code>0:/os/certs/ca-certificates.crt</code>)</li>
|
||||
<li>Intel Bluetooth controller and Wi-Fi (iwlwifi) firmware (<code>0:/os/firmware/intel</code>)</li>
|
||||
<li>Third-party license texts and notices (<code>0:/os/licenses</code>)</li>
|
||||
<li>Tiny C Compiler (<code>tcc</code>) with MontaukOS headers and libraries (<code>0:/sdk/tcc</code>)</li>
|
||||
<li>Lua 5.4 interpreter (<code>lua</code>) with headers and static library (<code>0:/sdk/lua</code>)</li>
|
||||
<li>Montauk SDK - native GCC/G++ and binutils (<code>0:/sdk</code>) - SDK image only</li>
|
||||
<li>Flat Remix icon pack</li>
|
||||
<li>JetBrains Mono, Noto Serif, Roboto, and C059 Roman (open Century Schoolbook) TrueType fonts</li>
|
||||
<li>DOOM engine with SDL_mixer audio support (game data not included)</li>
|
||||
<li>Experimental HTTP server (<code>httpd</code>) and default page (<code>0:/www/index.html</code>)</li>
|
||||
<li>Default wallpaper - blossoms by Nikhil Kumar, Unsplash (<code>0:/os/wallpapers</code>)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Release Archive</h2>
|
||||
<div class="box release-card" id="v018">
|
||||
<h3>MontaukOS 0.1.8</h3>
|
||||
<p class="release-meta">July 18, 2026</p>
|
||||
@@ -197,7 +281,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Release Archive</h2>
|
||||
<div class="box release-card" id="v017">
|
||||
<h3>MontaukOS 0.1.7</h3>
|
||||
<p class="release-meta">July 9, 2026</p>
|
||||
|
||||
@@ -155,14 +155,21 @@ termination.</p>
|
||||
|
||||
<p>The Software incorporates or is distributed alongside third-party components
|
||||
that remain the property of their respective owners and are licensed under
|
||||
their own terms, including the Limine bootloader, BearSSL, stb_image, the DOOM
|
||||
engine (via doomgeneric), the Tiny C Compiler, Lua, the GNU Compiler Collection
|
||||
(GCC), GNU Binutils, the Flat Remix icon theme, the bundled fonts, the Mozilla
|
||||
CA certificate bundle, and Intel Bluetooth firmware. GCC and GNU Binutils are
|
||||
their own terms, including the Limine bootloader, Flanterm, BearSSL, stb_image
|
||||
and stb_truetype, the DOOM engine (via doomgeneric), the NetSurf web browser and
|
||||
the NetSurf project libraries (libcss, libdom, libhubbub, libparserutils,
|
||||
libwapcaplet, libnsutils, libnslog, libnspsl, libnsgif, libnsbmp), utf8proc,
|
||||
zlib, the Tiny C Compiler, Lua, the GNU Compiler Collection (GCC), GNU Binutils,
|
||||
the Flat Remix icon theme, the bundled fonts, the Mozilla CA certificate bundle,
|
||||
Intel Bluetooth and Wi-Fi (iwlwifi) firmware, and the default wallpaper
|
||||
photograph. GCC and GNU Binutils are
|
||||
included as the native MontaukOS SDK toolchain (GCC 14.2.0 and Binutils 2.43.1)
|
||||
under the GNU GPLv3, with the GCC Runtime Library Exception where applicable.
|
||||
Copyright (C) 1987-2024 Free Software Foundation, Inc.; additional copyrights
|
||||
belong to the respective GCC, GNU Binutils, and other contributors.
|
||||
belong to the respective GCC, GNU Binutils, and other contributors. The NetSurf
|
||||
application is included under the GNU GPLv2; its visual artwork is under the MIT
|
||||
License. Copyright (C) the NetSurf Browser Project and its respective
|
||||
contributors.
|
||||
Nothing in this license modifies, supersedes, or limits the license terms
|
||||
applicable to those components; to the extent of any conflict, the third-party
|
||||
license controls for that component. The applicable copyright notices and
|
||||
|
||||
@@ -71,14 +71,21 @@ obtained by others are unaffected. Sections 3, 5, 6, and 7 survive termination.
|
||||
-------------------------
|
||||
The Software incorporates or is distributed alongside third-party components
|
||||
that remain the property of their respective owners and are licensed under
|
||||
their own terms, including the Limine bootloader, BearSSL, stb_image, the DOOM
|
||||
engine (via doomgeneric), the Tiny C Compiler, Lua, the GNU Compiler Collection
|
||||
(GCC), GNU Binutils, the Flat Remix icon theme, the bundled fonts, the Mozilla
|
||||
CA certificate bundle, and Intel Bluetooth firmware. GCC and GNU Binutils are
|
||||
their own terms, including the Limine bootloader, Flanterm, BearSSL, stb_image
|
||||
and stb_truetype, the DOOM engine (via doomgeneric), the NetSurf web browser and
|
||||
the NetSurf project libraries (libcss, libdom, libhubbub, libparserutils,
|
||||
libwapcaplet, libnsutils, libnslog, libnspsl, libnsgif, libnsbmp), utf8proc,
|
||||
zlib, the Tiny C Compiler, Lua, the GNU Compiler Collection (GCC), GNU Binutils,
|
||||
the Flat Remix icon theme, the bundled fonts, the Mozilla CA certificate bundle,
|
||||
Intel Bluetooth and Wi-Fi (iwlwifi) firmware, and the default wallpaper
|
||||
photograph. GCC and GNU Binutils are
|
||||
included as the native MontaukOS SDK toolchain (GCC 14.2.0 and Binutils 2.43.1)
|
||||
under the GNU GPLv3, with the GCC Runtime Library Exception where applicable.
|
||||
Copyright (C) 1987-2024 Free Software Foundation, Inc.; additional copyrights
|
||||
belong to the respective GCC, GNU Binutils, and other contributors.
|
||||
belong to the respective GCC, GNU Binutils, and other contributors. The NetSurf
|
||||
application is included under the GNU GPLv2; its visual artwork is under the MIT
|
||||
License. Copyright (C) the NetSurf Browser Project and its respective
|
||||
contributors.
|
||||
Nothing in this license modifies, supersedes, or limits the license terms
|
||||
applicable to those components; to the extent of any conflict, the third-party
|
||||
license controls for that component. The applicable copyright notices and
|
||||
|
||||
+24
-4
@@ -49,7 +49,7 @@ BINDIR := bin
|
||||
PROGRAMS := $(notdir $(wildcard src/*))
|
||||
|
||||
# Programs with custom Makefiles (built separately).
|
||||
CUSTOM_BUILDS := 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver terminal klog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd
|
||||
CUSTOM_BUILDS := 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display keyboard sshserver terminal klog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd
|
||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||
|
||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||
@@ -79,6 +79,13 @@ CONFIGDIR := data/config
|
||||
CONFIGSRC := $(wildcard $(CONFIGDIR)/*.toml)
|
||||
CONFIGDST := $(patsubst $(CONFIGDIR)/%,$(BINDIR)/config/%,$(CONFIGSRC))
|
||||
|
||||
# Read-only OS data tables bundled into bin/os/data/. These are reference
|
||||
# tables that ship with the OS (keyboard layouts, time zones), not config:
|
||||
# nothing writes them back, so they belong under os/ rather than config/.
|
||||
OSDATADIR := data/osdata
|
||||
OSDATASRC := $(wildcard $(OSDATADIR)/*.toml)
|
||||
OSDATADST := $(patsubst $(OSDATADIR)/%,$(BINDIR)/os/data/%,$(OSDATASRC))
|
||||
|
||||
# Third-party license texts and notices bundled into bin/os/licenses/.
|
||||
LICDIR := data/licenses
|
||||
LICSRC := $(wildcard $(LICDIR)/*.txt)
|
||||
@@ -89,9 +96,9 @@ WPDIR := data/wallpapers
|
||||
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
||||
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 sshserver sshd terminal klog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad 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 keyboard sshserver sshd terminal klog 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 check-syscalls gen-syscalls
|
||||
|
||||
all: bearssl libc libjpeg libjpegwrite tls libloader devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver terminal klog 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) $(FWDST) $(LICDST) $(WPDST)
|
||||
all: bearssl libc libjpeg libjpegwrite tls libloader devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver terminal klog 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)
|
||||
|
||||
# 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)
|
||||
@@ -212,6 +219,10 @@ network: libc
|
||||
display: libc
|
||||
$(MAKE) -C src/display
|
||||
|
||||
# Build keyboard layout settings applet (depends on libc).
|
||||
keyboard: libc
|
||||
$(MAKE) -C src/keyboard
|
||||
|
||||
# Build SSH Server settings applet.
|
||||
sshserver: bearssl libc
|
||||
$(MAKE) -C src/sshserver
|
||||
@@ -275,6 +286,9 @@ fonts:
|
||||
# Copy default system config files into bin/config/.
|
||||
configs: $(CONFIGDST)
|
||||
|
||||
# Copy read-only OS data tables into bin/os/data/.
|
||||
osdata: $(OSDATADST)
|
||||
|
||||
# Build paint standalone GUI tool (depends on libc).
|
||||
paint: libc
|
||||
$(MAKE) -C src/paint
|
||||
@@ -337,7 +351,7 @@ crashpad: libc
|
||||
$(MAKE) -C src/crashpad
|
||||
|
||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||
install-apps: 2048 paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver terminal klog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
||||
install-apps: 2048 paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display keyboard sshserver terminal klog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
||||
../scripts/install_apps.sh
|
||||
|
||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||
@@ -365,6 +379,11 @@ $(BINDIR)/config/%: $(CONFIGDIR)/%
|
||||
mkdir -p $(BINDIR)/config
|
||||
cp $< $@
|
||||
|
||||
# Copy read-only OS data tables into bin/os/data/.
|
||||
$(BINDIR)/os/data/%: $(OSDATADIR)/%
|
||||
mkdir -p $(BINDIR)/os/data
|
||||
cp $< $@
|
||||
|
||||
# Copy third-party license texts into bin/os/licenses/. The notices and
|
||||
# MontaukOS license files are sourced from the website copies so there is a
|
||||
# single file of each to keep updated.
|
||||
@@ -485,6 +504,7 @@ clean:
|
||||
$(MAKE) -C src/video clean
|
||||
$(MAKE) -C src/bluetooth clean
|
||||
$(MAKE) -C src/network clean
|
||||
$(MAKE) -C src/keyboard clean
|
||||
$(MAKE) -C src/terminal clean
|
||||
$(MAKE) -C src/klog clean
|
||||
$(MAKE) -C src/procmgr clean
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# MontaukOS Keyboard Layouts
|
||||
|
||||
# =============================================================================
|
||||
# STRUCTURE
|
||||
# =============================================================================
|
||||
# [registry]
|
||||
# layouts: layout ids, in the order they appear in the applet and in the
|
||||
# panel switcher. "en" is the base layout and is always available
|
||||
# even if this file is missing or malformed.
|
||||
#
|
||||
# [layouts.<id>]
|
||||
# name: full name shown in the settings applet
|
||||
# short_name: 2-3 letter tag shown in the desktop panel
|
||||
# scancodes: PS/2 scancode set 1 codes this layout overrides
|
||||
# base: character produced unshifted, one per scancode
|
||||
# shift: character produced with Shift held, one per scancode
|
||||
# altgr: character produced with AltGr held, one per scancode
|
||||
#
|
||||
# Characters are Windows-1252 byte values.
|
||||
#
|
||||
# A value of 0x00 means "no override" - the key keeps whatever the kernel's
|
||||
# US scancode table produced. This is how a layout declares only the keys it
|
||||
# actually changes.
|
||||
|
||||
[registry]
|
||||
layouts = ["en", "no"]
|
||||
|
||||
# =============================================================================
|
||||
# LAYOUTS
|
||||
# =============================================================================
|
||||
|
||||
[layouts.en]
|
||||
name = "English (US)"
|
||||
short_name = "en"
|
||||
# The US scancode table is built into the kernel, so we don't need to override any keys.
|
||||
|
||||
[layouts.no]
|
||||
name = "Norwegian"
|
||||
short_name = "no"
|
||||
scancodes = [
|
||||
0x03, 0x04, 0x05, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
||||
0x0c, 0x0d, 0x12, 0x1a, 0x1b, 0x27, 0x28, 0x29,
|
||||
0x2b, 0x33, 0x34, 0x35, 0x56,
|
||||
]
|
||||
base = [
|
||||
0x32, 0x33, 0x34, 0x36, 0x37, 0x38, 0x39, 0x30, # 2 3 4 6 7 8 9 0
|
||||
0x2B, 0x5C, 0x00, 0xE5, 0xA8, 0xF8, 0xE6, 0x7C, # + \ . aa ".." oe ae |
|
||||
0x27, 0x2C, 0x2E, 0x2D, 0x3C, # ' , . - <
|
||||
]
|
||||
shift = [
|
||||
0x22, 0x23, 0xA4, 0x26, 0x2F, 0x28, 0x29, 0x3D, # " # cur & / ( ) =
|
||||
0x3F, 0x60, 0x00, 0xC5, 0x5E, 0xD8, 0xC6, 0xA7, # ? ` . AA ^ OE AE sec
|
||||
0x2A, 0x3B, 0x3A, 0x5F, 0x3E, # * ; : _ >
|
||||
]
|
||||
altgr = [
|
||||
0x40, 0xA3, 0x24, 0x00, 0x7B, 0x5B, 0x5D, 0x7D, # @ GBP $ . { [ ] }
|
||||
0x00, 0xB4, 0x80, 0x00, 0x7E, 0x00, 0x00, 0x00, # . ' EUR . ~ . . .
|
||||
0x00, 0x00, 0x00, 0x00, 0x5C, # . . . . backslash
|
||||
]
|
||||
@@ -396,6 +396,13 @@ namespace montauk::abi {
|
||||
bool shift;
|
||||
bool ctrl;
|
||||
bool alt;
|
||||
bool capslock;
|
||||
// Right Alt held, reported separately from alt so keyboard layouts can
|
||||
// use it as AltGr without swallowing left-Alt shortcuts.
|
||||
bool altgr;
|
||||
// Key came from an E0-prefixed scancode (keypad "/" and Enter share a
|
||||
// scancode with the main-block keys). Layout translation must skip it.
|
||||
bool extended;
|
||||
};
|
||||
|
||||
struct MouseState {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "gui/mtk/widgets.hpp"
|
||||
#include "gui/terminal.hpp"
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <montauk/keyboard.h>
|
||||
|
||||
namespace gui {
|
||||
|
||||
@@ -234,6 +235,12 @@ struct DesktopState {
|
||||
uint64_t thermal_last_poll;
|
||||
Rect temp_icon_rect;
|
||||
|
||||
// Keyboard layouts. The desktop translates hardware scan codes before
|
||||
// dispatching events, so embedded and external apps see the same layout.
|
||||
montauk::keyboard::State keyboard;
|
||||
Rect keyboard_layout_rect;
|
||||
uint64_t keyboard_last_poll;
|
||||
|
||||
int screen_w, screen_h;
|
||||
uint32_t* background_cache;
|
||||
int background_cache_pitch;
|
||||
@@ -269,5 +276,6 @@ void desktop_draw_panel(DesktopState* ds);
|
||||
void desktop_draw_window(DesktopState* ds, int idx);
|
||||
void desktop_handle_mouse(DesktopState* ds);
|
||||
void desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key);
|
||||
bool desktop_refresh_keyboard_layouts(DesktopState* ds, bool force);
|
||||
|
||||
} // namespace gui
|
||||
|
||||
@@ -395,7 +395,7 @@ inline void text_input_delete_range(char* text, int* len, int start, int end) {
|
||||
inline bool text_input_char_allowed(char ch, TextInputCharFilter filter = nullptr,
|
||||
void* userdata = nullptr) {
|
||||
unsigned char u = (unsigned char)ch;
|
||||
if (u < 0x20 || u >= 0x7F) return false;
|
||||
if (u < 0x20 || u == 0x7F) return false;
|
||||
return filter == nullptr || filter(ch, userdata);
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,8 @@ struct TextBox {
|
||||
cursor--;
|
||||
text[text_len] = '\0';
|
||||
}
|
||||
} else if (key.ascii >= 32 && key.ascii < 127) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127) {
|
||||
// Printable character
|
||||
if (text_len < 254) {
|
||||
for (int i = text_len; i > cursor; i--) {
|
||||
|
||||
@@ -233,6 +233,9 @@ typedef struct {
|
||||
uint8_t shift;
|
||||
uint8_t ctrl;
|
||||
uint8_t alt;
|
||||
uint8_t capslock;
|
||||
uint8_t altgr;
|
||||
uint8_t extended;
|
||||
} mtk_key_event;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/*
|
||||
* config.h
|
||||
* Config file manager for MontaukOS programs
|
||||
* Loads, modifies, and saves TOML config files from 0:/config/
|
||||
* Loads, modifies, and saves TOML config files from 0:/config/ and
|
||||
* 0:/users/<name>/config/, and reads OS data tables from 0:/os/data/
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
@@ -187,29 +188,13 @@ namespace config {
|
||||
// ---- File operations ----
|
||||
|
||||
// Ensure the config directory exists
|
||||
inline void ensure_dir() {
|
||||
montauk::fmkdir(CONFIG_DIR);
|
||||
}
|
||||
|
||||
// Build full path: "0:/config/<name>.toml"
|
||||
inline void build_path(char* out, int outSz, const char* name) {
|
||||
int p = 0;
|
||||
const char* dir = CONFIG_DIR;
|
||||
while (*dir && p < outSz - 2) out[p++] = *dir++;
|
||||
out[p++] = '/';
|
||||
while (*name && p < outSz - 6) out[p++] = *name++;
|
||||
// Append ".toml"
|
||||
const char* ext = ".toml";
|
||||
while (*ext && p < outSz - 1) out[p++] = *ext++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// Load a config file by name (without extension).
|
||||
// Returns an initialized Doc (empty if file doesn't exist).
|
||||
inline toml::Doc load(const char* name) {
|
||||
char path[128];
|
||||
build_path(path, sizeof(path), name);
|
||||
// ---- Shared file I/O (absolute path in, Doc out) ----
|
||||
// Only the path builders below know where each class of file lives. These
|
||||
// two do the actual work for system config, per-user config and OS data.
|
||||
|
||||
// Read and parse a TOML file at an absolute path.
|
||||
// Returns an initialized Doc (empty if the file is missing or empty).
|
||||
inline toml::Doc load_path(const char* path) {
|
||||
int handle = montauk::open(path);
|
||||
if (handle < 0) {
|
||||
toml::Doc doc;
|
||||
@@ -235,15 +220,9 @@ namespace config {
|
||||
return doc;
|
||||
}
|
||||
|
||||
// Save a Doc to disk as a TOML file.
|
||||
// Creates the file if it doesn't exist.
|
||||
// Returns 0 on success, negative on error.
|
||||
inline int save(const char* name, toml::Doc* doc) {
|
||||
ensure_dir();
|
||||
|
||||
char path[128];
|
||||
build_path(path, sizeof(path), name);
|
||||
|
||||
// Serialize a Doc and write it to an absolute path. The caller creates the
|
||||
// parent directory. Returns 0 on success, negative on error.
|
||||
inline int save_path(const char* path, toml::Doc* doc) {
|
||||
char* text = serialize(doc);
|
||||
int textLen = montauk::slen(text);
|
||||
|
||||
@@ -262,6 +241,42 @@ namespace config {
|
||||
return ret < 0 ? ret : 0;
|
||||
}
|
||||
|
||||
inline void ensure_dir() {
|
||||
montauk::fmkdir(CONFIG_DIR);
|
||||
}
|
||||
|
||||
// Build full path: "0:/config/<name>.toml"
|
||||
inline void build_path(char* out, int outSz, const char* name) {
|
||||
int p = 0;
|
||||
const char* dir = CONFIG_DIR;
|
||||
while (*dir && p < outSz - 2) out[p++] = *dir++;
|
||||
out[p++] = '/';
|
||||
while (*name && p < outSz - 6) out[p++] = *name++;
|
||||
// Append ".toml"
|
||||
const char* ext = ".toml";
|
||||
while (*ext && p < outSz - 1) out[p++] = *ext++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// Load a config file by name (without extension).
|
||||
// Returns an initialized Doc (empty if file doesn't exist).
|
||||
inline toml::Doc load(const char* name) {
|
||||
char path[128];
|
||||
build_path(path, sizeof(path), name);
|
||||
return load_path(path);
|
||||
}
|
||||
|
||||
// Save a Doc to disk as a TOML file.
|
||||
// Creates the file if it doesn't exist.
|
||||
// Returns 0 on success, negative on error.
|
||||
inline int save(const char* name, toml::Doc* doc) {
|
||||
ensure_dir();
|
||||
|
||||
char path[128];
|
||||
build_path(path, sizeof(path), name);
|
||||
return save_path(path, doc);
|
||||
}
|
||||
|
||||
// ---- Per-user config ----
|
||||
|
||||
// Build path: "0:/users/<username>/config/<name>.toml"
|
||||
@@ -298,30 +313,7 @@ namespace config {
|
||||
inline toml::Doc load_user(const char* username, const char* name) {
|
||||
char path[192];
|
||||
build_user_path(path, sizeof(path), username, name);
|
||||
|
||||
int handle = montauk::open(path);
|
||||
if (handle < 0) {
|
||||
toml::Doc doc;
|
||||
doc.init();
|
||||
return doc;
|
||||
}
|
||||
|
||||
uint64_t size = montauk::getsize(handle);
|
||||
if (size == 0) {
|
||||
montauk::close(handle);
|
||||
toml::Doc doc;
|
||||
doc.init();
|
||||
return doc;
|
||||
}
|
||||
|
||||
char* text = (char*)montauk::malloc(size + 1);
|
||||
montauk::read(handle, (uint8_t*)text, 0, size);
|
||||
montauk::close(handle);
|
||||
text[size] = '\0';
|
||||
|
||||
toml::Doc doc = toml::parse(text);
|
||||
montauk::mfree(text);
|
||||
return doc;
|
||||
return load_path(path);
|
||||
}
|
||||
|
||||
// Save a per-user config file
|
||||
@@ -330,21 +322,7 @@ namespace config {
|
||||
|
||||
char path[192];
|
||||
build_user_path(path, sizeof(path), username, name);
|
||||
|
||||
char* text = serialize(doc);
|
||||
int textLen = montauk::slen(text);
|
||||
|
||||
montauk::fdelete(path);
|
||||
int handle = montauk::fcreate(path);
|
||||
if (handle < 0) {
|
||||
montauk::mfree(text);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ret = montauk::fwrite(handle, (const uint8_t*)text, 0, textLen);
|
||||
montauk::close(handle);
|
||||
montauk::mfree(text);
|
||||
return ret < 0 ? ret : 0;
|
||||
return save_path(path, doc);
|
||||
}
|
||||
|
||||
// Delete a config file. Returns 0 on success.
|
||||
@@ -426,4 +404,36 @@ namespace config {
|
||||
}
|
||||
|
||||
} // namespace config
|
||||
|
||||
// ---- Read-only OS data tables ----
|
||||
// Reference data that ships with the OS and is never written back: keyboard
|
||||
// layouts, time zone tables and the like. This lives under 0:/os because it is
|
||||
// OS payload, not configuration -- 0:/config is for state a user or admin
|
||||
// edits. There is deliberately no save() here, so the read-only nature of the
|
||||
// directory is enforced by the API rather than by convention.
|
||||
namespace data {
|
||||
|
||||
static constexpr const char* DATA_DIR = "0:/os/data";
|
||||
|
||||
// Build full path: "0:/os/data/<name>.toml"
|
||||
inline void build_path(char* out, int outSz, const char* name) {
|
||||
int p = 0;
|
||||
const char* dir = DATA_DIR;
|
||||
while (*dir && p < outSz - 2) out[p++] = *dir++;
|
||||
out[p++] = '/';
|
||||
while (*name && p < outSz - 6) out[p++] = *name++;
|
||||
const char* ext = ".toml";
|
||||
while (*ext && p < outSz - 1) out[p++] = *ext++;
|
||||
out[p] = '\0';
|
||||
}
|
||||
|
||||
// Load an OS data table by name (without extension).
|
||||
// Returns an initialized Doc (empty if the file is missing).
|
||||
inline toml::Doc load(const char* name) {
|
||||
char path[128];
|
||||
build_path(path, sizeof(path), name);
|
||||
return config::load_path(path);
|
||||
}
|
||||
|
||||
} // namespace data
|
||||
} // namespace montauk
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* keyboard.h
|
||||
* Keyboard layout registry, per-user selection, and scan-code translation
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/string.h>
|
||||
|
||||
namespace montauk::keyboard {
|
||||
|
||||
inline constexpr int MAX_LAYOUTS = 8;
|
||||
inline constexpr int MAX_KEYS = 64;
|
||||
|
||||
// One overridden key. Characters are Windows-1252 bytes to match the
|
||||
// single-byte GUI text stack; 0 means "no override, keep the kernel's value".
|
||||
struct KeyMap {
|
||||
uint8_t scancode;
|
||||
uint8_t base;
|
||||
uint8_t shift;
|
||||
uint8_t altgr;
|
||||
};
|
||||
|
||||
struct Layout {
|
||||
char id[8];
|
||||
char name[48];
|
||||
char short_name[8];
|
||||
KeyMap keys[MAX_KEYS];
|
||||
int key_count; // 0 = passthrough (the kernel's US table)
|
||||
};
|
||||
|
||||
struct Registry {
|
||||
Layout items[MAX_LAYOUTS];
|
||||
int count;
|
||||
};
|
||||
|
||||
struct State {
|
||||
Registry registry;
|
||||
bool enabled[MAX_LAYOUTS];
|
||||
int active;
|
||||
};
|
||||
|
||||
inline void build_key(char* out, int cap, const char* prefix,
|
||||
const char* id, const char* suffix = nullptr) {
|
||||
int pos = 0;
|
||||
const char* parts[3] = {prefix, id, suffix};
|
||||
for (int part = 0; part < 3; part++) {
|
||||
const char* text = parts[part];
|
||||
if (!text) continue;
|
||||
while (*text && pos < cap - 1) out[pos++] = *text++;
|
||||
}
|
||||
out[pos] = '\0';
|
||||
}
|
||||
|
||||
// Read an array of byte values from the layout table.
|
||||
// Returns the element count, or -1 if the key is missing or malformed.
|
||||
inline int read_bytes(const toml::Doc& doc, const char* key,
|
||||
uint8_t* out, int cap) {
|
||||
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;
|
||||
if (value->ival < 0 || value->ival > 0xFF) return -1;
|
||||
out[i] = (uint8_t)value->ival;
|
||||
}
|
||||
return arr->array.count;
|
||||
}
|
||||
|
||||
// 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
|
||||
// malformed table must never produce a half-applied layout.
|
||||
inline bool load_layout(Layout* out, const char* id, const toml::Doc& doc) {
|
||||
if (!out) return false;
|
||||
*out = {};
|
||||
montauk::strncpy(out->id, id, sizeof(out->id));
|
||||
|
||||
char key[64];
|
||||
build_key(key, sizeof(key), "layouts.", id, ".name");
|
||||
montauk::strncpy(out->name, doc.get_string(key, id), sizeof(out->name));
|
||||
build_key(key, sizeof(key), "layouts.", id, ".short_name");
|
||||
montauk::strncpy(out->short_name, doc.get_string(key, id),
|
||||
sizeof(out->short_name));
|
||||
|
||||
uint8_t scancodes[MAX_KEYS];
|
||||
uint8_t base[MAX_KEYS];
|
||||
uint8_t shift[MAX_KEYS];
|
||||
uint8_t altgr[MAX_KEYS] = {};
|
||||
|
||||
build_key(key, sizeof(key), "layouts.", id, ".scancodes");
|
||||
int count = read_bytes(doc, key, scancodes, MAX_KEYS);
|
||||
if (count < 0) {
|
||||
// No override table at all: a passthrough layout such as "en".
|
||||
out->key_count = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
build_key(key, sizeof(key), "layouts.", id, ".base");
|
||||
if (read_bytes(doc, key, base, MAX_KEYS) != count) return false;
|
||||
build_key(key, sizeof(key), "layouts.", id, ".shift");
|
||||
if (read_bytes(doc, key, shift, MAX_KEYS) != count) return false;
|
||||
build_key(key, sizeof(key), "layouts.", id, ".altgr");
|
||||
int altgr_count = read_bytes(doc, key, altgr, MAX_KEYS);
|
||||
if (altgr_count >= 0 && altgr_count != count) return false;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
out->keys[i].scancode = scancodes[i];
|
||||
out->keys[i].base = base[i];
|
||||
out->keys[i].shift = shift[i];
|
||||
out->keys[i].altgr = altgr[i];
|
||||
}
|
||||
out->key_count = count;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The compiled-in base layout. The kernel's scancode table is already US
|
||||
// English, so this overrides nothing; it exists so the registry is never
|
||||
// empty and input keeps working even with no data file on disk.
|
||||
inline void add_base_layout(Registry* registry) {
|
||||
if (!registry || registry->count >= MAX_LAYOUTS) return;
|
||||
Layout& layout = registry->items[registry->count++];
|
||||
layout = {};
|
||||
montauk::strcpy(layout.id, "en");
|
||||
montauk::strcpy(layout.name, "English (US)");
|
||||
montauk::strcpy(layout.short_name, "en");
|
||||
layout.key_count = 0;
|
||||
}
|
||||
|
||||
inline int find_layout(const Registry& registry, const char* id) {
|
||||
for (int i = 0; i < registry.count; i++)
|
||||
if (montauk::streq(registry.items[i].id, id)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline Registry load_registry() {
|
||||
Registry registry = {};
|
||||
toml::Doc doc = montauk::data::load("keyboard-layouts");
|
||||
|
||||
toml::Value* order = doc.get_array("registry.layouts");
|
||||
if (order) {
|
||||
for (int i = 0; i < order->array.count; i++) {
|
||||
toml::Value* value = order->array.items[i];
|
||||
if (!value || value->type != toml::Type::String || !value->str)
|
||||
continue;
|
||||
if (registry.count >= MAX_LAYOUTS) break;
|
||||
if (find_layout(registry, value->str) >= 0) continue;
|
||||
Layout candidate;
|
||||
if (!load_layout(&candidate, value->str, doc)) continue;
|
||||
registry.items[registry.count++] = candidate;
|
||||
}
|
||||
}
|
||||
doc.destroy();
|
||||
|
||||
// Guarantee a working layout even if the data file is missing, malformed,
|
||||
// or simply omits "en".
|
||||
if (find_layout(registry, "en") < 0) {
|
||||
if (registry.count >= MAX_LAYOUTS) registry.count = MAX_LAYOUTS - 1;
|
||||
for (int i = registry.count; i > 0; i--)
|
||||
registry.items[i] = registry.items[i - 1];
|
||||
registry.count++;
|
||||
Registry base = {};
|
||||
add_base_layout(&base);
|
||||
registry.items[0] = base.items[0];
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
// Re-read only the user's selection, leaving the registry alone. The registry
|
||||
// is read-only OS data that cannot change while the machine is running, so
|
||||
// callers polling for layout changes should use this rather than load_user:
|
||||
// it reads one small file instead of re-parsing the whole layout table.
|
||||
inline void refresh_selection(State* state, const char* username) {
|
||||
if (!state) return;
|
||||
for (int i = 0; i < state->registry.count; i++) state->enabled[i] = false;
|
||||
|
||||
toml::Doc doc = config::load_user(username, "keyboard");
|
||||
for (int i = 0; i < state->registry.count; i++) {
|
||||
char key[48];
|
||||
build_key(key, sizeof(key), "layouts.", state->registry.items[i].id);
|
||||
state->enabled[i] = doc.get_bool(key, i == 0);
|
||||
}
|
||||
const char* active_id = doc.get_string("selection.active", "en");
|
||||
state->active = find_layout(state->registry, active_id);
|
||||
doc.destroy();
|
||||
|
||||
if (state->registry.count == 0) return;
|
||||
if (state->active < 0 || !state->enabled[state->active]) {
|
||||
state->active = 0;
|
||||
while (state->active < state->registry.count
|
||||
&& !state->enabled[state->active])
|
||||
state->active++;
|
||||
}
|
||||
if (state->active >= state->registry.count) {
|
||||
state->active = 0;
|
||||
state->enabled[0] = true;
|
||||
}
|
||||
}
|
||||
|
||||
inline State load_user(const char* username) {
|
||||
State state = {};
|
||||
state.registry = load_registry();
|
||||
refresh_selection(&state, username);
|
||||
return state;
|
||||
}
|
||||
|
||||
inline bool save_user(const char* username, const State& state) {
|
||||
toml::Doc doc;
|
||||
doc.init();
|
||||
for (int i = 0; i < state.registry.count; i++) {
|
||||
char key[48];
|
||||
build_key(key, sizeof(key), "layouts.", state.registry.items[i].id);
|
||||
config::set_bool(&doc, key, state.enabled[i]);
|
||||
}
|
||||
int active = state.active >= 0 && state.active < state.registry.count
|
||||
? state.active : 0;
|
||||
config::set_string(&doc, "selection.active", state.registry.items[active].id);
|
||||
int result = config::save_user(username, "keyboard", &doc);
|
||||
doc.destroy();
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
// NOTE: layouts are per-user by design. The login screen runs before there is
|
||||
// a user, so it stays US-English until it grows its own layout switcher; there
|
||||
// is deliberately no machine-wide "current layout" for it to read, because a
|
||||
// wrong guess there is unrecoverable (you cannot type your password to fix it).
|
||||
|
||||
inline int next_enabled(const State& state, int current) {
|
||||
if (state.registry.count <= 0) return 0;
|
||||
for (int step = 1; step <= state.registry.count; step++) {
|
||||
int candidate = (current + step) % state.registry.count;
|
||||
if (state.enabled[candidate]) return candidate;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
// Caps Lock is derived rather than declared per key: it applies only where
|
||||
// base and shift are a Windows-1252 lower/upper letter pair. That covers the
|
||||
// accented letters (aa 0xE5 / AA 0xC5) without wrongly upper-casing keys such
|
||||
// as 2 / " where the shifted value is unrelated punctuation.
|
||||
inline constexpr bool is_letter_pair(uint8_t base, uint8_t shift) {
|
||||
if (base == 0 || shift == 0) return false;
|
||||
if (base < 0x61) return false;
|
||||
if (base > 0x7A && base < 0xE0) return false;
|
||||
if (base == 0xF7) return false; // division sign sits inside the range
|
||||
return shift == (uint8_t)(base - 0x20);
|
||||
}
|
||||
|
||||
static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA
|
||||
static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE
|
||||
static_assert(is_letter_pair(0xE6, 0xC6)); // ae / AE
|
||||
static_assert(is_letter_pair('a', 'A'));
|
||||
static_assert(!is_letter_pair('2', '"'));
|
||||
static_assert(!is_letter_pair(0xF7, 0xD7)); // divide / multiply
|
||||
|
||||
inline void translate(const State& state, abi::KeyEvent* key) {
|
||||
if (!key) return;
|
||||
if (state.active < 0 || state.active >= state.registry.count) return;
|
||||
const Layout& layout = state.registry.items[state.active];
|
||||
|
||||
// Extended keys carry a main-block scancode with the E0 prefix stripped
|
||||
// (keypad "/" arrives as 0x35, the same as the main "/"), so translating
|
||||
// them would turn keypad "/" into whatever the layout puts on that key.
|
||||
if (key->extended) return;
|
||||
|
||||
uint8_t scancode = key->scancode & 0x7F;
|
||||
for (int i = 0; i < layout.key_count; i++) {
|
||||
const KeyMap& mapping = layout.keys[i];
|
||||
if (mapping.scancode != scancode) continue;
|
||||
|
||||
uint8_t out;
|
||||
if (key->altgr) {
|
||||
out = mapping.altgr;
|
||||
} else {
|
||||
bool upper = key->shift;
|
||||
if (is_letter_pair(mapping.base, mapping.shift))
|
||||
upper = key->shift != key->capslock;
|
||||
out = upper ? mapping.shift : mapping.base;
|
||||
}
|
||||
if (out != 0) {
|
||||
key->ascii = (char)out;
|
||||
// An AltGr key that produced a character is text, not a shortcut.
|
||||
// Apps gate insertion on !alt (and alt is LeftAlt||RightAlt), so
|
||||
// leaving it set would silently swallow every AltGr character.
|
||||
if (key->altgr) key->alt = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Direct keyboard readers ----
|
||||
// The desktop translates events before routing them to windows, but programs
|
||||
// that read the kernel buffer themselves (terminal, login) never pass through
|
||||
// it and would otherwise always get the US layout. They translate through this
|
||||
// instead. The registry is parsed once; only the small per-user selection file
|
||||
// is re-read, at most once a second, so switching layout in the panel reaches
|
||||
// them shortly afterwards without re-parsing the whole layout table.
|
||||
|
||||
struct DirectInput {
|
||||
State state;
|
||||
bool loaded;
|
||||
uint64_t last_poll;
|
||||
};
|
||||
|
||||
inline DirectInput& direct_input() {
|
||||
static DirectInput input; // zero-initialised POD, so no guard variable
|
||||
return input;
|
||||
}
|
||||
|
||||
inline void translate_direct(abi::KeyEvent* key, const char* username) {
|
||||
DirectInput& input = direct_input();
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (!input.loaded) {
|
||||
input.state = load_user(username);
|
||||
input.loaded = true;
|
||||
input.last_poll = now;
|
||||
} else if (now - input.last_poll >= 1000) {
|
||||
refresh_selection(&input.state, username);
|
||||
input.last_poll = now;
|
||||
}
|
||||
translate(input.state, key);
|
||||
}
|
||||
|
||||
} // namespace montauk::keyboard
|
||||
@@ -352,7 +352,8 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
fm->pathbar_cursor = 0;
|
||||
} else if (key.scancode == 0x4F) {
|
||||
fm->pathbar_cursor = fm->pathbar_len;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127 && fm->pathbar_len < 254) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && fm->pathbar_len < 254) {
|
||||
for (int i = fm->pathbar_len; i > fm->pathbar_cursor; i--)
|
||||
fm->pathbar_buf[i] = fm->pathbar_buf[i - 1];
|
||||
fm->pathbar_buf[fm->pathbar_cursor] = key.ascii;
|
||||
@@ -399,7 +400,8 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
} else if (key.scancode == 0x4F) {
|
||||
// End
|
||||
fm->rename_cursor = fm->rename_len;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127) {
|
||||
// Printable character (reject / and other FS-unsafe chars)
|
||||
if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) {
|
||||
for (int i = fm->rename_len; i > fm->rename_cursor; i--)
|
||||
|
||||
@@ -7,6 +7,27 @@
|
||||
#include "desktop_internal.hpp"
|
||||
#include <montauk/user.h>
|
||||
|
||||
bool gui::desktop_refresh_keyboard_layouts(DesktopState* ds, bool force) {
|
||||
if (!ds) return false;
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (!force && now - ds->keyboard_last_poll < 1000) return false;
|
||||
|
||||
// Only the selection can change while we run: the layout tables are
|
||||
// read-only OS data, so there is no reason to re-parse them every second.
|
||||
int prev_active = ds->keyboard.active;
|
||||
bool prev_enabled[montauk::keyboard::MAX_LAYOUTS];
|
||||
for (int i = 0; i < montauk::keyboard::MAX_LAYOUTS; i++)
|
||||
prev_enabled[i] = ds->keyboard.enabled[i];
|
||||
|
||||
montauk::keyboard::refresh_selection(&ds->keyboard, ds->current_user);
|
||||
|
||||
bool changed = ds->keyboard.active != prev_active;
|
||||
for (int i = 0; !changed && i < ds->keyboard.registry.count; i++)
|
||||
changed = ds->keyboard.enabled[i] != prev_enabled[i];
|
||||
ds->keyboard_last_poll = now;
|
||||
return changed;
|
||||
}
|
||||
|
||||
static bool try_unlock(DesktopState* ds) {
|
||||
ds->lock_show_error = false;
|
||||
|
||||
@@ -85,7 +106,8 @@ static void handle_lock_keyboard(DesktopState* ds, const montauk::abi::KeyEvent&
|
||||
}
|
||||
|
||||
// Printable characters
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
if ((unsigned char)key.ascii >= 0x20
|
||||
&& (unsigned char)key.ascii != 0x7F) {
|
||||
if (ds->lock_password_len < 63) {
|
||||
ds->lock_password[ds->lock_password_len] = key.ascii;
|
||||
ds->lock_password_len++;
|
||||
@@ -538,6 +560,22 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keyboard layout indicator: cycle through the layouts enabled in the
|
||||
// Keyboard Settings applet and persist the new active choice.
|
||||
if (ds->keyboard_layout_rect.w > 0
|
||||
&& ds->keyboard_layout_rect.contains(mx, my)) {
|
||||
ds->keyboard.active = montauk::keyboard::next_enabled(
|
||||
ds->keyboard, ds->keyboard.active);
|
||||
montauk::keyboard::save_user(ds->current_user, ds->keyboard);
|
||||
ds->keyboard_last_poll = montauk::get_milliseconds();
|
||||
ds->app_menu_open = false;
|
||||
ds->net_popup_open = false;
|
||||
ds->wifi_popup_open = false;
|
||||
ds->vol_popup_open = false;
|
||||
gui::mtk::context_menu_close(ds->ctx_menu);
|
||||
return;
|
||||
}
|
||||
|
||||
// Volume icon
|
||||
if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) {
|
||||
ds->vol_popup_open = !ds->vol_popup_open;
|
||||
@@ -801,8 +839,12 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
}
|
||||
|
||||
void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key) {
|
||||
montauk::abi::KeyEvent translated_key = key;
|
||||
montauk::keyboard::translate(ds->keyboard, &translated_key);
|
||||
const montauk::abi::KeyEvent& routed_key = translated_key;
|
||||
|
||||
if (ds->screen_locked) {
|
||||
handle_lock_keyboard(ds, key);
|
||||
handle_lock_keyboard(ds, routed_key);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -816,7 +858,7 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
||||
montauk::abi::WinEvent ev;
|
||||
montauk::memset(&ev, 0, sizeof(ev));
|
||||
ev.type = 0; // key
|
||||
ev.key = key;
|
||||
ev.key = routed_key;
|
||||
montauk::win_sendevent(win->ext_win_id, &ev);
|
||||
ds->launcher_open = false;
|
||||
ds->app_menu_open = false;
|
||||
@@ -824,37 +866,37 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
||||
return;
|
||||
}
|
||||
|
||||
if (desktop_handle_launcher_keyboard(ds, key)) {
|
||||
if (desktop_handle_launcher_keyboard(ds, routed_key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Global shortcuts (only on key press)
|
||||
if (key.pressed && key.ctrl && key.alt) {
|
||||
if (key.ascii == 'l' || key.ascii == 'L') {
|
||||
if (routed_key.pressed && routed_key.ctrl && routed_key.alt) {
|
||||
if (routed_key.ascii == 'l' || routed_key.ascii == 'L') {
|
||||
desktop_lock_screen(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 't' || key.ascii == 'T') {
|
||||
if (routed_key.ascii == 't' || routed_key.ascii == 'T') {
|
||||
open_terminal(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'f' || key.ascii == 'F') {
|
||||
if (routed_key.ascii == 'f' || routed_key.ascii == 'F') {
|
||||
open_filemanager(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'c' || key.ascii == 'C') {
|
||||
if (routed_key.ascii == 'c' || routed_key.ascii == 'C') {
|
||||
open_calculator(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'e' || key.ascii == 'E') {
|
||||
if (routed_key.ascii == 'e' || routed_key.ascii == 'E') {
|
||||
open_texteditor(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'w' || key.ascii == 'W') {
|
||||
if (routed_key.ascii == 'w' || routed_key.ascii == 'W') {
|
||||
open_wordprocessor(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'k' || key.ascii == 'K') {
|
||||
if (routed_key.ascii == 'k' || routed_key.ascii == 'K') {
|
||||
open_klog(ds);
|
||||
return;
|
||||
}
|
||||
@@ -868,10 +910,10 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
||||
montauk::abi::WinEvent ev;
|
||||
montauk::memset(&ev, 0, sizeof(ev));
|
||||
ev.type = 0; // key
|
||||
ev.key = key;
|
||||
ev.key = routed_key;
|
||||
montauk::win_sendevent(win->ext_win_id, &ev);
|
||||
} else if (win->on_key) {
|
||||
win->on_key(win, key);
|
||||
win->on_key(win, routed_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +494,8 @@ bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyE
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F && ds->launcher_query_len < 63) {
|
||||
if ((unsigned char)key.ascii >= 0x20
|
||||
&& (unsigned char)key.ascii != 0x7F && ds->launcher_query_len < 63) {
|
||||
ds->launcher_query[ds->launcher_query_len++] = key.ascii;
|
||||
ds->launcher_query[ds->launcher_query_len] = '\0';
|
||||
desktop_update_launcher_results(ds);
|
||||
|
||||
@@ -405,6 +405,10 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
ds->vol_output = montauk::audio_get_output(-1);
|
||||
ds->vol_bt_status = montauk::audio_bt_status(-1);
|
||||
|
||||
ds->keyboard = montauk::keyboard::load_user(ds->current_user);
|
||||
ds->keyboard_layout_rect = {0, 0, 0, 0};
|
||||
ds->keyboard_last_poll = montauk::get_milliseconds();
|
||||
|
||||
ds->closing_ext_count = 0;
|
||||
|
||||
ds->screen_locked = false;
|
||||
@@ -813,6 +817,7 @@ void gui::desktop_run(DesktopState* ds) {
|
||||
}
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
sceneChanged |= desktop_refresh_keyboard_layouts(ds, false);
|
||||
sceneChanged |= desktop_refresh_panel_state(ds, now);
|
||||
|
||||
// Pick up any wallpaper buffer produced by a background loader.
|
||||
|
||||
@@ -108,8 +108,34 @@ void gui::desktop_draw_panel(DesktopState* ds) {
|
||||
int date_x = clock_x - date_w - 10;
|
||||
draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT);
|
||||
|
||||
// Volume icon (to the left of the date)
|
||||
int vol_icon_x = date_x - 16 - 12;
|
||||
// Only render switcher if more than one keyboard layout is installed
|
||||
int enabled_layouts = 0;
|
||||
for (int i = 0; i < ds->keyboard.registry.count; i++)
|
||||
if (ds->keyboard.enabled[i]) enabled_layouts++;
|
||||
|
||||
// Whatever text ends up leftmost is what the volume icon spaces off.
|
||||
int vol_anchor_x = date_x;
|
||||
|
||||
if (enabled_layouts > 1) {
|
||||
const char* layout_short = "en";
|
||||
if (ds->keyboard.active >= 0
|
||||
&& ds->keyboard.active < ds->keyboard.registry.count) {
|
||||
layout_short =
|
||||
ds->keyboard.registry.items[ds->keyboard.active].short_name;
|
||||
}
|
||||
int layout_tw = text_width(layout_short);
|
||||
int layout_x = date_x - layout_tw - 10;
|
||||
// Padded evenly past the glyphs so the click target is comfortable.
|
||||
ds->keyboard_layout_rect = {layout_x - 5, 4, layout_tw + 10, 24};
|
||||
draw_text(fb, layout_x, clock_y, layout_short, colors::PANEL_TEXT);
|
||||
vol_anchor_x = layout_x;
|
||||
} else {
|
||||
ds->keyboard_layout_rect = {0, 0, 0, 0};
|
||||
}
|
||||
|
||||
// Volume icon keeps its 12px icon-to-text gap whether it sits next to the
|
||||
// layout tag or, with one layout, the date.
|
||||
int vol_icon_x = vol_anchor_x - 12 - 16;
|
||||
int vol_icon_y = (PANEL_HEIGHT - 16) / 2;
|
||||
ds->vol_icon_rect = {vol_icon_x, vol_icon_y, 16, 16};
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Makefile for the Keyboard settings applet on MontaukOS
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -Wno-unused-parameter \
|
||||
-Wno-unused-function -ffreestanding -fno-stack-protector -fno-stack-check \
|
||||
-fno-rtti -fno-exceptions -ffunction-sections -fdata-sections -msse -msse2 \
|
||||
-MMD -MP -I $(PROG_INC) -isystem $(PROG_INC)/libc
|
||||
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
TARGET := $(BINDIR)/apps/keyboard/keyboard.elf
|
||||
LIBS := $(LIBDIR)/libc/liblibc.a
|
||||
|
||||
.PHONY: all clean
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(BINDIR)/apps/keyboard
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
-include $(OBJS:.o=.d)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Keyboard settings applet
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/keyboard.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int WIN_W = 620;
|
||||
static constexpr int WIN_H = 460;
|
||||
static constexpr int TAB_H = 36;
|
||||
static constexpr int FOOTER_H = 44;
|
||||
static constexpr int PAD = 18;
|
||||
static constexpr int GAP = 12;
|
||||
static constexpr int BUTTON_W = 88;
|
||||
static constexpr int ROW_H = 36;
|
||||
|
||||
static constexpr int MAP_ROW_H = 24;
|
||||
|
||||
enum Tab { TAB_LAYOUTS = 0, TAB_KEYMAP, TAB_COUNT };
|
||||
static const char* const kTabLabels[TAB_COUNT] = {"Layouts", "Key Map"};
|
||||
static Tab g_tab = TAB_LAYOUTS;
|
||||
|
||||
static WsWindow g_win;
|
||||
static montauk::keyboard::State g_state;
|
||||
static montauk::keyboard::State g_saved;
|
||||
static char g_username[32] = {};
|
||||
static int g_mouse_x = -1;
|
||||
static int g_mouse_y = -1;
|
||||
static char g_status[96] = {};
|
||||
static uint64_t g_status_time = 0;
|
||||
static Color g_accent = colors::ACCENT;
|
||||
|
||||
static mtk::Theme app_theme() { return mtk::make_theme(g_accent); }
|
||||
|
||||
// ==== Geometry ====
|
||||
|
||||
static Rect tab_bar_rect() {
|
||||
return {0, 0, g_win.width, TAB_H};
|
||||
}
|
||||
|
||||
static Rect footer_rect() {
|
||||
return {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
|
||||
}
|
||||
|
||||
static int heading_y() {
|
||||
return TAB_H + 20;
|
||||
}
|
||||
|
||||
static int list_separator_y() {
|
||||
return heading_y() + system_font_height() + 20;
|
||||
}
|
||||
|
||||
static int list_header_y() {
|
||||
return list_separator_y() + 16;
|
||||
}
|
||||
|
||||
static int list_y() {
|
||||
return list_header_y() + system_font_height() + 8;
|
||||
}
|
||||
|
||||
static Rect row_rect(int index) {
|
||||
return {PAD, list_y() + index * ROW_H, g_win.width - PAD * 2, ROW_H};
|
||||
}
|
||||
|
||||
// The checkbox owns only its indicator; the rest of the row activates the
|
||||
// layout, matching how the Display applet's mode rows select on row click.
|
||||
static Rect row_checkbox_rect(int index) {
|
||||
Rect row = row_rect(index);
|
||||
return {row.x + 8, row.y, row.w - 16, row.h};
|
||||
}
|
||||
|
||||
static Rect row_check_hit(int index) {
|
||||
Rect box = mtk::checkbox_indicator_rect(row_checkbox_rect(index));
|
||||
return {box.x - 6, box.y - 6, box.w + 12, box.h + 12};
|
||||
}
|
||||
|
||||
// Rows the key map can show without running into the footer.
|
||||
static int map_visible_rows() {
|
||||
int avail = footer_rect().y - 10 - list_y();
|
||||
int rows = avail / MAP_ROW_H;
|
||||
return rows < 0 ? 0 : rows;
|
||||
}
|
||||
|
||||
static Rect apply_button() {
|
||||
Rect foot = footer_rect();
|
||||
return {foot.x + foot.w - PAD - BUTTON_W, foot.y + 7, BUTTON_W, 30};
|
||||
}
|
||||
|
||||
static Rect revert_button() {
|
||||
Rect apply = apply_button();
|
||||
return {apply.x - GAP - BUTTON_W, apply.y, BUTTON_W, apply.h};
|
||||
}
|
||||
|
||||
// ==== State ====
|
||||
|
||||
static bool state_equal(const montauk::keyboard::State& a,
|
||||
const montauk::keyboard::State& b) {
|
||||
if (a.active != b.active || a.registry.count != b.registry.count) return false;
|
||||
for (int i = 0; i < a.registry.count; i++)
|
||||
if (a.enabled[i] != b.enabled[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool dirty() { return !state_equal(g_state, g_saved); }
|
||||
|
||||
static void set_status(const char* text) {
|
||||
montauk::strncpy(g_status, text, sizeof(g_status));
|
||||
g_status_time = montauk::get_milliseconds();
|
||||
}
|
||||
|
||||
static bool status_visible() {
|
||||
return g_status[0] && montauk::get_milliseconds() - g_status_time < 4000;
|
||||
}
|
||||
|
||||
static int enabled_count() {
|
||||
int count = 0;
|
||||
for (int i = 0; i < g_state.registry.count; i++)
|
||||
if (g_state.enabled[i]) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
static mtk::WidgetState button_state(const Rect& rect, bool enabled = true) {
|
||||
return mtk::widget_state(false, rect.contains(g_mouse_x, g_mouse_y), enabled);
|
||||
}
|
||||
|
||||
// ==== Rendering ====
|
||||
|
||||
static void draw_layouts_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
c.text(PAD, heading_y(),
|
||||
"Enabled layouts appear in the panel switcher.", theme.text_subtle);
|
||||
|
||||
mtk::draw_separator(c, PAD, list_separator_y(),
|
||||
g_win.width - PAD * 2, theme);
|
||||
|
||||
c.text(PAD, list_header_y(), "AVAILABLE LAYOUTS", theme.text_muted);
|
||||
const char* hint = "Click a layout to make it active";
|
||||
c.text(g_win.width - PAD - text_width(hint), list_header_y(),
|
||||
hint, theme.text_muted);
|
||||
|
||||
for (int i = 0; i < g_state.registry.count; i++) {
|
||||
Rect row = row_rect(i);
|
||||
bool active = g_state.active == i;
|
||||
if (active)
|
||||
c.fill_rounded_rect(row.x, row.y + 2, row.w, row.h - 4, 4,
|
||||
theme.accent_soft);
|
||||
|
||||
mtk::draw_checkbox(c, row_checkbox_rect(i),
|
||||
g_state.registry.items[i].name,
|
||||
mtk::check_state(g_state.enabled[i]), theme, true,
|
||||
row_check_hit(i).contains(g_mouse_x, g_mouse_y));
|
||||
|
||||
// Layout tag ("en", "no") after the name, then the active marker.
|
||||
const char* tag = g_state.registry.items[i].short_name;
|
||||
int text_y = row.y + (row.h - system_font_height()) / 2;
|
||||
int name_w = text_width(g_state.registry.items[i].name);
|
||||
c.text(row.x + 8 + 16 + theme.gap_sm + name_w + theme.gap_sm, text_y,
|
||||
tag, theme.text_muted);
|
||||
|
||||
if (active) {
|
||||
const char* state_text = "Active";
|
||||
c.text(row.x + row.w - 10 - text_width(state_text), text_y,
|
||||
state_text, theme.text_subtle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Key Map tab: what the active layout actually remaps. Rows are identified by
|
||||
// the character the key produces unshifted, so no copy of the US scancode
|
||||
// table is needed here to name the physical keys.
|
||||
static void draw_char_cell(Canvas& c, int x, int y, uint8_t byte,
|
||||
const mtk::Theme& theme) {
|
||||
char text[2] = {(char)byte, '\0'};
|
||||
if (byte == 0) {
|
||||
c.text(x, y, "-", theme.text_muted);
|
||||
} else {
|
||||
c.text(x, y, text, theme.text);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_keymap_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
const montauk::keyboard::Layout& layout =
|
||||
g_state.registry.items[g_state.active];
|
||||
|
||||
char heading[96];
|
||||
snprintf(heading, sizeof(heading), "Key map for %s", layout.name);
|
||||
c.text(PAD, heading_y(), heading, theme.text_subtle);
|
||||
mtk::draw_separator(c, PAD, list_separator_y(),
|
||||
g_win.width - PAD * 2, theme);
|
||||
|
||||
if (layout.key_count == 0) {
|
||||
c.text(PAD, list_header_y(),
|
||||
"This layout uses the built-in key map. No keys are remapped.",
|
||||
theme.text_muted);
|
||||
return;
|
||||
}
|
||||
|
||||
int group_w = (g_win.width - PAD * 2 - GAP) / 2;
|
||||
int per_group = (layout.key_count + 1) / 2;
|
||||
int shown = per_group;
|
||||
if (shown > map_visible_rows()) shown = map_visible_rows();
|
||||
const int col[3] = {0, 74, 148};
|
||||
|
||||
for (int group = 0; group < 2; group++) {
|
||||
int gx = PAD + group * (group_w + GAP);
|
||||
c.text(gx + col[0], list_header_y(), "NORMAL", theme.text_muted);
|
||||
c.text(gx + col[1], list_header_y(), "SHIFT", theme.text_muted);
|
||||
c.text(gx + col[2], list_header_y(), "ALTGR", theme.text_muted);
|
||||
|
||||
for (int row = 0; row < shown; row++) {
|
||||
int index = group * per_group + row;
|
||||
if (index >= layout.key_count) break;
|
||||
const montauk::keyboard::KeyMap& key = layout.keys[index];
|
||||
int y = list_y() + row * MAP_ROW_H;
|
||||
draw_char_cell(c, gx + col[0], y, key.base, theme);
|
||||
draw_char_cell(c, gx + col[1], y, key.shift, theme);
|
||||
draw_char_cell(c, gx + col[2], y, key.altgr, theme);
|
||||
}
|
||||
}
|
||||
|
||||
if (shown < per_group) {
|
||||
char note[64];
|
||||
snprintf(note, sizeof(note), "%d more keys not shown",
|
||||
layout.key_count - shown * 2);
|
||||
c.text(PAD, list_y() + shown * MAP_ROW_H + 4, note, theme.text_muted);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_footer(Canvas& c, const mtk::Theme& theme) {
|
||||
Rect foot = footer_rect();
|
||||
c.fill_rect(foot.x, foot.y, foot.w, foot.h, theme.surface);
|
||||
mtk::draw_separator(c, 0, foot.y, g_win.width, theme);
|
||||
|
||||
const char* message = status_visible()
|
||||
? g_status
|
||||
: (dirty() ? "Unsaved keyboard changes" : "Keyboard settings ready");
|
||||
c.text(PAD, foot.y + (foot.h - system_font_height()) / 2,
|
||||
message, dirty() ? theme.text : theme.text_subtle);
|
||||
|
||||
if (g_tab == TAB_LAYOUTS) {
|
||||
mtk::draw_button(c, revert_button(), "Revert", mtk::BUTTON_SECONDARY,
|
||||
button_state(revert_button(), dirty()), theme);
|
||||
mtk::draw_button(c, apply_button(), "Apply", mtk::BUTTON_PRIMARY,
|
||||
button_state(apply_button(), dirty()), theme);
|
||||
}
|
||||
}
|
||||
|
||||
static void render() {
|
||||
mtk::StandaloneHost host(&g_win);
|
||||
Canvas c = host.canvas();
|
||||
mtk::Theme theme = app_theme();
|
||||
c.fill(theme.window_bg);
|
||||
mtk::draw_tab_bar(c, tab_bar_rect(), kTabLabels, TAB_COUNT, (int)g_tab, theme);
|
||||
if (g_tab == TAB_LAYOUTS) draw_layouts_tab(c, theme);
|
||||
else draw_keymap_tab(c, theme);
|
||||
draw_footer(c, theme);
|
||||
host.present();
|
||||
}
|
||||
|
||||
// ==== Input ====
|
||||
|
||||
static void toggle_enabled(int index) {
|
||||
if (g_state.enabled[index] && enabled_count() == 1) {
|
||||
set_status("At least one layout must remain enabled");
|
||||
return;
|
||||
}
|
||||
g_state.enabled[index] = !g_state.enabled[index];
|
||||
if (!g_state.enabled[g_state.active])
|
||||
g_state.active = montauk::keyboard::next_enabled(g_state, g_state.active);
|
||||
}
|
||||
|
||||
static bool handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
g_mouse_x = ev.mouse.x;
|
||||
g_mouse_y = ev.mouse.y;
|
||||
bool pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
if (!pressed) return true;
|
||||
|
||||
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, g_mouse_x, g_mouse_y);
|
||||
if (tab >= 0) {
|
||||
g_tab = (Tab)tab;
|
||||
return true;
|
||||
}
|
||||
if (g_tab != TAB_LAYOUTS) return true;
|
||||
|
||||
for (int i = 0; i < g_state.registry.count; i++) {
|
||||
if (row_check_hit(i).contains(g_mouse_x, g_mouse_y)) {
|
||||
toggle_enabled(i);
|
||||
return true;
|
||||
}
|
||||
if (row_rect(i).contains(g_mouse_x, g_mouse_y)) {
|
||||
if (g_state.enabled[i]) {
|
||||
g_state.active = i;
|
||||
} else {
|
||||
set_status("Enable a layout before making it active");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dirty()) return true;
|
||||
if (revert_button().contains(g_mouse_x, g_mouse_y)) {
|
||||
g_state = g_saved;
|
||||
set_status("Changes reverted");
|
||||
} else if (apply_button().contains(g_mouse_x, g_mouse_y)) {
|
||||
if (montauk::keyboard::save_user(g_username, g_state)) {
|
||||
g_saved = g_state;
|
||||
set_status("Keyboard layouts applied");
|
||||
} else {
|
||||
set_status("Could not save keyboard settings");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init()) montauk::exit(1);
|
||||
if (montauk::getuser(g_username, sizeof(g_username)) <= 0)
|
||||
montauk::strcpy(g_username, "default");
|
||||
g_accent = mtk::load_system_accent();
|
||||
g_state = montauk::keyboard::load_user(g_username);
|
||||
g_saved = g_state;
|
||||
|
||||
if (!g_win.create("Keyboard", WIN_W, WIN_H)) montauk::exit(1);
|
||||
render();
|
||||
while (g_win.id >= 0 && !g_win.closed) {
|
||||
montauk::abi::WinEvent ev;
|
||||
int result = g_win.poll(&ev);
|
||||
if (result < 0) break;
|
||||
if (result == 0) {
|
||||
if (status_visible()) render();
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
if (ev.type == 3) break;
|
||||
if (ev.type == 1) handle_mouse(ev);
|
||||
else if (ev.type == 0 && ev.key.pressed && ev.key.scancode == 0x01)
|
||||
g_win.closed = true;
|
||||
render();
|
||||
}
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[app]
|
||||
id = "keyboard"
|
||||
name = "Keyboard"
|
||||
binary = "keyboard.elf"
|
||||
icon = "preferences-keyboard.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = false
|
||||
|
||||
[desktop]
|
||||
section = "settings"
|
||||
admin_only = false
|
||||
@@ -0,0 +1,2 @@
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
@@ -96,6 +96,10 @@ extern "C" void _start() {
|
||||
while (montauk::is_key_available()) {
|
||||
montauk::abi::KeyEvent key;
|
||||
montauk::getkey(&key);
|
||||
// Deliberately untranslated: the login screen is US-English only
|
||||
// until it grows its own layout switcher. There is no user yet, so
|
||||
// there is no per-user layout to apply, and silently picking one
|
||||
// would leave no way to type a password in a different layout.
|
||||
handle_key(ls, key);
|
||||
key_changed = true;
|
||||
}
|
||||
|
||||
@@ -415,8 +415,9 @@ static char ascii_lower(char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
|
||||
}
|
||||
|
||||
static bool is_printable_ascii(char c) {
|
||||
return c >= 32 && c < 127;
|
||||
static bool is_printable_byte(char c) {
|
||||
unsigned char value = (unsigned char)c;
|
||||
return value >= 32 && value != 127;
|
||||
}
|
||||
|
||||
static bool str_contains_case_insensitive(const char* text, const char* needle) {
|
||||
@@ -1448,7 +1449,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) {
|
||||
else g.filter_active = false;
|
||||
return true;
|
||||
}
|
||||
if (!key.ctrl && !key.alt && is_printable_ascii(key.ascii)) {
|
||||
if (!key.ctrl && !key.alt && is_printable_byte(key.ascii)) {
|
||||
append_filter_char(key.ascii);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -246,7 +246,8 @@ extern "C" void _start() {
|
||||
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
||||
} else if (key.scancode == 0x4D) {
|
||||
if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && g_pathbar_len < 254) {
|
||||
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||
|
||||
@@ -426,7 +426,8 @@ extern "C" void _start() {
|
||||
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
||||
} else if (key.scancode == 0x4D) {
|
||||
if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && g_pathbar_len < 254) {
|
||||
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||
@@ -669,7 +670,8 @@ extern "C" void _start() {
|
||||
redraw = true;
|
||||
}
|
||||
// Printable character: start editing or insert
|
||||
else if (key.ascii >= 32 && key.ascii < 127 && !key.ctrl) {
|
||||
else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && !key.ctrl) {
|
||||
if (!g_editing) {
|
||||
clear_selection();
|
||||
g_editing = true;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/keyboard.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/draw.hpp>
|
||||
@@ -392,6 +393,14 @@ static void term_handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
}
|
||||
|
||||
// Keyboard handling shared by both run modes.
|
||||
// User whose keyboard layout selection console mode follows.
|
||||
static char g_layout_user[32] = "default";
|
||||
|
||||
static void init_layout_user() {
|
||||
if (montauk::getuser(g_layout_user, sizeof(g_layout_user)) <= 0)
|
||||
montauk::strcpy(g_layout_user, "default");
|
||||
}
|
||||
|
||||
static void term_handle_key_core(const montauk::abi::KeyEvent& key,
|
||||
int width, int height) {
|
||||
if (g_tabs.tab_count <= 0) return;
|
||||
@@ -586,6 +595,8 @@ static void run_console() {
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
|
||||
init_layout_user();
|
||||
|
||||
g_tabs.tabs[0] = term_create_tab(cols, rows);
|
||||
if (!g_tabs.tabs[0])
|
||||
montauk::exit(1);
|
||||
@@ -610,6 +621,9 @@ static void run_console() {
|
||||
while (montauk::is_key_available()) {
|
||||
montauk::abi::KeyEvent key;
|
||||
montauk::getkey(&key);
|
||||
// Console mode reads the kernel buffer directly, so it does not
|
||||
// get the desktop's layout translation the windowed path does.
|
||||
montauk::keyboard::translate_direct(&key, g_layout_user);
|
||||
term_handle_key_core(key, sw, sh);
|
||||
redraw = true;
|
||||
if (g_should_exit) break;
|
||||
|
||||
@@ -1063,7 +1063,8 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
||||
}
|
||||
if (key.scancode == 0x4B) { if (g_pathbar_cursor > 0) g_pathbar_cursor--; return true; }
|
||||
if (key.scancode == 0x4D) { if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++; return true; }
|
||||
if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) {
|
||||
if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && g_pathbar_len < 254) {
|
||||
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||
@@ -1214,7 +1215,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
||||
}
|
||||
|
||||
// Printable
|
||||
if (key.ascii >= 32 && key.ascii < 127) {
|
||||
if ((unsigned char)key.ascii >= 32 && (unsigned char)key.ascii != 127) {
|
||||
if (g_has_selection) delete_selection();
|
||||
insert_char(key.ascii);
|
||||
return true;
|
||||
|
||||
@@ -302,7 +302,7 @@ static void sort_countries() {
|
||||
}
|
||||
|
||||
static void load_timezone_data() {
|
||||
g_data = montauk::config::load("timezonedata");
|
||||
g_data = montauk::data::load("timezones");
|
||||
g_data_loaded = true;
|
||||
g_country_count = 0;
|
||||
|
||||
@@ -798,7 +798,7 @@ static void render() {
|
||||
if (g_tab == TAB_TIME_ZONES) {
|
||||
if (g_country_count <= 0) {
|
||||
canvas.text(PAD, TAB_H + 24,
|
||||
"No time zone data was found in 0:/config/timezonedata.toml",
|
||||
"No time zone data was found in 0:/os/data/timezones.toml",
|
||||
theme.danger);
|
||||
} else {
|
||||
Rect country = country_list_rect();
|
||||
|
||||
@@ -628,7 +628,8 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
||||
if (wp->pathbar_cursor < wp->pathbar_len) wp->pathbar_cursor++;
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 32 && key.ascii < 127 && wp->pathbar_len < 254) {
|
||||
if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && wp->pathbar_len < 254) {
|
||||
for (int i = wp->pathbar_len; i > wp->pathbar_cursor; i--)
|
||||
wp->pathbar_text[i] = wp->pathbar_text[i - 1];
|
||||
wp->pathbar_text[wp->pathbar_cursor] = key.ascii;
|
||||
@@ -885,7 +886,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ascii >= 32 && key.ascii < 127) {
|
||||
if ((unsigned char)key.ascii >= 32 && (unsigned char)key.ascii != 127) {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
wp_insert_char(wp, key.ascii);
|
||||
wp_history_checkpoint(wp);
|
||||
|
||||
@@ -32,6 +32,7 @@ APPS=(
|
||||
"bluetooth|apps/scalable/bluetooth.svg"
|
||||
"network|devices/scalable/network-wired.svg"
|
||||
"display|apps/scalable/preferences-desktop-display.svg"
|
||||
"keyboard|categories/scalable/preferences-keyboard.svg"
|
||||
"sshserver|apps/scalable/utilities-terminal.svg"
|
||||
"terminal|apps/scalable/utilities-terminal.svg"
|
||||
"klog|apps/scalable/utilities-terminal.svg"
|
||||
|
||||
@@ -391,6 +391,13 @@ namespace montauk::abi {
|
||||
bool shift;
|
||||
bool ctrl;
|
||||
bool alt;
|
||||
bool capslock;
|
||||
// Right Alt held, reported separately from alt so keyboard layouts can
|
||||
// use it as AltGr without swallowing left-Alt shortcuts.
|
||||
bool altgr;
|
||||
// Key came from an E0-prefixed scancode (keypad "/" and Enter share a
|
||||
// scancode with the main-block keys). Layout translation must skip it.
|
||||
bool extended;
|
||||
};
|
||||
|
||||
struct MouseState {
|
||||
|
||||
@@ -228,6 +228,9 @@ typedef struct {
|
||||
uint8_t shift;
|
||||
uint8_t ctrl;
|
||||
uint8_t alt;
|
||||
uint8_t capslock;
|
||||
uint8_t altgr;
|
||||
uint8_t extended;
|
||||
} mtk_key_event;
|
||||
|
||||
typedef struct {
|
||||
|
||||
Reference in New Issue
Block a user