~ruther/sequence-detector

c09cd55bc2030fc0f9cb4ddfb1d963dd0ed5a15e — František Boháček 1 year, 9 months ago 0bcd05b
feat: add sequence detection behavior
5 files changed, 352 insertions(+), 0 deletions(-)

A Cargo.toml
A src/main.rs
A src/sequence_cacher.rs
A src/sequence_detector.rs
A src/settings.rs
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()
    }
}

Do not follow this link