mirror of
https://github.com/imgurbot12/rmenu.git
synced 2024-11-10 11:33:48 +01:00
feat: icon support
This commit is contained in:
parent
3f4fd61aa8
commit
e3598ebf2e
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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<String> {
|
||||
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),
|
||||
|
116
rmenu/src/icons.rs
Normal file
116
rmenu/src/icons.rs
Normal file
@ -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<u8>) -> String {
|
||||
general_purpose::STANDARD_NO_PAD.encode(data)
|
||||
}
|
||||
|
||||
/// Convert SVG to PNG Image
|
||||
fn svg_to_png(path: &str, dest: &PathBuf, pixels: u32) -> Result<Vec<u8>, 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<String, Option<String>>,
|
||||
}
|
||||
|
||||
impl IconCache {
|
||||
pub fn new() -> Result<Self, SvgError> {
|
||||
let path = PathBuf::from(TEMP_DIR);
|
||||
create_dir_all(&path)?;
|
||||
Ok(Self {
|
||||
path,
|
||||
rendered: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_svg(&self, path: &str) -> Option<Vec<u8>> {
|
||||
// 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<String>)> = 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<String>) -> &Option<String> {
|
||||
let Some(path) = icon else { return &None };
|
||||
if self.rendered.contains_key(path) {
|
||||
return self.rendered.get(path).unwrap();
|
||||
}
|
||||
&None
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -1,4 +1,4 @@
|
||||
{%- for i in start..end %}
|
||||
{%- for i in start..=end %}
|
||||
{% let entry = results[i] %}
|
||||
<div class="result-entry">
|
||||
<div
|
||||
@ -7,7 +7,19 @@
|
||||
onclick="sclick(this.id)"
|
||||
ondblclick="dclick(this.id)">
|
||||
{%-if config.use_icons %}
|
||||
<div class="icon"></div>
|
||||
<div class="icon">
|
||||
{%- if let Some(icon) = cache.locate(entry.icon) %}
|
||||
<img class="image" src="data:image/png;base64,{{ icon }}" alt="?">
|
||||
{%else%}
|
||||
<div class="icon_alt">
|
||||
{%- if let Some(alt) = entry.icon_alt %}
|
||||
{{ alt|safe }}
|
||||
{%else%}
|
||||
?
|
||||
{%endif %}
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
{%endif%}
|
||||
{%-if config.use_comments %}
|
||||
<div class="name">{{ entry.name|safe }}</div>
|
||||
|
Loading…
Reference in New Issue
Block a user