diff --git a/Cargo.lock b/Cargo.lock index e4f80276..80bfbf80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,7 +271,7 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "common" -version = "0.11.2-master" +version = "0.11.2-master2" dependencies = [ "fastrand", "pkg-config", @@ -1093,7 +1093,7 @@ dependencies = [ [[package]] name = "swww" -version = "0.11.2-master" +version = "0.11.2-master2" dependencies = [ "clap", "clap_complete", @@ -1106,7 +1106,7 @@ dependencies = [ [[package]] name = "swww-daemon" -version = "0.11.2-master" +version = "0.11.2-master2" dependencies = [ "common", "keyframe", @@ -1166,7 +1166,7 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "tests" -version = "0.11.2-master" +version = "0.11.2-master2" dependencies = [ "assert_cmd", "image", diff --git a/Cargo.toml b/Cargo.toml index 46f50228..63d5cee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["client", "daemon", "common", "tests"] default-members = ["client", "daemon"] [workspace.package] -version = "0.11.2-master" +version = "0.11.2-master2" authors = ["Leonardo Gibrowski FaƩ "] edition = "2024" license-file = "LICENSE" diff --git a/client/src/cli.rs b/client/src/cli.rs index 8c0a44e5..e5da9fb3 100644 --- a/client/src/cli.rs +++ b/client/src/cli.rs @@ -1,7 +1,6 @@ /// Note: this file only has basic declarations and some definitions in order to be possible to /// import it in the build script, to automate shell completion use clap::{Parser, ValueEnum}; -use std::fmt::Display; use std::path::PathBuf; fn from_hex(hex: &str) -> Result<[u8; 4], String> { @@ -41,7 +40,7 @@ pub enum PixelFormat { Argb, } -#[derive(Clone)] +#[derive(Clone, Copy)] pub enum Filter { Nearest, Bilinear, @@ -50,6 +49,18 @@ pub enum Filter { Lanczos3, } +impl Filter { + pub fn as_str(&self) -> &'static str { + match self { + Self::Nearest => "Nearest", + Self::Bilinear => "Bilinear", + Self::CatmullRom => "CatmullRom", + Self::Mitchell => "Mitchell", + Self::Lanczos3 => "Lanczos3", + } + } +} + impl std::str::FromStr for Filter { type Err = &'static str; @@ -60,26 +71,13 @@ impl std::str::FromStr for Filter { "CatmullRom" => Ok(Self::CatmullRom), "Mitchell" => Ok(Self::Mitchell), "Lanczos3" => Ok(Self::Lanczos3), - _ => Err("unrecognized filter. Valid filters are:\ - Nearest | Bilinear | CatmullRom | Mitchell | Lanczos3\ + _ => Err("unrecognized filter. Valid filters are:\n\ + \tNearest | Bilinear | CatmullRom | Mitchell | Lanczos3\n\ see swww img --help for more details"), } } } -impl Display for Filter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let str = match self { - Self::Nearest => "Nearest", - Self::Bilinear => "Bilinear", - Self::CatmullRom => "CatmullRom", - Self::Mitchell => "Mitchell", - Self::Lanczos3 => "Lanczos3", - }; - write!(f, "{}", str) - } -} - #[derive(Clone)] pub enum TransitionType { None, @@ -294,6 +292,35 @@ pub enum ResizeStrategy { Stretch, } +impl ResizeStrategy { + pub fn as_str(&self) -> &'static str { + match self { + ResizeStrategy::No => "no", + ResizeStrategy::Crop => "crop", + ResizeStrategy::Fit => "fit", + ResizeStrategy::Stretch => "stretch", + } + } +} + +impl std::str::FromStr for ResizeStrategy { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "no" => Ok(Self::No), + "crop" => Ok(Self::Crop), + "fit" => Ok(Self::Fit), + "stretch" => Ok(Self::Stretch), + _ => Err( + "unrecognized resize strategy. Valid resize strategies are:\n\ + no | crop | fit | stretch\n\ + see swww img --help for more details", + ), + } + } +} + #[derive(Parser)] pub struct Restore { /// Restore all swww-daemon instances (all namespaces) diff --git a/client/src/main.rs b/client/src/main.rs index b2cb5c6b..9f56a37d 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -144,6 +144,9 @@ fn make_img_request( let transition = make_transition(img); let mut img_req_builder = ipc::ImageRequestBuilder::new(transition); + let filter = img.filter.as_str(); + let resize = img.resize.as_str(); + match &img.image { CliImage::Color(color) => { for (&dim, outputs) in dims.iter().zip(outputs) { @@ -159,8 +162,9 @@ fn make_img_request( dim, format: pixel_format, }, - namespace.to_string(), - Filter::Lanczos3.to_string(), + namespace, + resize, + filter, outputs, None, ); @@ -186,14 +190,18 @@ fn make_img_request( let animation = if !imgbuf.is_animated() { None - } else if img.resize == ResizeStrategy::Crop { - match cache::load_animation_frames(path.as_ref(), dim, pixel_format) { + } else { + match cache::load_animation_frames( + path.as_ref(), + dim, + resize, + pixel_format, + ) { Ok(Some(animation)) => Some(animation), otherwise => { if let Err(e) = otherwise { eprintln!("Error loading cache for {:?}: {e}", img_path); } - Some({ ipc::Animation { animation: compress_frames( @@ -209,11 +217,8 @@ fn make_img_request( }) } } - } else { - None }; - let filter = img.filter.to_string(); let img = match img.resize { ResizeStrategy::No => img_pad(&img_raw, dim, &img.fill_color)?, ResizeStrategy::Crop => { @@ -237,7 +242,8 @@ fn make_img_request( dim, format: pixel_format, }, - namespace.to_string(), + namespace, + resize, filter, outputs, animation, @@ -258,7 +264,7 @@ fn make_img_request( } } }; - let filter = img.filter.to_string(); + let filter = img.filter.as_str(); let img_raw = imgbuf.decode(pixel_format, dim.0, dim.1)?; let img = match img.resize { ResizeStrategy::No => img_pad(&img_raw, dim, &img.fill_color)?, @@ -282,7 +288,8 @@ fn make_img_request( dim, format: pixel_format, }, - namespace.to_string(), + namespace, + resize, filter, outputs, None, @@ -365,23 +372,25 @@ fn restore_from_cache(requested_outputs: &[String], namespace: &str) -> Result<( } fn restore_output(output: &str, namespace: &str) -> Result<(), String> { - let (filter, img_path) = common::cache::get_previous_image_filter_and_path(output, namespace) - .map_err(|e| format!("failed to get previous image path: {e}"))?; - if img_path.is_empty() { - return Err("cache file does not exist".to_string()); - } + let cache_data = common::cache::read_cache_file(output) + .map_err(|e| format!("failed to read cache file: {e}"))?; + let cache = match common::cache::get_previous_image_cache(output, namespace, &cache_data) { + Ok(Some(cache)) => cache, + Ok(None) => return Err("cache entry does not exist".to_string()), + Err(e) => return Err(e.to_string()), + }; process_swww_args( &Swww::Img(cli::Img { all: false, - image: cli::parse_image(&img_path)?, + image: cli::parse_image(cache.img_path)?, outputs: output.to_string(), namespace: vec![namespace.to_string()], #[allow(deprecated)] no_resize: false, - resize: ResizeStrategy::Crop, + resize: ResizeStrategy::from_str(cache.resize).unwrap_or(ResizeStrategy::Crop), fill_color: [0, 0, 0, 255], - filter: Filter::from_str(&filter).unwrap_or(Filter::Lanczos3), + filter: Filter::from_str(cache.filter).unwrap_or(Filter::Lanczos3), transition_type: cli::TransitionType::None, transition_step: std::num::NonZeroU8::MAX, transition_duration: 0.0, diff --git a/common/src/cache.rs b/common/src/cache.rs index a7e54f8a..68ff2a9c 100644 --- a/common/src/cache.rs +++ b/common/src/cache.rs @@ -14,16 +14,25 @@ use crate::ipc::Animation; use crate::ipc::PixelFormat; use crate::mmap::Mmap; +const CACHE_DIRNAME: &str = env!("CARGO_PKG_VERSION"); + pub struct CacheEntry<'a> { - namespace: &'a str, - filter: &'a str, - img_path: &'a str, + pub namespace: &'a str, + pub resize: &'a str, + pub filter: &'a str, + pub img_path: &'a str, } impl<'a> CacheEntry<'a> { - pub(crate) fn new(namespace: &'a str, filter: &'a str, img_path: &'a str) -> Self { + pub(crate) fn new( + namespace: &'a str, + resize: &'a str, + filter: &'a str, + img_path: &'a str, + ) -> Self { Self { namespace, + resize, filter, img_path, } @@ -35,10 +44,16 @@ impl<'a> CacheEntry<'a> { let mut v = Vec::new(); let mut strings = data.split(|ch| *ch == 0); while let Some(namespace) = strings.next() { - let filter = match strings.next() { - Some(s) => s, - None => break, - }; + let resize = strings.next().ok_or_else(|| { + Error::other(format!( + "cache file for output {output_name} is in the wrong format (no resize)" + )) + })?; + let filter = strings.next().ok_or_else(|| { + Error::other(format!( + "cache file for output {output_name} is in the wrong format (no filter)" + )) + })?; let img_path = strings.next().ok_or_else(|| { Error::other(format!( "cache file for output {output_name} is in the wrong format (no image path)" @@ -47,11 +62,13 @@ impl<'a> CacheEntry<'a> { let err = format!("cache file for output {output_name} is not valid utf8"); let namespace = str::from_utf8(namespace).map_err(|_| Error::other(err.clone()))?; + let resize = str::from_utf8(resize).map_err(|_| Error::other(err.clone()))?; let filter = str::from_utf8(filter).map_err(|_| Error::other(err.clone()))?; let img_path = str::from_utf8(img_path).map_err(|_| Error::other(err))?; v.push(CacheEntry { namespace, + resize, filter, img_path, }) @@ -75,12 +92,13 @@ impl<'a> CacheEntry<'a> { let mut data = Vec::new(); file.read_to_end(&mut data)?; - let mut entries = Self::parse_file(output_name, &data)?; + let mut entries = Self::parse_file(output_name, &data).unwrap_or_else(|_| Vec::new()); if let Some(entry) = entries .iter_mut() .find(|elem| elem.namespace == self.namespace) { + entry.resize = self.resize; entry.filter = self.filter; entry.img_path = self.img_path; } else { @@ -91,10 +109,11 @@ impl<'a> CacheEntry<'a> { for entry in entries { let CacheEntry { namespace, + resize, filter, img_path, } = entry; - file.write_all(format!("{namespace}\0{filter}\0{img_path}\0").as_bytes())?; + file.write_all(format!("{namespace}\0{resize}\0{filter}\0{img_path}").as_bytes())?; } let len = file.stream_position().unwrap_or(0); @@ -107,9 +126,10 @@ pub(crate) fn store_animation_frames( animation: &[u8], path: &Path, dimensions: (u32, u32), + resize: &str, pixel_format: PixelFormat, ) -> io::Result<()> { - let filename = animation_filename(path, dimensions, pixel_format); + let filename = animation_filename(path, dimensions, resize, pixel_format); let mut filepath = cache_dir()?; filepath.push(&filename); @@ -123,9 +143,10 @@ pub(crate) fn store_animation_frames( pub fn load_animation_frames( path: &Path, dimensions: (u32, u32), + resize: &str, pixel_format: PixelFormat, ) -> io::Result> { - let filename = animation_filename(path, dimensions, pixel_format); + let filename = animation_filename(path, dimensions, resize, pixel_format); let cache_dir = cache_dir()?; let mut filepath = cache_dir.clone(); filepath.push(filename); @@ -147,30 +168,34 @@ pub fn load_animation_frames( Ok(None) } -pub fn get_previous_image_filter_and_path( - output_name: &str, - namespace: &str, -) -> io::Result<(String, String)> { +pub fn read_cache_file(output_name: &str) -> io::Result> { + clean_previous_versions(); + let mut filepath = cache_dir()?; - clean_previous_versions(&filepath); filepath.push(output_name); + std::fs::read(filepath) +} - let data = std::fs::read(filepath)?; - let entries = CacheEntry::parse_file(output_name, &data)?; +pub fn get_previous_image_cache<'a>( + output_name: &str, + namespace: &str, + cache_data: &'a [u8], +) -> io::Result>> { + let entries = CacheEntry::parse_file(output_name, cache_data)?; - match entries.iter().find(|entry| entry.namespace == namespace) { - Some(entry) => Ok((entry.filter.to_string(), entry.img_path.to_string())), - None => Ok(("".to_string(), "".to_string())), - } + Ok(entries + .into_iter() + .find(|entry| entry.namespace == namespace)) } pub fn load(output_name: &str, namespace: &str) -> io::Result<()> { - let (filter, img_path) = get_previous_image_filter_and_path(output_name, namespace)?; + let cache_data = read_cache_file(output_name)?; - if img_path.is_empty() { - return Ok(()); - } + let cache = match get_previous_image_cache(output_name, namespace, &cache_data)? { + Some(cache) => cache, + None => return Ok(()), + }; if let Ok(mut child) = std::process::Command::new("pidof").arg("swww").spawn() && let Ok(status) = child.wait() @@ -184,11 +209,17 @@ pub fn load(output_name: &str, namespace: &str) -> io::Result<()> { std::process::Command::new("swww") .arg("img") .args([ - &format!("--outputs={output_name}"), - &format!("--filter={filter}"), + "--outputs", + output_name, + "--resize", + cache.resize, + "--filter", + cache.filter, + // namespace needs a format because the empty namespace is valid, so we need to use the + // `=` format &format!("--namespace={namespace}"), "--transition-type=none", - &img_path, + cache.img_path, ]) .spawn()? .wait()?; @@ -196,39 +227,39 @@ pub fn load(output_name: &str, namespace: &str) -> io::Result<()> { } pub fn clean() -> io::Result<()> { + clean_previous_versions(); std::fs::remove_dir_all(cache_dir()?) } -fn clean_previous_versions(cache_dir: &Path) { - let mut read_dir = match std::fs::read_dir(cache_dir) { +fn clean_previous_versions() { + let user_cache = match user_cache_dir() { + Ok(path) => path, + Err(e) => { + eprintln!("WARNING: failed to get user cache dir {e}"); + return; + } + }; + + let mut read_dir = match std::fs::read_dir(&user_cache) { Ok(read_dir) => read_dir, Err(_) => { - eprintln!("WARNING: failed to read cache dir {:?} entries", cache_dir); + eprintln!("WARNING: failed to read cache dir {user_cache:?} entries"); return; } }; - let current_version = env!("CARGO_PKG_VERSION"); - while let Some(Ok(entry)) = read_dir.next() { - let filename = entry.file_name(); - let filename = match filename.to_str() { - Some(filename) => filename, - None => { - eprintln!("WARNING: failed to read filename of {:?}", filename); - continue; - } - }; + let entryname = entry.file_name(); + if entryname == CACHE_DIRNAME { + continue; + } - // only the images we've cached will have a _v token, indicating their version - if let Some(i) = filename.rfind("_v") - && &filename[i + 2..] != current_version - && let Err(e) = std::fs::remove_file(entry.path()) - { - eprintln!( - "WARNING: failed to remove cache file {} of old swww version {:?}", - filename, e - ); + if entry.path().is_dir() { + if let Err(e) = std::fs::remove_dir_all(entry.path()) { + eprintln!("failed to remove old cache directory {entryname:?}: {e}"); + } + } else if let Err(e) = std::fs::remove_file(entry.path()) { + eprintln!("failed to remove old cache directory {entryname:?}: {e}"); } } } @@ -241,17 +272,15 @@ fn create_dir(p: &Path) -> io::Result<()> { } } -fn cache_dir() -> io::Result { +fn user_cache_dir() -> io::Result { if let Ok(path) = std::env::var("XDG_CACHE_HOME") { let mut path: PathBuf = path.into(); path.push("swww"); - create_dir(&path)?; Ok(path) } else if let Ok(path) = std::env::var("HOME") { let mut path: PathBuf = path.into(); path.push(".cache"); path.push("swww"); - create_dir(&path)?; Ok(path) } else { Err(std::io::Error::other( @@ -260,15 +289,27 @@ fn cache_dir() -> io::Result { } } +fn cache_dir() -> io::Result { + let mut path = user_cache_dir()?; + path.push(CACHE_DIRNAME); + create_dir(&path)?; + Ok(path) +} + #[must_use] -fn animation_filename(path: &Path, dimensions: (u32, u32), pixel_format: PixelFormat) -> PathBuf { +fn animation_filename( + path: &Path, + dimensions: (u32, u32), + resize: &str, + pixel_format: PixelFormat, +) -> PathBuf { format!( - "{}__{}x{}_{:?}_v{}", + "{}__{}x{}_{}_{:?}", path.to_string_lossy().replace('/', "_"), dimensions.0, dimensions.1, + resize, pixel_format, - env!("CARGO_PKG_VERSION"), ) .into() } diff --git a/common/src/ipc/mod.rs b/common/src/ipc/mod.rs index cbf84e23..0fdcd9a3 100644 --- a/common/src/ipc/mod.rs +++ b/common/src/ipc/mod.rs @@ -62,8 +62,9 @@ impl ImageRequestBuilder { pub fn push( &mut self, img: ImgSend, - namespace: String, - filter: String, + namespace: &str, + resize: &str, + filter: &str, outputs: &[String], animation: Option, ) { @@ -96,7 +97,9 @@ impl ImageRequestBuilder { // cache the request for output in outputs.iter() { - if let Err(e) = super::cache::CacheEntry::new(&namespace, &filter, path).store(output) { + if let Err(e) = + super::cache::CacheEntry::new(namespace, resize, filter, path).store(output) + { eprintln!("ERROR: failed to store cache: {e}"); } } @@ -107,6 +110,7 @@ impl ImageRequestBuilder { &self.memory.slice()[animation_start..], &p, *dims, + resize, *format, ) { eprintln!("Error storing cache for {}: {e}", path); diff --git a/doc/swww-clear-cache.1.scd b/doc/swww-clear-cache.1.scd index 99e78d45..a6f3776c 100644 --- a/doc/swww-clear-cache.1.scd +++ b/doc/swww-clear-cache.1.scd @@ -16,15 +16,15 @@ swww-clear-cache Deletes the `swww` cache directory. The cache resides at _$XDG_CACHE_HOME/swww_ or _$HOME/.cache/swww_ -if $XDG_CACHE_HOME does not exist. For each monitor, there will be a file in -those locations corresponding to the current image/animation being displayed. - -Furthermore, the cache will keep preprocessed versions of `gif`s. So, if you -load a large `gif`, you would have to pay the price for its processing the first -time. - -Note that `swww` will automatically delete any preprocessed animation created -with a previous version of `swww` from the cache. +if $XDG_CACHE_HOME does not exist. In this directory, we create another +directory corresponding to `swww`s current version number. In there, we have +files that cache the last displayed image and animation frames for animated +wallpapers. There is one file per output. + +**Note that `swww` will automatically delete any files and directories in +_$XDG_CACHE_HOME/swww_ that do not correspond to the exact current `swww` +version number.** This is to ensure we only keep cache files for the latest +`swww` version. # SEE ALSO *swww-img*(1) diff --git a/doc/swww-img.1.scd b/doc/swww-img.1.scd index ecf1de76..65164d7e 100644 --- a/doc/swww-img.1.scd +++ b/doc/swww-img.1.scd @@ -233,16 +233,15 @@ to read from stdin instead. # ABOUT THE CACHE The images sent will be cached at _$XDG_CACHE_HOME/swww_ or _$HOME/.cache/swww_ -if $XDG_CACHE_HOME does not exist. For each monitor, there will be a file in -those locations corresponding to the current image/animation being displayed. - -The `swww-daemon` will actually wait until the first image has been set before -trying to load the cache. - -Finally, the cache will keep preprocessed versions of `gif`s. So, if you load a -large `gif`, you would have to pay the price for its processing the first time. -If you constantly load large `gif`s, this could cause the cache to get very big. -You can simply run `swww clean-cache` if this happens. +if $XDG_CACHE_HOME does not exist. In this directory, we create another +directory corresponding to `swww`s current version number. In there, we have +files that cache the last displayed image and animation frames for animated +wallpapers. There is one file per output. + +As mentioned, the cache will keep preprocessed versions of animations. So, if +you load a large `gif`, you would only have to pay the price for its processing +the first time. If you constantly load large `gif`s, this could cause the cache +to get very big. You can simply run `swww clean-cache` if this happens. # SEE ALSO *swww-clear-cache*(1) *swww-daemon*(1) *swww-query*(1)