From e3598ebf2eb6d242abedca606449ce63b90658d8 Mon Sep 17 00:00:00 2001 From: imgurbot12 Date: Thu, 14 Dec 2023 15:49:01 -0700 Subject: [PATCH] feat: icon support --- rmenu/Cargo.toml | 4 ++ rmenu/src/config.rs | 2 +- rmenu/src/gui.rs | 19 ++++-- rmenu/src/icons.rs | 116 +++++++++++++++++++++++++++++++++++ rmenu/src/main.rs | 10 +++ rmenu/templates/results.html | 16 ++++- 6 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 rmenu/src/icons.rs diff --git a/rmenu/Cargo.toml b/rmenu/Cargo.toml index 69b3023..92028cc 100644 --- a/rmenu/Cargo.toml +++ b/rmenu/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] askama = "0.12.1" +base64 = "0.21.5" clap = { version = "4.4.11", features = ["derive"] } env_logger = "0.10.1" heck = "0.4.1" @@ -14,7 +15,10 @@ keyboard-types = "0.7.0" lastlog = { version = "0.2.3", features = ["libc"] } log = "0.4.20" once_cell = "1.19.0" +png = "0.17.10" +rayon = "1.8.0" regex = "1.10.2" +resvg = "0.36.0" rmenu-plugin = { version = "0.0.1", path = "../rmenu-plugin" } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" diff --git a/rmenu/src/config.rs b/rmenu/src/config.rs index 1501fcf..806f98e 100644 --- a/rmenu/src/config.rs +++ b/rmenu/src/config.rs @@ -252,7 +252,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - page_size: 200, + page_size: 50, page_load: 0.8, jump_dist: 5, use_icons: true, diff --git a/rmenu/src/gui.rs b/rmenu/src/gui.rs index d4c782f..f6bf111 100644 --- a/rmenu/src/gui.rs +++ b/rmenu/src/gui.rs @@ -9,6 +9,7 @@ use web_view::*; use crate::config::{Config, Keybind}; use crate::exec::execute; +use crate::icons::IconCache; use crate::search::build_searchfn; use crate::AppData; @@ -77,6 +78,7 @@ struct ResultsTemplate<'a> { end: usize, results: &'a Vec<&'a Entry>, config: &'a Config, + cache: &'a IconCache, } #[derive(Debug)] @@ -87,6 +89,7 @@ struct AppState<'a> { search: String, results: Vec<&'a Entry>, data: &'a AppData, + icons: IconCache, } /// check if the current inputs match any of the given keybindings @@ -103,23 +106,30 @@ impl<'a> AppState<'a> { page: 0, search: "".to_owned(), results: vec![], + icons: IconCache::new().unwrap(), data, } } /// Render Current Page of Results - fn render_results_page(&self) -> String { + fn render_results_page(&mut self) -> String { let size = self.data.config.page_size; let start = self.page * size; let max = (self.page + 1) * size; - let nresults = std::cmp::max(self.results.len(), 1); - let end = std::cmp::min(max, nresults - 1); + let min = std::cmp::min(max, self.results.len()); + let end = std::cmp::max(min, 1) - 1; + self.icons.prepare(&self.results[..]); + // skip generation if results are empty + if self.results.is_empty() { + return "".to_owned(); + } // generate results html from template let template = ResultsTemplate { start, end, config: &self.data.config, results: &self.results, + cache: &self.icons, }; template.render().unwrap() } @@ -172,7 +182,8 @@ impl<'a> AppState<'a> { #[inline] fn move_down(&mut self, down: usize) -> Option { let max = (self.page + 1) * self.data.config.page_size; - let end = std::cmp::min(max, self.results.len()) - 1; + let n = std::cmp::max(self.results.len(), 1); + let end = std::cmp::min(max, n) - 1; self.pos = std::cmp::min(self.pos + down, end); match self.append_results(false) { Some(operation) => Some(operation), diff --git a/rmenu/src/icons.rs b/rmenu/src/icons.rs new file mode 100644 index 0000000..1bf21c9 --- /dev/null +++ b/rmenu/src/icons.rs @@ -0,0 +1,116 @@ +//! GUI Image Processing +use std::collections::HashMap; +use std::fs::{create_dir_all, write}; +use std::path::PathBuf; + +use base64::{engine::general_purpose, Engine as _}; +use rayon::prelude::*; +use resvg::usvg::TreeParsing; +use rmenu_plugin::Entry; +use thiserror::Error; + +static TEMP_DIR: &'static str = "/tmp/rmenu"; + +#[derive(Debug, Error)] +pub enum SvgError { + #[error("Invalid SVG Filepath")] + InvalidFile(#[from] std::io::Error), + #[error("Invalid Document")] + InvalidTree(#[from] resvg::usvg::Error), + #[error("Failed to Alloc PixBuf")] + NoPixBuf(u32, u32, u32), + #[error("Failed to Convert SVG to PNG")] + PngError(#[from] png::EncodingError), +} + +#[inline] +fn encode(data: Vec) -> String { + general_purpose::STANDARD_NO_PAD.encode(data) +} + +/// Convert SVG to PNG Image +fn svg_to_png(path: &str, dest: &PathBuf, pixels: u32) -> Result, SvgError> { + // read and convert to resvg document tree + let xml = std::fs::read(path)?; + let opt = resvg::usvg::Options::default(); + let tree = resvg::usvg::Tree::from_data(&xml, &opt)?; + let rtree = resvg::Tree::from_usvg(&tree); + // generate pixel-buffer and scale according to size preference + let size = rtree.size.to_int_size(); + let scale = pixels as f32 / size.width() as f32; + let width = (size.width() as f32 * scale) as u32; + let height = (size.height() as f32 * scale) as u32; + let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height) + .ok_or_else(|| SvgError::NoPixBuf(width, height, pixels))?; + let form = resvg::tiny_skia::Transform::from_scale(scale, scale); + // render as png to memory + rtree.render(form, &mut pixmap.as_mut()); + let png = pixmap.encode_png()?; + // base64 encode png + write(dest, png.clone())?; + Ok(png) +} + +#[derive(Debug)] +pub struct IconCache { + path: PathBuf, + rendered: HashMap>, +} + +impl IconCache { + pub fn new() -> Result { + let path = PathBuf::from(TEMP_DIR); + create_dir_all(&path)?; + Ok(Self { + path, + rendered: HashMap::new(), + }) + } + + fn convert_svg(&self, path: &str) -> Option> { + // convert path to new temporary png filepath + let (_, fname) = path.rsplit_once('/')?; + let (name, _) = fname.rsplit_once(".")?; + let name = format!("{name}.png"); + let new_path = self.path.join(name); + // generate png if it doesnt already exist + if !new_path.exists() { + log::debug!("generating png {new_path:?}"); + match svg_to_png(&path, &new_path, 64) { + Err(err) => log::error!("failed svg->png: {err:?}"), + Ok(data) => return Some(data), + } + } + std::fs::read(new_path).ok() + } + + /// Prepare and PreGenerate Icon Images + pub fn prepare(&mut self, entries: &[&Entry]) { + let icons: Vec<(String, Option)> = entries + .into_par_iter() + .filter_map(|e| e.icon.as_ref()) + .filter(|i| !self.rendered.contains_key(i.to_owned())) + .filter_map(|path| { + if path.ends_with(".png") { + let result = std::fs::read(path).ok().map(encode); + return Some((path.clone(), result)); + } + if path.ends_with(".svg") { + let result = self.convert_svg(&path).map(encode); + return Some((path.clone(), result)); + } + None + }) + .collect(); + self.rendered.extend(icons); + } + + // locate cached icon from specified path (if given) + pub fn locate(&self, icon: &Option) -> &Option { + let Some(path) = icon else { return &None }; + if self.rendered.contains_key(path) { + return self.rendered.get(path).unwrap(); + } + &None + } +} diff --git a/rmenu/src/main.rs b/rmenu/src/main.rs index f3c7596..1c3c52f 100644 --- a/rmenu/src/main.rs +++ b/rmenu/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod config; mod exec; mod gui; +mod icons; mod search; use clap::Parser; @@ -52,6 +53,15 @@ fn main() -> cli::Result<()> { options: None, }, ); + config.plugins.insert( + "drun".to_owned(), + PluginConfig { + exec: vec!["/home/andrew/.config/rmenu/rmenu-desktop".to_owned()], + cache: CacheSetting::OnLogin, + placeholder: None, + options: None, + }, + ); let entries = cli.get_entries(&mut config)?; let css = cli.get_css(&config); let theme = cli.get_theme(); diff --git a/rmenu/templates/results.html b/rmenu/templates/results.html index d50193c..28be40e 100644 --- a/rmenu/templates/results.html +++ b/rmenu/templates/results.html @@ -1,4 +1,4 @@ -{%- for i in start..end %} +{%- for i in start..=end %} {% let entry = results[i] %}
{%-if config.use_icons %} -
+
+ {%- if let Some(icon) = cache.locate(entry.icon) %} + ? + {%else%} +
+ {%- if let Some(alt) = entry.icon_alt %} + {{ alt|safe }} + {%else%} + ? + {%endif %} +
+ {%endif%} +
{%endif%} {%-if config.use_comments %}
{{ entry.name|safe }}