diff --git a/src/config/mod.rs b/src/config/mod.rs index db0fe03..e7ff4b1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -17,7 +17,7 @@ use keymap::Keymap; use modmap::Modmap; use nix::sys::inotify::{AddWatchFlags, InitFlags, Inotify}; use serde::{Deserialize, Deserializer}; -use std::{collections::HashMap, error, fs, path::Path, time::SystemTime}; +use std::{collections::HashMap, error, fs, path::PathBuf, time::SystemTime}; use self::{ key::parse_key, @@ -46,12 +46,21 @@ pub struct Config { pub keymap_table: HashMap>, } -pub fn load_config(filename: &Path) -> Result> { - let yaml = fs::read_to_string(&filename)?; +pub fn load_configs(filenames: &Vec) -> Result> { + // Assumes filenames is non-empty + let yaml = fs::read_to_string(&filenames[0])?; let mut config: Config = serde_yaml::from_str(&yaml)?; + for filename in &filenames[1..] { + let yaml = fs::read_to_string(&filename)?; + let c: Config = serde_yaml::from_str(&yaml)?; + config.modmap.extend(c.modmap); + config.keymap.extend(c.keymap); + config.virtual_modifiers.extend(c.virtual_modifiers); + } + // Timestamp for --watch=config - config.modify_time = filename.metadata()?.modified().ok(); + config.modify_time = filenames.last().and_then(|path| path.metadata().ok()?.modified().ok()); // Convert keymap for efficient keymap lookup config.keymap_table = build_keymap_table(&config.keymap); @@ -59,14 +68,16 @@ pub fn load_config(filename: &Path) -> Result> { Ok(config) } -pub fn config_watcher(watch: bool, file: &Path) -> anyhow::Result> { +pub fn config_watcher(watch: bool, files: &Vec) -> anyhow::Result> { if watch { let inotify = Inotify::init(InitFlags::IN_NONBLOCK)?; - inotify.add_watch( - file.parent().expect("config file has a parent directory"), - AddWatchFlags::IN_CREATE | AddWatchFlags::IN_MOVED_TO, - )?; - inotify.add_watch(file, AddWatchFlags::IN_MODIFY)?; + for file in files { + inotify.add_watch( + file.parent().expect("config file has a parent directory"), + AddWatchFlags::IN_CREATE | AddWatchFlags::IN_MOVED_TO, + )?; + inotify.add_watch(file, AddWatchFlags::IN_MODIFY)?; + } Ok(Some(inotify)) } else { Ok(None) diff --git a/src/main.rs b/src/main.rs index b91cd5e..2d52bc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, bail, Context}; use clap::{AppSettings, ArgEnum, IntoApp, Parser}; use clap_complete::Shell; use client::build_client; -use config::{config_watcher, load_config}; +use config::{config_watcher, load_configs}; use device::InputDevice; use event::Event; use nix::libc::ENODEV; @@ -17,7 +17,7 @@ use nix::sys::timerfd::{ClockId, TimerFd, TimerFlags}; use std::collections::HashMap; use std::io::stdout; use std::os::unix::io::{AsRawFd, RawFd}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::time::Duration; mod action; @@ -45,7 +45,7 @@ struct Opts { /// Targets to watch /// /// - device: add new devices automatically - /// - config: reload the config automatically + /// - config: reload the configs automatically #[clap( long, arg_enum, @@ -67,9 +67,9 @@ struct Opts { /// - in fish: xremap --completions fish | source #[clap(long, arg_enum, display_order = 100, value_name = "SHELL", verbatim_doc_comment)] completions: Option, - /// Config file - #[clap(required_unless_present = "completions")] - config: Option, + /// Config file(s) + #[clap(required_unless_present = "completions", multiple_values = true)] + configs: Vec, } #[derive(ArgEnum, Clone, Copy, Debug, PartialEq, Eq)] @@ -94,7 +94,7 @@ fn main() -> anyhow::Result<()> { ignore: ignore_filter, mouse, watch, - config, + configs, completions, } = Opts::parse(); @@ -104,10 +104,22 @@ fn main() -> anyhow::Result<()> { } // Configuration - let config_path = config.expect("config is set, if not completions"); - let mut config = match config::load_config(&config_path) { + let config_paths = match configs[..] { + [] => panic!("config is set, if not completions"), + _ => configs, + }; + + let mut config = match config::load_configs(&config_paths) { Ok(config) => config, - Err(e) => bail!("Failed to load config '{}': {}", config_path.display(), e), + Err(e) => bail!( + "Failed to load config '{}': {}", + config_paths + .iter() + .map(|p| p.to_string_lossy()) + .collect::>() + .join("', '"), + e + ), }; let watch_devices = watch.contains(&WatchTargets::Device); let watch_config = watch.contains(&WatchTargets::Config); @@ -121,7 +133,7 @@ fn main() -> anyhow::Result<()> { Err(e) => bail!("Failed to prepare input devices: {}", e), }; let device_watcher = device_watcher(watch_devices).context("Setting up device watcher")?; - let config_watcher = config_watcher(watch_config, &config_path).context("Setting up config watcher")?; + let config_watcher = config_watcher(watch_config, &config_paths).context("Setting up config watcher")?; let watchers: Vec<_> = device_watcher.iter().chain(config_watcher.iter()).collect(); let mut handler = EventHandler::new(timer, &config.default_mode, delay, build_client()); let output_device = match output_device(input_devices.values().next().map(InputDevice::bus_type)) { @@ -166,7 +178,7 @@ fn main() -> anyhow::Result<()> { &device_filter, &ignore_filter, mouse, - &config_path, + &config_paths, )? { break 'event_loop ReloadEvent::ReloadConfig; } @@ -183,10 +195,17 @@ fn main() -> anyhow::Result<()> { }; } ReloadEvent::ReloadConfig => { - match (config.modify_time, config_path.metadata().and_then(|m| m.modified())) { - (Some(last_mtime), Ok(current_mtim)) if last_mtime == current_mtim => continue, + match ( + config.modify_time, + config_paths + .iter() + .map(|p| p.metadata().ok()?.modified().ok()) + .flatten() + .max(), + ) { + (Some(last_mtime), Some(current_mtim)) if last_mtime == current_mtim => continue, _ => { - if let Ok(c) = load_config(&config_path) { + if let Ok(c) = load_configs(&config_paths) { println!("Reloading Config"); config = c; } @@ -281,12 +300,16 @@ fn handle_config_changes( device_filter: &[String], ignore_filter: &[String], mouse: bool, - config_path: &Path, + config_paths: &Vec, ) -> anyhow::Result { for event in &events { match (event.mask, &event.name) { // Dir events - (_, Some(name)) if name == config_path.file_name().expect("Config path has a file name") => { + (_, Some(name)) + if config_paths + .iter() + .any(|p| name == p.file_name().expect("Config path has a file name")) => + { return Ok(false) } // File events