handle multiple config files (#269)

* handle multiple config files

* shorter variable name

* run formatter

* extend virtual modifiers too
This commit is contained in:
Ching Chang 2023-03-28 23:01:21 -04:00 committed by GitHub
parent 89ac4e6aca
commit bec3ce9bd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 61 additions and 27 deletions

View File

@ -17,7 +17,7 @@ use keymap::Keymap;
use modmap::Modmap; use modmap::Modmap;
use nix::sys::inotify::{AddWatchFlags, InitFlags, Inotify}; use nix::sys::inotify::{AddWatchFlags, InitFlags, Inotify};
use serde::{Deserialize, Deserializer}; 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::{ use self::{
key::parse_key, key::parse_key,
@ -46,12 +46,21 @@ pub struct Config {
pub keymap_table: HashMap<Key, Vec<KeymapEntry>>, pub keymap_table: HashMap<Key, Vec<KeymapEntry>>,
} }
pub fn load_config(filename: &Path) -> Result<Config, Box<dyn error::Error>> { pub fn load_configs(filenames: &Vec<PathBuf>) -> Result<Config, Box<dyn error::Error>> {
let yaml = fs::read_to_string(&filename)?; // Assumes filenames is non-empty
let yaml = fs::read_to_string(&filenames[0])?;
let mut config: Config = serde_yaml::from_str(&yaml)?; 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 // 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 // Convert keymap for efficient keymap lookup
config.keymap_table = build_keymap_table(&config.keymap); config.keymap_table = build_keymap_table(&config.keymap);
@ -59,14 +68,16 @@ pub fn load_config(filename: &Path) -> Result<Config, Box<dyn error::Error>> {
Ok(config) Ok(config)
} }
pub fn config_watcher(watch: bool, file: &Path) -> anyhow::Result<Option<Inotify>> { pub fn config_watcher(watch: bool, files: &Vec<PathBuf>) -> anyhow::Result<Option<Inotify>> {
if watch { if watch {
let inotify = Inotify::init(InitFlags::IN_NONBLOCK)?; let inotify = Inotify::init(InitFlags::IN_NONBLOCK)?;
inotify.add_watch( for file in files {
file.parent().expect("config file has a parent directory"), inotify.add_watch(
AddWatchFlags::IN_CREATE | AddWatchFlags::IN_MOVED_TO, file.parent().expect("config file has a parent directory"),
)?; AddWatchFlags::IN_CREATE | AddWatchFlags::IN_MOVED_TO,
inotify.add_watch(file, AddWatchFlags::IN_MODIFY)?; )?;
inotify.add_watch(file, AddWatchFlags::IN_MODIFY)?;
}
Ok(Some(inotify)) Ok(Some(inotify))
} else { } else {
Ok(None) Ok(None)

View File

@ -6,7 +6,7 @@ use anyhow::{anyhow, bail, Context};
use clap::{AppSettings, ArgEnum, IntoApp, Parser}; use clap::{AppSettings, ArgEnum, IntoApp, Parser};
use clap_complete::Shell; use clap_complete::Shell;
use client::build_client; use client::build_client;
use config::{config_watcher, load_config}; use config::{config_watcher, load_configs};
use device::InputDevice; use device::InputDevice;
use event::Event; use event::Event;
use nix::libc::ENODEV; use nix::libc::ENODEV;
@ -17,7 +17,7 @@ use nix::sys::timerfd::{ClockId, TimerFd, TimerFlags};
use std::collections::HashMap; use std::collections::HashMap;
use std::io::stdout; use std::io::stdout;
use std::os::unix::io::{AsRawFd, RawFd}; use std::os::unix::io::{AsRawFd, RawFd};
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
mod action; mod action;
@ -45,7 +45,7 @@ struct Opts {
/// Targets to watch /// Targets to watch
/// ///
/// - device: add new devices automatically /// - device: add new devices automatically
/// - config: reload the config automatically /// - config: reload the configs automatically
#[clap( #[clap(
long, long,
arg_enum, arg_enum,
@ -67,9 +67,9 @@ struct Opts {
/// - in fish: xremap --completions fish | source /// - in fish: xremap --completions fish | source
#[clap(long, arg_enum, display_order = 100, value_name = "SHELL", verbatim_doc_comment)] #[clap(long, arg_enum, display_order = 100, value_name = "SHELL", verbatim_doc_comment)]
completions: Option<Shell>, completions: Option<Shell>,
/// Config file /// Config file(s)
#[clap(required_unless_present = "completions")] #[clap(required_unless_present = "completions", multiple_values = true)]
config: Option<PathBuf>, configs: Vec<PathBuf>,
} }
#[derive(ArgEnum, Clone, Copy, Debug, PartialEq, Eq)] #[derive(ArgEnum, Clone, Copy, Debug, PartialEq, Eq)]
@ -94,7 +94,7 @@ fn main() -> anyhow::Result<()> {
ignore: ignore_filter, ignore: ignore_filter,
mouse, mouse,
watch, watch,
config, configs,
completions, completions,
} = Opts::parse(); } = Opts::parse();
@ -104,10 +104,22 @@ fn main() -> anyhow::Result<()> {
} }
// Configuration // Configuration
let config_path = config.expect("config is set, if not completions"); let config_paths = match configs[..] {
let mut config = match config::load_config(&config_path) { [] => panic!("config is set, if not completions"),
_ => configs,
};
let mut config = match config::load_configs(&config_paths) {
Ok(config) => config, 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::<Vec<_>>()
.join("', '"),
e
),
}; };
let watch_devices = watch.contains(&WatchTargets::Device); let watch_devices = watch.contains(&WatchTargets::Device);
let watch_config = watch.contains(&WatchTargets::Config); let watch_config = watch.contains(&WatchTargets::Config);
@ -121,7 +133,7 @@ fn main() -> anyhow::Result<()> {
Err(e) => bail!("Failed to prepare input devices: {}", e), Err(e) => bail!("Failed to prepare input devices: {}", e),
}; };
let device_watcher = device_watcher(watch_devices).context("Setting up device watcher")?; 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 watchers: Vec<_> = device_watcher.iter().chain(config_watcher.iter()).collect();
let mut handler = EventHandler::new(timer, &config.default_mode, delay, build_client()); 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)) { let output_device = match output_device(input_devices.values().next().map(InputDevice::bus_type)) {
@ -166,7 +178,7 @@ fn main() -> anyhow::Result<()> {
&device_filter, &device_filter,
&ignore_filter, &ignore_filter,
mouse, mouse,
&config_path, &config_paths,
)? { )? {
break 'event_loop ReloadEvent::ReloadConfig; break 'event_loop ReloadEvent::ReloadConfig;
} }
@ -183,10 +195,17 @@ fn main() -> anyhow::Result<()> {
}; };
} }
ReloadEvent::ReloadConfig => { ReloadEvent::ReloadConfig => {
match (config.modify_time, config_path.metadata().and_then(|m| m.modified())) { match (
(Some(last_mtime), Ok(current_mtim)) if last_mtime == current_mtim => continue, 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"); println!("Reloading Config");
config = c; config = c;
} }
@ -281,12 +300,16 @@ fn handle_config_changes(
device_filter: &[String], device_filter: &[String],
ignore_filter: &[String], ignore_filter: &[String],
mouse: bool, mouse: bool,
config_path: &Path, config_paths: &Vec<PathBuf>,
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
for event in &events { for event in &events {
match (event.mask, &event.name) { match (event.mask, &event.name) {
// Dir events // 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) return Ok(false)
} }
// File events // File events