use bevy::ecs::resource::Resource;
use log::{error, info, warn};
use objc2_core_foundation::{CFData, CFString};
use regex::Regex;
use serde::{Deserialize, Deserializer, de};
use std::{
collections::HashMap,
env,
ffi::c_void,
path::{Path, PathBuf},
ptr::NonNull,
sync::{Arc, LazyLock, RwLock},
};
use stdext::function_name;
use stdext::prelude::RwLockExt;
use crate::errors::{Error, Result};
use crate::{
commands::{Command, Direction, Operation},
platform::OSStatus,
};
use crate::{platform::CFStringRef, util::AXUIWrapper};
pub static CONFIGURATION_FILE: LazyLock<PathBuf> = LazyLock::new(|| {
if let Ok(path_str) = env::var("PANERU_CONFIG") {
let path = PathBuf::from(path_str);
if path.exists() {
return path;
}
warn!(
"{}: $PANERU_CONFIG is set to {}, but the file does not exist. Falling back to default locations.",
function_name!(),
path.display()
);
}
let standard_paths = [
env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".paneru")),
env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".paneru.toml")),
env::var("XDG_CONFIG_HOME")
.ok()
.map(|x| PathBuf::from(x).join("paneru/paneru.toml")),
];
standard_paths
.into_iter()
.flatten()
.find(|path| path.exists())
.unwrap_or_else(|| {
panic!(
"{}: Configuration file not found. Tried: $PANERU_CONFIG, $HOME/.paneru, $HOME/.paneru.toml, $XDG_CONFIG_HOME/paneru/paneru.toml",
function_name!()
)
})
});
fn parse_direction(dir: &str) -> Result<Direction> {
Ok(match dir {
"north" => Direction::North,
"south" => Direction::South,
"west" => Direction::West,
"east" => Direction::East,
"first" => Direction::First,
"last" => Direction::Last,
_ => {
return Err(Error::InvalidConfig(format!(
"{}: Unhandled direction {dir}",
function_name!()
)));
}
})
}
fn parse_operation(argv: &[&str]) -> Result<Operation> {
let empty = "";
let cmd = *argv.first().unwrap_or(&empty);
let err = Error::InvalidConfig(format!("{}: Invalid command '{argv:?}'", function_name!()));
let out = match cmd {
"focus" => Operation::Focus(parse_direction(argv.get(1).ok_or(err)?)?),
"swap" => Operation::Swap(parse_direction(argv.get(1).ok_or(err)?)?),
"center" => Operation::Center,
"resize" => Operation::Resize,
"fullwidth" => Operation::FullWidth,
"manage" => Operation::Manage,
"stack" => Operation::Stack(true),
"unstack" => Operation::Stack(false),
"nextdisplay" => Operation::ToNextDisplay,
_ => {
return Err(err);
}
};
Ok(out)
}
pub fn parse_command(argv: &[&str]) -> Result<Command> {
let empty = "";
let cmd = *argv.first().unwrap_or(&empty);
let out = match cmd {
"printstate" => Command::PrintState,
"window" => Command::Window(parse_operation(&argv[1..])?),
"quit" => Command::Quit,
_ => {
return Err(Error::InvalidConfig(format!(
"{}: Unhandled command '{argv:?}'",
function_name!()
)));
}
};
Ok(out)
}
#[derive(Clone, Debug, Resource)]
pub struct Config {
inner: Arc<RwLock<InnerConfig>>,
}
impl Config {
pub fn new(path: &Path) -> Result<Self> {
Ok(Config {
inner: RwLock::new(InnerConfig::new(path)?).into(),
})
}
pub fn reload_config(&mut self, path: &Path) -> Result<()> {
let new = InnerConfig::new(path)?;
let mut old = self.inner.force_write();
*old = new;
Ok(())
}
fn inner(&self) -> std::sync::RwLockReadGuard<'_, InnerConfig> {
self.inner.force_read()
}
pub fn options(&self) -> MainOptions {
self.inner().options.clone()
}
pub fn animation_speed(&self) -> f64 {
self.options()
.animation_speed
.unwrap_or(1_000_000.0)
.max(5.0)
/ 10.0
}
pub fn find_keybind(&self, keycode: u8, mask: u8) -> Option<Command> {
let lock = self.inner();
lock.bindings
.values()
.flat_map(|binds| binds.all())
.find_map(|bind| {
(bind.code == keycode && bind.modifiers == mask).then_some(bind.command.clone())
})
}
pub fn find_window_properties(&self, title: &str, bundle_id: &str) -> Vec<WindowParams> {
self.inner()
.windows
.as_ref()
.map(|windows| {
windows
.values()
.filter(|params| {
let bundle_match =
params.bundle_id.as_ref().map(|id| id.as_str() == bundle_id);
bundle_match.is_none_or(|m| m) && params.title.is_match(title)
})
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
}
impl Default for Config {
fn default() -> Self {
Config {
inner: RwLock::new(InnerConfig::default()).into(),
}
}
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum OneOrMore {
Single(Keybinding),
Multiple(Vec<Keybinding>),
}
impl OneOrMore {
fn all(&self) -> Vec<&Keybinding> {
match self {
OneOrMore::Single(one) => vec![one],
OneOrMore::Multiple(many) => many.iter().collect::<Vec<_>>(),
}
}
fn all_mut(&mut self) -> Vec<&mut Keybinding> {
match self {
OneOrMore::Single(one) => vec![one],
OneOrMore::Multiple(many) => many.iter_mut().collect::<Vec<_>>(),
}
}
}
#[derive(Deserialize, Debug, Default)]
struct InnerConfig {
options: MainOptions,
bindings: HashMap<String, OneOrMore>,
windows: Option<HashMap<String, WindowParams>>,
}
impl InnerConfig {
fn new(path: &Path) -> Result<InnerConfig> {
let input = std::fs::read_to_string(path)?;
InnerConfig::parse_config(&input)
}
fn parse_config(input: &str) -> Result<InnerConfig> {
let virtual_keys = generate_virtual_keymap();
let mut config: InnerConfig = toml::from_str(input)?;
for (command, bindings) in &mut config.bindings {
let argv = command.split('_').collect::<Vec<_>>();
for binding in bindings.all_mut() {
binding.command = parse_command(&argv)?;
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;
} else {
error!("{}: invalid key '{}'", function_name!(), &binding.key);
}
info!("bind: {binding:?}");
}
}
Ok(config)
}
}
#[derive(Deserialize, Clone, Debug, Default)]
pub struct MainOptions {
pub focus_follows_mouse: Option<bool>,
pub mouse_follows_focus: 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 continuous_swipe: Option<bool>,
}
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: &Config) -> Vec<f64> {
config.options().preset_column_widths
}
#[derive(Debug)]
pub struct Keybinding {
pub key: String,
pub code: u8,
pub modifiers: u8,
pub command: Command,
}
impl<'de> Deserialize<'de> for Keybinding {
fn deserialize<D>(deserializer: D) -> std::result::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: Command::Quit,
})
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct WindowParams {
#[serde(deserialize_with = "deserialize_title")]
title: Regex,
bundle_id: Option<String>,
pub floating: Option<bool>,
pub index: Option<usize>,
pub vertical_padding: Option<u16>,
pub horizontal_padding: Option<u16>,
}
fn deserialize_title<'de, D>(deserializer: D) -> std::result::Result<Regex, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Regex::new(&s).map_err(de::Error::custom)
}
fn parse_modifiers(input: &str) -> Result<u8> {
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(Error::InvalidConfig(format!(
"{}: Invalid modifier: {modifier}",
function_name!()
)));
}
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"
window_manage = "ctrl+alt-t"
window_stack = ["ctrl-s", "alt-s"]
[windows]
[windows.pip]
title = "picture.*picture"
bundle_id = "com.something.apple"
floating = true
index = 1
"#;
let config = Config {
inner: RwLock::new(InnerConfig::parse_config(input).expect("Failed to parse config"))
.into(),
};
let find_key = |k| {
virtual_keycode()
.find_map(|(s, v)| (format!("{k}") == *s).then_some(*v))
.unwrap()
};
assert_eq!(config.inner().options.focus_follows_mouse, Some(true));
let mask = (1 << 0) | (1 << 3);
let keycode = find_key('q');
assert!(matches!(
config.find_keybind(keycode, mask),
Some(Command::Quit)
));
let keycode = find_key('t');
assert!(matches!(
config.find_keybind(keycode, mask),
Some(Command::Window(Operation::Manage))
));
let mask = 1 << 3; let keycode = find_key('s');
assert!(matches!(
config.find_keybind(keycode, mask),
Some(Command::Window(Operation::Stack(true)))
));
let mask = 1; assert!(matches!(
config.find_keybind(keycode, mask),
Some(Command::Window(Operation::Stack(true)))
));
let props = config.find_window_properties("picture in picture", "com.something.apple");
assert_eq!(props[0].floating, Some(true));
assert_eq!(props[0].index, Some(1));
}