mirror of
https://github.com/imgurbot12/rmenu.git
synced 2025-02-05 09:45:00 +01:00
feat: icon support
This commit is contained in:
parent
3f4fd61aa8
commit
e3598ebf2e
6 changed files with 160 additions and 7 deletions
|
@ -7,6 +7,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
askama = "0.12.1"
|
askama = "0.12.1"
|
||||||
|
base64 = "0.21.5"
|
||||||
clap = { version = "4.4.11", features = ["derive"] }
|
clap = { version = "4.4.11", features = ["derive"] }
|
||||||
env_logger = "0.10.1"
|
env_logger = "0.10.1"
|
||||||
heck = "0.4.1"
|
heck = "0.4.1"
|
||||||
|
@ -14,7 +15,10 @@ keyboard-types = "0.7.0"
|
||||||
lastlog = { version = "0.2.3", features = ["libc"] }
|
lastlog = { version = "0.2.3", features = ["libc"] }
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
|
png = "0.17.10"
|
||||||
|
rayon = "1.8.0"
|
||||||
regex = "1.10.2"
|
regex = "1.10.2"
|
||||||
|
resvg = "0.36.0"
|
||||||
rmenu-plugin = { version = "0.0.1", path = "../rmenu-plugin" }
|
rmenu-plugin = { version = "0.0.1", path = "../rmenu-plugin" }
|
||||||
serde = { version = "1.0.193", features = ["derive"] }
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
serde_json = "1.0.108"
|
serde_json = "1.0.108"
|
||||||
|
|
|
@ -252,7 +252,7 @@ pub struct Config {
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
page_size: 200,
|
page_size: 50,
|
||||||
page_load: 0.8,
|
page_load: 0.8,
|
||||||
jump_dist: 5,
|
jump_dist: 5,
|
||||||
use_icons: true,
|
use_icons: true,
|
||||||
|
|
|
@ -9,6 +9,7 @@ use web_view::*;
|
||||||
|
|
||||||
use crate::config::{Config, Keybind};
|
use crate::config::{Config, Keybind};
|
||||||
use crate::exec::execute;
|
use crate::exec::execute;
|
||||||
|
use crate::icons::IconCache;
|
||||||
use crate::search::build_searchfn;
|
use crate::search::build_searchfn;
|
||||||
use crate::AppData;
|
use crate::AppData;
|
||||||
|
|
||||||
|
@ -77,6 +78,7 @@ struct ResultsTemplate<'a> {
|
||||||
end: usize,
|
end: usize,
|
||||||
results: &'a Vec<&'a Entry>,
|
results: &'a Vec<&'a Entry>,
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
|
cache: &'a IconCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -87,6 +89,7 @@ struct AppState<'a> {
|
||||||
search: String,
|
search: String,
|
||||||
results: Vec<&'a Entry>,
|
results: Vec<&'a Entry>,
|
||||||
data: &'a AppData,
|
data: &'a AppData,
|
||||||
|
icons: IconCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// check if the current inputs match any of the given keybindings
|
/// check if the current inputs match any of the given keybindings
|
||||||
|
@ -103,23 +106,30 @@ impl<'a> AppState<'a> {
|
||||||
page: 0,
|
page: 0,
|
||||||
search: "".to_owned(),
|
search: "".to_owned(),
|
||||||
results: vec![],
|
results: vec![],
|
||||||
|
icons: IconCache::new().unwrap(),
|
||||||
data,
|
data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render Current Page of Results
|
/// 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 size = self.data.config.page_size;
|
||||||
let start = self.page * size;
|
let start = self.page * size;
|
||||||
let max = (self.page + 1) * size;
|
let max = (self.page + 1) * size;
|
||||||
let nresults = std::cmp::max(self.results.len(), 1);
|
let min = std::cmp::min(max, self.results.len());
|
||||||
let end = std::cmp::min(max, nresults - 1);
|
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
|
// generate results html from template
|
||||||
let template = ResultsTemplate {
|
let template = ResultsTemplate {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
config: &self.data.config,
|
config: &self.data.config,
|
||||||
results: &self.results,
|
results: &self.results,
|
||||||
|
cache: &self.icons,
|
||||||
};
|
};
|
||||||
template.render().unwrap()
|
template.render().unwrap()
|
||||||
}
|
}
|
||||||
|
@ -172,7 +182,8 @@ impl<'a> AppState<'a> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn move_down(&mut self, down: usize) -> Option<String> {
|
fn move_down(&mut self, down: usize) -> Option<String> {
|
||||||
let max = (self.page + 1) * self.data.config.page_size;
|
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);
|
self.pos = std::cmp::min(self.pos + down, end);
|
||||||
match self.append_results(false) {
|
match self.append_results(false) {
|
||||||
Some(operation) => Some(operation),
|
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 config;
|
||||||
mod exec;
|
mod exec;
|
||||||
mod gui;
|
mod gui;
|
||||||
|
mod icons;
|
||||||
mod search;
|
mod search;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
@ -52,6 +53,15 @@ fn main() -> cli::Result<()> {
|
||||||
options: None,
|
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 entries = cli.get_entries(&mut config)?;
|
||||||
let css = cli.get_css(&config);
|
let css = cli.get_css(&config);
|
||||||
let theme = cli.get_theme();
|
let theme = cli.get_theme();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{%- for i in start..end %}
|
{%- for i in start..=end %}
|
||||||
{% let entry = results[i] %}
|
{% let entry = results[i] %}
|
||||||
<div class="result-entry">
|
<div class="result-entry">
|
||||||
<div
|
<div
|
||||||
|
@ -7,7 +7,19 @@
|
||||||
onclick="sclick(this.id)"
|
onclick="sclick(this.id)"
|
||||||
ondblclick="dclick(this.id)">
|
ondblclick="dclick(this.id)">
|
||||||
{%-if config.use_icons %}
|
{%-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%}
|
{%endif%}
|
||||||
{%-if config.use_comments %}
|
{%-if config.use_comments %}
|
||||||
<div class="name">{{ entry.name|safe }}</div>
|
<div class="name">{{ entry.name|safe }}</div>
|
||||||
|
|
Loading…
Reference in a new issue