From a1a98ed34daee3ac2798961dcf379c097e61d40e Mon Sep 17 00:00:00 2001 From: _ <_@_> Date: Mon, 27 Jul 2026 14:25:20 -0500 Subject: [PATCH] add `paste` command that instantly pastes from the Wayland clipboard into the default log --- Cargo.lock | 5 +- Cargo.toml | 4 +- src/cli.rs | 3 ++ src/error.rs | 72 +++++++++++++++++++++++++ src/lib.rs | 1 + src/log.rs | 97 +++++++++------------------------ src/main.rs | 150 ++++++++++++++++++++++++++++----------------------- 7 files changed, 189 insertions(+), 143 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index f38b919..cfc6522 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,9 @@ name = "camino" version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] [[package]] name = "cc" @@ -494,7 +497,7 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "sql-hummus" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "camino", diff --git a/Cargo.toml b/Cargo.toml index 425e777..9e64bf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,11 @@ keywords = ["database", "sqlite"] license-file = "LICENSE.md" readme = "README.md" repository = "https://github.com/ReactorScram/sql-hummus" -version = "0.1.3" +version = "0.1.4" [dependencies] anyhow = { version = "1.0.103", features = ["backtrace"] } -camino = "1.2.4" +camino = { version = "1.2.4", features = ["serde1"] } chrono = "0.4.45" clap = { version = "4.6.1", features = ["derive"] } directories = "6.0.0" diff --git a/src/cli.rs b/src/cli.rs index bc32649..a47b250 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -33,6 +33,9 @@ pub(crate) enum Command { cmd: LogCommand, }, + /// Quick paste to the default log file. Uses `wl-paste` + Paste, + /// Quick push to the default log file Push { content: Option }, } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..c20056a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,72 @@ +/// Public-facing opaque error +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] ErrorRepr); + +pub type Result = std::result::Result; + +// FIXME: I'm not 100% about this error setup. If you have gdb, it's redundant. If you don't have gdb, it's better than nothing. +// Unlike anyhow it's only safe Rust, and it's very simple, and it's my code. +#[derive(Debug, thiserror::Error)] +#[error("kind={kind}; cookies={cookies:?}")] +struct ErrorRepr { + cookies: Vec<&'static str>, + #[source] + kind: ErrorKind, +} + +#[derive(Debug, thiserror::Error)] +pub(crate) enum ErrorKind { + #[error(transparent)] + FromPathBufError(#[from] camino::FromPathBufError), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Error: {0}")] + Str(&'static str), + #[error("SQLite error: {0}")] + Sqlite(#[from] sql_peas::Error), + #[error("ULID decode error: {0}")] + UlidDecode(#[from] ulid::DecodeError), +} + +impl From<&'static str> for ErrorKind { + fn from(s: &'static str) -> Self { + Self::Str(s) + } +} + +impl From for Error +where + ErrorKind: From, +{ + fn from(value: T) -> Self { + ErrorRepr { + cookies: vec![], + kind: ErrorKind::from(value), + } + .into() + } +} + +/// Just like `anyhow::Context` but simpler +/// +/// "Type 'cookie', you idiot." -- The Plague, Hackers (film) +pub(crate) trait Cookie { + fn cookie(self, s: &'static str) -> Result; +} + +impl Cookie for std::result::Result +where + Error: From, +{ + fn cookie(self, s: &'static str) -> Result { + match self { + Ok(x) => Ok(x), + Err(e) => { + let mut e = Error::from(e); + e.0.cookies.push(s); + Err(e) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7761908..e85b41b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod error; pub mod kv; pub mod log; diff --git a/src/log.rs b/src/log.rs index d930d78..6fe4640 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,5 +1,7 @@ +use crate::error::{Cookie as _, Error, Result}; use std::time::SystemTime; +use camino::Utf8PathBuf; use sql_peas::StatementHandle; pub struct Log { @@ -26,64 +28,6 @@ pub struct Element { value: String, } -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct PublicError(#[from] ErrorRepr); - -// FIXME: I'm not 100% about this error setup. If you have gdb, it's redundant. If you don't have gdb, it's nice. -#[derive(Debug, thiserror::Error)] -#[error("kind={kind}; cookies={cookies:?}")] -struct ErrorRepr { - cookies: Vec<&'static str>, - #[source] - kind: ErrorKind, -} - -#[derive(Debug, thiserror::Error)] -pub enum ErrorKind { - #[error("SQLite error: {0}")] - Sqlite(#[from] sql_peas::Error), - #[error("Error: {0}")] - Other(&'static str), - #[error("ULID decode error: {0}")] - UlidDecode(#[from] ulid::DecodeError), -} - -impl From for PublicError -where - ErrorKind: From, -{ - fn from(value: T) -> Self { - ErrorRepr { - cookies: vec![], - kind: ErrorKind::from(value), - } - .into() - } -} - -trait Cookie { - fn cookie(self, s: &'static str) -> Result; -} - -impl Cookie for std::result::Result -where - PublicError: From, -{ - fn cookie(self, s: &'static str) -> Result { - match self { - Ok(x) => Ok(x), - Err(e) => { - let mut e = PublicError::from(e); - e.0.cookies.push(s); - Err(e) - } - } - } -} - -type Result = std::result::Result; - impl Iterator for LogCursor<'_> { type Item = Result; @@ -108,6 +52,21 @@ const LOG_USER_VERSION_I64: i64 = LOG_USER_VERSION as i64; const LOG_TABLE_NAME: &str = "sql_hummus_0_log"; impl Log { + /// Opens the default user-scope log. + /// + /// Only use this as a result of user interaction. + /// + /// The default log is shared among everything the user does, so it would get spammed easily if automated processes write to it. + pub fn open_default() -> Result<(Log, Utf8PathBuf)> { + let dirs = directories::ProjectDirs::from("", "ReactorScram", "sql-hummus") + .ok_or(Error::from("directories::ProjectDirs failed"))?; + let dir = dirs.data_local_dir(); + std::fs::create_dir_all(dir).cookie("create_dir_all() for default data dir failed")?; + let path = dir.join("default-log.db").try_into()?; + let log = Log::new(&path)?; + Ok((log, path)) + } + pub fn new>(p: P) -> Result { let mut inner = sql_peas::Connection::open(p)?; @@ -120,16 +79,14 @@ impl Log { let stmt = inner.borrow_statement(handle)?; // FIXME: de-dupe single row read up into SQLite let mut rows = stmt.iter(); - let row = rows.next().ok_or(ErrorKind::Other( - "Expected one row from PRAGMA user_version", - ))??; + let row = rows + .next() + .ok_or("Expected one row from PRAGMA user_version")??; let needs_setup = match row.read(0) { 0 => true, LOG_USER_VERSION_I64 => false, _ => { - return Err(ErrorKind::Other( - "PRAGMA user_version looks like a non-KV file", - ))?; + return Err("PRAGMA user_version looks like a non-KV file")?; } }; @@ -176,9 +133,7 @@ impl Log { return Ok(None); } if index != stmt.read(0)? { - Err(ErrorKind::Other( - "Log::get didn't get the same index back from the DB", - ))?; + Err("Log::get didn't get the same index back from the DB")?; } let ulid = ulid::Ulid::from_string(&stmt.read::(1)?)?; let value = stmt.read(2)?; @@ -199,15 +154,11 @@ impl Log { stmt.bind((1, ts)).cookie("7RMQCY4N")?; stmt.bind((2, value)).cookie("AGRZOZBD")?; if stmt.next().cookie("WEMNOGO7")? != sql_peas::State::Row { - Err(ErrorKind::Other( - "We didn't get State::Row during Log::insert", - ))?; + Err("We didn't get State::Row during Log::insert")?; } let index = stmt.read(0).cookie("MEJYJBDW")?; if stmt.next().cookie("QOV7GD47")? != sql_peas::State::Done { - Err(ErrorKind::Other( - "We didn't get sql_peas::State::Done during Log::insert", - ))?; + Err("We didn't get State::Done during Log::insert")?; } Ok(index) } diff --git a/src/main.rs b/src/main.rs index 95f380e..c33801b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use std::{io::Read, process::ExitCode}; use anyhow::{Context as _, Result}; +use camino::Utf8PathBuf; use clap::Parser as _; use serde_json::json; use sql_hummus::{Kv, Log}; @@ -32,83 +33,98 @@ fn main() -> ExitCode { } } -fn inner_main() -> Result { - let cli = cli::Cli::try_parse()?; - match cli.cmd { - Command::Kv { cmd, path } => match cmd { - KvCommand::ContainsKey { key } => match Kv::new(path)?.contains_key(key)? { - true => { - println!("true"); - Ok(true) - } - false => { - println!("false"); - Ok(false) - } - }, - KvCommand::Get { key } => match Kv::new(path)?.get(key)? { - Some(value) => { - println!("{value}"); - Ok(true) - } - None => { - eprintln!("Key not in KV file"); - Ok(false) - } - }, - KvCommand::Insert { key, value } => { - Kv::new(path)?.insert(key, value)?; +fn run_kv_command(path: Utf8PathBuf, cmd: KvCommand) -> Result { + match cmd { + KvCommand::ContainsKey { key } => match Kv::new(path)?.contains_key(key)? { + true => { + println!("true"); Ok(true) } - KvCommand::WithPrefix { prefix } => { - let kv = Kv::new(path)?; - let cursor = kv.with_prefix(&prefix)?; - for row in cursor { - let (k, v) = row?; - println!("{}", serde_json::to_string(&(k, v))?); - } - Ok(true) + false => { + println!("false"); + Ok(false) } }, - Command::Log { cmd, path } => match cmd { - LogCommand::Get { index } => { - let index = match index { - cli::LogIndex::DateRange(..) => todo!(), - cli::LogIndex::DateScalar(..) => todo!(), - cli::LogIndex::NumberRange(..) => todo!(), - cli::LogIndex::NumberScalar(index) => index, - }; - let Some(line) = Log::new(&path)?.get_by_index(index)? else { - eprintln!("{path:?} has no element at index {index}"); - return Ok(false); - }; - println!("{}", serde_json::to_string(&line)?); + KvCommand::Get { key } => match Kv::new(path)?.get(key)? { + Some(value) => { + println!("{value}"); Ok(true) } - LogCommand::Iter => { - let log = Log::new(path)?; - let cursor = log.iter()?; - for row in cursor { - let line = row?; - println!("{}", serde_json::to_string(&line)?); - } - Ok(true) - } - LogCommand::Push { content } => { - let content = content_or_stdin(content.as_deref())?; - let index = Log::new(path)?.push(content)?; - println!("{index}"); - Ok(true) + None => { + eprintln!("Key not in KV file"); + Ok(false) } }, + KvCommand::Insert { key, value } => { + Kv::new(path)?.insert(key, value)?; + Ok(true) + } + KvCommand::WithPrefix { prefix } => { + let kv = Kv::new(path)?; + let cursor = kv.with_prefix(&prefix)?; + for row in cursor { + let (k, v) = row?; + println!("{}", serde_json::to_string(&(k, v))?); + } + Ok(true) + } + } +} + +fn run_log_command(path: Utf8PathBuf, cmd: LogCommand) -> Result { + match cmd { + LogCommand::Get { index } => { + let index = match index { + cli::LogIndex::DateRange(..) => todo!(), + cli::LogIndex::DateScalar(..) => todo!(), + cli::LogIndex::NumberRange(..) => todo!(), + cli::LogIndex::NumberScalar(index) => index, + }; + let Some(line) = Log::new(&path)?.get_by_index(index)? else { + eprintln!("{path:?} has no element at index {index}"); + return Ok(false); + }; + println!("{}", serde_json::to_string(&line)?); + Ok(true) + } + LogCommand::Iter => { + let log = Log::new(path)?; + let cursor = log.iter()?; + for row in cursor { + let line = row?; + println!("{}", serde_json::to_string(&line)?); + } + Ok(true) + } + LogCommand::Push { content } => { + let content = content_or_stdin(content.as_deref())?; + let index = Log::new(path)?.push(content)?; + println!("{index}"); + Ok(true) + } + } +} + +fn inner_main() -> Result { + let cli = cli::Cli::try_parse()?; + match cli.cmd { + Command::Kv { cmd, path } => run_kv_command(path, cmd), + Command::Log { cmd, path } => run_log_command(path, cmd), + Command::Paste => { + let output = std::process::Command::new("wl-paste") + .output() + .context("wl-paste failed")?; + let content = str::from_utf8(output.stdout.as_slice())?; + let (log, path) = Log::open_default().context("Log::open_default() failed")?; + let index = log.push(content)?; + let output = json!({ "content": content, "index": index, "path": path }); + println!("{}", serde_json::to_string(&output)?); + Ok(true) + } Command::Push { content } => { let content = content_or_stdin(content.as_deref())?; - let dirs = directories::ProjectDirs::from("", "ReactorScram", "sql-hummus") - .context("Couldn't create directories::ProjectDirs")?; - let dir = dirs.data_local_dir(); - std::fs::create_dir_all(dir)?; - let path = dir.join("default-log.db"); - let index = Log::new(&path)?.push(content)?; + let (log, path) = Log::open_default().context("Log::open_default() failed")?; + let index = log.push(content)?; let output = json!({ "index": index, "path": path }); println!("{}", serde_json::to_string(&output)?); Ok(true)