dua-cli 2.34.0

A tool to conveniently learn about the disk usage of directories, fast!
Documentation
use crate::interactive::path_of;
use dua::traverse::{Tree, TreeIndex};
use itertools::Itertools;
use petgraph::Direction;
use std::time::SystemTime;
use std::{cmp::Ordering, path::PathBuf};
use unicode_segmentation::UnicodeSegmentation;

#[derive(Default, Debug, Copy, Clone, PartialOrd, PartialEq, Eq)]
pub enum SortMode {
    #[default]
    SizeDescending,
    SizeAscending,
    MTimeDescending,
    MTimeAscending,
    CountDescending,
    CountAscending,
    NameDescending,
    NameAscending,
}

impl SortMode {
    pub fn toggle_size(&mut self) {
        use SortMode::*;
        *self = match self {
            SizeDescending => SizeAscending,
            SizeAscending => SizeDescending,
            _ => SizeDescending,
        }
    }

    pub fn toggle_mtime(&mut self) {
        use SortMode::*;
        *self = match self {
            MTimeAscending => MTimeDescending,
            MTimeDescending => MTimeAscending,
            _ => MTimeDescending,
        }
    }

    pub fn toggle_count(&mut self) {
        use SortMode::*;
        *self = match self {
            CountAscending => CountDescending,
            CountDescending => CountAscending,
            _ => CountDescending,
        }
    }

    pub fn toggle_name(&mut self) {
        use SortMode::*;
        *self = match self {
            NameAscending => NameDescending,
            NameDescending => NameAscending,
            _ => NameAscending,
        }
    }
}

pub struct EntryDataBundle {
    pub index: TreeIndex,
    pub name: PathBuf,
    pub size: u128,
    pub mtime: SystemTime,
    pub entry_count: Option<u64>,
    pub is_dir: bool,
    pub exists: bool,
}

pub enum EntryCheck {
    PossiblyCostlyLstat,
    Disabled,
}

impl EntryCheck {
    pub fn new(is_scanning: bool, allow_entry_check: bool) -> Self {
        if allow_entry_check && !is_scanning {
            EntryCheck::PossiblyCostlyLstat
        } else {
            EntryCheck::Disabled
        }
    }
}

/// Note that with `glob_root` present, we will not obtain metadata anymore as we might be seeing
/// a lot of entries. That way, displaying 250k entries is no problem.
pub fn sorted_entries(
    tree: &Tree,
    node_idx: TreeIndex,
    sorting: SortMode,
    glob_root: Option<TreeIndex>,
    check: EntryCheck,
) -> Vec<EntryDataBundle> {
    use SortMode::*;
    fn cmp_count(l: &EntryDataBundle, r: &EntryDataBundle) -> Ordering {
        l.entry_count
            .cmp(&r.entry_count)
            .then_with(|| l.name.cmp(&r.name))
    }
    fn cmp_name(l: &EntryDataBundle, r: &EntryDataBundle) -> Ordering {
        if l.is_dir && !r.is_dir {
            Ordering::Less
        } else if !l.is_dir && r.is_dir {
            Ordering::Greater
        } else {
            l.name.cmp(&r.name)
        }
    }
    tree.neighbors_directed(node_idx, Direction::Outgoing)
        .filter_map(|idx| {
            tree.node_weight(idx).map(|entry| {
                let use_glob_path = glob_root.is_some_and(|glob_root| glob_root == node_idx);
                let (path, exists, is_dir) = {
                    let path = path_of(tree, idx, glob_root);
                    if matches!(check, EntryCheck::Disabled) || glob_root == Some(node_idx) {
                        (path, true, entry.is_dir)
                    } else {
                        let meta = path.symlink_metadata();
                        (path, meta.is_ok(), meta.ok().is_some_and(|m| m.is_dir()))
                    }
                };
                EntryDataBundle {
                    index: idx,
                    name: if use_glob_path {
                        path
                    } else {
                        entry.name.clone()
                    },
                    size: entry.size,
                    mtime: entry.mtime,
                    entry_count: entry.entry_count,
                    exists,
                    is_dir,
                }
            })
        })
        .sorted_by(|l, r| match sorting {
            SizeDescending => r.size.cmp(&l.size),
            SizeAscending => l.size.cmp(&r.size),
            MTimeAscending => l.mtime.cmp(&r.mtime),
            MTimeDescending => r.mtime.cmp(&l.mtime),
            CountAscending => cmp_count(l, r),
            CountDescending => cmp_count(l, r).reverse(),
            NameAscending => cmp_name(l, r),
            NameDescending => cmp_name(l, r).reverse(),
        })
        .collect()
}

pub fn fit_string_graphemes_with_ellipsis(
    s: impl Into<String>,
    path_graphemes_count: usize,
    mut desired_graphemes: usize,
) -> (String, usize) {
    const ELLIPSIS: usize = 1;
    const MIN_GRAPHEMES_ON_SIDE: usize = 1;
    const MIN_LEN: usize = ELLIPSIS + MIN_GRAPHEMES_ON_SIDE;
    const USE_EXTENDED: bool = true;

    let s = s.into();
    desired_graphemes = desired_graphemes.max(MIN_LEN);

    debug_assert!(
        path_graphemes_count == s.graphemes(USE_EXTENDED).count(),
        "input grapheme count is actually correct"
    );

    let gc = path_graphemes_count;
    if gc <= desired_graphemes {
        return (s, gc);
    }

    let mut n = String::with_capacity(desired_graphemes);
    let to_be_removed = gc - desired_graphemes + ELLIPSIS;
    let gmi = s.graphemes(USE_EXTENDED);

    n.push('');
    n.extend(gmi.skip(to_be_removed));
    (n, desired_graphemes)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fit_string_inputs() {
        assert_eq!(
            ("aaa".into(), 3),
            fit_string_graphemes_with_ellipsis("aaa", 3, 4)
        );
        assert_eq!(
            ("…a".to_string(), 2),
            fit_string_graphemes_with_ellipsis("abbbba", 6, 1),
            "even amount of chars, desired too small"
        );
        assert_eq!(
            ("…ca".to_string(), 3),
            fit_string_graphemes_with_ellipsis("abbbbca", 7, 3),
            "uneven amount of chars, desired too small"
        );
        assert_eq!(
            ("… a".to_string(), 3),
            fit_string_graphemes_with_ellipsis("a    a", 6, 3),
            "spaces are counted as graphemes, too"
        );
    }
}