diff --git a/LICENSE b/LICENSE index e69de29..e68ca1a 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,26 @@ +MIT License + +Copyright (c) 2025 Adam Skotarczak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +1. The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + +2. Attribution Requirement: + If the Software is forked, reused, or substantially modified and redistributed, + visible attribution must be provided in the documentation (e.g., README file), stating: + “This software is based on work by Adam Skotarczak (https://github.com/realAscot)”. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f32c4b8..4440ff9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ ![TreeScanner-Logo](./media/logo-treescanner_512x512.png) -Dieses Tool ist im Rahmen meines persönlichen Projekts von C nach Rust zu wechseln. +TreeScanner ist ein leichtgewichtiges CLI-Tool zur Darstellung von Verzeichnisstrukturen als ASCII-Baum. Dieses Tool entstand im Rahmen meines persönlichen Projekts, systemnahe Werkzeuge von C nach Rust zu migrieren. + +Der original treeScanner in Python ist unter zu finden. Dieser ist auch als Python-Modul zu verwenden. + +--- ## Inhalt @@ -10,49 +14,79 @@ Dieses Tool ist im Rahmen meines persönlichen Projekts von C nach Rust zu wechs - [Inhalt](#inhalt) - [Struktur](#struktur) - [Features](#features) + - [Verwendung](#verwendung) + - [Beispielausgabe](#beispielausgabe) - [Lizenz](#lizenz) --- ## Struktur -**Diese Struktur ist eine Ausgabe des Tools:** - -```plaintext - - -``` - **GEPLANTE STRUKTUR (DEV)** ```plaintext - src/ ├── main.rs → CLI-Einstieg ├── app/ │ ├── mod.rs -│ └── treebuilder.rs → Feature 1: Verzeichnisbaum +│ └── treebuilder.rs → Verzeichnisbaum erstellen ├── config/ -│ ├── mod.rs -│ └── args.rs → Feature 2: Parameterübergabe -├── output/ -│ └── writer.rs → Datei schreiben -├── formatting/ -│ └── aligner.rs → Ausrichtung Kommentare -├── i18n/ -│ └── messages.rs → Sprachausgabe / Lokalisierung +│ └── args.rs → Parameterübergabe & Konfig ├── utils/ -│ ├── mod.rs +│ ├── ascii_spinner.rs → Fortschrittsanzeige │ └── logger.rs - +├── tests/ → Integrationstests +├── media/ → Logos / Assets +├── resources/ → .conf-Template, Icons, Versioninfo ``` --- ## Features +- 📁 ASCII-Baumstruktur mit Icons (📁, 📄) +- 📂 Max. Tiefe & Datei-Anzahl konfigurierbar (`--max-depth`, `--max-files-per-dir`) +- 🚫 Ignorieren von Verzeichnissen (`--ignore .git,target`) +- 💬 Optional ausrichtbare Kommentarspalte (`--align-comments`) +- ⚙ Konfigurierbar per CLI oder `~/.treescanner.conf` +- 🌀 Fortschrittsanzeige während des Scans +- 🛠 `--quiet`, `--debug`, `--viewonly`, `--output` u. a. +- 🧪 Tests, strukturierter Build, Markdown-fähige Ausgabe + +--- + +## Verwendung + +```bash +# Einfacher Scan (aktuelles Verzeichnis) +./treescanner.exe + +# Mit Tiefe 3, ohne speichern +./treescanner.exe --max-depth 3 --viewonly + +# Mit Kommentar-Ausrichtung +./treescanner.exe --align-comments + +# Ergebnis in Datei mit anderem Pfad speichern +./treescanner.exe --output ./struktur/tree.md +``` + +--- + +## Beispielausgabe + +```plaintext +📁 ./src/ +├── 📄 main.rs # +├── 📁 app/ # +│ └── 📄 treebuilder.rs # +└── 📁 utils/ # + ├── 📄 ascii_spinner.rs # + └── 📄 logger.rs # +``` + --- ## Lizenz ---- +MIT © [Adam Skotarczak](mailto:adam@skotarczak.net) siehe [LICENSE](./LICENSE) diff --git a/src/app/treebuilder.rs b/src/app/treebuilder.rs index 6e54242..9004b9d 100644 --- a/src/app/treebuilder.rs +++ b/src/app/treebuilder.rs @@ -9,11 +9,12 @@ pub struct TreeBuilderConfig { pub ignored_dirs: Vec, pub folder_icon: String, pub file_icon: String, + pub align_comments: bool, } /// Verantwortlich für das Erzeugen der ASCII-Baumstruktur. pub struct TreeBuilder { - config: TreeBuilderConfig, + pub config: TreeBuilderConfig, folder_count: usize, file_count: usize, } @@ -28,10 +29,11 @@ impl TreeBuilder { } /// Startet den Scan und liefert das Ergebnis als String. - pub fn build_tree(&mut self) -> String { - let mut lines = vec![format!("{} {}/", self.config.folder_icon, self.config.root_path.display())]; - self.scan_dir(&self.config.root_path.clone(), 0, "", &mut lines); - lines.join("\n") + pub fn build_tree(&mut self) -> Vec { + let root = self.config.root_path.clone(); + let mut lines = vec![format!("{} {}/", self.config.folder_icon, root.display())]; + self.scan_dir(&root, 0, "", &mut lines); + lines } fn scan_dir(&mut self, path: &Path, depth: usize, prefix: &str, lines: &mut Vec) { @@ -95,4 +97,16 @@ impl TreeBuilder { pub fn stats(&self) -> (usize, usize) { (self.folder_count, self.file_count) } + + /// Richtet die Ausgabezeilen mit Kommentarspalte aus. + pub fn align_lines_with_comments(&self, lines: &[String]) -> Vec { + let max_len = lines.iter().map(|l| l.len()).max().unwrap_or(0); + lines + .iter() + .map(|line| { + let padding = " ".repeat(max_len - line.len() + 2); + format!("{}{}#", line, padding) + }) + .collect() + } } diff --git a/src/config/args.rs b/src/config/args.rs index 831bafb..020da0f 100644 --- a/src/config/args.rs +++ b/src/config/args.rs @@ -1,52 +1,59 @@ use clap::Parser; use std::path::PathBuf; -/// Struktur für Kommandozeilenargumente mit Clap. +/// CLI-Argumente für TreeScanner #[derive(Parser, Debug)] -#[command( - name = "TreeScanner", - author = "Adam Skotarczak ", - version, - about = "Generiert eine ASCII-Baumstruktur eines Verzeichnisses." +#[command( author ="Adam Skotarczak ", + version= "1.0.0", + about = "TreeScanner: Verzeichnisse als ASCII-Baum visualisieren.", + +long_about = r#" +TreeScanner ist ein leichtgewichtiges CLI-Tool zur strukturierten Darstellung von Verzeichnisinhalten. + +Funktionen: +- Ausgabe als ASCII-Baum +- Optionen für Tiefe, Datei-Limit und Ignorierlisten +- Fortschrittsanzeige im Terminal +- Unterstützung für Konfigurationsdateien und CLI + +Beispiel: +treescanner.exe --max-depth 3 --ignore .git,target +"# )] pub struct CliArgs { - /// Aktiviere den Debug-Modus - #[clap(short = 'D', long = "debug", global = true, action)] - pub debug: bool, - - /// Aktiviere den Silent-Mode für Verwendung in Batch und Skripten - #[clap(short = 'q', long, global = true, action)] - pub quiet: bool, - - /// Stammverzeichnis (default: aktuelles Verzeichnis) + /// Root-Verzeichnis für den Scan (Standard: aktuelles Verzeichnis) #[arg(default_value = ".")] pub root_path: PathBuf, - /// Maximale Anzahl Dateien pro Verzeichnis - #[arg(short = 'n', long, default_value_t = 100)] - pub max_files_per_dir: usize, - - /// Maximale Rekursionstiefe (optional) - #[arg(short = 'd', long)] + /// Maximale Scan-Tiefe + #[arg(long)] pub max_depth: Option, - /// Keine Kommentar-Ausrichtung aktivieren (Zukunft) - #[arg(long)] - pub no_align_comments: bool, + /// Maximale Dateianzahl pro Verzeichnis (Standard: 100) + #[arg(long, default_value_t = 100)] + pub max_files_per_dir: usize, - /// Sprache der Programmausgabe (de oder en) - #[arg(short = 'l', long, default_value = "de")] - pub language: String, - - /// Ignorierte Verzeichnisse (mehrfach möglich) - #[arg(short = 'x', long, value_name = "DIR", num_args = 0..)] + /// Verzeichnisse ignorieren (z. B. .git,target) durch Komma getrennt, ohne Leerzeichen. + #[arg(short = 'x', long, value_delimiter = ',')] pub ignore: Vec, - /// Pfad zur Ausgabedatei (Default: tree.txt) - #[arg(short = 'o', long)] + /// Ausgabeziel (Standard: tree.txt) + #[arg(short, long)] pub output: Option, - /// Keine Dateiausgabe, nur Konsole + /// Nur in Konsole anzeigen, keine Ausgabedatei speichern #[arg(long)] pub viewonly: bool, -} + + /// Debug-Modus aktivieren + #[arg(short = 'D', long)] + pub debug: bool, + + /// Keine Statusausgaben + #[arg(short = 'q', long)] + pub quiet: bool, + + /// Kommentare ausrichten (DEV: optisch instabil) + #[arg(long, default_value_t = false)] + pub align_comments: bool, +} diff --git a/src/main.rs b/src/main.rs index fd1db1b..7aba035 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,26 @@ mod config; mod app; +mod utils; use app::treebuilder::{TreeBuilder, TreeBuilderConfig}; use config::args::CliArgs; use config::loader::load_config_from_home; +use utils::ascii_spinner::start_spinner; use clap::Parser; use std::fs; use std::time::Instant; +use utils::logger::init_logger; -use treescanner::utils::ascii_spinner::start_spinner; +/// Gibt die verstrichene Zeit seit `timer` aus. +fn view_timer(timer: &Instant) { + println!("\n⏱️ Gesamtlaufzeit des Scans: {:.2?}", timer.elapsed()); +} fn main() { + init_logger(); let args = CliArgs::parse(); let file_config = load_config_from_home().unwrap_or_default(); - - let start_time = Instant::now(); + let timer = Instant::now(); if !args.root_path.is_dir() { eprintln!("Fehler: '{}' ist kein gültiges Verzeichnis.", args.root_path.display()); @@ -43,23 +49,31 @@ fn main() { }, folder_icon: "📁".to_string(), file_icon: "📄".to_string(), + align_comments: args.align_comments, }; let mut builder = TreeBuilder::new(config); - // Spinner starten, wenn nicht quiet - let spinner = if !args.quiet { - Some(start_spinner(2)) + let (stop_spinner, spinner_handle) = if !args.quiet { + let (s, h) = start_spinner(8); + (Some(s), Some(h)) } else { - None + (None, None) }; - let output = builder.build_tree(); + let mut output = builder.build_tree().join("\n"); - if let Some(stop) = spinner { + if builder.config.align_comments { + let lines = output.lines().map(String::from).collect::>(); + output = builder.align_lines_with_comments(&lines).join("\n"); + } + + if let Some(stop) = stop_spinner { let _ = stop.send(()); } - println!(); // saubere Zeile nach Spinner + if let Some(handle) = spinner_handle { + let _ = handle.join(); + } let viewonly = args.viewonly || file_config.viewonly.unwrap_or(false); let output_path = args.output.clone().or_else(|| file_config.output.map(Into::into)).unwrap_or_else(|| "tree.txt".into()); @@ -77,9 +91,10 @@ fn main() { files, output_path.display() ); - println!("⏱️ Gesamtlaufzeit: {:.2?}", start_time.elapsed()); + view_timer(&timer); } } else { println!("{}", output); + view_timer(&timer); } -} +} diff --git a/src/utils/ascii_spinner.rs b/src/utils/ascii_spinner.rs index cfb41cf..8269962 100644 --- a/src/utils/ascii_spinner.rs +++ b/src/utils/ascii_spinner.rs @@ -1,6 +1,7 @@ use std::sync::mpsc::{self, Sender}; -use std::thread; +use std::thread::{self, JoinHandle}; use std::time::Duration; +use std::io::Write; /// Startet einen minimalistischen ASCII-Spinner in einem Hintergrundthread. /// @@ -8,28 +9,30 @@ use std::time::Duration; /// Die Frequenz wird über `ticks_per_second` gesteuert. /// /// # Beispiel -/// ``` -/// let stop = ascii_spinner::start_spinner(8); // 8 Ticks pro Sekunde -/// // ... lange Operation ... +/// ```no_run +/// # use treescanner::utils::ascii_spinner; +/// let (stop, handle) = ascii_spinner::start_spinner(8); // 8 Ticks pro Sekunde /// let _ = stop.send(()); +/// let _ = handle.join(); /// ``` -pub fn start_spinner(ticks_per_second: u64) -> Sender<()> { +pub fn start_spinner(ticks_per_second: u64) -> (Sender<()>, JoinHandle<()>) { let (tx, rx) = mpsc::channel(); let frames = vec!["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; let clamped = ticks_per_second.clamp(1, 20); let interval = Duration::from_millis(1000 / clamped); - thread::spawn(move || { + let handle = thread::spawn(move || { let mut idx = 0; while rx.try_recv().is_err() { print!("\r[{}] läuft ...", frames[idx % frames.len()]); - let _ = std::io::Write::flush(&mut std::io::stdout()); + let _ = std::io::stdout().flush(); idx += 1; thread::sleep(interval); } - print!("\r \r"); // Spinner löschen - let _ = std::io::Write::flush(&mut std::io::stdout()); + // Spinnerzeile zuverlässig löschen + print!("\rr\x1B[2K\r"); // ANSI: ganze Zeile löschen + let _ = std::io::stdout().flush(); }); - tx -} + (tx, handle) +} diff --git a/src/utils/logger.rs b/src/utils/logger.rs index f314922..8cb9bef 100644 --- a/src/utils/logger.rs +++ b/src/utils/logger.rs @@ -1,2 +1,2 @@ -pub fn init() { +pub fn init_logger() { } diff --git a/tests/treebuilder_tests.rs b/tests/treebuilder_tests.rs new file mode 100644 index 0000000..c8cbbf2 --- /dev/null +++ b/tests/treebuilder_tests.rs @@ -0,0 +1,35 @@ +#[cfg(test)] +mod tests { + use treescanner::app::treebuilder::{TreeBuilder, TreeBuilderConfig}; + use std::path::PathBuf; + + fn mock_tree_builder() -> TreeBuilder { + let config = TreeBuilderConfig { + root_path: PathBuf::from("/mock"), + max_depth: Some(1), + max_files_per_dir: 3, + ignored_dirs: vec![], + folder_icon: "📁".to_string(), + file_icon: "📄".to_string(), + align_comments: true, + }; + TreeBuilder::new(config) + } + + #[test] + fn test_align_lines_with_comments() { + let builder = mock_tree_builder(); + let lines = vec![ + "📁 src/".to_string(), + "├── 📄 main.rs".to_string(), + "└── 📄 lib.rs".to_string(), + ]; + + let aligned = builder.align_lines_with_comments(&lines); + + // Prüfe, ob jede Zeile mit einem # endet + for line in aligned { + assert!(line.trim_end().ends_with('#'), "Fehlendes #: {}", line); + } + } +}