use std::io::{self, Write};
use std::time::Duration;
use crossterm::cursor;
use crossterm::style::Print;
use crossterm::terminal;
use crossterm::ExecutableCommand;
pub fn detect_powerline_support() -> bool {
if !std::io::stdout().is_terminal_like() {
return false;
}
probe_glyph_width_one("\u{E0B0}").unwrap_or(false)
}
pub fn detect_tab_icon_support() -> bool {
if !std::io::stdout().is_terminal_like() {
return false;
}
probe_glyph_width_one("\u{F048B}").unwrap_or(false)
}
fn probe_glyph_width_one(glyph: &str) -> io::Result<bool> {
let mut stdout = io::stdout();
terminal::enable_raw_mode()?;
let restore = ProbeGuard;
stdout.execute(cursor::SavePosition)?;
let (col_before, _row) = cursor::position().unwrap_or((0, 0));
stdout.execute(Print(glyph))?;
stdout.flush()?;
std::thread::sleep(Duration::from_millis(20));
let (col_after, _row) = cursor::position().unwrap_or((col_before, 0));
stdout.execute(cursor::RestorePosition)?;
stdout.execute(Print(" "))?;
stdout.execute(cursor::RestorePosition)?;
drop(restore);
let advance = col_after.saturating_sub(col_before);
Ok(classify_advance(advance))
}
fn classify_advance(advance: u16) -> bool {
advance == 1
}
#[derive(Debug, PartialEq, Eq)]
pub struct AutoResolved {
pub icons: &'static str,
pub warn_tab_icons_missing: bool,
}
fn classify_auto(powerline: bool, tab_icons: bool) -> AutoResolved {
if powerline {
AutoResolved {
icons: "powerline",
warn_tab_icons_missing: !tab_icons,
}
} else {
AutoResolved {
icons: "unicode",
warn_tab_icons_missing: false,
}
}
}
pub fn resolve_icons_setting(raw: &str) -> String {
if raw.eq_ignore_ascii_case("auto") {
let resolved = classify_auto(detect_powerline_support(), detect_tab_icon_support());
if resolved.warn_tab_icons_missing {
tracing::warn!(
target: "ebman::font_probe",
"Powerline glyph (U+E0B0) renders, but Nerd Font MDI codepoint \
(U+F048B) does not — Detail-view tab strip may misalign. Install \
a Nerd Font (`brew install font-meslo-lg-nerd-font`) or set \
`icons = \"unicode\"` in ~/.config/ebman/config.toml."
);
}
resolved.icons.to_string()
} else {
raw.to_string()
}
}
struct ProbeGuard;
impl Drop for ProbeGuard {
fn drop(&mut self) {
let _ = terminal::disable_raw_mode();
}
}
trait IsTerminalLike {
fn is_terminal_like(&self) -> bool;
}
impl IsTerminalLike for std::io::Stdout {
fn is_terminal_like(&self) -> bool {
use std::io::IsTerminal;
self.is_terminal()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_one_cell_advance_is_supported() {
assert!(classify_advance(1));
}
#[test]
fn classify_other_advances_are_unsupported() {
assert!(!classify_advance(0));
assert!(!classify_advance(2));
assert!(!classify_advance(7));
}
#[test]
fn classify_auto_powerline_with_tab_icons_works_cleanly() {
let r = classify_auto(true, true);
assert_eq!(r.icons, "powerline");
assert!(!r.warn_tab_icons_missing);
}
#[test]
fn classify_auto_powerline_without_tab_icons_warns() {
let r = classify_auto(true, false);
assert_eq!(r.icons, "powerline");
assert!(r.warn_tab_icons_missing);
}
#[test]
fn classify_auto_no_powerline_picks_unicode_and_does_not_warn() {
let r = classify_auto(false, false);
assert_eq!(r.icons, "unicode");
assert!(!r.warn_tab_icons_missing);
let r = classify_auto(false, true);
assert_eq!(r.icons, "unicode");
assert!(!r.warn_tab_icons_missing);
}
#[test]
fn resolve_passes_through_non_auto_values() {
assert_eq!(resolve_icons_setting("unicode"), "unicode");
assert_eq!(resolve_icons_setting("ascii"), "ascii");
assert_eq!(resolve_icons_setting("powerline"), "powerline");
assert_eq!(resolve_icons_setting("nerd"), "nerd");
assert_eq!(resolve_icons_setting("bogus"), "bogus");
}
}