From 8aa549fe65227558ba274b247d96fefb68ece868 Mon Sep 17 00:00:00 2001 From: Huy Date: Mon, 20 Feb 2023 16:02:41 -0800 Subject: [PATCH] Support non-US keyboard layout on macOS This commit implements a look up table to map between CGKeyCode on the physical keyboard and the actual output character from the selected input source. This table is built once when the application starts. Currently, we have to restart the application if we change the input source. This can be improved in the future. Fixes #20 --- Cargo.lock | 114 ++++++++++++++++++++++++++++++++++++------ Cargo.toml | 1 + src/input.rs | 66 +++++++++++++++++++++++- src/main.rs | 17 ++++++- src/platform/macos.rs | 102 ++++++++++++++++++++----------------- 5 files changed, 235 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4344009..27c6919 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cocoa" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667fdc068627a2816b9ff831201dd9864249d6ee8d190b9532357f1fc0f61ea7" +dependencies = [ + "bitflags", + "block", + "core-foundation 0.9.3", + "core-graphics 0.21.0", + "foreign-types", + "libc", + "objc", +] + [[package]] name = "cocoa" version = "0.24.1" @@ -143,8 +158,8 @@ dependencies = [ "bitflags", "block", "cocoa-foundation", - "core-foundation", - "core-graphics", + "core-foundation 0.9.3", + "core-graphics 0.22.3", "foreign-types", "libc", "objc", @@ -158,7 +173,7 @@ checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" dependencies = [ "bitflags", "block", - "core-foundation", + "core-foundation 0.9.3", "core-graphics-types", "foreign-types", "libc", @@ -181,22 +196,62 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags", + "core-foundation 0.7.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" +dependencies = [ + "bitflags", + "core-foundation 0.9.3", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics" version = "0.22.3" @@ -204,7 +259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.3", "core-graphics-types", "foreign-types", "libc", @@ -217,7 +272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.3", "foreign-types", "libc", ] @@ -228,8 +283,8 @@ version = "19.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d74ada66e07c1cefa18f8abfba765b486f250de2e4a999e5727fc0dd4b4a25" dependencies = [ - "core-foundation", - "core-graphics", + "core-foundation 0.9.3", + "core-graphics 0.22.3", "foreign-types", "libc", ] @@ -294,8 +349,8 @@ dependencies = [ "block", "cairo-rs", "cfg-if", - "cocoa", - "core-graphics", + "cocoa 0.24.1", + "core-graphics 0.22.3", "foreign-types", "gdk", "gdk-pixbuf", @@ -673,14 +728,15 @@ name = "goxkey" version = "0.1.0" dependencies = [ "bitflags", - "core-foundation", - "core-graphics", + "core-foundation 0.9.3", + "core-graphics 0.22.3", "druid", "env_logger", "foreign-types", "libc", "log", "once_cell", + "rdev", "vi", ] @@ -1034,7 +1090,7 @@ dependencies = [ "cairo-rs", "cairo-sys-rs", "cfg-if", - "core-graphics", + "core-graphics 0.22.3", "piet", "piet-cairo", "piet-coregraphics", @@ -1050,9 +1106,9 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5af1ce12cf0a1dc7f894f6f90ba62467d685a39b7f1d235516c8688f98e92d18" dependencies = [ - "core-foundation", - "core-foundation-sys", - "core-graphics", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", "core-text", "foreign-types", "piet", @@ -1176,6 +1232,22 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rdev" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f183f2103ea19c0e2ab5c41c930f0fb3270b6ba7695fd0f5d12b73b86e64b10" +dependencies = [ + "cocoa 0.22.0", + "core-foundation 0.7.0", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "lazy_static", + "libc", + "winapi", + "x11", +] + [[package]] name = "regex" version = "1.7.1" @@ -1826,6 +1898,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "xi-unicode" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 2d542a6..7fd72de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ vi = "0.3.5" bitflags = "1.3.2" druid = "0.7.0" once_cell = "1.17.0" +rdev = "0.5.2" [package.metadata.bundle] copyright = "Copyright (c) Huy Tran 2023. All rights reserved." diff --git a/src/input.rs b/src/input.rs index 5122876..d3091bf 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,8 +1,8 @@ -use std::{fmt::Display, str::FromStr}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; use druid::{Data, Target}; use log::debug; -use once_cell::sync::Lazy; +use once_cell::sync::{Lazy, OnceCell}; use crate::{ config::{CONFIG_MANAGER, HOTKEY_CONFIG_KEY, TYPING_METHOD_CONFIG_KEY}, @@ -32,6 +32,68 @@ const TONABLE_VOWELS: [char; 144] = [ pub static mut INPUT_STATE: Lazy = Lazy::new(|| InputState::new()); +pub const PREDEFINED_CHARS: [char; 47] = [ + 'a', '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'q', 'w', 'e', 'r', 't', + 'y', 'u', 'i', 'o', 'p', '[', ']', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '\\', + 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', +]; + +pub fn get_key_from_char(c: char) -> rdev::Key { + use rdev::Key::*; + match &c { + 'a' => KeyA, + '`' => BackQuote, + '1' => Num1, + '2' => Num2, + '3' => Num3, + '4' => Num4, + '5' => Num5, + '6' => Num6, + '7' => Num7, + '8' => Num8, + '9' => Num9, + '0' => Num0, + '-' => Minus, + '=' => Equal, + 'q' => KeyQ, + 'w' => KeyW, + 'e' => KeyE, + 'r' => KeyR, + 't' => KeyT, + 'y' => KeyY, + 'u' => KeyU, + 'i' => KeyI, + 'o' => KeyO, + 'p' => KeyP, + '[' => LeftBracket, + ']' => RightBracket, + 's' => KeyS, + 'd' => KeyD, + 'f' => KeyF, + 'g' => KeyG, + 'h' => KeyH, + 'j' => KeyJ, + 'k' => KeyK, + 'l' => KeyL, + ';' => SemiColon, + '\'' => Quote, + '\\' => BackSlash, + 'z' => KeyZ, + 'x' => KeyX, + 'c' => KeyC, + 'v' => KeyV, + 'b' => KeyB, + 'n' => KeyN, + 'm' => KeyM, + ',' => Comma, + '.' => Dot, + '/' => Slash, + _ => Unknown(0), + } +} + +pub static KEYBOARD_LAYOUT_CHARACTER_MAP: OnceCell> = OnceCell::new(); + #[derive(PartialEq, Eq, Data, Clone, Copy)] pub enum TypingMethod { VNI, diff --git a/src/main.rs b/src/main.rs index bdf738b..6ee0934 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,15 @@ mod platform; mod ui; use druid::{AppLauncher, ExtEventSink, Target, WindowDesc}; -use input::INPUT_STATE; +use input::{get_key_from_char, INPUT_STATE, KEYBOARD_LAYOUT_CHARACTER_MAP, PREDEFINED_CHARS}; use log::debug; use once_cell::sync::OnceCell; use platform::{ run_event_listener, send_backspace, send_string, Handle, KeyModifier, KEY_DELETE, KEY_ENTER, KEY_ESCAPE, KEY_SPACE, KEY_TAB, }; -use std::thread; +use rdev::{EventType, Keyboard, KeyboardState}; +use std::{collections::HashMap, thread}; use ui::{UIDataAdapter, UPDATE_UI}; static UI_EVENT_SINK: OnceCell = OnceCell::new(); @@ -38,6 +39,7 @@ fn event_handler(handle: Handle, keycode: Option, modifiers: KeyModifier) unsafe { match keycode { Some(keycode) => { + println!("KEYCODE: {}", keycode); // Toggle Vietnamese input mod with Ctrl + Cmd + Space key if INPUT_STATE.get_hotkey().is_match(modifiers, &keycode) { INPUT_STATE.toggle_vietnamese(); @@ -99,6 +101,17 @@ fn event_handler(handle: Handle, keycode: Option, modifiers: KeyModifier) fn main() { env_logger::init(); + let mut map = HashMap::new(); + let mut kb = Keyboard::new().unwrap(); + for c in PREDEFINED_CHARS { + let key = rdev::EventType::KeyPress(get_key_from_char(c)); + if let Some(s) = kb.add(&key) { + let ch = s.chars().last().unwrap(); + map.insert(c, ch); + } + } + _ = KEYBOARD_LAYOUT_CHARACTER_MAP.set(map); + let win = WindowDesc::new(ui::main_ui_builder) .title("gõkey") .window_size((320.0, 234.0)) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index c6b952b..7d392ed 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,4 +1,6 @@ -use std::{env, os, path::PathBuf, ptr}; +use std::{env, path::PathBuf, ptr}; + +use crate::input::KEYBOARD_LAYOUT_CHARACTER_MAP; use super::{CallbackFn, KeyModifier, KEY_DELETE, KEY_ENTER, KEY_ESCAPE, KEY_SPACE, KEY_TAB}; use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop}; @@ -21,52 +23,61 @@ pub fn get_home_dir() -> Option { env::var("HOME").ok().map(PathBuf::from) } -// Modified from http://ritter.ist.psu.edu/projects/RUI/macosx/rui.c +// List of keycode: https://eastmanreference.com/complete-list-of-applescript-key-codes fn get_char(keycode: CGKeyCode) -> Option { - match keycode { - 0 => Some('a'), - 1 => Some('s'), - 2 => Some('d'), - 3 => Some('f'), - 4 => Some('h'), - 5 => Some('g'), - 6 => Some('z'), - 7 => Some('x'), - 8 => Some('c'), - 9 => Some('v'), - 11 => Some('b'), - 12 => Some('q'), - 13 => Some('w'), - 14 => Some('e'), - 15 => Some('r'), - 16 => Some('y'), - 17 => Some('t'), - 31 => Some('o'), - 32 => Some('u'), - 34 => Some('i'), - 35 => Some('p'), - 37 => Some('l'), - 38 => Some('j'), - 40 => Some('k'), - 45 => Some('n'), - 46 => Some('m'), - 18 => Some('1'), - 19 => Some('2'), - 20 => Some('3'), - 21 => Some('4'), - 22 => Some('6'), - 23 => Some('5'), - 25 => Some('9'), - 26 => Some('7'), - 28 => Some('8'), - 29 => Some('0'), - 36 | 52 => Some(KEY_ENTER), // ENTER - 49 => Some(KEY_SPACE), // SPACE - 48 => Some(KEY_TAB), // TAB - 51 => Some(KEY_DELETE), // DELETE - 53 => Some(KEY_ESCAPE), // ESC - _ => None, + if let Some(key_map) = KEYBOARD_LAYOUT_CHARACTER_MAP.get() { + println!("GOT KEYCODE {:?}", keycode); + return match keycode { + 0 => Some(key_map[&'a']), + 1 => Some(key_map[&'s']), + 2 => Some(key_map[&'d']), + 3 => Some(key_map[&'f']), + 4 => Some(key_map[&'h']), + 5 => Some(key_map[&'g']), + 6 => Some(key_map[&'z']), + 7 => Some(key_map[&'x']), + 8 => Some(key_map[&'c']), + 9 => Some(key_map[&'v']), + 11 => Some(key_map[&'b']), + 12 => Some(key_map[&'q']), + 13 => Some(key_map[&'w']), + 14 => Some(key_map[&'e']), + 15 => Some(key_map[&'r']), + 16 => Some(key_map[&'y']), + 17 => Some(key_map[&'t']), + 31 => Some(key_map[&'o']), + 32 => Some(key_map[&'u']), + 34 => Some(key_map[&'i']), + 35 => Some(key_map[&'p']), + 37 => Some(key_map[&'l']), + 38 => Some(key_map[&'j']), + 40 => Some(key_map[&'k']), + 45 => Some(key_map[&'n']), + 46 => Some(key_map[&'m']), + 18 => Some(key_map[&'1']), + 19 => Some(key_map[&'2']), + 20 => Some(key_map[&'3']), + 21 => Some(key_map[&'4']), + 22 => Some(key_map[&'6']), + 23 => Some(key_map[&'5']), + 25 => Some(key_map[&'9']), + 26 => Some(key_map[&'7']), + 28 => Some(key_map[&'8']), + 29 => Some(key_map[&'0']), + 27 => Some(key_map[&'-']), + 33 => Some(key_map[&'[']), + 30 => Some(key_map[&']']), + 41 => Some(key_map[&';']), + 43 => Some(key_map[&',']), + 36 | 52 => Some(KEY_ENTER), // ENTER + 49 => Some(KEY_SPACE), // SPACE + 48 => Some(KEY_TAB), // TAB + 51 => Some(KEY_DELETE), // DELETE + 53 => Some(KEY_ESCAPE), // ESC + _ => None, + }; } + None } #[link(name = "CoreGraphics", kind = "framework")] @@ -265,6 +276,7 @@ pub fn run_event_listener(callback: &CallbackFn) { if flags.contains(CGEventFlags::CGEventFlagAlternate) { modifiers.add_alt(); } + if callback(proxy, get_char(key_code), modifiers) { // block the key if already processed return None;