Skip to main content

fsearch/
output.rs

1// File: src\output.rs
2// Author: Hadi Cahyadi <cumulus13@gmail.com>
3// Date: 2026-05-11
4// Description:
5// License: MIT
6
7//! Coloured terminal output for both search and duplicate results.
8
9use crate::colors::{bold_hex, hex_color};
10use crate::config::Config;
11use crate::duplicates::{DuplicateGroup, DuplicateSummary};
12use crate::searcher::SearchMatch;
13use colored::Colorize;
14use std::path::Path;
15use std::time::Duration;
16
17pub struct Printer<'a> {
18    cfg: &'a Config,
19}
20
21impl<'a> Printer<'a> {
22    pub fn new(cfg: &'a Config) -> Self {
23        Self { cfg }
24    }
25
26    // ── Banners / status ──────────────────────────────────────────────────────
27
28    pub fn print_banner(&self) {
29        eprintln!(
30            "⚡ {} v{}",
31            bold_hex("fsearch", &self.cfg.color_info),
32            bold_hex(env!("CARGO_PKG_VERSION"), &self.cfg.color_count),
33        );
34    }
35
36    pub fn print_searching(&self, dirs: &[&Path], pattern: &str) {
37        let dir_list = dirs
38            .iter()
39            .map(|d| {
40                format!(
41                    "{}",
42                    bold_hex(d.display().to_string(), &self.cfg.color_path)
43                )
44            })
45            .collect::<Vec<_>>()
46            .join(hex_color(", ", "#888888").to_string().as_str());
47        eprintln!(
48            "🔍 Searching [{}] for {}",
49            dir_list,
50            bold_hex(pattern, &self.cfg.color_pattern),
51        );
52    }
53
54    pub fn print_scanning_dups(&self, dirs: &[&Path], mode: &str, algo: &str) {
55        let dir_list = dirs
56            .iter()
57            .map(|d| {
58                format!(
59                    "{}",
60                    bold_hex(d.display().to_string(), &self.cfg.color_path)
61                )
62            })
63            .collect::<Vec<_>>()
64            .join(hex_color(", ", "#888888").to_string().as_str());
65        eprintln!(
66            "🔎 Scanning [{}]  [mode: {}  algo: {}]",
67            dir_list,
68            bold_hex(mode, &self.cfg.color_pattern),
69            bold_hex(algo, &self.cfg.color_info),
70        );
71    }
72
73    // ── Search results ────────────────────────────────────────────────────────
74
75    pub fn print_results(
76        &self,
77        results: &[SearchMatch],
78        _search_in_files: bool,
79        elapsed: Duration,
80    ) {
81        if results.is_empty() {
82            println!(
83                "\n🕳️  {}\n",
84                bold_hex("No results found.", &self.cfg.color_warn)
85            );
86            return;
87        }
88
89        let count = results.len().to_string();
90        let ms = elapsed.as_millis();
91
92        println!(
93            "\n{} {}  {} {} {}",
94            "📋".bold(),
95            bold_hex("FOUND:", &self.cfg.color_header),
96            bold_hex(&count, &self.cfg.color_count),
97            hex_color("result(s)", &self.cfg.color_info),
98            hex_color(format!("[{ms}ms]"), "#888888"),
99        );
100        println!();
101
102        let zfill = count.len();
103        for (idx, item) in results.iter().enumerate() {
104            let num = bold_hex(
105                format!("{:0>w$}", idx + 1, w = zfill),
106                &self.cfg.color_index,
107            );
108            match item {
109                SearchMatch::Path(path) => {
110                    println!(
111                        "{}. {}",
112                        num,
113                        bold_hex(path.display().to_string(), &self.cfg.color_path)
114                    );
115                }
116                SearchMatch::Content { path, lines } => {
117                    println!(
118                        "{}. {}",
119                        num,
120                        bold_hex(path.display().to_string(), &self.cfg.color_path)
121                    );
122                    for (line_num, line_text) in lines {
123                        println!(
124                            "   {} {} {}",
125                            bold_hex(format!("{:>5}", line_num), &self.cfg.color_line_num),
126                            hex_color("│", "#555555"),
127                            hex_color(line_text.trim_end(), &self.cfg.color_line_text),
128                        );
129                    }
130                }
131            }
132        }
133        println!();
134    }
135
136    // ── Duplicate results ─────────────────────────────────────────────────────
137
138    pub fn print_duplicates(
139        &self,
140        groups: &[DuplicateGroup],
141        summary: &DuplicateSummary,
142        elapsed: Duration,
143    ) {
144        let ms = elapsed.as_millis();
145
146        if groups.is_empty() {
147            println!(
148                "\n✅  {} {}\n",
149                bold_hex("No duplicates found.", &self.cfg.color_info),
150                hex_color(format!("[{ms}ms]"), "#888888"),
151            );
152            self.print_dup_summary(summary);
153            return;
154        }
155
156        println!(
157            "\n{} {}  {} {} {}",
158            "🔁".bold(),
159            bold_hex("DUPLICATE GROUPS:", &self.cfg.color_header),
160            bold_hex(groups.len().to_string(), &self.cfg.color_count),
161            hex_color("group(s)", &self.cfg.color_info),
162            hex_color(format!("[{ms}ms]"), "#888888"),
163        );
164        println!();
165
166        let zfill = groups.len().to_string().len();
167        for (idx, group) in groups.iter().enumerate() {
168            let num = bold_hex(
169                format!("{:0>w$}", idx + 1, w = zfill),
170                &self.cfg.color_dup_group,
171            );
172            // Truncate hash for display (16 chars is enough to distinguish)
173            let hash_preview = &group.hash[..group.hash.len().min(16)];
174            println!(
175                "{}. {} {}  {} {}  {} {}",
176                num,
177                hex_color("size:", "#888888"),
178                bold_hex(group.size_human(), &self.cfg.color_dup_size),
179                hex_color("wasted:", "#888888"),
180                bold_hex(group.wasted_human(), &self.cfg.color_warn),
181                hex_color("hash:", "#888888"),
182                hex_color(hash_preview, "#666666"),
183            );
184
185            // Individual paths
186            for (pi, path) in group.paths.iter().enumerate() {
187                let marker = if pi == 0 { "  ◉" } else { "  ○" };
188                println!(
189                    "{}  {}",
190                    hex_color(marker, &self.cfg.color_dup_group),
191                    hex_color(path.display().to_string(), &self.cfg.color_dup_path),
192                );
193            }
194            println!();
195        }
196
197        self.print_dup_summary(summary);
198    }
199
200    fn print_dup_summary(&self, s: &DuplicateSummary) {
201        println!(
202            "{}  scanned {} file(s) in {} dir(s)  ·  {} group(s)  ·  {} duplicate(s)  ·  {} wasted",
203            bold_hex("📊 Summary:", &self.cfg.color_header),
204            bold_hex(s.files_scanned.to_string(), &self.cfg.color_count),
205            bold_hex(s.dirs_scanned.to_string(), &self.cfg.color_count),
206            bold_hex(s.groups_found.to_string(), &self.cfg.color_count),
207            bold_hex(s.duplicate_files.to_string(), &self.cfg.color_count),
208            bold_hex(s.wasted_human(), &self.cfg.color_warn),
209        );
210        println!();
211    }
212
213    // ── Diagnostics ───────────────────────────────────────────────────────────
214
215    pub fn print_error(&self, msg: &str) {
216        eprintln!("❌ {} {}", bold_hex("Error:", &self.cfg.color_error), msg);
217    }
218
219    pub fn print_warn(&self, msg: &str) {
220        eprintln!("⚠️  {}", hex_color(msg, &self.cfg.color_warn));
221    }
222
223    pub fn print_info(&self, msg: &str) {
224        eprintln!("ℹ️  {}", hex_color(msg, &self.cfg.color_info));
225    }
226}