helix/helix-view/src/icons.rs

1051 lines
28 KiB
Rust

use arc_swap::ArcSwap;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display, path::Path};
use smartstring::{LazyCompact, SmartString};
use crate::theme::Color;
type String = SmartString<LazyCompact>;
pub static ICONS: Lazy<ArcSwap<Icons>> = Lazy::new(ArcSwap::default);
/// Centralized location for icons that can be used throughout the UI.
#[derive(Debug, Default, Deserialize, PartialEq, Eq, Clone)]
#[serde(default, deny_unknown_fields)]
pub struct Icons {
fs: Fs,
kind: Kind,
diagnostic: Diagnostic,
vcs: Vcs,
dap: Dap,
ui: Ui,
}
impl Icons {
#[inline]
pub fn fs(&self) -> &Fs {
&self.fs
}
#[inline]
pub fn kind(&self) -> &Kind {
&self.kind
}
#[inline]
pub fn diagnostic(&self) -> &Diagnostic {
&self.diagnostic
}
#[inline]
pub fn vcs(&self) -> &Vcs {
&self.vcs
}
#[inline]
pub fn dap(&self) -> &Dap {
&self.dap
}
#[inline]
pub fn ui(&self) -> &Ui {
&self.ui
}
}
#[derive(Debug, Deserialize, Default, PartialEq, Eq, Clone)]
pub struct Kind {
enabled: bool,
file: Option<Icon>,
folder: Option<Icon>,
text: Option<Icon>,
module: Option<Icon>,
namespace: Option<Icon>,
package: Option<Icon>,
class: Option<Icon>,
method: Option<Icon>,
property: Option<Icon>,
field: Option<Icon>,
constructor: Option<Icon>,
#[serde(rename = "enum")]
r#enum: Option<Icon>,
interface: Option<Icon>,
function: Option<Icon>,
variable: Option<Icon>,
constant: Option<Icon>,
string: Option<Icon>,
number: Option<Icon>,
boolean: Option<Icon>,
array: Option<Icon>,
object: Option<Icon>,
key: Option<Icon>,
null: Option<Icon>,
enum_member: Option<Icon>,
#[serde(rename = "struct")]
r#struct: Option<Icon>,
event: Option<Icon>,
operator: Option<Icon>,
type_parameter: Option<Icon>,
color: Option<Icon>,
keyword: Option<Icon>,
value: Option<Icon>,
snippet: Option<Icon>,
reference: Option<Icon>,
unit: Option<Icon>,
word: Option<Icon>,
spellcheck: Option<Icon>,
}
impl Kind {
#[inline]
#[must_use]
pub fn get(&self, kind: &str) -> Option<Icon> {
if !self.enabled {
return None;
}
match kind {
"file" => self.file(),
"folder" => self.folder(),
"module" => self.module(),
"namespace" => self.namespace(),
"package" => self.package(),
"class" => self.class(),
"method" => self.method(),
"property" => self.property(),
"field" => self.field(),
"construct" => self.constructor(),
"enum" => self.r#enum(),
"interface" => self.interface(),
"function" => self.function(),
"variable" => self.variable(),
"constant" => self.constant(),
"string" => self.string(),
"number" => self.number(),
"boolean" => self.boolean(),
"array" => self.array(),
"object" => self.object(),
"key" => self.key(),
"null" => self.null(),
"enum_member" => self.enum_member(),
"struct" => self.r#struct(),
"event" => self.event(),
"operator" => self.operator(),
"typeparam" => self.type_parameter(),
"color" => Some(self.color()),
"keyword" => self.keyword(),
"value" => self.value(),
"snippet" => self.snippet(),
"reference" => self.reference(),
"text" => self.text(),
"unit" => self.unit(),
"word" => self.word(),
"spellcheck" => self.spellcheck(),
_ => None,
}
}
#[inline]
pub fn file(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.file.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn folder(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.folder.clone().or_else(|| Some(Icon::from("󰉋")))
}
#[inline]
pub fn module(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.module.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn namespace(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.namespace.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn package(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.package.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn class(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.class.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn method(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.method.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn property(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.property.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn field(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.field.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn constructor(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.constructor.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn r#enum(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.r#enum.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn interface(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.interface.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn function(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.function.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn variable(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.variable.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn constant(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.constant.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn string(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.string.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn number(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.number.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn boolean(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.boolean.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn array(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.array.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn object(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.object.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn key(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.key.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn null(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.null.clone().or_else(|| Some(Icon::from("󰟢")))
}
#[inline]
pub fn enum_member(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.enum_member.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn r#struct(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.r#struct.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn event(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.event.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn operator(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.operator.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn type_parameter(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.type_parameter
.clone()
.or_else(|| Some(Icon::from("")))
}
// Always enabled
#[inline]
pub fn color(&self) -> Icon {
self.color.clone().unwrap_or_else(|| Icon::from(""))
}
#[inline]
pub fn keyword(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.keyword.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn value(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.value.clone().or_else(|| Some(Icon::from("󰎠")))
}
#[inline]
pub fn snippet(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.snippet.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn reference(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.reference.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn text(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.text.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn unit(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.unit.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn word(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.word.clone().or_else(|| Some(Icon::from("")))
}
#[inline]
pub fn spellcheck(&self) -> Option<Icon> {
if !self.enabled {
return None;
}
self.spellcheck.clone().or_else(|| Some(Icon::from("󰓆")))
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Diagnostic {
hint: Option<String>,
info: Option<String>,
warning: Option<String>,
error: Option<String>,
}
impl Diagnostic {
#[inline]
pub fn hint(&self) -> &str {
self.hint.as_deref().unwrap_or("")
}
#[inline]
pub fn info(&self) -> &str {
self.info.as_deref().unwrap_or("")
}
#[inline]
pub fn warning(&self) -> &str {
self.warning.as_deref().unwrap_or("")
}
#[inline]
pub fn error(&self) -> &str {
self.error.as_deref().unwrap_or("")
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Vcs {
enabled: bool,
branch: Option<String>,
added: Option<String>,
removed: Option<String>,
ignored: Option<String>,
modified: Option<String>,
renamed: Option<String>,
conflict: Option<String>,
}
impl Vcs {
#[inline]
pub fn branch(&self) -> Option<&str> {
if !self.enabled {
return None;
}
self.branch.as_deref().or(Some(""))
}
#[inline]
pub fn added(&self) -> Option<&str> {
if !self.enabled {
return None;
}
self.added.as_deref().or(Some(""))
}
#[inline]
pub fn removed(&self) -> Option<&str> {
if !self.enabled {
return None;
}
self.removed.as_deref().or(Some(""))
}
#[inline]
pub fn ignored(&self) -> Option<&str> {
if !self.enabled {
return None;
}
self.ignored.as_deref().or(Some(""))
}
#[inline]
pub fn modified(&self) -> Option<&str> {
if !self.enabled {
return None;
}
self.modified.as_deref().or(Some(""))
}
#[inline]
pub fn renamed(&self) -> Option<&str> {
if !self.enabled {
return None;
}
self.renamed.as_deref().or(Some(""))
}
#[inline]
pub fn conflict(&self) -> Option<&str> {
if !self.enabled {
return None;
}
self.conflict.as_deref().or(Some(""))
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Fs {
enabled: bool,
directory: Option<String>,
#[serde(rename = "directory-open")]
directory_open: Option<String>,
#[serde(flatten)]
mime: HashMap<String, Icon>,
}
macro_rules! iconmap {
( $( $key:literal => { glyph: $glyph:expr $(, color: $color:expr)? } ),* $(,)? ) => {{
HashMap::from(
[
$(
(String::from($key), Icon {
glyph: String::from($glyph),
color: None $(.or( Some(Color::from_hex($color).unwrap())) )?,
}),
)*
]
)
}};
}
static MIMES: once_cell::sync::Lazy<HashMap<String, Icon>> = once_cell::sync::Lazy::new(|| {
iconmap! {
// Language name
"git-commit" => {glyph: "", color: "#f15233" },
"git-rebase" => {glyph: "", color: "#f15233" },
"git-config" => {glyph: "", color: "#f15233" },
"helm" => {glyph: "", color: "#277a9f" },
"nginx" => {glyph: "", color: "#019639" },
"text" => { glyph: "" },
// Exact
"README.md" => { glyph: "" },
"LICENSE" => { glyph: "󰗑", color: "#e7a933" },
"CHANGELOG.md" => { glyph: "", color: "#7bab43" },
".gitignore" => { glyph: "", color: "#f15233" },
".gitattributes" => { glyph: "", color: "#f15233" },
".editorconfig" => { glyph: "" },
".env" => { glyph: "" },
".dockerignore" => {glyph: "󰡨", color: "#0096e6" },
".ignore" => {glyph: "󰈉" },
"docker-compose.yaml" => {glyph: "󰡨", color: "#0096e6" },
"compose.yaml" => {glyph: "󰡨", color: "#0096e6" },
"Makefile" => {glyph: "" },
"Dockerfile" => {glyph: "󰡨", color: "#0096e6" },
// Extension
"rs" => {glyph: "󱘗", color: "#fab387" },
"py" => {glyph: "󰌠", color: "#ffd94a" },
"c" => {glyph: "", color: "#b0c4de" },
"cpp" => {glyph: "", color: "#0288d1" },
"cs" => {glyph: "", color: "#512bd4" },
"d" => {glyph: "", color: "#b03931" },
"ex" => {glyph: "", color: "#71567d" },
"fs" => {glyph: "", color: "#2fb9da" },
"go" => {glyph: "󰟓", color: "#00acd8" },
"hs" => {glyph: "󰲒", color: "#5e5089" },
"java" => {glyph: "󰬷", color: "#f58217" },
"js" => {glyph: "󰌞", color: "#f0dc4e" },
"ts" => {glyph: "󰛦", color: "#3179c7" },
"kt" => {glyph: "󱈙", color: "#8a48fc" },
"html" => {glyph: "󰌝", color: "#f15c29" },
"css" => {glyph: "󰌜", color: "#9479b6" },
"scss" => {glyph: "󰟬", color: "#d06599" },
"sh" => {glyph: "" },
"bash" => {glyph: "" },
"php" => {glyph: "󰌟", color: "#777bb3" },
"ps1" => {glyph: "󰨊", color: "#2670be" },
"dart" => {glyph: "", color: "#2db7f6" },
"ruby" => {glyph: "󰴭", color: "#d30000" },
"swift" => {glyph: "󰛥", color: "#fba03d" },
"r" => {glyph: "󰟔", color: "#236abd" },
"groovy" => {glyph: "", color: "#4298b8" },
"scala" => {glyph: "", color: "#db3331" },
"pl" => {glyph: "", color: "#006894" },
"clj" => {glyph: "", color: "#91b4ff" },
"jl" => {glyph: "", color: "#cb3c33" },
"zig" => {glyph: "", color: "#f7a41d" },
"f" => {glyph: "󱈚", color: "#734f96" },
"erl" => {glyph: "", color: "#a90432" },
"ml" => {glyph: "", color: "#f29000" },
"cr" => {glyph: "" },
"svelte" => {glyph: "", color: "#ff5620" },
"gd" => {glyph: "", color: "#478cbf" },
"nim" => {glyph: "", color: "#efc743" },
"jsx" => {glyph: "", color: "#61dafb" },
"tsx" => {glyph: "", color: "#61dafb" },
"twig" => {glyph: "", color: "#a8bf21" },
"lua" => {glyph: "", color: "#74c7ec" },
"vue" => {glyph: "", color: "#40b884" },
"lisp" => {glyph: "" },
"elm" => {glyph: "", color: "#5b6379" },
"res" => {glyph: "", color: "#ef5350" },
"sol" => {glyph: "" },
"vala" => {glyph: "", color: "#a972e4" },
"scm" => {glyph: "", color: "#d53d32" },
"v" => {glyph: "", color: "#5e87c0" },
"prisma" => {glyph: "" },
"ada" => {glyph: "", color: "#195c19" },
"astro" => {glyph: "", color: "#ed45cf" },
"m" => {glyph: "", color: "#ed8012" },
"rst" => {glyph: "", color: "#74aada" },
"cl" => {glyph: "" },
"njk" => {glyph: "", color: "#53a553" },
"jinja" => {glyph: "" },
"bicep" => {glyph: "", color: "#529ab7" },
"wat" => {glyph: "", color: "#644fef" },
"md" => {glyph: "" },
"make" => {glyph: "" },
"cmake" => {glyph: "", color: "#3eae2b" },
"nix" => {glyph: "", color: "#4f73bd" },
"awk" => {glyph: "" },
"ll" => {glyph: "", color: "#09627d" },
"regex" => {glyph: "" },
"gql" => {glyph: "", color: "#e534ab" },
"typst" => {glyph: "", color: "#5bc0af" },
"json" => {glyph: "" },
"toml" => {glyph: "", color: "#a8403e" },
"xml" => {glyph: "󰗀", color: "#8bc34a" },
"tex" => {glyph: "", color: "#008080" },
"todotxt" => {glyph: "", color: "#7cb342" },
"svg" => {glyph: "󰜡", color: "#ffb300" },
"png" => {glyph: "", color: "#26a69a" },
"jpeg" => {glyph: "", color: "#26a69a" },
"jpg" => {glyph: "", color: "#26a69a" },
"lock" => {glyph: "", color: "#70797d" },
"csv" => {glyph: "", color: "#1abb54" },
"ipynb" => {glyph: "", color: "#f47724" },
"ttf" => {glyph: "", color: "#144cb7" },
"exe" => {glyph: "" },
"bin" => {glyph: "" },
"bzl" => {glyph: "", color: "#76d275" },
"sql" => {glyph: "" },
"db" => {glyph: "" },
"yaml" => { glyph: "", color: "#cc1018" },
"yml" => { glyph: "", color: "#cc1018" },
"conf" => { glyph: "" },
}
});
impl Fs {
#[inline]
pub fn directory(&self, is_open: bool) -> Option<&str> {
if !self.enabled {
return None;
}
if is_open {
self.directory_open.as_deref().or(Some("󰝰"))
} else {
self.directory.as_deref().or(Some("󰉋"))
}
}
#[inline]
pub fn from_name<'a>(&'a self, name: &str) -> Option<&'a Icon> {
if !self.enabled {
return None;
}
self.mime.get(name).or_else(|| MIMES.get(name))
}
#[inline]
pub fn from_path<'b, 'a: 'b>(&'a self, path: &'b Path) -> Option<&'b Icon> {
self.__from_path_or_lang(Some(path), None)
}
#[inline]
pub fn from_optional_path<'b, 'a: 'b>(&'a self, path: Option<&'b Path>) -> Option<&'b Icon> {
self.__from_path_or_lang(path, None)
}
#[inline]
pub fn from_path_or_lang<'b, 'a: 'b>(
&'a self,
path: &'b Path,
lang: &'b str,
) -> Option<&'b Icon> {
self.__from_path_or_lang(Some(path), Some(lang))
}
#[inline]
pub fn from_optional_path_or_lang<'b, 'a: 'b>(
&'a self,
path: Option<&'b Path>,
lang: &'b str,
) -> Option<&'b Icon> {
self.__from_path_or_lang(path, Some(lang))
}
fn __from_path_or_lang<'b, 'a: 'b>(
&'a self,
path: Option<&'b Path>,
lang: Option<&'b str>,
) -> Option<&'b Icon> {
if !self.enabled {
return None;
}
if let Some(path) = path {
// Search for fully specified name first so that custom icons,
// for example for `README.md` or `docker-compose.yaml`, can
// take precedence over any extension it may have.
if let Some(Some(name)) = path.file_name().map(|name| name.to_str()) {
// Search config options first, then built-in.
if let Some(icon) = self.mime.get(name).or_else(|| MIMES.get(name)) {
return Some(icon);
}
}
// Try to search for icons based off of the extension.
if let Some(Some(ext)) = path.extension().map(|ext| ext.to_str()) {
// Search config options first, then built-in.
if let Some(icon) = self.mime.get(ext).or_else(|| MIMES.get(ext)) {
return Some(icon);
}
}
}
// Try to search via lang name.
if let Some(lang) = lang {
// Search config options first, then built-in.
if let Some(icon) = self.mime.get(lang).or_else(|| MIMES.get(lang)) {
return Some(icon);
}
}
// If icons are enabled but there is no matching found, default to the `text` icon.
// Check user configured first, then built-in.
self.mime.get("text").or_else(|| MIMES.get("text"))
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Dap {
verified: Option<String>,
unverified: Option<String>,
play: Option<String>,
}
impl Dap {
#[inline]
pub fn verified(&self) -> &str {
self.verified.as_deref().unwrap_or("")
}
#[inline]
pub fn unverified(&self) -> &str {
self.unverified.as_deref().unwrap_or("")
}
#[inline]
pub fn play(&self) -> &str {
self.play.as_deref().unwrap_or("")
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(default, deny_unknown_fields)]
pub struct Ui {
workspace: Option<Icon>,
gutter: Gutter,
#[serde(rename = "virtual")]
r#virtual: Virtual,
statusline: Statusline,
}
impl Ui {
/// Returns a workspace diagnostic icon.
///
/// If no icon is set in the config, it will return `W` by default.
#[inline]
pub fn workspace(&self) -> Icon {
self.workspace.clone().unwrap_or_else(|| Icon::from("W"))
}
#[inline]
pub fn gutter(&self) -> &Gutter {
&self.gutter
}
#[inline]
pub fn r#virtual(&self) -> &Virtual {
&self.r#virtual
}
#[inline]
pub fn statusline(&self) -> &Statusline {
&self.statusline
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Gutter {
added: Option<String>,
modified: Option<String>,
removed: Option<String>,
}
impl Gutter {
#[inline]
pub fn added(&self) -> &str {
self.added.as_deref().unwrap_or("")
}
#[inline]
pub fn modified(&self) -> &str {
self.modified.as_deref().unwrap_or("")
}
#[inline]
pub fn removed(&self) -> &str {
self.removed.as_deref().unwrap_or("")
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Virtual {
// Whitespace
space: Option<String>,
nbsp: Option<String>,
nnbsp: Option<String>,
tab: Option<String>,
newline: Option<String>,
tabpad: Option<String>,
// Soft-wrap
wrap: Option<String>,
// Indentation guide
indentation: Option<String>,
// Ruler
ruler: Option<String>,
}
impl Virtual {
#[inline]
pub fn space(&self) -> &str {
// Default: U+00B7
self.space.as_deref().unwrap_or("·")
}
#[inline]
pub fn nbsp(&self) -> &str {
// Default: U+237D
self.nbsp.as_deref().unwrap_or("")
}
#[inline]
pub fn nnbsp(&self) -> &str {
// Default: U+2423
self.nnbsp.as_deref().unwrap_or("")
}
#[inline]
pub fn tab(&self) -> &str {
// Default: U+2192
self.tab.as_deref().unwrap_or("")
}
#[inline]
pub fn newline(&self) -> &str {
// Default: U+23CE
self.newline.as_deref().unwrap_or("")
}
#[inline]
pub fn tabpad(&self) -> &str {
// Default: U+23CE
self.tabpad.as_deref().unwrap_or(" ")
}
#[inline]
pub fn wrap(&self) -> &str {
// Default: U+21AA
self.wrap.as_deref().unwrap_or("")
}
#[inline]
pub fn indentation(&self) -> &str {
// Default: U+254E
self.indentation.as_deref().unwrap_or("")
}
#[inline]
pub fn ruler(&self) -> &str {
// TODO: Default: U+00A6: ¦
self.ruler.as_deref().unwrap_or(" ")
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Statusline {
separator: Option<String>,
}
impl Statusline {
#[inline]
pub fn separator(&self) -> &str {
self.separator.as_deref().unwrap_or("")
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct Icon {
glyph: String,
color: Option<Color>,
}
impl Icon {
pub fn glyph(&self) -> &str {
self.glyph.as_str()
}
pub const fn color(&self) -> Option<Color> {
self.color
}
}
impl From<&str> for Icon {
fn from(icon: &str) -> Self {
Self {
glyph: String::from(icon),
color: None,
}
}
}
impl Display for Icon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.glyph)
}
}
impl<'de> Deserialize<'de> for Icon {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(IconVisitor)
}
}
struct IconVisitor;
impl<'de> serde::de::Visitor<'de> for IconVisitor {
type Value = Icon;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
formatter,
"a string glyph or a map with 'glyph' and optional 'color'"
)
}
fn visit_str<E>(self, glyph: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Icon {
glyph: String::from(glyph),
color: None,
})
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut glyph = None;
let mut color = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"glyph" => {
if glyph.is_some() {
return Err(serde::de::Error::duplicate_field("glyph"));
}
glyph = Some(map.next_value::<String>()?);
}
"color" => {
if color.is_some() {
return Err(serde::de::Error::duplicate_field("color"));
}
color = Some(map.next_value::<String>()?);
}
_ => return Err(serde::de::Error::unknown_field(&key, &["glyph", "color"])),
}
}
let glyph = glyph.ok_or_else(|| serde::de::Error::missing_field("glyph"))?;
let color = if let Some(hex) = color {
let color = Color::from_hex(&hex).ok_or_else(|| {
serde::de::Error::custom(format!("`{hex} is not a valid color code`"))
})?;
Some(color)
} else {
None
};
Ok(Icon { glyph, color })
}
}