use anyhow::Result;
use console::style;
use std::fs;
use std::io::{self, IsTerminal, Write};
use std::path::PathBuf;
use std::sync::OnceLock;
static NERDFONT_ENABLED: OnceLock<bool> = OnceLock::new();
#[derive(Clone, Copy)]
pub struct PrIcons {
pub draft: &'static str,
pub open: &'static str,
pub merged: &'static str,
pub closed: &'static str,
}
#[derive(Clone, Copy)]
pub struct GitIcons {
pub diff: &'static str,
pub conflict: &'static str,
pub rebase: &'static str,
}
const NERDFONT_PR_ICONS: PrIcons = PrIcons {
draft: "\u{f177}", open: "\u{f407}", merged: "\u{f419}", closed: "\u{f406}", };
const FALLBACK_PR_ICONS: PrIcons = PrIcons {
draft: "○",
open: "●",
merged: "◆",
closed: "×",
};
#[derive(Clone, Copy)]
pub struct CheckIcons {
pub success: &'static str,
pub failure: &'static str,
pub pending: &'static str,
}
const NERDFONT_CHECK_ICONS: CheckIcons = CheckIcons {
success: "\u{f0134}", failure: "\u{f0159}", pending: "\u{f0520}", };
const FALLBACK_CHECK_ICONS: CheckIcons = CheckIcons {
success: "✓",
failure: "×",
pending: "◷",
};
const NERDFONT_GIT_ICONS: GitIcons = GitIcons {
diff: "\u{f03eb}", conflict: "\u{f002a}", rebase: "\u{f47f}", };
const FALLBACK_GIT_ICONS: GitIcons = GitIcons {
diff: "*",
conflict: "!",
rebase: "R",
};
const GIT_BRANCH_ICON: &str = "\u{e725}";
pub fn init(config_nerdfont: Option<bool>, config_has_pua: bool) {
let enabled = config_nerdfont.unwrap_or(config_has_pua);
let _ = NERDFONT_ENABLED.set(enabled);
}
pub fn is_enabled() -> bool {
*NERDFONT_ENABLED.get().unwrap_or(&false)
}
pub fn pr_icons() -> PrIcons {
if is_enabled() {
NERDFONT_PR_ICONS
} else {
FALLBACK_PR_ICONS
}
}
pub fn check_icons() -> CheckIcons {
if is_enabled() {
NERDFONT_CHECK_ICONS
} else {
FALLBACK_CHECK_ICONS
}
}
pub fn git_icons() -> GitIcons {
if is_enabled() {
NERDFONT_GIT_ICONS
} else {
FALLBACK_GIT_ICONS
}
}
pub fn contains_pua(s: &str) -> bool {
s.chars().any(|c| {
let cp = c as u32;
(0xE000..=0xF8FF).contains(&cp) || (0xF0000..=0xFFFFF).contains(&cp)
})
}
pub fn config_has_pua(config: &crate::config::Config) -> bool {
if let Some(ref working) = config.status_icons.working
&& contains_pua(working)
{
return true;
}
if let Some(ref waiting) = config.status_icons.waiting
&& contains_pua(waiting)
{
return true;
}
if let Some(ref done) = config.status_icons.done
&& contains_pua(done)
{
return true;
}
if let Some(ref prefix) = config.window_prefix
&& contains_pua(prefix)
{
return true;
}
if let Some(ref prefix) = config.worktree_prefix
&& contains_pua(prefix)
{
return true;
}
false
}
fn global_config_path() -> Option<PathBuf> {
crate::config::global_config_path()
}
pub fn prompt_setup() -> Result<Option<bool>> {
if std::env::var("CI").is_ok() || std::env::var("WORKMUX_TEST").is_ok() {
return Ok(None);
}
if !io::stdin().is_terminal() {
return Ok(None);
}
let dim = style("│").dim();
let corner_top = style("┌").dim();
let corner_bottom = style("└─").dim();
println!();
println!("{} {}", corner_top, style("Nerdfont Setup").bold().cyan());
println!("{}", dim);
println!(
"{} Does this look like a git branch icon? {} {}",
dim,
style("→").yellow(),
style(GIT_BRANCH_ICON).green()
);
println!("{}", dim);
let prompt_line = format!(
"{} {}{}{} Yes {}{}{} No: ",
corner_bottom,
style("[").bold().cyan(),
style("y").bold(),
style("]").bold().cyan(),
style("[").bold().cyan(),
style("n").bold(),
style("]").bold().cyan(),
);
let enabled = loop {
print!("{}", prompt_line);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
match answer.as_str() {
"y" | "yes" => break true,
"n" | "no" => break false,
_ => {
println!("{}", style(" Please enter y or n").dim());
}
}
};
if enabled {
println!("{}", style("✔ Nerdfont icons enabled").green());
} else {
println!("{}", style("✔ Using Unicode fallbacks").green());
}
if let Err(e) = save_nerdfont_preference(enabled) {
println!(
" {}",
style(format!("Could not save preference: {}", e)).yellow()
);
println!(
" {}",
style(format!(
"Add 'nerdfont: {}' to ~/.config/workmux/config.yaml to persist this setting",
enabled
))
.dim()
);
} else if !enabled {
println!(
" {}",
style("Set nerdfont: true in ~/.config/workmux/config.yaml to enable later").dim()
);
}
println!();
Ok(Some(enabled))
}
fn save_nerdfont_preference(enabled: bool) -> Result<()> {
let config_path = global_config_path()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
let mut config_content = if config_path.exists() {
fs::read_to_string(&config_path)?
} else {
String::new()
};
if config_content.contains("nerdfont:") {
let re = regex::Regex::new(r"(?m)^nerdfont:.*$")?;
config_content = re
.replace(&config_content, format!("nerdfont: {}", enabled))
.to_string();
} else {
if !config_content.is_empty() && !config_content.ends_with('\n') {
config_content.push('\n');
}
config_content.push_str(&format!("nerdfont: {}\n", enabled));
}
fs::write(&config_path, config_content)?;
Ok(())
}
pub fn check_and_prompt(config: &crate::config::Config) -> Result<Option<bool>> {
if let Some(enabled) = config.nerdfont {
return Ok(Some(enabled));
}
if config_has_pua(config) {
return Ok(Some(true));
}
prompt_setup()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contains_pua_detects_bmp_pua() {
assert!(contains_pua("\u{E000}"));
assert!(contains_pua("\u{F8FF}"));
assert!(contains_pua("text \u{E725} more")); }
#[test]
fn contains_pua_detects_supplementary_pua() {
assert!(contains_pua("\u{F0000}"));
assert!(contains_pua("\u{FFFFF}"));
assert!(contains_pua("\u{f03eb}")); }
#[test]
fn contains_pua_rejects_normal_text() {
assert!(!contains_pua("hello world"));
assert!(!contains_pua("✓ ✗ → ↑ ↓"));
assert!(!contains_pua("●○◆×"));
}
#[test]
fn contains_pua_handles_empty_string() {
assert!(!contains_pua(""));
}
}