diff --git a/src/emulator.rs b/src/emulator.rs index 765af5f..127375e 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -117,6 +117,54 @@ pub fn parse_image_sysdir(config_ini: &str) -> Option { .map(|v| v.trim().trim_end_matches('/').to_string()) } +/// Ensure an AVD `config.ini` enables the emulated hardware keyboard so the host +/// (Mac) keyboard types into the guest. avdmanager's device profiles default +/// `hw.keyboard=no`, which silently drops host key events — you tap a field, the +/// on-screen keyboard shows, but typing on the Mac does nothing. Flipping it to +/// `yes` forwards the physical keyboard; the soft IME still appears because the +/// system images default `show_ime_with_hard_keyboard=1`. +/// +/// Pure string transform: returns the rewritten file when a change is needed, or +/// `None` if it already reads `hw.keyboard=yes` (so the caller can skip writing). +/// Only the exact `hw.keyboard=` key is touched — `hw.keyboard.charmap`/`.lid` +/// are left alone. +pub fn with_hw_keyboard_enabled(config_ini: &str) -> Option { + fn is_hw_keyboard(line: &str) -> bool { + line.trim_start() + .strip_prefix("hw.keyboard") + .is_some_and(|rest| rest.trim_start().starts_with('=')) + } + let mut found = false; + let mut changed = false; + let mut out: Vec = Vec::new(); + for line in config_ini.lines() { + if is_hw_keyboard(line) { + found = true; + let value = line.split_once('=').map_or("", |(_, v)| v.trim()); + if value == "yes" { + out.push(line.to_string()); + } else { + out.push("hw.keyboard=yes".to_string()); + changed = true; + } + } else { + out.push(line.to_string()); + } + } + if !found { + out.push("hw.keyboard=yes".to_string()); + changed = true; + } + if !changed { + return None; + } + let mut result = out.join("\n"); + if config_ini.ends_with('\n') { + result.push('\n'); + } + Some(result) +} + /// System images that are installed but referenced by no live AVD — i.e. safe /// to prune. Both lists are relative image paths (`system-images///`). pub fn unreferenced_images(installed: &[String], referenced: &[String]) -> Vec { @@ -178,6 +226,45 @@ mod tests { assert_eq!(parse_image_sysdir(""), None); } + #[test] + fn hw_keyboard_flips_no_to_yes_and_leaves_siblings() { + let ini = "hw.dPad=no\n\ + hw.keyboard=no\n\ + hw.keyboard.charmap=qwerty2\n\ + hw.keyboard.lid=yes\n\ + hw.mainKeys=no\n"; + let out = with_hw_keyboard_enabled(ini).expect("should rewrite"); + assert!(out.contains("hw.keyboard=yes")); + // siblings untouched + assert!(out.contains("hw.keyboard.charmap=qwerty2")); + assert!(out.contains("hw.keyboard.lid=yes")); + // the old value is gone and the file still ends in a newline + assert!(!out.contains("hw.keyboard=no")); + assert!(out.ends_with('\n')); + } + + #[test] + fn hw_keyboard_noop_when_already_yes() { + let ini = "hw.keyboard=yes\nhw.mainKeys=no\n"; + assert_eq!(with_hw_keyboard_enabled(ini), None); + } + + #[test] + fn hw_keyboard_appended_when_absent() { + let ini = "hw.dPad=no\nhw.mainKeys=no\n"; + let out = with_hw_keyboard_enabled(ini).expect("should append"); + assert!(out.contains("hw.keyboard=yes")); + assert!(out.ends_with('\n')); + } + + #[test] + fn hw_keyboard_noop_does_not_match_charmap_only() { + // a config that has the sibling keys but no bare hw.keyboard= must append. + let ini = "hw.keyboard.charmap=qwerty2\nhw.keyboard.lid=yes\n"; + let out = with_hw_keyboard_enabled(ini).expect("should append a bare key"); + assert!(out.lines().any(|l| l == "hw.keyboard=yes")); + } + #[test] fn unreferenced_images_is_set_difference() { let installed = vec![ diff --git a/src/provision.rs b/src/provision.rs index 039c2c0..7b80127 100644 --- a/src/provision.rs +++ b/src/provision.rs @@ -180,6 +180,7 @@ pub fn boot(sdk: &Sdk, cfg: &Config) -> Result<()> { } if !is_running(sdk) { clear_stale_locks(sdk, cfg); + enable_hw_keyboard(sdk, cfg); eprintln!("🚀 booting emulator ({})…", cfg.profile.avd_name()); let log = sdk.home().join("emulator.log"); let mut c = sdk.command(&sdk.emulator_bin()); @@ -222,6 +223,24 @@ fn boot_args(cfg: &Config) -> Vec { args } +/// Make the AVD type from the host (Mac) keyboard. avdmanager's device profiles +/// leave `hw.keyboard=no` in the generated `config.ini`, which drops physical +/// key events; patch it to `yes` in place before booting. Idempotent and applied +/// on every cold boot so AVDs created by an older andro heal automatically. The +/// on-screen keyboard is unaffected — the images keep `show_ime_with_hard_keyboard=1`. +/// Best-effort: a missing/unwritable config.ini just leaves the AVD as-is. +fn enable_hw_keyboard(sdk: &Sdk, cfg: &Config) { + let config = sdk + .avd_home() + .join(format!("{}.avd", cfg.profile.avd_name())) + .join("config.ini"); + if let Ok(text) = fs::read_to_string(&config) + && let Some(patched) = emulator::with_hw_keyboard_enabled(&text) + { + let _ = fs::write(&config, patched); + } +} + /// Remove stale AVD lock files left by a crashed emulator. Only safe because the /// caller already checked the emulator is NOT running; clears the spurious /// "AVD is already in use" error so the next boot just works.