dumpfs 0.1.0

A tool for dumping codebase information for LLMs efficiently and effectively
Documentation
use super::{error::FsFmtResult, opts};
use crate::fs::nodes;
use askama::Template;
use chrono::{DateTime, Local};
use std::fmt::Write;

#[derive(Template)]
#[template(path = "text-template.md")]
pub struct FsTextFmt<'a> {
    root: &'a nodes::FsDirectoryNode,
    opts: &'a opts::FsFmtOpts,
}

impl<'a> FsTextFmt<'a> {
    pub fn new(root: &'a nodes::FsDirectoryNode, opts: &'a opts::FsFmtOpts) -> Self {
        Self { root, opts }
    }

    pub fn get_tree_outline(&self) -> FsFmtResult<String> {
        let mut buf = ".".to_string();
        self.write_tree_outline(&mut buf, self.root, 0)?;
        Ok(buf)
    }

    pub fn format_metadata(&self, meta: &nodes::FsMetadata) -> String {
        let mut parts = Vec::new();
        if self.opts.show_permissions {
            // Simple octal representation
            parts.push(format!("perms={:04o}", meta.permissions & 0o7777));
        }
        if self.opts.show_size && meta.file_type != nodes::FsFileType::Directory {
            parts.push(format!("size={}", meta.size));
        }
        if self.opts.show_modified {
            let datetime: DateTime<Local> = meta.modified.into();
            parts.push(format!("modified={}", datetime.format("%Y-%m-%d %H:%M:%S")));
        }
        if !parts.is_empty() {
            format!(" `({})`", parts.join(", "))
        } else {
            String::new()
        }
    }

    fn write_tree_outline<W: Write>(
        &self,
        writer: &mut W,
        node: &nodes::FsDirectoryNode,
        depth: usize,
    ) -> FsFmtResult {
        // Sort contents: directories first, then files, all alphabetically
        let mut dirs = Vec::new();
        let mut files = Vec::new();

        for child in &node.contents {
            match child {
                nodes::FsNode::Directory(dir) => dirs.push(dir),
                _ => files.push(child),
            }
        }

        dirs.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
        files.sort_by_key(|a| a.name().to_lowercase());

        // Keep track if this is last item at each level for proper tree lines
        let total_items = dirs.len() + files.len();
        let mut item_count = 0;

        // Process directories recursively
        for dir in dirs {
            item_count += 1;
            let is_last = item_count == total_items;
            self.write_tree_prefix(writer, depth, is_last)?;
            writeln!(writer, "{}/", dir.name)?;
            self.write_tree_outline(writer, dir, depth + 1)?;
        }

        // List files
        for file in files {
            item_count += 1;
            let is_last = item_count == total_items;
            self.write_tree_prefix(writer, depth, is_last)?;
            match file {
                nodes::FsNode::File(f) => writeln!(writer, "{}", f.name)?,
                nodes::FsNode::Binary(b) => writeln!(writer, "{} [Binary]", b.name)?,
                nodes::FsNode::Symlink(s) => {
                    let target_info = format!(" -> {}", s.target);
                    writeln!(writer, "{} [Symlink]{}", s.name, target_info)?;
                }
                _ => {}
            }
        }

        Ok(())
    }

    fn write_tree_prefix<W: Write>(
        &self,
        writer: &mut W,
        depth: usize,
        is_last: bool,
    ) -> FsFmtResult {
        for _ in 0..depth {
            write!(writer, "")?;
        }
        if depth > 0 {
            if is_last {
                write!(writer, "└── ")?;
            } else {
                write!(writer, "├── ")?;
            }
        }
        Ok(())
    }
}