Skip to main content

st/formatters/
classic.rs

1use super::{Formatter, PathDisplayMode};
2use crate::emoji_mapper;
3use crate::scanner::{FileCategory, FileNode, TreeStats};
4use anyhow::Result;
5use colored::*;
6use humansize::{format_size, BINARY};
7use std::collections::{HashMap, HashSet};
8use std::io::Write;
9use std::path::{Path, PathBuf};
10
11pub struct ClassicFormatter {
12    pub no_emoji: bool,
13    pub use_color: bool,
14    pub path_mode: PathDisplayMode,
15    pub sort_field: Option<String>,
16}
17
18impl ClassicFormatter {
19    pub fn new(no_emoji: bool, use_color: bool, path_mode: PathDisplayMode) -> Self {
20        Self {
21            no_emoji,
22            use_color,
23            path_mode,
24            sort_field: None,
25        }
26    }
27
28    pub fn with_sort(mut self, sort_field: Option<String>) -> Self {
29        self.sort_field = sort_field;
30        self
31    }
32
33    /// Calculate visual weight based on directory size and depth
34    /// Larger directories and shallower depths get higher visual weight (thicker lines)
35    #[allow(dead_code)]
36    fn calculate_visual_weight(&self, node: &FileNode) -> u8 {
37        if !node.is_dir {
38            return 1; // Files get standard weight
39        }
40
41        // Base weight starts higher for directories
42        let mut weight = 2;
43
44        // Size-based scaling (logarithmic to avoid extreme values)
45        // Directories with more content get thicker lines
46        if node.size > 0 {
47            let size_factor = (node.size as f64).log10().max(1.0) as u8;
48            weight += (size_factor / 2).min(2); // Cap the size contribution
49        }
50
51        // Depth-based scaling - shallower directories get thicker lines
52        // Root level (depth 0) gets maximum thickness
53        let depth_bonus = if node.depth == 0 {
54            3 // Root gets the thickest lines
55        } else if node.depth == 1 {
56            2 // First level gets thick lines
57        } else if node.depth <= 3 {
58            1 // Moderate depth gets medium lines
59        } else {
60            0 // Deep levels get standard lines
61        };
62
63        weight += depth_bonus;
64
65        // Cap the maximum weight to avoid going beyond our character sets
66        weight.min(5)
67    }
68
69    /// Get terminal characters with gradient background based on file size
70    /// Returns formatted string with gradient background that fades to the right
71    fn get_terminal_chars(&self, file_size: u64, is_last: bool) -> String {
72        let base_char = if is_last { "└── " } else { "├── " };
73
74        if self.no_emoji || !self.use_color {
75            // No color/emoji mode - just return plain characters
76            return base_char.to_string();
77        }
78
79        // Calculate gradient intensity based on file size (0-100)
80        let intensity = self.calculate_gradient_intensity(file_size);
81
82        // Create gradient background that fades from left to right
83        self.apply_gradient_background(base_char, intensity)
84    }
85
86    /// Get continuation characters with gradient background
87    /// Returns formatted string with gradient background for vertical lines
88    fn get_continuation_chars(&self, file_size: u64, is_vertical: bool) -> String {
89        let base_char = if is_vertical { "│   " } else { "    " };
90
91        if self.no_emoji || !self.use_color {
92            // No color/emoji mode - just return plain characters
93            return base_char.to_string();
94        }
95
96        // Calculate gradient intensity based on file size
97        let intensity = self.calculate_gradient_intensity(file_size);
98
99        // Create gradient background that fades from left to right
100        self.apply_gradient_background(base_char, intensity)
101    }
102
103    /// Calculate gradient intensity (0-100) based on file size
104    /// Enhanced thresholds with 50% darker colors for better visibility
105    fn calculate_gradient_intensity(&self, file_size: u64) -> u8 {
106        // Enhanced size thresholds - make gradients much more visible!
107        const MB_100: u64 = 100 * 1024 * 1024; // 100MB
108        const MB_200: u64 = 200 * 1024 * 1024; // 200MB
109        const MB_500: u64 = 500 * 1024 * 1024; // 500MB
110        const GB_1: u64 = 1024 * 1024 * 1024; // 1GB
111        const GB_4: u64 = 4 * 1024 * 1024 * 1024; // 4GB
112
113        match file_size {
114            0..=1024 => 50,           // 0-1KB: 50% darker base
115            1025..=10240 => 55,       // 1-10KB: Slightly more
116            10241..=102400 => 60,     // 10-100KB: Visible
117            102401..=1048576 => 65,   // 100KB-1MB: More visible
118            1048577..=10485760 => 70, // 1-10MB: Quite visible
119            10485761..MB_100 => 75,   // 10-100MB: Strong
120            MB_100..MB_200 => 80,     // 100-200MB: 60% intensity
121            MB_200..MB_500 => 85,     // 200-500MB: 70% intensity
122            MB_500..GB_1 => 90,       // 500MB-1GB: 80% intensity
123            GB_1..GB_4 => 95,         // 1-4GB: 90% intensity
124            _ => 100,                 // 4GB+: 100% maximum intensity!
125        }
126    }
127
128    /// Apply solid gradient background blocks based on file size intensity
129    /// Creates stunning visual hierarchy with darker, more visible colors!
130    fn apply_gradient_background(&self, text: &str, intensity: u8) -> String {
131        let chars: Vec<char> = text.chars().collect();
132        let mut result = String::new();
133
134        // Choose solid background color based on intensity level
135        let bg_color = match intensity {
136            50..=60 => "\x1b[48;5;17m",  // Small files: Dark blue block
137            61..=70 => "\x1b[48;5;22m",  // Medium files: Dark green block
138            71..=75 => "\x1b[48;5;58m",  // Large files: Dark yellow/brown block
139            76..=80 => "\x1b[48;5;94m",  // 100-200MB: Dark orange block (60%)
140            81..=85 => "\x1b[48;5;130m", // 200-500MB: Darker orange block (70%)
141            86..=90 => "\x1b[48;5;124m", // 500MB-1GB: Dark red block (80%)
142            91..=95 => "\x1b[48;5;88m",  // 1-4GB: Very dark red block (90%)
143            96..=100 => "\x1b[48;5;52m", // 4GB+: Maximum dark red block (100%)
144            _ => "\x1b[48;5;236m",       // Fallback: Dark gray
145        };
146
147        // Apply solid background to entire tree structure as a block
148        for &ch in chars.iter() {
149            result.push_str(&format!("{}{}", bg_color, ch));
150        }
151
152        // Add reset at the end to prevent color bleeding
153        result.push_str("\x1b[0m");
154        result
155    }
156
157    /// Get context-aware emoji based on file type and node properties
158    /// Now uses the centralized emoji mapper for consistent, rich file type representation
159    fn get_file_emoji(&self, node: &FileNode) -> &'static str {
160        emoji_mapper::get_file_emoji(node, self.no_emoji)
161    }
162
163    fn build_tree_structure(
164        &self,
165        nodes: &[FileNode],
166        root_path: &Path,
167    ) -> Vec<(FileNode, Vec<bool>)> {
168        let mut result = Vec::new();
169
170        if nodes.is_empty() {
171            return result;
172        }
173
174        // Sort all nodes by path to ensure proper tree order
175        let mut sorted_nodes = nodes.to_vec();
176        sorted_nodes.sort_by(|a, b| a.path.cmp(&b.path));
177
178        // Remove duplicates based on path
179        let mut seen = HashSet::new();
180        sorted_nodes.retain(|node| seen.insert(node.path.clone()));
181
182        // Build parent-child relationships
183        let mut children_map: HashMap<PathBuf, Vec<usize>> = HashMap::new();
184        let mut parent_indices: Vec<Option<usize>> = vec![None; sorted_nodes.len()];
185
186        // Create a path-to-index map for O(1) parent lookups
187        let path_to_index: HashMap<PathBuf, usize> = sorted_nodes
188            .iter()
189            .enumerate()
190            .map(|(i, node)| (node.path.clone(), i))
191            .collect();
192
193        for (i, node) in sorted_nodes.iter().enumerate() {
194            if let Some(parent_path) = node.path.parent() {
195                // Find parent node index using HashMap lookup (O(1) instead of O(n))
196                if let Some(&parent_idx) = path_to_index.get(parent_path) {
197                    parent_indices[i] = Some(parent_idx);
198                    children_map
199                        .entry(parent_path.to_path_buf())
200                        .or_default()
201                        .push(i);
202                }
203            }
204        }
205
206        // Sort children based on sort field
207        for children in children_map.values_mut() {
208            let sort_field = self.sort_field.clone();
209            children.sort_by(|&a, &b| {
210                let node_a = &sorted_nodes[a];
211                let node_b = &sorted_nodes[b];
212
213                // Apply custom sorting if specified
214                match sort_field.as_deref() {
215                    Some("size") | Some("largest") => node_b.size.cmp(&node_a.size),
216                    Some("smallest") => node_a.size.cmp(&node_b.size),
217                    Some("date") | Some("newest") => node_b.modified.cmp(&node_a.modified),
218                    Some("oldest") => node_a.modified.cmp(&node_b.modified),
219                    Some("name") | Some("a-to-z") => {
220                        let name_a = node_a
221                            .path
222                            .file_name()
223                            .unwrap_or_default()
224                            .to_string_lossy();
225                        let name_b = node_b
226                            .path
227                            .file_name()
228                            .unwrap_or_default()
229                            .to_string_lossy();
230                        name_a.cmp(&name_b)
231                    }
232                    Some("z-to-a") => {
233                        let name_a = node_a
234                            .path
235                            .file_name()
236                            .unwrap_or_default()
237                            .to_string_lossy();
238                        let name_b = node_b
239                            .path
240                            .file_name()
241                            .unwrap_or_default()
242                            .to_string_lossy();
243                        name_b.cmp(&name_a)
244                    }
245                    Some("type") => {
246                        // Sort by extension, then by name
247                        let ext_a = node_a
248                            .path
249                            .extension()
250                            .unwrap_or_default()
251                            .to_string_lossy();
252                        let ext_b = node_b
253                            .path
254                            .extension()
255                            .unwrap_or_default()
256                            .to_string_lossy();
257                        match ext_a.cmp(&ext_b) {
258                            std::cmp::Ordering::Equal => {
259                                let name_a = node_a
260                                    .path
261                                    .file_name()
262                                    .unwrap_or_default()
263                                    .to_string_lossy();
264                                let name_b = node_b
265                                    .path
266                                    .file_name()
267                                    .unwrap_or_default()
268                                    .to_string_lossy();
269                                name_a.cmp(&name_b)
270                            }
271                            other => other,
272                        }
273                    }
274                    _ => {
275                        // Default: directories first, then by name
276                        match (node_a.is_dir, node_b.is_dir) {
277                            (true, false) => std::cmp::Ordering::Less,
278                            (false, true) => std::cmp::Ordering::Greater,
279                            _ => node_a.path.file_name().cmp(&node_b.path.file_name()),
280                        }
281                    }
282                }
283            });
284        }
285
286        // Build the tree structure recursively
287        fn add_node_to_result(
288            node_idx: usize,
289            nodes: &[FileNode],
290            children_map: &HashMap<PathBuf, Vec<usize>>,
291            result: &mut Vec<(FileNode, Vec<bool>)>,
292            is_last_stack: Vec<bool>,
293        ) {
294            let node = &nodes[node_idx];
295            result.push((node.clone(), is_last_stack.clone()));
296
297            if let Some(children) = children_map.get(&node.path) {
298                for (i, &child_idx) in children.iter().enumerate() {
299                    let is_last = i == children.len() - 1;
300                    let mut new_stack = is_last_stack.clone();
301                    new_stack.push(is_last);
302                    add_node_to_result(child_idx, nodes, children_map, result, new_stack);
303                }
304            }
305        }
306
307        // Find root node (should only be the scan root)
308        for (i, node) in sorted_nodes.iter().enumerate() {
309            if node.path == root_path {
310                add_node_to_result(i, &sorted_nodes, &children_map, &mut result, vec![]);
311                break;
312            }
313        }
314
315        result
316    }
317
318    fn get_color_for_category(&self, category: FileCategory) -> Option<Color> {
319        if !self.use_color {
320            return None;
321        }
322
323        match category {
324            // Programming languages
325            FileCategory::Rust => Some(Color::TrueColor {
326                r: 255,
327                g: 65,
328                b: 54,
329            }), // Rust orange
330            FileCategory::Python => Some(Color::TrueColor {
331                r: 55,
332                g: 118,
333                b: 171,
334            }), // Python blue
335            FileCategory::JavaScript => Some(Color::TrueColor {
336                r: 240,
337                g: 219,
338                b: 79,
339            }), // JS yellow
340            FileCategory::TypeScript => Some(Color::TrueColor {
341                r: 0,
342                g: 122,
343                b: 204,
344            }), // TS blue
345            FileCategory::Java => Some(Color::TrueColor {
346                r: 244,
347                g: 67,
348                b: 54,
349            }), // Java red
350            FileCategory::C => Some(Color::TrueColor {
351                r: 0,
352                g: 89,
353                b: 157,
354            }), // C blue
355            FileCategory::Cpp => Some(Color::TrueColor {
356                r: 0,
357                g: 89,
358                b: 157,
359            }), // C++ blue
360            FileCategory::Go => Some(Color::TrueColor {
361                r: 0,
362                g: 173,
363                b: 216,
364            }), // Go cyan
365            FileCategory::Ruby => Some(Color::TrueColor {
366                r: 204,
367                g: 52,
368                b: 45,
369            }), // Ruby red
370            FileCategory::PHP => Some(Color::TrueColor {
371                r: 119,
372                g: 123,
373                b: 180,
374            }), // PHP purple
375            FileCategory::Shell => Some(Color::Green),
376
377            // Markup/Data
378            FileCategory::Markdown => Some(Color::TrueColor {
379                r: 76,
380                g: 202,
381                b: 240,
382            }), // Light blue
383            FileCategory::Html => Some(Color::TrueColor {
384                r: 228,
385                g: 77,
386                b: 38,
387            }), // HTML orange
388            FileCategory::Css => Some(Color::TrueColor {
389                r: 33,
390                g: 150,
391                b: 243,
392            }), // CSS blue
393            FileCategory::Json => Some(Color::TrueColor {
394                r: 0,
395                g: 150,
396                b: 136,
397            }), // Changed to teal
398            FileCategory::Yaml => Some(Color::TrueColor {
399                r: 203,
400                g: 71,
401                b: 119,
402            }), // YAML pink
403            FileCategory::Xml => Some(Color::TrueColor {
404                r: 255,
405                g: 111,
406                b: 0,
407            }), // XML orange
408            FileCategory::Toml => Some(Color::TrueColor {
409                r: 150,
410                g: 111,
411                b: 214,
412            }), // TOML purple
413
414            // Build/Config
415            FileCategory::Makefile => Some(Color::TrueColor {
416                r: 66,
417                g: 165,
418                b: 245,
419            }), // Make blue
420            FileCategory::Dockerfile => Some(Color::TrueColor {
421                r: 33,
422                g: 150,
423                b: 243,
424            }), // Docker blue
425            FileCategory::GitConfig => Some(Color::TrueColor {
426                r: 241,
427                g: 80,
428                b: 47,
429            }), // Git orange
430
431            // Archives
432            FileCategory::Archive => Some(Color::TrueColor {
433                r: 121,
434                g: 134,
435                b: 203,
436            }), // Archive purple
437
438            // Media
439            FileCategory::Image => Some(Color::Magenta),
440            FileCategory::Video => Some(Color::TrueColor {
441                r: 255,
442                g: 87,
443                b: 34,
444            }), // Video orange
445            FileCategory::Audio => Some(Color::TrueColor {
446                r: 0,
447                g: 188,
448                b: 212,
449            }), // Audio cyan
450
451            // System
452            FileCategory::SystemFile => Some(Color::TrueColor {
453                r: 96,
454                g: 96,
455                b: 96,
456            }), // Dark grey
457            FileCategory::Binary => Some(Color::TrueColor {
458                r: 158,
459                g: 158,
460                b: 158,
461            }), // Light grey
462
463            // Database
464            FileCategory::Database => Some(Color::TrueColor {
465                r: 139,
466                g: 90,
467                b: 43,
468            }), // Brown
469
470            // Office & Documents
471            FileCategory::Office => Some(Color::TrueColor {
472                r: 21,
473                g: 101,
474                b: 192,
475            }), // Word blue
476            FileCategory::Spreadsheet => Some(Color::TrueColor {
477                r: 30,
478                g: 123,
479                b: 64,
480            }), // Excel green
481            FileCategory::PowerPoint => Some(Color::TrueColor {
482                r: 209,
483                g: 69,
484                b: 36,
485            }), // PowerPoint red
486            FileCategory::Pdf => Some(Color::TrueColor {
487                r: 215,
488                g: 41,
489                b: 42,
490            }), // PDF red
491            FileCategory::Ebook => Some(Color::TrueColor {
492                r: 65,
493                g: 105,
494                b: 225,
495            }), // Royal blue
496
497            // Text Variants
498            FileCategory::Log => Some(Color::TrueColor {
499                r: 128,
500                g: 128,
501                b: 128,
502            }), // Gray
503            FileCategory::Config => Some(Color::TrueColor {
504                r: 100,
505                g: 149,
506                b: 237,
507            }), // Cornflower blue
508            FileCategory::License => Some(Color::TrueColor {
509                r: 255,
510                g: 193,
511                b: 7,
512            }), // Amber
513            FileCategory::Readme => Some(Color::TrueColor {
514                r: 0,
515                g: 176,
516                b: 255,
517            }), // Deep sky blue
518            FileCategory::Txt => Some(Color::TrueColor {
519                r: 160,
520                g: 160,
521                b: 160,
522            }), // Light gray
523            FileCategory::Rtf => Some(Color::TrueColor {
524                r: 0,
525                g: 128,
526                b: 128,
527            }), // Teal
528            FileCategory::Csv => Some(Color::TrueColor {
529                r: 46,
530                g: 125,
531                b: 50,
532            }), // Green
533
534            // Security & Crypto
535            FileCategory::Certificate => Some(Color::TrueColor {
536                r: 255,
537                g: 87,
538                b: 34,
539            }), // Deep orange
540            FileCategory::Encrypted => Some(Color::TrueColor {
541                r: 156,
542                g: 39,
543                b: 176,
544            }), // Purple
545
546            // Fonts
547            FileCategory::Font => Some(Color::TrueColor {
548                r: 103,
549                g: 58,
550                b: 183,
551            }), // Deep purple
552
553            // Disk Images
554            FileCategory::DiskImage => Some(Color::TrueColor {
555                r: 33,
556                g: 33,
557                b: 33,
558            }), // Very dark gray
559
560            // 3D & CAD
561            FileCategory::Model3D => Some(Color::TrueColor {
562                r: 255,
563                g: 152,
564                b: 0,
565            }), // Orange
566
567            // Scientific & Data
568            FileCategory::Jupyter => Some(Color::TrueColor {
569                r: 255,
570                g: 109,
571                b: 0,
572            }), // Deep orange
573            FileCategory::RData => Some(Color::TrueColor {
574                r: 39,
575                g: 99,
576                b: 165,
577            }), // R blue
578            FileCategory::Matlab => Some(Color::TrueColor {
579                r: 237,
580                g: 119,
581                b: 3,
582            }), // MATLAB orange
583
584            // Web Assets
585            FileCategory::WebAsset => Some(Color::TrueColor {
586                r: 96,
587                g: 125,
588                b: 139,
589            }), // Blue grey
590
591            // Package & Dependencies
592            FileCategory::Package => Some(Color::TrueColor {
593                r: 203,
594                g: 31,
595                b: 26,
596            }), // NPM red
597            FileCategory::Lock => Some(Color::TrueColor {
598                r: 171,
599                g: 71,
600                b: 188,
601            }), // Medium purple
602
603            // Testing
604            FileCategory::Test => Some(Color::TrueColor {
605                r: 67,
606                g: 160,
607                b: 71,
608            }), // Test green
609
610            // Memory Files (MEM|8!)
611            FileCategory::Memory => Some(Color::TrueColor {
612                r: 147,
613                g: 112,
614                b: 219,
615            }), // Medium purple (brain-like)
616
617            // Others
618            FileCategory::Backup => Some(Color::TrueColor {
619                r: 189,
620                g: 189,
621                b: 189,
622            }), // Silver
623            FileCategory::Temp => Some(Color::TrueColor {
624                r: 117,
625                g: 117,
626                b: 117,
627            }), // Gray
628
629            // Default
630            FileCategory::Unknown => None,
631        }
632    }
633
634    fn format_node(&self, node: &FileNode, is_last: &[bool], root_path: &Path) -> String {
635        let mut prefix = String::new();
636
637        // Build tree prefix with gradient backgrounds based on file size
638        // Larger files get more intense gradient backgrounds that fade to the right
639
640        for (i, &last) in is_last.iter().enumerate() {
641            if i == is_last.len() - 1 {
642                // Terminal connectors (last level) - with gradient background
643                let terminal_chars = self.get_terminal_chars(node.size, last);
644                prefix.push_str(&terminal_chars);
645            } else {
646                // Continuation lines (intermediate levels) - with gradient background
647                let continuation_chars = self.get_continuation_chars(node.size, !last);
648                prefix.push_str(&continuation_chars);
649            }
650        }
651
652        let emoji = self.get_file_emoji(node);
653
654        // Determine what name to show based on path mode
655        let name = match self.path_mode {
656            PathDisplayMode::Off => node
657                .path
658                .file_name()
659                .unwrap_or(node.path.as_os_str())
660                .to_string_lossy()
661                .to_string(),
662            PathDisplayMode::Relative => {
663                if node.path == root_path {
664                    node.path
665                        .file_name()
666                        .unwrap_or(node.path.as_os_str())
667                        .to_string_lossy()
668                        .to_string()
669                } else {
670                    node.path
671                        .strip_prefix(root_path)
672                        .unwrap_or(&node.path)
673                        .to_string_lossy()
674                        .to_string()
675                }
676            }
677            PathDisplayMode::Full => node.path.display().to_string(),
678        };
679
680        let size_str = if node.is_dir {
681            String::new()
682        } else {
683            format!(" ({})", format_size(node.size, BINARY))
684        };
685
686        let indicator = if node.permission_denied {
687            " [*]"
688        } else if node.is_ignored {
689            " [ignored]"
690        } else {
691            ""
692        };
693
694        // Add search match indicator
695        let search_indicator = if let Some(matches) = &node.search_matches {
696            if matches.total_count > 0 {
697                let (line, col) = matches.first_match;
698                let truncated = if matches.truncated { ",TRUNCATED" } else { "" };
699                if matches.total_count > 1 {
700                    format!(
701                        " [FOUND:L{}:C{},{}x{}]",
702                        line, col, matches.total_count, truncated
703                    )
704                } else {
705                    format!(" [FOUND:L{}:C{}]", line, col)
706                }
707            } else {
708                String::new()
709            }
710        } else {
711            String::new()
712        };
713
714        // Apply color to the name based on file category
715        let colored_name = if node.is_dir {
716            // Directories get bright yellow and bold
717            let dir_name = if self.use_color {
718                name.bright_yellow().bold().to_string()
719            } else {
720                name
721            };
722            // Add git branch if present
723            if let Some(ref branch) = node.git_branch {
724                let branch_display = if self.use_color {
725                    format!(" [{}]", branch.cyan())
726                } else {
727                    format!(" [{}]", branch)
728                };
729                format!("{}{}", dir_name, branch_display)
730            } else {
731                dir_name
732            }
733        } else if let Some(color) = self.get_color_for_category(node.category) {
734            name.color(color).to_string()
735        } else {
736            name
737        };
738
739        if is_last.is_empty() {
740            // Root node
741            format!(
742                "{} {}{}{}{}",
743                emoji, colored_name, size_str, indicator, search_indicator
744            )
745        } else {
746            format!(
747                "{}{} {}{}{}{}",
748                prefix, emoji, colored_name, size_str, indicator, search_indicator
749            )
750        }
751    }
752}
753
754impl Formatter for ClassicFormatter {
755    fn format(
756        &self,
757        writer: &mut dyn Write,
758        nodes: &[FileNode],
759        stats: &TreeStats,
760        root_path: &Path,
761    ) -> Result<()> {
762        let tree_structure = self.build_tree_structure(nodes, root_path);
763
764        for (node, is_last) in tree_structure {
765            writeln!(writer, "{}", self.format_node(&node, &is_last, root_path))?;
766        }
767
768        // Print summary
769        writeln!(writer)?;
770        writeln!(
771            writer,
772            "{} directories, {} files, {} total",
773            stats.total_dirs,
774            stats.total_files,
775            format_size(stats.total_size, BINARY)
776        )?;
777
778        // Check if any nodes had permission denied
779        if nodes.iter().any(|n| n.permission_denied) {
780            writeln!(writer)?;
781            writeln!(writer, "[*] Permission denied")?;
782        }
783
784        Ok(())
785    }
786}