mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-13 14:33:47 +00:00
commands/watch: make it trait-based; move out of main
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I6a6a6964c16396f2013e7f8a5c1a6c0c3bb2aeaa
This commit is contained in:
parent
d9b0908ada
commit
9d40dde63a
3 changed files with 112 additions and 78 deletions
|
|
@ -3,4 +3,5 @@ pub mod delete;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
pub mod watch;
|
||||||
pub mod wipe;
|
pub mod wipe;
|
||||||
|
|
|
||||||
79
src/commands/watch.rs
Normal file
79
src/commands/watch.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
use crate::db::{ClipboardDb, Entry, SqliteClipboardDb};
|
||||||
|
use smol::Timer;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::time::Duration;
|
||||||
|
use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents};
|
||||||
|
|
||||||
|
pub trait WatchCommand {
|
||||||
|
fn watch(&self, max_dedupe_search: u64, max_items: u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WatchCommand for SqliteClipboardDb {
|
||||||
|
fn watch(&self, max_dedupe_search: u64, max_items: u64) {
|
||||||
|
smol::block_on(async {
|
||||||
|
log::info!("Starting clipboard watch daemon");
|
||||||
|
|
||||||
|
// Preallocate buffer for clipboard contents
|
||||||
|
let mut last_contents: Option<Vec<u8>> = None;
|
||||||
|
let mut buf = Vec::with_capacity(4096); // reasonable default, hopefully
|
||||||
|
|
||||||
|
// Initialize with current clipboard to avoid duplicating on startup
|
||||||
|
if let Ok((mut reader, _)) = get_contents(
|
||||||
|
ClipboardType::Regular,
|
||||||
|
Seat::Unspecified,
|
||||||
|
wl_clipboard_rs::paste::MimeType::Any,
|
||||||
|
) {
|
||||||
|
buf.clear();
|
||||||
|
if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() {
|
||||||
|
last_contents = Some(buf.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match get_contents(
|
||||||
|
ClipboardType::Regular,
|
||||||
|
Seat::Unspecified,
|
||||||
|
wl_clipboard_rs::paste::MimeType::Any,
|
||||||
|
) {
|
||||||
|
Ok((mut reader, mime_type)) => {
|
||||||
|
buf.clear();
|
||||||
|
if let Err(e) = reader.read_to_end(&mut buf) {
|
||||||
|
log::error!("Failed to read clipboard contents: {e}");
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only store if changed and not empty
|
||||||
|
if !buf.is_empty() && (last_contents.as_ref() != Some(&buf)) {
|
||||||
|
last_contents = Some(std::mem::take(&mut buf));
|
||||||
|
let mime = Some(mime_type.to_string());
|
||||||
|
let entry = Entry {
|
||||||
|
contents: last_contents.as_ref().unwrap().clone(),
|
||||||
|
mime,
|
||||||
|
};
|
||||||
|
let id = self.next_sequence();
|
||||||
|
match self.store_entry(
|
||||||
|
&entry.contents[..],
|
||||||
|
max_dedupe_search,
|
||||||
|
max_items,
|
||||||
|
) {
|
||||||
|
Ok(_) => log::info!("Stored new clipboard entry (id: {id})"),
|
||||||
|
Err(e) => log::error!("Failed to store clipboard entry: {e}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop clipboard contents after storing
|
||||||
|
last_contents = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let error_msg = e.to_string();
|
||||||
|
if !error_msg.contains("empty") {
|
||||||
|
log::error!("Failed to get clipboard contents: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/main.rs
110
src/main.rs
|
|
@ -5,26 +5,18 @@ use std::{
|
||||||
process,
|
process,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::CommandFactory;
|
use clap::{CommandFactory, Parser, Subcommand};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod db;
|
mod db;
|
||||||
mod import;
|
mod import;
|
||||||
|
|
||||||
use crate::db::ClipboardDb;
|
|
||||||
|
|
||||||
use smol::Timer;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::time::Duration;
|
|
||||||
use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents};
|
|
||||||
|
|
||||||
use crate::commands::decode::DecodeCommand;
|
use crate::commands::decode::DecodeCommand;
|
||||||
use crate::commands::delete::DeleteCommand;
|
use crate::commands::delete::DeleteCommand;
|
||||||
use crate::commands::list::ListCommand;
|
use crate::commands::list::ListCommand;
|
||||||
use crate::commands::query::QueryCommand;
|
use crate::commands::query::QueryCommand;
|
||||||
use crate::commands::store::StoreCommand;
|
use crate::commands::store::StoreCommand;
|
||||||
|
use crate::commands::watch::WatchCommand;
|
||||||
use crate::commands::wipe::WipeCommand;
|
use crate::commands::wipe::WipeCommand;
|
||||||
use crate::import::ImportCommand;
|
use crate::import::ImportCommand;
|
||||||
|
|
||||||
|
|
@ -57,7 +49,11 @@ enum Command {
|
||||||
Store,
|
Store,
|
||||||
|
|
||||||
/// List clipboard history
|
/// List clipboard history
|
||||||
List,
|
List {
|
||||||
|
/// Output format: "tsv" (default) or "json"
|
||||||
|
#[arg(long, value_parser = ["tsv", "json"])]
|
||||||
|
format: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Decode and output clipboard entry by id
|
/// Decode and output clipboard entry by id
|
||||||
Decode { input: Option<String> },
|
Decode { input: Option<String> },
|
||||||
|
|
@ -97,67 +93,6 @@ fn report_error<T>(result: Result<T, impl std::fmt::Display>, context: &str) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Watch clipboard and store changes
|
|
||||||
async fn run_daemon(db: &db::SqliteClipboardDb, max_dedupe_search: u64, max_items: u64) {
|
|
||||||
log::info!("Starting clipboard watch daemon");
|
|
||||||
|
|
||||||
// Initialize with current clipboard to avoid duplicating on startup
|
|
||||||
let mut last_contents: Option<Vec<u8>> = match get_contents(
|
|
||||||
ClipboardType::Regular,
|
|
||||||
Seat::Unspecified,
|
|
||||||
wl_clipboard_rs::paste::MimeType::Any,
|
|
||||||
) {
|
|
||||||
Ok((mut reader, _)) => {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
if reader.read_to_end(&mut buf).is_ok() && !buf.is_empty() {
|
|
||||||
Some(buf)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match get_contents(
|
|
||||||
ClipboardType::Regular,
|
|
||||||
Seat::Unspecified,
|
|
||||||
wl_clipboard_rs::paste::MimeType::Any,
|
|
||||||
) {
|
|
||||||
Ok((mut reader, mime_type)) => {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
if let Err(e) = reader.read_to_end(&mut buf) {
|
|
||||||
log::error!("Failed to read clipboard contents: {e}");
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Only store if changed and not empty
|
|
||||||
if !buf.is_empty() && Some(&buf) != last_contents.as_ref() {
|
|
||||||
last_contents = Some(buf.clone());
|
|
||||||
let mime = Some(mime_type.to_string());
|
|
||||||
let entry = db::Entry {
|
|
||||||
contents: buf,
|
|
||||||
mime,
|
|
||||||
};
|
|
||||||
let id = db.next_sequence();
|
|
||||||
match db.store_entry(&entry.contents[..], max_dedupe_search, max_items) {
|
|
||||||
Ok(_) => log::info!("Stored new clipboard entry (id: {id})"),
|
|
||||||
Err(e) => log::error!("Failed to store clipboard entry: {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Only log actual errors, not empty clipboard
|
|
||||||
let error_msg = e.to_string();
|
|
||||||
if !error_msg.contains("empty") {
|
|
||||||
log::error!("Failed to get clipboard contents: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
smol::block_on(async {
|
smol::block_on(async {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
@ -193,11 +128,30 @@ fn main() {
|
||||||
"Failed to store entry",
|
"Failed to store entry",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(Command::List) => {
|
Some(Command::List { format }) => {
|
||||||
report_error(
|
let format = format.as_deref().unwrap_or("tsv");
|
||||||
db.list(io::stdout(), cli.preview_width),
|
match format {
|
||||||
"Failed to list entries",
|
"tsv" => {
|
||||||
);
|
report_error(
|
||||||
|
db.list(io::stdout(), cli.preview_width),
|
||||||
|
"Failed to list entries",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"json" => {
|
||||||
|
// Implement JSON output
|
||||||
|
match db.list_json() {
|
||||||
|
Ok(json) => {
|
||||||
|
println!("{}", json);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to list entries as JSON: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!("Unsupported format: {}", format);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(Command::Decode { input }) => {
|
Some(Command::Decode { input }) => {
|
||||||
report_error(
|
report_error(
|
||||||
|
|
@ -255,7 +209,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Command::Watch) => {
|
Some(Command::Watch) => {
|
||||||
run_daemon(&db, cli.max_dedupe_search, cli.max_items).await;
|
db.watch(cli.max_dedupe_search, cli.max_items);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if let Err(e) = Cli::command().print_help() {
|
if let Err(e) = Cli::command().print_help() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue