feat: initialize K-Shrink workspace with multiple crates for image shrinking utility

- Add Cargo.toml for workspace configuration with dependencies.
- Create README.md with project description, usage, and architecture details.
- Implement `bin` crate for the main executable, including clipboard processing logic.
- Add `config` crate for handling configuration in TOML format.
- Develop `lib` crate containing core image processing logic and error handling.
- Introduce `platform` crate for platform-specific clipboard interactions, starting with Wayland.
- Implement tests for image shrinking functionality and clipboard interactions.
This commit is contained in:
2026-03-17 21:50:13 +01:00
commit 271d55ba57
18 changed files with 2788 additions and 0 deletions

47
crates/bin/src/lib.rs Normal file
View File

@@ -0,0 +1,47 @@
use lib::{image_hash, shrink, ClipboardError, ClipboardProvider, ShrinkOptions};
use tracing::{debug, info, trace};
pub fn process_once(
provider: &dyn ClipboardProvider,
opts: &ShrinkOptions,
last_hash: &mut Option<[u8; 32]>,
) -> Result<(), Box<dyn std::error::Error>> {
let (data, mime_type) = match provider.capture() {
Ok(x) => x,
Err(ClipboardError::Empty) => {
trace!("clipboard empty");
return Ok(());
}
Err(ClipboardError::InvalidType(t)) => {
trace!("no image in clipboard: {t}");
return Ok(());
}
Err(e) => return Err(e.into()),
};
if !mime_type.starts_with("image/") {
trace!("non-image mime type: {mime_type}");
return Ok(());
}
let incoming_hash = image_hash(&data);
if Some(incoming_hash) == *last_hash {
debug!("clipboard unchanged, skipping");
return Ok(());
}
info!("compressing {} bytes ({mime_type})", data.len());
let result = shrink(&data, opts)?;
info!(
"compressed to {} bytes ({})",
result.data.len(),
result.mime_type
);
provider.distribute(&[(&result.data, result.mime_type.as_str())])?;
// Track hash of OUTPUT. After distributing webp-only, next capture will
// find image/webp (from our subprocess) → same hash → skip. No loop.
*last_hash = Some(image_hash(&result.data));
Ok(())
}

35
crates/bin/src/main.rs Normal file
View File

@@ -0,0 +1,35 @@
use config::load_config;
use lib::ShrinkOptions;
use platform::WaylandBackend;
use std::time::Duration;
use tracing::{error, info, warn};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let cfg = match load_config() {
Ok(c) => c,
Err(e) => {
warn!("config error ({e}), using defaults");
config::Config::default()
}
};
let poll = Duration::from_millis(cfg.general.poll_ms);
let opts = ShrinkOptions {
quality: cfg.general.quality,
target_format: cfg.general.format.into(),
};
let backend = WaylandBackend;
let mut last_hash: Option<[u8; 32]> = None;
info!("k-shrink daemon started (poll={}ms)", cfg.general.poll_ms);
loop {
if let Err(e) = k_shrink::process_once(&backend, &opts, &mut last_hash) {
error!("error: {e}");
}
tokio::time::sleep(poll).await;
}
}