diff --git a/.gitignore b/.gitignore index 0ecb0f5..3e37f54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target/ *.exe -*.lock \ No newline at end of file +*.lock +/.cargo-plot/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..46d2793 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,129 @@ +Oto profesjonalny, szczegółowy i dwujęzyczny plik `CHANGELOG.md`, dokumentujący ewolucję projektu **cargo-plot** z wersji **0.1.5** do przełomowej wersji **0.2.0**. + +--- + +# CHANGELOG - cargo-plot + +All notable changes to this project will be documented in this file. +Wszystkie istotne zmiany w tym projekcie będą dokumentowane w tym pliku. + +--- + +## [0.2.0] — 2026-03-21 + +### "The Architecture Leap" / "Skok Architektoniczny" + +#### 1. Total Architectural Refactoring / Całkowity Refaktoring Architektury + +* **[ENG]** Transitioned from a flat library structure to a robust "Core + Interfaces" model. Business logic has been moved to `src/core/`, separating data processing from presentation. +* **[POL]** Przejście z płaskiej struktury biblioteki na solidny model „Core + Interfejsy”. Logika biznesowa została przeniesiona do `src/core/`, oddzielając przetwarzanie danych od prezentacji. +* **[ENG]** Introduced internal modularization: `path_matcher` for logic, `path_store` for data, and `path_view` for rendering. +* **[POL]** Wprowadzono wewnętrzną modularyzację: `path_matcher` dla logiki, `path_store` dla danych oraz `path_view` dla renderowania. + +#### 2. Introduction of Graphical User Interface (GUI) / Wprowadzenie Interfejsu Graficznego (GUI) + +* **[ENG]** Added a fully functional GUI powered by `eframe` and `egui`. Users can now manage patterns, preview trees, and generate reports in a dedicated window. +* **[POL]** Dodano w pełni funkcjonalny interfejs graficzny (GUI) oparty na `eframe` i `egui`. Użytkownicy mogą teraz zarządzać wzorcami, podglądać drzewa i generować raporty w dedykowanym oknie. +* **[ENG]** Features include a live "Zoom" scale, interactive file browsing using `rfd`, and real-time statistics. +* **[POL]** Funkcje obejmują skalowanie „Zoom” na żywo, interaktywne przeglądanie folderów za pomocą `rfd` oraz statystyki w czasie rzeczywistym. + +#### 3. Full Internationalization (i18n) / Pełna Internacjonalizacja (i18n) + +* **[ENG]** Implemented a comprehensive i18n system supporting English and Polish across all interfaces (CLI, TUI, GUI). +* **[POL]** Zaimplementowano kompleksowy system i18n wspierający język angielski i polski we wszystkich interfejsach (CLI, TUI, GUI). +* **[ENG]** Automatic language detection based on environment variables with an option for manual override via `--lang`. +* **[POL]** Automatyczna detekcja języka na podstawie zmiennych środowiskowych z opcją ręcznego wymuszenia przez `--lang`. + +#### 4. Advanced Sorting: "Merge" Strategies / Zaawansowane Sortowanie: Strategie „Merge” + +* **[ENG]** Introduced new sorting algorithms: `AzFileFirstMerge` and `ZaFileFirstMerge`. These group logical file-directory pairs (e.g., `mod.rs` and a directory of the same name) together. +* **[POL]** Wprowadzono nowe algorytmy sortowania: `AzFileFirstMerge` oraz `ZaFileFirstMerge`. Grupują one logiczne pary plik-katalog (np. `mod.rs` i katalog o tej samej nazwie) obok siebie. +* **[ENG]** Improved directory/file priority logic to ensure cleaner visual output in large Rust projects. +* **[POL]** Ulepszono logikę priorytetów katalogów/plików, aby zapewnić czystszy wynik wizualny w dużych projektach Rust. + +#### 5. Brace Expansion in Patterns / Rozwijanie Klamer we Wzorcach + +* **[ENG]** The pattern engine now supports brace expansion, e.g., `src/{lib,bin}/*.rs` expands automatically to multiple search patterns. +* **[POL]** Silnik wzorców obsługuje teraz rozwijanie klamer, np. `src/{lib,bin}/*.rs` automatycznie rozwija się do wielu wzorców wyszukiwania. +* **[ENG]** Added support for recursive expansion, allowing complex path filtering with single-line inputs. +* **[POL]** Dodano wsparcie dla rekurencyjnego rozwijania, co pozwala na złożone filtrowanie ścieżek za pomocą pojedynczej linii tekstu. + +#### 6. Enhanced Weight and Size Calculation / Ulepszone Obliczanie Wagi i Rozmiaru + +* **[ENG]** Added a toggle between Binary (IEC: KiB, MiB) and Decimal (SI: kB, MB) unit systems via the `-u` flag. +* **[POL]** Dodano przełącznik między binarnym (IEC: KiB, MiB) a dziesiętnym (SI: kB, MB) systemem jednostek za pomocą flagi `-u`. +* **[ENG]** Introduced the `-a` / `--all` flag to calculate the "Physical" size of directories (including hidden/ignored files) versus the "Filtered" sum. +* **[POL]** Wprowadzono flagę `-a` / `--all`, aby obliczać „Fizyczny” rozmiar katalogów (wliczając ukryte/ignorowane pliki) w przeciwieństwie do sumy „Przefiltrowanej”. + +#### 7. Grid View Mode / Nowy Tryb Widoku: Siatka (Grid) + +* **[ENG]** Added `ViewMode::Grid`, a new visualization style that aligns file names and their relative paths into a clean, readable table-like structure in the terminal. +* **[POL]** Dodano `ViewMode::Grid` – nowy styl wizualizacji, który wyrównuje nazwy plików i ich ścieżki relatywne w czystą, czytelną strukturę przypominającą tabelę w terminalu. +* **[ENG]** Dynamic width calculation ensures the grid adapts to the longest path in the result set. +* **[POL]** Dynamiczne obliczanie szerokości zapewnia dopasowanie siatki do najdłuższej ścieżki w zestawie wyników. + +#### 8. Professional Markdown Footers / Profesjonalne Stopki Markdown + +* **[ENG]** Reports now include a detailed metadata table at the end (when using the `-b` flag), containing the tool version, input path, TimeTag, and the exact CLI command used. +* **[POL]** Raporty zawierają teraz szczegółową tabelę metadanych na końcu (przy użyciu flagi `-b`), zawierającą wersję narzędzia, ścieżkę wejściową, TimeTag oraz dokładną użytą komendę CLI. +* **[ENG]** Improved report aesthetics with better use of blockquotes and horizontal rules. +* **[POL]** Ulepszono estetykę raportów dzięki lepszemu wykorzystaniu cytatów (blockquotes) i linii poziomych. + +#### 9. TUI "Cockpit" Evolution / Ewolucja „Kokpitu” TUI + +* **[ENG]** The Terminal User Interface has been rewritten to act as an interactive builder. It now features dynamic labels showing current configuration status before execution. +* **[POL]** Interfejs TUI został przepisany, aby działać jako interaktywny kreator. Posiada teraz dynamiczne etykiety pokazujące aktualny stan konfiguracji przed uruchomieniem. +* **[ENG]** Added a "CLI Mode" in TUI, allowing users to paste raw CLI arguments to instantly configure the interactive session. +* **[POL]** Dodano „Tryb CLI” w TUI, pozwalający użytkownikom na wklejanie surowych argumentów CLI w celu natychmiastowej konfiguracji sesji interaktywnej. + +#### 10. Security and Performance / Bezpieczeństwo i Wydajność + +* **[ENG]** Global implementation of `#![forbid(unsafe_code)]` to guarantee memory safety. +* **[POL]** Globalna implementacja `#![forbid(unsafe_code)]`, aby zagwarantować bezpieczeństwo pamięci. +* **[ENG]** Replaced standard manual path parsing with `walkdir` and `shlex` for better reliability across different operating systems. +* **[POL]** Zastąpiono standardowe ręczne parsowanie ścieżek bibliotekami `walkdir` i `shlex` dla lepszej niezawodności w różnych systemach operacyjnych. + +#### 11. Context-Aware Path Resolution / Inteligentne Rozwiązywanie Ścieżek + +* **[ENG]** New `PathContext` logic correctly identifies the relationship between the terminal's working directory and the scan target, ensuring correct `./relative/` path rendering. +* **[POL]** Nowa logika `PathContext` poprawnie identyfikuje relację między katalogiem roboczym terminala a celem skanowania, zapewniając poprawne renderowanie ścieżek `./relatywnych/`. +* **[ENG]** Improved handling of Windows extended-length paths (`\\?\`). +* **[POL]** Ulepszona obsługa długich ścieżek systemu Windows (`\\?\`). + +#### 12. Refined Pattern Logic Flags / Doprecyzowane Flagi Logiki Wzorców + +* **[ENG]** Clearly separated `@` (sibling), `$` (orphan), and `+` (deep/recursive) flags to give users surgical control over what is included in the documentation. +* **[POL]** Wyraźnie rozdzielono flagi `@` (rodzeństwo), `$` (sierota) oraz `+` (głęboki/rekurencyjny), aby dać użytkownikom chirurgiczną kontrolę nad tym, co znajdzie się w dokumentacji. +* **[ENG]** Negation (`!`) now acts as a "Hard Veto", overriding any positive matches for a path. +* **[POL]** Negacja (`!`) działa teraz jako „Twarde Weto”, unieważniając wszelkie pozytywne dopasowania dla danej ścieżki. + +--- + +### [0.1.5] — 2026-03-11 + +#### "The Foundation" / "Fundament" + +* **[ENG]** Initial stable release of the new generation documentation tool. +* **[POL]** Pierwsze stabilne wydanie nowej generacji narzędzia dokumentacyjnego. +* **[ENG]** Basic `Tree` and `Doc` commands implemented. +* **[POL]** Zaimplementowano podstawowe komendy `Tree` oraz `Doc`. +* **[ENG]** Support for automatic file identification and icons (Rust, TOML, Markdown). +* **[POL]** Wsparcie dla automatycznej identyfikacji plików i ikon (Rust, TOML, Markdown). +* **[ENG]** Simple TUI based on `cliclack`. +* **[POL]** Proste TUI oparte na `cliclack`. +* **[ENG]** Binary file blacklist to prevent Markdown corruption. +* **[POL]** Czarna lista plików binarnych zapobiegająca uszkodzeniu plików Markdown. + +--- + +### Statistics / Statystyki (v0.2.0) + +* **Lines of Code / Linii kodu:** ~4500+ +* **New Modules / Nowych modułów:** 22 +* **Supported Languages / Wspierane języki:** 2 (PL, EN) +* **Interface Modes / Tryby interfejsu:** 3 (CLI, TUI, GUI) + +--- + +> 🚀 **cargo-plot** | Generated by cargo-plot v0.2.0 | [GitHub](https://github.com/j-Cis/cargo-plot) +> 🚀 **cargo-plot** | Wygenerowano przez cargo-plot v0.2.0 | [GitHub](https://github.com/j-Cis/cargo-plot) diff --git a/Cargo.toml b/Cargo.toml index 21a3199..3794c56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,30 +1,16 @@ [package] name = "cargo-plot" -version = "0.1.5" +version = "0.2.0" authors = ["Jan Roman Cisowski „j-Cis”"] edition = "2024" rust-version = "1.94.0" -description = "Szwajcarski scyzoryk do wizualizacji struktury projektu i generowania raportów Markdown bezpośrednio z poziomu Cargo." +description = "Szwajcarski scyzoryk do wizualizacji struktury projektu i generowania dokumentacji bezpośrednio z poziomu Cargo." license = "MIT OR Apache-2.0" readme = "README.md" repository = "https://github.com/j-Cis/cargo-plot" -# Maksymalnie 5 słów kluczowych (limit crates.io) - zoptymalizowane pod SEO -keywords = [ - "cargo", - "tree", - "markdown", - "filesystem", - "documentation" -] - -# Rozszerzone kategorie (tutaj również jest limit max 5, my mamy 4 mocne) -categories = [ - "development-tools::cargo-plugins", - "command-line-utilities", - "command-line-interface", - "text-processing", -] +keywords = [ "cargo", "tree", "markdown", "filesystem", "documentation"] +categories = [ "development-tools::cargo-plugins", "command-line-utilities", "command-line-interface", "text-processing",] resolver = "3" [package.metadata.cargo] @@ -32,35 +18,30 @@ edition = "2024" [dependencies] -# Kluczowe dla logiki chrono = "0.4.44" walkdir = "2.5.0" regex = "1.12.3" - -# Kluczowe dla interfejsu (CLI/TUI) -# Wykorzystanie formatowania TOML v1.1.0 (wieloliniowe tabele z trailing comma) -clap = { - version = "4.5.60", - features = ["derive"], -} -cliclack = "0.4.1" +clap = { version = "4.5.60", features = ["derive"] } +cliclack = "0.5.0" colored = "3.1.1" +console = "0.16.3" +ctrlc = "3.5.2" +shlex = "1.3.0" +eframe = "0.33.3" +rfd = "0.17.2" -[lib] -name = "lib" -path = "src/lib/mod.rs" # ========================================== # Globalna konfiguracja lintów (Analiza kodu) # ========================================== -# [lints.rust] +[lints.rust] # Kategorycznie zabraniamy używania bloków `unsafe` w całym projekcie -# unsafe_code = "forbid" +unsafe_code = "forbid" # Ostrzegamy o nieużywanych importach, zmiennych i funkcjach # unused = "warn" # -# [lints.clippy] +[lints.clippy] # Włączamy surowsze reguły, ale jako ostrzeżenia (nie zepsują kompilacji) # pedantic = "warn" # Możemy tu też wyciszyć globalnie to, co nas irytuje (opcjonalnie): -# too_many_arguments = "allow" \ No newline at end of file +too_many_arguments = "allow" diff --git a/d.md b/CargoPlot-0-1-5_2026Q1D070W11_Wed11Mar_005445717.md similarity index 100% rename from d.md rename to CargoPlot-0-1-5_2026Q1D070W11_Wed11Mar_005445717.md diff --git a/CargoPlot-0-2-0_2026Q1D080W12_Sat21Mar_033235486.md b/CargoPlot-0-2-0_2026Q1D080W12_Sat21Mar_033235486.md new file mode 100644 index 0000000..06e3f26 --- /dev/null +++ b/CargoPlot-0-2-0_2026Q1D080W12_Sat21Mar_033235486.md @@ -0,0 +1,5265 @@ +```plaintext +[KiB 174.4] └──┬ 📂 cargo-plot-2 ./cargo-plot-2/ +[KiB 1.380] ├──• ⚙️ Cargo.toml ./Cargo.toml +[KiB 173.0] └──┬ 📂 src ./src/ +[ B 70.00] ├──• 🦀 addon.rs ./src/addon.rs +[KiB 2.431] ├──┬ 📂 addon ./src/addon/ +[KiB 2.431] │ └──• 🦀 time_tag.rs ./src/addon/time_tag.rs +[ B 120.0] ├──• 🦀 core.rs ./src/core.rs +[KiB 65.85] ├──┬ 📂 core ./src/core/ +[KiB 1.117] │ ├──• 🦀 file_stats.rs ./src/core/file_stats.rs +[KiB 3.177] │ ├──┬ 📂 file_stats ./src/core/file_stats/ +[KiB 3.177] │ │ └──• 🦀 weight.rs ./src/core/file_stats/weight.rs +[ B 285.0] │ ├──• 🦀 path_matcher.rs ./src/core/path_matcher.rs +[KiB 23.00] │ ├──┬ 📂 path_matcher ./src/core/path_matcher/ +[KiB 14.65] │ │ ├──• 🦀 matcher.rs ./src/core/path_matcher/matcher.rs +[KiB 4.501] │ │ ├──• 🦀 sort.rs ./src/core/path_matcher/sort.rs +[KiB 3.845] │ │ └──• 🦀 stats.rs ./src/core/path_matcher/stats.rs +[ B 101.0] │ ├──• 🦀 path_store.rs ./src/core/path_store.rs +[KiB 4.134] │ ├──┬ 📂 path_store ./src/core/path_store/ +[KiB 2.069] │ │ ├──• 🦀 context.rs ./src/core/path_store/context.rs +[KiB 2.065] │ │ └──• 🦀 store.rs ./src/core/path_store/store.rs +[ B 329.0] │ ├──• 🦀 path_view.rs ./src/core/path_view.rs +[KiB 24.31] │ ├──┬ 📂 path_view ./src/core/path_view/ +[KiB 11.03] │ │ ├──• 🦀 grid.rs ./src/core/path_view/grid.rs +[KiB 2.647] │ │ ├──• 🦀 list.rs ./src/core/path_view/list.rs +[KiB 2.528] │ │ ├──• 🦀 node.rs ./src/core/path_view/node.rs +[KiB 8.099] │ │ └──• 🦀 tree.rs ./src/core/path_view/tree.rs +[KiB 1.683] │ ├──• 🦀 patterns_expand.rs ./src/core/patterns_expand.rs +[KiB 7.727] │ └──• 🦀 save.rs ./src/core/save.rs +[KiB 5.772] ├──• 🦀 execute.rs ./src/execute.rs +[KiB 5.250] ├──• 🦀 i18n.rs ./src/i18n.rs +[ B 161.0] ├──• 🦀 interfaces.rs ./src/interfaces.rs +[KiB 88.51] ├──┬ 📂 interfaces ./src/interfaces/ +[KiB 1.306] │ ├──• 🦀 cli.rs ./src/interfaces/cli.rs +[KiB 16.90] │ ├──┬ 📂 cli ./src/interfaces/cli/ +[KiB 9.784] │ │ ├──• 🦀 args.rs ./src/interfaces/cli/args.rs +[KiB 7.119] │ │ └──• 🦀 engine.rs ./src/interfaces/cli/engine.rs +[KiB 4.423] │ ├──• 🦀 gui.rs ./src/interfaces/gui.rs +[KiB 40.22] │ ├──┬ 📂 gui ./src/interfaces/gui/ +[KiB 10.04] │ │ ├──• 🦀 code.rs ./src/interfaces/gui/code.rs +[KiB 3.955] │ │ ├──• 🦀 i18n.rs ./src/interfaces/gui/i18n.rs +[KiB 8.537] │ │ ├──• 🦀 paths.rs ./src/interfaces/gui/paths.rs +[KiB 12.95] │ │ ├──• 🦀 settings.rs ./src/interfaces/gui/settings.rs +[KiB 4.729] │ │ └──• 🦀 shared.rs ./src/interfaces/gui/shared.rs +[ B 390.0] │ ├──• 🦀 tui.rs ./src/interfaces/tui.rs +[KiB 25.27] │ └──┬ 📂 tui ./src/interfaces/tui/ +[KiB 10.62] │ ├──• 🦀 i18n.rs ./src/interfaces/tui/i18n.rs +[KiB 13.27] │ ├──• 🦀 menu.rs ./src/interfaces/tui/menu.rs +[KiB 1.375] │ └──• 🦀 state.rs ./src/interfaces/tui/state.rs +[ B 75.00] ├──• 🦀 lib.rs ./src/lib.rs +[ B 664.0] ├──• 🦀 main.rs ./src/main.rs +[ B 79.00] ├──• 🦀 output.rs ./src/output.rs +[ B 46.00] ├──• 🦀 theme.rs ./src/theme.rs +[KiB 4.084] └──┬ 📂 theme ./src/theme/ +[ B 837.0] ├──• 🦀 for_path_list.rs ./src/theme/for_path_list.rs +[KiB 3.267] └──• 🦀 for_path_tree.rs ./src/theme/for_path_tree.rs + +``` + +### 001: `./Cargo.toml` + +```rust +[package] +name = "cargo-plot" +version = "0.2.0" +authors = ["Jan Roman Cisowski „j-Cis”"] +edition = "2024" +rust-version = "1.94.0" +description = "Szwajcarski scyzoryk do wizualizacji struktury projektu i generowania dokumentacji bezpośrednio z poziomu Cargo." +license = "MIT OR Apache-2.0" +readme = "README.md" +repository = "https://github.com/j-Cis/cargo-plot" + +keywords = [ "cargo", "tree", "markdown", "filesystem", "documentation"] +categories = [ "development-tools::cargo-plugins", "command-line-utilities", "command-line-interface", "text-processing",] +resolver = "3" + +[package.metadata.cargo] +edition = "2024" + + +[dependencies] +chrono = "0.4.44" +walkdir = "2.5.0" +regex = "1.12.3" +clap = { version = "4.5.60", features = ["derive"] } +cliclack = "0.5.0" +colored = "3.1.1" +console = "0.16.3" +ctrlc = "3.5.2" +shlex = "1.3.0" +eframe = "0.33.3" +rfd = "0.17.2" + + +# ========================================== +# Globalna konfiguracja lintów (Analiza kodu) +# ========================================== +[lints.rust] +# Kategorycznie zabraniamy używania bloków `unsafe` w całym projekcie +unsafe_code = "forbid" +# Ostrzegamy o nieużywanych importach, zmiennych i funkcjach +# unused = "warn" +# +[lints.clippy] +# Włączamy surowsze reguły, ale jako ostrzeżenia (nie zepsują kompilacji) +# pedantic = "warn" +# Możemy tu też wyciszyć globalnie to, co nas irytuje (opcjonalnie): +too_many_arguments = "allow" + +``` + +### 002: `./src/addon.rs` + +```rust +pub mod time_tag; + +pub use time_tag::{NaiveDate, NaiveTime, TimeTag}; + +``` + +### 003: `./src/addon/time_tag.rs` + +```rust +// [EN]: Functions for creating consistent date and time stamps. +// [PL]: Funkcje do tworzenia spójnych sygnatur daty i czasu. + +use chrono::{Datelike, Local, Timelike, Weekday}; +pub use chrono::{NaiveDate, NaiveTime}; + +/// [EN]: Utility struct for generating consistent time tags. +/// [PL]: Struktura narzędziowa do generowania spójnych sygnatur czasowych. +pub struct TimeTag; + +impl TimeTag { + /// [EN]: Generates a time_tag for the current local time. + /// [PL]: Generuje time_tag dla obecnego, lokalnego czasu. + #[must_use] + pub fn now() -> String { + let now = Local::now(); + Self::format(now.date_naive(), now.time()) + } + + /// [EN]: Generates a time_tag for a specific provided date and time. + /// [PL]: Generuje time_tag dla konkretnej, podanej daty i czasu. + #[must_use] + pub fn custom(date: NaiveDate, time: NaiveTime) -> String { + Self::format(date, time) + } + + // [EN]: Private function that performs manual string construction (DRY principle). + // [PL]: PRYWATNA funkcja, która wykonuje ręczne budowanie ciągu znaków (zasada DRY). + fn format(date: NaiveDate, time: NaiveTime) -> String { + let year = date.year(); + let quarter = ((date.month() - 1) / 3) + 1; + + let weekday = match date.weekday() { + Weekday::Mon => "Mon", + Weekday::Tue => "Tue", + Weekday::Wed => "Wed", + Weekday::Thu => "Thu", + Weekday::Fri => "Fri", + Weekday::Sat => "Sat", + Weekday::Sun => "Sun", + }; + + let month = match date.month() { + 1 => "Jan", + 2 => "Feb", + 3 => "Mar", + 4 => "Apr", + 5 => "May", + 6 => "Jun", + 7 => "Jul", + 8 => "Aug", + 9 => "Sep", + 10 => "Oct", + 11 => "Nov", + 12 => "Dec", + _ => unreachable!(), + }; + + let millis = time.nanosecond() / 1_000_000; + + // [EN]: Format: YYYYQn Dnnn Wnn _ Day DD Mon _ HH MM SS mmm + // [PL]: Format: RRRRQn Dnnn Wnn _ Dzień DD Miesiąc _ GG MM SS mmm + format!( + "{}Q{}D{:03}W{:02}_{}{:02}{}_{:02}{:02}{:02}{:03}", + year, + quarter, + date.ordinal(), + date.iso_week().week(), + weekday, + date.day(), + month, + time.hour(), + time.minute(), + time.second(), + millis + ) + } +} + +``` + +### 004: `./src/core.rs` + +```rust +pub mod file_stats; +pub mod path_matcher; +pub mod path_store; +pub mod path_view; +pub mod patterns_expand; +pub mod save; + +``` + +### 005: `./src/core/file_stats.rs` + +```rust +// use std::fs; +use std::path::{Path, PathBuf}; +pub mod weight; + +use self::weight::get_path_weight; + +/// [POL]: Struktura przechowująca metadane (statystyki) pliku lub folderu. +#[derive(Debug, Clone)] +pub struct FileStats { + pub path: String, // Oryginalna ścieżka relatywna (np. "src/main.rs") + pub absolute: PathBuf, // Pełna ścieżka absolutna na dysku + pub weight_bytes: u64, // Rozmiar w bajtach + + // ⚡ Miejsce na przyszłe parametry: + // pub created_at: Option, + // pub modified_at: Option, +} + +impl FileStats { + /// [POL]: Pobiera statystyki pliku bezpośrednio z dysku. + pub fn fetch(path: &str, entry_absolute: &str) -> Self { + let absolute = Path::new(entry_absolute).join(path); + + let weight_bytes = get_path_weight(&absolute, true); + // let weight_bytes = fs::metadata(&absolute) + // .map(|m| m.len()) + // .unwrap_or(0); + + Self { + path: path.to_string(), + absolute, + weight_bytes, + } + } +} + +``` + +### 006: `./src/core/file_stats/weight.rs` + +```rust +// [ENG]: Logic for calculating and formatting file and directory weights. +// [POL]: Logika obliczania i formatowania wag plików oraz folderów. + +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UnitSystem { + Decimal, + Binary, + Both, + None, +} + +#[derive(Debug, Clone)] +pub struct WeightConfig { + pub system: UnitSystem, + pub precision: usize, + pub show_for_files: bool, + pub show_for_dirs: bool, + pub dir_sum_included: bool, +} + +impl Default for WeightConfig { + fn default() -> Self { + Self { + system: UnitSystem::Decimal, + precision: 5, + show_for_files: true, + show_for_dirs: true, + dir_sum_included: true, + } + } +} + +/// [POL]: Pobiera wagę ścieżki (plik lub folder rekurencyjnie). +pub fn get_path_weight(path: &Path, sum_included_only: bool) -> u64 { + let metadata = match fs::metadata(path) { + Ok(m) => m, + Err(_) => return 0, + }; + + if metadata.is_file() { + return metadata.len(); + } + + // ⚡ Jeśli sum_included_only jest false (flaga -a), liczymy rekurencyjnie fizyczny rozmiar + if metadata.is_dir() && !sum_included_only { + return get_dir_size(path); + } + + 0 +} + +/// [POL]: Prywatny pomocnik do liczenia rozmiaru folderu na dysku. +fn get_dir_size(path: &Path) -> u64 { + fs::read_dir(path) + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|e| { + let p = e.path(); + if p.is_dir() { + get_dir_size(&p) + } else { + e.metadata().map(|m| m.len()).unwrap_or(0) + } + }) + .sum() + }) + .unwrap_or(0) +} + +/// [POL]: Formatuje bajty na czytelny ciąg znaków (np. [kB 12.34]). +pub fn format_weight(bytes: u64, is_dir: bool, config: &WeightConfig) -> String { + if config.system == UnitSystem::None { + return String::new(); + } + + let should_show = (is_dir && config.show_for_dirs) || (!is_dir && config.show_for_files); + if !should_show { + let empty_width = 7 + config.precision; + return format!("{:width$}", "", width = empty_width); + } + + let (base, units) = match config.system { + UnitSystem::Binary => (1024.0_f64, vec!["B", "KiB", "MiB", "GiB", "TiB", "PiB"]), + _ => (1000.0_f64, vec!["B", "kB", "MB", "GB", "TB", "PB"]), + }; + + if bytes == 0 { + return format!( + "[{:>3} {:>width$}] ", + units[0], + "0", + width = config.precision + ); + } + + let bytes_f = bytes as f64; + let exp = (bytes_f.ln() / base.ln()).floor() as usize; + let exp = exp.min(units.len() - 1); + let value = bytes_f / base.powi(exp as i32); + let unit = units[exp]; + + let mut formatted_value = format!("{value:.10}"); + if formatted_value.len() > config.precision { + formatted_value = formatted_value[..config.precision] + .trim_end_matches('.') + .to_string(); + } else { + formatted_value = format!("{formatted_value:>width$}", width = config.precision); + } + + format!("[{unit:>3} {formatted_value}] ") +} + +``` + +### 007: `./src/core/path_matcher.rs` + +```rust +/// [POL]: Główny moduł logiki dopasowywania ścieżek. +/// [ENG]: Core module for path matching logic. +pub mod matcher; +pub mod sort; +pub mod stats; + +pub use self::matcher::{PathMatcher, PathMatchers}; +pub use self::sort::SortStrategy; +pub use self::stats::{MatchStats, ShowMode}; + +``` + +### 008: `./src/core/path_matcher/matcher.rs` + +```rust +use super::sort::SortStrategy; +use super::stats::{MatchStats, ResultSet, ShowMode}; +use regex::Regex; +use std::collections::HashSet; + +// ============================================================================== +// ⚡ POJEDYNCZY WZORZEC (PathMatcher) +// ============================================================================== + +/// [POL]: Struktura odpowiedzialna za dopasowanie pojedynczego wzorca z uwzględnieniem zależności strukturalnych. +/// [ENG]: Structure responsible for matching a single pattern considering structural dependencies. +pub struct PathMatcher { + regex: Regex, + targets_file: bool, + // [POL]: Flaga @ (para plik-folder) + // [ENG]: Flag @ (file-directory pair) + requires_sibling: bool, + // [POL]: Flaga $ (jednostronna relacja) + // [ENG]: Flag $ (one-way relation) + requires_orphan: bool, + // [POL]: Flaga + (rekurencyjne zacienianie) + // [ENG]: Flag + (recursive shadowing) + is_deep: bool, + // [POL]: Nazwa bazowa modułu do weryfikacji relacji + // [ENG]: Base name of the module for relation verification + base_name: String, + // [POL]: Flaga negacji (!). + // [ENG]: Negation flag (!). + pub is_negated: bool, +} + +impl PathMatcher { + pub fn new(pattern: &str, case_sensitive: bool) -> Result { + let is_negated = pattern.starts_with('!'); + let actual_pattern = if is_negated { &pattern[1..] } else { pattern }; + + let is_deep = actual_pattern.ends_with('+'); + let requires_sibling = actual_pattern.contains('@'); + let requires_orphan = actual_pattern.contains('$'); + let clean_pattern_str = actual_pattern.replace(['@', '$', '+'], ""); + + let base_name = clean_pattern_str + .trim_end_matches('/') + .trim_end_matches("**") + .split('/') + .next_back() + .unwrap_or("") + .split('.') + .next() + .unwrap_or("") + .to_string(); + + let mut re = String::new(); + + if !case_sensitive { + re.push_str("(?i)"); + } + + let mut is_anchored = false; + let mut p = clean_pattern_str.as_str(); + + let targets_file = !p.ends_with('/') && !p.ends_with("**"); + + if p.starts_with("./") { + is_anchored = true; + p = &p[2..]; + } else if p.starts_with("**/") { + is_anchored = true; + } + + if is_anchored { + re.push('^'); + } else { + re.push_str("(?:^|/)"); + } + + let chars: Vec = p.chars().collect(); + let mut i = 0; + + while i < chars.len() { + match chars[i] { + '\\' => { + if i + 1 < chars.len() { + i += 1; + re.push_str(®ex::escape(&chars[i].to_string())); + } + } + '.' => re.push_str("\\."), + '/' => { + if is_deep && i == chars.len() - 1 { + // [POL]: Pominięcie końcowego ukośnika dla flagi '+'. + // [ENG]: Omission of trailing slash for the '+' flag. + } else { + re.push('/'); + } + } + '*' => { + if i + 1 < chars.len() && chars[i + 1] == '*' { + if i + 2 < chars.len() && chars[i + 2] == '/' { + re.push_str("(?:[^/]+/)*"); + i += 2; + } else { + re.push_str(".+"); + i += 1; + } + } else { + re.push_str("[^/]*"); + } + } + '?' => re.push_str("[^/]"), + '{' => { + let mut options = String::new(); + i += 1; + while i < chars.len() && chars[i] != '}' { + options.push(chars[i]); + i += 1; + } + let escaped: Vec = options.split(',').map(regex::escape).collect(); + re.push_str(&format!("(?:{})", escaped.join("|"))); + } + '[' => { + re.push('['); + if i + 1 < chars.len() && chars[i + 1] == '!' { + re.push('^'); + i += 1; + } + } + ']' | '-' | '^' => re.push(chars[i]), + c => re.push_str(®ex::escape(&c.to_string())), + } + i += 1; + } + + if is_deep { + re.push_str("(?:/.*)?$"); + } else { + re.push('$'); + } + + Ok(Self { + regex: Regex::new(&re)?, + targets_file, + requires_sibling, + requires_orphan, + is_deep, + base_name, + is_negated, + }) + } + + /// [POL]: Sprawdza dopasowanie ścieżki, uwzględniając relacje rodzeństwa w strukturze plików. + /// [ENG]: Validates path matching, considering sibling relations within the file structure. + pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { + if self.targets_file && path.ends_with('/') { + return false; + } + + let clean_path = path.strip_prefix("./").unwrap_or(path); + + if !self.regex.is_match(clean_path) { + return false; + } + + // [POL]: Relacja rodzeństwa (@) lub sieroty ($) dla plików. + // [ENG]: Sibling relation (@) or orphan relation ($) for files. + if (self.requires_sibling || self.requires_orphan) && !path.ends_with('/') { + if self.is_deep && self.requires_sibling { + if !self.check_authorized_root(path, env) { + return false; + } + return true; + } + let mut components: Vec<&str> = path.split('/').collect(); + if let Some(file_name) = components.pop() { + let parent_dir = components.join("/"); + let core_name = file_name.split('.').next().unwrap_or(""); + let expected_folder = if parent_dir.is_empty() { + format!("{}/", core_name) + } else { + format!("{}/{}/", parent_dir, core_name) + }; + + if !env.contains(expected_folder.as_str()) { + return false; + } + } + } + + // [POL]: Dodatkowa weryfikacja rodzeństwa (@) dla katalogów. + // [ENG]: Additional sibling verification (@) for directories. + if self.requires_sibling && path.ends_with('/') { + if self.is_deep { + if !self.check_authorized_root(path, env) { + return false; + } + } else { + let dir_no_slash = path.trim_end_matches('/'); + let has_file_sibling = env.iter().any(|&p| { + p.starts_with(dir_no_slash) + && p[dir_no_slash.len()..].starts_with('.') + && !p.ends_with('/') + }); + + if !has_file_sibling { + return false; + } + } + } + + true + } + + /// [POL]: Ewaluuje kolekcję ścieżek, sortuje wyniki i wywołuje odpowiednie akcje. + /// [ENG]: Evaluates a path collection, sorts the results, and triggers respective actions. + // #[allow(clippy::too_many_arguments)] + pub fn evaluate( + &self, + paths: I, + env: &HashSet<&str>, + strategy: SortStrategy, + show_mode: ShowMode, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, + ) -> MatchStats + where + I: IntoIterator, + S: AsRef, + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), + { + let mut matched = Vec::new(); + let mut mismatched = Vec::new(); + + for path in paths { + if self.is_match(path.as_ref(), env) { + matched.push(path); + } else { + mismatched.push(path); + } + } + + strategy.apply(&mut matched); + strategy.apply(&mut mismatched); + + let stats = MatchStats { + m_size_matched: matched.len(), + x_size_mismatched: mismatched.len(), + total: matched.len() + mismatched.len(), + m_matched: ResultSet { + paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + x_mismatched: ResultSet { + paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + }; + + if show_mode == ShowMode::Include || show_mode == ShowMode::Context { + for path in &matched { + on_match(path.as_ref()); + } + } + + if show_mode == ShowMode::Exclude || show_mode == ShowMode::Context { + for path in &mismatched { + on_mismatch(path.as_ref()); + } + } + + stats + } + + /// [POL]: Weryfikuje autoryzację korzenia modułu w relacji plik-folder dla trybu 'deep'. + /// [ENG]: Verifies module root authorisation in the file-directory relation for 'deep' mode. + fn check_authorized_root(&self, path: &str, env: &HashSet<&str>) -> bool { + let clean = path.strip_prefix("./").unwrap_or(path); + let components: Vec<&str> = clean.split('/').collect(); + + for i in 0..components.len() { + let comp_core = components[i].split('.').next().unwrap_or(""); + + if comp_core == self.base_name { + let base_dir = if i == 0 { + self.base_name.clone() + } else { + format!("{}/{}", components[0..i].join("/"), self.base_name) + }; + + let full_base_dir = if path.starts_with("./") { + format!("./{}", base_dir) + } else { + base_dir + }; + let dir_path = format!("{}/", full_base_dir); + + let has_dir = env.contains(dir_path.as_str()); + let has_file = env.iter().any(|&p| { + p.starts_with(&full_base_dir) + && p[full_base_dir.len()..].starts_with('.') + && !p.ends_with('/') + }); + + if has_dir && has_file { + return true; + } + } + } + false + } +} + +// ============================================================================== +// ⚡ KONTENER WIELU WZORCÓW (PathMatchers) +// ============================================================================== + +/// [POL]: Kontener przechowujący kolekcję silników dopasowujących ścieżki. +/// [ENG]: A container holding a collection of path matching engines. +pub struct PathMatchers { + matchers: Vec, +} + +impl PathMatchers { + /// [POL]: Tworzy nową instancję, kompilując listę wzorców po uprzednim rozwinięciu klamer. + /// [ENG]: Creates a new instance by compiling a list of patterns after performing brace expansion. + pub fn new(patterns: I, case_sensitive: bool) -> Result + where + I: IntoIterator, + S: AsRef, + { + let mut matchers = Vec::new(); + for pat in patterns { + matchers.push(PathMatcher::new(pat.as_ref(), case_sensitive)?); + } + Ok(Self { matchers }) + } + + /// [POL]: Weryfikuje, czy ścieżka spełnia warunki narzucone przez zbiór wzorców (w tym negacje). + /// [ENG]: Verifies if the path meets the conditions imposed by the pattern set (including negations). + pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { + if self.matchers.is_empty() { + return false; + } + + let mut has_positive = false; + let mut matched_positive = false; + + for matcher in &self.matchers { + if matcher.is_negated { + // [POL]: Twarde WETO. Dopasowanie negatywne bezwzględnie odrzuca ścieżkę. + // [ENG]: Hard VETO. A negative match unconditionally rejects the path. + if matcher.is_match(path, env) { + return false; + } + } else { + has_positive = true; + if !matched_positive && matcher.is_match(path, env) { + matched_positive = true; + } + } + } + + // [POL]: Ostateczna decyzja na podstawie zebranych danych. + // [ENG]: Final decision based on collected data. + if has_positive { matched_positive } else { true } + } + + /// [POL]: Ewaluuje zbiór ścieżek, sortuje je i wykonuje odpowiednie domknięcia. + /// [ENG]: Evaluates a set of paths, sorts them, and executes respective closures. + pub fn evaluate( + &self, + paths: I, + env: &HashSet<&str>, + strategy: SortStrategy, + show_mode: ShowMode, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, + ) -> MatchStats + where + I: IntoIterator, + S: AsRef, + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), + { + let mut matched = Vec::new(); + let mut mismatched = Vec::new(); + + for path in paths { + if self.is_match(path.as_ref(), env) { + matched.push(path); + } else { + mismatched.push(path); + } + } + + strategy.apply(&mut matched); + strategy.apply(&mut mismatched); + + let stats = MatchStats { + m_size_matched: matched.len(), + x_size_mismatched: mismatched.len(), + total: matched.len() + mismatched.len(), + m_matched: ResultSet { + paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + x_mismatched: ResultSet { + paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + }; + + if show_mode == ShowMode::Include || show_mode == ShowMode::Context { + for path in matched { + on_match(path.as_ref()); + } + } + + if show_mode == ShowMode::Exclude || show_mode == ShowMode::Context { + for path in mismatched { + on_mismatch(path.as_ref()); + } + } + + stats + } +} + +``` + +### 009: `./src/core/path_matcher/sort.rs` + +```rust +use std::cmp::Ordering; + +/// [POL]: Definiuje dostępne strategie sortowania kolekcji ścieżek. +/// [ENG]: Defines available sorting strategies for path collections. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SortStrategy { + /// [POL]: Brak stosowania algorytmu sortowania. + /// [ENG]: No sorting algorithm applied. + None, + + /// [POL]: Sortowanie alfanumeryczne w porządku rosnącym. + /// [ENG]: Alphanumeric sorting in ascending order. + Az, + + /// [POL]: Sortowanie alfanumeryczne w porządku malejącym. + /// [ENG]: Alphanumeric sorting in descending order. + Za, + + /// [POL]: Priorytet dla plików, następnie sortowanie alfanumeryczne rosnąco. + /// [ENG]: Priority for files, followed by alphanumeric ascending sort. + AzFileFirst, + + /// [POL]: Priorytet dla plików, następnie sortowanie alfanumeryczne malejąco. + /// [ENG]: Priority for files, followed by alphanumeric descending sort. + ZaFileFirst, + + /// [POL]: Priorytet dla katalogów, następnie sortowanie alfanumeryczne rosnąco. + /// [ENG]: Priority for directories, followed by alphanumeric ascending sort. + AzDirFirst, + + /// [POL]: Priorytet dla katalogów, następnie sortowanie alfanumeryczne malejąco. + /// [ENG]: Priority for directories, followed by alphanumeric descending sort. + ZaDirFirst, + + /// [POL]: Sortowanie alfanumeryczne rosnąco, grupujące logiczne pary plik-katalog (np. moduły) z priorytetem dla plików. + /// [ENG]: Alphanumeric ascending sort grouping logical file-directory pairs (e.g. modules), prioritising files. + AzFileFirstMerge, + + /// [POL]: Sortowanie alfanumeryczne malejąco, grupujące logiczne pary plik-katalog z priorytetem dla plików. + /// [ENG]: Alphanumeric descending sort grouping logical file-directory pairs, prioritising files. + ZaFileFirstMerge, + + /// [POL]: Sortowanie alfanumeryczne rosnąco, grupujące logiczne pary plik-katalog z priorytetem dla katalogów. + /// [ENG]: Alphanumeric ascending sort grouping logical file-directory pairs, prioritising directories. + AzDirFirstMerge, + + /// [POL]: Sortowanie alfanumeryczne malejąco, grupujące logiczne pary plik-katalog z priorytetem dla katalogów. + /// [ENG]: Alphanumeric descending sort grouping logical file-directory pairs, prioritising directories. + ZaDirFirstMerge, +} + +impl SortStrategy { + /// [POL]: Sortuje kolekcję ścieżek w miejscu (in-place) na podstawie wybranej strategii. + /// [ENG]: Sorts a collection of paths in-place based on the selected strategy. + pub fn apply>(&self, paths: &mut [S]) { + if *self == SortStrategy::None { + return; + } + + paths.sort_by(|a_s, b_s| { + let a = a_s.as_ref(); + let b = b_s.as_ref(); + + let a_is_dir = a.ends_with('/'); + let b_is_dir = b.ends_with('/'); + + // Wywołujemy naszą prywatną, hermetyczną metodę + let a_merge = Self::get_merge_key(a); + let b_merge = Self::get_merge_key(b); + + match self { + SortStrategy::None => Ordering::Equal, + SortStrategy::Az => a.cmp(b), + SortStrategy::Za => b.cmp(a), + SortStrategy::AzFileFirst => (a_is_dir, a).cmp(&(b_is_dir, b)), + SortStrategy::ZaFileFirst => (a_is_dir, b).cmp(&(b_is_dir, a)), + SortStrategy::AzDirFirst => (!a_is_dir, a).cmp(&(!b_is_dir, b)), + SortStrategy::ZaDirFirst => (!a_is_dir, b).cmp(&(!b_is_dir, a)), + SortStrategy::AzFileFirstMerge => { + (a_merge, a_is_dir, a).cmp(&(b_merge, b_is_dir, b)) + } + SortStrategy::ZaFileFirstMerge => { + (b_merge, a_is_dir, b).cmp(&(a_merge, b_is_dir, a)) + } + SortStrategy::AzDirFirstMerge => { + (a_merge, !a_is_dir, a).cmp(&(b_merge, !b_is_dir, b)) + } + SortStrategy::ZaDirFirstMerge => { + (b_merge, !a_is_dir, b).cmp(&(a_merge, !b_is_dir, a)) + } + } + }); + } + + /// [POL]: Prywatna metoda. Ekstrahuje rdzenną nazwę ścieżki dla strategii Merge. + /// [ENG]: Private method. Extracts the core path name for Merge strategies. + fn get_merge_key(path: &str) -> &str { + let trimmed = path.trim_end_matches('/'); + if let Some(idx) = trimmed.rfind('.') + && idx > 0 + && trimmed.as_bytes()[idx - 1] != b'/' + { + return &trimmed[..idx]; + } + trimmed + } +} + +``` + +### 010: `./src/core/path_matcher/stats.rs` + +```rust +use crate::core::path_view::{PathGrid, PathList, PathTree, ViewMode}; + +/// [POL]: Podzbiór wyników zawierający surowe ścieżki i wygenerowane widoki. +#[derive(Default)] +pub struct ResultSet { + pub paths: Vec, + pub tree: Option, + pub list: Option, + pub grid: Option, +} + +// [ENG]: Simple stats object to avoid manual counting in the Engine. +// [POL]: Prosty obiekt statystyk, aby uniknąć ręcznego liczenia w Engine. +#[derive(Default)] +pub struct MatchStats { + pub m_size_matched: usize, + pub x_size_mismatched: usize, + pub total: usize, + pub m_matched: ResultSet, + pub x_mismatched: ResultSet, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ShowMode { + Include, + Exclude, + Context, +} + +impl MatchStats { + /// : Hermetyzacja renderowania po stronie rdzenia. + /// Zwraca gotowy, złożony ciąg znaków, gotowy do wrzucenia w konsolę lub plik. + #[must_use] + pub fn render_output( + &self, + view_mode: ViewMode, + show_mode: ShowMode, + print_info: bool, + use_color: bool, + ) -> String { + let mut out = String::new(); + let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; + let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; + + match view_mode { + ViewMode::Grid => { + if do_include && let Some(grid) = &self.m_matched.grid { + if print_info { + out.push_str("✅\n"); + } + if use_color { + out.push_str(&grid.render_cli()); + } else { + out.push_str(&grid.render_txt()); + } + } + if do_exclude && let Some(grid) = &self.x_mismatched.grid { + if print_info { + out.push_str("❌\n"); + } + if use_color { + out.push_str(&grid.render_cli()); + } else { + out.push_str(&grid.render_txt()); + } + } + } + ViewMode::Tree => { + if do_include && let Some(tree) = &self.m_matched.tree { + if print_info { + out.push_str("✅\n"); + } + if use_color { + out.push_str(&tree.render_cli()); + } else { + out.push_str(&tree.render_txt()); + } + } + if do_exclude && let Some(tree) = &self.x_mismatched.tree { + if print_info { + out.push_str("❌\n"); + } + if use_color { + out.push_str(&tree.render_cli()); + } else { + out.push_str(&tree.render_txt()); + } + } + } + ViewMode::List => { + if do_include && let Some(list) = &self.m_matched.list { + if print_info { + out.push_str("✅\n"); + } + if use_color { + out.push_str(&list.render_cli(true)); + } else { + out.push_str(&list.render_txt()); + } + } + if do_exclude && let Some(list) = &self.x_mismatched.list { + if print_info { + out.push_str("❌\n"); + } + if use_color { + out.push_str(&list.render_cli(false)); + } else { + out.push_str(&list.render_txt()); + } + } + } + } + + out + } +} + +``` + +### 011: `./src/core/path_store.rs` + +```rust +pub mod context; +pub mod store; + +pub use self::context::PathContext; +pub use self::store::PathStore; + +``` + +### 012: `./src/core/path_store/context.rs` + +```rust +use std::env; +use std::fs; +use std::path::Path; + +/// [POL]: Kontekst ścieżki roboczej - oblicza relacje między terminalem a celem skanowania. +/// [ENG]: Working path context - calculates relations between terminal and scan target. +#[derive(Debug)] +pub struct PathContext { + pub base_absolute: String, + pub entry_absolute: String, + pub entry_relative: String, +} + +impl PathContext { + pub fn resolve>(entered_path: P) -> Result { + let path_ref = entered_path.as_ref(); + + // 1. BASE ABSOLUTE: Gdzie fizycznie odpalono program? + let cwd = env::current_dir().map_err(|e| format!("Błąd odczytu CWD: {}", e))?; + let base_abs = cwd + .to_string_lossy() + .trim_start_matches(r"\\?\") + .replace('\\', "/"); + + // 2. ENTRY ABSOLUTE: Pełna ścieżka do folderu, który skanujemy + let abs_path = fs::canonicalize(path_ref) + .map_err(|e| format!("Nie można ustalić ścieżki '{:?}': {}", path_ref, e))?; + let entry_abs = abs_path + .to_string_lossy() + .trim_start_matches(r"\\?\") + .replace('\\', "/"); + + // 3. ENTRY RELATIVE: Ścieżka od terminala do skanowanego folderu + let entry_rel = match abs_path.strip_prefix(&cwd) { + Ok(rel) => { + let rel_str = rel.to_string_lossy().replace('\\', "/"); + if rel_str.is_empty() { + "./".to_string() // Cel to ten sam folder co terminal + } else { + format!("./{}/", rel_str) + } + } + Err(_) => { + // Jeśli cel jest na innym dysku (np. C:\ a terminal na D:\) + // lub całkiem poza strukturą CWD, relatywna nie istnieje. + // Wracamy wtedy do tego, co wpisał użytkownik, lub dajemy absolutną. + path_ref.to_string_lossy().replace('\\', "/") + } + }; + + Ok(Self { + base_absolute: base_abs, + entry_absolute: entry_abs, + entry_relative: entry_rel, + }) + } +} + +``` + +### 013: `./src/core/path_store/store.rs` + +```rust +use std::collections::HashSet; +use std::path::Path; +use walkdir::WalkDir; + +// use std::fs; +// use std::path::Path; + +// [ENG]: Container for scanned paths and their searchable pool. +// [POL]: Kontener na zeskanowane ścieżki i ich przeszukiwalną pulę. +#[derive(Debug)] +pub struct PathStore { + pub list: Vec, +} +impl PathStore { + /// [POL]: Skanuje katalog rekurencyjnie i zwraca znormalizowane ścieżki (prefix "./", separator "/", suffix "/" dla folderów). + /// [ENG]: Scans the directory recursively and returns normalised paths (prefix "./", separator "/", suffix "/" for directories). + pub fn scan>(dir_path: P) -> Self { + let mut list = Vec::new(); + let entry_path = dir_path.as_ref(); + + for entry in WalkDir::new(entry_path).into_iter().filter_map(|e| e.ok()) { + // [POL]: Pominięcie katalogu głównego (głębokość 0). + // [ENG]: Skip the root directory (depth 0). + if entry.depth() == 0 { + continue; + } + + // [POL]: Pominięcie dowiązań symbolicznych i punktów reparse. + // [ENG]: Skip symbolic links and reparse points. + if entry.path_is_symlink() { + continue; + } + + if let Ok(rel_path) = entry.path().strip_prefix(entry_path) { + // [POL]: Normalizacja separatorów systemowych do formatu uniwersalnego. + // [ENG]: Normalisation of system separators to a universal format. + let relative_str = rel_path.to_string_lossy().replace('\\', "/"); + let mut final_path = format!("./{}", relative_str); + + if entry.file_type().is_dir() { + final_path.push('/'); + } + + list.push(final_path); + } + } + + Self { list } + } + + // [ENG]: Creates a temporary pool of references for the matcher. + // [POL]: Tworzy tymczasową pulę referencji (paths_pool) dla matchera. + pub fn get_index(&self) -> HashSet<&str> { + self.list.iter().map(|s| s.as_str()).collect() + } +} + +``` + +### 014: `./src/core/path_view.rs` + +```rust +pub mod grid; +pub mod list; +pub mod node; +pub mod tree; + +// Re-eksportujemy dla wygody, aby w engine.rs używać PathTree i FileNode bezpośrednio +pub use grid::PathGrid; +pub use list::PathList; +pub use tree::PathTree; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ViewMode { + Tree, + List, + Grid, +} + +``` + +### 015: `./src/core/path_view/grid.rs` + +```rust +use colored::Colorize; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig}; +use crate::core::path_matcher::SortStrategy; +use crate::theme::for_path_tree::{DIR_ICON, TreeStyle, get_file_type}; + +pub struct PathGrid { + roots: Vec, + style: TreeStyle, +} + +impl PathGrid { + #[must_use] + pub fn build( + paths_strings: &[String], + base_dir: &str, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + root_name: Option<&str>, + no_emoji: bool, + ) -> Self { + // Dokładnie taka sama logika budowania struktury węzłów jak w PathTree::build + let base_path_obj = Path::new(base_dir); + let paths: Vec = paths_strings.iter().map(PathBuf::from).collect(); + let mut tree_map: BTreeMap> = BTreeMap::new(); + + for p in &paths { + let parent = p + .parent() + .map_or_else(|| PathBuf::from("."), Path::to_path_buf); + tree_map.entry(parent).or_default().push(p.clone()); + } + + fn build_node( + path: &PathBuf, + paths_map: &BTreeMap>, + base_path: &Path, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + no_emoji: bool, + ) -> FileNode { + let name = path + .file_name() + .map_or_else(|| "/".to_string(), |n| n.to_string_lossy().to_string()); + let is_dir = path.is_dir() || path.to_string_lossy().ends_with('/'); + let icon = if no_emoji { + String::new() + } else if is_dir { + DIR_ICON.to_string() + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + get_file_type(ext).icon.to_string() + } else { + "📄".to_string() + }; + + let absolute_path = base_path.join(path); + let mut weight_bytes = + weight::get_path_weight(&absolute_path, weight_cfg.dir_sum_included); + let mut children = vec![]; + + if let Some(child_paths) = paths_map.get(path) { + let mut child_nodes: Vec = child_paths + .iter() + .map(|c| { + build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji) + }) + .collect(); + + FileNode::sort_slice(&mut child_nodes, sort_strategy); + + if is_dir && weight_cfg.dir_sum_included { + weight_bytes = child_nodes.iter().map(|n| n.weight_bytes).sum(); + } + children = child_nodes; + } + + let weight_str = weight::format_weight(weight_bytes, is_dir, weight_cfg); + FileNode { + name, + path: path.clone(), + is_dir, + icon, + weight_str, + weight_bytes, + children, + } + } + + let roots_paths: Vec = paths + .iter() + .filter(|p| { + let parent = p.parent(); + parent.is_none() + || parent.unwrap() == Path::new("") + || !paths.contains(&parent.unwrap().to_path_buf()) + }) + .cloned() + .collect(); + + let mut top_nodes: Vec = roots_paths + .into_iter() + .map(|r| { + build_node( + &r, + &tree_map, + base_path_obj, + sort_strategy, + weight_cfg, + no_emoji, + ) + }) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_strategy); + + // [ENG]: Logic for creating the final root node with proper weight calculation. + // [POL]: Logika tworzenia końcowego węzła głównego z poprawnym obliczeniem wagi. + let final_roots = if let Some(r_name) = root_name { + // [ENG]: Calculate total weight for the root node. + // [POL]: Obliczenie całkowitej wagi dla węzła głównego. + let root_bytes = if weight_cfg.dir_sum_included { + // [POL]: Suma wag bezpośrednich dzieci (dopasowanych elementów). + top_nodes.iter().map(|n| n.weight_bytes).sum() + } else { + // [POL]: Fizyczna waga folderu wejściowego z dysku. + weight::get_path_weight(base_path_obj, false) + }; + + let root_weight_str = weight::format_weight(root_bytes, true, weight_cfg); + + vec![FileNode { + name: r_name.to_string(), + path: PathBuf::from(r_name), + is_dir: true, + icon: if no_emoji { + String::new() + } else { + DIR_ICON.to_string() + }, + weight_str: root_weight_str, + weight_bytes: root_bytes, + children: top_nodes, + }] + } else { + top_nodes + }; + + Self { + roots: final_roots, + style: TreeStyle::default(), + } + } + + #[must_use] + pub fn render_cli(&self) -> String { + let max_width = self.calc_max_width(&self.roots, 0); + self.plot(&self.roots, "", true, max_width) + } + + #[must_use] + pub fn render_txt(&self) -> String { + let max_width = self.calc_max_width(&self.roots, 0); + self.plot(&self.roots, "", false, max_width) + } + + fn calc_max_width(&self, nodes: &[FileNode], indent_len: usize) -> usize { + let mut max = 0; + for (i, node) in nodes.iter().enumerate() { + let is_last = i == nodes.len() - 1; + let has_children = !node.children.is_empty(); + let branch = if node.is_dir { + match (is_last, has_children) { + (true, true) => &self.style.dir_last_with_children, + (false, true) => &self.style.dir_mid_with_children, + (true, false) => &self.style.dir_last_no_children, + (false, false) => &self.style.dir_mid_no_children, + } + } else if is_last { + &self.style.file_last + } else { + &self.style.file_mid + }; + + let current_len = node.weight_str.chars().count() + + indent_len + + branch.chars().count() + + 1 + + node.icon.chars().count() + + 1 + + node.name.chars().count(); + if current_len > max { + max = current_len; + } + + if has_children { + let next_indent = indent_len + + if is_last { + self.style.indent_last.chars().count() + } else { + self.style.indent_mid.chars().count() + }; + let child_max = self.calc_max_width(&node.children, next_indent); + if child_max > max { + max = child_max; + } + } + } + max + } + + fn plot(&self, nodes: &[FileNode], indent: &str, use_color: bool, max_width: usize) -> String { + let mut result = String::new(); + for (i, node) in nodes.iter().enumerate() { + let is_last = i == nodes.len() - 1; + let has_children = !node.children.is_empty(); + let branch = if node.is_dir { + match (is_last, has_children) { + (true, true) => &self.style.dir_last_with_children, + (false, true) => &self.style.dir_mid_with_children, + (true, false) => &self.style.dir_last_no_children, + (false, false) => &self.style.dir_mid_no_children, + } + } else if is_last { + &self.style.file_last + } else { + &self.style.file_mid + }; + + let weight_prefix = if node.weight_str.is_empty() { + String::new() + } else if use_color { + node.weight_str.truecolor(120, 120, 120).to_string() + } else { + node.weight_str.clone() + }; + + let raw_left_len = node.weight_str.chars().count() + + indent.chars().count() + + branch.chars().count() + + 1 + + node.icon.chars().count() + + 1 + + node.name.chars().count(); + let pad_len = max_width.saturating_sub(raw_left_len) + 4; + let padding = " ".repeat(pad_len); + + let rel_path_str = node.path.to_string_lossy().replace('\\', "/"); + let display_path = if node.is_dir && !rel_path_str.ends_with('/') { + format!("./{}/", rel_path_str) + } else if !rel_path_str.starts_with("./") && !rel_path_str.starts_with('.') { + format!("./{}", rel_path_str) + } else { + rel_path_str + }; + + let right_colored = if use_color { + if node.is_dir { + display_path.truecolor(200, 200, 50).to_string() + } else { + display_path.white().to_string() + } + } else { + display_path + }; + + let left_colored = if use_color { + if node.is_dir { + format!( + "{}{}{} {}{}", + weight_prefix, + indent.green(), + branch.green(), + node.icon, + node.name.truecolor(200, 200, 50) + ) + } else { + format!( + "{}{}{} {}{}", + weight_prefix, + indent.green(), + branch.green(), + node.icon, + node.name.white() + ) + } + } else { + format!( + "{}{}{} {} {}", + weight_prefix, indent, branch, node.icon, node.name + ) + }; + + result.push_str(&format!("{}{}{}\n", left_colored, padding, right_colored)); + + if has_children { + let new_indent = if is_last { + format!("{}{}", indent, self.style.indent_last) + } else { + format!("{}{}", indent, self.style.indent_mid) + }; + result.push_str(&self.plot(&node.children, &new_indent, use_color, max_width)); + } + } + result + } +} + +``` + +### 016: `./src/core/path_view/list.rs` + +```rust +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig}; +use crate::core::path_matcher::SortStrategy; +use crate::theme::for_path_list::get_icon_for_path; +use colored::Colorize; +/// [POL]: Zarządca wyświetlania wyników w formie płaskiej listy. +pub struct PathList { + items: Vec, +} + +impl PathList { + /// [POL]: Buduje listę na podstawie zbioru ścieżek i statystyk. + pub fn build( + paths_strings: &[String], + base_dir: &str, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + no_emoji: bool, + ) -> Self { + // Wykorzystujemy istniejącą logikę węzłów, ale bez rekurencji (płaska lista) + let mut items: Vec = paths_strings + .iter() + .map(|p_str| { + let absolute = std::path::Path::new(base_dir).join(p_str); + let is_dir = p_str.ends_with('/'); + let weight_bytes = + crate::core::file_stats::weight::get_path_weight(&absolute, true); + let weight_str = weight::format_weight(weight_bytes, is_dir, weight_cfg); + + FileNode { + name: p_str.clone(), + path: absolute, + is_dir, + icon: if no_emoji { + String::new() + } else { + get_icon_for_path(p_str).to_string() + }, + weight_str, + weight_bytes, + children: vec![], // Lista nie ma dzieci + } + }) + .collect(); + + FileNode::sort_slice(&mut items, sort_strategy); + + Self { items } + } + + /// [POL]: Renderuje listę dla terminala (z kolorami i ikonami). + pub fn render_cli(&self, _is_match: bool) -> String { + let mut out = String::new(); + // let tag = if is_match { "✅ MATCH: ".green() } else { "❌ REJECT:".red() }; + + for item in &self.items { + let line = format!( + "{} {} {}\n", + item.weight_str.truecolor(120, 120, 120), + item.icon, + if item.is_dir { + item.name.yellow() + } else { + item.name.white() + } + ); + out.push_str(&line); + } + out + } + + #[must_use] + pub fn render_txt(&self) -> String { + let mut out = String::new(); + for item in &self.items { + // Brak formatowania ANSI + let line = format!("{} {} {}\n", item.weight_str, item.icon, item.name); + out.push_str(&line); + } + out + } +} + +``` + +### 017: `./src/core/path_view/node.rs` + +```rust +use crate::core::path_matcher::SortStrategy; +use std::path::PathBuf; + +/// [POL]: Reprezentuje pojedynczy węzeł w drzewie systemu plików. +#[derive(Debug, Clone)] +pub struct FileNode { + pub name: String, + pub path: PathBuf, + pub is_dir: bool, + pub icon: String, + pub weight_str: String, + pub weight_bytes: u64, + pub children: Vec, +} + +impl FileNode { + /// [POL]: Sortuje listę węzłów w miejscu zgodnie z wybraną strategią. + pub fn sort_slice(nodes: &mut [FileNode], strategy: SortStrategy) { + if strategy == SortStrategy::None { + return; + } + + nodes.sort_by(|a, b| { + let a_is_dir = a.is_dir; + let b_is_dir = b.is_dir; + + // Klucz Merge: "interfaces.rs" -> "interfaces", "interfaces/" -> "interfaces" + let a_merge = Self::get_merge_key(&a.name); + let b_merge = Self::get_merge_key(&b.name); + + match strategy { + // 1. CZYSTE ALFANUMERYCZNE + SortStrategy::Az => a.name.cmp(&b.name), + SortStrategy::Za => b.name.cmp(&a.name), + + // 2. PLIKI PIERWSZE (Globalnie) + SortStrategy::AzFileFirst => (a_is_dir, &a.name).cmp(&(b_is_dir, &b.name)), + SortStrategy::ZaFileFirst => (a_is_dir, &b.name).cmp(&(b_is_dir, &a.name)), + + // 3. KATALOGI PIERWSZE (Globalnie) + SortStrategy::AzDirFirst => (!a_is_dir, &a.name).cmp(&(!b_is_dir, &b.name)), + SortStrategy::ZaDirFirst => (!a_is_dir, &b.name).cmp(&(!b_is_dir, &a.name)), + + // 4. PLIKI PIERWSZE + MERGE (Grupowanie modułów) + SortStrategy::AzFileFirstMerge => { + (a_merge, a_is_dir, &a.name).cmp(&(b_merge, b_is_dir, &b.name)) + } + SortStrategy::ZaFileFirstMerge => { + (b_merge, a_is_dir, &b.name).cmp(&(a_merge, b_is_dir, &a.name)) + } + + // 5. KATALOGI PIERWSZE + MERGE (Zgodnie z Twoją notatką: fallback do DirFirst) + SortStrategy::AzDirFirstMerge => (!a_is_dir, &a.name).cmp(&(!b_is_dir, &b.name)), + SortStrategy::ZaDirFirstMerge => (!a_is_dir, &b.name).cmp(&(!b_is_dir, &a.name)), + + _ => a.name.cmp(&b.name), + } + }); + } + + /// [POL]: Wyciąga rdzeń nazwy do grupowania (np. "main.rs" -> "main"). + fn get_merge_key(name: &str) -> &str { + if let Some(idx) = name.rfind('.') + && idx > 0 + { + return &name[..idx]; + } + name + } +} + +``` + +### 018: `./src/core/path_view/tree.rs` + +```rust +use colored::Colorize; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +// Importy z rodzeństwa i innych modułów core +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig}; +use crate::core::path_matcher::SortStrategy; +use crate::theme::for_path_tree::{DIR_ICON, FILE_ICON, TreeStyle, get_file_type}; +pub struct PathTree { + roots: Vec, + style: TreeStyle, +} + +impl PathTree { + #[must_use] + pub fn build( + paths_strings: &[String], + base_dir: &str, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + root_name: Option<&str>, + no_emoji: bool, + ) -> Self { + let base_path_obj = Path::new(base_dir); + let paths: Vec = paths_strings.iter().map(PathBuf::from).collect(); + let mut tree_map: BTreeMap> = BTreeMap::new(); + + for p in &paths { + let parent = p + .parent() + .map_or_else(|| PathBuf::from("."), Path::to_path_buf); + tree_map.entry(parent).or_default().push(p.clone()); + } + + fn build_node( + path: &PathBuf, + paths_map: &BTreeMap>, + base_path: &Path, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + no_emoji: bool, + ) -> FileNode { + let name = path + .file_name() + .map_or_else(|| "/".to_string(), |n| n.to_string_lossy().to_string()); + + let is_dir = path.is_dir() || path.to_string_lossy().ends_with('/'); + let icon = if no_emoji { + String::new() + } else if is_dir { + DIR_ICON.to_string() + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + get_file_type(ext).icon.to_string() + } else { + FILE_ICON.to_string() + }; + + let absolute_path = base_path.join(path); + let mut weight_bytes = + weight::get_path_weight(&absolute_path, weight_cfg.dir_sum_included); + let mut children = vec![]; + + if let Some(child_paths) = paths_map.get(path) { + let mut child_nodes: Vec = child_paths + .iter() + .map(|c| { + build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji) + }) + .collect(); + + FileNode::sort_slice(&mut child_nodes, sort_strategy); + + if is_dir && weight_cfg.dir_sum_included { + weight_bytes = child_nodes.iter().map(|n| n.weight_bytes).sum(); + } + children = child_nodes; + } + + let weight_str = weight::format_weight(weight_bytes, is_dir, weight_cfg); + + FileNode { + name, + path: path.clone(), + is_dir, + icon, + weight_str, + weight_bytes, + children, + } + } + + let roots_paths: Vec = paths + .iter() + .filter(|p| { + let parent = p.parent(); + parent.is_none() + || parent.unwrap() == Path::new("") + || !paths.contains(&parent.unwrap().to_path_buf()) + }) + .cloned() + .collect(); + + let mut top_nodes: Vec = roots_paths + .into_iter() + .map(|r| { + build_node( + &r, + &tree_map, + base_path_obj, + sort_strategy, + weight_cfg, + no_emoji, + ) + }) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_strategy); + + // [ENG]: Logic for creating the final root node with proper weight calculation. + // [POL]: Logika tworzenia końcowego węzła głównego z poprawnym obliczeniem wagi. + let final_roots = if let Some(r_name) = root_name { + // [ENG]: Calculate total weight for the root node. + // [POL]: Obliczenie całkowitej wagi dla węzła głównego. + let root_bytes = if weight_cfg.dir_sum_included { + // [POL]: Suma wag bezpośrednich dzieci (dopasowanych elementów). + top_nodes.iter().map(|n| n.weight_bytes).sum() + } else { + // [POL]: Fizyczna waga folderu wejściowego z dysku. + weight::get_path_weight(base_path_obj, false) + }; + + let root_weight_str = weight::format_weight(root_bytes, true, weight_cfg); + + vec![FileNode { + name: r_name.to_string(), + path: PathBuf::from(r_name), + is_dir: true, + icon: if no_emoji { + String::new() + } else { + DIR_ICON.to_string() + }, + weight_str: root_weight_str, + weight_bytes: root_bytes, + children: top_nodes, + }] + } else { + top_nodes + }; + + Self { + roots: final_roots, + style: TreeStyle::default(), + } + } + + #[must_use] + pub fn render_cli(&self) -> String { + self.plot(&self.roots, "", true) + } + + #[must_use] + pub fn render_txt(&self) -> String { + self.plot(&self.roots, "", false) + } + + fn plot(&self, nodes: &[FileNode], indent: &str, use_color: bool) -> String { + let mut result = String::new(); + for (i, node) in nodes.iter().enumerate() { + let is_last = i == nodes.len() - 1; + let has_children = !node.children.is_empty(); + + let branch = if node.is_dir { + match (is_last, has_children) { + (true, true) => &self.style.dir_last_with_children, + (false, true) => &self.style.dir_mid_with_children, + (true, false) => &self.style.dir_last_no_children, + (false, false) => &self.style.dir_mid_no_children, + } + } else if is_last { + &self.style.file_last + } else { + &self.style.file_mid + }; + + let weight_prefix = if node.weight_str.is_empty() { + String::new() + } else if use_color { + node.weight_str.truecolor(120, 120, 120).to_string() + } else { + node.weight_str.clone() + }; + + let line = if use_color { + if node.is_dir { + format!( + "{weight_prefix}{}{branch_color} {icon} {name}\n", + indent.green(), + branch_color = branch.green(), + icon = node.icon, + name = node.name.truecolor(200, 200, 50) + ) + } else { + format!( + "{weight_prefix}{}{branch_color} {icon} {name}\n", + indent.green(), + branch_color = branch.green(), + icon = node.icon, + name = node.name.white() + ) + } + } else { + format!( + "{weight_prefix}{indent}{branch} {icon} {name}\n", + icon = node.icon, + name = node.name + ) + }; + + result.push_str(&line); + + if has_children { + let new_indent = if is_last { + format!("{indent}{}", self.style.indent_last) + } else { + format!("{indent}{}", self.style.indent_mid) + }; + result.push_str(&self.plot(&node.children, &new_indent, use_color)); + } + } + result + } +} + +``` + +### 019: `./src/core/patterns_expand.rs` + +```rust +/// [POL]: Kontekst wzorców - przechowuje oryginalne wzorce użytkownika oraz ich rozwiniętą formę. +/// [ENG]: Pattern context - stores original user patterns and their tok form. +#[derive(Debug, Clone)] +pub struct PatternContext { + pub raw: Vec, + pub tok: Vec, +} + +impl PatternContext { + /// [POL]: Tworzy nowy kontekst, automatycznie rozwijając klamry w podanych wzorcach. + /// [ENG]: Creates a new context, automatically expanding braces in the provided patterns. + pub fn new(patterns: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + let mut raw = Vec::new(); + let mut tok = Vec::new(); + + for pat in patterns { + let pat_str = pat.as_ref(); + raw.push(pat_str.to_string()); + tok.extend(Self::expand_braces(pat_str)); + } + + Self { raw, tok } + } + + /// [POL]: Prywatna metoda: rozwija klamry we wzorcu (np. {a,b} -> [a, b]). Obsługuje rekurencję. + /// [ENG]: Private method: expands braces in a pattern (e.g. {a,b} -> [a, b]). Supports recursion. + fn expand_braces(pattern: &str) -> Vec { + if let (Some(start), Some(end)) = (pattern.find('{'), pattern.find('}')) + && start < end + { + let prefix = &pattern[..start]; + let suffix = &pattern[end + 1..]; + let options = &pattern[start + 1..end]; + + let mut tok = Vec::new(); + for opt in options.split(',') { + let new_pattern = format!("{}{}{}", prefix, opt, suffix); + tok.extend(Self::expand_braces(&new_pattern)); + } + return tok; + } + vec![pattern.to_string()] + } +} + +``` + +### 020: `./src/core/save.rs` + +```rust +use super::super::i18n::I18n; +use crate::theme::for_path_tree::get_file_type; +use std::fs; +use std::path::Path; + +pub struct SaveFile; + +impl SaveFile { + // ⚡ Nowa funkcja tabelarycznej stopki + pub fn generate_by_section(tag: &str, enter_path: &str, i18n: &I18n, cmd: &str) -> String { + let mut f = String::new(); + f.push_str("\n\n---\n\n"); + f.push_str("> | Property | Value |\n"); + f.push_str("> | ---: | :--- |\n"); + f.push_str(&format!( + "> | **{}** | `cargo-plot v0.2.0` |\n", + i18n.footer_tool() + )); + f.push_str(&format!( + "> | **{}** | `{}` |\n", + i18n.footer_input(), + enter_path + )); + f.push_str(&format!("> | **{}** | `{}` |\n", i18n.footer_cmd(), cmd)); + f.push_str(&format!("> | **{}** | `{}` |\n", i18n.footer_tag(), tag)); + + let links = "[Crates.io](https://crates.io/crates/cargo-plot) \\| [GitHub](https://github.com/j-Cis/cargo-plot/releases)"; + f.push_str(&format!("> | **{}** | {} |\n", i18n.footer_links(), links)); + f.push_str(&format!( + "> | **{}** | `cargo install cargo-plot` |\n", + i18n.footer_links() + )); + f.push_str(&format!( + "> | **{}** | `cargo plot --help` |\n", + i18n.footer_help() + )); + f.push_str("\n---\n"); + f + } + + /// Wspólna logika zapisu do pliku (DRY): tworzenie folderów i zapis IO. + fn write_to_disk(filepath: &str, content: &str, log_name: &str, i18n: &I18n) { + let path = Path::new(filepath); + + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + && !parent.exists() + && let Err(e) = fs::create_dir_all(parent) + { + eprintln!( + "{}", + i18n.dir_create_err(&parent.to_string_lossy(), &e.to_string()) + ); + return; + } + + match fs::write(path, content) { + Ok(_) => println!("{}", i18n.save_success(log_name, filepath)), + Err(e) => eprintln!("{}", i18n.save_err(log_name, filepath, &e.to_string())), + } + } + + /// Formatowanie i zapis samego widoku struktury (ścieżek) + pub fn paths( + content: &str, + filepath: &str, + tag: &str, + add_by: bool, + i18n: &I18n, + cmd: &str, + enter_path: &str, + ) { + let by_section = if add_by { + Self::generate_by_section(tag, enter_path, i18n, cmd) + } else { + String::new() + }; + let internal_tag = if add_by { "" } else { tag }; + let file_name = Path::new(filepath) + .file_name() + .unwrap_or_default() + .to_string_lossy(); + + let markdown_content = format!( + "# {}\n\n```plaintext\n{}\n```\n\n{}{}", + file_name, content, internal_tag, by_section + ); + + Self::write_to_disk( + filepath, + &markdown_content, + if i18n.lang == crate::i18n::Lang::Pl { + "ścieżki" + } else { + "paths" + }, + i18n, + ); + } + + /// Formatowanie i zapis pełnego cache (drzewo + zawartość plików) + pub fn codes( + tree_text: &str, + paths: &[String], + base_dir: &str, + filepath: &str, + tag: &str, + add_by: bool, + i18n: &I18n, + cmd: &str, + enter_path: &str, + ) { + let by_section = if add_by { + Self::generate_by_section(tag, enter_path, i18n, cmd) + } else { + String::new() + }; + let internal_tag = if add_by { "" } else { tag }; + let file_name = Path::new(filepath) + .file_name() + .unwrap_or_default() + .to_string_lossy(); + + let mut content = String::new(); + content.push_str(&format!("# {}\n\n", file_name)); + + // Wstawiamy wygenerowane drzewo ścieżek + content.push_str("```plaintext\n"); + content.push_str(tree_text); + content.push_str("```\n\n"); + + let mut counter = 1; + + for p_str in paths { + if p_str.ends_with('/') { + continue; // Pomijamy katalogi + } + + let absolute_path = Path::new(base_dir).join(p_str); + let ext = absolute_path + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + + let lang = get_file_type(&ext).md_lang; + + if is_blacklisted_extension(&ext) { + content.push_str(&format!( + "### {:03}: `{}`\n\n{}\n\n", + counter, + p_str, + i18n.skip_binary() + )); + counter += 1; + continue; + } + + match fs::read_to_string(&absolute_path) { + Ok(file_content) => { + content.push_str(&format!( + "### {:03}: `{}`\n\n```{}\n{}\n```\n\n", + counter, p_str, lang, file_content + )); + } + Err(_) => { + content.push_str(&format!( + "### {:03}: `{}`\n\n{}\n\n", + counter, + p_str, + i18n.read_err() + )); + } + } + counter += 1; + } + + content.push_str(&format!("\n\n{}{}", internal_tag, by_section)); + Self::write_to_disk( + filepath, + &content, + if i18n.lang == crate::i18n::Lang::Pl { + "kod (cache)" + } else { + "code (cache)" + }, + i18n, + ); + } +} + +// [EN]: Security mechanisms to prevent processing non-text or binary files. +// [PL]: Mechanizmy bezpieczeństwa zapobiegające przetwarzaniu plików nietekstowych lub binarnych. + +/// [EN]: Checks if a file extension is on the list of forbidden binary types. +/// [PL]: Sprawdza, czy rozszerzenie pliku znajduje się na liście zabronionych typów binarnych. +pub fn is_blacklisted_extension(ext: &str) -> bool { + let e = ext.to_lowercase(); + + matches!( + e.as_str(), + // -------------------------------------------------- + // GRAFIKA I DESIGN + // -------------------------------------------------- + "png" | "jpg" | "jpeg" | "gif" | "bmp" | "ico" | "svg" | "webp" | "tiff" | "tif" | "heic" | "psd" | + "ai" | + // -------------------------------------------------- + // BINARKI | BIBLIOTEKI I ARTEFAKTY KOMPILACJI + // -------------------------------------------------- + "exe" | "dll" | "so" | "dylib" | "bin" | "wasm" | "pdb" | "rlib" | "rmeta" | "lib" | + "o" | "a" | "obj" | "pch" | "ilk" | "exp" | + "jar" | "class" | "war" | "ear" | + "pyc" | "pyd" | "pyo" | "whl" | + // -------------------------------------------------- + // ARCHIWA I PACZKI + // -------------------------------------------------- + "zip" | "tar" | "gz" | "tgz" | "7z" | "rar" | "bz2" | "xz" | "iso" | "dmg" | "pkg" | "apk" | + // -------------------------------------------------- + // DOKUMENTY | BAZY DANYCH I FONTY + // -------------------------------------------------- + "sqlite" | "sqlite3" | "db" | "db3" | "mdf" | "ldf" | "rdb" | + "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" | "ods" | "odp" | + "woff" | "woff2" | "ttf" | "eot" | "otf" | + // -------------------------------------------------- + // MEDIA (AUDIO / WIDEO) + // -------------------------------------------------- + "mp3" | "mp4" | "avi" | "mkv" | "wav" | "flac" | "ogg" | "m4a" | "mov" | "wmv" | "flv" + ) +} + +``` + +### 021: `./src/execute.rs` + +```rust +use crate::core::file_stats::FileStats; +use crate::core::file_stats::weight::WeightConfig; +pub use crate::core::path_matcher::SortStrategy; +use crate::core::path_matcher::{MatchStats, PathMatchers, ShowMode}; +use crate::core::path_store::{PathContext, PathStore}; +use crate::core::path_view::{PathGrid, PathList, PathTree, ViewMode}; +use crate::core::patterns_expand::PatternContext; +use std::path::Path; + +/// [ENG]: Primary execution function that coordinates scanning, matching, and view building. +/// [POL]: Główna funkcja wykonawcza koordynująca skanowanie, dopasowywanie i budowanie widoków. +pub fn execute( + enter_path: &str, + patterns: &[String], + is_case_sensitive: bool, + sort_strategy: SortStrategy, + show_mode: ShowMode, + view_mode: ViewMode, + weight_cfg: WeightConfig, // ⚡ Używamy konfiguracji przekazanej z CLI/GUI + no_root: bool, + print_info: bool, + no_emoji: bool, + i18n: &crate::i18n::I18n, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, +) -> MatchStats +where + OnMatch: FnMut(&FileStats), + OnMismatch: FnMut(&FileStats), +{ + // [ENG]: 1. Initialize contexts. + // [POL]: 1. Inicjalizacja kontekstów. + let pattern_ctx = PatternContext::new(patterns); + let path_ctx = PathContext::resolve(enter_path).unwrap_or_else(|e| { + eprintln!("❌ {}", e); + std::process::exit(1); + }); + + // [ENG]: 2. Initial state logging (Restored full verbosity). + // [POL]: 2. Logowanie stanu początkowego (Przywrócono pełną szczegółowość). + if print_info { + println!("{}", i18n.cli_base_abs(&path_ctx.base_absolute)); + println!("{}", i18n.cli_target_abs(&path_ctx.entry_absolute)); + println!("{}", i18n.cli_target_rel(&path_ctx.entry_relative)); + println!("---------------------------------------"); + println!("{}", i18n.cli_case_sensitive(is_case_sensitive)); + println!( + "{}", + i18n.cli_patterns_raw(&format!("{:?}", pattern_ctx.raw)) + ); + println!( + "{}", + i18n.cli_patterns_tok(&format!("{:?}", pattern_ctx.tok)) + ); + println!("---------------------------------------"); + } else { + println!("---------------------------------------"); + } + + // [ENG]: 3. Build matchers. + // [POL]: 3. Budowa silników dopasowujących. + let matchers = + PathMatchers::new(&pattern_ctx.tok, is_case_sensitive).expect("Błąd kompilacji wzorców"); + + // [ENG]: 4. Scan disk. + // [POL]: 4. Skanowanie dysku. + let paths_store = PathStore::scan(&path_ctx.entry_absolute); + let paths_set = paths_store.get_index(); + let entry_abs = path_ctx.entry_absolute.clone(); + + // [ENG]: 6. Evaluate paths and fetch stats via callbacks. + // [POL]: 6. Ewaluacja ścieżek i pobieranie statystyk przez callbacki. + let mut stats = matchers.evaluate( + &paths_store.list, + &paths_set, + sort_strategy, + show_mode, + |raw_path| { + let stats = FileStats::fetch(raw_path, &entry_abs); + on_match(&stats); + }, + |raw_path| { + let stats = FileStats::fetch(raw_path, &entry_abs); + on_mismatch(&stats); + }, + ); + + // [ENG]: 7. Build views using the provided weight configuration. + // [POL]: 7. Budowa widoków przy użyciu dostarczonej konfiguracji wagi. + let root_name = if no_root { + None + } else { + Path::new(&path_ctx.entry_absolute) + .file_name() + .and_then(|n| n.to_str()) + }; + + let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; + let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; + + match view_mode { + ViewMode::Grid => { + if do_include { + stats.m_matched.grid = Some(PathGrid::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + no_emoji, + )); + } + if do_exclude { + stats.x_mismatched.grid = Some(PathGrid::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + no_emoji, + )); + } + } + ViewMode::Tree => { + if do_include { + stats.m_matched.tree = Some(PathTree::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + no_emoji, + )); + } + if do_exclude { + stats.x_mismatched.tree = Some(PathTree::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + no_emoji, + )); + } + } + ViewMode::List => { + if do_include { + stats.m_matched.list = Some(PathList::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + no_emoji, + )); + } + if do_exclude { + stats.x_mismatched.list = Some(PathList::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + no_emoji, + )); + } + } + } + + stats +} + +``` + +### 022: `./src/i18n.rs` + +```rust +use std::env; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum Lang { + Pl, + En, +} + +impl Lang { + pub fn detect() -> Self { + if env::var("LANG") + .unwrap_or_default() + .to_lowercase() + .starts_with("pl") + { + Self::Pl + } else { + Self::En + } + } +} + +pub struct I18n { + pub lang: Lang, +} + +impl I18n { + pub fn new(lang: Option) -> Self { + Self { + lang: lang.unwrap_or_else(Lang::detect), + } + } + + // ===================================================================== + // 1. TOP - TEKST OGÓLNY + // ===================================================================== + pub fn save_success(&self, name: &str, path: &str) -> String { + match self.lang { + Lang::Pl => format!("💾 Pomyślnie zapisano {} do pliku: {}", name, path), + Lang::En => format!("💾 Successfully saved {} to file: {}", name, path), + } + } + pub fn save_err(&self, name: &str, path: &str, err: &str) -> String { + match self.lang { + Lang::Pl => format!("❌ Błąd zapisu {} do pliku {}: {}", name, path, err), + Lang::En => format!("❌ Error saving {} to file {}: {}", name, path, err), + } + } + pub fn dir_create_err(&self, dir: &str, err: &str) -> String { + match self.lang { + Lang::Pl => format!("❌ Błąd: Nie można utworzyć katalogu {} ({})", dir, err), + Lang::En => format!("❌ Error: Cannot create directory {} ({})", dir, err), + } + } + + // ===================================================================== + // 2. LIB / CORE - LOGIKA BAZOWA + // ===================================================================== + pub fn skip_binary(&self) -> &'static str { + match self.lang { + Lang::Pl => "> *(Plik binarny/graficzny - pominięto zawartość)*", + Lang::En => "> *(Binary/graphic file - content skipped)*", + } + } + pub fn read_err(&self) -> &'static str { + match self.lang { + Lang::Pl => "> *(Błąd odczytu / plik nie jest UTF-8)*", + Lang::En => "> *(Read error / file is not UTF-8)*", + } + } + + pub fn footer_tool(&self) -> &str { + match self.lang { + Lang::Pl => "Narzędzie", + _ => "Tool", + } + } + pub fn footer_input(&self) -> &str { + match self.lang { + Lang::Pl => "Folder", + _ => "Input", + } + } + pub fn footer_cmd(&self) -> &str { + match self.lang { + Lang::Pl => "Komenda", + _ => "Command", + } + } + pub fn footer_tag(&self) -> &str { + match self.lang { + Lang::Pl => "Tag", + _ => "TimeTag", + } + } + pub fn footer_links(&self) -> &str { + match self.lang { + Lang::Pl => "Linki", + _ => "Links", + } + } + pub fn footer_help(&self) -> &str { + match self.lang { + Lang::Pl => "Pomoc", + _ => "Help", + } + } + + // ===================================================================== + // 3. CLI - INTERFEJS TERMINALOWY + // ===================================================================== + pub fn cli_base_abs(&self, path: &str) -> String { + match self.lang { + Lang::Pl => format!("📂 Baza terminala (Absolutna): {}", path), + Lang::En => format!("📂 Terminal base (Absolute): {}", path), + } + } + pub fn cli_target_abs(&self, path: &str) -> String { + match self.lang { + Lang::Pl => format!("📂 Cel skanowania (Absolutna): {}", path), + Lang::En => format!("📂 Scan target (Absolute): {}", path), + } + } + pub fn cli_target_rel(&self, path: &str) -> String { + match self.lang { + Lang::Pl => format!("📂 Cel skanowania (Relatywna): {}", path), + Lang::En => format!("📂 Scan target (Relative): {}", path), + } + } + pub fn cli_case_sensitive(&self, val: bool) -> String { + match self.lang { + Lang::Pl => format!("🔠 Wrażliwość na litery: {}", val), + Lang::En => format!("🔠 Case sensitive: {}", val), + } + } + pub fn cli_patterns_raw(&self, pat: &str) -> String { + match self.lang { + Lang::Pl => format!("🔍 Wzorce (RAW): {}", pat), + Lang::En => format!("🔍 Patterns (RAW): {}", pat), + } + } + pub fn cli_patterns_tok(&self, pat: &str) -> String { + match self.lang { + Lang::Pl => format!("⚙️ Wzorce (TOK): {}", pat), + Lang::En => format!("⚙️ Patterns (TOK): {}", pat), + } + } + pub fn cli_summary_matched(&self, count: usize, total: usize) -> String { + match self.lang { + Lang::Pl => format!("📊 Podsumowanie: Dopasowano {} z {} ścieżek.", count, total), + Lang::En => format!("📊 Summary: Matched {} of {} paths.", count, total), + } + } + pub fn cli_summary_rejected(&self, count: usize, total: usize) -> String { + match self.lang { + Lang::Pl => format!("📊 Podsumowanie: Odrzucono {} z {} ścieżek.", count, total), + Lang::En => format!("📊 Summary: Rejected {} of {} paths.", count, total), + } + } +} + +``` + +### 023: `./src/interfaces.rs` + +```rust +// [ENG]: User interaction layer (Ports and Adapters). +// [POL]: Warstwa interakcji z użytkownikiem (Porty i Adaptery). + +pub mod cli; +pub mod gui; +pub mod tui; + +``` + +### 024: `./src/interfaces/cli.rs` + +```rust +pub mod args; +pub mod engine; + +use self::args::CargoCli; +use clap::Parser; + +// [ENG]: Main entry point for the CLI interface and global router. +// [POL]: Główny punkt wejścia dla interfejsu CLI i globalny router. +pub fn run_cli() { + let args_os = std::env::args(); + let mut args: Vec = args_os.collect(); + + // ⚡ NOWOŚĆ: Jeśli wywołano bez żadnych argumentów (samo `cargo plot`), + // wstrzykujemy domyślnie flagę `-g` (GUI). + let is_empty = args.len() == 1 || (args.len() == 2 && args[1] == "plot"); + if is_empty { + args.push("-g".to_string()); + } + + // [ENG]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. + // [POL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. + if args.len() > 1 && args[1] != "plot" { + args.insert(1, "plot".to_string()); + } + + // [ENG]: Parse from the modified list. + // [POL]: Parsowanie ze zmodyfikowanej listy. + let CargoCli::Plot(flags) = CargoCli::parse_from(args); + + // [ENG]: Transfer control based on parsed flags. + // [POL]: Przekazanie kontroli na podstawie sparsowanych flag. + if flags.gui { + crate::interfaces::gui::run_gui(flags); + } else if flags.tui { + crate::interfaces::tui::run_tui(); + } else { + engine::run(flags); + } +} + +``` + +### 025: `./src/interfaces/cli/args.rs` + +```rust +use cargo_plot::core::path_matcher::SortStrategy; +use cargo_plot::core::path_view::ViewMode; +use cargo_plot::i18n::Lang; +use clap::{Args, Parser, ValueEnum}; + +/// [ENG]: Main wrapper for the Cargo plugin. +/// [POL]: Główny wrapper dla wtyczki Cargo. +#[derive(Parser, Debug)] +#[command(name = "cargo", bin_name = "cargo")] +pub enum CargoCli { + /// [ENG]: Cargo plot subcommand. + /// [POL]: Podkomenda cargo plot. + Plot(CliArgs), +} + +/// [ENG]: Command line arguments for cargo-plot. +/// [POL]: Argumenty wiersza poleceń dla cargo-plot. +#[derive(Args, Debug, Clone)] +#[command(author, version, about = "Skaner struktury plików / File structure scanner", long_about = None)] +pub struct CliArgs { + /// [ENG]: 📂 Input path to scan. + /// [POL]: 📂 Ścieżka wejściowa do skanowania. + #[arg(short = 'd', long = "dir", default_value = ".")] + pub enter_path: String, + + /// [ENG]: 💾 Output directory path for saved results. + /// [POL]: 💾 Ścieżka do katalogu wyjściowego na rezultaty. + #[arg(short = 'o', long = "dir-out", num_args = 0..=1, default_missing_value = "AUTO")] + pub dir_out: Option, + + /// [ENG]: 🔍 Match patterns. + /// [POL]: 🔍 Wzorce dopasowań. + #[arg(short = 'p', long = "pat", required_unless_present_any = ["gui", "tui"])] + pub patterns: Vec, + + /// [ENG]: ✔️ Treat patterns as match (include) rules. + /// [POL]: ✔️ Traktuj wzorce jako zasady dopasowania (włącz). + #[arg(short = 'm', long = "pat-match", required_unless_present_any = ["exclude", "gui", "tui"])] + pub include: bool, + + /// [ENG]: ❌ Treat patterns as mismatch (exclude) rules. + /// [POL]: ❌ Traktuj wzorce jako zasady odrzucenia (wyklucz). + #[arg(short = 'x', long = "pat-mismatch", required_unless_present_any = ["include", "gui", "tui"])] + pub exclude: bool, + + /// [ENG]: 🔠 Ignore case sensitivity in patterns. + /// [POL]: 🔠 Ignoruj wielkość liter we wzorcach. + #[arg(short = 'c', long = "pat-ignore-case")] + pub ignore_case: bool, + + /// [ENG]: 🗂️ Results sorting strategy. + /// [POL]: 🗂️ Strategia sortowania wyników. + #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] + pub sort: CliSortStrategy, + + /// [ENG]: 👁️ Selects the display format (tree, list, grid). + /// [POL]: 👁️ Wybiera format wyświetlania wyników (drzewo, lista, siatka). + #[arg(short = 'v', long = "view", value_enum, default_value_t = CliViewMode::Tree)] + pub view: CliViewMode, + + /// [ENG]: 📝 Save the paths structure to a file. + /// [POL]: 📝 Zapisuje strukturę ścieżek do pliku. + #[arg(long = "save-address")] + pub save_address: bool, + + /// [ENG]: 📦 Save the file contents archive to a file. + /// [POL]: 📦 Zapisuje archiwum z zawartością plików. + #[arg(long = "save-archive")] + pub save_archive: bool, + + /// [ENG]: 🏷️ Add a footer with command information to saved files. + /// [POL]: 🏷️ Dodaje stopkę z informacją o komendzie do zapisanych plików. + #[arg(short = 'b', long = "by")] + pub by: bool, + + /// [ENG]: 🌳 Hide the root directory in the tree view. + /// [POL]: 🌳 Ukrywa główny folder (korzeń) w widoku drzewa. + #[arg(long = "treeview-no-root")] + pub no_root: bool, + + /// [ENG]: ℹ️ Display summary statistics and headers. + /// [POL]: ℹ️ Wyświetla statystyki podsumowujące i nagłówki. + #[arg(short = 'i', long = "info")] + pub info: bool, + + /// [ENG]: 🚫 Disable emoji rendering in the output. + /// [POL]: 🚫 Wyłącza renderowanie ikon/emoji w wynikach. + #[arg(long = "no-emoji")] + pub no_emoji: bool, + + /// [ENG]: 🖥️ Launch the application in Graphical User Interface (GUI) mode. + /// [POL]: 🖥️ Uruchamia aplikację w trybie graficznym (GUI). + #[arg(short = 'g', long = "gui")] + pub gui: bool, + + /// [ENG]: ⌨️ Launch the application in Terminal User Interface (TUI) mode. + /// [POL]: ⌨️ Uruchamia aplikację w interaktywnym trybie terminalowym (TUI). + #[arg(short = 't', long = "tui")] + pub tui: bool, + + /// [ENG]: 🌍 Force a specific interface language. + /// [POL]: 🌍 Wymusza określony język interfejsu. + #[arg(long, value_enum)] + pub lang: Option, + + /// [ENG]: ⚖️ Weight unit system (dec for SI, bin for IEC). + /// [POL]: ⚖️ System jednostek wagi (dec dla SI, bin dla IEC). + #[arg(short = 'u', long = "unit", value_enum, default_value_t = CliUnitSystem::Bin)] + pub unit: CliUnitSystem, + + /// [ENG]: 🧮 Calculate actual folder weight including unmatched files. + /// [POL]: 🧮 Oblicza rzeczywistą wagę folderu wliczając wszystkie pliki. + #[arg(short = 'a', long = "all")] + pub all: bool, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum CliViewMode { + Tree, + List, + Grid, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum CliSortStrategy { + None, + Az, + Za, + AzFile, + ZaFile, + AzDir, + ZaDir, + AzFileMerge, + ZaFileMerge, + AzDirMerge, + ZaDirMerge, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum CliUnitSystem { + Dec, + Bin, +} + +impl From for SortStrategy { + fn from(val: CliSortStrategy) -> Self { + match val { + CliSortStrategy::None => SortStrategy::None, + CliSortStrategy::Az => SortStrategy::Az, + CliSortStrategy::Za => SortStrategy::Za, + CliSortStrategy::AzFile => SortStrategy::AzFileFirst, + CliSortStrategy::ZaFile => SortStrategy::ZaFileFirst, + CliSortStrategy::AzDir => SortStrategy::AzDirFirst, + CliSortStrategy::ZaDir => SortStrategy::ZaDirFirst, + CliSortStrategy::AzFileMerge => SortStrategy::AzFileFirstMerge, + CliSortStrategy::ZaFileMerge => SortStrategy::ZaFileFirstMerge, + CliSortStrategy::AzDirMerge => SortStrategy::AzDirFirstMerge, + CliSortStrategy::ZaDirMerge => SortStrategy::ZaDirFirstMerge, + } + } +} + +impl From for ViewMode { + fn from(val: CliViewMode) -> Self { + match val { + CliViewMode::Tree => ViewMode::Tree, + CliViewMode::List => ViewMode::List, + CliViewMode::Grid => ViewMode::Grid, + } + } +} + +impl CliArgs { + /// [ENG]: Reconstructs a clean terminal command string. + /// [POL]: Odtwarza czystą komendę terminalową. + pub fn to_command_string( + &self, + is_m: bool, + is_x: bool, + is_address: bool, + is_archive: bool, + ) -> String { + let mut cmd = vec!["cargo".to_string(), "plot".to_string()]; + + if self.enter_path != "." && !self.enter_path.is_empty() { + cmd.push("-d".to_string()); + cmd.push(format!("\"{}\"", self.enter_path)); + } + + if let Some(dir) = &self.dir_out { + cmd.push("-o".to_string()); + if dir != "AUTO" { + cmd.push(format!("\"{}\"", dir)); + } else { + cmd.push("AUTO".to_string()); + } + } + + // ⚡ POPRAWKA 1: Wzorce -p są teraz iterowane i dodawane osobno + if !self.patterns.is_empty() { + for pattern in &self.patterns { + cmd.push("-p".to_string()); + cmd.push(format!("\"{}\"", pattern)); + } + } + + // ⚡ GWARANCJA POPRAWNOŚCI: Komenda idealnie dopasowana do zapisywanego pliku + if is_m { + cmd.push("-m".to_string()); + } + if is_x { + cmd.push("-x".to_string()); + } + + if self.ignore_case { + cmd.push("-c".to_string()); + } + + if self.sort != CliSortStrategy::AzFileMerge { + let sort_str = match self.sort { + CliSortStrategy::None => "none", + CliSortStrategy::Az => "az", + CliSortStrategy::Za => "za", + CliSortStrategy::AzFile => "az-file", + CliSortStrategy::ZaFile => "za-file", + CliSortStrategy::AzDir => "az-dir", + CliSortStrategy::ZaDir => "za-dir", + CliSortStrategy::AzFileMerge => "az-file-merge", + CliSortStrategy::ZaFileMerge => "za-file-merge", + CliSortStrategy::AzDirMerge => "az-dir-merge", + CliSortStrategy::ZaDirMerge => "za-dir-merge", + }; + cmd.push("-s".to_string()); + cmd.push(sort_str.to_string()); + } + + if self.view != CliViewMode::Tree { + let view_str = match self.view { + CliViewMode::Tree => "tree", + CliViewMode::List => "list", + CliViewMode::Grid => "grid", + }; + cmd.push("-v".to_string()); + cmd.push(view_str.to_string()); + } + + // ⚡ GWARANCJA POPRAWNOŚCI: Wymuszamy flagi zapisu zależnie od tego, z jakiego miejsca generujemy raport + if self.save_address || is_address { + cmd.push("--save-address".to_string()); + } + if self.save_archive || is_archive { + cmd.push("--save-archive".to_string()); + } + if self.by { + cmd.push("-b".to_string()); + } + if self.no_root { + cmd.push("--treeview-no-root".to_string()); + } + if self.info { + cmd.push("-i".to_string()); + } + if self.no_emoji { + cmd.push("--no-emoji".to_string()); + } + if self.all { + cmd.push("-a".to_string()); + } + + if self.unit != CliUnitSystem::Bin { + cmd.push("-u".to_string()); + cmd.push("dec".to_string()); + } + + if let Some(l) = &self.lang { + cmd.push("--lang".to_string()); + match l { + Lang::Pl => cmd.push("pl".to_string()), + Lang::En => cmd.push("en".to_string()), + } + } + + cmd.join(" ") + } +} + +``` + +### 026: `./src/interfaces/cli/engine.rs` + +```rust +use crate::interfaces::cli::args::{CliArgs, CliUnitSystem}; +use cargo_plot::addon::TimeTag; +use cargo_plot::core::file_stats::weight::{UnitSystem, WeightConfig}; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::path_store::PathContext; +use cargo_plot::core::path_view::ViewMode; +use cargo_plot::core::save::SaveFile; +use cargo_plot::execute::{self, SortStrategy}; +use cargo_plot::i18n::I18n; + +/// [ENG]: ⚙️ Main execution engine coordinating the scanning and rendering process. +/// [POL]: ⚙️ Główny silnik wykonawczy koordynujący proces skanowania i renderowania. +pub fn run(args: CliArgs) { + // [ENG]: 📝 Initialize i18n and resolve basic flags. + // [POL]: 📝 Inicjalizacja i18n i rozwiązanie podstawowych flag. + let i18n = I18n::new(args.lang); + let is_case_sensitive = !args.ignore_case; + let sort_strategy: SortStrategy = args.sort.into(); + let view_mode: ViewMode = args.view.into(); + + // [ENG]: ⚖️ Define weight calculation rules based on unit and 'all' flags. + // [POL]: ⚖️ Definicja reguł obliczania wagi na podstawie flag jednostki oraz 'all'. + let weight_cfg = WeightConfig { + system: match args.unit { + CliUnitSystem::Bin => UnitSystem::Binary, + CliUnitSystem::Dec => UnitSystem::Decimal, + }, + // [POL]: Jeśli 'all' (-a) jest true, liczymy fizyczną wagę z dysku dla folderów. + dir_sum_included: !args.all, + ..WeightConfig::default() + }; + + // [ENG]: 🎚️ Determines the display mode based on include (-m) and exclude (-x) flags. + // [POL]: 🎚️ Ustala tryb wyświetlania na podstawie flag włączania (-m) i wykluczania (-x). + let show_mode = match (args.include, args.exclude) { + (true, false) => ShowMode::Include, + (false, true) => ShowMode::Exclude, + _ => ShowMode::Context, + }; + + // [ENG]: 🚀 Executes the core matching logic with prepared weight configuration. + // [POL]: 🚀 Wykonuje główną logikę dopasowywania z przygotowaną konfiguracją wagi. + let stats = execute::execute( + &args.enter_path, + &args.patterns, + is_case_sensitive, + sort_strategy, + show_mode, + view_mode, + weight_cfg, // ⚡ WSTRZYKNIĘTE + args.no_root, + args.info, + args.no_emoji, + &i18n, + |_| {}, + |_| {}, + ); + + // [ENG]: 🖥️ Renders the output to the terminal with ANSI colors. + // [POL]: 🖥️ Renderuje wynik do terminala z użyciem kolorów ANSI. + let output_str_cli = stats.render_output(view_mode, show_mode, args.info, true); + print!("{}", output_str_cli); + + // [ENG]: 💾 Handles file saving if address or archive flags are active. + // [POL]: 💾 Obsługuje zapis do plików, jeśli aktywne są flagi adresu lub archiwum. + if args.save_address || args.save_archive { + let tag = TimeTag::now(); + + // [ENG]: 📄 Renders plain text for Markdown output. + // [POL]: 📄 Renderuje czysty tekst dla wyjścia w formacie Markdown. + let output_str_txt_m = stats.render_output(view_mode, ShowMode::Include, args.info, false); + let output_str_txt_x = stats.render_output(view_mode, ShowMode::Exclude, args.info, false); + + // [ENG]: 📂 Resolves the output directory path to .cargo-plot/ by default. + // [POL]: 📂 Rozwiązuje ścieżkę katalogu wyjściowego (domyślnie na .cargo-plot/). + let resolve_dir = |val: &Option, base_path: &str| -> String { + let is_auto = val + .as_ref() + .is_none_or(|v| v.trim().is_empty() || v == "AUTO"); + if is_auto { + let mut b = base_path.replace('\\', "/"); + if !b.ends_with('/') { + b.push('/'); + } + format!("{}.cargo-plot/", b) + } else { + let mut p = val.as_ref().unwrap().replace('\\', "/"); + if !p.ends_with('/') { + p.push('/'); + } + p + } + }; + + let output_dir = resolve_dir(&args.dir_out, &args.enter_path); + + // [ENG]: 📝 Saves the path structure (address). + // [POL]: 📝 Zapisuje strukturę ścieżek (adres). + if args.save_address { + if args.include || !args.exclude { + let filepath = format!("{}plot-address_{}_M.md", output_dir, tag); + let cmd_m = args.to_command_string(true, false, true, false); + SaveFile::paths( + &output_str_txt_m, + &filepath, + &tag, + args.by, + &i18n, + &cmd_m, + &args.enter_path, + ); + } + if args.exclude || !args.include { + let filepath = format!("{}plot-address_{}_X.md", output_dir, tag); + let cmd_x = args.to_command_string(false, true, true, false); + SaveFile::paths( + &output_str_txt_x, + &filepath, + &tag, + args.by, + &i18n, + &cmd_x, + &args.enter_path, + ); + } + } + + // [ENG]: 📦 Saves the full file contents (archive). + // [POL]: 📦 Zapisuje pełną zawartość plików (archiwum). + if args.save_archive + && let Ok(ctx) = PathContext::resolve(&args.enter_path) + { + if args.include || !args.exclude { + let filepath = format!("{}plot-archive_{}_M.md", output_dir, tag); + let cmd_m = args.to_command_string(true, false, false, true); + SaveFile::codes( + &output_str_txt_m, + &stats.m_matched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_m, + &args.enter_path, + ); + } + if args.exclude || !args.include { + let filepath = format!("{}plot-archive_{}_X.md", output_dir, tag); + let cmd_x = args.to_command_string(false, true, false, true); + SaveFile::codes( + &output_str_txt_x, + &stats.x_mismatched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_x, + &args.enter_path, + ); + } + } + } + + // [ENG]: 📊 Prints summary statistics if info flag is active. + // [POL]: 📊 Wyświetla statystyki podsumowujące, jeśli aktywna jest flaga info. + if args.info { + println!("---------------------------------------"); + println!( + "{}", + i18n.cli_summary_matched(stats.m_size_matched, stats.total) + ); + println!( + "{}", + i18n.cli_summary_rejected(stats.x_size_mismatched, stats.total) + ); + } else { + println!("---------------------------------------"); + } +} + +``` + +### 027: `./src/interfaces/gui.rs` + +```rust +pub mod code; +pub mod i18n; +pub mod paths; +pub mod settings; +pub mod shared; + +use crate::interfaces::cli::args::CliArgs; +use eframe::egui; + +#[derive(PartialEq)] +pub enum Tab { + Settings, + Paths, + Code, +} + +#[derive(PartialEq)] +pub enum PathsTab { + Match, + Mismatch, +} + +// ⚡ Dodana zakładka dla karty "Kod" +#[derive(PartialEq)] +pub enum CodeTab { + Match, + Mismatch, +} + +#[derive(Default, Clone)] +pub struct TreeStats { + pub txt_count: usize, + pub txt_weight: u64, + pub bin_count: usize, + pub bin_weight: u64, + pub err_count: usize, + pub err_weight: u64, + pub empty_count: usize, + pub matched_count: usize, + pub total_count: usize, +} + +pub struct CargoPlotApp { + pub args: CliArgs, + pub active_tab: Tab, + pub active_paths_tab: PathsTab, + pub active_code_tab: CodeTab, + pub new_pattern_input: String, + pub out_path_input: String, + pub generated_paths_m: String, + pub generated_paths_x: String, + pub generated_code_m: String, + pub generated_code_x: String, + pub stats_m: TreeStats, + pub stats_x: TreeStats, + pub ui_scale: f32, +} + +impl CargoPlotApp { + pub fn new(args: CliArgs) -> Self { + let default_out = args.dir_out.clone().unwrap_or_default(); + Self { + args, + active_tab: Tab::Settings, + active_paths_tab: PathsTab::Match, + active_code_tab: CodeTab::Match, // ⚡ Domyślnie ładujemy zakładkę MATCH + new_pattern_input: String::new(), + out_path_input: default_out, // Inicjalizacja ścieżki + generated_paths_m: String::new(), + generated_paths_x: String::new(), + generated_code_m: String::new(), // ⚡ Pusty na start + generated_code_x: String::new(), // ⚡ Pusty na start + stats_m: TreeStats::default(), + stats_x: TreeStats::default(), + ui_scale: 1.0, + } + } +} + +impl eframe::App for CargoPlotApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + ctx.set_zoom_factor(self.ui_scale); + + // GÓRNY PANEL (Teraz tylko 3 karty) + egui::TopBottomPanel::top("top_tabs").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.selectable_value(&mut self.active_tab, Tab::Settings, "Setting\nUstawienia"); + ui.selectable_value(&mut self.active_tab, Tab::Paths, "Paths\nŚcieżki"); + ui.selectable_value(&mut self.active_tab, Tab::Code, "Code\nKod"); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.add_space(10.0); + + if ui + .button("➕") + .on_hover_text("Powiększ (Zoom in)") + .clicked() + { + self.ui_scale += 0.1; // Powiększa o 10% + } + + if ui + .button("🔄") + .on_hover_text("Resetuj skalę (100%)") + .clicked() + { + self.ui_scale = 1.0; // Wraca do standardu + } + + if ui + .button("➖") + .on_hover_text("Pomniejsz (Zoom out)") + .clicked() + && self.ui_scale > 0.6 + { + // Zabezpieczenie, żeby nie zmniejszyć za bardzo + self.ui_scale -= 0.1; + } + + // Wyświetla aktualny procent powiększenia (np. "120%") + ui.label( + egui::RichText::new(format!("🔍 Skala: {:.0}%", self.ui_scale * 100.0)) + .weak(), + ); + }); + }); + }); + + // ŚRODEK OKNA + egui::CentralPanel::default().show(ctx, |ui| match self.active_tab { + Tab::Settings => settings::show(ui, self), + Tab::Paths => paths::show(ui, self), + Tab::Code => code::show(ui, self), + }); + } +} + +pub fn run_gui(args: CliArgs) { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([900.0, 700.0]) + .with_title("cargo-plot"), + ..Default::default() + }; + eframe::run_native( + "cargo-plot", + options, + Box::new(|_cc| Ok(Box::new(CargoPlotApp::new(args)))), + ) + .unwrap(); +} + +``` + +### 028: `./src/interfaces/gui/code.rs` + +```rust +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; +use crate::interfaces::gui::shared::{draw_editor, draw_footer, draw_tabs, resolve_dir}; +use crate::interfaces::gui::{CargoPlotApp, CodeTab, TreeStats}; +use cargo_plot::addon::TimeTag; +use cargo_plot::core::file_stats::FileStats; +use cargo_plot::core::file_stats::weight::{UnitSystem, WeightConfig}; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::save::is_blacklisted_extension; +use cargo_plot::execute; +use eframe::egui; + +/// [ENG]: View function for the Code tab, managing source extraction and statistics. +/// [POL]: Funkcja widoku dla karty Kod, zarządzająca ekstrakcją źródeł i statystykami. +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + let gt = GuiI18n::new(app.args.lang); + + // [ENG]: 1. TOP TABS - Navigation between matched and mismatched code buffers. + // [POL]: 1. GÓRNE ZAKŁADKI - Nawigacja między buforami kodu dopasowanego i odrzuconego. + let mut is_match = app.active_code_tab == CodeTab::Match; + draw_tabs(ui, >, &mut is_match); + app.active_code_tab = if is_match { + CodeTab::Match + } else { + CodeTab::Mismatch + }; + + ui.separator(); + + // [ENG]: 2. ACTION BAR - Controls for code generation and archival save. + // [POL]: 2. PASEK AKCJI - Sterowanie generowaniem kodu i zapisem archiwalnym. + ui.horizontal(|ui| { + if ui.button(gt.t(GT::BtnGenerateCode)).clicked() { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let show_mode = if is_match { + ShowMode::Include + } else { + ShowMode::Exclude + }; + + // [ENG]: Weight configuration remains fixed for code extraction to ensure consistency. + // [POL]: Konfiguracja wagi pozostaje stała dla ekstrakcji kodu, aby zapewnić spójność. + let weight_cfg = WeightConfig { + system: if app.args.unit == crate::interfaces::cli::args::CliUnitSystem::Bin { + UnitSystem::Binary + } else { + UnitSystem::Decimal + }, + dir_sum_included: !app.args.all, + ..WeightConfig::default() + }; + + let mut st_m = TreeStats::default(); + let mut st_x = TreeStats::default(); + + // [ENG]: Execute main engine with closures for statistics and file classification. + // [POL]: Wykonanie głównego silnika z domknięciami dla statystyk i klasyfikacji plików. + let stats = execute::execute( + &app.args.enter_path, + &app.args.patterns, + !app.args.ignore_case, + app.args.sort.into(), + show_mode, + app.args.view.into(), + weight_cfg, + app.args.no_root, + false, + app.args.no_emoji, + &i18n, + |f: &FileStats| { + if f.weight_bytes == 0 { + st_m.empty_count += 1; + } + if !f.path.ends_with('/') { + let ext = f + .absolute + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if is_blacklisted_extension(&ext) { + st_m.bin_count += 1; + st_m.bin_weight += f.weight_bytes; + } else { + st_m.txt_count += 1; + st_m.txt_weight += f.weight_bytes; + } + } + }, + |f: &FileStats| { + if f.weight_bytes == 0 { + st_x.empty_count += 1; + } + if !f.path.ends_with('/') { + let ext = f + .absolute + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if is_blacklisted_extension(&ext) { + st_x.bin_count += 1; + st_x.bin_weight += f.weight_bytes; + } else { + st_x.txt_count += 1; + st_x.txt_weight += f.weight_bytes; + } + } + }, + ); + + st_m.matched_count = stats.m_size_matched; + st_m.total_count = stats.total; + st_x.matched_count = stats.x_size_mismatched; + st_x.total_count = stats.total; + + app.stats_m = st_m; + app.stats_x = st_x; + + let base_dir = std::path::Path::new(&app.args.enter_path); + + // [ENG]: Process code extraction for the selected result set. + // [POL]: Przetwarzanie ekstrakcji kodu dla wybranego zestawu wyników. + if is_match { + let tree_m = + stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + let mut content_m = format!("```plaintext\n{}\n```\n\n", tree_m); + let mut counter_m = 1; + for p_str in &stats.m_matched.paths { + if p_str.ends_with('/') { + continue; + } + let absolute_path = base_dir.join(p_str); + match std::fs::read_to_string(&absolute_path) { + Ok(txt) => content_m.push_str(&format!( + "### {:03}: `{}`\n\n```rust\n{}\n```\n\n", + counter_m, p_str, txt + )), + Err(_) => content_m.push_str(&format!( + "### {:03}: `{}`\n\n{}\n\n", + counter_m, + p_str, + gt.t(GT::LabelSkipBinary) + )), + } + counter_m += 1; + } + app.generated_code_m = content_m; + } else { + let tree_x = + stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); + let mut content_x = format!("```plaintext\n{}\n```\n\n", tree_x); + let mut counter_x = 1; + for p_str in &stats.x_mismatched.paths { + if p_str.ends_with('/') { + continue; + } + let absolute_path = base_dir.join(p_str); + match std::fs::read_to_string(&absolute_path) { + Ok(txt) => content_x.push_str(&format!( + "### {:03}: `{}`\n\n```rust\n{}\n```\n\n", + counter_x, p_str, txt + )), + Err(_) => content_x.push_str(&format!( + "### {:03}: `{}`\n\n{}\n\n", + counter_x, + p_str, + gt.t(GT::LabelSkipBinary) + )), + } + counter_x += 1; + } + app.generated_code_x = content_x; + } + } + + ui.add_space(15.0); + ui.checkbox(&mut app.args.by, gt.t(GT::LabelAddFooter)); + ui.add_space(15.0); + + // [ENG]: Archival saving with metadata table. + // [POL]: Zapis archiwalny z tabelą metadanych. + if is_match { + if ui.button(gt.t(GT::BtnSaveMatch)).clicked() { + let tag = TimeTag::now(); + let filepath = format!( + "{}plot-archive_{}_M.md", + resolve_dir(&app.args.dir_out, &app.args.enter_path), + tag + ); + let mut final_text = app.generated_code_m.clone(); + if app.args.by { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(true, false, false, true); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( + &tag, + &app.args.enter_path, + &i18n, + &cmd_string, + )); + } + let _ = std::fs::write(&filepath, final_text); + } + } else { + if ui.button(gt.t(GT::BtnSaveMismatch)).clicked() { + let tag = TimeTag::now(); + let filepath = format!( + "{}plot-archive_{}_X.md", + resolve_dir(&app.args.dir_out, &app.args.enter_path), + tag + ); + let mut final_text = app.generated_code_x.clone(); + if app.args.by { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(false, true, false, true); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( + &tag, + &app.args.enter_path, + &i18n, + &cmd_string, + )); + } + let _ = std::fs::write(&filepath, final_text); + } + } + }); + + ui.separator(); + + // [ENG]: 3. FOOTER - Update statistics pinned to the bottom. + // [POL]: 3. STOPKA - Aktualizacja statystyk przypiętych do dołu. + let current_stats = if is_match { &app.stats_m } else { &app.stats_x }; + draw_footer(ui, "code_stats_footer", current_stats); + + // [ENG]: 4. MAIN EDITOR - Display extracted file contents. + // [POL]: 4. GŁÓWNY EDYTOR - Widok wyekstrahowanej zawartości plików. + let text_buffer = match app.active_code_tab { + CodeTab::Match => &mut app.generated_code_m, + CodeTab::Mismatch => &mut app.generated_code_x, + }; + draw_editor(ui, text_buffer); +} + +``` + +### 029: `./src/interfaces/gui/i18n.rs` + +```rust +// [ENG]: GUI Internationalization module. +// [POL]: Moduł internacjonalizacji interfejsu graficznego. + +use cargo_plot::i18n::Lang; + +pub struct GuiI18n { + pub lang: Lang, +} + +pub enum GuiText { + LabelLang, + LabelScanPath, + LabelOutFolder, + LabelSorting, + LabelViewMode, + LabelNoRoot, + HeadingPatterns, + LabelIgnoreCase, + LabelNewPattern, + BtnAddPattern, + BtnClearAll, + BtnBrowse, + MsgNoPatterns, + FooterDownload, + FooterInstall, + FooterUninstall, + BtnGenerate, + LabelAddFooter, + BtnSaveMatch, + BtnSaveMismatch, + TabMatch, + TabMismatch, + BtnGenerateCode, + LabelSkipBinary, +} + +impl GuiI18n { + pub fn new(lang: Option) -> Self { + Self { + lang: lang.unwrap_or(Lang::En), + } + } + + pub fn t(&self, text: GuiText) -> &'static str { + match self.lang { + Lang::Pl => match text { + GuiText::LabelLang => "🌍 Język:", + GuiText::LabelScanPath => "📂 Ścieżka skanowania:", + GuiText::LabelOutFolder => "💾 Folder zapisu (Output):", + GuiText::LabelSorting => "Sortowanie", + GuiText::LabelViewMode => "Tryb widoku", + GuiText::LabelNoRoot => "Ukryj ROOT w drzewie", + GuiText::HeadingPatterns => "🔍 Wzorce dopasowań (Patterns)", + GuiText::LabelIgnoreCase => "🔠 Ignoruj wielkość liter", + GuiText::LabelNewPattern => "Nowy:", + GuiText::BtnAddPattern => "➕ Dodaj wzorzec", + GuiText::BtnClearAll => "💣 Usuń wszystkie", + GuiText::BtnBrowse => "Wybierz...", + GuiText::MsgNoPatterns => "Brak wzorców. Dodaj przynajmniej jeden!", + GuiText::FooterDownload => "Pobierz binarkę (GitHub)", + GuiText::FooterInstall => "Instalacja:", + GuiText::FooterUninstall => "Usuwanie:", + GuiText::BtnGenerate => "🔄 Generuj / Regeneruj", + GuiText::LabelAddFooter => "Dodaj stopkę (--by)", + GuiText::BtnSaveMatch => "💾 Zapisz (-m)", + GuiText::BtnSaveMismatch => "💾 Zapisz (-x)", + GuiText::TabMatch => "✔ (-m) MATCH", + GuiText::TabMismatch => "✖ (-x) MISMATCH", + GuiText::BtnGenerateCode => "🔄 Generuj kod (Cache)", + GuiText::LabelSkipBinary => "> *(Pominięto plik binarny/graficzny)*", + }, + Lang::En => match text { + GuiText::LabelLang => "🌍 Language:", + GuiText::LabelScanPath => "📂 Scan path:", + GuiText::LabelOutFolder => "💾 Output folder:", + GuiText::LabelSorting => "Sorting", + GuiText::LabelViewMode => "View mode", + GuiText::LabelNoRoot => "Hide ROOT in tree", + GuiText::HeadingPatterns => "🔍 Match Patterns", + GuiText::LabelIgnoreCase => "🔠 Ignore case", + GuiText::LabelNewPattern => "New:", + GuiText::BtnAddPattern => "➕ Add pattern", + GuiText::BtnClearAll => "💣 Clear all", + GuiText::BtnBrowse => "Browse...", + GuiText::MsgNoPatterns => "No patterns. Add at least one!", + GuiText::FooterDownload => "Download binary (GitHub)", + GuiText::FooterInstall => "Install:", + GuiText::FooterUninstall => "Uninstall:", + GuiText::BtnGenerate => "🔄 Generate / Regenerate", + GuiText::LabelAddFooter => "Add footer (--by)", + GuiText::BtnSaveMatch => "💾 Save (-m)", + GuiText::BtnSaveMismatch => "💾 Save (-x)", + GuiText::TabMatch => "✔ (-m) MATCH", + GuiText::TabMismatch => "✖ (-x) MISMATCH", + GuiText::BtnGenerateCode => "🔄 Generate code (Cache)", + GuiText::LabelSkipBinary => "> *(Binary/graphic file skipped)*", + }, + } + } +} + +``` + +### 030: `./src/interfaces/gui/paths.rs` + +```rust +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; +use crate::interfaces::gui::shared::{draw_editor, draw_footer, draw_tabs, resolve_dir}; +use crate::interfaces::gui::{CargoPlotApp, PathsTab, TreeStats}; +use cargo_plot::addon::TimeTag; +use cargo_plot::core::file_stats::FileStats; +use cargo_plot::core::file_stats::weight::{UnitSystem, WeightConfig}; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::save::is_blacklisted_extension; +use cargo_plot::execute; +use eframe::egui; + +/// [ENG]: View function for the Paths tab, managing structure generation and unit toggling. +/// [POL]: Funkcja widoku dla karty Ścieżki, zarządzająca generowaniem struktury i przełączaniem jednostek. +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + let gt = GuiI18n::new(app.args.lang); + + // [ENG]: 1. TOP TABS - Sub-navigation for Match/Mismatch results. + // [POL]: 1. GÓRNE ZAKŁADKI - Podnawigacja dla wyników Match/Mismatch. + let mut is_match = app.active_paths_tab == PathsTab::Match; + draw_tabs(ui, >, &mut is_match); + app.active_paths_tab = if is_match { + PathsTab::Match + } else { + PathsTab::Mismatch + }; + + ui.separator(); + + // [ENG]: 2. ACTION BAR - Controls for generation, unit systems, and file saving. + // [POL]: 2. PASEK AKCJI - Sterowanie generowaniem, systemami jednostek i zapisem plików. + ui.horizontal(|ui| { + // [ENG]: Logic for triggering data generation. + // [POL]: Logika wyzwalająca generowanie danych. + if ui.button(gt.t(GT::BtnGenerate)).clicked() { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let show_mode = if is_match { + ShowMode::Include + } else { + ShowMode::Exclude + }; + + // [ENG]: Construct WeightConfig based on current application settings (-u and -a flags). + // [POL]: Konstrukcja WeightConfig na podstawie bieżących ustawień aplikacji (flagi -u oraz -a). + let weight_cfg = WeightConfig { + system: if app.args.unit == crate::interfaces::cli::args::CliUnitSystem::Bin { + UnitSystem::Binary + } else { + UnitSystem::Decimal + }, + dir_sum_included: !app.args.all, + ..WeightConfig::default() + }; + + let mut st_m = TreeStats::default(); + let mut st_x = TreeStats::default(); + + // [ENG]: Execute scan with statistics collectors via closures. + // [POL]: Wykonanie skanowania z kolektorami statystyk przez domknięcia. + let stats = execute::execute( + &app.args.enter_path, + &app.args.patterns, + !app.args.ignore_case, + app.args.sort.into(), + show_mode, + app.args.view.into(), + weight_cfg, + app.args.no_root, + false, + app.args.no_emoji, + &i18n, + |f: &FileStats| { + if f.weight_bytes == 0 { + st_m.empty_count += 1; + } + if !f.path.ends_with('/') { + let ext = f + .absolute + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if is_blacklisted_extension(&ext) { + st_m.bin_count += 1; + st_m.bin_weight += f.weight_bytes; + } else { + st_m.txt_count += 1; + st_m.txt_weight += f.weight_bytes; + } + } + }, + |f: &FileStats| { + if f.weight_bytes == 0 { + st_x.empty_count += 1; + } + if !f.path.ends_with('/') { + let ext = f + .absolute + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if is_blacklisted_extension(&ext) { + st_x.bin_count += 1; + st_x.bin_weight += f.weight_bytes; + } else { + st_x.txt_count += 1; + st_x.txt_weight += f.weight_bytes; + } + } + }, + ); + + // [ENG]: Update application state with results and calculated statistics. + // [POL]: Aktualizacja stanu aplikacji o wyniki i obliczone statystyki. + st_m.matched_count = stats.m_size_matched; + st_m.total_count = stats.total; + st_x.matched_count = stats.x_size_mismatched; + st_x.total_count = stats.total; + + app.stats_m = st_m; + app.stats_x = st_x; + + if is_match { + app.generated_paths_m = + stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + } else { + app.generated_paths_x = + stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); + } + } + + ui.add_space(10.0); + ui.checkbox(&mut app.args.by, gt.t(GT::LabelAddFooter)); + + ui.add_space(15.0); + + // [ENG]: Live unit system toggle. Label is pre-calculated to avoid borrow-checker conflicts. + // [POL]: Przełącznik systemu jednostek na żywo. Etykieta obliczona wcześniej, by uniknąć konfliktów borrow-checkera. + let mut is_bin = app.args.unit == crate::interfaces::cli::args::CliUnitSystem::Bin; + let unit_label = if is_bin { "IEC (Bin)" } else { "SI (Dec)" }; + + if ui + .checkbox(&mut is_bin, unit_label) + .on_hover_text("B/KB vs B/KiB") + .changed() + { + app.args.unit = if is_bin { + crate::interfaces::cli::args::CliUnitSystem::Bin + } else { + crate::interfaces::cli::args::CliUnitSystem::Dec + }; + } + + ui.add_space(15.0); + + // [ENG]: Handle contextual save actions. + // [POL]: Obsługa kontekstowych akcji zapisu. + if is_match { + if ui.button(gt.t(GT::BtnSaveMatch)).clicked() { + let tag = TimeTag::now(); + let filepath = format!( + "{}plot-address_{}_M.md", + resolve_dir(&app.args.dir_out, &app.args.enter_path), + tag + ); + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(true, false, true, false); + cargo_plot::core::save::SaveFile::paths( + &app.generated_paths_m, + &filepath, + &tag, + app.args.by, + &i18n, + &cmd_string, + &app.args.enter_path, + ); + } + } else { + if ui.button(gt.t(GT::BtnSaveMismatch)).clicked() { + let tag = TimeTag::now(); + let filepath = format!( + "{}plot-address_{}_X.md", + resolve_dir(&app.args.dir_out, &app.args.enter_path), + tag + ); + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(false, true, true, false); + cargo_plot::core::save::SaveFile::paths( + &app.generated_paths_x, + &filepath, + &tag, + app.args.by, + &i18n, + &cmd_string, + &app.args.enter_path, + ); + } + } + }); + + ui.separator(); + + // [ENG]: 3. FOOTER - Statistics display. + // [POL]: 3. STOPKA - Wyświetlanie statystyk. + let current_stats = if is_match { &app.stats_m } else { &app.stats_x }; + draw_footer(ui, "paths_stats_footer", current_stats); + + // [ENG]: 4. MAIN EDITOR - Generated content area. + // [POL]: 4. GŁÓWNY EDYTOR - Obszar wygenerowanej treści. + let text_buffer = match app.active_paths_tab { + PathsTab::Match => &mut app.generated_paths_m, + PathsTab::Mismatch => &mut app.generated_paths_x, + }; + draw_editor(ui, text_buffer); +} + +``` + +### 031: `./src/interfaces/gui/settings.rs` + +```rust +use crate::interfaces::cli::args::{CliSortStrategy, CliViewMode}; +use crate::interfaces::gui::CargoPlotApp; +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; +use cargo_plot::i18n::Lang; +use eframe::egui; + +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + // [ENG]: Initialize the GUI translation engine. + // [POL]: Inicjalizacja silnika tłumaczeń GUI. + let gt = GuiI18n::new(app.args.lang); + + // [ENG]: 1. Pinned Footer - Attached to the bottom of the screen. + // [POL]: 1. Przyklejona stopka - Przypięta do dołu ekranu. + egui::TopBottomPanel::bottom("settings_footer_panel") + .resizable(false) + .show_inside(ui, |ui| { + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("📦 cargo-plot v0.2.0").strong()); + ui.separator(); + ui.hyperlink_to("Crates.io", "https://crates.io/crates/cargo-plot"); + ui.separator(); + ui.hyperlink_to( + gt.t(GT::FooterDownload), + "https://github.com/j-Cis/cargo-plot/releases", + ); + }); + + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new(gt.t(GT::FooterInstall)).weak()); + ui.code("cargo install cargo-plot"); + ui.separator(); + ui.label(egui::RichText::new(gt.t(GT::FooterUninstall)).weak()); + ui.code("cargo uninstall cargo-plot"); + }); + ui.add_space(10.0); + }); + + // [ENG]: 2. Main Content Area - Scrollable settings. + // [POL]: 2. Główny obszar treści - Przewijalne ustawienia. + egui::CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + // ⚡ Ustawiamy globalny limit szerokości (bez ucinania krawędzi) + ui.set_max_width(600.0); + ui.add_space(10.0); + + // [ENG]: Language selection. + // [POL]: Wybór języka. + ui.horizontal(|ui| { + ui.label(gt.t(GT::LabelLang)); + ui.radio_value(&mut app.args.lang, Some(Lang::Pl), "Polski"); + ui.radio_value(&mut app.args.lang, Some(Lang::En), "English"); + }); + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // [ENG]: Path Selection Grid - Perfectly aligns labels and inputs to the right edge. + // [POL]: Siatka wyboru ścieżek - Idealnie wyrównuje etykiety i pola do prawej krawędzi. + egui::Grid::new("path_settings_grid") + .num_columns(2) + .spacing([10.0, 10.0]) + .min_col_width(120.0) + .show(ui, |ui| { + // Row 1: Scan path + ui.label(gt.t(GT::LabelScanPath)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button(gt.t(GT::BtnBrowse)).clicked() + && let Some(folder) = rfd::FileDialog::new().pick_folder() + { + app.args.enter_path = folder.to_string_lossy().replace('\\', "/"); + } + ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut app.args.enter_path), + ); + }); + ui.end_row(); + + // Row 2: Output folder + ui.label(gt.t(GT::LabelOutFolder)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button(gt.t(GT::BtnBrowse)).clicked() + && let Some(folder) = rfd::FileDialog::new().pick_folder() + { + let mut path = folder.to_string_lossy().replace('\\', "/"); + if !path.ends_with('/') { + path.push('/'); + } + app.out_path_input = path.clone(); + app.args.dir_out = Some(path); + } + + let txt_response = ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut app.out_path_input), + ); + if txt_response.changed() { + let trimmed = app.out_path_input.trim(); + app.args.dir_out = if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + }; + } + }); + ui.end_row(); + }); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // [ENG]: View and Sorting. + // [POL]: Widok i sortowanie. + ui.horizontal(|ui| { + egui::ComboBox::from_label(gt.t(GT::LabelSorting)) + .selected_text(format!("{:?}", app.args.sort)) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut app.args.sort, + CliSortStrategy::AzFileMerge, + "AzFileMerge", + ); + ui.selectable_value( + &mut app.args.sort, + CliSortStrategy::ZaFileMerge, + "ZaFileMerge", + ); + ui.selectable_value( + &mut app.args.sort, + CliSortStrategy::AzDirMerge, + "AzDirMerge", + ); + ui.selectable_value( + &mut app.args.sort, + CliSortStrategy::ZaDirMerge, + "ZaDirMerge", + ); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::AzFile, "AzFile"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::ZaFile, "ZaFile"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::AzDir, "AzDir"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::ZaDir, "ZaDir"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::Az, "Az"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::Za, "Za"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::None, "None"); + }); + + ui.add_space(15.0); + + egui::ComboBox::from_label(gt.t(GT::LabelViewMode)) + .selected_text(format!("{:?}", app.args.view)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut app.args.view, CliViewMode::Tree, "Tree"); + ui.selectable_value(&mut app.args.view, CliViewMode::List, "List"); + ui.selectable_value(&mut app.args.view, CliViewMode::Grid, "Grid"); + }); + + ui.add_space(15.0); + ui.checkbox(&mut app.args.no_root, gt.t(GT::LabelNoRoot)); + ui.add_space(15.0); + ui.checkbox(&mut app.args.all, "Fizyczna waga folderów (-a)"); + }); + + ui.add_space(20.0); + + // [ENG]: Match Patterns Section. + // [POL]: Sekcja wzorców dopasowań. + ui.heading(gt.t(GT::HeadingPatterns)); + ui.add_space(15.0); + + ui.horizontal(|ui| { + ui.checkbox(&mut app.args.ignore_case, gt.t(GT::LabelIgnoreCase)); + ui.label(gt.t(GT::LabelNewPattern)); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let btn_clicked = ui.button(gt.t(GT::BtnAddPattern)).clicked(); + let response = ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut app.new_pattern_input), + ); + + if (btn_clicked + || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))) + && !app.new_pattern_input.trim().is_empty() + { + let input = app.new_pattern_input.trim(); + + // ⚡ FAST-TRACK: Automatyczne parsowanie ciągów z CLI + if input.contains("-p ") || input.contains("--pat ") { + // Ujednolicamy znacznik flagi + let normalized = input.replace("--pat ", "-p "); + + for part in normalized.split("-p ") { + let mut trimmed = part.trim(); + + // Ignorujemy śmieci takie jak komenda bazowa na początku + if trimmed.starts_with("cargo") || trimmed.is_empty() { + continue; + } + + // Zdejmujemy cudzysłowy i odcinamy ewentualne inne flagi na końcu ciągu + if (trimmed.starts_with('"') && trimmed.ends_with('"')) + || (trimmed.starts_with('\'') && trimmed.ends_with('\'')) + { + trimmed = &trimmed[1..trimmed.len() - 1]; // Idealne cudzysłowy po obu stronach + } else if trimmed.starts_with('"') || trimmed.starts_with('\'') { + // Zaczyna się od cudzysłowu, ale ma śmieci po nim (np. inne flagi -i) + let quote = trimmed.chars().next().unwrap(); + if let Some(end_idx) = trimmed[1..].find(quote) { + trimmed = &trimmed[1..=end_idx]; + } + } else if let Some(space_idx) = trimmed.find(' ') { + // Brak cudzysłowów, ucinamy do pierwszej spacji (inne flagi) + trimmed = &trimmed[..space_idx]; + } + + if !trimmed.is_empty() { + app.args.patterns.push(trimmed.to_string()); + } + } + } else { + // Zwykłe dodanie pojedynczego wzorca wpisanego ręcznie + app.args.patterns.push(input.to_string()); + } + + app.new_pattern_input.clear(); + response.request_focus(); + } + }); + }); + + ui.add_space(5.0); + egui::Frame::group(ui.style()).show(ui, |ui| { + ui.set_min_height(200.0); + // ⚡ Naprawa krawędzi: Wypełnia idealnie dostępną przestrzeń (z uwzględnieniem paddingu ramki) + ui.set_min_width(ui.available_width()); + + let mut move_up = None; + let mut move_down = None; + let mut remove = None; + + for (i, pat) in app.args.patterns.iter().enumerate() { + ui.horizontal(|ui| { + if ui.button("🗑").clicked() { + remove = Some(i); + } + if ui.button("⬆").clicked() { + move_up = Some(i); + } + if ui.button("⬇").clicked() { + move_down = Some(i); + } + ui.label(pat); + }); + } + + if let Some(i) = remove { + app.args.patterns.remove(i); + } + if let Some(i) = move_up + && i > 0 + { + app.args.patterns.swap(i, i - 1); + } + if let Some(i) = move_down + && i + 1 < app.args.patterns.len() + { + app.args.patterns.swap(i, i + 1); + } + + if !app.args.patterns.is_empty() { + ui.separator(); + if ui.button(gt.t(GT::BtnClearAll)).clicked() { + app.args.patterns.clear(); + } + } else { + ui.label( + egui::RichText::new(gt.t(GT::MsgNoPatterns)) + .italics() + .weak(), + ); + } + }); + + ui.add_space(20.0); + }); + }); +} + +``` + +### 032: `./src/interfaces/gui/shared.rs` + +```rust +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; +use eframe::egui; + +// [ENG]: Helper to resolve output directory from app arguments. +// [POL]: Pomocnik do wyznaczania folderu zapisu z argumentów aplikacji. +pub fn resolve_dir(val: &Option, base_path: &str) -> String { + let is_auto = val + .as_ref() + .is_none_or(|v| v.trim().is_empty() || v == "AUTO"); + if is_auto { + let mut b = base_path.replace('\\', "/"); + if !b.ends_with('/') { + b.push('/'); + } + format!("{}.cargo-plot/", b) + } else { + let mut p = val.as_ref().unwrap().replace('\\', "/"); + if !p.ends_with('/') { + p.push('/'); + } + p + } +} + +// [ENG]: UI component: 50/50 Match & Mismatch tabs stretching across the top. +// [POL]: Komponent UI: Zakładki Match i Mismatch 50/50 rozciągnięte na górze. +pub fn draw_tabs(ui: &mut egui::Ui, gt: &GuiI18n, is_match: &mut bool) { + ui.horizontal(|ui| { + let item_width = (ui.available_width() - 8.0) / 2.0; + + // --- MATCH (-m) --- + let mut m_color = egui::Color32::from_rgb(150, 150, 150); + let mut m_bg = egui::Color32::TRANSPARENT; + if *is_match { + m_color = egui::Color32::from_rgb(138, 90, 255); + m_bg = egui::Color32::from_rgb(40, 40, 40); + } + + let m_btn = ui.add_sized( + [item_width, 40.0], + egui::Button::new( + egui::RichText::new(gt.t(GT::TabMatch)) + .size(16.0) + .strong() + .color(m_color), + ) + .fill(m_bg), + ); + if m_btn.clicked() { + *is_match = true; + } + + ui.add_space(8.0); + + // --- MISMATCH (-x) --- + let mut x_color = egui::Color32::from_rgb(150, 150, 150); + let mut x_bg = egui::Color32::TRANSPARENT; + if !*is_match { + x_color = egui::Color32::from_rgb(255, 80, 100); + x_bg = egui::Color32::from_rgb(40, 40, 40); + } + + let x_btn = ui.add_sized( + [item_width, 40.0], + egui::Button::new( + egui::RichText::new(gt.t(GT::TabMismatch)) + .size(16.0) + .strong() + .color(x_color), + ) + .fill(x_bg), + ); + if x_btn.clicked() { + *is_match = false; + } + }); +} + +// [ENG]: UI component: Statistics footer placeholder. +// [POL]: Komponent UI: Stopka ze statystykami. +pub fn draw_footer( + ui: &mut egui::Ui, + panel_id: &'static str, + stats: &crate::interfaces::gui::TreeStats, +) { + let fmt_bytes = |b: u64| -> String { + let kb = b as f64 / 1024.0; + if kb < 1.0 { + format!("{} B", b) + } else if kb < 1024.0 { + format!("{:.1} KB", kb) + } else { + format!("{:.2} MB", kb / 1024.0) + } + }; + + egui::TopBottomPanel::bottom(panel_id).show_inside(ui, |ui| { + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.label(format!( + "📝 Txt: {} ({})", + stats.txt_count, + fmt_bytes(stats.txt_weight) + )); + ui.separator(); + ui.label(format!( + "📦 Bin: {} ({})", + stats.bin_count, + fmt_bytes(stats.bin_weight) + )); + ui.separator(); + + if stats.err_count > 0 { + // ⚡ Zaznacza się na czerwono, jeśli są błędy + ui.label( + egui::RichText::new(format!( + "🚫 Err: {} ({})", + stats.err_count, + fmt_bytes(stats.err_weight) + )) + .color(egui::Color32::RED), + ); + ui.separator(); + } else { + ui.label("🚫 Err: 0 (0 B)"); + ui.separator(); + } + + ui.label(format!("🕳️ Empty: {}", stats.empty_count)); + ui.separator(); + ui.label(format!( + "🎯 Matched: {} / {}", + stats.matched_count, stats.total_count + )); + }); + ui.add_space(5.0); + }); +} + +// [ENG]: UI component: Central scrollable editor. +// [POL]: Komponent UI: Centralny przewijalny edytor. +pub fn draw_editor(ui: &mut egui::Ui, text_buffer: &mut String) { + egui::CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::both().show(ui, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + ui.add( + egui::TextEdit::multiline(text_buffer) + .font(egui::TextStyle::Monospace) + .desired_width(f32::INFINITY), + ); + }); + }); +} + +``` + +### 033: `./src/interfaces/tui.rs` + +```rust +// [ENG]: Interactive Terminal User Interface (TUI) module registry. +// [POL]: Rejestr modułu interaktywnego interfejsu tekstowego (TUI). + +pub mod i18n; +pub mod menu; +pub mod state; + +pub fn run_tui() { + let mut s = state::StateTui::new(); + cliclack::clear_screen().unwrap(); + //cliclack::intro(" 📖 https://crates.io/crates/cargo-plot").unwrap(); + menu::menu_main(&mut s); +} + +``` + +### 034: `./src/interfaces/tui/i18n.rs` + +```rust +use cargo_plot::i18n::Lang; +use console::style; + +fn pad(text: &str) -> String { + format!(" {} ", text) +} + +pub struct Txt { + pub pol: &'static str, + pub eng: &'static str, +} + +pub trait Translatable { + fn trans(&self) -> Txt; + fn theme(&self, text: String) -> String { + text + } +} + +pub enum Prompt { + HeaderMain, + BtnLang, + BtnQuickStart, + BtnPaths, + BtnView, + BtnOutput, + BtnFilters, + BtnRun, + BtnExit, + InputPatterns, + ExitBye, + WarnNoPatterns, + // Prompty dla podmenu (wersje dwujęzyczne w jednej linii dla szybkości) + SubBasePath, + SubIgnoreCase, + SubSelectView, + SubSelectSort, + SubNoRoot, + SubDirOut, + SubSaveAddress, + SubSaveArchive, + SubBy, + SubOnMatch, + SubOnMismatch, + SubInfo, + BtnCliMode, + InputCliCommand, + SuccessCliParse, + BtnHelp, + HelpPause, + SubHelpHeader, + HelpPatternsBtn, + HelpFlagsBtn, + HelpTextPatterns, + HelpTextFlags, + BtnGui, +} + +impl Translatable for Prompt { + fn trans(&self) -> Txt { + match self { + Prompt::HeaderMain => Txt { + pol: "📦 cargo-plot [POL] - Interaktywny Kreator", + eng: "📦 cargo-plot [ENG] - Interactive Builder", + }, + Prompt::BtnLang => Txt { + pol: "🌍 Zmień język / Change language", + eng: "🌍 Zmień język / Change language", + }, + Prompt::BtnQuickStart => Txt { + pol: "🚀 SZYBKI START (Podaj wzorce i uruchom)", + eng: "🚀 QUICK START (Enter patterns and run)", + }, + Prompt::BtnPaths => Txt { + pol: "🛠️ Ścieżki i Wzorce", + eng: "🛠️ Paths and Patterns", + }, + Prompt::BtnView => Txt { + pol: "👁️ Widok i Sortowanie", + eng: "👁️ View and Sorting", + }, + Prompt::BtnOutput => Txt { + pol: "💾 Zapis plików", + eng: "💾 Output and Saving", + }, + Prompt::BtnFilters => Txt { + pol: "⚙️ Filtry i Opcje", + eng: "⚙️ Filters and Options", + }, + Prompt::BtnRun => Txt { + pol: "▶️ URUCHOM SKANOWANIE", + eng: "▶️ RUN SCANNER", + }, + Prompt::BtnExit => Txt { + pol: "❌ WYJŚCIE", + eng: "❌ EXIT", + }, + Prompt::InputPatterns => Txt { + pol: "Podaj wzorce (oddzielone przecinkiem, np. *.rs, Cargo.toml):", + eng: "Enter patterns (comma separated, e.g. *.rs, Cargo.toml):", + }, + Prompt::ExitBye => Txt { + pol: "Do widzenia!", + eng: "Goodbye!", + }, + Prompt::WarnNoPatterns => Txt { + pol: "Brak wzorców! Podaj przynajmniej jeden.", + eng: "Missing patterns! Provide at least one.", + }, + + // Podmenu + Prompt::SubBasePath => Txt { + pol: "Ścieżka bazowa / Base path:", + eng: "Ścieżka bazowa / Base path:", + }, + Prompt::SubIgnoreCase => Txt { + pol: "Ignorować wielkość liter? / Ignore case?", + eng: "Ignorować wielkość liter? / Ignore case?", + }, + Prompt::SubSelectView => Txt { + pol: "Wybierz widok / Select view:", + eng: "Wybierz widok / Select view:", + }, + Prompt::SubSelectSort => Txt { + pol: "Wybierz sortowanie / Select sorting:", + eng: "Wybierz sortowanie / Select sorting:", + }, + Prompt::SubNoRoot => Txt { + pol: "Ukryć główny folder? / Hide root dir?", + eng: "Ukryć główny folder? / Hide root dir?", + }, + Prompt::SubDirOut => Txt { + pol: "Folder zapisu (--dir-out) [puste=CWD, AUTO=./other/]:", + eng: "Output folder (--dir-out) [empty=CWD, AUTO=./other/]:", + }, + Prompt::SubSaveAddress => Txt { + pol: "Zapisywać listę ścieżek (--save-address)?", + eng: "Save paths list (--save-address)?", + }, + Prompt::SubSaveArchive => Txt { + pol: "Zapisywać kody źródłowe (--save-archive)?", + eng: "Save source codes (--save-archive)?", + }, + Prompt::SubBy => Txt { + pol: "Dodać stopkę na dole pliku? / Add info footer?", + eng: "Dodać stopkę na dole pliku? / Add info footer?", + }, + Prompt::SubOnMatch => Txt { + pol: "Pokaż dopasowane? / Show matched?", + eng: "Pokaż dopasowane? / Show matched?", + }, + Prompt::SubOnMismatch => Txt { + pol: "Pokaż odrzucone? / Show rejected?", + eng: "Pokaż odrzucone? / Show rejected?", + }, + Prompt::SubInfo => Txt { + pol: "Pokaż statystyki? / Show info stats?", + eng: "Pokaż statystyki? / Show info stats?", + }, + Prompt::BtnCliMode => Txt { + pol: "⌨️ Wklej komendę (Raw CLI)", + eng: "⌨️ Paste command (Raw CLI)", + }, + Prompt::InputCliCommand => Txt { + pol: "Wklej flagi lub całą komendę (np. -d ./ -m):", + eng: "Paste flags or full command (e.g. -d ./ -m):", + }, + Prompt::SuccessCliParse => Txt { + pol: "Wczytano konfigurację!", + eng: "Configuration loaded!", + }, + Prompt::BtnHelp => Txt { + pol: "❓ Pomoc (Wzorce i Flagi)", + eng: "❓ Help (Patterns & Flags)", + }, + Prompt::SubHelpHeader => Txt { + pol: "Wybierz temat pomocy:", + eng: "Choose help topic:", + }, + Prompt::HelpPatternsBtn => Txt { + pol: "Składnia Wzorców", + eng: "Patterns Syntax", + }, + Prompt::HelpFlagsBtn => Txt { + pol: "Opis Flag i Opcji", + eng: "Flags & Options Description", + }, + Prompt::HelpTextPatterns => Txt { + pol: "=== WZORCE DOPASOWAŃ === +* - Dowolne znaki (np. *.rs) +** - Dowolne zagnieżdżenie (np. src/**/*.rs) +{a,b} - Rozwinięcie klamrowe (np. {src,tests}/*.rs) +! - Negacja / Odrzucenie (np. !*test*) ++ - Tryb głęboki: cała zawartość folderu (np. src/+) +@ - Rodzeństwo: wymaga pary plik + folder o tej samej nazwie +$ - Sierota: dopasowuje TYLKO, gdy brakuje pary plik/folder + +=== PRZYKŁADY === +*.rs -> Pokaż wszystkie pliki .rs +!@tui{.rs,/}+ -> Wyklucz plik tui.rs oraz folder tui/ z całą zawartością (+)", + + eng: "=== PATTERN SYNTAX === +* - Any characters (e.g. *.rs) +** - Any dir depth (e.g. src/**/*.rs) +{a,b} - Brace expansion (e.g. {src,tests}/*.rs) +! - Negation / Reject (e.g. !*test*) ++ - Deep mode: all contents of a directory (e.g. src/+) +@ - Sibling: requires file + dir pair with the same name +$ - Orphan: matches ONLY when file/dir pair is missing + +=== EXAMPLES === +*.rs -> Show all .rs files +!@tui{.rs,/}+ -> Exclude tui.rs file and tui/ dir with all its contents (+)", + }, + Prompt::HelpTextFlags => Txt { + pol: "=== FLAGI I OPCJE (W TUI JAKO PRZEŁĄCZNIKI) === +-d, --dir : Ścieżka bazowa skanowania (Domyślnie: ./) +-p, --pat : Wzorce dopasowań (wymagane, oddzielane przecinkiem) +-s, --sort : Strategia sortowania wyników (np. AzFileMerge) +-v, --view : Widok wyników (Tree, List, Grid) +-m, --on-match : Pokaż tylko dopasowane ścieżki +-x, --on-mismatch : Pokaż tylko odrzucone ścieżki +-o, --out-paths : Zapisz wynik jako listę ścieżek (Markdown) +-c, --out-cache : Zapisz wynik wraz z kodem plików (Markdown Cache) +-b, --by : Dodaj stopkę informacyjną z komendą na końcu pliku +-i, --info : Pokaż statystyki skanowania (Dopasowano/Odrzucono) +--ignore-case : Ignoruj wielkość liter we wzorcach +--treeview-no-root : Ukryj główny folder roboczy w widoku drzewa", + + eng: "=== FLAGS & OPTIONS (TOGGLES IN TUI) === +-d, --dir : Base input path to scan (Default: ./) +-p, --pat : Match patterns (required, comma separated) +-s, --sort : Sorting strategy (e.g. AzFileMerge) +-v, --view : Results view (Tree, List, Grid) +-m, --on-match : Show only matched paths +-x, --on-mismatch : Show only rejected paths +-o, --out-paths : Save result as paths list (Markdown) +-c, --out-cache : Save result with file codes (Markdown Cache) +-b, --by : Add info footer with command at the end of file +-i, --info : Show scan statistics (Matched/Rejected) +--ignore-case : Ignore case in patterns +--treeview-no-root : Hide main working directory in tree view", + }, + Prompt::HelpPause => Txt { + pol: "Naciśnij [Enter], aby wrócić do menu...", + eng: "Press [Enter] to return to menu...", + }, + Prompt::BtnGui => Txt { + pol: "🖥️ Otwórz w oknie (GUI)", + eng: "🖥️ Open in window (GUI)", + }, + } + } + + fn theme(&self, text: String) -> String { + match self { + Prompt::BtnQuickStart => style(text).on_blue().white().bold().to_string(), + Prompt::BtnRun => style(text).on_green().black().bold().to_string(), + Prompt::BtnLang => style(text).cyan().to_string(), + Prompt::BtnExit => style(text).red().to_string(), + Prompt::HeaderMain => style(text).on_white().black().bold().to_string(), + Prompt::BtnCliMode => style(text).on_black().yellow().bold().to_string(), + Prompt::BtnHelp => style(text).magenta().bold().to_string(), + Prompt::BtnGui => style(text).on_magenta().white().bold().to_string(), + _ => text, + } + } +} + +pub struct T { + lang: Lang, +} + +impl T { + pub fn new(lang: Lang) -> Self { + Self { lang } + } + pub fn fmt(&self, item: I) -> String { + let txt = item.trans(); + let text = match self.lang { + Lang::Pl => txt.pol, + Lang::En => txt.eng, + }; + item.theme(pad(text)) + } + pub fn raw(&self, item: I) -> String { + let txt = item.trans(); + match self.lang { + Lang::Pl => txt.pol.to_string(), + Lang::En => txt.eng.to_string(), + } + } +} + +``` + +### 035: `./src/interfaces/tui/menu.rs` + +```rust +use super::i18n::{Prompt, T}; +use super::state::StateTui; +use crate::interfaces::cli::args::CargoCli; +use crate::interfaces::cli::args::{CliSortStrategy, CliViewMode}; +use crate::interfaces::cli::engine; +use clap::Parser; +use console::style; + +#[derive(Clone, PartialEq, Eq)] +enum Action { + Lang, + QuickStart, + CliMode, + Paths, + View, + Output, + Filters, + Help, + Run, + Gui, + Exit, +} + +pub fn menu_main(s: &mut StateTui) { + let mut last_action = Action::Paths; + + loop { + let t = T::new(s.lang); + let header = t.fmt(Prompt::HeaderMain); + + // ⚡ DYNAMICZNE ETYKIETY KOKPITU + let pat_str = if s.args.patterns.is_empty() { + "[]".to_string() + } else { + format!("[{}...]", s.args.patterns[0]) + }; + let lbl_paths = format!( + "{} (dir: '{}', pat: {})", + t.fmt(Prompt::BtnPaths), + s.args.enter_path, + pat_str + ); + let lbl_view = format!( + "{} (view: {:?}, sort: {:?}, root: {})", + t.fmt(Prompt::BtnView), + s.args.view, + s.args.sort, + !s.args.no_root + ); + + let out_p = s.args.dir_out.as_deref().unwrap_or("AUTO"); + let lbl_out = format!( + "{} (dir-out: {}, address: {}, archive: {}, by: {})", + t.fmt(Prompt::BtnOutput), + out_p, + s.args.save_address, + s.args.save_archive, + s.args.by + ); + + let lbl_filt = format!( + "{} (match: {}, mismatch: {}, info: {})", + t.fmt(Prompt::BtnFilters), + s.args.include, + s.args.exclude, + s.args.info + ); + + // ⚡ BUDOWA MENU + let links_hint = style("crates.io/crates/cargo-plot | github.com/j-Cis/cargo-plot") + .dim() + .to_string(); + let action_result = cliclack::select(header) + .initial_value(last_action.clone()) + .item(Action::Lang, t.fmt(Prompt::BtnLang), "") + .item(Action::QuickStart, t.fmt(Prompt::BtnQuickStart), "") + .item(Action::CliMode, t.fmt(Prompt::BtnCliMode), "") + .item(Action::Paths, lbl_paths, "") + .item(Action::View, lbl_view, "") + .item(Action::Output, lbl_out, "") + .item(Action::Filters, lbl_filt, "") + .item(Action::Help, t.fmt(Prompt::BtnHelp), "") + .item(Action::Run, t.fmt(Prompt::BtnRun), "") + .item(Action::Gui, t.fmt(Prompt::BtnGui), "") + .item(Action::Exit, t.fmt(Prompt::BtnExit), links_hint) + .interact(); + + // ⚡ OBSŁUGA AKCJI + match action_result { + Ok(Action::Lang) => s.toggle_lang(), + Ok(Action::QuickStart) => { + let raw_pat: String = cliclack::input(t.raw(Prompt::InputPatterns)) + .interact() + .unwrap_or_default(); + if !raw_pat.trim().is_empty() { + s.args.patterns = split_patterns(&raw_pat); + cliclack::outro("🚀 ...").unwrap(); + engine::run(s.args.clone()); + return; + } + } + Ok(Action::CliMode) => { + let cmd: String = cliclack::input(t.raw(Prompt::InputCliCommand)) + .interact() + .unwrap_or_default(); + + if !cmd.trim().is_empty() { + // ⚡ Shlex idealnie tnie stringa jak bash, a jeśli ktoś zgubi cudzysłów, wyłapie błąd + if let Some(mut parsed_split) = shlex::split(&cmd) { + // Czyścimy początek (wywalamy "cargo", "run", "--", "plot") + while !parsed_split.is_empty() { + let first = parsed_split[0].to_lowercase(); + if first == "cargo" + || first == "run" + || first == "--" + || first == "plot" + || first.contains("cargo-plot") + { + parsed_split.remove(0); + } else { + break; + } + } + + // Podajemy do parsera Clap + let mut cli_args = vec!["cargo".to_string(), "plot".to_string()]; + cli_args.extend(parsed_split); + + match CargoCli::try_parse_from(cli_args) { + Ok(CargoCli::Plot(parsed_args)) => { + s.args = parsed_args; + cliclack::log::success(t.raw(Prompt::SuccessCliParse)).unwrap(); + } + Err(e) => { + cliclack::log::error(format!("{}", e)).unwrap(); + } + } + } else { + // Obsługa błędu ze strony shlex + cliclack::log::error( + "Błąd parsowania komendy! Prawdopodobnie nie domknięto cudzysłowu.", + ) + .unwrap(); + } + } + } + Ok(Action::Paths) => { + last_action = Action::Paths; + handle_paths(s, &t); + } + Ok(Action::View) => { + last_action = Action::View; + handle_view(s, &t); + } + Ok(Action::Output) => { + last_action = Action::Output; + handle_output(s, &t); + } + Ok(Action::Filters) => { + last_action = Action::Filters; + handle_filters(s, &t); + } + Ok(Action::Help) => { + let help_choice = cliclack::select(t.raw(Prompt::SubHelpHeader)) + .item(1, t.raw(Prompt::HelpPatternsBtn), "") + .item(2, t.raw(Prompt::HelpFlagsBtn), "") + .item(0, t.raw(Prompt::BtnExit), "") + .interact() + .unwrap_or(0); + + if help_choice == 1 { + cliclack::note("📖 WZORCE / PATTERNS", t.raw(Prompt::HelpTextPatterns)) + .unwrap(); + let _: String = cliclack::input(t.raw(Prompt::HelpPause)) + .required(false) // ⚡ TO POZWALA NA PUSTY ENTER + .interact() + .unwrap_or_default(); + } else if help_choice == 2 { + cliclack::note("⚙️ FLAGI / FLAGS", t.raw(Prompt::HelpTextFlags)).unwrap(); + let _: String = cliclack::input(t.raw(Prompt::HelpPause)) + .required(false) // ⚡ TO POZWALA NA PUSTY ENTER + .interact() + .unwrap_or_default(); + } + } + Ok(Action::Run) => { + if s.args.patterns.is_empty() { + cliclack::log::warning(t.raw(Prompt::WarnNoPatterns)).unwrap(); + continue; + } + cliclack::outro("🚀 ...").unwrap(); + engine::run(s.args.clone()); + return; + } + Ok(Action::Gui) => { + // Wyświetlamy komunikat na pożegnanie z terminalem + cliclack::outro(t.fmt(Prompt::BtnGui)).unwrap(); + + // Odpalamy nasze nowe okienko, przekazując mu całą zebraną konfigurację + crate::interfaces::gui::run_gui(s.args.clone()); + + // Zamykamy pętlę TUI - pałeczkę przejmuje egui! + return; + } + Ok(Action::Exit) | Err(_) => { + cliclack::outro(t.raw(Prompt::ExitBye)).unwrap(); + return; + } + } + cliclack::clear_screen().unwrap(); + } +} + +// ===================================================================== +// SZYBKIE PODMENU (Helpery modyfikujące stan) +// ===================================================================== + +fn handle_paths(s: &mut StateTui, t: &T) { + s.args.enter_path = cliclack::input(t.raw(Prompt::SubBasePath)) + .default_input(&s.args.enter_path) + .interact() + .unwrap_or(s.args.enter_path.clone()); + let current_pat = s.args.patterns.join(", "); + let new_pat: String = cliclack::input(t.raw(Prompt::InputPatterns)) + .default_input(¤t_pat) + .interact() + .unwrap_or(current_pat); + s.args.patterns = split_patterns(&new_pat); + s.args.ignore_case = cliclack::confirm(t.raw(Prompt::SubIgnoreCase)) + .initial_value(s.args.ignore_case) + .interact() + .unwrap_or(s.args.ignore_case); +} + +fn handle_view(s: &mut StateTui, t: &T) { + s.args.view = cliclack::select(t.raw(Prompt::SubSelectView)) + .initial_value(s.args.view) + .item(CliViewMode::Tree, "Tree", "") + .item(CliViewMode::List, "List", "") + .item(CliViewMode::Grid, "Grid", "") + .interact() + .unwrap_or(s.args.view); + + s.args.sort = cliclack::select(t.raw(Prompt::SubSelectSort)) + .initial_value(s.args.sort) + .item( + CliSortStrategy::AzFileMerge, + "AzFileMerge (Domyślne/Default)", + "", + ) + .item(CliSortStrategy::ZaFileMerge, "ZaFileMerge", "") + .item(CliSortStrategy::AzDirMerge, "AzDirMerge", "") + .item(CliSortStrategy::ZaDirMerge, "ZaDirMerge", "") + .item(CliSortStrategy::AzFile, "AzFile (Najpierw pliki)", "") + .item(CliSortStrategy::ZaFile, "ZaFile", "") + .item(CliSortStrategy::AzDir, "AzDir (Najpierw foldery)", "") + .item(CliSortStrategy::ZaDir, "ZaDir", "") + .item(CliSortStrategy::Az, "Az (Alfanumerycznie)", "") + .item(CliSortStrategy::Za, "Za (Odwrócone)", "") + .item(CliSortStrategy::None, "None (Brak sortowania)", "") + .interact() + .unwrap_or(s.args.sort); + + s.args.no_root = cliclack::confirm(t.raw(Prompt::SubNoRoot)) + .initial_value(s.args.no_root) + .interact() + .unwrap_or(s.args.no_root); +} + +fn handle_output(s: &mut StateTui, t: &T) { + let out_p: String = cliclack::input(t.raw(Prompt::SubDirOut)) + .default_input(s.args.dir_out.as_deref().unwrap_or("")) + .interact() + .unwrap_or_default(); + s.args.dir_out = if out_p.trim().is_empty() { + None + } else { + Some(out_p.trim().to_string()) + }; + + s.args.save_address = cliclack::confirm(t.raw(Prompt::SubSaveAddress)) + .initial_value(s.args.save_address) + .interact() + .unwrap_or(s.args.save_address); + + s.args.save_archive = cliclack::confirm(t.raw(Prompt::SubSaveArchive)) + .initial_value(s.args.save_archive) + .interact() + .unwrap_or(s.args.save_archive); + + s.args.by = cliclack::confirm(t.raw(Prompt::SubBy)) + .initial_value(s.args.by) + .interact() + .unwrap_or(s.args.by); +} + +fn handle_filters(s: &mut StateTui, t: &T) { + s.args.include = cliclack::confirm(t.raw(Prompt::SubOnMatch)) + .initial_value(s.args.include) + .interact() + .unwrap_or(s.args.include); + s.args.exclude = cliclack::confirm(t.raw(Prompt::SubOnMismatch)) + .initial_value(s.args.exclude) + .interact() + .unwrap_or(s.args.exclude); + s.args.info = cliclack::confirm(t.raw(Prompt::SubInfo)) + .initial_value(s.args.info) + .interact() + .unwrap_or(s.args.info); +} + +// ===================================================================== +// POMOCNICZY PARSER WZORCÓW +// ===================================================================== +fn split_patterns(input: &str) -> Vec { + let mut result = Vec::new(); + let mut current = String::new(); + let mut in_braces = 0; + + for c in input.chars() { + match c { + '{' => { + in_braces += 1; + current.push(c); + } + '}' => { + if in_braces > 0 { + in_braces -= 1; + } + current.push(c); + } + ',' if in_braces == 0 => { + if !current.trim().is_empty() { + result.push(current.trim().to_string()); + } + current.clear(); + } + _ => current.push(c), + } + } + if !current.trim().is_empty() { + result.push(current.trim().to_string()); + } + result +} + +/*/ +fn split_cli_args(input: &str) -> Vec { + let mut args = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + let mut quote_char = ' '; + + for c in input.chars() { + if in_quotes { + if c == quote_char { + in_quotes = false; + } else { + current.push(c); + } + } else { + match c { + '"' | '\'' => { + in_quotes = true; + quote_char = c; + } + ' ' => { + if !current.is_empty() { + args.push(current.clone()); + current.clear(); + } + } + _ => current.push(c), + } + } + } + if !current.is_empty() { + args.push(current); + } + args +} + */ + +``` + +### 036: `./src/interfaces/tui/state.rs` + +```rust +use crate::interfaces::cli::args::{CliArgs, CliSortStrategy, CliViewMode}; +use cargo_plot::i18n::Lang; + +pub struct StateTui { + pub lang: Lang, + pub args: CliArgs, +} + +impl StateTui { + pub fn new() -> Self { + let lang = Lang::detect(); + Self { + lang, + args: CliArgs { + // Domyślne wartości, dokładnie takie jak w CLI + enter_path: ".".to_string(), + patterns: vec![], + sort: CliSortStrategy::AzFileMerge, + view: CliViewMode::Tree, + include: true, + exclude: false, + dir_out: None, + save_address: false, + save_archive: false, + by: false, + tui: true, + unit: crate::interfaces::cli::args::CliUnitSystem::Bin, + all: false, + ignore_case: false, + no_root: false, + info: true, // Domyślnie włączamy statystyki (-i) + gui: false, + no_emoji: false, + lang: Some(lang), + }, + } + } + + /// Aktualizuje język w interfejsie i w argumentach dla silnika + pub fn toggle_lang(&mut self) { + self.lang = match self.lang { + Lang::Pl => Lang::En, + Lang::En => Lang::Pl, + }; + self.args.lang = Some(self.lang); + } +} + +``` + +### 037: `./src/lib.rs` + +```rust +pub mod addon; +pub mod core; +pub mod execute; +pub mod i18n; +pub mod theme; + +``` + +### 038: `./src/main.rs` + +```rust +// [ENG]: Main entry point switching between interfaces. +// [POL]: Główny punkt wejścia przełączający między interfejsami. + +#![allow(clippy::pedantic, clippy::struct_excessive_bools)] + +mod interfaces; + +fn main() { + // [ENG]: Register an empty Ctrl+C handler to prevent abrupt termination. + // [POL]: Rejestrujemy pusty handler Ctrl+C, zapobiegając natychmiastowemu zabiciu programu. + ctrlc::set_handler(move || {}).expect("Błąd podczas ustawiania handlera Ctrl+C"); + + // [ENG]: Pass execution directly to the CLI parser and router. + // [POL]: Przekazanie wykonania bezpośrednio do parsera i routera CLI. + interfaces::cli::run_cli(); +} + +``` + +### 039: `./src/output.rs` + +```rust +pub mod save_path; +pub mod save_code; +pub mod generator; +//pub use save_path +``` + +### 040: `./src/theme.rs` + +```rust +pub mod for_path_list; +pub mod for_path_tree; + +``` + +### 041: `./src/theme/for_path_list.rs` + +```rust +/// [POL]: Przypisuje ikonę (emoji) do ścieżki na podstawie atrybutów: katalog oraz status elementu ukrytego. +/// [ENG]: Assigns an icon (emoji) to a path based on attributes: directory status and hidden element status. +pub fn get_icon_for_path(path: &str) -> &'static str { + let is_dir = path.ends_with('/'); + + let nazwa = path + .trim_end_matches('/') + .split('/') + .next_back() + .unwrap_or(""); + let is_hidden = nazwa.starts_with('.'); + + match (is_dir, is_hidden) { + (true, false) => "📁", // [POL]: Folder | [ENG]: Directory + (true, true) => "🗃️", // [POL]: Ukryty folder | [ENG]: Hidden directory + (false, false) => "📄", // [POL]: Plik | [ENG]: File + (false, true) => "⚙️ ", // [POL]: Ukryty plik | [ENG]: Hidden file + } +} + +``` + +### 042: `./src/theme/for_path_tree.rs` + +```rust +// [ENG]: Path classification and icon mapping for tree visualization. +// [POL]: Klasyfikacja ścieżek i mapowanie ikon dla wizualizacji drzewa. + +/// [ENG]: Global icon used for directory nodes. +/// [POL]: Globalna ikona używana dla węzłów będących folderami. +pub const DIR_ICON: &str = "📂"; + +pub const FILE_ICON: &str = "📄"; + +/// [ENG]: Defines visual and metadata properties for a file type. +/// [POL]: Definiuje wizualne i metadanowe właściwości dla typu pliku. +pub struct PathFileType { + pub icon: &'static str, + pub md_lang: &'static str, +} + +/// [ENG]: Returns file properties based on its extension. +/// [POL]: Zwraca właściwości pliku na podstawie jego rozszerzenia. +#[must_use] +pub fn get_file_type(ext: &str) -> PathFileType { + match ext { + "rs" => PathFileType { + icon: "🦀", + md_lang: "rust", + }, + "toml" => PathFileType { + icon: "⚙️", + md_lang: "toml", + }, + "slint" => PathFileType { + icon: "🎨", + md_lang: "slint", + }, + "md" => PathFileType { + icon: "📝", + md_lang: "markdown", + }, + "json" => PathFileType { + icon: "🔣", + md_lang: "json", + }, + "yaml" | "yml" => PathFileType { + icon: "🛠️", + md_lang: "yaml", + }, + "html" => PathFileType { + icon: "📖", + md_lang: "html", + }, + "css" => PathFileType { + icon: "🖌️", + md_lang: "css", + }, + "js" => PathFileType { + icon: "📜", + md_lang: "javascript", + }, + "ts" => PathFileType { + icon: "📘", + md_lang: "typescript", + }, + // [ENG]: Default fallback for unknown file types. + // [POL]: Domyślny fallback dla nieznanych typów plików. + _ => PathFileType { + icon: "📄", + md_lang: "text", + }, + } +} + +/// [ENG]: Character set used for drawing tree branches and indents. +/// [POL]: Zestaw znaków używanych do rysowania gałęzi drzewa i wcięć. +#[derive(Debug, Clone)] +pub struct TreeStyle { + // [ENG]: Directories (d) + // [POL]: Foldery (d) + pub dir_last_with_children: String, // └──┬ + pub dir_last_no_children: String, // └─── + pub dir_mid_with_children: String, // ├──┬ + pub dir_mid_no_children: String, // ├─── + + // [ENG]: Files (f) + // [POL]: Pliki (f) + pub file_last: String, // └──• + pub file_mid: String, // ├──• + + // [ENG]: Indentations for subsequent levels (i) + // [POL]: Wcięcia dla kolejnych poziomów (i) + pub indent_last: String, // " " + pub indent_mid: String, // "│ " +} + +impl Default for TreeStyle { + fn default() -> Self { + Self { + dir_last_with_children: "└──┬".to_string(), + dir_last_no_children: "└───".to_string(), + dir_mid_with_children: "├──┬".to_string(), + dir_mid_no_children: "├───".to_string(), + + file_last: "└──•".to_string(), + file_mid: "├──•".to_string(), + + indent_last: " ".to_string(), + indent_mid: "│ ".to_string(), + } + } +} + +``` + + + +--- + +> | Property | Value | +> | ---: | :--- | +> | **Tool** | `cargo-plot v0.2.0` | +> | **Input** | `A:/A-JAN/git-rust/j-Cis/libs-util/cargo-plot-2` | +> | **Command** | `cargo plot -d "A:/A-JAN/git-rust/j-Cis/libs-util/cargo-plot-2" -o "A:/A-JAN/git-rust/j-Cis/libs-util/cargo-plot-2/" -p "./src/+" -p "Cargo.toml" -m -v grid --save-archive -b --lang en` | +> | **TimeTag** | `2026Q1D080W12_Sat21Mar_033235486` | +> | **Links** | [Crates.io](https://crates.io/crates/cargo-plot) \| [GitHub](https://github.com/j-Cis/cargo-plot/releases) | +> | **Links** | `cargo install cargo-plot` | +> | **Help** | `cargo plot --help` | + +--- diff --git a/README.md b/README.md index 46930d9..a4d5034 100644 --- a/README.md +++ b/README.md @@ -1,890 +1,114 @@ # cargo-plot -**cargo-plot** (v0.1.5) to biblioteka napisana w języku Rust (edycja 2024), której autorem jest Jan Roman Cisowski. +**cargo-plot** (v0.2.0) to wszechstronny „szwajcarski scyzoryk” dewelopera napisany w języku Rust (edycja 2024). Służy do zaawansowanej wizualizacji struktur projektów, audytu zajętości miejsca oraz automatycznego generowania dokumentacji technicznej bezpośrednio z poziomu Cargo. -🔗 **Crates.io (Paczka)**: [crates.io/crates/cargo-plot](https://crates.io/crates/cargo-plot) -🔗 **GitHub (Kod źródłowy)**: [github.com/j-Cis/cargo-plot](https://github.com/j-Cis/cargo-plot) +**cargo-plot** (v0.2.0) is a versatile developer's "Swiss Army knife" written in Rust (2024 edition). It is used for advanced project structure visualization, disk space auditing, and automatic technical documentation generation directly from Cargo. -```text -[KiB 1.689] ├──• ⚙️ Cargo.toml - └──┬ 📂 src -[ B 671.0] ├──• 🦀 main.rs - ├──┬ 📂 cli -[KiB 7.231] │ ├──• 🦀 args.rs -[ B 724.0] │ ├──• 🦀 dist.rs -[KiB 1.791] │ ├──• 🦀 doc.rs -[ B 408.0] │ ├──• 🦀 mod.rs -[ B 577.0] │ ├──• 🦀 stamp.rs -[KiB 3.690] │ ├──• 🦀 tree.rs -[KiB 2.486] │ └──• 🦀 utils.rs - ├──┬ 📂 lib -[KiB 6.758] │ ├──• 🦀 fn_copy_dist.rs -[KiB 1.702] │ ├──• 🦀 fn_datestamp.rs -[KiB 1.913] │ ├──• 🦀 fn_doc_gen.rs -[KiB 2.703] │ ├──• 🦀 fn_doc_id.rs -[ B 570.0] │ ├──• 🦀 fn_doc_models.rs -[KiB 4.593] │ ├──• 🦀 fn_doc_write.rs -[KiB 1.964] │ ├──• 🦀 fn_files_blacklist.rs -[KiB 8.222] │ ├──• 🦀 fn_filespath.rs -[KiB 4.604] │ ├──• 🦀 fn_filestree.rs -[ B 724.0] │ ├──• 🦀 fn_path_utils.rs -[KiB 1.546] │ ├──• 🦀 fn_pathtype.rs -[KiB 4.278] │ ├──• 🦀 fn_plotfiles.rs -[KiB 3.602] │ ├──• 🦀 fn_weight.rs -[ B 288.0] │ └──• 🦀 mod.rs - └──┬ 📂 tui -[KiB 1.393] ├──• 🦀 dist.rs -[KiB 4.244] ├──• 🦀 doc.rs -[KiB 1.487] ├──• 🦀 mod.rs -[KiB 1.023] ├──• 🦀 stamp.rs -[KiB 2.616] ├──• 🦀 tree.rs -[KiB 6.317] └──• 🦀 utils.rs -``` - -Powyższa struktura to wynik komendy: - -```bash -cargo plot tree -s files-first --no-default-excludes -e ./f.md -e ./d.md -e ./target/ -e ./.git/ -e ./test/ -e ./.gitignore -e ./u.md -e ./Cargo.lock -e ./LICENSE-APACHE -e ./LICENSE-MIT -e ./.github/ -e ./.cargo/ -e ./doc/ -e ./README.md -w binary --weight-precision 5 --no-dir-weight -``` - -Biblioteka `cargo-plot` oferuje bogate API, które pozwala na: - -* **Przeszukiwanie plików** w systemie z wykorzystaniem elastycznych reguł uwzględniających i wykluczających. -* **Budowanie struktury drzewa** na podstawie odnalezionych ścieżek. -* **Wizualizowanie struktury katalogów i plików** w postaci sformatowanego czystego tekstu ASCII lub kolorowego wydruku bezpośrednio w konsoli. -* **Generowanie kompleksowych raportów Markdown**, które zawierają zautomatyzowany skan wybranych kodów źródłowych spiętych w jeden plik dokumentacji. - -> **Założenia projektowe i dobre praktyki** -> -> Biblioteka została zaprojektowana z rygorystycznym naciskiem na jakość kodu, przejrzystość architektury oraz > użyteczność. Podczas jej tworzenia stosowano następujące zasady i dobre praktyki: -> -> * **DRY (Don't Repeat Yourself)** – eliminacja powielania logiki; reużywalność; pisanie z myślą o ponownym > użyciu. -> * **DDD (Domain-Driven Design)** – Projektowanie zorientowane na domenę; skupianie się na modelu problemu, a > nie na technologii. -> * **SRP (Single Responsibility Principle)** – każda struktura, moduł i funkcja ma jedną, jasno określoną > odpowiedzialność. -> * **LSP (Liskov Substitution Principle)** - Podtypy muszą być wymienialne z typami bazowymi bez łamania > kontraktu. -> * **ISP (Interface Segregation Principle)** - Interfejsy powinny być małe i wyspecjalizowane. -> * **DIP (Dependency Inversion Principle)** - Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego > poziomu. -> * **SoC (Separation of Concerns)** – wyraźny podział odpowiedzialności pomiędzy warstwy i moduły. -> * **LoD (Law of Demeter) & Minimal Dependencies** – minimalizacja zależności między modułami i strukturami; > ograniczanie zewnętrznych zależności dla stabilności projektu. -> * **SSoT (Single Source of Truth)** – unikanie definiowania tego samego stanu lub logiki w wielu miejscach. -> * **Reexporty i dużo małych plików** niż ogromne kilkuset linijkowe!!! -> * **Fail Fast & Fail Early & Defensive Programming** – szybkie wykrywanie i raportowanie błędów. -> * **Error Handling with Result / Option** – jawne i idiomatyczne zarządzanie błędami. -> * **The Boy Scout Rule** – pozostawianie kodu w lepszym stanie, niż go zastało. -> * **Modular Design** – małe, niezależne moduły łatwiejsze w utrzymaniu i testowaniu. -> * **Composition over Inheritance** – preferowanie kompozycji zamiast dziedziczenia. -> * **Encapsulation / Information Hiding** – ukrywanie szczegółów implementacji i eksponowanie tylko niezbędnego > API. -> * **Immutability by Default** – zmienne są niemutowalne, dopiero gdy naprawdę potrzebujesz, użyj `mut`. -> * **Idiomatic Rust** – czytelne, jednoznaczne i zgodne z konwencją nazwy funkcji, struktur, modułów oraz > folderów. Tam gdzie możliwe to po polsku. -> * **Zero-cost Abstractions** – korzystanie z idiomatycznych abstrakcji bez wpływu na wydajność. - ---- ---- - -## 🌟 Używasz cargo-plot? Daj znać! - -Jeśli wykorzystujesz to narzędzie w swoim projekcie, bardzo chętnie o tym usłyszę! -To dla mnie największa motywacja do dalszego rozwoju. - -* Wyślij mi wiadomość na GitHubie. -* Zostaw gwiazdkę ⭐ pod repozytorium. -* Dodaj swój projekt do listy "Użytkownicy" (otwórz Pull Request!). - -## 🛠 Zarządzanie Projektem i Instalacja - -Poniżej znajduje się zestawienie najczęściej używanych komend do budowania, testowania i instalacji narzędzia `cargo-plot`. - -### 🚀 Szybki Start (Budowanie i Uruchamianie) - -* `cargo build` – Kompilacja projektu w trybie debug. -* `cargo run -- ` – Uruchomienie narzędzia w trybie deweloperskim (np. `cargo run -- plot tree`). -* `cargo build --release` – Kompilacja zoptymalizowana do użytku produkcyjnego (wynik w `target/release/cargo-plot`). -* `cargo run --release -- ` – Uruchomienie zoptymalizowanej wersji binarnej. - -### 📦 Instalacja jako rozszerzenie Cargo - -Aby móc używać narzędzia z dowolnego miejsca w systemie za pomocą komendy `cargo plot`: - -* `cargo install cargo-plot` – Pobiera i instaluje najnowszą wersję bezpośrednio z oficjalnego rejestru crates.io (Zalecane). -* `cargo --list` – Sprawdzenie, czy `plot` widnieje na liście zainstalowanych rozszerzeń Cargo. -* `cargo plot --help` – Wyświetlenie pomocy zainstalowanego narzędzia. -* `cargo uninstall cargo-plot` – Całkowite usunięcie narzędzia z systemu. - -**Co zyskujesz dzięki `cargo install`?** - -Instalacja przenosi skompilowaną binarkę do folderu `~/.cargo/bin/`. Dzięki temu: - -1. Możesz wywoływać `cargo plot` z dowolnego folderu w systemie (nie musisz być wewnątrz katalogu projektu). -2. Nie musisz używać `cargo run --`, co skraca komendy używane w codziennej pracy. - -#### 1. Instalacja z Crates.io (Rekomendowane) - -Ponieważ projekt jest oficjalnie opublikowany w ekosystemie Rusta, jest to najszybsza metoda: - -* `cargo install cargo-plot` – Pobiera najnowszą wersję z gałęzi głównej i instaluje ją w systemie. - -#### 2. Instalacja bezpośrednio z GitHub - -Jeśli chcesz zainstalować najnowszą wersję deweloperską z pominięciem crates.io: - -* `cargo install --git https://github.com/j-Cis/cargo-plot` – Pobiera najnowszą wersję z gałęzi głównej i instaluje ją w systemie. - -#### 3. Klonowanie i instalacja lokalna - -Jeśli chcesz mieć dostęp do kodu źródłowego lub skryptów pomocniczych: - -* `git clone https://github.com/j-Cis/cargo-plot.git` – Klonuje repozytorium na Twój dysk. -* `cd cargo-plot` – Wejście do katalogu projektu. -* `cargo install --path .` – Instalacja narzędzia z lokalnych plików źródłowych. - -#### 4. Zarządzanie zainstalowanym narzędziem - -Po instalacji `cargo-plot` staje się integralną częścią Twojego środowiska Rust: - -* `cargo plot --help` – Sprawdzenie, czy instalacja przebiegła pomyślnie i wyświetlenie pomocy. -* `cargo --list` – Wyświetlenie listy wszystkich zainstalowanych rozszerzeń Cargo (na liście powinien widnieć `plot`). -* `cargo uninstall cargo-plot` – Całkowite usunięcie narzędzia z Twojego systemu. - -### 🧪 Jakość Kodu i Dokumentacja - -* `cargo check` – Błyskawiczne sprawdzenie poprawności kodu bez pełnej kompilacji. -* `cargo clippy` – Uruchomienie lintera w celu wykrycia potencjalnych problemów i optymalizacji. -* `cargo fmt` – Automatyczne formatowanie kodu zgodnie ze standardami Rust. -* `cargo doc --no-deps --open` – Wygenerowanie dokumentacji technicznej projektu i otwarcie jej w przeglądarce. - -### 🌍 Kompilacja skrośna (Cross-compilation) - -Przygotowanie binarek dla różnych systemów operacyjnych (wymaga zainstalowanych odpowiednich targetów przez `rustup`): - -| System | Komenda kompilacji | -| --- | --- | -| **Windows 64-bit** | `cargo build --target x86_64-pc-windows-msvc --release` | -| **Windows 32-bit** | `cargo build --target i686-pc-windows-msvc --release` | -| **Linux 64-bit** | `cargo build --target x86_64-unknown-linux-gnu --release` | -| **Linux (musl)** | `cargo build --target x86_64-unknown-linux-musl --release` | -| **macOS (Intel)** | `cargo build --target x86_64-apple-darwin --release` | -| **macOS (M1/M2)** | `cargo build --target aarch64-apple-darwin --release` | - -### 📂 Nawigacja deweloperska - -* `code .` – Otwarcie całego projektu w Visual Studio Code. -* `ii .` – Otwarcie katalogu projektu w Eksploratorze plików (tylko Windows/PowerShell). - ---- ---- - -## 🏅 Stan Projektu i Technologie - -[![Rust](https://img.shields.io/badge/Rust-v1.93.1%20%7C%202024_Edition-b8744a.svg?logo=rust&logoColor=white)](https://www.rust-lang.org/) -[![Crates.io](https://img.shields.io/crates/v/cargo-plot.svg)](https://crates.io/crates/cargo-plot) -[![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) - -### 🛠️ Główne Zależności - -Używamy sprawdzonych i wydajnych bibliotek z ekosystemu Rust: - -| Biblioteka | Odznaka | Rola w projekcie | -| --- | --- | --- | -| **Clap** | ![Clap](https://img.shields.io/badge/clap-v4.5.60-blue) | Obsługa argumentów wiersza poleceń (CLI). | -| **Cliclack** | ![Cliclack](https://img.shields.io/badge/cliclack-v0.4.1-ff69b4) | Nowoczesny, interaktywny interfejs użytkownika (TUI). | -| **Chrono** | ![Chrono](https://img.shields.io/badge/chrono-v0.4.44-green) | Precyzyjne generowanie unikalnych sygnatur czasowych. | -| **Regex** | ![Regex](https://img.shields.io/badge/regex-v1.12.3-red) | Zaawansowane filtrowanie plików za pomocą wzorców Glob. | -| **Colored** | ![Colored](https://img.shields.io/badge/colored-v3.1.1-yellow) | Kolorowanie drzewa plików w terminalu. | -| **Walkdir** | ![Walkdir](https://img.shields.io/badge/walkdir-v2.5.0-lightgrey) | Szybkie i bezpieczne skanowanie struktury katalogów. | - ---- ---- - -## CLI (Interfejs Wiersza Poleceń) - -Narzędzie `cargo-plot` zostało zaprojektowane jako oficjalne rozszerzenie (subcommand) dla menedżera pakietów Cargo. Pozwala na błyskawiczne przeszukiwanie systemu plików, wizualizowanie struktury oraz generowanie kompleksowych raportów Markdown bezpośrednio z poziomu terminala. - -Architektura CLI opiera się na podkomendach. Każdy poziom narzędzia i każda podkomenda posiada wbudowany system pomocy, który można wywołać dodając flagę `-h` lub `--help` (np. `cargo plot --help`, `cargo plot doc --help`). - -### Główne wywołanie - -* **`cargo plot`** (brak argumentów) – Uruchamia interaktywny kreator TUI (Text-based User Interface), który poprowadzi Cię krok po kroku przez proces konfiguracji zadania. -* **`cargo plot stamp [OPCJE]`** – Narzędzie pomocnicze. Generuje i wypisuje w konsoli unikalną, ujednoliconą sygnaturę czasową (np. do użycia w Twoich własnych skryptach bash/powershell). -* **`cargo plot tree [OPCJE]`** – Tryb wizualizacji. Buduje strukturę drzewa na podstawie odnalezionych ścieżek i wyrzuca kolorowy wydruk bezpośrednio na standardowe wyjście konsoli. -* **`cargo plot doc [OPCJE]`** – Tryb generatora. Automatycznie skanuje wybrane kody źródłowe i spina je w jeden plik dokumentacji Markdown w folderze `doc/`. -* **`cargo plot dist-copy [OPCJE]`** – Menedżer dystrybucji. Wyciąga skompilowane binarki (wskazane lub automatycznie wykryte wszystkie) z folderu `target/` i organizuje je w ustrukturyzowanym katalogu wydawniczym `dist/`. - ---- - -### Podkomenda: `stamp` - OPCJE - -Służy do szybkiego wygenerowania unikalnego znacznika czasu w standardzie biblioteki. - -#### Opcje dla komendy `stamp` - -* **`-d, --date `** – Zwraca stempel dla konkretnej daty (jeśli nie podano czasu, użyje domyślnie `00:00:00`). -* **`-t, --time `** – Zwraca stempel dla konkretnej godziny (wymaga podania flagi daty). -* **`-m, --millis `** – Opcjonalne milisekundy (wartość od 0 do 999). Używane tylko w połączeniu z flagą `-t`. Domyślnie: `000`. -* *(Brak flag)* – Domyślne zachowanie: błyskawicznie zwraca znacznik dla obecnego, lokalnego czasu (`datestamp_now`). - ---- - -### Podkomenda: `stamp` - Przykład: Generowanie sygnatury (Tryb `stamp`) - -Poniższe komendy pokazują, jak uzyskać identyczny rezultat jak w Twoim kodzie źródłowym. - -**1. Sygnatura dla konkretnej daty i czasu** (odpowiednik `datestamp(d, t)`): -Wymaga podania flagi `--date` oraz `--time`. Opcjonalnie możesz podać milisekund. - -```powershell -# Wywołanie w PowerShell lub Bash -cargo run -- plot stamp --date 2026-03-09 --time 13:51:01 --millis 123 - -``` - -* **Wynik**: Otrzymasz sformatowany ciąg, np.: `2026Q1D068W11_Mon09Mar_135101123`. - -**2. Sygnatura dla aktualnego czasu** (odpowiednik `datestamp_now()`): -Uruchomienie podkomendy bez żadnych parametrów natychmiast generuje stempel dla chwili obecnej. - -```powershell -# Błyskawiczne wygenerowanie stempla "teraz" -cargo run -- plot stamp - -``` - -* **Wynik**: Sygnatura oparta na Twoim lokalnym czasie systemowym. - ---- - -### Podkomenda: `tree` - OPCJE - -Służy do błyskawicznego podglądu struktury projektu. Pozwala na testowanie wzorców (Glob) przed wygenerowaniem właściwego raportu. - -#### Opcje szybkiego zadania (Globalne flagi) - -Te flagi służą do stworzenia pojedynczego, szybkiego zadania skanowania "w locie". - -* **`-p, --path <ŚCIEŻKA>`** – Ścieżka bazowa do rozpoczęcia skanowania (Domyślnie: `.`). -* **`--no-default-excludes`** – Wyłącza domyślne ignorowanie folderów technicznych takich jak `.git/`, `target/`, `node_modules/`, `.vs/`, itp.. -* **`-e, --exclude ...`** – Wzorce Glob ignorujące ścieżki i foldery. Odrzucenie następuje na wczesnym etapie skanowania, co znacząco przyspiesza działanie. Można podawać wielokrotnie (np. `-e "./target/" -e "*.toml"`). -* **`-i, --include-only ...`** – Rygorystyczna biała lista. Jeśli użyta, program pominie wszystko, co nie pasuje do podanych wzorców. -* **`-f, --filter-files ...`** – Filtruje wyłącznie pliki do wyświetlenia (np. `-f "*.rs"`). -* **`-t, --type `** – Tryb wyświetlania węzłów: -* `dirs` – Wyświetla wyłącznie foldery. -* `files` – Wyświetla odnalezione pliki i automatycznie podciąga ich nadrzędne foldery. -* `all` – (Domyślnie) Wyświetla wszystko. - -#### Opcje zaawansowanego zarządzania zadaniami - -Zastępują szybkie flagi, pozwalając na budowanie skomplikowanych macierzy skanowania. - -* **`--task ...`** – Tryb Inline Multi-Task. Definiuje pełne zadanie w jednym ciągu znaków. Można używać tej flagi wielokrotnie, a wyniki zostaną połączone. Dostępne klucze: `loc`, `exc`, `inc`, `fil`, `out` (np. `--task loc=.,inc=Cargo.toml,out=files`). -* **`--tasks `** – Tryb Zewnętrznej Konfiguracji. Wczytuje gotową definicję listy zadań `[[task]]` bezpośrednio z pliku `.toml`. - -#### Opcje wyjściowe formatowania - -* **`-s, --sort `** – Sposób sortowania węzłów drzewa. -* `dirs-first` – Foldery wyświetlane są przed plikami. -* `files-first` – Pliki wyświetlane są przed folderami. -* `alpha` – (Domyślnie) Klasyczne sortowanie alfabetyczne. - -#### Opcje wagi i rozmiaru plików (Zajętość dysku) - -Te flagi pozwalają zamienić `cargo-plot` w analizator zajętości przestrzeni dyskowej, pokazując obok każdego węzła drzewa specjalną ramkę z wyliczonym rozmiarem (np. `[KiB 86.10]`). - -* **`-w, --weight `** – Włącza system obliczania wagi. Dostępne systemy: - * `binary` – Tradycyjny system programistyczny ($1024^n$: KiB, MiB, GiB). - * `decimal` – System metryczny używany na dyskach ($1000^n$: kB, MB, GB). - * `none` – (Domyślnie) Całkowicie ukrywa ramkę wagi. -* **`--weight-precision `** – Ustala precyzję znakową (szerokość) pola wyświetlającego rozmiar. Gwarantuje pionowe wyrównanie gałęzi. Domyślnie `5`. (np. wartość `3` skróci zapis do `[KiB 86]`). -* **`--no-dir-weight`** – Ukrywa wyświetlanie wag przy folderach, zachowując przy tym idealne wcięcia ramki. Waga zostanie pokazana tylko przy plikach. -* **`--no-file-weight`** – Ukrywa wagi przy plikach. Pokazuje wyłącznie podsumowania zsumowane dla katalogów. -* **`--real-dir-weight`** – Zmienia tryb obliczania. Domyślnie (bez tej flagi), waga folderu to suma wykazanych i odfiltrowanych plików na Twoim wykresie. Dodanie tej flagi sprawia, że program pokaże rzeczywisty fizyczny rozmiar folderu na dysku, ignorując wzorce pomijania (`--exclude` / `whitelist`). - -#### Opcje eksportu i nazewnictwa (Generowanie plików z drzewa) - -Drzewo można nie tylko wyświetlić w konsoli, ale i zapisać do gotowego pliku Markdown z dedykowanymi metadanymi. - -* **`--out-file <ŚCIEŻKA>`** – Zapisuje wynikowe drzewo do pliku Markdown (np. `struktura.md`) zamiast wyrzucać je do konsoli. -* **`--print-console`** – Domyślnie użycie `--out-file` wycisza terminal. Ta flaga wymusza jednoczesny wydruk kolorowego drzewa w konsoli oraz zapis do pliku. -* **`--watermark `** – Dodaje informację o narzędziu z linkiem do GitHuba/Crates. Opcje: `first` (góra), `last` (dół - domyślnie), `none` (brak). -* **`--print-command`** – Rzutuje użytą komendę CLI do bloku kodu `bash` na początku zapisanego pliku. -* **`--suffix-stamp`** (alias: `--sufix-stamp`) – Dokleja unikalny znacznik czasu do nazwy pliku wyjściowego (np. `drzewo__2026Q1...md`). Jeśli flaga nie zostanie użyta, stempel wyląduje w tytule wewnątrz pliku. -* **`--title-file `** – Nadpisuje główny nagłówek H1 w wygenerowanym pliku (Domyślnie: `RAPORT`). -* **`--title-file-with-path`** – Dokleja ścieżkę pliku do głównego nagłówka H1 (np. `# RAPORT (doc/drzewo.md)`). - ---- - -### Podkomenda: `tree` - Przykłady wywołań i niuanse terminali - -Podczas pracy z wieloliniowymi komendami w środowisku deweloperskim (używając `cargo run --`), należy zwrócić uwagę na znaki kontynuacji linii specyficzne dla danego systemu operacyjnego. - -> **Ważna uwaga deweloperska**: Separator `--` po komendzie `cargo run` informuje Cargo, aby nie interpretował kolejnych flag jako własnych parametrów, lecz przekazał je bezpośrednio do skompilowanej binarki `cargo-plot`. - -#### Przykład: Złożone skanowanie wielozadaniowe (Multi-Task) - -Poniższe komendy wykonują identyczne zadanie: skanują wybrane pliki główne projektu oraz wszystkie pliki `.rs` w folderze `lib`, a następnie wyświetlają je w formie drzewa z plikami na górze. - -**1. Wywołanie uniwersalne (jednoliniowe)** – Działa w każdym terminalu (CMD, PowerShell, Bash): - -```powershell -cargo run -- plot tree --task "loc=.,inc=./Cargo.toml,inc=./README.md,inc=./src/main.rs,inc=./src/cli.rs,out=files" --task "loc=./src/lib,fil=*.rs,out=files" --sort files-first - -``` - -**2. Wywołanie wieloliniowe w PowerShell (Windows)** – Wymaga znaku grawisu (backtick: ```) na końcu każdej linii: - -```powershell -cargo run -- plot tree ` - --task "loc=.,inc=./Cargo.toml,inc=./README.md,inc=./src/main.rs,inc=./src/cli.rs,out=files" ` - --task "loc=./src/lib,fil=*.rs,out=files" ` - --sort files-first - -``` - -**3. Wywołanie wieloliniowe w Bash (Linux/macOS)** – Wymaga znaku backslash (`\`) na końcu każdej linii: - -```bash -cargo run -- plot tree \ - --task "loc=.,inc=./Cargo.toml,inc=./README.md,inc=./src/main.rs,inc=./src/cli.rs,out=files" \ - --task "loc=./src/lib,fil=*.rs,out=files" \ - --sort files-first - -``` - ---- - -### Podkomenda: `doc` - OPCJE - -Główny orkiestrator biblioteki. Służy do zautomatyzowanego tworzenia gotowych raportów. **Podkomenda `doc` dziedziczy wszystkie opcje budowania zadań z podkomendy `tree**` (czyli `-p`, `--no-default-excludes`, `-e`, `-i`, `-f`, `-t`, `--task`, `--tasks`). - -Oprócz nich posiada dedykowane opcje sterujące procesem zapisu plików i formatowania Markdown: - -#### Opcje wejścia/wyjścia (I/O) - -* **`--out-dir <ŚCIEŻKA>`** – Określa katalog, w którym zostanie zapisany wygenerowany raport (Domyślnie: `doc`). Program automatycznie utworzy ten folder, jeśli nie istnieje. -* **`-o, --out `** – Bazowa nazwa pliku wyjściowego. Program automatycznie utworzy folder `doc/`, doklei do nazwy wygenerowaną sygnaturę czasową oraz rozszerzenie `.md` (Domyślnie: `code`). -* **`--dry-run`** (lub **`--simulate`**) – Tryb symulacji (Fail Fast). Przechodzi przez cały proces skanowania, formatowania drzewa oraz identyfikatorów, wypisując podsumowanie w terminalu, ale **blokuje fizyczny zapis na dysku**. - -#### Opcje formatowania Markdown - -* **`-I, --id-style `** – Formatowanie zautomatyzowanych nagłówków sekcji (Identyfikatorów): - * `tag` – (Domyślnie) Pełne tagowanie opisowe (np. `## Plik-RustLibPub_01:`). - * `num` – Numeracja sekwencyjna (np. `## Plik-001:`). - * `none` – Minimalizm (samą ścieżkę, np. `## Plik: ./src/main.rs`). -* **`-T, --insert-tree `** – Decyduje o spisie treści na początku dokumentu: - * `dirs-first` / `files-first` (Domyślnie) / `none`. - -#### Opcje metadanych i znaków wodnych - -* **`--watermark `** – Pozycja stopki reklamowej cargo-plot w raporcie: `first`, `last` (Domyślnie) lub `none`. -* **`--print-command`** – Rzutuje użytą komendę CLI na samą górę raportu, ułatwiając reprodukcję zadania np. w CI/CD. -* **`--suffix-stamp`** (alias: `--sufix-stamp`) – Przenosi znacznik czasu z wnętrza pliku (tytułu) bezpośrednio do jego nazwy na dysku. -* **`--title-file `** – Zmienia główny nagłówek raportu (Domyślnie: `RAPORT`). -* **`--title-file-with-path`** – Dokleja informację o ścieżce do głównego tytułu raportu. - ---- - -### Podkomenda: `doc` - Przykład: Generowanie raportu z wielu lokalizacji (Multi-Task) - -Poniższa komenda zbiera dane z dwóch zadań (pliki główne oraz biblioteka), ustawia prefiks nazwy pliku na `doc`, włącza numerację sekwencyjną sekcji (`id-num`) i generuje spis treści w formacie `files-first`. - -**1. Wywołanie wieloliniowe w PowerShell (Windows):** - -```powershell -cargo run -- plot doc ` - --task "loc=.,inc=./Cargo.toml,inc=./README.md,inc=./src/main.rs,inc=./src/cli.rs,out=files" ` - --task "loc=./src/lib,fil=*.rs,out=files" ` - --out "doc" ` - --out-dir "doc" ` - --id-style num ` - --insert-tree files-first - -``` - -**2. Wywołanie wieloliniowe w Bash (Linux/macOS):** - -```bash -cargo run -- plot doc \ - --task "loc=.,inc=./Cargo.toml,inc=./README.md,inc=./src/main.rs,inc=./src/cli.rs,out=files" \ - --task "loc=./src/lib,fil=*.rs,out=files" \ - --out "doc" \ - --out-dir "doc" \ - --id-style num \ - --insert-tree files-first - -``` - -**3. Wywołanie uniwersalne (jednoliniowe):** - -```powershell -cargo run -- plot doc --task "loc=.,inc=./Cargo.toml,inc=./README.md,inc=./src/main.rs,inc=./src/cli.rs,out=files" --task "loc=./src/lib,fil=*.rs,out=files" --out "doc" --out-dir "doc" --id-style num --insert-tree files-first +🔗 **Crates.io**: [crates.io/crates/cargo-plot](https://crates.io/crates/cargo-plot) +🔗 **GitHub**: [github.com/j-Cis/cargo-plot](https://github.com/j-Cis/cargo-plot) +```text +[KiB 174.4] └──┬ 📂 cargo-plot-2 ./cargo-plot-2/ +[KiB 1.380] ├──• ⚙️ Cargo.toml ./Cargo.toml +[KiB 173.0] └──┬ 📂 src ./src/ +[ B 70.00] ├──• 🦀 addon.rs ./src/addon.rs +[KiB 2.431] ├──┬ 📂 addon ./src/addon/ +[KiB 2.431] │ └──• 🦀 time_tag.rs ./src/addon/time_tag.rs +[ B 120.0] ├──• 🦀 core.rs ./src/core.rs +[KiB 65.85] ├──┬ 📂 core ./src/core/ +[KiB 1.117] │ ├──• 🦀 file_stats.rs ./src/core/file_stats.rs +[KiB 3.177] │ ├──┬ 📂 file_stats ./src/core/file_stats/ +[KiB 3.177] │ │ └──• 🦀 weight.rs ./src/core/file_stats/weight.rs +[ B 285.0] │ ├──• 🦀 path_matcher.rs ./src/core/path_matcher.rs +[KiB 23.00] │ ├──┬ 📂 path_matcher ./src/core/path_matcher/ +[KiB 14.65] │ │ ├──• 🦀 matcher.rs ./src/core/path_matcher/matcher.rs +[KiB 4.501] │ │ ├──• 🦀 sort.rs ./src/core/path_matcher/sort.rs +[KiB 3.845] │ │ └──• 🦀 stats.rs ./src/core/path_matcher/stats.rs ``` --- -### Podkomenda: `dist-copy` - OPCJE - -Automatyzuje proces przygotowywania paczek do wydań (releases). Przeszukuje katalog kompilacji Rusta i kopiuje wskazane pliki wykonywalne (lub automatycznie wykrywa wszystkie prawdziwe binarki, sprytnie odsiewając pliki techniczne Rusta) do uporządkowanej struktury z podziałem na system operacyjny i profil (np. `dist/windows/release/`). - -#### Opcje dla komendy `dist-copy` +## 🚀 Główne Funkcje / Key Features -* **`-b, --bin ...`** – Nazwa pliku binarnego (bez rozszerzenia `.exe`), którego program ma szukać. Flagę można podać wielokrotnie, aby skopiować kilka konkretnych plików (np. `-b server -b client`). **Jeśli nie podano tej flagi, program użyje wbudowanej heurystyki i skopiuje WSZYSTKIE odnalezione pliki wykonywalne**. -* **`--target-dir <ŚCIEŻKA>`** – Ścieżka do technicznego folderu kompilacji Rusta (Domyślnie: `./target`). -* **`--dist-dir <ŚCIEŻKA>`** – Ścieżka do docelowego folderu, w którym ma zostać zbudowana struktura dystrybucyjna (Domyślnie: `./dist`). -* **`--clear`** – Czyści (usuwa) cały folder dystrybucyjny (`dist-dir`) przed rozpoczęciem skanowania i kopiowania, gwarantując paczkę bez starych artefaktów. -* **`--no-overwrite`** – Zabezpiecza przed nadpisaniem istniejących plików w folderze docelowym (domyślnie program nadpisuje pliki). -* **`--dry-run`** (lub **`--simulate`**) – Tryb symulacji (Fail Fast). Przeszukuje folder `target`, rozpoznaje architektury i systemy operacyjne, wypisując w konsoli informację o tym, co *zostałoby* skopiowane, ale **nie wykonuje absolutnie żadnych zapisów ani usunięć na fizycznym dysku**. +* **Silnik Wzorców 2.0 / Pattern Engine 2.0**: Zaawansowane filtrowanie strukturalne z użyciem flag relacyjnych: `@` (rodzeństwo), `$` (sierota) oraz `+` (głęboki skan). Wspiera również rozwijanie klamer `{a,b}`. + * **Structural Flags**: Advanced filtering using relational flags: `@` (sibling), `$` (orphan), and `+` (deep scan). Also supports brace expansion `{a,b}`. +* **Audyt Miejsca / Disk Audit**: Obliczanie wag w systemach binarnym (IEC) i dziesiętnym (SI). Flaga `-a` pozwala na odczyt rzeczywistego fizycznego rozmiaru folderów z dysku. + * **Weight Systems**: Calculate sizes in Binary (IEC) and Decimal (SI) systems. The `-a` flag enables reading the actual physical directory size from the disk. +* **Trzy Interfejsy / Triple Interface**: Pełna swoboda pracy dzięki natywnej aplikacji GUI (`egui`), interaktywnemu TUI (`cliclack 0.5.0`) oraz klasycznemu CLI. + * **Multi-Modal**: Full workflow flexibility with a native GUI (`egui`), interactive TUI (`cliclack 0.5.0`), and classic CLI. +* **Automatyczna Dokumentacja / Auto-Doc**: Generowanie raportów Markdown i pełnych archiwów kodu źródłowego z profesjonalną tabelaryczną stopką metadanych. + * **Technical Reporting**: Generate Markdown reports and full source code archives with professional tabular metadata footers. --- -### Podkomenda: `dist-copy` - Przykład: Przygotowanie czystej dystrybucji - -Komenda ta wyczyści folder `./dist`, a następnie spróbuje skopiować binarkę `cargo-plot` z folderu `target`, dbając o to, by nie nadpisać istniejących plików, jeśli proces czyszczenia by je pominął. - -**1. Wywołanie wieloliniowe w PowerShell (Windows):** - -```powershell -cargo run -- plot dist-copy ` - --bin "cargo-plot" ` - --target-dir "./target" ` - --dist-dir "./dist" ` - --clear ` - --no-overwrite - -``` - -**2. Wywołanie wieloliniowe w Bash (Linux/macOS):** +## 🔍 Składnia Wzorców / Pattern Syntax -```bash -cargo run -- plot dist-copy \ - --bin "cargo-plot" \ - \--target-dir "./target" \ - \--dist-dir "./dist" \ - --clear \ - --no-overwrite - -``` - -**3. Wywołanie uniwersalne (jednoliniowe):** - -```powershell -cargo run -- plot dist-copy --bin "cargo-plot" --target-dir "./target" --dist-dir "./dist" --clear --no-overwrite - -``` +| Symbol | Opis (PL) | Description (ENG) | +| :--- | :--- | :--- | +| `src/{lib,bin}` | Rozwijanie klamer | Brace expansion | +| `!*test*` | Twarde Weto (Negacja) | Hard Veto (Negation) | +| `src/+` | Tryb głęboki (rekurencja) | Deep mode (recursive) | +| `@tui` | Rodzeństwo (wymaga plik+dir) | Sibling (requires file+dir) | +| `$core` | Sierota (tylko brak pary) | Orphan (only if pair is missing) | --- ---- - -### Przykłady wywołań CLI - -**1. Szybki podgląd drzewa w konsoli (tylko pliki Rust, wykluczenie katalogu target):** - -```bash -cargo plot tree -p . -e "./target/" -f "*.rs" -t files --sort dirs-first - -``` - -**2. Symulacja generowania raportu (Dry Run) z użyciem zewnętrznego pliku konfiguracyjnego:** - -```bash -cargo plot doc --tasks ./config/plot_tasks.toml --dry-run -``` +## 🛠 Instalacja / Installation -**3. Generowanie docelowego raportu przy użyciu Inline Multi-Task (wiele zadań naraz):** +**Jako rozszerzenie Cargo (Zalecane) / As Cargo extension (Recommended):** ```bash -cargo plot doc \ - --task loc=.,inc=Cargo.toml,inc=src/main.rs,out=files \ - --task loc=./src/lib,fil=*.rs,out=files \ - --out "raport_architektury" \ - --id-style num \ - --insert-tree files-first - +cargo install cargo-plot ``` -**4. Wyświetlenie pełnego ekranu pomocy dla danej podkomendy:** +**Budowanie deweloperskie / Development build:** ```bash -cargo plot doc --help - -``` - -**5. Analizator dyskowy (Tylko foldery z prawdziwym rozmiarem dziesiętnym na dysku):** - -```bash -cargo plot tree -t dirs -w decimal --no-file-weight --real-dir-weight - +git clone https://github.com/j-Cis/cargo-plot.git +cd cargo-plot +cargo build --release ``` -**6. Raport z pełną analityką, własnym tytułem i komendą na górze (Power-User Move):** - -```bash -cargo plot doc -w binary -I num -T files-first --title-file "Dokumentacja Architektury" --title-file-with-path --print-command --suffix-stamp --task "loc=.,inc=src,out=files" -``` - ---- --- ---- - -## TUI (Interfejs Interaktywny) - -Tryb TUI (Text-based User Interface) to potężny, interaktywny kreator, który pozwala na wygodne korzystanie ze **100% możliwości API** biblioteki `cargo-plot` bez konieczności ręcznego wpisywania skomplikowanych flag w terminalu. - -### Szybki start +## Zestawienie Różnic / Comparison Table -Aby uruchomić interaktywny panel sterowania, wystarczy wywołać narzędzie bez żadnych dodatkowych argumentów: - -```powershell -cargo plot - -``` - -Narzędzie przywita Cię czytelnym menu, które poprowadzi Cię przez proces konfiguracji wybranych zadań. +| Cecha / Feature | Wersja / Version 0.1.5 | Wersja / Version 0.2.0 | +| :--- | :--- | :--- | +| **Architektura**
**Architecture** | **[PL]** Płaska struktura biblioteki (`src/lib/*.rs`).
**[ENG]** Flat library structure (`src/lib/*.rs`). | **[PL]** Modularna struktura „Core + Interfaces” (Porty i Adaptery).
**[ENG]** Modular "Core + Interfaces" structure (Ports & Adapters). | +| **Interfejsy**
**Interfaces** | **[PL]** Klasyczne CLI oraz uproszczone TUI.
**[ENG]** Classic CLI and simplified TUI. | **[PL]** Trio: CLI, TUI (v0.5.0) oraz natywne GUI (egui).
**[ENG]** Triple: CLI, TUI (v0.5.0), and native GUI (egui). | +| **Silnik Wzorców**
**Pattern Engine** | **[PL]** Proste filtrowanie oparte na maskach Glob.
**[ENG]** Simple filtering based on Glob masks. | **[PL]** Regex + flagi relacyjne: `@` (rodzeństwo), `$` (sierota), `+` (głęboki skan).
**[ENG]** Regex + relational flags: `@` (sibling), `$` (orphan), `+` (deep scan). | +| **Statystyki**
**Statistics** | **[PL]** Brak lub tylko sumaryczna waga projektu.
**[ENG]** None or only total project weight. | **[PL]** Live Update: podział na pliki tekstowe (Txt), binarne (Bin) i błędy (Err).
**[ENG]** Live Update: split into Text (Txt), Binary (Bin), and Errors (Err). | +| **Raportowanie**
**Reporting** | **[PL]** Rozbudowana, opisowa stopka tekstowa.
**[ENG]** Long, descriptive text footer. | **[PL]** Profesjonalna tabela metadanych w bloku Markdown.
**[ENG]** Professional metadata table in a Markdown block. | +| **System Wag**
**Weight System** | **[PL]** Podstawowe obliczenia (SI/IEC).
**[ENG]** Basic calculations (SI/IEC). | **[PL]** SI/IEC + flaga `-a` (fizyczny rozmiar folderów z dysku).
**[ENG]** SI/IEC + `-a` flag (actual physical folder size from disk). | +| **Bezpieczeństwo**
**Safety** | **[PL]** Standardowe mechanizmy Rusta.
**[ENG]** Standard Rust mechanisms. | **[PL]** Rygorystyczny zakaz używania bloków `unsafe`.
**[ENG]** Strict prohibition of `unsafe` blocks. | +| **Logika Widoku**
**View Logic** | **[PL]** Zduplikowana w plikach `tree.rs` i `grid.rs`.
**[ENG]** Duplicated in `tree.rs` and `grid.rs` files. | **[PL]** Zunifikowane budowanie struktury w `shared.rs` (DRY).
**[ENG]** Unified structure building in `shared.rs` (DRY). | +| **Internacjonalizacja**
**i18n** | **[PL]** Tylko twardo zakodowane teksty.
**[ENG]** Hardcoded texts only. | **[PL]** Pełne wsparcie PL/EN we wszystkich modułach i interfejsach.
**[ENG]** Full PL/EN support across all modules and interfaces. | --- -### Funkcje i Moduły TUI - -Architektura TUI została podzielona na wyspecjalizowane moduły, z których każdy oferuje pełną kontrolę nad parametrami domeny. - -#### 1. 🌲 Tree Explorer (Eksplorator Drzewa) - -Pozwala na interaktywne budowanie wizualizacji struktury projektu. - -* **Obsługa Multi-Task**: Możesz zdefiniować wiele niezależnych lokalizacji (zadań) do przeskanowania, które zostaną połączone w jeden wspólny widok drzewa. -* **Inteligentna Whitelista**: TUI automatycznie rozpoznaje, czy podana ścieżka jest folderem i stosuje wzorce rekurencyjne (np. zamienia `src` na `src/**/*`), aby zapewnić pełny skan zawartości. -* **Pełne filtrowanie**: Interaktywne ustawianie czarnych list (blacklist), białych list (whitelist) oraz filtrów rozszerzeń plików (np. `*.rs, *.toml`). - -#### 2. 📄 Doc Orchestrator (Orkiestrator Raportów) +## Kluczowe usprawnienia techniczne / Key Technical Enhancements -Najbardziej zaawansowany moduł TUI, odzwierciedlający pełną moc silnika generowania dokumentacji. - -* **Hierarchiczna konfiguracja**: Pozwala na zdefiniowanie wielu plików raportów (`DocTask`) w jednej sesji. -* **Wiele zadań na raport**: Każdy raport może składać się z dowolnej liczby zadań skanowania (`Task`), co pozwala na łączenie w jednym dokumencie kodów z różnych, odległych od siebie katalogów. -* **Personalizacja**: Wybór stylu identyfikatorów sekcji (`tag`, `num`, `none`) oraz formatu spisu treści (drzewa). - -#### 3. 📦 Dist Manager (Zarządzanie Wydaniem) - -Automatyzuje przygotowanie binarek do dystrybucji. - -* **Wykrywanie binarek**: Możliwość podania konkretnych nazw lub automatycznego wykrycia wszystkich plików wykonywalnych w folderze `target`. -* **Zarządzanie ścieżkami**: Dowolna konfiguracja katalogów źródłowych i docelowych. -* **Bezpieczeństwo**: Opcja czyszczenia folderu `dist` przed operacją oraz tryb symulacji (**Dry Run**). - -#### 4. 🕒 Stamp Tool (Generator Sygnatur) - -Szybki kreator unikalnych znaczników czasu. - -* **Tryb Auto**: Błyskawiczne generowanie stempla dla aktualnego czasu systemowego. -* **Tryb Manual**: Precyzyjne wpisywanie własnej daty i godziny z walidacją formatu. +* **[PL] Precyzja wagi korzenia:** W wersji 0.2.0 wyeliminowano błąd wyświetlania wagi `0` dla głównego folderu; teraz korzeń dumnie reprezentuje sumę całego skanowania. +* **[ENG] Root weight precision:** Version 0.2.0 eliminates the bug displaying `0` weight for the main folder; now the root proudly represents the sum of the entire scan. +* **[PL] Izolacja procesów GUI:** Dzięki nowej architekturze, generowanie podglądu kodu w GUI odbywa się tylko dla żądanej sekcji, co drastycznie optymalizuje zużycie pamięci. +* **[ENG] GUI process isolation:** Thanks to the new architecture, code preview generation in the GUI occurs only for the requested section, drastically optimizing memory usage. +* **[PL] Modernizacja TUI:** Pełna przesiadka na `cliclack 0.5.0` oraz integracja z `shlex` zapewniają bezbłędne parsowanie złożonych komend CLI wewnątrz interfejsu interaktywnego. +* **[ENG] TUI Modernization:** Full transition to `cliclack 0.5.0` and `shlex` integration ensures flawless parsing of complex CLI commands within the interactive interface. --- -### Zalety pracy w trybie TUI +## 🌍 Wspierane Systemy / Supported Systems -* **Pętla akcji**: Program nie zamyka się po wykonaniu zadania. Po każdej operacji zostaniesz zapytany, czy chcesz wykonać kolejną czynność, co pozwala na błyskawiczne generowanie serii raportów i podglądów drzew. -* **Odporność na błędy**: TUI automatycznie czyści ścieżki (np. usuwa kłopotliwe przedrostki `./`), dzięki czemu wzorce dopasowania zawsze działają poprawnie. -* **Feedback w czasie rzeczywistym**: Użycie animowanych spinnerów i kolorowych komunikatów informuje Cię na bieżąco o postępie skanowania i generowania plików. - -> **Wskazówka deweloperska**: Jeśli często powtarzasz te same złożone zadania, TUI jest idealne do ich szybkiego wyklikania, ale pamiętaj, że możesz je również zautomatyzować w skryptach CI/CD korzystając z [Interfejsu CLI](https://www.google.com/search?q=%23cli-interfejs-wiersza-polece%C5%84). +| System | Target Triple | +| :--- | :--- | +| **Windows 64-bit** | `x86_64-pc-windows-msvc` | +| **Linux 64-bit** | `x86_64-unknown-linux-gnu` | +| **macOS (Intel/M1)** | `x86_64-apple-darwin` / `aarch64-apple-darwin` | --- ---- ---- - -## API -> BIBLIOTEKA - -Biblioteka `cargo-plot` oferuje bogate API do przeszukiwania plików, budowania z nich struktury drzewa, wizualizowania jej w konsoli lub czystym tekście oraz generowania raportów Markdown zawierających skan wybranych kodów źródłowych. - -### API - -Oto pełna instrukcja opisująca możliwości użycia: - -#### 1. Moduł `fn_datestamp` (Generowanie sygnatur czasowych) - -Zapewnia funkcje do tworzenia spójnych sygnatur czasowych (np. `2026Q1D069W11_Tue10Mar_063950222`). - -* **`pub use chrono::{NaiveDate, NaiveTime}`** – Wygodny reeksport typów. Użytkownik API nie musi dodawać zależności `chrono` do swojego `Cargo.toml`, aby ręcznie definiować czas. -* **`datestamp_now() -> String`** – Generuje sygnaturę dla obecnego, lokalnego czasu. -* **`datestamp(date: NaiveDate, time: NaiveTime) -> String`** – Generuje sygnaturę dla precyzyjnie wskazanej daty i czasu (używając udostępnionych przez moduł typów). +> 🚀 **cargo-plot** | Wygenerowano przez cargo-plot v0.2.0 | [GitHub](https://github.com/j-Cis/cargo-plot) +> 🚀 **cargo-plot** | Generated by cargo-plot v0.2.0 | [GitHub](https://github.com/j-Cis/cargo-plot) -#### 2. Moduł `fn_path_utils` (Narzędzia ścieżek) - -Służy do formatowania ścieżek w jednolity sposób niezależnie od systemu operacyjnego. - -* **`standardize_path(path: &Path) -> String`** – Konwertuje ścieżkę do stringa, zamienia ukośniki na uniksowe (`/`) i usuwa windowsowe prefiksy rozszerzone (`//?/`). -* **`to_display_path(path: &Path, base_dir: &Path) -> String`** – Zwraca czystą, relatywną ścieżkę (np. `./src/main.rs`), jeśli podany `path` znajduje się wewnątrz `base_dir`. W przeciwnym razie zwraca po prostu ustandaryzowaną ścieżkę bazową. - -#### 3. Moduł `fn_pathtype` (Typowanie plików i ikony) - -Dostarcza jedno źródło prawdy (SSoT) dla ikon drzewa oraz oznaczania języków w Markdown. - -* **Struktura `PathFileType`** – Przechowuje publiczne pola: `icon: &'static str` oraz `md_lang: &'static str`. -* **Stała `DIR_ICON: &str`** – Globalna ikona zdefiniowana dla folderów. -* **`get_file_type(ext: &str) -> PathFileType`** – Oczekuje rozszerzenia pliku (np. `"rs"`) i zwraca odpowiadający mu język Markdown oraz ikonę. Obsługiwane rozszerzenia: `rs`, `toml`, `slint`, `md`, `json`, `yaml`/`yml`, `html`, `css`, `js`, `ts` oraz domyślny fallback jako `text`. - -#### 4. Moduł `fn_filespath` (Silnik wyszukiwania ścieżek) - -Główne narzędzie API do filtrowania i pozyskiwania ścieżek z systemu plików. - -* **Struktura `Task<'a>`** – Definiuje zadanie wyszukiwania. Posiada następujące publiczne pola: - * `path_location: &'a str` – Ścieżka początkowa (domyślnie `"."`). - * `path_exclude: Vec<&'a str>` – Wzorce Glob (np. `"folder/*"`) do ignorowania. Odrzucenie następuje już na etapie skanowania, oszczędzając czas. - * `path_include_only: Vec<&'a str>` – Ścisłe wzorce uwzględniające (np. `"*.rs"`). Jeśli lista nie jest pusta, narzędzie pominie wszystko, co nie pasuje. - * `filter_files: Vec<&'a str>` – Dodatkowe odfiltrowanie samych plików po etapie skanowania. - * `output_type: &'a str` – Steruje wynikiem. Przyjmuje `"dirs"`, `"files"` lub `"dirs_and_files"` (domyślne). Posiada ukrytą logikę dla trybu `"files"`, która automatycznie podciąga nadrzędne foldery, aby drzewo się nie spłaszczyło. -* **Wdrożony Trait `Default` dla `Task`** – Możliwość tworzenia zadania przez `..Default::default()`. -* **`filespath(tasks: &[Task]) -> Vec`** – Funkcja konsumująca referencję do listy zadań i zwracająca unikalny wektor znalezionych ścieżek. - -#### 5. Moduł `fn_filestree` (Budowanie struktury drzewa) - -Przetwarza płaską listę ścieżek na hierarchiczną strukturę danych. - -* **Struktura `FileNode`** – Węzeł drzewa zawierający pola: `name: String`, `path: PathBuf`, `is_dir: bool`, `icon: String`, `children: Vec`. -* **`filestree(paths: Vec, sort_method: &str, weight_cfg: &WeightConfig) -> Vec`** – Buduje wektor węzłów ze ścieżek przypisując do nich automatycznie ikony oraz wyliczając wagę na podstawie konfiguracji `WeightConfig`. Obsługuje trzy sposoby sortowania argumentem `sort_method`. - * `"files-first"` – Pliki wyświetlane są przed folderami. - * `"dirs-first"` – Foldery wyświetlane są przed plikami. - * Dowolna inna wartość – Sortuje domyślnie tylko alfabetycznie. - -#### 6. Moduł `fn_plotfiles` (Wizualizacja drzewa) - -Zamienia obiekt `FileNode` w wizualne drzewo ASCII. - -* **Struktura `TreeStyle`** – Udostępnia pełną kontrolę nad znakami graficznymi (box-drawing) budującymi gałęzie. Posiada publiczne pola: `dir_last_with_children`, `dir_last_no_children`, `dir_mid_with_children`, `dir_mid_no_children`, `file_last`, `file_mid`, `indent_last`, `indent_mid`. -* **Wdrożony Trait `Default` dla `TreeStyle`** – Inicjalizuje klasyczne załamania typu `└──┬`, `├───` itp.. -* **`plotfiles_txt(nodes: &[FileNode], indent: &str, style: Option<&TreeStyle>) -> String`** – Generuje strukturę w postaci czystego tekstu (bez kolorów), z podanym wcięciem początkowym. Styl jest opcjonalny (można użyć `None`). -* **`plotfiles_cli(nodes: &[FileNode], indent: &str, style: Option<&TreeStyle>) -> String`** – Generuje gotowy do wydruku (do konsoli CLI) pokolorowany ciąg znaków (zielone gałęzie, kolorowe ikony, różnicowane foldery i pliki) z wykorzystaniem biblioteki `colored`. - -#### 7. Moduł `fn_doc_models` (Model zadania generatora dokumentacji) - -Definiuje strukturę wejściową dla orkiestratora raportów. - -* **Struktura `DocTask<'a>`** – Posiada publiczne pola konfigurujące raport Markdown: - * `output_filename`, `insert_tree`, `id_style`, `tasks` (bez zmian). - * `weight_config: WeightConfig` – Konfiguracja wagi i rozmiarów plików na spisie treści. - * `watermark: &'a str` / `command_str: Option` – Sterowanie znakiem wodnym i wydrukiem komendy. - * `suffix_stamp: bool` / `title_file: &'a str` / `title_file_with_path: bool` – Mechanizmy nadpisywania tytułów i nazw plików. - -#### 8. Moduł `fn_doc_id` (Generowanie etykiet identyfikacyjnych) - -* **`generate_ids(paths: &[PathBuf]) -> HashMap`** – Iteruje po wektorze ścieżek plików i nadaje każdemu specjalny ciąg identyfikacyjny według twardych i dynamicznych reguł (np. dla `"Cargo.toml"` nadaje ID `"TomlCargo"`, główny `mod.rs` w bibliotece otrzyma `"RustLibMod_00"`, a ogólne pliki dostaną np. `"FileMd"` lub `"RustBin_01"`). Zwraca mapowanie ułatwiające nadawanie nagłówków w raporcie. - -#### 9. Moduł `fn_doc_write` (Fizyczny zapis raportu) - -* **`write_md(out_path: &str, files: &[PathBuf], id_map: &HashMap, tree_text: Option, id_style: &str, watermark: &str, command_str: &Option, stamp: &str, suffix_stamp: bool, title_file: &str, title_file_with_path: bool) -> io::Result<()>`** – Główny silnik zapisu. Parsuje całe wejście, omijając pliki binarne (zależność od `is_blacklisted_extension`). Buduje strukturę tytułów, wkleja znak wodny oraz komendę, a na końcu opakowuje kody źródłowe w bloki kodu Markdown. Zapisuje gotowy tekst na dysk, obsługując transparentnie błędy I/O. - -#### 10. Moduł `fn_doc_gen` (Orkiestracja generowania dokumentacji) - -Moduł, który łączy wszystkie powyższe silniki w jeden proces. - -* **`generate_docs(doc_tasks: Vec, output_dir: &str) -> io::Result<()>`** – Główna i jedyna funkcja wysokiego poziomu w tym module. Iteruje po zadaniach typu `DocTask`, asynchronicznie (pod kątem działania, nie `async` Rusta) tworzy znacznik czasu, buduje pełną nazwę pliku, pobiera ścieżki (`filespath`), rysuje drzewo (`filestree` + `plotfiles_txt`), nakłada identyfikatory (`generate_ids`), wymusza utworzenie folderu wynikowego (o ile nie istnieje) i przekazuje to do zapisania na dysku (`write_md`). Wydrukuje w konsoli informację po wygenerowaniu każdego pliku. Zwraca transparentny `Result` do obsługi ewentualnych braków uprawnień odczytu/zapisu na dysku. - -#### 11. Moduł `fn_copy_dist` (Zarządzanie paczkami dystrybucyjnymi) - -Służy do zautomatyzowanego wyciągania skompilowanych plików wykonywalnych z technicznego folderu `target/` i organizowania ich w czystą, zorganizowaną strukturę gotową do dystrybucji (np. pod wydania (releases) na GitHubie). - -* **Struktura `DistConfig<'a>`** – Wdraża wzorzec *Parameter Object*, udostępniając zaawansowaną kontrolę nad wdrażaniem (deploymentem). Posiada następujące pola: - * `target_dir: &'a str` / `dist_dir: &'a str` – Wskazują katalog źródłowy i docelowy. - * `binaries: Vec<&'a str>` – Lista poszukiwanych plików. **Potężna heurystyka**: Jeśli podasz pustą listę (`vec![]`), funkcja spróbuje automatycznie odnaleźć i przenieść **wszystkie** skompilowane pliki wykonywalne, sprytnie odrzucając "śmieci" kompilacyjne (jak pliki `.d`, `.rlib`, `.pdb` itp.). - * `clear_dist: bool` – Bezpiecznie usuwa stary folder dystrybucji przed nowym kopiowaniem. - * `overwrite: bool` – Pozwala zablokować nadpisywanie już istniejących plików. - * `dry_run: bool` – Wbudowany tryb symulacji. Program przeszukuje foldery, mapuje ścieżki i zwraca wynik, ale **nie wykonuje absolutnie żadnych zapisów/usunięć na dysku** (Defensive Programming). -* **Wdrożony Trait `Default` dla `DistConfig`** – Pozwala na wywołanie domyślnej, bezpiecznej konfiguracji (bez czyszczenia, z domyślnymi ścieżkami). -* **`copy_dist(config: &DistConfig) -> io::Result>`** – Funkcja przeszukująca katalog kompilacji. - * Rozpoznaje profile (`release`, `debug`) oraz systemy natywne i krzyżowe (*Target Triples* np. `windows`, `linux`, `macos`, `android`, `wasm`). - * Kopiuje pliki tworząc intuicyjną hierarchię według klucza `/` (np. `dist/linux/release` lub `dist/windows/debug`). - * Realizuje założenia *Fail Fast* i *SRP* – nie drukuje niczego samowolnie w terminalu. Jeśli katalog źródłowy nie istnieje, natychmiast rzuca błąd. Zwraca `Ok` zawierające wektor krotek ze ścieżkami `(Źródło, Cel)`, oddając pełną kontrolę nad prezentacją informacji (np. w konsoli) programiście. - -#### 12. Moduł `fn_files_blacklist` (Weryfikacja typów danych) - -Dostarcza mechanizmy ochronne (Defensive Programming), które zapobiegają wczytywaniu do pamięci RAM ogromnych lub nieczytelnych plików binarnych podczas generowania raportów tekstowych. - -* **`is_blacklisted_extension(ext: &str) -> bool`** – Funkcja weryfikująca rozszerzenie pliku pod kątem przynależności do rozbudowanej "czarnej listy". - * **Zakres ochrony**: Obejmuje grafikę (np. `.png`, `.psd`), binarki i artefakty kompilacji (np. `.exe`, `.rlib`, `.obj`), archiwa (np. `.zip`, `.iso`), bazy danych (np. `.sqlite`), dokumenty biurowe (np. `.docx`, `.pdf`), fonty oraz media (audio/wideo). - * **Zastosowanie**: Wykorzystywana przez silnik zapisu (`write_md`) do filtrowania plików przed próbą ich odczytu. Jeśli rozszerzenie znajduje się na liście, generator pomija treść pliku, wstawiając jedynie stosowną informację w raporcie, co drastycznie redukuje zużycie zasobów systemowych i zapobiega błędom kodowania UTF-8. - -#### 13. Moduł `fn_weight` (Analiza zajętości miejsca) - -Dostarcza zaawansowane mechanizmy obliczania i formatowania wag (rozmiarów) plików oraz folderów. - -* **Enum `UnitSystem`** – Definiuje system matematyczny wyliczeń: `Decimal` (system SI, $1000^n$: kB, MB), `Binary` (system programistyczny IEC, $1024^n$: KiB, MiB) oraz `None` (wyłącza wagi). -* **Struktura `WeightConfig`** – Centralny obiekt konfigurujący proces wizualizacji wag: - * `system: UnitSystem` – Wybór jednostek. - * `precision: usize` – Określa całkowitą szerokość pola liczbowego, gwarantując idealne pionowe wyrównanie drzewa (domyślnie 5, minimum 3). - * `show_for_files: bool` / `show_for_dirs: bool` – Niezależna kontrola widoczności wag dla plików i folderów (ukrycie wagi zachowuje oryginalne wcięcia ramki). - * `dir_sum_included: bool` – Potężna opcja sterująca logiką. Jeśli `true` (domyślnie), waga folderu to suma wyłącznie plików uwzględnionych przez Twoje filtry (whitelist/blacklist). Jeśli `false`, program zejdzie bezpośrednio na dysk twardy i wyliczy faktyczny, fizyczny rozmiar katalogu. -* **`format_weight(bytes: u64, config: &WeightConfig) -> String`** – Główna funkcja parsująca bajty do wyrównanego ciągu tekstowego w formacie `[qq xxxxx] `. - -### Użycie - Przykłady - -Biblioteka `cargo-plot` została zaprojektowana w sposób modułowy. Poniżej znajdują się kompleksowe przykłady użycia pokazujące krok po kroku, jak w pełni wykorzystać możliwości naszego API w Twoich plikach binarnych. - -#### 1. Generowanie precyzyjnych sygnatur czasowych - -Moduł `fn_datestamp` jest idealny do oznaczania generowanych plików raportów jednolitym formatem. Możesz wygenerować znacznik dla aktualnego czasu lub wymusić własny: - -```rust -use lib::fn_datestamp::{datestamp, datestamp_now, NaiveDate, NaiveTime}; - -fn main() { - // Użycie z precyzyjnie podaną datą i czasem - let d = NaiveDate::from_ymd_opt(2026, 3, 9).unwrap(); - let t = NaiveTime::from_hms_milli_opt(13, 51, 1, 123).unwrap(); - let s1 = datestamp(d, t); - println!("Datestamp z podanym czasem: {}", s1); - - // Błyskawiczne użycie dla aktualnego, lokalnego czasu - let s2 = datestamp_now(); - println!("Datestamp teraz: {}", s2); -} -``` - -#### 2. Skanowanie plików i wizualizacja struktury drzewa ASCII - -Narzędzie pozwala zdefiniować złożone reguły wyszukiwania (wzorce ignorujące, wymuszające i filtrujące), a następnie przetworzyć wyniki w piękne drzewo plików, które można zapisać do czystego pliku Markdown lub wydrukować w kolorze w konsoli. - -```rust -use lib::fn_filespath::{filespath, Task}; -use lib::fn_filestree::{filestree, FileNode}; -use lib::fn_plotfiles::{plotfiles_cli, plotfiles_txt}; - -fn main() { - // 1. Definiowanie tablicy zadań skanowania systemu plików - let tasks = vec![ - Task { - output_type: "files", - // Dzięki wdrożeniu cechy Default, nie musimy wypełniać pustych filtrów - ..Default::default() - }, - ]; - - // 2. Zebranie unikalnych ścieżek - let paths = filespath(&tasks); - - // 3. Zbudowanie struktury węzłów drzewa z sortowaniem "pliki na górze" - let tree_ff: Vec = filestree(paths.clone(), "files-first"); - - // 4a. Generowanie czystego tekstu ASCII ze strukturą (np. do Markdowna) - let txt = plotfiles_txt(&tree_ff, "", None); - println!("MARKDOWN:\n{}", txt); - - // 3b. Zbudowanie struktury z sortowaniem "foldery na górze" - let tree_df: Vec = filestree(paths.clone(), "dirs-first"); - - // 4b. Wyświetlanie pokolorowanego, czytelnego drzewa w terminalu CLI - let cli = plotfiles_cli(&tree_df, "", None); - println!("CLI:\n{}", cli); -} -``` - -#### 3. Orkiestracja i generowanie pełnych raportów Markdown - -Najpotężniejszą funkcją biblioteki jest `generate_docs`, która łączy wszystkie silniki: skanowanie, rysowanie drzewa, nadawanie identyfikatorów i zapisywanie plików kodowych bezpośrednio do fizycznego katalogu. - -```rust -use lib::fn_filespath::Task; -use lib::fn_doc_models::DocTask; -use lib::fn_doc_gen::generate_docs; - -fn main() { - // Konfigurujemy jedno lub więcej niezależnych zadań generowania dokumentacji - let doc_tasks = vec![ - DocTask { - output_filename: "doc", // Przedrostek tworzonego pliku - insert_tree: "files-first", // Jak umieścić wizualizację drzewa w pliku - id_style: "id-num", // Metoda oznaczania nagłówków poszczególnych plików - - // Definiujemy, co fizycznie ma zostać wyciągnięte do raportu: - tasks: vec![ - Task { - path_location: ".", - path_include_only: vec!["./Cargo.toml", "./src/main.rs"], - output_type: "files", - ..Default::default() - }, - Task { - path_location: "./src/lib", - filter_files: vec!["*.rs"], // Chcemy tylko pliki Rust - output_type: "files", - ..Default::default() - }, - ], - }, - ]; - - // Uruchamiamy orkiestrator. - // Funkcja tworzy katalog wyjściowy ("doc"), generuje datestamp i zapisuje raport. - // Transparentnie zwraca Result, by móc obsłużyć potencjalne błędy zapisu I/O. - if let Err(e) = generate_docs(doc_tasks, "doc") { - eprintln!("[-] KRYTYCZNY BŁĄD podczas generowania dokumentacji: {}", e); - } else { - println!("[+] Zakończono generowanie wszystkich raportów!"); - } -} -``` - -#### 4. Ekstrakcja binarek i przygotowanie dystrybucji - -Moduł `fn_copy_dist` automatyzuje żmudny proces wyciągania skompilowanych plików wykonywalnych z folderu `target/` po wykonaniu `cargo build`. Poniższy przykład pokazuje, jak wywołać funkcję kopiującą i samodzielnie obsłużyć wynik (zgodnie z zasadą SRP), drukując ładne podsumowanie operacji w terminalu. - -```rust -use lib::fn_copy_dist::{copy_dist, DistConfig}; - -fn main() { - println!("[*] Rozpoczynam przygotowanie paczek dystrybucyjnych..."); - - // Pełna kontrola nad dystrybucją dzięki strukturze konfiguracyjnej - let config = DistConfig { - target_dir: "target", - dist_dir: "dist", - binaries: vec!["cargo-plot"], // Zostaw puste vec![], by pobrać wszystkie! - clear_dist: true, // Wyczyści stare pliki w ./dist - overwrite: false, // Nie nadpisze istniejących (choć tu clear_dist i tak usunie folder) - dry_run: false, // Zmień na true, by tylko zasymulować - }; - - match copy_dist(&config) { - Ok(skopiowane) => { - if skopiowane.is_empty() { - println!(" [-] Brak plików. Upewnij się, że użyto `cargo build`."); - } else { - for (src, cel) in skopiowane { - println!(" [+] Skopiowano: {} -> {}", src.display(), cel.display()); - } - } - }, - Err(e) => eprintln!("[-] KRYTYCZNY BŁĄD: {}", e), - } -} -``` - -#### 5. Analiza zajętości dysku (Drzewo z rozmiarami) - -Narzędzie posiada wbudowany silnik do sprawdzania rozmiaru plików, który pozwala na wygenerowanie drzewa przypominającego to z polecenia `du` w systemach Unix. - -```rust -use lib::fn_filespath::{filespath, Task}; -use lib::fn_filestree::{filestree, FileNode}; -use lib::fn_weight::{WeightConfig, UnitSystem}; -use lib::fn_plotfiles::plotfiles_cli; - -fn main() { - let tasks = vec![Task { ..Default::default() }]; - let paths = filespath(&tasks); - - // Konfiguracja wag: System binarny (KiB, MiB), ukrycie wagi dla plików - let w_cfg = WeightConfig { - system: UnitSystem::Binary, - precision: 5, - show_for_files: false, - ..Default::default() - }; - - let nodes = filestree(paths, "files-first", &w_cfg); - println!("{}", plotfiles_cli(&nodes, "", None)); -} -``` - ---- --- diff --git a/f.md b/f.md deleted file mode 100644 index bd981f7..0000000 --- a/f.md +++ /dev/null @@ -1,47 +0,0 @@ -# Struktura Projektu 2026Q1D070W11_Wed11Mar_001706338 - -**Wywołana komenda:** -```bash -target\debug\cargo-plot.exe plot tree -s files-first --no-default-excludes -e ./f.md -e ./d.md -e ./target/ -e ./.git/ -e ./test/ -e ./.gitignore -e ./u.md -e ./Cargo.lock -e ./LICENSE-APACHE -e ./LICENSE-MIT -e ./.github/ -e ./.cargo/ -e ./doc/ -e ./README.md -w binary --weight-precision 5 --no-dir-weight --out-file f.md --print-console --watermark last --print-command --title-file Struktura Projektu -``` - -```text -[KiB 1.689] ├──• ⚙️ Cargo.toml - └──┬ 📂 src -[ B 671.0] ├──• 🦀 main.rs - ├──┬ 📂 cli -[KiB 7.231] │ ├──• 🦀 args.rs -[ B 724.0] │ ├──• 🦀 dist.rs -[KiB 1.791] │ ├──• 🦀 doc.rs -[ B 408.0] │ ├──• 🦀 mod.rs -[ B 577.0] │ ├──• 🦀 stamp.rs -[KiB 3.690] │ ├──• 🦀 tree.rs -[KiB 2.486] │ └──• 🦀 utils.rs - ├──┬ 📂 lib -[KiB 6.758] │ ├──• 🦀 fn_copy_dist.rs -[KiB 1.702] │ ├──• 🦀 fn_datestamp.rs -[KiB 1.913] │ ├──• 🦀 fn_doc_gen.rs -[KiB 2.703] │ ├──• 🦀 fn_doc_id.rs -[ B 570.0] │ ├──• 🦀 fn_doc_models.rs -[KiB 4.593] │ ├──• 🦀 fn_doc_write.rs -[KiB 1.964] │ ├──• 🦀 fn_files_blacklist.rs -[KiB 8.222] │ ├──• 🦀 fn_filespath.rs -[KiB 4.604] │ ├──• 🦀 fn_filestree.rs -[ B 724.0] │ ├──• 🦀 fn_path_utils.rs -[KiB 1.546] │ ├──• 🦀 fn_pathtype.rs -[KiB 4.278] │ ├──• 🦀 fn_plotfiles.rs -[KiB 3.602] │ ├──• 🦀 fn_weight.rs -[ B 288.0] │ └──• 🦀 mod.rs - └──┬ 📂 tui -[KiB 1.393] ├──• 🦀 dist.rs -[KiB 4.244] ├──• 🦀 doc.rs -[KiB 1.487] ├──• 🦀 mod.rs -[KiB 1.023] ├──• 🦀 stamp.rs -[KiB 2.616] ├──• 🦀 tree.rs -[KiB 6.317] └──• 🦀 utils.rs - -``` - ---- -> 🚀 Wygenerowano przy użyciu [cargo-plot](https://crates.io/crates/cargo-plot) | Źródło: [GitHub](https://github.com/j-Cis/cargo-plot) - diff --git a/lib.md b/lib.md new file mode 100644 index 0000000..d07f2c2 --- /dev/null +++ b/lib.md @@ -0,0 +1,103 @@ + +### Specyfikacja Wzorców Dopasowań (Pattern Matching Specification) + +--- + +### 1. Standardowe modyfikatory dopasowań (Globbing & Wildcards) + +Warstwa parsowania przekształcająca znaki tekstowe w reguły wyrażeń regularnych. + +| Wzorzec | Nazwa techniczna | Zachowanie silnika | +| :--- | :--- | :--- | +| `*` | Single-level Wildcard | **[POL]:** Dopasowuje zero lub więcej znaków, ale nie przekracza granicy katalogu (nie dopasowuje `/`).

**[ENG]:** Matches zero or more characters but does not cross the directory boundary (does not match `/`). | +| `**` | Multi-level Wildcard | **[POL]:** Dopasowuje dowolną liczbę znaków łącznie z separatorami katalogów `/` (rekurencja wielopoziomowa).

**[ENG]:** Matches any number of characters including directory separators `/` (multi-level recursion). | +| `?` | Single Character | **[POL]:** Dopasowuje dokładnie jeden dowolny znak, z wyłączeniem separatora `/`.

**[ENG]:** Matches exactly one arbitrary character, excluding the `/` separator. | +| `{a,b}` | Brace Expansion | **[POL]:** Middleware. Klonuje i rozwija wzorzec na oddzielne ścieżki przed kompilacją. Z `src/{a,b}.rs` generuje `src/a.rs` oraz `src/b.rs`. Obsługuje rekurencję.

**[ENG]:** Middleware. Clones and expands the pattern into separate paths before compilation. From `src/{a,b}.rs` it generates `src/a.rs` and `src/b.rs`. Supports recursion. | +| `[a-z]` | Character Class | **[POL]:** Dopasowuje dokładnie jeden znak z podanego zakresu lub zbioru.

**[ENG]:** Matches exactly one character from the specified range or set. | +| `[!a-z]` | Negated Class | **[POL]:** Dopasowuje jeden znak, który nie należy do podanego zbioru.

**[ENG]:** Matches one character that does not belong to the specified set. | +| `\` | Escape Character | **[POL]:** Traktuje następny znak dosłownie (np. `\.` dopasowuje kropkę, a nie dowolny znak).

**[ENG]:** Treats the next character literally (e.g. `\.` matches a dot, not an arbitrary character). | + +--- + +### 2. Kotwiczenie i Typowanie (Target Typing) + +Bariery logiczne analizujące surowy wzorzec w celu precyzyjnego ustalenia docelowych obiektów w systemie plików. + +| Wzorzec | Nazwa techniczna | Zachowanie silnika | +| :--- | :--- | :--- | +| `./...` | Root Anchor | **[POL]:** Wymusza szukanie ścieżki dokładnie od korzenia skanowanego środowiska.

**[ENG]:** Enforces path searching exactly from the root of the scanned environment. | +| `.../` | Directory Target | **[POL]:** Wzorzec kończący się ukośnikiem. Natychmiast wyklucza pliki. Dopasowuje wyłącznie katalogi.

**[ENG]:** Pattern ending with a slash. Instantly excludes files. Matches directories exclusively. | +| *(brak)* | File Target | **[POL]:** Bariera logiczna. Jeśli znormalizowany wzorzec nie kończy się na `/` ani na `**`, silnik traktuje go jako wzorzec plikowy i odrzuca badane katalogi.

**[ENG]:** Logical barrier. If the normalised pattern does not end with `/` or `**`, the engine treats it as a file pattern and rejects examined directories. | + +--- + +### 3. Flagi Relacji Strukturalnych (Rdzeń Logiki Biznesowej) + +Autorskie modyfikatory zachowania bazujące na stanie globalnego środowiska plików (`env`), weryfikujące istnienie zależności w strukturze na dysku. + +| Flaga | Nazwa techniczna | Zasada działania weryfikatora kontekstowego | +| :--- | :--- | :--- | +| **`@`** | Sibling Requirement
*(Relacja Obustronna)* | **[POL]:** Dla plików: Wymaga istnienia katalogu o tej samej nazwie rdzennej (np. `A.rs` wymaga `A/`). Dla katalogów: Wymaga istnienia pliku o tej samej nazwie rdzennej obok (np. `A/` wymaga `A.rs` lub `.A.rs`).

**[ENG]:** For files: Requires a directory with the same core name (e.g. `A.rs` requires `A/`). For directories: Requires a file with the same core name (e.g. `A/` requires `A.rs` or `.A.rs`). | +| **`$`** | Orphan Requirement
*(Relacja Jednostronna)* | **[POL]:** Działa tylko na wzorce plikowe. Wymaga, aby dopasowany plik posiadał odpowiadający mu katalog (podobnie jak `@`). Nie nakłada żadnych restrykcji na same katalogi.

**[ENG]:** Acts on file patterns only. Requires the matched file to have a corresponding directory (similar to `@`). Does not impose any restrictions on directories themselves. | +| **`+`** | Deep Root Authorization
*(Rekurencja Autoryzowana)* | **[POL]:** Działa w symbiozie z `@`. Weryfikuje, czy korzeń modułu zdefiniowany we wzorcu posiada autoryzowaną relację `@` (plik + katalog). Jeśli korzeń jest poprawny, silnik akceptuje wszystko w jego poddrzewie.

**[ENG]:** Works in symbiosis with `@`. Verifies if the module root defined in the pattern possesses an authorised `@` relation (file + directory). If the root is valid, the engine accepts everything within its subtree. | + + +### API Wewnętrzne (Core API) + +--- + +### 1. Skanowanie i Normalizacja (`core::path_getter`) + +Moduł generujący bazowy zbiór znormalizowanych ścieżek wejściowych. + +| Sygnatura | Opis Techniczny | +| :--- | :--- | +| `get_paths>(dir_path: P) -> Vec` | **[POL]:** Wykonuje rekurencyjny odczyt drzewa katalogów (pomijając głębokość `0` oraz dowiązania symboliczne). Normalizuje ścieżki: wymusza prefiks `./`, unifikuje separatory na `/` i dokleja `/` na końcu katalogów.

**[ENG]:** Performs a recursive read of the directory tree (ignoring depth `0` and symbolic links). Normalises paths: enforces `./` prefix, unifies separators to `/`, and appends `/` to directories. | + +--- + +### 2. Klasyfikacja Wizualna (`core::path_class`) + +Moduł przypisujący graficzne identyfikatory do znormalizowanych ścieżek. + +| Sygnatura | Opis Techniczny | +| :--- | :--- | +| `get_icon_for_path(path: &str) -> &'static str` | **[POL]:** Rozpoznaje typ obiektu na podstawie końcowego `/` (katalog) oraz prefiksu `.` w nazwie rdzennej (ukryty). Zwraca statyczne emoji: `📁` (folder), `🗃️` (ukryty folder), `📄` (plik), `⚙️` (ukryty plik).

**[ENG]:** Identifies the object type based on the trailing `/` (directory) and the `.` prefix in the core name (hidden). Returns static emojis: `📁` (folder), `🗃️` (hidden folder), `📄` (file), `⚙️` (hidden file). | + +--- + +### 3. Narzędzia Dopasowywania i Sortowania (`core::path_matcher::matcher_utils`) + +Narzędzia transformujące wzorce oraz porządkujące kolekcje ścieżek wejściowych. + +| Sygnatura / Typ | Opis Techniczny | +| :--- | :--- | +| `expand_braces(pattern: &str) -> Vec` | **[POL]:** Middleware rekurencyjnie rozwijający klamry (np. `{a,b}`) względem przecinków na niezależne wzorce tekstowe.

**[ENG]:** Middleware recursively expanding braces (e.g. `{a,b}`) separated by commas into independent text patterns. | +| `SortStrategy` (Enum) | **[POL]:** 11 wariantów określających algorytm sortowania: `None`, `Az`, `Za`, `AzFileFirst`, `ZaFileFirst`, `AzDirFirst`, `ZaDirFirst` oraz 4 strategie typu `Merge` (`AzFileFirstMerge`, itd.) grupujące pary plik-katalog w bloki logiczne.

**[ENG]:** 11 variants defining the sorting algorithm: `None`, `Az`, `Za`, `AzFileFirst`, `ZaFileFirst`, `AzDirFirst`, `ZaDirFirst` and 4 `Merge` strategies (`AzFileFirstMerge`, etc.) grouping file-directory pairs into logical blocks. | +| `get_merge_key(path: &str) -> &str` | **[POL]:** Funkcja wewnętrzna ekstrahująca klucz do sortowania logicznego `Merge`. Obcina ukośniki i rozszerzenia plików, chroniąc ukryte pliki konfiguracyjne przed błędnym ucięciem nazwy.

**[ENG]:** Internal function extracting the key for `Merge` logical sorting. Trims slashes and file extensions while protecting hidden config files from erroneous name truncation. | +| `sort_paths>(paths: &mut Vec, strategy: SortStrategy)` | **[POL]:** Wykonuje sortowanie struktury `Vec` w miejscu (in-place) przy użyciu zdefiniowanej strategii porównawczej.

**[ENG]:** Performs an in-place sort of a `Vec` structure using the defined comparative strategy. | + +--- + +### 4. Niskopoziomowy Silnik Dopasowujący (`core::path_matcher::matcher`) + +Kompiluje i ewaluuje pojedyncze wzorce tekstowe, wymuszając reguły strukturalne. + +| Sygnatura Metody (`PathMatcher`) | Opis Techniczny | +| :--- | :--- | +| `new(pattern: &str, case_sensitive: bool) -> Result` | **[POL]:** Konstruktor. Wyciąga flagi kontrolne: `@` (para plik-folder), `$` (sierota), `+` (rekurencyjne zacienianie). Oblicza `base_name`. Ustanawia barierę `targets_file` na wyczyszczonym ze znaków kontrolnych wzorcu `p`, weryfikując brak ukośnika lub `**` na końcu. Kompiluje ostateczny Regex.

**[ENG]:** Constructor. Extracts control flags: `@` (file-directory pair), `$` (orphan), `+` (recursive shadowing). Calculates `base_name`. Establishes the `targets_file` barrier on the cleaned pattern `p`, verifying the absence of a trailing slash or `**`. Compiles the final Regex. | +| `is_match(&self, path: &str, env: &HashSet<&str>) -> bool` | **[POL]:** Ewaluator logiczny. Odrzuca katalogi, jeśli aktywna jest bariera `targets_file`. Po weryfikacji wyrażeniem regularnym wymusza zależności względem globalnego zbioru `env` (sprawdza wymogi `@` i `$`).

**[ENG]:** Logical evaluator. Rejects directories if the `targets_file` barrier is active. Post-regex validation enforces dependencies against the global `env` set (validating `@` and `$` requirements). | +| `evaluate(&self, paths: I, env: &HashSet<&str>, strategy: SortStrategy, on_match: OnMatch, on_mismatch: OnMismatch)` | **[POL]:** Kolektuje ścieżki na wektory `matched` i `mismatched`, poddaje je sortowaniu zgodnie z parametrem `strategy`, a następnie w pętli wywołuje wstrzyknięte domknięcia dla każdego rekordu.

**[ENG]:** Collects paths into `matched` and `mismatched` vectors, sorts them according to the `strategy` parameter, and then loops to invoke injected closures for each record. | +| `check_authorized_root(&self, path: &str, env: &HashSet<&str>) -> bool` | **[POL]:** Metoda wewnętrzna trybu `deep` (`+`). Waliduje, czy w zadanej strukturze nadrzędnej istnieje poprawny rdzeń modułu (katalog i przypisany plik), bazując na wyliczonym `base_name`.

**[ENG]:** Internal method for `deep` mode (`+`). Validates whether a valid module root (directory and assigned file) exists in the parent structure, based on the computed `base_name`. | + +--- + +### 5. Koordynator Dopasowań (`core::path_matcher::matchers`) + +Zarządza kolekcją pojedynczych silników, agregując ich zachowanie w model logiczny OR. + +| Sygnatura Metody (`PathMatchers`) | Opis Techniczny | +| :--- | :--- | +| `new(patterns: I, case_sensitive: bool) -> Result` | **[POL]:** Przepuszcza każdy wejściowy ciąg znaków przez warstwę middleware `expand_braces`, po czym kompiluje rozszerzone wzorce do obiektów `PathMatcher`.

**[ENG]:** Passes each input string through the `expand_braces` middleware layer, then compiles the expanded patterns into `PathMatcher` objects. | +| `is_match(&self, path: &str, env: &HashSet<&str>) -> bool` | **[POL]:** Iteruje po zainicjalizowanych instancjach `PathMatcher`. Zwraca `true`, gdy zidentyfikuje pierwsze poprawne dopasowanie we wzorcu logicznym OR.

**[ENG]:** Iterates over initialised `PathMatcher` instances. Returns `true` upon identifying the first valid match in a logical OR pattern. | +| `evaluate(...)` | **[POL]:** Analogiczna logika segregacji i sortowania wyników jak w `PathMatcher::evaluate`, lecz operująca na całym kontenerze wzorców równolegle.

**[ENG]:** Identical segregation and result sorting logic as in `PathMatcher::evaluate`, but operating concurrently across the entire pattern container. | diff --git a/src/addon.rs b/src/addon.rs new file mode 100644 index 0000000..5198bca --- /dev/null +++ b/src/addon.rs @@ -0,0 +1,3 @@ +pub mod time_tag; + +pub use time_tag::{NaiveDate, NaiveTime, TimeTag}; diff --git a/src/addon/time_tag.rs b/src/addon/time_tag.rs new file mode 100644 index 0000000..a82de11 --- /dev/null +++ b/src/addon/time_tag.rs @@ -0,0 +1,78 @@ +// [EN]: Functions for creating consistent date and time stamps. +// [PL]: Funkcje do tworzenia spójnych sygnatur daty i czasu. + +use chrono::{Datelike, Local, Timelike, Weekday}; +pub use chrono::{NaiveDate, NaiveTime}; + +/// [EN]: Utility struct for generating consistent time tags. +/// [PL]: Struktura narzędziowa do generowania spójnych sygnatur czasowych. +pub struct TimeTag; + +impl TimeTag { + /// [EN]: Generates a time_tag for the current local time. + /// [PL]: Generuje time_tag dla obecnego, lokalnego czasu. + #[must_use] + pub fn now() -> String { + let now = Local::now(); + Self::format(now.date_naive(), now.time()) + } + + /// [EN]: Generates a time_tag for a specific provided date and time. + /// [PL]: Generuje time_tag dla konkretnej, podanej daty i czasu. + #[must_use] + pub fn custom(date: NaiveDate, time: NaiveTime) -> String { + Self::format(date, time) + } + + // [EN]: Private function that performs manual string construction (DRY principle). + // [PL]: PRYWATNA funkcja, która wykonuje ręczne budowanie ciągu znaków (zasada DRY). + fn format(date: NaiveDate, time: NaiveTime) -> String { + let year = date.year(); + let quarter = ((date.month() - 1) / 3) + 1; + + let weekday = match date.weekday() { + Weekday::Mon => "Mon", + Weekday::Tue => "Tue", + Weekday::Wed => "Wed", + Weekday::Thu => "Thu", + Weekday::Fri => "Fri", + Weekday::Sat => "Sat", + Weekday::Sun => "Sun", + }; + + let month = match date.month() { + 1 => "Jan", + 2 => "Feb", + 3 => "Mar", + 4 => "Apr", + 5 => "May", + 6 => "Jun", + 7 => "Jul", + 8 => "Aug", + 9 => "Sep", + 10 => "Oct", + 11 => "Nov", + 12 => "Dec", + _ => unreachable!(), + }; + + let millis = time.nanosecond() / 1_000_000; + + // [EN]: Format: YYYYQn Dnnn Wnn _ Day DD Mon _ HH MM SS mmm + // [PL]: Format: RRRRQn Dnnn Wnn _ Dzień DD Miesiąc _ GG MM SS mmm + format!( + "{}Q{}D{:03}W{:02}_{}{:02}{}_{:02}{:02}{:02}{:03}", + year, + quarter, + date.ordinal(), + date.iso_week().week(), + weekday, + date.day(), + month, + time.hour(), + time.minute(), + time.second(), + millis + ) + } +} diff --git a/src/cli/args.rs b/src/cli/args.rs deleted file mode 100644 index 6141650..0000000 --- a/src/cli/args.rs +++ /dev/null @@ -1,256 +0,0 @@ -// Plik: src/cli/args.rs -use clap::{Args, Parser, Subcommand, ValueEnum}; - -#[derive(Parser, Debug)] -#[command(name = "cargo", bin_name = "cargo")] -pub enum CargoCli { - /// Narzędzie do wizualizacji struktury projektu i generowania dokumentacji Markdown - Plot(PlotArgs), -} - -#[derive(Args, Debug)] -#[command( - author, - version, - about = "cargo-plot - Twój szwajcarski scyzoryk do dokumentacji w Rust" -)] -pub struct PlotArgs { - #[command(subcommand)] - pub command: Option, -} - -#[derive(Subcommand, Debug)] -pub enum Commands { - /// Rysuje kolorowe drzewo plików i folderów w terminalu - Tree(TreeArgs), - /// Generuje kompletny raport Markdown ze struktury i zawartości plików - Doc(DocArgs), - /// Generuje unikalny, ujednolicony znacznik czasu - Stamp(StampArgs), - /// Kopiuje skompilowane binarki Rusta do folderu dystrybucyjnego (dist/) - DistCopy(DistCopyArgs), -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -pub enum CliUnitSystem { - Decimal, - Binary, - Both, // Jeśli zdecydujemy się obsłużyć ten tryb później - None, -} - -#[derive(Args, Debug, Clone)] -pub struct SharedTaskArgs { - /// Ścieżka bazowa do rozpoczęcia skanowania - #[arg(short, long, default_value = ".")] - pub path: String, - - /// Wyłącza domyślne ignorowanie folderów technicznych (.git, target, node_modules, itp.) - #[arg(long)] - pub no_default_excludes: bool, - - /// Wzorce Glob ignorujące ścieżki i foldery (np. "./target/") - #[arg(short, long)] - pub exclude: Vec, - - /// Rygorystyczna biała lista - ignoruje wszystko, co do niej nie pasuje - #[arg(short, long)] - pub include_only: Vec, - - /// Filtr wyświetlający wyłącznie wybrane pliki (np. "*.rs") - #[arg(short, long)] - pub filter_files: Vec, - - /// Tryb wyświetlania węzłów - #[arg(short, long, value_enum, default_value_t = OutputType::All)] - pub r#type: OutputType, - - /// Tryb Inline Multi-Task (np. loc=.,inc=Cargo.toml,out=files) - #[arg(long)] - pub task: Vec, - - /// Ścieżka do zewnętrznego pliku konfiguracyjnego (.toml) - #[arg(long)] - pub tasks: Option, - - /// System jednostek wagi plików - #[arg(short = 'w', long = "weight", value_enum, default_value_t = CliUnitSystem::None)] - pub weight_system: CliUnitSystem, - - /// Szerokość całkowita formatowania liczby wagi (domyślnie 5) - #[arg(long = "weight-precision", default_value = "5")] - pub weight_precision: usize, - - /// Czy ukryć wagi dla folderów - #[arg(long = "no-dir-weight")] - pub no_dir_weight: bool, - - /// Czy ukryć wagi dla plików - #[arg(long = "no-file-weight")] - pub no_file_weight: bool, - - /// Jeśli użyto, waga folderu to jego prawdziwy rozmiar na dysku, a nie tylko suma wyszukanych plików - #[arg(long = "real-dir-weight")] - pub real_dir_weight: bool, -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -pub enum OutputType { - Dirs, - Files, - All, -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -pub enum SortMethod { - DirsFirst, - FilesFirst, - Alpha, -} - -#[derive(Args, Debug)] -pub struct TreeArgs { - #[command(flatten)] - pub shared: SharedTaskArgs, - - /// Sposób sortowania węzłów drzewa - #[arg(short, long, value_enum, default_value_t = SortMethod::Alpha)] - pub sort: SortMethod, - - /// Zapisuje wynikowe drzewo do pliku Markdown (np. drzewo.md) - #[arg(long = "out-file")] - pub out_file: Option, - - /// Wymusza wydruk drzewa w konsoli, nawet jeśli podano --out-file (zapisz i wyświetl) - #[arg(long = "print-console")] - pub print_console: bool, - - /// Pozycja znaku wodnego z informacją o cargo-plot (tylko w zapisanym pliku) - #[arg(long, value_enum, default_value_t = WatermarkPosition::Last)] - pub watermark: WatermarkPosition, - - /// Wyświetla użytą komendę CLI na początku pliku - #[arg(long = "print-command")] - pub print_command: bool, - - /// Dodaje unikalny znacznik czasu do nazwy pliku wyjściowego - #[arg(long, visible_alias = "sufix-stamp")] - pub suffix_stamp: bool, - - /// Główny tytuł dokumentu w zapisanym pliku - #[arg(long, default_value = "RAPORT")] - pub title_file: String, - - /// Dodaje ścieżkę pliku do głównego tytułu dokumentu - #[arg(long)] - pub title_file_with_path: bool, -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -pub enum IdStyle { - Tag, - Num, - None, -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -pub enum InsertTreeMethod { - DirsFirst, - FilesFirst, - None, -} - -#[derive(ValueEnum, Clone, Debug, PartialEq)] -pub enum WatermarkPosition { - First, - Last, - None, -} - -#[derive(Args, Debug)] -pub struct DocArgs { - #[command(flatten)] - pub shared: SharedTaskArgs, - - /// Ścieżka do katalogu wyjściowego, w którym zostaną zapisane raporty - #[arg(long, default_value = "doc")] - pub out_dir: String, - - /// Bazowa nazwa pliku wyjściowego - #[arg(short, long, default_value = "code")] - pub out: String, - - /// Tryb symulacji (nie modyfikuje plików na dysku) - #[arg(long, visible_alias = "simulate")] - pub dry_run: bool, - - /// Formatowanie identyfikatorów plików w raporcie - #[arg(short = 'I', long, value_enum, default_value_t = IdStyle::Tag)] - pub id_style: IdStyle, - - /// Sposób rzutowania drzewa struktury na początku raportu - #[arg(short = 'T', long, value_enum, default_value_t = InsertTreeMethod::FilesFirst)] - pub insert_tree: InsertTreeMethod, - - /// Pozycja znaku wodnego z informacją o cargo-plot - #[arg(long, value_enum, default_value_t = WatermarkPosition::Last)] - pub watermark: WatermarkPosition, - - /// Wyświetla użytą komendę CLI na początku pliku - #[arg(long = "print-command")] - pub print_command: bool, - - /// Dodaje unikalny znacznik czasu do nazwy pliku wyjściowego - #[arg(long, visible_alias = "sufix-stamp")] - pub suffix_stamp: bool, - - /// Główny tytuł dokumentu w zapisanym pliku - #[arg(long, default_value = "RAPORT")] - pub title_file: String, - - /// Dodaje ścieżkę pliku do głównego tytułu dokumentu - #[arg(long)] - pub title_file_with_path: bool, -} - -#[derive(Args, Debug)] -pub struct StampArgs { - /// Data w formacie RRRR-MM-DD - #[arg(short, long)] - pub date: Option, - - /// Czas w formacie GG:MM:SS (wymaga również flagi --date) - #[arg(short, long)] - pub time: Option, - - /// Milisekundy. Używane tylko w połączeniu z flagą --time - #[arg(short, long, default_value = "000")] - pub millis: String, -} - -#[derive(Args, Debug)] -pub struct DistCopyArgs { - /// Nazwy plików do skopiowania (domyślnie: automatycznie kopiuje WSZYSTKIE binarki) - #[arg(short, long)] - pub bin: Vec, - - /// Ścieżka do technicznego folderu kompilacji - #[arg(long, default_value = "./target")] - pub target_dir: String, - - /// Ścieżka do docelowego folderu dystrybucyjnego - #[arg(long, default_value = "./dist")] - pub dist_dir: String, - - /// Bezpiecznie czyści stary folder dystrybucyjny przed rozpoczęciem kopiowania - #[arg(long)] - pub clear: bool, - - /// Zabezpiecza przed nadpisaniem istniejących plików - #[arg(long)] - pub no_overwrite: bool, - - /// Tryb symulacji (nic nie tworzy i nic nie usuwa na dysku) - #[arg(long, visible_alias = "simulate")] - pub dry_run: bool, -} diff --git a/src/cli/dist.rs b/src/cli/dist.rs deleted file mode 100644 index dd0fe15..0000000 --- a/src/cli/dist.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Plik: src/cli/dist.rs -use crate::cli::args::DistCopyArgs; -use lib::fn_copy_dist::{DistConfig, copy_dist}; - -pub fn handle_dist_copy(args: DistCopyArgs) { - let bin_refs: Vec<&str> = args.bin.iter().map(|s| s.as_str()).collect(); - let config = DistConfig { - target_dir: &args.target_dir, - dist_dir: &args.dist_dir, - binaries: bin_refs, - clear_dist: args.clear, - overwrite: !args.no_overwrite, - dry_run: args.dry_run, - }; - - match copy_dist(&config) { - Ok(files) => { - for (s, d) in files { - println!(" [+] {} -> {}", s.display(), d.display()); - } - } - Err(e) => eprintln!("[-] Błąd dystrybucji: {}", e), - } -} diff --git a/src/cli/doc.rs b/src/cli/doc.rs deleted file mode 100644 index e9603dc..0000000 --- a/src/cli/doc.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Plik: src/cli/doc.rs -use crate::cli::args::{DocArgs, IdStyle, InsertTreeMethod}; -use crate::cli::utils::{build_weight_config, collect_tasks}; -use lib::fn_doc_gen::generate_docs; -use lib::fn_doc_models::DocTask; -use lib::fn_filespath::filespath; - -pub fn handle_doc(args: DocArgs) { - let tasks = collect_tasks(&args.shared); - let w_cfg = build_weight_config(&args.shared); - - // Klonujemy wywołanie z konsoli, aby umieścić je w pliku - let cmd_str = if args.print_command { - Some(std::env::args().collect::>().join(" ")) - } else { - None - }; - - let watermark_str = match args.watermark { - crate::cli::args::WatermarkPosition::First => "first", - crate::cli::args::WatermarkPosition::Last => "last", - crate::cli::args::WatermarkPosition::None => "none", - }; - - let doc_task = DocTask { - output_filename: &args.out, - insert_tree: match args.insert_tree { - InsertTreeMethod::DirsFirst => "dirs-first", - InsertTreeMethod::None => "with-out", - _ => "files-first", - }, - id_style: match args.id_style { - IdStyle::Num => "id-num", - IdStyle::None => "id-non", - _ => "id-tag", - }, - tasks, - weight_config: w_cfg, - watermark: watermark_str, - command_str: cmd_str, - suffix_stamp: args.suffix_stamp, - title_file: &args.title_file, - title_file_with_path: args.title_file_with_path, - }; - - if args.dry_run { - println!( - "[!] SYMULACJA: Wykryto {} plików do przetworzenia.", - filespath(&doc_task.tasks).len() - ); - return; - } - - if let Err(e) = generate_docs(vec![doc_task], &args.out_dir) { - eprintln!("[-] Błąd generowania raportu w '{}': {}", args.out_dir, e); - } -} diff --git a/src/cli/mod.rs b/src/cli/mod.rs deleted file mode 100644 index d80b6c8..0000000 --- a/src/cli/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Plik: src/cli/mod.rs -pub mod args; -mod dist; -mod doc; -mod stamp; -mod tree; -mod utils; - -use args::Commands; - -pub fn run_command(cmd: Commands) { - match cmd { - Commands::Tree(args) => tree::handle_tree(args), - Commands::Doc(args) => doc::handle_doc(args), - Commands::Stamp(args) => stamp::handle_stamp(args), - Commands::DistCopy(args) => dist::handle_dist_copy(args), - } -} diff --git a/src/cli/stamp.rs b/src/cli/stamp.rs deleted file mode 100644 index ba5af30..0000000 --- a/src/cli/stamp.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Plik: src/cli/stamp.rs -use crate::cli::args::StampArgs; -use lib::fn_datestamp::{NaiveDate, NaiveTime, datestamp, datestamp_now}; - -pub fn handle_stamp(args: StampArgs) { - if let (Some(d_str), Some(t_str)) = (args.date, args.time) { - let d = NaiveDate::parse_from_str(&d_str, "%Y-%m-%d").expect("Błędny format daty"); - let t = NaiveTime::parse_from_str(&format!("{}.{}", t_str, args.millis), "%H:%M:%S%.3f") - .expect("Błędny format czasu"); - println!("{}", datestamp(d, t)); - } else { - println!("{}", datestamp_now()); - } -} diff --git a/src/cli/tree.rs b/src/cli/tree.rs deleted file mode 100644 index 6dd8289..0000000 --- a/src/cli/tree.rs +++ /dev/null @@ -1,100 +0,0 @@ -// Plik: src/cli/tree.rs -use crate::cli::args::{SortMethod, TreeArgs}; -use crate::cli::utils::{build_weight_config, collect_tasks}; -use lib::fn_filespath::filespath; -use lib::fn_filestree::filestree; -use lib::fn_plotfiles::plotfiles_cli; - -pub fn handle_tree(args: TreeArgs) { - let tasks = collect_tasks(&args.shared); - let paths = filespath(&tasks); - - let sort_str = match args.sort { - SortMethod::DirsFirst => "dirs-first", - SortMethod::FilesFirst => "files-first", - _ => "alpha", - }; - - // POBIERAMY KONFIGURACJĘ WAG NA PODSTAWIE FLAG CLI - let w_cfg = build_weight_config(&args.shared); - - let nodes = filestree(paths, sort_str, &w_cfg); - - // ========================================== - // NOWA LOGIKA WYDRUKU / ZAPISU DO PLIKU - // ========================================== - - // 1. Zawsze drukuj do konsoli, chyba że użytkownik podał plik i NIE poprosił o konsolę - let print_to_console = args.out_file.is_none() || args.print_console; - - if print_to_console { - println!("{}", plotfiles_cli(&nodes, "", None)); - } - - // 2. Zapisz do pliku, jeśli podano argument --out-file - // 2. Zapisz do pliku, jeśli podano argument --out-file - if let Some(out_file) = args.out_file { - let stamp = lib::fn_datestamp::datestamp_now(); - - // Magia ucinania rozszerzenia (np. z "plik.md" robimy "plik__STAMP.md") - let final_out_file = if args.suffix_stamp { - let path = std::path::Path::new(&out_file); - let stem = path.file_stem().unwrap_or_default().to_string_lossy(); - let ext = path.extension().unwrap_or_default().to_string_lossy(); - let parent = path.parent().unwrap_or_else(|| std::path::Path::new("")); - - let new_name = if ext.is_empty() { - format!("{}__{}", stem, stamp) - } else { - format!("{}__{}.{}", stem, stamp, ext) - }; - - let pb = parent.join(new_name); - if parent.as_os_str().is_empty() { - pb.to_string_lossy().into_owned() - } else { - pb.to_string_lossy().replace('\\', "/") - } - } else { - out_file.clone() - }; - - let mut content = String::new(); - - // ========================================== - // LOGIKA TYTUŁU DLA TREE - // ========================================== - let mut title_line = format!("# {}", args.title_file); - if !args.suffix_stamp { - title_line.push_str(&format!(" {}", stamp)); - } - if args.title_file_with_path { - title_line.push_str(&format!(" ({})", final_out_file)); - } - content.push_str(&title_line); - content.push_str("\n\n"); - // ========================================== - - let watermark_text = "> 🚀 Wygenerowano przy użyciu [cargo-plot](https://crates.io/crates/cargo-plot) | Źródło: [GitHub](https://github.com/j-Cis/cargo-plot)\n\n"; - - if args.watermark == crate::cli::args::WatermarkPosition::First { - content.push_str(watermark_text); - } - - if args.print_command { - let cmd = std::env::args().collect::>().join(" "); - content.push_str(&format!("**Wywołana komenda:**\n```bash\n{}\n```\n\n", cmd)); - } - - let txt = lib::fn_plotfiles::plotfiles_txt(&nodes, "", None); - content.push_str(&format!("```text\n{}\n```\n", txt)); - - if args.watermark == crate::cli::args::WatermarkPosition::Last { - content.push_str("\n---\n"); - content.push_str(watermark_text); - } - - std::fs::write(&final_out_file, content).unwrap(); - println!(" [+] Sukces! Drzewo zapisano do pliku: {}", final_out_file); - } -} diff --git a/src/cli/utils.rs b/src/cli/utils.rs deleted file mode 100644 index f0eb61e..0000000 --- a/src/cli/utils.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Plik: src/cli/utils.rs -use crate::cli::args::{CliUnitSystem, OutputType, SharedTaskArgs}; -use lib::fn_filespath::Task; -use lib::fn_weight::{UnitSystem, WeightConfig}; - -pub fn collect_tasks(args: &SharedTaskArgs) -> Vec> { - let mut tasks = Vec::new(); - - for t_str in &args.task { - tasks.push(parse_inline_task(t_str)); - } - - if tasks.is_empty() && args.tasks.is_none() { - let mut excludes: Vec<&str> = args.exclude.iter().map(|s| s.as_str()).collect(); - if !args.no_default_excludes { - excludes.extend(vec![ - ".git/", - "target/", - "node_modules/", - ".vs/", - ".idea/", - ".vscode/", - ]); - } - - tasks.push(Task { - path_location: &args.path, - path_exclude: excludes, - path_include_only: args.include_only.iter().map(|s| s.as_str()).collect(), - filter_files: args.filter_files.iter().map(|s| s.as_str()).collect(), - output_type: match args.r#type { - OutputType::Dirs => "dirs", - OutputType::Files => "files", - _ => "dirs_and_files", - }, - }); - } - - tasks -} - -fn parse_inline_task(input: &str) -> Task<'_> { - let mut task = Task::default(); - let parts = input.split(','); - for part in parts { - let kv: Vec<&str> = part.split('=').collect(); - if kv.len() == 2 { - match kv[0] { - "loc" => task.path_location = kv[1], - "inc" => task.path_include_only.push(kv[1]), - "exc" => task.path_exclude.push(kv[1]), - "fil" => task.filter_files.push(kv[1]), - "out" => task.output_type = kv[1], - _ => {} - } - } - } - task -} - -/// Konwertuje parametry z linii poleceń na strukturę konfiguracyjną API -pub fn build_weight_config(args: &SharedTaskArgs) -> WeightConfig { - let system = match args.weight_system { - CliUnitSystem::Decimal => UnitSystem::Decimal, - CliUnitSystem::Binary => UnitSystem::Binary, - CliUnitSystem::Both => UnitSystem::Both, - CliUnitSystem::None => UnitSystem::None, - }; - - WeightConfig { - system, - precision: args.weight_precision.max(3), // Minimum 3 znaki na liczbę - show_for_dirs: !args.no_dir_weight, - show_for_files: !args.no_file_weight, - dir_sum_included: !args.real_dir_weight, // Domyślnie sumujemy tylko ujęte w filtrach - } -} diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..f18f4fd --- /dev/null +++ b/src/core.rs @@ -0,0 +1,6 @@ +pub mod file_stats; +pub mod path_matcher; +pub mod path_store; +pub mod path_view; +pub mod patterns_expand; +pub mod save; diff --git a/src/core/file_stats.rs b/src/core/file_stats.rs new file mode 100644 index 0000000..d911f51 --- /dev/null +++ b/src/core/file_stats.rs @@ -0,0 +1,35 @@ +// use std::fs; +use std::path::{Path, PathBuf}; +pub mod weight; + +use self::weight::get_path_weight; + +/// [POL]: Struktura przechowująca metadane (statystyki) pliku lub folderu. +#[derive(Debug, Clone)] +pub struct FileStats { + pub path: String, // Oryginalna ścieżka relatywna (np. "src/main.rs") + pub absolute: PathBuf, // Pełna ścieżka absolutna na dysku + pub weight_bytes: u64, // Rozmiar w bajtach + + // ⚡ Miejsce na przyszłe parametry: + // pub created_at: Option, + // pub modified_at: Option, +} + +impl FileStats { + /// [POL]: Pobiera statystyki pliku bezpośrednio z dysku. + pub fn fetch(path: &str, entry_absolute: &str) -> Self { + let absolute = Path::new(entry_absolute).join(path); + + let weight_bytes = get_path_weight(&absolute, true); + // let weight_bytes = fs::metadata(&absolute) + // .map(|m| m.len()) + // .unwrap_or(0); + + Self { + path: path.to_string(), + absolute, + weight_bytes, + } + } +} diff --git a/src/lib/fn_weight.rs b/src/core/file_stats/weight.rs similarity index 54% rename from src/lib/fn_weight.rs rename to src/core/file_stats/weight.rs index fb07511..9f9c335 100644 --- a/src/lib/fn_weight.rs +++ b/src/core/file_stats/weight.rs @@ -1,10 +1,13 @@ +// [ENG]: Logic for calculating and formatting file and directory weights. +// [POL]: Logika obliczania i formatowania wag plików oraz folderów. + use std::fs; use std::path::Path; #[derive(Debug, Clone, Copy, PartialEq)] pub enum UnitSystem { - Decimal, // 1000^n (kB, MB...) - Binary, // 1024^n (KiB, MiB...) + Decimal, + Binary, Both, None, } @@ -12,10 +15,10 @@ pub enum UnitSystem { #[derive(Debug, Clone)] pub struct WeightConfig { pub system: UnitSystem, - pub precision: usize, // Całkowita szerokość pola "xxxxx" (min 3) + pub precision: usize, pub show_for_files: bool, pub show_for_dirs: bool, - pub dir_sum_included: bool, // true = tylko uwzględnione, false = rzeczywista waga folderu + pub dir_sum_included: bool, } impl Default for WeightConfig { @@ -30,67 +33,7 @@ impl Default for WeightConfig { } } -/// Główna funkcja formatująca wagę do postaci [qq xxxxx] -pub fn format_weight(bytes: u64, config: &WeightConfig) -> String { - if config.system == UnitSystem::None { - return String::new(); - } - - let (base, units) = match config.system { - UnitSystem::Binary => (1024.0_f64, vec!["B", "KiB", "MiB", "GiB", "TiB", "PiB"]), - _ => (1000.0_f64, vec!["B", "kB", "MB", "GB", "TB", "PB"]), - }; - - if bytes == 0 { - return format!( - "[{:>3} {:>width$}] ", - units[0], - "0", - width = config.precision - ); - } - - let bytes_f = bytes as f64; - let exp = (bytes_f.ln() / base.ln()).floor() as usize; - let exp = exp.min(units.len() - 1); - let value = bytes_f / base.powi(exp as i32); - let unit = units[exp]; - - // Formatowanie liczby do stałej szerokości "xxxxx" - let formatted_value = format_value_with_precision(value, config.precision); - - format!("[{:>3} {}] ", unit, formatted_value) -} - -fn format_value_with_precision(value: f64, width: usize) -> String { - // Sprawdzamy ile cyfr ma część całkowita - let integer_part = value.floor() as u64; - let integer_str = integer_part.to_string(); - let int_len = integer_str.len(); - - if int_len >= width { - // Jeśli sama liczba całkowita zajmuje całe miejsce lub więcej - return integer_str[..width].to_string(); - } - - // Obliczamy ile miejsc po przecinku nam zostało (width - int_len - 1 dla kropki) - let available_precision = if width > int_len + 1 { - width - int_len - 1 - } else { - 0 - }; - - let formatted = format!("{:.1$}", value, available_precision); - - // Na wypadek zaokrągleń (np. 99.99 -> 100.0), przycinamy do width - if formatted.len() > width { - formatted[..width].trim_end_matches('.').to_string() - } else { - format!("{:>width$}", formatted, width = width) - } -} - -/// Pobiera wagę pliku lub folderu (rekurencyjnie) +/// [POL]: Pobiera wagę ścieżki (plik lub folder rekurencyjnie). pub fn get_path_weight(path: &Path, sum_included_only: bool) -> u64 { let metadata = match fs::metadata(path) { Ok(m) => m, @@ -101,19 +44,20 @@ pub fn get_path_weight(path: &Path, sum_included_only: bool) -> u64 { return metadata.len(); } + // ⚡ Jeśli sum_included_only jest false (flaga -a), liczymy rekurencyjnie fizyczny rozmiar if metadata.is_dir() && !sum_included_only { - // Rzeczywista waga folderu na dysku return get_dir_size(path); } - 0 // Jeśli liczymy tylko sumę plików, bazowo folder ma 0 (sumowanie nastąpi w drzewie) + 0 } +/// [POL]: Prywatny pomocnik do liczenia rozmiaru folderu na dysku. fn get_dir_size(path: &Path) -> u64 { fs::read_dir(path) .map(|entries| { entries - .filter_map(|e| e.ok()) + .filter_map(Result::ok) .map(|e| { let p = e.path(); if p.is_dir() { @@ -126,3 +70,47 @@ fn get_dir_size(path: &Path) -> u64 { }) .unwrap_or(0) } + +/// [POL]: Formatuje bajty na czytelny ciąg znaków (np. [kB 12.34]). +pub fn format_weight(bytes: u64, is_dir: bool, config: &WeightConfig) -> String { + if config.system == UnitSystem::None { + return String::new(); + } + + let should_show = (is_dir && config.show_for_dirs) || (!is_dir && config.show_for_files); + if !should_show { + let empty_width = 7 + config.precision; + return format!("{:width$}", "", width = empty_width); + } + + let (base, units) = match config.system { + UnitSystem::Binary => (1024.0_f64, vec!["B", "KiB", "MiB", "GiB", "TiB", "PiB"]), + _ => (1000.0_f64, vec!["B", "kB", "MB", "GB", "TB", "PB"]), + }; + + if bytes == 0 { + return format!( + "[{:>3} {:>width$}] ", + units[0], + "0", + width = config.precision + ); + } + + let bytes_f = bytes as f64; + let exp = (bytes_f.ln() / base.ln()).floor() as usize; + let exp = exp.min(units.len() - 1); + let value = bytes_f / base.powi(exp as i32); + let unit = units[exp]; + + let mut formatted_value = format!("{value:.10}"); + if formatted_value.len() > config.precision { + formatted_value = formatted_value[..config.precision] + .trim_end_matches('.') + .to_string(); + } else { + formatted_value = format!("{formatted_value:>width$}", width = config.precision); + } + + format!("[{unit:>3} {formatted_value}] ") +} diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs new file mode 100644 index 0000000..446bc2b --- /dev/null +++ b/src/core/path_matcher.rs @@ -0,0 +1,9 @@ +/// [POL]: Główny moduł logiki dopasowywania ścieżek. +/// [ENG]: Core module for path matching logic. +pub mod matcher; +pub mod sort; +pub mod stats; + +pub use self::matcher::{PathMatcher, PathMatchers}; +pub use self::sort::SortStrategy; +pub use self::stats::{MatchStats, ShowMode}; diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs new file mode 100644 index 0000000..617dce3 --- /dev/null +++ b/src/core/path_matcher/matcher.rs @@ -0,0 +1,435 @@ +use super::sort::SortStrategy; +use super::stats::{MatchStats, ResultSet, ShowMode}; +use regex::Regex; +use std::collections::HashSet; + +// ============================================================================== +// ⚡ POJEDYNCZY WZORZEC (PathMatcher) +// ============================================================================== + +/// [POL]: Struktura odpowiedzialna za dopasowanie pojedynczego wzorca z uwzględnieniem zależności strukturalnych. +/// [ENG]: Structure responsible for matching a single pattern considering structural dependencies. +pub struct PathMatcher { + regex: Regex, + targets_file: bool, + // [POL]: Flaga @ (para plik-folder) + // [ENG]: Flag @ (file-directory pair) + requires_sibling: bool, + // [POL]: Flaga $ (jednostronna relacja) + // [ENG]: Flag $ (one-way relation) + requires_orphan: bool, + // [POL]: Flaga + (rekurencyjne zacienianie) + // [ENG]: Flag + (recursive shadowing) + is_deep: bool, + // [POL]: Nazwa bazowa modułu do weryfikacji relacji + // [ENG]: Base name of the module for relation verification + base_name: String, + // [POL]: Flaga negacji (!). + // [ENG]: Negation flag (!). + pub is_negated: bool, +} + +impl PathMatcher { + pub fn new(pattern: &str, case_sensitive: bool) -> Result { + let is_negated = pattern.starts_with('!'); + let actual_pattern = if is_negated { &pattern[1..] } else { pattern }; + + let is_deep = actual_pattern.ends_with('+'); + let requires_sibling = actual_pattern.contains('@'); + let requires_orphan = actual_pattern.contains('$'); + let clean_pattern_str = actual_pattern.replace(['@', '$', '+'], ""); + + let base_name = clean_pattern_str + .trim_end_matches('/') + .trim_end_matches("**") + .split('/') + .next_back() + .unwrap_or("") + .split('.') + .next() + .unwrap_or("") + .to_string(); + + let mut re = String::new(); + + if !case_sensitive { + re.push_str("(?i)"); + } + + let mut is_anchored = false; + let mut p = clean_pattern_str.as_str(); + + let targets_file = !p.ends_with('/') && !p.ends_with("**"); + + if p.starts_with("./") { + is_anchored = true; + p = &p[2..]; + } else if p.starts_with("**/") { + is_anchored = true; + } + + if is_anchored { + re.push('^'); + } else { + re.push_str("(?:^|/)"); + } + + let chars: Vec = p.chars().collect(); + let mut i = 0; + + while i < chars.len() { + match chars[i] { + '\\' => { + if i + 1 < chars.len() { + i += 1; + re.push_str(®ex::escape(&chars[i].to_string())); + } + } + '.' => re.push_str("\\."), + '/' => { + if is_deep && i == chars.len() - 1 { + // [POL]: Pominięcie końcowego ukośnika dla flagi '+'. + // [ENG]: Omission of trailing slash for the '+' flag. + } else { + re.push('/'); + } + } + '*' => { + if i + 1 < chars.len() && chars[i + 1] == '*' { + if i + 2 < chars.len() && chars[i + 2] == '/' { + re.push_str("(?:[^/]+/)*"); + i += 2; + } else { + re.push_str(".+"); + i += 1; + } + } else { + re.push_str("[^/]*"); + } + } + '?' => re.push_str("[^/]"), + '{' => { + let mut options = String::new(); + i += 1; + while i < chars.len() && chars[i] != '}' { + options.push(chars[i]); + i += 1; + } + let escaped: Vec = options.split(',').map(regex::escape).collect(); + re.push_str(&format!("(?:{})", escaped.join("|"))); + } + '[' => { + re.push('['); + if i + 1 < chars.len() && chars[i + 1] == '!' { + re.push('^'); + i += 1; + } + } + ']' | '-' | '^' => re.push(chars[i]), + c => re.push_str(®ex::escape(&c.to_string())), + } + i += 1; + } + + if is_deep { + re.push_str("(?:/.*)?$"); + } else { + re.push('$'); + } + + Ok(Self { + regex: Regex::new(&re)?, + targets_file, + requires_sibling, + requires_orphan, + is_deep, + base_name, + is_negated, + }) + } + + /// [POL]: Sprawdza dopasowanie ścieżki, uwzględniając relacje rodzeństwa w strukturze plików. + /// [ENG]: Validates path matching, considering sibling relations within the file structure. + pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { + if self.targets_file && path.ends_with('/') { + return false; + } + + let clean_path = path.strip_prefix("./").unwrap_or(path); + + if !self.regex.is_match(clean_path) { + return false; + } + + // [POL]: Relacja rodzeństwa (@) lub sieroty ($) dla plików. + // [ENG]: Sibling relation (@) or orphan relation ($) for files. + if (self.requires_sibling || self.requires_orphan) && !path.ends_with('/') { + if self.is_deep && self.requires_sibling { + if !self.check_authorized_root(path, env) { + return false; + } + return true; + } + let mut components: Vec<&str> = path.split('/').collect(); + if let Some(file_name) = components.pop() { + let parent_dir = components.join("/"); + let core_name = file_name.split('.').next().unwrap_or(""); + let expected_folder = if parent_dir.is_empty() { + format!("{}/", core_name) + } else { + format!("{}/{}/", parent_dir, core_name) + }; + + if !env.contains(expected_folder.as_str()) { + return false; + } + } + } + + // [POL]: Dodatkowa weryfikacja rodzeństwa (@) dla katalogów. + // [ENG]: Additional sibling verification (@) for directories. + if self.requires_sibling && path.ends_with('/') { + if self.is_deep { + if !self.check_authorized_root(path, env) { + return false; + } + } else { + let dir_no_slash = path.trim_end_matches('/'); + let has_file_sibling = env.iter().any(|&p| { + p.starts_with(dir_no_slash) + && p[dir_no_slash.len()..].starts_with('.') + && !p.ends_with('/') + }); + + if !has_file_sibling { + return false; + } + } + } + + true + } + + /// [POL]: Ewaluuje kolekcję ścieżek, sortuje wyniki i wywołuje odpowiednie akcje. + /// [ENG]: Evaluates a path collection, sorts the results, and triggers respective actions. + // #[allow(clippy::too_many_arguments)] + pub fn evaluate( + &self, + paths: I, + env: &HashSet<&str>, + strategy: SortStrategy, + show_mode: ShowMode, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, + ) -> MatchStats + where + I: IntoIterator, + S: AsRef, + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), + { + let mut matched = Vec::new(); + let mut mismatched = Vec::new(); + + for path in paths { + if self.is_match(path.as_ref(), env) { + matched.push(path); + } else { + mismatched.push(path); + } + } + + strategy.apply(&mut matched); + strategy.apply(&mut mismatched); + + let stats = MatchStats { + m_size_matched: matched.len(), + x_size_mismatched: mismatched.len(), + total: matched.len() + mismatched.len(), + m_matched: ResultSet { + paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + x_mismatched: ResultSet { + paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + }; + + if show_mode == ShowMode::Include || show_mode == ShowMode::Context { + for path in &matched { + on_match(path.as_ref()); + } + } + + if show_mode == ShowMode::Exclude || show_mode == ShowMode::Context { + for path in &mismatched { + on_mismatch(path.as_ref()); + } + } + + stats + } + + /// [POL]: Weryfikuje autoryzację korzenia modułu w relacji plik-folder dla trybu 'deep'. + /// [ENG]: Verifies module root authorisation in the file-directory relation for 'deep' mode. + fn check_authorized_root(&self, path: &str, env: &HashSet<&str>) -> bool { + let clean = path.strip_prefix("./").unwrap_or(path); + let components: Vec<&str> = clean.split('/').collect(); + + for i in 0..components.len() { + let comp_core = components[i].split('.').next().unwrap_or(""); + + if comp_core == self.base_name { + let base_dir = if i == 0 { + self.base_name.clone() + } else { + format!("{}/{}", components[0..i].join("/"), self.base_name) + }; + + let full_base_dir = if path.starts_with("./") { + format!("./{}", base_dir) + } else { + base_dir + }; + let dir_path = format!("{}/", full_base_dir); + + let has_dir = env.contains(dir_path.as_str()); + let has_file = env.iter().any(|&p| { + p.starts_with(&full_base_dir) + && p[full_base_dir.len()..].starts_with('.') + && !p.ends_with('/') + }); + + if has_dir && has_file { + return true; + } + } + } + false + } +} + +// ============================================================================== +// ⚡ KONTENER WIELU WZORCÓW (PathMatchers) +// ============================================================================== + +/// [POL]: Kontener przechowujący kolekcję silników dopasowujących ścieżki. +/// [ENG]: A container holding a collection of path matching engines. +pub struct PathMatchers { + matchers: Vec, +} + +impl PathMatchers { + /// [POL]: Tworzy nową instancję, kompilując listę wzorców po uprzednim rozwinięciu klamer. + /// [ENG]: Creates a new instance by compiling a list of patterns after performing brace expansion. + pub fn new(patterns: I, case_sensitive: bool) -> Result + where + I: IntoIterator, + S: AsRef, + { + let mut matchers = Vec::new(); + for pat in patterns { + matchers.push(PathMatcher::new(pat.as_ref(), case_sensitive)?); + } + Ok(Self { matchers }) + } + + /// [POL]: Weryfikuje, czy ścieżka spełnia warunki narzucone przez zbiór wzorców (w tym negacje). + /// [ENG]: Verifies if the path meets the conditions imposed by the pattern set (including negations). + pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { + if self.matchers.is_empty() { + return false; + } + + let mut has_positive = false; + let mut matched_positive = false; + + for matcher in &self.matchers { + if matcher.is_negated { + // [POL]: Twarde WETO. Dopasowanie negatywne bezwzględnie odrzuca ścieżkę. + // [ENG]: Hard VETO. A negative match unconditionally rejects the path. + if matcher.is_match(path, env) { + return false; + } + } else { + has_positive = true; + if !matched_positive && matcher.is_match(path, env) { + matched_positive = true; + } + } + } + + // [POL]: Ostateczna decyzja na podstawie zebranych danych. + // [ENG]: Final decision based on collected data. + if has_positive { matched_positive } else { true } + } + + /// [POL]: Ewaluuje zbiór ścieżek, sortuje je i wykonuje odpowiednie domknięcia. + /// [ENG]: Evaluates a set of paths, sorts them, and executes respective closures. + pub fn evaluate( + &self, + paths: I, + env: &HashSet<&str>, + strategy: SortStrategy, + show_mode: ShowMode, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, + ) -> MatchStats + where + I: IntoIterator, + S: AsRef, + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), + { + let mut matched = Vec::new(); + let mut mismatched = Vec::new(); + + for path in paths { + if self.is_match(path.as_ref(), env) { + matched.push(path); + } else { + mismatched.push(path); + } + } + + strategy.apply(&mut matched); + strategy.apply(&mut mismatched); + + let stats = MatchStats { + m_size_matched: matched.len(), + x_size_mismatched: mismatched.len(), + total: matched.len() + mismatched.len(), + m_matched: ResultSet { + paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + x_mismatched: ResultSet { + paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + }; + + if show_mode == ShowMode::Include || show_mode == ShowMode::Context { + for path in matched { + on_match(path.as_ref()); + } + } + + if show_mode == ShowMode::Exclude || show_mode == ShowMode::Context { + for path in mismatched { + on_mismatch(path.as_ref()); + } + } + + stats + } +} diff --git a/src/core/path_matcher/sort.rs b/src/core/path_matcher/sort.rs new file mode 100644 index 0000000..ea55131 --- /dev/null +++ b/src/core/path_matcher/sort.rs @@ -0,0 +1,107 @@ +use std::cmp::Ordering; + +/// [POL]: Definiuje dostępne strategie sortowania kolekcji ścieżek. +/// [ENG]: Defines available sorting strategies for path collections. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SortStrategy { + /// [POL]: Brak stosowania algorytmu sortowania. + /// [ENG]: No sorting algorithm applied. + None, + + /// [POL]: Sortowanie alfanumeryczne w porządku rosnącym. + /// [ENG]: Alphanumeric sorting in ascending order. + Az, + + /// [POL]: Sortowanie alfanumeryczne w porządku malejącym. + /// [ENG]: Alphanumeric sorting in descending order. + Za, + + /// [POL]: Priorytet dla plików, następnie sortowanie alfanumeryczne rosnąco. + /// [ENG]: Priority for files, followed by alphanumeric ascending sort. + AzFileFirst, + + /// [POL]: Priorytet dla plików, następnie sortowanie alfanumeryczne malejąco. + /// [ENG]: Priority for files, followed by alphanumeric descending sort. + ZaFileFirst, + + /// [POL]: Priorytet dla katalogów, następnie sortowanie alfanumeryczne rosnąco. + /// [ENG]: Priority for directories, followed by alphanumeric ascending sort. + AzDirFirst, + + /// [POL]: Priorytet dla katalogów, następnie sortowanie alfanumeryczne malejąco. + /// [ENG]: Priority for directories, followed by alphanumeric descending sort. + ZaDirFirst, + + /// [POL]: Sortowanie alfanumeryczne rosnąco, grupujące logiczne pary plik-katalog (np. moduły) z priorytetem dla plików. + /// [ENG]: Alphanumeric ascending sort grouping logical file-directory pairs (e.g. modules), prioritising files. + AzFileFirstMerge, + + /// [POL]: Sortowanie alfanumeryczne malejąco, grupujące logiczne pary plik-katalog z priorytetem dla plików. + /// [ENG]: Alphanumeric descending sort grouping logical file-directory pairs, prioritising files. + ZaFileFirstMerge, + + /// [POL]: Sortowanie alfanumeryczne rosnąco, grupujące logiczne pary plik-katalog z priorytetem dla katalogów. + /// [ENG]: Alphanumeric ascending sort grouping logical file-directory pairs, prioritising directories. + AzDirFirstMerge, + + /// [POL]: Sortowanie alfanumeryczne malejąco, grupujące logiczne pary plik-katalog z priorytetem dla katalogów. + /// [ENG]: Alphanumeric descending sort grouping logical file-directory pairs, prioritising directories. + ZaDirFirstMerge, +} + +impl SortStrategy { + /// [POL]: Sortuje kolekcję ścieżek w miejscu (in-place) na podstawie wybranej strategii. + /// [ENG]: Sorts a collection of paths in-place based on the selected strategy. + pub fn apply>(&self, paths: &mut [S]) { + if *self == SortStrategy::None { + return; + } + + paths.sort_by(|a_s, b_s| { + let a = a_s.as_ref(); + let b = b_s.as_ref(); + + let a_is_dir = a.ends_with('/'); + let b_is_dir = b.ends_with('/'); + + // Wywołujemy naszą prywatną, hermetyczną metodę + let a_merge = Self::get_merge_key(a); + let b_merge = Self::get_merge_key(b); + + match self { + SortStrategy::None => Ordering::Equal, + SortStrategy::Az => a.cmp(b), + SortStrategy::Za => b.cmp(a), + SortStrategy::AzFileFirst => (a_is_dir, a).cmp(&(b_is_dir, b)), + SortStrategy::ZaFileFirst => (a_is_dir, b).cmp(&(b_is_dir, a)), + SortStrategy::AzDirFirst => (!a_is_dir, a).cmp(&(!b_is_dir, b)), + SortStrategy::ZaDirFirst => (!a_is_dir, b).cmp(&(!b_is_dir, a)), + SortStrategy::AzFileFirstMerge => { + (a_merge, a_is_dir, a).cmp(&(b_merge, b_is_dir, b)) + } + SortStrategy::ZaFileFirstMerge => { + (b_merge, a_is_dir, b).cmp(&(a_merge, b_is_dir, a)) + } + SortStrategy::AzDirFirstMerge => { + (a_merge, !a_is_dir, a).cmp(&(b_merge, !b_is_dir, b)) + } + SortStrategy::ZaDirFirstMerge => { + (b_merge, !a_is_dir, b).cmp(&(a_merge, !b_is_dir, a)) + } + } + }); + } + + /// [POL]: Prywatna metoda. Ekstrahuje rdzenną nazwę ścieżki dla strategii Merge. + /// [ENG]: Private method. Extracts the core path name for Merge strategies. + fn get_merge_key(path: &str) -> &str { + let trimmed = path.trim_end_matches('/'); + if let Some(idx) = trimmed.rfind('.') + && idx > 0 + && trimmed.as_bytes()[idx - 1] != b'/' + { + return &trimmed[..idx]; + } + trimmed + } +} diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs new file mode 100644 index 0000000..0dfa387 --- /dev/null +++ b/src/core/path_matcher/stats.rs @@ -0,0 +1,116 @@ +use crate::core::path_view::{PathGrid, PathList, PathTree, ViewMode}; + +/// [POL]: Podzbiór wyników zawierający surowe ścieżki i wygenerowane widoki. +#[derive(Default)] +pub struct ResultSet { + pub paths: Vec, + pub tree: Option, + pub list: Option, + pub grid: Option, +} + +// [ENG]: Simple stats object to avoid manual counting in the Engine. +// [POL]: Prosty obiekt statystyk, aby uniknąć ręcznego liczenia w Engine. +#[derive(Default)] +pub struct MatchStats { + pub m_size_matched: usize, + pub x_size_mismatched: usize, + pub total: usize, + pub m_matched: ResultSet, + pub x_mismatched: ResultSet, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ShowMode { + Include, + Exclude, + Context, +} + +impl MatchStats { + /// : Hermetyzacja renderowania po stronie rdzenia. + /// Zwraca gotowy, złożony ciąg znaków, gotowy do wrzucenia w konsolę lub plik. + #[must_use] + pub fn render_output( + &self, + view_mode: ViewMode, + show_mode: ShowMode, + print_info: bool, + use_color: bool, + ) -> String { + let mut out = String::new(); + let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; + let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; + + match view_mode { + ViewMode::Grid => { + if do_include && let Some(grid) = &self.m_matched.grid { + if print_info { + out.push_str("✅\n"); + } + if use_color { + out.push_str(&grid.render_cli()); + } else { + out.push_str(&grid.render_txt()); + } + } + if do_exclude && let Some(grid) = &self.x_mismatched.grid { + if print_info { + out.push_str("❌\n"); + } + if use_color { + out.push_str(&grid.render_cli()); + } else { + out.push_str(&grid.render_txt()); + } + } + } + ViewMode::Tree => { + if do_include && let Some(tree) = &self.m_matched.tree { + if print_info { + out.push_str("✅\n"); + } + if use_color { + out.push_str(&tree.render_cli()); + } else { + out.push_str(&tree.render_txt()); + } + } + if do_exclude && let Some(tree) = &self.x_mismatched.tree { + if print_info { + out.push_str("❌\n"); + } + if use_color { + out.push_str(&tree.render_cli()); + } else { + out.push_str(&tree.render_txt()); + } + } + } + ViewMode::List => { + if do_include && let Some(list) = &self.m_matched.list { + if print_info { + out.push_str("✅\n"); + } + if use_color { + out.push_str(&list.render_cli(true)); + } else { + out.push_str(&list.render_txt()); + } + } + if do_exclude && let Some(list) = &self.x_mismatched.list { + if print_info { + out.push_str("❌\n"); + } + if use_color { + out.push_str(&list.render_cli(false)); + } else { + out.push_str(&list.render_txt()); + } + } + } + } + + out + } +} diff --git a/src/core/path_store.rs b/src/core/path_store.rs new file mode 100644 index 0000000..17099a4 --- /dev/null +++ b/src/core/path_store.rs @@ -0,0 +1,5 @@ +pub mod context; +pub mod store; + +pub use self::context::PathContext; +pub use self::store::PathStore; diff --git a/src/core/path_store/context.rs b/src/core/path_store/context.rs new file mode 100644 index 0000000..917f345 --- /dev/null +++ b/src/core/path_store/context.rs @@ -0,0 +1,57 @@ +use std::env; +use std::fs; +use std::path::Path; + +/// [POL]: Kontekst ścieżki roboczej - oblicza relacje między terminalem a celem skanowania. +/// [ENG]: Working path context - calculates relations between terminal and scan target. +#[derive(Debug)] +pub struct PathContext { + pub base_absolute: String, + pub entry_absolute: String, + pub entry_relative: String, +} + +impl PathContext { + pub fn resolve>(entered_path: P) -> Result { + let path_ref = entered_path.as_ref(); + + // 1. BASE ABSOLUTE: Gdzie fizycznie odpalono program? + let cwd = env::current_dir().map_err(|e| format!("Błąd odczytu CWD: {}", e))?; + let base_abs = cwd + .to_string_lossy() + .trim_start_matches(r"\\?\") + .replace('\\', "/"); + + // 2. ENTRY ABSOLUTE: Pełna ścieżka do folderu, który skanujemy + let abs_path = fs::canonicalize(path_ref) + .map_err(|e| format!("Nie można ustalić ścieżki '{:?}': {}", path_ref, e))?; + let entry_abs = abs_path + .to_string_lossy() + .trim_start_matches(r"\\?\") + .replace('\\', "/"); + + // 3. ENTRY RELATIVE: Ścieżka od terminala do skanowanego folderu + let entry_rel = match abs_path.strip_prefix(&cwd) { + Ok(rel) => { + let rel_str = rel.to_string_lossy().replace('\\', "/"); + if rel_str.is_empty() { + "./".to_string() // Cel to ten sam folder co terminal + } else { + format!("./{}/", rel_str) + } + } + Err(_) => { + // Jeśli cel jest na innym dysku (np. C:\ a terminal na D:\) + // lub całkiem poza strukturą CWD, relatywna nie istnieje. + // Wracamy wtedy do tego, co wpisał użytkownik, lub dajemy absolutną. + path_ref.to_string_lossy().replace('\\', "/") + } + }; + + Ok(Self { + base_absolute: base_abs, + entry_absolute: entry_abs, + entry_relative: entry_rel, + }) + } +} diff --git a/src/core/path_store/store.rs b/src/core/path_store/store.rs new file mode 100644 index 0000000..fbda9f8 --- /dev/null +++ b/src/core/path_store/store.rs @@ -0,0 +1,56 @@ +use std::collections::HashSet; +use std::path::Path; +use walkdir::WalkDir; + +// use std::fs; +// use std::path::Path; + +// [ENG]: Container for scanned paths and their searchable pool. +// [POL]: Kontener na zeskanowane ścieżki i ich przeszukiwalną pulę. +#[derive(Debug)] +pub struct PathStore { + pub list: Vec, +} +impl PathStore { + /// [POL]: Skanuje katalog rekurencyjnie i zwraca znormalizowane ścieżki (prefix "./", separator "/", suffix "/" dla folderów). + /// [ENG]: Scans the directory recursively and returns normalised paths (prefix "./", separator "/", suffix "/" for directories). + pub fn scan>(dir_path: P) -> Self { + let mut list = Vec::new(); + let entry_path = dir_path.as_ref(); + + for entry in WalkDir::new(entry_path).into_iter().filter_map(|e| e.ok()) { + // [POL]: Pominięcie katalogu głównego (głębokość 0). + // [ENG]: Skip the root directory (depth 0). + if entry.depth() == 0 { + continue; + } + + // [POL]: Pominięcie dowiązań symbolicznych i punktów reparse. + // [ENG]: Skip symbolic links and reparse points. + if entry.path_is_symlink() { + continue; + } + + if let Ok(rel_path) = entry.path().strip_prefix(entry_path) { + // [POL]: Normalizacja separatorów systemowych do formatu uniwersalnego. + // [ENG]: Normalisation of system separators to a universal format. + let relative_str = rel_path.to_string_lossy().replace('\\', "/"); + let mut final_path = format!("./{}", relative_str); + + if entry.file_type().is_dir() { + final_path.push('/'); + } + + list.push(final_path); + } + } + + Self { list } + } + + // [ENG]: Creates a temporary pool of references for the matcher. + // [POL]: Tworzy tymczasową pulę referencji (paths_pool) dla matchera. + pub fn get_index(&self) -> HashSet<&str> { + self.list.iter().map(|s| s.as_str()).collect() + } +} diff --git a/src/core/path_view.rs b/src/core/path_view.rs new file mode 100644 index 0000000..890cad7 --- /dev/null +++ b/src/core/path_view.rs @@ -0,0 +1,16 @@ +pub mod grid; +pub mod list; +pub mod node; +pub mod tree; + +// Re-eksportujemy dla wygody, aby w engine.rs używać PathTree i FileNode bezpośrednio +pub use grid::PathGrid; +pub use list::PathList; +pub use tree::PathTree; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ViewMode { + Tree, + List, + Grid, +} diff --git a/src/core/path_view/grid.rs b/src/core/path_view/grid.rs new file mode 100644 index 0000000..2236753 --- /dev/null +++ b/src/core/path_view/grid.rs @@ -0,0 +1,309 @@ +use colored::Colorize; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig}; +use crate::core::path_matcher::SortStrategy; +use crate::theme::for_path_tree::{DIR_ICON, TreeStyle, get_file_type}; + +pub struct PathGrid { + roots: Vec, + style: TreeStyle, +} + +impl PathGrid { + #[must_use] + pub fn build( + paths_strings: &[String], + base_dir: &str, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + root_name: Option<&str>, + no_emoji: bool, + ) -> Self { + // Dokładnie taka sama logika budowania struktury węzłów jak w PathTree::build + let base_path_obj = Path::new(base_dir); + let paths: Vec = paths_strings.iter().map(PathBuf::from).collect(); + let mut tree_map: BTreeMap> = BTreeMap::new(); + + for p in &paths { + let parent = p + .parent() + .map_or_else(|| PathBuf::from("."), Path::to_path_buf); + tree_map.entry(parent).or_default().push(p.clone()); + } + + fn build_node( + path: &PathBuf, + paths_map: &BTreeMap>, + base_path: &Path, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + no_emoji: bool, + ) -> FileNode { + let name = path + .file_name() + .map_or_else(|| "/".to_string(), |n| n.to_string_lossy().to_string()); + let is_dir = path.is_dir() || path.to_string_lossy().ends_with('/'); + let icon = if no_emoji { + String::new() + } else if is_dir { + DIR_ICON.to_string() + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + get_file_type(ext).icon.to_string() + } else { + "📄".to_string() + }; + + let absolute_path = base_path.join(path); + let mut weight_bytes = + weight::get_path_weight(&absolute_path, weight_cfg.dir_sum_included); + let mut children = vec![]; + + if let Some(child_paths) = paths_map.get(path) { + let mut child_nodes: Vec = child_paths + .iter() + .map(|c| { + build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji) + }) + .collect(); + + FileNode::sort_slice(&mut child_nodes, sort_strategy); + + if is_dir && weight_cfg.dir_sum_included { + weight_bytes = child_nodes.iter().map(|n| n.weight_bytes).sum(); + } + children = child_nodes; + } + + let weight_str = weight::format_weight(weight_bytes, is_dir, weight_cfg); + FileNode { + name, + path: path.clone(), + is_dir, + icon, + weight_str, + weight_bytes, + children, + } + } + + let roots_paths: Vec = paths + .iter() + .filter(|p| { + let parent = p.parent(); + parent.is_none() + || parent.unwrap() == Path::new("") + || !paths.contains(&parent.unwrap().to_path_buf()) + }) + .cloned() + .collect(); + + let mut top_nodes: Vec = roots_paths + .into_iter() + .map(|r| { + build_node( + &r, + &tree_map, + base_path_obj, + sort_strategy, + weight_cfg, + no_emoji, + ) + }) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_strategy); + + // [ENG]: Logic for creating the final root node with proper weight calculation. + // [POL]: Logika tworzenia końcowego węzła głównego z poprawnym obliczeniem wagi. + let final_roots = if let Some(r_name) = root_name { + // [ENG]: Calculate total weight for the root node. + // [POL]: Obliczenie całkowitej wagi dla węzła głównego. + let root_bytes = if weight_cfg.dir_sum_included { + // [POL]: Suma wag bezpośrednich dzieci (dopasowanych elementów). + top_nodes.iter().map(|n| n.weight_bytes).sum() + } else { + // [POL]: Fizyczna waga folderu wejściowego z dysku. + weight::get_path_weight(base_path_obj, false) + }; + + let root_weight_str = weight::format_weight(root_bytes, true, weight_cfg); + + vec![FileNode { + name: r_name.to_string(), + path: PathBuf::from(r_name), + is_dir: true, + icon: if no_emoji { + String::new() + } else { + DIR_ICON.to_string() + }, + weight_str: root_weight_str, + weight_bytes: root_bytes, + children: top_nodes, + }] + } else { + top_nodes + }; + + Self { + roots: final_roots, + style: TreeStyle::default(), + } + } + + #[must_use] + pub fn render_cli(&self) -> String { + let max_width = self.calc_max_width(&self.roots, 0); + self.plot(&self.roots, "", true, max_width) + } + + #[must_use] + pub fn render_txt(&self) -> String { + let max_width = self.calc_max_width(&self.roots, 0); + self.plot(&self.roots, "", false, max_width) + } + + fn calc_max_width(&self, nodes: &[FileNode], indent_len: usize) -> usize { + let mut max = 0; + for (i, node) in nodes.iter().enumerate() { + let is_last = i == nodes.len() - 1; + let has_children = !node.children.is_empty(); + let branch = if node.is_dir { + match (is_last, has_children) { + (true, true) => &self.style.dir_last_with_children, + (false, true) => &self.style.dir_mid_with_children, + (true, false) => &self.style.dir_last_no_children, + (false, false) => &self.style.dir_mid_no_children, + } + } else if is_last { + &self.style.file_last + } else { + &self.style.file_mid + }; + + let current_len = node.weight_str.chars().count() + + indent_len + + branch.chars().count() + + 1 + + node.icon.chars().count() + + 1 + + node.name.chars().count(); + if current_len > max { + max = current_len; + } + + if has_children { + let next_indent = indent_len + + if is_last { + self.style.indent_last.chars().count() + } else { + self.style.indent_mid.chars().count() + }; + let child_max = self.calc_max_width(&node.children, next_indent); + if child_max > max { + max = child_max; + } + } + } + max + } + + fn plot(&self, nodes: &[FileNode], indent: &str, use_color: bool, max_width: usize) -> String { + let mut result = String::new(); + for (i, node) in nodes.iter().enumerate() { + let is_last = i == nodes.len() - 1; + let has_children = !node.children.is_empty(); + let branch = if node.is_dir { + match (is_last, has_children) { + (true, true) => &self.style.dir_last_with_children, + (false, true) => &self.style.dir_mid_with_children, + (true, false) => &self.style.dir_last_no_children, + (false, false) => &self.style.dir_mid_no_children, + } + } else if is_last { + &self.style.file_last + } else { + &self.style.file_mid + }; + + let weight_prefix = if node.weight_str.is_empty() { + String::new() + } else if use_color { + node.weight_str.truecolor(120, 120, 120).to_string() + } else { + node.weight_str.clone() + }; + + let raw_left_len = node.weight_str.chars().count() + + indent.chars().count() + + branch.chars().count() + + 1 + + node.icon.chars().count() + + 1 + + node.name.chars().count(); + let pad_len = max_width.saturating_sub(raw_left_len) + 4; + let padding = " ".repeat(pad_len); + + let rel_path_str = node.path.to_string_lossy().replace('\\', "/"); + let display_path = if node.is_dir && !rel_path_str.ends_with('/') { + format!("./{}/", rel_path_str) + } else if !rel_path_str.starts_with("./") && !rel_path_str.starts_with('.') { + format!("./{}", rel_path_str) + } else { + rel_path_str + }; + + let right_colored = if use_color { + if node.is_dir { + display_path.truecolor(200, 200, 50).to_string() + } else { + display_path.white().to_string() + } + } else { + display_path + }; + + let left_colored = if use_color { + if node.is_dir { + format!( + "{}{}{} {}{}", + weight_prefix, + indent.green(), + branch.green(), + node.icon, + node.name.truecolor(200, 200, 50) + ) + } else { + format!( + "{}{}{} {}{}", + weight_prefix, + indent.green(), + branch.green(), + node.icon, + node.name.white() + ) + } + } else { + format!( + "{}{}{} {} {}", + weight_prefix, indent, branch, node.icon, node.name + ) + }; + + result.push_str(&format!("{}{}{}\n", left_colored, padding, right_colored)); + + if has_children { + let new_indent = if is_last { + format!("{}{}", indent, self.style.indent_last) + } else { + format!("{}{}", indent, self.style.indent_mid) + }; + result.push_str(&self.plot(&node.children, &new_indent, use_color, max_width)); + } + } + result + } +} diff --git a/src/core/path_view/list.rs b/src/core/path_view/list.rs new file mode 100644 index 0000000..1c20d23 --- /dev/null +++ b/src/core/path_view/list.rs @@ -0,0 +1,82 @@ +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig}; +use crate::core::path_matcher::SortStrategy; +use crate::theme::for_path_list::get_icon_for_path; +use colored::Colorize; +/// [POL]: Zarządca wyświetlania wyników w formie płaskiej listy. +pub struct PathList { + items: Vec, +} + +impl PathList { + /// [POL]: Buduje listę na podstawie zbioru ścieżek i statystyk. + pub fn build( + paths_strings: &[String], + base_dir: &str, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + no_emoji: bool, + ) -> Self { + // Wykorzystujemy istniejącą logikę węzłów, ale bez rekurencji (płaska lista) + let mut items: Vec = paths_strings + .iter() + .map(|p_str| { + let absolute = std::path::Path::new(base_dir).join(p_str); + let is_dir = p_str.ends_with('/'); + let weight_bytes = + crate::core::file_stats::weight::get_path_weight(&absolute, true); + let weight_str = weight::format_weight(weight_bytes, is_dir, weight_cfg); + + FileNode { + name: p_str.clone(), + path: absolute, + is_dir, + icon: if no_emoji { + String::new() + } else { + get_icon_for_path(p_str).to_string() + }, + weight_str, + weight_bytes, + children: vec![], // Lista nie ma dzieci + } + }) + .collect(); + + FileNode::sort_slice(&mut items, sort_strategy); + + Self { items } + } + + /// [POL]: Renderuje listę dla terminala (z kolorami i ikonami). + pub fn render_cli(&self, _is_match: bool) -> String { + let mut out = String::new(); + // let tag = if is_match { "✅ MATCH: ".green() } else { "❌ REJECT:".red() }; + + for item in &self.items { + let line = format!( + "{} {} {}\n", + item.weight_str.truecolor(120, 120, 120), + item.icon, + if item.is_dir { + item.name.yellow() + } else { + item.name.white() + } + ); + out.push_str(&line); + } + out + } + + #[must_use] + pub fn render_txt(&self) -> String { + let mut out = String::new(); + for item in &self.items { + // Brak formatowania ANSI + let line = format!("{} {} {}\n", item.weight_str, item.icon, item.name); + out.push_str(&line); + } + out + } +} diff --git a/src/core/path_view/node.rs b/src/core/path_view/node.rs new file mode 100644 index 0000000..c7a58cb --- /dev/null +++ b/src/core/path_view/node.rs @@ -0,0 +1,70 @@ +use crate::core::path_matcher::SortStrategy; +use std::path::PathBuf; + +/// [POL]: Reprezentuje pojedynczy węzeł w drzewie systemu plików. +#[derive(Debug, Clone)] +pub struct FileNode { + pub name: String, + pub path: PathBuf, + pub is_dir: bool, + pub icon: String, + pub weight_str: String, + pub weight_bytes: u64, + pub children: Vec, +} + +impl FileNode { + /// [POL]: Sortuje listę węzłów w miejscu zgodnie z wybraną strategią. + pub fn sort_slice(nodes: &mut [FileNode], strategy: SortStrategy) { + if strategy == SortStrategy::None { + return; + } + + nodes.sort_by(|a, b| { + let a_is_dir = a.is_dir; + let b_is_dir = b.is_dir; + + // Klucz Merge: "interfaces.rs" -> "interfaces", "interfaces/" -> "interfaces" + let a_merge = Self::get_merge_key(&a.name); + let b_merge = Self::get_merge_key(&b.name); + + match strategy { + // 1. CZYSTE ALFANUMERYCZNE + SortStrategy::Az => a.name.cmp(&b.name), + SortStrategy::Za => b.name.cmp(&a.name), + + // 2. PLIKI PIERWSZE (Globalnie) + SortStrategy::AzFileFirst => (a_is_dir, &a.name).cmp(&(b_is_dir, &b.name)), + SortStrategy::ZaFileFirst => (a_is_dir, &b.name).cmp(&(b_is_dir, &a.name)), + + // 3. KATALOGI PIERWSZE (Globalnie) + SortStrategy::AzDirFirst => (!a_is_dir, &a.name).cmp(&(!b_is_dir, &b.name)), + SortStrategy::ZaDirFirst => (!a_is_dir, &b.name).cmp(&(!b_is_dir, &a.name)), + + // 4. PLIKI PIERWSZE + MERGE (Grupowanie modułów) + SortStrategy::AzFileFirstMerge => { + (a_merge, a_is_dir, &a.name).cmp(&(b_merge, b_is_dir, &b.name)) + } + SortStrategy::ZaFileFirstMerge => { + (b_merge, a_is_dir, &b.name).cmp(&(a_merge, b_is_dir, &a.name)) + } + + // 5. KATALOGI PIERWSZE + MERGE (Zgodnie z Twoją notatką: fallback do DirFirst) + SortStrategy::AzDirFirstMerge => (!a_is_dir, &a.name).cmp(&(!b_is_dir, &b.name)), + SortStrategy::ZaDirFirstMerge => (!a_is_dir, &b.name).cmp(&(!b_is_dir, &a.name)), + + _ => a.name.cmp(&b.name), + } + }); + } + + /// [POL]: Wyciąga rdzeń nazwy do grupowania (np. "main.rs" -> "main"). + fn get_merge_key(name: &str) -> &str { + if let Some(idx) = name.rfind('.') + && idx > 0 + { + return &name[..idx]; + } + name + } +} diff --git a/src/core/path_view/tree.rs b/src/core/path_view/tree.rs new file mode 100644 index 0000000..3cdcf67 --- /dev/null +++ b/src/core/path_view/tree.rs @@ -0,0 +1,234 @@ +use colored::Colorize; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +// Importy z rodzeństwa i innych modułów core +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig}; +use crate::core::path_matcher::SortStrategy; +use crate::theme::for_path_tree::{DIR_ICON, FILE_ICON, TreeStyle, get_file_type}; +pub struct PathTree { + roots: Vec, + style: TreeStyle, +} + +impl PathTree { + #[must_use] + pub fn build( + paths_strings: &[String], + base_dir: &str, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + root_name: Option<&str>, + no_emoji: bool, + ) -> Self { + let base_path_obj = Path::new(base_dir); + let paths: Vec = paths_strings.iter().map(PathBuf::from).collect(); + let mut tree_map: BTreeMap> = BTreeMap::new(); + + for p in &paths { + let parent = p + .parent() + .map_or_else(|| PathBuf::from("."), Path::to_path_buf); + tree_map.entry(parent).or_default().push(p.clone()); + } + + fn build_node( + path: &PathBuf, + paths_map: &BTreeMap>, + base_path: &Path, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + no_emoji: bool, + ) -> FileNode { + let name = path + .file_name() + .map_or_else(|| "/".to_string(), |n| n.to_string_lossy().to_string()); + + let is_dir = path.is_dir() || path.to_string_lossy().ends_with('/'); + let icon = if no_emoji { + String::new() + } else if is_dir { + DIR_ICON.to_string() + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + get_file_type(ext).icon.to_string() + } else { + FILE_ICON.to_string() + }; + + let absolute_path = base_path.join(path); + let mut weight_bytes = + weight::get_path_weight(&absolute_path, weight_cfg.dir_sum_included); + let mut children = vec![]; + + if let Some(child_paths) = paths_map.get(path) { + let mut child_nodes: Vec = child_paths + .iter() + .map(|c| { + build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji) + }) + .collect(); + + FileNode::sort_slice(&mut child_nodes, sort_strategy); + + if is_dir && weight_cfg.dir_sum_included { + weight_bytes = child_nodes.iter().map(|n| n.weight_bytes).sum(); + } + children = child_nodes; + } + + let weight_str = weight::format_weight(weight_bytes, is_dir, weight_cfg); + + FileNode { + name, + path: path.clone(), + is_dir, + icon, + weight_str, + weight_bytes, + children, + } + } + + let roots_paths: Vec = paths + .iter() + .filter(|p| { + let parent = p.parent(); + parent.is_none() + || parent.unwrap() == Path::new("") + || !paths.contains(&parent.unwrap().to_path_buf()) + }) + .cloned() + .collect(); + + let mut top_nodes: Vec = roots_paths + .into_iter() + .map(|r| { + build_node( + &r, + &tree_map, + base_path_obj, + sort_strategy, + weight_cfg, + no_emoji, + ) + }) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_strategy); + + // [ENG]: Logic for creating the final root node with proper weight calculation. + // [POL]: Logika tworzenia końcowego węzła głównego z poprawnym obliczeniem wagi. + let final_roots = if let Some(r_name) = root_name { + // [ENG]: Calculate total weight for the root node. + // [POL]: Obliczenie całkowitej wagi dla węzła głównego. + let root_bytes = if weight_cfg.dir_sum_included { + // [POL]: Suma wag bezpośrednich dzieci (dopasowanych elementów). + top_nodes.iter().map(|n| n.weight_bytes).sum() + } else { + // [POL]: Fizyczna waga folderu wejściowego z dysku. + weight::get_path_weight(base_path_obj, false) + }; + + let root_weight_str = weight::format_weight(root_bytes, true, weight_cfg); + + vec![FileNode { + name: r_name.to_string(), + path: PathBuf::from(r_name), + is_dir: true, + icon: if no_emoji { + String::new() + } else { + DIR_ICON.to_string() + }, + weight_str: root_weight_str, + weight_bytes: root_bytes, + children: top_nodes, + }] + } else { + top_nodes + }; + + Self { + roots: final_roots, + style: TreeStyle::default(), + } + } + + #[must_use] + pub fn render_cli(&self) -> String { + self.plot(&self.roots, "", true) + } + + #[must_use] + pub fn render_txt(&self) -> String { + self.plot(&self.roots, "", false) + } + + fn plot(&self, nodes: &[FileNode], indent: &str, use_color: bool) -> String { + let mut result = String::new(); + for (i, node) in nodes.iter().enumerate() { + let is_last = i == nodes.len() - 1; + let has_children = !node.children.is_empty(); + + let branch = if node.is_dir { + match (is_last, has_children) { + (true, true) => &self.style.dir_last_with_children, + (false, true) => &self.style.dir_mid_with_children, + (true, false) => &self.style.dir_last_no_children, + (false, false) => &self.style.dir_mid_no_children, + } + } else if is_last { + &self.style.file_last + } else { + &self.style.file_mid + }; + + let weight_prefix = if node.weight_str.is_empty() { + String::new() + } else if use_color { + node.weight_str.truecolor(120, 120, 120).to_string() + } else { + node.weight_str.clone() + }; + + let line = if use_color { + if node.is_dir { + format!( + "{weight_prefix}{}{branch_color} {icon} {name}\n", + indent.green(), + branch_color = branch.green(), + icon = node.icon, + name = node.name.truecolor(200, 200, 50) + ) + } else { + format!( + "{weight_prefix}{}{branch_color} {icon} {name}\n", + indent.green(), + branch_color = branch.green(), + icon = node.icon, + name = node.name.white() + ) + } + } else { + format!( + "{weight_prefix}{indent}{branch} {icon} {name}\n", + icon = node.icon, + name = node.name + ) + }; + + result.push_str(&line); + + if has_children { + let new_indent = if is_last { + format!("{indent}{}", self.style.indent_last) + } else { + format!("{indent}{}", self.style.indent_mid) + }; + result.push_str(&self.plot(&node.children, &new_indent, use_color)); + } + } + result + } +} diff --git a/src/core/patterns_expand.rs b/src/core/patterns_expand.rs new file mode 100644 index 0000000..9354ecf --- /dev/null +++ b/src/core/patterns_expand.rs @@ -0,0 +1,48 @@ +/// [POL]: Kontekst wzorców - przechowuje oryginalne wzorce użytkownika oraz ich rozwiniętą formę. +/// [ENG]: Pattern context - stores original user patterns and their tok form. +#[derive(Debug, Clone)] +pub struct PatternContext { + pub raw: Vec, + pub tok: Vec, +} + +impl PatternContext { + /// [POL]: Tworzy nowy kontekst, automatycznie rozwijając klamry w podanych wzorcach. + /// [ENG]: Creates a new context, automatically expanding braces in the provided patterns. + pub fn new(patterns: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + let mut raw = Vec::new(); + let mut tok = Vec::new(); + + for pat in patterns { + let pat_str = pat.as_ref(); + raw.push(pat_str.to_string()); + tok.extend(Self::expand_braces(pat_str)); + } + + Self { raw, tok } + } + + /// [POL]: Prywatna metoda: rozwija klamry we wzorcu (np. {a,b} -> [a, b]). Obsługuje rekurencję. + /// [ENG]: Private method: expands braces in a pattern (e.g. {a,b} -> [a, b]). Supports recursion. + fn expand_braces(pattern: &str) -> Vec { + if let (Some(start), Some(end)) = (pattern.find('{'), pattern.find('}')) + && start < end + { + let prefix = &pattern[..start]; + let suffix = &pattern[end + 1..]; + let options = &pattern[start + 1..end]; + + let mut tok = Vec::new(); + for opt in options.split(',') { + let new_pattern = format!("{}{}{}", prefix, opt, suffix); + tok.extend(Self::expand_braces(&new_pattern)); + } + return tok; + } + vec![pattern.to_string()] + } +} diff --git a/src/core/save.rs b/src/core/save.rs new file mode 100644 index 0000000..e8923de --- /dev/null +++ b/src/core/save.rs @@ -0,0 +1,229 @@ +use super::super::i18n::I18n; +use crate::theme::for_path_tree::get_file_type; +use std::fs; +use std::path::Path; + +pub struct SaveFile; + +impl SaveFile { + // ⚡ Nowa funkcja tabelarycznej stopki + pub fn generate_by_section(tag: &str, enter_path: &str, i18n: &I18n, cmd: &str) -> String { + let mut f = String::new(); + f.push_str("\n\n---\n\n"); + f.push_str("> | Property | Value |\n"); + f.push_str("> | ---: | :--- |\n"); + f.push_str(&format!( + "> | **{}** | `cargo-plot v0.2.0` |\n", + i18n.footer_tool() + )); + f.push_str(&format!( + "> | **{}** | `{}` |\n", + i18n.footer_input(), + enter_path + )); + f.push_str(&format!("> | **{}** | `{}` |\n", i18n.footer_cmd(), cmd)); + f.push_str(&format!("> | **{}** | `{}` |\n", i18n.footer_tag(), tag)); + + let links = "[Crates.io](https://crates.io/crates/cargo-plot) \\| [GitHub](https://github.com/j-Cis/cargo-plot/releases)"; + f.push_str(&format!("> | **{}** | {} |\n", i18n.footer_links(), links)); + f.push_str(&format!( + "> | **{}** | `cargo install cargo-plot` |\n", + i18n.footer_links() + )); + f.push_str(&format!( + "> | **{}** | `cargo plot --help` |\n", + i18n.footer_help() + )); + f.push_str("\n---\n"); + f + } + + /// Wspólna logika zapisu do pliku (DRY): tworzenie folderów i zapis IO. + fn write_to_disk(filepath: &str, content: &str, log_name: &str, i18n: &I18n) { + let path = Path::new(filepath); + + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + && !parent.exists() + && let Err(e) = fs::create_dir_all(parent) + { + eprintln!( + "{}", + i18n.dir_create_err(&parent.to_string_lossy(), &e.to_string()) + ); + return; + } + + match fs::write(path, content) { + Ok(_) => println!("{}", i18n.save_success(log_name, filepath)), + Err(e) => eprintln!("{}", i18n.save_err(log_name, filepath, &e.to_string())), + } + } + + /// Formatowanie i zapis samego widoku struktury (ścieżek) + pub fn paths( + content: &str, + filepath: &str, + tag: &str, + add_by: bool, + i18n: &I18n, + cmd: &str, + enter_path: &str, + ) { + let by_section = if add_by { + Self::generate_by_section(tag, enter_path, i18n, cmd) + } else { + String::new() + }; + let internal_tag = if add_by { "" } else { tag }; + let file_name = Path::new(filepath) + .file_name() + .unwrap_or_default() + .to_string_lossy(); + + let markdown_content = format!( + "# {}\n\n```plaintext\n{}\n```\n\n{}{}", + file_name, content, internal_tag, by_section + ); + + Self::write_to_disk( + filepath, + &markdown_content, + if i18n.lang == crate::i18n::Lang::Pl { + "ścieżki" + } else { + "paths" + }, + i18n, + ); + } + + /// Formatowanie i zapis pełnego cache (drzewo + zawartość plików) + pub fn codes( + tree_text: &str, + paths: &[String], + base_dir: &str, + filepath: &str, + tag: &str, + add_by: bool, + i18n: &I18n, + cmd: &str, + enter_path: &str, + ) { + let by_section = if add_by { + Self::generate_by_section(tag, enter_path, i18n, cmd) + } else { + String::new() + }; + let internal_tag = if add_by { "" } else { tag }; + let file_name = Path::new(filepath) + .file_name() + .unwrap_or_default() + .to_string_lossy(); + + let mut content = String::new(); + content.push_str(&format!("# {}\n\n", file_name)); + + // Wstawiamy wygenerowane drzewo ścieżek + content.push_str("```plaintext\n"); + content.push_str(tree_text); + content.push_str("```\n\n"); + + let mut counter = 1; + + for p_str in paths { + if p_str.ends_with('/') { + continue; // Pomijamy katalogi + } + + let absolute_path = Path::new(base_dir).join(p_str); + let ext = absolute_path + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + + let lang = get_file_type(&ext).md_lang; + + if is_blacklisted_extension(&ext) { + content.push_str(&format!( + "### {:03}: `{}`\n\n{}\n\n", + counter, + p_str, + i18n.skip_binary() + )); + counter += 1; + continue; + } + + match fs::read_to_string(&absolute_path) { + Ok(file_content) => { + content.push_str(&format!( + "### {:03}: `{}`\n\n```{}\n{}\n```\n\n", + counter, p_str, lang, file_content + )); + } + Err(_) => { + content.push_str(&format!( + "### {:03}: `{}`\n\n{}\n\n", + counter, + p_str, + i18n.read_err() + )); + } + } + counter += 1; + } + + content.push_str(&format!("\n\n{}{}", internal_tag, by_section)); + Self::write_to_disk( + filepath, + &content, + if i18n.lang == crate::i18n::Lang::Pl { + "kod (cache)" + } else { + "code (cache)" + }, + i18n, + ); + } +} + +// [EN]: Security mechanisms to prevent processing non-text or binary files. +// [PL]: Mechanizmy bezpieczeństwa zapobiegające przetwarzaniu plików nietekstowych lub binarnych. + +/// [EN]: Checks if a file extension is on the list of forbidden binary types. +/// [PL]: Sprawdza, czy rozszerzenie pliku znajduje się na liście zabronionych typów binarnych. +pub fn is_blacklisted_extension(ext: &str) -> bool { + let e = ext.to_lowercase(); + + matches!( + e.as_str(), + // -------------------------------------------------- + // GRAFIKA I DESIGN + // -------------------------------------------------- + "png" | "jpg" | "jpeg" | "gif" | "bmp" | "ico" | "svg" | "webp" | "tiff" | "tif" | "heic" | "psd" | + "ai" | + // -------------------------------------------------- + // BINARKI | BIBLIOTEKI I ARTEFAKTY KOMPILACJI + // -------------------------------------------------- + "exe" | "dll" | "so" | "dylib" | "bin" | "wasm" | "pdb" | "rlib" | "rmeta" | "lib" | + "o" | "a" | "obj" | "pch" | "ilk" | "exp" | + "jar" | "class" | "war" | "ear" | + "pyc" | "pyd" | "pyo" | "whl" | + // -------------------------------------------------- + // ARCHIWA I PACZKI + // -------------------------------------------------- + "zip" | "tar" | "gz" | "tgz" | "7z" | "rar" | "bz2" | "xz" | "iso" | "dmg" | "pkg" | "apk" | + // -------------------------------------------------- + // DOKUMENTY | BAZY DANYCH I FONTY + // -------------------------------------------------- + "sqlite" | "sqlite3" | "db" | "db3" | "mdf" | "ldf" | "rdb" | + "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "odt" | "ods" | "odp" | + "woff" | "woff2" | "ttf" | "eot" | "otf" | + // -------------------------------------------------- + // MEDIA (AUDIO / WIDEO) + // -------------------------------------------------- + "mp3" | "mp4" | "avi" | "mkv" | "wav" | "flac" | "ogg" | "m4a" | "mov" | "wmv" | "flv" + ) +} diff --git a/src/execute.rs b/src/execute.rs new file mode 100644 index 0000000..021f47f --- /dev/null +++ b/src/execute.rs @@ -0,0 +1,169 @@ +use crate::core::file_stats::FileStats; +use crate::core::file_stats::weight::WeightConfig; +pub use crate::core::path_matcher::SortStrategy; +use crate::core::path_matcher::{MatchStats, PathMatchers, ShowMode}; +use crate::core::path_store::{PathContext, PathStore}; +use crate::core::path_view::{PathGrid, PathList, PathTree, ViewMode}; +use crate::core::patterns_expand::PatternContext; +use std::path::Path; + +/// [ENG]: Primary execution function that coordinates scanning, matching, and view building. +/// [POL]: Główna funkcja wykonawcza koordynująca skanowanie, dopasowywanie i budowanie widoków. +pub fn execute( + enter_path: &str, + patterns: &[String], + is_case_sensitive: bool, + sort_strategy: SortStrategy, + show_mode: ShowMode, + view_mode: ViewMode, + weight_cfg: WeightConfig, // ⚡ Używamy konfiguracji przekazanej z CLI/GUI + no_root: bool, + print_info: bool, + no_emoji: bool, + i18n: &crate::i18n::I18n, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, +) -> MatchStats +where + OnMatch: FnMut(&FileStats), + OnMismatch: FnMut(&FileStats), +{ + // [ENG]: 1. Initialize contexts. + // [POL]: 1. Inicjalizacja kontekstów. + let pattern_ctx = PatternContext::new(patterns); + let path_ctx = PathContext::resolve(enter_path).unwrap_or_else(|e| { + eprintln!("❌ {}", e); + std::process::exit(1); + }); + + // [ENG]: 2. Initial state logging (Restored full verbosity). + // [POL]: 2. Logowanie stanu początkowego (Przywrócono pełną szczegółowość). + if print_info { + println!("{}", i18n.cli_base_abs(&path_ctx.base_absolute)); + println!("{}", i18n.cli_target_abs(&path_ctx.entry_absolute)); + println!("{}", i18n.cli_target_rel(&path_ctx.entry_relative)); + println!("---------------------------------------"); + println!("{}", i18n.cli_case_sensitive(is_case_sensitive)); + println!( + "{}", + i18n.cli_patterns_raw(&format!("{:?}", pattern_ctx.raw)) + ); + println!( + "{}", + i18n.cli_patterns_tok(&format!("{:?}", pattern_ctx.tok)) + ); + println!("---------------------------------------"); + } else { + println!("---------------------------------------"); + } + + // [ENG]: 3. Build matchers. + // [POL]: 3. Budowa silników dopasowujących. + let matchers = + PathMatchers::new(&pattern_ctx.tok, is_case_sensitive).expect("Błąd kompilacji wzorców"); + + // [ENG]: 4. Scan disk. + // [POL]: 4. Skanowanie dysku. + let paths_store = PathStore::scan(&path_ctx.entry_absolute); + let paths_set = paths_store.get_index(); + let entry_abs = path_ctx.entry_absolute.clone(); + + // [ENG]: 6. Evaluate paths and fetch stats via callbacks. + // [POL]: 6. Ewaluacja ścieżek i pobieranie statystyk przez callbacki. + let mut stats = matchers.evaluate( + &paths_store.list, + &paths_set, + sort_strategy, + show_mode, + |raw_path| { + let stats = FileStats::fetch(raw_path, &entry_abs); + on_match(&stats); + }, + |raw_path| { + let stats = FileStats::fetch(raw_path, &entry_abs); + on_mismatch(&stats); + }, + ); + + // [ENG]: 7. Build views using the provided weight configuration. + // [POL]: 7. Budowa widoków przy użyciu dostarczonej konfiguracji wagi. + let root_name = if no_root { + None + } else { + Path::new(&path_ctx.entry_absolute) + .file_name() + .and_then(|n| n.to_str()) + }; + + let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; + let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; + + match view_mode { + ViewMode::Grid => { + if do_include { + stats.m_matched.grid = Some(PathGrid::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + no_emoji, + )); + } + if do_exclude { + stats.x_mismatched.grid = Some(PathGrid::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + no_emoji, + )); + } + } + ViewMode::Tree => { + if do_include { + stats.m_matched.tree = Some(PathTree::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + no_emoji, + )); + } + if do_exclude { + stats.x_mismatched.tree = Some(PathTree::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + no_emoji, + )); + } + } + ViewMode::List => { + if do_include { + stats.m_matched.list = Some(PathList::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + no_emoji, + )); + } + if do_exclude { + stats.x_mismatched.list = Some(PathList::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + no_emoji, + )); + } + } + } + + stats +} diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..f3d4d6c --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,160 @@ +use std::env; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum Lang { + Pl, + En, +} + +impl Lang { + pub fn detect() -> Self { + if env::var("LANG") + .unwrap_or_default() + .to_lowercase() + .starts_with("pl") + { + Self::Pl + } else { + Self::En + } + } +} + +pub struct I18n { + pub lang: Lang, +} + +impl I18n { + pub fn new(lang: Option) -> Self { + Self { + lang: lang.unwrap_or_else(Lang::detect), + } + } + + // ===================================================================== + // 1. TOP - TEKST OGÓLNY + // ===================================================================== + pub fn save_success(&self, name: &str, path: &str) -> String { + match self.lang { + Lang::Pl => format!("💾 Pomyślnie zapisano {} do pliku: {}", name, path), + Lang::En => format!("💾 Successfully saved {} to file: {}", name, path), + } + } + pub fn save_err(&self, name: &str, path: &str, err: &str) -> String { + match self.lang { + Lang::Pl => format!("❌ Błąd zapisu {} do pliku {}: {}", name, path, err), + Lang::En => format!("❌ Error saving {} to file {}: {}", name, path, err), + } + } + pub fn dir_create_err(&self, dir: &str, err: &str) -> String { + match self.lang { + Lang::Pl => format!("❌ Błąd: Nie można utworzyć katalogu {} ({})", dir, err), + Lang::En => format!("❌ Error: Cannot create directory {} ({})", dir, err), + } + } + + // ===================================================================== + // 2. LIB / CORE - LOGIKA BAZOWA + // ===================================================================== + pub fn skip_binary(&self) -> &'static str { + match self.lang { + Lang::Pl => "> *(Plik binarny/graficzny - pominięto zawartość)*", + Lang::En => "> *(Binary/graphic file - content skipped)*", + } + } + pub fn read_err(&self) -> &'static str { + match self.lang { + Lang::Pl => "> *(Błąd odczytu / plik nie jest UTF-8)*", + Lang::En => "> *(Read error / file is not UTF-8)*", + } + } + + pub fn footer_tool(&self) -> &str { + match self.lang { + Lang::Pl => "Narzędzie", + _ => "Tool", + } + } + pub fn footer_input(&self) -> &str { + match self.lang { + Lang::Pl => "Folder", + _ => "Input", + } + } + pub fn footer_cmd(&self) -> &str { + match self.lang { + Lang::Pl => "Komenda", + _ => "Command", + } + } + pub fn footer_tag(&self) -> &str { + match self.lang { + Lang::Pl => "Tag", + _ => "TimeTag", + } + } + pub fn footer_links(&self) -> &str { + match self.lang { + Lang::Pl => "Linki", + _ => "Links", + } + } + pub fn footer_help(&self) -> &str { + match self.lang { + Lang::Pl => "Pomoc", + _ => "Help", + } + } + + // ===================================================================== + // 3. CLI - INTERFEJS TERMINALOWY + // ===================================================================== + pub fn cli_base_abs(&self, path: &str) -> String { + match self.lang { + Lang::Pl => format!("📂 Baza terminala (Absolutna): {}", path), + Lang::En => format!("📂 Terminal base (Absolute): {}", path), + } + } + pub fn cli_target_abs(&self, path: &str) -> String { + match self.lang { + Lang::Pl => format!("📂 Cel skanowania (Absolutna): {}", path), + Lang::En => format!("📂 Scan target (Absolute): {}", path), + } + } + pub fn cli_target_rel(&self, path: &str) -> String { + match self.lang { + Lang::Pl => format!("📂 Cel skanowania (Relatywna): {}", path), + Lang::En => format!("📂 Scan target (Relative): {}", path), + } + } + pub fn cli_case_sensitive(&self, val: bool) -> String { + match self.lang { + Lang::Pl => format!("🔠 Wrażliwość na litery: {}", val), + Lang::En => format!("🔠 Case sensitive: {}", val), + } + } + pub fn cli_patterns_raw(&self, pat: &str) -> String { + match self.lang { + Lang::Pl => format!("🔍 Wzorce (RAW): {}", pat), + Lang::En => format!("🔍 Patterns (RAW): {}", pat), + } + } + pub fn cli_patterns_tok(&self, pat: &str) -> String { + match self.lang { + Lang::Pl => format!("⚙️ Wzorce (TOK): {}", pat), + Lang::En => format!("⚙️ Patterns (TOK): {}", pat), + } + } + pub fn cli_summary_matched(&self, count: usize, total: usize) -> String { + match self.lang { + Lang::Pl => format!("📊 Podsumowanie: Dopasowano {} z {} ścieżek.", count, total), + Lang::En => format!("📊 Summary: Matched {} of {} paths.", count, total), + } + } + pub fn cli_summary_rejected(&self, count: usize, total: usize) -> String { + match self.lang { + Lang::Pl => format!("📊 Podsumowanie: Odrzucono {} z {} ścieżek.", count, total), + Lang::En => format!("📊 Summary: Rejected {} of {} paths.", count, total), + } + } +} diff --git a/src/interfaces.rs b/src/interfaces.rs new file mode 100644 index 0000000..171421d --- /dev/null +++ b/src/interfaces.rs @@ -0,0 +1,6 @@ +// [ENG]: User interaction layer (Ports and Adapters). +// [POL]: Warstwa interakcji z użytkownikiem (Porty i Adaptery). + +pub mod cli; +pub mod gui; +pub mod tui; diff --git a/src/interfaces/cli.rs b/src/interfaces/cli.rs new file mode 100644 index 0000000..b298b53 --- /dev/null +++ b/src/interfaces/cli.rs @@ -0,0 +1,39 @@ +pub mod args; +pub mod engine; + +use self::args::CargoCli; +use clap::Parser; + +// [ENG]: Main entry point for the CLI interface and global router. +// [POL]: Główny punkt wejścia dla interfejsu CLI i globalny router. +pub fn run_cli() { + let args_os = std::env::args(); + let mut args: Vec = args_os.collect(); + + // ⚡ NOWOŚĆ: Jeśli wywołano bez żadnych argumentów (samo `cargo plot`), + // wstrzykujemy domyślnie flagę `-g` (GUI). + let is_empty = args.len() == 1 || (args.len() == 2 && args[1] == "plot"); + if is_empty { + args.push("-g".to_string()); + } + + // [ENG]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. + // [POL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. + if args.len() > 1 && args[1] != "plot" { + args.insert(1, "plot".to_string()); + } + + // [ENG]: Parse from the modified list. + // [POL]: Parsowanie ze zmodyfikowanej listy. + let CargoCli::Plot(flags) = CargoCli::parse_from(args); + + // [ENG]: Transfer control based on parsed flags. + // [POL]: Przekazanie kontroli na podstawie sparsowanych flag. + if flags.gui { + crate::interfaces::gui::run_gui(flags); + } else if flags.tui { + crate::interfaces::tui::run_tui(); + } else { + engine::run(flags); + } +} diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs new file mode 100644 index 0000000..58ba9a7 --- /dev/null +++ b/src/interfaces/cli/args.rs @@ -0,0 +1,285 @@ +use cargo_plot::core::path_matcher::SortStrategy; +use cargo_plot::core::path_view::ViewMode; +use cargo_plot::i18n::Lang; +use clap::{Args, Parser, ValueEnum}; + +/// [ENG]: Main wrapper for the Cargo plugin. +/// [POL]: Główny wrapper dla wtyczki Cargo. +#[derive(Parser, Debug)] +#[command(name = "cargo", bin_name = "cargo")] +pub enum CargoCli { + /// [ENG]: Cargo plot subcommand. + /// [POL]: Podkomenda cargo plot. + Plot(CliArgs), +} + +/// [ENG]: Command line arguments for cargo-plot. +/// [POL]: Argumenty wiersza poleceń dla cargo-plot. +#[derive(Args, Debug, Clone)] +#[command(author, version, about = "Skaner struktury plików / File structure scanner", long_about = None)] +pub struct CliArgs { + /// [ENG]: 📂 Input path to scan. + /// [POL]: 📂 Ścieżka wejściowa do skanowania. + #[arg(short = 'd', long = "dir", default_value = ".")] + pub enter_path: String, + + /// [ENG]: 💾 Output directory path for saved results. + /// [POL]: 💾 Ścieżka do katalogu wyjściowego na rezultaty. + #[arg(short = 'o', long = "dir-out", num_args = 0..=1, default_missing_value = "AUTO")] + pub dir_out: Option, + + /// [ENG]: 🔍 Match patterns. + /// [POL]: 🔍 Wzorce dopasowań. + #[arg(short = 'p', long = "pat", required_unless_present_any = ["gui", "tui"])] + pub patterns: Vec, + + /// [ENG]: ✔️ Treat patterns as match (include) rules. + /// [POL]: ✔️ Traktuj wzorce jako zasady dopasowania (włącz). + #[arg(short = 'm', long = "pat-match", required_unless_present_any = ["exclude", "gui", "tui"])] + pub include: bool, + + /// [ENG]: ❌ Treat patterns as mismatch (exclude) rules. + /// [POL]: ❌ Traktuj wzorce jako zasady odrzucenia (wyklucz). + #[arg(short = 'x', long = "pat-mismatch", required_unless_present_any = ["include", "gui", "tui"])] + pub exclude: bool, + + /// [ENG]: 🔠 Ignore case sensitivity in patterns. + /// [POL]: 🔠 Ignoruj wielkość liter we wzorcach. + #[arg(short = 'c', long = "pat-ignore-case")] + pub ignore_case: bool, + + /// [ENG]: 🗂️ Results sorting strategy. + /// [POL]: 🗂️ Strategia sortowania wyników. + #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] + pub sort: CliSortStrategy, + + /// [ENG]: 👁️ Selects the display format (tree, list, grid). + /// [POL]: 👁️ Wybiera format wyświetlania wyników (drzewo, lista, siatka). + #[arg(short = 'v', long = "view", value_enum, default_value_t = CliViewMode::Tree)] + pub view: CliViewMode, + + /// [ENG]: 📝 Save the paths structure to a file. + /// [POL]: 📝 Zapisuje strukturę ścieżek do pliku. + #[arg(long = "save-address")] + pub save_address: bool, + + /// [ENG]: 📦 Save the file contents archive to a file. + /// [POL]: 📦 Zapisuje archiwum z zawartością plików. + #[arg(long = "save-archive")] + pub save_archive: bool, + + /// [ENG]: 🏷️ Add a footer with command information to saved files. + /// [POL]: 🏷️ Dodaje stopkę z informacją o komendzie do zapisanych plików. + #[arg(short = 'b', long = "by")] + pub by: bool, + + /// [ENG]: 🌳 Hide the root directory in the tree view. + /// [POL]: 🌳 Ukrywa główny folder (korzeń) w widoku drzewa. + #[arg(long = "treeview-no-root")] + pub no_root: bool, + + /// [ENG]: ℹ️ Display summary statistics and headers. + /// [POL]: ℹ️ Wyświetla statystyki podsumowujące i nagłówki. + #[arg(short = 'i', long = "info")] + pub info: bool, + + /// [ENG]: 🚫 Disable emoji rendering in the output. + /// [POL]: 🚫 Wyłącza renderowanie ikon/emoji w wynikach. + #[arg(long = "no-emoji")] + pub no_emoji: bool, + + /// [ENG]: 🖥️ Launch the application in Graphical User Interface (GUI) mode. + /// [POL]: 🖥️ Uruchamia aplikację w trybie graficznym (GUI). + #[arg(short = 'g', long = "gui")] + pub gui: bool, + + /// [ENG]: ⌨️ Launch the application in Terminal User Interface (TUI) mode. + /// [POL]: ⌨️ Uruchamia aplikację w interaktywnym trybie terminalowym (TUI). + #[arg(short = 't', long = "tui")] + pub tui: bool, + + /// [ENG]: 🌍 Force a specific interface language. + /// [POL]: 🌍 Wymusza określony język interfejsu. + #[arg(long, value_enum)] + pub lang: Option, + + /// [ENG]: ⚖️ Weight unit system (dec for SI, bin for IEC). + /// [POL]: ⚖️ System jednostek wagi (dec dla SI, bin dla IEC). + #[arg(short = 'u', long = "unit", value_enum, default_value_t = CliUnitSystem::Bin)] + pub unit: CliUnitSystem, + + /// [ENG]: 🧮 Calculate actual folder weight including unmatched files. + /// [POL]: 🧮 Oblicza rzeczywistą wagę folderu wliczając wszystkie pliki. + #[arg(short = 'a', long = "all")] + pub all: bool, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum CliViewMode { + Tree, + List, + Grid, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum CliSortStrategy { + None, + Az, + Za, + AzFile, + ZaFile, + AzDir, + ZaDir, + AzFileMerge, + ZaFileMerge, + AzDirMerge, + ZaDirMerge, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum CliUnitSystem { + Dec, + Bin, +} + +impl From for SortStrategy { + fn from(val: CliSortStrategy) -> Self { + match val { + CliSortStrategy::None => SortStrategy::None, + CliSortStrategy::Az => SortStrategy::Az, + CliSortStrategy::Za => SortStrategy::Za, + CliSortStrategy::AzFile => SortStrategy::AzFileFirst, + CliSortStrategy::ZaFile => SortStrategy::ZaFileFirst, + CliSortStrategy::AzDir => SortStrategy::AzDirFirst, + CliSortStrategy::ZaDir => SortStrategy::ZaDirFirst, + CliSortStrategy::AzFileMerge => SortStrategy::AzFileFirstMerge, + CliSortStrategy::ZaFileMerge => SortStrategy::ZaFileFirstMerge, + CliSortStrategy::AzDirMerge => SortStrategy::AzDirFirstMerge, + CliSortStrategy::ZaDirMerge => SortStrategy::ZaDirFirstMerge, + } + } +} + +impl From for ViewMode { + fn from(val: CliViewMode) -> Self { + match val { + CliViewMode::Tree => ViewMode::Tree, + CliViewMode::List => ViewMode::List, + CliViewMode::Grid => ViewMode::Grid, + } + } +} + +impl CliArgs { + /// [ENG]: Reconstructs a clean terminal command string. + /// [POL]: Odtwarza czystą komendę terminalową. + pub fn to_command_string( + &self, + is_m: bool, + is_x: bool, + is_address: bool, + is_archive: bool, + ) -> String { + let mut cmd = vec!["cargo".to_string(), "plot".to_string()]; + + if self.enter_path != "." && !self.enter_path.is_empty() { + cmd.push("-d".to_string()); + cmd.push(format!("\"{}\"", self.enter_path)); + } + + if let Some(dir) = &self.dir_out { + cmd.push("-o".to_string()); + if dir != "AUTO" { + cmd.push(format!("\"{}\"", dir)); + } else { + cmd.push("AUTO".to_string()); + } + } + + // ⚡ POPRAWKA 1: Wzorce -p są teraz iterowane i dodawane osobno + if !self.patterns.is_empty() { + for pattern in &self.patterns { + cmd.push("-p".to_string()); + cmd.push(format!("\"{}\"", pattern)); + } + } + + // ⚡ GWARANCJA POPRAWNOŚCI: Komenda idealnie dopasowana do zapisywanego pliku + if is_m { + cmd.push("-m".to_string()); + } + if is_x { + cmd.push("-x".to_string()); + } + + if self.ignore_case { + cmd.push("-c".to_string()); + } + + if self.sort != CliSortStrategy::AzFileMerge { + let sort_str = match self.sort { + CliSortStrategy::None => "none", + CliSortStrategy::Az => "az", + CliSortStrategy::Za => "za", + CliSortStrategy::AzFile => "az-file", + CliSortStrategy::ZaFile => "za-file", + CliSortStrategy::AzDir => "az-dir", + CliSortStrategy::ZaDir => "za-dir", + CliSortStrategy::AzFileMerge => "az-file-merge", + CliSortStrategy::ZaFileMerge => "za-file-merge", + CliSortStrategy::AzDirMerge => "az-dir-merge", + CliSortStrategy::ZaDirMerge => "za-dir-merge", + }; + cmd.push("-s".to_string()); + cmd.push(sort_str.to_string()); + } + + if self.view != CliViewMode::Tree { + let view_str = match self.view { + CliViewMode::Tree => "tree", + CliViewMode::List => "list", + CliViewMode::Grid => "grid", + }; + cmd.push("-v".to_string()); + cmd.push(view_str.to_string()); + } + + // ⚡ GWARANCJA POPRAWNOŚCI: Wymuszamy flagi zapisu zależnie od tego, z jakiego miejsca generujemy raport + if self.save_address || is_address { + cmd.push("--save-address".to_string()); + } + if self.save_archive || is_archive { + cmd.push("--save-archive".to_string()); + } + if self.by { + cmd.push("-b".to_string()); + } + if self.no_root { + cmd.push("--treeview-no-root".to_string()); + } + if self.info { + cmd.push("-i".to_string()); + } + if self.no_emoji { + cmd.push("--no-emoji".to_string()); + } + if self.all { + cmd.push("-a".to_string()); + } + + if self.unit != CliUnitSystem::Bin { + cmd.push("-u".to_string()); + cmd.push("dec".to_string()); + } + + if let Some(l) = &self.lang { + cmd.push("--lang".to_string()); + match l { + Lang::Pl => cmd.push("pl".to_string()), + Lang::En => cmd.push("en".to_string()), + } + } + + cmd.join(" ") + } +} diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs new file mode 100644 index 0000000..318ccbb --- /dev/null +++ b/src/interfaces/cli/engine.rs @@ -0,0 +1,181 @@ +use crate::interfaces::cli::args::{CliArgs, CliUnitSystem}; +use cargo_plot::addon::TimeTag; +use cargo_plot::core::file_stats::weight::{UnitSystem, WeightConfig}; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::path_store::PathContext; +use cargo_plot::core::path_view::ViewMode; +use cargo_plot::core::save::SaveFile; +use cargo_plot::execute::{self, SortStrategy}; +use cargo_plot::i18n::I18n; + +/// [ENG]: ⚙️ Main execution engine coordinating the scanning and rendering process. +/// [POL]: ⚙️ Główny silnik wykonawczy koordynujący proces skanowania i renderowania. +pub fn run(args: CliArgs) { + // [ENG]: 📝 Initialize i18n and resolve basic flags. + // [POL]: 📝 Inicjalizacja i18n i rozwiązanie podstawowych flag. + let i18n = I18n::new(args.lang); + let is_case_sensitive = !args.ignore_case; + let sort_strategy: SortStrategy = args.sort.into(); + let view_mode: ViewMode = args.view.into(); + + // [ENG]: ⚖️ Define weight calculation rules based on unit and 'all' flags. + // [POL]: ⚖️ Definicja reguł obliczania wagi na podstawie flag jednostki oraz 'all'. + let weight_cfg = WeightConfig { + system: match args.unit { + CliUnitSystem::Bin => UnitSystem::Binary, + CliUnitSystem::Dec => UnitSystem::Decimal, + }, + // [POL]: Jeśli 'all' (-a) jest true, liczymy fizyczną wagę z dysku dla folderów. + dir_sum_included: !args.all, + ..WeightConfig::default() + }; + + // [ENG]: 🎚️ Determines the display mode based on include (-m) and exclude (-x) flags. + // [POL]: 🎚️ Ustala tryb wyświetlania na podstawie flag włączania (-m) i wykluczania (-x). + let show_mode = match (args.include, args.exclude) { + (true, false) => ShowMode::Include, + (false, true) => ShowMode::Exclude, + _ => ShowMode::Context, + }; + + // [ENG]: 🚀 Executes the core matching logic with prepared weight configuration. + // [POL]: 🚀 Wykonuje główną logikę dopasowywania z przygotowaną konfiguracją wagi. + let stats = execute::execute( + &args.enter_path, + &args.patterns, + is_case_sensitive, + sort_strategy, + show_mode, + view_mode, + weight_cfg, // ⚡ WSTRZYKNIĘTE + args.no_root, + args.info, + args.no_emoji, + &i18n, + |_| {}, + |_| {}, + ); + + // [ENG]: 🖥️ Renders the output to the terminal with ANSI colors. + // [POL]: 🖥️ Renderuje wynik do terminala z użyciem kolorów ANSI. + let output_str_cli = stats.render_output(view_mode, show_mode, args.info, true); + print!("{}", output_str_cli); + + // [ENG]: 💾 Handles file saving if address or archive flags are active. + // [POL]: 💾 Obsługuje zapis do plików, jeśli aktywne są flagi adresu lub archiwum. + if args.save_address || args.save_archive { + let tag = TimeTag::now(); + + // [ENG]: 📄 Renders plain text for Markdown output. + // [POL]: 📄 Renderuje czysty tekst dla wyjścia w formacie Markdown. + let output_str_txt_m = stats.render_output(view_mode, ShowMode::Include, args.info, false); + let output_str_txt_x = stats.render_output(view_mode, ShowMode::Exclude, args.info, false); + + // [ENG]: 📂 Resolves the output directory path to .cargo-plot/ by default. + // [POL]: 📂 Rozwiązuje ścieżkę katalogu wyjściowego (domyślnie na .cargo-plot/). + let resolve_dir = |val: &Option, base_path: &str| -> String { + let is_auto = val + .as_ref() + .is_none_or(|v| v.trim().is_empty() || v == "AUTO"); + if is_auto { + let mut b = base_path.replace('\\', "/"); + if !b.ends_with('/') { + b.push('/'); + } + format!("{}.cargo-plot/", b) + } else { + let mut p = val.as_ref().unwrap().replace('\\', "/"); + if !p.ends_with('/') { + p.push('/'); + } + p + } + }; + + let output_dir = resolve_dir(&args.dir_out, &args.enter_path); + + // [ENG]: 📝 Saves the path structure (address). + // [POL]: 📝 Zapisuje strukturę ścieżek (adres). + if args.save_address { + if args.include || !args.exclude { + let filepath = format!("{}plot-address_{}_M.md", output_dir, tag); + let cmd_m = args.to_command_string(true, false, true, false); + SaveFile::paths( + &output_str_txt_m, + &filepath, + &tag, + args.by, + &i18n, + &cmd_m, + &args.enter_path, + ); + } + if args.exclude || !args.include { + let filepath = format!("{}plot-address_{}_X.md", output_dir, tag); + let cmd_x = args.to_command_string(false, true, true, false); + SaveFile::paths( + &output_str_txt_x, + &filepath, + &tag, + args.by, + &i18n, + &cmd_x, + &args.enter_path, + ); + } + } + + // [ENG]: 📦 Saves the full file contents (archive). + // [POL]: 📦 Zapisuje pełną zawartość plików (archiwum). + if args.save_archive + && let Ok(ctx) = PathContext::resolve(&args.enter_path) + { + if args.include || !args.exclude { + let filepath = format!("{}plot-archive_{}_M.md", output_dir, tag); + let cmd_m = args.to_command_string(true, false, false, true); + SaveFile::codes( + &output_str_txt_m, + &stats.m_matched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_m, + &args.enter_path, + ); + } + if args.exclude || !args.include { + let filepath = format!("{}plot-archive_{}_X.md", output_dir, tag); + let cmd_x = args.to_command_string(false, true, false, true); + SaveFile::codes( + &output_str_txt_x, + &stats.x_mismatched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_x, + &args.enter_path, + ); + } + } + } + + // [ENG]: 📊 Prints summary statistics if info flag is active. + // [POL]: 📊 Wyświetla statystyki podsumowujące, jeśli aktywna jest flaga info. + if args.info { + println!("---------------------------------------"); + println!( + "{}", + i18n.cli_summary_matched(stats.m_size_matched, stats.total) + ); + println!( + "{}", + i18n.cli_summary_rejected(stats.x_size_mismatched, stats.total) + ); + } else { + println!("---------------------------------------"); + } +} diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs new file mode 100644 index 0000000..f9b272f --- /dev/null +++ b/src/interfaces/gui.rs @@ -0,0 +1,151 @@ +pub mod code; +pub mod i18n; +pub mod paths; +pub mod settings; +pub mod shared; + +use crate::interfaces::cli::args::CliArgs; +use eframe::egui; + +#[derive(PartialEq)] +pub enum Tab { + Settings, + Paths, + Code, +} + +#[derive(PartialEq)] +pub enum PathsTab { + Match, + Mismatch, +} + +// ⚡ Dodana zakładka dla karty "Kod" +#[derive(PartialEq)] +pub enum CodeTab { + Match, + Mismatch, +} + +#[derive(Default, Clone)] +pub struct TreeStats { + pub txt_count: usize, + pub txt_weight: u64, + pub bin_count: usize, + pub bin_weight: u64, + pub err_count: usize, + pub err_weight: u64, + pub empty_count: usize, + pub matched_count: usize, + pub total_count: usize, +} + +pub struct CargoPlotApp { + pub args: CliArgs, + pub active_tab: Tab, + pub active_paths_tab: PathsTab, + pub active_code_tab: CodeTab, + pub new_pattern_input: String, + pub out_path_input: String, + pub generated_paths_m: String, + pub generated_paths_x: String, + pub generated_code_m: String, + pub generated_code_x: String, + pub stats_m: TreeStats, + pub stats_x: TreeStats, + pub ui_scale: f32, +} + +impl CargoPlotApp { + pub fn new(args: CliArgs) -> Self { + let default_out = args.dir_out.clone().unwrap_or_default(); + Self { + args, + active_tab: Tab::Settings, + active_paths_tab: PathsTab::Match, + active_code_tab: CodeTab::Match, // ⚡ Domyślnie ładujemy zakładkę MATCH + new_pattern_input: String::new(), + out_path_input: default_out, // Inicjalizacja ścieżki + generated_paths_m: String::new(), + generated_paths_x: String::new(), + generated_code_m: String::new(), // ⚡ Pusty na start + generated_code_x: String::new(), // ⚡ Pusty na start + stats_m: TreeStats::default(), + stats_x: TreeStats::default(), + ui_scale: 1.0, + } + } +} + +impl eframe::App for CargoPlotApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + ctx.set_zoom_factor(self.ui_scale); + + // GÓRNY PANEL (Teraz tylko 3 karty) + egui::TopBottomPanel::top("top_tabs").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.selectable_value(&mut self.active_tab, Tab::Settings, "Setting\nUstawienia"); + ui.selectable_value(&mut self.active_tab, Tab::Paths, "Paths\nŚcieżki"); + ui.selectable_value(&mut self.active_tab, Tab::Code, "Code\nKod"); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.add_space(10.0); + + if ui + .button("➕") + .on_hover_text("Powiększ (Zoom in)") + .clicked() + { + self.ui_scale += 0.1; // Powiększa o 10% + } + + if ui + .button("🔄") + .on_hover_text("Resetuj skalę (100%)") + .clicked() + { + self.ui_scale = 1.0; // Wraca do standardu + } + + if ui + .button("➖") + .on_hover_text("Pomniejsz (Zoom out)") + .clicked() + && self.ui_scale > 0.6 + { + // Zabezpieczenie, żeby nie zmniejszyć za bardzo + self.ui_scale -= 0.1; + } + + // Wyświetla aktualny procent powiększenia (np. "120%") + ui.label( + egui::RichText::new(format!("🔍 Skala: {:.0}%", self.ui_scale * 100.0)) + .weak(), + ); + }); + }); + }); + + // ŚRODEK OKNA + egui::CentralPanel::default().show(ctx, |ui| match self.active_tab { + Tab::Settings => settings::show(ui, self), + Tab::Paths => paths::show(ui, self), + Tab::Code => code::show(ui, self), + }); + } +} + +pub fn run_gui(args: CliArgs) { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_inner_size([900.0, 700.0]) + .with_title("cargo-plot"), + ..Default::default() + }; + eframe::run_native( + "cargo-plot", + options, + Box::new(|_cc| Ok(Box::new(CargoPlotApp::new(args)))), + ) + .unwrap(); +} diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs new file mode 100644 index 0000000..ff80b9c --- /dev/null +++ b/src/interfaces/gui/code.rs @@ -0,0 +1,241 @@ +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; +use crate::interfaces::gui::shared::{draw_editor, draw_footer, draw_tabs, resolve_dir}; +use crate::interfaces::gui::{CargoPlotApp, CodeTab, TreeStats}; +use cargo_plot::addon::TimeTag; +use cargo_plot::core::file_stats::FileStats; +use cargo_plot::core::file_stats::weight::{UnitSystem, WeightConfig}; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::save::is_blacklisted_extension; +use cargo_plot::execute; +use eframe::egui; + +/// [ENG]: View function for the Code tab, managing source extraction and statistics. +/// [POL]: Funkcja widoku dla karty Kod, zarządzająca ekstrakcją źródeł i statystykami. +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + let gt = GuiI18n::new(app.args.lang); + + // [ENG]: 1. TOP TABS - Navigation between matched and mismatched code buffers. + // [POL]: 1. GÓRNE ZAKŁADKI - Nawigacja między buforami kodu dopasowanego i odrzuconego. + let mut is_match = app.active_code_tab == CodeTab::Match; + draw_tabs(ui, >, &mut is_match); + app.active_code_tab = if is_match { + CodeTab::Match + } else { + CodeTab::Mismatch + }; + + ui.separator(); + + // [ENG]: 2. ACTION BAR - Controls for code generation and archival save. + // [POL]: 2. PASEK AKCJI - Sterowanie generowaniem kodu i zapisem archiwalnym. + ui.horizontal(|ui| { + if ui.button(gt.t(GT::BtnGenerateCode)).clicked() { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let show_mode = if is_match { + ShowMode::Include + } else { + ShowMode::Exclude + }; + + // [ENG]: Weight configuration remains fixed for code extraction to ensure consistency. + // [POL]: Konfiguracja wagi pozostaje stała dla ekstrakcji kodu, aby zapewnić spójność. + let weight_cfg = WeightConfig { + system: if app.args.unit == crate::interfaces::cli::args::CliUnitSystem::Bin { + UnitSystem::Binary + } else { + UnitSystem::Decimal + }, + dir_sum_included: !app.args.all, + ..WeightConfig::default() + }; + + let mut st_m = TreeStats::default(); + let mut st_x = TreeStats::default(); + + // [ENG]: Execute main engine with closures for statistics and file classification. + // [POL]: Wykonanie głównego silnika z domknięciami dla statystyk i klasyfikacji plików. + let stats = execute::execute( + &app.args.enter_path, + &app.args.patterns, + !app.args.ignore_case, + app.args.sort.into(), + show_mode, + app.args.view.into(), + weight_cfg, + app.args.no_root, + false, + app.args.no_emoji, + &i18n, + |f: &FileStats| { + if f.weight_bytes == 0 { + st_m.empty_count += 1; + } + if !f.path.ends_with('/') { + let ext = f + .absolute + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if is_blacklisted_extension(&ext) { + st_m.bin_count += 1; + st_m.bin_weight += f.weight_bytes; + } else { + st_m.txt_count += 1; + st_m.txt_weight += f.weight_bytes; + } + } + }, + |f: &FileStats| { + if f.weight_bytes == 0 { + st_x.empty_count += 1; + } + if !f.path.ends_with('/') { + let ext = f + .absolute + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if is_blacklisted_extension(&ext) { + st_x.bin_count += 1; + st_x.bin_weight += f.weight_bytes; + } else { + st_x.txt_count += 1; + st_x.txt_weight += f.weight_bytes; + } + } + }, + ); + + st_m.matched_count = stats.m_size_matched; + st_m.total_count = stats.total; + st_x.matched_count = stats.x_size_mismatched; + st_x.total_count = stats.total; + + app.stats_m = st_m; + app.stats_x = st_x; + + let base_dir = std::path::Path::new(&app.args.enter_path); + + // [ENG]: Process code extraction for the selected result set. + // [POL]: Przetwarzanie ekstrakcji kodu dla wybranego zestawu wyników. + if is_match { + let tree_m = + stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + let mut content_m = format!("```plaintext\n{}\n```\n\n", tree_m); + let mut counter_m = 1; + for p_str in &stats.m_matched.paths { + if p_str.ends_with('/') { + continue; + } + let absolute_path = base_dir.join(p_str); + match std::fs::read_to_string(&absolute_path) { + Ok(txt) => content_m.push_str(&format!( + "### {:03}: `{}`\n\n```rust\n{}\n```\n\n", + counter_m, p_str, txt + )), + Err(_) => content_m.push_str(&format!( + "### {:03}: `{}`\n\n{}\n\n", + counter_m, + p_str, + gt.t(GT::LabelSkipBinary) + )), + } + counter_m += 1; + } + app.generated_code_m = content_m; + } else { + let tree_x = + stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); + let mut content_x = format!("```plaintext\n{}\n```\n\n", tree_x); + let mut counter_x = 1; + for p_str in &stats.x_mismatched.paths { + if p_str.ends_with('/') { + continue; + } + let absolute_path = base_dir.join(p_str); + match std::fs::read_to_string(&absolute_path) { + Ok(txt) => content_x.push_str(&format!( + "### {:03}: `{}`\n\n```rust\n{}\n```\n\n", + counter_x, p_str, txt + )), + Err(_) => content_x.push_str(&format!( + "### {:03}: `{}`\n\n{}\n\n", + counter_x, + p_str, + gt.t(GT::LabelSkipBinary) + )), + } + counter_x += 1; + } + app.generated_code_x = content_x; + } + } + + ui.add_space(15.0); + ui.checkbox(&mut app.args.by, gt.t(GT::LabelAddFooter)); + ui.add_space(15.0); + + // [ENG]: Archival saving with metadata table. + // [POL]: Zapis archiwalny z tabelą metadanych. + if is_match { + if ui.button(gt.t(GT::BtnSaveMatch)).clicked() { + let tag = TimeTag::now(); + let filepath = format!( + "{}plot-archive_{}_M.md", + resolve_dir(&app.args.dir_out, &app.args.enter_path), + tag + ); + let mut final_text = app.generated_code_m.clone(); + if app.args.by { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(true, false, false, true); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( + &tag, + &app.args.enter_path, + &i18n, + &cmd_string, + )); + } + let _ = std::fs::write(&filepath, final_text); + } + } else { + if ui.button(gt.t(GT::BtnSaveMismatch)).clicked() { + let tag = TimeTag::now(); + let filepath = format!( + "{}plot-archive_{}_X.md", + resolve_dir(&app.args.dir_out, &app.args.enter_path), + tag + ); + let mut final_text = app.generated_code_x.clone(); + if app.args.by { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(false, true, false, true); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( + &tag, + &app.args.enter_path, + &i18n, + &cmd_string, + )); + } + let _ = std::fs::write(&filepath, final_text); + } + } + }); + + ui.separator(); + + // [ENG]: 3. FOOTER - Update statistics pinned to the bottom. + // [POL]: 3. STOPKA - Aktualizacja statystyk przypiętych do dołu. + let current_stats = if is_match { &app.stats_m } else { &app.stats_x }; + draw_footer(ui, "code_stats_footer", current_stats); + + // [ENG]: 4. MAIN EDITOR - Display extracted file contents. + // [POL]: 4. GŁÓWNY EDYTOR - Widok wyekstrahowanej zawartości plików. + let text_buffer = match app.active_code_tab { + CodeTab::Match => &mut app.generated_code_m, + CodeTab::Mismatch => &mut app.generated_code_x, + }; + draw_editor(ui, text_buffer); +} diff --git a/src/interfaces/gui/i18n.rs b/src/interfaces/gui/i18n.rs new file mode 100644 index 0000000..b9a83bf --- /dev/null +++ b/src/interfaces/gui/i18n.rs @@ -0,0 +1,100 @@ +// [ENG]: GUI Internationalization module. +// [POL]: Moduł internacjonalizacji interfejsu graficznego. + +use cargo_plot::i18n::Lang; + +pub struct GuiI18n { + pub lang: Lang, +} + +pub enum GuiText { + LabelLang, + LabelScanPath, + LabelOutFolder, + LabelSorting, + LabelViewMode, + LabelNoRoot, + HeadingPatterns, + LabelIgnoreCase, + LabelNewPattern, + BtnAddPattern, + BtnClearAll, + BtnBrowse, + MsgNoPatterns, + FooterDownload, + FooterInstall, + FooterUninstall, + BtnGenerate, + LabelAddFooter, + BtnSaveMatch, + BtnSaveMismatch, + TabMatch, + TabMismatch, + BtnGenerateCode, + LabelSkipBinary, +} + +impl GuiI18n { + pub fn new(lang: Option) -> Self { + Self { + lang: lang.unwrap_or(Lang::En), + } + } + + pub fn t(&self, text: GuiText) -> &'static str { + match self.lang { + Lang::Pl => match text { + GuiText::LabelLang => "🌍 Język:", + GuiText::LabelScanPath => "📂 Ścieżka skanowania:", + GuiText::LabelOutFolder => "💾 Folder zapisu (Output):", + GuiText::LabelSorting => "Sortowanie", + GuiText::LabelViewMode => "Tryb widoku", + GuiText::LabelNoRoot => "Ukryj ROOT w drzewie", + GuiText::HeadingPatterns => "🔍 Wzorce dopasowań (Patterns)", + GuiText::LabelIgnoreCase => "🔠 Ignoruj wielkość liter", + GuiText::LabelNewPattern => "Nowy:", + GuiText::BtnAddPattern => "➕ Dodaj wzorzec", + GuiText::BtnClearAll => "💣 Usuń wszystkie", + GuiText::BtnBrowse => "Wybierz...", + GuiText::MsgNoPatterns => "Brak wzorców. Dodaj przynajmniej jeden!", + GuiText::FooterDownload => "Pobierz binarkę (GitHub)", + GuiText::FooterInstall => "Instalacja:", + GuiText::FooterUninstall => "Usuwanie:", + GuiText::BtnGenerate => "🔄 Generuj / Regeneruj", + GuiText::LabelAddFooter => "Dodaj stopkę (--by)", + GuiText::BtnSaveMatch => "💾 Zapisz (-m)", + GuiText::BtnSaveMismatch => "💾 Zapisz (-x)", + GuiText::TabMatch => "✔ (-m) MATCH", + GuiText::TabMismatch => "✖ (-x) MISMATCH", + GuiText::BtnGenerateCode => "🔄 Generuj kod (Cache)", + GuiText::LabelSkipBinary => "> *(Pominięto plik binarny/graficzny)*", + }, + Lang::En => match text { + GuiText::LabelLang => "🌍 Language:", + GuiText::LabelScanPath => "📂 Scan path:", + GuiText::LabelOutFolder => "💾 Output folder:", + GuiText::LabelSorting => "Sorting", + GuiText::LabelViewMode => "View mode", + GuiText::LabelNoRoot => "Hide ROOT in tree", + GuiText::HeadingPatterns => "🔍 Match Patterns", + GuiText::LabelIgnoreCase => "🔠 Ignore case", + GuiText::LabelNewPattern => "New:", + GuiText::BtnAddPattern => "➕ Add pattern", + GuiText::BtnClearAll => "💣 Clear all", + GuiText::BtnBrowse => "Browse...", + GuiText::MsgNoPatterns => "No patterns. Add at least one!", + GuiText::FooterDownload => "Download binary (GitHub)", + GuiText::FooterInstall => "Install:", + GuiText::FooterUninstall => "Uninstall:", + GuiText::BtnGenerate => "🔄 Generate / Regenerate", + GuiText::LabelAddFooter => "Add footer (--by)", + GuiText::BtnSaveMatch => "💾 Save (-m)", + GuiText::BtnSaveMismatch => "💾 Save (-x)", + GuiText::TabMatch => "✔ (-m) MATCH", + GuiText::TabMismatch => "✖ (-x) MISMATCH", + GuiText::BtnGenerateCode => "🔄 Generate code (Cache)", + GuiText::LabelSkipBinary => "> *(Binary/graphic file skipped)*", + }, + } + } +} diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs new file mode 100644 index 0000000..d80d6eb --- /dev/null +++ b/src/interfaces/gui/paths.rs @@ -0,0 +1,215 @@ +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; +use crate::interfaces::gui::shared::{draw_editor, draw_footer, draw_tabs, resolve_dir}; +use crate::interfaces::gui::{CargoPlotApp, PathsTab, TreeStats}; +use cargo_plot::addon::TimeTag; +use cargo_plot::core::file_stats::FileStats; +use cargo_plot::core::file_stats::weight::{UnitSystem, WeightConfig}; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::save::is_blacklisted_extension; +use cargo_plot::execute; +use eframe::egui; + +/// [ENG]: View function for the Paths tab, managing structure generation and unit toggling. +/// [POL]: Funkcja widoku dla karty Ścieżki, zarządzająca generowaniem struktury i przełączaniem jednostek. +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + let gt = GuiI18n::new(app.args.lang); + + // [ENG]: 1. TOP TABS - Sub-navigation for Match/Mismatch results. + // [POL]: 1. GÓRNE ZAKŁADKI - Podnawigacja dla wyników Match/Mismatch. + let mut is_match = app.active_paths_tab == PathsTab::Match; + draw_tabs(ui, >, &mut is_match); + app.active_paths_tab = if is_match { + PathsTab::Match + } else { + PathsTab::Mismatch + }; + + ui.separator(); + + // [ENG]: 2. ACTION BAR - Controls for generation, unit systems, and file saving. + // [POL]: 2. PASEK AKCJI - Sterowanie generowaniem, systemami jednostek i zapisem plików. + ui.horizontal(|ui| { + // [ENG]: Logic for triggering data generation. + // [POL]: Logika wyzwalająca generowanie danych. + if ui.button(gt.t(GT::BtnGenerate)).clicked() { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let show_mode = if is_match { + ShowMode::Include + } else { + ShowMode::Exclude + }; + + // [ENG]: Construct WeightConfig based on current application settings (-u and -a flags). + // [POL]: Konstrukcja WeightConfig na podstawie bieżących ustawień aplikacji (flagi -u oraz -a). + let weight_cfg = WeightConfig { + system: if app.args.unit == crate::interfaces::cli::args::CliUnitSystem::Bin { + UnitSystem::Binary + } else { + UnitSystem::Decimal + }, + dir_sum_included: !app.args.all, + ..WeightConfig::default() + }; + + let mut st_m = TreeStats::default(); + let mut st_x = TreeStats::default(); + + // [ENG]: Execute scan with statistics collectors via closures. + // [POL]: Wykonanie skanowania z kolektorami statystyk przez domknięcia. + let stats = execute::execute( + &app.args.enter_path, + &app.args.patterns, + !app.args.ignore_case, + app.args.sort.into(), + show_mode, + app.args.view.into(), + weight_cfg, + app.args.no_root, + false, + app.args.no_emoji, + &i18n, + |f: &FileStats| { + if f.weight_bytes == 0 { + st_m.empty_count += 1; + } + if !f.path.ends_with('/') { + let ext = f + .absolute + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if is_blacklisted_extension(&ext) { + st_m.bin_count += 1; + st_m.bin_weight += f.weight_bytes; + } else { + st_m.txt_count += 1; + st_m.txt_weight += f.weight_bytes; + } + } + }, + |f: &FileStats| { + if f.weight_bytes == 0 { + st_x.empty_count += 1; + } + if !f.path.ends_with('/') { + let ext = f + .absolute + .extension() + .unwrap_or_default() + .to_string_lossy() + .to_lowercase(); + if is_blacklisted_extension(&ext) { + st_x.bin_count += 1; + st_x.bin_weight += f.weight_bytes; + } else { + st_x.txt_count += 1; + st_x.txt_weight += f.weight_bytes; + } + } + }, + ); + + // [ENG]: Update application state with results and calculated statistics. + // [POL]: Aktualizacja stanu aplikacji o wyniki i obliczone statystyki. + st_m.matched_count = stats.m_size_matched; + st_m.total_count = stats.total; + st_x.matched_count = stats.x_size_mismatched; + st_x.total_count = stats.total; + + app.stats_m = st_m; + app.stats_x = st_x; + + if is_match { + app.generated_paths_m = + stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + } else { + app.generated_paths_x = + stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); + } + } + + ui.add_space(10.0); + ui.checkbox(&mut app.args.by, gt.t(GT::LabelAddFooter)); + + ui.add_space(15.0); + + // [ENG]: Live unit system toggle. Label is pre-calculated to avoid borrow-checker conflicts. + // [POL]: Przełącznik systemu jednostek na żywo. Etykieta obliczona wcześniej, by uniknąć konfliktów borrow-checkera. + let mut is_bin = app.args.unit == crate::interfaces::cli::args::CliUnitSystem::Bin; + let unit_label = if is_bin { "IEC (Bin)" } else { "SI (Dec)" }; + + if ui + .checkbox(&mut is_bin, unit_label) + .on_hover_text("B/KB vs B/KiB") + .changed() + { + app.args.unit = if is_bin { + crate::interfaces::cli::args::CliUnitSystem::Bin + } else { + crate::interfaces::cli::args::CliUnitSystem::Dec + }; + } + + ui.add_space(15.0); + + // [ENG]: Handle contextual save actions. + // [POL]: Obsługa kontekstowych akcji zapisu. + if is_match { + if ui.button(gt.t(GT::BtnSaveMatch)).clicked() { + let tag = TimeTag::now(); + let filepath = format!( + "{}plot-address_{}_M.md", + resolve_dir(&app.args.dir_out, &app.args.enter_path), + tag + ); + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(true, false, true, false); + cargo_plot::core::save::SaveFile::paths( + &app.generated_paths_m, + &filepath, + &tag, + app.args.by, + &i18n, + &cmd_string, + &app.args.enter_path, + ); + } + } else { + if ui.button(gt.t(GT::BtnSaveMismatch)).clicked() { + let tag = TimeTag::now(); + let filepath = format!( + "{}plot-address_{}_X.md", + resolve_dir(&app.args.dir_out, &app.args.enter_path), + tag + ); + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(false, true, true, false); + cargo_plot::core::save::SaveFile::paths( + &app.generated_paths_x, + &filepath, + &tag, + app.args.by, + &i18n, + &cmd_string, + &app.args.enter_path, + ); + } + } + }); + + ui.separator(); + + // [ENG]: 3. FOOTER - Statistics display. + // [POL]: 3. STOPKA - Wyświetlanie statystyk. + let current_stats = if is_match { &app.stats_m } else { &app.stats_x }; + draw_footer(ui, "paths_stats_footer", current_stats); + + // [ENG]: 4. MAIN EDITOR - Generated content area. + // [POL]: 4. GŁÓWNY EDYTOR - Obszar wygenerowanej treści. + let text_buffer = match app.active_paths_tab { + PathsTab::Match => &mut app.generated_paths_m, + PathsTab::Mismatch => &mut app.generated_paths_x, + }; + draw_editor(ui, text_buffer); +} diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs new file mode 100644 index 0000000..16bd55b --- /dev/null +++ b/src/interfaces/gui/settings.rs @@ -0,0 +1,293 @@ +use crate::interfaces::cli::args::{CliSortStrategy, CliViewMode}; +use crate::interfaces::gui::CargoPlotApp; +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; +use cargo_plot::i18n::Lang; +use eframe::egui; + +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + // [ENG]: Initialize the GUI translation engine. + // [POL]: Inicjalizacja silnika tłumaczeń GUI. + let gt = GuiI18n::new(app.args.lang); + + // [ENG]: 1. Pinned Footer - Attached to the bottom of the screen. + // [POL]: 1. Przyklejona stopka - Przypięta do dołu ekranu. + egui::TopBottomPanel::bottom("settings_footer_panel") + .resizable(false) + .show_inside(ui, |ui| { + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("📦 cargo-plot v0.2.0").strong()); + ui.separator(); + ui.hyperlink_to("Crates.io", "https://crates.io/crates/cargo-plot"); + ui.separator(); + ui.hyperlink_to( + gt.t(GT::FooterDownload), + "https://github.com/j-Cis/cargo-plot/releases", + ); + }); + + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new(gt.t(GT::FooterInstall)).weak()); + ui.code("cargo install cargo-plot"); + ui.separator(); + ui.label(egui::RichText::new(gt.t(GT::FooterUninstall)).weak()); + ui.code("cargo uninstall cargo-plot"); + }); + ui.add_space(10.0); + }); + + // [ENG]: 2. Main Content Area - Scrollable settings. + // [POL]: 2. Główny obszar treści - Przewijalne ustawienia. + egui::CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + // ⚡ Ustawiamy globalny limit szerokości (bez ucinania krawędzi) + ui.set_max_width(600.0); + ui.add_space(10.0); + + // [ENG]: Language selection. + // [POL]: Wybór języka. + ui.horizontal(|ui| { + ui.label(gt.t(GT::LabelLang)); + ui.radio_value(&mut app.args.lang, Some(Lang::Pl), "Polski"); + ui.radio_value(&mut app.args.lang, Some(Lang::En), "English"); + }); + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // [ENG]: Path Selection Grid - Perfectly aligns labels and inputs to the right edge. + // [POL]: Siatka wyboru ścieżek - Idealnie wyrównuje etykiety i pola do prawej krawędzi. + egui::Grid::new("path_settings_grid") + .num_columns(2) + .spacing([10.0, 10.0]) + .min_col_width(120.0) + .show(ui, |ui| { + // Row 1: Scan path + ui.label(gt.t(GT::LabelScanPath)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button(gt.t(GT::BtnBrowse)).clicked() + && let Some(folder) = rfd::FileDialog::new().pick_folder() + { + app.args.enter_path = folder.to_string_lossy().replace('\\', "/"); + } + ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut app.args.enter_path), + ); + }); + ui.end_row(); + + // Row 2: Output folder + ui.label(gt.t(GT::LabelOutFolder)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button(gt.t(GT::BtnBrowse)).clicked() + && let Some(folder) = rfd::FileDialog::new().pick_folder() + { + let mut path = folder.to_string_lossy().replace('\\', "/"); + if !path.ends_with('/') { + path.push('/'); + } + app.out_path_input = path.clone(); + app.args.dir_out = Some(path); + } + + let txt_response = ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut app.out_path_input), + ); + if txt_response.changed() { + let trimmed = app.out_path_input.trim(); + app.args.dir_out = if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + }; + } + }); + ui.end_row(); + }); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // [ENG]: View and Sorting. + // [POL]: Widok i sortowanie. + ui.horizontal(|ui| { + egui::ComboBox::from_label(gt.t(GT::LabelSorting)) + .selected_text(format!("{:?}", app.args.sort)) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut app.args.sort, + CliSortStrategy::AzFileMerge, + "AzFileMerge", + ); + ui.selectable_value( + &mut app.args.sort, + CliSortStrategy::ZaFileMerge, + "ZaFileMerge", + ); + ui.selectable_value( + &mut app.args.sort, + CliSortStrategy::AzDirMerge, + "AzDirMerge", + ); + ui.selectable_value( + &mut app.args.sort, + CliSortStrategy::ZaDirMerge, + "ZaDirMerge", + ); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::AzFile, "AzFile"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::ZaFile, "ZaFile"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::AzDir, "AzDir"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::ZaDir, "ZaDir"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::Az, "Az"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::Za, "Za"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::None, "None"); + }); + + ui.add_space(15.0); + + egui::ComboBox::from_label(gt.t(GT::LabelViewMode)) + .selected_text(format!("{:?}", app.args.view)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut app.args.view, CliViewMode::Tree, "Tree"); + ui.selectable_value(&mut app.args.view, CliViewMode::List, "List"); + ui.selectable_value(&mut app.args.view, CliViewMode::Grid, "Grid"); + }); + + ui.add_space(15.0); + ui.checkbox(&mut app.args.no_root, gt.t(GT::LabelNoRoot)); + ui.add_space(15.0); + ui.checkbox(&mut app.args.all, "Fizyczna waga folderów (-a)"); + }); + + ui.add_space(20.0); + + // [ENG]: Match Patterns Section. + // [POL]: Sekcja wzorców dopasowań. + ui.heading(gt.t(GT::HeadingPatterns)); + ui.add_space(15.0); + + ui.horizontal(|ui| { + ui.checkbox(&mut app.args.ignore_case, gt.t(GT::LabelIgnoreCase)); + ui.label(gt.t(GT::LabelNewPattern)); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let btn_clicked = ui.button(gt.t(GT::BtnAddPattern)).clicked(); + let response = ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut app.new_pattern_input), + ); + + if (btn_clicked + || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))) + && !app.new_pattern_input.trim().is_empty() + { + let input = app.new_pattern_input.trim(); + + // ⚡ FAST-TRACK: Automatyczne parsowanie ciągów z CLI + if input.contains("-p ") || input.contains("--pat ") { + // Ujednolicamy znacznik flagi + let normalized = input.replace("--pat ", "-p "); + + for part in normalized.split("-p ") { + let mut trimmed = part.trim(); + + // Ignorujemy śmieci takie jak komenda bazowa na początku + if trimmed.starts_with("cargo") || trimmed.is_empty() { + continue; + } + + // Zdejmujemy cudzysłowy i odcinamy ewentualne inne flagi na końcu ciągu + if (trimmed.starts_with('"') && trimmed.ends_with('"')) + || (trimmed.starts_with('\'') && trimmed.ends_with('\'')) + { + trimmed = &trimmed[1..trimmed.len() - 1]; // Idealne cudzysłowy po obu stronach + } else if trimmed.starts_with('"') || trimmed.starts_with('\'') { + // Zaczyna się od cudzysłowu, ale ma śmieci po nim (np. inne flagi -i) + let quote = trimmed.chars().next().unwrap(); + if let Some(end_idx) = trimmed[1..].find(quote) { + trimmed = &trimmed[1..=end_idx]; + } + } else if let Some(space_idx) = trimmed.find(' ') { + // Brak cudzysłowów, ucinamy do pierwszej spacji (inne flagi) + trimmed = &trimmed[..space_idx]; + } + + if !trimmed.is_empty() { + app.args.patterns.push(trimmed.to_string()); + } + } + } else { + // Zwykłe dodanie pojedynczego wzorca wpisanego ręcznie + app.args.patterns.push(input.to_string()); + } + + app.new_pattern_input.clear(); + response.request_focus(); + } + }); + }); + + ui.add_space(5.0); + egui::Frame::group(ui.style()).show(ui, |ui| { + ui.set_min_height(200.0); + // ⚡ Naprawa krawędzi: Wypełnia idealnie dostępną przestrzeń (z uwzględnieniem paddingu ramki) + ui.set_min_width(ui.available_width()); + + let mut move_up = None; + let mut move_down = None; + let mut remove = None; + + for (i, pat) in app.args.patterns.iter().enumerate() { + ui.horizontal(|ui| { + if ui.button("🗑").clicked() { + remove = Some(i); + } + if ui.button("⬆").clicked() { + move_up = Some(i); + } + if ui.button("⬇").clicked() { + move_down = Some(i); + } + ui.label(pat); + }); + } + + if let Some(i) = remove { + app.args.patterns.remove(i); + } + if let Some(i) = move_up + && i > 0 + { + app.args.patterns.swap(i, i - 1); + } + if let Some(i) = move_down + && i + 1 < app.args.patterns.len() + { + app.args.patterns.swap(i, i + 1); + } + + if !app.args.patterns.is_empty() { + ui.separator(); + if ui.button(gt.t(GT::BtnClearAll)).clicked() { + app.args.patterns.clear(); + } + } else { + ui.label( + egui::RichText::new(gt.t(GT::MsgNoPatterns)) + .italics() + .weak(), + ); + } + }); + + ui.add_space(20.0); + }); + }); +} diff --git a/src/interfaces/gui/shared.rs b/src/interfaces/gui/shared.rs new file mode 100644 index 0000000..dd0a7bb --- /dev/null +++ b/src/interfaces/gui/shared.rs @@ -0,0 +1,153 @@ +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; +use eframe::egui; + +// [ENG]: Helper to resolve output directory from app arguments. +// [POL]: Pomocnik do wyznaczania folderu zapisu z argumentów aplikacji. +pub fn resolve_dir(val: &Option, base_path: &str) -> String { + let is_auto = val + .as_ref() + .is_none_or(|v| v.trim().is_empty() || v == "AUTO"); + if is_auto { + let mut b = base_path.replace('\\', "/"); + if !b.ends_with('/') { + b.push('/'); + } + format!("{}.cargo-plot/", b) + } else { + let mut p = val.as_ref().unwrap().replace('\\', "/"); + if !p.ends_with('/') { + p.push('/'); + } + p + } +} + +// [ENG]: UI component: 50/50 Match & Mismatch tabs stretching across the top. +// [POL]: Komponent UI: Zakładki Match i Mismatch 50/50 rozciągnięte na górze. +pub fn draw_tabs(ui: &mut egui::Ui, gt: &GuiI18n, is_match: &mut bool) { + ui.horizontal(|ui| { + let item_width = (ui.available_width() - 8.0) / 2.0; + + // --- MATCH (-m) --- + let mut m_color = egui::Color32::from_rgb(150, 150, 150); + let mut m_bg = egui::Color32::TRANSPARENT; + if *is_match { + m_color = egui::Color32::from_rgb(138, 90, 255); + m_bg = egui::Color32::from_rgb(40, 40, 40); + } + + let m_btn = ui.add_sized( + [item_width, 40.0], + egui::Button::new( + egui::RichText::new(gt.t(GT::TabMatch)) + .size(16.0) + .strong() + .color(m_color), + ) + .fill(m_bg), + ); + if m_btn.clicked() { + *is_match = true; + } + + ui.add_space(8.0); + + // --- MISMATCH (-x) --- + let mut x_color = egui::Color32::from_rgb(150, 150, 150); + let mut x_bg = egui::Color32::TRANSPARENT; + if !*is_match { + x_color = egui::Color32::from_rgb(255, 80, 100); + x_bg = egui::Color32::from_rgb(40, 40, 40); + } + + let x_btn = ui.add_sized( + [item_width, 40.0], + egui::Button::new( + egui::RichText::new(gt.t(GT::TabMismatch)) + .size(16.0) + .strong() + .color(x_color), + ) + .fill(x_bg), + ); + if x_btn.clicked() { + *is_match = false; + } + }); +} + +// [ENG]: UI component: Statistics footer placeholder. +// [POL]: Komponent UI: Stopka ze statystykami. +pub fn draw_footer( + ui: &mut egui::Ui, + panel_id: &'static str, + stats: &crate::interfaces::gui::TreeStats, +) { + let fmt_bytes = |b: u64| -> String { + let kb = b as f64 / 1024.0; + if kb < 1.0 { + format!("{} B", b) + } else if kb < 1024.0 { + format!("{:.1} KB", kb) + } else { + format!("{:.2} MB", kb / 1024.0) + } + }; + + egui::TopBottomPanel::bottom(panel_id).show_inside(ui, |ui| { + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.label(format!( + "📝 Txt: {} ({})", + stats.txt_count, + fmt_bytes(stats.txt_weight) + )); + ui.separator(); + ui.label(format!( + "📦 Bin: {} ({})", + stats.bin_count, + fmt_bytes(stats.bin_weight) + )); + ui.separator(); + + if stats.err_count > 0 { + // ⚡ Zaznacza się na czerwono, jeśli są błędy + ui.label( + egui::RichText::new(format!( + "🚫 Err: {} ({})", + stats.err_count, + fmt_bytes(stats.err_weight) + )) + .color(egui::Color32::RED), + ); + ui.separator(); + } else { + ui.label("🚫 Err: 0 (0 B)"); + ui.separator(); + } + + ui.label(format!("🕳️ Empty: {}", stats.empty_count)); + ui.separator(); + ui.label(format!( + "🎯 Matched: {} / {}", + stats.matched_count, stats.total_count + )); + }); + ui.add_space(5.0); + }); +} + +// [ENG]: UI component: Central scrollable editor. +// [POL]: Komponent UI: Centralny przewijalny edytor. +pub fn draw_editor(ui: &mut egui::Ui, text_buffer: &mut String) { + egui::CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::both().show(ui, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + ui.add( + egui::TextEdit::multiline(text_buffer) + .font(egui::TextStyle::Monospace) + .desired_width(f32::INFINITY), + ); + }); + }); +} diff --git a/src/interfaces/tui.rs b/src/interfaces/tui.rs new file mode 100644 index 0000000..ca1b168 --- /dev/null +++ b/src/interfaces/tui.rs @@ -0,0 +1,13 @@ +// [ENG]: Interactive Terminal User Interface (TUI) module registry. +// [POL]: Rejestr modułu interaktywnego interfejsu tekstowego (TUI). + +pub mod i18n; +pub mod menu; +pub mod state; + +pub fn run_tui() { + let mut s = state::StateTui::new(); + cliclack::clear_screen().unwrap(); + //cliclack::intro(" 📖 https://crates.io/crates/cargo-plot").unwrap(); + menu::menu_main(&mut s); +} diff --git a/src/interfaces/tui/i18n.rs b/src/interfaces/tui/i18n.rs new file mode 100644 index 0000000..62b71b2 --- /dev/null +++ b/src/interfaces/tui/i18n.rs @@ -0,0 +1,293 @@ +use cargo_plot::i18n::Lang; +use console::style; + +fn pad(text: &str) -> String { + format!(" {} ", text) +} + +pub struct Txt { + pub pol: &'static str, + pub eng: &'static str, +} + +pub trait Translatable { + fn trans(&self) -> Txt; + fn theme(&self, text: String) -> String { + text + } +} + +pub enum Prompt { + HeaderMain, + BtnLang, + BtnQuickStart, + BtnPaths, + BtnView, + BtnOutput, + BtnFilters, + BtnRun, + BtnExit, + InputPatterns, + ExitBye, + WarnNoPatterns, + // Prompty dla podmenu (wersje dwujęzyczne w jednej linii dla szybkości) + SubBasePath, + SubIgnoreCase, + SubSelectView, + SubSelectSort, + SubNoRoot, + SubDirOut, + SubSaveAddress, + SubSaveArchive, + SubBy, + SubOnMatch, + SubOnMismatch, + SubInfo, + BtnCliMode, + InputCliCommand, + SuccessCliParse, + BtnHelp, + HelpPause, + SubHelpHeader, + HelpPatternsBtn, + HelpFlagsBtn, + HelpTextPatterns, + HelpTextFlags, + BtnGui, +} + +impl Translatable for Prompt { + fn trans(&self) -> Txt { + match self { + Prompt::HeaderMain => Txt { + pol: "📦 cargo-plot [POL] - Interaktywny Kreator", + eng: "📦 cargo-plot [ENG] - Interactive Builder", + }, + Prompt::BtnLang => Txt { + pol: "🌍 Zmień język / Change language", + eng: "🌍 Zmień język / Change language", + }, + Prompt::BtnQuickStart => Txt { + pol: "🚀 SZYBKI START (Podaj wzorce i uruchom)", + eng: "🚀 QUICK START (Enter patterns and run)", + }, + Prompt::BtnPaths => Txt { + pol: "🛠️ Ścieżki i Wzorce", + eng: "🛠️ Paths and Patterns", + }, + Prompt::BtnView => Txt { + pol: "👁️ Widok i Sortowanie", + eng: "👁️ View and Sorting", + }, + Prompt::BtnOutput => Txt { + pol: "💾 Zapis plików", + eng: "💾 Output and Saving", + }, + Prompt::BtnFilters => Txt { + pol: "⚙️ Filtry i Opcje", + eng: "⚙️ Filters and Options", + }, + Prompt::BtnRun => Txt { + pol: "▶️ URUCHOM SKANOWANIE", + eng: "▶️ RUN SCANNER", + }, + Prompt::BtnExit => Txt { + pol: "❌ WYJŚCIE", + eng: "❌ EXIT", + }, + Prompt::InputPatterns => Txt { + pol: "Podaj wzorce (oddzielone przecinkiem, np. *.rs, Cargo.toml):", + eng: "Enter patterns (comma separated, e.g. *.rs, Cargo.toml):", + }, + Prompt::ExitBye => Txt { + pol: "Do widzenia!", + eng: "Goodbye!", + }, + Prompt::WarnNoPatterns => Txt { + pol: "Brak wzorców! Podaj przynajmniej jeden.", + eng: "Missing patterns! Provide at least one.", + }, + + // Podmenu + Prompt::SubBasePath => Txt { + pol: "Ścieżka bazowa / Base path:", + eng: "Ścieżka bazowa / Base path:", + }, + Prompt::SubIgnoreCase => Txt { + pol: "Ignorować wielkość liter? / Ignore case?", + eng: "Ignorować wielkość liter? / Ignore case?", + }, + Prompt::SubSelectView => Txt { + pol: "Wybierz widok / Select view:", + eng: "Wybierz widok / Select view:", + }, + Prompt::SubSelectSort => Txt { + pol: "Wybierz sortowanie / Select sorting:", + eng: "Wybierz sortowanie / Select sorting:", + }, + Prompt::SubNoRoot => Txt { + pol: "Ukryć główny folder? / Hide root dir?", + eng: "Ukryć główny folder? / Hide root dir?", + }, + Prompt::SubDirOut => Txt { + pol: "Folder zapisu (--dir-out) [puste=CWD, AUTO=./other/]:", + eng: "Output folder (--dir-out) [empty=CWD, AUTO=./other/]:", + }, + Prompt::SubSaveAddress => Txt { + pol: "Zapisywać listę ścieżek (--save-address)?", + eng: "Save paths list (--save-address)?", + }, + Prompt::SubSaveArchive => Txt { + pol: "Zapisywać kody źródłowe (--save-archive)?", + eng: "Save source codes (--save-archive)?", + }, + Prompt::SubBy => Txt { + pol: "Dodać stopkę na dole pliku? / Add info footer?", + eng: "Dodać stopkę na dole pliku? / Add info footer?", + }, + Prompt::SubOnMatch => Txt { + pol: "Pokaż dopasowane? / Show matched?", + eng: "Pokaż dopasowane? / Show matched?", + }, + Prompt::SubOnMismatch => Txt { + pol: "Pokaż odrzucone? / Show rejected?", + eng: "Pokaż odrzucone? / Show rejected?", + }, + Prompt::SubInfo => Txt { + pol: "Pokaż statystyki? / Show info stats?", + eng: "Pokaż statystyki? / Show info stats?", + }, + Prompt::BtnCliMode => Txt { + pol: "⌨️ Wklej komendę (Raw CLI)", + eng: "⌨️ Paste command (Raw CLI)", + }, + Prompt::InputCliCommand => Txt { + pol: "Wklej flagi lub całą komendę (np. -d ./ -m):", + eng: "Paste flags or full command (e.g. -d ./ -m):", + }, + Prompt::SuccessCliParse => Txt { + pol: "Wczytano konfigurację!", + eng: "Configuration loaded!", + }, + Prompt::BtnHelp => Txt { + pol: "❓ Pomoc (Wzorce i Flagi)", + eng: "❓ Help (Patterns & Flags)", + }, + Prompt::SubHelpHeader => Txt { + pol: "Wybierz temat pomocy:", + eng: "Choose help topic:", + }, + Prompt::HelpPatternsBtn => Txt { + pol: "Składnia Wzorców", + eng: "Patterns Syntax", + }, + Prompt::HelpFlagsBtn => Txt { + pol: "Opis Flag i Opcji", + eng: "Flags & Options Description", + }, + Prompt::HelpTextPatterns => Txt { + pol: "=== WZORCE DOPASOWAŃ === +* - Dowolne znaki (np. *.rs) +** - Dowolne zagnieżdżenie (np. src/**/*.rs) +{a,b} - Rozwinięcie klamrowe (np. {src,tests}/*.rs) +! - Negacja / Odrzucenie (np. !*test*) ++ - Tryb głęboki: cała zawartość folderu (np. src/+) +@ - Rodzeństwo: wymaga pary plik + folder o tej samej nazwie +$ - Sierota: dopasowuje TYLKO, gdy brakuje pary plik/folder + +=== PRZYKŁADY === +*.rs -> Pokaż wszystkie pliki .rs +!@tui{.rs,/}+ -> Wyklucz plik tui.rs oraz folder tui/ z całą zawartością (+)", + + eng: "=== PATTERN SYNTAX === +* - Any characters (e.g. *.rs) +** - Any dir depth (e.g. src/**/*.rs) +{a,b} - Brace expansion (e.g. {src,tests}/*.rs) +! - Negation / Reject (e.g. !*test*) ++ - Deep mode: all contents of a directory (e.g. src/+) +@ - Sibling: requires file + dir pair with the same name +$ - Orphan: matches ONLY when file/dir pair is missing + +=== EXAMPLES === +*.rs -> Show all .rs files +!@tui{.rs,/}+ -> Exclude tui.rs file and tui/ dir with all its contents (+)", + }, + Prompt::HelpTextFlags => Txt { + pol: "=== FLAGI I OPCJE (W TUI JAKO PRZEŁĄCZNIKI) === +-d, --dir : Ścieżka bazowa skanowania (Domyślnie: ./) +-p, --pat : Wzorce dopasowań (wymagane, oddzielane przecinkiem) +-s, --sort : Strategia sortowania wyników (np. AzFileMerge) +-v, --view : Widok wyników (Tree, List, Grid) +-m, --on-match : Pokaż tylko dopasowane ścieżki +-x, --on-mismatch : Pokaż tylko odrzucone ścieżki +-o, --out-paths : Zapisz wynik jako listę ścieżek (Markdown) +-c, --out-cache : Zapisz wynik wraz z kodem plików (Markdown Cache) +-b, --by : Dodaj stopkę informacyjną z komendą na końcu pliku +-i, --info : Pokaż statystyki skanowania (Dopasowano/Odrzucono) +--ignore-case : Ignoruj wielkość liter we wzorcach +--treeview-no-root : Ukryj główny folder roboczy w widoku drzewa", + + eng: "=== FLAGS & OPTIONS (TOGGLES IN TUI) === +-d, --dir : Base input path to scan (Default: ./) +-p, --pat : Match patterns (required, comma separated) +-s, --sort : Sorting strategy (e.g. AzFileMerge) +-v, --view : Results view (Tree, List, Grid) +-m, --on-match : Show only matched paths +-x, --on-mismatch : Show only rejected paths +-o, --out-paths : Save result as paths list (Markdown) +-c, --out-cache : Save result with file codes (Markdown Cache) +-b, --by : Add info footer with command at the end of file +-i, --info : Show scan statistics (Matched/Rejected) +--ignore-case : Ignore case in patterns +--treeview-no-root : Hide main working directory in tree view", + }, + Prompt::HelpPause => Txt { + pol: "Naciśnij [Enter], aby wrócić do menu...", + eng: "Press [Enter] to return to menu...", + }, + Prompt::BtnGui => Txt { + pol: "🖥️ Otwórz w oknie (GUI)", + eng: "🖥️ Open in window (GUI)", + }, + } + } + + fn theme(&self, text: String) -> String { + match self { + Prompt::BtnQuickStart => style(text).on_blue().white().bold().to_string(), + Prompt::BtnRun => style(text).on_green().black().bold().to_string(), + Prompt::BtnLang => style(text).cyan().to_string(), + Prompt::BtnExit => style(text).red().to_string(), + Prompt::HeaderMain => style(text).on_white().black().bold().to_string(), + Prompt::BtnCliMode => style(text).on_black().yellow().bold().to_string(), + Prompt::BtnHelp => style(text).magenta().bold().to_string(), + Prompt::BtnGui => style(text).on_magenta().white().bold().to_string(), + _ => text, + } + } +} + +pub struct T { + lang: Lang, +} + +impl T { + pub fn new(lang: Lang) -> Self { + Self { lang } + } + pub fn fmt(&self, item: I) -> String { + let txt = item.trans(); + let text = match self.lang { + Lang::Pl => txt.pol, + Lang::En => txt.eng, + }; + item.theme(pad(text)) + } + pub fn raw(&self, item: I) -> String { + let txt = item.trans(); + match self.lang { + Lang::Pl => txt.pol.to_string(), + Lang::En => txt.eng.to_string(), + } + } +} diff --git a/src/interfaces/tui/menu.rs b/src/interfaces/tui/menu.rs new file mode 100644 index 0000000..6508bde --- /dev/null +++ b/src/interfaces/tui/menu.rs @@ -0,0 +1,382 @@ +use super::i18n::{Prompt, T}; +use super::state::StateTui; +use crate::interfaces::cli::args::CargoCli; +use crate::interfaces::cli::args::{CliSortStrategy, CliViewMode}; +use crate::interfaces::cli::engine; +use clap::Parser; +use console::style; + +#[derive(Clone, PartialEq, Eq)] +enum Action { + Lang, + QuickStart, + CliMode, + Paths, + View, + Output, + Filters, + Help, + Run, + Gui, + Exit, +} + +pub fn menu_main(s: &mut StateTui) { + let mut last_action = Action::Paths; + + loop { + let t = T::new(s.lang); + let header = t.fmt(Prompt::HeaderMain); + + // ⚡ DYNAMICZNE ETYKIETY KOKPITU + let pat_str = if s.args.patterns.is_empty() { + "[]".to_string() + } else { + format!("[{}...]", s.args.patterns[0]) + }; + let lbl_paths = format!( + "{} (dir: '{}', pat: {})", + t.fmt(Prompt::BtnPaths), + s.args.enter_path, + pat_str + ); + let lbl_view = format!( + "{} (view: {:?}, sort: {:?}, root: {})", + t.fmt(Prompt::BtnView), + s.args.view, + s.args.sort, + !s.args.no_root + ); + + let out_p = s.args.dir_out.as_deref().unwrap_or("AUTO"); + let lbl_out = format!( + "{} (dir-out: {}, address: {}, archive: {}, by: {})", + t.fmt(Prompt::BtnOutput), + out_p, + s.args.save_address, + s.args.save_archive, + s.args.by + ); + + let lbl_filt = format!( + "{} (match: {}, mismatch: {}, info: {})", + t.fmt(Prompt::BtnFilters), + s.args.include, + s.args.exclude, + s.args.info + ); + + // ⚡ BUDOWA MENU + let links_hint = style("crates.io/crates/cargo-plot | github.com/j-Cis/cargo-plot") + .dim() + .to_string(); + let action_result = cliclack::select(header) + .initial_value(last_action.clone()) + .item(Action::Lang, t.fmt(Prompt::BtnLang), "") + .item(Action::QuickStart, t.fmt(Prompt::BtnQuickStart), "") + .item(Action::CliMode, t.fmt(Prompt::BtnCliMode), "") + .item(Action::Paths, lbl_paths, "") + .item(Action::View, lbl_view, "") + .item(Action::Output, lbl_out, "") + .item(Action::Filters, lbl_filt, "") + .item(Action::Help, t.fmt(Prompt::BtnHelp), "") + .item(Action::Run, t.fmt(Prompt::BtnRun), "") + .item(Action::Gui, t.fmt(Prompt::BtnGui), "") + .item(Action::Exit, t.fmt(Prompt::BtnExit), links_hint) + .interact(); + + // ⚡ OBSŁUGA AKCJI + match action_result { + Ok(Action::Lang) => s.toggle_lang(), + Ok(Action::QuickStart) => { + let raw_pat: String = cliclack::input(t.raw(Prompt::InputPatterns)) + .interact() + .unwrap_or_default(); + if !raw_pat.trim().is_empty() { + s.args.patterns = split_patterns(&raw_pat); + cliclack::outro("🚀 ...").unwrap(); + engine::run(s.args.clone()); + return; + } + } + Ok(Action::CliMode) => { + let cmd: String = cliclack::input(t.raw(Prompt::InputCliCommand)) + .interact() + .unwrap_or_default(); + + if !cmd.trim().is_empty() { + // ⚡ Shlex idealnie tnie stringa jak bash, a jeśli ktoś zgubi cudzysłów, wyłapie błąd + if let Some(mut parsed_split) = shlex::split(&cmd) { + // Czyścimy początek (wywalamy "cargo", "run", "--", "plot") + while !parsed_split.is_empty() { + let first = parsed_split[0].to_lowercase(); + if first == "cargo" + || first == "run" + || first == "--" + || first == "plot" + || first.contains("cargo-plot") + { + parsed_split.remove(0); + } else { + break; + } + } + + // Podajemy do parsera Clap + let mut cli_args = vec!["cargo".to_string(), "plot".to_string()]; + cli_args.extend(parsed_split); + + match CargoCli::try_parse_from(cli_args) { + Ok(CargoCli::Plot(parsed_args)) => { + s.args = parsed_args; + cliclack::log::success(t.raw(Prompt::SuccessCliParse)).unwrap(); + } + Err(e) => { + cliclack::log::error(format!("{}", e)).unwrap(); + } + } + } else { + // Obsługa błędu ze strony shlex + cliclack::log::error( + "Błąd parsowania komendy! Prawdopodobnie nie domknięto cudzysłowu.", + ) + .unwrap(); + } + } + } + Ok(Action::Paths) => { + last_action = Action::Paths; + handle_paths(s, &t); + } + Ok(Action::View) => { + last_action = Action::View; + handle_view(s, &t); + } + Ok(Action::Output) => { + last_action = Action::Output; + handle_output(s, &t); + } + Ok(Action::Filters) => { + last_action = Action::Filters; + handle_filters(s, &t); + } + Ok(Action::Help) => { + let help_choice = cliclack::select(t.raw(Prompt::SubHelpHeader)) + .item(1, t.raw(Prompt::HelpPatternsBtn), "") + .item(2, t.raw(Prompt::HelpFlagsBtn), "") + .item(0, t.raw(Prompt::BtnExit), "") + .interact() + .unwrap_or(0); + + if help_choice == 1 { + cliclack::note("📖 WZORCE / PATTERNS", t.raw(Prompt::HelpTextPatterns)) + .unwrap(); + let _: String = cliclack::input(t.raw(Prompt::HelpPause)) + .required(false) // ⚡ TO POZWALA NA PUSTY ENTER + .interact() + .unwrap_or_default(); + } else if help_choice == 2 { + cliclack::note("⚙️ FLAGI / FLAGS", t.raw(Prompt::HelpTextFlags)).unwrap(); + let _: String = cliclack::input(t.raw(Prompt::HelpPause)) + .required(false) // ⚡ TO POZWALA NA PUSTY ENTER + .interact() + .unwrap_or_default(); + } + } + Ok(Action::Run) => { + if s.args.patterns.is_empty() { + cliclack::log::warning(t.raw(Prompt::WarnNoPatterns)).unwrap(); + continue; + } + cliclack::outro("🚀 ...").unwrap(); + engine::run(s.args.clone()); + return; + } + Ok(Action::Gui) => { + // Wyświetlamy komunikat na pożegnanie z terminalem + cliclack::outro(t.fmt(Prompt::BtnGui)).unwrap(); + + // Odpalamy nasze nowe okienko, przekazując mu całą zebraną konfigurację + crate::interfaces::gui::run_gui(s.args.clone()); + + // Zamykamy pętlę TUI - pałeczkę przejmuje egui! + return; + } + Ok(Action::Exit) | Err(_) => { + cliclack::outro(t.raw(Prompt::ExitBye)).unwrap(); + return; + } + } + cliclack::clear_screen().unwrap(); + } +} + +// ===================================================================== +// SZYBKIE PODMENU (Helpery modyfikujące stan) +// ===================================================================== + +fn handle_paths(s: &mut StateTui, t: &T) { + s.args.enter_path = cliclack::input(t.raw(Prompt::SubBasePath)) + .default_input(&s.args.enter_path) + .interact() + .unwrap_or(s.args.enter_path.clone()); + let current_pat = s.args.patterns.join(", "); + let new_pat: String = cliclack::input(t.raw(Prompt::InputPatterns)) + .default_input(¤t_pat) + .interact() + .unwrap_or(current_pat); + s.args.patterns = split_patterns(&new_pat); + s.args.ignore_case = cliclack::confirm(t.raw(Prompt::SubIgnoreCase)) + .initial_value(s.args.ignore_case) + .interact() + .unwrap_or(s.args.ignore_case); +} + +fn handle_view(s: &mut StateTui, t: &T) { + s.args.view = cliclack::select(t.raw(Prompt::SubSelectView)) + .initial_value(s.args.view) + .item(CliViewMode::Tree, "Tree", "") + .item(CliViewMode::List, "List", "") + .item(CliViewMode::Grid, "Grid", "") + .interact() + .unwrap_or(s.args.view); + + s.args.sort = cliclack::select(t.raw(Prompt::SubSelectSort)) + .initial_value(s.args.sort) + .item( + CliSortStrategy::AzFileMerge, + "AzFileMerge (Domyślne/Default)", + "", + ) + .item(CliSortStrategy::ZaFileMerge, "ZaFileMerge", "") + .item(CliSortStrategy::AzDirMerge, "AzDirMerge", "") + .item(CliSortStrategy::ZaDirMerge, "ZaDirMerge", "") + .item(CliSortStrategy::AzFile, "AzFile (Najpierw pliki)", "") + .item(CliSortStrategy::ZaFile, "ZaFile", "") + .item(CliSortStrategy::AzDir, "AzDir (Najpierw foldery)", "") + .item(CliSortStrategy::ZaDir, "ZaDir", "") + .item(CliSortStrategy::Az, "Az (Alfanumerycznie)", "") + .item(CliSortStrategy::Za, "Za (Odwrócone)", "") + .item(CliSortStrategy::None, "None (Brak sortowania)", "") + .interact() + .unwrap_or(s.args.sort); + + s.args.no_root = cliclack::confirm(t.raw(Prompt::SubNoRoot)) + .initial_value(s.args.no_root) + .interact() + .unwrap_or(s.args.no_root); +} + +fn handle_output(s: &mut StateTui, t: &T) { + let out_p: String = cliclack::input(t.raw(Prompt::SubDirOut)) + .default_input(s.args.dir_out.as_deref().unwrap_or("")) + .interact() + .unwrap_or_default(); + s.args.dir_out = if out_p.trim().is_empty() { + None + } else { + Some(out_p.trim().to_string()) + }; + + s.args.save_address = cliclack::confirm(t.raw(Prompt::SubSaveAddress)) + .initial_value(s.args.save_address) + .interact() + .unwrap_or(s.args.save_address); + + s.args.save_archive = cliclack::confirm(t.raw(Prompt::SubSaveArchive)) + .initial_value(s.args.save_archive) + .interact() + .unwrap_or(s.args.save_archive); + + s.args.by = cliclack::confirm(t.raw(Prompt::SubBy)) + .initial_value(s.args.by) + .interact() + .unwrap_or(s.args.by); +} + +fn handle_filters(s: &mut StateTui, t: &T) { + s.args.include = cliclack::confirm(t.raw(Prompt::SubOnMatch)) + .initial_value(s.args.include) + .interact() + .unwrap_or(s.args.include); + s.args.exclude = cliclack::confirm(t.raw(Prompt::SubOnMismatch)) + .initial_value(s.args.exclude) + .interact() + .unwrap_or(s.args.exclude); + s.args.info = cliclack::confirm(t.raw(Prompt::SubInfo)) + .initial_value(s.args.info) + .interact() + .unwrap_or(s.args.info); +} + +// ===================================================================== +// POMOCNICZY PARSER WZORCÓW +// ===================================================================== +fn split_patterns(input: &str) -> Vec { + let mut result = Vec::new(); + let mut current = String::new(); + let mut in_braces = 0; + + for c in input.chars() { + match c { + '{' => { + in_braces += 1; + current.push(c); + } + '}' => { + if in_braces > 0 { + in_braces -= 1; + } + current.push(c); + } + ',' if in_braces == 0 => { + if !current.trim().is_empty() { + result.push(current.trim().to_string()); + } + current.clear(); + } + _ => current.push(c), + } + } + if !current.trim().is_empty() { + result.push(current.trim().to_string()); + } + result +} + +/*/ +fn split_cli_args(input: &str) -> Vec { + let mut args = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + let mut quote_char = ' '; + + for c in input.chars() { + if in_quotes { + if c == quote_char { + in_quotes = false; + } else { + current.push(c); + } + } else { + match c { + '"' | '\'' => { + in_quotes = true; + quote_char = c; + } + ' ' => { + if !current.is_empty() { + args.push(current.clone()); + current.clear(); + } + } + _ => current.push(c), + } + } + } + if !current.is_empty() { + args.push(current); + } + args +} + */ diff --git a/src/interfaces/tui/state.rs b/src/interfaces/tui/state.rs new file mode 100644 index 0000000..be79254 --- /dev/null +++ b/src/interfaces/tui/state.rs @@ -0,0 +1,47 @@ +use crate::interfaces::cli::args::{CliArgs, CliSortStrategy, CliViewMode}; +use cargo_plot::i18n::Lang; + +pub struct StateTui { + pub lang: Lang, + pub args: CliArgs, +} + +impl StateTui { + pub fn new() -> Self { + let lang = Lang::detect(); + Self { + lang, + args: CliArgs { + // Domyślne wartości, dokładnie takie jak w CLI + enter_path: ".".to_string(), + patterns: vec![], + sort: CliSortStrategy::AzFileMerge, + view: CliViewMode::Tree, + include: true, + exclude: false, + dir_out: None, + save_address: false, + save_archive: false, + by: false, + tui: true, + unit: crate::interfaces::cli::args::CliUnitSystem::Bin, + all: false, + ignore_case: false, + no_root: false, + info: true, // Domyślnie włączamy statystyki (-i) + gui: false, + no_emoji: false, + lang: Some(lang), + }, + } + } + + /// Aktualizuje język w interfejsie i w argumentach dla silnika + pub fn toggle_lang(&mut self) { + self.lang = match self.lang { + Lang::Pl => Lang::En, + Lang::En => Lang::Pl, + }; + self.args.lang = Some(self.lang); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4a28aef --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod addon; +pub mod core; +pub mod execute; +pub mod i18n; +pub mod theme; diff --git a/src/lib/fn_copy_dist.rs b/src/lib/fn_copy_dist.rs deleted file mode 100644 index 3691719..0000000 --- a/src/lib/fn_copy_dist.rs +++ /dev/null @@ -1,195 +0,0 @@ -// src/lib/fn_copy_dist.rs -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; - -/// Struktura konfiguracyjna do zarządzania dystrybucją (Wzorzec: Parameter Object). -pub struct DistConfig<'a> { - pub target_dir: &'a str, - pub dist_dir: &'a str, - /// Lista nazw binarek (bez rozszerzeń). Jeśli pusta - kopiuje wszystkie odnalezione binarki. - pub binaries: Vec<&'a str>, - pub clear_dist: bool, - pub overwrite: bool, - pub dry_run: bool, -} - -impl<'a> Default for DistConfig<'a> { - fn default() -> Self { - Self { - target_dir: "./target", - dist_dir: "./dist", - binaries: vec![], - clear_dist: false, - overwrite: true, - dry_run: false, - } - } -} - -/// Helper: Mapuje architekturę na przyjazne nazwy systemów. -fn parse_os_from_triple(triple: &str) -> String { - let t = triple.to_lowercase(); - if t.contains("windows") { - "windows".to_string() - } else if t.contains("linux") { - "linux".to_string() - } else if t.contains("darwin") || t.contains("apple") { - "macos".to_string() - } else if t.contains("android") { - "android".to_string() - } else if t.contains("wasm") { - "wasm".to_string() - } else { - "unknown".to_string() - } -} - -/// Helper: Prosta heurystyka odróżniająca prawdziwą binarkę od śmieci po kompilacji w systemach Unix/Windows. -fn is_likely_binary(path: &Path, os_name: &str) -> bool { - if !path.is_file() { - return false; - } - - // Ignorujemy ukryte pliki (na wszelki wypadek) - let file_name = path.file_name().unwrap_or_default().to_string_lossy(); - if file_name.starts_with('.') { - return false; - } - - if let Some(ext) = path.extension() { - let ext_str = ext.to_string_lossy().to_lowercase(); - // Odrzucamy techniczne pliki Rusta - if ["d", "rlib", "rmeta", "pdb", "lib", "dll", "so", "dylib"].contains(&ext_str.as_str()) { - return false; - } - if os_name == "windows" { - return ext_str == "exe"; - } - if os_name == "wasm" { - return ext_str == "wasm"; - } - } else { - // Brak rozszerzenia to standard dla plików wykonywalnych na Linux/macOS - if os_name == "windows" { - return false; - } - } - - true -} - -/// Przeszukuje katalog kompilacji i kopiuje pliki według konfiguracji `DistConfig`. -/// Zwraca listę przetworzonych plików: Vec<(Źródło, Cel)> -pub fn copy_dist(config: &DistConfig) -> io::Result> { - let target_path = Path::new(config.target_dir); - let dist_path = Path::new(config.dist_dir); - - // Fail Fast - if !target_path.exists() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - format!( - "Katalog '{}' nie istnieje. Uruchom najpierw `cargo build`.", - config.target_dir - ), - )); - } - - // Opcja: Czyszczenie folderu dystrybucyjnego przed kopiowaniem - if config.clear_dist && dist_path.exists() && !config.dry_run { - // Używamy `let _` bo jeśli folder nie istnieje lub jest zablokowany, chcemy po prostu iść dalej - let _ = fs::remove_dir_all(dist_path); - } - - let mut found_files = Vec::new(); // Lista krotek (źródło, docelowy_folder, docelowy_plik) - let profiles = ["debug", "release"]; - - // Funkcja wewnętrzna: Przeszukuje folder (np. target/release) i dopasowuje reguły - let mut scan_directory = |search_dir: &Path, os_name: &str, dest_base_dir: &Path| { - if config.binaries.is_empty() { - // TRYB 1: Kopiuj WSZYSTKIE odnalezione binarki - if let Ok(entries) = fs::read_dir(search_dir) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - if is_likely_binary(&path, os_name) { - let dest_file = dest_base_dir.join(path.file_name().unwrap()); - found_files.push((path, dest_base_dir.to_path_buf(), dest_file)); - } - } - } - } else { - // TRYB 2: Kopiuj KONKRETNE binarki - for bin in &config.binaries { - let suffix = if os_name == "windows" { - ".exe" - } else if os_name == "wasm" { - ".wasm" - } else { - "" - }; - let full_name = format!("{}{}", bin, suffix); - let path = search_dir.join(&full_name); - if path.exists() { - let dest_file = dest_base_dir.join(&full_name); - found_files.push((path, dest_base_dir.to_path_buf(), dest_file)); - } - } - } - }; - - // ========================================================= - // KROK 1: Skanowanie kompilacji natywnej (Hosta) - // ========================================================= - let host_os = std::env::consts::OS; - for profile in &profiles { - let search_dir = target_path.join(profile); - let dest_base = dist_path.join(host_os).join(profile); - if search_dir.exists() { - scan_directory(&search_dir, host_os, &dest_base); - } - } - - // ========================================================= - // KROK 2: Skanowanie cross-kompilacji (Target Triples) - // ========================================================= - if let Ok(entries) = fs::read_dir(target_path) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - if path.is_dir() { - let dir_name = path.file_name().unwrap_or_default().to_string_lossy(); - if dir_name.contains('-') { - let os_name = parse_os_from_triple(&dir_name); - for profile in &profiles { - let search_dir = path.join(profile); - let dest_base = dist_path.join(&os_name).join(profile); - if search_dir.exists() { - scan_directory(&search_dir, &os_name, &dest_base); - } - } - } - } - } - } - - // ========================================================= - // KROK 3: Fizyczne operacje (z uwzględnieniem overwrite i dry_run) - // ========================================================= - let mut processed_files = Vec::new(); - - for (src, dest_dir, dest_file) in found_files { - // Obsługa nadpisywania - if dest_file.exists() && !config.overwrite { - continue; // Pomijamy ten plik - } - - if !config.dry_run { - fs::create_dir_all(&dest_dir)?; - fs::copy(&src, &dest_file)?; - } - - processed_files.push((src, dest_file)); - } - - Ok(processed_files) -} diff --git a/src/lib/fn_datestamp.rs b/src/lib/fn_datestamp.rs deleted file mode 100644 index 7ff511a..0000000 --- a/src/lib/fn_datestamp.rs +++ /dev/null @@ -1,66 +0,0 @@ -// ./lib/fn_datestamp.rs -use chrono::{Datelike, Local, Timelike, Weekday}; -pub use chrono::{NaiveDate, NaiveTime}; - -/// Generuje datestamp dla obecnego, lokalnego czasu. -/// Wywołanie: `datestamp_now()` -pub fn datestamp_now() -> String { - let now = Local::now(); - format_datestamp(now.date_naive(), now.time()) -} - -/// Generuje datestamp dla konkretnej, podanej daty i czasu. -/// Wywołanie: `datestamp(date, time)` -pub fn datestamp(date: NaiveDate, time: NaiveTime) -> String { - format_datestamp(date, time) -} - -/// PRYWATNA funkcja, która odwala całą brudną robotę (zasada DRY). -/// Nie ma modyfikatora `pub`, więc jest niewidoczna poza tym plikiem. -fn format_datestamp(date: NaiveDate, time: NaiveTime) -> String { - let year = date.year(); - let quarter = ((date.month() - 1) / 3) + 1; - - let weekday = match date.weekday() { - Weekday::Mon => "Mon", - Weekday::Tue => "Tue", - Weekday::Wed => "Wed", - Weekday::Thu => "Thu", - Weekday::Fri => "Fri", - Weekday::Sat => "Sat", - Weekday::Sun => "Sun", - }; - - let month = match date.month() { - 1 => "Jan", - 2 => "Feb", - 3 => "Mar", - 4 => "Apr", - 5 => "May", - 6 => "Jun", - 7 => "Jul", - 8 => "Aug", - 9 => "Sep", - 10 => "Oct", - 11 => "Nov", - 12 => "Dec", - _ => unreachable!(), - }; - - let millis = time.nanosecond() / 1_000_000; - - format!( - "{}Q{}D{:03}W{:02}_{}{:02}{}_{:02}{:02}{:02}{:03}", - year, - quarter, - date.ordinal(), - date.iso_week().week(), - weekday, - date.day(), - month, - time.hour(), - time.minute(), - time.second(), - millis - ) -} diff --git a/src/lib/fn_doc_gen.rs b/src/lib/fn_doc_gen.rs deleted file mode 100644 index 324d5ff..0000000 --- a/src/lib/fn_doc_gen.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::fn_datestamp::datestamp_now; -use crate::fn_doc_id::generate_ids; -use crate::fn_doc_models::DocTask; -use crate::fn_doc_write::write_md; -use crate::fn_filespath::filespath; -use crate::fn_filestree::filestree; -use crate::fn_plotfiles::plotfiles_txt; -use std::fs; -use std::io; - -pub fn generate_docs(doc_tasks: Vec, output_dir: &str) -> io::Result<()> { - fs::create_dir_all(output_dir)?; - - for doc_task in doc_tasks { - // Generujemy jeden wspólny znacznik czasu dla zadania - let stamp = datestamp_now(); - - // LOGIKA NAZWY PLIKU - let out_file = if doc_task.suffix_stamp { - format!("{}__{}.md", doc_task.output_filename, stamp) - } else { - format!("{}.md", doc_task.output_filename) - }; - - let out_path = format!("{}/{}", output_dir, out_file); - - // 1. Zbieramy ścieżki - let paths = filespath(&doc_task.tasks); - - // 2. Generowanie tekstu drzewa - let tree_text = if doc_task.insert_tree != "with-out" { - // używamy konfiguracji wbudowanej w zadanie! - let tree_nodes = - filestree(paths.clone(), doc_task.insert_tree, &doc_task.weight_config); - let txt = plotfiles_txt(&tree_nodes, "", None); - Some(txt) - } else { - None - }; - - // 3. Nadajemy identyfikatory - let id_map = generate_ids(&paths); - - // 4. Przekazujemy styl ID do funkcji zapisu - write_md( - &out_path, - &paths, - &id_map, - tree_text, - doc_task.id_style, - doc_task.watermark, - &doc_task.command_str, - &stamp, - doc_task.suffix_stamp, - doc_task.title_file, - doc_task.title_file_with_path, - )?; - - // Możemy wydrukować info o POJEDYNCZYM wygenerowanym pliku - println!(" [+] Wygenerowano raport: {}", out_path); - } - - Ok(()) -} diff --git a/src/lib/fn_doc_id.rs b/src/lib/fn_doc_id.rs deleted file mode 100644 index cb9f339..0000000 --- a/src/lib/fn_doc_id.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; - -pub fn generate_ids(paths: &[PathBuf]) -> HashMap { - let mut map = HashMap::new(); - let mut counters: HashMap = HashMap::new(); - - // Klonujemy i sortujemy ścieżki, żeby ID były nadawane powtarzalnie - let mut sorted_paths = paths.to_vec(); - sorted_paths.sort(); - - for path in sorted_paths { - // Ignorujemy foldery, przypisujemy ID tylko plikom - if path.is_dir() { - continue; - } - // DODAJEMY .to_string() NA KOŃCU, ABY ZROBIĆ NIEZALEŻNĄ KOPIĘ - let file_name = path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - // Tutaj .replace() i tak zwraca już własnego Stringa, więc jest bezpiecznie - let path_str = path.to_string_lossy().replace('\\', "/"); - - // 1. Twarde reguły dla znanych plików - if file_name == "Cargo.toml" { - map.insert(path.clone(), "TomlCargo".to_string()); - continue; - } - if file_name == "Makefile.toml" { - map.insert(path.clone(), "TomlMakefile".to_string()); - continue; - } - if file_name == "build.rs" { - map.insert(path.clone(), "RustBuild".to_string()); - continue; - } - if path_str.contains("src/ui/index.slint") { - map.insert(path.clone(), "SlintIndex".to_string()); - continue; - } - - // 2. Dynamiczne ID na podstawie ścieżki - let prefix = if path_str.contains("src/lib") { - if file_name == "mod.rs" { - "RustLibMod".to_string() - } else { - "RustLibPub".to_string() - } - } else if path_str.contains("src/bin") || path_str.contains("src/main.rs") { - "RustBin".to_string() - } else if path_str.contains("src/ui") { - "Slint".to_string() - } else { - let ext = path.extension().unwrap_or_default().to_string_lossy(); - format!("File{}", capitalize(&ext)) - }; - - // Licznik dla danej kategorii - let count = counters.entry(prefix.clone()).or_insert(1); - - let id = if file_name == "mod.rs" && prefix == "RustLibMod" { - format!("{}_00", prefix) // mod.rs zawsze jako 00 - } else { - format!("{}_{:02}", prefix, count) - }; - - map.insert(path, id); - if !(file_name == "mod.rs" && prefix == "RustLibMod") { - *count += 1; - } - } - - map -} - -fn capitalize(s: &str) -> String { - let mut c = s.chars(); - match c.next() { - None => String::new(), - Some(f) => f.to_uppercase().collect::() + c.as_str(), - } -} diff --git a/src/lib/fn_doc_models.rs b/src/lib/fn_doc_models.rs deleted file mode 100644 index c55cb4e..0000000 --- a/src/lib/fn_doc_models.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::fn_filespath::Task; -use crate::fn_weight::WeightConfig; - -/// Struktura definiująca jedno zadanie generowania pliku Markdown -pub struct DocTask<'a> { - pub output_filename: &'a str, - pub insert_tree: &'a str, // "dirs-first", "files-first", "with-out" - pub id_style: &'a str, // "id-tag", "id-num", "id-non" - pub tasks: Vec>, - pub weight_config: WeightConfig, // Nowe pole - pub watermark: &'a str, - pub command_str: Option, - pub suffix_stamp: bool, - pub title_file: &'a str, - pub title_file_with_path: bool, -} diff --git a/src/lib/fn_doc_write.rs b/src/lib/fn_doc_write.rs deleted file mode 100644 index a434e4a..0000000 --- a/src/lib/fn_doc_write.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::fn_files_blacklist::is_blacklisted_extension; -use crate::fn_path_utils::to_display_path; -use crate::fn_pathtype::get_file_type; -use std::collections::HashMap; -use std::fs; -use std::io; -use std::path::PathBuf; - -#[allow(clippy::too_many_arguments)] -pub fn write_md( - out_path: &str, - files: &[PathBuf], - id_map: &HashMap, - tree_text: Option, - id_style: &str, - watermark: &str, - command_str: &Option, - stamp: &str, - suffix_stamp: bool, - title_file: &str, - title_file_with_path: bool, -) -> io::Result<()> { - let mut content = String::new(); - - // ========================================== - // LOGIKA TYTUŁU - // ========================================== - let mut title_line = format!("# {}", title_file); - - if !suffix_stamp { - title_line.push_str(&format!(" {}", stamp)); - } - - if title_file_with_path { - title_line.push_str(&format!(" ({})", out_path)); - } - - content.push_str(&title_line); - content.push_str("\n\n"); - // ========================================== - - let watermark_text = "> 🚀 Raport wygenerowany przy użyciu [cargo-plot](https://crates.io/crates/cargo-plot) | Źródło: [GitHub](https://github.com/j-Cis/cargo-plot)\n\n"; - - // 1. Znak wodny na początku - if watermark == "first" { - content.push_str(watermark_text); - } - - // 2. Reprodukcja komendy - if let Some(cmd) = command_str { - content.push_str(&format!("**Wywołana komenda:**\n```bash\n{}\n```\n\n", cmd)); - } - - if let Some(tree) = tree_text { - content.push_str("```text\n"); - content.push_str(&tree); - content.push_str("```\n\n"); - } - - let current_dir = std::env::current_dir().unwrap_or_default(); - let mut file_counter = 1; - - for path in files { - if path.is_dir() { - continue; - } - - let display_path = to_display_path(path, ¤t_dir); - - if path.exists() { - let original_id = id_map - .get(path) - .cloned() - .unwrap_or_else(|| "BrakID".to_string()); - - // <-- POPRAWIONE: używamy id_style bezpośrednio - let header_name = match id_style { - "id-num" => format!("Plik-{:03}", file_counter), - "id-non" => "Plik".to_string(), - _ => format!("Plik-{}", original_id), - }; - file_counter += 1; - - let ext = path - .extension() - .unwrap_or_default() - .to_string_lossy() - .to_lowercase(); - let lang = get_file_type(&ext).md_lang; - - // KROK 1: Sprawdzenie czarnej listy rozszerzeń - if is_blacklisted_extension(&ext) { - content.push_str(&format!( - "## {}: `{}`\n\n> *(Plik binarny/graficzny - pominięto zawartość)*\n\n", - header_name, display_path - )); - continue; - } - - // KROK 2: Bezpieczna próba odczytu zawartości - match fs::read_to_string(path) { - Ok(file_content) => { - if lang == "markdown" { - content.push_str(&format!("## {}: `{}`\n\n", header_name, display_path)); - for line in file_content.lines() { - if line.trim().is_empty() { - content.push_str(">\n"); - } else { - content.push_str(&format!("> {}\n", line)); - } - } - content.push_str("\n\n"); - } else { - content.push_str(&format!( - "## {}: `{}`\n\n```{}\n{}\n```\n\n", - header_name, display_path, lang, file_content - )); - } - } - Err(_) => { - // Fallback: Plik nie ma rozszerzenia binarnego, ale jego zawartość to nie jest czysty tekst UTF-8 - content.push_str(&format!("## {}: `{}`\n\n> *(Nie można odczytać pliku jako tekst UTF-8 - pominięto)*\n\n", header_name, display_path)); - } - } - } else { - content.push_str(&format!( - "## BŁĄD: `{}` (Plik nie istnieje)\n\n", - display_path - )); - } - } - - // 3. Znak wodny na końcu (Domyślnie) - if watermark == "last" { - content.push_str("---\n"); - content.push_str(watermark_text); - } - - fs::write(out_path, &content)?; - Ok(()) -} diff --git a/src/lib/fn_files_blacklist.rs b/src/lib/fn_files_blacklist.rs deleted file mode 100644 index 11337aa..0000000 --- a/src/lib/fn_files_blacklist.rs +++ /dev/null @@ -1,40 +0,0 @@ -// src/lib/fn_files_blacklist.rs - -/// Sprawdza, czy podane rozszerzenie pliku należy do czarnej listy (pliki binarne, graficzne, media, archiwa). -/// Zwraca `true`, jeśli plik powinien zostać pominięty podczas wczytywania zawartości tekstowej. -pub fn is_blacklisted_extension(ext: &str) -> bool { - let binary_extensions = [ - // -------------------------------------------------- - // GRAFIKA I DESIGN - // -------------------------------------------------- - "png", "jpg", "jpeg", "gif", "bmp", "ico", "svg", "webp", "tiff", "tif", "heic", "psd", - "ai", - // -------------------------------------------------- - // BINARKI, BIBLIOTEKI I ARTEFAKTY KOMPILACJI - // -------------------------------------------------- - // Rust / Windows / Linux / Mac - "exe", "dll", "so", "dylib", "bin", "wasm", "pdb", "rlib", "rmeta", "lib", - // C / C++ - "o", "a", "obj", "pch", "ilk", "exp", // Java / JVM - "jar", "class", "war", "ear", // Python - "pyc", "pyd", "pyo", "whl", - // -------------------------------------------------- - // ARCHIWA I PACZKI - // -------------------------------------------------- - "zip", "tar", "gz", "tgz", "7z", "rar", "bz2", "xz", "iso", "dmg", "pkg", "apk", - // -------------------------------------------------- - // DOKUMENTY, BAZY DANYCH I FONTY - // -------------------------------------------------- - // Bazy danych - "sqlite", "sqlite3", "db", "db3", "mdf", "ldf", "rdb", // Dokumenty Office / PDF - "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", - // Fonty - "woff", "woff2", "ttf", "eot", "otf", - // -------------------------------------------------- - // MEDIA (AUDIO / WIDEO) - // -------------------------------------------------- - "mp3", "mp4", "avi", "mkv", "wav", "flac", "ogg", "m4a", "mov", "wmv", "flv", - ]; - - binary_extensions.contains(&ext) -} diff --git a/src/lib/fn_filespath.rs b/src/lib/fn_filespath.rs deleted file mode 100644 index 110a488..0000000 --- a/src/lib/fn_filespath.rs +++ /dev/null @@ -1,263 +0,0 @@ -use regex::Regex; -use std::collections::HashSet; -use std::fs; -use std::path::{Path, PathBuf}; - -#[derive(Debug, Clone)] -struct Rule { - regex: Regex, - only_dir: bool, - // is_generic: bool, - // raw_clean: String, -} - -fn glob_to_regex(pattern: &str) -> Rule { - let raw = pattern.trim(); - let mut p = raw.replace('\\', "/"); - - // NAPRAWA: Jeśli użytkownik podał "./folder", ucinamy "./", - // ponieważ relatywna ścieżka (rel_path) nigdy tego nie zawiera. - if p.starts_with("./") { - p = p[2..].to_string(); - } - - // let is_generic = p == "*" || p == "**/*"; - let only_dir = p.ends_with('/'); - if only_dir { - p.pop(); - } - // let raw_clean = p.clone(); - - let mut regex_str = regex::escape(&p); - regex_str = regex_str.replace(r"\*\*", ".*"); - regex_str = regex_str.replace(r"\*", "[^/]*"); - regex_str = regex_str.replace(r"\?", "[^/]"); - regex_str = regex_str.replace(r"\[!", "[^"); - - if regex_str.starts_with('/') { - regex_str = format!("^{}", ®ex_str[1..]); - } else if regex_str.starts_with(".*") { - regex_str = format!("^{}", regex_str); - } else { - regex_str = format!("(?:^|/){}", regex_str); - } - - if only_dir { - regex_str.push_str("(?:/.*)?$"); - } else { - regex_str.push('$'); - } - - let final_regex = format!("(?i){}", regex_str); - - Rule { - regex: Regex::new(&final_regex).unwrap_or_else(|_| Regex::new("(?i)$.^").unwrap()), - only_dir, - // is_generic, - // raw_clean, - } -} - -/// Element tablicy wejściowej -/// Element tablicy wejściowej -pub struct Task<'a> { - pub path_location: &'a str, - pub path_exclude: Vec<&'a str>, - pub path_include_only: Vec<&'a str>, - pub filter_files: Vec<&'a str>, - pub output_type: &'a str, // "dirs", "files", "dirs_and_files" -} - -// Implementujemy wartości domyślne, co pozwoli nam pomijać nieużywane pola -impl<'a> Default for Task<'a> { - fn default() -> Self { - Self { - path_location: ".", - path_exclude: vec![], - path_include_only: vec![], - filter_files: vec![], - output_type: "dirs_and_files", - } - } -} - -pub fn filespath(tasks: &[Task]) -> Vec { - let mut all_results = HashSet::new(); - - for task in tasks { - let root_path = Path::new(task.path_location); - let canonical_root = - fs::canonicalize(root_path).unwrap_or_else(|_| root_path.to_path_buf()); - - // Przygotowanie reguł - let mut exclude_rules = Vec::new(); - for p in &task.path_exclude { - if !p.trim().is_empty() { - exclude_rules.push(glob_to_regex(p)); - } - } - - let mut include_only_rules = Vec::new(); - for p in &task.path_include_only { - if !p.trim().is_empty() { - include_only_rules.push(glob_to_regex(p)); - } - } - - let mut filter_files_rules = Vec::new(); - for p in &task.filter_files { - if !p.trim().is_empty() { - filter_files_rules.push(glob_to_regex(p)); - } - } - - // ========================================================= - // KROK 1: PEŁNY SKAN Z ODRZUCENIEM CAŁYCH GAŁĘZI EXCLUDE - // ========================================================= - let mut scanned_paths = Vec::new(); - scan_step1( - &canonical_root, - &canonical_root, - &exclude_rules, - &mut scanned_paths, - ); - - // ========================================================= - // KROK 2: ZACHOWANIE FOLDERÓW I FILTROWANIE PLIKÓW INCLUDE - // ========================================================= - for path in scanned_paths { - let rel_path = path - .strip_prefix(&canonical_root) - .unwrap() - .to_string_lossy() - .replace('\\', "/"); - let path_slash = format!("{}/", rel_path); - - if !include_only_rules.is_empty() { - let mut matches = false; - for rule in &include_only_rules { - if rule.only_dir { - // Jeśli reguła dotyczy TYLKO folderów - if path.is_dir() && rule.regex.is_match(&path_slash) { - matches = true; - break; - } - } else { - // Jeśli reguła jest uniwersalna (pliki i foldery) - if rule.regex.is_match(&rel_path) || rule.regex.is_match(&path_slash) { - matches = true; - break; - } - } - } - if !matches { - continue; - } - } - - if path.is_dir() { - // Jeśli tryb to NIE "files" (czyli "dirs" lub "dirs_and_files") - // to dodajemy folder normalnie. - if task.output_type != "files" { - all_results.insert(path); - } - } else { - // Jeśli tryb to "dirs", całkowicie ignorujemy pliki - if task.output_type == "dirs" { - continue; - } - - // Pliki sprawdzamy pod kątem filter_files - let mut is_file_matched = false; - if filter_files_rules.is_empty() { - is_file_matched = true; - } else { - for rule in &filter_files_rules { - if rule.only_dir { - continue; - } - if rule.regex.is_match(&rel_path) { - is_file_matched = true; - break; - } - } - } - - if is_file_matched { - all_results.insert(path.clone()); - - // MAGIA DLA "files": - // Aby drzewo nie spłaszczyło się do zwykłej listy, musimy dodać foldery nadrzędne - // tylko dla TEGO KONKRETNEGO dopasowanego pliku. (Ukrywa to puste foldery!) - if task.output_type == "files" { - let mut current_parent = path.parent(); - while let Some(p) = current_parent { - all_results.insert(p.to_path_buf()); - if p == canonical_root { - break; - } - current_parent = p.parent(); - } - } - } - } - } - } - - let result: Vec = all_results.into_iter().collect(); - result -} - -// Prywatna funkcja pomocnicza do wykonania Kroku 1 -fn scan_step1( - root_path: &Path, - current_path: &Path, - exclude_rules: &[Rule], - scanned_paths: &mut Vec, -) { - let read_dir = match fs::read_dir(current_path) { - Ok(rd) => rd, - Err(_) => return, - }; - - for entry in read_dir.filter_map(|e| e.ok()) { - let path = entry.path(); - let is_dir = path.is_dir(); - - let rel_path = match path.strip_prefix(root_path) { - Ok(p) => p.to_string_lossy().replace('\\', "/"), - Err(_) => continue, - }; - - if rel_path.is_empty() { - continue; - } - - let path_slash = format!("{}/", rel_path); - - // KROK 1.1: Czy wykluczone przez EXCLUDE? - let mut is_excluded = false; - for rule in exclude_rules { - if rule.only_dir && !is_dir { - continue; - } - if rule.regex.is_match(&rel_path) || (is_dir && rule.regex.is_match(&path_slash)) { - is_excluded = true; - break; - } - } - - // Jeśli folder/plik jest wykluczony - URYWAMY GAŁĄŹ I NIE WCHODZIMY GŁĘBIEJ - if is_excluded { - continue; - } - - // KROK 1.2: Dodajemy do tymczasowych wyników KROKU 1 - scanned_paths.push(path.clone()); - - // KROK 1.3: Jeśli to bezpieczny folder, skanujemy jego zawartość - if is_dir { - scan_step1(root_path, &path, exclude_rules, scanned_paths); - } - } -} diff --git a/src/lib/fn_filestree.rs b/src/lib/fn_filestree.rs deleted file mode 100644 index b96cb64..0000000 --- a/src/lib/fn_filestree.rs +++ /dev/null @@ -1,144 +0,0 @@ -// Zaktualizowany Plik-004: src/lib/fn_filestree.rs -use crate::fn_pathtype::{DIR_ICON, get_file_type}; -use crate::fn_weight::{WeightConfig, format_weight, get_path_weight}; -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::path::PathBuf; - -/// Struktura węzła drzewa -#[derive(Debug, Clone)] -pub struct FileNode { - pub name: String, - pub path: PathBuf, - pub is_dir: bool, - pub icon: String, - pub weight_str: String, // Nowe pole na sformatowaną wagę [qq xxxxx] - pub weight_bytes: u64, // Surowa waga do obliczeń sumarycznych - pub children: Vec, -} - -/// Helper do sortowania węzłów zgodnie z wybraną metodą -fn sort_nodes(nodes: &mut [FileNode], sort_method: &str) { - match sort_method { - "files-first" => nodes.sort_by(|a, b| { - if a.is_dir == b.is_dir { - a.name.cmp(&b.name) - } else if !a.is_dir { - Ordering::Less - } else { - Ordering::Greater - } - }), - "dirs-first" => nodes.sort_by(|a, b| { - if a.is_dir == b.is_dir { - a.name.cmp(&b.name) - } else if a.is_dir { - Ordering::Less - } else { - Ordering::Greater - } - }), - _ => nodes.sort_by(|a, b| a.name.cmp(&b.name)), - } -} - -/// Funkcja formatująca - buduje drzewo i przypisuje ikony oraz wagi -pub fn filestree( - paths: Vec, - sort_method: &str, - weight_cfg: &WeightConfig, // NOWY ARGUMENT -) -> Vec { - let mut tree_map: BTreeMap> = BTreeMap::new(); - for p in &paths { - let parent = p - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| PathBuf::from("/")); - tree_map.entry(parent).or_default().push(p.clone()); - } - - fn build_node( - path: &PathBuf, - paths: &BTreeMap>, - sort_method: &str, - weight_cfg: &WeightConfig, // NOWY ARGUMENT - ) -> FileNode { - let name = path - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_else(|| "/".to_string()); - - let is_dir = path.is_dir(); - - let icon = if is_dir { - DIR_ICON.to_string() - } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - get_file_type(ext).icon.to_string() - } else { - "📄".to_string() - }; - - // KROK A: Pobieramy bazową wagę (0 dla folderów w trybie sumy uwzględnionych) - let mut weight_bytes = get_path_weight(path, weight_cfg.dir_sum_included); - - let mut children = vec![]; - if let Some(child_paths) = paths.get(path) { - let mut child_nodes: Vec = child_paths - .iter() - .map(|c| build_node(c, paths, sort_method, weight_cfg)) - .collect(); - - crate::fn_filestree::sort_nodes(&mut child_nodes, sort_method); - - // KROK B: Jeśli to folder i sumujemy tylko ujęte pliki, zsumuj wagi dzieci - if is_dir && weight_cfg.dir_sum_included { - weight_bytes = child_nodes.iter().map(|n| n.weight_bytes).sum(); - } - - children = child_nodes; - } - - // KROK C: Formatowanie wagi do ciągu "[qq xxxxx]" - let mut weight_str = String::new(); - - // Sprawdzamy czy system wag jest w ogóle włączony - if weight_cfg.system != crate::fn_weight::UnitSystem::None { - let should_show = - (is_dir && weight_cfg.show_for_dirs) || (!is_dir && weight_cfg.show_for_files); - - if should_show { - weight_str = format_weight(weight_bytes, weight_cfg); - } else { - // Jeśli ukrywamy wagę dla tego węzła, wstawiamy puste spacje - // szerokość = 7 (nawiasy, jednostka, spacje) + precyzja - let empty_width = 7 + weight_cfg.precision; - weight_str = format!("{:width$}", "", width = empty_width); - } - } - - FileNode { - name, - path: path.clone(), - is_dir, - icon, - weight_str, - weight_bytes, - children, - } - } - - let roots: Vec = paths - .iter() - .filter(|p| p.parent().is_none() || !paths.contains(&p.parent().unwrap().to_path_buf())) - .cloned() - .collect(); - - let mut top_nodes: Vec = roots - .into_iter() - .map(|r| build_node(&r, &tree_map, sort_method, weight_cfg)) - .collect(); - - crate::fn_filestree::sort_nodes(&mut top_nodes, sort_method); - - top_nodes -} diff --git a/src/lib/fn_path_utils.rs b/src/lib/fn_path_utils.rs deleted file mode 100644 index 9011173..0000000 --- a/src/lib/fn_path_utils.rs +++ /dev/null @@ -1,19 +0,0 @@ -// src/lib/fn_path_utils.rs -use std::path::Path; - -/// Standaryzuje ścieżkę: zamienia ukośniki na uniksowe i usuwa windowsowy prefiks rozszerzony. -pub fn standardize_path(path: &Path) -> String { - path.to_string_lossy() - .replace('\\', "/") - .trim_start_matches("//?/") - .to_string() -} - -/// Formatuje ścieżkę względem podanego katalogu bazowego (np. obecnego katalogu roboczego). -/// Jeśli ścieżka zawiera się w bazowej, zwraca ładny format `./relatywna/sciezka`. -pub fn to_display_path(path: &Path, base_dir: &Path) -> String { - match path.strip_prefix(base_dir) { - Ok(rel_path) => format!("./{}", standardize_path(rel_path)), - Err(_) => standardize_path(path), - } -} diff --git a/src/lib/fn_pathtype.rs b/src/lib/fn_pathtype.rs deleted file mode 100644 index 8a87faa..0000000 --- a/src/lib/fn_pathtype.rs +++ /dev/null @@ -1,60 +0,0 @@ -// src/lib/fn_fileslang.rs - -/// Struktura przechowująca metadane dla danego typu pliku -pub struct PathFileType { - pub icon: &'static str, - pub md_lang: &'static str, -} -/// SSoT dla ikony folderu -pub const DIR_ICON: &str = "📂"; - -/// SSoT (Single Source of Truth) dla rozszerzeń plików. -/// Zwraca odpowiednią ikonę do drzewa ASCII oraz język formatowania Markdown. -pub fn get_file_type(ext: &str) -> PathFileType { - match ext { - "rs" => PathFileType { - icon: "🦀", - md_lang: "rust", - }, - "toml" => PathFileType { - icon: "⚙️", - md_lang: "toml", - }, - "slint" => PathFileType { - icon: "🎨", - md_lang: "slint", - }, - "md" => PathFileType { - icon: "📝", - md_lang: "markdown", - }, - "json" => PathFileType { - icon: "🔣", - md_lang: "json", - }, - "yaml" | "yml" => PathFileType { - icon: "🛠️", - md_lang: "yaml", - }, - "html" => PathFileType { - icon: "🌐", - md_lang: "html", - }, - "css" => PathFileType { - icon: "🖌️", - md_lang: "css", - }, - "js" => PathFileType { - icon: "📜", - md_lang: "javascript", - }, - "ts" => PathFileType { - icon: "📘", - md_lang: "typescript", - }, - _ => PathFileType { - icon: "📄", - md_lang: "text", - }, // Domyślny fallback - } -} diff --git a/src/lib/fn_plotfiles.rs b/src/lib/fn_plotfiles.rs deleted file mode 100644 index 2d61fc7..0000000 --- a/src/lib/fn_plotfiles.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Zaktualizowany Plik-021: src/lib/fn_plotfiles.rs -use crate::fn_filestree::FileNode; -use colored::*; - -/// Zestaw znaków używanych do rysowania gałęzi drzewa. -#[derive(Debug, Clone)] -pub struct TreeStyle { - // Foldery (d) - pub dir_last_with_children: String, // └──┬ - pub dir_last_no_children: String, // └─── - pub dir_mid_with_children: String, // ├──┬ - pub dir_mid_no_children: String, // ├─── - - // Pliki (f) - pub file_last: String, // └── - pub file_mid: String, // ├── - - // Wcięcia dla kolejnych poziomów (i) - pub indent_last: String, // " " (3 spacje) - pub indent_mid: String, // "│ " (kreska + 2 spacje) -} - -impl Default for TreeStyle { - fn default() -> Self { - Self { - dir_last_with_children: "└──┬".to_string(), - dir_last_no_children: "└───".to_string(), - dir_mid_with_children: "├──┬".to_string(), - dir_mid_no_children: "├───".to_string(), - - file_last: "└──•".to_string(), - file_mid: "├──•".to_string(), - - indent_last: " ".to_string(), - indent_mid: "│ ".to_string(), - } - } -} - -/// Prywatna funkcja pomocnicza, która odwala całą powtarzalną robotę. -fn plot(nodes: &[FileNode], indent: &str, s: &TreeStyle, use_color: bool) -> String { - let mut result = String::new(); - - for (i, node) in nodes.iter().enumerate() { - let is_last = i == nodes.len() - 1; - let has_children = !node.children.is_empty(); - - // 1. Wybór odpowiedniego znaku gałęzi - let branch = if node.is_dir { - match (is_last, has_children) { - (true, true) => &s.dir_last_with_children, - (false, true) => &s.dir_mid_with_children, - (true, false) => &s.dir_last_no_children, - (false, false) => &s.dir_mid_no_children, - } - } else if is_last { - &s.file_last - } else { - &s.file_mid - }; - - // KROK NOWY: Przygotowanie kolorowanej (lub nie) ramki z wagą - let weight_prefix = if node.weight_str.is_empty() { - String::new() - } else if use_color { - // W CLI waga będzie szara, by nie odciągać uwagi od struktury plików - node.weight_str.truecolor(120, 120, 120).to_string() - } else { - node.weight_str.clone() - }; - - // 2. Formatowanie konkretnej linii (z kolorami lub bez) - let line = if use_color { - if node.is_dir { - format!( - "{}{}{} {}{}/\n", - weight_prefix, // ZMIANA TUTAJ - indent.green(), - branch.green(), - node.icon, - node.name.truecolor(200, 200, 50) - ) - } else { - format!( - "{}{}{} {}{}\n", - weight_prefix, // ZMIANA TUTAJ - indent.green(), - branch.green(), - node.icon, - node.name.white() - ) - } - } else { - // ZMIANA TUTAJ: Doklejenie prefixu dla zwykłego tekstu - format!( - "{}{}{} {} {}\n", - weight_prefix, indent, branch, node.icon, node.name - ) - }; - - result.push_str(&line); - - // 3. Rekurencja dla dzieci z wyliczonym nowym wcięciem - if has_children { - let new_indent = if is_last { - format!("{}{}", indent, s.indent_last) - } else { - format!("{}{}", indent, s.indent_mid) - }; - result.push_str(&plot(&node.children, &new_indent, s, use_color)); - } - } - - result -} - -/// GENEROWANIE PLAIN TEXT / MARKDOWN -pub fn plotfiles_txt(nodes: &[FileNode], indent: &str, style: Option<&TreeStyle>) -> String { - let default_style = TreeStyle::default(); - let s = style.unwrap_or(&default_style); - - plot(nodes, indent, s, false) -} - -/// GENEROWANIE KOLOROWANEGO ASCII DO CLI -pub fn plotfiles_cli(nodes: &[FileNode], indent: &str, style: Option<&TreeStyle>) -> String { - let default_style = TreeStyle::default(); - let s = style.unwrap_or(&default_style); - - plot(nodes, indent, s, true) -} diff --git a/src/lib/mod.rs b/src/lib/mod.rs deleted file mode 100644 index 38ab185..0000000 --- a/src/lib/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub mod fn_datestamp; -pub mod fn_filespath; -pub mod fn_filestree; -pub mod fn_plotfiles; -pub mod fn_weight; - -pub mod fn_doc_gen; -pub mod fn_doc_id; -pub mod fn_doc_models; -pub mod fn_doc_write; - -pub mod fn_files_blacklist; -pub mod fn_path_utils; -pub mod fn_pathtype; - -pub mod fn_copy_dist; diff --git a/src/main.rs b/src/main.rs index a8087dc..947ac4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,16 @@ -// Plik: src/main.rs -use clap::Parser; -use std::env; +// [ENG]: Main entry point switching between interfaces. +// [POL]: Główny punkt wejścia przełączający między interfejsami. -mod cli; -mod tui; +#![allow(clippy::pedantic, clippy::struct_excessive_bools)] -fn main() { - // [QoL Fix]: Jeśli uruchomiono binarkę bez żadnych argumentów (np. czyste `cargo run` - // lub podwójne kliknięcie na cargo-plot.exe), pomijamy walidację Clapa i odpalamy TUI. - if env::args().len() <= 1 { - tui::run_tui(); - return; - } +mod interfaces; - // Jeśli są argumenty, pozwalamy Clapowi je sparsować (wymaga słowa 'plot') - let cli::args::CargoCli::Plot(plot_args) = cli::args::CargoCli::parse(); +fn main() { + // [ENG]: Register an empty Ctrl+C handler to prevent abrupt termination. + // [POL]: Rejestrujemy pusty handler Ctrl+C, zapobiegając natychmiastowemu zabiciu programu. + ctrlc::set_handler(move || {}).expect("Błąd podczas ustawiania handlera Ctrl+C"); - match plot_args.command { - Some(cmd) => cli::run_command(cmd), - None => tui::run_tui(), // Zadziała np. dla `cargo run -- plot` - } + // [ENG]: Pass execution directly to the CLI parser and router. + // [POL]: Przekazanie wykonania bezpośrednio do parsera i routera CLI. + interfaces::cli::run_cli(); } diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..90c8184 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,4 @@ +pub mod save_path; +pub mod save_code; +pub mod generator; +//pub use save_path \ No newline at end of file diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..0735c38 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,2 @@ +pub mod for_path_list; +pub mod for_path_tree; diff --git a/src/theme/for_path_list.rs b/src/theme/for_path_list.rs new file mode 100644 index 0000000..c364d35 --- /dev/null +++ b/src/theme/for_path_list.rs @@ -0,0 +1,19 @@ +/// [POL]: Przypisuje ikonę (emoji) do ścieżki na podstawie atrybutów: katalog oraz status elementu ukrytego. +/// [ENG]: Assigns an icon (emoji) to a path based on attributes: directory status and hidden element status. +pub fn get_icon_for_path(path: &str) -> &'static str { + let is_dir = path.ends_with('/'); + + let nazwa = path + .trim_end_matches('/') + .split('/') + .next_back() + .unwrap_or(""); + let is_hidden = nazwa.starts_with('.'); + + match (is_dir, is_hidden) { + (true, false) => "📁", // [POL]: Folder | [ENG]: Directory + (true, true) => "🗃️", // [POL]: Ukryty folder | [ENG]: Hidden directory + (false, false) => "📄", // [POL]: Plik | [ENG]: File + (false, true) => "⚙️ ", // [POL]: Ukryty plik | [ENG]: Hidden file + } +} diff --git a/src/theme/for_path_tree.rs b/src/theme/for_path_tree.rs new file mode 100644 index 0000000..3bfbe50 --- /dev/null +++ b/src/theme/for_path_tree.rs @@ -0,0 +1,108 @@ +// [ENG]: Path classification and icon mapping for tree visualization. +// [POL]: Klasyfikacja ścieżek i mapowanie ikon dla wizualizacji drzewa. + +/// [ENG]: Global icon used for directory nodes. +/// [POL]: Globalna ikona używana dla węzłów będących folderami. +pub const DIR_ICON: &str = "📂"; + +pub const FILE_ICON: &str = "📄"; + +/// [ENG]: Defines visual and metadata properties for a file type. +/// [POL]: Definiuje wizualne i metadanowe właściwości dla typu pliku. +pub struct PathFileType { + pub icon: &'static str, + pub md_lang: &'static str, +} + +/// [ENG]: Returns file properties based on its extension. +/// [POL]: Zwraca właściwości pliku na podstawie jego rozszerzenia. +#[must_use] +pub fn get_file_type(ext: &str) -> PathFileType { + match ext { + "rs" => PathFileType { + icon: "🦀", + md_lang: "rust", + }, + "toml" => PathFileType { + icon: "⚙️", + md_lang: "toml", + }, + "slint" => PathFileType { + icon: "🎨", + md_lang: "slint", + }, + "md" => PathFileType { + icon: "📝", + md_lang: "markdown", + }, + "json" => PathFileType { + icon: "🔣", + md_lang: "json", + }, + "yaml" | "yml" => PathFileType { + icon: "🛠️", + md_lang: "yaml", + }, + "html" => PathFileType { + icon: "📖", + md_lang: "html", + }, + "css" => PathFileType { + icon: "🖌️", + md_lang: "css", + }, + "js" => PathFileType { + icon: "📜", + md_lang: "javascript", + }, + "ts" => PathFileType { + icon: "📘", + md_lang: "typescript", + }, + // [ENG]: Default fallback for unknown file types. + // [POL]: Domyślny fallback dla nieznanych typów plików. + _ => PathFileType { + icon: "📄", + md_lang: "text", + }, + } +} + +/// [ENG]: Character set used for drawing tree branches and indents. +/// [POL]: Zestaw znaków używanych do rysowania gałęzi drzewa i wcięć. +#[derive(Debug, Clone)] +pub struct TreeStyle { + // [ENG]: Directories (d) + // [POL]: Foldery (d) + pub dir_last_with_children: String, // └──┬ + pub dir_last_no_children: String, // └─── + pub dir_mid_with_children: String, // ├──┬ + pub dir_mid_no_children: String, // ├─── + + // [ENG]: Files (f) + // [POL]: Pliki (f) + pub file_last: String, // └──• + pub file_mid: String, // ├──• + + // [ENG]: Indentations for subsequent levels (i) + // [POL]: Wcięcia dla kolejnych poziomów (i) + pub indent_last: String, // " " + pub indent_mid: String, // "│ " +} + +impl Default for TreeStyle { + fn default() -> Self { + Self { + dir_last_with_children: "└──┬".to_string(), + dir_last_no_children: "└───".to_string(), + dir_mid_with_children: "├──┬".to_string(), + dir_mid_no_children: "├───".to_string(), + + file_last: "└──•".to_string(), + file_mid: "├──•".to_string(), + + indent_last: " ".to_string(), + indent_mid: "│ ".to_string(), + } + } +} diff --git a/src/tui/dist.rs b/src/tui/dist.rs deleted file mode 100644 index 550f1b8..0000000 --- a/src/tui/dist.rs +++ /dev/null @@ -1,48 +0,0 @@ -use cliclack::{confirm, input, intro, spinner}; -use lib::fn_copy_dist::{DistConfig, copy_dist}; - -pub fn run_dist_flow() { - intro(" 📦 Zarządzanie Dystrybucją ").unwrap(); - - let target: String = input("Katalog kompilacji (target):") - .default_input("./target") - .interact() - .unwrap(); - let dist: String = input("Katalog docelowy (dist):") - .default_input("./dist") - .interact() - .unwrap(); - let bins: String = input("Binarki (przecinek) [Enter = wszystkie]:") - .required(false) - .interact() - .unwrap_or_default(); - - let clear = confirm("Wyczyścić katalog docelowy?") - .initial_value(true) - .interact() - .unwrap(); - let dry = confirm("Tryb symulacji (Dry Run)?") - .initial_value(false) - .interact() - .unwrap(); - - let spin = spinner(); - spin.start("Kopiowanie artefaktów..."); - - let owned_bins = super::utils::split_and_trim(&bins); - let bin_refs: Vec<&str> = owned_bins.iter().map(|s| s.as_str()).collect(); - - let config = DistConfig { - target_dir: &target, - dist_dir: &dist, - binaries: bin_refs, - clear_dist: clear, - overwrite: true, - dry_run: dry, - }; - - match copy_dist(&config) { - Ok(f) => spin.stop(format!("Zakończono. Przetworzono {} plików.", f.len())), - Err(e) => spin.error(format!("Błąd: {}", e)), - } -} diff --git a/src/tui/doc.rs b/src/tui/doc.rs deleted file mode 100644 index 30e54ec..0000000 --- a/src/tui/doc.rs +++ /dev/null @@ -1,142 +0,0 @@ -use cliclack::{confirm, input, intro, spinner}; -use lib::fn_doc_gen::generate_docs; -use lib::fn_doc_models::DocTask; -use lib::fn_filespath::Task; - -// Importujemy niezbędne narzędzia z modułu utils -use super::utils::{TaskData, ask_for_task_data}; - -pub fn run_doc_flow() { - let output_dir: String = input("Katalog wyjściowy dla raportów:") - .default_input("doc") - .interact() - .unwrap(); - - let mut reports_configs = Vec::new(); - - loop { - intro(format!( - " 📄 Konfiguracja raportu nr {} ", - reports_configs.len() + 1 - )) - .unwrap(); - - let name: String = input("Nazwa pliku (prefix):") - .default_input("code") - .interact() - .unwrap(); - - let id_s = super::utils::select_id_style(); - let tree_s = super::utils::select_tree_style(); - - // -- NOWY BLOK WAG -- - let w_cfg = if tree_s != "with-out" { - super::utils::ask_for_weight_config() - } else { - lib::fn_weight::WeightConfig { - system: lib::fn_weight::UnitSystem::None, - ..Default::default() - } - }; - - let mut tasks_for_this_report = Vec::new(); - - let wm = cliclack::select("Gdzie umieścić podpis (watermark) cargo-plot?") - .item("last", "Na końcu pliku (Domyślnie)", "") - .item("first", "Na początku pliku", "") - .item("none", "Nie dodawaj podpisu", "") - .interact() - .unwrap(); - - let print_cmd = - confirm("Czy wygenerować na górze raportu komendę odtwarzającą to zadanie?") - .initial_value(true) - .interact() - .unwrap(); - - loop { - // Teraz funkcja jest zaimportowana, więc zadziała bezpośrednio - tasks_for_this_report.push(ask_for_task_data(tasks_for_this_report.len() + 1)); - - if !confirm("Czy dodać kolejne zadanie skanowania (Task) DO TEGO raportu?") - .initial_value(false) - .interact() - .unwrap() - { - break; - } - } - - reports_configs.push(( - name, - id_s, - tree_s, - tasks_for_this_report, - w_cfg, - wm, - print_cmd, - )); - - if !confirm("Czy chcesz zdefiniować KOLEJNY, osobny raport (DocTask)?") - .initial_value(false) - .interact() - .unwrap() - { - break; - } - } - - let is_dry = confirm("Czy uruchomić tryb symulacji (Dry-Run)?") - .initial_value(false) - .interact() - .unwrap(); - - let spin = spinner(); - spin.start("Generowanie wszystkich raportów..."); - - let mut final_doc_tasks = Vec::new(); - - for r in &reports_configs { - let api_tasks: Vec = r.3.iter().map(|t: &TaskData| t.to_api_task()).collect(); - - // TUI generuje "zastępczą" komendę CLI, którą można skopiować! - let cmd_str = if r.6 { - let mut mock_cmd = format!( - "cargo plot doc --out-dir \"{}\" --out \"{}\" -I {} -T {}", - output_dir, r.0, r.1, r.2 - ); - for t in &r.3 { - mock_cmd.push_str(&format!(" --task \"loc={},out={}\"", t.loc, t.out_type)); - } - Some(mock_cmd) - } else { - None - }; - - final_doc_tasks.push(DocTask { - output_filename: &r.0, - insert_tree: r.2, - id_style: r.1, - tasks: api_tasks, - weight_config: r.4.clone(), - watermark: r.5, - command_str: cmd_str, - // W TUI domyślnie zachowujemy się jak wcześniej (możemy to w przyszłości rozbudować) - suffix_stamp: true, - title_file: "RAPORT", - title_file_with_path: true, - }); - } - - if is_dry { - spin.stop(format!( - "Symulacja zakończona. Wygenerowano by {} raportów.", - final_doc_tasks.len() - )); - } else { - match generate_docs(final_doc_tasks, &output_dir) { - Ok(_) => spin.stop(format!("Wszystkie raporty zapisano w /{}/", output_dir)), - Err(e) => spin.error(format!("Błąd krytyczny: {}", e)), - } - } -} diff --git a/src/tui/mod.rs b/src/tui/mod.rs deleted file mode 100644 index fd31373..0000000 --- a/src/tui/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -use cliclack::{confirm, intro, outro, outro_cancel, select}; -use std::process::exit; - -mod dist; -mod doc; -mod stamp; -mod tree; -mod utils; - -pub fn run_tui() { - intro(" 📦 cargo-plot - Profesjonalny Panel Sterowania ").unwrap(); - - loop { - let action = select("Wybierz moduł API:") - .item( - "tree", - "🌲 Tree Explorer", - "Wizualizacja struktur (Multi-Task)", - ) - .item( - "doc", - "📄 Doc Orchestrator", - "Generowanie raportów Markdown", - ) - .item("dist", "📦 Dist Manager", "Zarządzanie paczkami binarnymi") - .item("stamp", "🕒 Stamp Tool", "Generator sygnatur czasowych") - .item("quit", "❌ Wyjdź", "") - .interact(); - - match action { - Ok("tree") => tree::run_tree_flow(), - Ok("doc") => doc::run_doc_flow(), - Ok("dist") => dist::run_dist_flow(), - Ok("stamp") => stamp::run_stamp_flow(), - Ok("quit") => { - outro("Zamykanie panelu...").unwrap(); - exit(0); - } - _ => { - outro_cancel("Przerwano.").unwrap(); - exit(0); - } - } - - if !confirm("Czy chcesz wykonać inną operację?") - .initial_value(true) - .interact() - .unwrap_or(false) - { - outro("Do zobaczenia!").unwrap(); - break; - } - } -} diff --git a/src/tui/stamp.rs b/src/tui/stamp.rs deleted file mode 100644 index 5892772..0000000 --- a/src/tui/stamp.rs +++ /dev/null @@ -1,32 +0,0 @@ -use cliclack::{confirm, input, intro, outro}; -use lib::fn_datestamp::{NaiveDate, NaiveTime, datestamp, datestamp_now}; - -pub fn run_stamp_flow() { - intro(" 🕒 Generator Sygnatur Czasowych ").unwrap(); - - let custom = confirm("Czy chcesz podać własną datę i czas?") - .initial_value(false) - .interact() - .unwrap(); - - if custom { - let d_str: String = input("Data (RRRR-MM-DD):") - .placeholder("2026-03-10") - .interact() - .unwrap(); - - let t_str: String = input("Czas (GG:MM:SS):") - .placeholder("14:30:00") - .interact() - .unwrap(); - - let d = NaiveDate::parse_from_str(&d_str, "%Y-%m-%d").expect("Błędny format daty"); - let t = NaiveTime::parse_from_str(&t_str, "%H:%M:%S").expect("Błędny format czasu"); - - let s = datestamp(d, t); - outro(format!("Wygenerowana sygnatura: {}", s)).unwrap(); - } else { - let s = datestamp_now(); - outro(format!("Aktualna sygnatura: {}", s)).unwrap(); - } -} diff --git a/src/tui/tree.rs b/src/tui/tree.rs deleted file mode 100644 index df6e582..0000000 --- a/src/tui/tree.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::utils::TaskData; -use cliclack::{confirm, intro, spinner}; // Usunięto outro i select -use lib::fn_filespath::{Task, filespath}; -use lib::fn_filestree::filestree; -use lib::fn_plotfiles::plotfiles_cli; // Usunięto ask_for_task_data (jeśli nieużywane bezpośrednio) - -pub fn run_tree_flow() { - intro(" 🌲 Eksplorator Drzewa (Multi-Task) ").unwrap(); - - let mut tasks_data: Vec = Vec::new(); - - loop { - tasks_data.push(super::utils::ask_for_task_data(tasks_data.len() + 1)); - if !confirm("Czy dodać kolejną lokalizację (Task)?") - .initial_value(false) - .interact() - .unwrap() - { - break; - } - } - - let sort = super::utils::select_sort(); - - // -- ZMIANA: Wywołujemy nowy konfigurator wag -- - let w_cfg = super::utils::ask_for_weight_config(); - - // Prefix '_' mówi Rustowi: "Wiem, że tego nie używam (jeszcze), nie krzycz" - let _use_custom_style = confirm("Czy użyć niestandardowego stylu gałęzi?") - .initial_value(false) - .interact() - .unwrap(); - - let save_to_file = - confirm("Czy zapisać wynikowe drzewo do pliku .md (zamiast pokazywać w konsoli)?") - .initial_value(false) - .interact() - .unwrap(); - - let md_path = if save_to_file { - // Wymuszamy typowanie bezpośrednio na zmiennej wejściowej 'path', tak jak to robiliśmy w innych miejscach - let path: String = cliclack::input("Podaj nazwę pliku (np. drzewo.md):") - .default_input("drzewo.md") - .interact() - .unwrap(); - Some(path) - } else { - None - }; - - let spin = spinner(); - spin.start("Budowanie złożonej struktury..."); - - let tasks: Vec = tasks_data - .iter() - .map(|t: &super::utils::TaskData| t.to_api_task()) - .collect(); - - let nodes = filestree(filespath(&tasks), sort, &w_cfg); - - spin.stop("Skanowanie zakończone:"); - - // Generujemy tekst drzewa - if let Some(path) = md_path { - let txt = lib::fn_plotfiles::plotfiles_txt(&nodes, "", None); - std::fs::write(&path, format!("```text\n{}\n```\n", txt)).unwrap(); - cliclack::outro(format!("Sukces! Drzewo zapisano do pliku: {}", path)).unwrap(); - } else { - let tree_output = plotfiles_cli(&nodes, "", None); - if tree_output.trim().is_empty() { - cliclack::outro_cancel("Brak wyników: Żaden plik nie pasuje do podanych filtrów.") - .unwrap(); - } else { - println!("\n{}\n", tree_output); - cliclack::outro("Drzewo wyrenderowane pomyślnie!").unwrap(); - } - } -} diff --git a/src/tui/utils.rs b/src/tui/utils.rs deleted file mode 100644 index f623df8..0000000 --- a/src/tui/utils.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Plik: src/tui/utils.rs -use cliclack::{input, select}; -use lib::fn_weight::{UnitSystem, WeightConfig}; - -pub struct TaskData { - pub loc: String, - pub inc: Vec, - pub exc: Vec, - pub fil: Vec, - pub out_type: &'static str, -} - -impl TaskData { - // FIX: Dodaliśmy <'_>, aby uciszyć ostrzeżenie o elidowanych lifetime'ach - pub fn to_api_task(&self) -> lib::fn_filespath::Task<'_> { - lib::fn_filespath::Task { - path_location: &self.loc, - path_include_only: self.inc.iter().map(|s| s.as_str()).collect(), - path_exclude: self.exc.iter().map(|s| s.as_str()).collect(), - filter_files: self.fil.iter().map(|s| s.as_str()).collect(), - output_type: self.out_type, - // FIX: Usunięto ..Default::default(), bo wypełniamy wszystkie pola - } - } -} - -pub fn ask_for_task_data(idx: usize) -> TaskData { - println!("\n--- Konfiguracja zadania #{} ---", idx); - let loc: String = input(" Lokalizacja (loc):") - .default_input(".") - .interact() - .unwrap(); - - let use_defaults = cliclack::confirm( - "Czy użyć domyślnej listy ignorowanych (pomiń .git, target, node_modules itp.)?", - ) - .initial_value(true) - .interact() - .unwrap(); - - let inc; - let exc; - let fil; - - if use_defaults { - inc = vec![]; - exc = vec![ - ".git/".to_string(), - "target/".to_string(), - "node_modules/".to_string(), - ".vs/".to_string(), - ".idea/".to_string(), - ".vscode/".to_string(), - ".cargo/".to_string(), - ".github/".to_string(), - ]; - fil = vec![]; - } else { - let inc_raw: String = cliclack::input(" Whitelist (inc) [oddzielaj przecinkiem]:") - .placeholder("np. ./src/, Cargo.toml, ./lib/") - .required(false) - .interact() - .unwrap_or_default(); - - let exc_raw: String = cliclack::input(" Blacklist (exc) [oddzielaj przecinkiem]:") - .placeholder("np. ./target/, .git/, node_modules/, Cargo.lock") - .required(false) - .interact() - .unwrap_or_default(); - - let fil_raw: String = cliclack::input(" Filtry plików (fil) [oddzielaj przecinkiem]:") - .placeholder("np. *.rs, *.md, build.rs") - .required(false) - .interact() - .unwrap_or_default(); - - inc = process_inc(split_and_trim(&inc_raw)); - exc = split_and_trim(&exc_raw); - fil = split_and_trim(&fil_raw); - } - - let out_type = select_type(); - - TaskData { - loc, - inc, - exc, - fil, - out_type, - } -} - -fn process_inc(list: Vec) -> Vec { - list.into_iter() - .map(|s| { - // FIX na "Brak Wyniku": Usuwamy ./ z początku, bo Glob tego nie lubi - let cleaned = s.trim_start_matches("./"); - - if cleaned.ends_with('/') || !cleaned.contains('.') { - let base = cleaned.trim_end_matches('/'); - if base.is_empty() { - "**/*".to_string() - } else { - format!("{}/**/*", base) - } - } else { - cleaned.to_string() - } - }) - .collect() -} - -pub fn split_and_trim(input: &str) -> Vec { - input - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() -} - -pub fn select_sort() -> &'static str { - select("Sortowanie:") - .item("alpha", "Alfabetyczne", "") - .item("dirs-first", "Katalogi najpierw", "") - .item("files-first", "Pliki najpierw", "") - .interact() - .unwrap() -} - -pub fn select_type() -> &'static str { - select("Co wyświetlić?") - .item("dirs_and_files", "Wszystko", "") - .item("files", "Tylko pliki", "") - .item("dirs", "Tylko foldery", "") - .interact() - .unwrap() -} - -pub fn select_id_style() -> &'static str { - select("Styl nagłówków (ID):") - .item("id-tag", "Opisowy (tag)", "") - .item("id-num", "Numerowany (num)", "") - .item("id-non", "Tylko ścieżka", "") - .interact() - .unwrap() -} - -pub fn select_tree_style() -> &'static str { - select("Spis treści (drzewo):") - .item("files-first", "Pliki na górze", "") - .item("dirs-first", "Foldery na górze", "") - .item("with-out", "Brak drzewa", "") - .interact() - .unwrap() -} - -pub fn ask_for_weight_config() -> WeightConfig { - let system_str = select("Czy wyświetlać wagę (rozmiar) plików i folderów?") - .item("none", "❌ Nie (wyłączone)", "") - .item("binary", "💾 System binarny (KiB, MiB)", "IEC: 1024^n") - .item("decimal", "💽 System dziesiętny (kB, MB)", "SI: 1000^n") - .interact() - .unwrap(); - - let system = match system_str { - "binary" => UnitSystem::Binary, - "decimal" => UnitSystem::Decimal, - _ => { - return WeightConfig { - system: UnitSystem::None, - ..Default::default() - }; - } - }; - - // Jeśli wybrano system, zadajemy pytania szczegółowe - let precision_str: String = input("Precyzja (szerokość ramki liczbowej):") - .default_input("5") - .interact() - .unwrap(); - - let precision = precision_str.parse::().unwrap_or(5).max(3); - - let show_for_files = cliclack::confirm("Czy pokazywać rozmiar przy plikach?") - .initial_value(true) - .interact() - .unwrap(); - - let show_for_dirs = cliclack::confirm("Czy pokazywać zsumowany rozmiar przy folderach?") - .initial_value(true) - .interact() - .unwrap(); - - let mut dir_sum_included = true; - if show_for_dirs { - let sum_mode = select("Jak liczyć pojemność folderów?") - .item( - "filtered", - "Suma widocznych plików", - "Tylko pliki ujęte na liście", - ) - .item( - "real", - "Rzeczywisty rozmiar", - "Bezpośrednio z dysku twardego", - ) - .interact() - .unwrap(); - dir_sum_included = sum_mode == "filtered"; - } - - WeightConfig { - system, - precision, - show_for_files, - show_for_dirs, - dir_sum_included, - } -} diff --git a/u.md b/u.md deleted file mode 100644 index b78e838..0000000 --- a/u.md +++ /dev/null @@ -1,79 +0,0 @@ -# 🛠 Notatki Dewelopera (Cheat Sheet) - -Ten plik zawiera zbiór najważniejszych komend i procedur używanych podczas tworzenia, utrzymania i publikacji paczki `cargo-plot`. - -## 🧹 Higiena kodu (Zanim zrobisz commit) - -Jeśli GitHub Actions (CI) odrzuca Twój kod z powodu błędów formatowania, zawsze przepuść go przez te dwa narzędzia przed wysłaniem na serwer: - -* `cargo fmt` – Automatycznie formatuje cały kod do oficjalnego standardu Rusta (naprawia błędy wyrzucane przez CI). -* `cargo clippy` – Uruchamia zaawansowanego lintera, który wyłapuje nieoptymalny lub niebezpieczny kod. - -```bash -# Szybka ścieżka naprawcza po odrzuceniu przez CI: -cargo fmt -git add . -git commit -m "style: apply cargo fmt to fix CI pipeline" -git push origin main - -``` - -## 🏷️ Zarządzanie Wydaniami i CI/CD (GitHub Actions) - -Proces automatycznego budowania binarek (`.exe`, `.tar.gz`) opiera się na tagach. - -### Standardowe wydanie nowej wersji: - -```bash -git tag v0.1.2 -git push origin v0.1.2 - -``` - -### 🚨 Procedura Ratunkowa: Jak cofnąć i naprawić zepsuty Tag? - -Jeśli nadałeś tag (np. `v0.1.1`), ale akcja na GitHubie zakończyła się błędem (np. zapomniałeś zrobić `cargo fmt` lub wkradł się błąd), musisz usunąć ten znacznik z obu miejsc i wypchnąć go ponownie po naprawieniu kodu: - -```bash -# 1. Usuń zepsuty tag z serwera GitHuba -git push origin --delete v0.1.1 - -# 2. Usuń zepsuty tag ze swojego lokalnego komputera -git tag -d v0.1.1 - -# 3. [TUTAJ NAPRAW BŁĄD, ZRÓB COMMIT I PUSH DO MAIN] - -# 4. Stwórz nowy tag (już na poprawionym kodzie) -git tag v0.1.1 - -# 5. Wypchnij go ponownie, by odpalić maszynę budującą na czysto! -git push origin v0.1.1 - -``` - -## 📦 Publikacja w rejestrze (Crates.io) - -Projekt wykorzystuje **Trusted Publishing** (OIDC). Oznacza to, że po pierwszej ręcznej publikacji, serwer crates.io nie przyjmuje już publikacji bezpośrednio z terminala (API tokenów), lecz ufa wyłącznie plikowi `release.yml` z GitHuba. - -Mimo to, komendy lokalne są przydatne do testowania: - -* `cargo login` – (Używane tylko raz przy autoryzacji środowiska). -* `cargo publish --dry-run` – Pakuje projekt w izolowanym środowisku i weryfikuje poprawność metadanych w `Cargo.toml`. Zawsze wykonuj przed planowanym wydaniem! - -```bash -# Test przed-wydawniczy: -cargo publish --dry-run - -``` - -## ⚡ - -* `cargo plot tree -s files-first --no-default-excludes -e "./f.md" -e "./d.md" -e "./target/" -e "./.git/" -e "./test/" -e "./.gitignore" -e "./u.md" -e "./Cargo.lock" -e "./LICENSE-APACHE" -e "./LICENSE-MIT" -e "./.github/" -e "./.cargo/" -e "./doc/" -e "./README.md" -w binary --weight-precision 5 --no-dir-weight --out-file "f.md" --print-console --watermark last --print-command --title-file "Struktura Projektu"` - -* `cargo plot doc --out-dir "." --out "d" -I num -T files-first --no-default-excludes -e "./f.md" -e "./d.md" -e "./target/" -e "./.git/" -e "./test/" -e "./.gitignore" -e "./u.md" -e "./Cargo.lock" -e "./LICENSE-APACHE" -e "./LICENSE-MIT" -e "./.github/" -e "./.cargo/" -e "./doc/" -e "./README.md" -w binary --weight-precision 5 --no-dir-weight --watermark last --print-command --title-file "Dokumentacja Projektu"` - -* `cargo run -- plot tree -s files-first --no-default-excludes -e "./f.md" -e "./d.md" -e "./target/" -e "./.git/" -e "./test/" -e "./.gitignore" -e "./u.md" -e "./Cargo.lock" -e "./LICENSE-APACHE" -e "./LICENSE-MIT" -e "./.github/" -e "./.cargo/" -e "./doc/" -e "./README.md" -w binary --weight-precision 5 --no-dir-weight --out-file "f.md" --print-console --watermark last --print-command --title-file "Struktura Projektu"` - -* `cargo run -- plot doc --out-dir "." --out "d" -I num -T files-first --no-default-excludes -e "./f.md" -e "./d.md" -e "./target/" -e "./.git/" -e "./test/" -e "./.gitignore" -e "./u.md" -e "./Cargo.lock" -e "./LICENSE-APACHE" -e "./LICENSE-MIT" -e "./.github/" -e "./.cargo/" -e "./doc/" -e "./README.md" -w binary --weight-precision 5 --no-dir-weight --watermark last --print-command --title-file "Dokumentacja Projektu"` - --------------------------------------------