diff --git a/bin/omarchy-iso-test b/bin/omarchy-iso-test index 004afd7..5e2e1b3 100755 --- a/bin/omarchy-iso-test +++ b/bin/omarchy-iso-test @@ -13,6 +13,9 @@ # # Usage: omarchy-iso-test [release/omarchy.iso] [options] # +# --keyboard LABEL English (US/UK), Japanese, Japanese (US keyboard), Korean, +# or Chinese (Simplified, Pinyin)/(Traditional, Zhuyin). +# OMARCHY_PATH must be the source tree used to build the ISO. # --encrypt Drive the encrypted install flow (default: unencrypted) # --region REGION Pick global (default) or cn on the first screen # --provision Drive the deferred-provisioning install flow: no user during install, the @@ -49,12 +52,14 @@ NO_PREVIEW=false SYNC_DIR="" SYNC_ALL=false +KEYBOARD_LABEL="English (US)" GUEST_USER="omarchy" GUEST_PASSWORD="omarchy" GUEST_HOSTNAME="omarchy-test" while (($#)); do case "$1" in + --keyboard) KEYBOARD_LABEL="$2"; shift ;; --encrypt) ENCRYPT=true ;; --region) REGION="$2"; shift ;; --provision) PROVISION=true ;; @@ -91,6 +96,10 @@ if [[ $REGION != "global" && $REGION != "cn" ]]; then echo "--region must be global or cn, not $REGION" >&2 exit 1 fi +case "$KEYBOARD_LABEL" in + "English (US)" | "English (UK)" | "Japanese" | "Japanese (US keyboard)" | "Korean" | "Chinese (Simplified, Pinyin)" | "Chinese (Traditional, Zhuyin)") ;; + *) echo "The VM harness cannot type credentials with keyboard: $KEYBOARD_LABEL" >&2; exit 1 ;; +esac omarchy-pkg-add qemu-full edk2-ovmf socat imagemagick tesseract tesseract-data-eng tesseract-data-chi_sim @@ -110,6 +119,9 @@ OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd" OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd" BASE_NAME="$(basename "$ISO" .iso)" +if [[ $KEYBOARD_LABEL != "English (US)" ]]; then + BASE_NAME+="-$(printf '%s' "$KEYBOARD_LABEL" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')" +fi $ENCRYPT && BASE_NAME+="-encrypted" [[ $REGION == "cn" ]] && BASE_NAME+="-cn" $PROVISION && BASE_NAME+="-provision" @@ -637,6 +649,35 @@ audio-panel|meta_l-ctrl-a|omarchy-keyboard-panel|omarchy.audio' # ----------------------------------------------------- driving the installer +select_keyboard() { + local label="$1" index direction="down" + # gum choose uses arrows, not a searchable text field. Read the same ordered + # choices the ISO vendors from the runtime rather than hard-code row numbers. + index=$(OMARCHY_TEST_KEYBOARD="$label" python - <<'PYTHON' +import os, re +from pathlib import Path +source = Path(os.environ.get("OMARCHY_PATH", "")) / "install/provisioning/setup-form.sh" +if not source.is_file(): + raise SystemExit("Set OMARCHY_PATH to the source tree used to build this ISO") +text = source.read_text() +rows = re.search(r"OMARCHY_KEYBOARD_LAYOUTS=\$'(.*?)'\n", text, re.S).group(1).splitlines() +labels = [row.split("|")[0] for row in rows] +label = os.environ["OMARCHY_TEST_KEYBOARD"] +if label not in labels: + raise SystemExit(f"Keyboard choice is absent from the ISO source: {label}") +print(labels.index(label) - labels.index("English (US)")) +PYTHON + ) || return 1 + if (( index < 0 )); then + direction="up" + index=$((-index)) + fi + for (( i = 0; i < index; i++ )); do + press "$direction" + sleep 0.1 + done +} + drive_configurator() { log "Driving the installer wizard" @@ -683,7 +724,11 @@ drive_configurator() { return 0 fi - press ret # English (US), or China, is preselected + if [[ $KEYBOARD_LABEL != "English (US)" ]]; then + select_keyboard "$KEYBOARD_LABEL" + capture_console "success-installer-01-selected-keyboard" + fi + press ret # Normal flow: user step comes before disk selection. wait_for_screen "$(region_text "Username" "用户名")" 120 @@ -707,7 +752,15 @@ drive_configurator() { press ret wait_for_screen "$(region_text "Email address" "邮箱")" 60 - type_text "test@omarchy.org" + type_text "test" + if [[ $KEYBOARD_LABEL == "Japanese" ]]; then + press bracket_left + elif [[ $KEYBOARD_LABEL == "English (UK)" ]]; then + press shift-apostrophe + else + press shift-2 + fi + type_text "omarchy.org" capture_console "success-installer-06-email" press ret @@ -768,8 +821,12 @@ drive_provision_owner() { press ret wait_for_screen "keyboard layout" 120 - press down - capture_console "success-provision-01-keyboard-layout-uk" + if [[ $KEYBOARD_LABEL == "English (US)" ]]; then + press down # preserve the harness's default UK provisioning coverage + else + select_keyboard "$KEYBOARD_LABEL" + fi + capture_console "success-provision-01-keyboard-layout" press ret wait_for_screen "Username" 120 @@ -795,7 +852,13 @@ drive_provision_owner() { # Shift+2 as on US); send that keycode so the typed email is correct under # the new layout — itself further proof the deferred keymap took effect. type_text "test" - press shift-apostrophe + if [[ $KEYBOARD_LABEL == "English (US)" || $KEYBOARD_LABEL == "English (UK)" ]]; then + press shift-apostrophe + elif [[ $KEYBOARD_LABEL == "Japanese" ]]; then + press bracket_left + else + press shift-2 + fi type_text "omarchy.org" capture_console "success-provision-04-email" press ret diff --git a/builder/build-iso.sh b/builder/build-iso.sh index cf30e8c..71f194f 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -46,11 +46,14 @@ pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Sy omarchy-keyring pacman-key --populate omarchy -# Append the [omarchy] repo to the container's /etc/pacman.conf so subsequent -# tools (notably makepkg in build-omarchy-packages.sh) can resolve omarchy- -# only build deps like limine-snapper-sync and limine-mkinitcpio-hook. +# Put OPR first in the container too so makepkg resolves the same overrides +# as the online installer configuration. if ! grep -q '^\[omarchy\]' /etc/pacman.conf; then - awk '/^\[omarchy\]/,/^$/' /configs/pacman-online-${OMARCHY_MIRROR}.conf >> /etc/pacman.conf + opr_section=$(awk '/^\[omarchy\]/,/^$/' /configs/pacman-online-${OMARCHY_MIRROR}.conf) + awk -v opr="$opr_section" '/^\[core\]/ { print opr; print "" } { print }' \ + /etc/pacman.conf >/tmp/pacman-opr-first.conf + install -m 644 /tmp/pacman-opr-first.conf /etc/pacman.conf + rm -f /tmp/pacman-opr-first.conf fi # Build locations diff --git a/configs/airootfs/root/configurator b/configs/airootfs/root/configurator index e317e8d..863fa81 100644 --- a/configs/airootfs/root/configurator +++ b/configs/airootfs/root/configurator @@ -904,6 +904,8 @@ run_partition_execute() { "bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false }, "custom_commands": [], "omarchy_install": { + "input_method": "${keyboard_input_method:-none}", + "input_xkb_layout": "${keyboard_xkb_layout:-}", "mode": "protected", "region": "$region", "target_mount": "/mnt", @@ -1167,6 +1169,8 @@ if $defer_provisioning; then # their real layout in the OOBE, which re-keys the console and, on encrypted # installs, rebuilds the initramfs so the LUKS prompt matches. keyboard="us" + keyboard_input_method="none" + keyboard_xkb_layout="" username="" password="" password_hash="" @@ -1249,6 +1253,8 @@ cat <<-_EOF_ >user_configuration.json "bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false }, "custom_commands": [], "omarchy_install": { + "input_method": "${keyboard_input_method:-none}", + "input_xkb_layout": "${keyboard_xkb_layout:-}", "mode": "full_disk", "region": "$region", "defer_provisioning": $defer_provisioning, diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/archinstall_adapter.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/archinstall_adapter.py index a1ba900..4d9b20d 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/archinstall_adapter.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/archinstall_adapter.py @@ -3,7 +3,7 @@ ONLY this module imports from archinstall. Everything else uses these helpers. If archinstall's API churns, the blast radius is contained here. -Tested against archinstall 4.5 (Python 3.14). +Tested against archinstall 4.4 (Python 3.14). The canonical call sequence (mirrored from archinstall.scripts.guided.py) is: @@ -13,7 +13,7 @@ The canonical call sequence (mirrored from archinstall.scripts.guided.py) is: # configs do their own mounting before the Installer context opens. if disk_config.config_type != DiskLayoutType.Pre_mount: inst.mount_ordered_layout() - inst.sanity_check(skip_ntp=, skip_wkd=) + sanity_check(inst) inst.generate_key_files() # encrypted only inst.set_mirrors(handler, mirror_config, on_target=False) inst.minimal_installation(...) # base + linux pacstrap @@ -214,6 +214,14 @@ def _method_accepts(method, name: str) -> bool: return name in (*positional, *kwonly) +def sanity_check(installer) -> None: + """Archinstall 4.5 removed offline; retain it for older releases.""" + options = {"skip_ntp": True, "skip_wkd": True} + if _method_accepts(installer.sanity_check, "offline"): + options["offline"] = True + installer.sanity_check(**options) + + def _method_accepts_users(method) -> bool: return _method_accepts(method, "users") diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py index 0ac8a2d..62e51da 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py @@ -11,6 +11,8 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any +from .keyboard import validate_input_selection + @dataclass class InstallContext: @@ -57,6 +59,8 @@ class InstallContext: kernel = (omarchy_install.get("storage") or {}).get("kernel") or _default_kernel() user_configuration["kernels"] = [kernel] + validate_input_selection(omarchy_install.get("input_method"), omarchy_install.get("input_xkb_layout", "")) + # Deferred provisioning: the whole system installs but user creation is deferred to # first boot. Selected by the configurator (omarchy_install.defer_provisioning) or by # an `defer-provisioning` marker file on an autoinstall drive, which also replaces the diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py index 48c457c..969ae2f 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/keyboard.py @@ -16,21 +16,31 @@ from pathlib import Path from .command import capture -def configure_keyboard(target: Path, language: str) -> bool: +def configure_keyboard(target: Path, language: str, input_method: str | None = None, xkb_layout: str = "") -> bool: """Write the console keymap into a mounted target without booting it. Returns False for layouts localectl doesn't know, matching archinstall: warn and keep the default. Every layout the configurator offers is known; the guard is for the kb_layout an autoinstall drive can name freely. """ + validate_input_selection(input_method, xkb_layout) if not language.strip(): - return True + if input_method is None: + return True + language = "us" + + # Input engines are separate from real console keymaps. The shared picker + # uses US for Chinese/Korean and carries its preference in omarchy_install. + if input_method is None: + input_method = "mozc" if language == "jp106" else "none" result = capture(["localectl", "--no-pager", "list-keymaps"]) if result.returncode != 0: detail = result.stderr.strip() or "localectl returned an error" raise RuntimeError(f"Unable to list keyboard layouts: {detail}") if language.lower() not in {layout.lower() for layout in result.stdout.splitlines()}: + if input_method != "none": + raise ValueError(f"Unknown keyboard language: {language}") return False # systemd-firstboot --force rewrites vconsole.conf wholesale, dropping the @@ -51,4 +61,15 @@ def configure_keyboard(target: Path, language: str) -> bool: if font: vconsole_path.write_text(vconsole_path.read_text() + font + "\n") + preference = target / "etc/omarchy/input-method" + preference.parent.mkdir(parents=True, exist_ok=True) + preference.write_text(f"INPUT_METHOD={input_method}\nXKB_LAYOUT={xkb_layout}\n") + return True + + +def validate_input_selection(input_method: str | None, xkb_layout: str) -> None: + if input_method is not None and input_method not in {"none", "mozc", "hangul", "pinyin", "chewing"}: + raise ValueError(f"Unknown input method: {input_method}") + if xkb_layout not in {"", "kr"}: + raise ValueError(f"Unknown input keyboard layout: {xkb_layout}") diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py index 3620b56..66b16c5 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -261,10 +261,7 @@ def arch_install_system(ctx: InstallContext) -> None: if not pre_mounted: installer.mount_ordered_layout() - installer.sanity_check( - skip_ntp=True, - skip_wkd=True, - ) + arch.sanity_check(installer) if not pre_mounted and arch.is_encrypted(config): installer.generate_key_files() @@ -299,7 +296,11 @@ def arch_install_system(ctx: InstallContext) -> None: # use the target kernel instead of the live ISO's kernel. installer.add_additional_packages([f"{kernel}-headers" for kernel in config.kernels]) - if not configure_keyboard(installer.target, kb_layout): + if not configure_keyboard( + installer.target, kb_layout, + ctx.omarchy_install.get("input_method"), + ctx.omarchy_install.get("input_xkb_layout", ""), + ): error(f"Invalid keyboard language specified: {kb_layout}") if config.mirror_config: diff --git a/configs/pacman-online-edge.conf b/configs/pacman-online-edge.conf index 0ac18d8..8f280f4 100644 --- a/configs/pacman-online-edge.conf +++ b/configs/pacman-online-edge.conf @@ -13,6 +13,10 @@ ParallelDownloads = 5 SigLevel = Required DatabaseOptional LocalFileSigLevel = Optional +[omarchy] +SigLevel = Optional TrustAll +Server = https://pkgs.omarchy.org/edge/$arch + [core] Server = https://mirror.omarchy.org/$repo/os/$arch Include = /etc/pacman.d/mirrorlist @@ -25,10 +29,6 @@ Include = /etc/pacman.d/mirrorlist Server = https://mirror.omarchy.org/$repo/os/$arch Include = /etc/pacman.d/mirrorlist -[omarchy] -SigLevel = Optional TrustAll -Server = https://pkgs.omarchy.org/edge/$arch - [arch-mact2] Server = https://mirror.funami.tech/arch-mact2/os/x86_64 Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release diff --git a/configs/pacman-online-rc.conf b/configs/pacman-online-rc.conf index 09bff86..f9f9020 100644 --- a/configs/pacman-online-rc.conf +++ b/configs/pacman-online-rc.conf @@ -13,6 +13,10 @@ ParallelDownloads = 5 SigLevel = Required DatabaseOptional LocalFileSigLevel = Optional +[omarchy] +SigLevel = Optional TrustAll +Server = https://pkgs.omarchy.org/rc/$arch + [core] Server = https://rc-mirror.omarchy.org/$repo/os/$arch Include = /etc/pacman.d/mirrorlist @@ -25,10 +29,6 @@ Include = /etc/pacman.d/mirrorlist Server = https://rc-mirror.omarchy.org/$repo/os/$arch Include = /etc/pacman.d/mirrorlist -[omarchy] -SigLevel = Optional TrustAll -Server = https://pkgs.omarchy.org/rc/$arch - [arch-mact2] Server = https://mirror.funami.tech/arch-mact2/os/x86_64 Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release diff --git a/configs/pacman-online-stable.conf b/configs/pacman-online-stable.conf index c3be65b..9589e62 100644 --- a/configs/pacman-online-stable.conf +++ b/configs/pacman-online-stable.conf @@ -13,6 +13,10 @@ ParallelDownloads = 5 SigLevel = Required DatabaseOptional LocalFileSigLevel = Optional +[omarchy] +SigLevel = Optional TrustAll +Server = https://pkgs.omarchy.org/stable/$arch + [core] Server = https://stable-mirror.omarchy.org/$repo/os/$arch Include = /etc/pacman.d/mirrorlist @@ -25,10 +29,6 @@ Include = /etc/pacman.d/mirrorlist Server = https://stable-mirror.omarchy.org/$repo/os/$arch Include = /etc/pacman.d/mirrorlist -[omarchy] -SigLevel = Optional TrustAll -Server = https://pkgs.omarchy.org/stable/$arch - [arch-mact2] Server = https://mirror.funami.tech/arch-mact2/os/x86_64 Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release diff --git a/test/unit/keyboard-selection-test.sh b/test/unit/keyboard-selection-test.sh new file mode 100644 index 0000000..674a3ad --- /dev/null +++ b/test/unit/keyboard-selection-test.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/install/provisioning" +cat > "$work/install/provisioning/setup-form.sh" <<'FORM' +OMARCHY_KEYBOARD_LAYOUTS=$'Chinese (Simplified, Pinyin)|us|pinyin +English (US)|us +Japanese|jp106|mozc' +FORM +export OMARCHY_PATH="$work" +eval "$(sed -n '/^select_keyboard() {$/,/^}$/p' "$ROOT/bin/omarchy-iso-test")" +press() { pressed+=("$1"); } +sleep() { :; } + +pressed=() +select_keyboard "Japanese" +[[ ${pressed[*]} == "down" ]] +pressed=() +select_keyboard "Chinese (Simplified, Pinyin)" +[[ ${pressed[*]} == "up" ]] +pressed=() +select_keyboard "English (US)" +(( ${#pressed[@]} == 0 )) +if (set -e; select_keyboard "Missing") 2>"$work/error"; then + echo "Unknown keyboard unexpectedly succeeded" >&2 + exit 1 +fi +grep -q 'Keyboard choice is absent' "$work/error" +echo "ok - VM keyboard choices are relative to the default and reject absent rows" diff --git a/test/unit/test_archinstall_sanity.py b/test/unit/test_archinstall_sanity.py new file mode 100644 index 0000000..ddf63b7 --- /dev/null +++ b/test/unit/test_archinstall_sanity.py @@ -0,0 +1,29 @@ +"""Exercise the compatibility boundary without importing host archinstall.""" +import ast +from pathlib import Path +import types +import unittest + +ROOT = Path(__file__).resolve().parents[2] +source = ROOT / "configs/airootfs/usr/share/omarchy-iso/orchestrator/archinstall_adapter.py" +tree = ast.parse(source.read_text()) +namespace = {} +functions = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name in {"_method_accepts", "sanity_check"}] +exec(compile(ast.Module(body=functions, type_ignores=[]), str(source), "exec"), namespace) + + +class SanityCheckTest(unittest.TestCase): + def test_legacy_archinstall_stays_offline(self): + calls = [] + def check(offline=False, skip_ntp=False, skip_wkd=False): + calls.append((offline, skip_ntp, skip_wkd)) + namespace["sanity_check"](types.SimpleNamespace(sanity_check=check)) + self.assertEqual(calls, [(True, True, True)]) + + def test_archinstall_45_does_not_receive_removed_argument(self): + calls = [] + class Installer: + def sanity_check(self, skip_ntp=False, skip_wkd=False): + calls.append((skip_ntp, skip_wkd)) + namespace["sanity_check"](Installer()) + self.assertEqual(calls, [(True, True)]) diff --git a/test/unit/test_kernel_selection.py b/test/unit/test_kernel_selection.py index ceabe50..98c023a 100644 --- a/test/unit/test_kernel_selection.py +++ b/test/unit/test_kernel_selection.py @@ -108,7 +108,7 @@ class KernelSelectionTest(unittest.TestCase): ) ctx = types.SimpleNamespace( state={"arch_config_handler": types.SimpleNamespace(config=config), "mirror_handler": None}, - target=Path("/unused"), tailscale_authkey_path=None, + target=Path("/unused"), tailscale_authkey_path=None, omarchy_install={}, ) for name in ("_mount_offline_package_cache", "_mask_mkinitcpio_pacman_hooks", "_configure_limine_boot", "_write_pre_mounted_fstab"): @@ -121,6 +121,7 @@ class KernelSelectionTest(unittest.TestCase): stack.enter_context(mock.patch.object(phases_impl, "_runtime_package_list", return_value=["omarchy"])) stack.enter_context(mock.patch.object(phases_impl.arch, "is_pre_mount", return_value=True, create=True)) stack.enter_context(mock.patch.object(phases_impl.arch, "root_user", return_value=None, create=True)) + stack.enter_context(mock.patch.object(phases_impl.arch, "sanity_check", create=True)) opened = stack.enter_context(mock.patch.object(phases_impl.arch, "open_installer", create=True)) opened.return_value.__enter__.return_value = installer if fail_headers: diff --git a/test/unit/test_keyboard.py b/test/unit/test_keyboard.py index 16c3c41..1269b1c 100644 --- a/test/unit/test_keyboard.py +++ b/test/unit/test_keyboard.py @@ -1,6 +1,7 @@ #!/usr/bin/python import re +import os import sys import tempfile import unittest @@ -17,6 +18,7 @@ from orchestrator import keyboard as KEYBOARD # noqa: E402 # wherever this checkout can see a runtime, and skip the coverage test rather # than fail when none is around (a bare CI checkout of just this repo). SETUP_FORM_CANDIDATES = ( + Path(os.environ.get("OMARCHY_PATH", "/nonexistent")) / "install/provisioning/setup-form.sh", Path("/omarchy-source/install/provisioning/setup-form.sh"), ROOT.parent / "omarchy/install/provisioning/setup-form.sh", Path("/usr/share/omarchy/install/provisioning/setup-form.sh"), @@ -31,7 +33,7 @@ def supported_keymaps(): r"OMARCHY_KEYBOARD_LAYOUTS=\$'(.*?)'\n", candidate.read_text(), re.DOTALL ) assert block, f"no layout list in {candidate}" - return [line.split("|", 1)[1] for line in block.group(1).splitlines()] + return [line.split("|")[1] for line in block.group(1).splitlines()] return None @@ -79,6 +81,36 @@ class KeyboardConfigurationTest(unittest.TestCase): "KEYMAP=us\nFONT=default8x16\n", ) + def test_input_choice_survives_while_console_keymap_stays_real(self): + for method, keymap, layout in (("mozc", "jp106", ""), ("mozc", "us", ""), ("hangul", "us", "kr"), ("pinyin", "us", ""), ("chewing", "us", "")): + with self.subTest(method=method), tempfile.TemporaryDirectory() as directory: + target = self.target(directory) + self.assertTrue(KEYBOARD.configure_keyboard(target, keymap, method, layout)) + self.assertIn(f"KEYMAP={keymap}", (target / "etc/vconsole.conf").read_text()) + if keymap == "jp106": + self.assertIn("XKBLAYOUT=jp", (target / "etc/vconsole.conf").read_text()) + self.assertEqual((target / "etc/omarchy/input-method").read_text(), f"INPUT_METHOD={method}\nXKB_LAYOUT={layout}\n") + + def test_input_choice_with_no_console_map_defaults_to_us(self): + with tempfile.TemporaryDirectory() as directory: + target = self.target(directory) + self.assertTrue(KEYBOARD.configure_keyboard(target, "", "pinyin")) + self.assertIn("INPUT_METHOD=pinyin", (target / "etc/omarchy/input-method").read_text()) + + def test_unknown_keyboard_does_not_silently_drop_an_input_choice(self): + with tempfile.TemporaryDirectory() as directory: + target = self.target(directory) + with self.assertRaises(ValueError): + KEYBOARD.configure_keyboard(target, "unknown", "pinyin") + + def test_invalid_input_preferences_are_rejected_without_writes(self): + for method, layout in (("unknown", ""), ("hangul", "kr\nKEYMAP=ru")): + with tempfile.TemporaryDirectory() as directory: + target = self.target(directory) + with self.assertRaises(ValueError): + KEYBOARD.configure_keyboard(target, "us", method, layout) + self.assertFalse((target / "etc/omarchy/input-method").exists()) + if __name__ == "__main__": unittest.main()