paneship 1.1.2

A blazingly fast, high-performance shell prompt optimized for tmux and large Git repositories
use crate::core::prompt::PromptContext;
use std::path::{Path, PathBuf};
use unicode_width::UnicodeWidthStr;

pub fn render_with_max_width(context: &PromptContext, max_visible_width: usize) -> String {
    if max_visible_width == 0 {
        return String::new();
    }

    let config = &context.config.directory;
    let icon_width = if config.icon.trim().is_empty() {
        0
    } else {
        UnicodeWidthStr::width(config.icon.as_str()) + 1
    };

    let path_budget = max_visible_width.saturating_sub(icon_width).max(1);
    let raw_path = compact_path(&context.cwd, config);
    let trimmed_path = if UnicodeWidthStr::width(raw_path.as_str()) > path_budget {
        smart_truncate_path(raw_path.as_str(), path_budget)
    } else {
        raw_path
    };

    let mut plain = if config.icon.trim().is_empty() {
        trimmed_path
    } else {
        format!("{} {}", config.icon, trimmed_path)
    };
    if UnicodeWidthStr::width(plain.as_str()) > max_visible_width {
        plain = crate::core::layout::truncate_plain_to_width(plain.as_str(), max_visible_width);
    }

    format!("\x1b[1;34m{}\x1b[0m", plain)
}

fn smart_truncate_path(input: &str, budget: usize) -> String {
    let parts: Vec<&str> = input.split('/').filter(|p| !p.is_empty()).collect();
    if parts.len() <= 2 {
        return crate::core::layout::truncate_plain_to_width(input, budget);
    }

    let mut prefix = "";
    if input.starts_with('~') {
        prefix = "~";
    } else if input.starts_with('/') {
        prefix = "/";
    }

    let last = parts[parts.len() - 1];
    let candidate = if prefix.is_empty() {
        format!(".../{last}")
    } else {
        format!("{prefix}/.../{last}")
    };

    if UnicodeWidthStr::width(candidate.as_str()) <= budget {
        candidate
    } else {
        crate::core::layout::truncate_plain_to_width(input, budget)
    }
}

fn compact_path(path: &Path, config: &crate::core::config::DirectoryConfig) -> String {
    if config.truncate_to_repo {
        if let Some(repo_root) = find_repo_root(path) {
            return compact_path_from_repo(path, repo_root.as_path(), config.truncation_length);
        }
    }

    compact_general_path(path, config.truncation_length)
}

fn compact_path_from_repo(path: &Path, repo_root: &Path, truncation_length: usize) -> String {
    let Some(repo_name) = repo_root
        .file_name()
        .map(|name| name.to_string_lossy().to_string())
    else {
        return compact_general_path(path, truncation_length);
    };

    let rel = path.strip_prefix(repo_root).ok();
    let mut suffix = vec![repo_name];
    if let Some(rel) = rel {
        suffix.extend(path_segments(rel));
    }

    let mut hidden_prefix = false;
    let prefix = leading_prefix(repo_root, &mut hidden_prefix);
    let suffix = trim_segments(suffix, truncation_length.max(1), true, &mut hidden_prefix);

    join_path_display(prefix.as_str(), hidden_prefix, suffix.as_slice())
}

fn compact_general_path(path: &Path, truncation_length: usize) -> String {
    let home = std::env::var("HOME")
        .or_else(|_| std::env::var("USERPROFILE"))
        .map(std::path::PathBuf::from)
        .ok();

    let mut hidden_prefix = false;
    let (prefix, base) = if let Some(home_path) = home {
        if let Ok(rel) = path.strip_prefix(home_path) {
            let has_hidden = path_segments(rel).len() > truncation_length.max(1);
            hidden_prefix = hidden_prefix || has_hidden;
            ("~".to_string(), rel.to_path_buf())
        } else {
            (
                "/".to_string(),
                PathBuf::from(path.to_string_lossy().replace('\\', "/")),
            )
        }
    } else {
        (
            "/".to_string(),
            PathBuf::from(path.to_string_lossy().replace('\\', "/")),
        )
    };

    let segments = path_segments(base.as_path());
    let segments = trim_segments(
        segments,
        truncation_length.max(1),
        false,
        &mut hidden_prefix,
    );

    join_path_display(prefix.as_str(), hidden_prefix, segments.as_slice())
}

fn find_repo_root(start: &Path) -> Option<PathBuf> {
    crate::cache::repo_root_for(start)
}

fn leading_prefix(path: &Path, hidden_prefix: &mut bool) -> String {
    let home = std::env::var("HOME")
        .or_else(|_| std::env::var("USERPROFILE"))
        .map(PathBuf::from)
        .ok();

    if let Some(home_path) = home {
        if let Ok(rel) = path.strip_prefix(&home_path) {
            if !rel.as_os_str().is_empty() {
                *hidden_prefix = true;
            }
            return "~".to_string();
        }
    }

    if path.is_absolute() {
        let root = Path::new("/");
        if let Ok(rel) = path.strip_prefix(root) {
            if rel.components().next().is_some() {
                *hidden_prefix = true;
            }
        }
        "/".to_string()
    } else {
        String::new()
    }
}

fn path_segments(path: &Path) -> Vec<String> {
    path.components()
        .filter_map(|component| match component {
            std::path::Component::Normal(value) => Some(value.to_string_lossy().to_string()),
            _ => None,
        })
        .collect()
}

fn trim_segments(
    segments: Vec<String>,
    truncation_length: usize,
    preserve_first: bool,
    hidden_prefix: &mut bool,
) -> Vec<String> {
    if segments.len() <= truncation_length {
        return segments;
    }

    *hidden_prefix = true;

    if preserve_first && truncation_length > 1 {
        let mut out = Vec::with_capacity(truncation_length);
        out.push(segments[0].clone());
        out.extend(
            segments
                .iter()
                .skip(segments.len().saturating_sub(truncation_length - 1))
                .cloned(),
        );
        out
    } else {
        segments
            .iter()
            .skip(segments.len().saturating_sub(truncation_length))
            .cloned()
            .collect()
    }
}

fn join_path_display(prefix: &str, hidden_prefix: bool, segments: &[String]) -> String {
    if prefix.is_empty() {
        if segments.is_empty() {
            return ".".to_string();
        }
        if hidden_prefix {
            return format!(".../{}", segments.join("/"));
        }
        return segments.join("/");
    }

    if segments.is_empty() {
        return prefix.to_string();
    }

    if hidden_prefix {
        format!("{prefix}/.../{}", segments.join("/"))
    } else {
        format!("{prefix}/{}", segments.join("/"))
    }
}