ZShell settings add

This commit is contained in:
2026-05-12 19:51:47 +02:00
parent 2903819626
commit 075cd42064
5 changed files with 27 additions and 175 deletions
Generated
+20 -76
View File
@@ -260,12 +260,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "exr" name = "exr"
version = "1.74.0" version = "1.74.0"
@@ -345,12 +339,6 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.10" version = "0.25.10"
@@ -391,16 +379,6 @@ version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "interpolate_name" name = "interpolate_name"
version = "0.2.4" version = "0.2.4"
@@ -421,6 +399,12 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.34" version = "0.1.34"
@@ -813,8 +797,8 @@ dependencies = [
"anyhow", "anyhow",
"image", "image",
"serde", "serde",
"serde_json",
"tiny-skia", "tiny-skia",
"toml",
] ]
[[package]] [[package]]
@@ -854,12 +838,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "serde_spanned" name = "serde_json"
version = "0.6.9" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"itoa",
"memchr",
"serde", "serde",
"serde_core",
"zmij",
] ]
[[package]] [[package]]
@@ -972,47 +960,6 @@ dependencies = [
"strict-num", "strict-num",
] ]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
@@ -1090,15 +1037,6 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.57.1" version = "0.57.1"
@@ -1131,6 +1069,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]] [[package]]
name = "zune-core" name = "zune-core"
version = "0.5.1" version = "0.5.1"
+1 -1
View File
@@ -16,10 +16,10 @@ tiny-skia = "0.11"
# Config serialization # Config serialization
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
toml = "0.8"
# Error handling # Error handling
anyhow = "1" anyhow = "1"
serde_json = "1.0.149"
# ── Build profiles ──────────────────────────────────────────────────────────── # ── Build profiles ────────────────────────────────────────────────────────────
+6 -50
View File
@@ -2,35 +2,20 @@ use anyhow::{Context, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
/// Top-level application configuration.
/// Serialized to/from ~/.config/rs-pictures/config.toml
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config { pub struct Config {
/// Visual effects applied to images. #[serde(rename = "screenshot")]
pub effects: EffectsConfig, pub effects: EffectsConfig,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectsConfig { pub struct EffectsConfig {
/// Apply rounded corners to the image.
pub rounded_corners: bool, pub rounded_corners: bool,
/// Radius in pixels for rounded corners.
pub corner_radius: f32, pub corner_radius: f32,
/// Apply a drop shadow beneath the image.
pub drop_shadow: bool, pub drop_shadow: bool,
/// Blur radius for the drop shadow (higher = softer shadow).
pub shadow_blur_radius: f32, pub shadow_blur_radius: f32,
/// Horizontal offset of the shadow in pixels.
pub shadow_offset_x: f32, pub shadow_offset_x: f32,
/// Vertical offset of the shadow in pixels.
pub shadow_offset_y: f32, pub shadow_offset_y: f32,
/// Shadow color as [R, G, B, A] in 0..=255.
pub shadow_color: [u8; 4], pub shadow_color: [u8; 4],
} }
@@ -57,52 +42,23 @@ impl Default for Config {
} }
impl Config { impl Config {
/// Returns the path to the config file: ~/.config/rs-pictures/config.toml
pub fn config_path() -> Option<PathBuf> { pub fn config_path() -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?; let home = std::env::var("HOME").ok()?;
Some( Some(
PathBuf::from(home) PathBuf::from(home)
.join(".config") .join(".config")
.join("rs-pictures") .join("zshell")
.join("config.toml"), .join("config.json"),
) )
} }
/// Load config from disk, or write and return the default config if the file doesn't exist.
pub fn load() -> Result<Self> { pub fn load() -> Result<Self> {
let path = match Self::config_path() { let path = Self::config_path().context("Could not determine HOME directory")?;
Some(p) => p,
None => return Ok(Self::default()),
};
if !path.exists() {
let config = Self::default();
config.save()?;
return Ok(config);
}
let raw = std::fs::read_to_string(&path) let raw = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read config at {}", path.display()))?; .with_context(|| format!("Failed to read config at {}", path.display()))?;
toml::from_str(&raw) serde_json::from_str(&raw)
.with_context(|| format!("Failed to parse config at {}", path.display())) .with_context(|| format!("Failed to parse JSON config at {}", path.display()))
}
/// Persist the current config to disk.
pub fn save(&self) -> Result<()> {
let path =
Self::config_path().context("Could not determine config directory (HOME not set)")?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config dir {}", parent.display()))?;
}
let serialized = toml::to_string_pretty(self).context("Failed to serialize config")?;
std::fs::write(&path, serialized)
.with_context(|| format!("Failed to write config to {}", path.display()))?;
Ok(())
} }
} }
-40
View File
@@ -1,26 +1,9 @@
//! Post-capture image effects: rounded corners and drop shadow.
//!
//! Pipeline:
//! RgbaImage (captured)
//! → apply_rounded_corners() clips corners to transparent via tiny-skia mask
//! → apply_drop_shadow() composites a blurred shadow beneath the image
//! → final RgbaImage (may be larger when shadow is added)
//!
//! Performance notes:
//! - Box blur uses a sliding-window algorithm: O(W*H) regardless of radius.
//! Three passes of the box filter approximate a Gaussian.
//! - Pixel format conversions between RgbaImage and tiny-skia Pixmap are done
//! with a single pass each way.
//! - This module is called from a background thread in review.rs so the UI
//! never blocks.
use crate::config::EffectsConfig; use crate::config::EffectsConfig;
use image::RgbaImage; use image::RgbaImage;
use tiny_skia::{ use tiny_skia::{
BlendMode, Color, FillRule, Paint, Path, PathBuilder, Pixmap, PixmapPaint, Transform, BlendMode, Color, FillRule, Paint, Path, PathBuilder, Pixmap, PixmapPaint, Transform,
}; };
/// Apply all configured effects in order. Returns a new image.
pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage { pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage {
let img = if cfg.rounded_corners { let img = if cfg.rounded_corners {
apply_rounded_corners(img, cfg.corner_radius) apply_rounded_corners(img, cfg.corner_radius)
@@ -40,8 +23,6 @@ pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage {
} }
} }
// ─── Rounded corners ─────────────────────────────────────────────────────────
pub fn apply_rounded_corners(img: RgbaImage, radius: f32) -> RgbaImage { pub fn apply_rounded_corners(img: RgbaImage, radius: f32) -> RgbaImage {
let (w, h) = img.dimensions(); let (w, h) = img.dimensions();
let mut mask = Pixmap::new(w, h).expect("mask pixmap"); let mut mask = Pixmap::new(w, h).expect("mask pixmap");
@@ -64,8 +45,6 @@ pub fn apply_rounded_corners(img: RgbaImage, radius: f32) -> RgbaImage {
pixmap_to_rgba_image(pixmap) pixmap_to_rgba_image(pixmap)
} }
// ─── Drop shadow ─────────────────────────────────────────────────────────────
pub fn apply_drop_shadow( pub fn apply_drop_shadow(
img: RgbaImage, img: RgbaImage,
blur_radius: f32, blur_radius: f32,
@@ -84,7 +63,6 @@ pub fn apply_drop_shadow(
let canvas_w = iw + extra_left + extra_right; let canvas_w = iw + extra_left + extra_right;
let canvas_h = ih + extra_top + extra_bottom; let canvas_h = ih + extra_top + extra_bottom;
// 1. Place the image silhouette at the shadow position.
let mut shadow_pixmap = Pixmap::new(canvas_w, canvas_h).expect("shadow pixmap"); let mut shadow_pixmap = Pixmap::new(canvas_w, canvas_h).expect("shadow pixmap");
let img_pixmap = rgba_image_to_pixmap(&img); let img_pixmap = rgba_image_to_pixmap(&img);
let shadow_x = (extra_left as f32 + offset_x) as i32; let shadow_x = (extra_left as f32 + offset_x) as i32;
@@ -101,15 +79,12 @@ pub fn apply_drop_shadow(
None, None,
); );
// 2. Tint the silhouette with the shadow colour.
tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color); tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color);
// 3. Blur the shadow (sliding-window box blur, 3 passes).
let shadow_img = pixmap_to_rgba_image(shadow_pixmap); let shadow_img = pixmap_to_rgba_image(shadow_pixmap);
let blurred = box_blur_rgba(&shadow_img, br); let blurred = box_blur_rgba(&shadow_img, br);
let blurred_pixmap = rgba_image_to_pixmap(&blurred); let blurred_pixmap = rgba_image_to_pixmap(&blurred);
// 4. Composite: shadow first, image on top.
let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap"); let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap");
let mut p = PixmapPaint::default(); let mut p = PixmapPaint::default();
p.blend_mode = BlendMode::Source; p.blend_mode = BlendMode::Source;
@@ -136,8 +111,6 @@ pub fn apply_drop_shadow(
pixmap_to_rgba_image(canvas) pixmap_to_rgba_image(canvas)
} }
// ─── Helpers ─────────────────────────────────────────────────────────────────
fn rounded_rect_path(x: f32, y: f32, w: f32, h: f32, r: f32) -> Path { fn rounded_rect_path(x: f32, y: f32, w: f32, h: f32, r: f32) -> Path {
let r = r.min(w / 2.0).min(h / 2.0); let r = r.min(w / 2.0).min(h / 2.0);
let mut pb = PathBuilder::new(); let mut pb = PathBuilder::new();
@@ -211,18 +184,10 @@ fn tint_pixmap_as_shadow(pixmap: &mut Pixmap, color: [u8; 4]) {
} }
} }
// ─── Sliding-window box blur (O(W*H) regardless of radius) ───────────────────
//
// Classic algorithm: maintain a running sum over a window of (2r+1) pixels.
// When the window slides by one pixel, subtract the pixel leaving the window
// and add the pixel entering it. Three passes (H→V→H or H→V→H) approximate
// a Gaussian kernel.
fn box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage { fn box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage {
if radius == 0 { if radius == 0 {
return img.clone(); return img.clone();
} }
// Three passes of H+V to approximate a Gaussian.
let mut buf = sliding_horizontal(img, radius); let mut buf = sliding_horizontal(img, radius);
buf = sliding_vertical(&buf, radius); buf = sliding_vertical(&buf, radius);
buf = sliding_horizontal(&buf, radius); buf = sliding_horizontal(&buf, radius);
@@ -230,7 +195,6 @@ fn box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage {
buf buf
} }
/// Horizontal sliding-window box blur, single pass.
fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage { fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage {
let (w, h) = img.dimensions(); let (w, h) = img.dimensions();
let r = radius as i32; let r = radius as i32;
@@ -238,13 +202,11 @@ fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage {
let mut out = RgbaImage::new(w, h); let mut out = RgbaImage::new(w, h);
for y in 0..h { for y in 0..h {
// Accumulator for the current window.
let mut sr = 0u32; let mut sr = 0u32;
let mut sg = 0u32; let mut sg = 0u32;
let mut sb = 0u32; let mut sb = 0u32;
let mut sa = 0u32; let mut sa = 0u32;
// Seed the window around x=0.
for dx in -r..=r { for dx in -r..=r {
let sx = dx.clamp(0, w as i32 - 1) as u32; let sx = dx.clamp(0, w as i32 - 1) as u32;
let p = img.get_pixel(sx, y).0; let p = img.get_pixel(sx, y).0;
@@ -266,7 +228,6 @@ fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage {
]), ]),
); );
// Slide: remove left edge, add right edge.
let remove_x = (x as i32 - r).clamp(0, w as i32 - 1) as u32; let remove_x = (x as i32 - r).clamp(0, w as i32 - 1) as u32;
let add_x = (x as i32 + r + 1).clamp(0, w as i32 - 1) as u32; let add_x = (x as i32 + r + 1).clamp(0, w as i32 - 1) as u32;
let rp = img.get_pixel(remove_x, y).0; let rp = img.get_pixel(remove_x, y).0;
@@ -280,7 +241,6 @@ fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage {
out out
} }
/// Vertical sliding-window box blur, single pass.
fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage { fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage {
let (w, h) = img.dimensions(); let (w, h) = img.dimensions();
let r = radius as i32; let r = radius as i32;
-8
View File
@@ -6,7 +6,6 @@ use std::io::Write as _;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
fn main() -> Result<()> { fn main() -> Result<()> {
// ── 1. Collect image paths from CLI arguments ─────────────────────────────
let paths: Vec<String> = std::env::args().skip(1).collect(); let paths: Vec<String> = std::env::args().skip(1).collect();
if paths.is_empty() { if paths.is_empty() {
@@ -15,10 +14,8 @@ fn main() -> Result<()> {
std::process::exit(1); std::process::exit(1);
} }
// ── 2. Load config (generates default if missing) ─────────────────────────
let config = config::Config::load().context("Failed to load config")?; let config = config::Config::load().context("Failed to load config")?;
// ── 3. Process each image ─────────────────────────────────────────────────
for path in &paths { for path in &paths {
if let Err(e) = process_image(path, &config) { if let Err(e) = process_image(path, &config) {
eprintln!("Error processing '{}': {e:#}", path); eprintln!("Error processing '{}': {e:#}", path);
@@ -29,15 +26,12 @@ fn main() -> Result<()> {
} }
fn process_image(path: &str, config: &config::Config) -> Result<()> { fn process_image(path: &str, config: &config::Config) -> Result<()> {
// ── a. Load image ─────────────────────────────────────────────────────────
let img = image::open(path) let img = image::open(path)
.with_context(|| format!("Failed to open image '{path}'"))? .with_context(|| format!("Failed to open image '{path}'"))?
.into_rgba8(); .into_rgba8();
// ── b. Apply effects (rounded corners + drop shadow) ─────────────────────
let processed = effects::apply_effects(img, &config.effects); let processed = effects::apply_effects(img, &config.effects);
// ── c. Encode as PNG into memory ──────────────────────────────────────────
let mut png_bytes: Vec<u8> = Vec::new(); let mut png_bytes: Vec<u8> = Vec::new();
image::DynamicImage::ImageRgba8(processed) image::DynamicImage::ImageRgba8(processed)
.write_to( .write_to(
@@ -45,9 +39,7 @@ fn process_image(path: &str, config: &config::Config) -> Result<()> {
image::ImageFormat::Png, image::ImageFormat::Png,
) )
.context("Failed to encode processed image as PNG")?; .context("Failed to encode processed image as PNG")?;
// Re-unwrap into RgbaImage — not needed further, png_bytes is ready
// ── d. Pipe PNG bytes to `swappy -f -` ────────────────────────────────────
let mut child = Command::new("swappy") let mut child = Command::new("swappy")
.args(["-f", "-"]) .args(["-f", "-"])
.stdin(Stdio::piped()) .stdin(Stdio::piped())