snippy 0.1.3

A command-line tool for that makes using LLMs for code generation a breeze
Documentation
use crate::applier::{Applier, DiffApplier, FullContentApplier, SearchReplaceApplier};
use crate::errors::ClipboardError;
use crate::extractor::Extractor;
use arboard::Clipboard;
use std::path::PathBuf;
use tokio::signal;
use tokio::time::{self, Duration};
use tracing::{debug, error, info, trace};

pub struct WatcherConfig {
    pub watch_path: PathBuf,
    pub interval_ms: u64,
    pub first_line_identifier: String,
}

pub struct ClipboardWatcher<E: Extractor + Send + Sync> {
    config: WatcherConfig,
    extractor: E,
}

impl<E: Extractor + Send + Sync> ClipboardWatcher<E> {
    pub fn new(config: WatcherConfig, extractor: E) -> Self {
        ClipboardWatcher { config, extractor }
    }

    pub async fn run(&self) -> Result<(), ClipboardError> {
        let mut clipboard =
            Clipboard::new().map_err(|e| ClipboardError::ClipboardInitError(e.to_string()))?;
        let mut interval = time::interval(Duration::from_millis(self.config.interval_ms));
        let mut last_content = String::new();

        info!("Started watching clipboard at {:?}", self.config.watch_path);

        loop {
            tokio::select! {
                _ = interval.tick() => {
                    trace!("Checking clipboard content");
                    match clipboard.get_text() {
                        Ok(content) => {
                            trace!("Clipboard content length: {}", content.len());
                            if content.starts_with(&self.config.first_line_identifier) {
                                trace!("Ignoring self-copied content to avoid recursion");
                                continue;
                            }

                            if content != last_content {
                                info!("New clipboard content detected");
                                match self.extractor.extract(&content) {
                                    Ok(blocks) => {
                                        self.apply_blocks(blocks).await?;
                                        last_content = content;
                                    },
                                    Err(e) => {
                                        error!("Failed to extract content: {}", e);
                                    }
                                }
                            }
                        },
                        Err(e) => {
                            error!("Failed to read clipboard content: {}", e);
                        }
                    }
                },
                _ = signal::ctrl_c() => {
                    info!("Received Ctrl+C, terminating clipboard watcher.");
                    break;
                }
            }
        }

        Ok(())
    }

    async fn apply_blocks(
        &self,
        blocks: Vec<crate::extractor::ParsedBlock>,
    ) -> Result<(), ClipboardError> {
        for block in blocks {
            debug!("Applying block: {:?}", block);
            let applier: Box<dyn Applier> = match block.block_type {
                crate::extractor::BlockType::FullContent => {
                    Box::new(FullContentApplier::new(&self.config.watch_path))
                }
                crate::extractor::BlockType::UnifiedDiff => {
                    Box::new(DiffApplier::new(&self.config.watch_path))
                }
                crate::extractor::BlockType::SearchReplaceBlock => {
                    Box::new(SearchReplaceApplier::new(&self.config.watch_path))
                }
            };

            if let Err(e) = applier.apply(&block).await {
                error!("Failed to apply block: {}", e);
            } else {
                info!("Successfully applied block to {}", block.filename);
            }
        }
        Ok(())
    }
}