diff --git a/Makefile b/Makefile index 6681a92a..d7612db9 100644 --- a/Makefile +++ b/Makefile @@ -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" diff --git a/docs/architecture.md b/docs/architecture.md index 1b678d84..c77fb722 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/mac-keyboard.md b/docs/mac-keyboard.md new file mode 100644 index 00000000..a698f3c7 --- /dev/null +++ b/docs/mac-keyboard.md @@ -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. diff --git a/guest/native-overlay/usr/share/try-omarchy/apple-keyboard-input.lua b/guest/native-overlay/usr/share/try-omarchy/apple-keyboard-input.lua new file mode 100644 index 00000000..3a56dbeb --- /dev/null +++ b/guest/native-overlay/usr/share/try-omarchy/apple-keyboard-input.lua @@ -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, + }, +}) diff --git a/guest/scripts/materialize-omarchy.sh b/guest/scripts/materialize-omarchy.sh index 1053c8d7..2efc2378 100755 --- a/guest/scripts/materialize-omarchy.sh +++ b/guest/scripts/materialize-omarchy.sh @@ -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" diff --git a/guest/tests/apple_keyboard_input_harness.lua b/guest/tests/apple_keyboard_input_harness.lua new file mode 100644 index 00000000..fa00b61f --- /dev/null +++ b/guest/tests/apple_keyboard_input_harness.lua @@ -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 diff --git a/guest/tests/test_apple_keyboard_input.py b/guest/tests/test_apple_keyboard_input.py new file mode 100644 index 00000000..e2687d06 --- /dev/null +++ b/guest/tests/test_apple_keyboard_input.py @@ -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() diff --git a/guest/tests/verify.py b/guest/tests/verify.py index cfac9174..cca7f70d 100755 --- a/guest/tests/verify.py +++ b/guest/tests/verify.py @@ -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( diff --git a/macos/Sources/OmarchyVMHelper/HostKeyboardGeometry.swift b/macos/Sources/OmarchyVMHelper/HostKeyboardGeometry.swift new file mode 100644 index 00000000..00a523d2 --- /dev/null +++ b/macos/Sources/OmarchyVMHelper/HostKeyboardGeometry.swift @@ -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())))) + } +} diff --git a/macos/Sources/OmarchyVMHelper/main.swift b/macos/Sources/OmarchyVMHelper/main.swift index 2c0046b4..f41f4362 100644 --- a/macos/Sources/OmarchyVMHelper/main.swift +++ b/macos/Sources/OmarchyVMHelper/main.swift @@ -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) } @@ -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() diff --git a/macos/Tests/OmarchyVMHelperTests/HostKeyboardGeometryTests.swift b/macos/Tests/OmarchyVMHelperTests/HostKeyboardGeometryTests.swift new file mode 100644 index 00000000..ba6e8d75 --- /dev/null +++ b/macos/Tests/OmarchyVMHelperTests/HostKeyboardGeometryTests.swift @@ -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") + } +} diff --git a/macos/Tests/OmarchyVMHelperTests/MacKeyboardGeometryContractTests.swift b/macos/Tests/OmarchyVMHelperTests/MacKeyboardGeometryContractTests.swift new file mode 100644 index 00000000..754fc727 --- /dev/null +++ b/macos/Tests/OmarchyVMHelperTests/MacKeyboardGeometryContractTests.swift @@ -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 + ) + } +} diff --git a/macos/Tests/qemu-memory-contract.test.sh b/macos/Tests/qemu-memory-contract.test.sh index fc9b21c0..813db609 100755 --- a/macos/Tests/qemu-memory-contract.test.sh +++ b/macos/Tests/qemu-memory-contract.test.sh @@ -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.??????) ;; @@ -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 \ @@ -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:"): @@ -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 diff --git a/macos/Tests/qemu-persistent-storage.test.sh b/macos/Tests/qemu-persistent-storage.test.sh index b4f46bd7..411d89cc 100755 --- a/macos/Tests/qemu-persistent-storage.test.sh +++ b/macos/Tests/qemu-persistent-storage.test.sh @@ -891,4 +891,14 @@ export OMARCHY_QEMU_GPU_STATE_ROOT=$marker_root assert_fails _qps_prepare_state_root export OMARCHY_QEMU_GPU_STATE_ROOT=$saved_marker_state_root +# Launch-time keyboard and SSH tokens must not be persisted or recovered. +valid_command_line='root=/dev/vda rw rootwait console=tty0 console=hvc0 loglevel=4' +assert _qps_validate_kernel_command_line "$valid_command_line" +assert_fails _qps_validate_kernel_command_line \ + "$valid_command_line tryomarchy.keyboard=iso" +assert_fails _qps_validate_kernel_command_line \ + "$valid_command_line tryomarchy.ssh_access=1" +assert_fails _qps_validate_kernel_command_line \ + "$valid_command_line tryomarchy.export_boot=1" + printf 'qemu-persistent-storage.test: PASS\n' diff --git a/macos/Tests/run-qemu-ssh-contract.test.sh b/macos/Tests/run-qemu-ssh-contract.test.sh index 54a9cde0..50be43d2 100755 --- a/macos/Tests/run-qemu-ssh-contract.test.sh +++ b/macos/Tests/run-qemu-ssh-contract.test.sh @@ -27,6 +27,22 @@ assert_line_pair() { "$file" || fail "expected adjacent lines [$first] and [$second] in $file" } +assert_keyboard_lockstep() { + local log=$1 + local geometry=$2 + local other + [[ $(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" + for other in ansi iso jis; do + if [[ $other != "$geometry" ]]; then + assert_not_contains "$(<"$log")" "tryomarchy.keyboard=$other" + assert_not_contains "$(<"$log")" "TRYOMARCHY_KEYBOARD=$other" + fi + done +} + test_root=$(mktemp -d '/private/tmp/omarchy-qemu-ssh-contract.XXXXXX') case "$test_root" in /private/tmp/omarchy-qemu-ssh-contract.??????) ;; @@ -53,6 +69,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 \ @@ -112,7 +136,10 @@ import time arguments = sys.argv[1:] is_recovery = "Try Omarchy Boot Recovery" in arguments log_variable = "FAKE_QEMU_RECOVERY_LOG" if is_recovery else "FAKE_QEMU_LOG" -Path(os.environ[log_variable]).write_text("\n".join(arguments) + "\n") +geometry = os.environ.get("TRYOMARCHY_KEYBOARD", "") +Path(os.environ[log_variable]).write_text( + "\n".join(arguments) + f"\nTRYOMARCHY_KEYBOARD={geometry}\n" +) if is_recovery: export_path = None @@ -442,6 +469,7 @@ assert_line_pair "$test_root/disabled/qemu.log" -kernel "$persistent_root/boot/k assert_line_pair "$test_root/disabled/qemu.log" -initrd "$persistent_root/boot/initramfs" assert_not_contains "$disabled_qemu" hostfwd assert_not_contains "$disabled_qemu" tryomarchy.ssh_access +assert_keyboard_lockstep "$test_root/disabled/qemu.log" iso assert_contains "$disabled_qemu" \ 'cocoa,gl=es,show-cursor=on,zoom-to-fit=on,full-screen=on,full-grab=on,immersive=on,swap-opt-cmd=off' assert_contains "$disabled_qemu" \ @@ -453,6 +481,21 @@ assert_contains "$(<"$test_root/disabled/storage.log")" create assert_line_pair "$test_root/disabled/qemu.log" -smp '8,sockets=1,cores=8,threads=1' assert_line_pair "$test_root/disabled/qemu.log" -m 4096M +run_scenario keyboard-ansi 0 '' FAKE_HOST_KEYBOARD=ansi +assert_keyboard_lockstep "$test_root/keyboard-ansi/qemu.log" ansi +run_scenario keyboard-jis 0 '' FAKE_HOST_KEYBOARD=jis +assert_keyboard_lockstep "$test_root/keyboard-jis/qemu.log" jis +run_scenario keyboard-helper-fail 1 '' FAKE_HOST_KEYBOARD_FAIL=1 +assert_contains "$(<"$test_root/keyboard-helper-fail/stderr")" \ + 'cannot detect the host Mac keyboard geometry' +[[ ! -e $test_root/keyboard-helper-fail/qemu.log ]] || \ + fail 'failed keyboard probe started QEMU' +run_scenario keyboard-invalid 1 '' FAKE_HOST_KEYBOARD=ISO +assert_contains "$(<"$test_root/keyboard-invalid/stderr")" \ + 'host Mac keyboard geometry is invalid' +[[ ! -e $test_root/keyboard-invalid/qemu.log ]] || \ + fail 'invalid keyboard geometry started QEMU' + # Exercise resource values through the real launcher and its QEMU boundary. run_scenario resources 0 '' FAKE_HOST_CPUS=18 \ OMARCHY_QEMU_GPU_CPUS=18 OMARCHY_QEMU_GPU_MEMORY_MIB=12288 @@ -525,6 +568,7 @@ assert_line_pair "$test_root/enabled/qemu.log" -netdev \ assert_line_pair "$test_root/enabled/qemu.log" -kernel "$persistent_root/boot/kernel" assert_line_pair "$test_root/enabled/qemu.log" -initrd "$persistent_root/boot/initramfs" assert_contains "$enabled_qemu" tryomarchy.ssh_access=1 +assert_keyboard_lockstep "$test_root/enabled/qemu.log" iso assert_contains "$enabled_qemu" loglevel=4 assert_not_contains "$enabled_qemu" loglevel=5 assert_not_contains "$enabled_qemu" 0.0.0.0 @@ -604,6 +648,10 @@ assert_line_pair "$test_root/recovery-allowed/qemu.log" -kernel "$recovery_root/ assert_line_pair "$test_root/recovery-allowed/qemu.log" -initrd "$recovery_root/boot/initramfs" assert_contains "$(<"$test_root/recovery-allowed/qemu.log")" loglevel=3 assert_not_contains "$(<"$test_root/recovery-allowed/qemu.log")" loglevel=5 +assert_not_contains "$(<"$test_root/recovery-allowed/recovery.log")" tryomarchy.keyboard= +[[ $(grep -c '^TRYOMARCHY_KEYBOARD=$' "$test_root/recovery-allowed/recovery.log") == 1 ]] || \ + fail 'recovery QEMU must not inherit TRYOMARCHY_KEYBOARD' +assert_keyboard_lockstep "$test_root/recovery-allowed/qemu.log" iso assert_contains "$(<"$recovery_root/boot/kernel")" recovered-kernel assert_contains "$(<"$recovery_root/boot/initramfs")" recovered-initramfs assert_contains "$(<"$recovery_root/boot/command-line")" loglevel=3 @@ -622,6 +670,8 @@ assert_not_contains "$(<"$test_root/recovery-relaunch/stderr")" 'needs consent' assert_line_pair "$test_root/recovery-relaunch/qemu.log" -kernel "$recovery_root/boot/kernel" assert_line_pair "$test_root/recovery-relaunch/qemu.log" -initrd "$recovery_root/boot/initramfs" assert_contains "$(<"$test_root/recovery-relaunch/qemu.log")" loglevel=3 +assert_keyboard_lockstep "$test_root/recovery-relaunch/qemu.log" iso +assert_not_contains "$(<"$recovery_root/boot/command-line")" tryomarchy.keyboard= assert_contains "$(<"$recovery_root/rootfs.ext4")" legacy-user-disk run_scenario preset 0 '' OMARCHY_QEMU_GPU_PORT_FORWARDS=tcp:2222:22 @@ -655,6 +705,7 @@ assert_line_pair "$test_root/ephemeral/qemu.log" -m 12288M assert_contains "$(<"$test_root/ephemeral/qemu.log")" \ 'user,id=omarchy-net,hostfwd=tcp:127.0.0.1:2224-:22' assert_contains "$(<"$test_root/ephemeral/qemu.log")" tryomarchy.ssh_access=1 +assert_keyboard_lockstep "$test_root/ephemeral/qemu.log" iso assert_contains "$(<"$test_root/ephemeral/storage.log")" 'select ephemeral' assert_line_pair "$test_root/ephemeral/qemu.log" -kernel "$guest/vmlinuz-linux" assert_line_pair "$test_root/ephemeral/qemu.log" -initrd "$guest/initramfs-linux.img" @@ -665,7 +716,8 @@ run_scenario malformed 1 '' OMARCHY_QEMU_GPU_PORT_FORWARDS=tcp:02222:22 assert_contains "$(<"$test_root/malformed/stderr")" 'canonical decimal' run_scenario reset-only 0 --reset-storage-only \ - OMARCHY_QEMU_GPU_PORT_FORWARDS=tcp:2225:22 + OMARCHY_QEMU_GPU_PORT_FORWARDS=tcp:2225:22 \ + FAKE_HOST_KEYBOARD_FAIL=1 assert_contains "$(<"$test_root/reset-only/storage.log")" 'select reset' [[ ! -e $test_root/reset-only/qemu.log ]] || fail 'reset-only launch started QEMU' assert_not_contains "$(<"$test_root/reset-only/stderr")" tryomarchy.ssh_access @@ -680,4 +732,12 @@ run_scenario prebaked-token 1 '' OMARCHY_QEMU_GPU_PORT_FORWARDS=tcp:2222:22 [[ ! -s $test_root/prebaked-token/storage.log ]] || fail 'prebaked token touched storage' assert_contains "$(<"$test_root/prebaked-token/stderr")" 'launcher-owned SSH activation argument' +/usr/bin/plutil -replace kernelCommandLine -string \ + 'root=/dev/vda rw rootwait console=tty0 console=hvc0 tryomarchy.keyboard=iso' \ + "$guest/launch.plist" +run_scenario prebaked-keyboard 1 '' +[[ ! -s $test_root/prebaked-keyboard/storage.log ]] || fail 'prebaked keyboard token touched storage' +assert_contains "$(<"$test_root/prebaked-keyboard/stderr")" \ + 'launcher-owned keyboard geometry argument' + printf 'run-qemu-ssh-contract.test: PASS\n' diff --git a/macos/Tests/test-cocoa-iso-keyboard.py b/macos/Tests/test-cocoa-iso-keyboard.py new file mode 100644 index 00000000..412da2bc --- /dev/null +++ b/macos/Tests/test-cocoa-iso-keyboard.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Checksum and compile the Cocoa ISO Section/Grave swap, without AppKit.""" + +from __future__ import annotations + +import hashlib +import os +from pathlib import Path +import re +import shlex +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +PATCH = ROOT / "macos/patches/qemu-cocoa-iso-section-grave-swap.patch" +BUILDER = ROOT / "macos/build-qemu-gpu-runtime.sh" +RUNNER = ROOT / "macos/run-qemu-gpu.sh" +KEY_GRAVE = 41 +KEY_102ND = 86 + + +def plus_lines() -> str: + return "\n".join( + line[1:] + for line in PATCH.read_text(encoding="utf-8").splitlines() + if line.startswith("+") and not line.startswith("+++") + ) + + +def extract_c_block(source: str, start: str, end: str) -> str: + begin = source.index(start) + finish = source.index(end, begin) + len(end) + return source[begin:finish] + + +class CocoaIsoKeyboardTests(unittest.TestCase): + def test_iso_swap_compiles_and_only_swaps_when_env_is_iso(self) -> None: + added = plus_lines() + probe = extract_c_block( + added, + "static int cocoa_iso_swap_cached = -1;", + " return false;\n}", + ) + swap = extract_c_block( + added, + " if (cocoa_host_keyboard_is_iso()) {", + " }\n }", + ) + self.assertIn('strcmp(geometry, "iso") == 0', probe) + self.assertNotIn("KBGetLayoutType", added) + self.assertLess(added.index("linux_keycode == KEY_GRAVE"), added.index("return KEY_102ND;")) + self.assertLess(added.index("return KEY_102ND;"), added.index("linux_keycode == KEY_102ND")) + + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + (work / "iso-swap.c").write_text( + f""" +#include +#include +#include +#include + +#define KEY_GRAVE {KEY_GRAVE} +#define KEY_102ND {KEY_102ND} + +{probe} + +static int apply_iso_swap(int linux_keycode) +{{ +{swap} + return linux_keycode; +}} + +int main(void) +{{ + const char *expect = getenv("TRYOMARCHY_ISO_EXPECT"); + int grave = apply_iso_swap(KEY_GRAVE); + int iso = apply_iso_swap(KEY_102ND); + int other = apply_iso_swap(30); + if (expect && strcmp(expect, "swap") == 0) {{ + assert(grave == KEY_102ND); + assert(iso == KEY_GRAVE); + }} else {{ + assert(grave == KEY_GRAVE); + assert(iso == KEY_102ND); + }} + assert(other == 30); + return 0; +}} +""", + encoding="utf-8", + ) + compiler = shlex.split(os.environ.get("CC", "cc")) + binary = work / "iso-swap" + subprocess.run( + compiler + + ["-std=c11", "-Wall", "-Wextra", "-Werror", str(work / "iso-swap.c"), "-o", str(binary)], + check=True, + ) + cases = ( + ({"TRYOMARCHY_KEYBOARD": "iso", "TRYOMARCHY_ISO_EXPECT": "swap"},), + ({"TRYOMARCHY_KEYBOARD": "ansi", "TRYOMARCHY_ISO_EXPECT": "keep"},), + ({"TRYOMARCHY_KEYBOARD": "jis", "TRYOMARCHY_ISO_EXPECT": "keep"},), + ({"TRYOMARCHY_KEYBOARD": "", "TRYOMARCHY_ISO_EXPECT": "keep"},), + ({"TRYOMARCHY_ISO_EXPECT": "keep"},), + ) + for (env,) in cases: + completed = subprocess.run( + [str(binary)], + check=False, + env={**os.environ, **env}, + ) + self.assertEqual(completed.returncode, 0, env) + + def test_builder_verifies_exact_patch(self) -> None: + builder = BUILDER.read_text(encoding="utf-8") + expected = re.search(r"^iso_swap_patch_sha256=([a-f0-9]{64})$", builder, re.M) + self.assertIsNotNone(expected) + self.assertEqual( + hashlib.sha256(PATCH.read_bytes()).hexdigest(), expected.group(1) + ) + self.assertIn( + 'patch -d "$source_dir" -p1 -f -i "$iso_swap_patch"', + builder, + ) + + def test_runner_exports_helper_geometry(self) -> None: + runner = RUNNER.read_text(encoding="utf-8") + self.assertIn("--host-keyboard-geometry", runner) + self.assertIn( + 'keyboard_kernel_argument=" tryomarchy.keyboard=$host_keyboard_geometry"', + runner, + ) + self.assertIn("export TRYOMARCHY_KEYBOARD=$host_keyboard_geometry", runner) + self.assertNotIn("/usr/bin/swift", runner) + + +if __name__ == "__main__": + unittest.main() diff --git a/macos/build-qemu-gpu-runtime.sh b/macos/build-qemu-gpu-runtime.sh index a96ce3ce..23a90b0b 100755 --- a/macos/build-qemu-gpu-runtime.sh +++ b/macos/build-qemu-gpu-runtime.sh @@ -7,8 +7,8 @@ usage() { Usage: macos/build-qemu-gpu-runtime.sh [--archive-dir DIR] Build the pinned QEMU/VirGL source stack with Try Omarchy's Cocoa identity, -dynamic-display, immersive-mode, pause-ownership, and pinch-zoom patches, then relocate, -sign, validate, and +dynamic-display, immersive-mode, pause-ownership, pinch-zoom, and ISO +keyboard patches, then relocate, sign, validate, and atomically stage it at: macos/.build/qemu-gpu-runtime @@ -50,6 +50,7 @@ immersive_patch="$native_dir/patches/qemu-cocoa-immersive-mode.patch" full_grab_patch="$native_dir/patches/qemu-cocoa-full-grab-focus.patch" pause_ownership_patch="$native_dir/patches/qemu-cocoa-pause-ownership.patch" pinch_patch="$native_dir/patches/qemu-cocoa-pinch-zoom.patch" +iso_swap_patch="$native_dir/patches/qemu-cocoa-iso-section-grave-swap.patch" audio_device_patch="$native_dir/patches/qemu-sdl-audio-device-selection.patch" shared_folder_patch="$native_dir/patches/qemu-9p-guest-owner.patch" strchrnul_patch="$native_dir/patches/qemu-darwin-strchrnul-compat.patch" @@ -70,6 +71,7 @@ immersive_patch_sha256=2462463932f7db0d659f754f7f9c182884564dbcd7d4b8e523f1b57f0 full_grab_patch_sha256=d94aaa7b8b8b97eb25a5ace2b3a1268985e1b16e4e6201847b926b8ee709dbfb pause_ownership_patch_sha256=1a5729b36eb3e437395d41883a10c3c652df71d289d5df84d95aebd49c78a8f0 pinch_patch_sha256=37acb8895dddd35fc66812d0c49ec5fc697f9127e9e12ed2e60d17999bf32aee +iso_swap_patch_sha256=57f33a5fb08fb90a7813b13bb7037a13198e4d7db230085b1faa28b284cf2387 audio_device_patch_sha256=03aca71c26163c337338cc3b2013c35430690fc0e8b66c5ce92a42f59a9b3334 shared_folder_patch_sha256=41247692501655393ae3a40f56915472ab29b6e89c5173e33db1f62cca56632f strchrnul_patch_sha256=ec1048dd0e8ebe53bf7e8a3bca9bf2f5f4336cd607d4cd077437470e9a32094a @@ -157,6 +159,8 @@ macos_major=$(sw_vers -productVersion | awk -F. '{ print $1 }') die "missing Cocoa pause-ownership patch: $pause_ownership_patch" [[ -f $pinch_patch && ! -L $pinch_patch ]] || \ die "missing Cocoa pinch-zoom patch: $pinch_patch" +[[ -f $iso_swap_patch && ! -L $iso_swap_patch ]] || \ + die "missing Cocoa ISO Section/Grave swap patch: $iso_swap_patch" [[ -f $audio_device_patch && ! -L $audio_device_patch ]] || \ die "missing SDL audio-device patch: $audio_device_patch" [[ -f $texture_patch && ! -L $texture_patch ]] || \ @@ -343,6 +347,8 @@ verify_file_sha "Try Omarchy Cocoa pause-ownership patch" \ "$pause_ownership_patch" "$pause_ownership_patch_sha256" verify_file_sha "Try Omarchy Cocoa pinch-zoom patch" \ "$pinch_patch" "$pinch_patch_sha256" +verify_file_sha "Try Omarchy Cocoa ISO Section/Grave swap patch" \ + "$iso_swap_patch" "$iso_swap_patch_sha256" verify_file_sha "Try Omarchy SDL audio-device patch" \ "$audio_device_patch" "$audio_device_patch_sha256" verify_file_sha "Try Omarchy 9p shared-folder patch" \ @@ -350,7 +356,7 @@ verify_file_sha "Try Omarchy 9p shared-folder patch" \ verify_file_sha "Try Omarchy Darwin strchrnul compatibility patch" \ "$strchrnul_patch" "$strchrnul_patch_sha256" -log "Applying the exact render, identity, display, immersive, pause-ownership, audio, folder, Darwin compatibility, and pinch patches" +log "Applying the exact render, identity, display, immersive, pause-ownership, audio, folder, Darwin compatibility, pinch, and ISO keyboard patches" patch -d "$source_dir" -p1 -f -i "$texture_patch" patch -d "$source_dir" -p1 -f -i "$gpu_fix_patch" patch -d "$source_dir" -p1 -f -i "$identity_patch" @@ -362,6 +368,7 @@ patch -d "$source_dir" -p1 -f -i "$audio_device_patch" patch -d "$source_dir" -p1 -f -i "$shared_folder_patch" patch -d "$source_dir" -p1 -f -i "$strchrnul_patch" patch -d "$source_dir" -p1 -f -i "$pinch_patch" +patch -d "$source_dir" -p1 -f -i "$iso_swap_patch" virgl_root="$dependency_root/virglrenderer/$virgl_version" angle_root="$dependency_root/angle/$angle_version" diff --git a/macos/patches/qemu-cocoa-iso-section-grave-swap.patch b/macos/patches/qemu-cocoa-iso-section-grave-swap.patch new file mode 100644 index 00000000..f4d3e3bb --- /dev/null +++ b/macos/patches/qemu-cocoa-iso-section-grave-swap.patch @@ -0,0 +1,66 @@ +From: Try Omarchy contributors +Subject: [PATCH] cocoa: swap ISO Section/Grave Linux keycodes on Mac hosts + +Applies after the Try Omarchy Cocoa input patches. + +Apple ISO keyboards report KEY_GRAVE and KEY_102ND swapped relative to the +physical positions Linux XKB and applealu_iso models expect. The launcher +sets TRYOMARCHY_KEYBOARD from the bundled helper; that value is the only +authority for the swap so Cocoa and the guest applealu_* model cannot +disagree. Missing or non-iso values do not swap (no live HID fallback). +ANSI and JIS hosts are unchanged. See omacom/try-omarchy#67. + +diff --git a/ui/cocoa.m b/ui/cocoa.m +--- a/ui/cocoa.m ++++ b/ui/cocoa.m +@@ -141,13 +141,49 @@ + return val; + } + ++/* ++ * Apple ISO keyboards engrave Section / the extra ISO key differently from PC ++ * ISO boards. Through Cocoa, KEY_GRAVE and KEY_102ND arrive swapped relative to ++ * the physical positions Linux XKB (and Applealu ISO models) expect. Only the ++ * launch-time TRYOMARCHY_KEYBOARD value may enable this swap so it matches the ++ * guest model. No env means no swap. ++ */ ++static int cocoa_iso_swap_cached = -1; ++ ++static bool cocoa_host_keyboard_is_iso(void) ++{ ++ const char *geometry; ++ ++ if (cocoa_iso_swap_cached >= 0) { ++ return cocoa_iso_swap_cached; ++ } ++ geometry = getenv("TRYOMARCHY_KEYBOARD"); ++ if (geometry != NULL && geometry[0] != '\0') { ++ cocoa_iso_swap_cached = strcmp(geometry, "iso") == 0; ++ return cocoa_iso_swap_cached; ++ } ++ cocoa_iso_swap_cached = 0; ++ return false; ++} ++ + static int cocoa_keycode_to_linux(int keycode) + { ++ int linux_keycode; ++ + if (qemu_input_map_osx_to_linux_len <= keycode) { + error_report("(cocoa) warning unknown keycode 0x%x", keycode); + return 0; + } +- return qemu_input_map_osx_to_linux[keycode]; ++ linux_keycode = qemu_input_map_osx_to_linux[keycode]; ++ if (cocoa_host_keyboard_is_iso()) { ++ if (linux_keycode == KEY_GRAVE) { ++ return KEY_102ND; ++ } ++ if (linux_keycode == KEY_102ND) { ++ return KEY_GRAVE; ++ } ++ } ++ return linux_keycode; + } + + /* Displays an alert dialog box with the specified message */ diff --git a/macos/qemu-persistent-storage.sh b/macos/qemu-persistent-storage.sh index 08ea6321..a96dbc2c 100755 --- a/macos/qemu-persistent-storage.sh +++ b/macos/qemu-persistent-storage.sh @@ -639,7 +639,7 @@ _qps_validate_kernel_command_line() { console=tty0) ((qps_console_zero_count += 1)) ;; console=hvc0) ((qps_console_hvc_count += 1)) ;; omarchy.qemu_virgl=*|omarchy.shared_folder_name=*|tryomarchy.ssh_access=*|\ - tryomarchy.export_boot=*) return 1 ;; + tryomarchy.keyboard=*|tryomarchy.export_boot=*) return 1 ;; esac done (( qps_root_count == 1 && qps_rw_count == 1 && qps_rootwait_count == 1 && \ diff --git a/macos/run-qemu-gpu.sh b/macos/run-qemu-gpu.sh index a10c96b6..d19b4139 100755 --- a/macos/run-qemu-gpu.sh +++ b/macos/run-qemu-gpu.sh @@ -813,6 +813,8 @@ if any(argument.startswith("omarchy.shared_folder_name=") for argument in argume fail("kernel command line already contains a shared folder name") if any(argument.startswith("tryomarchy.ssh_access=") for argument in arguments): fail("kernel command line contains a launcher-owned SSH activation argument") +if any(argument.startswith("tryomarchy.keyboard=") for argument in arguments): + fail("kernel command line contains a launcher-owned keyboard geometry argument") records = manifest.get("artifacts") if not isinstance(records, list) or len(records) != len(expected_artifacts): @@ -919,6 +921,9 @@ case " $kernel_command_line " in *' tryomarchy.ssh_access='*) fail "validated kernel command line contains a launcher-owned SSH activation argument" ;; + *' tryomarchy.keyboard='*) + fail "validated kernel command line contains a launcher-owned keyboard geometry argument" + ;; esac if [[ ${OMARCHY_QEMU_GPU_INSPECT_ONLY:-0} == 1 ]]; then printf '%s\n' "$bundle_validation" @@ -950,6 +955,23 @@ if ((QEMU_PORT_FORWARDING_ENABLES_SSH)); then ssh_kernel_argument=' tryomarchy.ssh_access=1' fi +keyboard_kernel_argument="" +if ((reset_only)); then + unset TRYOMARCHY_KEYBOARD +else + host_keyboard_geometry=$("$native_bridge" --host-keyboard-geometry) || { + fail "cannot detect the host Mac keyboard geometry" + } + case "$host_keyboard_geometry" in + ansi|iso|jis) ;; + *) + fail "host Mac keyboard geometry is invalid: $host_keyboard_geometry" + ;; + esac + keyboard_kernel_argument=" tryomarchy.keyboard=$host_keyboard_geometry" + export TRYOMARCHY_KEYBOARD=$host_keyboard_geometry +fi + host_cpu_count=$( sysctl -n hw.logicalcpu 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || @@ -1238,29 +1260,32 @@ recover_persistent_boot_kit() { recovery_command_line+=' rootflags=noload fsck.mode=skip tryomarchy.export_boot=1' echo '[qemu-gpu] Pairing the saved VM with its original boot files (one time).' >&2 - "$qemu_bin" \ - -name 'Try Omarchy Boot Recovery' \ - -machine "$qemu_machine" \ - -cpu 'host,pmu=off' \ - -smp '2,sockets=1,cores=2,threads=1' \ - -m 2G \ - -nodefaults \ - -no-reboot \ - -display none \ - -serial none \ - -monitor none \ - -qmp "unix:$qmp_socket,server=on,wait=off" \ - -kernel "$bundled_kernel" \ - -initrd "$bundled_initramfs" \ - -append "$recovery_command_line" \ - -drive "if=none,id=omarchy-recovery-root,file=$working_disk,format=raw,media=disk,cache=none,readonly=on" \ - -device 'virtio-blk-pci,drive=omarchy-recovery-root,serial=omarchy-root' \ - -device 'virtio-serial-pci,id=omarchy-recovery-serial' \ - -chardev 'stdio,id=omarchy-recovery-hvc0,signal=off' \ - -device 'virtconsole,bus=omarchy-recovery-serial.0,nr=0,chardev=omarchy-recovery-hvc0' \ - -fsdev "local,id=omarchy-boot-export,path=$boot_export_dir,security_model=none,multidevs=remap" \ - -device 'virtio-9p-pci,fsdev=omarchy-boot-export,mount_tag=try-omarchy-boot-export,romfile=' \ - -add-fd "$QEMU_PERSISTENT_STORAGE_QEMU_ADD_FD" & + ( + unset TRYOMARCHY_KEYBOARD + exec "$qemu_bin" \ + -name 'Try Omarchy Boot Recovery' \ + -machine "$qemu_machine" \ + -cpu 'host,pmu=off' \ + -smp '2,sockets=1,cores=2,threads=1' \ + -m 2G \ + -nodefaults \ + -no-reboot \ + -display none \ + -serial none \ + -monitor none \ + -qmp "unix:$qmp_socket,server=on,wait=off" \ + -kernel "$bundled_kernel" \ + -initrd "$bundled_initramfs" \ + -append "$recovery_command_line" \ + -drive "if=none,id=omarchy-recovery-root,file=$working_disk,format=raw,media=disk,cache=none,readonly=on" \ + -device 'virtio-blk-pci,drive=omarchy-recovery-root,serial=omarchy-root' \ + -device 'virtio-serial-pci,id=omarchy-recovery-serial' \ + -chardev 'stdio,id=omarchy-recovery-hvc0,signal=off' \ + -device 'virtconsole,bus=omarchy-recovery-serial.0,nr=0,chardev=omarchy-recovery-hvc0' \ + -fsdev "local,id=omarchy-boot-export,path=$boot_export_dir,security_model=none,multidevs=remap" \ + -device 'virtio-9p-pci,fsdev=omarchy-boot-export,mount_tag=try-omarchy-boot-export,romfile=' \ + -add-fd "$QEMU_PERSISTENT_STORAGE_QEMU_ADD_FD" + ) & qemu_pid=$! printf '%s\n' "$qemu_pid" >"$work_dir/.qemu.pid" || \ boot_recovery_fail 'could not record the recovery process' @@ -1505,7 +1530,7 @@ qemu_args=( -qmp "unix:$qmp_socket,server=on,wait=off" -kernel "$launch_kernel" -initrd "$launch_initramfs" - -append "$launch_kernel_command_line omarchy.qemu_virgl=1$shared_folder_kernel_argument$ssh_kernel_argument" + -append "$launch_kernel_command_line omarchy.qemu_virgl=1$shared_folder_kernel_argument$ssh_kernel_argument$keyboard_kernel_argument" -drive "if=none,id=omarchy-root,file=$working_disk,format=raw,media=disk,cache=writeback" -device 'virtio-blk-pci,drive=omarchy-root,serial=omarchy-root' -device "$gpu_device"