disky 0.11.0

Fast macOS disk analyzer and cleanup CLI in Rust — ncdu / dust / GrandPerspective alternative with snapshot diff, agent-native JSON, and Trash-restorable cleanup.
Documentation
use anyhow::Result;
use duckdb::Connection;

#[derive(Debug, Clone)]
pub struct DirEntry {
    pub path: String,
    pub name: String,
    pub is_dir: bool,
    pub size: i64,
    pub depth: usize,
    pub expanded: bool,
    pub loaded: bool,
    pub children: Vec<DirEntry>,
}

pub fn load_children(
    conn: &Connection,
    parent_path: &str,
    parent_depth: usize,
) -> Result<Vec<DirEntry>> {
    let child_depth = parent_depth + 1;
    let like_pat = format!("{}/%", parent_path);

    let mut stmt = conn.prepare(
        "SELECT name, path, is_dir, size
         FROM files
         WHERE depth = ? AND path LIKE ?
         ORDER BY is_dir DESC, size DESC
         LIMIT 500",
    )?;

    let mut children: Vec<DirEntry> = stmt
        .query_map(duckdb::params![child_depth as i32, like_pat], |row| {
            Ok(DirEntry {
                name: row.get(0)?,
                path: row.get(1)?,
                is_dir: row.get(2)?,
                size: row.get(3)?,
                depth: child_depth,
                expanded: false,
                loaded: false,
                children: vec![],
            })
        })?
        .flatten()
        .collect();

    // compute accumulated sizes for dirs
    for entry in children.iter_mut() {
        if entry.is_dir {
            let acc_like = format!("{}/%", entry.path);
            let acc: i64 = conn
                .query_row(
                    "SELECT COALESCE(SUM(size), 0) FROM files WHERE path LIKE ? AND is_dir = false",
                    duckdb::params![acc_like],
                    |r| r.get(0),
                )
                .unwrap_or(0);
            entry.size = acc;
        }
    }

    // re-sort after computing dir sizes
    children.sort_by_key(|b| std::cmp::Reverse(b.size));
    Ok(children)
}

pub fn load_root(conn: &Connection, root: &str) -> Result<DirEntry> {
    let total: i64 = conn.query_row(
        "SELECT COALESCE(SUM(size), 0) FROM files WHERE is_dir = false",
        [],
        |r| r.get(0),
    )?;

    let mut root_entry = DirEntry {
        path: root.to_string(),
        name: root.to_string(),
        is_dir: true,
        size: total,
        depth: 0,
        expanded: true,
        loaded: true,
        children: vec![],
    };

    root_entry.children = load_children(conn, root, 0)?;
    Ok(root_entry)
}