A Cargo.toml => Cargo.toml +13 -0
@@ 0,0 1,13 @@
+[package]
+name = "sequence_detector"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+clap = { version = "4.3.11", features = ["derive"] }
+config = "0.13.3"
+serde = "1.0.169"
+serde_derive = "1.0.169"
+serde_json = { version = "1.0.100", features = ["std"] }
A src/main.rs => src/main.rs +102 -0
@@ 0,0 1,102 @@
+use std::{path::PathBuf, time::Duration, io, thread};
+
+use clap::Parser;
+use sequence_cacher::{SequenceCacher, CacheError, SequenceFile};
+use sequence_detector::{SequenceDetector, HandleResult};
+use settings::Settings;
+
+pub mod sequence_detector;
+pub mod settings;
+pub mod sequence_cacher;
+
+#[derive(Debug, Parser)]
+struct Cli {
+ #[arg(short = 'c', long)]
+ pub config: Option<PathBuf>,
+ #[arg(short = 'd', long)]
+ pub debounce_time: Option<u64>,
+ #[arg(short = 'g', long, default_value = "default")]
+ pub group_id: String,
+ #[arg(short = 'f', long, default_value = r"/tmp/{group}.seq_dect")]
+ pub sequence_file: PathBuf,
+ #[arg(help = "The key to append to the sequence")]
+ pub key: String,
+}
+
+fn main() {
+ let args = Cli::parse();
+ let settings = match Settings::new(&args.config, "config.json") {
+ Ok(settings) => settings,
+ Err(err) => {
+ eprintln!(
+ "Could not open the settings file. {}",
+ err
+ );
+ return;
+ }
+ };
+
+ let debounce_time = Duration::from_millis(args.debounce_time.unwrap_or(settings.debounce_time));
+
+ let group = match settings.groups.iter().find(|x| x.group_id == args.group_id) {
+ Some(group) => group,
+ None => {
+ eprintln!("There is no group with the id {} you given.", args.group_id);
+ return;
+ }
+ };
+
+ let mut cacher = SequenceCacher::new(&args.sequence_file, &args.group_id);
+ let matcher = SequenceDetector::new(group.sequences.clone());
+
+ let current_sequence = match cacher.try_load(debounce_time) {
+ Ok(sequence) => sequence,
+ Err(err) => {
+ match err {
+ CacheError::Expired => (),
+ CacheError::IO(err) if err.kind() == io::ErrorKind::NotFound => (),
+ _ => eprintln!("Could not load from cache: {}", err)
+ };
+ SequenceFile::empty()
+ }
+ };
+
+ let mut handle_result = matcher.handle_next(current_sequence.keys(), &args.key);
+
+ if let HandleResult::Debounce(sequence) = handle_result {
+ let mut keys = current_sequence.keys().clone();
+ keys.push(args.key);
+ if let Err(err) = cacher.try_cache(keys) {
+ eprintln!("Could not save cache for debounce, aborting. {}", err);
+ return;
+ }
+
+ thread::sleep(debounce_time);
+
+ handle_result = match cacher.modified() {
+ Ok(modified) if !modified => HandleResult::Execute(sequence),
+ Err(err) => {
+ eprintln!("Could not check whether the cache is modified. {}", err);
+ HandleResult::Exit
+ },
+ _ => HandleResult::Exit,
+ }
+ }
+
+ match handle_result {
+ HandleResult::Execute(sequence) => {
+ if let Err(err) = sequence.execute() {
+ eprintln!("Could not execute the action. {}", err);
+ } else {
+ println!("Found one matching sequence and executed.");
+ println!("{:?}", sequence);
+ }
+
+ if let Err(err) = cacher.remove() {
+ eprintln!("Could not remove the cache. {}", err);
+ }
+ },
+ HandleResult::Exit => (),
+ _ => panic!("Unreachable, debounce handled") // debounce already handled
+ };
+}
A src/sequence_cacher.rs => src/sequence_cacher.rs +132 -0
@@ 0,0 1,132 @@
+use std::{time::{SystemTime, Duration}, io::{self, BufReader, BufWriter, Write}, fmt, path::PathBuf, fs::{File, self}, ops::Add};
+
+use serde_derive::{Serialize, Deserialize};
+
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct SequenceFile {
+ time: SystemTime,
+ keys: Vec<String>,
+}
+
+impl SequenceFile {
+ pub fn empty() -> Self {
+ Self {
+ time: SystemTime::now(),
+ keys: Vec::new()
+ }
+ }
+
+ pub fn time(&self) -> SystemTime {
+ self.time
+ }
+
+ pub fn keys(&self) -> &Vec<String> {
+ &self.keys
+ }
+}
+
+impl From<Vec<String>> for SequenceFile {
+ fn from(value: Vec<String>) -> Self {
+ Self {
+ time: SystemTime::now(),
+ keys: value
+ }
+ }
+}
+
+pub struct SequenceCacher {
+ cache_path: PathBuf,
+ cache_metadata: SystemTime
+}
+
+pub enum CacheError {
+ Expired,
+ IO(io::Error),
+ Serde(serde_json::Error)
+}
+
+impl fmt::Display for CacheError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match *self {
+ CacheError::Expired =>
+ write!(f, "cache has expired"),
+ CacheError::IO(..) =>
+ write!(f, "an I/O error has occurred."),
+ CacheError::Serde(..) =>
+ write!(f, "there was a problem with serialization or deserialization")
+ }
+ }
+}
+
+impl From<io::Error> for CacheError {
+ fn from(err: io::Error) -> Self {
+ CacheError::IO(err)
+ }
+}
+
+impl From<serde_json::Error> for CacheError {
+ fn from(err: serde_json::Error) -> Self {
+ CacheError::Serde(err)
+ }
+}
+
+impl SequenceCacher {
+ pub fn new(cache_path: &PathBuf, group_id: &str) -> Self {
+ let file_path = cache_path
+ .to_string_lossy()
+ .replace("{group}", group_id);
+ let file_path = PathBuf::from(file_path);
+
+ Self {
+ cache_path: file_path,
+ cache_metadata: SystemTime::now()
+ }
+ }
+
+ pub fn try_load(&self, debounce_time: Duration) -> Result<SequenceFile, CacheError> {
+ let file = File::open(&self.cache_path)?;
+ let reader = BufReader::new(file);
+ let read: SequenceFile = serde_json::from_reader(reader)?;
+
+ let debounce_time = read.time.add(debounce_time);
+ if SystemTime::now() > debounce_time {
+ return Err(CacheError::Expired.into());
+ }
+
+ Ok(read)
+ }
+
+ pub fn try_cache(&mut self, keys: Vec<String>) -> Result<(), CacheError> {
+ {
+ let file = File::create(&self.cache_path)?;
+ let mut writer = BufWriter::new(file);
+ let data: SequenceFile = keys.into();
+ serde_json::to_writer(&mut writer, &data)?;
+ writer.flush()?;
+ }
+
+ self.cache_metadata = fs::metadata(&self.cache_path)?.modified()?;
+ Ok(())
+ }
+
+ pub fn exists(&self) -> bool {
+ self.cache_path.exists()
+ }
+
+ pub fn modified(&self) -> io::Result<bool> {
+ if !self.exists() {
+ Ok(true)
+ } else {
+ Ok(self.cache_metadata != fs::metadata(&self.cache_path)?.modified()?)
+ }
+ }
+
+ pub fn remove(&self) -> io::Result<()> {
+ if !self.exists() {
+ Ok(())
+ } else {
+ fs::remove_file(&self.cache_path)
+ }
+ }
+}
A src/sequence_detector.rs => src/sequence_detector.rs +59 -0
@@ 0,0 1,59 @@
+use crate::settings::Sequence;
+
+pub enum HandleResult {
+ Execute(Sequence),
+ Debounce(Sequence),
+ Exit
+}
+
+pub struct SequenceDetector {
+ sequences: Vec<Sequence>
+}
+
+impl SequenceDetector {
+ pub fn new(sequences: Vec<Sequence>) -> Self {
+ Self {
+ sequences
+ }
+ }
+
+ pub fn match_sequences(&self, keys: &Vec<String>) -> Vec<&Sequence> {
+ let mut matched_sequences: Vec<&Sequence> = Vec::new();
+
+ for sequence in &self.sequences {
+ if sequence.keys.len() < keys.len() {
+ continue;
+ }
+
+ let mut matches = true;
+ for (i, key) in keys.iter().enumerate() {
+ let match_key = &sequence.keys[i];
+
+ if match_key != key {
+ matches = false;
+ break;
+ }
+ }
+
+ if matches {
+ matched_sequences.push(sequence);
+ }
+ }
+
+ matched_sequences.sort_by(|&x, &y| x.keys.len().cmp(&y.keys.len()));
+ matched_sequences
+ }
+
+ pub fn handle_next(&self, current_keys: &Vec<String>, key: &str) -> HandleResult {
+ let mut keys = current_keys.clone();
+ keys.push(key.to_string());
+
+ let matched = self.match_sequences(&keys);
+
+ match matched.len() {
+ 0 => HandleResult::Exit,
+ 1 => HandleResult::Execute(matched[0].clone()),
+ _ => HandleResult::Debounce(matched[0].clone())
+ }
+ }
+}
A src/settings.rs => src/settings.rs +46 -0
@@ 0,0 1,46 @@
+use std::{process::{Output, Command}, io, path::PathBuf};
+
+use config::{Config, ConfigError};
+use serde_derive::Deserialize;
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct Group {
+ pub group_id: String,
+ pub sequences: Vec<Sequence>,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct Sequence {
+ pub keys: Vec<String>,
+ pub action: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Settings {
+ pub debounce_time: u64,
+ pub groups: Vec<Group>,
+}
+
+impl Sequence {
+ pub fn execute(&self) -> io::Result<Output> {
+ let action: Vec<&str> = self.action.split(" ").collect();
+ Command::new(&action[0]).args(&action[1..]).output()
+ }
+}
+
+impl Settings {
+ pub fn new(config: &Option<PathBuf>, default: &str) -> Result<Self, ConfigError> {
+ let settings_path = &config.clone().unwrap_or_else(|| {
+ let mut exe_path = std::env::current_exe().expect("Failed to get current executable path");
+ exe_path.pop(); // Remove the executable name
+ exe_path.push(default); // Append the default configuration file name
+ exe_path
+ });
+
+ let s = Config::builder()
+ .add_source(config::File::from(settings_path.clone()))
+ .build()?;
+
+ s.try_deserialize()
+ }
+}