use bevy::ecs::{resource::Resource, system::Res};
use log::{error, info};
use notify::EventHandler;
use objc2_core_foundation::{CFData, CFString};
use serde::{Deserialize, Deserializer, de};
use std::{
collections::HashMap,
ffi::c_void,
path::Path,
ptr::NonNull,
sync::{Arc, LazyLock, RwLock},
};
use stdext::function_name;
use stdext::prelude::RwLockExt;
use crate::{platform::CFStringRef, skylight::OSStatus, util::AXUIWrapper};
#[derive(Clone, Debug, Resource)]
pub struct Config {
inner: Arc<RwLock<InnerConfig>>,
}
impl Config {
pub fn new(path: &Path) -> Result<Self, String> {
Ok(Config {
inner: RwLock::new(InnerConfig::new(path)?).into(),
})
}
pub fn reload_config(&mut self, path: &Path) -> Result<(), String> {
let new = InnerConfig::new(path)?;
let mut old = self.inner.force_write();
old.options = new.options;
old.bindings = new.bindings;
Ok(())
}
fn inner(&self) -> std::sync::RwLockReadGuard<'_, InnerConfig> {
self.inner.force_read()
}
pub fn options(&self) -> MainOptions {
self.inner().options.clone()
}
pub fn find_keybind(&self, keycode: u8, mask: u8) -> Option<Keybinding> {
let lock = self.inner();
lock.bindings
.values()
.find(|bind| bind.code == keycode && bind.modifiers == mask)
.cloned()
}
}
impl EventHandler for Config {
fn handle_event(&mut self, event: notify::Result<notify::Event>) {
if let Ok(event) = event {
info!("Event: {event:?}");
}
}
}
#[derive(Deserialize, Debug)]
struct InnerConfig {
options: MainOptions,
bindings: HashMap<String, Keybinding>,
}
impl InnerConfig {
fn new(path: &Path) -> Result<InnerConfig, String> {
let input = std::fs::read_to_string(path).map_err(|err| {
format!(
"{}: can't open configuration in {} - {err}",
function_name!(),
path.display(),
)
})?;
InnerConfig::parse_config(&input)
}
fn parse_config(input: &str) -> Result<InnerConfig, String> {
let virtual_keys = generate_virtual_keymap();
let mut config: InnerConfig = toml::from_str(input)
.map_err(|err| format!("{}: error loading config: {err}", function_name!()))?;
config.bindings.iter_mut().for_each(|(command, binding)| {
binding.command.clone_from(command);
let code = virtual_keys
.iter()
.find(|(key, _)| key == &binding.key)
.map(|(_, code)| *code)
.or_else(|| {
literal_keycode()
.find(|(key, _)| key == &binding.key)
.map(|(_, code)| *code)
});
if let Some(code) = code {
binding.code = code;
}
info!("bind: {binding:?}");
});
Ok(config)
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct MainOptions {
pub focus_follows_mouse: Option<bool>,
pub swipe_gesture_fingers: Option<usize>,
#[serde(default = "default_preset_column_widths")]
pub preset_column_widths: Vec<f64>,
pub animation_speed: Option<f64>,
}
pub fn default_preset_column_widths() -> Vec<f64> {
vec![0.25, 0.33333, 0.50, 0.66667, 0.75]
}
pub fn preset_column_widths(config: Option<&Res<Config>>) -> Vec<f64> {
match config {
Some(config) => config.options().preset_column_widths,
None => default_preset_column_widths(),
}
}
#[derive(Clone, Debug)]
pub struct Keybinding {
pub key: String,
pub code: u8,
pub modifiers: u8,
pub command: String,
}
impl<'de> Deserialize<'de> for Keybinding {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let input = String::deserialize(deserializer)?;
let mut parts = input.split('-').map(str::trim).collect::<Vec<_>>();
let key = parts.pop();
if parts.len() > 1 || key.is_none() {
return Err(de::Error::custom(format!("Too many dashes: {input:?}")));
}
let modifiers = match parts.pop() {
Some(modifiers) => parse_modifiers(modifiers).map_err(de::Error::custom)?,
None => 0,
};
Ok(Keybinding {
key: key.unwrap().to_string(),
code: 0,
modifiers,
command: String::new(),
})
}
}
fn parse_modifiers(input: &str) -> Result<u8, String> {
static MOD_NAMES: [&str; 4] = ["alt", "shift", "cmd", "ctrl"];
let mut out = 0;
let modifiers = input.split('+').map(str::trim).collect::<Vec<_>>();
for modifier in &modifiers {
if !MOD_NAMES.iter().any(|name| name == modifier) {
return Err(format!("Invalid modifier: {modifier}"));
}
if let Some((shift, _)) = MOD_NAMES
.iter()
.enumerate()
.find(|(_, name)| *name == modifier)
{
out += 1 << shift;
}
}
Ok(out)
}
#[link(name = "Carbon", kind = "framework")]
unsafe extern "C" {
fn TISCopyCurrentASCIICapableKeyboardLayoutInputSource() -> *mut c_void;
fn TISGetInputSourceProperty(keyboard: *const c_void, property: CFStringRef) -> *mut CFData;
fn UCKeyTranslate(
keyLayoutPtr: *mut u8,
virtualKeyCode: u16,
keyAction: u16,
modifierKeyState: u32,
keyboardType: u32,
keyTranslateOptions: u32,
deadKeyState: &mut u32,
maxStringLength: usize,
actualStringLength: &mut isize,
unicodeString: *mut u16,
) -> OSStatus;
fn LMGetKbdType() -> u8;
static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
}
fn virtual_keycode() -> impl Iterator<Item = &'static (&'static str, u8)> {
static VIRTUAL_KEYCODE: LazyLock<Vec<(&'static str, u8)>> = LazyLock::new(|| {
vec![
("a", 0x00),
("s", 0x01),
("d", 0x02),
("f", 0x03),
("h", 0x04),
("g", 0x05),
("z", 0x06),
("x", 0x07),
("c", 0x08),
("v", 0x09),
("section", 0x0a), ("b", 0x0b),
("q", 0x0c),
("w", 0x0d),
("e", 0x0e),
("r", 0x0f),
("y", 0x10),
("t", 0x11),
("1", 0x12),
("2", 0x13),
("3", 0x14),
("4", 0x15),
("6", 0x16),
("5", 0x17),
("equal", 0x18),
("9", 0x19),
("7", 0x1a),
("minus", 0x1b),
("8", 0x1c),
("0", 0x1d),
("rightbracket", 0x1e),
("o", 0x1f),
("u", 0x20),
("leftbracket", 0x21),
("i", 0x22),
("p", 0x23),
("l", 0x25),
("j", 0x26),
("quote", 0x27),
("k", 0x28),
("semicolon", 0x29),
("backslash", 0x2a),
("comma", 0x2b),
("slash", 0x2c),
("n", 0x2d),
("m", 0x2e),
("period", 0x2f),
("grave", 0x32),
("keypaddecimal", 0x41),
("keypadmultiply", 0x43),
("keypadplus", 0x45),
("keypadclear", 0x47),
("keypaddivide", 0x4b),
("keypadenter", 0x4c),
("keypadminus", 0x4e),
("keypadequals", 0x51),
("keypad0", 0x52),
("keypad1", 0x53),
("keypad2", 0x54),
("keypad3", 0x55),
("keypad4", 0x56),
("keypad5", 0x57),
("keypad6", 0x58),
("keypad7", 0x59),
("keypad8", 0x5b),
("keypad9", 0x5c),
]
});
VIRTUAL_KEYCODE.iter()
}
fn literal_keycode() -> impl Iterator<Item = &'static (&'static str, u8)> {
static LITERAL_KEYCODE: LazyLock<Vec<(&'static str, u8)>> = LazyLock::new(|| {
vec![
("return", 0x24),
("tab", 0x30),
("space", 0x31),
("delete", 0x33),
("escape", 0x35),
("command", 0x37),
("shift", 0x38),
("capslock", 0x39),
("option", 0x3a),
("control", 0x3b),
("rightcommand", 0x36),
("rightshift", 0x3c),
("rightoption", 0x3d),
("rightcontrol", 0x3e),
("function", 0x3f),
("f17", 0x40),
("volumeup", 0x48),
("volumedown", 0x49),
("mute", 0x4a),
("f18", 0x4f),
("f19", 0x50),
("f20", 0x5a),
("f5", 0x60),
("f6", 0x61),
("f7", 0x62),
("f3", 0x63),
("f8", 0x64),
("f9", 0x65),
("f11", 0x67),
("f13", 0x69),
("f16", 0x6a),
("f14", 0x6b),
("f10", 0x6d),
("contextualmenu", 0x6e),
("f12", 0x6f),
("f15", 0x71),
("help", 0x72),
("home", 0x73),
("pageup", 0x74),
("forwarddelete", 0x75),
("f4", 0x76),
("end", 0x77),
("f2", 0x78),
("pagedown", 0x79),
("f1", 0x7a),
("leftarrow", 0x7b),
("rightarrow", 0x7c),
("downarrow", 0x7d),
("uparrow", 0x7e),
]
});
LITERAL_KEYCODE.iter()
}
enum UCKeyAction {
Down = 0,
}
fn generate_virtual_keymap() -> Vec<(String, u8)> {
let keyboard = AXUIWrapper::from_retained(unsafe {
TISCopyCurrentASCIICapableKeyboardLayoutInputSource()
})
.ok();
let keyboard_layout = keyboard
.and_then(|keyboard| {
NonNull::new(unsafe {
TISGetInputSourceProperty(
keyboard.as_ptr::<c_void>(),
kTISPropertyUnicodeKeyLayoutData,
)
})
})
.and_then(|uchr| NonNull::new(unsafe { CFData::byte_ptr(uchr.as_ref()).cast_mut() }));
let Some(keyboard_layout) = keyboard_layout else {
error!(
"{}: problem fetching current virtual keyboard layout.",
function_name!()
);
return vec![];
};
let mut state = 0u32;
let mut chars = vec![0u16; 256];
let mut got: isize = 0;
virtual_keycode()
.filter_map(|(_, keycode)| unsafe {
(0 == UCKeyTranslate(
keyboard_layout.as_ptr(),
(*keycode).into(),
UCKeyAction::Down as u16,
0,
LMGetKbdType().into(),
1,
&mut state,
chars.len(),
&mut got,
chars.as_mut_ptr(),
))
.then(|| {
let name = CFString::with_characters(None, chars.as_ptr(), got)
.map(|chars| chars.to_string());
name.zip(Some(*keycode))
})
})
.flatten()
.collect()
}
#[test]
fn test_config_parsing() {
let input = r#"
[options]
focus_follows_mouse = true
[bindings]
quit = "ctrl+alt-q"
manage = "ctrl+alt-t"
"#;
let config = InnerConfig::parse_config(input).expect("Failed to parse config");
assert_eq!(config.options.focus_follows_mouse, Some(true));
let quit_binding = config.bindings.get("quit").expect("Quit binding not found");
assert_eq!(quit_binding.key, "q");
assert_eq!(quit_binding.modifiers, (1 << 0) | (1 << 3));
assert_eq!(quit_binding.command, "quit");
let manage_binding = config
.bindings
.get("manage")
.expect("Manage binding not found");
assert_eq!(manage_binding.key, "t");
assert_eq!(manage_binding.modifiers, (1 << 0) | (1 << 3));
assert_eq!(manage_binding.command, "manage");
}