use {
super::{
BRANCH_FILLING,
Col,
CropWriter,
GitStatusDisplay,
MatchedString,
SPACE_FILLING,
num_format::format_count,
},
crate::{
app::AppState,
content_search::ContentMatch,
errors::ProgramError,
file_sum::FileSum,
pattern::PatternObject,
skin::{
ExtColorMap,
StyleMap,
},
task_sync::ComputationResult,
tree::{
Tree,
TreeLine,
TreeLineType,
},
},
chrono::{
DateTime,
Local,
LocalResult,
TimeZone,
},
crokey::crossterm::{
QueueableCommand,
cursor,
},
file_size,
git2::Status,
std::io::Write,
termimad::{
CompoundStyle,
ProgressBar,
},
unicode_width::{
UnicodeWidthChar,
UnicodeWidthStr,
},
};
pub struct DisplayableTree<'a, 's, 't> {
pub app_state: Option<&'a AppState>,
pub tree: &'t Tree,
pub skin: &'s StyleMap,
pub area: termimad::Area,
pub in_app: bool, pub ext_colors: &'s ExtColorMap,
}
impl<'a, 's, 't> DisplayableTree<'a, 's, 't> {
pub fn out_of_app(
tree: &'t Tree,
skin: &'s StyleMap,
ext_colors: &'s ExtColorMap,
width: u16,
height: u16,
) -> DisplayableTree<'a, 's, 't> {
DisplayableTree {
app_state: None,
tree,
skin,
ext_colors,
area: termimad::Area {
left: 0,
top: 0,
width,
height,
},
in_app: false,
}
}
fn label_style(
&self,
line: &TreeLine,
selected: bool,
) -> CompoundStyle {
let style = match &line.line_type {
TreeLineType::Dir => &self.skin.directory,
TreeLineType::File => {
if line.is_exe() {
&self.skin.exe
} else {
&self.skin.file
}
}
TreeLineType::BrokenSymLink(_) | TreeLineType::SymLink { .. } => &self.skin.link,
TreeLineType::Pruning => &self.skin.pruning,
};
let mut style = style.clone();
if let Some(ext_color) = line.extension().and_then(|ext| self.ext_colors.get(ext)) {
style.set_fg(ext_color);
}
if selected {
if let Some(c) = self.skin.selected_line.get_bg() {
style.set_bg(c);
}
}
style
}
fn write_line_count<W: Write>(
&self,
cw: &mut CropWriter<W>,
line: &TreeLine,
count_len: usize,
selected: bool,
) -> Result<usize, termimad::Error> {
Ok(if let Some(s) = line.sum {
cond_bg!(count_style, self, selected, self.skin.count);
let s = format_count(s.to_count());
cw.queue_g_string(count_style, format!("{s:>count_len$}"))?;
1
} else {
count_len + 1
})
}
#[allow(unused_variables, dead_code)]
fn write_line_device_id<W: Write>(
&self,
cw: &mut CropWriter<W>,
line: &TreeLine,
selected: bool,
) -> Result<usize, termimad::Error> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let device_id = line.device_id();
cond_bg!(style, self, selected, self.skin.device_id_major);
cw.queue_g_string(style, format!("{:>3}", device_id.major))?;
cond_bg!(style, self, selected, self.skin.device_id_sep);
cw.queue_char(style, ':')?;
cond_bg!(style, self, selected, self.skin.device_id_minor);
cw.queue_g_string(style, format!("{:<3}", device_id.minor))?;
}
#[cfg(target_os = "windows")]
{
let device_id = line.device_id().map_or(" ".to_string(), |dev| dev.to_string());
cond_bg!(style, self, selected, self.skin.device_id_major);
cw.queue_g_string(style, format!("{}", device_id))?;
}
Ok(0)
}
fn write_line_selection_mark<W: Write>(
cw: &mut CropWriter<W>,
style: &CompoundStyle,
selected: bool,
) -> Result<usize, termimad::Error> {
Ok(if selected {
cw.queue_char(style, '▶')?;
0
} else {
1
})
}
fn write_line_size<W: Write>(
cw: &mut CropWriter<W>,
line: &TreeLine,
style: &CompoundStyle,
_selected: bool,
) -> Result<usize, termimad::Error> {
Ok(if let Some(s) = line.sum {
cw.queue_g_string(style, format!("{:>4}", file_size::fit_4(s.to_size())))?;
1
} else {
5
})
}
fn write_line_size_with_bar<W: Write>(
&self,
cw: &mut CropWriter<W>,
line: &TreeLine,
label_style: &CompoundStyle,
total_size: FileSum,
selected: bool,
) -> Result<usize, termimad::Error> {
Ok(if let Some(s) = line.sum {
let pb = ProgressBar::new(s.part_of_size(total_size), 10);
cond_bg!(sparse_style, self, selected, self.skin.sparse);
cw.queue_g_string(label_style, format!("{:>4}", file_size::fit_4(s.to_size())))?;
cw.queue_char(
sparse_style,
if s.is_sparse() && line.is_file() {
's'
} else {
' '
},
)?;
cw.queue_g_string(label_style, format!("{pb:<10}"))?;
1
} else {
16
})
}
fn write_line_git_status<W: Write>(
&self,
cw: &mut CropWriter<W>,
line: &TreeLine,
selected: bool,
) -> Result<usize, termimad::Error> {
let (style, char) = if line.is_selectable() {
match line.git_status.map(|s| s.status) {
Some(Status::CURRENT) => (&self.skin.git_status_current, ' '),
Some(Status::WT_NEW) => (&self.skin.git_status_new, 'N'),
Some(Status::CONFLICTED) => (&self.skin.git_status_conflicted, 'C'),
Some(Status::WT_MODIFIED) => (&self.skin.git_status_modified, 'M'),
Some(Status::IGNORED) => (&self.skin.git_status_ignored, 'I'),
None => (&self.skin.tree, ' '),
_ => (&self.skin.git_status_other, '?'),
}
} else {
(&self.skin.tree, ' ')
};
cond_bg!(git_style, self, selected, style);
cw.queue_char(git_style, char)?;
Ok(0)
}
fn write_date<W: Write>(
&self,
cw: &mut CropWriter<W>,
seconds: i64,
selected: bool,
) -> Result<usize, termimad::Error> {
if let LocalResult::Single(date_time) = Local.timestamp_opt(seconds, 0) {
cond_bg!(date_style, self, selected, self.skin.dates);
cw.queue_g_string(
date_style,
date_time
.format(self.tree.options.date_time_format)
.to_string(),
)?;
}
Ok(1)
}
fn write_branch<W: Write>(
&self,
cw: &mut CropWriter<W>,
line_index: usize,
line: &TreeLine,
selected: bool,
staged: bool,
) -> Result<usize, ProgramError> {
cond_bg!(branch_style, self, selected, self.skin.tree);
let mut branch = String::new();
for depth in 0..line.depth {
branch.push_str(if line.left_branches[depth as usize] {
if self.tree.has_branch(line_index + 1, depth as usize) {
if depth == line.depth - 1 {
if staged { "├◍─" } else { "├──" }
} else {
"│ "
}
} else if staged {
"└◍─"
} else {
"└──"
}
} else {
" "
});
}
if !branch.is_empty() {
cw.queue_g_string(branch_style, branch)?;
}
Ok(0)
}
fn write_line_stage_mark<W: Write>(
cw: &mut CropWriter<W>,
style: &CompoundStyle,
staged: bool,
) -> Result<usize, termimad::Error> {
Ok(if staged {
cw.queue_char(style, '◍')?; 0
} else {
1
})
}
fn write_line_label<W: Write>(
&self,
cw: &mut CropWriter<W>,
line: &TreeLine,
style: &CompoundStyle,
pattern_object: PatternObject,
selected: bool,
) -> Result<usize, ProgramError> {
cond_bg!(char_match_style, self, selected, self.skin.char_match);
if let Some(icon) = line.icon {
cw.queue_char(style, icon)?;
cw.queue_char(style, ' ')?;
cw.queue_char(style, ' ')?;
}
if pattern_object.subpath {
if self.tree.options.show_matching_characters_on_path_searches && line.unlisted == 0 {
let name_match = self.tree.options.pattern.pattern.find_string(&line.subpath);
let mut path_ms =
MatchedString::new(name_match, &line.subpath, style, char_match_style);
let name_ms = path_ms.split_on_last('/');
cond_bg!(parent_style, self, selected, self.skin.parent);
if let Some(name_ms) = name_ms {
path_ms.base_style = parent_style;
let allowed = cw.allowed - 2.min(cw.allowed);
let tail_len = name_ms.width();
if tail_len < allowed {
let path_width = path_ms.width();
if path_width > 1 && path_width + tail_len > allowed + 1 {
cw.queue_char(style, '…')?;
path_ms.cut_left_to_fit(allowed - tail_len - 1);
}
path_ms.queue_on(cw)?;
}
name_ms.queue_on(cw)?;
} else {
path_ms.queue_on(cw)?;
}
} else {
cw.queue_str(style, &line.name)?;
}
} else {
let name_match = self.tree.options.pattern.pattern.find_string(&line.name);
let matched_string =
MatchedString::new(name_match, &line.name, style, char_match_style);
matched_string.queue_on(cw)?;
}
match &line.line_type {
TreeLineType::Dir => {
if line.unlisted > 0 {
cw.queue_str(style, " …")?;
}
}
TreeLineType::BrokenSymLink(direct_path) => {
cw.queue_str(style, " -> ")?;
cond_bg!(error_style, self, selected, self.skin.file_error);
cw.queue_str(error_style, direct_path)?;
}
TreeLineType::SymLink {
final_is_dir,
direct_target,
..
} => {
cw.queue_str(style, " -> ")?;
let target_style = if *final_is_dir {
&self.skin.directory
} else {
&self.skin.file
};
cond_bg!(target_style, self, selected, target_style);
cw.queue_str(target_style, direct_target)?;
}
_ => {}
}
Ok(1)
}
fn write_content_extract<W: Write>(
&self,
cw: &mut CropWriter<W>,
extract: &ContentMatch,
selected: bool,
) -> Result<(), ProgramError> {
cond_bg!(extract_style, self, selected, self.skin.content_extract);
cond_bg!(match_style, self, selected, self.skin.content_match);
cw.queue_str(extract_style, " ")?;
if extract.needle_start > 0 {
cw.queue_str(extract_style, &extract.extract[0..extract.needle_start])?;
}
cw.queue_str(
match_style,
&extract.extract[extract.needle_start..extract.needle_end],
)?;
if extract.needle_end < extract.extract.len() {
cw.queue_str(extract_style, &extract.extract[extract.needle_end..])?;
}
Ok(())
}
pub fn write_root_line<W: Write>(
&self,
cw: &mut CropWriter<W>,
selected: bool,
) -> Result<(), ProgramError> {
cond_bg!(style, self, selected, self.skin.directory);
let line = &self.tree.lines[0];
if self.tree.options.show_sizes {
if let Some(s) = line.sum {
cw.queue_g_string(style, format!("{:>4} ", file_size::fit_4(s.to_size())))?;
}
}
let title = line.path.to_string_lossy();
let title_len = UnicodeWidthStr::width(title.as_ref());
if title_len > cw.allowed {
cw.queue_char(style, '…')?;
let mut width = 0;
let mut bytes = 0;
for c in title.chars().rev() {
let char_width = c.width().unwrap_or(0);
if width + char_width > cw.allowed - 1 {
break;
}
width += char_width;
bytes += c.len_utf8();
}
let right_cropped_title = &title[title.len() - bytes..];
cw.queue_str(style, right_cropped_title)?;
} else {
cw.queue_str(style, &title)?;
}
if self.in_app && !cw.is_full() {
if let ComputationResult::Done(git_status) = &self.tree.git_status {
let git_status_display = GitStatusDisplay::from(git_status, self.skin, cw.allowed);
git_status_display.write(cw, selected)?;
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
if self.tree.options.show_root_fs {
if let Some(mount) = line.mount() {
let fs_space_display =
crate::filesystems::MountSpaceDisplay::from(&mount, self.skin, cw.allowed);
fs_space_display.write(cw, selected)?;
}
}
self.extend_line_bg(cw, selected)?;
}
Ok(())
}
pub fn extend_line_bg<W: Write>(
&self,
cw: &mut CropWriter<W>,
selected: bool,
) -> Result<(), ProgramError> {
if self.in_app && !cw.is_full() {
let style = if selected {
&self.skin.selected_line
} else {
&self.skin.default
};
cw.fill(style, &SPACE_FILLING)?;
}
Ok(())
}
pub fn write_on<W: Write>(
&self,
f: &mut W,
) -> Result<(), ProgramError> {
#[cfg(not(any(target_family = "windows", target_os = "android")))]
let perm_writer = super::PermWriter::for_tree(self.skin, self.tree);
let tree = self.tree;
let total_size = tree.total_sum();
let scrollbar = if self.in_app {
termimad::compute_scrollbar(
tree.scroll,
tree.lines.len() - 1, self.area.height - 1, self.area.top + 1,
)
} else {
None
};
if self.in_app {
f.queue(cursor::MoveTo(self.area.left, self.area.top))?;
}
let mut cw = CropWriter::new(f, self.area.width as usize);
let pattern_object = tree.options.pattern.pattern.object();
self.write_root_line(&mut cw, self.in_app && tree.selection == 0)?;
self.skin.queue_reset(f)?;
let visible_cols: Vec<Col> = tree
.options
.cols_order
.iter()
.filter(|col| col.is_visible(tree, self.app_state))
.copied()
.collect();
let count_len = if tree.options.show_counts {
tree.lines
.iter()
.skip(1) .map(|l| l.sum.map_or(0, FileSum::to_count))
.max()
.map(|c| format_count(c).len())
.unwrap_or(0)
} else {
0
};
let date_len = if tree.options.show_dates {
let date_time: DateTime<Local> = Local::now();
date_time
.format(tree.options.date_time_format)
.to_string()
.len()
} else {
0 };
for y in 1..self.area.height {
if self.in_app {
f.queue(cursor::MoveTo(self.area.left, y + self.area.top))?;
} else {
write!(f, "\r\n")?;
}
let mut line_index = y as usize;
if line_index > 0 {
line_index += tree.scroll;
}
let mut selected = false;
let mut cw = CropWriter::new(f, self.area.width as usize);
let cw = &mut cw;
if line_index < tree.lines.len() {
let line = &tree.lines[line_index];
selected = self.in_app && line_index == tree.selection;
let label_style = self.label_style(line, selected);
let mut in_branch = false;
let space_style = if selected {
&self.skin.selected_line
} else {
&self.skin.default
};
if visible_cols[0].needs_left_margin() {
cw.queue_char(space_style, ' ')?;
}
let staged = self
.app_state
.is_some_and(|a| a.stage.contains(&line.path));
for col in &visible_cols {
let void_len = match col {
Col::Mark => Self::write_line_selection_mark(cw, &label_style, selected)?,
Col::Git => self.write_line_git_status(cw, line, selected)?,
Col::Branch => {
in_branch = true;
self.write_branch(cw, line_index, line, selected, staged)?
}
Col::DeviceId => {
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
0
}
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
self.write_line_device_id(cw, line, selected)?
}
Col::Permission => {
#[cfg(any(target_family = "windows", target_os = "android"))]
{
0
}
#[cfg(not(any(target_family = "windows", target_os = "android")))]
perm_writer.write_permissions(cw, line, selected)?
}
Col::Date => {
if let Some(seconds) = line.sum.and_then(FileSum::to_valid_seconds) {
self.write_date(cw, seconds, selected)?
} else {
date_len + 1
}
}
Col::Size => {
if tree.options.sort.prevent_deep_display() {
self.write_line_size_with_bar(
cw,
line,
&label_style,
total_size,
selected,
)?
} else {
Self::write_line_size(cw, line, &label_style, selected)?
}
}
Col::Count => self.write_line_count(cw, line, count_len, selected)?,
Col::Staged => Self::write_line_stage_mark(cw, &label_style, staged)?,
Col::Name => {
in_branch = false;
self.write_line_label(cw, line, &label_style, pattern_object, selected)?
}
};
if in_branch && void_len > 2 {
cond_bg!(void_style, self, selected, self.skin.tree);
cw.repeat(void_style, &BRANCH_FILLING, void_len)?;
} else {
cond_bg!(void_style, self, selected, self.skin.default);
cw.repeat(void_style, &SPACE_FILLING, void_len)?;
}
}
if cw.allowed > 8 && pattern_object.content {
let extract = tree
.options
.pattern
.pattern
.find_content(&line.path, cw.allowed - 2);
if let Some(extract) = extract {
self.write_content_extract(cw, &extract, selected)?;
}
}
}
self.extend_line_bg(cw, selected)?;
self.skin.queue_reset(f)?;
if self.in_app {
if let Some((sctop, scbottom)) = scrollbar {
f.queue(cursor::MoveTo(self.area.left + self.area.width - 1, y))?;
let style = if sctop <= y && y <= scbottom {
&self.skin.scrollbar_thumb
} else {
&self.skin.scrollbar_track
};
style.queue_str(f, "▐")?;
}
}
}
if !self.in_app {
write!(f, "\r\n")?;
}
Ok(())
}
}