#![allow(unsafe_code)]
#![deny(clippy::undocumented_unsafe_blocks)]
const KEYS: [(&str, u8, u8); 37] = [
("KeyA", 30, 0x00),
("KeyB", 48, 0x0B),
("KeyC", 46, 0x08),
("KeyD", 32, 0x02),
("KeyE", 18, 0x0E),
("KeyF", 33, 0x03),
("KeyG", 34, 0x05),
("KeyH", 35, 0x04),
("KeyI", 23, 0x22),
("KeyJ", 36, 0x26),
("KeyK", 37, 0x28),
("KeyL", 38, 0x25),
("KeyM", 50, 0x2E),
("KeyN", 49, 0x2D),
("KeyO", 24, 0x1F),
("KeyP", 25, 0x23),
("KeyQ", 16, 0x0C),
("KeyR", 19, 0x0F),
("KeyS", 31, 0x01),
("KeyT", 20, 0x11),
("KeyU", 22, 0x20),
("KeyV", 47, 0x09),
("KeyW", 17, 0x0D),
("KeyX", 45, 0x07),
("KeyY", 21, 0x10),
("KeyZ", 44, 0x06),
("Comma", 51, 0x2B),
("Period", 52, 0x2F),
("Slash", 53, 0x2C),
("Semicolon", 39, 0x29),
("Quote", 40, 0x27),
("BracketLeft", 26, 0x21),
("BracketRight", 27, 0x1E),
("Backslash", 43, 0x2A),
("Minus", 12, 0x1B),
("Equal", 13, 0x18),
("Backquote", 41, 0x32),
];
#[must_use]
pub fn query() -> Vec<(&'static str, char)> {
platform::query().unwrap_or_default()
}
#[cfg(target_os = "linux")]
mod platform {
use super::KEYS;
use x11rb::connection::Connection;
use x11rb::protocol::xproto::ConnectionExt;
const X11_OFFSET: u8 = 8;
pub fn query() -> Option<Vec<(&'static str, char)>> {
let (conn, _) = x11rb::connect(None).ok()?;
let (first, last) = {
let setup = conn.setup();
(setup.min_keycode, setup.max_keycode)
};
let count = last.checked_sub(first)?.checked_add(1)?;
let map = conn.get_keyboard_mapping(first, count).ok()?.reply().ok()?;
let per_key = usize::from(map.keysyms_per_keycode);
if per_key == 0 {
return None;
}
let mut caps = Vec::new();
for (key, evdev, _) in KEYS {
let Some(row) = evdev
.checked_add(X11_OFFSET)
.filter(|code| (first..=last).contains(code))
.and_then(|code| code.checked_sub(first))
.map(usize::from)
else {
continue;
};
let Some(&keysym) = map.keysyms.get(row * per_key) else {
continue;
};
if let Some(cap) = char::from_u32(keysym).filter(char::is_ascii_graphic) {
caps.push((key, cap));
}
}
(!caps.is_empty()).then_some(caps)
}
}
#[cfg(target_os = "windows")]
mod platform {
use super::KEYS;
const VSC_TO_VK_EX: u32 = 3;
const VK_TO_CHAR: u32 = 2;
const DEAD_KEY: u32 = 0x8000_0000;
#[link(name = "user32")]
unsafe extern "system" {
#[link_name = "GetKeyboardLayout"]
fn get_keyboard_layout(thread: u32) -> isize;
#[link_name = "MapVirtualKeyExW"]
fn map_virtual_key_ex_w(code: u32, translation: u32, layout: isize) -> u32;
}
pub fn query() -> Option<Vec<(&'static str, char)>> {
let layout = unsafe { get_keyboard_layout(0) };
if layout == 0 {
return None;
}
let mut caps = Vec::new();
for (key, scancode, _) in KEYS {
let virtual_key =
unsafe { map_virtual_key_ex_w(u32::from(scancode), VSC_TO_VK_EX, layout) };
if virtual_key == 0 {
continue;
}
let printed = unsafe { map_virtual_key_ex_w(virtual_key, VK_TO_CHAR, layout) };
if printed & DEAD_KEY != 0 {
continue;
}
if let Some(cap) = char::from_u32(printed).filter(char::is_ascii_graphic) {
caps.push((key, cap));
}
}
(!caps.is_empty()).then_some(caps)
}
}
#[cfg(target_os = "macos")]
mod platform {
use super::KEYS;
use std::ffi::c_void;
use std::os::raw::c_ulong;
const ACTION_DISPLAY: u16 = 3;
const NO_DEAD_KEYS: u32 = 1;
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
#[link_name = "kTISPropertyUnicodeKeyLayoutData"]
static UNICODE_KEY_LAYOUT_DATA: *const c_void;
#[link_name = "TISCopyCurrentKeyboardLayoutInputSource"]
fn copy_current_keyboard_layout_input_source() -> *mut c_void;
#[link_name = "TISGetInputSourceProperty"]
fn get_input_source_property(source: *mut c_void, key: *const c_void) -> *mut c_void;
#[link_name = "LMGetKbdType"]
fn get_kbd_type() -> u8;
#[link_name = "UCKeyTranslate"]
fn key_translate(
layout: *const c_void,
virtual_key: u16,
action: u16,
modifiers: u32,
keyboard_type: u32,
options: u32,
dead_key_state: *mut u32,
max_len: c_ulong,
len: *mut c_ulong,
unicode: *mut u16,
) -> i32;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
#[link_name = "CFDataGetBytePtr"]
fn data_byte_ptr(data: *mut c_void) -> *const u8;
#[link_name = "CFRelease"]
fn release(item: *mut c_void);
}
pub fn query() -> Option<Vec<(&'static str, char)>> {
let source = unsafe { copy_current_keyboard_layout_input_source() };
if source.is_null() {
return None;
}
let data = unsafe { get_input_source_property(source, UNICODE_KEY_LAYOUT_DATA) };
if data.is_null() {
unsafe { release(source) };
return None;
}
let layout = unsafe { data_byte_ptr(data) }.cast::<c_void>();
let keyboard_type = u32::from(unsafe { get_kbd_type() });
let mut caps = Vec::new();
for (key, _, virtual_key) in KEYS {
let mut unicode = [0u16; 4];
let mut len: c_ulong = 0;
let mut dead_key_state: u32 = 0;
let status = unsafe {
key_translate(
layout,
u16::from(virtual_key),
ACTION_DISPLAY,
0,
keyboard_type,
NO_DEAD_KEYS,
&raw mut dead_key_state,
unicode.len() as c_ulong,
&raw mut len,
unicode.as_mut_ptr(),
)
};
if status != 0 || len != 1 {
continue;
}
if let Some(cap) = char::from_u32(u32::from(unicode[0])).filter(char::is_ascii_graphic)
{
caps.push((key, cap));
}
}
unsafe { release(source) };
(!caps.is_empty()).then_some(caps)
}
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
mod platform {
pub fn query() -> Option<Vec<(&'static str, char)>> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_key_table_is_sound() {
for column in 0..3 {
let mut seen: Vec<String> = KEYS
.iter()
.map(|&(name, evdev, mac)| match column {
0 => name.to_string(),
1 => evdev.to_string(),
_ => mac.to_string(),
})
.collect();
seen.sort();
seen.dedup();
assert_eq!(seen.len(), KEYS.len(), "column {column} names a key twice");
}
for (name, evdev, _) in KEYS {
assert!(!name.is_empty() && name.is_ascii(), "{name}");
assert!(evdev > 0, "{name} has no scancode");
}
}
#[test]
fn the_answer_keeps_its_promises() {
for (key, cap) in query() {
assert!(cap.is_ascii_graphic(), "{key} said {cap:?}");
assert!(KEYS.iter().any(|&(k, ..)| k == key), "{key} is not a key");
}
}
}