bnb-shell 0.1.1

A fast, polished, cross-platform terminal shell built in Rust with Powerlevel10k aesthetics.
use std::env;
use std::process::Command;
use std::time::Duration;
use terminal_size::{terminal_size, Width};

fn get_home_dir() -> Option<String> {
    env::var("HOME").or_else(|_| env::var("USERPROFILE")).ok()
}

pub fn get_prompt(last_duration: Option<Duration>, last_status: Option<i32>) -> String {
    let cwd = env::current_dir()
        .map(|p| {
            let path = p.display().to_string();
            if let Some(home) = get_home_dir() {
                if path.starts_with(&home) {
                    return path.replacen(&home, "~", 1);
                }
            }
            path
        })
        .unwrap_or_else(|_| "/".into());

    let git_branch = get_git_branch();

    let os_icon = if cfg!(target_os = "macos") {
        ""
    } else if cfg!(target_os = "linux") {
        ""
    } else {
        ""
    };

    let seg1_bg = "\x1b[48;5;238m";
    let seg1_fg = "\x1b[1;38;5;255m";
    let seg1_text = format!("{}{} {}  ", seg1_bg, seg1_fg, os_icon);

    let seg2_bg = "\x1b[48;5;75m";
    let seg2_fg = "\x1b[1;38;5;255m";
    let trans1 = format!("\x1b[38;5;238m{}\u{e0b0}", seg2_bg);
    let seg2_text = format!("{} 📁 {} ", seg2_fg, cwd);

    let (seg3_text, last_bg_fg) = match git_branch {
        Some(branch) => {
            let git_bg = "\x1b[48;5;141m";
            let trans2 = format!("\x1b[38;5;75m{}\u{e0b0}", git_bg);
            let git_content = format!("{}\x1b[1;38;5;232m  {} ", git_bg, branch);
            (format!("{}{}", trans2, git_content), "\x1b[38;5;141m")
        }
        None => (String::new(), "\x1b[38;5;75m"),
    };

    let left_bar_end = format!("\x1b[0m{}\u{e0b0}\x1b[0m", last_bg_fg);
    let left_bar = format!("{}{}{}{}{}", seg1_text, trans1, seg2_text, seg3_text, left_bar_end);

    let status_badge = match last_status {
        Some(code) if code != 0 => {
            format!("\x1b[48;5;196m\x1b[1;38;5;255m ✘ {} \x1b[0m", code)
        }
        _ => String::new(),
    };

    let duration_badge = match last_duration {
        Some(d) if d.as_secs() >= 1 => {
            let duration_str = format_duration(d);
            format!("\x1b[48;5;215m\x1b[1;38;5;232m took {} ⌛ \x1b[0m", duration_str)
        }
        _ => String::new(),
    };

    let right_badges = match (!status_badge.is_empty(), !duration_badge.is_empty()) {
        (true, true) => format!("{} {}", status_badge, duration_badge),
        (true, false) => status_badge,
        (false, true) => duration_badge,
        (false, false) => String::new(),
    };

    let term_width = terminal_size()
        .map(|(Width(w), _)| w as usize)
        .unwrap_or(80);

    let left_len = strip_ansi(&left_bar).chars().count() + 1;
    let right_len = if right_badges.is_empty() {
        0
    } else {
        strip_ansi(&right_badges).chars().count() + (if right_badges.contains('⌛') { 1 } else { 0 })
    };

    let line1 = if !right_badges.is_empty() && term_width > (left_len + right_len + 2) {
        let spaces_count = term_width.saturating_sub(left_len + right_len + 1);
        let spaces = " ".repeat(spaces_count);
        format!("{}{}{}", left_bar, spaces, right_badges)
    } else {
        format!("{}{}", left_bar, right_badges)
    };

    let prompt_arrow = if last_status.unwrap_or(0) != 0 {
        "\x1b[1;31m❯\x1b[0m "
    } else {
        "\x1b[1;32m❯\x1b[0m "
    };

    format!("{}\n{}", line1, prompt_arrow)
}

fn strip_ansi(input: &str) -> String {
    let mut result = String::new();
    let mut in_escape = false;

    for c in input.chars() {
        if c == '\x1b' {
            in_escape = true;
        } else if in_escape {
            if c == 'm' || c == 'K' || c == 'H' || c == 'J' {
                in_escape = false;
            }
        } else {
            result.push(c);
        }
    }

    result
}

fn get_git_branch() -> Option<String> {
    let output = Command::new("git")
        .args(["rev-parse", "--abbrev-ref", "HEAD"])
        .output()
        .ok()?;

    if output.status.success() {
        let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
        if !branch.is_empty() {
            return Some(branch);
        }
    }
    None
}

fn format_duration(d: Duration) -> String {
    let secs = d.as_secs();
    if secs >= 60 {
        let mins = secs / 60;
        let rem_secs = secs % 60;
        format!("{}m {}s", mins, rem_secs)
    } else {
        format!("{}s", secs)
    }
}