mirror of
https://github.com/2e3s/awatcher.git
synced 2025-06-06 03:25:32 +00:00
269 lines
7.9 KiB
Rust
269 lines
7.9 KiB
Rust
use anyhow::{anyhow, Context};
|
|
use chrono::TimeDelta;
|
|
use serde::Deserialize;
|
|
use serde_default::DefaultFromSerde;
|
|
use std::{fs, io::ErrorKind, path::PathBuf};
|
|
|
|
use crate::config::defaults;
|
|
|
|
use super::filters::Filter;
|
|
|
|
pub fn default_config() -> String {
|
|
format!(
|
|
r#"# The commented values are the defaults on the file creation
|
|
[server]
|
|
# port = {}
|
|
# host = "{}"
|
|
|
|
[awatcher]
|
|
# idle-timeout-seconds={}
|
|
# poll-time-idle-seconds={}
|
|
# poll-time-window-seconds={}
|
|
|
|
# Add as many filters as needed. The first matching filter stops the replacement.
|
|
# There should be at least 1 match field, and at least 1 replace field.
|
|
# Matches are case sensitive regular expressions between implici ^ and $, e.g.
|
|
# - "." matches 1 any character
|
|
# - ".*" matches any number of any characters
|
|
# - ".+" matches 1 or more any characters.
|
|
# - "word" is an exact match.
|
|
# [[awatcher.filters]]
|
|
# match-app-id = "navigator"
|
|
# match-title = ".*Firefox.*"
|
|
# replace-app-id = "firefox"
|
|
# replace-title = "Unknown"
|
|
|
|
# Use captures for app-id or title in the regular form to use parts of the original text
|
|
# (parentheses for a capture, $1, $2 etc for each capture).
|
|
# The example rule removes the changed file indicator from the title in Visual Studio Code:
|
|
# "● file_config.rs - awatcher - Visual Studio Code" to "file_config.rs - awatcher - Visual Studio Code".
|
|
# [[awatcher.filters]]
|
|
# match-app-id = "code"
|
|
# match-title = "● (.*)"
|
|
# replace-title = "$1"
|
|
"#,
|
|
defaults::port(),
|
|
defaults::host(),
|
|
defaults::idle_timeout_seconds(),
|
|
defaults::poll_time_idle_seconds(),
|
|
defaults::poll_time_window_seconds(),
|
|
)
|
|
}
|
|
|
|
#[derive(Deserialize, DefaultFromSerde)]
|
|
pub struct ServerConfig {
|
|
#[serde(default = "defaults::port")]
|
|
pub port: u16,
|
|
#[serde(default = "defaults::host")]
|
|
pub host: String,
|
|
}
|
|
|
|
#[derive(Deserialize, DefaultFromSerde)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct ClientConfig {
|
|
#[serde(default = "defaults::idle_timeout_seconds")]
|
|
pub idle_timeout_seconds: u32,
|
|
#[serde(default = "defaults::poll_time_idle_seconds")]
|
|
pub poll_time_idle_seconds: u32,
|
|
#[serde(default = "defaults::poll_time_window_seconds")]
|
|
pub poll_time_window_seconds: u32,
|
|
#[serde(default)]
|
|
pub filters: Vec<Filter>,
|
|
}
|
|
|
|
impl ClientConfig {
|
|
pub fn get_idle_timeout(&self) -> TimeDelta {
|
|
TimeDelta::seconds(self.idle_timeout_seconds.into())
|
|
}
|
|
|
|
pub fn get_poll_time_idle(&self) -> TimeDelta {
|
|
TimeDelta::seconds(self.poll_time_idle_seconds.into())
|
|
}
|
|
|
|
pub fn get_poll_time_window(&self) -> TimeDelta {
|
|
TimeDelta::seconds(self.poll_time_window_seconds.into())
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
pub struct FileConfig {
|
|
#[serde(default)]
|
|
pub server: ServerConfig,
|
|
#[serde(default)]
|
|
#[serde(rename = "awatcher")]
|
|
pub client: ClientConfig,
|
|
#[serde(default)]
|
|
pub config_file: PathBuf,
|
|
}
|
|
|
|
impl FileConfig {
|
|
pub fn new(config_override: Option<PathBuf>) -> anyhow::Result<Self> {
|
|
let is_config_overridden = config_override.is_some();
|
|
let config_path = if let Some(config_override) = config_override {
|
|
if config_override.starts_with("~/") {
|
|
dirs::home_dir()
|
|
.ok_or(anyhow!("Home directory is not found"))?
|
|
.join(config_override.strip_prefix("~").unwrap())
|
|
} else {
|
|
config_override
|
|
}
|
|
} else {
|
|
let mut system_config_path: PathBuf =
|
|
dirs::config_dir().ok_or(anyhow!("Config directory is unknown"))?;
|
|
system_config_path.push("awatcher");
|
|
system_config_path.push("config.toml");
|
|
|
|
system_config_path
|
|
};
|
|
|
|
let mut config = if fs::metadata(&config_path).is_ok() {
|
|
debug!("Reading config at {}", config_path.display());
|
|
let config_content = std::fs::read_to_string(&config_path).with_context(|| {
|
|
format!("Impossible to read config file {}", config_path.display())
|
|
})?;
|
|
|
|
toml::from_str(&config_content)?
|
|
} else {
|
|
if is_config_overridden {
|
|
anyhow::bail!("Config file is not accessible at {}", config_path.display());
|
|
}
|
|
let config = default_config();
|
|
let error = std::fs::create_dir(config_path.parent().unwrap());
|
|
if let Err(e) = error {
|
|
if e.kind() != ErrorKind::AlreadyExists {
|
|
Err(e)?;
|
|
}
|
|
}
|
|
debug!("Creading config at {}", config_path.display());
|
|
std::fs::write(&config_path, config)?;
|
|
|
|
Self::default()
|
|
};
|
|
config.config_file = config_path;
|
|
|
|
Ok(config)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::config::FilterResult;
|
|
|
|
use super::*;
|
|
use rstest::rstest;
|
|
use std::io::Write;
|
|
use tempfile::NamedTempFile;
|
|
|
|
#[rstest]
|
|
fn all() {
|
|
let mut file = NamedTempFile::new().unwrap();
|
|
write!(
|
|
file,
|
|
r#"
|
|
[server]
|
|
port = 1234
|
|
host = "http://address.com"
|
|
|
|
[awatcher]
|
|
idle-timeout-seconds=14
|
|
poll-time-idle-seconds=13
|
|
poll-time-window-seconds=12
|
|
|
|
# Add as many filters as needed.
|
|
# There should be at least 1 match field, and at least 1 replace field.
|
|
[[awatcher.filters]]
|
|
match-app-id = "firefox"
|
|
replace-title = "Unknown"
|
|
|
|
[[awatcher.filters]]
|
|
match-app-id = "code"
|
|
match-title = "title"
|
|
replace-app-id = "VSCode"
|
|
replace-title = "Title"
|
|
"#
|
|
)
|
|
.unwrap();
|
|
|
|
let config = FileConfig::new(Some(file.path().to_path_buf())).unwrap();
|
|
|
|
assert_eq!(1234, config.server.port);
|
|
assert_eq!("http://address.com", config.server.host);
|
|
|
|
assert_eq!(14, config.client.idle_timeout_seconds);
|
|
assert_eq!(13, config.client.poll_time_idle_seconds);
|
|
assert_eq!(12, config.client.poll_time_window_seconds);
|
|
|
|
assert_eq!(2, config.client.filters.len());
|
|
|
|
let replacement1 = config.client.filters[0].apply("firefox", "any");
|
|
assert!(matches!(replacement1, FilterResult::Replace(ref r) if
|
|
r.replace_app_id.is_none() &&
|
|
r.replace_title == Some("Unknown".to_string())
|
|
));
|
|
|
|
let replacement2 = config.client.filters[1].apply("code", "title");
|
|
assert!(matches!(replacement2, FilterResult::Replace(ref r) if
|
|
r.replace_app_id == Some("VSCode".to_string()) &&
|
|
r.replace_title == Some("Title".to_string())
|
|
));
|
|
}
|
|
|
|
#[rstest]
|
|
fn match_filter() {
|
|
let mut file = NamedTempFile::new().unwrap();
|
|
write!(
|
|
file,
|
|
r#"
|
|
[[awatcher.filters]]
|
|
match-app-id = "firefox"
|
|
"#
|
|
)
|
|
.unwrap();
|
|
|
|
let config = FileConfig::new(Some(file.path().to_path_buf())).unwrap();
|
|
|
|
assert_eq!(1, config.client.filters.len());
|
|
let replacement1 = config.client.filters[0].apply("firefox", "any");
|
|
assert!(matches!(replacement1, FilterResult::Match));
|
|
}
|
|
|
|
#[rstest]
|
|
fn empty() {
|
|
let mut file = NamedTempFile::new().unwrap();
|
|
write!(file, "[awatcher]").unwrap();
|
|
|
|
let config = FileConfig::new(Some(file.path().to_path_buf())).unwrap();
|
|
|
|
assert_eq!(defaults::port(), config.server.port);
|
|
assert_eq!(defaults::host(), config.server.host);
|
|
|
|
assert_eq!(
|
|
defaults::idle_timeout_seconds(),
|
|
config.client.idle_timeout_seconds
|
|
);
|
|
assert_eq!(
|
|
defaults::poll_time_idle_seconds(),
|
|
config.client.poll_time_idle_seconds
|
|
);
|
|
assert_eq!(
|
|
defaults::poll_time_window_seconds(),
|
|
config.client.poll_time_window_seconds
|
|
);
|
|
|
|
assert_eq!(0, config.client.filters.len());
|
|
}
|
|
|
|
#[rstest]
|
|
fn wrong_file() {
|
|
let file = PathBuf::new();
|
|
|
|
let config = FileConfig::new(Some(file));
|
|
|
|
assert!(config.is_err());
|
|
assert_eq!(
|
|
"Config file is not accessible at ",
|
|
config.err().unwrap().to_string()
|
|
);
|
|
}
|
|
}
|