use std::collections::BTreeMap;
use std::path::Path;
pub const WT_DIR_ICON: &str = "\u{f07b}";
pub const WT_DIR_OPEN_ICON: &str = "\u{f07c}";
pub const WT_FILE_ICON: &str = "\u{f15b}";
pub const WT_RUST_ICON: &str = "\u{e7a8}";
pub const WT_MARKDOWN_ICON: &str = "\u{f48a}";
pub const WT_TOML_ICON: &str = "\u{e615}";
pub const WT_JSON_ICON: &str = "\u{e60b}";
pub const WT_JS_ICON: &str = "\u{e74e}";
pub const WT_TS_ICON: &str = "\u{e628}";
pub const WT_LOCK_ICON: &str = "\u{f023}";
pub const WT_YAML_ICON: &str = "\u{e6a8}";
pub const WT_SHELL_ICON: &str = "\u{f489}";
pub const WT_TEXT_ICON: &str = "\u{f15c}";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WtCategory {
Created,
Modified,
Deleted,
}
pub fn working_tree_category(x: char, y: char) -> WtCategory {
if (x == '?' && y == '?') || x == 'A' || y == 'A' {
WtCategory::Created
} else if x == 'D' || y == 'D' {
WtCategory::Deleted
} else {
WtCategory::Modified
}
}
pub fn status_badge(x: char, y: char) -> char {
if x == '?' && y == '?' {
'?'
} else if x == 'A' || y == 'A' {
'A'
} else if x == 'D' || y == 'D' {
'D'
} else {
'M'
}
}
pub fn file_icon(name: &str) -> &'static str {
let ext = Path::new(name)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_ascii_lowercase();
match ext.as_str() {
"rs" => WT_RUST_ICON,
"md" | "markdown" => WT_MARKDOWN_ICON,
"toml" => WT_TOML_ICON,
"json" => WT_JSON_ICON,
"js" | "cjs" | "mjs" => WT_JS_ICON,
"ts" | "tsx" => WT_TS_ICON,
"lock" => WT_LOCK_ICON,
"yml" | "yaml" => WT_YAML_ICON,
"sh" | "bash" | "zsh" => WT_SHELL_ICON,
"txt" => WT_TEXT_ICON,
_ => WT_FILE_ICON,
}
}
pub fn sanitize_name(name: &str) -> String {
crate::naming::sanitise_for_terminal(name)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WtNode {
Dir {
name: String,
children: Vec<WtNode>,
category: Option<WtCategory>,
},
File {
name: String,
icon: &'static str,
badge: char,
category: WtCategory,
},
}
fn aggregate_category(children: &[WtNode]) -> Option<WtCategory> {
let mut found: Option<WtCategory> = None;
for child in children {
let cat = match child {
WtNode::File { category, .. } => *category,
WtNode::Dir { category: Some(c), .. } => *c,
WtNode::Dir { category: None, .. } => return None,
};
match found {
None => found = Some(cat),
Some(f) if f == cat => {}
Some(_) => return None,
}
}
found
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusRecord {
pub x: char,
pub y: char,
pub path: String,
}
pub fn parse_status_z(raw: &str) -> Vec<StatusRecord> {
let mut records = Vec::new();
let mut tokens = raw.split('\0');
while let Some(tok) = tokens.next() {
if tok.is_empty() {
continue;
}
let mut chars = tok.chars();
let x = match chars.next() {
Some(c) => c,
None => continue,
};
let y = match chars.next() {
Some(c) => c,
None => continue,
};
if chars.next().is_none() {
continue;
}
let path: String = chars.collect();
if path.is_empty() {
continue;
}
if x == 'R' || x == 'C' || y == 'R' || y == 'C' {
tokens.next();
}
records.push(StatusRecord { x, y, path });
}
records
}
pub fn build_tree(status_z: &str) -> Vec<WtNode> {
build_capped_tree(&parse_status_z(status_z), usize::MAX).0
}
pub const WT_TREE_MAX_FILES: usize = 500;
pub fn build_capped_tree(records: &[StatusRecord], max: usize) -> (Vec<WtNode>, usize) {
let shown = records.len().min(max);
let mut root = DirBuilder::default();
for rec in &records[..shown] {
root.insert(&rec.path, rec.x, rec.y);
}
(root.into_nodes(), records.len() - shown)
}
#[derive(Default)]
struct DirBuilder {
dirs: BTreeMap<String, DirBuilder>,
files: BTreeMap<String, FileLeaf>,
}
struct FileLeaf {
icon: &'static str,
badge: char,
category: WtCategory,
}
impl DirBuilder {
fn insert(&mut self, path: &str, x: char, y: char) {
let mut segments = path.split('/').filter(|s| !s.is_empty()).peekable();
let mut node = self;
while let Some(seg) = segments.next() {
if segments.peek().is_none() {
node.files.insert(
seg.to_string(),
FileLeaf {
icon: file_icon(seg),
badge: status_badge(x, y),
category: working_tree_category(x, y),
},
);
return;
}
node = node.dirs.entry(seg.to_string()).or_default();
}
}
fn into_nodes(self) -> Vec<WtNode> {
let mut out = Vec::with_capacity(self.dirs.len() + self.files.len());
for (mut name, mut dir) in self.dirs {
while dir.files.is_empty() && dir.dirs.len() == 1 {
let (child_name, child_dir) = dir.dirs.into_iter().next().unwrap();
name.push('/');
name.push_str(&child_name);
dir = child_dir;
}
let children = dir.into_nodes();
let category = aggregate_category(&children);
out.push(WtNode::Dir {
name,
children,
category,
});
}
for (name, leaf) in self.files {
out.push(WtNode::File {
name,
icon: leaf.icon,
badge: leaf.badge,
category: leaf.category,
});
}
out
}
}