preRelease
- vorerst alle geplanten Features Implementiert. - Tests stehen noch aus und Schönheitskorrekturen geplant
This commit is contained in:
parent
1210da7601
commit
d683594d6a
26
LICENSE
26
LICENSE
@ -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.
|
76
README.md
76
README.md
@ -2,7 +2,11 @@
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
39
src/main.rs
39
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::<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
pub fn init() {
|
||||
pub fn init_logger() {
|
||||
}
|
||||
|
35
tests/treebuilder_tests.rs
Normal file
35
tests/treebuilder_tests.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user