mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-18 00:28:12 +00:00
treewide: format with rustfmt
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I6a6a69642c2865f41a4b141ddf39a198a3fc2e09
This commit is contained in:
parent
404990f928
commit
6a5cd9b95d
10 changed files with 1191 additions and 1132 deletions
|
|
@ -1,10 +1,9 @@
|
||||||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
use crate::db::StashError;
|
|
||||||
use wl_clipboard_rs::paste::{ClipboardType, MimeType, Seat, get_contents};
|
use wl_clipboard_rs::paste::{ClipboardType, MimeType, Seat, get_contents};
|
||||||
|
|
||||||
|
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||||
|
|
||||||
pub trait DecodeCommand {
|
pub trait DecodeCommand {
|
||||||
fn decode(
|
fn decode(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -50,10 +49,14 @@ impl DecodeCommand for SqliteClipboardDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try decode as usual
|
// Try decode as usual
|
||||||
match self.decode_entry(input_str.as_bytes(), &mut out, Some(input_str.clone())) {
|
match self.decode_entry(
|
||||||
|
input_str.as_bytes(),
|
||||||
|
&mut out,
|
||||||
|
Some(input_str.clone()),
|
||||||
|
) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
log::info!("Entry decoded");
|
log::info!("Entry decoded");
|
||||||
}
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to decode entry: {e}");
|
log::error!("Failed to decode entry: {e}");
|
||||||
if let Ok((mut reader, _mime)) =
|
if let Ok((mut reader, _mime)) =
|
||||||
|
|
@ -68,7 +71,7 @@ impl DecodeCommand for SqliteClipboardDb {
|
||||||
} else {
|
} else {
|
||||||
log::error!("Failed to get clipboard contents for relay");
|
log::error!("Failed to get clipboard contents for relay");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
|
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||||
|
|
||||||
pub trait DeleteCommand {
|
pub trait DeleteCommand {
|
||||||
fn delete(&self, input: impl Read) -> Result<usize, StashError>;
|
fn delete(&self, input: impl Read) -> Result<usize, StashError>;
|
||||||
}
|
}
|
||||||
|
|
@ -12,11 +12,11 @@ impl DeleteCommand for SqliteClipboardDb {
|
||||||
Ok(deleted) => {
|
Ok(deleted) => {
|
||||||
log::info!("Deleted {deleted} entries");
|
log::info!("Deleted {deleted} entries");
|
||||||
Ok(deleted)
|
Ok(deleted)
|
||||||
}
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to delete entries: {e}");
|
log::error!("Failed to delete entries: {e}");
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||||
|
|
||||||
pub trait ListCommand {
|
pub trait ListCommand {
|
||||||
fn list(&self, out: impl Write, preview_width: u32) -> Result<(), StashError>;
|
fn list(&self, out: impl Write, preview_width: u32)
|
||||||
|
-> Result<(), StashError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListCommand for SqliteClipboardDb {
|
impl ListCommand for SqliteClipboardDb {
|
||||||
fn list(&self, out: impl Write, preview_width: u32) -> Result<(), StashError> {
|
fn list(
|
||||||
|
&self,
|
||||||
|
out: impl Write,
|
||||||
|
preview_width: u32,
|
||||||
|
) -> Result<(), StashError> {
|
||||||
self.list_entries(out, preview_width)?;
|
self.list_entries(out, preview_width)?;
|
||||||
log::info!("Listed clipboard entries");
|
log::info!("Listed clipboard entries");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -20,11 +26,16 @@ impl SqliteClipboardDb {
|
||||||
/// Public TUI listing function for use in main.rs
|
/// Public TUI listing function for use in main.rs
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn list_tui(&self, preview_width: u32) -> Result<(), StashError> {
|
pub fn list_tui(&self, preview_width: u32) -> Result<(), StashError> {
|
||||||
|
use std::io::stdout;
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||||
execute,
|
execute,
|
||||||
terminal::{
|
terminal::{
|
||||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
EnterAlternateScreen,
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
disable_raw_mode,
|
||||||
|
enable_raw_mode,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
|
@ -34,7 +45,6 @@ impl SqliteClipboardDb {
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, List, ListItem, ListState},
|
widgets::{Block, Borders, List, ListItem, ListState},
|
||||||
};
|
};
|
||||||
use std::io::stdout;
|
|
||||||
|
|
||||||
// Query entries from DB
|
// Query entries from DB
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
|
|
@ -61,7 +71,8 @@ impl SqliteClipboardDb {
|
||||||
let mime: Option<String> = row
|
let mime: Option<String> = row
|
||||||
.get(2)
|
.get(2)
|
||||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||||
let preview = crate::db::preview_entry(&contents, mime.as_deref(), preview_width);
|
let preview =
|
||||||
|
crate::db::preview_entry(&contents, mime.as_deref(), preview_width);
|
||||||
let mime_str = mime.as_deref().unwrap_or("").to_string();
|
let mime_str = mime.as_deref().unwrap_or("").to_string();
|
||||||
let id_str = id.to_string();
|
let id_str = id.to_string();
|
||||||
max_id_width = max_id_width.max(id_str.width());
|
max_id_width = max_id_width.max(id_str.width());
|
||||||
|
|
@ -74,8 +85,8 @@ impl SqliteClipboardDb {
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
|
||||||
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
let mut terminal =
|
let mut terminal = Terminal::new(backend)
|
||||||
Terminal::new(backend).map_err(|e| StashError::ListDecode(e.to_string()))?;
|
.map_err(|e| StashError::ListDecode(e.to_string()))?;
|
||||||
|
|
||||||
let mut state = ListState::default();
|
let mut state = ListState::default();
|
||||||
if !entries.is_empty() {
|
if !entries.is_empty() {
|
||||||
|
|
@ -165,7 +176,8 @@ impl SqliteClipboardDb {
|
||||||
mwidth += g_width;
|
mwidth += g_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose the row as highlight + id + space + preview + space + mimetype
|
// Compose the row as highlight + id + space + preview + space +
|
||||||
|
// mimetype
|
||||||
let mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
let (id, preview, mime) = entry;
|
let (id, preview, mime) = entry;
|
||||||
if Some(i) == selected {
|
if Some(i) == selected {
|
||||||
|
|
@ -234,11 +246,11 @@ impl SqliteClipboardDb {
|
||||||
} else {
|
} else {
|
||||||
i + 1
|
i + 1
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
state.select(Some(i));
|
state.select(Some(i));
|
||||||
}
|
},
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
let i = match state.selected() {
|
let i = match state.selected() {
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
|
|
@ -247,12 +259,12 @@ impl SqliteClipboardDb {
|
||||||
} else {
|
} else {
|
||||||
i - 1
|
i - 1
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
state.select(Some(i));
|
state.select(Some(i));
|
||||||
}
|
},
|
||||||
_ => {}
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||||
|
|
||||||
use crate::db::StashError;
|
|
||||||
|
|
||||||
pub trait QueryCommand {
|
pub trait QueryCommand {
|
||||||
fn query_delete(&self, query: &str) -> Result<usize, StashError>;
|
fn query_delete(&self, query: &str) -> Result<usize, StashError>;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
|
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
||||||
|
|
||||||
pub trait StoreCommand {
|
pub trait StoreCommand {
|
||||||
fn store(
|
fn store(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::db::{ClipboardDb, Entry, SqliteClipboardDb};
|
use std::{io::Read, time::Duration};
|
||||||
|
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use std::io::Read;
|
|
||||||
use std::time::Duration;
|
|
||||||
use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents};
|
use wl_clipboard_rs::paste::{ClipboardType, Seat, get_contents};
|
||||||
|
|
||||||
|
use crate::db::{ClipboardDb, Entry, SqliteClipboardDb};
|
||||||
|
|
||||||
pub trait WatchCommand {
|
pub trait WatchCommand {
|
||||||
fn watch(&self, max_dedupe_search: u64, max_items: u64);
|
fn watch(&self, max_dedupe_search: u64, max_items: u64);
|
||||||
}
|
}
|
||||||
|
|
@ -64,13 +65,13 @@ impl WatchCommand for SqliteClipboardDb {
|
||||||
// Drop clipboard contents after storing
|
// Drop clipboard contents after storing
|
||||||
last_contents = None;
|
last_contents = None;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_msg = e.to_string();
|
let error_msg = e.to_string();
|
||||||
if !error_msg.contains("empty") {
|
if !error_msg.contains("empty") {
|
||||||
log::error!("Failed to get clipboard contents: {e}");
|
log::error!("Failed to get clipboard contents: {e}");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use crate::db::{ClipboardDb, SqliteClipboardDb};
|
use crate::db::{ClipboardDb, SqliteClipboardDb, StashError};
|
||||||
|
|
||||||
use crate::db::StashError;
|
|
||||||
|
|
||||||
pub trait WipeCommand {
|
pub trait WipeCommand {
|
||||||
fn wipe(&self) -> Result<(), StashError>;
|
fn wipe(&self) -> Result<(), StashError>;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
use std::env;
|
use std::{
|
||||||
use std::fmt;
|
env,
|
||||||
use std::fs;
|
fmt,
|
||||||
use std::io::{BufRead, BufReader, Read, Write};
|
fs,
|
||||||
use std::str;
|
io::{BufRead, BufReader, Read, Write},
|
||||||
|
str,
|
||||||
|
};
|
||||||
|
|
||||||
|
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||||
use imagesize::{ImageSize, ImageType};
|
use imagesize::{ImageSize, ImageType};
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use rusqlite::{Connection, OptionalExtension, params};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum StashError {
|
pub enum StashError {
|
||||||
|
|
@ -67,7 +66,11 @@ pub trait ClipboardDb {
|
||||||
fn trim_db(&self, max: u64) -> Result<(), StashError>;
|
fn trim_db(&self, max: u64) -> Result<(), StashError>;
|
||||||
fn delete_last(&self) -> Result<(), StashError>;
|
fn delete_last(&self) -> Result<(), StashError>;
|
||||||
fn wipe_db(&self) -> Result<(), StashError>;
|
fn wipe_db(&self) -> Result<(), StashError>;
|
||||||
fn list_entries(&self, out: impl Write, preview_width: u32) -> Result<usize, StashError>;
|
fn list_entries(
|
||||||
|
&self,
|
||||||
|
out: impl Write,
|
||||||
|
preview_width: u32,
|
||||||
|
) -> Result<usize, StashError>;
|
||||||
fn decode_entry(
|
fn decode_entry(
|
||||||
&self,
|
&self,
|
||||||
in_: impl Read,
|
in_: impl Read,
|
||||||
|
|
@ -98,7 +101,8 @@ pub struct SqliteClipboardDb {
|
||||||
|
|
||||||
impl SqliteClipboardDb {
|
impl SqliteClipboardDb {
|
||||||
pub fn new(conn: Connection) -> Result<Self, StashError> {
|
pub fn new(conn: Connection) -> Result<Self, StashError> {
|
||||||
conn.execute_batch(
|
conn
|
||||||
|
.execute_batch(
|
||||||
"CREATE TABLE IF NOT EXISTS clipboard (
|
"CREATE TABLE IF NOT EXISTS clipboard (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
contents BLOB NOT NULL,
|
contents BLOB NOT NULL,
|
||||||
|
|
@ -138,7 +142,7 @@ impl SqliteClipboardDb {
|
||||||
let contents_str = match mime.as_deref() {
|
let contents_str = match mime.as_deref() {
|
||||||
Some(m) if m.starts_with("text/") || m == "application/json" => {
|
Some(m) if m.starts_with("text/") || m == "application/json" => {
|
||||||
String::from_utf8_lossy(&contents).to_string()
|
String::from_utf8_lossy(&contents).to_string()
|
||||||
}
|
},
|
||||||
_ => STANDARD.encode(&contents),
|
_ => STANDARD.encode(&contents),
|
||||||
};
|
};
|
||||||
entries.push(json!({
|
entries.push(json!({
|
||||||
|
|
@ -148,7 +152,8 @@ impl SqliteClipboardDb {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
serde_json::to_string_pretty(&entries).map_err(|e| StashError::ListDecode(e.to_string()))
|
serde_json::to_string_pretty(&entries)
|
||||||
|
.map_err(|e| StashError::ListDecode(e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,7 +165,10 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
max_items: u64,
|
max_items: u64,
|
||||||
) -> Result<u64, StashError> {
|
) -> Result<u64, StashError> {
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
if input.read_to_end(&mut buf).is_err() || buf.is_empty() || buf.len() > 5 * 1_000_000 {
|
if input.read_to_end(&mut buf).is_err()
|
||||||
|
|| buf.is_empty()
|
||||||
|
|| buf.len() > 5 * 1_000_000
|
||||||
|
{
|
||||||
return Err(StashError::EmptyOrTooLarge);
|
return Err(StashError::EmptyOrTooLarge);
|
||||||
}
|
}
|
||||||
if buf.iter().all(u8::is_ascii_whitespace) {
|
if buf.iter().all(u8::is_ascii_whitespace) {
|
||||||
|
|
@ -175,7 +183,7 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
other => other,
|
other => other,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -186,14 +194,17 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
if let Ok(s) = std::str::from_utf8(&buf) {
|
if let Ok(s) = std::str::from_utf8(&buf) {
|
||||||
if re.is_match(s) {
|
if re.is_match(s) {
|
||||||
warn!("Clipboard entry matches sensitive regex, skipping store.");
|
warn!("Clipboard entry matches sensitive regex, skipping store.");
|
||||||
return Err(StashError::Store("Filtered by sensitive regex".to_string()));
|
return Err(StashError::Store(
|
||||||
|
"Filtered by sensitive regex".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.deduplicate(&buf, max_dedupe_search)?;
|
self.deduplicate(&buf, max_dedupe_search)?;
|
||||||
|
|
||||||
self.conn
|
self
|
||||||
|
.conn
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)",
|
"INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)",
|
||||||
params![buf, mime],
|
params![buf, mime],
|
||||||
|
|
@ -224,7 +235,8 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
.get(1)
|
.get(1)
|
||||||
.map_err(|e| StashError::DeduplicationDecode(e.to_string()))?;
|
.map_err(|e| StashError::DeduplicationDecode(e.to_string()))?;
|
||||||
if contents == buf {
|
if contents == buf {
|
||||||
self.conn
|
self
|
||||||
|
.conn
|
||||||
.execute("DELETE FROM clipboard WHERE id = ?1", params![id])
|
.execute("DELETE FROM clipboard WHERE id = ?1", params![id])
|
||||||
.map_err(|e| StashError::DeduplicationRemove(e.to_string()))?;
|
.map_err(|e| StashError::DeduplicationRemove(e.to_string()))?;
|
||||||
deduped += 1;
|
deduped += 1;
|
||||||
|
|
@ -240,10 +252,14 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
.map_err(|e| StashError::Trim(e.to_string()))?;
|
.map_err(|e| StashError::Trim(e.to_string()))?;
|
||||||
if count > max {
|
if count > max {
|
||||||
let to_delete = count - max;
|
let to_delete = count - max;
|
||||||
self.conn.execute(
|
self
|
||||||
"DELETE FROM clipboard WHERE id IN (SELECT id FROM clipboard ORDER BY id ASC LIMIT ?1)",
|
.conn
|
||||||
|
.execute(
|
||||||
|
"DELETE FROM clipboard WHERE id IN (SELECT id FROM clipboard ORDER \
|
||||||
|
BY id ASC LIMIT ?1)",
|
||||||
params![i64::try_from(to_delete).unwrap_or(i64::MAX)],
|
params![i64::try_from(to_delete).unwrap_or(i64::MAX)],
|
||||||
).map_err(|e| StashError::Trim(e.to_string()))?;
|
)
|
||||||
|
.map_err(|e| StashError::Trim(e.to_string()))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +275,8 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| StashError::DeleteLast(e.to_string()))?;
|
.map_err(|e| StashError::DeleteLast(e.to_string()))?;
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
self.conn
|
self
|
||||||
|
.conn
|
||||||
.execute("DELETE FROM clipboard WHERE id = ?1", params![id])
|
.execute("DELETE FROM clipboard WHERE id = ?1", params![id])
|
||||||
.map_err(|e| StashError::DeleteLast(e.to_string()))?;
|
.map_err(|e| StashError::DeleteLast(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -269,13 +286,18 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wipe_db(&self) -> Result<(), StashError> {
|
fn wipe_db(&self) -> Result<(), StashError> {
|
||||||
self.conn
|
self
|
||||||
|
.conn
|
||||||
.execute("DELETE FROM clipboard", [])
|
.execute("DELETE FROM clipboard", [])
|
||||||
.map_err(|e| StashError::Wipe(e.to_string()))?;
|
.map_err(|e| StashError::Wipe(e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_entries(&self, mut out: impl Write, preview_width: u32) -> Result<usize, StashError> {
|
fn list_entries(
|
||||||
|
&self,
|
||||||
|
mut out: impl Write,
|
||||||
|
preview_width: u32,
|
||||||
|
) -> Result<usize, StashError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC")
|
.prepare("SELECT id, contents, mime FROM clipboard ORDER BY id DESC")
|
||||||
|
|
@ -315,11 +337,13 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
input
|
input
|
||||||
} else {
|
} else {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
in_.read_to_string(&mut buf)
|
in_
|
||||||
|
.read_to_string(&mut buf)
|
||||||
.map_err(|e| StashError::DecodeRead(e.to_string()))?;
|
.map_err(|e| StashError::DecodeRead(e.to_string()))?;
|
||||||
buf
|
buf
|
||||||
};
|
};
|
||||||
let id = extract_id(&s).map_err(|e| StashError::DecodeExtractId(e.to_string()))?;
|
let id =
|
||||||
|
extract_id(&s).map_err(|e| StashError::DecodeExtractId(e.to_string()))?;
|
||||||
let (contents, _mime): (Vec<u8>, Option<String>) = self
|
let (contents, _mime): (Vec<u8>, Option<String>) = self
|
||||||
.conn
|
.conn
|
||||||
.query_row(
|
.query_row(
|
||||||
|
|
@ -328,7 +352,8 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
)
|
)
|
||||||
.map_err(|e| StashError::DecodeGet(e.to_string()))?;
|
.map_err(|e| StashError::DecodeGet(e.to_string()))?;
|
||||||
out.write_all(&contents)
|
out
|
||||||
|
.write_all(&contents)
|
||||||
.map_err(|e| StashError::DecodeWrite(e.to_string()))?;
|
.map_err(|e| StashError::DecodeWrite(e.to_string()))?;
|
||||||
info!("Decoded entry with id {id}");
|
info!("Decoded entry with id {id}");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -354,7 +379,8 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
.get(1)
|
.get(1)
|
||||||
.map_err(|e| StashError::QueryDelete(e.to_string()))?;
|
.map_err(|e| StashError::QueryDelete(e.to_string()))?;
|
||||||
if contents.windows(query.len()).any(|w| w == query.as_bytes()) {
|
if contents.windows(query.len()).any(|w| w == query.as_bytes()) {
|
||||||
self.conn
|
self
|
||||||
|
.conn
|
||||||
.execute("DELETE FROM clipboard WHERE id = ?1", params![id])
|
.execute("DELETE FROM clipboard WHERE id = ?1", params![id])
|
||||||
.map_err(|e| StashError::QueryDelete(e.to_string()))?;
|
.map_err(|e| StashError::QueryDelete(e.to_string()))?;
|
||||||
deleted += 1;
|
deleted += 1;
|
||||||
|
|
@ -368,7 +394,8 @@ impl ClipboardDb for SqliteClipboardDb {
|
||||||
let mut deleted = 0;
|
let mut deleted = 0;
|
||||||
for line in reader.lines().map_while(Result::ok) {
|
for line in reader.lines().map_while(Result::ok) {
|
||||||
if let Ok(id) = extract_id(&line) {
|
if let Ok(id) = extract_id(&line) {
|
||||||
self.conn
|
self
|
||||||
|
.conn
|
||||||
.execute("DELETE FROM clipboard WHERE id = ?1", params![id])
|
.execute("DELETE FROM clipboard WHERE id = ?1", params![id])
|
||||||
.map_err(|e| StashError::DeleteEntry(id, e.to_string()))?;
|
.map_err(|e| StashError::DeleteEntry(id, e.to_string()))?;
|
||||||
deleted += 1;
|
deleted += 1;
|
||||||
|
|
@ -476,7 +503,7 @@ pub fn preview_entry(data: &[u8], mime: Option<&str>, width: u32) -> String {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to decode UTF-8 clipboard data: {e}");
|
error!("Failed to decode UTF-8 clipboard data: {e}");
|
||||||
""
|
""
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
let s = s.trim().replace(|c: char| c.is_whitespace(), " ");
|
let s = s.trim().replace(|c: char| c.is_whitespace(), " ");
|
||||||
return truncate(&s, width as usize, "…");
|
return truncate(&s, width as usize, "…");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use crate::db::{Entry, SqliteClipboardDb, detect_mime};
|
|
||||||
use log::{error, info};
|
|
||||||
use std::io::{self, BufRead};
|
use std::io::{self, BufRead};
|
||||||
|
|
||||||
|
use log::{error, info};
|
||||||
|
|
||||||
|
use crate::db::{Entry, SqliteClipboardDb, detect_mime};
|
||||||
|
|
||||||
pub trait ImportCommand {
|
pub trait ImportCommand {
|
||||||
fn import_tsv(&self, input: impl io::Read);
|
fn import_tsv(&self, input: impl io::Read);
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +36,7 @@ impl ImportCommand for SqliteClipboardDb {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
imported += 1;
|
imported += 1;
|
||||||
info!("Imported entry from TSV");
|
info!("Imported entry from TSV");
|
||||||
}
|
},
|
||||||
Err(e) => error!("Failed to insert entry: {e}"),
|
Err(e) => error!("Failed to insert entry: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
92
src/main.rs
92
src/main.rs
|
|
@ -6,7 +6,6 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use atty::Stream;
|
use atty::Stream;
|
||||||
|
|
||||||
use clap::{CommandFactory, Parser, Subcommand};
|
use clap::{CommandFactory, Parser, Subcommand};
|
||||||
use inquire::Confirm;
|
use inquire::Confirm;
|
||||||
|
|
||||||
|
|
@ -14,14 +13,18 @@ mod commands;
|
||||||
mod db;
|
mod db;
|
||||||
mod import;
|
mod import;
|
||||||
|
|
||||||
use crate::commands::decode::DecodeCommand;
|
use crate::{
|
||||||
use crate::commands::delete::DeleteCommand;
|
commands::{
|
||||||
use crate::commands::list::ListCommand;
|
decode::DecodeCommand,
|
||||||
use crate::commands::query::QueryCommand;
|
delete::DeleteCommand,
|
||||||
use crate::commands::store::StoreCommand;
|
list::ListCommand,
|
||||||
use crate::commands::watch::WatchCommand;
|
query::QueryCommand,
|
||||||
use crate::commands::wipe::WipeCommand;
|
store::StoreCommand,
|
||||||
use crate::import::ImportCommand;
|
watch::WatchCommand,
|
||||||
|
wipe::WipeCommand,
|
||||||
|
},
|
||||||
|
import::ImportCommand,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "stash")]
|
#[command(name = "stash")]
|
||||||
|
|
@ -65,8 +68,9 @@ enum Command {
|
||||||
/// Decode and output clipboard entry by id
|
/// Decode and output clipboard entry by id
|
||||||
Decode { input: Option<String> },
|
Decode { input: Option<String> },
|
||||||
|
|
||||||
/// Delete clipboard entry by id (if numeric), or entries matching a query (if not).
|
/// Delete clipboard entry by id (if numeric), or entries matching a query (if
|
||||||
/// Numeric arguments are treated as ids. Use --type to specify explicitly.
|
/// not). Numeric arguments are treated as ids. Use --type to specify
|
||||||
|
/// explicitly.
|
||||||
Delete {
|
Delete {
|
||||||
/// Id or query string
|
/// Id or query string
|
||||||
arg: Option<String>,
|
arg: Option<String>,
|
||||||
|
|
@ -102,13 +106,16 @@ enum Command {
|
||||||
Watch,
|
Watch,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn report_error<T>(result: Result<T, impl std::fmt::Display>, context: &str) -> Option<T> {
|
fn report_error<T>(
|
||||||
|
result: Result<T, impl std::fmt::Display>,
|
||||||
|
context: &str,
|
||||||
|
) -> Option<T> {
|
||||||
match result {
|
match result {
|
||||||
Ok(val) => Some(val),
|
Ok(val) => Some(val),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("{context}: {e}");
|
log::error!("{context}: {e}");
|
||||||
None
|
None
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +151,7 @@ fn main() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to initialize SQLite database: {e}");
|
log::error!("Failed to initialize SQLite database: {e}");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
|
|
@ -154,25 +161,28 @@ fn main() {
|
||||||
db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state),
|
db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state),
|
||||||
"Failed to store entry",
|
"Failed to store entry",
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
Some(Command::List { format }) => match format.as_deref() {
|
Some(Command::List { format }) => {
|
||||||
|
match format.as_deref() {
|
||||||
Some("tsv") => {
|
Some("tsv") => {
|
||||||
report_error(
|
report_error(
|
||||||
db.list(io::stdout(), cli.preview_width),
|
db.list(io::stdout(), cli.preview_width),
|
||||||
"Failed to list entries",
|
"Failed to list entries",
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
Some("json") => match db.list_json() {
|
Some("json") => {
|
||||||
|
match db.list_json() {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
println!("{json}");
|
println!("{json}");
|
||||||
}
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to list entries as JSON: {e}");
|
log::error!("Failed to list entries as JSON: {e}");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(other) => {
|
Some(other) => {
|
||||||
log::error!("Unsupported format: {other}");
|
log::error!("Unsupported format: {other}");
|
||||||
}
|
},
|
||||||
None => {
|
None => {
|
||||||
if atty::is(Stream::Stdout) {
|
if atty::is(Stream::Stdout) {
|
||||||
report_error(
|
report_error(
|
||||||
|
|
@ -185,6 +195,7 @@ fn main() {
|
||||||
"Failed to list entries",
|
"Failed to list entries",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(Command::Decode { input }) => {
|
Some(Command::Decode { input }) => {
|
||||||
|
|
@ -192,7 +203,7 @@ fn main() {
|
||||||
db.decode(io::stdin(), io::stdout(), input),
|
db.decode(io::stdin(), io::stdout(), input),
|
||||||
"Failed to decode entry",
|
"Failed to decode entry",
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
Some(Command::Delete { arg, r#type, ask }) => {
|
Some(Command::Delete { arg, r#type, ask }) => {
|
||||||
let mut should_proceed = true;
|
let mut should_proceed = true;
|
||||||
if ask {
|
if ask {
|
||||||
|
|
@ -218,10 +229,13 @@ fn main() {
|
||||||
} else {
|
} else {
|
||||||
log::error!("Argument is not a valid id");
|
log::error!("Argument is not a valid id");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
(Some(s), Some("query")) => {
|
(Some(s), Some("query")) => {
|
||||||
report_error(db.query_delete(&s), "Failed to delete entry by query");
|
report_error(
|
||||||
}
|
db.query_delete(&s),
|
||||||
|
"Failed to delete entry by query",
|
||||||
|
);
|
||||||
|
},
|
||||||
(Some(s), None) => {
|
(Some(s), None) => {
|
||||||
if let Ok(id) = s.parse::<u64>() {
|
if let Ok(id) = s.parse::<u64>() {
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
@ -235,24 +249,25 @@ fn main() {
|
||||||
"Failed to delete entry by query",
|
"Failed to delete entry by query",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
(None, _) => {
|
(None, _) => {
|
||||||
report_error(
|
report_error(
|
||||||
db.delete(io::stdin()),
|
db.delete(io::stdin()),
|
||||||
"Failed to delete entry from stdin",
|
"Failed to delete entry from stdin",
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
(_, Some(_)) => {
|
(_, Some(_)) => {
|
||||||
log::error!("Unknown type for --type. Use \"id\" or \"query\".");
|
log::error!("Unknown type for --type. Use \"id\" or \"query\".");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
Some(Command::Wipe { ask }) => {
|
Some(Command::Wipe { ask }) => {
|
||||||
let mut should_proceed = true;
|
let mut should_proceed = true;
|
||||||
if ask {
|
if ask {
|
||||||
should_proceed =
|
should_proceed = Confirm::new(
|
||||||
Confirm::new("Are you sure you want to wipe all clipboard history?")
|
"Are you sure you want to wipe all clipboard history?",
|
||||||
|
)
|
||||||
.with_default(false)
|
.with_default(false)
|
||||||
.prompt()
|
.prompt()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
@ -263,12 +278,15 @@ fn main() {
|
||||||
if should_proceed {
|
if should_proceed {
|
||||||
report_error(db.wipe(), "Failed to wipe database");
|
report_error(db.wipe(), "Failed to wipe database");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
Some(Command::Import { r#type, ask }) => {
|
Some(Command::Import { r#type, ask }) => {
|
||||||
let mut should_proceed = true;
|
let mut should_proceed = true;
|
||||||
if ask {
|
if ask {
|
||||||
should_proceed = Confirm::new("Are you sure you want to import clipboard data? This may overwrite existing entries.")
|
should_proceed = Confirm::new(
|
||||||
|
"Are you sure you want to import clipboard data? This may \
|
||||||
|
overwrite existing entries.",
|
||||||
|
)
|
||||||
.with_default(false)
|
.with_default(false)
|
||||||
.prompt()
|
.prompt()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
@ -281,22 +299,22 @@ fn main() {
|
||||||
match format {
|
match format {
|
||||||
"tsv" => {
|
"tsv" => {
|
||||||
db.import_tsv(io::stdin());
|
db.import_tsv(io::stdin());
|
||||||
}
|
},
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("Unsupported import format: {format}");
|
log::error!("Unsupported import format: {format}");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
Some(Command::Watch) => {
|
Some(Command::Watch) => {
|
||||||
db.watch(cli.max_dedupe_search, cli.max_items);
|
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() {
|
||||||
log::error!("Failed to print help: {e}");
|
log::error!("Failed to print help: {e}");
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue