mirror of
https://github.com/NotAShelf/stash.git
synced 2026-04-23 18:35:04 +00:00
Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a819e2948d | |||
| df31f5e67c |
7 changed files with 70 additions and 98 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -192,17 +192,6 @@ version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atty"
|
|
||||||
version = "0.2.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
|
||||||
dependencies = [
|
|
||||||
"hermit-abi 0.1.19",
|
|
||||||
"libc",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -712,15 +701,6 @@ version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hermit-abi"
|
|
||||||
version = "0.1.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
|
@ -1058,7 +1038,7 @@ checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"hermit-abi 0.5.2",
|
"hermit-abi",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustix 1.0.8",
|
"rustix 1.0.8",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
|
|
@ -1342,7 +1322,6 @@ dependencies = [
|
||||||
name = "stash"
|
name = "stash"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atty",
|
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"clap-verbosity-flag",
|
"clap-verbosity-flag",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ serde_json = "1.0.142"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
atty = "0.2.14"
|
|
||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
unicode-width = "0.2.0"
|
unicode-width = "0.2.0"
|
||||||
|
|
|
||||||
|
|
@ -24,26 +24,33 @@ impl DecodeCommand for SqliteClipboardDb {
|
||||||
s
|
s
|
||||||
} else {
|
} else {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
if let Err(e) = in_.read_to_string(&mut buf) {
|
in_
|
||||||
log::error!("Failed to read stdin for decode: {e}");
|
.read_to_string(&mut buf)
|
||||||
}
|
.map_err(|e| StashError::DecodeRead(e.to_string()))?;
|
||||||
buf
|
buf
|
||||||
};
|
};
|
||||||
|
|
||||||
// If input is empty or whitespace, treat as error and trigger fallback
|
// If input is empty or whitespace, treat as error and trigger fallback
|
||||||
if input_str.trim().is_empty() {
|
if input_str.trim().is_empty() {
|
||||||
log::info!("No input provided to decode; relaying clipboard to stdout");
|
log::debug!("No input provided to decode; relaying clipboard to stdout");
|
||||||
if let Ok((mut reader, _mime)) =
|
if let Ok((mut reader, _mime)) =
|
||||||
get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any)
|
get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any)
|
||||||
{
|
{
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
if let Err(err) = reader.read_to_end(&mut buf) {
|
reader.read_to_end(&mut buf).map_err(|e| {
|
||||||
log::error!("Failed to read clipboard for relay: {err}");
|
StashError::DecodeRead(format!(
|
||||||
|
"Failed to read clipboard for relay: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
out.write_all(&buf).map_err(|e| {
|
||||||
|
StashError::DecodeWrite(format!(
|
||||||
|
"Failed to write clipboard relay: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
} else {
|
} else {
|
||||||
let _ = out.write_all(&buf);
|
return Err(StashError::DecodeGet(
|
||||||
}
|
"Failed to get clipboard contents for relay".to_string(),
|
||||||
} else {
|
));
|
||||||
log::error!("Failed to get clipboard contents for relay");
|
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -54,25 +61,28 @@ impl DecodeCommand for SqliteClipboardDb {
|
||||||
&mut out,
|
&mut out,
|
||||||
Some(input_str.clone()),
|
Some(input_str.clone()),
|
||||||
) {
|
) {
|
||||||
Ok(()) => {
|
Ok(()) => Ok(()),
|
||||||
log::info!("Entry decoded");
|
|
||||||
},
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to decode entry: {e}");
|
// On decode failure, relay clipboard as fallback
|
||||||
if let Ok((mut reader, _mime)) =
|
if let Ok((mut reader, _mime)) =
|
||||||
get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any)
|
get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any)
|
||||||
{
|
{
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
if let Err(err) = reader.read_to_end(&mut buf) {
|
reader.read_to_end(&mut buf).map_err(|err| {
|
||||||
log::error!("Failed to read clipboard for relay: {err}");
|
StashError::DecodeRead(format!(
|
||||||
|
"Failed to read clipboard for relay: {err}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
out.write_all(&buf).map_err(|err| {
|
||||||
|
StashError::DecodeWrite(format!(
|
||||||
|
"Failed to write clipboard relay: {err}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
let _ = out.write_all(&buf);
|
Err(e)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::error!("Failed to get clipboard contents for relay");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,8 @@ pub trait DeleteCommand {
|
||||||
|
|
||||||
impl DeleteCommand for SqliteClipboardDb {
|
impl DeleteCommand for SqliteClipboardDb {
|
||||||
fn delete(&self, input: impl Read) -> Result<usize, StashError> {
|
fn delete(&self, input: impl Read) -> Result<usize, StashError> {
|
||||||
match self.delete_entries(input) {
|
let deleted = self.delete_entries(input)?;
|
||||||
Ok(deleted) => {
|
|
||||||
log::info!("Deleted {deleted} entries");
|
log::info!("Deleted {deleted} entries");
|
||||||
Ok(deleted)
|
Ok(deleted)
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to delete entries: {e}");
|
|
||||||
Err(e)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
use std::io::{self, BufRead};
|
use std::io::{self, BufRead};
|
||||||
|
|
||||||
use log::{error, info};
|
|
||||||
|
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
ClipboardDb,
|
ClipboardDb,
|
||||||
Entry,
|
Entry,
|
||||||
|
|
@ -12,18 +10,6 @@ use crate::db::{
|
||||||
|
|
||||||
pub trait ImportCommand {
|
pub trait ImportCommand {
|
||||||
/// Import clipboard entries from TSV format.
|
/// Import clipboard entries from TSV format.
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `input` - A readable stream containing TSV lines, each of the form
|
|
||||||
/// `<id>\t<contents>`.
|
|
||||||
/// * `max_items` - The maximum number of clipboard entries to keep after
|
|
||||||
/// import. If set to `u64::MAX`, no trimming occurs.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// * `Ok(())` if all entries are imported and trimming succeeds.
|
|
||||||
/// * `Err(StashError)` if any error occurs during import or trimming.
|
|
||||||
fn import_tsv(
|
fn import_tsv(
|
||||||
&self,
|
&self,
|
||||||
input: impl io::Read,
|
input: impl io::Read,
|
||||||
|
|
@ -39,16 +25,21 @@ impl ImportCommand for SqliteClipboardDb {
|
||||||
) -> Result<(), StashError> {
|
) -> Result<(), StashError> {
|
||||||
let reader = io::BufReader::new(input);
|
let reader = io::BufReader::new(input);
|
||||||
let mut imported = 0;
|
let mut imported = 0;
|
||||||
for line in reader.lines().map_while(Result::ok) {
|
for (lineno, line) in reader.lines().enumerate() {
|
||||||
|
let line = line.map_err(|e| {
|
||||||
|
StashError::Store(format!("Failed to read line {lineno}: {e}"))
|
||||||
|
})?;
|
||||||
let mut parts = line.splitn(2, '\t');
|
let mut parts = line.splitn(2, '\t');
|
||||||
let (Some(id_str), Some(val)) = (parts.next(), parts.next()) else {
|
let (Some(id_str), Some(val)) = (parts.next(), parts.next()) else {
|
||||||
error!("Malformed TSV line: {line:?}");
|
return Err(StashError::Store(format!(
|
||||||
continue;
|
"Malformed TSV line {lineno}: {line:?}"
|
||||||
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let Ok(_id) = id_str.parse::<u64>() else {
|
let Ok(_id) = id_str.parse::<u64>() else {
|
||||||
error!("Failed to parse id from line: {id_str}");
|
return Err(StashError::Store(format!(
|
||||||
continue;
|
"Failed to parse id from line {lineno}: {id_str}"
|
||||||
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let entry = Entry {
|
let entry = Entry {
|
||||||
|
|
@ -56,22 +47,26 @@ impl ImportCommand for SqliteClipboardDb {
|
||||||
mime: detect_mime(val.as_bytes()),
|
mime: detect_mime(val.as_bytes()),
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.conn.execute(
|
self
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
"INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)",
|
"INSERT INTO clipboard (contents, mime) VALUES (?1, ?2)",
|
||||||
rusqlite::params![entry.contents, entry.mime],
|
rusqlite::params![entry.contents, entry.mime],
|
||||||
) {
|
)
|
||||||
Ok(_) => {
|
.map_err(|e| {
|
||||||
|
StashError::Store(format!(
|
||||||
|
"Failed to insert entry at line {lineno}: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
imported += 1;
|
imported += 1;
|
||||||
info!("Imported entry from TSV");
|
|
||||||
},
|
|
||||||
Err(e) => error!("Failed to insert entry: {e}"),
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
info!("Imported {imported} records from TSV into SQLite database.");
|
log::info!("Imported {imported} records from TSV into SQLite database.");
|
||||||
|
|
||||||
// Trim database to max_items after import
|
// Trim database to max_items after import
|
||||||
self.trim_db(max_items)?;
|
self.trim_db(max_items)?;
|
||||||
info!("Trimmed clipboard database to max_items = {max_items}");
|
log::info!("Trimmed clipboard database to max_items = {max_items}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,11 @@ impl ListCommand for SqliteClipboardDb {
|
||||||
out: impl Write,
|
out: impl Write,
|
||||||
preview_width: u32,
|
preview_width: u32,
|
||||||
) -> Result<(), StashError> {
|
) -> Result<(), StashError> {
|
||||||
self.list_entries(out, preview_width)?;
|
self.list_entries(out, preview_width).map(|_| ())
|
||||||
log::info!("Listed clipboard entries");
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteClipboardDb {
|
impl SqliteClipboardDb {
|
||||||
/// 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 std::io::stdout;
|
||||||
|
|
@ -272,14 +269,14 @@ impl SqliteClipboardDb {
|
||||||
Ok(())
|
Ok(())
|
||||||
})();
|
})();
|
||||||
|
|
||||||
disable_raw_mode().ok();
|
// Ignore errors during terminal restore, as we can't recover here.
|
||||||
execute!(
|
let _ = disable_raw_mode();
|
||||||
|
let _ = execute!(
|
||||||
terminal.backend_mut(),
|
terminal.backend_mut(),
|
||||||
LeaveAlternateScreen,
|
LeaveAlternateScreen,
|
||||||
DisableMouseCapture
|
DisableMouseCapture
|
||||||
)
|
);
|
||||||
.ok();
|
let _ = terminal.show_cursor();
|
||||||
terminal.show_cursor().ok();
|
|
||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
io::{self},
|
io::{self, IsTerminal},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process,
|
process,
|
||||||
};
|
};
|
||||||
|
|
||||||
use atty::Stream;
|
|
||||||
use clap::{CommandFactory, Parser, Subcommand};
|
use clap::{CommandFactory, Parser, Subcommand};
|
||||||
use inquire::Confirm;
|
use inquire::Confirm;
|
||||||
|
|
||||||
|
|
@ -44,7 +43,7 @@ struct Cli {
|
||||||
#[arg(long, default_value_t = 100)]
|
#[arg(long, default_value_t = 100)]
|
||||||
preview_width: u32,
|
preview_width: u32,
|
||||||
|
|
||||||
/// Path to the SQLite clipboard database file.
|
/// Path to the `SQLite` clipboard database file.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
db_path: Option<PathBuf>,
|
db_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
|
@ -187,7 +186,7 @@ fn main() {
|
||||||
log::error!("Unsupported format: {other}");
|
log::error!("Unsupported format: {other}");
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
if atty::is(Stream::Stdout) {
|
if std::io::stdout().is_terminal() {
|
||||||
report_error(
|
report_error(
|
||||||
db.list_tui(cli.preview_width),
|
db.list_tui(cli.preview_width),
|
||||||
"Failed to list entries in TUI",
|
"Failed to list entries in TUI",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue