spacehog 0.1.7

A simple utility for finding large files on your system.
Documentation
#![allow(clippy::cast_possible_truncation)]
use clap::Parser;
use crossterm::{
    cursor,
    terminal::{self, ClearType},
    ExecutableCommand, QueueableCommand,
};
use std::{
    io::{self, Write},
    path::PathBuf,
};

use spacehog::{find_top_n_largest_files, FileSize};

#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
    #[arg(default_value = ".")]
    path: String,

    #[arg(short, default_value_t = 5)]
    number: usize,

    #[arg(long, default_value_t = false)]
    #[arg(help = "Include hidden files and directories")]
    hidden: bool,
}

fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let ignore_hidden = !args.hidden;
    let mut app = App::new(std::io::stdout());

    let rx = find_top_n_largest_files(&args.path, args.number, ignore_hidden)?;
    while let Ok(results) = rx.recv() {
        if !results.is_empty() {
            app.update(results);
            app.render()?;
        }
    }
    app.close()?;

    Ok(())
}

struct App<Out: Write> {
    out: Out,
    files: Vec<(FileSize, PathBuf)>,
}

impl<Out: Write> App<Out> {
    fn new(out: Out) -> Self {
        Self {
            out,
            files: Vec::new(),
        }
    }

    fn update(&mut self, files: Vec<(FileSize, PathBuf)>) {
        self.files = files;
    }

    fn render(&mut self) -> io::Result<()> {
        self.out.queue(cursor::SavePosition)?;
        self.out.queue(terminal::Clear(ClearType::FromCursorDown))?;

        let mut buf = Vec::new();
        writeln!(&mut buf, "*** Top {} largest files ***", self.files.len())?;
        for (size, path) in &self.files {
            writeln!(&mut buf, "{} {}", size, path.display())?;
        }
        self.out.write_all(&buf)?;

        self.out.queue(cursor::RestorePosition)?;
        self.out.flush()
    }

    fn close(&mut self) -> io::Result<()> {
        if self.files.is_empty() {
            writeln!(self.out, "No files found.")?;
        } else {
            let row = self.files.len() + 1;
            self.out.execute(cursor::MoveDown(row as u16))?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::{App, Args};
    use clap::Parser;
    use std::path::PathBuf;

    #[test]
    fn cli_args_can_be_parsed_without_panicing() {
        Args::parse();
    }

    #[test]
    fn test_close_with_no_files() {
        let mut output = Vec::new();
        let mut app = App::new(&mut output);

        app.close().unwrap();

        let output_str = String::from_utf8(output).unwrap();
        assert_eq!(output_str, "No files found.\n");
    }

    #[test]
    fn test_close_with_files_moves_cursor_to_row_after_last_file() {
        let mut output = Vec::new();
        let mut app = App::new(&mut output);

        app.update(vec![
            (2048.into(), PathBuf::from("file1.txt")),
            (1024.into(), PathBuf::from("file2.txt")),
        ]);
        app.close().unwrap();

        let output_str = String::from_utf8(output).unwrap();
        assert!(output_str.ends_with("\x1B[3B"));
    }
}