Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ doctor:

test:
@PYTHONDONTWRITEBYTECODE=1 python3 "$(ROOT)/macos/Tests/test-cocoa-pinch.py"
@PYTHONDONTWRITEBYTECODE=1 python3 "$(ROOT)/macos/Tests/test-cocoa-iso-keyboard.py"
@PYTHONDONTWRITEBYTECODE=1 python3 "$(ROOT)/macos/Tests/test-virtio-pinch.py"
@PYTHONDONTWRITEBYTECODE=1 python3 "$(ROOT)/tests/test-build-cache.py"
@PYTHONDONTWRITEBYTECODE=1 python3 "$(ROOT)/tests/test-pack-app-icon.py"
Expand Down
6 changes: 6 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ pinch and releases them on cancellation or focus loss; the guest disables
tapping for this gesture-only device. See [pinch zoom](pinch-zoom.md) for the
input contract, existing-guest setup, and integration validation.

Mac keyboard geometry (ANSI / ISO / JIS) is detected once per launch and
given to Cocoa. New and reset factory users also load an overlay that sets
`kb_model=applealu_*`. App upgrade applies the Cocoa swap only; existing
homes keep their current Hyprland input. See
[Mac keyboard](mac-keyboard.md).

The macOS helper opens an authenticated connection to QEMU's private,
single-client machine protocol socket before host sleep and retains that control
session through wake. Before macOS sleeps it synchronously pauses the guest
Expand Down
56 changes: 56 additions & 0 deletions docs/mac-keyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Mac keyboard geometry

Try Omarchy maps the host Mac keyboard class (ANSI / ISO / JIS) into the
guest. That corrects the ISO Section / extra-ISO key inversion for every
layout. For xkeyboard-config Mac vendor layouts (`ch de dk fi fr gb is it
latam nl no pt se us` with an empty variant) it also selects Macintosh
legends.

## Host

`omarchy-vm-helper --host-keyboard-geometry` reports `ansi`, `iso`, or
`jis`. Unknown Carbon classes fail the launch. The launcher exports
`TRYOMARCHY_KEYBOARD` and appends `tryomarchy.keyboard=` on a normal boot
only. Cocoa swaps `KEY_GRAVE` and `KEY_102ND` only when that value is
`iso`. Recovery unsets the env. `--reset-storage-only` skips the probe.

## Guest

New users load `/usr/share/try-omarchy/apple-keyboard-input.lua`, which
sets only `kb_model = "applealu_" .. geometry`. Layout and variant stay
whatever Omarchy setup chose. A missing token is a no-op.

## Existing VMs

App upgrade applies the Cocoa ISO keycode swap immediately. That uninverts
`@#` / `<>` on ISO boards without changing `kb_model`. **Remove any local
`frmac` (or similar) TLDE/LSGT symbol swap first**, or those keys invert
again.

Macintosh legends still need `kb_model`. If a rebuilt guest image installed
`/usr/share/try-omarchy/apple-keyboard-input.lua`, add this to
`~/.config/hypr/input.lua`:

```lua
dofile("/usr/share/try-omarchy/apple-keyboard-input.lua")
```

Otherwise set the model yourself (do not `dofile` a missing path):

```lua
hl.config({
input = {
kb_model = "applealu_iso", -- or applealu_ansi / applealu_jis
},
})
```

Save, then run `hyprctl reload` and `hyprctl configerrors`.

## Validation

`make test` checksums the Cocoa patch, compiles the ISO swap helper, and
runs the guest Lua overlay. The upstream QEMU pin is unchanged. Review
`iso_swap_patch_sha256` in `macos/build-qemu-gpu-runtime.sh` against
`macos/patches/qemu-cocoa-iso-section-grave-swap.patch`. `make runtime`
applies the patch after the existing Cocoa series.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Set kb_model from tryomarchy.keyboard=ansi|iso|jis. Leave layout/variant
-- alone. Unknown or missing tokens are a no-op (never guess iso).

local function host_keyboard_geometry()
local file = io.open("/proc/cmdline", "r")
if not file then
return nil
end
local cmdline = file:read("*a") or ""
file:close()

for token in cmdline:gmatch("%S+") do
local value = token:match("^tryomarchy%.keyboard=(%w+)$")
if value == "ansi" or value == "iso" or value == "jis" then
return value
end
end
return nil
end

local geometry = host_keyboard_geometry()
if not geometry then
return
end

hl.config({
input = {
kb_model = "applealu_" .. geometry,
},
})
3 changes: 3 additions & 0 deletions guest/scripts/materialize-omarchy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ cat >> "$root/etc/skel/.config/hypr/input.lua" <<'EOF'

-- Try Omarchy's host pinch device carries gestures only.
dofile("/usr/share/try-omarchy/pinch-input.lua")

-- Match Apple keyboard geometry (ansi/iso/jis) from the host cmdline.
dofile("/usr/share/try-omarchy/apple-keyboard-input.lua")
EOF
install_file 0644 "$source_dir/default/bashrc" "$root/etc/skel/.bashrc"
mkdir -p "$root/etc/skel/.local/share/applications"
Expand Down
28 changes: 28 additions & 0 deletions guest/tests/apple_keyboard_input_harness.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- Run apple-keyboard-input.lua against a fake /proc/cmdline (arg[1]).
local cmdline = assert(arg[1], "cmdline required")
local overlay = assert(arg[2], "overlay path required")
local recorded = nil

hl = {
config = function(tbl)
recorded = tbl
end,
}

function io.open(path)
if path == "/proc/cmdline" then
return {
read = function()
return cmdline
end,
close = function() end,
}
end
return nil
end

assert(loadfile(overlay))()

if recorded and recorded.input and recorded.input.kb_model then
io.write(recorded.input.kb_model)
end
47 changes: 47 additions & 0 deletions guest/tests/test_apple_keyboard_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Run apple-keyboard-input.lua against fake cmdlines."""

from __future__ import annotations

from pathlib import Path
import shutil
import subprocess
import unittest


GUEST = Path(__file__).resolve().parents[1]
LUA = GUEST / "native-overlay/usr/share/try-omarchy/apple-keyboard-input.lua"
HARNESS = GUEST / "tests/apple_keyboard_input_harness.lua"


class AppleKeyboardInputTests(unittest.TestCase):
def test_lua_geometry_token_parsing(self) -> None:
lua = shutil.which("lua") or shutil.which("lua5.4") or shutil.which("luajit")
if lua is None:
self.skipTest(
"lua is required to execute apple-keyboard-input.lua "
"(install with: brew install lua)"
)

cases = {
"root=/dev/vda rw": None,
"root=/dev/vda tryomarchy.keyboard=iso": "applealu_iso",
"tryomarchy.keyboard=ansi console=hvc0": "applealu_ansi",
"xtryomarchy.keyboard=iso": None,
"tryomarchy.keyboard=isoextra": None,
"tryomarchy.keyboard=jis": "applealu_jis",
"tryomarchy.keyboard=fr": None,
}
for cmdline, expected in cases.items():
completed = subprocess.run(
[lua, str(HARNESS), cmdline, str(LUA)],
check=True,
capture_output=True,
text=True,
)
model = completed.stdout or None
self.assertEqual(model, expected, cmdline)


if __name__ == "__main__":
unittest.main()
10 changes: 10 additions & 0 deletions guest/tests/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,16 @@ def main() -> None:
and 'toggles/flags.lua' in materialize,
"skel hypr toggles seed only flags.lua, not the catalog",
)
apple_keyboard = read(
GUEST / "native-overlay/usr/share/try-omarchy/apple-keyboard-input.lua"
)
check(
'kb_model = "applealu_" .. geometry' in apple_keyboard
and "kb_layout" not in apple_keyboard
and "kb_variant" not in apple_keyboard
and 'dofile("/usr/share/try-omarchy/apple-keyboard-input.lua")' in materialize,
"skel input loads Apple keyboard geometry without overriding layout",
)

configure = read(GUEST / "scripts/configure-rootfs.sh")
check(
Expand Down
28 changes: 28 additions & 0 deletions macos/Sources/OmarchyVMHelper/HostKeyboardGeometry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Carbon

enum HostKeyboardGeometry: String {
case ansi
case iso
case jis

struct UnknownLayoutType: Error, Equatable {
let rawValue: Int
}

static func classify(_ layoutType: Int) throws -> HostKeyboardGeometry {
switch layoutType {
case Int(kKeyboardANSI):
return .ansi
case Int(kKeyboardISO):
return .iso
case Int(kKeyboardJIS):
return .jis
default:
throw UnknownLayoutType(rawValue: layoutType)
}
}

static func detect() throws -> HostKeyboardGeometry {
try classify(Int(KBGetLayoutType(Int16(LMGetKbdType()))))
}
}
13 changes: 12 additions & 1 deletion macos/Sources/OmarchyVMHelper/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Foundation
private var terminationSignalSources: [DispatchSourceSignal] = []

private func usage() -> Never {
fputs("Usage: omarchy-vm-helper --run-qemu [--ephemeral | --reset-storage | --reset-storage-only] [GUEST_DIR] | --bridge-command-super QEMU_PID QMP_SOCKET | --bridge-native-audio QEMU_PID SOCKET ROUTE_DIRECTORY | --bridge-native-authentication QEMU_PID SOCKET | --bridge-native-camera QEMU_PID SOCKET | --bridge-native-clipboard QEMU_PID SOCKET\n", stderr)
fputs("Usage: omarchy-vm-helper --run-qemu [--ephemeral | --reset-storage | --reset-storage-only] [GUEST_DIR] | --host-keyboard-geometry | --bridge-command-super QEMU_PID QMP_SOCKET | --bridge-native-audio QEMU_PID SOCKET ROUTE_DIRECTORY | --bridge-native-authentication QEMU_PID SOCKET | --bridge-native-camera QEMU_PID SOCKET | --bridge-native-clipboard QEMU_PID SOCKET\n", stderr)
exit(64)
}

Expand Down Expand Up @@ -139,6 +139,17 @@ do {
exit(0)
}

if arguments.first == "--host-keyboard-geometry" {
guard arguments.count == 1 else { usage() }
do {
fputs(try HostKeyboardGeometry.detect().rawValue + "\n", stdout)
exit(0)
} catch {
fputs("unknown host Mac keyboard geometry\n", stderr)
exit(1)
}
}

if arguments.first == "--run-qemu" {
guard let request = QEMUGPULaunchRequest(arguments: Array(arguments.dropFirst())) else {
usage()
Expand Down
27 changes: 27 additions & 0 deletions macos/Tests/OmarchyVMHelperTests/HostKeyboardGeometryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Carbon
import Testing
@testable import OmarchyVMHelper

@Suite("Host keyboard geometry")
struct HostKeyboardGeometryTests {
@Test("classify maps only the three Carbon keyboard classes")
func classifyKnownClasses() throws {
#expect(try HostKeyboardGeometry.classify(Int(kKeyboardANSI)) == .ansi)
#expect(try HostKeyboardGeometry.classify(Int(kKeyboardISO)) == .iso)
#expect(try HostKeyboardGeometry.classify(Int(kKeyboardJIS)) == .jis)
}

@Test("classify fails closed on an unknown Carbon class")
func classifyUnknownFails() {
#expect(throws: HostKeyboardGeometry.UnknownLayoutType.self) {
try HostKeyboardGeometry.classify(99)
}
}

@Test("raw values match the guest cmdline token")
func rawValues() {
#expect(HostKeyboardGeometry.ansi.rawValue == "ansi")
#expect(HostKeyboardGeometry.iso.rawValue == "iso")
#expect(HostKeyboardGeometry.jis.rawValue == "jis")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation
import Testing

@Suite("Mac keyboard geometry native contract")
struct MacKeyboardGeometryContractTests {
@Test("Runner publishes helper geometry on the guest cmdline and to Cocoa")
func runnerMapping() throws {
let runner = try source(named: "run-qemu-gpu.sh")
let helper = try source(named: "Sources/OmarchyVMHelper/main.swift")
let detector = try source(named: "Sources/OmarchyVMHelper/HostKeyboardGeometry.swift")
let storage = try source(named: "qemu-persistent-storage.sh")

#expect(runner.contains("--host-keyboard-geometry"))
#expect(runner.contains(
"keyboard_kernel_argument=\" tryomarchy.keyboard=$host_keyboard_geometry\""
))
#expect(runner.contains("export TRYOMARCHY_KEYBOARD=$host_keyboard_geometry"))
#expect(helper.contains("--host-keyboard-geometry"))
#expect(detector.contains("kKeyboardANSI"))
#expect(detector.contains("UnknownLayoutType"))
#expect(storage.contains("tryomarchy.keyboard=*"))
#expect(!runner.contains("/usr/bin/swift"))
}

@Test("Cocoa swaps Grave/102nd only when TRYOMARCHY_KEYBOARD is iso")
func cocoaIsoSwap() throws {
let patch = try source(named: "patches/qemu-cocoa-iso-section-grave-swap.patch")

#expect(patch.contains("strcmp(geometry, \"iso\") == 0"))
#expect(!patch.contains("KBGetLayoutType"))
#expect(patch.contains("return KEY_102ND;"))
#expect(patch.contains("return KEY_GRAVE;"))
}

private func source(named relativePath: String) throws -> String {
let testFile = URL(fileURLWithPath: #filePath)
let macosDirectory = testFile
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
return try String(
contentsOf: macosDirectory.appendingPathComponent(relativePath),
encoding: .utf8
)
}
}
23 changes: 22 additions & 1 deletion macos/Tests/qemu-memory-contract.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ assert_line_pair() {
"$file" || fail "expected adjacent lines [$first] and [$second] in $file"
}

assert_keyboard_lockstep() {
local log=$1
local geometry=$2
[[ $(grep -o "tryomarchy.keyboard=$geometry" "$log" | wc -l | tr -d ' ') == 1 ]] || \
fail "expected exactly one tryomarchy.keyboard=$geometry token in $log"
[[ $(grep -c "^TRYOMARCHY_KEYBOARD=$geometry$" "$log") == 1 ]] || \
fail "Cocoa env must match cmdline token $geometry in $log"
}

test_root=$(mktemp -d '/private/tmp/omarchy-qemu-memory-contract.XXXXXX')
case "$test_root" in
/private/tmp/omarchy-qemu-memory-contract.??????) ;;
Expand All @@ -49,6 +58,14 @@ chmod 644 "$resources/scripts/qemu-port-forwarding.sh"
cat >"$contents/MacOS/omarchy-vm-helper" <<'SH'
#!/bin/bash
set -euo pipefail
if [[ ${1:-} == --host-keyboard-geometry ]]; then
if [[ -n ${FAKE_HOST_KEYBOARD_FAIL:-} ]]; then
printf 'cannot detect host keyboard geometry\n' >&2
exit 1
fi
printf '%s\n' "${FAKE_HOST_KEYBOARD:-iso}"
exit 0
fi
if [[ ${1:-} == --bridge-native-audio \
|| ${1:-} == --bridge-native-authentication \
|| ${1:-} == --bridge-native-clipboard \
Expand Down Expand Up @@ -106,7 +123,10 @@ import sys
import time

arguments = sys.argv[1:]
Path(os.environ["FAKE_QEMU_LOG"]).write_text("\n".join(arguments) + "\n")
geometry = os.environ.get("TRYOMARCHY_KEYBOARD", "")
Path(os.environ["FAKE_QEMU_LOG"]).write_text(
"\n".join(arguments) + f"\nTRYOMARCHY_KEYBOARD={geometry}\n"
)
socket_paths = []
for argument in arguments:
if argument.startswith("unix:"):
Expand Down Expand Up @@ -314,6 +334,7 @@ run_scenario() {
run_scenario default 0
assert_line_pair "$test_root/default/qemu.log" -m 4096M
assert_contains "$(<"$test_root/default/stderr")" '4 GiB RAM'
assert_keyboard_lockstep "$test_root/default/qemu.log" iso

# A whole-GiB choice reaches QEMU verbatim and reads as GiB in the log.
run_scenario six-gib 0 OMARCHY_QEMU_GPU_MEMORY_MIB=6144
Expand Down
Loading