preRelease

- vorerst alle geplanten Features Implementiert.
- Tests stehen noch aus und Schönheitskorrekturen geplant
This commit is contained in:
Adam Skotarczak 2025-05-17 13:23:04 +02:00
parent 1210da7601
commit d683594d6a
Signed by: realAscot
GPG Key ID: 4CB9B8D93A96A538
8 changed files with 218 additions and 84 deletions

26
LICENSE
View File

@ -0,0 +1,26 @@
MIT License
Copyright (c) 2025 Adam Skotarczak <adam@skotarczak.net>
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.

View File

@ -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 <https://github.com/realAscot/treeScannerASCII> 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)

View File

@ -9,11 +9,12 @@ pub struct TreeBuilderConfig {
pub ignored_dirs: Vec<String>,
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<String> {
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<String>) {
@ -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<String> {
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()
}
}

View File

@ -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 <adam@skotarczak.net>",
version,
about = "Generiert eine ASCII-Baumstruktur eines Verzeichnisses."
#[command( author ="Adam Skotarczak <adam@skotarczak.net>",
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<usize>,
/// 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<String>,
/// Pfad zur Ausgabedatei (Default: tree.txt)
#[arg(short = 'o', long)]
/// Ausgabeziel (Standard: tree.txt)
#[arg(short, long)]
pub output: Option<PathBuf>,
/// 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,
}

View File

@ -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::<Vec<_>>();
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);
}
}
}

View File

@ -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)
}

View File

@ -1,2 +1,2 @@
pub fn init() {
pub fn init_logger() {
}

View File

@ -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);
}
}
}