Skip to main content

st/formatters/
hex.rs

1use super::{Formatter, PathDisplayMode, StreamingFormatter};
2use crate::emoji_mapper;
3use crate::scanner::{FileNode, TreeStats};
4use anyhow::Result;
5use std::io::Write;
6use std::path::Path;
7
8pub struct HexFormatter {
9    pub use_color: bool,
10    pub no_emoji: bool,
11    pub show_ignored: bool,
12    pub path_mode: PathDisplayMode,
13    pub show_filesystems: bool,
14}
15
16impl HexFormatter {
17    pub fn new(
18        use_color: bool,
19        no_emoji: bool,
20        show_ignored: bool,
21        path_mode: PathDisplayMode,
22        show_filesystems: bool,
23    ) -> Self {
24        Self {
25            use_color,
26            no_emoji,
27            show_ignored,
28            path_mode,
29            show_filesystems,
30        }
31    }
32
33    /// Get context-aware emoji based on file type and node properties
34    /// Returns different emojis for empty files, empty directories, and locked directories
35    fn get_file_emoji(&self, node: &FileNode) -> &'static str {
36        emoji_mapper::get_file_emoji(node, self.no_emoji)
37    }
38
39    fn format_node(&self, node: &FileNode, root_path: &Path) -> String {
40        let depth_hex = format!("{:x}", node.depth);
41        let perms_hex = format!("{:03x}", node.permissions);
42        let uid_hex = format!("{:04x}", node.uid);
43        let gid_hex = format!("{:04x}", node.gid);
44        let size_hex = if node.is_dir {
45            format!("{:08x}", 0)
46        } else {
47            format!("{:08x}", node.size)
48        };
49        let time_hex = format!(
50            "{:08x}",
51            node.modified
52                .duration_since(std::time::UNIX_EPOCH)
53                .unwrap_or_default()
54                .as_secs()
55        );
56
57        let emoji = self.get_file_emoji(node);
58
59        // Add filesystem indicator if enabled
60        let fs_indicator = if self.show_filesystems && node.filesystem_type.should_show_by_default()
61        {
62            format!("{} ", node.filesystem_type.to_char())
63        } else {
64            String::new()
65        };
66
67        // Get name based on path mode
68        let name = match self.path_mode {
69            PathDisplayMode::Off => node
70                .path
71                .file_name()
72                .unwrap_or(node.path.as_os_str())
73                .to_string_lossy()
74                .to_string(),
75            PathDisplayMode::Relative => {
76                if node.path == root_path {
77                    node.path
78                        .file_name()
79                        .unwrap_or(node.path.as_os_str())
80                        .to_string_lossy()
81                        .to_string()
82                } else {
83                    node.path
84                        .strip_prefix(root_path)
85                        .unwrap_or(&node.path)
86                        .to_string_lossy()
87                        .to_string()
88                }
89            }
90            PathDisplayMode::Full => node.path.display().to_string(),
91        };
92
93        // Add brackets for permission denied or ignored
94        let display_name = if node.permission_denied || node.is_ignored {
95            format!("[{}]", name)
96        } else {
97            name
98        };
99
100        // Add git branch for directories
101        let display_name = if let Some(ref branch) = node.git_branch {
102            format!("{} [{}]", display_name, branch)
103        } else {
104            display_name
105        };
106
107        // Add search matches if present
108        let display_name_with_search = if let Some(matches) = &node.search_matches {
109            if matches.total_count > 0 {
110                // Show first match position and total count (in hex for consistency)
111                let (line, col) = matches.first_match;
112                let truncated_indicator = if matches.truncated { ",TRUNCATED" } else { "" };
113
114                if matches.total_count > 1 {
115                    format!(
116                        "{} [SEARCH:L{:x}:C{:x},{:x}x{}]",
117                        display_name, line, col, matches.total_count, truncated_indicator
118                    )
119                } else {
120                    format!("{} [SEARCH:L{:x}:C{:x}]", display_name, line, col)
121                }
122            } else {
123                display_name
124            }
125        } else {
126            display_name
127        };
128
129        if self.use_color {
130            // ANSI color codes
131            const CYAN: &str = "\x1b[36m";
132            const YELLOW: &str = "\x1b[33m";
133            const MAGENTA: &str = "\x1b[35m";
134            const GREEN: &str = "\x1b[32m";
135            const BLUE: &str = "\x1b[34m";
136            const RESET: &str = "\x1b[0m";
137
138            format!(
139                "{}{}{} {}{}{} {}{} {}{} {}{}{} {}{}{} {}{} {}",
140                CYAN,
141                depth_hex,
142                RESET,
143                YELLOW,
144                perms_hex,
145                RESET,
146                MAGENTA,
147                uid_hex,
148                gid_hex,
149                RESET,
150                GREEN,
151                size_hex,
152                RESET,
153                BLUE,
154                time_hex,
155                RESET,
156                fs_indicator,
157                emoji,
158                display_name_with_search
159            )
160        } else {
161            format!(
162                "{} {} {} {} {} {} {}{} {}",
163                depth_hex,
164                perms_hex,
165                uid_hex,
166                gid_hex,
167                size_hex,
168                time_hex,
169                fs_indicator,
170                emoji,
171                display_name_with_search
172            )
173        }
174    }
175}
176
177impl Formatter for HexFormatter {
178    fn format(
179        &self,
180        writer: &mut dyn Write,
181        nodes: &[FileNode],
182        _stats: &TreeStats,
183        root_path: &Path,
184    ) -> Result<()> {
185        // Sort nodes by path to ensure proper tree order
186        let mut sorted_nodes = nodes.to_vec();
187        sorted_nodes.sort_by(|a, b| a.path.cmp(&b.path));
188
189        for node in &sorted_nodes {
190            writeln!(writer, "{}", self.format_node(node, root_path))?;
191        }
192
193        Ok(())
194    }
195}
196
197impl StreamingFormatter for HexFormatter {
198    fn start_stream(&self, _writer: &mut dyn Write, _root_path: &Path) -> Result<()> {
199        // No header needed for hex format
200        Ok(())
201    }
202
203    fn format_node(&self, writer: &mut dyn Write, node: &FileNode, root_path: &Path) -> Result<()> {
204        writeln!(writer, "{}", self.format_node(node, root_path))?;
205        writer.flush()?; // Ensure immediate output
206        Ok(())
207    }
208
209    fn end_stream(
210        &self,
211        _writer: &mut dyn Write,
212        _stats: &TreeStats,
213        _root_path: &Path,
214    ) -> Result<()> {
215        // No footer needed for hex format
216        Ok(())
217    }
218}