feat: add keyboard layout support
This commit is contained in:
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 109
|
#define MONTAUK_BUILD_NUMBER 116
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ namespace montauk::abi {
|
|||||||
outEvent->shift = k.Shift;
|
outEvent->shift = k.Shift;
|
||||||
outEvent->ctrl = k.Ctrl;
|
outEvent->ctrl = k.Ctrl;
|
||||||
outEvent->alt = k.Alt;
|
outEvent->alt = k.Alt;
|
||||||
|
outEvent->capslock = k.CapsLock;
|
||||||
|
outEvent->altgr = k.AltGr;
|
||||||
|
outEvent->extended = k.Extended;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char Sys_GetChar() {
|
static char Sys_GetChar() {
|
||||||
|
|||||||
@@ -458,6 +458,13 @@ namespace montauk::abi {
|
|||||||
bool shift;
|
bool shift;
|
||||||
bool ctrl;
|
bool ctrl;
|
||||||
bool alt;
|
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 {
|
struct MouseState {
|
||||||
|
|||||||
@@ -179,7 +179,9 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
||||||
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
||||||
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
||||||
.CapsLock = g_Modifiers.CapsLock
|
.CapsLock = g_Modifiers.CapsLock,
|
||||||
|
.AltGr = g_Modifiers.RightAlt,
|
||||||
|
.Extended = true
|
||||||
};
|
};
|
||||||
PushAndNotify(event);
|
PushAndNotify(event);
|
||||||
|
|
||||||
@@ -237,7 +239,9 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
||||||
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
||||||
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
||||||
.CapsLock = g_Modifiers.CapsLock
|
.CapsLock = g_Modifiers.CapsLock,
|
||||||
|
.AltGr = g_Modifiers.RightAlt,
|
||||||
|
.Extended = false
|
||||||
};
|
};
|
||||||
PushAndNotify(event);
|
PushAndNotify(event);
|
||||||
return;
|
return;
|
||||||
@@ -261,7 +265,9 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
.Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift,
|
||||||
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
.Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl,
|
||||||
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
.Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt,
|
||||||
.CapsLock = g_Modifiers.CapsLock
|
.CapsLock = g_Modifiers.CapsLock,
|
||||||
|
.AltGr = g_Modifiers.RightAlt,
|
||||||
|
.Extended = false
|
||||||
};
|
};
|
||||||
|
|
||||||
PushAndNotify(event);
|
PushAndNotify(event);
|
||||||
@@ -298,7 +304,18 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void InjectKeyEvent(const KeyEvent& event) {
|
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 Ctrl;
|
||||||
bool Alt;
|
bool Alt;
|
||||||
bool CapsLock;
|
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
|
// Modifier key state
|
||||||
@@ -52,4 +59,8 @@ namespace Drivers::PS2::Keyboard {
|
|||||||
// Inject a key event from an external source (e.g., USB HID keyboard)
|
// Inject a key event from an external source (e.g., USB HID keyboard)
|
||||||
void InjectKeyEvent(const KeyEvent& event);
|
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);
|
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
|
// Build and inject a KeyEvent into the PS/2 keyboard buffer
|
||||||
static void InjectKey(uint8_t scancode, bool pressed, uint8_t modifiers,
|
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 shift = (modifiers & (MOD_LEFT_SHIFT | MOD_RIGHT_SHIFT)) != 0;
|
||||||
bool ctrl = (modifiers & (MOD_LEFT_CTRL | MOD_RIGHT_CTRL)) != 0;
|
bool ctrl = (modifiers & (MOD_LEFT_CTRL | MOD_RIGHT_CTRL)) != 0;
|
||||||
bool alt = (modifiers & (MOD_LEFT_ALT | MOD_RIGHT_ALT)) != 0;
|
bool alt = (modifiers & (MOD_LEFT_ALT | MOD_RIGHT_ALT)) != 0;
|
||||||
|
bool caps = Drivers::PS2::Keyboard::GetCapsLock();
|
||||||
|
|
||||||
char ascii = 0;
|
char ascii = 0;
|
||||||
if (!nonChar && scancode < 128 && pressed) {
|
if (!nonChar && scancode < 128 && pressed) {
|
||||||
if (shift) {
|
// Caps Lock inverts shift for letters only, matching the PS/2 path.
|
||||||
ascii = g_ScancodeToAsciiShifted[scancode];
|
char base = g_ScancodeToAscii[scancode];
|
||||||
} else {
|
bool upper = shift;
|
||||||
ascii = g_ScancodeToAscii[scancode];
|
if (caps && base >= 'a' && base <= 'z') upper = !shift;
|
||||||
}
|
ascii = upper ? g_ScancodeToAsciiShifted[scancode] : base;
|
||||||
}
|
}
|
||||||
|
|
||||||
Drivers::PS2::Keyboard::KeyEvent event = {
|
Drivers::PS2::Keyboard::KeyEvent event = {
|
||||||
@@ -223,7 +231,9 @@ namespace Drivers::USB::HidKeyboard {
|
|||||||
.Shift = shift,
|
.Shift = shift,
|
||||||
.Ctrl = ctrl,
|
.Ctrl = ctrl,
|
||||||
.Alt = alt,
|
.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);
|
Drivers::PS2::Keyboard::InjectKeyEvent(event);
|
||||||
@@ -238,7 +248,9 @@ namespace Drivers::USB::HidKeyboard {
|
|||||||
.Shift = (modifiers & (MOD_LEFT_SHIFT | MOD_RIGHT_SHIFT)) != 0,
|
.Shift = (modifiers & (MOD_LEFT_SHIFT | MOD_RIGHT_SHIFT)) != 0,
|
||||||
.Ctrl = (modifiers & (MOD_LEFT_CTRL | MOD_RIGHT_CTRL)) != 0,
|
.Ctrl = (modifiers & (MOD_LEFT_CTRL | MOD_RIGHT_CTRL)) != 0,
|
||||||
.Alt = (modifiers & (MOD_LEFT_ALT | MOD_RIGHT_ALT)) != 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);
|
Drivers::PS2::Keyboard::InjectKeyEvent(event);
|
||||||
@@ -271,7 +283,7 @@ namespace Drivers::USB::HidKeyboard {
|
|||||||
|
|
||||||
uint8_t scancode = g_HidToScancode[key];
|
uint8_t scancode = g_HidToScancode[key];
|
||||||
if (scancode != 0) {
|
if (scancode != 0) {
|
||||||
InjectKey(scancode, false, modifiers, IsNonCharKey(key));
|
InjectKey(scancode, false, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
||||||
}
|
}
|
||||||
g_PrevKeys[i] = 0;
|
g_PrevKeys[i] = 0;
|
||||||
}
|
}
|
||||||
@@ -354,7 +366,7 @@ namespace Drivers::USB::HidKeyboard {
|
|||||||
if (!KeyInArray(key, g_PrevKeys)) {
|
if (!KeyInArray(key, g_PrevKeys)) {
|
||||||
uint8_t scancode = g_HidToScancode[key];
|
uint8_t scancode = g_HidToScancode[key];
|
||||||
if (scancode != 0) {
|
if (scancode != 0) {
|
||||||
InjectKey(scancode, true, modifiers, IsNonCharKey(key));
|
InjectKey(scancode, true, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
||||||
g_RepeatKey = key;
|
g_RepeatKey = key;
|
||||||
g_RepeatStartMs = now;
|
g_RepeatStartMs = now;
|
||||||
g_LastRepeatMs = 0;
|
g_LastRepeatMs = 0;
|
||||||
@@ -373,7 +385,7 @@ namespace Drivers::USB::HidKeyboard {
|
|||||||
if (!KeyInArray(key, keys)) {
|
if (!KeyInArray(key, keys)) {
|
||||||
uint8_t scancode = g_HidToScancode[key];
|
uint8_t scancode = g_HidToScancode[key];
|
||||||
if (scancode != 0) {
|
if (scancode != 0) {
|
||||||
InjectKey(scancode, false, modifiers, IsNonCharKey(key));
|
InjectKey(scancode, false, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
||||||
}
|
}
|
||||||
if (key == g_RepeatKey) {
|
if (key == g_RepeatKey) {
|
||||||
g_RepeatKey = 0;
|
g_RepeatKey = 0;
|
||||||
@@ -405,7 +417,7 @@ namespace Drivers::USB::HidKeyboard {
|
|||||||
(now - g_LastRepeatMs) >= TYPEMATIC_PERIOD_MS) {
|
(now - g_LastRepeatMs) >= TYPEMATIC_PERIOD_MS) {
|
||||||
uint8_t scancode = g_HidToScancode[g_RepeatKey];
|
uint8_t scancode = g_HidToScancode[g_RepeatKey];
|
||||||
if (scancode != 0) {
|
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;
|
g_LastRepeatMs = now;
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-4
@@ -49,7 +49,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# 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))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
# 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)
|
CONFIGSRC := $(wildcard $(CONFIGDIR)/*.toml)
|
||||||
CONFIGDST := $(patsubst $(CONFIGDIR)/%,$(BINDIR)/config/%,$(CONFIGSRC))
|
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/.
|
# Third-party license texts and notices bundled into bin/os/licenses/.
|
||||||
LICDIR := data/licenses
|
LICDIR := data/licenses
|
||||||
LICSRC := $(wildcard $(LICDIR)/*.txt)
|
LICSRC := $(wildcard $(LICDIR)/*.txt)
|
||||||
@@ -89,9 +96,9 @@ WPDIR := data/wallpapers
|
|||||||
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
||||||
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
||||||
|
|
||||||
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display 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).
|
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||||
@@ -212,6 +219,10 @@ network: libc
|
|||||||
display: libc
|
display: libc
|
||||||
$(MAKE) -C src/display
|
$(MAKE) -C src/display
|
||||||
|
|
||||||
|
# Build keyboard layout settings applet (depends on libc).
|
||||||
|
keyboard: libc
|
||||||
|
$(MAKE) -C src/keyboard
|
||||||
|
|
||||||
# Build SSH Server settings applet.
|
# Build SSH Server settings applet.
|
||||||
sshserver: bearssl libc
|
sshserver: bearssl libc
|
||||||
$(MAKE) -C src/sshserver
|
$(MAKE) -C src/sshserver
|
||||||
@@ -275,6 +286,9 @@ fonts:
|
|||||||
# Copy default system config files into bin/config/.
|
# Copy default system config files into bin/config/.
|
||||||
configs: $(CONFIGDST)
|
configs: $(CONFIGDST)
|
||||||
|
|
||||||
|
# Copy read-only OS data tables into bin/os/data/.
|
||||||
|
osdata: $(OSDATADST)
|
||||||
|
|
||||||
# Build paint standalone GUI tool (depends on libc).
|
# Build paint standalone GUI tool (depends on libc).
|
||||||
paint: libc
|
paint: libc
|
||||||
$(MAKE) -C src/paint
|
$(MAKE) -C src/paint
|
||||||
@@ -337,7 +351,7 @@ crashpad: libc
|
|||||||
$(MAKE) -C src/crashpad
|
$(MAKE) -C src/crashpad
|
||||||
|
|
||||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
# 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
|
../scripts/install_apps.sh
|
||||||
|
|
||||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||||
@@ -365,6 +379,11 @@ $(BINDIR)/config/%: $(CONFIGDIR)/%
|
|||||||
mkdir -p $(BINDIR)/config
|
mkdir -p $(BINDIR)/config
|
||||||
cp $< $@
|
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
|
# 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
|
# MontaukOS license files are sourced from the website copies so there is a
|
||||||
# single file of each to keep updated.
|
# single file of each to keep updated.
|
||||||
@@ -485,6 +504,7 @@ clean:
|
|||||||
$(MAKE) -C src/video clean
|
$(MAKE) -C src/video clean
|
||||||
$(MAKE) -C src/bluetooth clean
|
$(MAKE) -C src/bluetooth clean
|
||||||
$(MAKE) -C src/network clean
|
$(MAKE) -C src/network clean
|
||||||
|
$(MAKE) -C src/keyboard clean
|
||||||
$(MAKE) -C src/terminal clean
|
$(MAKE) -C src/terminal clean
|
||||||
$(MAKE) -C src/klog clean
|
$(MAKE) -C src/klog clean
|
||||||
$(MAKE) -C src/procmgr 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 shift;
|
||||||
bool ctrl;
|
bool ctrl;
|
||||||
bool alt;
|
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 {
|
struct MouseState {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "gui/mtk/widgets.hpp"
|
#include "gui/mtk/widgets.hpp"
|
||||||
#include "gui/terminal.hpp"
|
#include "gui/terminal.hpp"
|
||||||
#include <Api/Syscall.hpp>
|
#include <Api/Syscall.hpp>
|
||||||
|
#include <montauk/keyboard.h>
|
||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
@@ -234,6 +235,12 @@ struct DesktopState {
|
|||||||
uint64_t thermal_last_poll;
|
uint64_t thermal_last_poll;
|
||||||
Rect temp_icon_rect;
|
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;
|
int screen_w, screen_h;
|
||||||
uint32_t* background_cache;
|
uint32_t* background_cache;
|
||||||
int background_cache_pitch;
|
int background_cache_pitch;
|
||||||
@@ -269,5 +276,6 @@ void desktop_draw_panel(DesktopState* ds);
|
|||||||
void desktop_draw_window(DesktopState* ds, int idx);
|
void desktop_draw_window(DesktopState* ds, int idx);
|
||||||
void desktop_handle_mouse(DesktopState* ds);
|
void desktop_handle_mouse(DesktopState* ds);
|
||||||
void desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key);
|
void desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key);
|
||||||
|
bool desktop_refresh_keyboard_layouts(DesktopState* ds, bool force);
|
||||||
|
|
||||||
} // namespace gui
|
} // 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,
|
inline bool text_input_char_allowed(char ch, TextInputCharFilter filter = nullptr,
|
||||||
void* userdata = nullptr) {
|
void* userdata = nullptr) {
|
||||||
unsigned char u = (unsigned char)ch;
|
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);
|
return filter == nullptr || filter(ch, userdata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,8 @@ struct TextBox {
|
|||||||
cursor--;
|
cursor--;
|
||||||
text[text_len] = '\0';
|
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
|
// Printable character
|
||||||
if (text_len < 254) {
|
if (text_len < 254) {
|
||||||
for (int i = text_len; i > cursor; i--) {
|
for (int i = text_len; i > cursor; i--) {
|
||||||
|
|||||||
@@ -233,6 +233,9 @@ typedef struct {
|
|||||||
uint8_t shift;
|
uint8_t shift;
|
||||||
uint8_t ctrl;
|
uint8_t ctrl;
|
||||||
uint8_t alt;
|
uint8_t alt;
|
||||||
|
uint8_t capslock;
|
||||||
|
uint8_t altgr;
|
||||||
|
uint8_t extended;
|
||||||
} mtk_key_event;
|
} mtk_key_event;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* config.h
|
* config.h
|
||||||
* Config file manager for MontaukOS programs
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -187,29 +188,13 @@ namespace config {
|
|||||||
// ---- File operations ----
|
// ---- File operations ----
|
||||||
|
|
||||||
// Ensure the config directory exists
|
// Ensure the config directory exists
|
||||||
inline void ensure_dir() {
|
// ---- Shared file I/O (absolute path in, Doc out) ----
|
||||||
montauk::fmkdir(CONFIG_DIR);
|
// 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.
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
|
// 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);
|
int handle = montauk::open(path);
|
||||||
if (handle < 0) {
|
if (handle < 0) {
|
||||||
toml::Doc doc;
|
toml::Doc doc;
|
||||||
@@ -235,15 +220,9 @@ namespace config {
|
|||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save a Doc to disk as a TOML file.
|
// Serialize a Doc and write it to an absolute path. The caller creates the
|
||||||
// Creates the file if it doesn't exist.
|
// parent directory. Returns 0 on success, negative on error.
|
||||||
// Returns 0 on success, negative on error.
|
inline int save_path(const char* path, toml::Doc* doc) {
|
||||||
inline int save(const char* name, toml::Doc* doc) {
|
|
||||||
ensure_dir();
|
|
||||||
|
|
||||||
char path[128];
|
|
||||||
build_path(path, sizeof(path), name);
|
|
||||||
|
|
||||||
char* text = serialize(doc);
|
char* text = serialize(doc);
|
||||||
int textLen = montauk::slen(text);
|
int textLen = montauk::slen(text);
|
||||||
|
|
||||||
@@ -262,6 +241,42 @@ namespace config {
|
|||||||
return ret < 0 ? ret : 0;
|
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 ----
|
// ---- Per-user config ----
|
||||||
|
|
||||||
// Build path: "0:/users/<username>/config/<name>.toml"
|
// 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) {
|
inline toml::Doc load_user(const char* username, const char* name) {
|
||||||
char path[192];
|
char path[192];
|
||||||
build_user_path(path, sizeof(path), username, name);
|
build_user_path(path, sizeof(path), username, name);
|
||||||
|
return load_path(path);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save a per-user config file
|
// Save a per-user config file
|
||||||
@@ -330,21 +322,7 @@ namespace config {
|
|||||||
|
|
||||||
char path[192];
|
char path[192];
|
||||||
build_user_path(path, sizeof(path), username, name);
|
build_user_path(path, sizeof(path), username, name);
|
||||||
|
return save_path(path, doc);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a config file. Returns 0 on success.
|
// Delete a config file. Returns 0 on success.
|
||||||
@@ -426,4 +404,36 @@ namespace config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // 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
|
} // 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;
|
fm->pathbar_cursor = 0;
|
||||||
} else if (key.scancode == 0x4F) {
|
} else if (key.scancode == 0x4F) {
|
||||||
fm->pathbar_cursor = fm->pathbar_len;
|
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--)
|
for (int i = fm->pathbar_len; i > fm->pathbar_cursor; i--)
|
||||||
fm->pathbar_buf[i] = fm->pathbar_buf[i - 1];
|
fm->pathbar_buf[i] = fm->pathbar_buf[i - 1];
|
||||||
fm->pathbar_buf[fm->pathbar_cursor] = key.ascii;
|
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) {
|
} else if (key.scancode == 0x4F) {
|
||||||
// End
|
// End
|
||||||
fm->rename_cursor = fm->rename_len;
|
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)
|
// Printable character (reject / and other FS-unsafe chars)
|
||||||
if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) {
|
if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) {
|
||||||
for (int i = fm->rename_len; i > fm->rename_cursor; i--)
|
for (int i = fm->rename_len; i > fm->rename_cursor; i--)
|
||||||
|
|||||||
@@ -7,6 +7,27 @@
|
|||||||
#include "desktop_internal.hpp"
|
#include "desktop_internal.hpp"
|
||||||
#include <montauk/user.h>
|
#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) {
|
static bool try_unlock(DesktopState* ds) {
|
||||||
ds->lock_show_error = false;
|
ds->lock_show_error = false;
|
||||||
|
|
||||||
@@ -85,7 +106,8 @@ static void handle_lock_keyboard(DesktopState* ds, const montauk::abi::KeyEvent&
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Printable characters
|
// 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) {
|
if (ds->lock_password_len < 63) {
|
||||||
ds->lock_password[ds->lock_password_len] = key.ascii;
|
ds->lock_password[ds->lock_password_len] = key.ascii;
|
||||||
ds->lock_password_len++;
|
ds->lock_password_len++;
|
||||||
@@ -538,6 +560,22 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
|||||||
return;
|
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
|
// Volume icon
|
||||||
if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) {
|
if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) {
|
||||||
ds->vol_popup_open = !ds->vol_popup_open;
|
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) {
|
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) {
|
if (ds->screen_locked) {
|
||||||
handle_lock_keyboard(ds, key);
|
handle_lock_keyboard(ds, routed_key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,7 +858,7 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
|||||||
montauk::abi::WinEvent ev;
|
montauk::abi::WinEvent ev;
|
||||||
montauk::memset(&ev, 0, sizeof(ev));
|
montauk::memset(&ev, 0, sizeof(ev));
|
||||||
ev.type = 0; // key
|
ev.type = 0; // key
|
||||||
ev.key = key;
|
ev.key = routed_key;
|
||||||
montauk::win_sendevent(win->ext_win_id, &ev);
|
montauk::win_sendevent(win->ext_win_id, &ev);
|
||||||
ds->launcher_open = false;
|
ds->launcher_open = false;
|
||||||
ds->app_menu_open = false;
|
ds->app_menu_open = false;
|
||||||
@@ -824,37 +866,37 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (desktop_handle_launcher_keyboard(ds, key)) {
|
if (desktop_handle_launcher_keyboard(ds, routed_key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global shortcuts (only on key press)
|
// Global shortcuts (only on key press)
|
||||||
if (key.pressed && key.ctrl && key.alt) {
|
if (routed_key.pressed && routed_key.ctrl && routed_key.alt) {
|
||||||
if (key.ascii == 'l' || key.ascii == 'L') {
|
if (routed_key.ascii == 'l' || routed_key.ascii == 'L') {
|
||||||
desktop_lock_screen(ds);
|
desktop_lock_screen(ds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.ascii == 't' || key.ascii == 'T') {
|
if (routed_key.ascii == 't' || routed_key.ascii == 'T') {
|
||||||
open_terminal(ds);
|
open_terminal(ds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.ascii == 'f' || key.ascii == 'F') {
|
if (routed_key.ascii == 'f' || routed_key.ascii == 'F') {
|
||||||
open_filemanager(ds);
|
open_filemanager(ds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.ascii == 'c' || key.ascii == 'C') {
|
if (routed_key.ascii == 'c' || routed_key.ascii == 'C') {
|
||||||
open_calculator(ds);
|
open_calculator(ds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.ascii == 'e' || key.ascii == 'E') {
|
if (routed_key.ascii == 'e' || routed_key.ascii == 'E') {
|
||||||
open_texteditor(ds);
|
open_texteditor(ds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.ascii == 'w' || key.ascii == 'W') {
|
if (routed_key.ascii == 'w' || routed_key.ascii == 'W') {
|
||||||
open_wordprocessor(ds);
|
open_wordprocessor(ds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (key.ascii == 'k' || key.ascii == 'K') {
|
if (routed_key.ascii == 'k' || routed_key.ascii == 'K') {
|
||||||
open_klog(ds);
|
open_klog(ds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -868,10 +910,10 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
|||||||
montauk::abi::WinEvent ev;
|
montauk::abi::WinEvent ev;
|
||||||
montauk::memset(&ev, 0, sizeof(ev));
|
montauk::memset(&ev, 0, sizeof(ev));
|
||||||
ev.type = 0; // key
|
ev.type = 0; // key
|
||||||
ev.key = key;
|
ev.key = routed_key;
|
||||||
montauk::win_sendevent(win->ext_win_id, &ev);
|
montauk::win_sendevent(win->ext_win_id, &ev);
|
||||||
} else if (win->on_key) {
|
} 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;
|
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++] = key.ascii;
|
||||||
ds->launcher_query[ds->launcher_query_len] = '\0';
|
ds->launcher_query[ds->launcher_query_len] = '\0';
|
||||||
desktop_update_launcher_results(ds);
|
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_output = montauk::audio_get_output(-1);
|
||||||
ds->vol_bt_status = montauk::audio_bt_status(-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->closing_ext_count = 0;
|
||||||
|
|
||||||
ds->screen_locked = false;
|
ds->screen_locked = false;
|
||||||
@@ -813,6 +817,7 @@ void gui::desktop_run(DesktopState* ds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint64_t now = montauk::get_milliseconds();
|
uint64_t now = montauk::get_milliseconds();
|
||||||
|
sceneChanged |= desktop_refresh_keyboard_layouts(ds, false);
|
||||||
sceneChanged |= desktop_refresh_panel_state(ds, now);
|
sceneChanged |= desktop_refresh_panel_state(ds, now);
|
||||||
|
|
||||||
// Pick up any wallpaper buffer produced by a background loader.
|
// 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;
|
int date_x = clock_x - date_w - 10;
|
||||||
draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT);
|
draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT);
|
||||||
|
|
||||||
// Volume icon (to the left of the date)
|
// Only render switcher if more than one keyboard layout is installed
|
||||||
int vol_icon_x = date_x - 16 - 12;
|
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;
|
int vol_icon_y = (PANEL_HEIGHT - 16) / 2;
|
||||||
ds->vol_icon_rect = {vol_icon_x, vol_icon_y, 16, 16};
|
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()) {
|
while (montauk::is_key_available()) {
|
||||||
montauk::abi::KeyEvent key;
|
montauk::abi::KeyEvent key;
|
||||||
montauk::getkey(&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);
|
handle_key(ls, key);
|
||||||
key_changed = true;
|
key_changed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,8 +415,9 @@ static char ascii_lower(char c) {
|
|||||||
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
|
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_printable_ascii(char c) {
|
static bool is_printable_byte(char c) {
|
||||||
return c >= 32 && c < 127;
|
unsigned char value = (unsigned char)c;
|
||||||
|
return value >= 32 && value != 127;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool str_contains_case_insensitive(const char* text, const char* needle) {
|
static bool str_contains_case_insensitive(const char* text, const char* needle) {
|
||||||
@@ -1448,7 +1449,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) {
|
|||||||
else g.filter_active = false;
|
else g.filter_active = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!key.ctrl && !key.alt && is_printable_ascii(key.ascii)) {
|
if (!key.ctrl && !key.alt && is_printable_byte(key.ascii)) {
|
||||||
append_filter_char(key.ascii);
|
append_filter_char(key.ascii);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,7 +246,8 @@ extern "C" void _start() {
|
|||||||
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
||||||
} else if (key.scancode == 0x4D) {
|
} else if (key.scancode == 0x4D) {
|
||||||
if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++;
|
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--)
|
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||||
|
|||||||
@@ -426,7 +426,8 @@ extern "C" void _start() {
|
|||||||
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
||||||
} else if (key.scancode == 0x4D) {
|
} else if (key.scancode == 0x4D) {
|
||||||
if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++;
|
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--)
|
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||||
@@ -669,7 +670,8 @@ extern "C" void _start() {
|
|||||||
redraw = true;
|
redraw = true;
|
||||||
}
|
}
|
||||||
// Printable character: start editing or insert
|
// 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) {
|
if (!g_editing) {
|
||||||
clear_selection();
|
clear_selection();
|
||||||
g_editing = true;
|
g_editing = true;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
#include <montauk/string.h>
|
#include <montauk/string.h>
|
||||||
#include <montauk/heap.h>
|
#include <montauk/heap.h>
|
||||||
|
#include <montauk/keyboard.h>
|
||||||
#include <gui/gui.hpp>
|
#include <gui/gui.hpp>
|
||||||
#include <gui/canvas.hpp>
|
#include <gui/canvas.hpp>
|
||||||
#include <gui/draw.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.
|
// 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,
|
static void term_handle_key_core(const montauk::abi::KeyEvent& key,
|
||||||
int width, int height) {
|
int width, int height) {
|
||||||
if (g_tabs.tab_count <= 0) return;
|
if (g_tabs.tab_count <= 0) return;
|
||||||
@@ -586,6 +595,8 @@ static void run_console() {
|
|||||||
if (cols < 1) cols = 1;
|
if (cols < 1) cols = 1;
|
||||||
if (rows < 1) rows = 1;
|
if (rows < 1) rows = 1;
|
||||||
|
|
||||||
|
init_layout_user();
|
||||||
|
|
||||||
g_tabs.tabs[0] = term_create_tab(cols, rows);
|
g_tabs.tabs[0] = term_create_tab(cols, rows);
|
||||||
if (!g_tabs.tabs[0])
|
if (!g_tabs.tabs[0])
|
||||||
montauk::exit(1);
|
montauk::exit(1);
|
||||||
@@ -610,6 +621,9 @@ static void run_console() {
|
|||||||
while (montauk::is_key_available()) {
|
while (montauk::is_key_available()) {
|
||||||
montauk::abi::KeyEvent key;
|
montauk::abi::KeyEvent key;
|
||||||
montauk::getkey(&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);
|
term_handle_key_core(key, sw, sh);
|
||||||
redraw = true;
|
redraw = true;
|
||||||
if (g_should_exit) break;
|
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 == 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.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--)
|
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||||
@@ -1214,7 +1215,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Printable
|
// 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();
|
if (g_has_selection) delete_selection();
|
||||||
insert_char(key.ascii);
|
insert_char(key.ascii);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ static void sort_countries() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void load_timezone_data() {
|
static void load_timezone_data() {
|
||||||
g_data = montauk::config::load("timezonedata");
|
g_data = montauk::data::load("timezones");
|
||||||
g_data_loaded = true;
|
g_data_loaded = true;
|
||||||
g_country_count = 0;
|
g_country_count = 0;
|
||||||
|
|
||||||
@@ -798,7 +798,7 @@ static void render() {
|
|||||||
if (g_tab == TAB_TIME_ZONES) {
|
if (g_tab == TAB_TIME_ZONES) {
|
||||||
if (g_country_count <= 0) {
|
if (g_country_count <= 0) {
|
||||||
canvas.text(PAD, TAB_H + 24,
|
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);
|
theme.danger);
|
||||||
} else {
|
} else {
|
||||||
Rect country = country_list_rect();
|
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++;
|
if (wp->pathbar_cursor < wp->pathbar_len) wp->pathbar_cursor++;
|
||||||
return;
|
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--)
|
for (int i = wp->pathbar_len; i > wp->pathbar_cursor; i--)
|
||||||
wp->pathbar_text[i] = wp->pathbar_text[i - 1];
|
wp->pathbar_text[i] = wp->pathbar_text[i - 1];
|
||||||
wp->pathbar_text[wp->pathbar_cursor] = key.ascii;
|
wp->pathbar_text[wp->pathbar_cursor] = key.ascii;
|
||||||
@@ -885,7 +886,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
|||||||
return;
|
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);
|
if (wp->has_selection) wp_delete_selection(wp);
|
||||||
wp_insert_char(wp, key.ascii);
|
wp_insert_char(wp, key.ascii);
|
||||||
wp_history_checkpoint(wp);
|
wp_history_checkpoint(wp);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ APPS=(
|
|||||||
"bluetooth|apps/scalable/bluetooth.svg"
|
"bluetooth|apps/scalable/bluetooth.svg"
|
||||||
"network|devices/scalable/network-wired.svg"
|
"network|devices/scalable/network-wired.svg"
|
||||||
"display|apps/scalable/preferences-desktop-display.svg"
|
"display|apps/scalable/preferences-desktop-display.svg"
|
||||||
|
"keyboard|categories/scalable/preferences-keyboard.svg"
|
||||||
"sshserver|apps/scalable/utilities-terminal.svg"
|
"sshserver|apps/scalable/utilities-terminal.svg"
|
||||||
"terminal|apps/scalable/utilities-terminal.svg"
|
"terminal|apps/scalable/utilities-terminal.svg"
|
||||||
"klog|apps/scalable/utilities-terminal.svg"
|
"klog|apps/scalable/utilities-terminal.svg"
|
||||||
|
|||||||
@@ -391,6 +391,13 @@ namespace montauk::abi {
|
|||||||
bool shift;
|
bool shift;
|
||||||
bool ctrl;
|
bool ctrl;
|
||||||
bool alt;
|
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 {
|
struct MouseState {
|
||||||
|
|||||||
@@ -228,6 +228,9 @@ typedef struct {
|
|||||||
uint8_t shift;
|
uint8_t shift;
|
||||||
uint8_t ctrl;
|
uint8_t ctrl;
|
||||||
uint8_t alt;
|
uint8_t alt;
|
||||||
|
uint8_t capslock;
|
||||||
|
uint8_t altgr;
|
||||||
|
uint8_t extended;
|
||||||
} mtk_key_event;
|
} mtk_key_event;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user