Skip to content

Commit

Permalink
Enable support for SVG icons
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvkb committed Sep 11, 2024
1 parent adcb917 commit e8fc76b
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 38 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ regex = { version = "1.8.4", default-features = false, features = ["std", "perf"
resvg = { version = "0.43.0", default-features = false }
serde = { version = "1.0.164", features = ["derive"] }
serde_regex = "1.1.0"
shellexpand = { version = "3.1.0", default-features = false, features = ["base-0"] }
time = { version = "0.3.22", default-features = false, features = ["std", "alloc", "local-offset", "formatting"] }
unicode-segmentation = "1.10.1"
uzers = { version = "0.12.1", default-features = false, features = ["cache"] }
Expand Down
2 changes: 1 addition & 1 deletion src/config/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ macro_rules! map_str_str {
/// Refer to [`Args`](crate::config::Args) for those.
#[derive(Serialize, Deserialize)]
pub struct Conf {
/// mapping of icon names to actual glyphs from Nerd Fonts
/// mapping of icon names to actual glyphs from Nerd Fonts or paths to SVGs
pub icons: HashMap<String, String>,
/// list of node specs, in ascending order of specificity
pub specs: Vec<Spec>,
Expand Down
20 changes: 10 additions & 10 deletions src/config/entry_const.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ impl Default for EntryConst {
dir_plur: String::from(""),
},
typ: [
(Typ::Dir, "d", "<dimmed>/</>", Some("dir"), "blue"),
(Typ::Symlink, "l", "<dimmed>@</>", Some("symlink"), ""),
(Typ::Fifo, "p", "<dimmed>|</>", None, ""),
(Typ::Socket, "s", "<dimmed>=</>", None, ""),
(Typ::BlockDevice, "b", "", None, ""),
(Typ::CharDevice, "c", "", None, ""),
(Typ::File, "<dimmed>f</>", "", None, ""),
(Typ::Unknown, "<red>?</>", "", None, ""),
(Typ::Dir, "d", "<dimmed>/</>", "dir", "blue"),
(Typ::Symlink, "l", "<dimmed>@</>", "symlink", ""),
(Typ::Fifo, "p", "<dimmed>|</>", "fifo", ""),
(Typ::Socket, "s", "<dimmed>=</>", "socket", ""),
(Typ::BlockDevice, "b", "", "block_device", ""),
(Typ::CharDevice, "c", "", "char_device", ""),
(Typ::File, "<dimmed>f</>", "", "file", ""),
(Typ::Unknown, "<red>?</>", "", "unknown", ""),
]
.into_iter()
.map(|(k, ch, suffix, icon, style)| {
Expand All @@ -58,7 +58,7 @@ impl Default for EntryConst {
TypInfo {
ch: ch.to_string(),
suffix: suffix.to_string(),
icon: icon.map(String::from),
icons: Some(vec![format!("{}-svg", icon), String::from(icon)]),
style: style.to_string(),
},
)
Expand Down Expand Up @@ -167,7 +167,7 @@ pub struct TypInfo {
/// the suffix for a node type, placed after the node name
pub suffix: String,
/// the fallback icon for the node type, used if no other icon is found
pub icon: Option<String>, // not all node types need to have an icon
pub icons: Option<Vec<String>>, // not all node types need to have an icon
/// the style to use for nodes of a particular node type
pub style: String, // applies to name, `ch`, `suffix` and `icon`
}
Expand Down
2 changes: 2 additions & 0 deletions src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod appearance;
mod collapse;
mod detail_field;
mod entity;
mod icon;
mod perm;
mod sort_field;
mod sym;
Expand All @@ -12,6 +13,7 @@ pub use appearance::Appearance;
pub use collapse::Collapse;
pub use detail_field::DetailField;
pub use entity::Entity;
pub use icon::Icon;
pub use perm::{Oct, Sym};
pub use sort_field::SortField;
pub use sym::{SymState, SymTarget};
Expand Down
80 changes: 80 additions & 0 deletions src/enums/icon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::gfx::{get_rgba, render_image};
use crate::PLS;
use std::path::PathBuf;

/// This enum contains the two formats of icons supported by `pls`.
pub enum Icon {
/// a Nerd Font or emoji icon
Text(String),
/// the path to an SVG icon
Image(String),
}

impl From<&str> for Icon {
fn from(s: &str) -> Self {
if s.ends_with(".svg") {
Icon::Image(s.to_string())
} else {
Icon::Text(s.to_string())
}
}
}

impl Icon {
/// Get the size of the icon in pixels.
///
/// The icon size is determined by the width of a cell in the terminal
/// multiplied by a scaling factor.
pub fn size() -> u8 {
let scale = std::env::var("PLS_ICON_SCALE")
.ok()
.and_then(|string| string.parse().ok())
.unwrap_or(1.0f32)
.min(2.0); // We only allocate two cells for an icon.

return (scale * PLS.window.as_ref().unwrap().cell_width() as f32) // Convert to px.s
.round() as u8;
}

/// Get the output of the icon using the appropriate method:
///
/// * For text icons, it generates the markup string with the
/// directives.
/// * For image icons, it generates the Kitty terminal graphics APC
/// sequence. If that fails, it falls back to a blank text icon.
///
/// The formatting directives for textual icons are a subset of the
/// formatting directives for text.
///
/// # Arguments
///
/// * `directives` - the formatting directives to apply to text
pub fn render(&self, text_directives: &str) -> String {
match self {
Icon::Text(text) => {
// Nerd Font icons look weird with underlines and
// synthesised italics.
let directives = text_directives
.replace("underline", "")
.replace("italic", "");
// We leave a space after the icon to allow Nerd Font
// icons that are slightly bigger than one cell to be
// displayed correctly.
format!("<{directives}>{text:<1} </>")
}

Icon::Image(path) => {
// SVG icons support expanding environment variables in
// the path for theming purposes.
if let Ok(path) = shellexpand::env(path) {
let size = Icon::size();
if let Some(rgba_data) = get_rgba(&PathBuf::from(path.as_ref()), size) {
return render_image(&rgba_data, size);
}
}
// This would be exactly as if the node had no icon.
Icon::Text(String::default()).render(text_directives)
}
}
}
}
20 changes: 10 additions & 10 deletions src/enums/typ.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ impl Typ {
///
/// This icon is used as a fallback in cases where no other icon is found
/// for the node from matching specs.
pub fn icon<'conf>(&self, entry_const: &'conf EntryConst) -> &'conf Option<String> {
&entry_const.typ.get(self).unwrap().icon
pub fn icons<'conf>(&self, entry_const: &'conf EntryConst) -> &'conf Option<Vec<String>> {
&entry_const.typ.get(self).unwrap().icons
}

/// Get the suffix associated with the nodes type.
Expand Down Expand Up @@ -206,21 +206,21 @@ mod tests {
#[test]
fn $name() {
let entry_const = EntryConst::default();
assert_eq!($typ.icon(&entry_const), $icon);
assert_eq!($typ.icons(&entry_const), &Some(vec![format!("{}-svg", $icon), String::from($icon)]));
assert_eq!($typ.suffix(&entry_const), $suffix);
}
)*
};
}

make_name_components_test!(
test_icon_suffix_for_dir: Typ::Dir => &Some(String::from("dir")), "<dimmed>/</>",
test_icon_suffix_for_symlink: Typ::Symlink => &Some(String::from("symlink")), "<dimmed>@</>",
test_icon_suffix_for_fifo: Typ::Fifo => &None, "<dimmed>|</>",
test_icon_suffix_for_socket: Typ::Socket => &None, "<dimmed>=</>",
test_icon_suffix_for_block_device: Typ::BlockDevice => &None, "",
test_icon_suffix_for_char_device: Typ::CharDevice => &None, "",
test_icon_suffix_for_file: Typ::File => &None, "",
test_icon_suffix_for_dir: Typ::Dir => "dir", "<dimmed>/</>",
test_icon_suffix_for_symlink: Typ::Symlink => "symlink", "<dimmed>@</>",
test_icon_suffix_for_fifo: Typ::Fifo => "fifo", "<dimmed>|</>",
test_icon_suffix_for_socket: Typ::Socket => "socket", "<dimmed>=</>",
test_icon_suffix_for_block_device: Typ::BlockDevice => "block_device", "",
test_icon_suffix_for_char_device: Typ::CharDevice => "char_device", "",
test_icon_suffix_for_file: Typ::File => "file", "",
);

macro_rules! make_renderables_test {
Expand Down
42 changes: 28 additions & 14 deletions src/models/node.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::config::{AppConst, Conf, EntryConst};
use crate::enums::{Appearance, Collapse, DetailField, Typ};
use crate::enums::{Appearance, Collapse, DetailField, Icon, Typ};
use crate::models::{OwnerMan, Spec};
use crate::traits::{Detail, Imp, Name, Sym};
use crate::PLS;
Expand Down Expand Up @@ -179,21 +179,38 @@ impl<'pls> Node<'pls> {
// Name components
// ===============

/// Get the icon associated with the node.
/// Get the icons associated with the node, filtered by the
/// capabilities of the current terminal.
///
/// A node can get its icon from two sources:
///
/// * specs associated with the node
/// * the node's type
fn icon(&self, conf: &Conf, entry_const: &EntryConst) -> String {
self.specs
fn icon(&self, conf: &Conf, entry_const: &EntryConst) -> Icon {
let icon = self
.specs
.iter()
.rev()
.find(|spec| spec.icon.is_some())
.and_then(|spec| spec.icon.as_ref())
.or_else(|| self.typ.icon(entry_const).as_ref())
.and_then(|icon_name| conf.icons.get(icon_name).cloned())
.unwrap_or_default()
.filter_map(|spec| spec.icons.as_ref())
.chain(self.typ.icons(entry_const))
.flatten()
.find_map(|icon_name| {
conf.icons
.get(icon_name.as_str())
.filter(|icon| !icon.ends_with(".svg") || PLS.supports_gfx)
});

match icon {
Some(icon) => {
let icon = String::from(icon);
if icon.ends_with(".svg") {
Icon::Image(icon)
} else {
Icon::Text(icon)
}
}
None => Icon::Text(String::default()),
}
}

// ===========
Expand All @@ -219,7 +236,6 @@ impl<'pls> Node<'pls> {
tree_shapes: &[&str],
) -> String {
let text_directives = self.directives(app_const, entry_const);
let icon_directives = text_directives.replace("underline", "");

let mut parts = String::default();

Expand All @@ -234,10 +250,8 @@ impl<'pls> Node<'pls> {

// Icon
if PLS.args.icon && !self.appearances.contains(&Appearance::Symlink) {
parts.push_str(&format!(
"<{icon_directives}>{:<1}</> ",
self.icon(conf, entry_const),
));
let icon = self.icon(conf, entry_const);
parts.push_str(&icon.render(&text_directives));
}

// Name and suffix
Expand Down
6 changes: 3 additions & 3 deletions src/models/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ pub struct Spec {
/// a regex pattern to match against the node's name
#[serde(with = "serde_regex")]
pub pattern: Regex,
/// the name of the icon to use for the node
pub icon: Option<String>,
/// names of the icon to use for the node
pub icons: Option<Vec<String>>,
/// styles to apply to the node name and icon
pub style: Option<String>,
/// the importance level of the node
Expand All @@ -33,7 +33,7 @@ impl Spec {
pub fn new(pattern: &str, icon: &str) -> Self {
Self {
pattern: RegexBuilder::new(pattern).unicode(false).build().unwrap(),
icon: Some(String::from(icon)),
icons: Some(vec![String::from(icon)]),
style: None,
importance: None,
collapse: None,
Expand Down

0 comments on commit e8fc76b

Please sign in to comment.