From 5b5c06f1f6ed42800f997b60119bd88263162553 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sat, 14 Mar 2026 17:39:36 +0100 Subject: [PATCH 01/45] =?UTF-8?q?przygotowanie=20pola=20pod=20now=C4=85=20?= =?UTF-8?q?wersje?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 36 +- d.md | 2795 --------------------------------- f.md | 47 - src/cli/args.rs | 256 --- src/cli/dist.rs | 24 - src/cli/doc.rs | 57 - src/cli/mod.rs | 18 - src/cli/stamp.rs | 14 - src/cli/tree.rs | 100 -- src/cli/utils.rs | 77 - src/lib/fn_copy_dist.rs | 195 --- src/lib/fn_datestamp.rs | 66 - src/lib/fn_doc_gen.rs | 64 - src/lib/fn_doc_id.rs | 85 - src/lib/fn_doc_models.rs | 16 - src/lib/fn_doc_write.rs | 141 -- src/lib/fn_files_blacklist.rs | 40 - src/lib/fn_filespath.rs | 263 ---- src/lib/fn_filestree.rs | 144 -- src/lib/fn_path_utils.rs | 19 - src/lib/fn_pathtype.rs | 60 - src/lib/fn_plotfiles.rs | 131 -- src/lib/fn_weight.rs | 128 -- src/lib/mod.rs | 16 - src/main.rs | 23 - src/tui/dist.rs | 48 - src/tui/doc.rs | 142 -- src/tui/mod.rs | 54 - src/tui/stamp.rs | 32 - src/tui/tree.rs | 78 - src/tui/utils.rs | 219 --- u.md | 79 - 32 files changed, 7 insertions(+), 5460 deletions(-) delete mode 100644 d.md delete mode 100644 f.md delete mode 100644 src/cli/args.rs delete mode 100644 src/cli/dist.rs delete mode 100644 src/cli/doc.rs delete mode 100644 src/cli/mod.rs delete mode 100644 src/cli/stamp.rs delete mode 100644 src/cli/tree.rs delete mode 100644 src/cli/utils.rs delete mode 100644 src/lib/fn_copy_dist.rs delete mode 100644 src/lib/fn_datestamp.rs delete mode 100644 src/lib/fn_doc_gen.rs delete mode 100644 src/lib/fn_doc_id.rs delete mode 100644 src/lib/fn_doc_models.rs delete mode 100644 src/lib/fn_doc_write.rs delete mode 100644 src/lib/fn_files_blacklist.rs delete mode 100644 src/lib/fn_filespath.rs delete mode 100644 src/lib/fn_filestree.rs delete mode 100644 src/lib/fn_path_utils.rs delete mode 100644 src/lib/fn_pathtype.rs delete mode 100644 src/lib/fn_plotfiles.rs delete mode 100644 src/lib/fn_weight.rs delete mode 100644 src/lib/mod.rs delete mode 100644 src/main.rs delete mode 100644 src/tui/dist.rs delete mode 100644 src/tui/doc.rs delete mode 100644 src/tui/mod.rs delete mode 100644 src/tui/stamp.rs delete mode 100644 src/tui/tree.rs delete mode 100644 src/tui/utils.rs delete mode 100644 u.md diff --git a/Cargo.toml b/Cargo.toml index 21a3199..22d31ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,30 +1,16 @@ [package] name = "cargo-plot" -version = "0.1.5" +version = "0.2.0-alpha.1" 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,23 +18,15 @@ 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"], -} +clap = { version = "4.5.60", features = ["derive"] } cliclack = "0.4.1" colored = "3.1.1" +console = "0.16.3" +ctrlc = "3.5.2" -[lib] -name = "lib" -path = "src/lib/mod.rs" # ========================================== # Globalna konfiguracja lintów (Analiza kodu) diff --git a/d.md b/d.md deleted file mode 100644 index f0eb641..0000000 --- a/d.md +++ /dev/null @@ -1,2795 +0,0 @@ -# Dokumentacja Projektu 2026Q1D070W11_Wed11Mar_005445717 - -**Wywołana komenda:** -```bash -target\debug\cargo-plot.exe 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 -``` - -```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 -``` - -## Plik-001: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_doc_write.rs` - -```rust -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(()) -} - -``` - -## Plik-002: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_plotfiles.rs` - -```rust -// 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) -} - -``` - -## Plik-003: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/stamp.rs` - -```rust -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(); - } -} - -``` - -## Plik-004: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_doc_models.rs` - -```rust -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, -} - -``` - -## Plik-005: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_doc_gen.rs` - -```rust -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(()) -} - -``` - -## Plik-006: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_files_blacklist.rs` - -```rust -// 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) -} - -``` - -## Plik-007: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_copy_dist.rs` - -```rust -// 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) -} - -``` - -## Plik-008: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/mod.rs` - -```rust -// 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), - } -} - -``` - -## Plik-009: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/utils.rs` - -```rust -// 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 - } -} - -``` - -## Plik-010: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/tree.rs` - -```rust -// 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); - } -} - -``` - -## Plik-011: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/tree.rs` - -```rust -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(); - } - } -} - -``` - -## Plik-012: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_doc_id.rs` - -```rust -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(), - } -} - -``` - -## Plik-013: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/mod.rs` - -```rust -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; - } - } -} - -``` - -## Plik-014: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/doc.rs` - -```rust -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)), - } - } -} - -``` - -## Plik-015: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/main.rs` - -```rust -// Plik: src/main.rs -use clap::Parser; -use std::env; - -mod cli; -mod tui; - -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; - } - - // Jeśli są argumenty, pozwalamy Clapowi je sparsować (wymaga słowa 'plot') - let cli::args::CargoCli::Plot(plot_args) = cli::args::CargoCli::parse(); - - match plot_args.command { - Some(cmd) => cli::run_command(cmd), - None => tui::run_tui(), // Zadziała np. dla `cargo run -- plot` - } -} - -``` - -## Plik-016: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_filestree.rs` - -```rust -// 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 -} - -``` - -## Plik-017: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/stamp.rs` - -```rust -// 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()); - } -} - -``` - -## Plik-018: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/mod.rs` - -```rust -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; - -``` - -## Plik-019: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/utils.rs` - -```rust -// 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, - } -} - -``` - -## Plik-020: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/args.rs` - -```rust -// 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, -} - -``` - -## Plik-021: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/doc.rs` - -```rust -// 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); - } -} - -``` - -## Plik-022: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_path_utils.rs` - -```rust -// 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), - } -} - -``` - -## Plik-023: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_weight.rs` - -```rust -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...) - Both, - None, -} - -#[derive(Debug, Clone)] -pub struct WeightConfig { - pub system: UnitSystem, - pub precision: usize, // Całkowita szerokość pola "xxxxx" (min 3) - pub show_for_files: bool, - pub show_for_dirs: bool, - pub dir_sum_included: bool, // true = tylko uwzględnione, false = rzeczywista waga folderu -} - -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, - } - } -} - -/// 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) -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(); - } - - 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) -} - -fn get_dir_size(path: &Path) -> u64 { - fs::read_dir(path) - .map(|entries| { - entries - .filter_map(|e| e.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) -} - -``` - -## Plik-024: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/dist.rs` - -```rust -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)), - } -} - -``` - -## Plik-025: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_datestamp.rs` - -```rust -// ./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 - ) -} - -``` - -## Plik-026: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/Cargo.toml` - -```toml -[package] -name = "cargo-plot" -version = "0.1.5" -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." -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", -] -resolver = "3" - -[package.metadata.cargo] -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" -colored = "3.1.1" - -[lib] -name = "lib" -path = "src/lib/mod.rs" - -# ========================================== -# 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" -``` - -## Plik-027: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_pathtype.rs` - -```rust -// 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 - } -} - -``` - -## Plik-028: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_filespath.rs` - -```rust -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); - } - } -} - -``` - -## Plik-029: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/dist.rs` - -```rust -// 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), - } -} - -``` - ---- -> 🚀 Raport wygenerowany 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/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/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/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/fn_weight.rs b/src/lib/fn_weight.rs deleted file mode 100644 index fb07511..0000000 --- a/src/lib/fn_weight.rs +++ /dev/null @@ -1,128 +0,0 @@ -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...) - Both, - None, -} - -#[derive(Debug, Clone)] -pub struct WeightConfig { - pub system: UnitSystem, - pub precision: usize, // Całkowita szerokość pola "xxxxx" (min 3) - pub show_for_files: bool, - pub show_for_dirs: bool, - pub dir_sum_included: bool, // true = tylko uwzględnione, false = rzeczywista waga folderu -} - -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, - } - } -} - -/// 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) -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(); - } - - 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) -} - -fn get_dir_size(path: &Path) -> u64 { - fs::read_dir(path) - .map(|entries| { - entries - .filter_map(|e| e.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) -} 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 deleted file mode 100644 index a8087dc..0000000 --- a/src/main.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Plik: src/main.rs -use clap::Parser; -use std::env; - -mod cli; -mod tui; - -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; - } - - // Jeśli są argumenty, pozwalamy Clapowi je sparsować (wymaga słowa 'plot') - let cli::args::CargoCli::Plot(plot_args) = cli::args::CargoCli::parse(); - - match plot_args.command { - Some(cmd) => cli::run_command(cmd), - None => tui::run_tui(), // Zadziała np. dla `cargo run -- plot` - } -} 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"` - -------------------------------------------- From cda000f5d624b796d4cf9e287459b50c69cca5ee Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 12:43:36 +0100 Subject: [PATCH 02/45] (new: new tui) --- src/interfaces.rs | 4 + src/interfaces/tui.rs | 15 + src/interfaces/tui/i18n.rs | 156 ++++++++ src/interfaces/tui/menu.rs | 6 + src/interfaces/tui/menu/enter.rs | 198 ++++++++++ src/interfaces/tui/menu/job_add.rs | 119 ++++++ src/interfaces/tui/menu/job_add_custom.rs | 366 ++++++++++++++++++ src/interfaces/tui/menu/jobs_manager.rs | 162 ++++++++ src/interfaces/tui/menu/output_save.rs | 321 +++++++++++++++ src/interfaces/tui/menu/paths_struct_style.rs | 301 ++++++++++++++ src/interfaces/tui/state.rs | 142 +++++++ src/main.rs | 16 + 12 files changed, 1806 insertions(+) create mode 100644 src/interfaces.rs create mode 100644 src/interfaces/tui.rs create mode 100644 src/interfaces/tui/i18n.rs create mode 100644 src/interfaces/tui/menu.rs create mode 100644 src/interfaces/tui/menu/enter.rs create mode 100644 src/interfaces/tui/menu/job_add.rs create mode 100644 src/interfaces/tui/menu/job_add_custom.rs create mode 100644 src/interfaces/tui/menu/jobs_manager.rs create mode 100644 src/interfaces/tui/menu/output_save.rs create mode 100644 src/interfaces/tui/menu/paths_struct_style.rs create mode 100644 src/interfaces/tui/state.rs create mode 100644 src/main.rs diff --git a/src/interfaces.rs b/src/interfaces.rs new file mode 100644 index 0000000..5c5f238 --- /dev/null +++ b/src/interfaces.rs @@ -0,0 +1,4 @@ +// [EN]: User interaction layer (Ports and Adapters). +// [PL]: Warstwa interakcji z użytkownikiem (Porty i Adaptery). + +pub mod tui; diff --git a/src/interfaces/tui.rs b/src/interfaces/tui.rs new file mode 100644 index 0000000..00c97c0 --- /dev/null +++ b/src/interfaces/tui.rs @@ -0,0 +1,15 @@ +// [EN]: Interactive Terminal User Interface (TUI) module registry. +// [PL]: Rejestr modułu interaktywnego interfejsu tekstowego (TUI). + +pub mod i18n; +pub mod menu; +pub mod state; + +use state::StateTui; + +pub fn run_tui() { + let mut s = StateTui::new(); + cliclack::intro(" 📖 https://crates.io/crates/cargo-plot").unwrap(); + + menu::enter::menu_enter(&mut s); +} diff --git a/src/interfaces/tui/i18n.rs b/src/interfaces/tui/i18n.rs new file mode 100644 index 0000000..57645f9 --- /dev/null +++ b/src/interfaces/tui/i18n.rs @@ -0,0 +1,156 @@ +use super::state::Lang; + +// ===================================================================== +// BAZOWY FORMATER +// ===================================================================== + +// Uniwersalny padding - dodaje spacje po bokach każdego tekstu. +// Zmieniasz to tutaj -> zmienia się w CAŁEJ aplikacji! +fn pad(text: &str) -> String { + format!(" {} ", text) +} + +// ===================================================================== +// STRUKTURA WYMUSZAJĄCA JAWNE NAZWY JĘZYKÓW +// ===================================================================== + +pub struct Txt { + pub pol: &'static str, + pub eng: &'static str, +} + +// ===================================================================== +// GŁÓWNA CECHA (TRAIT) +// ===================================================================== + +pub trait Translatable { + // 1. Zwraca tekst w wielu językach + fn trans(&self) -> Txt; + + // 2. Opcjonalna stylizacja (kolory itp.). + // Domyślnie zwraca tekst bez zmian. + fn theme(&self, text: String) -> String { + text + } +} + +// ===================================================================== +// GLOBALNE TEKSTY (Nagłówki, Prompty, Komunikaty) +// ===================================================================== + +pub enum Prompt { + HeaderEnter, + HeaderJobAdd, + HeaderJobAddCustom, + HeaderJobsManager, + HeaderStyle, + HeaderOutput, + EnterDepth, + NoJobsWarning, + SuccessJobAdd, + JobAlreadyAtTop, + JobAlreadyAtBottom, + SuccessJobDeleted, + JobTitlePrefix, + ExitBye, + Canceled, +} + +impl Translatable for Prompt { + fn trans(&self) -> Txt { + match self { + Prompt::HeaderEnter => Txt { + pol: "📦 j-Cis/cargo-plot [POL]", + eng: "📦 j-Cis/cargo-plot [ENG]", + }, + Prompt::HeaderJobAdd => Txt { + pol: "Dodawanie zadania", + eng: "Adding a job", + }, + Prompt::HeaderJobAddCustom => Txt { + pol: "Definiowanie zadania", + eng: "Customizing job", + }, + Prompt::HeaderJobsManager => Txt { + pol: "Menadżer zadań", + eng: "Jobs manager", + }, + Prompt::HeaderStyle => Txt { + pol: "Stylizacja struktury", + eng: "Paths structure styling", + }, + Prompt::HeaderOutput => Txt { + pol: "Zapisywanie wyniku", + eng: "Saving output", + }, + Prompt::EnterDepth => Txt { + pol: "Podaj głębokość:", + eng: "Enter depth:", + }, + Prompt::NoJobsWarning => Txt { + pol: "Brak zadań w kolejce!", + eng: "No jobs in the queue!", + }, + Prompt::SuccessJobAdd => Txt { + pol: "Dodano zadanie do kolejki!", + eng: "Job added to queue!", + }, + Prompt::JobAlreadyAtTop => Txt { + pol: "Zadanie jest już na samej górze!", + eng: "Job is already at the top!", + }, + Prompt::JobAlreadyAtBottom => Txt { + pol: "Zadanie jest już na samym dole!", + eng: "Job is already at the bottom!", + }, + Prompt::SuccessJobDeleted => Txt { + pol: "Usunięto zadanie.", + eng: "Job deleted.", + }, + Prompt::JobTitlePrefix => Txt { + pol: "Zadanie: ", + eng: "Job: ", + }, + Prompt::ExitBye => Txt { + pol: "Do widzenia!", + eng: "Goodbye!", + }, + Prompt::Canceled => Txt { + pol: "Anulowano", + eng: "Canceled", + }, + } + } +} + +// ===================================================================== +// KONTEKST TŁUMACZA (Translator) +// ===================================================================== + +pub struct T { + lang: Lang, +} +impl T { + pub fn new(lang: Lang) -> Self { + Self { lang } + } + + fn get_text(&self, item: &I) -> String { + let txt = item.trans(); + let text = match self.lang { + Lang::POL => txt.pol, + Lang::ENG => txt.eng, + }; + pad(text) + } + + // Zmiana nazwy na `fmt`! + pub fn fmt(&self, item: I) -> String { + let base_text = self.get_text(&item); + item.theme(base_text) + } + + pub fn raw(&self, item: I) -> String { + self.get_text(&item) + } +} diff --git a/src/interfaces/tui/menu.rs b/src/interfaces/tui/menu.rs new file mode 100644 index 0000000..e03b3dd --- /dev/null +++ b/src/interfaces/tui/menu.rs @@ -0,0 +1,6 @@ +pub mod enter; +pub mod job_add; +pub mod job_add_custom; +pub mod jobs_manager; +pub mod output_save; +pub mod paths_struct_style; diff --git a/src/interfaces/tui/menu/enter.rs b/src/interfaces/tui/menu/enter.rs new file mode 100644 index 0000000..892e511 --- /dev/null +++ b/src/interfaces/tui/menu/enter.rs @@ -0,0 +1,198 @@ +use super::super::i18n::{Prompt, T, Translatable, Txt}; +use super::super::state::{Lang, StateTui}; +use super::job_add::menu_job_add; +use super::jobs_manager::menu_jobs_manager; +use super::output_save::menu_output_save; +use super::paths_struct_style::menu_paths_struct_style; +use console::style; + +#[derive(Default, Clone, Eq, PartialEq)] +enum ActionEnter { + #[default] + ChangeLang, + JobAdd, + JobsManager, + PathsStructStyle, + PathsStructPrint, + OutputSave, + CommandViewStructure, + CommandSaveDocuments, + Exit, +} + +//impl Default for ActionEnter { +// fn default() -> Self { +// Self::ChangeLang +// } +//} + +// ===================================================================== +// WARSTWA JĘZYKOWO - STYLIZACYJNA +// ===================================================================== + +impl Translatable for ActionEnter { + fn trans(&self) -> Txt { + match self { + ActionEnter::ChangeLang => Txt { + pol: "ustaw POL język", + eng: "set ENG lang", + }, + ActionEnter::JobAdd => Txt { + pol: "dodaj zadanie", + eng: "add job", + }, + ActionEnter::JobsManager => Txt { + pol: "menadżer zadań", + eng: "manager jobs", + }, + ActionEnter::PathsStructStyle => Txt { + pol: "stylizacja struktury ścieżek", + eng: "style of paths structure", + }, + ActionEnter::PathsStructPrint => Txt { + pol: "wyświetl strukturę ścieżek", + eng: "print the path structure", + }, + ActionEnter::OutputSave => Txt { + pol: "zapisz wynik", + eng: "save output", + }, + ActionEnter::CommandViewStructure => Txt { + pol: "komenda podglądu struktury", + eng: "command print structure", + }, + ActionEnter::CommandSaveDocuments => Txt { + pol: "komenda zapisu dokumentacji", + eng: "command save documents", + }, + ActionEnter::Exit => Txt { + pol: "wyjście", + eng: "exit", + }, + } + } + + fn theme(&self, text: String) -> String { + match self { + // Wyróżniamy "Dodaj zadanie" na białym tle + ActionEnter::JobAdd => style(text).on_white().black().to_string(), + + // Wyróżniamy akcje związane z wynikiem mocnym niebieskim + ActionEnter::PathsStructPrint | ActionEnter::OutputSave => { + style(text).bold().on_blue().white().to_string() + } + + // NOWOŚĆ: Fioletowe tło (magenta) i biały tekst dla komendy + ActionEnter::CommandViewStructure => { + style(text).bold().on_magenta().white().to_string() + } + // NOWOŚĆ: Fioletowe tło (magenta) i biały tekst dla komendy + ActionEnter::CommandSaveDocuments => { + style(text).bold().on_magenta().white().to_string() + } + + // Reszta bez specjalnego formatowania + _ => text, + } + } +} + +// ===================================================================== +// WIDOK MENU GŁÓWNEGO +// ===================================================================== + +pub fn menu_enter(s: &mut StateTui) { + let mut last_action = ActionEnter::default(); + + loop { + // 1. INICJUJEMY TŁUMACZA DLA TEJ PĘTLI + let t = T::new(s.lang); + + // 2. STYLIZUJEMY GŁÓWNY NAGŁÓWEK (Pobierany z globalnych Promptów) + let header = style(t.raw(Prompt::HeaderEnter)) + .on_white() + .black() + .to_string(); + + // 3. BUDUJEMY MENU + let action_result = cliclack::select(header) + .initial_value(last_action.clone()) + .item(ActionEnter::ChangeLang, t.fmt(ActionEnter::ChangeLang), "") + .item(ActionEnter::JobAdd, t.fmt(ActionEnter::JobAdd), "") + .item( + ActionEnter::JobsManager, + t.fmt(ActionEnter::JobsManager), + "", + ) + .item( + ActionEnter::PathsStructStyle, + t.fmt(ActionEnter::PathsStructStyle), + "", + ) + .item( + ActionEnter::PathsStructPrint, + t.fmt(ActionEnter::PathsStructPrint), + "", + ) + .item(ActionEnter::OutputSave, t.fmt(ActionEnter::OutputSave), "") + .item( + ActionEnter::CommandViewStructure, + t.fmt(ActionEnter::CommandViewStructure), + "", + ) + .item( + ActionEnter::CommandSaveDocuments, + t.fmt(ActionEnter::CommandSaveDocuments), + "", + ) + .item(ActionEnter::Exit, t.fmt(ActionEnter::Exit), "") + .interact(); + + // 4. OBSŁUGA AKCJI + match action_result { + Ok(action) => { + last_action = action.clone(); + + match action { + ActionEnter::ChangeLang => { + s.lang = match s.lang { + Lang::POL => Lang::ENG, + Lang::ENG => Lang::POL, + }; + let t_new = T::new(s.lang); + cliclack::intro(t_new.raw(Prompt::HeaderEnter)).unwrap(); + } + ActionEnter::JobAdd => { + menu_job_add(s); + } + ActionEnter::JobsManager => { + menu_jobs_manager(s); + } + ActionEnter::PathsStructStyle => { + menu_paths_struct_style(s); + } + ActionEnter::PathsStructPrint => { + // TUTAJ MIEJSCE NA FUNKCJE WYŚWIETLANIA DRZEWA (np. pager minus) + } + ActionEnter::OutputSave => { + menu_output_save(s); + } + ActionEnter::CommandViewStructure => { + // Na razie opcja nic nie robi - wraca z powrotem do pętli menu głównego + } + ActionEnter::CommandSaveDocuments => { + // Na razie opcja nic nie robi - wraca z powrotem do pętli menu głównego + } + ActionEnter::Exit => { + cliclack::outro(t.raw(Prompt::ExitBye)).unwrap(); + break; + } + } + } + Err(_) => { + cliclack::outro_cancel(t.raw(Prompt::Canceled)).unwrap(); + break; + } + } + } +} diff --git a/src/interfaces/tui/menu/job_add.rs b/src/interfaces/tui/menu/job_add.rs new file mode 100644 index 0000000..3f84c1d --- /dev/null +++ b/src/interfaces/tui/menu/job_add.rs @@ -0,0 +1,119 @@ +use super::super::i18n::{Prompt, T, Translatable, Txt}; // Importujemy nasz nowy, odchudzony silnik! +use super::super::state::{JobConfig, StateTui}; +use super::job_add_custom::menu_job_add_custom; +use console::style; + +#[derive(Default, Clone, Eq, PartialEq)] +enum ActionJobAdd { + #[default] + JobAddDefault, + JobAddCustom, + Back, +} + +//impl Default for ActionJobAdd { +// fn default() -> Self { +// Self::JobAddDefault +// } +//} + +// ===================================================================== +// WARSTWA JĘZYKOWO - STYLIZACYJNA +// ===================================================================== + +impl Translatable for ActionJobAdd { + fn trans(&self) -> Txt { + match self { + ActionJobAdd::JobAddDefault => Txt { + pol: "Dodaj zadanie domyślne", + eng: "Add default job", + }, + ActionJobAdd::JobAddCustom => Txt { + pol: "Zdefiniuj zadanie", + eng: "Customize job", + }, + ActionJobAdd::Back => Txt { + pol: "Powrót", + eng: "Back", + }, + } + } + + // Nadpisujemy domyślny styl tylko dla jednego przycisku! + fn theme(&self, text: String) -> String { + match self { + ActionJobAdd::JobAddCustom => style(text).on_white().blue().to_string(), + _ => text, // Pozostałe opcje zwracają zwykły tekst + } + } +} + +// ===================================================================== +// WIDOK MENU +// ===================================================================== + +pub fn menu_job_add(s: &mut StateTui) { + let mut last_action = ActionJobAdd::default(); + + loop { + // 1. INICJUJEMY TŁUMACZA DLA TEJ PĘTLI + let t = T::new(s.lang); + + // 2. STYLIZUJEMY NAGŁÓWEK (pobierany z globalnych Promptów) + let header = style(t.raw(Prompt::HeaderJobAdd)) + .on_white() + .black() + .to_string(); + + // 3. BUDUJEMY MENU (czysto, zwięźle i z podpowiedziami kompilatora) + let action_result = cliclack::select(header) + .initial_value(last_action.clone()) + .item( + ActionJobAdd::JobAddDefault, + t.fmt(ActionJobAdd::JobAddDefault), + "", + ) + .item( + ActionJobAdd::JobAddCustom, + t.fmt(ActionJobAdd::JobAddCustom), + "", + ) + .item(ActionJobAdd::Back, t.fmt(ActionJobAdd::Back), "") + .interact(); + + match action_result { + Ok(action) => { + last_action = action.clone(); + + match action { + ActionJobAdd::JobAddDefault => { + s.add_job(JobConfig::default()); + + // Sukces i wyjście też przepuszczamy przez tłumacza + cliclack::log::success(t.raw(Prompt::SuccessJobAdd)).unwrap(); + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + ActionJobAdd::JobAddCustom => { + let saved = menu_job_add_custom(s); + if saved { + return; + } + } + ActionJobAdd::Back => { + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + } + } + Err(_) => { + cliclack::clear_screen().unwrap(); + let t_err = T::new(s.lang); // Tłumacz na wypadek błędu (np. Ctrl+C) + cliclack::intro(t_err.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + } + } +} diff --git a/src/interfaces/tui/menu/job_add_custom.rs b/src/interfaces/tui/menu/job_add_custom.rs new file mode 100644 index 0000000..677a40d --- /dev/null +++ b/src/interfaces/tui/menu/job_add_custom.rs @@ -0,0 +1,366 @@ +use super::super::i18n::{Prompt, T, Translatable, Txt}; +use super::super::state::{JobConfig, Lang, StateTui}; +use console::style; + +#[derive(Default, Clone, Eq, PartialEq)] +enum ActionJobAddCustom { + #[default] + JobTitle, + PathEnter, + PathIncludeParentFile, + GlobPathWhiteList, + ClearWhiteList, + GlobPathBlackList, + ClearBlackList, + FileTypes, + ClearFileTypes, + DirsIncludeEmpty, + DirsOnly, + DirsKeepExcludedAsEmptyToDepth, + Save, + Back, +} + +// ===================================================================== +// WARSTWA JĘZYKOWO - STYLIZACYJNA +// ===================================================================== + +impl Translatable for ActionJobAddCustom { + fn trans(&self) -> Txt { + match self { + ActionJobAddCustom::JobTitle => Txt { + pol: "Tytuł zadania", + eng: "Job title", + }, + ActionJobAddCustom::PathEnter => Txt { + pol: "Ścieżka wejściowa", + eng: "Enter path", + }, + ActionJobAddCustom::PathIncludeParentFile => Txt { + pol: "Dołącz plik o tej samej nazwie poziom wyżej", + eng: "Include file with same name one level up", + }, + ActionJobAddCustom::GlobPathWhiteList => Txt { + pol: "Biała lista (include)", + eng: "White list (include)", + }, + ActionJobAddCustom::ClearWhiteList => Txt { + pol: " [ Wyczyść białą listę ]", + eng: " [ Clear white list ]", + }, + ActionJobAddCustom::GlobPathBlackList => Txt { + pol: "Czarna lista (exclude)", + eng: "Black list (exclude)", + }, + ActionJobAddCustom::ClearBlackList => Txt { + pol: " [ Wyczyść czarną listę ]", + eng: " [ Clear black list ]", + }, + ActionJobAddCustom::FileTypes => Txt { + pol: "Filtry plików w podkatalogach", + eng: "Subdirectory file filters", + }, + ActionJobAddCustom::ClearFileTypes => Txt { + pol: " [ Wyczyść filtry plików ]", + eng: " [ Clear file filters ]", + }, + ActionJobAddCustom::DirsIncludeEmpty => Txt { + pol: "Uwzględnij puste ścieżki", + eng: "Include empty paths", + }, + ActionJobAddCustom::DirsOnly => Txt { + pol: "Zachowuj tylko foldery - bez plików", + eng: "Keep only directories without files", + }, + ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth => Txt { + pol: "Zachowuj wykluczone katalogi jako puste aż do określonej głębokości", + eng: "Keep excluded directories as empty up to a specified depth", + }, + ActionJobAddCustom::Save => Txt { + pol: "--- ZAPISZ ZADANIE ---", + eng: "--- SAVE JOB ---", + }, + ActionJobAddCustom::Back => Txt { + pol: "Powrót", + eng: "Back", + }, + } + } + + fn theme(&self, text: String) -> String { + match self { + ActionJobAddCustom::Save => style(text).on_green().black().bold().to_string(), + ActionJobAddCustom::ClearWhiteList + | ActionJobAddCustom::ClearBlackList + | ActionJobAddCustom::ClearFileTypes => style(text).yellow().italic().to_string(), + _ => text, + } + } +} + +// ===================================================================== +// WIDOK MENU +// ===================================================================== + +pub fn menu_job_add_custom(s: &mut StateTui) -> bool { + let mut last_action = ActionJobAddCustom::default(); + let mut current_job = JobConfig::default(); + + loop { + let t = T::new(s.lang); + let header = style(t.raw(Prompt::HeaderJobAddCustom)) + .on_white() + .black() + .to_string(); + + // 1. Zabezpieczenie UX (Ukrywanie opcji zależnych od czarnej listy) + if current_job.glob_excludes.is_empty() { + current_job.dirs_only = false; + current_job.dirs_keep_excluded_as_empty_to_depth = 0; + + if last_action == ActionJobAddCustom::DirsOnly + || last_action == ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth + { + last_action = ActionJobAddCustom::GlobPathBlackList; + } + } + + // 2. Przygotowanie etykiet + let toggle = |b: bool| if b { "[x]" } else { "[ ]" }; + let rules_txt = match s.lang { + Lang::POL => "reguł", + Lang::ENG => "rules", + }; + let format_list = |v: &Vec| { + if v.is_empty() { + "[]".to_string() + } else { + format!("[{}]", v.join(", ")) + } + }; + + let title_lbl = format!( + "{} [{}]", + t.fmt(ActionJobAddCustom::JobTitle), + current_job.title + ); + let path_lbl = format!( + "{} [{}]", + t.fmt(ActionJobAddCustom::PathEnter), + current_job.path_enter + ); + let parent_file_lbl = format!( + "{} {}", + t.fmt(ActionJobAddCustom::PathIncludeParentFile), + toggle(current_job.path_include_parent_file) + ); + + let include_lbl = format!( + "{} {}", + t.fmt(ActionJobAddCustom::GlobPathWhiteList), + format_list(¤t_job.glob_includes) + ); + let exclude_lbl = format!( + "{} {}", + t.fmt(ActionJobAddCustom::GlobPathBlackList), + format_list(¤t_job.glob_excludes) + ); + let file_types_lbl = format!( + "{} ({} {}) {}", + t.fmt(ActionJobAddCustom::FileTypes), + current_job.file_types.len(), + rules_txt, + format_list(¤t_job.file_types) + ); + + let dirs_empty_lbl = format!( + "{} {}", + t.fmt(ActionJobAddCustom::DirsIncludeEmpty), + toggle(current_job.dirs_include_empty) + ); + let dirs_only_lbl = format!( + "{} {}", + t.fmt(ActionJobAddCustom::DirsOnly), + toggle(current_job.dirs_only) + ); + + // 3. Budowanie Menu + let mut menu = cliclack::select(header) + .initial_value(last_action.clone()) + .item(ActionJobAddCustom::JobTitle, title_lbl, "") + .item(ActionJobAddCustom::PathEnter, path_lbl, "") + .item( + ActionJobAddCustom::PathIncludeParentFile, + parent_file_lbl, + "", + ) + .item(ActionJobAddCustom::GlobPathWhiteList, include_lbl, ""); + + if !current_job.glob_includes.is_empty() { + menu = menu.item( + ActionJobAddCustom::ClearWhiteList, + t.fmt(ActionJobAddCustom::ClearWhiteList), + "", + ); + } + + menu = menu.item(ActionJobAddCustom::GlobPathBlackList, exclude_lbl, ""); + + if !current_job.glob_excludes.is_empty() { + menu = menu.item( + ActionJobAddCustom::ClearBlackList, + t.fmt(ActionJobAddCustom::ClearBlackList), + "", + ); + } + + menu = menu.item(ActionJobAddCustom::FileTypes, file_types_lbl, ""); + + if !current_job.file_types.is_empty() { + menu = menu.item( + ActionJobAddCustom::ClearFileTypes, + t.fmt(ActionJobAddCustom::ClearFileTypes), + "", + ); + } + + menu = menu.item(ActionJobAddCustom::DirsIncludeEmpty, dirs_empty_lbl, ""); + + if !current_job.glob_excludes.is_empty() { + menu = menu.item(ActionJobAddCustom::DirsOnly, dirs_only_lbl, ""); + menu = menu.item( + ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth, + t.fmt(ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth), + "", + ); + } + + let action_result = menu + .item( + ActionJobAddCustom::Save, + t.fmt(ActionJobAddCustom::Save), + "", + ) + .item( + ActionJobAddCustom::Back, + t.fmt(ActionJobAddCustom::Back), + "", + ) + .interact(); + + // 4. Obsługa akcji + match action_result { + Ok(action) => { + last_action = action.clone(); + + match action { + ActionJobAddCustom::JobTitle => { + let val: String = cliclack::input(t.raw(ActionJobAddCustom::JobTitle)) + .default_input(¤t_job.title) + .interact() + .unwrap_or(current_job.title.clone()); + current_job.title = val; + } + ActionJobAddCustom::PathEnter => { + let val: String = cliclack::input(t.raw(ActionJobAddCustom::PathEnter)) + .default_input(¤t_job.path_enter) + .interact() + .unwrap_or(current_job.path_enter.clone()); + current_job.path_enter = val; + } + ActionJobAddCustom::PathIncludeParentFile => { + current_job.path_include_parent_file = + !current_job.path_include_parent_file; + } + ActionJobAddCustom::GlobPathWhiteList => { + let val: String = + cliclack::input(t.raw(ActionJobAddCustom::GlobPathWhiteList)) + .multiline() + .default_input(¤t_job.glob_includes.join("\n")) + .interact() + .unwrap_or(current_job.glob_includes.join("\n")); + current_job.glob_includes = val + .lines() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + ActionJobAddCustom::ClearWhiteList => { + current_job.glob_includes.clear(); + last_action = ActionJobAddCustom::GlobPathWhiteList; + } + ActionJobAddCustom::GlobPathBlackList => { + let val: String = + cliclack::input(t.raw(ActionJobAddCustom::GlobPathBlackList)) + .multiline() + .default_input(¤t_job.glob_excludes.join("\n")) + .interact() + .unwrap_or(current_job.glob_excludes.join("\n")); + current_job.glob_excludes = val + .lines() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + ActionJobAddCustom::ClearBlackList => { + current_job.glob_excludes.clear(); + last_action = ActionJobAddCustom::GlobPathBlackList; + } + ActionJobAddCustom::FileTypes => { + let val: String = cliclack::input(t.raw(ActionJobAddCustom::FileTypes)) + .multiline() + .default_input(¤t_job.file_types.join("\n")) + .interact() + .unwrap_or(current_job.file_types.join("\n")); + current_job.file_types = val + .lines() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + ActionJobAddCustom::ClearFileTypes => { + current_job.file_types.clear(); + last_action = ActionJobAddCustom::FileTypes; + } + ActionJobAddCustom::DirsIncludeEmpty => { + current_job.dirs_include_empty = !current_job.dirs_include_empty; + } + ActionJobAddCustom::DirsOnly => { + current_job.dirs_only = !current_job.dirs_only; + } + ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth => { + let val: String = cliclack::input(t.raw(Prompt::EnterDepth)) + .default_input( + ¤t_job.dirs_keep_excluded_as_empty_to_depth.to_string(), + ) + .interact() + .unwrap_or_else(|_| { + current_job.dirs_keep_excluded_as_empty_to_depth.to_string() + }); + if let Ok(num) = val.parse::() { + current_job.dirs_keep_excluded_as_empty_to_depth = num; + } + } + ActionJobAddCustom::Save => { + s.add_job(current_job); + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); + return true; + } + ActionJobAddCustom::Back => { + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderJobAdd)).unwrap(); + return false; + } + } + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderJobAddCustom)).unwrap(); + } + Err(_) => { + cliclack::clear_screen().unwrap(); + cliclack::intro(T::new(s.lang).raw(Prompt::HeaderJobAdd)).unwrap(); + return false; + } + } + } +} diff --git a/src/interfaces/tui/menu/jobs_manager.rs b/src/interfaces/tui/menu/jobs_manager.rs new file mode 100644 index 0000000..e98f108 --- /dev/null +++ b/src/interfaces/tui/menu/jobs_manager.rs @@ -0,0 +1,162 @@ +use super::super::i18n::{Prompt, T, Translatable, Txt}; +use super::super::state::StateTui; +use console::style; + +#[derive(Default, Clone, Eq, PartialEq)] +enum ActionJobManage { + MoveUp, + MoveDown, + View, + Delete, + #[default] + Back, +} + +// ===================================================================== +// WARSTWA JĘZYKOWO - STYLIZACYJNA +// ===================================================================== + +impl Translatable for ActionJobManage { + fn trans(&self) -> Txt { + match self { + ActionJobManage::MoveUp => Txt { + pol: "Przesuń wyżej", + eng: "Move up", + }, + ActionJobManage::MoveDown => Txt { + pol: "Przesuń niżej", + eng: "Move down", + }, + ActionJobManage::View => Txt { + pol: "Podgląd", + eng: "View", + }, + ActionJobManage::Delete => Txt { + pol: "Usuń zadanie", + eng: "Delete job", + }, + ActionJobManage::Back => Txt { + pol: "Powrót", + eng: "Back", + }, + } + } + + fn theme(&self, text: String) -> String { + match self { + ActionJobManage::Delete => style(text).on_red().white().bold().to_string(), // Ostrzegawczy czerwony! + _ => text, + } + } +} + +// ===================================================================== +// WIDOK MENU GŁÓWNEGO MENADŻERA +// ===================================================================== + +pub fn menu_jobs_manager(s: &mut StateTui) { + loop { + let t = T::new(s.lang); + + if s.jobs.is_empty() { + cliclack::log::warning(t.raw(Prompt::NoJobsWarning)).unwrap(); + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + + let header = style(t.raw(Prompt::HeaderJobsManager)) + .on_white() + .black() + .to_string(); + + // USTAWIALNY START: usize::MAX to nasz przycisk "Wstecz" + let mut menu = cliclack::select(header).initial_value(usize::MAX); + + for (index, job) in s.jobs.iter().enumerate() { + let styled_title = style(format!(" [{}] {}", index, job.title)) + .yellow() + .to_string(); + menu = menu.item(index, styled_title, ""); + } + + menu = menu.item(usize::MAX, t.fmt(ActionJobManage::Back), ""); + + let selection = menu.interact(); + + match selection { + Ok(usize::MAX) | Err(_) => { + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + Ok(job_index) => { + // Odpalamy podmenu dla wybranego zadania + manage_single_job(s, job_index); + } + } + } +} + +// ===================================================================== +// WIDOK ZARZĄDZANIA POJEDYNCZYM ZADANIEM +// ===================================================================== + +fn manage_single_job(s: &mut StateTui, index: usize) { + loop { + let t = T::new(s.lang); + + if index >= s.jobs.len() { + return; + } + + let job_title = &s.jobs[index].title; + // Sklejamy: "Zadanie: " + "Tytuł" + let prompt_title = format!("{}{}", t.raw(Prompt::JobTitlePrefix), job_title); + + let action_result = cliclack::select(prompt_title) + .item(ActionJobManage::MoveUp, t.fmt(ActionJobManage::MoveUp), "") + .item( + ActionJobManage::MoveDown, + t.fmt(ActionJobManage::MoveDown), + "", + ) + .item(ActionJobManage::View, t.fmt(ActionJobManage::View), "") + .item(ActionJobManage::Delete, t.fmt(ActionJobManage::Delete), "") + .item(ActionJobManage::Back, t.fmt(ActionJobManage::Back), "") + .interact(); + + match action_result { + Ok(ActionJobManage::MoveUp) => { + if index > 0 { + s.jobs.swap(index, index - 1); + return; + } else { + cliclack::log::warning(t.raw(Prompt::JobAlreadyAtTop)).unwrap(); + } + } + Ok(ActionJobManage::MoveDown) => { + if index < s.jobs.len() - 1 { + s.jobs.swap(index, index + 1); + return; + } else { + cliclack::log::warning(t.raw(Prompt::JobAlreadyAtBottom)).unwrap(); + } + } + Ok(ActionJobManage::View) => { + let job_info = format!("{:#?}", s.jobs[index]); + cliclack::log::info(job_info).unwrap(); + } + Ok(ActionJobManage::Delete) => { + s.jobs.remove(index); + cliclack::log::success(t.raw(Prompt::SuccessJobDeleted)).unwrap(); + return; + } + Ok(ActionJobManage::Back) | Err(_) => { + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderJobsManager)).unwrap(); + return; + } + } + } +} diff --git a/src/interfaces/tui/menu/output_save.rs b/src/interfaces/tui/menu/output_save.rs new file mode 100644 index 0000000..bfd5891 --- /dev/null +++ b/src/interfaces/tui/menu/output_save.rs @@ -0,0 +1,321 @@ +use super::super::i18n::{Prompt, T, Translatable, Txt}; +use super::super::state::{Lang, StateTui}; +use console::style; + +#[derive(Default, Clone, Eq, PartialEq)] +enum ActionOutput { + #[default] + ExecuteSave, + SaveFileF, + SaveFileD, + SaveFileC, + AddStructToDoc, + AddCmdToStruct, + AddCmdToDoc, + AutoSectionNum, + TimestampInFile, + TimestampInFilename, + SelfPromo, + OutputFolder, + SectionPrefix, + DocTitle, + Back, +} + +//impl Default for ActionOutput { +// fn default() -> Self { Self::ExecuteSave } +//} + +// ===================================================================== +// WARSTWA JĘZYKOWO - STYLIZACYJNA +// ===================================================================== + +impl Translatable for ActionOutput { + fn trans(&self) -> Txt { + match self { + ActionOutput::ExecuteSave => Txt { + pol: "--- WYKONAJ ZAPIS (START) ---", + eng: "--- EXECUTE SAVE (START) ---", + }, + ActionOutput::SaveFileF => Txt { + pol: "Zapisz plik *f* (struktura)", + eng: "Save file *f* (structure)", + }, + ActionOutput::SaveFileD => Txt { + pol: "Zapisz plik *d* (dokumentacja)", + eng: "Save file *d* (documentation)", + }, + ActionOutput::SaveFileC => Txt { + pol: "Zapisz plik *c* (komenda)", + eng: "Save file *c* (command)", + }, + ActionOutput::AddStructToDoc => Txt { + pol: "Dodaj strukturę do dokumentacji", + eng: "Add structure to doc", + }, + ActionOutput::AddCmdToStruct => Txt { + pol: "Dodaj komendę do struktury", + eng: "Add command to structure", + }, + ActionOutput::AddCmdToDoc => Txt { + pol: "Dodaj komendę do dokumentacji", + eng: "Add command to doc", + }, + ActionOutput::AutoSectionNum => Txt { + pol: "Autonumeracja sekcji", + eng: "Auto section numbering", + }, + ActionOutput::TimestampInFile => Txt { + pol: "Znacznik czasu w każdym pliku", + eng: "Timestamp in every file", + }, + ActionOutput::TimestampInFilename => Txt { + pol: "Znacznik czasu jako prefix nazwy", + eng: "Timestamp as filename prefix", + }, + ActionOutput::SelfPromo => Txt { + pol: "Autoreklama w każdym pliku", + eng: "Self-promo in every file", + }, + ActionOutput::OutputFolder => Txt { + pol: "Folder na pliki", + eng: "Output folder", + }, + ActionOutput::SectionPrefix => Txt { + pol: "Prefix sekcji pliku", + eng: "Section prefix", + }, + ActionOutput::DocTitle => Txt { + pol: "Tytuł dokumentu", + eng: "Document title", + }, + ActionOutput::Back => Txt { + pol: "Powrót", + eng: "Back", + }, + } + } + + fn theme(&self, text: String) -> String { + match self { + // Wyróżniamy główny przycisk akcji! + ActionOutput::ExecuteSave => style(text).on_green().black().bold().to_string(), + _ => text, + } + } +} + +// ===================================================================== +// WIDOK MENU +// ===================================================================== + +pub fn menu_output_save(s: &mut StateTui) { + let mut last_action = ActionOutput::default(); + + loop { + let t = T::new(s.lang); + let oc = &s.output_config; // Skrót dla wygody czytania + + let header = style(t.raw(Prompt::HeaderOutput)) + .on_white() + .black() + .to_string(); + + // Formaty przełączników [x] / [ ] + let toggle = |b: bool| if b { "[x]" } else { "[ ]" }; + + let f_lbl = format!( + "{} {}", + t.fmt(ActionOutput::SaveFileF), + toggle(oc.save_file_f) + ); + let d_lbl = format!( + "{} {}", + t.fmt(ActionOutput::SaveFileD), + toggle(oc.save_file_d) + ); + let c_lbl = format!( + "{} {}", + t.fmt(ActionOutput::SaveFileC), + toggle(oc.save_file_c) + ); + + let s2d_lbl = format!( + "{} {}", + t.fmt(ActionOutput::AddStructToDoc), + toggle(oc.add_struct_to_doc) + ); + let c2s_lbl = format!( + "{} {}", + t.fmt(ActionOutput::AddCmdToStruct), + toggle(oc.add_cmd_to_struct) + ); + let c2d_lbl = format!( + "{} {}", + t.fmt(ActionOutput::AddCmdToDoc), + toggle(oc.add_cmd_to_doc) + ); + + let auto_num_lbl = format!( + "{} {}", + t.fmt(ActionOutput::AutoSectionNum), + toggle(oc.auto_section_num) + ); + let ts_file_lbl = format!( + "{} {}", + t.fmt(ActionOutput::TimestampInFile), + toggle(oc.timestamp_in_file) + ); + let ts_name_lbl = format!( + "{} {}", + t.fmt(ActionOutput::TimestampInFilename), + toggle(oc.timestamp_in_filename) + ); + let promo_lbl = format!( + "{} {}", + t.fmt(ActionOutput::SelfPromo), + toggle(oc.self_promo) + ); + + let folder_lbl = format!( + "{} [{}]", + t.fmt(ActionOutput::OutputFolder), + oc.output_folder + ); + let prefix_lbl = format!( + "{} [{}]", + t.fmt(ActionOutput::SectionPrefix), + oc.section_prefix + ); + let title_lbl = format!("{} [{}]", t.fmt(ActionOutput::DocTitle), oc.doc_title); + + // Tłumaczenia hintów (podpowiedzi) + let hint_f = match s.lang { + Lang::POL => "Zapisuje drzewo katalogów", + Lang::ENG => "Saves directory tree", + }; + let hint_d = match s.lang { + Lang::POL => "Zapisuje zawartość plików", + Lang::ENG => "Saves file contents", + }; + let hint_c = match s.lang { + Lang::POL => "Zapisuje komendę terminala", + Lang::ENG => "Saves terminal command", + }; + + let action_result = cliclack::select(header) + .initial_value(last_action.clone()) + .item( + ActionOutput::ExecuteSave, + t.fmt(ActionOutput::ExecuteSave), + "", + ) + .item(ActionOutput::SaveFileF, f_lbl, hint_f) + .item(ActionOutput::SaveFileD, d_lbl, hint_d) + .item(ActionOutput::SaveFileC, c_lbl, hint_c) + .item(ActionOutput::AddStructToDoc, s2d_lbl, "") + .item(ActionOutput::AddCmdToStruct, c2s_lbl, "") + .item(ActionOutput::AddCmdToDoc, c2d_lbl, "") + .item(ActionOutput::AutoSectionNum, auto_num_lbl, "") + .item(ActionOutput::TimestampInFile, ts_file_lbl, "") + .item(ActionOutput::TimestampInFilename, ts_name_lbl, "") + .item(ActionOutput::SelfPromo, promo_lbl, "") + .item(ActionOutput::OutputFolder, folder_lbl, "") + .item(ActionOutput::SectionPrefix, prefix_lbl, "") + .item(ActionOutput::DocTitle, title_lbl, "") + .item(ActionOutput::Back, t.fmt(ActionOutput::Back), "") + .interact(); + + match action_result { + Ok(action) => { + last_action = action.clone(); + + match action { + // Natychmiastowe przełączniki (negacja obecnej wartości) + ActionOutput::SaveFileF => { + s.output_config.save_file_f = !s.output_config.save_file_f + } + ActionOutput::SaveFileD => { + s.output_config.save_file_d = !s.output_config.save_file_d + } + ActionOutput::SaveFileC => { + s.output_config.save_file_c = !s.output_config.save_file_c + } + ActionOutput::AddStructToDoc => { + s.output_config.add_struct_to_doc = !s.output_config.add_struct_to_doc + } + ActionOutput::AddCmdToStruct => { + s.output_config.add_cmd_to_struct = !s.output_config.add_cmd_to_struct + } + ActionOutput::AddCmdToDoc => { + s.output_config.add_cmd_to_doc = !s.output_config.add_cmd_to_doc + } + ActionOutput::AutoSectionNum => { + s.output_config.auto_section_num = !s.output_config.auto_section_num + } + ActionOutput::TimestampInFile => { + s.output_config.timestamp_in_file = !s.output_config.timestamp_in_file + } + ActionOutput::TimestampInFilename => { + s.output_config.timestamp_in_filename = + !s.output_config.timestamp_in_filename + } + ActionOutput::SelfPromo => { + s.output_config.self_promo = !s.output_config.self_promo + } + + // Pola tekstowe + ActionOutput::OutputFolder => { + let val: String = cliclack::input(t.raw(ActionOutput::OutputFolder)) + .default_input(&s.output_config.output_folder) + .interact() + .unwrap_or(s.output_config.output_folder.clone()); + s.output_config.output_folder = val; + } + ActionOutput::SectionPrefix => { + let val: String = cliclack::input(t.raw(ActionOutput::SectionPrefix)) + .default_input(&s.output_config.section_prefix) + .interact() + .unwrap_or(s.output_config.section_prefix.clone()); + s.output_config.section_prefix = val; + } + ActionOutput::DocTitle => { + let val: String = cliclack::input(t.raw(ActionOutput::DocTitle)) + .default_input(&s.output_config.doc_title) + .interact() + .unwrap_or(s.output_config.doc_title.clone()); + s.output_config.doc_title = val; + } + + ActionOutput::ExecuteSave => { + // TUTAJ W PRZYSZŁOŚCI WYWOŁAMY WŁAŚCIWY ZAPIS PLIKÓW + let msg = match s.lang { + Lang::POL => format!( + "Zapisano pliki do folderu: {}", + s.output_config.output_folder + ), + Lang::ENG => { + format!("Saved files to folder: {}", s.output_config.output_folder) + } + }; + cliclack::log::success(msg).unwrap(); + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + ActionOutput::Back => { + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + } + } + Err(_) => { + cliclack::clear_screen().unwrap(); + let t_err = T::new(s.lang); + cliclack::intro(t_err.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + } + } +} diff --git a/src/interfaces/tui/menu/paths_struct_style.rs b/src/interfaces/tui/menu/paths_struct_style.rs new file mode 100644 index 0000000..c1e37c3 --- /dev/null +++ b/src/interfaces/tui/menu/paths_struct_style.rs @@ -0,0 +1,301 @@ +use super::super::i18n::{Prompt, T, Translatable, Txt}; +use super::super::state::{Lang, SizeBase, SortOrder, StateTui}; +use console::style; + +#[derive(Default, Clone, Eq, PartialEq)] +enum ActionStyle { + SortOrder, + SizeFiles, + SizeDirs, + SizeDirsReal, + SizeBase, + Precision, + #[default] + Back, +} + +//impl Default for ActionStyle { +// fn default() -> Self { Self::SortOrder } +//} + +// ===================================================================== +// LOKALNE ENUMY POMOCNICZE +// ===================================================================== + +#[derive(Clone, Eq, PartialEq)] +enum ActionSort { + FilesFirst, + DirsFirst, + Alphanumeric, +} + +enum LocalPrompt { + SortMode, + PrecisionInput, + ErrPrecisionRange, + ErrPrecisionNotNum, +} + +// ===================================================================== +// WARSTWA JĘZYKOWO - STYLIZACYJNA +// ===================================================================== + +impl Translatable for ActionStyle { + fn trans(&self) -> Txt { + match self { + ActionStyle::SortOrder => Txt { + pol: "Sortowanie", + eng: "Sort order", + }, + ActionStyle::SizeFiles => Txt { + pol: "Rozmiar przy plikach", + eng: "Size for files", + }, + ActionStyle::SizeDirs => Txt { + pol: "Rozmiar przy folderach", + eng: "Size for directories", + }, + ActionStyle::SizeDirsReal => Txt { + pol: "Rzeczywisty rozmiar folderów", + eng: "Real directory size", + }, + ActionStyle::SizeBase => Txt { + pol: "Podstawa rozmiaru", + eng: "Size base", + }, + ActionStyle::Precision => Txt { + pol: "Precyzja", + eng: "Precision", + }, + ActionStyle::Back => Txt { + pol: "Powrót", + eng: "Back", + }, + } + } +} + +impl Translatable for ActionSort { + fn trans(&self) -> Txt { + match self { + ActionSort::FilesFirst => Txt { + pol: "Najpierw pliki", + eng: "Files first", + }, + ActionSort::DirsFirst => Txt { + pol: "Najpierw foldery", + eng: "Directories first", + }, + ActionSort::Alphanumeric => Txt { + pol: "Alfanumerycznie", + eng: "Alphanumeric", + }, + } + } +} + +impl Translatable for LocalPrompt { + fn trans(&self) -> Txt { + match self { + LocalPrompt::SortMode => Txt { + pol: "Wybierz tryb sortowania:", + eng: "Choose sort mode:", + }, + LocalPrompt::PrecisionInput => Txt { + pol: "Podaj precyzję (od 3 do 9):", + eng: "Enter precision (from 3 to 9):", + }, + LocalPrompt::ErrPrecisionRange => Txt { + pol: "Precyzja musi być w przedziale 3-9!", + eng: "Precision must be between 3 and 9!", + }, + LocalPrompt::ErrPrecisionNotNum => Txt { + pol: "To nie jest liczba!", + eng: "This is not a number!", + }, + } + } +} + +// ===================================================================== +// WIDOK MENU +// ===================================================================== + +pub fn menu_paths_struct_style(s: &mut StateTui) { + // Ta linia teraz automatycznie wybierze Back jako start: + let mut last_action = ActionStyle::default(); + + loop { + let t = T::new(s.lang); + let header = style(t.raw(Prompt::HeaderStyle)) + .on_white() + .black() + .to_string(); + + // 1. ZABEZPIECZENIE UX PRZED BUDOWĄ MENU + // Jeśli rozmiar folderów wyłączony, ukrywamy opcję rzeczywistego rozmiaru + if !s.struct_config.size_dirs { + s.struct_config.size_dirs_real = false; + if last_action == ActionStyle::SizeDirsReal { + last_action = ActionStyle::SizeDirs; + } + } + + // Jeśli rozmiar plików I folderów wyłączony, ukrywamy też bazę i precyzję + if !s.struct_config.size_files && !s.struct_config.size_dirs { + s.struct_config.size_base = SizeBase::Base1024; // Wartość domyślna + s.struct_config.precision = 3; // Wartość domyślna + + if last_action == ActionStyle::SizeBase || last_action == ActionStyle::Precision { + // Cofamy kursor na cokolwiek, co jest jeszcze widoczne nad nimi + last_action = ActionStyle::SizeDirs; + } + } + + // 2. DYNAMICZNE ETYKIETY + let sort_str = match s.struct_config.sort { + SortOrder::FilesFirst => match s.lang { + Lang::POL => "najpierw pliki", + Lang::ENG => "files first", + }, + SortOrder::DirsFirst => match s.lang { + Lang::POL => "najpierw foldery", + Lang::ENG => "directories first", + }, + SortOrder::Alphanumeric => match s.lang { + Lang::POL => "alfanumerycznie", + Lang::ENG => "alphanumeric", + }, + }; + + let base_str = match s.struct_config.size_base { + SizeBase::Base1024 => "1024", + SizeBase::Base1000 => "1000", + }; + + let toggle = |b: bool| if b { "[x]" } else { "[ ]" }; + + let sort_lbl = format!("{} [{}]", t.fmt(ActionStyle::SortOrder), sort_str); + let s_files_lbl = format!( + "{} {}", + t.fmt(ActionStyle::SizeFiles), + toggle(s.struct_config.size_files) + ); + let s_dirs_lbl = format!( + "{} {}", + t.fmt(ActionStyle::SizeDirs), + toggle(s.struct_config.size_dirs) + ); + let s_dirs_real_lbl = format!( + "{} {}", + t.fmt(ActionStyle::SizeDirsReal), + toggle(s.struct_config.size_dirs_real) + ); + let base_lbl = format!("{} [{}]", t.fmt(ActionStyle::SizeBase), base_str); + let prec_lbl = format!( + "{} [{}]", + t.fmt(ActionStyle::Precision), + s.struct_config.precision + ); + + // 3. BUDOWANIE MENU + let mut menu = cliclack::select(header) + .initial_value(last_action.clone()) + .item(ActionStyle::SortOrder, sort_lbl, "") + .item(ActionStyle::SizeFiles, s_files_lbl, "") + .item(ActionStyle::SizeDirs, s_dirs_lbl, ""); + + // Warunkowe opcje dla folderów + if s.struct_config.size_dirs { + menu = menu.item(ActionStyle::SizeDirsReal, s_dirs_real_lbl, ""); + } + + // Warunkowe opcje globalne dla rozmiarów + if s.struct_config.size_files || s.struct_config.size_dirs { + menu = menu.item(ActionStyle::SizeBase, base_lbl, ""); + menu = menu.item(ActionStyle::Precision, prec_lbl, ""); + } + + let action_result = menu + .item(ActionStyle::Back, t.fmt(ActionStyle::Back), "") + .interact(); + + // 4. OBSŁUGA AKCJI + match action_result { + Ok(action) => { + last_action = action.clone(); + + match action { + ActionStyle::SortOrder => { + let initial_sort_action = match s.struct_config.sort { + SortOrder::FilesFirst => ActionSort::FilesFirst, + SortOrder::DirsFirst => ActionSort::DirsFirst, + SortOrder::Alphanumeric => ActionSort::Alphanumeric, + }; + + let val_action = cliclack::select(t.raw(LocalPrompt::SortMode)) + .initial_value(initial_sort_action) + .item(ActionSort::FilesFirst, t.fmt(ActionSort::FilesFirst), "") + .item(ActionSort::DirsFirst, t.fmt(ActionSort::DirsFirst), "") + .item( + ActionSort::Alphanumeric, + t.fmt(ActionSort::Alphanumeric), + "", + ) + .interact(); + + if let Ok(selected_sort) = val_action { + s.struct_config.sort = match selected_sort { + ActionSort::FilesFirst => SortOrder::FilesFirst, + ActionSort::DirsFirst => SortOrder::DirsFirst, + ActionSort::Alphanumeric => SortOrder::Alphanumeric, + }; + } + } + ActionStyle::SizeFiles => { + s.struct_config.size_files = !s.struct_config.size_files + } + ActionStyle::SizeDirs => s.struct_config.size_dirs = !s.struct_config.size_dirs, + ActionStyle::SizeDirsReal => { + s.struct_config.size_dirs_real = !s.struct_config.size_dirs_real + } + ActionStyle::SizeBase => { + s.struct_config.size_base = match s.struct_config.size_base { + SizeBase::Base1024 => SizeBase::Base1000, + SizeBase::Base1000 => SizeBase::Base1024, + }; + } + ActionStyle::Precision => { + let default_val = s.struct_config.precision.to_string(); + let val: String = cliclack::input(t.raw(LocalPrompt::PrecisionInput)) + .default_input(&default_val) + .interact() + .unwrap_or(default_val); + + if let Ok(num) = val.parse::() { + if (3..=9).contains(&num) { + s.struct_config.precision = num; + } else { + cliclack::log::error(t.raw(LocalPrompt::ErrPrecisionRange)) + .unwrap(); + } + } else { + cliclack::log::error(t.raw(LocalPrompt::ErrPrecisionNotNum)).unwrap(); + } + } + ActionStyle::Back => { + cliclack::clear_screen().unwrap(); + cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + } + } + Err(_) => { + cliclack::clear_screen().unwrap(); + let t_err = T::new(s.lang); + cliclack::intro(t_err.raw(Prompt::HeaderEnter)).unwrap(); + return; + } + } + } +} diff --git a/src/interfaces/tui/state.rs b/src/interfaces/tui/state.rs new file mode 100644 index 0000000..7030109 --- /dev/null +++ b/src/interfaces/tui/state.rs @@ -0,0 +1,142 @@ +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Lang { + POL, + ENG, +} + +#[derive(Clone, Debug)] +pub struct JobConfig { + pub title: String, + pub path_enter: String, + pub glob_includes: Vec, + pub glob_excludes: Vec, + pub file_types: Vec, + pub dirs_include_empty: bool, + pub dirs_only: bool, + pub dirs_keep_excluded_as_empty_to_depth: u32, + pub path_include_parent_file: bool, +} + +impl Default for JobConfig { + fn default() -> Self { + Self { + title: "default".to_string(), + path_enter: "./src/".to_string(), + glob_includes: vec!["./Cargo.toml".to_string()], + glob_excludes: vec![], + file_types: vec!["*.rs".to_string()], + dirs_include_empty: true, + dirs_only: false, + dirs_keep_excluded_as_empty_to_depth: 0, + path_include_parent_file: false, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SortOrder { + FilesFirst, + DirsFirst, + Alphanumeric, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SizeBase { + Base1024, + Base1000, +} + +#[derive(Clone, Debug)] +pub struct PathsStructStyleConfig { + pub sort: SortOrder, + pub size_files: bool, + pub size_dirs: bool, + pub size_dirs_real: bool, + pub size_base: SizeBase, + pub precision: u8, +} + +impl Default for PathsStructStyleConfig { + fn default() -> Self { + Self { + sort: SortOrder::FilesFirst, + size_files: true, + size_dirs: false, + size_dirs_real: false, + size_base: SizeBase::Base1024, + precision: 5, + } + } +} + +#[derive(Clone, Debug)] +pub struct OutputSaveConfig { + pub save_file_f: bool, // Struktura (f) + pub save_file_d: bool, // Dokumentacja (d) + pub save_file_c: bool, // Komenda (c) + pub add_struct_to_doc: bool, + pub add_cmd_to_struct: bool, + pub add_cmd_to_doc: bool, + pub auto_section_num: bool, + pub timestamp_in_file: bool, + pub timestamp_in_filename: bool, + pub self_promo: bool, + pub output_folder: String, + pub section_prefix: String, + pub doc_title: String, +} + +impl Default for OutputSaveConfig { + fn default() -> Self { + Self { + save_file_f: true, + save_file_d: false, + save_file_c: false, + add_struct_to_doc: true, + add_cmd_to_struct: true, + add_cmd_to_doc: true, + auto_section_num: true, + timestamp_in_file: true, + timestamp_in_filename: false, + self_promo: false, + output_folder: "./other/".to_string(), + section_prefix: "File-".to_string(), + doc_title: "".to_string(), + } + } +} + +pub struct StateTui { + pub lang: Lang, + pub jobs: Vec, + pub struct_config: PathsStructStyleConfig, + pub output_config: OutputSaveConfig, +} + +impl StateTui { + pub fn new() -> Self { + Self { + lang: Lang::POL, + jobs: Vec::new(), + struct_config: PathsStructStyleConfig::default(), + output_config: OutputSaveConfig::default(), + } + } + + // Teraz add_job przyjmuje CAŁĄ konfigurację, a nie tylko stringa + pub fn add_job(&mut self, mut job: JobConfig) { + let base_title = job.title.clone(); + let mut title = base_title.clone(); + let mut counter = 1; + + // Magia sufiksów (_1, _2...) + while self.jobs.iter().any(|j| j.title == title) { + title = format!("{}_{}", base_title, counter); + counter += 1; + } + + job.title = title; + self.jobs.push(job); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..74c4d29 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,16 @@ +use std::env; + +mod interfaces; + +fn main() { + // Rejestrujemy pusty handler Ctrl+C. + // Dzięki temu system nie zabije programu natychmiast, a `cliclack` + // przejmie sygnał i bezpiecznie wyjdzie z prompta. + ctrlc::set_handler(move || {}).expect("Błąd podczas ustawiania handlera Ctrl+C"); + + // [EN]: Start TUI if no arguments are provided. + if env::args().len() <= 2 { + interfaces::tui::run_tui(); + // return; + } +} From 66e26f7d0cdd6a400e1676ba0dbe1825d25b1694 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 12:45:41 +0100 Subject: [PATCH 03/45] (new: lib) --- src/core.rs | 2 + src/core/path_getter.rs | 46 +++++++++ src/core/path_matcher.rs | 207 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 4 files changed, 256 insertions(+) create mode 100644 src/core.rs create mode 100644 src/core/path_getter.rs create mode 100644 src/core/path_matcher.rs create mode 100644 src/lib.rs diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..1b4cea2 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,2 @@ +pub mod path_matcher; +pub mod path_getter; \ No newline at end of file diff --git a/src/core/path_getter.rs b/src/core/path_getter.rs new file mode 100644 index 0000000..0a5180e --- /dev/null +++ b/src/core/path_getter.rs @@ -0,0 +1,46 @@ +use std::path::Path; +use walkdir::WalkDir; + +/// Skanuje podany katalog i zwraca listę ścieżek znormalizowanych do formatu: +/// - zaczynają się od "./" +/// - mają ukośniki "/" +/// - foldery kończą się na "/" +/// - ignoruje symlinki/junctions (!ReparsePoint) +pub fn get_paths>(dir_path: P) -> Vec { + let mut result = Vec::new(); + let root_path = dir_path.as_ref(); + + // WalkDir::new domyślnie iteruje rekurencyjnie (odpowiednik -Recurse) + // filter_map(|e| e.ok()) bezpiecznie ignoruje błędy braku uprawnień (Access Denied) + for entry in WalkDir::new(root_path).into_iter().filter_map(|e| e.ok()) { + + // Pomijamy sam katalog główny (depth == 0), interesuje nas tylko zawartość + if entry.depth() == 0 { + continue; + } + + // Pomijamy symlinki i punkty reparse (odpowiednik !ReparsePoint) + if entry.path_is_symlink() { + continue; + } + + // Ucinamy bezwzględną część ścieżki (zostaje nam np. "src\main.rs") + if let Ok(rel_path) = entry.path().strip_prefix(root_path) { + + // Konwersja ścieżki i ujednolicenie ukośników (Windows '\' -> '/') + let relative_str = rel_path.to_string_lossy().replace('\\', "/"); + + // Doklejamy wymagany prefix "./" + let mut final_path = format!("./{}", relative_str); + + // Jeśli to folder (odpowiednik PSIsContainer), dodajemy "/" na końcu + if entry.file_type().is_dir() { + final_path.push('/'); + } + + result.push(final_path); + } + } + + result +} \ No newline at end of file diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs new file mode 100644 index 0000000..6bc8be1 --- /dev/null +++ b/src/core/path_matcher.rs @@ -0,0 +1,207 @@ +use regex::Regex; + +pub struct PathMatcher { + regex: Regex, + targets_file: bool, +} + +impl PathMatcher { + pub fn new(pattern: &str, case_sensitive: bool) -> Result { + let mut re = String::new(); + + // 🔥 MAGIA: Wstrzykujemy flagę niewrażliwości na wielkość liter + if !case_sensitive { + re.push_str("(?i)"); + } + + let mut is_anchored = false; + let mut p = pattern; + + // BARIERA LOGICZNA: Jeśli wzorzec nie kończy się na ukośnik ani na '**', + // to według Twojej tabeli celuje WYŁĄCZNIE w pliki. + let targets_file = !pattern.ends_with('/') && !pattern.ends_with("**"); + + // 1. ZASADY KOTWICZENIA + if p.starts_with("./") { + is_anchored = true; + p = &p[2..]; // Ucinamy ./ + } else if p.starts_with("**/") { + is_anchored = true; + } + + if is_anchored { + re.push('^'); + } else { + re.push_str("(?:^|/)"); + } + + // 2. PARSOWANIE ZNAKÓW + 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("\\."), + '/' => re.push('/'), + '*' => { + if i + 1 < chars.len() && chars[i + 1] == '*' { + if i + 2 < chars.len() && chars[i + 2] == '/' { + // Wzorzec: **/ + re.push_str("(?:[^/]+/)*"); + i += 2; + } else { + // Wzorzec: ** + // POPRAWKA: Zamiana z .* na .+ (wymaga minimum 1 znaku zawartości!) + re.push_str(".+"); + i += 1; + } + } else { + // Zwykła gwiazdka * + 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(|s| regex::escape(s)).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; + } + + re.push('$'); + + Ok(Self { + regex: Regex::new(&re)?, + targets_file, + }) + } + + pub fn is_match(&self, path: &str) -> bool { + // TWARDA ZASADA: Jeśli wzorzec to plik, to natychmiastowo + // odrzucamy każdą ścieżkę testową, która kończy się na ukośnik! + if self.targets_file && path.ends_with('/') { + return false; + } + + let clean_path = path.strip_prefix("./").unwrap_or(path); + self.regex.is_match(clean_path) + } + + /// Ewaluuje kolekcję ścieżek, wywołując odpowiedni callback dla dopasowania i braku dopasowania. + pub fn evaluate( + &self, + paths: I, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, + ) where + I: IntoIterator, + S: AsRef, + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), + { + for path in paths { + let path_ref = path.as_ref(); + if self.is_match(path_ref) { + on_match(path_ref); + } else { + on_mismatch(path_ref); + } + } + } +} + +pub struct PathMatchers { + matchers: Vec, +} + +impl PathMatchers { + /// Przyjmuje kolekcję wzorców i kompiluje je wszystkie + pub fn new(patterns: I, case_sensitive: bool) -> Result + where + I: IntoIterator, + S: AsRef, + { + let mut matchers = Vec::new(); + for pat in patterns { + // Używamy "świętej" funkcji do skompilowania każdego z nich + matchers.push(PathMatcher::new(pat.as_ref(), case_sensitive)?); + } + Ok(Self { matchers }) + } + + /// Zwraca true, jeśli ścieżka pasuje do JAKIEGOKOLWIEK wzorca (logiczne OR) + pub fn is_match(&self, path: &str) -> bool { + for matcher in &self.matchers { + if matcher.is_match(path) { + return true; + } + } + false + } + + /// Ewaluuje kolekcję ścieżek względem wszystkich wzorców (logiczne OR), wywołując callbacki. + pub fn evaluate( + &self, + paths: I, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, + ) where + I: IntoIterator, + S: AsRef, + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), + { + for path in paths { + let path_ref = path.as_ref(); + if self.is_match(path_ref) { + on_match(path_ref); + } else { + on_mismatch(path_ref); + } + } + } +} + + +/// Zwraca odpowiednią ikonę (emoji) dla podanej ścieżki, +/// rozpoznając foldery (końcówka '/') oraz elementy ukryte (kropka na początku nazwy). +pub fn get_icon_for_path(path: &str) -> &'static str { + let is_dir = path.ends_with('/'); + + // Wyciągamy samą nazwę pliku/folderu: + // 1. Usuwamy ew. ukośnik z końca (żeby folder nie zwrócił pustego stringa) + // 2. Dzielimy przez ukośniki i bierzemy ostatni element + let nazwa = path.trim_end_matches('/').split('/').last().unwrap_or(""); + let is_hidden = nazwa.starts_with('.'); + + // Dobieramy odpowiednią ikonę na podstawie dwóch cech + match (is_dir, is_hidden) { + (true, false) => "📁", // Zwykły folder + (true, true) => "🗃️", // Ukryty folder (z kropką) + (false, false)=> "📄", // Zwykły plik + (false, true) => "⚙️ ", // Ukryty plik (konfiguracyjny z kropką) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5a7ca06 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod core; From fc51cacdd5e48f7d9ea629f8b6549b93531fd6d2 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 12:47:46 +0100 Subject: [PATCH 04/45] (new: example) --- examples/skaner.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 examples/skaner.rs diff --git a/examples/skaner.rs b/examples/skaner.rs new file mode 100644 index 0000000..c10dec4 --- /dev/null +++ b/examples/skaner.rs @@ -0,0 +1,67 @@ +use cargo_plot::core::path_matcher::{PathMatcher, PathMatchers, get_icon_for_path}; +use cargo_plot::core::path_getter::get_paths; + +use std::env; +use std::process; + + +/// Wyciąga flagi `-x` z argumentów wywołania. +/// Jeśli nie znajdzie żadnych wzorców, wypisuje instrukcję i kończy program. +fn get_patterns_from_cli() -> Vec { + let args: Vec = env::args().collect(); + let mut patterns = Vec::new(); + + let mut i = 1; + while i < args.len() { + if args[i] == "-x" && i + 1 < args.len() { + patterns.push(args[i + 1].clone()); + i += 2; + } else { + i += 1; + } + } + + if patterns.is_empty() { + eprintln!("⚠️ Nie podano żadnych wzorców!"); + eprintln!("💡 Użycie: cargo run --example skaner -- -x \"src/\" -x \"src/**\""); + process::exit(1); // Brutalne przerwanie programu (z kodem błędu 1) + } + + patterns +} + +fn main() { + // 1. ODCZYT ARGUMENTÓW Z KONSOLI + let patterns = get_patterns_from_cli(); + println!("🔍 Skanuję używając wzorców: {:?}", patterns); + + let is_case_sensitive = false; + let matchers = PathMatchers::new(&patterns, is_case_sensitive).expect("Błąd kompilacji wzorców"); + // let paths_to_test: Vec<&str> = include!("data.rs"); + let paths_to_test = get_paths("./src"); + // let wycinek = &paths_to_test[..std::cmp::min(25, paths_to_test.len())]; + // println!("{:#?}", wycinek); + // for path in paths_to_test.iter().take(25) { + // println!("{}", path); + // } + let mut dopasowane = 0; + let total = paths_to_test.len(); + + + // Ewaluacja + matchers.evaluate( + paths_to_test, + |path| { + // 🔥 Używamy naszej nowej, czystej funkcji! + let icon = get_icon_for_path(path); + println!("✅ MATCH: {} {}", icon, path); + dopasowane += 1; + }, + |_path| { + // Miejsce na logikę dla odrzuconych ścieżek + } + ); + + println!("----------"); + println!("📊 Podsumowanie: Dopasowano {} z {} ścieżek.", dopasowane, total); +} \ No newline at end of file From 46b8e33db44abf1fae51c8f82fb64051985617c2 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 13:19:55 +0100 Subject: [PATCH 05/45] (new: added context to the matcher function) --- examples/skaner.rs | 12 ++++++--- src/core/path_matcher.rs | 54 ++++++++++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/examples/skaner.rs b/examples/skaner.rs index c10dec4..5d697a7 100644 --- a/examples/skaner.rs +++ b/examples/skaner.rs @@ -1,6 +1,6 @@ -use cargo_plot::core::path_matcher::{PathMatcher, PathMatchers, get_icon_for_path}; +use cargo_plot::core::path_matcher::{PathMatchers, get_icon_for_path}; use cargo_plot::core::path_getter::get_paths; - +use std::collections::HashSet; use std::env; use std::process; @@ -44,13 +44,19 @@ fn main() { // for path in paths_to_test.iter().take(25) { // println!("{}", path); // } + + // 🔴 NOWOŚĆ: Budujemy mapę środowiska z naszej listy ścieżek. + // Używamy .copied(), bo elements w Vec<&str> to &str, a chcemy mieć HashSet<&str> + let environment: HashSet<&str> = paths_to_test.iter().map(|s| s.as_str()).collect(); + let mut dopasowane = 0; let total = paths_to_test.len(); // Ewaluacja matchers.evaluate( - paths_to_test, + &paths_to_test, + &environment, // 🔴 NOWOŚĆ: Wstrzykujemy środowisko do silnika! |path| { // 🔥 Używamy naszej nowej, czystej funkcji! let icon = get_icon_for_path(path); diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index 6bc8be1..2fbc501 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -1,21 +1,28 @@ use regex::Regex; +use std::collections::HashSet; pub struct PathMatcher { regex: Regex, targets_file: bool, + requires_sibling: bool, // ⚡ : Flaga świadomości kontekstu } impl PathMatcher { pub fn new(pattern: &str, case_sensitive: bool) -> Result { + // ⚡: Wykrywamy '@' i od razu usuwamy go ze wzorca! + let requires_sibling = pattern.contains('@'); + let clean_pattern_str = pattern.replace('@', ""); + let mut re = String::new(); - // 🔥 MAGIA: Wstrzykujemy flagę niewrażliwości na wielkość liter + // 🔥: Wstrzykujemy flagę niewrażliwości na wielkość liter if !case_sensitive { re.push_str("(?i)"); } let mut is_anchored = false; - let mut p = pattern; + // Używamy wyczyszczonego wzorca (bez '@'), żeby nie psuć parsera z tabeli! + let mut p = clean_pattern_str.as_str(); // BARIERA LOGICZNA: Jeśli wzorzec nie kończy się na ukośnik ani na '**', // to według Twojej tabeli celuje WYŁĄCZNIE w pliki. @@ -96,10 +103,12 @@ impl PathMatcher { Ok(Self { regex: Regex::new(&re)?, targets_file, + requires_sibling, }) } - pub fn is_match(&self, path: &str) -> bool { + // ⚡: is_match przyjmuje środowisko do sprawdzania rodzeństwa + pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { // TWARDA ZASADA: Jeśli wzorzec to plik, to natychmiastowo // odrzucamy każdą ścieżkę testową, która kończy się na ukośnik! if self.targets_file && path.ends_with('/') { @@ -107,13 +116,40 @@ impl PathMatcher { } let clean_path = path.strip_prefix("./").unwrap_or(path); - self.regex.is_match(clean_path) + + // Jeśli Regex odrzuca ścieżkę, nie ma o czym gadać + if !self.regex.is_match(clean_path) { + return false; + } + + // ⚡: Regex powiedział "TAK", ale wzorzec miał "@", więc sprawdzamy brata! + if self.requires_sibling && !path.ends_with('/') { + // Z "./interfaces/tui.rs" robimy folder nadrzędny i nazwę pliku + let mut components: Vec<&str> = path.split('/').collect(); + if let Some(file_name) = components.pop() { + let parent_dir = components.join("/"); // np. "./interfaces" + + // Z "tui.rs" bierzemy "tui" + let core_name = file_name.split('.').next().unwrap_or(""); + + // Budujemy docelową nazwę folderu: "./interfaces/tui/" + let expected_sibling = format!("{}/{}/", parent_dir, core_name); + + // Sprawdzamy, czy takie rodzeństwo istnieje na dysku + if !env.contains(expected_sibling.as_str()) { + return false; // Sierota! Odrzucamy. + } + } + } + + true } /// Ewaluuje kolekcję ścieżek, wywołując odpowiedni callback dla dopasowania i braku dopasowania. pub fn evaluate( &self, paths: I, + env: &HashSet<&str>, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) where @@ -124,7 +160,7 @@ impl PathMatcher { { for path in paths { let path_ref = path.as_ref(); - if self.is_match(path_ref) { + if self.is_match(path_ref, env) { on_match(path_ref); } else { on_mismatch(path_ref); @@ -153,9 +189,10 @@ impl PathMatchers { } /// Zwraca true, jeśli ścieżka pasuje do JAKIEGOKOLWIEK wzorca (logiczne OR) - pub fn is_match(&self, path: &str) -> bool { + pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { for matcher in &self.matchers { - if matcher.is_match(path) { + // Przekazujemy `env` w dół do pojedynczego matchera + if matcher.is_match(path, env) { return true; } } @@ -166,6 +203,7 @@ impl PathMatchers { pub fn evaluate( &self, paths: I, + env: &HashSet<&str>, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) where @@ -176,7 +214,7 @@ impl PathMatchers { { for path in paths { let path_ref = path.as_ref(); - if self.is_match(path_ref) { + if self.is_match(path_ref, env) { on_match(path_ref); } else { on_mismatch(path_ref); From bd727a5644e9116f89a6195072dc2dbecfb7a4ec Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 14:15:38 +0100 Subject: [PATCH 06/45] fix --- examples/skaner.rs | 15 +++++++++++---- src/core/path_matcher.rs | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/examples/skaner.rs b/examples/skaner.rs index 5d697a7..2b7d9ae 100644 --- a/examples/skaner.rs +++ b/examples/skaner.rs @@ -1,4 +1,4 @@ -use cargo_plot::core::path_matcher::{PathMatchers, get_icon_for_path}; +use cargo_plot::core::path_matcher::{PathMatchers, get_icon_for_path, expand_braces}; use cargo_plot::core::path_getter::get_paths; use std::collections::HashSet; use std::env; @@ -32,11 +32,18 @@ fn get_patterns_from_cli() -> Vec { fn main() { // 1. ODCZYT ARGUMENTÓW Z KONSOLI - let patterns = get_patterns_from_cli(); - println!("🔍 Skanuję używając wzorców: {:?}", patterns); + let patterns_raw = get_patterns_from_cli(); + println!("🔍 Wzorce wejściowe (RAW): {:?}", patterns_raw); + + // 🔴 NOWOŚĆ: Przepuszczamy wzorce przez middleware w celach wizualnych + let mut patterns_tok = Vec::new(); + for pat in &patterns_raw { + patterns_tok.extend(expand_braces(pat)); + } + println!("⚙️ Wzorce po middleware (TOK): {:?}", patterns_tok); let is_case_sensitive = false; - let matchers = PathMatchers::new(&patterns, is_case_sensitive).expect("Błąd kompilacji wzorców"); + let matchers = PathMatchers::new(&patterns_raw, is_case_sensitive).expect("Błąd kompilacji wzorców"); // let paths_to_test: Vec<&str> = include!("data.rs"); let paths_to_test = get_paths("./src"); // let wycinek = &paths_to_test[..std::cmp::min(25, paths_to_test.len())]; diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index 2fbc501..77aedf1 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -1,6 +1,29 @@ use regex::Regex; use std::collections::HashSet; +/// MIDDLEWARE: Rozwija klamry we wzorcach (Brace Expansion). +/// Np. "@tui{.rs,/,/**}" -> ["@tui.rs", "@tui/", "@tui/**"] +pub fn expand_braces(pattern: &str) -> Vec { + // Szukamy pierwszej otwierającej i zamykającej klamry + if let (Some(start), Some(end)) = (pattern.find('{'), pattern.find('}')) { + if start < end { + let prefix = &pattern[..start]; + let suffix = &pattern[end + 1..]; + let options = &pattern[start + 1..end]; + + let mut expanded = Vec::new(); + for opt in options.split(',') { + let new_pattern = format!("{}{}{}", prefix, opt, suffix); + // Rekurencja! Jeśli wzorzec miał więcej klamer, rozwijamy dalej + expanded.extend(expand_braces(&new_pattern)); + } + return expanded; + } + } + // Jeśli nie ma (więcej) klamer, po prostu zwracamy gotowy string + vec![pattern.to_string()] +} + pub struct PathMatcher { regex: Regex, targets_file: bool, @@ -75,6 +98,8 @@ impl PathMatcher { } '?' => re.push_str("[^/]"), '{' => { + // UWAGA: Ten blok '{' działa tylko jeśli klamry NIE ZOSTAŁY ROZWINIĘTE + // przez middleware (bo np. nie miały przecinka). Inaczej parser nigdy tu nie wejdzie. let mut options = String::new(); i += 1; while i < chars.len() && chars[i] != '}' { @@ -182,8 +207,14 @@ impl PathMatchers { { let mut matchers = Vec::new(); for pat in patterns { - // Używamy "świętej" funkcji do skompilowania każdego z nich - matchers.push(PathMatcher::new(pat.as_ref(), case_sensitive)?); + // ⚡ TUTAJ JEST GŁÓWNA ZMIANA: + // Najpierw przepuszczamy wzorzec przez nasz nowy Middleware: + let expanded_patterns = expand_braces(pat.as_ref()); + + // Dopiero potem kompilujemy każdą z wygenerowanych wersji: + for expanded_pat in expanded_patterns { + matchers.push(PathMatcher::new(&expanded_pat, case_sensitive)?); + } } Ok(Self { matchers }) } From f7e249ff3e541eddb277a4989a5fd7d4eb47d1c9 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 15:24:22 +0100 Subject: [PATCH 07/45] (new : @/$) --- examples/skaner.rs | 4 +- src/core.rs | 4 +- src/core/path_class.rs | 20 ++++++++ src/core/path_matcher.rs | 91 +++++++++++++++------------------- src/core/path_matcher_utils.rs | 23 +++++++++ 5 files changed, 88 insertions(+), 54 deletions(-) create mode 100644 src/core/path_class.rs create mode 100644 src/core/path_matcher_utils.rs diff --git a/examples/skaner.rs b/examples/skaner.rs index 2b7d9ae..51f2b67 100644 --- a/examples/skaner.rs +++ b/examples/skaner.rs @@ -1,5 +1,7 @@ -use cargo_plot::core::path_matcher::{PathMatchers, get_icon_for_path, expand_braces}; +use cargo_plot::core::path_matcher::PathMatchers; +use cargo_plot::core::path_matcher_utils::expand_braces; use cargo_plot::core::path_getter::get_paths; +use cargo_plot::core::path_class::get_icon_for_path; use std::collections::HashSet; use std::env; use std::process; diff --git a/src/core.rs b/src/core.rs index 1b4cea2..3a98682 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,2 +1,4 @@ pub mod path_matcher; -pub mod path_getter; \ No newline at end of file +pub mod path_getter; +pub mod path_class; +pub mod path_matcher_utils; \ No newline at end of file diff --git a/src/core/path_class.rs b/src/core/path_class.rs new file mode 100644 index 0000000..dea55c3 --- /dev/null +++ b/src/core/path_class.rs @@ -0,0 +1,20 @@ + +/// Zwraca odpowiednią ikonę (emoji) dla podanej ścieżki, +/// rozpoznając foldery (końcówka '/') oraz elementy ukryte (kropka na początku nazwy). +pub fn get_icon_for_path(path: &str) -> &'static str { + let is_dir = path.ends_with('/'); + + // Wyciągamy samą nazwę pliku/folderu: + // 1. Usuwamy ew. ukośnik z końca (żeby folder nie zwrócił pustego stringa) + // 2. Dzielimy przez ukośniki i bierzemy ostatni element + let nazwa = path.trim_end_matches('/').split('/').last().unwrap_or(""); + let is_hidden = nazwa.starts_with('.'); + + // Dobieramy odpowiednią ikonę na podstawie dwóch cech + match (is_dir, is_hidden) { + (true, false) => "📁", // Zwykły folder + (true, true) => "🗃️", // Ukryty folder (z kropką) + (false, false)=> "📄", // Zwykły plik + (false, true) => "⚙️ ", // Ukryty plik (konfiguracyjny z kropką) + } +} \ No newline at end of file diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index 77aedf1..015f2f7 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -1,40 +1,20 @@ use regex::Regex; use std::collections::HashSet; - -/// MIDDLEWARE: Rozwija klamry we wzorcach (Brace Expansion). -/// Np. "@tui{.rs,/,/**}" -> ["@tui.rs", "@tui/", "@tui/**"] -pub fn expand_braces(pattern: &str) -> Vec { - // Szukamy pierwszej otwierającej i zamykającej klamry - if let (Some(start), Some(end)) = (pattern.find('{'), pattern.find('}')) { - if start < end { - let prefix = &pattern[..start]; - let suffix = &pattern[end + 1..]; - let options = &pattern[start + 1..end]; - - let mut expanded = Vec::new(); - for opt in options.split(',') { - let new_pattern = format!("{}{}{}", prefix, opt, suffix); - // Rekurencja! Jeśli wzorzec miał więcej klamer, rozwijamy dalej - expanded.extend(expand_braces(&new_pattern)); - } - return expanded; - } - } - // Jeśli nie ma (więcej) klamer, po prostu zwracamy gotowy string - vec![pattern.to_string()] -} +use super::path_matcher_utils::expand_braces; pub struct PathMatcher { regex: Regex, targets_file: bool, - requires_sibling: bool, // ⚡ : Flaga świadomości kontekstu + requires_sibling: bool, // @ : Para (Plik <=> Folder) + requires_orphan: bool, // $ : Jednostronne (Plik => Folder) } impl PathMatcher { pub fn new(pattern: &str, case_sensitive: bool) -> Result { - // ⚡: Wykrywamy '@' i od razu usuwamy go ze wzorca! + // Detekcja flag i usuwanie ich ze wzorca (replace nie psuje reszty stringa) let requires_sibling = pattern.contains('@'); - let clean_pattern_str = pattern.replace('@', ""); + let requires_orphan = pattern.contains('$'); + let clean_pattern_str = pattern.replace('@', "").replace('$', ""); let mut re = String::new(); @@ -44,12 +24,15 @@ impl PathMatcher { } let mut is_anchored = false; + // Używamy wyczyszczonego wzorca (bez '@'), żeby nie psuć parsera z tabeli! let mut p = clean_pattern_str.as_str(); // BARIERA LOGICZNA: Jeśli wzorzec nie kończy się na ukośnik ani na '**', // to według Twojej tabeli celuje WYŁĄCZNIE w pliki. - let targets_file = !pattern.ends_with('/') && !pattern.ends_with("**"); + // let targets_file = !pattern.ends_with('/') && !pattern.ends_with("**"); + // BARDZO WAŻNE: targets_file sprawdzamy na 'p' (czystym wzorcu) + let targets_file = !p.ends_with('/') && !p.ends_with("**"); // 1. ZASADY KOTWICZENIA if p.starts_with("./") { @@ -129,6 +112,7 @@ impl PathMatcher { regex: Regex::new(&re)?, targets_file, requires_sibling, + requires_orphan, }) } @@ -147,8 +131,10 @@ impl PathMatcher { return false; } - // ⚡: Regex powiedział "TAK", ale wzorzec miał "@", więc sprawdzamy brata! - if self.requires_sibling && !path.ends_with('/') { + // ⚡: Regex powiedział "TAK", ale wzorzec miał "@" lub "$", więc sprawdzamy brata! + // --- ZASADA RODZEŃSTWA (@) LUB SIEROT ($) dla PLIKÓW --- + // Obie zasady wymagają od pliku posiadania folderu-brata + if (self.requires_sibling || self.requires_orphan) && !path.ends_with('/') { // Z "./interfaces/tui.rs" robimy folder nadrzędny i nazwę pliku let mut components: Vec<&str> = path.split('/').collect(); if let Some(file_name) = components.pop() { @@ -158,15 +144,36 @@ impl PathMatcher { let core_name = file_name.split('.').next().unwrap_or(""); // Budujemy docelową nazwę folderu: "./interfaces/tui/" - let expected_sibling = format!("{}/{}/", parent_dir, core_name); + let expected_folder = if parent_dir.is_empty() { + format!("{}/", core_name) + } else { + format!("{}/{}/", parent_dir, core_name) + }; // Sprawdzamy, czy takie rodzeństwo istnieje na dysku - if !env.contains(expected_sibling.as_str()) { - return false; // Sierota! Odrzucamy. + if !env.contains(expected_folder.as_str()) { + return false; // Plik nie ma folderu -> Odrzucamy dla @ i $ } } } + // --- DODATKOWA ZASADA RODZEŃSTWA (@) DLA FOLDERÓW --- + // Tylko @ wymaga, aby folder też miał plik-indeks + if self.requires_sibling && path.ends_with('/') { + let dir_no_slash = path.trim_end_matches('/'); + + // Szukamy w środowisku pliku, który autoryzuje ten folder + 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; // Folder nie ma pliku -> Odrzucamy TYLKO dla @ + } + } + true } @@ -254,23 +261,3 @@ impl PathMatchers { } } - -/// Zwraca odpowiednią ikonę (emoji) dla podanej ścieżki, -/// rozpoznając foldery (końcówka '/') oraz elementy ukryte (kropka na początku nazwy). -pub fn get_icon_for_path(path: &str) -> &'static str { - let is_dir = path.ends_with('/'); - - // Wyciągamy samą nazwę pliku/folderu: - // 1. Usuwamy ew. ukośnik z końca (żeby folder nie zwrócił pustego stringa) - // 2. Dzielimy przez ukośniki i bierzemy ostatni element - let nazwa = path.trim_end_matches('/').split('/').last().unwrap_or(""); - let is_hidden = nazwa.starts_with('.'); - - // Dobieramy odpowiednią ikonę na podstawie dwóch cech - match (is_dir, is_hidden) { - (true, false) => "📁", // Zwykły folder - (true, true) => "🗃️", // Ukryty folder (z kropką) - (false, false)=> "📄", // Zwykły plik - (false, true) => "⚙️ ", // Ukryty plik (konfiguracyjny z kropką) - } -} \ No newline at end of file diff --git a/src/core/path_matcher_utils.rs b/src/core/path_matcher_utils.rs new file mode 100644 index 0000000..9763fd6 --- /dev/null +++ b/src/core/path_matcher_utils.rs @@ -0,0 +1,23 @@ + +/// MIDDLEWARE: Rozwija klamry we wzorcach (Brace Expansion). +/// Np. "@tui{.rs,/,/**}" -> ["@tui.rs", "@tui/", "@tui/**"] +pub fn expand_braces(pattern: &str) -> Vec { + // Szukamy pierwszej otwierającej i zamykającej klamry + if let (Some(start), Some(end)) = (pattern.find('{'), pattern.find('}')) { + if start < end { + let prefix = &pattern[..start]; + let suffix = &pattern[end + 1..]; + let options = &pattern[start + 1..end]; + + let mut expanded = Vec::new(); + for opt in options.split(',') { + let new_pattern = format!("{}{}{}", prefix, opt, suffix); + // Rekurencja! Jeśli wzorzec miał więcej klamer, rozwijamy dalej + expanded.extend(expand_braces(&new_pattern)); + } + return expanded; + } + } + // Jeśli nie ma (więcej) klamer, po prostu zwracamy gotowy string + vec![pattern.to_string()] +} \ No newline at end of file From bc83f2702381c40db5668abce539123dc8ce261b Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 17:25:47 +0100 Subject: [PATCH 08/45] (fix&cleaning) --- examples/skaner.rs | 23 +- src/core.rs | 5 +- src/core/path_class.rs | 21 +- src/core/path_getter.rs | 26 +- src/core/path_matcher.rs | 272 +----------------- src/core/path_matcher/matcher.rs | 268 +++++++++++++++++ .../matcher_utils.rs} | 10 +- src/core/path_matcher/matchers.rs | 66 +++++ 8 files changed, 377 insertions(+), 314 deletions(-) create mode 100644 src/core/path_matcher/matcher.rs rename src/core/{path_matcher_utils.rs => path_matcher/matcher_utils.rs} (64%) create mode 100644 src/core/path_matcher/matchers.rs diff --git a/examples/skaner.rs b/examples/skaner.rs index 51f2b67..a6d03e0 100644 --- a/examples/skaner.rs +++ b/examples/skaner.rs @@ -1,12 +1,10 @@ -use cargo_plot::core::path_matcher::PathMatchers; -use cargo_plot::core::path_matcher_utils::expand_braces; -use cargo_plot::core::path_getter::get_paths; use cargo_plot::core::path_class::get_icon_for_path; +use cargo_plot::core::path_getter::get_paths; +use cargo_plot::core::path_matcher::{/*PathMatcher,*/ PathMatchers, expand_braces}; use std::collections::HashSet; use std::env; use std::process; - /// Wyciąga flagi `-x` z argumentów wywołania. /// Jeśli nie znajdzie żadnych wzorców, wypisuje instrukcję i kończy program. fn get_patterns_from_cli() -> Vec { @@ -44,8 +42,9 @@ fn main() { } println!("⚙️ Wzorce po middleware (TOK): {:?}", patterns_tok); - let is_case_sensitive = false; - let matchers = PathMatchers::new(&patterns_raw, is_case_sensitive).expect("Błąd kompilacji wzorców"); + let is_case_sensitive = false; + let matchers = + PathMatchers::new(&patterns_raw, is_case_sensitive).expect("Błąd kompilacji wzorców"); // let paths_to_test: Vec<&str> = include!("data.rs"); let paths_to_test = get_paths("./src"); // let wycinek = &paths_to_test[..std::cmp::min(25, paths_to_test.len())]; @@ -60,8 +59,7 @@ fn main() { let mut dopasowane = 0; let total = paths_to_test.len(); - - + // Ewaluacja matchers.evaluate( &paths_to_test, @@ -74,9 +72,12 @@ fn main() { }, |_path| { // Miejsce na logikę dla odrzuconych ścieżek - } + }, ); println!("----------"); - println!("📊 Podsumowanie: Dopasowano {} z {} ścieżek.", dopasowane, total); -} \ No newline at end of file + println!( + "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", + dopasowane, total + ); +} diff --git a/src/core.rs b/src/core.rs index 3a98682..b7e48e6 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,4 +1,3 @@ -pub mod path_matcher; -pub mod path_getter; pub mod path_class; -pub mod path_matcher_utils; \ No newline at end of file +pub mod path_getter; +pub mod path_matcher; diff --git a/src/core/path_class.rs b/src/core/path_class.rs index dea55c3..d0fa738 100644 --- a/src/core/path_class.rs +++ b/src/core/path_class.rs @@ -1,20 +1,15 @@ - -/// Zwraca odpowiednią ikonę (emoji) dla podanej ścieżki, -/// rozpoznając foldery (końcówka '/') oraz elementy ukryte (kropka na początku nazwy). +/// [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('/'); - - // Wyciągamy samą nazwę pliku/folderu: - // 1. Usuwamy ew. ukośnik z końca (żeby folder nie zwrócił pustego stringa) - // 2. Dzielimy przez ukośniki i bierzemy ostatni element + let nazwa = path.trim_end_matches('/').split('/').last().unwrap_or(""); let is_hidden = nazwa.starts_with('.'); - // Dobieramy odpowiednią ikonę na podstawie dwóch cech match (is_dir, is_hidden) { - (true, false) => "📁", // Zwykły folder - (true, true) => "🗃️", // Ukryty folder (z kropką) - (false, false)=> "📄", // Zwykły plik - (false, true) => "⚙️ ", // Ukryty plik (konfiguracyjny z kropką) + (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 } -} \ No newline at end of file +} diff --git a/src/core/path_getter.rs b/src/core/path_getter.rs index 0a5180e..43ae07b 100644 --- a/src/core/path_getter.rs +++ b/src/core/path_getter.rs @@ -1,39 +1,31 @@ use std::path::Path; use walkdir::WalkDir; -/// Skanuje podany katalog i zwraca listę ścieżek znormalizowanych do formatu: -/// - zaczynają się od "./" -/// - mają ukośniki "/" -/// - foldery kończą się na "/" -/// - ignoruje symlinki/junctions (!ReparsePoint) +/// [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 get_paths>(dir_path: P) -> Vec { let mut result = Vec::new(); let root_path = dir_path.as_ref(); - // WalkDir::new domyślnie iteruje rekurencyjnie (odpowiednik -Recurse) - // filter_map(|e| e.ok()) bezpiecznie ignoruje błędy braku uprawnień (Access Denied) for entry in WalkDir::new(root_path).into_iter().filter_map(|e| e.ok()) { - - // Pomijamy sam katalog główny (depth == 0), interesuje nas tylko zawartość + // [POL]: Pominięcie katalogu głównego (głębokość 0). + // [ENG]: Skip the root directory (depth 0). if entry.depth() == 0 { continue; } - // Pomijamy symlinki i punkty reparse (odpowiednik !ReparsePoint) + // [POL]: Pominięcie dowiązań symbolicznych i punktów reparse. + // [ENG]: Skip symbolic links and reparse points. if entry.path_is_symlink() { continue; } - // Ucinamy bezwzględną część ścieżki (zostaje nam np. "src\main.rs") if let Ok(rel_path) = entry.path().strip_prefix(root_path) { - - // Konwersja ścieżki i ujednolicenie ukośników (Windows '\' -> '/') + // [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('\\', "/"); - - // Doklejamy wymagany prefix "./" let mut final_path = format!("./{}", relative_str); - // Jeśli to folder (odpowiednik PSIsContainer), dodajemy "/" na końcu if entry.file_type().is_dir() { final_path.push('/'); } @@ -43,4 +35,4 @@ pub fn get_paths>(dir_path: P) -> Vec { } result -} \ No newline at end of file +} diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index 015f2f7..018beb4 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -1,263 +1,9 @@ -use regex::Regex; -use std::collections::HashSet; -use super::path_matcher_utils::expand_braces; - -pub struct PathMatcher { - regex: Regex, - targets_file: bool, - requires_sibling: bool, // @ : Para (Plik <=> Folder) - requires_orphan: bool, // $ : Jednostronne (Plik => Folder) -} - -impl PathMatcher { - pub fn new(pattern: &str, case_sensitive: bool) -> Result { - // Detekcja flag i usuwanie ich ze wzorca (replace nie psuje reszty stringa) - let requires_sibling = pattern.contains('@'); - let requires_orphan = pattern.contains('$'); - let clean_pattern_str = pattern.replace('@', "").replace('$', ""); - - let mut re = String::new(); - - // 🔥: Wstrzykujemy flagę niewrażliwości na wielkość liter - if !case_sensitive { - re.push_str("(?i)"); - } - - let mut is_anchored = false; - - // Używamy wyczyszczonego wzorca (bez '@'), żeby nie psuć parsera z tabeli! - let mut p = clean_pattern_str.as_str(); - - // BARIERA LOGICZNA: Jeśli wzorzec nie kończy się na ukośnik ani na '**', - // to według Twojej tabeli celuje WYŁĄCZNIE w pliki. - // let targets_file = !pattern.ends_with('/') && !pattern.ends_with("**"); - // BARDZO WAŻNE: targets_file sprawdzamy na 'p' (czystym wzorcu) - let targets_file = !p.ends_with('/') && !p.ends_with("**"); - - // 1. ZASADY KOTWICZENIA - if p.starts_with("./") { - is_anchored = true; - p = &p[2..]; // Ucinamy ./ - } else if p.starts_with("**/") { - is_anchored = true; - } - - if is_anchored { - re.push('^'); - } else { - re.push_str("(?:^|/)"); - } - - // 2. PARSOWANIE ZNAKÓW - 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("\\."), - '/' => re.push('/'), - '*' => { - if i + 1 < chars.len() && chars[i + 1] == '*' { - if i + 2 < chars.len() && chars[i + 2] == '/' { - // Wzorzec: **/ - re.push_str("(?:[^/]+/)*"); - i += 2; - } else { - // Wzorzec: ** - // POPRAWKA: Zamiana z .* na .+ (wymaga minimum 1 znaku zawartości!) - re.push_str(".+"); - i += 1; - } - } else { - // Zwykła gwiazdka * - re.push_str("[^/]*"); - } - } - '?' => re.push_str("[^/]"), - '{' => { - // UWAGA: Ten blok '{' działa tylko jeśli klamry NIE ZOSTAŁY ROZWINIĘTE - // przez middleware (bo np. nie miały przecinka). Inaczej parser nigdy tu nie wejdzie. - 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(|s| regex::escape(s)).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; - } - - re.push('$'); - - Ok(Self { - regex: Regex::new(&re)?, - targets_file, - requires_sibling, - requires_orphan, - }) - } - - // ⚡: is_match przyjmuje środowisko do sprawdzania rodzeństwa - pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { - // TWARDA ZASADA: Jeśli wzorzec to plik, to natychmiastowo - // odrzucamy każdą ścieżkę testową, która kończy się na ukośnik! - if self.targets_file && path.ends_with('/') { - return false; - } - - let clean_path = path.strip_prefix("./").unwrap_or(path); - - // Jeśli Regex odrzuca ścieżkę, nie ma o czym gadać - if !self.regex.is_match(clean_path) { - return false; - } - - // ⚡: Regex powiedział "TAK", ale wzorzec miał "@" lub "$", więc sprawdzamy brata! - // --- ZASADA RODZEŃSTWA (@) LUB SIEROT ($) dla PLIKÓW --- - // Obie zasady wymagają od pliku posiadania folderu-brata - if (self.requires_sibling || self.requires_orphan) && !path.ends_with('/') { - // Z "./interfaces/tui.rs" robimy folder nadrzędny i nazwę pliku - let mut components: Vec<&str> = path.split('/').collect(); - if let Some(file_name) = components.pop() { - let parent_dir = components.join("/"); // np. "./interfaces" - - // Z "tui.rs" bierzemy "tui" - let core_name = file_name.split('.').next().unwrap_or(""); - - // Budujemy docelową nazwę folderu: "./interfaces/tui/" - let expected_folder = if parent_dir.is_empty() { - format!("{}/", core_name) - } else { - format!("{}/{}/", parent_dir, core_name) - }; - - // Sprawdzamy, czy takie rodzeństwo istnieje na dysku - if !env.contains(expected_folder.as_str()) { - return false; // Plik nie ma folderu -> Odrzucamy dla @ i $ - } - } - } - - // --- DODATKOWA ZASADA RODZEŃSTWA (@) DLA FOLDERÓW --- - // Tylko @ wymaga, aby folder też miał plik-indeks - if self.requires_sibling && path.ends_with('/') { - let dir_no_slash = path.trim_end_matches('/'); - - // Szukamy w środowisku pliku, który autoryzuje ten folder - 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; // Folder nie ma pliku -> Odrzucamy TYLKO dla @ - } - } - - true - } - - /// Ewaluuje kolekcję ścieżek, wywołując odpowiedni callback dla dopasowania i braku dopasowania. - pub fn evaluate( - &self, - paths: I, - env: &HashSet<&str>, - mut on_match: OnMatch, - mut on_mismatch: OnMismatch, - ) where - I: IntoIterator, - S: AsRef, - OnMatch: FnMut(&str), - OnMismatch: FnMut(&str), - { - for path in paths { - let path_ref = path.as_ref(); - if self.is_match(path_ref, env) { - on_match(path_ref); - } else { - on_mismatch(path_ref); - } - } - } -} - -pub struct PathMatchers { - matchers: Vec, -} - -impl PathMatchers { - /// Przyjmuje kolekcję wzorców i kompiluje je wszystkie - pub fn new(patterns: I, case_sensitive: bool) -> Result - where - I: IntoIterator, - S: AsRef, - { - let mut matchers = Vec::new(); - for pat in patterns { - // ⚡ TUTAJ JEST GŁÓWNA ZMIANA: - // Najpierw przepuszczamy wzorzec przez nasz nowy Middleware: - let expanded_patterns = expand_braces(pat.as_ref()); - - // Dopiero potem kompilujemy każdą z wygenerowanych wersji: - for expanded_pat in expanded_patterns { - matchers.push(PathMatcher::new(&expanded_pat, case_sensitive)?); - } - } - Ok(Self { matchers }) - } - - /// Zwraca true, jeśli ścieżka pasuje do JAKIEGOKOLWIEK wzorca (logiczne OR) - pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { - for matcher in &self.matchers { - // Przekazujemy `env` w dół do pojedynczego matchera - if matcher.is_match(path, env) { - return true; - } - } - false - } - - /// Ewaluuje kolekcję ścieżek względem wszystkich wzorców (logiczne OR), wywołując callbacki. - pub fn evaluate( - &self, - paths: I, - env: &HashSet<&str>, - mut on_match: OnMatch, - mut on_mismatch: OnMismatch, - ) where - I: IntoIterator, - S: AsRef, - OnMatch: FnMut(&str), - OnMismatch: FnMut(&str), - { - for path in paths { - let path_ref = path.as_ref(); - if self.is_match(path_ref, env) { - on_match(path_ref); - } else { - on_mismatch(path_ref); - } - } - } -} - +/// [POL]: Główny moduł logiki dopasowywania ścieżek. +/// [ENG]: Core module for path matching logic. +pub mod matcher; +pub mod matcher_utils; +pub mod matchers; + +pub use self::matcher::PathMatcher; +pub use self::matcher_utils::expand_braces; +pub use self::matchers::PathMatchers; diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs new file mode 100644 index 0000000..f452ec6 --- /dev/null +++ b/src/core/path_matcher/matcher.rs @@ -0,0 +1,268 @@ +use regex::Regex; +use std::collections::HashSet; + +/// [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, + requires_sibling: bool, // [POL]: Flaga @ (para plik-folder) | [ENG]: Flag @ (file-directory pair) + requires_orphan: bool, // [POL]: Flaga $ (jednostronna relacja) | [ENG]: Flag $ (one-way relation) + is_deep: bool, // [POL]: Flaga + (rekurencyjne zacienianie) | [ENG]: Flag + (recursive shadowing) + base_name: String, // [POL]: Nazwa bazowa modułu do weryfikacji relacji | [ENG]: Base name of the module for relation verification +} + +impl PathMatcher { + pub fn new(pattern: &str, case_sensitive: bool) -> Result { + /// [POL]: Kompiluje wzorzec tekstowy do wyrażenia regularnego, ekstrahując flagi sterujące. + /// [ENG]: Compiles a text pattern into a regular expression, extracting control flags. + let is_deep = pattern.ends_with('+'); + let requires_sibling = pattern.contains('@'); + let requires_orphan = pattern.contains('$'); + let clean_pattern_str = pattern.replace('@', "").replace('$', "").replace('+', ""); + + let base_name = clean_pattern_str + .trim_end_matches('/') + .trim_end_matches("**") + .split('/') + .last() + .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(); + + // [POL]: KLASYFIKACJA CELU DOPASOWANIA. Zmienna określa, czy wzorzec odnosi się wyłącznie do plików. + // Brak ukośnika '/' lub sekwencji '**' na końcu oznacza restrykcję do obiektów niebędących katalogami. + // [ENG]: MATCH TARGET CLASSIFICATION. This variable determines if the pattern is restricted to files only. + // The absence of a trailing slash '/' or the '**' sequence implies a restriction to non-directory objects. + // // let targets_file = !pattern.ends_with('/') && !pattern.ends_with("**"); + // [POL]: ANALIZA CIĄGU ZNORMALIZOWANEGO. Weryfikacja odbywa się na zmiennej 'p' (wzorzec bazowy), + // a nie na surowym 'pattern'. Gwarantuje to, że flagi sterujące (np. '@', '$', '+') nie zostaną + // błędnie zinterpretowane jako część ścieżki, co zafałszowałoby wykrycie intencji wzorca. + // [ENG]: NORMALISED STRING ANALYSIS. Verification is performed on variable 'p' (base pattern) + // instead of the raw 'pattern'. This ensures that control flags (e.g. '@', '$', '+') are not + // misinterpreted as path components, which would compromise the detection of the intended target type. + 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("\\."), + // '/' => re.push('/'), + '/' => { + 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(|s| regex::escape(s)).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, // 🆕 + }) + } + + /// [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 ścieżkę i wywołuje odpowiednie akcje. + /// [ENG]: Evaluates path and triggers respective actions. + pub fn evaluate( + &self, + paths: I, + env: &HashSet<&str>, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, + ) where + I: IntoIterator, + S: AsRef, + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), + { + for path in paths { + let path_ref = path.as_ref(); + if self.is_match(path_ref, env) { + on_match(path_ref); + } else { + on_mismatch(path_ref); + } + } + } + + /// [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 + } +} diff --git a/src/core/path_matcher_utils.rs b/src/core/path_matcher/matcher_utils.rs similarity index 64% rename from src/core/path_matcher_utils.rs rename to src/core/path_matcher/matcher_utils.rs index 9763fd6..10fcb92 100644 --- a/src/core/path_matcher_utils.rs +++ b/src/core/path_matcher/matcher_utils.rs @@ -1,8 +1,6 @@ - -/// MIDDLEWARE: Rozwija klamry we wzorcach (Brace Expansion). -/// Np. "@tui{.rs,/,/**}" -> ["@tui.rs", "@tui/", "@tui/**"] +/// [POL]: Wykonuje rozwinięcie klamer we wzorcu (np. {a,b} -> [a, b]). Obsługuje rekurencję. +/// [ENG]: Performs brace expansion in the pattern (e.g. {a,b} -> [a, b]). Supports recursion. pub fn expand_braces(pattern: &str) -> Vec { - // Szukamy pierwszej otwierającej i zamykającej klamry if let (Some(start), Some(end)) = (pattern.find('{'), pattern.find('}')) { if start < end { let prefix = &pattern[..start]; @@ -12,12 +10,10 @@ pub fn expand_braces(pattern: &str) -> Vec { let mut expanded = Vec::new(); for opt in options.split(',') { let new_pattern = format!("{}{}{}", prefix, opt, suffix); - // Rekurencja! Jeśli wzorzec miał więcej klamer, rozwijamy dalej expanded.extend(expand_braces(&new_pattern)); } return expanded; } } - // Jeśli nie ma (więcej) klamer, po prostu zwracamy gotowy string vec![pattern.to_string()] -} \ No newline at end of file +} diff --git a/src/core/path_matcher/matchers.rs b/src/core/path_matcher/matchers.rs new file mode 100644 index 0000000..88bb67b --- /dev/null +++ b/src/core/path_matcher/matchers.rs @@ -0,0 +1,66 @@ +use super::matcher::PathMatcher; +use super::matcher_utils::expand_braces; +use std::collections::HashSet; + +/// [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 { + // [POL]: Przetwarzanie wstępne wzorca (Brace Expansion). + // [ENG]: Pattern preprocessing (Brace Expansion). + let expanded_patterns = expand_braces(pat.as_ref()); + + for expanded_pat in expanded_patterns { + matchers.push(PathMatcher::new(&expanded_pat, case_sensitive)?); + } + } + Ok(Self { matchers }) + } + + /// [POL]: Weryfikuje, czy ścieżka pasuje do dowolnego ze skonfigurowanych wzorców (logika OR). + /// [ENG]: Verifies if the path matches any of the configured patterns (OR logic). + pub fn is_match(&self, path: &str, env: &HashSet<&str>) -> bool { + for matcher in &self.matchers { + if matcher.is_match(path, env) { + return true; + } + } + false + } + + /// [POL]: Ewaluuje zbiór ścieżek, wykonując odpowiednie domknięcia dla dopasowanych i niedopasowanych elementów. + /// [ENG]: Evaluates a set of paths, executing respective closures for matched and mismatched elements. + pub fn evaluate( + &self, + paths: I, + env: &HashSet<&str>, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, + ) where + I: IntoIterator, + S: AsRef, + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), + { + for path in paths { + let path_ref = path.as_ref(); + if self.is_match(path_ref, env) { + on_match(path_ref); + } else { + on_mismatch(path_ref); + } + } + } +} From 1d0248e86dd58de3b2adcb3ea242a9926a555e8d Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 17:55:56 +0100 Subject: [PATCH 09/45] fix --- src/core/path_matcher/matcher.rs | 6 +-- src/core/path_matcher/matcher_utils.rs | 51 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index f452ec6..3943a3b 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -13,9 +13,9 @@ pub struct PathMatcher { } impl PathMatcher { - pub fn new(pattern: &str, case_sensitive: bool) -> Result { - /// [POL]: Kompiluje wzorzec tekstowy do wyrażenia regularnego, ekstrahując flagi sterujące. - /// [ENG]: Compiles a text pattern into a regular expression, extracting control flags. + pub fn new(pattern: &str, case_sensitive: bool) -> Result { + // [POL]: Kompiluje wzorzec tekstowy do wyrażenia regularnego, ekstrahując flagi sterujące. + // [ENG]: Compiles a text pattern into a regular expression, extracting control flags. let is_deep = pattern.ends_with('+'); let requires_sibling = pattern.contains('@'); let requires_orphan = pattern.contains('$'); diff --git a/src/core/path_matcher/matcher_utils.rs b/src/core/path_matcher/matcher_utils.rs index 10fcb92..3d21336 100644 --- a/src/core/path_matcher/matcher_utils.rs +++ b/src/core/path_matcher/matcher_utils.rs @@ -17,3 +17,54 @@ pub fn expand_braces(pattern: &str) -> Vec { } vec![pattern.to_string()] } + + +/// [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, +} \ No newline at end of file From 00f43fba7688518387bc9c00c6fbea31c9918578 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sun, 15 Mar 2026 18:10:34 +0100 Subject: [PATCH 10/45] (add: sort) --- examples/skaner.rs | 3 +- src/core/path_matcher.rs | 2 +- src/core/path_matcher/matcher.rs | 26 ++++++++++--- src/core/path_matcher/matcher_utils.rs | 54 +++++++++++++++++++++++++- src/core/path_matcher/matchers.rs | 27 +++++++++---- 5 files changed, 95 insertions(+), 17 deletions(-) diff --git a/examples/skaner.rs b/examples/skaner.rs index a6d03e0..0a493b9 100644 --- a/examples/skaner.rs +++ b/examples/skaner.rs @@ -1,6 +1,6 @@ use cargo_plot::core::path_class::get_icon_for_path; use cargo_plot::core::path_getter::get_paths; -use cargo_plot::core::path_matcher::{/*PathMatcher,*/ PathMatchers, expand_braces}; +use cargo_plot::core::path_matcher::{/*PathMatcher,*/ PathMatchers, expand_braces,SortStrategy}; use std::collections::HashSet; use std::env; use std::process; @@ -64,6 +64,7 @@ fn main() { matchers.evaluate( &paths_to_test, &environment, // 🔴 NOWOŚĆ: Wstrzykujemy środowisko do silnika! + SortStrategy::AzFileFirstMerge, |path| { // 🔥 Używamy naszej nowej, czystej funkcji! let icon = get_icon_for_path(path); diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index 018beb4..6675ce9 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -5,5 +5,5 @@ pub mod matcher_utils; pub mod matchers; pub use self::matcher::PathMatcher; -pub use self::matcher_utils::expand_braces; +pub use self::matcher_utils::{expand_braces,SortStrategy,sort_paths}; pub use self::matchers::PathMatchers; diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index 3943a3b..892a158 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -1,5 +1,6 @@ use regex::Regex; use std::collections::HashSet; +use super::matcher_utils::{SortStrategy, sort_paths}; /// [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. @@ -204,12 +205,13 @@ impl PathMatcher { true } - /// [POL]: Ewaluuje ścieżkę i wywołuje odpowiednie akcje. - /// [ENG]: Evaluates path and triggers respective actions. + /// [POL]: Ewaluuje kolekcję ścieżek, sortuje wyniki i wywołuje odpowiednie akcje. + /// [ENG]: Evaluates a path collection, sorts the results, and triggers respective actions. pub fn evaluate( &self, paths: I, env: &HashSet<&str>, + strategy: SortStrategy, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) where @@ -218,14 +220,26 @@ impl PathMatcher { OnMatch: FnMut(&str), OnMismatch: FnMut(&str), { + let mut matched = Vec::new(); + let mut mismatched = Vec::new(); + for path in paths { - let path_ref = path.as_ref(); - if self.is_match(path_ref, env) { - on_match(path_ref); + if self.is_match(path.as_ref(), env) { + matched.push(path); } else { - on_mismatch(path_ref); + mismatched.push(path); } } + + sort_paths(&mut matched, strategy); + sort_paths(&mut mismatched, strategy); + + for path in matched { + on_match(path.as_ref()); + } + for path in mismatched { + on_mismatch(path.as_ref()); + } } /// [POL]: Weryfikuje autoryzację korzenia modułu w relacji plik-folder dla trybu 'deep'. diff --git a/src/core/path_matcher/matcher_utils.rs b/src/core/path_matcher/matcher_utils.rs index 3d21336..67132f5 100644 --- a/src/core/path_matcher/matcher_utils.rs +++ b/src/core/path_matcher/matcher_utils.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + /// [POL]: Wykonuje rozwinięcie klamer we wzorcu (np. {a,b} -> [a, b]). Obsługuje rekurencję. /// [ENG]: Performs brace expansion in the pattern (e.g. {a,b} -> [a, b]). Supports recursion. pub fn expand_braces(pattern: &str) -> Vec { @@ -18,7 +20,6 @@ pub fn expand_braces(pattern: &str) -> Vec { vec![pattern.to_string()] } - /// [POL]: Definiuje dostępne strategie sortowania kolekcji ścieżek. /// [ENG]: Defines available sorting strategies for path collections. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -67,4 +68,53 @@ pub enum SortStrategy { /// [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, -} \ No newline at end of file +} + +/// [POL]: Ekstrahuje rdzenną nazwę ścieżki dla strategii Merge (np. usuwa rozszerzenia plików). +/// [ENG]: Extracts the core path name for Merge strategies (e.g. removes file extensions). +fn get_merge_key(path: &str) -> &str { + let trimmed = path.trim_end_matches('/'); + if let Some(idx) = trimmed.rfind('.') { + // [POL]: Ochrona przed usunięciem nazw plików ukrytych (np. ".env") + // [ENG]: Protection against stripping hidden file names (e.g. ".env") + if idx > 0 && trimmed.as_bytes()[idx - 1] != b'/' { + return &trimmed[..idx]; + } + } + trimmed +} + +/// [POL]: Sortuje kolekcję ścieżek na podstawie wybranej strategii. +/// [ENG]: Sorts a collection of paths based on the selected strategy. +pub fn sort_paths>(paths: &mut Vec, strategy: SortStrategy) { + if strategy == 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('/'); + + let a_merge = get_merge_key(a); + let b_merge = get_merge_key(b); + + match strategy { + 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)), + } + }); +} + + diff --git a/src/core/path_matcher/matchers.rs b/src/core/path_matcher/matchers.rs index 88bb67b..6f6f552 100644 --- a/src/core/path_matcher/matchers.rs +++ b/src/core/path_matcher/matchers.rs @@ -1,5 +1,5 @@ use super::matcher::PathMatcher; -use super::matcher_utils::expand_braces; +use super::matcher_utils::{expand_braces, SortStrategy, sort_paths}; use std::collections::HashSet; /// [POL]: Kontener przechowujący kolekcję silników dopasowujących ścieżki. @@ -40,12 +40,13 @@ impl PathMatchers { false } - /// [POL]: Ewaluuje zbiór ścieżek, wykonując odpowiednie domknięcia dla dopasowanych i niedopasowanych elementów. - /// [ENG]: Evaluates a set of paths, executing respective closures for matched and mismatched elements. + /// [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, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) where @@ -54,13 +55,25 @@ impl PathMatchers { OnMatch: FnMut(&str), OnMismatch: FnMut(&str), { + let mut matched = Vec::new(); + let mut mismatched = Vec::new(); + for path in paths { - let path_ref = path.as_ref(); - if self.is_match(path_ref, env) { - on_match(path_ref); + if self.is_match(path.as_ref(), env) { + matched.push(path); } else { - on_mismatch(path_ref); + mismatched.push(path); } } + + sort_paths(&mut matched, strategy); + sort_paths(&mut mismatched, strategy); + + for path in matched { + on_match(path.as_ref()); + } + for path in mismatched { + on_mismatch(path.as_ref()); + } } } From 9569e27af5702274d16fbf42b4a42de7f988ec71 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 11:09:23 +0100 Subject: [PATCH 11/45] (add: many corrections) --- lib.md | 103 ++++++++++++++++++++++++++++++ src/core.rs | 3 +- src/core/path_getter.rs | 38 ----------- src/core/path_matcher.rs | 6 +- src/core/path_matcher/matcher.rs | 40 ++++++++---- src/core/path_matcher/matchers.rs | 79 ++++++++++++++++++----- src/core/path_matcher/sort.rs | 99 ++++++++++++++++++++++++++++ src/core/path_matcher/stats.rs | 10 +++ src/core/path_store.rs | 6 ++ src/core/path_store/context.rs | 51 +++++++++++++++ src/core/path_store/store.rs | 56 ++++++++++++++++ src/core/patterns_expand.rs | 48 ++++++++++++++ src/interfaces.rs | 1 + src/interfaces/cli.rs | 29 +++++++++ src/interfaces/cli/args.rs | 80 +++++++++++++++++++++++ src/interfaces/cli/engine.rs | 72 +++++++++++++++++++++ src/main.rs | 8 ++- 17 files changed, 656 insertions(+), 73 deletions(-) create mode 100644 lib.md delete mode 100644 src/core/path_getter.rs create mode 100644 src/core/path_matcher/sort.rs create mode 100644 src/core/path_matcher/stats.rs create mode 100644 src/core/path_store.rs create mode 100644 src/core/path_store/context.rs create mode 100644 src/core/path_store/store.rs create mode 100644 src/core/patterns_expand.rs create mode 100644 src/interfaces/cli.rs create mode 100644 src/interfaces/cli/args.rs create mode 100644 src/interfaces/cli/engine.rs 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/core.rs b/src/core.rs index b7e48e6..4b10ee3 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,3 +1,4 @@ pub mod path_class; -pub mod path_getter; +pub mod path_store; pub mod path_matcher; +pub mod patterns_expand; \ No newline at end of file diff --git a/src/core/path_getter.rs b/src/core/path_getter.rs deleted file mode 100644 index 43ae07b..0000000 --- a/src/core/path_getter.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::path::Path; -use walkdir::WalkDir; - -/// [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 get_paths>(dir_path: P) -> Vec { - let mut result = Vec::new(); - let root_path = dir_path.as_ref(); - - for entry in WalkDir::new(root_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(root_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('/'); - } - - result.push(final_path); - } - } - - result -} diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index 6675ce9..bcd16ab 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -1,9 +1,11 @@ /// [POL]: Główny moduł logiki dopasowywania ścieżek. /// [ENG]: Core module for path matching logic. pub mod matcher; -pub mod matcher_utils; pub mod matchers; +pub mod sort; +pub mod stats; pub use self::matcher::PathMatcher; -pub use self::matcher_utils::{expand_braces,SortStrategy,sort_paths}; pub use self::matchers::PathMatchers; +pub use self::sort::SortStrategy; +pub use self::stats::MatchStats; \ No newline at end of file diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index 892a158..07ec692 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -1,26 +1,34 @@ use regex::Regex; use std::collections::HashSet; -use super::matcher_utils::{SortStrategy, sort_paths}; +use super::sort::SortStrategy; +use super::stats::MatchStats; /// [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, - requires_sibling: bool, // [POL]: Flaga @ (para plik-folder) | [ENG]: Flag @ (file-directory pair) - requires_orphan: bool, // [POL]: Flaga $ (jednostronna relacja) | [ENG]: Flag $ (one-way relation) - is_deep: bool, // [POL]: Flaga + (rekurencyjne zacienianie) | [ENG]: Flag + (recursive shadowing) - base_name: String, // [POL]: Nazwa bazowa modułu do weryfikacji relacji | [ENG]: Base name of the module for relation verification + requires_sibling: bool, // [POL]: Flaga @ (para plik-folder) | [ENG]: Flag @ (file-directory pair) + requires_orphan: bool, // [POL]: Flaga $ (jednostronna relacja) | [ENG]: Flag $ (one-way relation) + is_deep: bool, // [POL]: Flaga + (rekurencyjne zacienianie) | [ENG]: Flag + (recursive shadowing) + base_name: String, // [POL]: Nazwa bazowa modułu do weryfikacji relacji | [ENG]: Base name of the module for relation verification + pub is_negated: bool, // [POL]: Flaga negacji (!). | [ENG]: Negation flag (!). } impl PathMatcher { pub fn new(pattern: &str, case_sensitive: bool) -> Result { // [POL]: Kompiluje wzorzec tekstowy do wyrażenia regularnego, ekstrahując flagi sterujące. // [ENG]: Compiles a text pattern into a regular expression, extracting control flags. - let is_deep = pattern.ends_with('+'); - let requires_sibling = pattern.contains('@'); - let requires_orphan = pattern.contains('$'); - let clean_pattern_str = pattern.replace('@', "").replace('$', "").replace('+', ""); + + // [POL]: Detekcja negacji. Jeśli obecny '!', oznaczamy i obcinamy go do dalszej analizy. + // [ENG]: Negation detection. If '!' is present, mark it and trim it for further analysis. + 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('@', "").replace('$', "").replace('+', ""); let base_name = clean_pattern_str .trim_end_matches('/') @@ -138,8 +146,9 @@ impl PathMatcher { targets_file, requires_sibling, requires_orphan, - is_deep, // 🆕 - base_name, // 🆕 + is_deep, + base_name, + is_negated, }) } @@ -212,9 +221,12 @@ impl PathMatcher { paths: I, env: &HashSet<&str>, strategy: SortStrategy, + show_include: bool, + show_exclude: bool, mut on_match: OnMatch, mut on_mismatch: OnMismatch, - ) where + ) + where I: IntoIterator, S: AsRef, OnMatch: FnMut(&str), @@ -231,8 +243,8 @@ impl PathMatcher { } } - sort_paths(&mut matched, strategy); - sort_paths(&mut mismatched, strategy); + strategy.apply(&mut matched); + strategy.apply(&mut mismatched); for path in matched { on_match(path.as_ref()); diff --git a/src/core/path_matcher/matchers.rs b/src/core/path_matcher/matchers.rs index 6f6f552..e6fae26 100644 --- a/src/core/path_matcher/matchers.rs +++ b/src/core/path_matcher/matchers.rs @@ -1,7 +1,9 @@ use super::matcher::PathMatcher; -use super::matcher_utils::{expand_braces, SortStrategy, sort_paths}; +use super::sort::SortStrategy; +use super::stats::MatchStats; use std::collections::HashSet; + /// [POL]: Kontener przechowujący kolekcję silników dopasowujących ścieżki. /// [ENG]: A container holding a collection of path matching engines. pub struct PathMatchers { @@ -17,27 +19,51 @@ impl PathMatchers { S: AsRef, { let mut matchers = Vec::new(); - for pat in patterns { + //for pat in patterns { // [POL]: Przetwarzanie wstępne wzorca (Brace Expansion). // [ENG]: Pattern preprocessing (Brace Expansion). - let expanded_patterns = expand_braces(pat.as_ref()); - - for expanded_pat in expanded_patterns { - matchers.push(PathMatcher::new(&expanded_pat, case_sensitive)?); - } + // let expanded_patterns = expand_braces(pat.as_ref()); + for pat in patterns { + matchers.push(PathMatcher::new(pat.as_ref(), case_sensitive)?); } + //} Ok(Self { matchers }) } /// [POL]: Weryfikuje, czy ścieżka pasuje do dowolnego ze skonfigurowanych wzorców (logika OR). /// [ENG]: Verifies if the path matches any of the configured patterns (OR logic). + /// [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_match(path, env) { - return true; + 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; + } } } - false + + // [POL]: Ostateczna decyzja na podstawie zebranych danych. + // [ENG]: Final decision based on collected data. + if has_positive { + matched_positive // Zwykłe dopasowanie pozytywne (OR) + } else { + true // Jeśli użytkownik podał TYLKO wzorce z '!' (np. "!tests/"), domyślnie akceptujemy resztę. + } } /// [POL]: Ewaluuje zbiór ścieżek, sortuje je i wykonuje odpowiednie domknięcia. @@ -47,9 +73,12 @@ impl PathMatchers { paths: I, env: &HashSet<&str>, strategy: SortStrategy, + show_include: bool, + show_exclude: bool, mut on_match: OnMatch, mut on_mismatch: OnMismatch, - ) where + ) -> MatchStats + where I: IntoIterator, S: AsRef, OnMatch: FnMut(&str), @@ -66,14 +95,30 @@ impl PathMatchers { } } - sort_paths(&mut matched, strategy); - sort_paths(&mut mismatched, strategy); + strategy.apply(&mut matched); + strategy.apply(&mut mismatched); + + let stats = MatchStats { + matched: matched.len(), + rejected: mismatched.len(), + total: matched.len() + mismatched.len(), + included: matched.iter().map(|s| s.as_ref().to_string()).collect(), + excluded: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + }; + - for path in matched { - on_match(path.as_ref()); + if show_include { + for path in matched { + on_match(path.as_ref()); + } } - for path in mismatched { - on_mismatch(path.as_ref()); + + if show_exclude { + 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..4129baf --- /dev/null +++ b/src/core/path_matcher/sort.rs @@ -0,0 +1,99 @@ +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('.') { + if idx > 0 && trimmed.as_bytes()[idx - 1] != b'/' { + return &trimmed[..idx]; + } + } + trimmed + } +} \ No newline at end of file diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs new file mode 100644 index 0000000..2758ba0 --- /dev/null +++ b/src/core/path_matcher/stats.rs @@ -0,0 +1,10 @@ +// [EN]: Simple stats object to avoid manual counting in the Engine. +// [PL]: Prosty obiekt statystyk, aby uniknąć ręcznego liczenia w Engine. +#[derive(Debug, Default, Clone)] +pub struct MatchStats { + pub matched: usize, + pub rejected: usize, + pub total: usize, + pub included: Vec, + pub excluded: Vec, +} \ No newline at end of file diff --git a/src/core/path_store.rs b/src/core/path_store.rs new file mode 100644 index 0000000..5507580 --- /dev/null +++ b/src/core/path_store.rs @@ -0,0 +1,6 @@ +pub mod store; +pub mod context; + + +pub use self::store::PathStore; +pub use self::context::PathContext; diff --git a/src/core/path_store/context.rs b/src/core/path_store/context.rs new file mode 100644 index 0000000..3cac3db --- /dev/null +++ b/src/core/path_store/context.rs @@ -0,0 +1,51 @@ +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, + }) + } +} \ No newline at end of file diff --git a/src/core/path_store/store.rs b/src/core/path_store/store.rs new file mode 100644 index 0000000..97bee50 --- /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 } + } + + // [EN]: Creates a temporary pool of references for the matcher. + // [PL]: 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/patterns_expand.rs b/src/core/patterns_expand.rs new file mode 100644 index 0000000..c927002 --- /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('}')) { + if 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()] + } +} \ No newline at end of file diff --git a/src/interfaces.rs b/src/interfaces.rs index 5c5f238..7439b85 100644 --- a/src/interfaces.rs +++ b/src/interfaces.rs @@ -2,3 +2,4 @@ // [PL]: Warstwa interakcji z użytkownikiem (Porty i Adaptery). pub mod tui; +pub mod cli; diff --git a/src/interfaces/cli.rs b/src/interfaces/cli.rs new file mode 100644 index 0000000..79e5b55 --- /dev/null +++ b/src/interfaces/cli.rs @@ -0,0 +1,29 @@ +pub mod args; +pub mod engine; + +use clap::Parser; +use self::args::CargoCli; + +// [EN]: Main entry point for the CLI interface. +// [PL]: Główny punkt wejścia dla interfejsu CLI. +pub fn run_cli() { + // [PL]: Pobieramy surowe argumenty bezpośrednio z systemu. + let args_os = std::env::args(); + let mut args: Vec = args_os.collect(); + + // [EN]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. + // We insert it manually so the parser matches the Cargo plugin structure. + // [PL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. + // Wstawiamy go ręcznie, aby parser pasował do struktury wtyczki Cargo. + if args.len() > 1 && args[1] != "plot" { + args.insert(1, "plot".to_string()); + } + + // [EN]: Now parse from the modified list. + // [PL]: Teraz parsujemy ze zmodyfikowanej listy. + let CargoCli::Plot(flags) = CargoCli::parse_from(args); + + // [EN]: Transfer control to our execution engine. + // [PL]: Przekazanie kontroli do naszego silnika wykonawczego. + engine::run(flags); +} diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs new file mode 100644 index 0000000..4ab5627 --- /dev/null +++ b/src/interfaces/cli/args.rs @@ -0,0 +1,80 @@ +use clap::{Parser, Args, ValueEnum}; +use cargo_plot::core::path_matcher::SortStrategy; + +/// [POL]: Główny wrapper dla wtyczki Cargo. +/// Oszukuje clap'a, mówiąc mu: "Główny program nazywa się 'cargo', a 'plot' to jego subkomenda". +#[derive(Parser, Debug)] +#[command(name = "cargo", bin_name = "cargo")] +pub enum CargoCli { + /// [EN]: Cargo plot subcommand. + /// [PL]: Podkomenda cargo plot. + Plot(CliArgs), +} + +/// [POL]: Nasze docelowe argumenty CLI. Zauważ, że teraz to jest `Args`, a nie `Parser`. +#[derive(Args, Debug)] +#[command(author, version, about = "Zaawansowany skaner struktury plików Rusta", long_about = None)] +pub struct CliArgs { + /// [EN]: Input path to scan. + /// [PL]: Ścieżka wejściowa do skanowania. + #[arg(short = 'd', long = "dir", default_value = ".")] + pub enter_path: String, + + /// [EN]: Match patterns. + /// [PL]: Wzorce dopasowań. + #[arg(short = 'p', long = "pat", required = true)] + pub patterns: Vec, + + /// [EN]: Display only matched paths. + /// [PL]: Wyświetlaj tylko dopasowane ścieżki. + #[arg(long)] + pub include: bool, + + /// [EN]: Display only rejected paths. + /// [PL]: Wyświetlaj tylko odrzucone ścieżki. + #[arg(long)] + pub exclude: bool, + + /// [EN]: Ignore case. + /// [PL]: Ignoruj wielkość liter. + #[arg(short = 'i', long = "ignore-case")] + pub ignore_case: bool, + + /// [EN]: Results sorting strategy. + /// [PL]: Strategia sortowania wyników. + #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] + pub sort: CliSortStrategy, +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum CliSortStrategy { + None, + Az, + Za, + AzFile, + ZaFile, + AzDir, + ZaDir, + AzFileMerge, + ZaFileMerge, + AzDirMerge, + ZaDirMerge, +} + +impl Into for CliSortStrategy { + fn into(self) -> SortStrategy { + match self { + 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, + } + } +} \ No newline at end of file diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs new file mode 100644 index 0000000..b98da57 --- /dev/null +++ b/src/interfaces/cli/engine.rs @@ -0,0 +1,72 @@ +use std::path::Path; +use crate::interfaces::cli::args::CliArgs; + +// [EN]: Imports from our library core. +// [PL]: Importy z rdzenia naszej biblioteki. +use cargo_plot::core::path_class::get_icon_for_path; +use cargo_plot::core::path_store::{PathContext, PathStore}; +use cargo_plot::core::path_matcher::{PathMatchers, SortStrategy}; +use cargo_plot::core::patterns_expand::PatternContext; + +/// [EN]: The execution engine (Cockpit). +/// [PL]: Silnik wykonawczy (Kokpit). +pub fn run(args: CliArgs) { + let mut show_include = args.include; + let mut show_exclude = args.exclude; + if !show_include && !show_exclude { + show_include = true; + show_exclude = true; + } + + let is_case_sensitive = !args.ignore_case; + let sort_strategy: SortStrategy = args.sort.into(); + let pattern_ctx = PatternContext::new(&args.patterns); + let path_ctx = PathContext::resolve(&args.enter_path).unwrap_or_else(|e| { + eprintln!("❌ {}", e); + std::process::exit(1); + }); + + println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); + println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); + println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); + println!("---------------------------------------"); + println!("🔠 Wrażliwość na litery: {}", is_case_sensitive); + println!("🔍 Wzorce (RAW): {:?}", pattern_ctx.raw); + println!("⚙️ Wzorce (TOK): {:?}", pattern_ctx.tok); + println!("---------------------------------------"); + + //let path_obj = Path::new(&args.enter_path); + //if path_obj.is_relative() { + // let abs_path = resolve_absolute_path(&args.enter_path) + // .unwrap_or_else(|| "[Nie można ustalić ścieżki]".to_string()); + // + // println!("📂 Ścieżka wejściowa: {} (Absolutna: {})", args.enter_path, abs_path); + //} else { + // println!("📂 Ścieżka wejściowa: {}", args.enter_path); + //} + + + + let matchers = PathMatchers::new(&pattern_ctx.tok, is_case_sensitive) + .expect("Błąd kompilacji wzorców"); + + + // [PL]: Ładujemy dane do rejestru z rdzenia + let paths_store = PathStore::scan(&path_ctx.entry_absolute); + // [PL]: Wyciągamy PULĘ ŚCIEŻEK + let paths_set = paths_store.get_index(); + + let stats = matchers.evaluate( + &paths_store.list, + &paths_set, + sort_strategy, + show_include, + show_exclude, + |path| println!("✅ MATCH: {} {}", get_icon_for_path(path), path), + |path| println!("❌ REJECT: {} {}", get_icon_for_path(path), path), + ); + + println!("----------"); + println!("📊 Podsumowanie: Dopasowano {} z {} ścieżek.", stats.matched, stats.total); + println!("📊 Podsumowanie: Odrzucono {} z {} ścieżek.", stats.rejected, stats.total); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 74c4d29..121f50d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,9 @@ -use std::env; +// [EN]: Main entry point switching between interactive TUI and automated CLI. +// [PL]: Główny punkt wejścia przełączający między interaktywnym TUI a automatycznym CLI. + +#![allow(clippy::pedantic, clippy::struct_excessive_bools)] +use std::env; mod interfaces; fn main() { @@ -13,4 +17,6 @@ fn main() { interfaces::tui::run_tui(); // return; } + + interfaces::cli::run_cli(); } From 84c10a3e993938baacd7492d30e633f98171c084 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 11:13:57 +0100 Subject: [PATCH 12/45] (fix) --- src/core/path_matcher/matcher.rs | 25 ++++++++++++++++++++----- src/core/path_matcher/matcher_utils.rs | 20 +------------------- src/interfaces/cli/engine.rs | 1 - 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index 07ec692..e53cf3e 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -225,7 +225,7 @@ impl PathMatcher { show_exclude: bool, mut on_match: OnMatch, mut on_mismatch: OnMismatch, - ) + ) -> MatchStats where I: IntoIterator, S: AsRef, @@ -246,12 +246,27 @@ impl PathMatcher { strategy.apply(&mut matched); strategy.apply(&mut mismatched); - for path in matched { - on_match(path.as_ref()); + let stats = MatchStats { + matched: matched.len(), + rejected: mismatched.len(), + total: matched.len() + mismatched.len(), + included: matched.iter().map(|s| s.as_ref().to_string()).collect(), + excluded: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + }; + + if show_include { + for path in &matched { + on_match(path.as_ref()); + } } - for path in mismatched { - on_mismatch(path.as_ref()); + + if show_exclude { + for path in &mismatched { + on_mismatch(path.as_ref()); + } } + + stats } /// [POL]: Weryfikuje autoryzację korzenia modułu w relacji plik-folder dla trybu 'deep'. diff --git a/src/core/path_matcher/matcher_utils.rs b/src/core/path_matcher/matcher_utils.rs index 67132f5..ddd7186 100644 --- a/src/core/path_matcher/matcher_utils.rs +++ b/src/core/path_matcher/matcher_utils.rs @@ -1,28 +1,10 @@ use std::cmp::Ordering; -/// [POL]: Wykonuje rozwinięcie klamer we wzorcu (np. {a,b} -> [a, b]). Obsługuje rekurencję. -/// [ENG]: Performs brace expansion in the pattern (e.g. {a,b} -> [a, b]). Supports recursion. -pub fn expand_braces(pattern: &str) -> Vec { - if let (Some(start), Some(end)) = (pattern.find('{'), pattern.find('}')) { - if start < end { - let prefix = &pattern[..start]; - let suffix = &pattern[end + 1..]; - let options = &pattern[start + 1..end]; - - let mut expanded = Vec::new(); - for opt in options.split(',') { - let new_pattern = format!("{}{}{}", prefix, opt, suffix); - expanded.extend(expand_braces(&new_pattern)); - } - return expanded; - } - } - vec![pattern.to_string()] -} /// [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. diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index b98da57..4376833 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,4 +1,3 @@ -use std::path::Path; use crate::interfaces::cli::args::CliArgs; // [EN]: Imports from our library core. From 3360648e619efab256ba851283aefa56f9b1e071 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 12:09:49 +0100 Subject: [PATCH 13/45] (finish: the basics) --- examples/skaner.rs | 84 --------------- src/core.rs | 5 +- src/core/path_matcher.rs | 2 +- src/core/path_matcher/matcher.rs | 27 ++--- src/core/path_matcher/matcher_utils.rs | 102 ------------------ src/core/path_matcher/matchers.rs | 10 +- src/core/path_matcher/sort.rs | 21 ++-- src/core/path_matcher/stats.rs | 6 +- src/core/path_store.rs | 5 +- src/core/path_store/context.rs | 16 ++- src/core/path_store/store.rs | 2 +- src/core/patterns_expand.rs | 2 +- src/execuctor.rs | 2 + src/execuctor/for_many_patterns_tok.rs | 65 +++++++++++ src/execuctor/for_one_pattern_raw.rs | 61 +++++++++++ src/interfaces.rs | 2 +- src/interfaces/cli.rs | 4 +- src/interfaces/cli/args.rs | 6 +- src/interfaces/cli/engine.rs | 71 ++++-------- src/lib.rs | 2 + src/theme.rs | 1 + .../path_class.rs => theme/for_path_list.rs} | 0 22 files changed, 211 insertions(+), 285 deletions(-) delete mode 100644 examples/skaner.rs delete mode 100644 src/core/path_matcher/matcher_utils.rs create mode 100644 src/execuctor.rs create mode 100644 src/execuctor/for_many_patterns_tok.rs create mode 100644 src/execuctor/for_one_pattern_raw.rs create mode 100644 src/theme.rs rename src/{core/path_class.rs => theme/for_path_list.rs} (100%) diff --git a/examples/skaner.rs b/examples/skaner.rs deleted file mode 100644 index 0a493b9..0000000 --- a/examples/skaner.rs +++ /dev/null @@ -1,84 +0,0 @@ -use cargo_plot::core::path_class::get_icon_for_path; -use cargo_plot::core::path_getter::get_paths; -use cargo_plot::core::path_matcher::{/*PathMatcher,*/ PathMatchers, expand_braces,SortStrategy}; -use std::collections::HashSet; -use std::env; -use std::process; - -/// Wyciąga flagi `-x` z argumentów wywołania. -/// Jeśli nie znajdzie żadnych wzorców, wypisuje instrukcję i kończy program. -fn get_patterns_from_cli() -> Vec { - let args: Vec = env::args().collect(); - let mut patterns = Vec::new(); - - let mut i = 1; - while i < args.len() { - if args[i] == "-x" && i + 1 < args.len() { - patterns.push(args[i + 1].clone()); - i += 2; - } else { - i += 1; - } - } - - if patterns.is_empty() { - eprintln!("⚠️ Nie podano żadnych wzorców!"); - eprintln!("💡 Użycie: cargo run --example skaner -- -x \"src/\" -x \"src/**\""); - process::exit(1); // Brutalne przerwanie programu (z kodem błędu 1) - } - - patterns -} - -fn main() { - // 1. ODCZYT ARGUMENTÓW Z KONSOLI - let patterns_raw = get_patterns_from_cli(); - println!("🔍 Wzorce wejściowe (RAW): {:?}", patterns_raw); - - // 🔴 NOWOŚĆ: Przepuszczamy wzorce przez middleware w celach wizualnych - let mut patterns_tok = Vec::new(); - for pat in &patterns_raw { - patterns_tok.extend(expand_braces(pat)); - } - println!("⚙️ Wzorce po middleware (TOK): {:?}", patterns_tok); - - let is_case_sensitive = false; - let matchers = - PathMatchers::new(&patterns_raw, is_case_sensitive).expect("Błąd kompilacji wzorców"); - // let paths_to_test: Vec<&str> = include!("data.rs"); - let paths_to_test = get_paths("./src"); - // let wycinek = &paths_to_test[..std::cmp::min(25, paths_to_test.len())]; - // println!("{:#?}", wycinek); - // for path in paths_to_test.iter().take(25) { - // println!("{}", path); - // } - - // 🔴 NOWOŚĆ: Budujemy mapę środowiska z naszej listy ścieżek. - // Używamy .copied(), bo elements w Vec<&str> to &str, a chcemy mieć HashSet<&str> - let environment: HashSet<&str> = paths_to_test.iter().map(|s| s.as_str()).collect(); - - let mut dopasowane = 0; - let total = paths_to_test.len(); - - // Ewaluacja - matchers.evaluate( - &paths_to_test, - &environment, // 🔴 NOWOŚĆ: Wstrzykujemy środowisko do silnika! - SortStrategy::AzFileFirstMerge, - |path| { - // 🔥 Używamy naszej nowej, czystej funkcji! - let icon = get_icon_for_path(path); - println!("✅ MATCH: {} {}", icon, path); - dopasowane += 1; - }, - |_path| { - // Miejsce na logikę dla odrzuconych ścieżek - }, - ); - - println!("----------"); - println!( - "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", - dopasowane, total - ); -} diff --git a/src/core.rs b/src/core.rs index 4b10ee3..d0d42fc 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,4 +1,3 @@ -pub mod path_class; -pub mod path_store; pub mod path_matcher; -pub mod patterns_expand; \ No newline at end of file +pub mod path_store; +pub mod patterns_expand; diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index bcd16ab..cde63cd 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -8,4 +8,4 @@ pub mod stats; pub use self::matcher::PathMatcher; pub use self::matchers::PathMatchers; pub use self::sort::SortStrategy; -pub use self::stats::MatchStats; \ No newline at end of file +pub use self::stats::MatchStats; diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index e53cf3e..f0b8a58 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -1,7 +1,7 @@ +use super::sort::SortStrategy; +use super::stats::MatchStats; use regex::Regex; use std::collections::HashSet; -use super::sort::SortStrategy; -use super::stats::MatchStats; /// [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. @@ -9,17 +9,17 @@ pub struct PathMatcher { regex: Regex, targets_file: bool, requires_sibling: bool, // [POL]: Flaga @ (para plik-folder) | [ENG]: Flag @ (file-directory pair) - requires_orphan: bool, // [POL]: Flaga $ (jednostronna relacja) | [ENG]: Flag $ (one-way relation) - is_deep: bool, // [POL]: Flaga + (rekurencyjne zacienianie) | [ENG]: Flag + (recursive shadowing) - base_name: String, // [POL]: Nazwa bazowa modułu do weryfikacji relacji | [ENG]: Base name of the module for relation verification - pub is_negated: bool, // [POL]: Flaga negacji (!). | [ENG]: Negation flag (!). + requires_orphan: bool, // [POL]: Flaga $ (jednostronna relacja) | [ENG]: Flag $ (one-way relation) + is_deep: bool, // [POL]: Flaga + (rekurencyjne zacienianie) | [ENG]: Flag + (recursive shadowing) + base_name: String, // [POL]: Nazwa bazowa modułu do weryfikacji relacji | [ENG]: Base name of the module for relation verification + pub is_negated: bool, // [POL]: Flaga negacji (!). | [ENG]: Negation flag (!). } impl PathMatcher { - pub fn new(pattern: &str, case_sensitive: bool) -> Result { + pub fn new(pattern: &str, case_sensitive: bool) -> Result { // [POL]: Kompiluje wzorzec tekstowy do wyrażenia regularnego, ekstrahując flagi sterujące. // [ENG]: Compiles a text pattern into a regular expression, extracting control flags. - + // [POL]: Detekcja negacji. Jeśli obecny '!', oznaczamy i obcinamy go do dalszej analizy. // [ENG]: Negation detection. If '!' is present, mark it and trim it for further analysis. let is_negated = pattern.starts_with('!'); @@ -28,7 +28,10 @@ impl PathMatcher { 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('@', "").replace('$', "").replace('+', ""); + let clean_pattern_str = actual_pattern + .replace('@', "") + .replace('$', "") + .replace('+', ""); let base_name = clean_pattern_str .trim_end_matches('/') @@ -146,8 +149,8 @@ impl PathMatcher { targets_file, requires_sibling, requires_orphan, - is_deep, - base_name, + is_deep, + base_name, is_negated, }) } @@ -259,7 +262,7 @@ impl PathMatcher { on_match(path.as_ref()); } } - + if show_exclude { for path in &mismatched { on_mismatch(path.as_ref()); diff --git a/src/core/path_matcher/matcher_utils.rs b/src/core/path_matcher/matcher_utils.rs deleted file mode 100644 index ddd7186..0000000 --- a/src/core/path_matcher/matcher_utils.rs +++ /dev/null @@ -1,102 +0,0 @@ -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, -} - -/// [POL]: Ekstrahuje rdzenną nazwę ścieżki dla strategii Merge (np. usuwa rozszerzenia plików). -/// [ENG]: Extracts the core path name for Merge strategies (e.g. removes file extensions). -fn get_merge_key(path: &str) -> &str { - let trimmed = path.trim_end_matches('/'); - if let Some(idx) = trimmed.rfind('.') { - // [POL]: Ochrona przed usunięciem nazw plików ukrytych (np. ".env") - // [ENG]: Protection against stripping hidden file names (e.g. ".env") - if idx > 0 && trimmed.as_bytes()[idx - 1] != b'/' { - return &trimmed[..idx]; - } - } - trimmed -} - -/// [POL]: Sortuje kolekcję ścieżek na podstawie wybranej strategii. -/// [ENG]: Sorts a collection of paths based on the selected strategy. -pub fn sort_paths>(paths: &mut Vec, strategy: SortStrategy) { - if strategy == 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('/'); - - let a_merge = get_merge_key(a); - let b_merge = get_merge_key(b); - - match strategy { - 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)), - } - }); -} - - diff --git a/src/core/path_matcher/matchers.rs b/src/core/path_matcher/matchers.rs index e6fae26..79d6411 100644 --- a/src/core/path_matcher/matchers.rs +++ b/src/core/path_matcher/matchers.rs @@ -1,9 +1,8 @@ use super::matcher::PathMatcher; -use super::sort::SortStrategy; +use super::sort::SortStrategy; use super::stats::MatchStats; use std::collections::HashSet; - /// [POL]: Kontener przechowujący kolekcję silników dopasowujących ścieżki. /// [ENG]: A container holding a collection of path matching engines. pub struct PathMatchers { @@ -20,9 +19,9 @@ impl PathMatchers { { let mut matchers = Vec::new(); //for pat in patterns { - // [POL]: Przetwarzanie wstępne wzorca (Brace Expansion). - // [ENG]: Pattern preprocessing (Brace Expansion). - // let expanded_patterns = expand_braces(pat.as_ref()); + // [POL]: Przetwarzanie wstępne wzorca (Brace Expansion). + // [ENG]: Pattern preprocessing (Brace Expansion). + // let expanded_patterns = expand_braces(pat.as_ref()); for pat in patterns { matchers.push(PathMatcher::new(pat.as_ref(), case_sensitive)?); } @@ -106,7 +105,6 @@ impl PathMatchers { excluded: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), }; - if show_include { for path in matched { on_match(path.as_ref()); diff --git a/src/core/path_matcher/sort.rs b/src/core/path_matcher/sort.rs index 4129baf..0c7157c 100644 --- a/src/core/path_matcher/sort.rs +++ b/src/core/path_matcher/sort.rs @@ -32,7 +32,6 @@ pub enum SortStrategy { /// [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, @@ -51,7 +50,7 @@ pub enum SortStrategy { } impl SortStrategy { - /// [POL]: Sortuje kolekcję ścieżek w miejscu (in-place) na podstawie wybranej strategii. + /// [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 { @@ -77,10 +76,18 @@ impl SortStrategy { 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)), + 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)) + } } }); } @@ -96,4 +103,4 @@ impl SortStrategy { } trimmed } -} \ No newline at end of file +} diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs index 2758ba0..1910f48 100644 --- a/src/core/path_matcher/stats.rs +++ b/src/core/path_matcher/stats.rs @@ -5,6 +5,6 @@ pub struct MatchStats { pub matched: usize, pub rejected: usize, pub total: usize, - pub included: Vec, - pub excluded: Vec, -} \ No newline at end of file + pub included: Vec, + pub excluded: Vec, +} diff --git a/src/core/path_store.rs b/src/core/path_store.rs index 5507580..17099a4 100644 --- a/src/core/path_store.rs +++ b/src/core/path_store.rs @@ -1,6 +1,5 @@ -pub mod store; pub mod context; +pub mod store; - -pub use self::store::PathStore; 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 index 3cac3db..917f345 100644 --- a/src/core/path_store/context.rs +++ b/src/core/path_store/context.rs @@ -17,12 +17,18 @@ impl PathContext { // 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('\\', "/"); + 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('\\', "/"); + 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) { @@ -35,8 +41,8 @@ impl PathContext { } } Err(_) => { - // Jeśli cel jest na innym dysku (np. C:\ a terminal na D:\) - // lub całkiem poza strukturą CWD, relatywna nie istnieje. + // 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('\\', "/") } @@ -48,4 +54,4 @@ impl PathContext { entry_relative: entry_rel, }) } -} \ No newline at end of file +} diff --git a/src/core/path_store/store.rs b/src/core/path_store/store.rs index 97bee50..1186afb 100644 --- a/src/core/path_store/store.rs +++ b/src/core/path_store/store.rs @@ -53,4 +53,4 @@ impl PathStore { pub fn get_index(&self) -> HashSet<&str> { self.list.iter().map(|s| s.as_str()).collect() } -} +} diff --git a/src/core/patterns_expand.rs b/src/core/patterns_expand.rs index c927002..81c7192 100644 --- a/src/core/patterns_expand.rs +++ b/src/core/patterns_expand.rs @@ -45,4 +45,4 @@ impl PatternContext { } vec![pattern.to_string()] } -} \ No newline at end of file +} diff --git a/src/execuctor.rs b/src/execuctor.rs new file mode 100644 index 0000000..e691450 --- /dev/null +++ b/src/execuctor.rs @@ -0,0 +1,2 @@ +pub mod for_many_patterns_tok; +pub mod for_one_pattern_raw; diff --git a/src/execuctor/for_many_patterns_tok.rs b/src/execuctor/for_many_patterns_tok.rs new file mode 100644 index 0000000..9f5df89 --- /dev/null +++ b/src/execuctor/for_many_patterns_tok.rs @@ -0,0 +1,65 @@ +use crate::core::path_matcher::matchers::PathMatchers; +use crate::core::path_matcher::stats::MatchStats; +use crate::core::path_store::context::PathContext; +use crate::core::path_store::store::PathStore; +use crate::core::patterns_expand::PatternContext; +// [PL]: Reeksportujemy strategię, aby Kokpit nie musiał szukać jej w core. +pub use crate::core::path_matcher::sort::SortStrategy; + +/// [POL]: Egzekutor operujący na wielu wzorcach (wersja po rozwinięciu klamer/tokenizacji). +/// [ENG]: Executor operating on multiple patterns (post brace expansion/tokenisation). +pub fn execute( + enter_path: &str, + patterns: &[String], + is_case_sensitive: bool, + sort_strategy: SortStrategy, + show_include: bool, + show_exclude: bool, + on_match: OnMatch, + on_mismatch: OnMismatch, +) -> MatchStats +where + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), +{ + // 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); + }); + + // 2. Logowanie stanu początkowego + println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); + println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); + println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); + println!("---------------------------------------"); + println!("🔠 Wrażliwość na litery: {}", is_case_sensitive); + println!("🔍 Wzorce (RAW): {:?}", pattern_ctx.raw); + println!("⚙️ Wzorce (TOK): {:?}", pattern_ctx.tok); + println!("---------------------------------------"); + + // 3. Budowa silników dopasowujących (Generał) + let matchers = + PathMatchers::new(&pattern_ctx.tok, is_case_sensitive).expect("Błąd kompilacji wzorców"); + + // 4. Skanowanie dysku (Getter) + // [PL]: Ładujemy dane do rejestru z rdzenia + let paths_store = PathStore::scan(&path_ctx.entry_absolute); + // [PL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) + let paths_set = paths_store.get_index(); + + // 5. Ewaluacja i wykonanie callbacków + let stats = matchers.evaluate( + &paths_store.list, + &paths_set, + sort_strategy, + show_include, + show_exclude, + on_match, + on_mismatch, + ); + + // 6. Zwracamy statystyki do Engine'u + stats +} diff --git a/src/execuctor/for_one_pattern_raw.rs b/src/execuctor/for_one_pattern_raw.rs new file mode 100644 index 0000000..d29b83a --- /dev/null +++ b/src/execuctor/for_one_pattern_raw.rs @@ -0,0 +1,61 @@ +use crate::core::path_matcher::matcher::PathMatcher; +use crate::core::path_matcher::stats::MatchStats; +use crate::core::path_store::PathStore; +use crate::core::path_store::context::PathContext; +// [PL]: Reeksportujemy strategię, aby Kokpit nie musiał szukać jej w core. +pub use crate::core::path_matcher::sort::SortStrategy; + +/// [POL]: Egzekutor operujący na pojedynczym, surowym wzorcu wpisanym przez użytkownika. +/// [ENG]: Executor operating on a single, raw pattern provided by the user. +pub fn execute( + enter_path: &str, + raw_pattern: &str, + is_case_sensitive: bool, + sort_strategy: SortStrategy, + show_include: bool, + show_exclude: bool, + on_match: OnMatch, + on_mismatch: OnMismatch, +) -> MatchStats +where + OnMatch: FnMut(&str), + OnMismatch: FnMut(&str), +{ + // 1. Inicjalizacja kontekstów + let path_ctx = PathContext::resolve(enter_path).unwrap_or_else(|e| { + eprintln!("❌ {}", e); + std::process::exit(1); + }); + + // 2. Logowanie stanu początkowego + println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); + println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); + println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); + println!("---------------------------------------"); + println!("🔠 Wrażliwość na litery: {}", is_case_sensitive); + println!("🔍 Wzorzec (RAW): {:?}", raw_pattern); + println!("---------------------------------------"); + + // 3. Budowa silników dopasowujących (Generał) + let matcher = PathMatcher::new(raw_pattern, is_case_sensitive).expect("Błąd kompilacji wzorca"); + + // 4. Skanowanie dysku (Getter) + // [PL]: Ładujemy dane do rejestru z rdzenia + let paths_store = PathStore::scan(&path_ctx.entry_absolute); + // [PL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) + let paths_set = paths_store.get_index(); + + // 5. Ewaluacja i wykonanie callbacków + let stats = matcher.evaluate( + &paths_store.list, + &paths_set, + sort_strategy, + show_include, + show_exclude, + on_match, + on_mismatch, + ); + + // 6. Zwracamy statystyki do Engine'u + stats +} diff --git a/src/interfaces.rs b/src/interfaces.rs index 7439b85..e672806 100644 --- a/src/interfaces.rs +++ b/src/interfaces.rs @@ -1,5 +1,5 @@ // [EN]: User interaction layer (Ports and Adapters). // [PL]: Warstwa interakcji z użytkownikiem (Porty i Adaptery). -pub mod tui; pub mod cli; +pub mod tui; diff --git a/src/interfaces/cli.rs b/src/interfaces/cli.rs index 79e5b55..51adb29 100644 --- a/src/interfaces/cli.rs +++ b/src/interfaces/cli.rs @@ -1,8 +1,8 @@ pub mod args; pub mod engine; -use clap::Parser; use self::args::CargoCli; +use clap::Parser; // [EN]: Main entry point for the CLI interface. // [PL]: Główny punkt wejścia dla interfejsu CLI. @@ -11,7 +11,7 @@ pub fn run_cli() { let args_os = std::env::args(); let mut args: Vec = args_os.collect(); - // [EN]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. + // [EN]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. // We insert it manually so the parser matches the Cargo plugin structure. // [PL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. // Wstawiamy go ręcznie, aby parser pasował do struktury wtyczki Cargo. diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 4ab5627..2903ceb 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -1,7 +1,7 @@ -use clap::{Parser, Args, ValueEnum}; use cargo_plot::core::path_matcher::SortStrategy; +use clap::{Args, Parser, ValueEnum}; -/// [POL]: Główny wrapper dla wtyczki Cargo. +/// [POL]: Główny wrapper dla wtyczki Cargo. /// Oszukuje clap'a, mówiąc mu: "Główny program nazywa się 'cargo', a 'plot' to jego subkomenda". #[derive(Parser, Debug)] #[command(name = "cargo", bin_name = "cargo")] @@ -77,4 +77,4 @@ impl Into for CliSortStrategy { CliSortStrategy::ZaDirMerge => SortStrategy::ZaDirFirstMerge, } } -} \ No newline at end of file +} diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 4376833..d26d9db 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,63 +1,26 @@ use crate::interfaces::cli::args::CliArgs; - -// [EN]: Imports from our library core. -// [PL]: Importy z rdzenia naszej biblioteki. -use cargo_plot::core::path_class::get_icon_for_path; -use cargo_plot::core::path_store::{PathContext, PathStore}; -use cargo_plot::core::path_matcher::{PathMatchers, SortStrategy}; -use cargo_plot::core::patterns_expand::PatternContext; +use cargo_plot::execuctor::for_many_patterns_tok::{self, SortStrategy}; +use cargo_plot::theme::for_path_list::get_icon_for_path; +// lub dla jednego wzorca: +// use cargo_plot::execuctor::for_one_pattern_raw; /// [EN]: The execution engine (Cockpit). /// [PL]: Silnik wykonawczy (Kokpit). pub fn run(args: CliArgs) { + let is_case_sensitive = !args.ignore_case; + let sort_strategy: SortStrategy = args.sort.into(); + let mut show_include = args.include; let mut show_exclude = args.exclude; if !show_include && !show_exclude { show_include = true; show_exclude = true; } - - let is_case_sensitive = !args.ignore_case; - let sort_strategy: SortStrategy = args.sort.into(); - let pattern_ctx = PatternContext::new(&args.patterns); - let path_ctx = PathContext::resolve(&args.enter_path).unwrap_or_else(|e| { - eprintln!("❌ {}", e); - std::process::exit(1); - }); - - println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); - println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); - println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); - println!("---------------------------------------"); - println!("🔠 Wrażliwość na litery: {}", is_case_sensitive); - println!("🔍 Wzorce (RAW): {:?}", pattern_ctx.raw); - println!("⚙️ Wzorce (TOK): {:?}", pattern_ctx.tok); - println!("---------------------------------------"); - - //let path_obj = Path::new(&args.enter_path); - //if path_obj.is_relative() { - // let abs_path = resolve_absolute_path(&args.enter_path) - // .unwrap_or_else(|| "[Nie można ustalić ścieżki]".to_string()); - // - // println!("📂 Ścieżka wejściowa: {} (Absolutna: {})", args.enter_path, abs_path); - //} else { - // println!("📂 Ścieżka wejściowa: {}", args.enter_path); - //} - - - let matchers = PathMatchers::new(&pattern_ctx.tok, is_case_sensitive) - .expect("Błąd kompilacji wzorców"); - - - // [PL]: Ładujemy dane do rejestru z rdzenia - let paths_store = PathStore::scan(&path_ctx.entry_absolute); - // [PL]: Wyciągamy PULĘ ŚCIEŻEK - let paths_set = paths_store.get_index(); - - let stats = matchers.evaluate( - &paths_store.list, - &paths_set, + let stats = for_many_patterns_tok::execute( + &args.enter_path, + &args.patterns, + is_case_sensitive, sort_strategy, show_include, show_exclude, @@ -66,6 +29,12 @@ pub fn run(args: CliArgs) { ); println!("----------"); - println!("📊 Podsumowanie: Dopasowano {} z {} ścieżek.", stats.matched, stats.total); - println!("📊 Podsumowanie: Odrzucono {} z {} ścieżek.", stats.rejected, stats.total); -} \ No newline at end of file + println!( + "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", + stats.matched, stats.total + ); + println!( + "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", + stats.rejected, stats.total + ); +} diff --git a/src/lib.rs b/src/lib.rs index 5a7ca06..0b73d80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,3 @@ pub mod core; +pub mod execuctor; +pub mod theme; diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..e313cf5 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1 @@ +pub mod for_path_list; diff --git a/src/core/path_class.rs b/src/theme/for_path_list.rs similarity index 100% rename from src/core/path_class.rs rename to src/theme/for_path_list.rs From 5ca9a2c5c9ab5ebba54ccb5cfd869493f6741b6e Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 12:17:56 +0100 Subject: [PATCH 14/45] (fix: correct finish) --- Cargo.toml | 8 ++++---- src/core/path_matcher/matcher.rs | 9 ++++----- src/core/path_matcher/matchers.rs | 1 + src/core/path_matcher/sort.rs | 5 ++--- src/core/patterns_expand.rs | 5 ++--- src/execuctor/for_many_patterns_tok.rs | 11 ++++++----- src/execuctor/for_one_pattern_raw.rs | 11 ++++++----- src/interfaces/cli/args.rs | 6 +++--- src/theme/for_path_list.rs | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22d31ef..b705649 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,14 +31,14 @@ ctrlc = "3.5.2" # ========================================== # 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" \ No newline at end of file diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index f0b8a58..3d2019b 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -29,15 +29,13 @@ impl PathMatcher { let requires_sibling = actual_pattern.contains('@'); let requires_orphan = actual_pattern.contains('$'); let clean_pattern_str = actual_pattern - .replace('@', "") - .replace('$', "") - .replace('+', ""); + .replace(['@', '$', '+'], ""); let base_name = clean_pattern_str .trim_end_matches('/') .trim_end_matches("**") .split('/') - .last() + .next_back() .unwrap_or("") .split('.') .next() @@ -122,7 +120,7 @@ impl PathMatcher { i += 1; } let escaped: Vec = - options.split(',').map(|s| regex::escape(s)).collect(); + options.split(',').map(regex::escape).collect(); re.push_str(&format!("(?:{})", escaped.join("|"))); } '[' => { @@ -219,6 +217,7 @@ impl PathMatcher { /// [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, diff --git a/src/core/path_matcher/matchers.rs b/src/core/path_matcher/matchers.rs index 79d6411..c7da997 100644 --- a/src/core/path_matcher/matchers.rs +++ b/src/core/path_matcher/matchers.rs @@ -67,6 +67,7 @@ impl PathMatchers { /// [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. + // #[allow(clippy::too_many_arguments)] pub fn evaluate( &self, paths: I, diff --git a/src/core/path_matcher/sort.rs b/src/core/path_matcher/sort.rs index 0c7157c..466c04a 100644 --- a/src/core/path_matcher/sort.rs +++ b/src/core/path_matcher/sort.rs @@ -96,11 +96,10 @@ impl SortStrategy { /// [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('.') { - if idx > 0 && trimmed.as_bytes()[idx - 1] != b'/' { + if let Some(idx) = trimmed.rfind('.') + && idx > 0 && trimmed.as_bytes()[idx - 1] != b'/' { return &trimmed[..idx]; } - } trimmed } } diff --git a/src/core/patterns_expand.rs b/src/core/patterns_expand.rs index 81c7192..04a0c2e 100644 --- a/src/core/patterns_expand.rs +++ b/src/core/patterns_expand.rs @@ -29,8 +29,8 @@ impl PatternContext { /// [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('}')) { - if start < end { + 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]; @@ -42,7 +42,6 @@ impl PatternContext { } return tok; } - } vec![pattern.to_string()] } } diff --git a/src/execuctor/for_many_patterns_tok.rs b/src/execuctor/for_many_patterns_tok.rs index 9f5df89..ac3c0b0 100644 --- a/src/execuctor/for_many_patterns_tok.rs +++ b/src/execuctor/for_many_patterns_tok.rs @@ -8,6 +8,7 @@ pub use crate::core::path_matcher::sort::SortStrategy; /// [POL]: Egzekutor operujący na wielu wzorcach (wersja po rozwinięciu klamer/tokenizacji). /// [ENG]: Executor operating on multiple patterns (post brace expansion/tokenisation). +// #[allow(clippy::too_many_arguments)] pub fn execute( enter_path: &str, patterns: &[String], @@ -50,7 +51,10 @@ where let paths_set = paths_store.get_index(); // 5. Ewaluacja i wykonanie callbacków - let stats = matchers.evaluate( + + + // 6. Zwracamy statystyki do Engine'u + matchers.evaluate( &paths_store.list, &paths_set, sort_strategy, @@ -58,8 +62,5 @@ where show_exclude, on_match, on_mismatch, - ); - - // 6. Zwracamy statystyki do Engine'u - stats + ) } diff --git a/src/execuctor/for_one_pattern_raw.rs b/src/execuctor/for_one_pattern_raw.rs index d29b83a..6f6b496 100644 --- a/src/execuctor/for_one_pattern_raw.rs +++ b/src/execuctor/for_one_pattern_raw.rs @@ -7,6 +7,7 @@ pub use crate::core::path_matcher::sort::SortStrategy; /// [POL]: Egzekutor operujący na pojedynczym, surowym wzorcu wpisanym przez użytkownika. /// [ENG]: Executor operating on a single, raw pattern provided by the user. +// #[allow(clippy::too_many_arguments)] pub fn execute( enter_path: &str, raw_pattern: &str, @@ -46,7 +47,10 @@ where let paths_set = paths_store.get_index(); // 5. Ewaluacja i wykonanie callbacków - let stats = matcher.evaluate( + + + // 6. Zwracamy statystyki do Engine'u + matcher.evaluate( &paths_store.list, &paths_set, sort_strategy, @@ -54,8 +58,5 @@ where show_exclude, on_match, on_mismatch, - ); - - // 6. Zwracamy statystyki do Engine'u - stats + ) } diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 2903ceb..eba8d66 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -61,9 +61,9 @@ pub enum CliSortStrategy { ZaDirMerge, } -impl Into for CliSortStrategy { - fn into(self) -> SortStrategy { - match self { +impl From for SortStrategy { + fn from(val: CliSortStrategy) -> Self { + match val { CliSortStrategy::None => SortStrategy::None, CliSortStrategy::Az => SortStrategy::Az, CliSortStrategy::Za => SortStrategy::Za, diff --git a/src/theme/for_path_list.rs b/src/theme/for_path_list.rs index d0fa738..1628029 100644 --- a/src/theme/for_path_list.rs +++ b/src/theme/for_path_list.rs @@ -3,7 +3,7 @@ pub fn get_icon_for_path(path: &str) -> &'static str { let is_dir = path.ends_with('/'); - let nazwa = path.trim_end_matches('/').split('/').last().unwrap_or(""); + let nazwa = path.trim_end_matches('/').split('/').next_back().unwrap_or(""); let is_hidden = nazwa.starts_with('.'); match (is_dir, is_hidden) { From 002e708f1c2314c29fb500d9dabdb506100541dd Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 13:37:24 +0100 Subject: [PATCH 15/45] (add: tree) --- src/core.rs | 2 + src/core/file_stats.rs | 31 +++ src/core/path_treeview.rs | 351 +++++++++++++++++++++++++ src/execuctor/for_many_patterns_tok.rs | 28 +- src/interfaces/cli/args.rs | 4 + src/interfaces/cli/engine.rs | 51 +++- src/theme.rs | 1 + src/theme/for_path_tree.rs | 106 ++++++++ 8 files changed, 562 insertions(+), 12 deletions(-) create mode 100644 src/core/file_stats.rs create mode 100644 src/core/path_treeview.rs create mode 100644 src/theme/for_path_tree.rs diff --git a/src/core.rs b/src/core.rs index d0d42fc..4d4b4fb 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,3 +1,5 @@ pub mod path_matcher; pub mod path_store; pub mod patterns_expand; +pub mod path_treeview; +pub mod file_stats; \ No newline at end of file diff --git a/src/core/file_stats.rs b/src/core/file_stats.rs new file mode 100644 index 0000000..dcd52b9 --- /dev/null +++ b/src/core/file_stats.rs @@ -0,0 +1,31 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +/// [PL]: 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 { + /// [PL]: 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 = fs::metadata(&absolute) + .map(|m| m.len()) + .unwrap_or(0); + + Self { + path: path.to_string(), + absolute, + weight_bytes, + } + } +} \ No newline at end of file diff --git a/src/core/path_treeview.rs b/src/core/path_treeview.rs new file mode 100644 index 0000000..7fcbaf6 --- /dev/null +++ b/src/core/path_treeview.rs @@ -0,0 +1,351 @@ +// [EN]: Logic for transforming flat paths into a hierarchical tree structure with icons and weights. +// [PL]: Logika przekształcania płaskich ścieżek w hierarchiczną strukturę drzewa z ikonami i wagami. + +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use colored::Colorize; +use super::super::theme::for_path_tree::{get_file_type, TreeStyle, DIR_ICON}; + +// ========================================== +// 1. STRUKTURY DANYCH I KONFIGURACJA +// ========================================== + +#[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, + } + } +} + +#[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 { + /// [PL]: Sortuje listę węzłów w miejscu. + pub fn sort_slice(nodes: &mut [FileNode], method: &str) { + match 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)), + } + } +} + +// ========================================== +// 2. HERMETYCZNY SILNIK DRZEWA (PathTree) +// ========================================== + +/// [PL]: Główna struktura zarządzająca budową i renderowaniem drzewa. +pub struct PathTree { + roots: Vec, + style: TreeStyle, +} + +impl PathTree { + /// [PL]: Inicjuje i buduje drzewo na podstawie płaskiej listy ścieżek. + #[must_use] + /// [PL]: Inicjuje i buduje drzewo na podstawie płaskiej listy ścieżek. + #[must_use] + pub fn build( + paths_strings: &[String], + base_dir: &str, // ⚡ Wstrzyknięta ścieżka bazowa + sort_method: &str, + weight_cfg: &WeightConfig, + ) -> Self { + // 1. Zdefiniowanie ścieżki bazowej (widocznej w całej funkcji 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()); + } + + // Wewnętrzna funkcja rekurencyjna + fn build_node( + path: &PathBuf, + paths_map: &BTreeMap>, + base_path: &Path, // ⚡ Argument dla rekurencji + sort_method: &str, + weight_cfg: &WeightConfig, + ) -> 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 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() + }; + + // ⚡ Tworzymy ścieżkę absolutną, żeby pobrać prawdziwą wagę z dysku + let absolute_path = base_path.join(path); + let mut weight_bytes = PathTree::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_method, weight_cfg)) // ⚡ Rekurencja z base_path + .collect(); + + FileNode::sort_slice(&mut child_nodes, sort_method); + + 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 = PathTree::format_weight(weight_bytes, is_dir, weight_cfg); + + FileNode { + name, + path: path.clone(), + is_dir, + icon, + weight_str, + weight_bytes, + children, + } + } + + let roots: 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 + .into_iter() + // ⚡ Tutaj przekazujemy obiekt zainicjowany na samym początku funkcji + .map(|r| build_node(&r, &tree_map, base_path_obj, sort_method, weight_cfg)) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_method); + + Self { + roots: top_nodes, + style: TreeStyle::default(), + } + } + + /// [PL]: Opcjonalnie nadpisuje domyślny styl drzewa. (Wzorzec Builder) + #[must_use] + pub fn with_style(mut self, style: TreeStyle) -> Self { + self.style = style; + self + } + + /// [PL]: Renderuje drzewo do formatu CLI (z kolorami). + #[must_use] + pub fn render_cli(&self) -> String { + self.plot(&self.roots, "", true) + } + + /// [PL]: Renderuje drzewo do czystego tekstu (np. do Markdown). + #[must_use] + pub fn render_txt(&self) -> String { + self.plot(&self.roots, "", false) + } + + // --- PRYWATNE METODY POMOCNICZE W IMPL --- + + 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!( + "{}{}{} {}{}/\n", + weight_prefix, + indent.green(), + branch.green(), + node.icon, + node.name.truecolor(200, 200, 50) + ) + } else { + format!( + "{}{}{} {}{}\n", + weight_prefix, + indent.green(), + branch.green(), + node.icon, + node.name.white() + ) + } + } else { + format!("{}{}{} {} {}\n", weight_prefix, indent, branch, node.icon, 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 + } + + 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(); + } + + if metadata.is_dir() && !sum_included_only { + return Self::get_dir_size(path); + } + + 0 + } + + 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() { + Self::get_dir_size(&p) + } else { + e.metadata().map(|m| m.len()).unwrap_or(0) + } + }) + .sum() + }) + .unwrap_or(0) + } + + 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}] ") + } +} \ No newline at end of file diff --git a/src/execuctor/for_many_patterns_tok.rs b/src/execuctor/for_many_patterns_tok.rs index ac3c0b0..d8ae61f 100644 --- a/src/execuctor/for_many_patterns_tok.rs +++ b/src/execuctor/for_many_patterns_tok.rs @@ -5,6 +5,7 @@ use crate::core::path_store::store::PathStore; use crate::core::patterns_expand::PatternContext; // [PL]: Reeksportujemy strategię, aby Kokpit nie musiał szukać jej w core. pub use crate::core::path_matcher::sort::SortStrategy; +use crate::core::file_stats::FileStats; /// [POL]: Egzekutor operujący na wielu wzorcach (wersja po rozwinięciu klamer/tokenizacji). /// [ENG]: Executor operating on multiple patterns (post brace expansion/tokenisation). @@ -16,12 +17,15 @@ pub fn execute( sort_strategy: SortStrategy, show_include: bool, show_exclude: bool, - on_match: OnMatch, - on_mismatch: OnMismatch, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, ) -> MatchStats where - OnMatch: FnMut(&str), - OnMismatch: FnMut(&str), + // OnMatch: FnMut(&str), + // OnMismatch: FnMut(&str), + // ⚡ Teraz callbacki oczekują bogatego obiektu, a nie tylko tekstu + OnMatch: FnMut(&FileStats), + OnMismatch: FnMut(&FileStats), { // 1. Inicjalizacja kontekstów let pattern_ctx = PatternContext::new(patterns); @@ -50,9 +54,9 @@ where // [PL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) let paths_set = paths_store.get_index(); - // 5. Ewaluacja i wykonanie callbacków + - + let entry_abs = path_ctx.entry_absolute.clone(); // 6. Zwracamy statystyki do Engine'u matchers.evaluate( &paths_store.list, @@ -60,7 +64,15 @@ where sort_strategy, show_include, show_exclude, - on_match, - on_mismatch, + |raw_path| { + // Pośrednik pobiera statystyki + let stats = FileStats::fetch(raw_path, &entry_abs); + on_match(&stats); + }, + |raw_path| { + // Pośrednik pobiera statystyki + let stats = FileStats::fetch(raw_path, &entry_abs); + on_mismatch(&stats); + }, ) } diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index eba8d66..bbb8b09 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -44,6 +44,10 @@ pub struct CliArgs { /// [PL]: Strategia sortowania wyników. #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] pub sort: CliSortStrategy, + + /// [POL]: Wyświetla wyniki w formie hierarchicznego drzewa zamiast płaskiej listy. + #[arg(short = 't', long = "treeview", default_value_t = false)] + pub treeview: bool, } #[derive(Debug, Clone, Copy, ValueEnum)] diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index d26d9db..3ed9c4d 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,8 +1,10 @@ use crate::interfaces::cli::args::CliArgs; use cargo_plot::execuctor::for_many_patterns_tok::{self, SortStrategy}; -use cargo_plot::theme::for_path_list::get_icon_for_path; // lub dla jednego wzorca: -// use cargo_plot::execuctor::for_one_pattern_raw; +// use cargo_plot::execuctor::for_one_pattern_raw::{self, SortStrategy}; +use cargo_plot::theme::for_path_list::get_icon_for_path; +use cargo_plot::core::path_treeview::{PathTree, WeightConfig}; +use cargo_plot::core::path_store::context::PathContext; /// [EN]: The execution engine (Cockpit). /// [PL]: Silnik wykonawczy (Kokpit). @@ -24,10 +26,51 @@ pub fn run(args: CliArgs) { sort_strategy, show_include, show_exclude, - |path| println!("✅ MATCH: {} {}", get_icon_for_path(path), path), - |path| println!("❌ REJECT: {} {}", get_icon_for_path(path), path), + |file_stat| { + if !args.treeview { + println!( + "✅ MATCH: {} {} ({} B)", + get_icon_for_path(&file_stat.path), + file_stat.path, + file_stat.weight_bytes + ); + } + }, + |file_stat| { + if !args.treeview && show_exclude { + println!( + "❌ REJECT: {} {} ({} B)", + get_icon_for_path(&file_stat.path), + file_stat.path, + file_stat.weight_bytes + ); + } + }, ); + // ⚡ Logika Drzewa vs Listy + if args.treeview { + println!("🌲 Widok Drzewa:"); + + let weight_cfg = WeightConfig::default(); + + // ⚡ Rozwiązujemy ścieżkę z argumentów CLI w locie, żeby wiedzieć, gdzie szukać wag + let path_ctx = PathContext::resolve(&args.enter_path).unwrap_or_else(|e| { + eprintln!("❌ Błąd ścieżki: {}", e); + std::process::exit(1); + }); + + // Teraz mamy dostęp do `path_ctx.entry_absolute` i możemy przekazać to do Buildera! + let tree = PathTree::build( + &stats.included, + &path_ctx.entry_absolute, // ⚡ Używamy odzyskanej ścieżki + "dirs-first", + &weight_cfg + ); + + print!("{}", tree.render_cli()); + } + println!("----------"); println!( "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", diff --git a/src/theme.rs b/src/theme.rs index e313cf5..26ae589 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1 +1,2 @@ pub mod for_path_list; +pub mod for_path_tree; \ No newline at end of file diff --git a/src/theme/for_path_tree.rs b/src/theme/for_path_tree.rs new file mode 100644 index 0000000..a799ce7 --- /dev/null +++ b/src/theme/for_path_tree.rs @@ -0,0 +1,106 @@ +// [EN]: Path classification and icon mapping for tree visualization. +// [PL]: Klasyfikacja ścieżek i mapowanie ikon dla wizualizacji drzewa. + +/// [EN]: Global icon used for directory nodes. +/// [PL]: Globalna ikona używana dla węzłów będących folderami. +pub const DIR_ICON: &str = "📂"; + +/// [EN]: Defines visual and metadata properties for a file type. +/// [PL]: Definiuje wizualne i metadanowe właściwości dla typu pliku. +pub struct PathFileType { + pub icon: &'static str, + pub md_lang: &'static str, +} + +/// [EN]: Returns file properties based on its extension. +/// [PL]: 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", + }, + // [EN]: Default fallback for unknown file types. + // [PL]: Domyślny fallback dla nieznanych typów plików. + _ => PathFileType { + icon: "📄", + md_lang: "text", + }, + } +} + +/// [EN]: Character set used for drawing tree branches and indents. +/// [PL]: Zestaw znaków używanych do rysowania gałęzi drzewa i wcięć. +#[derive(Debug, Clone)] +pub struct TreeStyle { + // [EN]: Directories (d) + // [PL]: 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, // ├─── + + // [EN]: Files (f) + // [PL]: Pliki (f) + pub file_last: String, // └──• + pub file_mid: String, // ├──• + + // [EN]: Indentations for subsequent levels (i) + // [PL]: 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(), + } + } +} \ No newline at end of file From aac40b3b908a790e1e7cec61ca87f832ef8ff895 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 20:53:37 +0100 Subject: [PATCH 16/45] (add: view mode) --- src/core.rs | 2 +- src/core/file_stats.rs | 12 +- src/core/file_stats/weight.rs | 108 ++++++++ src/core/path_matcher/matcher.rs | 16 +- src/core/path_matcher/matchers.rs | 23 +- src/core/path_matcher/stats.rs | 24 +- src/core/path_treeview.rs | 351 ------------------------- src/core/path_view.rs | 16 ++ src/core/path_view/grid.rs | 180 +++++++++++++ src/core/path_view/list.rs | 62 +++++ src/core/path_view/node.rs | 74 ++++++ src/core/path_view/tree.rs | 184 +++++++++++++ src/execuctor/for_many_patterns_tok.rs | 63 ++++- src/execuctor/for_one_pattern_raw.rs | 71 ++++- src/interfaces/cli/args.rs | 36 ++- src/interfaces/cli/engine.rs | 136 ++++++---- 16 files changed, 907 insertions(+), 451 deletions(-) create mode 100644 src/core/file_stats/weight.rs delete mode 100644 src/core/path_treeview.rs create mode 100644 src/core/path_view.rs create mode 100644 src/core/path_view/grid.rs create mode 100644 src/core/path_view/list.rs create mode 100644 src/core/path_view/node.rs create mode 100644 src/core/path_view/tree.rs diff --git a/src/core.rs b/src/core.rs index 4d4b4fb..fb3a716 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,5 +1,5 @@ pub mod path_matcher; pub mod path_store; pub mod patterns_expand; -pub mod path_treeview; +pub mod path_view; pub mod file_stats; \ No newline at end of file diff --git a/src/core/file_stats.rs b/src/core/file_stats.rs index dcd52b9..de982e3 100644 --- a/src/core/file_stats.rs +++ b/src/core/file_stats.rs @@ -1,5 +1,8 @@ -use std::fs; +// use std::fs; use std::path::{Path, PathBuf}; +pub mod weight; + +use self::weight::get_path_weight; /// [PL]: Struktura przechowująca metadane (statystyki) pliku lub folderu. #[derive(Debug, Clone)] @@ -18,9 +21,10 @@ impl FileStats { pub fn fetch(path: &str, entry_absolute: &str) -> Self { let absolute = Path::new(entry_absolute).join(path); - let weight_bytes = fs::metadata(&absolute) - .map(|m| m.len()) - .unwrap_or(0); + 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(), diff --git a/src/core/file_stats/weight.rs b/src/core/file_stats/weight.rs new file mode 100644 index 0000000..e4bc936 --- /dev/null +++ b/src/core/file_stats/weight.rs @@ -0,0 +1,108 @@ +// [EN]: Logic for calculating and formatting file and directory weights. +// [PL]: 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, + } + } +} + +/// [PL]: 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(); + } + + if metadata.is_dir() && !sum_included_only { + return get_dir_size(path); + } + + 0 +} + +/// [PL]: 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) +} + +/// [PL]: 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}] ") +} \ No newline at end of file diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index 3d2019b..2a56813 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -1,5 +1,5 @@ use super::sort::SortStrategy; -use super::stats::MatchStats; +use super::stats::{MatchStats,ResultSet}; use regex::Regex; use std::collections::HashSet; @@ -252,8 +252,18 @@ impl PathMatcher { matched: matched.len(), rejected: mismatched.len(), total: matched.len() + mismatched.len(), - included: matched.iter().map(|s| s.as_ref().to_string()).collect(), - excluded: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + included: ResultSet { + paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + excluded: ResultSet { + paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, }; if show_include { diff --git a/src/core/path_matcher/matchers.rs b/src/core/path_matcher/matchers.rs index c7da997..1c0f9af 100644 --- a/src/core/path_matcher/matchers.rs +++ b/src/core/path_matcher/matchers.rs @@ -1,6 +1,6 @@ use super::matcher::PathMatcher; use super::sort::SortStrategy; -use super::stats::MatchStats; +use super::stats::{MatchStats,ResultSet,ShowMode}; use std::collections::HashSet; /// [POL]: Kontener przechowujący kolekcję silników dopasowujących ścieżki. @@ -73,8 +73,7 @@ impl PathMatchers { paths: I, env: &HashSet<&str>, strategy: SortStrategy, - show_include: bool, - show_exclude: bool, + show_mode: ShowMode, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) -> MatchStats @@ -102,17 +101,27 @@ impl PathMatchers { matched: matched.len(), rejected: mismatched.len(), total: matched.len() + mismatched.len(), - included: matched.iter().map(|s| s.as_ref().to_string()).collect(), - excluded: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + included: ResultSet { + paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + excluded: ResultSet { + paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, }; - if show_include { + if show_mode == ShowMode::Include || show_mode == ShowMode::Context { for path in matched { on_match(path.as_ref()); } } - if show_exclude { + if show_mode == ShowMode::Exclude || show_mode == ShowMode::Context { for path in mismatched { on_mismatch(path.as_ref()); } diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs index 1910f48..1cb9adb 100644 --- a/src/core/path_matcher/stats.rs +++ b/src/core/path_matcher/stats.rs @@ -1,10 +1,28 @@ +use crate::core::path_view::{PathList, PathTree, PathGrid}; + +/// [PL]: 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, +} + // [EN]: Simple stats object to avoid manual counting in the Engine. // [PL]: Prosty obiekt statystyk, aby uniknąć ręcznego liczenia w Engine. -#[derive(Debug, Default, Clone)] +#[derive(Default)] pub struct MatchStats { pub matched: usize, pub rejected: usize, pub total: usize, - pub included: Vec, - pub excluded: Vec, + pub included: ResultSet, + pub excluded: ResultSet, } + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ShowMode { + Include, + Exclude, + Context, +} \ No newline at end of file diff --git a/src/core/path_treeview.rs b/src/core/path_treeview.rs deleted file mode 100644 index 7fcbaf6..0000000 --- a/src/core/path_treeview.rs +++ /dev/null @@ -1,351 +0,0 @@ -// [EN]: Logic for transforming flat paths into a hierarchical tree structure with icons and weights. -// [PL]: Logika przekształcania płaskich ścieżek w hierarchiczną strukturę drzewa z ikonami i wagami. - -use std::cmp::Ordering; -use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; - -use colored::Colorize; -use super::super::theme::for_path_tree::{get_file_type, TreeStyle, DIR_ICON}; - -// ========================================== -// 1. STRUKTURY DANYCH I KONFIGURACJA -// ========================================== - -#[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, - } - } -} - -#[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 { - /// [PL]: Sortuje listę węzłów w miejscu. - pub fn sort_slice(nodes: &mut [FileNode], method: &str) { - match 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)), - } - } -} - -// ========================================== -// 2. HERMETYCZNY SILNIK DRZEWA (PathTree) -// ========================================== - -/// [PL]: Główna struktura zarządzająca budową i renderowaniem drzewa. -pub struct PathTree { - roots: Vec, - style: TreeStyle, -} - -impl PathTree { - /// [PL]: Inicjuje i buduje drzewo na podstawie płaskiej listy ścieżek. - #[must_use] - /// [PL]: Inicjuje i buduje drzewo na podstawie płaskiej listy ścieżek. - #[must_use] - pub fn build( - paths_strings: &[String], - base_dir: &str, // ⚡ Wstrzyknięta ścieżka bazowa - sort_method: &str, - weight_cfg: &WeightConfig, - ) -> Self { - // 1. Zdefiniowanie ścieżki bazowej (widocznej w całej funkcji 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()); - } - - // Wewnętrzna funkcja rekurencyjna - fn build_node( - path: &PathBuf, - paths_map: &BTreeMap>, - base_path: &Path, // ⚡ Argument dla rekurencji - sort_method: &str, - weight_cfg: &WeightConfig, - ) -> 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 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() - }; - - // ⚡ Tworzymy ścieżkę absolutną, żeby pobrać prawdziwą wagę z dysku - let absolute_path = base_path.join(path); - let mut weight_bytes = PathTree::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_method, weight_cfg)) // ⚡ Rekurencja z base_path - .collect(); - - FileNode::sort_slice(&mut child_nodes, sort_method); - - 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 = PathTree::format_weight(weight_bytes, is_dir, weight_cfg); - - FileNode { - name, - path: path.clone(), - is_dir, - icon, - weight_str, - weight_bytes, - children, - } - } - - let roots: 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 - .into_iter() - // ⚡ Tutaj przekazujemy obiekt zainicjowany na samym początku funkcji - .map(|r| build_node(&r, &tree_map, base_path_obj, sort_method, weight_cfg)) - .collect(); - - FileNode::sort_slice(&mut top_nodes, sort_method); - - Self { - roots: top_nodes, - style: TreeStyle::default(), - } - } - - /// [PL]: Opcjonalnie nadpisuje domyślny styl drzewa. (Wzorzec Builder) - #[must_use] - pub fn with_style(mut self, style: TreeStyle) -> Self { - self.style = style; - self - } - - /// [PL]: Renderuje drzewo do formatu CLI (z kolorami). - #[must_use] - pub fn render_cli(&self) -> String { - self.plot(&self.roots, "", true) - } - - /// [PL]: Renderuje drzewo do czystego tekstu (np. do Markdown). - #[must_use] - pub fn render_txt(&self) -> String { - self.plot(&self.roots, "", false) - } - - // --- PRYWATNE METODY POMOCNICZE W IMPL --- - - 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!( - "{}{}{} {}{}/\n", - weight_prefix, - indent.green(), - branch.green(), - node.icon, - node.name.truecolor(200, 200, 50) - ) - } else { - format!( - "{}{}{} {}{}\n", - weight_prefix, - indent.green(), - branch.green(), - node.icon, - node.name.white() - ) - } - } else { - format!("{}{}{} {} {}\n", weight_prefix, indent, branch, node.icon, 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 - } - - 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(); - } - - if metadata.is_dir() && !sum_included_only { - return Self::get_dir_size(path); - } - - 0 - } - - 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() { - Self::get_dir_size(&p) - } else { - e.metadata().map(|m| m.len()).unwrap_or(0) - } - }) - .sum() - }) - .unwrap_or(0) - } - - 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}] ") - } -} \ No newline at end of file diff --git a/src/core/path_view.rs b/src/core/path_view.rs new file mode 100644 index 0000000..9fce4b1 --- /dev/null +++ b/src/core/path_view.rs @@ -0,0 +1,16 @@ +pub mod node; +pub mod tree; +pub mod list; +pub mod grid; + +// Re-eksportujemy dla wygody, aby w engine.rs używać PathTree i FileNode bezpośrednio +pub use tree::PathTree; +pub use list::PathList; +pub use grid::PathGrid; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ViewMode { + Tree, + List, + Grid, +} \ No newline at end of file diff --git a/src/core/path_view/grid.rs b/src/core/path_view/grid.rs new file mode 100644 index 0000000..e0a73a3 --- /dev/null +++ b/src/core/path_view/grid.rs @@ -0,0 +1,180 @@ +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use colored::Colorize; + +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig, UnitSystem}; +use crate::theme::for_path_tree::{get_file_type, TreeStyle, DIR_ICON}; +use crate::core::path_matcher::SortStrategy; + +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>, + ) -> 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, + ) -> 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 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)) + .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)) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_strategy); + + let final_roots = if let Some(r_name) = root_name { + let empty_weight = if weight_cfg.system != UnitSystem::None { + " ".repeat(7 + weight_cfg.precision) + } else { String::new() }; + + vec![FileNode { + name: r_name.to_string(), path: PathBuf::from(r_name), is_dir: true, + icon: DIR_ICON.to_string(), weight_str: empty_weight, weight_bytes: 0, 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) + } + + 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 + } +} \ No newline at end of file diff --git a/src/core/path_view/list.rs b/src/core/path_view/list.rs new file mode 100644 index 0000000..1a53823 --- /dev/null +++ b/src/core/path_view/list.rs @@ -0,0 +1,62 @@ +use colored::Colorize; +use crate::theme::for_path_list::get_icon_for_path; +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig}; +use crate::core::path_matcher::SortStrategy; +/// [PL]: Zarządca wyświetlania wyników w formie płaskiej listy. +pub struct PathList { + items: Vec, +} + +impl PathList { + /// [PL]: Buduje listę na podstawie zbioru ścieżek i statystyk. + pub fn build( + paths_strings: &[String], + base_dir: &str, + sort_strategy: SortStrategy, + weight_cfg: &WeightConfig, + ) -> 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: 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 } + } + + /// [PL]: 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), + tag, + item.icon, + if item.is_dir { item.name.yellow() } else { item.name.white() } + ); + out.push_str(&line); + } + out + } +} \ No newline at end of file diff --git a/src/core/path_view/node.rs b/src/core/path_view/node.rs new file mode 100644 index 0000000..c208cae --- /dev/null +++ b/src/core/path_view/node.rs @@ -0,0 +1,74 @@ +use std::path::PathBuf; +use crate::core::path_matcher::SortStrategy; + +/// [PL]: 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 { + /// [PL]: 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), + } + }); + } + + /// [PL]: Wyciąga rdzeń nazwy do grupowania (np. "main.rs" -> "main"). + fn get_merge_key(name: &str) -> &str { + if let Some(idx) = name.rfind('.') { + if idx > 0 { + return &name[..idx]; + } + } + name + } +} \ No newline at end of file diff --git a/src/core/path_view/tree.rs b/src/core/path_view/tree.rs new file mode 100644 index 0000000..2d7f86b --- /dev/null +++ b/src/core/path_view/tree.rs @@ -0,0 +1,184 @@ +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use colored::Colorize; + +// Importy z rodzeństwa i innych modułów core +use super::node::FileNode; +use crate::core::file_stats::weight::{self, WeightConfig, UnitSystem}; +use crate::theme::for_path_tree::{get_file_type, TreeStyle, DIR_ICON}; +use crate::core::path_matcher::SortStrategy; +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>, + ) -> 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, + ) -> 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 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)) + .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)) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_strategy); + + let final_roots = if let Some(r_name) = root_name { + let empty_weight = if weight_cfg.system != UnitSystem::None { + " ".repeat(7 + weight_cfg.precision) + } else { + String::new() + }; + + vec![FileNode { + name: r_name.to_string(), + path: PathBuf::from(r_name), + is_dir: true, + icon: DIR_ICON.to_string(), + weight_str: empty_weight, + weight_bytes: 0, + 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 + } +} \ No newline at end of file diff --git a/src/execuctor/for_many_patterns_tok.rs b/src/execuctor/for_many_patterns_tok.rs index d8ae61f..c350427 100644 --- a/src/execuctor/for_many_patterns_tok.rs +++ b/src/execuctor/for_many_patterns_tok.rs @@ -1,8 +1,11 @@ use crate::core::path_matcher::matchers::PathMatchers; -use crate::core::path_matcher::stats::MatchStats; +use crate::core::path_matcher::stats::{MatchStats, ShowMode}; use crate::core::path_store::context::PathContext; use crate::core::path_store::store::PathStore; use crate::core::patterns_expand::PatternContext; +use crate::core::path_view::{PathList, PathTree, PathGrid, ViewMode}; +use crate::core::file_stats::weight::WeightConfig; +use std::path::Path; // [PL]: Reeksportujemy strategię, aby Kokpit nie musiał szukać jej w core. pub use crate::core::path_matcher::sort::SortStrategy; use crate::core::file_stats::FileStats; @@ -15,8 +18,9 @@ pub fn execute( patterns: &[String], is_case_sensitive: bool, sort_strategy: SortStrategy, - show_include: bool, - show_exclude: bool, + show_mode: ShowMode, + view_mode: ViewMode, + no_root: bool, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) -> MatchStats @@ -58,13 +62,12 @@ where let entry_abs = path_ctx.entry_absolute.clone(); // 6. Zwracamy statystyki do Engine'u - matchers.evaluate( + let mut stats = matchers.evaluate( &paths_store.list, &paths_set, sort_strategy, - show_include, - show_exclude, - |raw_path| { + show_mode, + |raw_path| { // Pośrednik pobiera statystyki let stats = FileStats::fetch(raw_path, &entry_abs); on_match(&stats); @@ -74,5 +77,47 @@ where let stats = FileStats::fetch(raw_path, &entry_abs); on_mismatch(&stats); }, - ) -} + ); + + // 7. ⚡ MAGIA BUDOWANIA WIDOKÓW + let weight_cfg = WeightConfig::default(); + let root_name = if no_root { + None + } else { + Path::new(&path_ctx.entry_absolute).file_name().and_then(|n| n.to_str()) + }; + + // Pomocnicze flagi do budowania (żeby kod w match był krótki) + let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; + let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; + + // ⚡ Czysty match dla widoków (Grid, Tree, List) + match view_mode { + ViewMode::Grid => { + if do_include { + stats.included.grid = Some(PathGrid::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + } + if do_exclude { + stats.excluded.grid = Some(PathGrid::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + } + } + ViewMode::Tree => { + if do_include { + stats.included.tree = Some(PathTree::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + } + if do_exclude { + stats.excluded.tree = Some(PathTree::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + } + } + ViewMode::List => { + if do_include { + stats.included.list = Some(PathList::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); + } + if do_exclude { + stats.excluded.list = Some(PathList::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); + } + } + } + + stats +} \ No newline at end of file diff --git a/src/execuctor/for_one_pattern_raw.rs b/src/execuctor/for_one_pattern_raw.rs index 6f6b496..2a0dc5a 100644 --- a/src/execuctor/for_one_pattern_raw.rs +++ b/src/execuctor/for_one_pattern_raw.rs @@ -1,9 +1,13 @@ use crate::core::path_matcher::matcher::PathMatcher; -use crate::core::path_matcher::stats::MatchStats; -use crate::core::path_store::PathStore; +use crate::core::path_store::store::PathStore; use crate::core::path_store::context::PathContext; // [PL]: Reeksportujemy strategię, aby Kokpit nie musiał szukać jej w core. pub use crate::core::path_matcher::sort::SortStrategy; +use crate::core::path_matcher::stats::MatchStats; +use crate::core::path_view::{PathList, PathTree, PathGrid}; +use crate::core::file_stats::weight::WeightConfig; +use crate::core::file_stats::FileStats; +use std::path::Path; /// [POL]: Egzekutor operujący na pojedynczym, surowym wzorcu wpisanym przez użytkownika. /// [ENG]: Executor operating on a single, raw pattern provided by the user. @@ -15,12 +19,15 @@ pub fn execute( sort_strategy: SortStrategy, show_include: bool, show_exclude: bool, - on_match: OnMatch, - on_mismatch: OnMismatch, + is_treeview: bool, + is_gridview: bool, + no_root: bool, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, ) -> MatchStats where - OnMatch: FnMut(&str), - OnMismatch: FnMut(&str), + OnMatch: FnMut(&FileStats), + OnMismatch: FnMut(&FileStats), { // 1. Inicjalizacja kontekstów let path_ctx = PathContext::resolve(enter_path).unwrap_or_else(|e| { @@ -46,17 +53,55 @@ where // [PL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) let paths_set = paths_store.get_index(); - // 5. Ewaluacja i wykonanie callbacków - + let entry_abs = path_ctx.entry_absolute.clone(); - // 6. Zwracamy statystyki do Engine'u - matcher.evaluate( + // 5. Ewaluacja i wykonanie callbacków + let mut stats = matcher.evaluate( &paths_store.list, &paths_set, sort_strategy, show_include, show_exclude, - on_match, - on_mismatch, - ) + |raw_path| { + let s = FileStats::fetch(raw_path, &entry_abs); + on_match(&s); + }, + |raw_path| { + let s = FileStats::fetch(raw_path, &entry_abs); + on_mismatch(&s); + }, + ); + // 6. ⚡ MAGIA BUDOWANIA WIDOKÓW + let weight_cfg = WeightConfig::default(); + let root_name = if no_root { + None + } else { + Path::new(&path_ctx.entry_absolute).file_name().and_then(|n| n.to_str()) + }; + + if is_gridview { + if show_include { + stats.included.grid = Some(PathGrid::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + } + if show_exclude { + stats.excluded.grid = Some(PathGrid::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + } + } else if is_treeview { + if show_include { + stats.included.tree = Some(PathTree::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + } + if show_exclude { + stats.excluded.tree = Some(PathTree::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + } + } else { + if show_include { + stats.included.list = Some(PathList::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); + } + if show_exclude { + stats.excluded.list = Some(PathList::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); + } + } + + // 7. Zwracamy statystyki do Engine'u + stats } diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index bbb8b09..c483beb 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -1,4 +1,5 @@ use cargo_plot::core::path_matcher::SortStrategy; +use cargo_plot::core::path_view::ViewMode; use clap::{Args, Parser, ValueEnum}; /// [POL]: Główny wrapper dla wtyczki Cargo. @@ -13,7 +14,7 @@ pub enum CargoCli { /// [POL]: Nasze docelowe argumenty CLI. Zauważ, że teraz to jest `Args`, a nie `Parser`. #[derive(Args, Debug)] -#[command(author, version, about = "Zaawansowany skaner struktury plików Rusta", long_about = None)] +#[command(author, version, about = "Zaawansowany skaner struktury plików", long_about = None)] pub struct CliArgs { /// [EN]: Input path to scan. /// [PL]: Ścieżka wejściowa do skanowania. @@ -27,17 +28,17 @@ pub struct CliArgs { /// [EN]: Display only matched paths. /// [PL]: Wyświetlaj tylko dopasowane ścieżki. - #[arg(long)] + #[arg(short = 'm', long = "on-match")] pub include: bool, /// [EN]: Display only rejected paths. /// [PL]: Wyświetlaj tylko odrzucone ścieżki. - #[arg(long)] + #[arg(short = 'x', long = "on-mismatch")] pub exclude: bool, /// [EN]: Ignore case. /// [PL]: Ignoruj wielkość liter. - #[arg(short = 'i', long = "ignore-case")] + #[arg(long = "ignore-case")] pub ignore_case: bool, /// [EN]: Results sorting strategy. @@ -45,9 +46,20 @@ pub struct CliArgs { #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] pub sort: CliSortStrategy, - /// [POL]: Wyświetla wyniki w formie hierarchicznego drzewa zamiast płaskiej listy. - #[arg(short = 't', long = "treeview", default_value_t = false)] - pub treeview: bool, + /// [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, + + /// [POL]: Ukrywa główny folder (root) w widoku drzewa. + #[arg(long = "treeview-no-root", default_value_t = false)] + pub no_root: bool, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] +pub enum CliViewMode { + Tree, + List, + Grid, } #[derive(Debug, Clone, Copy, ValueEnum)] @@ -82,3 +94,13 @@ impl From for SortStrategy { } } } + +impl From for ViewMode { + fn from(val: CliViewMode) -> Self { + match val { + CliViewMode::Tree => ViewMode::Tree, + CliViewMode::List => ViewMode::List, + CliViewMode::Grid => ViewMode::Grid, + } + } +} \ No newline at end of file diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 3ed9c4d..dd0e221 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,76 +1,106 @@ use crate::interfaces::cli::args::CliArgs; use cargo_plot::execuctor::for_many_patterns_tok::{self, SortStrategy}; +use cargo_plot::core::path_view::ViewMode; +use cargo_plot::core::path_matcher::stats::ShowMode; // lub dla jednego wzorca: // use cargo_plot::execuctor::for_one_pattern_raw::{self, SortStrategy}; -use cargo_plot::theme::for_path_list::get_icon_for_path; -use cargo_plot::core::path_treeview::{PathTree, WeightConfig}; -use cargo_plot::core::path_store::context::PathContext; +// use cargo_plot::theme::for_path_list::get_icon_for_path; /// [EN]: The execution engine (Cockpit). /// [PL]: Silnik wykonawczy (Kokpit). pub fn run(args: CliArgs) { let is_case_sensitive = !args.ignore_case; let sort_strategy: SortStrategy = args.sort.into(); + let view_mode: ViewMode = args.view.into(); - let mut show_include = args.include; - let mut show_exclude = args.exclude; - if !show_include && !show_exclude { - show_include = true; - show_exclude = true; - } + let show_mode = match (args.include, args.exclude) { + (true, false) => ShowMode::Include, // Tylko flaga -m + (false, true) => ShowMode::Exclude, // Tylko flaga -x + _ => ShowMode::Context, // Brak flag (lub podane obie) = pokazujemy wszystko + }; let stats = for_many_patterns_tok::execute( &args.enter_path, &args.patterns, is_case_sensitive, sort_strategy, - show_include, - show_exclude, - |file_stat| { - if !args.treeview { - println!( - "✅ MATCH: {} {} ({} B)", - get_icon_for_path(&file_stat.path), - file_stat.path, - file_stat.weight_bytes - ); - } - }, - |file_stat| { - if !args.treeview && show_exclude { - println!( - "❌ REJECT: {} {} ({} B)", - get_icon_for_path(&file_stat.path), - file_stat.path, - file_stat.weight_bytes - ); - } - }, + show_mode, + view_mode, + args.no_root, + |_| {}, // ⚡ Closure są puste, bo renderujemy PO zebraniu statystyk + |_| {}, + // |file_stat| { + // if !args.treeview { + // println!( + // "✅ MATCH: {} {} ({} B)", + // get_icon_for_path(&file_stat.path), + // file_stat.path, + // file_stat.weight_bytes + // ); + // } + // }, + // |file_stat| { + // if !args.treeview && show_exclude { + // println!( + // "❌ REJECT: {} {} ({} B)", + // get_icon_for_path(&file_stat.path), + // file_stat.path, + // file_stat.weight_bytes + // ); + // } + // }, ); - // ⚡ Logika Drzewa vs Listy - if args.treeview { - println!("🌲 Widok Drzewa:"); - - let weight_cfg = WeightConfig::default(); - - // ⚡ Rozwiązujemy ścieżkę z argumentów CLI w locie, żeby wiedzieć, gdzie szukać wag - let path_ctx = PathContext::resolve(&args.enter_path).unwrap_or_else(|e| { - eprintln!("❌ Błąd ścieżki: {}", e); - std::process::exit(1); - }); - - // Teraz mamy dostęp do `path_ctx.entry_absolute` i możemy przekazać to do Buildera! - let tree = PathTree::build( - &stats.included, - &path_ctx.entry_absolute, // ⚡ Używamy odzyskanej ścieżki - "dirs-first", - &weight_cfg - ); - - print!("{}", tree.render_cli()); + let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; + let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; + + // 2. RENDEROWANIE WYNIKÓW + match view_mode { + ViewMode::Grid => { + if do_include { + if let Some(grid) = &stats.included.grid { + println!("🌲 Widok Siatki (DOPASOWANE):"); + print!("{}", grid.render_cli()); + } + } + if do_exclude { + if let Some(grid) = &stats.excluded.grid { + println!("🌲 Widok Siatki (ODRZUCONE):"); + print!("{}", grid.render_cli()); + } + } + } + ViewMode::Tree => { + if do_include { + if let Some(tree) = &stats.included.tree { + println!("🌲 Widok Drzewa (DOPASOWANE):"); + print!("{}", tree.render_cli()); + } + } + if do_exclude { + if let Some(tree) = &stats.excluded.tree { + println!("🌲 Widok Drzewa (ODRZUCONE):"); + print!("{}", tree.render_cli()); + } + } + } + ViewMode::List => { + if do_include { + if let Some(list) = &stats.included.list { + // render_cli(true) -> dodaje zielony znaczek ✅ + print!("{}", list.render_cli(true)); + } + } + if do_exclude { + if let Some(list) = &stats.excluded.list { + // render_cli(false) -> dodaje czerwony znaczek ❌ + print!("{}", list.render_cli(false)); + } + } + } } + // 3. PODSUMOWANIE println!("----------"); println!( "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", @@ -80,4 +110,4 @@ pub fn run(args: CliArgs) { "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", stats.rejected, stats.total ); -} +} \ No newline at end of file From 6a69a5fb0261bf8751071d639f7670ba4b978584 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 21:27:51 +0100 Subject: [PATCH 17/45] (refactoring) --- src/core/path_view/tree.rs | 4 +- src/execuctor.rs | 2 - src/execuctor/for_one_pattern_raw.rs | 107 ------------------ .../for_many_patterns_tok.rs => execute.rs} | 0 src/interfaces/cli/engine.rs | 6 +- src/lib.rs | 2 +- src/theme/for_path_tree.rs | 2 + 7 files changed, 7 insertions(+), 116 deletions(-) delete mode 100644 src/execuctor.rs delete mode 100644 src/execuctor/for_one_pattern_raw.rs rename src/{execuctor/for_many_patterns_tok.rs => execute.rs} (100%) diff --git a/src/core/path_view/tree.rs b/src/core/path_view/tree.rs index 2d7f86b..b69813d 100644 --- a/src/core/path_view/tree.rs +++ b/src/core/path_view/tree.rs @@ -5,7 +5,7 @@ use colored::Colorize; // Importy z rodzeństwa i innych modułów core use super::node::FileNode; use crate::core::file_stats::weight::{self, WeightConfig, UnitSystem}; -use crate::theme::for_path_tree::{get_file_type, TreeStyle, DIR_ICON}; +use crate::theme::for_path_tree::{get_file_type, TreeStyle, DIR_ICON, FILE_ICON}; use crate::core::path_matcher::SortStrategy; pub struct PathTree { roots: Vec, @@ -47,7 +47,7 @@ impl PathTree { } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { get_file_type(ext).icon.to_string() } else { - "📄".to_string() + FILE_ICON.to_string() }; let absolute_path = base_path.join(path); diff --git a/src/execuctor.rs b/src/execuctor.rs deleted file mode 100644 index e691450..0000000 --- a/src/execuctor.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod for_many_patterns_tok; -pub mod for_one_pattern_raw; diff --git a/src/execuctor/for_one_pattern_raw.rs b/src/execuctor/for_one_pattern_raw.rs deleted file mode 100644 index 2a0dc5a..0000000 --- a/src/execuctor/for_one_pattern_raw.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::core::path_matcher::matcher::PathMatcher; -use crate::core::path_store::store::PathStore; -use crate::core::path_store::context::PathContext; -// [PL]: Reeksportujemy strategię, aby Kokpit nie musiał szukać jej w core. -pub use crate::core::path_matcher::sort::SortStrategy; -use crate::core::path_matcher::stats::MatchStats; -use crate::core::path_view::{PathList, PathTree, PathGrid}; -use crate::core::file_stats::weight::WeightConfig; -use crate::core::file_stats::FileStats; -use std::path::Path; - -/// [POL]: Egzekutor operujący na pojedynczym, surowym wzorcu wpisanym przez użytkownika. -/// [ENG]: Executor operating on a single, raw pattern provided by the user. -// #[allow(clippy::too_many_arguments)] -pub fn execute( - enter_path: &str, - raw_pattern: &str, - is_case_sensitive: bool, - sort_strategy: SortStrategy, - show_include: bool, - show_exclude: bool, - is_treeview: bool, - is_gridview: bool, - no_root: bool, - mut on_match: OnMatch, - mut on_mismatch: OnMismatch, -) -> MatchStats -where - OnMatch: FnMut(&FileStats), - OnMismatch: FnMut(&FileStats), -{ - // 1. Inicjalizacja kontekstów - let path_ctx = PathContext::resolve(enter_path).unwrap_or_else(|e| { - eprintln!("❌ {}", e); - std::process::exit(1); - }); - - // 2. Logowanie stanu początkowego - println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); - println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); - println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); - println!("---------------------------------------"); - println!("🔠 Wrażliwość na litery: {}", is_case_sensitive); - println!("🔍 Wzorzec (RAW): {:?}", raw_pattern); - println!("---------------------------------------"); - - // 3. Budowa silników dopasowujących (Generał) - let matcher = PathMatcher::new(raw_pattern, is_case_sensitive).expect("Błąd kompilacji wzorca"); - - // 4. Skanowanie dysku (Getter) - // [PL]: Ładujemy dane do rejestru z rdzenia - let paths_store = PathStore::scan(&path_ctx.entry_absolute); - // [PL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) - let paths_set = paths_store.get_index(); - - let entry_abs = path_ctx.entry_absolute.clone(); - - // 5. Ewaluacja i wykonanie callbacków - let mut stats = matcher.evaluate( - &paths_store.list, - &paths_set, - sort_strategy, - show_include, - show_exclude, - |raw_path| { - let s = FileStats::fetch(raw_path, &entry_abs); - on_match(&s); - }, - |raw_path| { - let s = FileStats::fetch(raw_path, &entry_abs); - on_mismatch(&s); - }, - ); - // 6. ⚡ MAGIA BUDOWANIA WIDOKÓW - let weight_cfg = WeightConfig::default(); - let root_name = if no_root { - None - } else { - Path::new(&path_ctx.entry_absolute).file_name().and_then(|n| n.to_str()) - }; - - if is_gridview { - if show_include { - stats.included.grid = Some(PathGrid::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); - } - if show_exclude { - stats.excluded.grid = Some(PathGrid::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); - } - } else if is_treeview { - if show_include { - stats.included.tree = Some(PathTree::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); - } - if show_exclude { - stats.excluded.tree = Some(PathTree::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); - } - } else { - if show_include { - stats.included.list = Some(PathList::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); - } - if show_exclude { - stats.excluded.list = Some(PathList::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); - } - } - - // 7. Zwracamy statystyki do Engine'u - stats -} diff --git a/src/execuctor/for_many_patterns_tok.rs b/src/execute.rs similarity index 100% rename from src/execuctor/for_many_patterns_tok.rs rename to src/execute.rs diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index dd0e221..39fc440 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,9 +1,7 @@ use crate::interfaces::cli::args::CliArgs; -use cargo_plot::execuctor::for_many_patterns_tok::{self, SortStrategy}; +use cargo_plot::execute::{self, SortStrategy}; use cargo_plot::core::path_view::ViewMode; use cargo_plot::core::path_matcher::stats::ShowMode; -// lub dla jednego wzorca: -// use cargo_plot::execuctor::for_one_pattern_raw::{self, SortStrategy}; // use cargo_plot::theme::for_path_list::get_icon_for_path; /// [EN]: The execution engine (Cockpit). @@ -19,7 +17,7 @@ pub fn run(args: CliArgs) { _ => ShowMode::Context, // Brak flag (lub podane obie) = pokazujemy wszystko }; - let stats = for_many_patterns_tok::execute( + let stats = execute::execute( &args.enter_path, &args.patterns, is_case_sensitive, diff --git a/src/lib.rs b/src/lib.rs index 0b73d80..7100c22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ pub mod core; -pub mod execuctor; +pub mod execute; pub mod theme; diff --git a/src/theme/for_path_tree.rs b/src/theme/for_path_tree.rs index a799ce7..f22b5d3 100644 --- a/src/theme/for_path_tree.rs +++ b/src/theme/for_path_tree.rs @@ -5,6 +5,8 @@ /// [PL]: Globalna ikona używana dla węzłów będących folderami. pub const DIR_ICON: &str = "📂"; +pub const FILE_ICON: &str = "📄"; + /// [EN]: Defines visual and metadata properties for a file type. /// [PL]: Definiuje wizualne i metadanowe właściwości dla typu pliku. pub struct PathFileType { From 73736702b2b1e6c8f36cfcbf3ef66a1c5c7c792c Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 22:26:26 +0100 Subject: [PATCH 18/45] (refactoring) --- src/core/path_matcher.rs | 6 +- src/core/path_matcher/matcher.rs | 178 +++++++++++++++++++++++++----- src/core/path_matcher/matchers.rs | 132 ---------------------- src/execute.rs | 12 +- src/interfaces/cli/engine.rs | 12 +- 5 files changed, 161 insertions(+), 179 deletions(-) delete mode 100644 src/core/path_matcher/matchers.rs diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index cde63cd..2ca5ec6 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -1,11 +1,9 @@ /// [POL]: Główny moduł logiki dopasowywania ścieżek. /// [ENG]: Core module for path matching logic. pub mod matcher; -pub mod matchers; pub mod sort; pub mod stats; -pub use self::matcher::PathMatcher; -pub use self::matchers::PathMatchers; +pub use self::matcher::{PathMatcher,PathMatchers}; pub use self::sort::SortStrategy; -pub use self::stats::MatchStats; +pub use self::stats::{MatchStats, ShowMode}; diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index 2a56813..d82c6e6 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -1,35 +1,43 @@ use super::sort::SortStrategy; -use super::stats::{MatchStats,ResultSet}; +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, - requires_sibling: bool, // [POL]: Flaga @ (para plik-folder) | [ENG]: Flag @ (file-directory pair) - requires_orphan: bool, // [POL]: Flaga $ (jednostronna relacja) | [ENG]: Flag $ (one-way relation) - is_deep: bool, // [POL]: Flaga + (rekurencyjne zacienianie) | [ENG]: Flag + (recursive shadowing) - base_name: String, // [POL]: Nazwa bazowa modułu do weryfikacji relacji | [ENG]: Base name of the module for relation verification - pub is_negated: bool, // [POL]: Flaga negacji (!). | [ENG]: Negation flag (!). + // [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 { - // [POL]: Kompiluje wzorzec tekstowy do wyrażenia regularnego, ekstrahując flagi sterujące. - // [ENG]: Compiles a text pattern into a regular expression, extracting control flags. - - // [POL]: Detekcja negacji. Jeśli obecny '!', oznaczamy i obcinamy go do dalszej analizy. - // [ENG]: Negation detection. If '!' is present, mark it and trim it for further analysis. 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 clean_pattern_str = actual_pattern.replace(['@', '$', '+'], ""); let base_name = clean_pattern_str .trim_end_matches('/') @@ -51,17 +59,6 @@ impl PathMatcher { let mut is_anchored = false; let mut p = clean_pattern_str.as_str(); - // [POL]: KLASYFIKACJA CELU DOPASOWANIA. Zmienna określa, czy wzorzec odnosi się wyłącznie do plików. - // Brak ukośnika '/' lub sekwencji '**' na końcu oznacza restrykcję do obiektów niebędących katalogami. - // [ENG]: MATCH TARGET CLASSIFICATION. This variable determines if the pattern is restricted to files only. - // The absence of a trailing slash '/' or the '**' sequence implies a restriction to non-directory objects. - // // let targets_file = !pattern.ends_with('/') && !pattern.ends_with("**"); - // [POL]: ANALIZA CIĄGU ZNORMALIZOWANEGO. Weryfikacja odbywa się na zmiennej 'p' (wzorzec bazowy), - // a nie na surowym 'pattern'. Gwarantuje to, że flagi sterujące (np. '@', '$', '+') nie zostaną - // błędnie zinterpretowane jako część ścieżki, co zafałszowałoby wykrycie intencji wzorca. - // [ENG]: NORMALISED STRING ANALYSIS. Verification is performed on variable 'p' (base pattern) - // instead of the raw 'pattern'. This ensures that control flags (e.g. '@', '$', '+') are not - // misinterpreted as path components, which would compromise the detection of the intended target type. let targets_file = !p.ends_with('/') && !p.ends_with("**"); if p.starts_with("./") { @@ -89,7 +86,6 @@ impl PathMatcher { } } '.' => re.push_str("\\."), - // '/' => re.push('/'), '/' => { if is_deep && i == chars.len() - 1 { // [POL]: Pominięcie końcowego ukośnika dla flagi '+'. @@ -223,8 +219,7 @@ impl PathMatcher { paths: I, env: &HashSet<&str>, strategy: SortStrategy, - show_include: bool, - show_exclude: bool, + show_mode: ShowMode, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) -> MatchStats @@ -266,13 +261,13 @@ impl PathMatcher { }, }; - if show_include { + if show_mode == ShowMode::Include || show_mode == ShowMode::Context { for path in &matched { on_match(path.as_ref()); } } - if show_exclude { + if show_mode == ShowMode::Exclude || show_mode == ShowMode::Context { for path in &mismatched { on_mismatch(path.as_ref()); } @@ -319,3 +314,128 @@ impl PathMatcher { 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 { + matched: matched.len(), + rejected: mismatched.len(), + total: matched.len() + mismatched.len(), + included: ResultSet { + paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), + tree: None, + list: None, + grid: None, + }, + excluded: 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 + } +} \ No newline at end of file diff --git a/src/core/path_matcher/matchers.rs b/src/core/path_matcher/matchers.rs deleted file mode 100644 index 1c0f9af..0000000 --- a/src/core/path_matcher/matchers.rs +++ /dev/null @@ -1,132 +0,0 @@ -use super::matcher::PathMatcher; -use super::sort::SortStrategy; -use super::stats::{MatchStats,ResultSet,ShowMode}; -use std::collections::HashSet; - -/// [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 { - // [POL]: Przetwarzanie wstępne wzorca (Brace Expansion). - // [ENG]: Pattern preprocessing (Brace Expansion). - // let expanded_patterns = expand_braces(pat.as_ref()); - for pat in patterns { - matchers.push(PathMatcher::new(pat.as_ref(), case_sensitive)?); - } - //} - Ok(Self { matchers }) - } - - /// [POL]: Weryfikuje, czy ścieżka pasuje do dowolnego ze skonfigurowanych wzorców (logika OR). - /// [ENG]: Verifies if the path matches any of the configured patterns (OR logic). - /// [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 // Zwykłe dopasowanie pozytywne (OR) - } else { - true // Jeśli użytkownik podał TYLKO wzorce z '!' (np. "!tests/"), domyślnie akceptujemy resztę. - } - } - - /// [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. - // #[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 { - matched: matched.len(), - rejected: mismatched.len(), - total: matched.len() + mismatched.len(), - included: ResultSet { - paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), - tree: None, - list: None, - grid: None, - }, - excluded: 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/execute.rs b/src/execute.rs index c350427..f5e0703 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -1,18 +1,14 @@ -use crate::core::path_matcher::matchers::PathMatchers; -use crate::core::path_matcher::stats::{MatchStats, ShowMode}; -use crate::core::path_store::context::PathContext; -use crate::core::path_store::store::PathStore; +pub use crate::core::path_matcher::SortStrategy; +use crate::core::path_matcher::{PathMatchers,MatchStats, ShowMode}; +use crate::core::path_store::{PathContext,PathStore}; use crate::core::patterns_expand::PatternContext; use crate::core::path_view::{PathList, PathTree, PathGrid, ViewMode}; use crate::core::file_stats::weight::WeightConfig; -use std::path::Path; -// [PL]: Reeksportujemy strategię, aby Kokpit nie musiał szukać jej w core. -pub use crate::core::path_matcher::sort::SortStrategy; use crate::core::file_stats::FileStats; +use std::path::Path; /// [POL]: Egzekutor operujący na wielu wzorcach (wersja po rozwinięciu klamer/tokenizacji). /// [ENG]: Executor operating on multiple patterns (post brace expansion/tokenisation). -// #[allow(clippy::too_many_arguments)] pub fn execute( enter_path: &str, patterns: &[String], diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 39fc440..b82d01a 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -57,13 +57,13 @@ pub fn run(args: CliArgs) { ViewMode::Grid => { if do_include { if let Some(grid) = &stats.included.grid { - println!("🌲 Widok Siatki (DOPASOWANE):"); + println!("✅ DOPASOWANIA"); print!("{}", grid.render_cli()); } } if do_exclude { if let Some(grid) = &stats.excluded.grid { - println!("🌲 Widok Siatki (ODRZUCONE):"); + println!("❌ ODRZUCENIA"); print!("{}", grid.render_cli()); } } @@ -71,13 +71,13 @@ pub fn run(args: CliArgs) { ViewMode::Tree => { if do_include { if let Some(tree) = &stats.included.tree { - println!("🌲 Widok Drzewa (DOPASOWANE):"); + println!("✅ DOPASOWANIA"); print!("{}", tree.render_cli()); } } if do_exclude { if let Some(tree) = &stats.excluded.tree { - println!("🌲 Widok Drzewa (ODRZUCONE):"); + println!("❌ ODRZUCENIA"); print!("{}", tree.render_cli()); } } @@ -85,13 +85,13 @@ pub fn run(args: CliArgs) { ViewMode::List => { if do_include { if let Some(list) = &stats.included.list { - // render_cli(true) -> dodaje zielony znaczek ✅ + println!("✅ DOPASOWANIA"); print!("{}", list.render_cli(true)); } } if do_exclude { if let Some(list) = &stats.excluded.list { - // render_cli(false) -> dodaje czerwony znaczek ❌ + println!("❌ ODRZUCENIA"); print!("{}", list.render_cli(false)); } } From 4d2af1e25b97029ec677424374d8ef305656b35c Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 22:57:02 +0100 Subject: [PATCH 19/45] (refactoring) --- src/core/path_matcher/stats.rs | 60 +++++++++++++++++++++++++++++++++- src/core/path_view/list.rs | 5 ++- src/execute.rs | 5 ++- src/interfaces/cli/engine.rs | 50 ++-------------------------- 4 files changed, 65 insertions(+), 55 deletions(-) diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs index 1cb9adb..7df3b01 100644 --- a/src/core/path_matcher/stats.rs +++ b/src/core/path_matcher/stats.rs @@ -1,4 +1,4 @@ -use crate::core::path_view::{PathList, PathTree, PathGrid}; +use crate::core::path_view::{PathList, PathTree, PathGrid, ViewMode}; /// [PL]: Podzbiór wyników zawierający surowe ścieżki i wygenerowane widoki. #[derive(Default)] @@ -25,4 +25,62 @@ pub enum ShowMode { Include, Exclude, Context, +} + +impl MatchStats { + /// ⚡ NOWOŚĆ: 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) -> 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 { + if let Some(grid) = &self.included.grid { + out.push_str("✅ DOPASOWANIA\n"); + out.push_str(&grid.render_cli()); + } + } + if do_exclude { + if let Some(grid) = &self.excluded.grid { + out.push_str("❌ ODRZUCENIA\n"); + out.push_str(&grid.render_cli()); + } + } + } + ViewMode::Tree => { + if do_include { + if let Some(tree) = &self.included.tree { + out.push_str("✅ DOPASOWANIA\n"); + out.push_str(&tree.render_cli()); + } + } + if do_exclude { + if let Some(tree) = &self.excluded.tree { + out.push_str("❌ ODRZUCENIA\n"); + out.push_str(&tree.render_cli()); + } + } + } + ViewMode::List => { + if do_include { + if let Some(list) = &self.included.list { + out.push_str("✅ DOPASOWANIA\n"); + out.push_str(&list.render_cli(true)); + } + } + if do_exclude { + if let Some(list) = &self.excluded.list { + out.push_str("❌ ODRZUCENIA\n"); + out.push_str(&list.render_cli(false)); + } + } + } + } + + out + } } \ No newline at end of file diff --git a/src/core/path_view/list.rs b/src/core/path_view/list.rs index 1a53823..463c776 100644 --- a/src/core/path_view/list.rs +++ b/src/core/path_view/list.rs @@ -45,13 +45,12 @@ impl PathList { /// [PL]: 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() }; + // let tag = if is_match { "✅ MATCH: ".green() } else { "❌ REJECT:".red() }; for item in &self.items { let line = format!( - "{} {} {} {}\n", + "{} {} {}\n", item.weight_str.truecolor(120, 120, 120), - tag, item.icon, if item.is_dir { item.name.yellow() } else { item.name.white() } ); diff --git a/src/execute.rs b/src/execute.rs index f5e0703..a458904 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -54,8 +54,6 @@ where // [PL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) let paths_set = paths_store.get_index(); - - let entry_abs = path_ctx.entry_absolute.clone(); // 6. Zwracamy statystyki do Engine'u let mut stats = matchers.evaluate( @@ -116,4 +114,5 @@ where } stats -} \ No newline at end of file +} + diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index b82d01a..d95d7ee 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -25,7 +25,7 @@ pub fn run(args: CliArgs) { show_mode, view_mode, args.no_root, - |_| {}, // ⚡ Closure są puste, bo renderujemy PO zebraniu statystyk + |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk |_| {}, // |file_stat| { // if !args.treeview { @@ -49,54 +49,8 @@ pub fn run(args: CliArgs) { // }, ); - let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; - let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; - // 2. RENDEROWANIE WYNIKÓW - match view_mode { - ViewMode::Grid => { - if do_include { - if let Some(grid) = &stats.included.grid { - println!("✅ DOPASOWANIA"); - print!("{}", grid.render_cli()); - } - } - if do_exclude { - if let Some(grid) = &stats.excluded.grid { - println!("❌ ODRZUCENIA"); - print!("{}", grid.render_cli()); - } - } - } - ViewMode::Tree => { - if do_include { - if let Some(tree) = &stats.included.tree { - println!("✅ DOPASOWANIA"); - print!("{}", tree.render_cli()); - } - } - if do_exclude { - if let Some(tree) = &stats.excluded.tree { - println!("❌ ODRZUCENIA"); - print!("{}", tree.render_cli()); - } - } - } - ViewMode::List => { - if do_include { - if let Some(list) = &stats.included.list { - println!("✅ DOPASOWANIA"); - print!("{}", list.render_cli(true)); - } - } - if do_exclude { - if let Some(list) = &stats.excluded.list { - println!("❌ ODRZUCENIA"); - print!("{}", list.render_cli(false)); - } - } - } - } + print!("{}", stats.render_output(view_mode, show_mode)); // 3. PODSUMOWANIE println!("----------"); From d7a756bbd138a2c55945630720770641e5abcbd3 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 23:39:12 +0100 Subject: [PATCH 20/45] (refactoring) --- src/core/path_matcher/matcher.rs | 16 ++++++++-------- src/core/path_matcher/stats.rs | 20 ++++++++++---------- src/execute.rs | 12 ++++++------ src/interfaces/cli/engine.rs | 4 ++-- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/core/path_matcher/matcher.rs b/src/core/path_matcher/matcher.rs index d82c6e6..1e6f1a5 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -244,16 +244,16 @@ impl PathMatcher { strategy.apply(&mut mismatched); let stats = MatchStats { - matched: matched.len(), - rejected: mismatched.len(), + m_size_matched: matched.len(), + x_size_mismatched: mismatched.len(), total: matched.len() + mismatched.len(), - included: ResultSet { + m_matched: ResultSet { paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), tree: None, list: None, grid: None, }, - excluded: ResultSet { + x_mismatched: ResultSet { paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), tree: None, list: None, @@ -407,16 +407,16 @@ impl PathMatchers { strategy.apply(&mut mismatched); let stats = MatchStats { - matched: matched.len(), - rejected: mismatched.len(), + m_size_matched: matched.len(), + x_size_mismatched: mismatched.len(), total: matched.len() + mismatched.len(), - included: ResultSet { + m_matched: ResultSet { paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), tree: None, list: None, grid: None, }, - excluded: ResultSet { + x_mismatched: ResultSet { paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), tree: None, list: None, diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs index 7df3b01..c00d057 100644 --- a/src/core/path_matcher/stats.rs +++ b/src/core/path_matcher/stats.rs @@ -13,11 +13,11 @@ pub struct ResultSet { // [PL]: Prosty obiekt statystyk, aby uniknąć ręcznego liczenia w Engine. #[derive(Default)] pub struct MatchStats { - pub matched: usize, - pub rejected: usize, + pub m_size_matched: usize, + pub x_size_mismatched: usize, pub total: usize, - pub included: ResultSet, - pub excluded: ResultSet, + pub m_matched: ResultSet, + pub x_mismatched: ResultSet, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -39,13 +39,13 @@ impl MatchStats { match view_mode { ViewMode::Grid => { if do_include { - if let Some(grid) = &self.included.grid { + if let Some(grid) = &self.m_matched.grid { out.push_str("✅ DOPASOWANIA\n"); out.push_str(&grid.render_cli()); } } if do_exclude { - if let Some(grid) = &self.excluded.grid { + if let Some(grid) = &self.x_mismatched.grid { out.push_str("❌ ODRZUCENIA\n"); out.push_str(&grid.render_cli()); } @@ -53,13 +53,13 @@ impl MatchStats { } ViewMode::Tree => { if do_include { - if let Some(tree) = &self.included.tree { + if let Some(tree) = &self.m_matched.tree { out.push_str("✅ DOPASOWANIA\n"); out.push_str(&tree.render_cli()); } } if do_exclude { - if let Some(tree) = &self.excluded.tree { + if let Some(tree) = &self.x_mismatched.tree { out.push_str("❌ ODRZUCENIA\n"); out.push_str(&tree.render_cli()); } @@ -67,13 +67,13 @@ impl MatchStats { } ViewMode::List => { if do_include { - if let Some(list) = &self.included.list { + if let Some(list) = &self.m_matched.list { out.push_str("✅ DOPASOWANIA\n"); out.push_str(&list.render_cli(true)); } } if do_exclude { - if let Some(list) = &self.excluded.list { + if let Some(list) = &self.x_mismatched.list { out.push_str("❌ ODRZUCENIA\n"); out.push_str(&list.render_cli(false)); } diff --git a/src/execute.rs b/src/execute.rs index a458904..801b4a1 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -89,26 +89,26 @@ where match view_mode { ViewMode::Grid => { if do_include { - stats.included.grid = Some(PathGrid::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + stats.m_matched.grid = Some(PathGrid::build(&stats.m_matched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); } if do_exclude { - stats.excluded.grid = Some(PathGrid::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + stats.x_mismatched.grid = Some(PathGrid::build(&stats.x_mismatched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); } } ViewMode::Tree => { if do_include { - stats.included.tree = Some(PathTree::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + stats.m_matched.tree = Some(PathTree::build(&stats.m_matched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); } if do_exclude { - stats.excluded.tree = Some(PathTree::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + stats.x_mismatched.tree = Some(PathTree::build(&stats.x_mismatched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); } } ViewMode::List => { if do_include { - stats.included.list = Some(PathList::build(&stats.included.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); + stats.m_matched.list = Some(PathList::build(&stats.m_matched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); } if do_exclude { - stats.excluded.list = Some(PathList::build(&stats.excluded.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); + stats.x_mismatched.list = Some(PathList::build(&stats.x_mismatched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); } } } diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index d95d7ee..0e3c70d 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -56,10 +56,10 @@ pub fn run(args: CliArgs) { println!("----------"); println!( "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", - stats.matched, stats.total + stats.m_size_matched, stats.total ); println!( "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", - stats.rejected, stats.total + stats.x_size_mismatched, stats.total ); } \ No newline at end of file From be294e63a59f0618bd5663d7ffa36c28e6afad1a Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Mon, 16 Mar 2026 23:42:25 +0100 Subject: [PATCH 21/45] (fmt) --- src/core.rs | 4 +- src/core/file_stats.rs | 18 ++-- src/core/file_stats/weight.rs | 13 ++- src/core/path_matcher.rs | 2 +- src/core/path_matcher/matcher.rs | 36 +++---- src/core/path_matcher/sort.rs | 8 +- src/core/path_matcher/stats.rs | 52 ++++------ src/core/path_view.rs | 10 +- src/core/path_view/grid.rs | 164 ++++++++++++++++++++++++------- src/core/path_view/list.rs | 17 ++-- src/core/path_view/node.rs | 22 ++--- src/core/path_view/tree.rs | 51 +++++++--- src/core/patterns_expand.rs | 21 ++-- src/execute.rs | 61 +++++++++--- src/interfaces/cli/args.rs | 2 +- src/interfaces/cli/engine.rs | 22 ++--- src/theme.rs | 2 +- src/theme/for_path_list.rs | 6 +- src/theme/for_path_tree.rs | 2 +- 19 files changed, 332 insertions(+), 181 deletions(-) diff --git a/src/core.rs b/src/core.rs index fb3a716..eb50076 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,5 +1,5 @@ +pub mod file_stats; pub mod path_matcher; pub mod path_store; -pub mod patterns_expand; pub mod path_view; -pub mod file_stats; \ No newline at end of file +pub mod patterns_expand; diff --git a/src/core/file_stats.rs b/src/core/file_stats.rs index de982e3..a1145c5 100644 --- a/src/core/file_stats.rs +++ b/src/core/file_stats.rs @@ -7,20 +7,20 @@ use self::weight::get_path_weight; /// [PL]: 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, + 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 { /// [PL]: 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()) @@ -32,4 +32,4 @@ impl FileStats { weight_bytes, } } -} \ No newline at end of file +} diff --git a/src/core/file_stats/weight.rs b/src/core/file_stats/weight.rs index e4bc936..b48a7bd 100644 --- a/src/core/file_stats/weight.rs +++ b/src/core/file_stats/weight.rs @@ -88,7 +88,12 @@ pub fn format_weight(bytes: u64, is_dir: bool, config: &WeightConfig) -> String }; if bytes == 0 { - return format!("[{:>3} {:>width$}] ", units[0], "0", width = config.precision); + return format!( + "[{:>3} {:>width$}] ", + units[0], + "0", + width = config.precision + ); } let bytes_f = bytes as f64; @@ -99,10 +104,12 @@ pub fn format_weight(bytes: u64, is_dir: bool, config: &WeightConfig) -> String let mut formatted_value = format!("{value:.10}"); if formatted_value.len() > config.precision { - formatted_value = formatted_value[..config.precision].trim_end_matches('.').to_string(); + 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}] ") -} \ No newline at end of file +} diff --git a/src/core/path_matcher.rs b/src/core/path_matcher.rs index 2ca5ec6..446bc2b 100644 --- a/src/core/path_matcher.rs +++ b/src/core/path_matcher.rs @@ -4,6 +4,6 @@ pub mod matcher; pub mod sort; pub mod stats; -pub use self::matcher::{PathMatcher,PathMatchers}; +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 index 1e6f1a5..617dce3 100644 --- a/src/core/path_matcher/matcher.rs +++ b/src/core/path_matcher/matcher.rs @@ -1,5 +1,5 @@ use super::sort::SortStrategy; -use super::stats::{MatchStats,ResultSet, ShowMode}; +use super::stats::{MatchStats, ResultSet, ShowMode}; use regex::Regex; use std::collections::HashSet; @@ -12,21 +12,21 @@ use std::collections::HashSet; pub struct PathMatcher { regex: Regex, targets_file: bool, - // [POL]: Flaga @ (para plik-folder) + // [POL]: Flaga @ (para plik-folder) // [ENG]: Flag @ (file-directory pair) - requires_sibling: bool, + requires_sibling: bool, // [POL]: Flaga $ (jednostronna relacja) // [ENG]: Flag $ (one-way relation) - requires_orphan: bool, - // [POL]: Flaga + (rekurencyjne zacienianie) + requires_orphan: bool, + // [POL]: Flaga + (rekurencyjne zacienianie) // [ENG]: Flag + (recursive shadowing) - is_deep: bool, - // [POL]: Nazwa bazowa modułu do weryfikacji relacji + is_deep: bool, + // [POL]: Nazwa bazowa modułu do weryfikacji relacji // [ENG]: Base name of the module for relation verification - base_name: String, + base_name: String, // [POL]: Flaga negacji (!). // [ENG]: Negation flag (!). - pub is_negated: bool, + pub is_negated: bool, } impl PathMatcher { @@ -115,8 +115,7 @@ impl PathMatcher { options.push(chars[i]); i += 1; } - let escaped: Vec = - options.split(',').map(regex::escape).collect(); + let escaped: Vec = options.split(',').map(regex::escape).collect(); re.push_str(&format!("(?:{})", escaped.join("|"))); } '[' => { @@ -251,7 +250,7 @@ impl PathMatcher { paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), tree: None, list: None, - grid: None, + grid: None, }, x_mismatched: ResultSet { paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), @@ -315,7 +314,6 @@ impl PathMatcher { } } - // ============================================================================== // ⚡ KONTENER WIELU WZORCÓW (PathMatchers) // ============================================================================== @@ -368,11 +366,7 @@ impl PathMatchers { // [POL]: Ostateczna decyzja na podstawie zebranych danych. // [ENG]: Final decision based on collected data. - if has_positive { - matched_positive - } else { - true - } + if has_positive { matched_positive } else { true } } /// [POL]: Ewaluuje zbiór ścieżek, sortuje je i wykonuje odpowiednie domknięcia. @@ -414,13 +408,13 @@ impl PathMatchers { paths: matched.iter().map(|s| s.as_ref().to_string()).collect(), tree: None, list: None, - grid: None, + grid: None, }, x_mismatched: ResultSet { paths: mismatched.iter().map(|s| s.as_ref().to_string()).collect(), tree: None, list: None, - grid: None, + grid: None, }, }; @@ -438,4 +432,4 @@ impl PathMatchers { stats } -} \ No newline at end of file +} diff --git a/src/core/path_matcher/sort.rs b/src/core/path_matcher/sort.rs index 466c04a..ea55131 100644 --- a/src/core/path_matcher/sort.rs +++ b/src/core/path_matcher/sort.rs @@ -97,9 +97,11 @@ impl SortStrategy { 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]; - } + && 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 index c00d057..8f99f3b 100644 --- a/src/core/path_matcher/stats.rs +++ b/src/core/path_matcher/stats.rs @@ -1,4 +1,4 @@ -use crate::core::path_view::{PathList, PathTree, PathGrid, ViewMode}; +use crate::core::path_view::{PathGrid, PathList, PathTree, ViewMode}; /// [PL]: Podzbiór wyników zawierający surowe ścieżki i wygenerowane widoki. #[derive(Default)] @@ -38,49 +38,37 @@ impl MatchStats { match view_mode { ViewMode::Grid => { - if do_include { - if let Some(grid) = &self.m_matched.grid { - out.push_str("✅ DOPASOWANIA\n"); - out.push_str(&grid.render_cli()); - } + if do_include && let Some(grid) = &self.m_matched.grid { + out.push_str("✅ DOPASOWANIA\n"); + out.push_str(&grid.render_cli()); } - if do_exclude { - if let Some(grid) = &self.x_mismatched.grid { - out.push_str("❌ ODRZUCENIA\n"); - out.push_str(&grid.render_cli()); - } + if do_exclude && let Some(grid) = &self.x_mismatched.grid { + out.push_str("❌ ODRZUCENIA\n"); + out.push_str(&grid.render_cli()); } } ViewMode::Tree => { - if do_include { - if let Some(tree) = &self.m_matched.tree { - out.push_str("✅ DOPASOWANIA\n"); - out.push_str(&tree.render_cli()); - } + if do_include && let Some(tree) = &self.m_matched.tree { + out.push_str("✅ DOPASOWANIA\n"); + out.push_str(&tree.render_cli()); } - if do_exclude { - if let Some(tree) = &self.x_mismatched.tree { - out.push_str("❌ ODRZUCENIA\n"); - out.push_str(&tree.render_cli()); - } + if do_exclude && let Some(tree) = &self.x_mismatched.tree { + out.push_str("❌ ODRZUCENIA\n"); + out.push_str(&tree.render_cli()); } } ViewMode::List => { - if do_include { - if let Some(list) = &self.m_matched.list { - out.push_str("✅ DOPASOWANIA\n"); - out.push_str(&list.render_cli(true)); - } + if do_include && let Some(list) = &self.m_matched.list { + out.push_str("✅ DOPASOWANIA\n"); + out.push_str(&list.render_cli(true)); } - if do_exclude { - if let Some(list) = &self.x_mismatched.list { - out.push_str("❌ ODRZUCENIA\n"); - out.push_str(&list.render_cli(false)); - } + if do_exclude && let Some(list) = &self.x_mismatched.list { + out.push_str("❌ ODRZUCENIA\n"); + out.push_str(&list.render_cli(false)); } } } out } -} \ No newline at end of file +} diff --git a/src/core/path_view.rs b/src/core/path_view.rs index 9fce4b1..890cad7 100644 --- a/src/core/path_view.rs +++ b/src/core/path_view.rs @@ -1,16 +1,16 @@ +pub mod grid; +pub mod list; pub mod node; pub mod tree; -pub mod list; -pub mod grid; // Re-eksportujemy dla wygody, aby w engine.rs używać PathTree i FileNode bezpośrednio -pub use tree::PathTree; -pub use list::PathList; pub use grid::PathGrid; +pub use list::PathList; +pub use tree::PathTree; #[derive(Debug, Clone, Copy, PartialEq)] pub enum ViewMode { Tree, List, Grid, -} \ No newline at end of file +} diff --git a/src/core/path_view/grid.rs b/src/core/path_view/grid.rs index e0a73a3..1793fca 100644 --- a/src/core/path_view/grid.rs +++ b/src/core/path_view/grid.rs @@ -1,11 +1,11 @@ +use colored::Colorize; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use colored::Colorize; use super::node::FileNode; -use crate::core::file_stats::weight::{self, WeightConfig, UnitSystem}; -use crate::theme::for_path_tree::{get_file_type, TreeStyle, DIR_ICON}; +use crate::core::file_stats::weight::{self, UnitSystem, WeightConfig}; use crate::core::path_matcher::SortStrategy; +use crate::theme::for_path_tree::{DIR_ICON, TreeStyle, get_file_type}; pub struct PathGrid { roots: Vec, @@ -27,7 +27,9 @@ impl PathGrid { let mut tree_map: BTreeMap> = BTreeMap::new(); for p in &paths { - let parent = p.parent().map_or_else(|| PathBuf::from("."), Path::to_path_buf); + let parent = p + .parent() + .map_or_else(|| PathBuf::from("."), Path::to_path_buf); tree_map.entry(parent).or_default().push(p.clone()); } @@ -38,7 +40,9 @@ impl PathGrid { sort_strategy: SortStrategy, weight_cfg: &WeightConfig, ) -> FileNode { - let name = path.file_name().map_or_else(|| "/".to_string(), |n| n.to_string_lossy().to_string()); + 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 is_dir { DIR_ICON.to_string() @@ -49,7 +53,8 @@ impl PathGrid { }; let absolute_path = base_path.join(path); - let mut weight_bytes = weight::get_path_weight(&absolute_path, weight_cfg.dir_sum_included); + 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) { @@ -67,14 +72,24 @@ impl PathGrid { } 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 } + 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()) + parent.is_none() + || parent.unwrap() == Path::new("") + || !paths.contains(&parent.unwrap().to_path_buf()) }) .cloned() .collect(); @@ -89,15 +104,27 @@ impl PathGrid { let final_roots = if let Some(r_name) = root_name { let empty_weight = if weight_cfg.system != UnitSystem::None { " ".repeat(7 + weight_cfg.precision) - } else { String::new() }; + } else { + String::new() + }; vec![FileNode { - name: r_name.to_string(), path: PathBuf::from(r_name), is_dir: true, - icon: DIR_ICON.to_string(), weight_str: empty_weight, weight_bytes: 0, children: top_nodes, + name: r_name.to_string(), + path: PathBuf::from(r_name), + is_dir: true, + icon: DIR_ICON.to_string(), + weight_str: empty_weight, + weight_bytes: 0, + children: top_nodes, }] - } else { top_nodes }; + } else { + top_nodes + }; - Self { roots: final_roots, style: TreeStyle::default() } + Self { + roots: final_roots, + style: TreeStyle::default(), + } } #[must_use] @@ -118,15 +145,34 @@ impl PathGrid { (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 }; + } 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; } + 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 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; } + if child_max > max { + max = child_max; + } } } max @@ -144,37 +190,87 @@ impl PathGrid { (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 }; + } 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 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 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 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 }; + 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) }; + 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) }; + 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 } -} \ No newline at end of file +} diff --git a/src/core/path_view/list.rs b/src/core/path_view/list.rs index 463c776..85c23d2 100644 --- a/src/core/path_view/list.rs +++ b/src/core/path_view/list.rs @@ -1,8 +1,8 @@ -use colored::Colorize; -use crate::theme::for_path_list::get_icon_for_path; 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; /// [PL]: Zarządca wyświetlania wyników w formie płaskiej listy. pub struct PathList { items: Vec, @@ -22,9 +22,10 @@ impl PathList { .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_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, @@ -43,7 +44,7 @@ impl PathList { } /// [PL]: Renderuje listę dla terminala (z kolorami i ikonami). - pub fn render_cli(&self, is_match: bool) -> String { + pub fn render_cli(&self, _is_match: bool) -> String { let mut out = String::new(); // let tag = if is_match { "✅ MATCH: ".green() } else { "❌ REJECT:".red() }; @@ -52,7 +53,11 @@ impl PathList { "{} {} {}\n", item.weight_str.truecolor(120, 120, 120), item.icon, - if item.is_dir { item.name.yellow() } else { item.name.white() } + if item.is_dir { + item.name.yellow() + } else { + item.name.white() + } ); out.push_str(&line); } diff --git a/src/core/path_view/node.rs b/src/core/path_view/node.rs index c208cae..3c70747 100644 --- a/src/core/path_view/node.rs +++ b/src/core/path_view/node.rs @@ -1,5 +1,5 @@ -use std::path::PathBuf; use crate::core::path_matcher::SortStrategy; +use std::path::PathBuf; /// [PL]: Reprezentuje pojedynczy węzeł w drzewie systemu plików. #[derive(Debug, Clone)] @@ -23,7 +23,7 @@ impl FileNode { 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); @@ -50,12 +50,8 @@ impl FileNode { } // 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)) - } + 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), } @@ -64,11 +60,11 @@ impl FileNode { /// [PL]: Wyciąga rdzeń nazwy do grupowania (np. "main.rs" -> "main"). fn get_merge_key(name: &str) -> &str { - if let Some(idx) = name.rfind('.') { - if idx > 0 { - return &name[..idx]; - } + if let Some(idx) = name.rfind('.') + && idx > 0 + { + return &name[..idx]; } name } -} \ No newline at end of file +} diff --git a/src/core/path_view/tree.rs b/src/core/path_view/tree.rs index b69813d..268b16d 100644 --- a/src/core/path_view/tree.rs +++ b/src/core/path_view/tree.rs @@ -1,12 +1,12 @@ +use colored::Colorize; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use colored::Colorize; // Importy z rodzeństwa i innych modułów core use super::node::FileNode; -use crate::core::file_stats::weight::{self, WeightConfig, UnitSystem}; -use crate::theme::for_path_tree::{get_file_type, TreeStyle, DIR_ICON, FILE_ICON}; +use crate::core::file_stats::weight::{self, UnitSystem, 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, @@ -26,7 +26,9 @@ impl PathTree { let mut tree_map: BTreeMap> = BTreeMap::new(); for p in &paths { - let parent = p.parent().map_or_else(|| PathBuf::from("."), Path::to_path_buf); + let parent = p + .parent() + .map_or_else(|| PathBuf::from("."), Path::to_path_buf); tree_map.entry(parent).or_default().push(p.clone()); } @@ -51,7 +53,8 @@ impl PathTree { }; let absolute_path = base_path.join(path); - let mut weight_bytes = weight::get_path_weight(&absolute_path, weight_cfg.dir_sum_included); + 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) { @@ -70,14 +73,24 @@ impl PathTree { 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 } + 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()) + parent.is_none() + || parent.unwrap() == Path::new("") + || !paths.contains(&parent.unwrap().to_path_buf()) }) .cloned() .collect(); @@ -109,14 +122,21 @@ impl PathTree { top_nodes }; - Self { roots: final_roots, style: TreeStyle::default() } + Self { + roots: final_roots, + style: TreeStyle::default(), + } } #[must_use] - pub fn render_cli(&self) -> String { self.plot(&self.roots, "", true) } + pub fn render_cli(&self) -> String { + self.plot(&self.roots, "", true) + } #[must_use] - pub fn render_txt(&self) -> String { self.plot(&self.roots, "", false) } + 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(); @@ -147,14 +167,16 @@ impl PathTree { let line = if use_color { if node.is_dir { - format!("{weight_prefix}{}{branch_color} {icon} {name}\n", + 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", + format!( + "{weight_prefix}{}{branch_color} {icon} {name}\n", indent.green(), branch_color = branch.green(), icon = node.icon, @@ -162,7 +184,8 @@ impl PathTree { ) } } else { - format!("{weight_prefix}{indent}{branch} {icon} {name}\n", + format!( + "{weight_prefix}{indent}{branch} {icon} {name}\n", icon = node.icon, name = node.name ) @@ -181,4 +204,4 @@ impl PathTree { } result } -} \ No newline at end of file +} diff --git a/src/core/patterns_expand.rs b/src/core/patterns_expand.rs index 04a0c2e..9354ecf 100644 --- a/src/core/patterns_expand.rs +++ b/src/core/patterns_expand.rs @@ -30,18 +30,19 @@ impl PatternContext { /// [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]; + && 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; + 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/execute.rs b/src/execute.rs index 801b4a1..d05cef7 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -1,10 +1,10 @@ +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::{PathMatchers,MatchStats, ShowMode}; -use crate::core::path_store::{PathContext,PathStore}; +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 crate::core::path_view::{PathList, PathTree, PathGrid, ViewMode}; -use crate::core::file_stats::weight::WeightConfig; -use crate::core::file_stats::FileStats; use std::path::Path; /// [POL]: Egzekutor operujący na wielu wzorcach (wersja po rozwinięciu klamer/tokenizacji). @@ -78,7 +78,9 @@ where let root_name = if no_root { None } else { - Path::new(&path_ctx.entry_absolute).file_name().and_then(|n| n.to_str()) + Path::new(&path_ctx.entry_absolute) + .file_name() + .and_then(|n| n.to_str()) }; // Pomocnicze flagi do budowania (żeby kod w match był krótki) @@ -89,30 +91,63 @@ where 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)); + stats.m_matched.grid = Some(PathGrid::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + )); } if do_exclude { - stats.x_mismatched.grid = Some(PathGrid::build(&stats.x_mismatched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + stats.x_mismatched.grid = Some(PathGrid::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + )); } } 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)); + stats.m_matched.tree = Some(PathTree::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + )); } if do_exclude { - stats.x_mismatched.tree = Some(PathTree::build(&stats.x_mismatched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, root_name)); + stats.x_mismatched.tree = Some(PathTree::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + )); } } ViewMode::List => { if do_include { - stats.m_matched.list = Some(PathList::build(&stats.m_matched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); + stats.m_matched.list = Some(PathList::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + )); } if do_exclude { - stats.x_mismatched.list = Some(PathList::build(&stats.x_mismatched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg)); + stats.x_mismatched.list = Some(PathList::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + )); } } } stats } - diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index c483beb..6c9c8ef 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -103,4 +103,4 @@ impl From for ViewMode { CliViewMode::Grid => ViewMode::Grid, } } -} \ No newline at end of file +} diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 0e3c70d..6aec149 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,7 +1,7 @@ use crate::interfaces::cli::args::CliArgs; -use cargo_plot::execute::{self, SortStrategy}; -use cargo_plot::core::path_view::ViewMode; use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::path_view::ViewMode; +use cargo_plot::execute::{self, SortStrategy}; // use cargo_plot::theme::for_path_list::get_icon_for_path; /// [EN]: The execution engine (Cockpit). @@ -27,12 +27,12 @@ pub fn run(args: CliArgs) { args.no_root, |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk |_| {}, - // |file_stat| { + // |file_stat| { // if !args.treeview { // println!( - // "✅ MATCH: {} {} ({} B)", - // get_icon_for_path(&file_stat.path), - // file_stat.path, + // "✅ MATCH: {} {} ({} B)", + // get_icon_for_path(&file_stat.path), + // file_stat.path, // file_stat.weight_bytes // ); // } @@ -40,9 +40,9 @@ pub fn run(args: CliArgs) { // |file_stat| { // if !args.treeview && show_exclude { // println!( - // "❌ REJECT: {} {} ({} B)", - // get_icon_for_path(&file_stat.path), - // file_stat.path, + // "❌ REJECT: {} {} ({} B)", + // get_icon_for_path(&file_stat.path), + // file_stat.path, // file_stat.weight_bytes // ); // } @@ -50,7 +50,7 @@ pub fn run(args: CliArgs) { ); // 2. RENDEROWANIE WYNIKÓW - print!("{}", stats.render_output(view_mode, show_mode)); + print!("{}", stats.render_output(view_mode, show_mode)); // 3. PODSUMOWANIE println!("----------"); @@ -62,4 +62,4 @@ pub fn run(args: CliArgs) { "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", stats.x_size_mismatched, stats.total ); -} \ No newline at end of file +} diff --git a/src/theme.rs b/src/theme.rs index 26ae589..0735c38 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,2 +1,2 @@ pub mod for_path_list; -pub mod for_path_tree; \ No newline at end of file +pub mod for_path_tree; diff --git a/src/theme/for_path_list.rs b/src/theme/for_path_list.rs index 1628029..c364d35 100644 --- a/src/theme/for_path_list.rs +++ b/src/theme/for_path_list.rs @@ -3,7 +3,11 @@ 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 nazwa = path + .trim_end_matches('/') + .split('/') + .next_back() + .unwrap_or(""); let is_hidden = nazwa.starts_with('.'); match (is_dir, is_hidden) { diff --git a/src/theme/for_path_tree.rs b/src/theme/for_path_tree.rs index f22b5d3..6df8eba 100644 --- a/src/theme/for_path_tree.rs +++ b/src/theme/for_path_tree.rs @@ -105,4 +105,4 @@ impl Default for TreeStyle { indent_mid: "│ ".to_string(), } } -} \ No newline at end of file +} From f56ae13a1b2f1c4ae7c8f346db77046ed43e8da3 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 08:28:30 +0100 Subject: [PATCH 22/45] (add: hide info) --- .gitignore | 3 ++- src/core/file_stats.rs | 4 +-- src/core/file_stats/weight.rs | 10 +++---- src/core/path_matcher/stats.rs | 22 ++++++++-------- src/core/path_store/store.rs | 4 +-- src/core/path_view/list.rs | 6 ++--- src/core/path_view/node.rs | 6 ++--- src/execute.rs | 25 +++++++++++------- src/interfaces.rs | 4 +-- src/interfaces/cli.rs | 18 ++++++------- src/interfaces/cli/args.rs | 48 ++++++++++++++++++---------------- src/interfaces/cli/engine.rs | 29 +++++++++++--------- src/interfaces/tui.rs | 4 +-- src/main.rs | 19 +++++++++----- src/theme/for_path_tree.rs | 36 ++++++++++++------------- 15 files changed, 130 insertions(+), 108 deletions(-) diff --git a/.gitignore b/.gitignore index 0ecb0f5..7585494 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target/ *.exe -*.lock \ No newline at end of file +*.lock +/new/ \ No newline at end of file diff --git a/src/core/file_stats.rs b/src/core/file_stats.rs index a1145c5..d911f51 100644 --- a/src/core/file_stats.rs +++ b/src/core/file_stats.rs @@ -4,7 +4,7 @@ pub mod weight; use self::weight::get_path_weight; -/// [PL]: Struktura przechowująca metadane (statystyki) pliku lub folderu. +/// [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") @@ -17,7 +17,7 @@ pub struct FileStats { } impl FileStats { - /// [PL]: Pobiera statystyki pliku bezpośrednio z dysku. + /// [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); diff --git a/src/core/file_stats/weight.rs b/src/core/file_stats/weight.rs index b48a7bd..297e01c 100644 --- a/src/core/file_stats/weight.rs +++ b/src/core/file_stats/weight.rs @@ -1,5 +1,5 @@ -// [EN]: Logic for calculating and formatting file and directory weights. -// [PL]: Logika obliczania i formatowania wag plików oraz folderów. +// [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; @@ -33,7 +33,7 @@ impl Default for WeightConfig { } } -/// [PL]: Pobiera wagę ścieżki (plik lub folder 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, @@ -51,7 +51,7 @@ pub fn get_path_weight(path: &Path, sum_included_only: bool) -> u64 { 0 } -/// [PL]: Prywatny pomocnik do liczenia rozmiaru folderu na dysku. +/// [POL]: Prywatny pomocnik do liczenia rozmiaru folderu na dysku. fn get_dir_size(path: &Path) -> u64 { fs::read_dir(path) .map(|entries| { @@ -70,7 +70,7 @@ fn get_dir_size(path: &Path) -> u64 { .unwrap_or(0) } -/// [PL]: Formatuje bajty na czytelny ciąg znaków (np. [kB 12.34]). +/// [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(); diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs index 8f99f3b..0afe991 100644 --- a/src/core/path_matcher/stats.rs +++ b/src/core/path_matcher/stats.rs @@ -1,6 +1,6 @@ use crate::core::path_view::{PathGrid, PathList, PathTree, ViewMode}; -/// [PL]: Podzbiór wyników zawierający surowe ścieżki i wygenerowane widoki. +/// [POL]: Podzbiór wyników zawierający surowe ścieżki i wygenerowane widoki. #[derive(Default)] pub struct ResultSet { pub paths: Vec, @@ -9,8 +9,8 @@ pub struct ResultSet { pub grid: Option, } -// [EN]: Simple stats object to avoid manual counting in the Engine. -// [PL]: Prosty obiekt statystyk, aby uniknąć ręcznego liczenia w Engine. +// [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, @@ -28,10 +28,10 @@ pub enum ShowMode { } impl MatchStats { - /// ⚡ NOWOŚĆ: Hermetyzacja renderowania po stronie rdzenia. + /// : 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) -> String { + pub fn render_output(&self, view_mode: ViewMode, show_mode: ShowMode, print_info: 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; @@ -39,31 +39,31 @@ impl MatchStats { match view_mode { ViewMode::Grid => { if do_include && let Some(grid) = &self.m_matched.grid { - out.push_str("✅ DOPASOWANIA\n"); + if print_info { out.push_str("✅\n");} out.push_str(&grid.render_cli()); } if do_exclude && let Some(grid) = &self.x_mismatched.grid { - out.push_str("❌ ODRZUCENIA\n"); + if print_info { out.push_str("❌\n");} out.push_str(&grid.render_cli()); } } ViewMode::Tree => { if do_include && let Some(tree) = &self.m_matched.tree { - out.push_str("✅ DOPASOWANIA\n"); + if print_info { out.push_str("✅\n");} out.push_str(&tree.render_cli()); } if do_exclude && let Some(tree) = &self.x_mismatched.tree { - out.push_str("❌ ODRZUCENIA\n"); + if print_info { out.push_str("❌\n");} out.push_str(&tree.render_cli()); } } ViewMode::List => { if do_include && let Some(list) = &self.m_matched.list { - out.push_str("✅ DOPASOWANIA\n"); + if print_info { out.push_str("✅\n");} out.push_str(&list.render_cli(true)); } if do_exclude && let Some(list) = &self.x_mismatched.list { - out.push_str("❌ ODRZUCENIA\n"); + if print_info { out.push_str("❌\n");} out.push_str(&list.render_cli(false)); } } diff --git a/src/core/path_store/store.rs b/src/core/path_store/store.rs index 1186afb..fbda9f8 100644 --- a/src/core/path_store/store.rs +++ b/src/core/path_store/store.rs @@ -48,8 +48,8 @@ impl PathStore { Self { list } } - // [EN]: Creates a temporary pool of references for the matcher. - // [PL]: Tworzy tymczasową pulę referencji (paths_pool) dla matchera. + // [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/list.rs b/src/core/path_view/list.rs index 85c23d2..b72f8ee 100644 --- a/src/core/path_view/list.rs +++ b/src/core/path_view/list.rs @@ -3,13 +3,13 @@ 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; -/// [PL]: Zarządca wyświetlania wyników w formie płaskiej listy. +/// [POL]: Zarządca wyświetlania wyników w formie płaskiej listy. pub struct PathList { items: Vec, } impl PathList { - /// [PL]: Buduje listę na podstawie zbioru ścieżek i statystyk. + /// [POL]: Buduje listę na podstawie zbioru ścieżek i statystyk. pub fn build( paths_strings: &[String], base_dir: &str, @@ -43,7 +43,7 @@ impl PathList { Self { items } } - /// [PL]: Renderuje listę dla terminala (z kolorami i ikonami). + /// [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() }; diff --git a/src/core/path_view/node.rs b/src/core/path_view/node.rs index 3c70747..c7a58cb 100644 --- a/src/core/path_view/node.rs +++ b/src/core/path_view/node.rs @@ -1,7 +1,7 @@ use crate::core::path_matcher::SortStrategy; use std::path::PathBuf; -/// [PL]: Reprezentuje pojedynczy węzeł w drzewie systemu plików. +/// [POL]: Reprezentuje pojedynczy węzeł w drzewie systemu plików. #[derive(Debug, Clone)] pub struct FileNode { pub name: String, @@ -14,7 +14,7 @@ pub struct FileNode { } impl FileNode { - /// [PL]: Sortuje listę węzłów w miejscu zgodnie z wybraną strategią. + /// [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; @@ -58,7 +58,7 @@ impl FileNode { }); } - /// [PL]: Wyciąga rdzeń nazwy do grupowania (np. "main.rs" -> "main"). + /// [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 diff --git a/src/execute.rs b/src/execute.rs index d05cef7..da3f522 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -17,6 +17,7 @@ pub fn execute( show_mode: ShowMode, view_mode: ViewMode, no_root: bool, + print_info: bool, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) -> MatchStats @@ -35,23 +36,27 @@ where }); // 2. Logowanie stanu początkowego - println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); - println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); - println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); - println!("---------------------------------------"); - println!("🔠 Wrażliwość na litery: {}", is_case_sensitive); - println!("🔍 Wzorce (RAW): {:?}", pattern_ctx.raw); - println!("⚙️ Wzorce (TOK): {:?}", pattern_ctx.tok); - println!("---------------------------------------"); + if print_info { + println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); + println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); + println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); + println!("---------------------------------------"); + println!("🔠 Wrażliwość na litery: {}", is_case_sensitive); + println!("🔍 Wzorce (RAW): {:?}", pattern_ctx.raw); + println!("⚙️ Wzorce (TOK): {:?}", pattern_ctx.tok); + println!("---------------------------------------"); + } else { + println!("---------------------------------------"); + } // 3. Budowa silników dopasowujących (Generał) let matchers = PathMatchers::new(&pattern_ctx.tok, is_case_sensitive).expect("Błąd kompilacji wzorców"); // 4. Skanowanie dysku (Getter) - // [PL]: Ładujemy dane do rejestru z rdzenia + // [POL]: Ładujemy dane do rejestru z rdzenia let paths_store = PathStore::scan(&path_ctx.entry_absolute); - // [PL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) + // [POL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) let paths_set = paths_store.get_index(); let entry_abs = path_ctx.entry_absolute.clone(); diff --git a/src/interfaces.rs b/src/interfaces.rs index e672806..38acfe3 100644 --- a/src/interfaces.rs +++ b/src/interfaces.rs @@ -1,5 +1,5 @@ -// [EN]: User interaction layer (Ports and Adapters). -// [PL]: Warstwa interakcji z użytkownikiem (Porty i Adaptery). +// [ENG]: User interaction layer (Ports and Adapters). +// [POL]: Warstwa interakcji z użytkownikiem (Porty i Adaptery). pub mod cli; pub mod tui; diff --git a/src/interfaces/cli.rs b/src/interfaces/cli.rs index 51adb29..949efcc 100644 --- a/src/interfaces/cli.rs +++ b/src/interfaces/cli.rs @@ -4,26 +4,26 @@ pub mod engine; use self::args::CargoCli; use clap::Parser; -// [EN]: Main entry point for the CLI interface. -// [PL]: Główny punkt wejścia dla interfejsu CLI. +// [ENG]: Main entry point for the CLI interface. +// [POL]: Główny punkt wejścia dla interfejsu CLI. pub fn run_cli() { - // [PL]: Pobieramy surowe argumenty bezpośrednio z systemu. + // [POL]: Pobieramy surowe argumenty bezpośrednio z systemu. let args_os = std::env::args(); let mut args: Vec = args_os.collect(); - // [EN]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. + // [ENG]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. // We insert it manually so the parser matches the Cargo plugin structure. - // [PL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. + // [POL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. // Wstawiamy go ręcznie, aby parser pasował do struktury wtyczki Cargo. if args.len() > 1 && args[1] != "plot" { args.insert(1, "plot".to_string()); } - // [EN]: Now parse from the modified list. - // [PL]: Teraz parsujemy ze zmodyfikowanej listy. + // [ENG]: Now parse from the modified list. + // [POL]: Teraz parsujemy ze zmodyfikowanej listy. let CargoCli::Plot(flags) = CargoCli::parse_from(args); - // [EN]: Transfer control to our execution engine. - // [PL]: Przekazanie kontroli do naszego silnika wykonawczego. + // [ENG]: Transfer control to our execution engine. + // [POL]: Przekazanie kontroli do naszego silnika wykonawczego. engine::run(flags); } diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 6c9c8ef..44094d3 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -7,8 +7,8 @@ use clap::{Args, Parser, ValueEnum}; #[derive(Parser, Debug)] #[command(name = "cargo", bin_name = "cargo")] pub enum CargoCli { - /// [EN]: Cargo plot subcommand. - /// [PL]: Podkomenda cargo plot. + /// [ENG]: Cargo plot subcommand. + /// [POL]: Podkomenda cargo plot. Plot(CliArgs), } @@ -16,43 +16,47 @@ pub enum CargoCli { #[derive(Args, Debug)] #[command(author, version, about = "Zaawansowany skaner struktury plików", long_about = None)] pub struct CliArgs { - /// [EN]: Input path to scan. - /// [PL]: Ścieżka wejściowa do skanowania. + /// [ENG]: Input path to scan. + /// [POL]: Ścieżka wejściowa do skanowania. #[arg(short = 'd', long = "dir", default_value = ".")] pub enter_path: String, - /// [EN]: Match patterns. - /// [PL]: Wzorce dopasowań. + /// [ENG]: Match patterns. + /// [POL]: Wzorce dopasowań. #[arg(short = 'p', long = "pat", required = true)] pub patterns: Vec, - /// [EN]: Display only matched paths. - /// [PL]: Wyświetlaj tylko dopasowane ścieżki. + /// [ENG]: Results sorting strategy. + /// [POL]: Strategia sortowania wyników. + #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] + pub sort: CliSortStrategy, + + /// [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]: Display only matched paths. + /// [POL]: Wyświetlaj tylko dopasowane ścieżki. #[arg(short = 'm', long = "on-match")] pub include: bool, - /// [EN]: Display only rejected paths. - /// [PL]: Wyświetlaj tylko odrzucone ścieżki. + /// [ENG]: Display only rejected paths. + /// [POL]: Wyświetlaj tylko odrzucone ścieżki. #[arg(short = 'x', long = "on-mismatch")] pub exclude: bool, - /// [EN]: Ignore case. - /// [PL]: Ignoruj wielkość liter. + /// [ENG]: Ignore case. + /// [POL]: Ignoruj wielkość liter. #[arg(long = "ignore-case")] - pub ignore_case: bool, - - /// [EN]: Results sorting strategy. - /// [PL]: Strategia sortowania wyników. - #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] - pub sort: CliSortStrategy, - - /// [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, + pub ignore_case: bool, /// [POL]: Ukrywa główny folder (root) w widoku drzewa. #[arg(long = "treeview-no-root", default_value_t = false)] pub no_root: bool, + + /// [POL]: Wyświetla dodatkowe informacje, statystyki i nagłówki (tryb gadatliwy). + #[arg(short = 'i', long = "info", default_value_t = false)] + pub info: bool, } #[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 6aec149..15713bf 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -4,8 +4,8 @@ use cargo_plot::core::path_view::ViewMode; use cargo_plot::execute::{self, SortStrategy}; // use cargo_plot::theme::for_path_list::get_icon_for_path; -/// [EN]: The execution engine (Cockpit). -/// [PL]: Silnik wykonawczy (Kokpit). +/// [ENG]: The execution engine (Cockpit). +/// [POL]: Silnik wykonawczy (Kokpit). pub fn run(args: CliArgs) { let is_case_sensitive = !args.ignore_case; let sort_strategy: SortStrategy = args.sort.into(); @@ -25,6 +25,7 @@ pub fn run(args: CliArgs) { show_mode, view_mode, args.no_root, + args.info, |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk |_| {}, // |file_stat| { @@ -50,16 +51,20 @@ pub fn run(args: CliArgs) { ); // 2. RENDEROWANIE WYNIKÓW - print!("{}", stats.render_output(view_mode, show_mode)); + print!("{}", stats.render_output(view_mode, show_mode, args.info)); // 3. PODSUMOWANIE - println!("----------"); - println!( - "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", - stats.m_size_matched, stats.total - ); - println!( - "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", - stats.x_size_mismatched, stats.total - ); + if args.info { + println!("---------------------------------------"); + println!( + "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", + stats.m_size_matched, stats.total + ); + println!( + "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", + stats.x_size_mismatched, stats.total + ); + } else { + println!("---------------------------------------"); + } } diff --git a/src/interfaces/tui.rs b/src/interfaces/tui.rs index 00c97c0..a70e7de 100644 --- a/src/interfaces/tui.rs +++ b/src/interfaces/tui.rs @@ -1,5 +1,5 @@ -// [EN]: Interactive Terminal User Interface (TUI) module registry. -// [PL]: Rejestr modułu interaktywnego interfejsu tekstowego (TUI). +// [ENG]: Interactive Terminal User Interface (TUI) module registry. +// [POL]: Rejestr modułu interaktywnego interfejsu tekstowego (TUI). pub mod i18n; pub mod menu; diff --git a/src/main.rs b/src/main.rs index 121f50d..3f43b6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -// [EN]: Main entry point switching between interactive TUI and automated CLI. -// [PL]: Główny punkt wejścia przełączający między interaktywnym TUI a automatycznym CLI. +// [ENG]: Main entry point switching between interactive TUI and automated CLI. +// [POL]: Główny punkt wejścia przełączający między interaktywnym TUI a automatycznym CLI. #![allow(clippy::pedantic, clippy::struct_excessive_bools)] @@ -12,11 +12,18 @@ fn main() { // przejmie sygnał i bezpiecznie wyjdzie z prompta. ctrlc::set_handler(move || {}).expect("Błąd podczas ustawiania handlera Ctrl+C"); - // [EN]: Start TUI if no arguments are provided. - if env::args().len() <= 2 { + let args: Vec = env::args().collect(); + + // [POL]: Uruchom TUI tylko jeśli: + // 1. Brak argumentów (tylko nazwa pliku binarnego) -> len == 1 + // 2. Wywołanie subkomendy bez flag (cargo-plot plot) -> len == 2 && args[1] == "plot" + let is_tui = args.len() == 1 || (args.len() == 2 && args[1] == "plot"); + + if is_tui { interfaces::tui::run_tui(); - // return; + return; // ⚡ ODKOMENTOWANE: Zapobiega odpaleniu CLI po wyjściu z TUI } + // Wszystko inne (w tym --help) trafia do parsera CLI interfaces::cli::run_cli(); -} +} \ No newline at end of file diff --git a/src/theme/for_path_tree.rs b/src/theme/for_path_tree.rs index 6df8eba..3bfbe50 100644 --- a/src/theme/for_path_tree.rs +++ b/src/theme/for_path_tree.rs @@ -1,21 +1,21 @@ -// [EN]: Path classification and icon mapping for tree visualization. -// [PL]: Klasyfikacja ścieżek i mapowanie ikon dla wizualizacji drzewa. +// [ENG]: Path classification and icon mapping for tree visualization. +// [POL]: Klasyfikacja ścieżek i mapowanie ikon dla wizualizacji drzewa. -/// [EN]: Global icon used for directory nodes. -/// [PL]: Globalna ikona używana dla węzłów będących folderami. +/// [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 = "📄"; -/// [EN]: Defines visual and metadata properties for a file type. -/// [PL]: Definiuje wizualne i metadanowe właściwości dla typu pliku. +/// [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, } -/// [EN]: Returns file properties based on its extension. -/// [PL]: Zwraca właściwości pliku na podstawie jego rozszerzenia. +/// [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 { @@ -59,8 +59,8 @@ pub fn get_file_type(ext: &str) -> PathFileType { icon: "📘", md_lang: "typescript", }, - // [EN]: Default fallback for unknown file types. - // [PL]: Domyślny fallback dla nieznanych typów plików. + // [ENG]: Default fallback for unknown file types. + // [POL]: Domyślny fallback dla nieznanych typów plików. _ => PathFileType { icon: "📄", md_lang: "text", @@ -68,24 +68,24 @@ pub fn get_file_type(ext: &str) -> PathFileType { } } -/// [EN]: Character set used for drawing tree branches and indents. -/// [PL]: Zestaw znaków używanych do rysowania gałęzi drzewa i wcięć. +/// [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 { - // [EN]: Directories (d) - // [PL]: Foldery (d) + // [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, // ├─── - // [EN]: Files (f) - // [PL]: Pliki (f) + // [ENG]: Files (f) + // [POL]: Pliki (f) pub file_last: String, // └──• pub file_mid: String, // ├──• - // [EN]: Indentations for subsequent levels (i) - // [PL]: Wcięcia dla kolejnych poziomów (i) + // [ENG]: Indentations for subsequent levels (i) + // [POL]: Wcięcia dla kolejnych poziomów (i) pub indent_last: String, // " " pub indent_mid: String, // "│ " } From a9be699881ce8b906bc8e9d30dc62f6038969803 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 10:04:53 +0100 Subject: [PATCH 23/45] (add: plot doc/code) --- .gitignore | 3 +- Cargo.toml | 2 +- src/addon.rs | 4 ++ src/addon/time_tag.rs | 78 +++++++++++++++++++++++++ src/core/path_matcher/stats.rs | 14 ++--- src/core/path_view/grid.rs | 6 ++ src/core/path_view/list.rs | 11 ++++ src/interfaces/cli/args.rs | 10 ++++ src/interfaces/cli/engine.rs | 54 ++++++++++++++++- src/lib.rs | 2 + src/output.rs | 4 ++ src/output/generator.rs | 1 + src/output/generator/config_backlist.rs | 47 +++++++++++++++ src/output/save_code.rs | 67 +++++++++++++++++++++ src/output/save_path.rs | 26 +++++++++ 15 files changed, 319 insertions(+), 10 deletions(-) create mode 100644 src/addon.rs create mode 100644 src/addon/time_tag.rs create mode 100644 src/output.rs create mode 100644 src/output/generator.rs create mode 100644 src/output/generator/config_backlist.rs create mode 100644 src/output/save_code.rs create mode 100644 src/output/save_path.rs diff --git a/.gitignore b/.gitignore index 7585494..e185046 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target/ *.exe *.lock -/new/ \ No newline at end of file +/new/ +/other/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b705649..004a8f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,4 +41,4 @@ unsafe_code = "forbid" # 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/src/addon.rs b/src/addon.rs new file mode 100644 index 0000000..6e9a196 --- /dev/null +++ b/src/addon.rs @@ -0,0 +1,4 @@ +pub mod time_tag; + + +pub use time_tag::{TimeTag,NaiveDate,NaiveTime}; \ No newline at end of file diff --git a/src/addon/time_tag.rs b/src/addon/time_tag.rs new file mode 100644 index 0000000..49a23bb --- /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 + ) + } +} \ No newline at end of file diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs index 0afe991..2584226 100644 --- a/src/core/path_matcher/stats.rs +++ b/src/core/path_matcher/stats.rs @@ -31,7 +31,7 @@ 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) -> String { + 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; @@ -40,31 +40,31 @@ impl MatchStats { ViewMode::Grid => { if do_include && let Some(grid) = &self.m_matched.grid { if print_info { out.push_str("✅\n");} - out.push_str(&grid.render_cli()); + 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");} - out.push_str(&grid.render_cli()); + 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");} - out.push_str(&tree.render_cli()); + 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");} - out.push_str(&tree.render_cli()); + 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");} - out.push_str(&list.render_cli(true)); + 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");} - out.push_str(&list.render_cli(false)); + if use_color { out.push_str( &list.render_cli(false)); } else { out.push_str( &list.render_txt()); } } } } diff --git a/src/core/path_view/grid.rs b/src/core/path_view/grid.rs index 1793fca..601f2e2 100644 --- a/src/core/path_view/grid.rs +++ b/src/core/path_view/grid.rs @@ -133,6 +133,12 @@ impl PathGrid { 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() { diff --git a/src/core/path_view/list.rs b/src/core/path_view/list.rs index b72f8ee..837a131 100644 --- a/src/core/path_view/list.rs +++ b/src/core/path_view/list.rs @@ -63,4 +63,15 @@ impl PathList { } 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 + } } \ No newline at end of file diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 44094d3..21c6b40 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -45,6 +45,16 @@ pub struct CliArgs { #[arg(short = 'x', long = "on-mismatch")] pub exclude: bool, + // ⚡ FLAGA ZAPISU ŚCIEŻEK (MARKDOWN) + /// [POL]: Opcjonalna ścieżka do pliku, w którym zostanie zapisany wynik. + #[arg(short = 'o', long = "out-paths", num_args = 0..=1, default_missing_value = "AUTO")] + pub out_path: Option, + + // ⚡ NOWA FLAGA ZAPISU KODU (MARKDOWN) + /// [POL]: Opcjonalna ścieżka do pliku z kodem (cache). Samo -c wygeneruje domyślną ścieżkę w ./other/ + #[arg(short = 'c', long = "out-cache", num_args = 0..=1, default_missing_value = "AUTO")] + pub out_code: Option, + /// [ENG]: Ignore case. /// [POL]: Ignoruj wielkość liter. #[arg(long = "ignore-case")] diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 15713bf..cf9d68f 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -2,6 +2,8 @@ use crate::interfaces::cli::args::CliArgs; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::core::path_view::ViewMode; use cargo_plot::execute::{self, SortStrategy}; +use cargo_plot::addon::TimeTag; +use cargo_plot::core::path_store::PathContext; // use cargo_plot::theme::for_path_list::get_icon_for_path; /// [ENG]: The execution engine (Cockpit). @@ -51,7 +53,57 @@ pub fn run(args: CliArgs) { ); // 2. RENDEROWANIE WYNIKÓW - print!("{}", stats.render_output(view_mode, show_mode, args.info)); + let output_str_cli = stats.render_output(view_mode, show_mode, args.info, true); + print!("{}", output_str_cli); + + let has_out_paths = args.out_path.is_some(); + let has_out_codes = args.out_code.is_some(); + + if has_out_paths || has_out_codes { + let tag = TimeTag::now(); + let output_str_txt = stats.render_output(view_mode, show_mode, args.info, false); + + // Closure do automatycznego generowania ścieżki + let resolve_filepath = |val: &str, prefix: &str| -> String { + if val == "AUTO" { + format!("./other/{}_{}.md", prefix, tag) + } else if val.ends_with('/') || val.ends_with('\\') { + format!("{}{}_{}.md", val, prefix, tag) + } else { + let path = std::path::Path::new(val); + 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 parent_str = parent.to_string_lossy().replace('\\', "/"); + let ext_str = if ext.is_empty() { String::new() } else { format!(".{}", ext) }; + let stem_str = if stem.is_empty() { prefix } else { &stem }; + + if parent_str.is_empty() { + format!("{}_{}{}", stem_str, tag, ext_str) + } else { + format!("{}/{}_{}{}", parent_str, stem_str, tag, ext_str) + } + } + }; + + if let Some(val) = &args.out_path { + let filepath = resolve_filepath(val, "paths"); + cargo_plot::output::save_path::save(&output_str_txt, &filepath); + } + + if let Some(val) = &args.out_code { + let filepath = resolve_filepath(val, "cache"); + if let Ok(ctx) = PathContext::resolve(&args.enter_path) { + cargo_plot::output::save_code::save( + &output_str_txt, + &stats.m_matched.paths, + &ctx.entry_absolute, + &filepath + ); + } + } + } // 3. PODSUMOWANIE if args.info { diff --git a/src/lib.rs b/src/lib.rs index 7100c22..94f80ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ pub mod core; pub mod execute; pub mod theme; +pub mod addon; +pub mod output; \ No newline at end of file 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/output/generator.rs b/src/output/generator.rs new file mode 100644 index 0000000..9c10333 --- /dev/null +++ b/src/output/generator.rs @@ -0,0 +1 @@ +pub mod config_backlist; \ No newline at end of file diff --git a/src/output/generator/config_backlist.rs b/src/output/generator/config_backlist.rs new file mode 100644 index 0000000..6e5a61f --- /dev/null +++ b/src/output/generator/config_backlist.rs @@ -0,0 +1,47 @@ +// [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. +#[must_use] +pub fn is_blacklisted_extension(ext: &str) -> bool { + // [EN]: Standardize to lowercase to handle e.g., .PNG and .png the same way. + // [PL]: Standaryzacja do małych liter, aby traktować np. .PNG i .png tak samo. + let e = ext.to_lowercase(); + + // [EN]: We use matches! macro for better performance than array.contains(). + // [PL]: Używamy makra matches! dla lepszej wydajności niż array.contains(). + 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 + // -------------------------------------------------- + // 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" + ) +} diff --git a/src/output/save_code.rs b/src/output/save_code.rs new file mode 100644 index 0000000..ea42fbb --- /dev/null +++ b/src/output/save_code.rs @@ -0,0 +1,67 @@ +use std::fs; +use std::path::Path; +use crate::output::generator::config_backlist::is_blacklisted_extension; +use crate::theme::for_path_tree::get_file_type; + +pub fn save(tree_text: &str, paths: &[String], base_dir: &str, filepath: &str) { + let path = Path::new(filepath); + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() && !parent.exists() { + let _ = fs::create_dir_all(parent); + } + } + + let mut content = String::new(); + + // 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> *(Plik binarny/graficzny - pominięto)*\n\n", + counter, p_str + )); + 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> *(Błąd odczytu / plik nie jest UTF-8)*\n\n", + counter, p_str + )); + } + } + counter += 1; + } + + match fs::write(path, content) { + Ok(_) => println!("💾 Pomyślnie zapisano cache (kod) do pliku: {}", filepath), + Err(e) => eprintln!("❌ Błąd zapisu kodu do pliku {}: {}", filepath, e), + } +} \ No newline at end of file diff --git a/src/output/save_path.rs b/src/output/save_path.rs new file mode 100644 index 0000000..888bc42 --- /dev/null +++ b/src/output/save_path.rs @@ -0,0 +1,26 @@ +use std::fs; +use std::path::Path; + +/// [POL]: Zapisuje wygenerowany ciąg znaków do pliku tekstowego. +/// Tworzy brakujące katalogi po drodze, jeśli to konieczne. +pub fn save(content: &str, filepath: &str) { + let path = Path::new(filepath); + + // Upewnij się, że foldery nadrzędne istnieją + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() && !parent.exists() { + if let Err(e) = fs::create_dir_all(parent) { + eprintln!("❌ Błąd: Nie można utworzyć katalogu {:?} ({})", parent, e); + return; + } + } + } + + let markdown_content = format!("```plaintext\n{}\n```\n", content); + + // Właściwy zapis do pliku + match fs::write(path, markdown_content) { + Ok(_) => println!("💾 Pomyślnie zapisano wynik do pliku: {}", filepath), + Err(e) => eprintln!("❌ Błąd zapisu do pliku {}: {}", filepath, e), + } +} \ No newline at end of file From 4a9965431ed84962f286fba55c5628c5cc78da68 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 10:26:31 +0100 Subject: [PATCH 24/45] (refactoring) --- src/core.rs | 1 + src/core/save.rs | 130 ++++++++++++++++++++++++ src/interfaces/cli/engine.rs | 8 +- src/lib.rs | 3 +- src/output/generator.rs | 1 - src/output/generator/config_backlist.rs | 47 --------- src/output/save_code.rs | 67 ------------ src/output/save_path.rs | 26 ----- 8 files changed, 137 insertions(+), 146 deletions(-) create mode 100644 src/core/save.rs delete mode 100644 src/output/generator.rs delete mode 100644 src/output/generator/config_backlist.rs delete mode 100644 src/output/save_code.rs delete mode 100644 src/output/save_path.rs diff --git a/src/core.rs b/src/core.rs index eb50076..82be383 100644 --- a/src/core.rs +++ b/src/core.rs @@ -3,3 +3,4 @@ pub mod path_matcher; pub mod path_store; pub mod path_view; pub mod patterns_expand; +pub mod save; \ No newline at end of file diff --git a/src/core/save.rs b/src/core/save.rs new file mode 100644 index 0000000..9d96841 --- /dev/null +++ b/src/core/save.rs @@ -0,0 +1,130 @@ +use std::fs; +use std::path::Path; +use crate::theme::for_path_tree::get_file_type; + +pub struct SaveFile; + +impl SaveFile { + /// Wspólna logika zapisu do pliku (DRY): tworzenie folderów i zapis IO. + fn write_to_disk(filepath: &str, content: &str, log_name: &str) { + let path = Path::new(filepath); + + // Upewnienie się, że foldery nadrzędne istnieją + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() && !parent.exists() { + if let Err(e) = fs::create_dir_all(parent) { + eprintln!("❌ Błąd: Nie można utworzyć katalogu {:?} ({})", parent, e); + return; + } + } + } + + // Zapis pliku + match fs::write(path, content) { + Ok(_) => println!("💾 Pomyślnie zapisano {} do pliku: {}", log_name, filepath), + Err(e) => eprintln!("❌ Błąd zapisu {} do pliku {}: {}", log_name, filepath, e), + } + } + + /// Formatowanie i zapis samego widoku struktury (ścieżek) + pub fn paths(content: &str, filepath: &str, tag: &str) { + let markdown_content = format!("```plaintext\n{}\n```\n\n{}", content, tag); + Self::write_to_disk(filepath, &markdown_content, "ścieżki"); + } + + /// 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) { + let mut content = String::new(); + + // 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> *(Plik binarny/graficzny - pominięto)*\n\n", + counter, p_str + )); + 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> *(Błąd odczytu / plik nie jest UTF-8)*\n\n", + counter, p_str + )); + } + } + counter += 1; + } + + // Znacznik na końcu + content.push_str(&format!("\n\n{}", tag)); + + Self::write_to_disk(filepath, &content, "kod (cache)"); + } +} + +// [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. +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" + ) +} \ No newline at end of file diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index cf9d68f..0bcae69 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -4,6 +4,7 @@ use cargo_plot::core::path_view::ViewMode; use cargo_plot::execute::{self, SortStrategy}; use cargo_plot::addon::TimeTag; use cargo_plot::core::path_store::PathContext; +use cargo_plot::core::save::SaveFile; // use cargo_plot::theme::for_path_list::get_icon_for_path; /// [ENG]: The execution engine (Cockpit). @@ -89,17 +90,18 @@ pub fn run(args: CliArgs) { if let Some(val) = &args.out_path { let filepath = resolve_filepath(val, "paths"); - cargo_plot::output::save_path::save(&output_str_txt, &filepath); + SaveFile::paths(&output_str_txt, &filepath,&tag); } if let Some(val) = &args.out_code { let filepath = resolve_filepath(val, "cache"); if let Ok(ctx) = PathContext::resolve(&args.enter_path) { - cargo_plot::output::save_code::save( + SaveFile::codes( &output_str_txt, &stats.m_matched.paths, &ctx.entry_absolute, - &filepath + &filepath, + &tag ); } } diff --git a/src/lib.rs b/src/lib.rs index 94f80ee..1fb17b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ pub mod core; pub mod execute; pub mod theme; -pub mod addon; -pub mod output; \ No newline at end of file +pub mod addon; \ No newline at end of file diff --git a/src/output/generator.rs b/src/output/generator.rs deleted file mode 100644 index 9c10333..0000000 --- a/src/output/generator.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod config_backlist; \ No newline at end of file diff --git a/src/output/generator/config_backlist.rs b/src/output/generator/config_backlist.rs deleted file mode 100644 index 6e5a61f..0000000 --- a/src/output/generator/config_backlist.rs +++ /dev/null @@ -1,47 +0,0 @@ -// [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. -#[must_use] -pub fn is_blacklisted_extension(ext: &str) -> bool { - // [EN]: Standardize to lowercase to handle e.g., .PNG and .png the same way. - // [PL]: Standaryzacja do małych liter, aby traktować np. .PNG i .png tak samo. - let e = ext.to_lowercase(); - - // [EN]: We use matches! macro for better performance than array.contains(). - // [PL]: Używamy makra matches! dla lepszej wydajności niż array.contains(). - 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 - // -------------------------------------------------- - // 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" - ) -} diff --git a/src/output/save_code.rs b/src/output/save_code.rs deleted file mode 100644 index ea42fbb..0000000 --- a/src/output/save_code.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::fs; -use std::path::Path; -use crate::output::generator::config_backlist::is_blacklisted_extension; -use crate::theme::for_path_tree::get_file_type; - -pub fn save(tree_text: &str, paths: &[String], base_dir: &str, filepath: &str) { - let path = Path::new(filepath); - if let Some(parent) = path.parent() { - if !parent.as_os_str().is_empty() && !parent.exists() { - let _ = fs::create_dir_all(parent); - } - } - - let mut content = String::new(); - - // 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> *(Plik binarny/graficzny - pominięto)*\n\n", - counter, p_str - )); - 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> *(Błąd odczytu / plik nie jest UTF-8)*\n\n", - counter, p_str - )); - } - } - counter += 1; - } - - match fs::write(path, content) { - Ok(_) => println!("💾 Pomyślnie zapisano cache (kod) do pliku: {}", filepath), - Err(e) => eprintln!("❌ Błąd zapisu kodu do pliku {}: {}", filepath, e), - } -} \ No newline at end of file diff --git a/src/output/save_path.rs b/src/output/save_path.rs deleted file mode 100644 index 888bc42..0000000 --- a/src/output/save_path.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::fs; -use std::path::Path; - -/// [POL]: Zapisuje wygenerowany ciąg znaków do pliku tekstowego. -/// Tworzy brakujące katalogi po drodze, jeśli to konieczne. -pub fn save(content: &str, filepath: &str) { - let path = Path::new(filepath); - - // Upewnij się, że foldery nadrzędne istnieją - if let Some(parent) = path.parent() { - if !parent.as_os_str().is_empty() && !parent.exists() { - if let Err(e) = fs::create_dir_all(parent) { - eprintln!("❌ Błąd: Nie można utworzyć katalogu {:?} ({})", parent, e); - return; - } - } - } - - let markdown_content = format!("```plaintext\n{}\n```\n", content); - - // Właściwy zapis do pliku - match fs::write(path, markdown_content) { - Ok(_) => println!("💾 Pomyślnie zapisano wynik do pliku: {}", filepath), - Err(e) => eprintln!("❌ Błąd zapisu do pliku {}: {}", filepath, e), - } -} \ No newline at end of file From 56174b25f8351d10747495763a77b49db4b48080 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 10:27:04 +0100 Subject: [PATCH 25/45] (fmt) --- src/addon.rs | 3 +- src/addon/time_tag.rs | 2 +- src/core.rs | 2 +- src/core/path_matcher/stats.rs | 68 +++++++++++++++++++++++++++------- src/core/path_view/list.rs | 2 +- src/core/save.rs | 8 ++-- src/interfaces/cli/args.rs | 2 +- src/interfaces/cli/engine.rs | 24 +++++++----- src/lib.rs | 2 +- src/main.rs | 2 +- 10 files changed, 80 insertions(+), 35 deletions(-) diff --git a/src/addon.rs b/src/addon.rs index 6e9a196..5198bca 100644 --- a/src/addon.rs +++ b/src/addon.rs @@ -1,4 +1,3 @@ pub mod time_tag; - -pub use time_tag::{TimeTag,NaiveDate,NaiveTime}; \ No newline at end of file +pub use time_tag::{NaiveDate, NaiveTime, TimeTag}; diff --git a/src/addon/time_tag.rs b/src/addon/time_tag.rs index 49a23bb..a82de11 100644 --- a/src/addon/time_tag.rs +++ b/src/addon/time_tag.rs @@ -75,4 +75,4 @@ impl TimeTag { millis ) } -} \ No newline at end of file +} diff --git a/src/core.rs b/src/core.rs index 82be383..f18f4fd 100644 --- a/src/core.rs +++ b/src/core.rs @@ -3,4 +3,4 @@ pub mod path_matcher; pub mod path_store; pub mod path_view; pub mod patterns_expand; -pub mod save; \ No newline at end of file +pub mod save; diff --git a/src/core/path_matcher/stats.rs b/src/core/path_matcher/stats.rs index 2584226..0dfa387 100644 --- a/src/core/path_matcher/stats.rs +++ b/src/core/path_matcher/stats.rs @@ -31,7 +31,13 @@ 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 { + 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; @@ -39,32 +45,68 @@ impl MatchStats { 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 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()); } + 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 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()); } + 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 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()); } + if print_info { + out.push_str("❌\n"); + } + if use_color { + out.push_str(&list.render_cli(false)); + } else { + out.push_str(&list.render_txt()); + } } } } diff --git a/src/core/path_view/list.rs b/src/core/path_view/list.rs index 837a131..d2f5e8a 100644 --- a/src/core/path_view/list.rs +++ b/src/core/path_view/list.rs @@ -74,4 +74,4 @@ impl PathList { } out } -} \ No newline at end of file +} diff --git a/src/core/save.rs b/src/core/save.rs index 9d96841..778683a 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -1,6 +1,6 @@ +use crate::theme::for_path_tree::get_file_type; use std::fs; use std::path::Path; -use crate::theme::for_path_tree::get_file_type; pub struct SaveFile; @@ -35,7 +35,7 @@ impl SaveFile { /// 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) { let mut content = String::new(); - + // Wstawiamy wygenerowane drzewo ścieżek content.push_str("```plaintext\n"); content.push_str(tree_text); @@ -54,7 +54,7 @@ impl SaveFile { .unwrap_or_default() .to_string_lossy() .to_lowercase(); - + let lang = get_file_type(&ext).md_lang; if is_blacklisted_extension(&ext) { @@ -127,4 +127,4 @@ fn is_blacklisted_extension(ext: &str) -> bool { // -------------------------------------------------- "mp3" | "mp4" | "avi" | "mkv" | "wav" | "flac" | "ogg" | "m4a" | "mov" | "wmv" | "flv" ) -} \ No newline at end of file +} diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 21c6b40..74f8b4c 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -58,7 +58,7 @@ pub struct CliArgs { /// [ENG]: Ignore case. /// [POL]: Ignoruj wielkość liter. #[arg(long = "ignore-case")] - pub ignore_case: bool, + pub ignore_case: bool, /// [POL]: Ukrywa główny folder (root) w widoku drzewa. #[arg(long = "treeview-no-root", default_value_t = false)] diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 0bcae69..d092f71 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,10 +1,10 @@ use crate::interfaces::cli::args::CliArgs; -use cargo_plot::core::path_matcher::stats::ShowMode; -use cargo_plot::core::path_view::ViewMode; -use cargo_plot::execute::{self, SortStrategy}; use cargo_plot::addon::TimeTag; +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::theme::for_path_list::get_icon_for_path; /// [ENG]: The execution engine (Cockpit). @@ -77,7 +77,11 @@ pub fn run(args: CliArgs) { let parent = path.parent().unwrap_or_else(|| std::path::Path::new("")); let parent_str = parent.to_string_lossy().replace('\\', "/"); - let ext_str = if ext.is_empty() { String::new() } else { format!(".{}", ext) }; + let ext_str = if ext.is_empty() { + String::new() + } else { + format!(".{}", ext) + }; let stem_str = if stem.is_empty() { prefix } else { &stem }; if parent_str.is_empty() { @@ -90,18 +94,18 @@ pub fn run(args: CliArgs) { if let Some(val) = &args.out_path { let filepath = resolve_filepath(val, "paths"); - SaveFile::paths(&output_str_txt, &filepath,&tag); + SaveFile::paths(&output_str_txt, &filepath, &tag); } if let Some(val) = &args.out_code { let filepath = resolve_filepath(val, "cache"); if let Ok(ctx) = PathContext::resolve(&args.enter_path) { SaveFile::codes( - &output_str_txt, - &stats.m_matched.paths, - &ctx.entry_absolute, - &filepath, - &tag + &output_str_txt, + &stats.m_matched.paths, + &ctx.entry_absolute, + &filepath, + &tag, ); } } diff --git a/src/lib.rs b/src/lib.rs index 1fb17b7..0c16e39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ +pub mod addon; pub mod core; pub mod execute; pub mod theme; -pub mod addon; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3f43b6f..a1e27ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,4 +26,4 @@ fn main() { // Wszystko inne (w tym --help) trafia do parsera CLI interfaces::cli::run_cli(); -} \ No newline at end of file +} From 3972696cf39442fb0d623c1cd2dcaebebffcfda8 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 11:55:01 +0100 Subject: [PATCH 26/45] (version: v2.0.0-alpha.1) --- ...lpha-1_2026Q1D076W12_Tue17Mar_115023586.md | 2832 +++++++++++++++++ src/core.rs | 1 + src/core/by.rs | 44 + src/core/save.rs | 29 +- src/interfaces/cli/args.rs | 5 + src/interfaces/cli/engine.rs | 15 +- 6 files changed, 2912 insertions(+), 14 deletions(-) create mode 100644 CargoPlot-2-0-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md create mode 100644 src/core/by.rs diff --git a/CargoPlot-2-0-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md b/CargoPlot-2-0-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md new file mode 100644 index 0000000..c3bd652 --- /dev/null +++ b/CargoPlot-2-0-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md @@ -0,0 +1,2832 @@ +```plaintext + └──┬ 📂 cargo-plot-2 ./cargo-plot-2/ +[ kB 1.373] ├──• ⚙️ Cargo.toml ./Cargo.toml +[ kB 87.63] └──┬ 📂 src ./src/ +[ B 70.00] ├──• 🦀 addon.rs ./src/addon.rs +[ kB 2.490] ├──┬ 📂 addon ./src/addon/ +[ kB 2.490] │ └──• 🦀 time_tag.rs ./src/addon/time_tag.rs +[ B 132.0] ├──• 🦀 core.rs ./src/core.rs +[ kB 63.25] ├──┬ 📂 core ./src/core/ +[ kB 1.384] │ ├──• 🦀 by.rs ./src/core/by.rs +[ kB 1.144] │ ├──• 🦀 file_stats.rs ./src/core/file_stats.rs +[ kB 3.156] │ ├──┬ 📂 file_stats ./src/core/file_stats/ +[ kB 3.156] │ │ └──• 🦀 weight.rs ./src/core/file_stats/weight.rs +[ B 285.0] │ ├──• 🦀 path_matcher.rs ./src/core/path_matcher.rs +[ kB 23.55] │ ├──┬ 📂 path_matcher ./src/core/path_matcher/ +[ kB 15.00] │ │ ├──• 🦀 matcher.rs ./src/core/path_matcher/matcher.rs +[ kB 4.610] │ │ ├──• 🦀 sort.rs ./src/core/path_matcher/sort.rs +[ kB 3.938] │ │ └──• 🦀 stats.rs ./src/core/path_matcher/stats.rs +[ B 101.0] │ ├──• 🦀 path_store.rs ./src/core/path_store.rs +[ kB 4.234] │ ├──┬ 📂 path_store ./src/core/path_store/ +[ kB 2.119] │ │ ├──• 🦀 context.rs ./src/core/path_store/context.rs +[ kB 2.115] │ │ └──• 🦀 store.rs ./src/core/path_store/store.rs +[ B 313.0] │ ├──• 🦀 path_view.rs ./src/core/path_view.rs +[ kB 22.08] │ ├──┬ 📂 path_view ./src/core/path_view/ +[ kB 9.936] │ │ ├──• 🦀 grid.rs ./src/core/path_view/grid.rs +[ kB 2.560] │ │ ├──• 🦀 list.rs ./src/core/path_view/list.rs +[ kB 2.589] │ │ ├──• 🦀 node.rs ./src/core/path_view/node.rs +[ kB 7.001] │ │ └──• 🦀 tree.rs ./src/core/path_view/tree.rs +[ kB 1.724] │ ├──• 🦀 patterns_expand.rs ./src/core/patterns_expand.rs +[ kB 5.267] │ └──• 🦀 save.rs ./src/core/save.rs +[ kB 5.602] ├──• 🦀 execute.rs ./src/execute.rs +[ B 148.0] ├──• 🦀 interfaces.rs ./src/interfaces.rs +[ kB 10.46] ├──┬ 📂 interfaces ./src/interfaces/ +[ kB 1.104] │ ├──• 🦀 cli.rs ./src/interfaces/cli.rs +[ kB 9.362] │ └──┬ 📂 cli ./src/interfaces/cli/ +[ kB 4.396] │ ├──• 🦀 args.rs ./src/interfaces/cli/args.rs +[ kB 4.966] │ └──• 🦀 engine.rs ./src/interfaces/cli/engine.rs +[ B 61.00] ├──• 🦀 lib.rs ./src/lib.rs +[ kB 1.105] ├──• 🦀 main.rs ./src/main.rs +[ B 79.00] ├──• 🦀 output.rs ./src/output.rs +[ B 46.00] ├──• 🦀 theme.rs ./src/theme.rs +[ kB 4.183] └──┬ 📂 theme ./src/theme/ +[ B 837.0] ├──• 🦀 for_path_list.rs ./src/theme/for_path_list.rs +[ kB 3.346] └──• 🦀 for_path_tree.rs ./src/theme/for_path_tree.rs +``` + +### 001: `./Cargo.toml` + +```toml +[package] +name = "cargo-plot" +version = "0.2.0-alpha.1" +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.4.1" +colored = "3.1.1" +console = "0.16.3" +ctrlc = "3.5.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; +pub mod by; + +``` + +### 005: `./src/core/by.rs` + +```rust +use std::env; + +pub struct BySection; + +impl BySection { + #[must_use] + pub fn generate(tag: &str) -> String { + let args: Vec = env::args().collect(); + let command = args.join(" "); + + let instructions = "\ +**Krótka instrukcja flag:** +- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`) +- `-p, --pat ...` : Wzorce dopasowań (wymagane) +- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`) +- `-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 [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`) +- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`) +- `-i, --info` : Tryb gadatliwy w terminalu +- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku +- `--ignore-case` : Ignoruj wielkość liter we wzorcach +- `--treeview-no-root` : Ukryj główny folder w widoku drzewa"; + + let markdown = format!( + "\n\n---\n\ +---\n\n\ +## Command\n\n\ +**Wywołana komenda:**\n\n\ +```bash\n\ +{command}\n\ +```\n\n\ +{instructions}\n\n\ +[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot)\n\n\ +**Wersja raportu:** +{tag}\n\n\ +---\n\ +" + ); + + markdown + } +} +``` + +### 006: `./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, + } + } +} + +``` + +### 007: `./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(); + } + + 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}] ") +} + +``` + +### 008: `./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}; + +``` + +### 009: `./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 + } +} + +``` + +### 010: `./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 + } +} + +``` + +### 011: `./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 + } +} + +``` + +### 012: `./src/core/path_store.rs` + +```rust +pub mod context; +pub mod store; + +pub use self::context::PathContext; +pub use self::store::PathStore; + +``` + +### 013: `./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, + }) + } +} + +``` + +### 014: `./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() + } +} + +``` + +### 015: `./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, +} + +``` + +### 016: `./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, UnitSystem, 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>, + ) -> 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, + ) -> 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 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)) + .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)) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_strategy); + + let final_roots = if let Some(r_name) = root_name { + let empty_weight = if weight_cfg.system != UnitSystem::None { + " ".repeat(7 + weight_cfg.precision) + } else { + String::new() + }; + + vec![FileNode { + name: r_name.to_string(), + path: PathBuf::from(r_name), + is_dir: true, + icon: DIR_ICON.to_string(), + weight_str: empty_weight, + weight_bytes: 0, + 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 + } +} + +``` + +### 017: `./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, + ) -> 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: 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 + } +} + +``` + +### 018: `./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 + } +} + +``` + +### 019: `./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, UnitSystem, 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>, + ) -> 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, + ) -> 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 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)) + .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)) + .collect(); + + FileNode::sort_slice(&mut top_nodes, sort_strategy); + + let final_roots = if let Some(r_name) = root_name { + let empty_weight = if weight_cfg.system != UnitSystem::None { + " ".repeat(7 + weight_cfg.precision) + } else { + String::new() + }; + + vec![FileNode { + name: r_name.to_string(), + path: PathBuf::from(r_name), + is_dir: true, + icon: DIR_ICON.to_string(), + weight_str: empty_weight, + weight_bytes: 0, + 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 + } +} + +``` + +### 020: `./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()] + } +} + +``` + +### 021: `./src/core/save.rs` + +```rust +use crate::theme::for_path_tree::get_file_type; +use std::fs; +use std::path::Path; + +pub struct SaveFile; + +impl SaveFile { + /// Wspólna logika zapisu do pliku (DRY): tworzenie folderów i zapis IO. + fn write_to_disk(filepath: &str, content: &str, log_name: &str) { + let path = Path::new(filepath); + + // Upewnienie się, że foldery nadrzędne istnieją + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() && !parent.exists() + && let Err(e) = fs::create_dir_all(parent) { + eprintln!("❌ Błąd: Nie można utworzyć katalogu {:?} ({})", parent, e); + return; + } + + // Zapis pliku + match fs::write(path, content) { + Ok(_) => println!("💾 Pomyślnie zapisano {} do pliku: {}", log_name, filepath), + Err(e) => eprintln!("❌ Błąd zapisu {} do pliku {}: {}", log_name, filepath, e), + } + } + + /// Formatowanie i zapis samego widoku struktury (ścieżek) + pub fn paths(content: &str, filepath: &str, tag: &str, by_section: &str) { + let markdown_content = format!("```plaintext\n{}\n```\n\n{}{}", content, tag, by_section); + Self::write_to_disk(filepath, &markdown_content, "ścieżki"); + } + + /// 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, by_section: &str) { + let mut content = String::new(); + + // 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> *(Plik binarny/graficzny - pominięto)*\n\n", + counter, p_str + )); + 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> *(Błąd odczytu / plik nie jest UTF-8)*\n\n", + counter, p_str + )); + } + } + counter += 1; + } + + // Znacznik na końcu + content.push_str(&format!("\n\n{}{}", tag, by_section)); + + Self::write_to_disk(filepath, &content, "kod (cache)"); + } +} + +// [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. +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" + ) +} + +``` + +### 022: `./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; + +/// [POL]: Egzekutor operujący na wielu wzorcach (wersja po rozwinięciu klamer/tokenizacji). +/// [ENG]: Executor operating on multiple patterns (post brace expansion/tokenisation). +pub fn execute( + enter_path: &str, + patterns: &[String], + is_case_sensitive: bool, + sort_strategy: SortStrategy, + show_mode: ShowMode, + view_mode: ViewMode, + no_root: bool, + print_info: bool, + mut on_match: OnMatch, + mut on_mismatch: OnMismatch, +) -> MatchStats +where + // OnMatch: FnMut(&str), + // OnMismatch: FnMut(&str), + // ⚡ Teraz callbacki oczekują bogatego obiektu, a nie tylko tekstu + OnMatch: FnMut(&FileStats), + OnMismatch: FnMut(&FileStats), +{ + // 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); + }); + + // 2. Logowanie stanu początkowego + if print_info { + println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); + println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); + println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); + println!("---------------------------------------"); + println!("🔠 Wrażliwość na litery: {}", is_case_sensitive); + println!("🔍 Wzorce (RAW): {:?}", pattern_ctx.raw); + println!("⚙️ Wzorce (TOK): {:?}", pattern_ctx.tok); + println!("---------------------------------------"); + } else { + println!("---------------------------------------"); + } + + // 3. Budowa silników dopasowujących (Generał) + let matchers = + PathMatchers::new(&pattern_ctx.tok, is_case_sensitive).expect("Błąd kompilacji wzorców"); + + // 4. Skanowanie dysku (Getter) + // [POL]: Ładujemy dane do rejestru z rdzenia + let paths_store = PathStore::scan(&path_ctx.entry_absolute); + // [POL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) + let paths_set = paths_store.get_index(); + + let entry_abs = path_ctx.entry_absolute.clone(); + // 6. Zwracamy statystyki do Engine'u + let mut stats = matchers.evaluate( + &paths_store.list, + &paths_set, + sort_strategy, + show_mode, + |raw_path| { + // Pośrednik pobiera statystyki + let stats = FileStats::fetch(raw_path, &entry_abs); + on_match(&stats); + }, + |raw_path| { + // Pośrednik pobiera statystyki + let stats = FileStats::fetch(raw_path, &entry_abs); + on_mismatch(&stats); + }, + ); + + // 7. ⚡ MAGIA BUDOWANIA WIDOKÓW + let weight_cfg = WeightConfig::default(); + let root_name = if no_root { + None + } else { + Path::new(&path_ctx.entry_absolute) + .file_name() + .and_then(|n| n.to_str()) + }; + + // Pomocnicze flagi do budowania (żeby kod w match był krótki) + let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; + let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; + + // ⚡ Czysty match dla widoków (Grid, Tree, List) + 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, + )); + } + if do_exclude { + stats.x_mismatched.grid = Some(PathGrid::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + )); + } + } + 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, + )); + } + if do_exclude { + stats.x_mismatched.tree = Some(PathTree::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + root_name, + )); + } + } + ViewMode::List => { + if do_include { + stats.m_matched.list = Some(PathList::build( + &stats.m_matched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + )); + } + if do_exclude { + stats.x_mismatched.list = Some(PathList::build( + &stats.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + )); + } + } + } + + stats +} + +``` + +### 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 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. +// [POL]: Główny punkt wejścia dla interfejsu CLI. +pub fn run_cli() { + // [POL]: Pobieramy surowe argumenty bezpośrednio z systemu. + let args_os = std::env::args(); + let mut args: Vec = args_os.collect(); + + // [ENG]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. + // We insert it manually so the parser matches the Cargo plugin structure. + // [POL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. + // Wstawiamy go ręcznie, aby parser pasował do struktury wtyczki Cargo. + if args.len() > 1 && args[1] != "plot" { + args.insert(1, "plot".to_string()); + } + + // [ENG]: Now parse from the modified list. + // [POL]: Teraz parsujemy ze zmodyfikowanej listy. + let CargoCli::Plot(flags) = CargoCli::parse_from(args); + + // [ENG]: Transfer control to our execution engine. + // [POL]: Przekazanie kontroli do naszego silnika wykonawczego. + 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 clap::{Args, Parser, ValueEnum}; + +/// [POL]: Główny wrapper dla wtyczki Cargo. +/// Oszukuje clap'a, mówiąc mu: "Główny program nazywa się 'cargo', a 'plot' to jego subkomenda". +#[derive(Parser, Debug)] +#[command(name = "cargo", bin_name = "cargo")] +pub enum CargoCli { + /// [ENG]: Cargo plot subcommand. + /// [POL]: Podkomenda cargo plot. + Plot(CliArgs), +} + +/// [POL]: Nasze docelowe argumenty CLI. Zauważ, że teraz to jest `Args`, a nie `Parser`. +#[derive(Args, Debug)] +#[command(author, version, about = "Zaawansowany skaner struktury plików", 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]: Match patterns. + /// [POL]: Wzorce dopasowań. + #[arg(short = 'p', long = "pat", required = true)] + pub patterns: Vec, + + /// [ENG]: Results sorting strategy. + /// [POL]: Strategia sortowania wyników. + #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] + pub sort: CliSortStrategy, + + /// [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]: Display only matched paths. + /// [POL]: Wyświetlaj tylko dopasowane ścieżki. + #[arg(short = 'm', long = "on-match")] + pub include: bool, + + /// [ENG]: Display only rejected paths. + /// [POL]: Wyświetlaj tylko odrzucone ścieżki. + #[arg(short = 'x', long = "on-mismatch")] + pub exclude: bool, + + // ⚡ FLAGA ZAPISU ŚCIEŻEK (MARKDOWN) + /// [POL]: Opcjonalna ścieżka do pliku, w którym zostanie zapisany wynik. + #[arg(short = 'o', long = "out-paths", num_args = 0..=1, default_missing_value = "AUTO")] + pub out_path: Option, + + // ⚡ NOWA FLAGA ZAPISU KODU (MARKDOWN) + /// [POL]: Opcjonalna ścieżka do pliku z kodem (cache). Samo -c wygeneruje domyślną ścieżkę w ./other/ + #[arg(short = 'c', long = "out-cache", num_args = 0..=1, default_missing_value = "AUTO")] + pub out_code: Option, + + // ⚡ FLAGA BY (STOPKA) + /// [POL]: Dodaje sekcję informacyjną z wywołaną komendą na dole pliku. + #[arg(short = 'b', long = "by", default_value_t = false)] + pub by: bool, + + /// [ENG]: Ignore case. + /// [POL]: Ignoruj wielkość liter. + #[arg(long = "ignore-case")] + pub ignore_case: bool, + + /// [POL]: Ukrywa główny folder (root) w widoku drzewa. + #[arg(long = "treeview-no-root", default_value_t = false)] + pub no_root: bool, + + /// [POL]: Wyświetla dodatkowe informacje, statystyki i nagłówki (tryb gadatliwy). + #[arg(short = 'i', long = "info", default_value_t = false)] + pub info: bool, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] +pub enum CliViewMode { + Tree, + List, + Grid, +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum CliSortStrategy { + None, + Az, + Za, + AzFile, + ZaFile, + AzDir, + ZaDir, + AzFileMerge, + ZaFileMerge, + AzDirMerge, + ZaDirMerge, +} + +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, + } + } +} + +``` + +### 026: `./src/interfaces/cli/engine.rs` + +```rust +use crate::interfaces::cli::args::CliArgs; +use cargo_plot::addon::TimeTag; +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::core::by::BySection; +use cargo_plot::execute::{self, SortStrategy}; +// use cargo_plot::theme::for_path_list::get_icon_for_path; + +/// [ENG]: The execution engine (Cockpit). +/// [POL]: Silnik wykonawczy (Kokpit). +pub fn run(args: CliArgs) { + let is_case_sensitive = !args.ignore_case; + let sort_strategy: SortStrategy = args.sort.into(); + let view_mode: ViewMode = args.view.into(); + + let show_mode = match (args.include, args.exclude) { + (true, false) => ShowMode::Include, // Tylko flaga -m + (false, true) => ShowMode::Exclude, // Tylko flaga -x + _ => ShowMode::Context, // Brak flag (lub podane obie) = pokazujemy wszystko + }; + + let stats = execute::execute( + &args.enter_path, + &args.patterns, + is_case_sensitive, + sort_strategy, + show_mode, + view_mode, + args.no_root, + args.info, + |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk + |_| {}, + // |file_stat| { + // if !args.treeview { + // println!( + // "✅ MATCH: {} {} ({} B)", + // get_icon_for_path(&file_stat.path), + // file_stat.path, + // file_stat.weight_bytes + // ); + // } + // }, + // |file_stat| { + // if !args.treeview && show_exclude { + // println!( + // "❌ REJECT: {} {} ({} B)", + // get_icon_for_path(&file_stat.path), + // file_stat.path, + // file_stat.weight_bytes + // ); + // } + // }, + ); + + // 2. RENDEROWANIE WYNIKÓW + let output_str_cli = stats.render_output(view_mode, show_mode, args.info, true); + print!("{}", output_str_cli); + + let has_out_paths = args.out_path.is_some(); + let has_out_codes = args.out_code.is_some(); + + if has_out_paths || has_out_codes { + let tag = TimeTag::now(); + let internal_tag = if args.by { "" } else { &tag }; + + let by_content = if args.by { + BySection::generate(&tag) + } else { + String::new() + }; + + let output_str_txt = stats.render_output(view_mode, show_mode, args.info, false); + + + // Closure do automatycznego generowania ścieżki + let resolve_filepath = |val: &str, prefix: &str| -> String { + if val == "AUTO" { + format!("./other/{}_{}.md", prefix, tag) + } else if val.ends_with('/') || val.ends_with('\\') { + format!("{}{}_{}.md", val, prefix, tag) + } else { + let path = std::path::Path::new(val); + 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 parent_str = parent.to_string_lossy().replace('\\', "/"); + let ext_str = if ext.is_empty() { + String::new() + } else { + format!(".{}", ext) + }; + let stem_str = if stem.is_empty() { prefix } else { &stem }; + if parent_str.is_empty() { + format!("{}_{}{}", stem_str, tag, ext_str) + } else { + format!("{}/{}_{}{}", parent_str, stem_str, tag, ext_str) + } + + } + }; + + if let Some(val) = &args.out_path { + let filepath = resolve_filepath(val, "paths"); + SaveFile::paths(&output_str_txt, &filepath, internal_tag, &by_content); + } + + if let Some(val) = &args.out_code { + let filepath = resolve_filepath(val, "cache"); + if let Ok(ctx) = PathContext::resolve(&args.enter_path) { + SaveFile::codes( + &output_str_txt, + &stats.m_matched.paths, + &ctx.entry_absolute, + &filepath, + internal_tag, + &by_content + ); + } + } + } + + // 3. PODSUMOWANIE + if args.info { + println!("---------------------------------------"); + println!( + "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", + stats.m_size_matched, stats.total + ); + println!( + "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", + stats.x_size_mismatched, stats.total + ); + } else { + println!("---------------------------------------"); + } +} + +``` + +### 027: `./src/lib.rs` + +```rust +pub mod addon; +pub mod core; +pub mod execute; +pub mod theme; + +``` + +### 028: `./src/main.rs` + +```rust +// [ENG]: Main entry point switching between interactive TUI and automated CLI. +// [POL]: Główny punkt wejścia przełączający między interaktywnym TUI a automatycznym CLI. + +#![allow(clippy::pedantic, clippy::struct_excessive_bools)] + +use std::env; +mod interfaces; + +fn main() { + // Rejestrujemy pusty handler Ctrl+C. + // Dzięki temu system nie zabije programu natychmiast, a `cliclack` + // przejmie sygnał i bezpiecznie wyjdzie z prompta. + ctrlc::set_handler(move || {}).expect("Błąd podczas ustawiania handlera Ctrl+C"); + + let args: Vec = env::args().collect(); + + // [POL]: Uruchom TUI tylko jeśli: + // 1. Brak argumentów (tylko nazwa pliku binarnego) -> len == 1 + // 2. Wywołanie subkomendy bez flag (cargo-plot plot) -> len == 2 && args[1] == "plot" + let is_tui = args.len() == 1 || (args.len() == 2 && args[1] == "plot"); + + if is_tui { + interfaces::tui::run_tui(); + return; // ⚡ ODKOMENTOWANE: Zapobiega odpaleniu CLI po wyjściu z TUI + } + + // Wszystko inne (w tym --help) trafia do parsera CLI + interfaces::cli::run_cli(); +} + +``` + +### 029: `./src/output.rs` + +```rust +pub mod save_path; +pub mod save_code; +pub mod generator; +//pub use save_path +``` + +### 030: `./src/theme.rs` + +```rust +pub mod for_path_list; +pub mod for_path_tree; + +``` + +### 031: `./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 + } +} + +``` + +### 032: `./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(), + } + } +} + +``` + + + + + +--- +--- + +## Command + +**Wywołana komenda:** + +```bash +target\debug\cargo-plot.exe -d ./ -p ./src/+ -p !@tui{.rs,/}+ -p ./Cargo.toml -s az-file-merge -v grid -m -c -o -b +``` + +**Krótka instrukcja flag:** +- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`) +- `-p, --pat ...` : Wzorce dopasowań (wymagane) +- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`) +- `-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 [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`) +- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`) +- `-i, --info` : Tryb gadatliwy w terminalu +- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku +- `--ignore-case` : Ignoruj wielkość liter we wzorcach +- `--treeview-no-root` : Ukryj główny folder w widoku drzewa + +[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot) + +**Wersja raportu:** +2026Q1D076W12_Tue17Mar_115023586 + +--- diff --git a/src/core.rs b/src/core.rs index f18f4fd..9dcc7be 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,3 +1,4 @@ +pub mod by; pub mod file_stats; pub mod path_matcher; pub mod path_store; diff --git a/src/core/by.rs b/src/core/by.rs new file mode 100644 index 0000000..273d0d2 --- /dev/null +++ b/src/core/by.rs @@ -0,0 +1,44 @@ +use std::env; + +pub struct BySection; + +impl BySection { + #[must_use] + pub fn generate(tag: &str) -> String { + let args: Vec = env::args().collect(); + let command = args.join(" "); + + let instructions = "\ +**Krótka instrukcja flag:** +- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`) +- `-p, --pat ...` : Wzorce dopasowań (wymagane) +- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`) +- `-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 [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`) +- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`) +- `-i, --info` : Tryb gadatliwy w terminalu +- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku +- `--ignore-case` : Ignoruj wielkość liter we wzorcach +- `--treeview-no-root` : Ukryj główny folder w widoku drzewa"; + + let markdown = format!( + "\n\n---\n\ +---\n\n\ +## Command\n\n\ +**Wywołana komenda:**\n\n\ +```bash\n\ +{command}\n\ +```\n\n\ +{instructions}\n\n\ +[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot)\n\n\ +**Wersja raportu:** +{tag}\n\n\ +---\n\ +" + ); + + markdown + } +} diff --git a/src/core/save.rs b/src/core/save.rs index 778683a..c2d20ef 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -10,13 +10,13 @@ impl SaveFile { let path = Path::new(filepath); // Upewnienie się, że foldery nadrzędne istnieją - if let Some(parent) = path.parent() { - if !parent.as_os_str().is_empty() && !parent.exists() { - if let Err(e) = fs::create_dir_all(parent) { - eprintln!("❌ Błąd: Nie można utworzyć katalogu {:?} ({})", parent, e); - return; - } - } + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + && !parent.exists() + && let Err(e) = fs::create_dir_all(parent) + { + eprintln!("❌ Błąd: Nie można utworzyć katalogu {:?} ({})", parent, e); + return; } // Zapis pliku @@ -27,13 +27,20 @@ impl SaveFile { } /// Formatowanie i zapis samego widoku struktury (ścieżek) - pub fn paths(content: &str, filepath: &str, tag: &str) { - let markdown_content = format!("```plaintext\n{}\n```\n\n{}", content, tag); + pub fn paths(content: &str, filepath: &str, tag: &str, by_section: &str) { + let markdown_content = format!("```plaintext\n{}\n```\n\n{}{}", content, tag, by_section); Self::write_to_disk(filepath, &markdown_content, "ścieżki"); } /// 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) { + pub fn codes( + tree_text: &str, + paths: &[String], + base_dir: &str, + filepath: &str, + tag: &str, + by_section: &str, + ) { let mut content = String::new(); // Wstawiamy wygenerowane drzewo ścieżek @@ -84,7 +91,7 @@ impl SaveFile { } // Znacznik na końcu - content.push_str(&format!("\n\n{}", tag)); + content.push_str(&format!("\n\n{}{}", tag, by_section)); Self::write_to_disk(filepath, &content, "kod (cache)"); } diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 74f8b4c..1e8bd59 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -55,6 +55,11 @@ pub struct CliArgs { #[arg(short = 'c', long = "out-cache", num_args = 0..=1, default_missing_value = "AUTO")] pub out_code: Option, + // ⚡ FLAGA BY (STOPKA) + /// [POL]: Dodaje sekcję informacyjną z wywołaną komendą na dole pliku. + #[arg(short = 'b', long = "by", default_value_t = false)] + pub by: bool, + /// [ENG]: Ignore case. /// [POL]: Ignoruj wielkość liter. #[arg(long = "ignore-case")] diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index d092f71..94daedf 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,5 +1,6 @@ use crate::interfaces::cli::args::CliArgs; use cargo_plot::addon::TimeTag; +use cargo_plot::core::by::BySection; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::core::path_store::PathContext; use cargo_plot::core::path_view::ViewMode; @@ -62,6 +63,14 @@ pub fn run(args: CliArgs) { if has_out_paths || has_out_codes { let tag = TimeTag::now(); + let internal_tag = if args.by { "" } else { &tag }; + + let by_content = if args.by { + BySection::generate(&tag) + } else { + String::new() + }; + let output_str_txt = stats.render_output(view_mode, show_mode, args.info, false); // Closure do automatycznego generowania ścieżki @@ -83,7 +92,6 @@ pub fn run(args: CliArgs) { format!(".{}", ext) }; let stem_str = if stem.is_empty() { prefix } else { &stem }; - if parent_str.is_empty() { format!("{}_{}{}", stem_str, tag, ext_str) } else { @@ -94,7 +102,7 @@ pub fn run(args: CliArgs) { if let Some(val) = &args.out_path { let filepath = resolve_filepath(val, "paths"); - SaveFile::paths(&output_str_txt, &filepath, &tag); + SaveFile::paths(&output_str_txt, &filepath, internal_tag, &by_content); } if let Some(val) = &args.out_code { @@ -105,7 +113,8 @@ pub fn run(args: CliArgs) { &stats.m_matched.paths, &ctx.entry_absolute, &filepath, - &tag, + internal_tag, + &by_content, ); } } From 1fc5c092e4cb9b5dee66a55274689fa2b3554bf3 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 11:56:57 +0100 Subject: [PATCH 27/45] (fix: name) --- ...-0-1-5_2026Q1D070W11_Wed11Mar_005445717.md | 2795 +++++++++++++++++ ...lpha-1_2026Q1D076W12_Tue17Mar_115023586.md | 0 2 files changed, 2795 insertions(+) create mode 100644 CargoPlot-0-1-5_2026Q1D070W11_Wed11Mar_005445717.md rename CargoPlot-2-0-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md => CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md (100%) diff --git a/CargoPlot-0-1-5_2026Q1D070W11_Wed11Mar_005445717.md b/CargoPlot-0-1-5_2026Q1D070W11_Wed11Mar_005445717.md new file mode 100644 index 0000000..f0eb641 --- /dev/null +++ b/CargoPlot-0-1-5_2026Q1D070W11_Wed11Mar_005445717.md @@ -0,0 +1,2795 @@ +# Dokumentacja Projektu 2026Q1D070W11_Wed11Mar_005445717 + +**Wywołana komenda:** +```bash +target\debug\cargo-plot.exe 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 +``` + +```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 +``` + +## Plik-001: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_doc_write.rs` + +```rust +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(()) +} + +``` + +## Plik-002: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_plotfiles.rs` + +```rust +// 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) +} + +``` + +## Plik-003: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/stamp.rs` + +```rust +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(); + } +} + +``` + +## Plik-004: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_doc_models.rs` + +```rust +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, +} + +``` + +## Plik-005: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_doc_gen.rs` + +```rust +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(()) +} + +``` + +## Plik-006: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_files_blacklist.rs` + +```rust +// 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) +} + +``` + +## Plik-007: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_copy_dist.rs` + +```rust +// 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) +} + +``` + +## Plik-008: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/mod.rs` + +```rust +// 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), + } +} + +``` + +## Plik-009: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/utils.rs` + +```rust +// 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 + } +} + +``` + +## Plik-010: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/tree.rs` + +```rust +// 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); + } +} + +``` + +## Plik-011: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/tree.rs` + +```rust +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(); + } + } +} + +``` + +## Plik-012: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_doc_id.rs` + +```rust +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(), + } +} + +``` + +## Plik-013: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/mod.rs` + +```rust +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; + } + } +} + +``` + +## Plik-014: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/doc.rs` + +```rust +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)), + } + } +} + +``` + +## Plik-015: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/main.rs` + +```rust +// Plik: src/main.rs +use clap::Parser; +use std::env; + +mod cli; +mod tui; + +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; + } + + // Jeśli są argumenty, pozwalamy Clapowi je sparsować (wymaga słowa 'plot') + let cli::args::CargoCli::Plot(plot_args) = cli::args::CargoCli::parse(); + + match plot_args.command { + Some(cmd) => cli::run_command(cmd), + None => tui::run_tui(), // Zadziała np. dla `cargo run -- plot` + } +} + +``` + +## Plik-016: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_filestree.rs` + +```rust +// 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 +} + +``` + +## Plik-017: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/stamp.rs` + +```rust +// 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()); + } +} + +``` + +## Plik-018: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/mod.rs` + +```rust +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; + +``` + +## Plik-019: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/utils.rs` + +```rust +// 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, + } +} + +``` + +## Plik-020: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/args.rs` + +```rust +// 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, +} + +``` + +## Plik-021: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/doc.rs` + +```rust +// 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); + } +} + +``` + +## Plik-022: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_path_utils.rs` + +```rust +// 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), + } +} + +``` + +## Plik-023: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_weight.rs` + +```rust +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...) + Both, + None, +} + +#[derive(Debug, Clone)] +pub struct WeightConfig { + pub system: UnitSystem, + pub precision: usize, // Całkowita szerokość pola "xxxxx" (min 3) + pub show_for_files: bool, + pub show_for_dirs: bool, + pub dir_sum_included: bool, // true = tylko uwzględnione, false = rzeczywista waga folderu +} + +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, + } + } +} + +/// 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) +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(); + } + + 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) +} + +fn get_dir_size(path: &Path) -> u64 { + fs::read_dir(path) + .map(|entries| { + entries + .filter_map(|e| e.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) +} + +``` + +## Plik-024: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/tui/dist.rs` + +```rust +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)), + } +} + +``` + +## Plik-025: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_datestamp.rs` + +```rust +// ./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 + ) +} + +``` + +## Plik-026: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/Cargo.toml` + +```toml +[package] +name = "cargo-plot" +version = "0.1.5" +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." +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", +] +resolver = "3" + +[package.metadata.cargo] +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" +colored = "3.1.1" + +[lib] +name = "lib" +path = "src/lib/mod.rs" + +# ========================================== +# 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" +``` + +## Plik-027: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_pathtype.rs` + +```rust +// 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 + } +} + +``` + +## Plik-028: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/lib/fn_filespath.rs` + +```rust +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); + } + } +} + +``` + +## Plik-029: `A:/A-JAN/git-rust/j-Cis/libs-utl/cargo-plot/src/cli/dist.rs` + +```rust +// 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), + } +} + +``` + +--- +> 🚀 Raport wygenerowany 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/CargoPlot-2-0-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md b/CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md similarity index 100% rename from CargoPlot-2-0-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md rename to CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md From b92ca9def915d1018fb775289ee4930c3f536c22 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 12:30:31 +0100 Subject: [PATCH 28/45] (fix) --- ...lpha-1_2026Q1D076W12_Tue17Mar_122807021.md | 441 +++++++++++++----- src/core/by.rs | 44 +- src/core/save.rs | 54 ++- src/execute.rs | 19 +- src/i18n.rs | 164 +++++++ src/interfaces/cli/args.rs | 5 + src/interfaces/cli/engine.rs | 37 +- src/lib.rs | 1 + 8 files changed, 576 insertions(+), 189 deletions(-) rename CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md => CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_122807021.md (87%) create mode 100644 src/i18n.rs diff --git a/CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md b/CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_122807021.md similarity index 87% rename from CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md rename to CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_122807021.md index c3bd652..073798e 100644 --- a/CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_115023586.md +++ b/CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_122807021.md @@ -1,13 +1,14 @@ ```plaintext +✅ └──┬ 📂 cargo-plot-2 ./cargo-plot-2/ [ kB 1.373] ├──• ⚙️ Cargo.toml ./Cargo.toml -[ kB 87.63] └──┬ 📂 src ./src/ +[ kB 95.32] └──┬ 📂 src ./src/ [ B 70.00] ├──• 🦀 addon.rs ./src/addon.rs [ kB 2.490] ├──┬ 📂 addon ./src/addon/ [ kB 2.490] │ └──• 🦀 time_tag.rs ./src/addon/time_tag.rs [ B 132.0] ├──• 🦀 core.rs ./src/core.rs -[ kB 63.25] ├──┬ 📂 core ./src/core/ -[ kB 1.384] │ ├──• 🦀 by.rs ./src/core/by.rs +[ kB 62.87] ├──┬ 📂 core ./src/core/ +[ B 561.0] │ ├──• 🦀 by.rs ./src/core/by.rs [ kB 1.144] │ ├──• 🦀 file_stats.rs ./src/core/file_stats.rs [ kB 3.156] │ ├──┬ 📂 file_stats ./src/core/file_stats/ [ kB 3.156] │ │ └──• 🦀 weight.rs ./src/core/file_stats/weight.rs @@ -27,15 +28,16 @@ [ kB 2.589] │ │ ├──• 🦀 node.rs ./src/core/path_view/node.rs [ kB 7.001] │ │ └──• 🦀 tree.rs ./src/core/path_view/tree.rs [ kB 1.724] │ ├──• 🦀 patterns_expand.rs ./src/core/patterns_expand.rs -[ kB 5.267] │ └──• 🦀 save.rs ./src/core/save.rs -[ kB 5.602] ├──• 🦀 execute.rs ./src/execute.rs +[ kB 5.713] │ └──• 🦀 save.rs ./src/core/save.rs +[ kB 5.698] ├──• 🦀 execute.rs ./src/execute.rs +[ kB 7.397] ├──• 🦀 i18n.rs ./src/i18n.rs [ B 148.0] ├──• 🦀 interfaces.rs ./src/interfaces.rs -[ kB 10.46] ├──┬ 📂 interfaces ./src/interfaces/ +[ kB 11.03] ├──┬ 📂 interfaces ./src/interfaces/ [ kB 1.104] │ ├──• 🦀 cli.rs ./src/interfaces/cli.rs -[ kB 9.362] │ └──┬ 📂 cli ./src/interfaces/cli/ -[ kB 4.396] │ ├──• 🦀 args.rs ./src/interfaces/cli/args.rs -[ kB 4.966] │ └──• 🦀 engine.rs ./src/interfaces/cli/engine.rs -[ B 61.00] ├──• 🦀 lib.rs ./src/lib.rs +[ kB 9.929] │ └──┬ 📂 cli ./src/interfaces/cli/ +[ kB 4.564] │ ├──• 🦀 args.rs ./src/interfaces/cli/args.rs +[ kB 5.365] │ └──• 🦀 engine.rs ./src/interfaces/cli/engine.rs +[ B 75.00] ├──• 🦀 lib.rs ./src/lib.rs [ kB 1.105] ├──• 🦀 main.rs ./src/main.rs [ B 79.00] ├──• 🦀 output.rs ./src/output.rs [ B 46.00] ├──• 🦀 theme.rs ./src/theme.rs @@ -190,63 +192,42 @@ impl TimeTag { ### 004: `./src/core.rs` ```rust +pub mod by; pub mod file_stats; pub mod path_matcher; pub mod path_store; pub mod path_view; pub mod patterns_expand; pub mod save; -pub mod by; ``` ### 005: `./src/core/by.rs` ```rust +use super::super::i18n::I18n; use std::env; pub struct BySection; impl BySection { #[must_use] - pub fn generate(tag: &str) -> String { + pub fn generate(tag: &str, typ: &str, i18n: &I18n) -> String { let args: Vec = env::args().collect(); let command = args.join(" "); - let instructions = "\ -**Krótka instrukcja flag:** -- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`) -- `-p, --pat ...` : Wzorce dopasowań (wymagane) -- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`) -- `-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 [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`) -- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`) -- `-i, --info` : Tryb gadatliwy w terminalu -- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku -- `--ignore-case` : Ignoruj wielkość liter we wzorcach -- `--treeview-no-root` : Ukryj główny folder w widoku drzewa"; - - let markdown = format!( - "\n\n---\n\ ----\n\n\ -## Command\n\n\ -**Wywołana komenda:**\n\n\ -```bash\n\ -{command}\n\ -```\n\n\ -{instructions}\n\n\ -[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot)\n\n\ -**Wersja raportu:** -{tag}\n\n\ ----\n\ -" - ); - - markdown + format!( + "\n\n---\n---\n\n{}\n\n{}\n\n```bash\n{}\n```\n\n{}\n\n{}\n\n{}\n\n---\n", + i18n.by_title(typ), + i18n.by_cmd(), + command, + i18n.by_instructions(), + i18n.by_link(), + i18n.by_version(tag) + ) } } + ``` ### 006: `./src/core/file_stats.rs` @@ -1977,6 +1958,7 @@ impl PatternContext { ### 021: `./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; @@ -1985,32 +1967,51 @@ pub struct SaveFile; impl SaveFile { /// Wspólna logika zapisu do pliku (DRY): tworzenie folderów i zapis IO. - fn write_to_disk(filepath: &str, content: &str, log_name: &str) { + fn write_to_disk(filepath: &str, content: &str, log_name: &str, i18n: &I18n) { let path = Path::new(filepath); - // Upewnienie się, że foldery nadrzędne istnieją if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() && !parent.exists() - && let Err(e) = fs::create_dir_all(parent) { - eprintln!("❌ Błąd: Nie można utworzyć katalogu {:?} ({})", parent, e); - return; - } + && !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; + } - // Zapis pliku match fs::write(path, content) { - Ok(_) => println!("💾 Pomyślnie zapisano {} do pliku: {}", log_name, filepath), - Err(e) => eprintln!("❌ Błąd zapisu {} do pliku {}: {}", log_name, filepath, e), + 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, by_section: &str) { + pub fn paths(content: &str, filepath: &str, tag: &str, by_section: &str, i18n: &I18n) { let markdown_content = format!("```plaintext\n{}\n```\n\n{}{}", content, tag, by_section); - Self::write_to_disk(filepath, &markdown_content, "ścieżki"); + 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, by_section: &str) { + pub fn codes( + tree_text: &str, + paths: &[String], + base_dir: &str, + filepath: &str, + tag: &str, + by_section: &str, + i18n: &I18n, + ) { let mut content = String::new(); // Wstawiamy wygenerowane drzewo ścieżek @@ -2036,8 +2037,10 @@ impl SaveFile { if is_blacklisted_extension(&ext) { content.push_str(&format!( - "### {:03}: `{}`\n\n> *(Plik binarny/graficzny - pominięto)*\n\n", - counter, p_str + "### {:03}: `{}`\n\n{}\n\n", + counter, + p_str, + i18n.skip_binary() )); counter += 1; continue; @@ -2052,18 +2055,27 @@ impl SaveFile { } Err(_) => { content.push_str(&format!( - "### {:03}: `{}`\n\n> *(Błąd odczytu / plik nie jest UTF-8)*\n\n", - counter, p_str + "### {:03}: `{}`\n\n{}\n\n", + counter, + p_str, + i18n.read_err() )); } } counter += 1; } - // Znacznik na końcu content.push_str(&format!("\n\n{}{}", tag, by_section)); - - Self::write_to_disk(filepath, &content, "kod (cache)"); + Self::write_to_disk( + filepath, + &content, + if i18n.lang == crate::i18n::Lang::Pl { + "kod (cache)" + } else { + "code (cache)" + }, + i18n, + ); } } @@ -2131,6 +2143,7 @@ pub fn execute( view_mode: ViewMode, no_root: bool, print_info: bool, + i18n: &crate::i18n::I18n, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) -> MatchStats @@ -2150,13 +2163,19 @@ where // 2. Logowanie stanu początkowego if print_info { - println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); - println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); - println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); + 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!("🔠 Wrażliwość na litery: {}", is_case_sensitive); - println!("🔍 Wzorce (RAW): {:?}", pattern_ctx.raw); - println!("⚙️ Wzorce (TOK): {:?}", pattern_ctx.tok); + 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!("---------------------------------------"); @@ -2272,7 +2291,177 @@ where ``` -### 023: `./src/interfaces.rs` +### 023: `./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 by_title(&self, typ: &str) -> String { + match self.lang { + Lang::Pl => format!("## Command - Query ({typ})"), + Lang::En => format!("## Command - Query ({typ})"), + } + } + pub fn by_cmd(&self) -> &'static str { + match self.lang { + Lang::Pl => "**Wywołana komenda:**", + Lang::En => "**Executed command:**", + } + } + pub fn by_instructions(&self) -> &'static str { + match self.lang { + Lang::Pl => { + "**Krótka instrukcja flag:**\n- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`)\n- `-p, --pat ...` : Wzorce dopasowań (wymagane)\n- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`)\n- `-v, --view ` : Widok wyników (`tree`, `list`, `grid`)\n- `-m, --on-match` : Pokaż tylko dopasowane ścieżki\n- `-x, --on-mismatch` : Pokaż tylko odrzucone ścieżki\n- `-o, --out-paths [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`)\n- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`)\n- `-i, --info` : Tryb gadatliwy w terminalu\n- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku\n- `--ignore-case` : Ignoruj wielkość liter we wzorcach\n- `--treeview-no-root` : Ukryj główny folder w widoku drzewa" + } + Lang::En => { + "**Short flags manual:**\n- `-d, --dir ` : Input path to scan (default: `.`)\n- `-p, --pat ...` : Match patterns (required)\n- `-s, --sort ` : Sorting strategy (e.g. `az-file-merge`)\n- `-v, --view ` : Results view (`tree`, `list`, `grid`)\n- `-m, --on-match` : Show only matched paths\n- `-x, --on-mismatch` : Show only rejected paths\n- `-o, --out-paths [PATH]` : Save paths to file (AUTO: `./other/`)\n- `-c, --out-cache [PATH]` : Save code to file (AUTO: `./other/`)\n- `-i, --info` : Verbose terminal mode\n- `-b, --by` : Add info section at end of file\n- `--ignore-case` : Ignore case in patterns\n- `--treeview-no-root` : Hide root directory in tree view" + } + } + } + pub fn by_link(&self) -> &'static str { + match self.lang { + Lang::Pl => { + "[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot)" + } + Lang::En => "[📊 Check `cargo-plot` on crates.io](https://crates.io/crates/cargo-plot)", + } + } + pub fn by_version(&self, tag: &str) -> String { + match self.lang { + Lang::Pl => format!("**Wersja raportu:** {tag}"), + Lang::En => format!("**Report version:** {tag}"), + } + } + + // ===================================================================== + // 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), + } + } + + // ===================================================================== + // 4. TUI - INTERAKTYWNY PANEL + // ===================================================================== + // (Zostawiamy tu miejsce na przyszłość) +} + +``` + +### 024: `./src/interfaces.rs` ```rust // [ENG]: User interaction layer (Ports and Adapters). @@ -2283,7 +2472,7 @@ pub mod tui; ``` -### 024: `./src/interfaces/cli.rs` +### 025: `./src/interfaces/cli.rs` ```rust pub mod args; @@ -2318,11 +2507,12 @@ pub fn run_cli() { ``` -### 025: `./src/interfaces/cli/args.rs` +### 026: `./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}; /// [POL]: Główny wrapper dla wtyczki Cargo. @@ -2395,6 +2585,10 @@ pub struct CliArgs { /// [POL]: Wyświetla dodatkowe informacje, statystyki i nagłówki (tryb gadatliwy). #[arg(short = 'i', long = "info", default_value_t = false)] pub info: bool, + + /// [POL]: Wymusza język interfejsu (pl / en). Domyślnie pobiera z systemu. + #[arg(long, value_enum)] + pub lang: Option, } #[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] @@ -2449,22 +2643,24 @@ impl From for ViewMode { ``` -### 026: `./src/interfaces/cli/engine.rs` +### 027: `./src/interfaces/cli/engine.rs` ```rust use crate::interfaces::cli::args::CliArgs; use cargo_plot::addon::TimeTag; +use cargo_plot::core::by::BySection; 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::core::by::BySection; use cargo_plot::execute::{self, SortStrategy}; +use cargo_plot::i18n::I18n; // use cargo_plot::theme::for_path_list::get_icon_for_path; /// [ENG]: The execution engine (Cockpit). /// [POL]: Silnik wykonawczy (Kokpit). pub fn run(args: CliArgs) { + 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(); @@ -2484,6 +2680,7 @@ pub fn run(args: CliArgs) { view_mode, args.no_root, args.info, + &i18n, |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk |_| {}, // |file_stat| { @@ -2519,15 +2716,14 @@ pub fn run(args: CliArgs) { let tag = TimeTag::now(); let internal_tag = if args.by { "" } else { &tag }; - let by_content = if args.by { - BySection::generate(&tag) - } else { - String::new() - }; + // let by_content = if args.by { + // BySection::generate(&tag) + // } else { + // String::new() + // }; let output_str_txt = stats.render_output(view_mode, show_mode, args.info, false); - // Closure do automatycznego generowania ścieżki let resolve_filepath = |val: &str, prefix: &str| -> String { if val == "AUTO" { @@ -2552,25 +2748,35 @@ pub fn run(args: CliArgs) { } else { format!("{}/{}_{}{}", parent_str, stem_str, tag, ext_str) } - } }; if let Some(val) = &args.out_path { let filepath = resolve_filepath(val, "paths"); - SaveFile::paths(&output_str_txt, &filepath, internal_tag, &by_content); + let by_content = if args.by { + BySection::generate(&tag, "paths", &i18n) + } else { + String::new() + }; + SaveFile::paths(&output_str_txt, &filepath, internal_tag, &by_content, &i18n); } if let Some(val) = &args.out_code { let filepath = resolve_filepath(val, "cache"); if let Ok(ctx) = PathContext::resolve(&args.enter_path) { + let by_content = if args.by { + BySection::generate(&tag, "codes", &i18n) + } else { + String::new() + }; SaveFile::codes( &output_str_txt, &stats.m_matched.paths, &ctx.entry_absolute, &filepath, internal_tag, - &by_content + &by_content, + &i18n, ); } } @@ -2579,14 +2785,9 @@ pub fn run(args: CliArgs) { // 3. PODSUMOWANIE if args.info { println!("---------------------------------------"); - println!( - "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", - stats.m_size_matched, stats.total - ); - println!( - "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", - stats.x_size_mismatched, stats.total - ); + // ⚡ PODMIENIONO NA WYWOŁANIA Z I18N + println!("{}", i18n.cli_summary_matched(stats.m_size_matched, stats.total)); + println!("{}", i18n.cli_summary_rejected(stats.x_size_mismatched, stats.total)); } else { println!("---------------------------------------"); } @@ -2594,17 +2795,18 @@ pub fn run(args: CliArgs) { ``` -### 027: `./src/lib.rs` +### 028: `./src/lib.rs` ```rust pub mod addon; pub mod core; pub mod execute; +pub mod i18n; pub mod theme; ``` -### 028: `./src/main.rs` +### 029: `./src/main.rs` ```rust // [ENG]: Main entry point switching between interactive TUI and automated CLI. @@ -2639,7 +2841,7 @@ fn main() { ``` -### 029: `./src/output.rs` +### 030: `./src/output.rs` ```rust pub mod save_path; @@ -2648,7 +2850,7 @@ pub mod generator; //pub use save_path ``` -### 030: `./src/theme.rs` +### 031: `./src/theme.rs` ```rust pub mod for_path_list; @@ -2656,7 +2858,7 @@ pub mod for_path_tree; ``` -### 031: `./src/theme/for_path_list.rs` +### 032: `./src/theme/for_path_list.rs` ```rust /// [POL]: Przypisuje ikonę (emoji) do ścieżki na podstawie atrybutów: katalog oraz status elementu ukrytego. @@ -2681,7 +2883,7 @@ pub fn get_icon_for_path(path: &str) -> &'static str { ``` -### 032: `./src/theme/for_path_tree.rs` +### 033: `./src/theme/for_path_tree.rs` ```rust // [ENG]: Path classification and icon mapping for tree visualization. @@ -2802,31 +3004,30 @@ impl Default for TreeStyle { --- --- -## Command +## Command - Query (codes) -**Wywołana komenda:** +**Executed command:** ```bash -target\debug\cargo-plot.exe -d ./ -p ./src/+ -p !@tui{.rs,/}+ -p ./Cargo.toml -s az-file-merge -v grid -m -c -o -b +target\debug\cargo-plot.exe -d ./ -p ./src/+ -p !@tui{.rs,/}+ -p ./Cargo.toml -s az-file-merge -v grid -m -c -o -b -i --lang en ``` -**Krótka instrukcja flag:** -- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`) -- `-p, --pat ...` : Wzorce dopasowań (wymagane) -- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`) -- `-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 [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`) -- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`) -- `-i, --info` : Tryb gadatliwy w terminalu -- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku -- `--ignore-case` : Ignoruj wielkość liter we wzorcach -- `--treeview-no-root` : Ukryj główny folder w widoku drzewa - -[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot) - -**Wersja raportu:** -2026Q1D076W12_Tue17Mar_115023586 +**Short flags manual:** +- `-d, --dir ` : Input path to scan (default: `.`) +- `-p, --pat ...` : Match patterns (required) +- `-s, --sort ` : Sorting strategy (e.g. `az-file-merge`) +- `-v, --view ` : Results view (`tree`, `list`, `grid`) +- `-m, --on-match` : Show only matched paths +- `-x, --on-mismatch` : Show only rejected paths +- `-o, --out-paths [PATH]` : Save paths to file (AUTO: `./other/`) +- `-c, --out-cache [PATH]` : Save code to file (AUTO: `./other/`) +- `-i, --info` : Verbose terminal mode +- `-b, --by` : Add info section at end of file +- `--ignore-case` : Ignore case in patterns +- `--treeview-no-root` : Hide root directory in tree view + +[📊 Check `cargo-plot` on crates.io](https://crates.io/crates/cargo-plot) + +**Report version:** 2026Q1D076W12_Tue17Mar_122807021 --- diff --git a/src/core/by.rs b/src/core/by.rs index 273d0d2..0c0924b 100644 --- a/src/core/by.rs +++ b/src/core/by.rs @@ -1,44 +1,22 @@ +use super::super::i18n::I18n; use std::env; pub struct BySection; impl BySection { #[must_use] - pub fn generate(tag: &str) -> String { + pub fn generate(tag: &str, typ: &str, i18n: &I18n) -> String { let args: Vec = env::args().collect(); let command = args.join(" "); - let instructions = "\ -**Krótka instrukcja flag:** -- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`) -- `-p, --pat ...` : Wzorce dopasowań (wymagane) -- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`) -- `-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 [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`) -- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`) -- `-i, --info` : Tryb gadatliwy w terminalu -- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku -- `--ignore-case` : Ignoruj wielkość liter we wzorcach -- `--treeview-no-root` : Ukryj główny folder w widoku drzewa"; - - let markdown = format!( - "\n\n---\n\ ----\n\n\ -## Command\n\n\ -**Wywołana komenda:**\n\n\ -```bash\n\ -{command}\n\ -```\n\n\ -{instructions}\n\n\ -[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot)\n\n\ -**Wersja raportu:** -{tag}\n\n\ ----\n\ -" - ); - - markdown + format!( + "\n\n---\n---\n\n{}\n\n{}\n\n```bash\n{}\n```\n\n{}\n\n{}\n\n{}\n\n---\n", + i18n.by_title(typ), + i18n.by_cmd(), + command, + i18n.by_instructions(), + i18n.by_link(), + i18n.by_version(tag) + ) } } diff --git a/src/core/save.rs b/src/core/save.rs index c2d20ef..da83501 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -1,3 +1,4 @@ +use super::super::i18n::I18n; use crate::theme::for_path_tree::get_file_type; use std::fs; use std::path::Path; @@ -6,30 +7,39 @@ pub struct SaveFile; impl SaveFile { /// Wspólna logika zapisu do pliku (DRY): tworzenie folderów i zapis IO. - fn write_to_disk(filepath: &str, content: &str, log_name: &str) { + fn write_to_disk(filepath: &str, content: &str, log_name: &str, i18n: &I18n) { let path = Path::new(filepath); - // Upewnienie się, że foldery nadrzędne istnieją if let Some(parent) = path.parent() && !parent.as_os_str().is_empty() && !parent.exists() && let Err(e) = fs::create_dir_all(parent) { - eprintln!("❌ Błąd: Nie można utworzyć katalogu {:?} ({})", parent, e); + eprintln!( + "{}", + i18n.dir_create_err(&parent.to_string_lossy(), &e.to_string()) + ); return; } - // Zapis pliku match fs::write(path, content) { - Ok(_) => println!("💾 Pomyślnie zapisano {} do pliku: {}", log_name, filepath), - Err(e) => eprintln!("❌ Błąd zapisu {} do pliku {}: {}", log_name, filepath, e), + 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, by_section: &str) { + pub fn paths(content: &str, filepath: &str, tag: &str, by_section: &str, i18n: &I18n) { let markdown_content = format!("```plaintext\n{}\n```\n\n{}{}", content, tag, by_section); - Self::write_to_disk(filepath, &markdown_content, "ścieżki"); + 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) @@ -40,6 +50,7 @@ impl SaveFile { filepath: &str, tag: &str, by_section: &str, + i18n: &I18n, ) { let mut content = String::new(); @@ -66,8 +77,10 @@ impl SaveFile { if is_blacklisted_extension(&ext) { content.push_str(&format!( - "### {:03}: `{}`\n\n> *(Plik binarny/graficzny - pominięto)*\n\n", - counter, p_str + "### {:03}: `{}`\n\n{}\n\n", + counter, + p_str, + i18n.skip_binary() )); counter += 1; continue; @@ -82,18 +95,27 @@ impl SaveFile { } Err(_) => { content.push_str(&format!( - "### {:03}: `{}`\n\n> *(Błąd odczytu / plik nie jest UTF-8)*\n\n", - counter, p_str + "### {:03}: `{}`\n\n{}\n\n", + counter, + p_str, + i18n.read_err() )); } } counter += 1; } - // Znacznik na końcu content.push_str(&format!("\n\n{}{}", tag, by_section)); - - Self::write_to_disk(filepath, &content, "kod (cache)"); + Self::write_to_disk( + filepath, + &content, + if i18n.lang == crate::i18n::Lang::Pl { + "kod (cache)" + } else { + "code (cache)" + }, + i18n, + ); } } diff --git a/src/execute.rs b/src/execute.rs index da3f522..226740c 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -18,6 +18,7 @@ pub fn execute( view_mode: ViewMode, no_root: bool, print_info: bool, + i18n: &crate::i18n::I18n, mut on_match: OnMatch, mut on_mismatch: OnMismatch, ) -> MatchStats @@ -37,13 +38,19 @@ where // 2. Logowanie stanu początkowego if print_info { - println!("📂 Baza terminala (Absolutna): {}", path_ctx.base_absolute); - println!("📂 Cel skanowania (Absolutna): {}", path_ctx.entry_absolute); - println!("📂 Cel skanowania (Relatywna): {}", path_ctx.entry_relative); + 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!("🔠 Wrażliwość na litery: {}", is_case_sensitive); - println!("🔍 Wzorce (RAW): {:?}", pattern_ctx.raw); - println!("⚙️ Wzorce (TOK): {:?}", pattern_ctx.tok); + 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!("---------------------------------------"); diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 0000000..242df3a --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,164 @@ +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 by_title(&self, typ: &str) -> String { + match self.lang { + Lang::Pl => format!("## Command - Query ({typ})"), + Lang::En => format!("## Command - Query ({typ})"), + } + } + pub fn by_cmd(&self) -> &'static str { + match self.lang { + Lang::Pl => "**Wywołana komenda:**", + Lang::En => "**Executed command:**", + } + } + pub fn by_instructions(&self) -> &'static str { + match self.lang { + Lang::Pl => { + "**Krótka instrukcja flag:**\n- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`)\n- `-p, --pat ...` : Wzorce dopasowań (wymagane)\n- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`)\n- `-v, --view ` : Widok wyników (`tree`, `list`, `grid`)\n- `-m, --on-match` : Pokaż tylko dopasowane ścieżki\n- `-x, --on-mismatch` : Pokaż tylko odrzucone ścieżki\n- `-o, --out-paths [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`)\n- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`)\n- `-i, --info` : Tryb gadatliwy w terminalu\n- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku\n- `--ignore-case` : Ignoruj wielkość liter we wzorcach\n- `--treeview-no-root` : Ukryj główny folder w widoku drzewa" + } + Lang::En => { + "**Short flags manual:**\n- `-d, --dir ` : Input path to scan (default: `.`)\n- `-p, --pat ...` : Match patterns (required)\n- `-s, --sort ` : Sorting strategy (e.g. `az-file-merge`)\n- `-v, --view ` : Results view (`tree`, `list`, `grid`)\n- `-m, --on-match` : Show only matched paths\n- `-x, --on-mismatch` : Show only rejected paths\n- `-o, --out-paths [PATH]` : Save paths to file (AUTO: `./other/`)\n- `-c, --out-cache [PATH]` : Save code to file (AUTO: `./other/`)\n- `-i, --info` : Verbose terminal mode\n- `-b, --by` : Add info section at end of file\n- `--ignore-case` : Ignore case in patterns\n- `--treeview-no-root` : Hide root directory in tree view" + } + } + } + pub fn by_link(&self) -> &'static str { + match self.lang { + Lang::Pl => { + "[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot)" + } + Lang::En => "[📊 Check `cargo-plot` on crates.io](https://crates.io/crates/cargo-plot)", + } + } + pub fn by_version(&self, tag: &str) -> String { + match self.lang { + Lang::Pl => format!("**Wersja raportu:** {tag}"), + Lang::En => format!("**Report version:** {tag}"), + } + } + + // ===================================================================== + // 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), + } + } + + // ===================================================================== + // 4. TUI - INTERAKTYWNY PANEL + // ===================================================================== + // (Zostawiamy tu miejsce na przyszłość) +} diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 1e8bd59..6f9077c 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -1,5 +1,6 @@ use cargo_plot::core::path_matcher::SortStrategy; use cargo_plot::core::path_view::ViewMode; +use cargo_plot::i18n::Lang; use clap::{Args, Parser, ValueEnum}; /// [POL]: Główny wrapper dla wtyczki Cargo. @@ -72,6 +73,10 @@ pub struct CliArgs { /// [POL]: Wyświetla dodatkowe informacje, statystyki i nagłówki (tryb gadatliwy). #[arg(short = 'i', long = "info", default_value_t = false)] pub info: bool, + + /// [POL]: Wymusza język interfejsu (pl / en). Domyślnie pobiera z systemu. + #[arg(long, value_enum)] + pub lang: Option, } #[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 94daedf..e32bd3d 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -6,11 +6,13 @@ 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; // use cargo_plot::theme::for_path_list::get_icon_for_path; /// [ENG]: The execution engine (Cockpit). /// [POL]: Silnik wykonawczy (Kokpit). pub fn run(args: CliArgs) { + 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(); @@ -30,6 +32,7 @@ pub fn run(args: CliArgs) { view_mode, args.no_root, args.info, + &i18n, |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk |_| {}, // |file_stat| { @@ -65,11 +68,11 @@ pub fn run(args: CliArgs) { let tag = TimeTag::now(); let internal_tag = if args.by { "" } else { &tag }; - let by_content = if args.by { - BySection::generate(&tag) - } else { - String::new() - }; + // let by_content = if args.by { + // BySection::generate(&tag) + // } else { + // String::new() + // }; let output_str_txt = stats.render_output(view_mode, show_mode, args.info, false); @@ -102,12 +105,22 @@ pub fn run(args: CliArgs) { if let Some(val) = &args.out_path { let filepath = resolve_filepath(val, "paths"); - SaveFile::paths(&output_str_txt, &filepath, internal_tag, &by_content); + let by_content = if args.by { + BySection::generate(&tag, "paths", &i18n) + } else { + String::new() + }; + SaveFile::paths(&output_str_txt, &filepath, internal_tag, &by_content, &i18n); } if let Some(val) = &args.out_code { let filepath = resolve_filepath(val, "cache"); if let Ok(ctx) = PathContext::resolve(&args.enter_path) { + let by_content = if args.by { + BySection::generate(&tag, "codes", &i18n) + } else { + String::new() + }; SaveFile::codes( &output_str_txt, &stats.m_matched.paths, @@ -115,6 +128,7 @@ pub fn run(args: CliArgs) { &filepath, internal_tag, &by_content, + &i18n, ); } } @@ -123,14 +137,9 @@ pub fn run(args: CliArgs) { // 3. PODSUMOWANIE if args.info { println!("---------------------------------------"); - println!( - "📊 Podsumowanie: Dopasowano {} z {} ścieżek.", - stats.m_size_matched, stats.total - ); - println!( - "📊 Podsumowanie: Odrzucono {} z {} ścieżek.", - stats.x_size_mismatched, stats.total - ); + // ⚡ PODMIENIONO NA WYWOŁANIA Z I18N + 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/lib.rs b/src/lib.rs index 0c16e39..4a28aef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod addon; pub mod core; pub mod execute; +pub mod i18n; pub mod theme; From 2ea11ef858f3dc5548d5b1fca48373a50f58a735 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 12:44:33 +0100 Subject: [PATCH 29/45] (fix: save) --- src/core.rs | 1 - src/core/by.rs | 22 -------------------- src/core/save.rs | 39 ++++++++++++++++++++++++++++++++---- src/interfaces/cli/engine.rs | 37 ++++++++++++---------------------- 4 files changed, 48 insertions(+), 51 deletions(-) delete mode 100644 src/core/by.rs diff --git a/src/core.rs b/src/core.rs index 9dcc7be..f18f4fd 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,4 +1,3 @@ -pub mod by; pub mod file_stats; pub mod path_matcher; pub mod path_store; diff --git a/src/core/by.rs b/src/core/by.rs deleted file mode 100644 index 0c0924b..0000000 --- a/src/core/by.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::super::i18n::I18n; -use std::env; - -pub struct BySection; - -impl BySection { - #[must_use] - pub fn generate(tag: &str, typ: &str, i18n: &I18n) -> String { - let args: Vec = env::args().collect(); - let command = args.join(" "); - - format!( - "\n\n---\n---\n\n{}\n\n{}\n\n```bash\n{}\n```\n\n{}\n\n{}\n\n{}\n\n---\n", - i18n.by_title(typ), - i18n.by_cmd(), - command, - i18n.by_instructions(), - i18n.by_link(), - i18n.by_version(tag) - ) - } -} diff --git a/src/core/save.rs b/src/core/save.rs index da83501..0b52b51 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -1,11 +1,27 @@ use super::super::i18n::I18n; use crate::theme::for_path_tree::get_file_type; +use std::env; use std::fs; use std::path::Path; pub struct SaveFile; impl SaveFile { + fn generate_by_section(tag: &str, typ: &str, i18n: &I18n) -> String { + let args: Vec = env::args().collect(); + let command = args.join(" "); + + format!( + "\n\n---\n---\n\n{}\n\n{}\n\n```bash\n{}\n```\n\n{}\n\n{}\n\n{}\n\n---\n", + i18n.by_title(typ), + i18n.by_cmd(), + command, + i18n.by_instructions(), + i18n.by_link(), + i18n.by_version(tag) + ) + } + /// 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); @@ -28,8 +44,17 @@ impl SaveFile { } } /// Formatowanie i zapis samego widoku struktury (ścieżek) - pub fn paths(content: &str, filepath: &str, tag: &str, by_section: &str, i18n: &I18n) { - let markdown_content = format!("```plaintext\n{}\n```\n\n{}{}", content, tag, by_section); + pub fn paths(content: &str, filepath: &str, tag: &str, add_by: bool, i18n: &I18n) { + let by_section = if add_by { + Self::generate_by_section(tag, "paths", i18n) + } else { + String::new() + }; + let internal_tag = if add_by { "" } else { tag }; // Zapobiega dublowaniu tagu + let markdown_content = format!( + "```plaintext\n{}\n```\n\n{}{}", + content, internal_tag, by_section + ); Self::write_to_disk( filepath, &markdown_content, @@ -49,9 +74,15 @@ impl SaveFile { base_dir: &str, filepath: &str, tag: &str, - by_section: &str, + add_by: bool, i18n: &I18n, ) { + let by_section = if add_by { + Self::generate_by_section(tag, "codes", i18n) + } else { + String::new() + }; + let internal_tag = if add_by { "" } else { tag }; // Zapobiega dublowaniu tagu let mut content = String::new(); // Wstawiamy wygenerowane drzewo ścieżek @@ -105,7 +136,7 @@ impl SaveFile { counter += 1; } - content.push_str(&format!("\n\n{}{}", tag, by_section)); + content.push_str(&format!("\n\n{}{}", internal_tag, by_section)); Self::write_to_disk( filepath, &content, diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index e32bd3d..ac2a661 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,6 +1,5 @@ use crate::interfaces::cli::args::CliArgs; use cargo_plot::addon::TimeTag; -use cargo_plot::core::by::BySection; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::core::path_store::PathContext; use cargo_plot::core::path_view::ViewMode; @@ -66,14 +65,6 @@ pub fn run(args: CliArgs) { if has_out_paths || has_out_codes { let tag = TimeTag::now(); - let internal_tag = if args.by { "" } else { &tag }; - - // let by_content = if args.by { - // BySection::generate(&tag) - // } else { - // String::new() - // }; - let output_str_txt = stats.render_output(view_mode, show_mode, args.info, false); // Closure do automatycznego generowania ścieżki @@ -105,29 +96,21 @@ pub fn run(args: CliArgs) { if let Some(val) = &args.out_path { let filepath = resolve_filepath(val, "paths"); - let by_content = if args.by { - BySection::generate(&tag, "paths", &i18n) - } else { - String::new() - }; - SaveFile::paths(&output_str_txt, &filepath, internal_tag, &by_content, &i18n); + // ⚡ CZYSTE WYWOŁANIE: podajemy args.by + SaveFile::paths(&output_str_txt, &filepath, &tag, args.by, &i18n); } if let Some(val) = &args.out_code { let filepath = resolve_filepath(val, "cache"); if let Ok(ctx) = PathContext::resolve(&args.enter_path) { - let by_content = if args.by { - BySection::generate(&tag, "codes", &i18n) - } else { - String::new() - }; + // ⚡ CZYSTE WYWOŁANIE: podajemy args.by SaveFile::codes( &output_str_txt, &stats.m_matched.paths, &ctx.entry_absolute, &filepath, - internal_tag, - &by_content, + &tag, + args.by, &i18n, ); } @@ -138,8 +121,14 @@ pub fn run(args: CliArgs) { if args.info { println!("---------------------------------------"); // ⚡ PODMIENIONO NA WYWOŁANIA Z I18N - println!("{}", i18n.cli_summary_matched(stats.m_size_matched, stats.total)); - println!("{}", i18n.cli_summary_rejected(stats.x_size_mismatched, stats.total)); + println!( + "{}", + i18n.cli_summary_matched(stats.m_size_matched, stats.total) + ); + println!( + "{}", + i18n.cli_summary_rejected(stats.x_size_mismatched, stats.total) + ); } else { println!("---------------------------------------"); } From 31fa43858173d9d76b9a8b1f1757b0fd2c30d260 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 12:48:39 +0100 Subject: [PATCH 30/45] (add h1) --- src/core/save.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/core/save.rs b/src/core/save.rs index 0b52b51..d0be90c 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -51,10 +51,11 @@ impl SaveFile { String::new() }; let internal_tag = if add_by { "" } else { tag }; // Zapobiega dublowaniu tagu - let markdown_content = format!( - "```plaintext\n{}\n```\n\n{}{}", - content, internal_tag, by_section - ); + let file_name = Path::new(filepath).file_name().unwrap_or_default().to_string_lossy(); + + // ⚡ DODAJE NAGŁÓWEK H1 NA POCZĄTKU + 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, @@ -83,8 +84,11 @@ impl SaveFile { String::new() }; let internal_tag = if add_by { "" } else { tag }; // Zapobiega dublowaniu tagu + 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); From 152d3919a2ad412936029e7b2ba4eb86f1d07086 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 12:52:03 +0100 Subject: [PATCH 31/45] fix --- src/core/save.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/core/save.rs b/src/core/save.rs index d0be90c..8af875d 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -51,10 +51,16 @@ impl SaveFile { String::new() }; let internal_tag = if add_by { "" } else { tag }; // Zapobiega dublowaniu tagu - let file_name = Path::new(filepath).file_name().unwrap_or_default().to_string_lossy(); - + let file_name = Path::new(filepath) + .file_name() + .unwrap_or_default() + .to_string_lossy(); + // ⚡ DODAJE NAGŁÓWEK H1 NA POCZĄTKU - let markdown_content = format!("# {}\n\n```plaintext\n{}\n```\n\n{}{}", file_name, content, internal_tag, by_section); + let markdown_content = format!( + "# {}\n\n```plaintext\n{}\n```\n\n{}{}", + file_name, content, internal_tag, by_section + ); Self::write_to_disk( filepath, @@ -84,11 +90,14 @@ impl SaveFile { String::new() }; let internal_tag = if add_by { "" } else { tag }; // Zapobiega dublowaniu tagu - let file_name = Path::new(filepath).file_name().unwrap_or_default().to_string_lossy(); - + 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); From ded0e9f0091a41514624ddfc398a1ccb4ad525f8 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Tue, 17 Mar 2026 13:31:19 +0100 Subject: [PATCH 32/45] (add: tui) --- Cargo.toml | 2 +- src/interfaces/cli/args.rs | 6 +- src/interfaces/tui.rs | 10 +- src/interfaces/tui/i18n.rs | 224 ++++++----- src/interfaces/tui/menu.rs | 261 ++++++++++++- src/interfaces/tui/menu/enter.rs | 198 ---------- src/interfaces/tui/menu/job_add.rs | 119 ------ src/interfaces/tui/menu/job_add_custom.rs | 366 ------------------ src/interfaces/tui/menu/jobs_manager.rs | 162 -------- src/interfaces/tui/menu/output_save.rs | 321 --------------- src/interfaces/tui/menu/paths_struct_style.rs | 301 -------------- src/interfaces/tui/state.rs | 157 ++------ 12 files changed, 416 insertions(+), 1711 deletions(-) delete mode 100644 src/interfaces/tui/menu/enter.rs delete mode 100644 src/interfaces/tui/menu/job_add.rs delete mode 100644 src/interfaces/tui/menu/job_add_custom.rs delete mode 100644 src/interfaces/tui/menu/jobs_manager.rs delete mode 100644 src/interfaces/tui/menu/output_save.rs delete mode 100644 src/interfaces/tui/menu/paths_struct_style.rs diff --git a/Cargo.toml b/Cargo.toml index 004a8f1..13b7b18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-plot" -version = "0.2.0-alpha.1" +version = "0.2.0-beta" authors = ["Jan Roman Cisowski „j-Cis”"] edition = "2024" rust-version = "1.94.0" diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 6f9077c..1a838b1 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -14,7 +14,7 @@ pub enum CargoCli { } /// [POL]: Nasze docelowe argumenty CLI. Zauważ, że teraz to jest `Args`, a nie `Parser`. -#[derive(Args, Debug)] +#[derive(Args, Debug, Clone)] #[command(author, version, about = "Zaawansowany skaner struktury plików", long_about = None)] pub struct CliArgs { /// [ENG]: Input path to scan. @@ -79,14 +79,14 @@ pub struct CliArgs { pub lang: Option, } -#[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] pub enum CliViewMode { Tree, List, Grid, } -#[derive(Debug, Clone, Copy, ValueEnum)] +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] pub enum CliSortStrategy { None, Az, diff --git a/src/interfaces/tui.rs b/src/interfaces/tui.rs index a70e7de..ca1b168 100644 --- a/src/interfaces/tui.rs +++ b/src/interfaces/tui.rs @@ -5,11 +5,9 @@ pub mod i18n; pub mod menu; pub mod state; -use state::StateTui; - pub fn run_tui() { - let mut s = StateTui::new(); - cliclack::intro(" 📖 https://crates.io/crates/cargo-plot").unwrap(); - - menu::enter::menu_enter(&mut s); + 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 index 57645f9..41a022b 100644 --- a/src/interfaces/tui/i18n.rs +++ b/src/interfaces/tui/i18n.rs @@ -1,156 +1,182 @@ -use super::state::Lang; +use cargo_plot::i18n::Lang; +use console::style; -// ===================================================================== -// BAZOWY FORMATER -// ===================================================================== - -// Uniwersalny padding - dodaje spacje po bokach każdego tekstu. -// Zmieniasz to tutaj -> zmienia się w CAŁEJ aplikacji! fn pad(text: &str) -> String { format!(" {} ", text) } -// ===================================================================== -// STRUKTURA WYMUSZAJĄCA JAWNE NAZWY JĘZYKÓW -// ===================================================================== - pub struct Txt { pub pol: &'static str, pub eng: &'static str, } -// ===================================================================== -// GŁÓWNA CECHA (TRAIT) -// ===================================================================== - pub trait Translatable { - // 1. Zwraca tekst w wielu językach fn trans(&self) -> Txt; - - // 2. Opcjonalna stylizacja (kolory itp.). - // Domyślnie zwraca tekst bez zmian. fn theme(&self, text: String) -> String { text } } -// ===================================================================== -// GLOBALNE TEKSTY (Nagłówki, Prompty, Komunikaty) -// ===================================================================== - pub enum Prompt { - HeaderEnter, - HeaderJobAdd, - HeaderJobAddCustom, - HeaderJobsManager, - HeaderStyle, - HeaderOutput, - EnterDepth, - NoJobsWarning, - SuccessJobAdd, - JobAlreadyAtTop, - JobAlreadyAtBottom, - SuccessJobDeleted, - JobTitlePrefix, + HeaderMain, + BtnLang, + BtnQuickStart, + BtnPaths, + BtnView, + BtnOutput, + BtnFilters, + BtnRun, + BtnExit, + InputPatterns, ExitBye, - Canceled, + WarnNoPatterns, + // Prompty dla podmenu (wersje dwujęzyczne w jednej linii dla szybkości) + SubBasePath, + SubIgnoreCase, + SubSelectView, + SubSelectSort, + SubNoRoot, + SubOutPaths, + SubOutCode, + SubBy, + SubOnMatch, + SubOnMismatch, + SubInfo, } impl Translatable for Prompt { fn trans(&self) -> Txt { match self { - Prompt::HeaderEnter => Txt { - pol: "📦 j-Cis/cargo-plot [POL]", - eng: "📦 j-Cis/cargo-plot [ENG]", + Prompt::HeaderMain => Txt { + pol: "📦 cargo-plot [POL] - Interaktywny Kreator", + eng: "📦 cargo-plot [ENG] - Interactive Builder", }, - Prompt::HeaderJobAdd => Txt { - pol: "Dodawanie zadania", - eng: "Adding a job", + Prompt::BtnLang => Txt { + pol: "🌍 Zmień język / Change language", + eng: "🌍 Zmień język / Change language", }, - Prompt::HeaderJobAddCustom => Txt { - pol: "Definiowanie zadania", - eng: "Customizing job", + Prompt::BtnQuickStart => Txt { + pol: "🚀 SZYBKI START (Podaj wzorce i uruchom)", + eng: "🚀 QUICK START (Enter patterns and run)", }, - Prompt::HeaderJobsManager => Txt { - pol: "Menadżer zadań", - eng: "Jobs manager", + Prompt::BtnPaths => Txt { + pol: "🛠️ Ścieżki i Wzorce", + eng: "🛠️ Paths and Patterns", }, - Prompt::HeaderStyle => Txt { - pol: "Stylizacja struktury", - eng: "Paths structure styling", + Prompt::BtnView => Txt { + pol: "👁️ Widok i Sortowanie", + eng: "👁️ View and Sorting", }, - Prompt::HeaderOutput => Txt { - pol: "Zapisywanie wyniku", - eng: "Saving output", + Prompt::BtnOutput => Txt { + pol: "💾 Zapis plików", + eng: "💾 Output and Saving", }, - Prompt::EnterDepth => Txt { - pol: "Podaj głębokość:", - eng: "Enter depth:", + Prompt::BtnFilters => Txt { + pol: "⚙️ Filtry i Opcje", + eng: "⚙️ Filters and Options", }, - Prompt::NoJobsWarning => Txt { - pol: "Brak zadań w kolejce!", - eng: "No jobs in the queue!", + Prompt::BtnRun => Txt { + pol: "▶️ URUCHOM SKANOWANIE", + eng: "▶️ RUN SCANNER", }, - Prompt::SuccessJobAdd => Txt { - pol: "Dodano zadanie do kolejki!", - eng: "Job added to queue!", + Prompt::BtnExit => Txt { + pol: "❌ WYJŚCIE", + eng: "❌ EXIT", }, - Prompt::JobAlreadyAtTop => Txt { - pol: "Zadanie jest już na samej górze!", - eng: "Job is already at the top!", - }, - Prompt::JobAlreadyAtBottom => Txt { - pol: "Zadanie jest już na samym dole!", - eng: "Job is already at the bottom!", - }, - Prompt::SuccessJobDeleted => Txt { - pol: "Usunięto zadanie.", - eng: "Job deleted.", - }, - Prompt::JobTitlePrefix => Txt { - pol: "Zadanie: ", - eng: "Job: ", + 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::Canceled => Txt { - pol: "Anulowano", - eng: "Canceled", + 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::SubOutPaths => Txt { + pol: "Plik na ścieżki (puste=Brak, AUTO=domyślny) / Paths output file:", + eng: "Plik na ścieżki (puste=Brak, AUTO=domyślny) / Paths output file:", + }, + Prompt::SubOutCode => Txt { + pol: "Plik na kod (puste=Brak, AUTO=domyślny) / Code output file:", + eng: "Plik na kod (puste=Brak, AUTO=domyślny) / Code output file:", + }, + 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?", }, } } -} -// ===================================================================== -// KONTEKST TŁUMACZA (Translator) -// ===================================================================== + 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(), + _ => text, + } + } +} pub struct T { lang: Lang, } + impl T { pub fn new(lang: Lang) -> Self { Self { lang } } - - fn get_text(&self, item: &I) -> String { + pub fn fmt(&self, item: I) -> String { let txt = item.trans(); let text = match self.lang { - Lang::POL => txt.pol, - Lang::ENG => txt.eng, + Lang::Pl => txt.pol, + Lang::En => txt.eng, }; - pad(text) - } - - // Zmiana nazwy na `fmt`! - pub fn fmt(&self, item: I) -> String { - let base_text = self.get_text(&item); - item.theme(base_text) + item.theme(pad(text)) } - pub fn raw(&self, item: I) -> String { - self.get_text(&item) + 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 index e03b3dd..b61149e 100644 --- a/src/interfaces/tui/menu.rs +++ b/src/interfaces/tui/menu.rs @@ -1,6 +1,255 @@ -pub mod enter; -pub mod job_add; -pub mod job_add_custom; -pub mod jobs_manager; -pub mod output_save; -pub mod paths_struct_style; +use super::i18n::{Prompt, T}; +use super::state::StateTui; +use crate::interfaces::cli::args::{CliSortStrategy, CliViewMode}; +use crate::interfaces::cli::engine; + +#[derive(Clone, PartialEq, Eq)] +enum Action { + Lang, + QuickStart, + Paths, + View, + Output, + Filters, + Run, + 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.out_path.as_deref().unwrap_or("NONE"); + let out_c = s.args.out_code.as_deref().unwrap_or("NONE"); + let lbl_out = format!( + "{} (paths: {}, cache: {}, by: {})", + t.fmt(Prompt::BtnOutput), + out_p, + out_c, + 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 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::Paths, lbl_paths, "") + .item(Action::View, lbl_view, "") + .item(Action::Output, lbl_out, "") + .item(Action::Filters, lbl_filt, "") + .item(Action::Run, t.fmt(Prompt::BtnRun), "") + .item(Action::Exit, t.fmt(Prompt::BtnExit), "") + .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::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::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::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::SubOutPaths)) + .default_input(s.args.out_path.as_deref().unwrap_or("")) + .interact() + .unwrap_or_default(); + s.args.out_path = if out_p.trim().is_empty() { + None + } else { + Some(out_p.trim().to_string()) + }; + + let out_c: String = cliclack::input(t.raw(Prompt::SubOutCode)) + .default_input(s.args.out_code.as_deref().unwrap_or("")) + .interact() + .unwrap_or_default(); + s.args.out_code = if out_c.trim().is_empty() { + None + } else { + Some(out_c.trim().to_string()) + }; + + 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 +} diff --git a/src/interfaces/tui/menu/enter.rs b/src/interfaces/tui/menu/enter.rs deleted file mode 100644 index 892e511..0000000 --- a/src/interfaces/tui/menu/enter.rs +++ /dev/null @@ -1,198 +0,0 @@ -use super::super::i18n::{Prompt, T, Translatable, Txt}; -use super::super::state::{Lang, StateTui}; -use super::job_add::menu_job_add; -use super::jobs_manager::menu_jobs_manager; -use super::output_save::menu_output_save; -use super::paths_struct_style::menu_paths_struct_style; -use console::style; - -#[derive(Default, Clone, Eq, PartialEq)] -enum ActionEnter { - #[default] - ChangeLang, - JobAdd, - JobsManager, - PathsStructStyle, - PathsStructPrint, - OutputSave, - CommandViewStructure, - CommandSaveDocuments, - Exit, -} - -//impl Default for ActionEnter { -// fn default() -> Self { -// Self::ChangeLang -// } -//} - -// ===================================================================== -// WARSTWA JĘZYKOWO - STYLIZACYJNA -// ===================================================================== - -impl Translatable for ActionEnter { - fn trans(&self) -> Txt { - match self { - ActionEnter::ChangeLang => Txt { - pol: "ustaw POL język", - eng: "set ENG lang", - }, - ActionEnter::JobAdd => Txt { - pol: "dodaj zadanie", - eng: "add job", - }, - ActionEnter::JobsManager => Txt { - pol: "menadżer zadań", - eng: "manager jobs", - }, - ActionEnter::PathsStructStyle => Txt { - pol: "stylizacja struktury ścieżek", - eng: "style of paths structure", - }, - ActionEnter::PathsStructPrint => Txt { - pol: "wyświetl strukturę ścieżek", - eng: "print the path structure", - }, - ActionEnter::OutputSave => Txt { - pol: "zapisz wynik", - eng: "save output", - }, - ActionEnter::CommandViewStructure => Txt { - pol: "komenda podglądu struktury", - eng: "command print structure", - }, - ActionEnter::CommandSaveDocuments => Txt { - pol: "komenda zapisu dokumentacji", - eng: "command save documents", - }, - ActionEnter::Exit => Txt { - pol: "wyjście", - eng: "exit", - }, - } - } - - fn theme(&self, text: String) -> String { - match self { - // Wyróżniamy "Dodaj zadanie" na białym tle - ActionEnter::JobAdd => style(text).on_white().black().to_string(), - - // Wyróżniamy akcje związane z wynikiem mocnym niebieskim - ActionEnter::PathsStructPrint | ActionEnter::OutputSave => { - style(text).bold().on_blue().white().to_string() - } - - // NOWOŚĆ: Fioletowe tło (magenta) i biały tekst dla komendy - ActionEnter::CommandViewStructure => { - style(text).bold().on_magenta().white().to_string() - } - // NOWOŚĆ: Fioletowe tło (magenta) i biały tekst dla komendy - ActionEnter::CommandSaveDocuments => { - style(text).bold().on_magenta().white().to_string() - } - - // Reszta bez specjalnego formatowania - _ => text, - } - } -} - -// ===================================================================== -// WIDOK MENU GŁÓWNEGO -// ===================================================================== - -pub fn menu_enter(s: &mut StateTui) { - let mut last_action = ActionEnter::default(); - - loop { - // 1. INICJUJEMY TŁUMACZA DLA TEJ PĘTLI - let t = T::new(s.lang); - - // 2. STYLIZUJEMY GŁÓWNY NAGŁÓWEK (Pobierany z globalnych Promptów) - let header = style(t.raw(Prompt::HeaderEnter)) - .on_white() - .black() - .to_string(); - - // 3. BUDUJEMY MENU - let action_result = cliclack::select(header) - .initial_value(last_action.clone()) - .item(ActionEnter::ChangeLang, t.fmt(ActionEnter::ChangeLang), "") - .item(ActionEnter::JobAdd, t.fmt(ActionEnter::JobAdd), "") - .item( - ActionEnter::JobsManager, - t.fmt(ActionEnter::JobsManager), - "", - ) - .item( - ActionEnter::PathsStructStyle, - t.fmt(ActionEnter::PathsStructStyle), - "", - ) - .item( - ActionEnter::PathsStructPrint, - t.fmt(ActionEnter::PathsStructPrint), - "", - ) - .item(ActionEnter::OutputSave, t.fmt(ActionEnter::OutputSave), "") - .item( - ActionEnter::CommandViewStructure, - t.fmt(ActionEnter::CommandViewStructure), - "", - ) - .item( - ActionEnter::CommandSaveDocuments, - t.fmt(ActionEnter::CommandSaveDocuments), - "", - ) - .item(ActionEnter::Exit, t.fmt(ActionEnter::Exit), "") - .interact(); - - // 4. OBSŁUGA AKCJI - match action_result { - Ok(action) => { - last_action = action.clone(); - - match action { - ActionEnter::ChangeLang => { - s.lang = match s.lang { - Lang::POL => Lang::ENG, - Lang::ENG => Lang::POL, - }; - let t_new = T::new(s.lang); - cliclack::intro(t_new.raw(Prompt::HeaderEnter)).unwrap(); - } - ActionEnter::JobAdd => { - menu_job_add(s); - } - ActionEnter::JobsManager => { - menu_jobs_manager(s); - } - ActionEnter::PathsStructStyle => { - menu_paths_struct_style(s); - } - ActionEnter::PathsStructPrint => { - // TUTAJ MIEJSCE NA FUNKCJE WYŚWIETLANIA DRZEWA (np. pager minus) - } - ActionEnter::OutputSave => { - menu_output_save(s); - } - ActionEnter::CommandViewStructure => { - // Na razie opcja nic nie robi - wraca z powrotem do pętli menu głównego - } - ActionEnter::CommandSaveDocuments => { - // Na razie opcja nic nie robi - wraca z powrotem do pętli menu głównego - } - ActionEnter::Exit => { - cliclack::outro(t.raw(Prompt::ExitBye)).unwrap(); - break; - } - } - } - Err(_) => { - cliclack::outro_cancel(t.raw(Prompt::Canceled)).unwrap(); - break; - } - } - } -} diff --git a/src/interfaces/tui/menu/job_add.rs b/src/interfaces/tui/menu/job_add.rs deleted file mode 100644 index 3f84c1d..0000000 --- a/src/interfaces/tui/menu/job_add.rs +++ /dev/null @@ -1,119 +0,0 @@ -use super::super::i18n::{Prompt, T, Translatable, Txt}; // Importujemy nasz nowy, odchudzony silnik! -use super::super::state::{JobConfig, StateTui}; -use super::job_add_custom::menu_job_add_custom; -use console::style; - -#[derive(Default, Clone, Eq, PartialEq)] -enum ActionJobAdd { - #[default] - JobAddDefault, - JobAddCustom, - Back, -} - -//impl Default for ActionJobAdd { -// fn default() -> Self { -// Self::JobAddDefault -// } -//} - -// ===================================================================== -// WARSTWA JĘZYKOWO - STYLIZACYJNA -// ===================================================================== - -impl Translatable for ActionJobAdd { - fn trans(&self) -> Txt { - match self { - ActionJobAdd::JobAddDefault => Txt { - pol: "Dodaj zadanie domyślne", - eng: "Add default job", - }, - ActionJobAdd::JobAddCustom => Txt { - pol: "Zdefiniuj zadanie", - eng: "Customize job", - }, - ActionJobAdd::Back => Txt { - pol: "Powrót", - eng: "Back", - }, - } - } - - // Nadpisujemy domyślny styl tylko dla jednego przycisku! - fn theme(&self, text: String) -> String { - match self { - ActionJobAdd::JobAddCustom => style(text).on_white().blue().to_string(), - _ => text, // Pozostałe opcje zwracają zwykły tekst - } - } -} - -// ===================================================================== -// WIDOK MENU -// ===================================================================== - -pub fn menu_job_add(s: &mut StateTui) { - let mut last_action = ActionJobAdd::default(); - - loop { - // 1. INICJUJEMY TŁUMACZA DLA TEJ PĘTLI - let t = T::new(s.lang); - - // 2. STYLIZUJEMY NAGŁÓWEK (pobierany z globalnych Promptów) - let header = style(t.raw(Prompt::HeaderJobAdd)) - .on_white() - .black() - .to_string(); - - // 3. BUDUJEMY MENU (czysto, zwięźle i z podpowiedziami kompilatora) - let action_result = cliclack::select(header) - .initial_value(last_action.clone()) - .item( - ActionJobAdd::JobAddDefault, - t.fmt(ActionJobAdd::JobAddDefault), - "", - ) - .item( - ActionJobAdd::JobAddCustom, - t.fmt(ActionJobAdd::JobAddCustom), - "", - ) - .item(ActionJobAdd::Back, t.fmt(ActionJobAdd::Back), "") - .interact(); - - match action_result { - Ok(action) => { - last_action = action.clone(); - - match action { - ActionJobAdd::JobAddDefault => { - s.add_job(JobConfig::default()); - - // Sukces i wyjście też przepuszczamy przez tłumacza - cliclack::log::success(t.raw(Prompt::SuccessJobAdd)).unwrap(); - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - ActionJobAdd::JobAddCustom => { - let saved = menu_job_add_custom(s); - if saved { - return; - } - } - ActionJobAdd::Back => { - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - } - } - Err(_) => { - cliclack::clear_screen().unwrap(); - let t_err = T::new(s.lang); // Tłumacz na wypadek błędu (np. Ctrl+C) - cliclack::intro(t_err.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - } - } -} diff --git a/src/interfaces/tui/menu/job_add_custom.rs b/src/interfaces/tui/menu/job_add_custom.rs deleted file mode 100644 index 677a40d..0000000 --- a/src/interfaces/tui/menu/job_add_custom.rs +++ /dev/null @@ -1,366 +0,0 @@ -use super::super::i18n::{Prompt, T, Translatable, Txt}; -use super::super::state::{JobConfig, Lang, StateTui}; -use console::style; - -#[derive(Default, Clone, Eq, PartialEq)] -enum ActionJobAddCustom { - #[default] - JobTitle, - PathEnter, - PathIncludeParentFile, - GlobPathWhiteList, - ClearWhiteList, - GlobPathBlackList, - ClearBlackList, - FileTypes, - ClearFileTypes, - DirsIncludeEmpty, - DirsOnly, - DirsKeepExcludedAsEmptyToDepth, - Save, - Back, -} - -// ===================================================================== -// WARSTWA JĘZYKOWO - STYLIZACYJNA -// ===================================================================== - -impl Translatable for ActionJobAddCustom { - fn trans(&self) -> Txt { - match self { - ActionJobAddCustom::JobTitle => Txt { - pol: "Tytuł zadania", - eng: "Job title", - }, - ActionJobAddCustom::PathEnter => Txt { - pol: "Ścieżka wejściowa", - eng: "Enter path", - }, - ActionJobAddCustom::PathIncludeParentFile => Txt { - pol: "Dołącz plik o tej samej nazwie poziom wyżej", - eng: "Include file with same name one level up", - }, - ActionJobAddCustom::GlobPathWhiteList => Txt { - pol: "Biała lista (include)", - eng: "White list (include)", - }, - ActionJobAddCustom::ClearWhiteList => Txt { - pol: " [ Wyczyść białą listę ]", - eng: " [ Clear white list ]", - }, - ActionJobAddCustom::GlobPathBlackList => Txt { - pol: "Czarna lista (exclude)", - eng: "Black list (exclude)", - }, - ActionJobAddCustom::ClearBlackList => Txt { - pol: " [ Wyczyść czarną listę ]", - eng: " [ Clear black list ]", - }, - ActionJobAddCustom::FileTypes => Txt { - pol: "Filtry plików w podkatalogach", - eng: "Subdirectory file filters", - }, - ActionJobAddCustom::ClearFileTypes => Txt { - pol: " [ Wyczyść filtry plików ]", - eng: " [ Clear file filters ]", - }, - ActionJobAddCustom::DirsIncludeEmpty => Txt { - pol: "Uwzględnij puste ścieżki", - eng: "Include empty paths", - }, - ActionJobAddCustom::DirsOnly => Txt { - pol: "Zachowuj tylko foldery - bez plików", - eng: "Keep only directories without files", - }, - ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth => Txt { - pol: "Zachowuj wykluczone katalogi jako puste aż do określonej głębokości", - eng: "Keep excluded directories as empty up to a specified depth", - }, - ActionJobAddCustom::Save => Txt { - pol: "--- ZAPISZ ZADANIE ---", - eng: "--- SAVE JOB ---", - }, - ActionJobAddCustom::Back => Txt { - pol: "Powrót", - eng: "Back", - }, - } - } - - fn theme(&self, text: String) -> String { - match self { - ActionJobAddCustom::Save => style(text).on_green().black().bold().to_string(), - ActionJobAddCustom::ClearWhiteList - | ActionJobAddCustom::ClearBlackList - | ActionJobAddCustom::ClearFileTypes => style(text).yellow().italic().to_string(), - _ => text, - } - } -} - -// ===================================================================== -// WIDOK MENU -// ===================================================================== - -pub fn menu_job_add_custom(s: &mut StateTui) -> bool { - let mut last_action = ActionJobAddCustom::default(); - let mut current_job = JobConfig::default(); - - loop { - let t = T::new(s.lang); - let header = style(t.raw(Prompt::HeaderJobAddCustom)) - .on_white() - .black() - .to_string(); - - // 1. Zabezpieczenie UX (Ukrywanie opcji zależnych od czarnej listy) - if current_job.glob_excludes.is_empty() { - current_job.dirs_only = false; - current_job.dirs_keep_excluded_as_empty_to_depth = 0; - - if last_action == ActionJobAddCustom::DirsOnly - || last_action == ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth - { - last_action = ActionJobAddCustom::GlobPathBlackList; - } - } - - // 2. Przygotowanie etykiet - let toggle = |b: bool| if b { "[x]" } else { "[ ]" }; - let rules_txt = match s.lang { - Lang::POL => "reguł", - Lang::ENG => "rules", - }; - let format_list = |v: &Vec| { - if v.is_empty() { - "[]".to_string() - } else { - format!("[{}]", v.join(", ")) - } - }; - - let title_lbl = format!( - "{} [{}]", - t.fmt(ActionJobAddCustom::JobTitle), - current_job.title - ); - let path_lbl = format!( - "{} [{}]", - t.fmt(ActionJobAddCustom::PathEnter), - current_job.path_enter - ); - let parent_file_lbl = format!( - "{} {}", - t.fmt(ActionJobAddCustom::PathIncludeParentFile), - toggle(current_job.path_include_parent_file) - ); - - let include_lbl = format!( - "{} {}", - t.fmt(ActionJobAddCustom::GlobPathWhiteList), - format_list(¤t_job.glob_includes) - ); - let exclude_lbl = format!( - "{} {}", - t.fmt(ActionJobAddCustom::GlobPathBlackList), - format_list(¤t_job.glob_excludes) - ); - let file_types_lbl = format!( - "{} ({} {}) {}", - t.fmt(ActionJobAddCustom::FileTypes), - current_job.file_types.len(), - rules_txt, - format_list(¤t_job.file_types) - ); - - let dirs_empty_lbl = format!( - "{} {}", - t.fmt(ActionJobAddCustom::DirsIncludeEmpty), - toggle(current_job.dirs_include_empty) - ); - let dirs_only_lbl = format!( - "{} {}", - t.fmt(ActionJobAddCustom::DirsOnly), - toggle(current_job.dirs_only) - ); - - // 3. Budowanie Menu - let mut menu = cliclack::select(header) - .initial_value(last_action.clone()) - .item(ActionJobAddCustom::JobTitle, title_lbl, "") - .item(ActionJobAddCustom::PathEnter, path_lbl, "") - .item( - ActionJobAddCustom::PathIncludeParentFile, - parent_file_lbl, - "", - ) - .item(ActionJobAddCustom::GlobPathWhiteList, include_lbl, ""); - - if !current_job.glob_includes.is_empty() { - menu = menu.item( - ActionJobAddCustom::ClearWhiteList, - t.fmt(ActionJobAddCustom::ClearWhiteList), - "", - ); - } - - menu = menu.item(ActionJobAddCustom::GlobPathBlackList, exclude_lbl, ""); - - if !current_job.glob_excludes.is_empty() { - menu = menu.item( - ActionJobAddCustom::ClearBlackList, - t.fmt(ActionJobAddCustom::ClearBlackList), - "", - ); - } - - menu = menu.item(ActionJobAddCustom::FileTypes, file_types_lbl, ""); - - if !current_job.file_types.is_empty() { - menu = menu.item( - ActionJobAddCustom::ClearFileTypes, - t.fmt(ActionJobAddCustom::ClearFileTypes), - "", - ); - } - - menu = menu.item(ActionJobAddCustom::DirsIncludeEmpty, dirs_empty_lbl, ""); - - if !current_job.glob_excludes.is_empty() { - menu = menu.item(ActionJobAddCustom::DirsOnly, dirs_only_lbl, ""); - menu = menu.item( - ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth, - t.fmt(ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth), - "", - ); - } - - let action_result = menu - .item( - ActionJobAddCustom::Save, - t.fmt(ActionJobAddCustom::Save), - "", - ) - .item( - ActionJobAddCustom::Back, - t.fmt(ActionJobAddCustom::Back), - "", - ) - .interact(); - - // 4. Obsługa akcji - match action_result { - Ok(action) => { - last_action = action.clone(); - - match action { - ActionJobAddCustom::JobTitle => { - let val: String = cliclack::input(t.raw(ActionJobAddCustom::JobTitle)) - .default_input(¤t_job.title) - .interact() - .unwrap_or(current_job.title.clone()); - current_job.title = val; - } - ActionJobAddCustom::PathEnter => { - let val: String = cliclack::input(t.raw(ActionJobAddCustom::PathEnter)) - .default_input(¤t_job.path_enter) - .interact() - .unwrap_or(current_job.path_enter.clone()); - current_job.path_enter = val; - } - ActionJobAddCustom::PathIncludeParentFile => { - current_job.path_include_parent_file = - !current_job.path_include_parent_file; - } - ActionJobAddCustom::GlobPathWhiteList => { - let val: String = - cliclack::input(t.raw(ActionJobAddCustom::GlobPathWhiteList)) - .multiline() - .default_input(¤t_job.glob_includes.join("\n")) - .interact() - .unwrap_or(current_job.glob_includes.join("\n")); - current_job.glob_includes = val - .lines() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } - ActionJobAddCustom::ClearWhiteList => { - current_job.glob_includes.clear(); - last_action = ActionJobAddCustom::GlobPathWhiteList; - } - ActionJobAddCustom::GlobPathBlackList => { - let val: String = - cliclack::input(t.raw(ActionJobAddCustom::GlobPathBlackList)) - .multiline() - .default_input(¤t_job.glob_excludes.join("\n")) - .interact() - .unwrap_or(current_job.glob_excludes.join("\n")); - current_job.glob_excludes = val - .lines() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } - ActionJobAddCustom::ClearBlackList => { - current_job.glob_excludes.clear(); - last_action = ActionJobAddCustom::GlobPathBlackList; - } - ActionJobAddCustom::FileTypes => { - let val: String = cliclack::input(t.raw(ActionJobAddCustom::FileTypes)) - .multiline() - .default_input(¤t_job.file_types.join("\n")) - .interact() - .unwrap_or(current_job.file_types.join("\n")); - current_job.file_types = val - .lines() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } - ActionJobAddCustom::ClearFileTypes => { - current_job.file_types.clear(); - last_action = ActionJobAddCustom::FileTypes; - } - ActionJobAddCustom::DirsIncludeEmpty => { - current_job.dirs_include_empty = !current_job.dirs_include_empty; - } - ActionJobAddCustom::DirsOnly => { - current_job.dirs_only = !current_job.dirs_only; - } - ActionJobAddCustom::DirsKeepExcludedAsEmptyToDepth => { - let val: String = cliclack::input(t.raw(Prompt::EnterDepth)) - .default_input( - ¤t_job.dirs_keep_excluded_as_empty_to_depth.to_string(), - ) - .interact() - .unwrap_or_else(|_| { - current_job.dirs_keep_excluded_as_empty_to_depth.to_string() - }); - if let Ok(num) = val.parse::() { - current_job.dirs_keep_excluded_as_empty_to_depth = num; - } - } - ActionJobAddCustom::Save => { - s.add_job(current_job); - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); - return true; - } - ActionJobAddCustom::Back => { - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderJobAdd)).unwrap(); - return false; - } - } - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderJobAddCustom)).unwrap(); - } - Err(_) => { - cliclack::clear_screen().unwrap(); - cliclack::intro(T::new(s.lang).raw(Prompt::HeaderJobAdd)).unwrap(); - return false; - } - } - } -} diff --git a/src/interfaces/tui/menu/jobs_manager.rs b/src/interfaces/tui/menu/jobs_manager.rs deleted file mode 100644 index e98f108..0000000 --- a/src/interfaces/tui/menu/jobs_manager.rs +++ /dev/null @@ -1,162 +0,0 @@ -use super::super::i18n::{Prompt, T, Translatable, Txt}; -use super::super::state::StateTui; -use console::style; - -#[derive(Default, Clone, Eq, PartialEq)] -enum ActionJobManage { - MoveUp, - MoveDown, - View, - Delete, - #[default] - Back, -} - -// ===================================================================== -// WARSTWA JĘZYKOWO - STYLIZACYJNA -// ===================================================================== - -impl Translatable for ActionJobManage { - fn trans(&self) -> Txt { - match self { - ActionJobManage::MoveUp => Txt { - pol: "Przesuń wyżej", - eng: "Move up", - }, - ActionJobManage::MoveDown => Txt { - pol: "Przesuń niżej", - eng: "Move down", - }, - ActionJobManage::View => Txt { - pol: "Podgląd", - eng: "View", - }, - ActionJobManage::Delete => Txt { - pol: "Usuń zadanie", - eng: "Delete job", - }, - ActionJobManage::Back => Txt { - pol: "Powrót", - eng: "Back", - }, - } - } - - fn theme(&self, text: String) -> String { - match self { - ActionJobManage::Delete => style(text).on_red().white().bold().to_string(), // Ostrzegawczy czerwony! - _ => text, - } - } -} - -// ===================================================================== -// WIDOK MENU GŁÓWNEGO MENADŻERA -// ===================================================================== - -pub fn menu_jobs_manager(s: &mut StateTui) { - loop { - let t = T::new(s.lang); - - if s.jobs.is_empty() { - cliclack::log::warning(t.raw(Prompt::NoJobsWarning)).unwrap(); - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - - let header = style(t.raw(Prompt::HeaderJobsManager)) - .on_white() - .black() - .to_string(); - - // USTAWIALNY START: usize::MAX to nasz przycisk "Wstecz" - let mut menu = cliclack::select(header).initial_value(usize::MAX); - - for (index, job) in s.jobs.iter().enumerate() { - let styled_title = style(format!(" [{}] {}", index, job.title)) - .yellow() - .to_string(); - menu = menu.item(index, styled_title, ""); - } - - menu = menu.item(usize::MAX, t.fmt(ActionJobManage::Back), ""); - - let selection = menu.interact(); - - match selection { - Ok(usize::MAX) | Err(_) => { - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - Ok(job_index) => { - // Odpalamy podmenu dla wybranego zadania - manage_single_job(s, job_index); - } - } - } -} - -// ===================================================================== -// WIDOK ZARZĄDZANIA POJEDYNCZYM ZADANIEM -// ===================================================================== - -fn manage_single_job(s: &mut StateTui, index: usize) { - loop { - let t = T::new(s.lang); - - if index >= s.jobs.len() { - return; - } - - let job_title = &s.jobs[index].title; - // Sklejamy: "Zadanie: " + "Tytuł" - let prompt_title = format!("{}{}", t.raw(Prompt::JobTitlePrefix), job_title); - - let action_result = cliclack::select(prompt_title) - .item(ActionJobManage::MoveUp, t.fmt(ActionJobManage::MoveUp), "") - .item( - ActionJobManage::MoveDown, - t.fmt(ActionJobManage::MoveDown), - "", - ) - .item(ActionJobManage::View, t.fmt(ActionJobManage::View), "") - .item(ActionJobManage::Delete, t.fmt(ActionJobManage::Delete), "") - .item(ActionJobManage::Back, t.fmt(ActionJobManage::Back), "") - .interact(); - - match action_result { - Ok(ActionJobManage::MoveUp) => { - if index > 0 { - s.jobs.swap(index, index - 1); - return; - } else { - cliclack::log::warning(t.raw(Prompt::JobAlreadyAtTop)).unwrap(); - } - } - Ok(ActionJobManage::MoveDown) => { - if index < s.jobs.len() - 1 { - s.jobs.swap(index, index + 1); - return; - } else { - cliclack::log::warning(t.raw(Prompt::JobAlreadyAtBottom)).unwrap(); - } - } - Ok(ActionJobManage::View) => { - let job_info = format!("{:#?}", s.jobs[index]); - cliclack::log::info(job_info).unwrap(); - } - Ok(ActionJobManage::Delete) => { - s.jobs.remove(index); - cliclack::log::success(t.raw(Prompt::SuccessJobDeleted)).unwrap(); - return; - } - Ok(ActionJobManage::Back) | Err(_) => { - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderJobsManager)).unwrap(); - return; - } - } - } -} diff --git a/src/interfaces/tui/menu/output_save.rs b/src/interfaces/tui/menu/output_save.rs deleted file mode 100644 index bfd5891..0000000 --- a/src/interfaces/tui/menu/output_save.rs +++ /dev/null @@ -1,321 +0,0 @@ -use super::super::i18n::{Prompt, T, Translatable, Txt}; -use super::super::state::{Lang, StateTui}; -use console::style; - -#[derive(Default, Clone, Eq, PartialEq)] -enum ActionOutput { - #[default] - ExecuteSave, - SaveFileF, - SaveFileD, - SaveFileC, - AddStructToDoc, - AddCmdToStruct, - AddCmdToDoc, - AutoSectionNum, - TimestampInFile, - TimestampInFilename, - SelfPromo, - OutputFolder, - SectionPrefix, - DocTitle, - Back, -} - -//impl Default for ActionOutput { -// fn default() -> Self { Self::ExecuteSave } -//} - -// ===================================================================== -// WARSTWA JĘZYKOWO - STYLIZACYJNA -// ===================================================================== - -impl Translatable for ActionOutput { - fn trans(&self) -> Txt { - match self { - ActionOutput::ExecuteSave => Txt { - pol: "--- WYKONAJ ZAPIS (START) ---", - eng: "--- EXECUTE SAVE (START) ---", - }, - ActionOutput::SaveFileF => Txt { - pol: "Zapisz plik *f* (struktura)", - eng: "Save file *f* (structure)", - }, - ActionOutput::SaveFileD => Txt { - pol: "Zapisz plik *d* (dokumentacja)", - eng: "Save file *d* (documentation)", - }, - ActionOutput::SaveFileC => Txt { - pol: "Zapisz plik *c* (komenda)", - eng: "Save file *c* (command)", - }, - ActionOutput::AddStructToDoc => Txt { - pol: "Dodaj strukturę do dokumentacji", - eng: "Add structure to doc", - }, - ActionOutput::AddCmdToStruct => Txt { - pol: "Dodaj komendę do struktury", - eng: "Add command to structure", - }, - ActionOutput::AddCmdToDoc => Txt { - pol: "Dodaj komendę do dokumentacji", - eng: "Add command to doc", - }, - ActionOutput::AutoSectionNum => Txt { - pol: "Autonumeracja sekcji", - eng: "Auto section numbering", - }, - ActionOutput::TimestampInFile => Txt { - pol: "Znacznik czasu w każdym pliku", - eng: "Timestamp in every file", - }, - ActionOutput::TimestampInFilename => Txt { - pol: "Znacznik czasu jako prefix nazwy", - eng: "Timestamp as filename prefix", - }, - ActionOutput::SelfPromo => Txt { - pol: "Autoreklama w każdym pliku", - eng: "Self-promo in every file", - }, - ActionOutput::OutputFolder => Txt { - pol: "Folder na pliki", - eng: "Output folder", - }, - ActionOutput::SectionPrefix => Txt { - pol: "Prefix sekcji pliku", - eng: "Section prefix", - }, - ActionOutput::DocTitle => Txt { - pol: "Tytuł dokumentu", - eng: "Document title", - }, - ActionOutput::Back => Txt { - pol: "Powrót", - eng: "Back", - }, - } - } - - fn theme(&self, text: String) -> String { - match self { - // Wyróżniamy główny przycisk akcji! - ActionOutput::ExecuteSave => style(text).on_green().black().bold().to_string(), - _ => text, - } - } -} - -// ===================================================================== -// WIDOK MENU -// ===================================================================== - -pub fn menu_output_save(s: &mut StateTui) { - let mut last_action = ActionOutput::default(); - - loop { - let t = T::new(s.lang); - let oc = &s.output_config; // Skrót dla wygody czytania - - let header = style(t.raw(Prompt::HeaderOutput)) - .on_white() - .black() - .to_string(); - - // Formaty przełączników [x] / [ ] - let toggle = |b: bool| if b { "[x]" } else { "[ ]" }; - - let f_lbl = format!( - "{} {}", - t.fmt(ActionOutput::SaveFileF), - toggle(oc.save_file_f) - ); - let d_lbl = format!( - "{} {}", - t.fmt(ActionOutput::SaveFileD), - toggle(oc.save_file_d) - ); - let c_lbl = format!( - "{} {}", - t.fmt(ActionOutput::SaveFileC), - toggle(oc.save_file_c) - ); - - let s2d_lbl = format!( - "{} {}", - t.fmt(ActionOutput::AddStructToDoc), - toggle(oc.add_struct_to_doc) - ); - let c2s_lbl = format!( - "{} {}", - t.fmt(ActionOutput::AddCmdToStruct), - toggle(oc.add_cmd_to_struct) - ); - let c2d_lbl = format!( - "{} {}", - t.fmt(ActionOutput::AddCmdToDoc), - toggle(oc.add_cmd_to_doc) - ); - - let auto_num_lbl = format!( - "{} {}", - t.fmt(ActionOutput::AutoSectionNum), - toggle(oc.auto_section_num) - ); - let ts_file_lbl = format!( - "{} {}", - t.fmt(ActionOutput::TimestampInFile), - toggle(oc.timestamp_in_file) - ); - let ts_name_lbl = format!( - "{} {}", - t.fmt(ActionOutput::TimestampInFilename), - toggle(oc.timestamp_in_filename) - ); - let promo_lbl = format!( - "{} {}", - t.fmt(ActionOutput::SelfPromo), - toggle(oc.self_promo) - ); - - let folder_lbl = format!( - "{} [{}]", - t.fmt(ActionOutput::OutputFolder), - oc.output_folder - ); - let prefix_lbl = format!( - "{} [{}]", - t.fmt(ActionOutput::SectionPrefix), - oc.section_prefix - ); - let title_lbl = format!("{} [{}]", t.fmt(ActionOutput::DocTitle), oc.doc_title); - - // Tłumaczenia hintów (podpowiedzi) - let hint_f = match s.lang { - Lang::POL => "Zapisuje drzewo katalogów", - Lang::ENG => "Saves directory tree", - }; - let hint_d = match s.lang { - Lang::POL => "Zapisuje zawartość plików", - Lang::ENG => "Saves file contents", - }; - let hint_c = match s.lang { - Lang::POL => "Zapisuje komendę terminala", - Lang::ENG => "Saves terminal command", - }; - - let action_result = cliclack::select(header) - .initial_value(last_action.clone()) - .item( - ActionOutput::ExecuteSave, - t.fmt(ActionOutput::ExecuteSave), - "", - ) - .item(ActionOutput::SaveFileF, f_lbl, hint_f) - .item(ActionOutput::SaveFileD, d_lbl, hint_d) - .item(ActionOutput::SaveFileC, c_lbl, hint_c) - .item(ActionOutput::AddStructToDoc, s2d_lbl, "") - .item(ActionOutput::AddCmdToStruct, c2s_lbl, "") - .item(ActionOutput::AddCmdToDoc, c2d_lbl, "") - .item(ActionOutput::AutoSectionNum, auto_num_lbl, "") - .item(ActionOutput::TimestampInFile, ts_file_lbl, "") - .item(ActionOutput::TimestampInFilename, ts_name_lbl, "") - .item(ActionOutput::SelfPromo, promo_lbl, "") - .item(ActionOutput::OutputFolder, folder_lbl, "") - .item(ActionOutput::SectionPrefix, prefix_lbl, "") - .item(ActionOutput::DocTitle, title_lbl, "") - .item(ActionOutput::Back, t.fmt(ActionOutput::Back), "") - .interact(); - - match action_result { - Ok(action) => { - last_action = action.clone(); - - match action { - // Natychmiastowe przełączniki (negacja obecnej wartości) - ActionOutput::SaveFileF => { - s.output_config.save_file_f = !s.output_config.save_file_f - } - ActionOutput::SaveFileD => { - s.output_config.save_file_d = !s.output_config.save_file_d - } - ActionOutput::SaveFileC => { - s.output_config.save_file_c = !s.output_config.save_file_c - } - ActionOutput::AddStructToDoc => { - s.output_config.add_struct_to_doc = !s.output_config.add_struct_to_doc - } - ActionOutput::AddCmdToStruct => { - s.output_config.add_cmd_to_struct = !s.output_config.add_cmd_to_struct - } - ActionOutput::AddCmdToDoc => { - s.output_config.add_cmd_to_doc = !s.output_config.add_cmd_to_doc - } - ActionOutput::AutoSectionNum => { - s.output_config.auto_section_num = !s.output_config.auto_section_num - } - ActionOutput::TimestampInFile => { - s.output_config.timestamp_in_file = !s.output_config.timestamp_in_file - } - ActionOutput::TimestampInFilename => { - s.output_config.timestamp_in_filename = - !s.output_config.timestamp_in_filename - } - ActionOutput::SelfPromo => { - s.output_config.self_promo = !s.output_config.self_promo - } - - // Pola tekstowe - ActionOutput::OutputFolder => { - let val: String = cliclack::input(t.raw(ActionOutput::OutputFolder)) - .default_input(&s.output_config.output_folder) - .interact() - .unwrap_or(s.output_config.output_folder.clone()); - s.output_config.output_folder = val; - } - ActionOutput::SectionPrefix => { - let val: String = cliclack::input(t.raw(ActionOutput::SectionPrefix)) - .default_input(&s.output_config.section_prefix) - .interact() - .unwrap_or(s.output_config.section_prefix.clone()); - s.output_config.section_prefix = val; - } - ActionOutput::DocTitle => { - let val: String = cliclack::input(t.raw(ActionOutput::DocTitle)) - .default_input(&s.output_config.doc_title) - .interact() - .unwrap_or(s.output_config.doc_title.clone()); - s.output_config.doc_title = val; - } - - ActionOutput::ExecuteSave => { - // TUTAJ W PRZYSZŁOŚCI WYWOŁAMY WŁAŚCIWY ZAPIS PLIKÓW - let msg = match s.lang { - Lang::POL => format!( - "Zapisano pliki do folderu: {}", - s.output_config.output_folder - ), - Lang::ENG => { - format!("Saved files to folder: {}", s.output_config.output_folder) - } - }; - cliclack::log::success(msg).unwrap(); - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - ActionOutput::Back => { - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - } - } - Err(_) => { - cliclack::clear_screen().unwrap(); - let t_err = T::new(s.lang); - cliclack::intro(t_err.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - } - } -} diff --git a/src/interfaces/tui/menu/paths_struct_style.rs b/src/interfaces/tui/menu/paths_struct_style.rs deleted file mode 100644 index c1e37c3..0000000 --- a/src/interfaces/tui/menu/paths_struct_style.rs +++ /dev/null @@ -1,301 +0,0 @@ -use super::super::i18n::{Prompt, T, Translatable, Txt}; -use super::super::state::{Lang, SizeBase, SortOrder, StateTui}; -use console::style; - -#[derive(Default, Clone, Eq, PartialEq)] -enum ActionStyle { - SortOrder, - SizeFiles, - SizeDirs, - SizeDirsReal, - SizeBase, - Precision, - #[default] - Back, -} - -//impl Default for ActionStyle { -// fn default() -> Self { Self::SortOrder } -//} - -// ===================================================================== -// LOKALNE ENUMY POMOCNICZE -// ===================================================================== - -#[derive(Clone, Eq, PartialEq)] -enum ActionSort { - FilesFirst, - DirsFirst, - Alphanumeric, -} - -enum LocalPrompt { - SortMode, - PrecisionInput, - ErrPrecisionRange, - ErrPrecisionNotNum, -} - -// ===================================================================== -// WARSTWA JĘZYKOWO - STYLIZACYJNA -// ===================================================================== - -impl Translatable for ActionStyle { - fn trans(&self) -> Txt { - match self { - ActionStyle::SortOrder => Txt { - pol: "Sortowanie", - eng: "Sort order", - }, - ActionStyle::SizeFiles => Txt { - pol: "Rozmiar przy plikach", - eng: "Size for files", - }, - ActionStyle::SizeDirs => Txt { - pol: "Rozmiar przy folderach", - eng: "Size for directories", - }, - ActionStyle::SizeDirsReal => Txt { - pol: "Rzeczywisty rozmiar folderów", - eng: "Real directory size", - }, - ActionStyle::SizeBase => Txt { - pol: "Podstawa rozmiaru", - eng: "Size base", - }, - ActionStyle::Precision => Txt { - pol: "Precyzja", - eng: "Precision", - }, - ActionStyle::Back => Txt { - pol: "Powrót", - eng: "Back", - }, - } - } -} - -impl Translatable for ActionSort { - fn trans(&self) -> Txt { - match self { - ActionSort::FilesFirst => Txt { - pol: "Najpierw pliki", - eng: "Files first", - }, - ActionSort::DirsFirst => Txt { - pol: "Najpierw foldery", - eng: "Directories first", - }, - ActionSort::Alphanumeric => Txt { - pol: "Alfanumerycznie", - eng: "Alphanumeric", - }, - } - } -} - -impl Translatable for LocalPrompt { - fn trans(&self) -> Txt { - match self { - LocalPrompt::SortMode => Txt { - pol: "Wybierz tryb sortowania:", - eng: "Choose sort mode:", - }, - LocalPrompt::PrecisionInput => Txt { - pol: "Podaj precyzję (od 3 do 9):", - eng: "Enter precision (from 3 to 9):", - }, - LocalPrompt::ErrPrecisionRange => Txt { - pol: "Precyzja musi być w przedziale 3-9!", - eng: "Precision must be between 3 and 9!", - }, - LocalPrompt::ErrPrecisionNotNum => Txt { - pol: "To nie jest liczba!", - eng: "This is not a number!", - }, - } - } -} - -// ===================================================================== -// WIDOK MENU -// ===================================================================== - -pub fn menu_paths_struct_style(s: &mut StateTui) { - // Ta linia teraz automatycznie wybierze Back jako start: - let mut last_action = ActionStyle::default(); - - loop { - let t = T::new(s.lang); - let header = style(t.raw(Prompt::HeaderStyle)) - .on_white() - .black() - .to_string(); - - // 1. ZABEZPIECZENIE UX PRZED BUDOWĄ MENU - // Jeśli rozmiar folderów wyłączony, ukrywamy opcję rzeczywistego rozmiaru - if !s.struct_config.size_dirs { - s.struct_config.size_dirs_real = false; - if last_action == ActionStyle::SizeDirsReal { - last_action = ActionStyle::SizeDirs; - } - } - - // Jeśli rozmiar plików I folderów wyłączony, ukrywamy też bazę i precyzję - if !s.struct_config.size_files && !s.struct_config.size_dirs { - s.struct_config.size_base = SizeBase::Base1024; // Wartość domyślna - s.struct_config.precision = 3; // Wartość domyślna - - if last_action == ActionStyle::SizeBase || last_action == ActionStyle::Precision { - // Cofamy kursor na cokolwiek, co jest jeszcze widoczne nad nimi - last_action = ActionStyle::SizeDirs; - } - } - - // 2. DYNAMICZNE ETYKIETY - let sort_str = match s.struct_config.sort { - SortOrder::FilesFirst => match s.lang { - Lang::POL => "najpierw pliki", - Lang::ENG => "files first", - }, - SortOrder::DirsFirst => match s.lang { - Lang::POL => "najpierw foldery", - Lang::ENG => "directories first", - }, - SortOrder::Alphanumeric => match s.lang { - Lang::POL => "alfanumerycznie", - Lang::ENG => "alphanumeric", - }, - }; - - let base_str = match s.struct_config.size_base { - SizeBase::Base1024 => "1024", - SizeBase::Base1000 => "1000", - }; - - let toggle = |b: bool| if b { "[x]" } else { "[ ]" }; - - let sort_lbl = format!("{} [{}]", t.fmt(ActionStyle::SortOrder), sort_str); - let s_files_lbl = format!( - "{} {}", - t.fmt(ActionStyle::SizeFiles), - toggle(s.struct_config.size_files) - ); - let s_dirs_lbl = format!( - "{} {}", - t.fmt(ActionStyle::SizeDirs), - toggle(s.struct_config.size_dirs) - ); - let s_dirs_real_lbl = format!( - "{} {}", - t.fmt(ActionStyle::SizeDirsReal), - toggle(s.struct_config.size_dirs_real) - ); - let base_lbl = format!("{} [{}]", t.fmt(ActionStyle::SizeBase), base_str); - let prec_lbl = format!( - "{} [{}]", - t.fmt(ActionStyle::Precision), - s.struct_config.precision - ); - - // 3. BUDOWANIE MENU - let mut menu = cliclack::select(header) - .initial_value(last_action.clone()) - .item(ActionStyle::SortOrder, sort_lbl, "") - .item(ActionStyle::SizeFiles, s_files_lbl, "") - .item(ActionStyle::SizeDirs, s_dirs_lbl, ""); - - // Warunkowe opcje dla folderów - if s.struct_config.size_dirs { - menu = menu.item(ActionStyle::SizeDirsReal, s_dirs_real_lbl, ""); - } - - // Warunkowe opcje globalne dla rozmiarów - if s.struct_config.size_files || s.struct_config.size_dirs { - menu = menu.item(ActionStyle::SizeBase, base_lbl, ""); - menu = menu.item(ActionStyle::Precision, prec_lbl, ""); - } - - let action_result = menu - .item(ActionStyle::Back, t.fmt(ActionStyle::Back), "") - .interact(); - - // 4. OBSŁUGA AKCJI - match action_result { - Ok(action) => { - last_action = action.clone(); - - match action { - ActionStyle::SortOrder => { - let initial_sort_action = match s.struct_config.sort { - SortOrder::FilesFirst => ActionSort::FilesFirst, - SortOrder::DirsFirst => ActionSort::DirsFirst, - SortOrder::Alphanumeric => ActionSort::Alphanumeric, - }; - - let val_action = cliclack::select(t.raw(LocalPrompt::SortMode)) - .initial_value(initial_sort_action) - .item(ActionSort::FilesFirst, t.fmt(ActionSort::FilesFirst), "") - .item(ActionSort::DirsFirst, t.fmt(ActionSort::DirsFirst), "") - .item( - ActionSort::Alphanumeric, - t.fmt(ActionSort::Alphanumeric), - "", - ) - .interact(); - - if let Ok(selected_sort) = val_action { - s.struct_config.sort = match selected_sort { - ActionSort::FilesFirst => SortOrder::FilesFirst, - ActionSort::DirsFirst => SortOrder::DirsFirst, - ActionSort::Alphanumeric => SortOrder::Alphanumeric, - }; - } - } - ActionStyle::SizeFiles => { - s.struct_config.size_files = !s.struct_config.size_files - } - ActionStyle::SizeDirs => s.struct_config.size_dirs = !s.struct_config.size_dirs, - ActionStyle::SizeDirsReal => { - s.struct_config.size_dirs_real = !s.struct_config.size_dirs_real - } - ActionStyle::SizeBase => { - s.struct_config.size_base = match s.struct_config.size_base { - SizeBase::Base1024 => SizeBase::Base1000, - SizeBase::Base1000 => SizeBase::Base1024, - }; - } - ActionStyle::Precision => { - let default_val = s.struct_config.precision.to_string(); - let val: String = cliclack::input(t.raw(LocalPrompt::PrecisionInput)) - .default_input(&default_val) - .interact() - .unwrap_or(default_val); - - if let Ok(num) = val.parse::() { - if (3..=9).contains(&num) { - s.struct_config.precision = num; - } else { - cliclack::log::error(t.raw(LocalPrompt::ErrPrecisionRange)) - .unwrap(); - } - } else { - cliclack::log::error(t.raw(LocalPrompt::ErrPrecisionNotNum)).unwrap(); - } - } - ActionStyle::Back => { - cliclack::clear_screen().unwrap(); - cliclack::intro(t.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - } - } - Err(_) => { - cliclack::clear_screen().unwrap(); - let t_err = T::new(s.lang); - cliclack::intro(t_err.raw(Prompt::HeaderEnter)).unwrap(); - return; - } - } - } -} diff --git a/src/interfaces/tui/state.rs b/src/interfaces/tui/state.rs index 7030109..596fb46 100644 --- a/src/interfaces/tui/state.rs +++ b/src/interfaces/tui/state.rs @@ -1,142 +1,41 @@ -#[allow(clippy::upper_case_acronyms)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Lang { - POL, - ENG, -} - -#[derive(Clone, Debug)] -pub struct JobConfig { - pub title: String, - pub path_enter: String, - pub glob_includes: Vec, - pub glob_excludes: Vec, - pub file_types: Vec, - pub dirs_include_empty: bool, - pub dirs_only: bool, - pub dirs_keep_excluded_as_empty_to_depth: u32, - pub path_include_parent_file: bool, -} - -impl Default for JobConfig { - fn default() -> Self { - Self { - title: "default".to_string(), - path_enter: "./src/".to_string(), - glob_includes: vec!["./Cargo.toml".to_string()], - glob_excludes: vec![], - file_types: vec!["*.rs".to_string()], - dirs_include_empty: true, - dirs_only: false, - dirs_keep_excluded_as_empty_to_depth: 0, - path_include_parent_file: false, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SortOrder { - FilesFirst, - DirsFirst, - Alphanumeric, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SizeBase { - Base1024, - Base1000, -} - -#[derive(Clone, Debug)] -pub struct PathsStructStyleConfig { - pub sort: SortOrder, - pub size_files: bool, - pub size_dirs: bool, - pub size_dirs_real: bool, - pub size_base: SizeBase, - pub precision: u8, -} - -impl Default for PathsStructStyleConfig { - fn default() -> Self { - Self { - sort: SortOrder::FilesFirst, - size_files: true, - size_dirs: false, - size_dirs_real: false, - size_base: SizeBase::Base1024, - precision: 5, - } - } -} - -#[derive(Clone, Debug)] -pub struct OutputSaveConfig { - pub save_file_f: bool, // Struktura (f) - pub save_file_d: bool, // Dokumentacja (d) - pub save_file_c: bool, // Komenda (c) - pub add_struct_to_doc: bool, - pub add_cmd_to_struct: bool, - pub add_cmd_to_doc: bool, - pub auto_section_num: bool, - pub timestamp_in_file: bool, - pub timestamp_in_filename: bool, - pub self_promo: bool, - pub output_folder: String, - pub section_prefix: String, - pub doc_title: String, -} - -impl Default for OutputSaveConfig { - fn default() -> Self { - Self { - save_file_f: true, - save_file_d: false, - save_file_c: false, - add_struct_to_doc: true, - add_cmd_to_struct: true, - add_cmd_to_doc: true, - auto_section_num: true, - timestamp_in_file: true, - timestamp_in_filename: false, - self_promo: false, - output_folder: "./other/".to_string(), - section_prefix: "File-".to_string(), - doc_title: "".to_string(), - } - } -} +use crate::interfaces::cli::args::{CliArgs, CliSortStrategy, CliViewMode}; +use cargo_plot::i18n::Lang; pub struct StateTui { pub lang: Lang, - pub jobs: Vec, - pub struct_config: PathsStructStyleConfig, - pub output_config: OutputSaveConfig, + pub args: CliArgs, } impl StateTui { pub fn new() -> Self { + let lang = Lang::detect(); Self { - lang: Lang::POL, - jobs: Vec::new(), - struct_config: PathsStructStyleConfig::default(), - output_config: OutputSaveConfig::default(), + 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, // Domyślnie pokazujemy dopasowania (-m) + exclude: false, + out_path: None, + out_code: None, + by: false, + ignore_case: false, + no_root: false, + info: true, // Domyślnie włączamy statystyki (-i) + lang: Some(lang), + }, } } - // Teraz add_job przyjmuje CAŁĄ konfigurację, a nie tylko stringa - pub fn add_job(&mut self, mut job: JobConfig) { - let base_title = job.title.clone(); - let mut title = base_title.clone(); - let mut counter = 1; - - // Magia sufiksów (_1, _2...) - while self.jobs.iter().any(|j| j.title == title) { - title = format!("{}_{}", base_title, counter); - counter += 1; - } - - job.title = title; - self.jobs.push(job); + /// 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); } } From 10533e68f981efd83a4453e3020eec9a1d550c7f Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Wed, 18 Mar 2026 09:00:33 +0100 Subject: [PATCH 33/45] (add: fast path in TUI - cli) --- Cargo.toml | 1 + src/interfaces/tui/i18n.rs | 7 ++++ src/interfaces/tui/menu.rs | 72 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 13b7b18..857c2af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ cliclack = "0.4.1" colored = "3.1.1" console = "0.16.3" ctrlc = "3.5.2" +shlex = "1.3.0" # ========================================== diff --git a/src/interfaces/tui/i18n.rs b/src/interfaces/tui/i18n.rs index 41a022b..a16ecda 100644 --- a/src/interfaces/tui/i18n.rs +++ b/src/interfaces/tui/i18n.rs @@ -42,6 +42,9 @@ pub enum Prompt { SubOnMatch, SubOnMismatch, SubInfo, + BtnCliMode, + InputCliCommand, + SuccessCliParse, } impl Translatable for Prompt { @@ -141,6 +144,9 @@ impl Translatable for Prompt { 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!" }, } } @@ -151,6 +157,7 @@ impl Translatable for Prompt { 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(), _ => text, } } diff --git a/src/interfaces/tui/menu.rs b/src/interfaces/tui/menu.rs index b61149e..1890567 100644 --- a/src/interfaces/tui/menu.rs +++ b/src/interfaces/tui/menu.rs @@ -2,11 +2,14 @@ use super::i18n::{Prompt, T}; use super::state::StateTui; use crate::interfaces::cli::args::{CliSortStrategy, CliViewMode}; use crate::interfaces::cli::engine; +use clap::Parser; +use crate::interfaces::cli::args::CargoCli; #[derive(Clone, PartialEq, Eq)] enum Action { Lang, QuickStart, + CliMode, Paths, View, Output, @@ -65,6 +68,7 @@ pub fn menu_main(s: &mut StateTui) { .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, "") @@ -87,6 +91,39 @@ pub fn menu_main(s: &mut StateTui) { return; } } + Ok(Action::CliMode) => { + let cmd: String = cliclack::input(t.raw(Prompt::InputCliCommand)).interact().unwrap_or_default(); + if !cmd.trim().is_empty() { + let mut parsed_split = split_cli_args(&cmd); + + // ⚡ Sprytne czyszczenie: wywalamy "cargo", "run", "--", "plot", "cargo-plot.exe" z początku + 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; + } + } + + // Budujemy fałszywą listę argumentów dla clapa (żeby zgadzała się struktura) + let mut cli_args = vec!["cargo".to_string(), "plot".to_string()]; + cli_args.extend(parsed_split); + + // ⚡ CLAP PARSUJE CIĄG ZNAKÓW I ZMIENIA STAN TUI! + 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) => { + // Wrzucamy błąd clapa na ekran, żeby użytkownik wiedział, co zepsuł we flagach + cliclack::log::error(format!("{}", e)).unwrap(); + } + } + } + // Pozostajemy w pętli, aby interfejs odświeżył się z nowymi wartościami + }, Ok(Action::Paths) => { last_action = Action::Paths; handle_paths(s, &t); @@ -253,3 +290,38 @@ fn split_patterns(input: &str) -> Vec { } 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 +} \ No newline at end of file From 6c5c6b086d472b4e5a4f9be59770748435a0b233 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Thu, 19 Mar 2026 10:47:01 +0100 Subject: [PATCH 34/45] (add: gui) --- Cargo.toml | 2 + src/core/path_view/grid.rs | 12 ++- src/core/path_view/list.rs | 3 +- src/core/path_view/tree.rs | 10 ++- src/core/save.rs | 27 ++++++- src/execute.rs | 7 ++ src/interfaces.rs | 1 + src/interfaces/cli.rs | 8 +- src/interfaces/cli/args.rs | 10 ++- src/interfaces/cli/engine.rs | 1 + src/interfaces/gui.rs | 93 +++++++++++++++++++++ src/interfaces/gui/code.rs | 88 ++++++++++++++++++++ src/interfaces/gui/paths.rs | 102 +++++++++++++++++++++++ src/interfaces/gui/settings.rs | 144 +++++++++++++++++++++++++++++++++ src/interfaces/tui/i18n.rs | 101 ++++++++++++++++++++++- src/interfaces/tui/menu.rs | 110 ++++++++++++++++++------- src/interfaces/tui/state.rs | 2 + 17 files changed, 677 insertions(+), 44 deletions(-) create mode 100644 src/interfaces/gui.rs create mode 100644 src/interfaces/gui/code.rs create mode 100644 src/interfaces/gui/paths.rs create mode 100644 src/interfaces/gui/settings.rs diff --git a/Cargo.toml b/Cargo.toml index 857c2af..2d36891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ colored = "3.1.1" console = "0.16.3" ctrlc = "3.5.2" shlex = "1.3.0" +eframe = "0.33.3" +rfd = "0.17.2" # ========================================== diff --git a/src/core/path_view/grid.rs b/src/core/path_view/grid.rs index 601f2e2..60f31fe 100644 --- a/src/core/path_view/grid.rs +++ b/src/core/path_view/grid.rs @@ -20,6 +20,7 @@ impl PathGrid { 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); @@ -38,13 +39,16 @@ impl PathGrid { paths_map: &BTreeMap>, base_path: &Path, sort_strategy: SortStrategy, - weight_cfg: &WeightConfig, + 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 is_dir { + 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() @@ -60,7 +64,7 @@ impl PathGrid { 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)) + .map(|c| build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji)) .collect(); FileNode::sort_slice(&mut child_nodes, sort_strategy); @@ -96,7 +100,7 @@ impl PathGrid { let mut top_nodes: Vec = roots_paths .into_iter() - .map(|r| build_node(&r, &tree_map, base_path_obj, sort_strategy, weight_cfg)) + .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); diff --git a/src/core/path_view/list.rs b/src/core/path_view/list.rs index d2f5e8a..c210e4f 100644 --- a/src/core/path_view/list.rs +++ b/src/core/path_view/list.rs @@ -15,6 +15,7 @@ impl PathList { 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 @@ -30,7 +31,7 @@ impl PathList { name: p_str.clone(), path: absolute, is_dir, - icon: get_icon_for_path(p_str).to_string(), + 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 diff --git a/src/core/path_view/tree.rs b/src/core/path_view/tree.rs index 268b16d..cb7f742 100644 --- a/src/core/path_view/tree.rs +++ b/src/core/path_view/tree.rs @@ -20,6 +20,7 @@ impl PathTree { 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(); @@ -38,13 +39,16 @@ impl PathTree { 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 is_dir { + 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() @@ -60,7 +64,7 @@ impl PathTree { 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)) + .map(|c| build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji)) .collect(); FileNode::sort_slice(&mut child_nodes, sort_strategy); @@ -97,7 +101,7 @@ impl PathTree { let mut top_nodes: Vec = roots_paths .into_iter() - .map(|r| build_node(&r, &tree_map, base_path_obj, sort_strategy, weight_cfg)) + .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); diff --git a/src/core/save.rs b/src/core/save.rs index 8af875d..b478868 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -1,6 +1,5 @@ use super::super::i18n::I18n; use crate::theme::for_path_tree::get_file_type; -use std::env; use std::fs; use std::path::Path; @@ -8,8 +7,30 @@ pub struct SaveFile; impl SaveFile { fn generate_by_section(tag: &str, typ: &str, i18n: &I18n) -> String { - let args: Vec = env::args().collect(); - let command = args.join(" "); + let raw_args: Vec = std::env::args().collect(); + let mut formatted_args = Vec::new(); + + // 1. Obsługa początku komendy (estetyka: zamiana ścieżki na "cargo plot") + formatted_args.push("cargo".to_string()); + + // Jeśli nie wywołaliśmy tego przez cargo (np. bezpośrednio binarkę), + // a w argumentach nie ma jeszcze "plot", dodajmy go dla czytelności raportu. + if !raw_args.iter().any(|a| a == "plot") { + formatted_args.push("plot".to_string()); + } + + // 2. Przetwarzanie reszty argumentów (pomijamy arg[0], bo to ścieżka do binarki) + for arg in raw_args.into_iter().skip(1) { + if arg.starts_with('-') || arg == "plot" || arg == "--" { + // Flagi i słowa kluczowe zostawiamy gołe + formatted_args.push(arg); + } else { + // Ścieżki, wartości i wzorce owijamy w cudzysłowy + formatted_args.push(format!("\"{}\"", arg.replace('\"', "\\\""))); + } + } + + let command = formatted_args.join(" "); format!( "\n\n---\n---\n\n{}\n\n{}\n\n```bash\n{}\n```\n\n{}\n\n{}\n\n{}\n\n---\n", diff --git a/src/execute.rs b/src/execute.rs index 226740c..8d08b6d 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -18,6 +18,7 @@ pub fn execute( view_mode: ViewMode, no_root: bool, print_info: bool, + no_emoji: bool, i18n: &crate::i18n::I18n, mut on_match: OnMatch, mut on_mismatch: OnMismatch, @@ -109,6 +110,7 @@ where sort_strategy, &weight_cfg, root_name, + no_emoji, )); } if do_exclude { @@ -118,6 +120,7 @@ where sort_strategy, &weight_cfg, root_name, + no_emoji, )); } } @@ -129,6 +132,7 @@ where sort_strategy, &weight_cfg, root_name, + no_emoji, )); } if do_exclude { @@ -138,6 +142,7 @@ where sort_strategy, &weight_cfg, root_name, + no_emoji, )); } } @@ -148,6 +153,7 @@ where &path_ctx.entry_absolute, sort_strategy, &weight_cfg, + no_emoji, )); } if do_exclude { @@ -156,6 +162,7 @@ where &path_ctx.entry_absolute, sort_strategy, &weight_cfg, + no_emoji, )); } } diff --git a/src/interfaces.rs b/src/interfaces.rs index 38acfe3..5bc0411 100644 --- a/src/interfaces.rs +++ b/src/interfaces.rs @@ -3,3 +3,4 @@ pub mod cli; pub mod tui; +pub mod gui; \ No newline at end of file diff --git a/src/interfaces/cli.rs b/src/interfaces/cli.rs index 949efcc..bece8ee 100644 --- a/src/interfaces/cli.rs +++ b/src/interfaces/cli.rs @@ -25,5 +25,11 @@ pub fn run_cli() { // [ENG]: Transfer control to our execution engine. // [POL]: Przekazanie kontroli do naszego silnika wykonawczego. - engine::run(flags); + if flags.gui { + // Jeśli podano -g, od razu ładujemy okienko ze sparsowaną konfiguracją + crate::interfaces::gui::run_gui(flags); + } else { + // Jeśli nie, uruchamiamy standardowy silnik generujący raport w terminalu + engine::run(flags); + } } diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 1a838b1..0928b8f 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -24,7 +24,7 @@ pub struct CliArgs { /// [ENG]: Match patterns. /// [POL]: Wzorce dopasowań. - #[arg(short = 'p', long = "pat", required = true)] + #[arg(short = 'p', long = "pat", required_unless_present = "gui")] pub patterns: Vec, /// [ENG]: Results sorting strategy. @@ -74,6 +74,14 @@ pub struct CliArgs { #[arg(short = 'i', long = "info", default_value_t = false)] pub info: bool, + /// [POL]: Uruchamia aplikację natychmiast w trybie graficznym (GUI). + #[arg(short = 'g', long = "gui", default_value_t = false)] + pub gui: bool, + + /// [POL]: Wyłącza renderowanie ikon/emoji (przydatne w czystych terminalach lub GUI). + #[arg(long = "no-emoji", default_value_t = false)] + pub no_emoji: bool, + /// [POL]: Wymusza język interfejsu (pl / en). Domyślnie pobiera z systemu. #[arg(long, value_enum)] pub lang: Option, diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index ac2a661..267cf2e 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -31,6 +31,7 @@ pub fn run(args: CliArgs) { view_mode, args.no_root, args.info, + args.no_emoji, &i18n, |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk |_| {}, diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs new file mode 100644 index 0000000..4734d7b --- /dev/null +++ b/src/interfaces/gui.rs @@ -0,0 +1,93 @@ +pub mod settings; +pub mod paths; +pub mod code; + +use eframe::egui; +use crate::interfaces::cli::args::CliArgs; + +#[derive(PartialEq)] +pub enum Tab { Settings, Paths, Code } + +#[derive(PartialEq)] +pub enum PathsTab { Match, Mismatch } + +pub struct CargoPlotApp { + pub args: CliArgs, + pub active_tab: Tab, + pub active_paths_tab: PathsTab, + pub new_pattern_input: String, + pub generated_paths_m: String, // ⚡ Bufor tylko dla MATCH + pub generated_paths_x: String, // ⚡ Bufor tylko dla MISMATCH + pub generated_code: String, // ⚡ Bufor na wygenerowany kod + pub ui_scale: f32, +} + +impl CargoPlotApp { + pub fn new(args: CliArgs) -> Self { + Self { + args, + active_tab: Tab::Settings, + active_paths_tab: PathsTab::Match, + new_pattern_input: String::new(), + generated_paths_m: String::new(), + generated_paths_x: String::new(), + generated_code: String::new(), + 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(); +} \ No newline at end of file diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs new file mode 100644 index 0000000..d687318 --- /dev/null +++ b/src/interfaces/gui/code.rs @@ -0,0 +1,88 @@ +use eframe::egui; +use crate::interfaces::gui::CargoPlotApp; +use cargo_plot::i18n::Lang; +use cargo_plot::execute; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::addon::TimeTag; + +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + let is_pl = app.args.lang.unwrap_or(Lang::En) == Lang::Pl; + + // 1. BELKA GÓRNA + ui.horizontal(|ui| { + if ui.button(if is_pl { "🔄 Generuj kod (Cache)" } else { "🔄 Generate code" }).clicked() { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let show_mode = match (app.args.include, app.args.exclude) { + (true, false) => ShowMode::Include, + (false, true) => ShowMode::Exclude, + _ => ShowMode::Context, + }; + + 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(), + app.args.no_root, + false, + true, + &i18n, + |_| {}, |_| {}, + ); + + let tree_text = stats.render_output(app.args.view.into(), show_mode, false, false); + let mut content = format!("```plaintext\n{}\n```\n\n", tree_text); + + let base_dir = std::path::Path::new(&app.args.enter_path); + let mut counter = 1; + + // Ręcznie budujemy podgląd kodu dla GUI + 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.push_str(&format!("### {:03}: `{}`\n\n```rust\n{}\n```\n\n", counter, p_str, txt)); + } + Err(_) => { + content.push_str(&format!("### {:03}: `{}`\n\n> *(Pominięto binarkę)*\n\n", counter, p_str)); + } + } + counter += 1; + } + app.generated_code = content; + } + + ui.add_space(15.0); + ui.checkbox(&mut app.args.by, if is_pl { "Dodaj stopkę (--by) przy zapisie" } else { "Add footer (--by) on save" }); + ui.add_space(15.0); + + if ui.button(if is_pl { "💾 Zapisz do pliku" } else { "💾 Save to file" }).clicked() { + let tag = TimeTag::now(); + let filepath = format!("./cache_{}.md", tag); + + // Prosty zapis tego, co użytkownik ma w polu tekstowym + let mut final_text = app.generated_code.clone(); + if app.args.by { + final_text.push_str(&format!("\n\n---\n**Wersja raportu:** {}\n---", tag)); + } + + let _ = std::fs::write(&filepath, final_text); + } + }); + + ui.separator(); + + // 2. POLE TEKSTOWE / NOTATNIK + egui::ScrollArea::both().show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut app.generated_code) + .font(egui::TextStyle::Monospace) + .desired_width(f32::INFINITY) + .desired_rows(30), + ); + }); +} \ No newline at end of file diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs new file mode 100644 index 0000000..4597166 --- /dev/null +++ b/src/interfaces/gui/paths.rs @@ -0,0 +1,102 @@ +use eframe::egui; +use crate::interfaces::gui::{CargoPlotApp, PathsTab}; +use cargo_plot::i18n::Lang; +use cargo_plot::execute; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::addon::TimeTag; + +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + let is_pl = app.args.lang.unwrap_or(Lang::En) == Lang::Pl; + + // 1. GÓRNA BELKA (Generowanie i Zapis) + ui.horizontal(|ui| { + if ui.button(if is_pl { "🔄 Generuj / Regeneruj" } else { "🔄 Generate" }).clicked() { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + + // Skanujemy dysk tylko raz, wyciągając pełny kontekst + let stats = execute::execute( + &app.args.enter_path, + &app.args.patterns, + !app.args.ignore_case, + app.args.sort.into(), + ShowMode::Context, + app.args.view.into(), + app.args.no_root, + false, + true, + &i18n, + |_| {}, |_| {}, + ); + + // ⚡ Od razu zapisujemy oba wyniki do niezależnych buforów + app.generated_paths_m = stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + app.generated_paths_x = stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); + } + + ui.add_space(15.0); + ui.checkbox(&mut app.args.by, if is_pl { "Dodaj stopkę (--by) przy zapisie" } else { "Add footer (--by) on save" }); + ui.add_space(15.0); + + if ui.button(if is_pl { "💾 Zapisz do pliku" } else { "💾 Save to file" }).clicked() { + let tag = TimeTag::now(); + let filepath = format!("./paths_{}.md", tag); + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + + // ⚡ Zapisujemy tylko ten bufor, na który użytkownik aktualnie patrzy! + let current_text = if app.active_paths_tab == PathsTab::Match { + &app.generated_paths_m + } else { + &app.generated_paths_x + }; + + cargo_plot::core::save::SaveFile::paths(current_text, &filepath, &tag, app.args.by, &i18n); + } + }); + + ui.separator(); + + // 2. DOLNA BELKA (ZAKŁADKI) - Przypinamy ją do dołu ekranu + egui::TopBottomPanel::bottom("paths_subtabs").show_inside(ui, |ui| { + ui.add_space(8.0); + ui.horizontal(|ui| { + + // ⚡ ZMIENIAMY TŁO TYLKO DLA LEWEGO GUZIKA (MATCH) + if app.active_paths_tab == PathsTab::Match { + ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); // Złoty + } + // Jeśli aktywny jest Mismatch, egui użyje swojego domyślnego, nienaruszonego tła! + + let m_text = egui::RichText::new("✔ (-m) MATCH") + .size(18.0).strong().color(egui::Color32::from_rgb(138, 90, 255)); // Fiolet + + let x_text = egui::RichText::new("✖ (-x) MISMATCH") + .size(18.0).strong().color(egui::Color32::from_rgb(255, 80, 100)); // Czerwień + + ui.selectable_value(&mut app.active_paths_tab, PathsTab::Match, m_text); + ui.add_space(20.0); + ui.selectable_value(&mut app.active_paths_tab, PathsTab::Mismatch, x_text); + }); + ui.add_space(8.0); + }); + + // 3. ŚRODKOWA PRZESTRZEŃ (NOTATNIK) - Wypełnia całą resztę miejsca między górą a dołem + egui::CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::both().show(ui, |ui| { + // ⚡ Wyłączamy łamanie wierszy - włącza się poziomy scroll! + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + // Wybieramy odpowiedni bufor tekstowy do edycji i podglądu + let text_buffer = match app.active_paths_tab { + PathsTab::Match => &mut app.generated_paths_m, + PathsTab::Mismatch => &mut app.generated_paths_x, + }; + + ui.add( + egui::TextEdit::multiline(text_buffer) + .font(egui::TextStyle::Monospace) + .desired_width(f32::INFINITY) // Rozciąga na max szerokość + // desired_rows() usunięte - teraz bierze całą wolną wysokość okna! + ); + }); + }); +} \ No newline at end of file diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs new file mode 100644 index 0000000..40b2c77 --- /dev/null +++ b/src/interfaces/gui/settings.rs @@ -0,0 +1,144 @@ +use eframe::egui; +use crate::interfaces::gui::CargoPlotApp; +use cargo_plot::i18n::Lang; +use crate::interfaces::cli::args::{CliSortStrategy, CliViewMode}; + +pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { + // Sprawdzamy, czy wybrany jest język polski (dla dynamicznych tłumaczeń w locie) + let is_pl = app.args.lang.unwrap_or(Lang::En) == Lang::Pl; + + egui::ScrollArea::vertical().show(ui, |ui| { + ui.add_space(10.0); + + // 1. WYBÓR JĘZYKA (Teraz dynamicznie aktualizuje resztę interfejsu!) + ui.horizontal(|ui| { + ui.label(if is_pl { "🌍 Język:" } else { "🌍 Language:" }); + ui.radio_value(&mut app.args.lang, Some(Lang::Pl), "Polski"); + ui.radio_value(&mut app.args.lang, Some(Lang::En), "English"); + }); + ui.separator(); + + // 2. WYBÓR FOLDERU (Z działającym przyciskiem okna systemowego) + ui.horizontal(|ui| { + ui.label(if is_pl { "📂 Ścieżka skanowania:" } else { "📂 Scan path:" }); + ui.text_edit_singleline(&mut app.args.enter_path); + + // ⚡ NATYWNE OKNO WYBORU FOLDERU + if ui.button(if is_pl { "Wybierz..." } else { "Browse..." }).clicked() + && let Some(folder) = rfd::FileDialog::new().pick_folder() { + // Aktualizujemy ścieżkę i ujednolicamy ukośniki + app.args.enter_path = folder.to_string_lossy().replace('\\', "/"); + } + }); + ui.separator(); + + // 3. WZORCE DOPASOWAŃ (Z połączonymi opcjami z linii "X") + ui.heading(if is_pl { "🔍 Wzorce dopasowań (Patterns)" } else { "🔍 Match Patterns" }); + + // Trzy opcje logiczne przytulone do wzorców + //ui.horizontal(|ui| { + + //ui.checkbox(&mut app.args.include, if is_pl { "✅ Pokaż dopasowane (m)" } else { "✅ Show matched (m)" }); + //ui.checkbox(&mut app.args.exclude, if is_pl { "❌ Pokaż odrzucone (x)" } else { "❌ Show rejected (x)" }); + //}); + ui.add_space(5.0); + + // Pole dodawania nowego wzorca + ui.horizontal(|ui| { + ui.checkbox(&mut app.args.ignore_case, if is_pl { "🔠 Ignoruj wielkość liter" } else { "🔠 Ignore case" }); + ui.label(if is_pl { "Nowy:" } else { "New:" }); + let response = ui.text_edit_singleline(&mut app.new_pattern_input); + let btn_clicked = ui.button(if is_pl { "➕ Dodaj wzorzec" } else { "➕ Add pattern" }).clicked(); + + if (btn_clicked || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))) + && !app.new_pattern_input.trim().is_empty() + { + app.args.patterns.push(app.new_pattern_input.trim().to_string()); + app.new_pattern_input.clear(); + response.request_focus(); + } + }); + + // 4. LISTA DODANYCH WZORCÓW + ui.add_space(5.0); + egui::Frame::group(ui.style()).show(ui, |ui| { + ui.set_min_height(100.0); + + 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(if is_pl { "💣 Usuń wszystkie" } else { "💣 Clear all" }).clicked() { + app.args.patterns.clear(); + } + } else { + let empty_msg = if is_pl { "Brak wzorców. Dodaj przynajmniej jeden!" } else { "No patterns. Add at least one!" }; + ui.label(egui::RichText::new(empty_msg).italics().weak()); + } + }); + ui.separator(); + + // 5. WIDOK I SORTOWANIE + ui.horizontal(|ui| { + egui::ComboBox::from_label(if is_pl { "Sortowanie" } else { "Sorting" }) + .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::AzDir, "AzDir"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::Az, "Az"); + ui.selectable_value(&mut app.args.sort, CliSortStrategy::None, "None"); + }); + + ui.add_space(15.0); + + egui::ComboBox::from_label(if is_pl { "Tryb widoku" } else { "View mode" }) + .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, if is_pl { "Ukryj ROOT w drzewie" } else { "Hide ROOT in tree" }); + }); + + ui.add_space(30.0); + + // 6. STOPKA + ui.separator(); + ui.add_space(10.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("📦 cargo-plot v0.2.0-beta").strong()); + ui.separator(); + ui.hyperlink_to("Crates.io", "https://crates.io/crates/cargo-plot"); + ui.separator(); + ui.hyperlink_to(if is_pl { "Pobierz binarkę (GitHub)" } else { "Download binary (GitHub)" }, "https://github.com/j-Cis/cargo-plot/releases"); + }); + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new(if is_pl { "Instalacja:" } else { "Install:" }).weak()); + ui.code("cargo install cargo-plot"); + ui.separator(); + ui.label(egui::RichText::new(if is_pl { "Usuwanie:" } else { "Uninstall:" }).weak()); + ui.code("cargo uninstall cargo-plot"); + }); + ui.add_space(10.0); + }); +} \ No newline at end of file diff --git a/src/interfaces/tui/i18n.rs b/src/interfaces/tui/i18n.rs index a16ecda..5c2c26f 100644 --- a/src/interfaces/tui/i18n.rs +++ b/src/interfaces/tui/i18n.rs @@ -45,6 +45,10 @@ pub enum Prompt { BtnCliMode, InputCliCommand, SuccessCliParse, + BtnHelp, + HelpPause, + SubHelpHeader, HelpPatternsBtn, HelpFlagsBtn, HelpTextPatterns, HelpTextFlags, + BtnGui, } impl Translatable for Prompt { @@ -144,9 +148,98 @@ impl Translatable for Prompt { 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::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)", + }, } } @@ -158,6 +251,8 @@ impl Translatable for Prompt { 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, } } diff --git a/src/interfaces/tui/menu.rs b/src/interfaces/tui/menu.rs index 1890567..4edc986 100644 --- a/src/interfaces/tui/menu.rs +++ b/src/interfaces/tui/menu.rs @@ -1,9 +1,10 @@ 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 crate::interfaces::cli::args::CargoCli; +use console::style; #[derive(Clone, PartialEq, Eq)] enum Action { @@ -14,7 +15,9 @@ enum Action { View, Output, Filters, + Help, Run, + Gui, Exit, } @@ -22,6 +25,7 @@ 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); @@ -64,6 +68,7 @@ pub fn menu_main(s: &mut StateTui) { ); // ⚡ 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), "") @@ -73,8 +78,10 @@ pub fn menu_main(s: &mut StateTui) { .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::Exit, t.fmt(Prompt::BtnExit), "") + .item(Action::Gui, t.fmt(Prompt::BtnGui), "") + .item(Action::Exit, t.fmt(Prompt::BtnExit), links_hint) .interact(); // ⚡ OBSŁUGA AKCJI @@ -92,38 +99,50 @@ pub fn menu_main(s: &mut StateTui) { } } Ok(Action::CliMode) => { - let cmd: String = cliclack::input(t.raw(Prompt::InputCliCommand)).interact().unwrap_or_default(); + let cmd: String = cliclack::input(t.raw(Prompt::InputCliCommand)) + .interact() + .unwrap_or_default(); + if !cmd.trim().is_empty() { - let mut parsed_split = split_cli_args(&cmd); - - // ⚡ Sprytne czyszczenie: wywalamy "cargo", "run", "--", "plot", "cargo-plot.exe" z początku - 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; + // ⚡ 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; + } } - } - // Budujemy fałszywą listę argumentów dla clapa (żeby zgadzała się struktura) - let mut cli_args = vec!["cargo".to_string(), "plot".to_string()]; - cli_args.extend(parsed_split); + // Podajemy do parsera Clap + let mut cli_args = vec!["cargo".to_string(), "plot".to_string()]; + cli_args.extend(parsed_split); - // ⚡ CLAP PARSUJE CIĄG ZNAKÓW I ZMIENIA STAN TUI! - 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) => { - // Wrzucamy błąd clapa na ekran, żeby użytkownik wiedział, co zepsuł we flagach - cliclack::log::error(format!("{}", e)).unwrap(); + 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(); } } - // Pozostajemy w pętli, aby interfejs odświeżył się z nowymi wartościami - }, + } Ok(Action::Paths) => { last_action = Action::Paths; handle_paths(s, &t); @@ -140,6 +159,28 @@ pub fn menu_main(s: &mut StateTui) { 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(); @@ -149,12 +190,23 @@ pub fn menu_main(s: &mut StateTui) { 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(); + } } @@ -291,6 +343,7 @@ fn split_patterns(input: &str) -> Vec { result } +/*/ fn split_cli_args(input: &str) -> Vec { let mut args = Vec::new(); let mut current = String::new(); @@ -324,4 +377,5 @@ fn split_cli_args(input: &str) -> Vec { args.push(current); } args -} \ No newline at end of file +} + */ diff --git a/src/interfaces/tui/state.rs b/src/interfaces/tui/state.rs index 596fb46..b6043b6 100644 --- a/src/interfaces/tui/state.rs +++ b/src/interfaces/tui/state.rs @@ -25,6 +25,8 @@ impl StateTui { ignore_case: false, no_root: false, info: true, // Domyślnie włączamy statystyki (-i) + gui: false, + no_emoji: false, lang: Some(lang), }, } From 6206612c2888881a79f0b1552cc9ee6e151e4a0a Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Thu, 19 Mar 2026 11:30:00 +0100 Subject: [PATCH 35/45] fix --- src/core/path_view/grid.rs | 2 +- src/core/path_view/tree.rs | 2 +- src/interfaces/gui.rs | 17 +++++++++++++++++ src/interfaces/gui/settings.rs | 28 ++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/core/path_view/grid.rs b/src/core/path_view/grid.rs index 60f31fe..427cb84 100644 --- a/src/core/path_view/grid.rs +++ b/src/core/path_view/grid.rs @@ -116,7 +116,7 @@ impl PathGrid { name: r_name.to_string(), path: PathBuf::from(r_name), is_dir: true, - icon: DIR_ICON.to_string(), + icon: if no_emoji { String::new() } else { DIR_ICON.to_string() }, weight_str: empty_weight, weight_bytes: 0, children: top_nodes, diff --git a/src/core/path_view/tree.rs b/src/core/path_view/tree.rs index cb7f742..f611131 100644 --- a/src/core/path_view/tree.rs +++ b/src/core/path_view/tree.rs @@ -117,7 +117,7 @@ impl PathTree { name: r_name.to_string(), path: PathBuf::from(r_name), is_dir: true, - icon: DIR_ICON.to_string(), + icon: if no_emoji { String::new() } else { DIR_ICON.to_string() }, weight_str: empty_weight, weight_bytes: 0, children: top_nodes, diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs index 4734d7b..7db5a46 100644 --- a/src/interfaces/gui.rs +++ b/src/interfaces/gui.rs @@ -11,27 +11,44 @@ pub enum Tab { Settings, Paths, Code } #[derive(PartialEq)] pub enum PathsTab { Match, Mismatch } +#[derive(Default, Clone)] +pub struct TreeStats { + pub file_count: usize, + pub total_weight: u64, + pub text_count: usize, + pub text_weight: u64, + pub bin_count: usize, + pub bin_weight: u64, +} + pub struct CargoPlotApp { pub args: CliArgs, pub active_tab: Tab, pub active_paths_tab: PathsTab, pub new_pattern_input: String, + pub out_path_input: String, pub generated_paths_m: String, // ⚡ Bufor tylko dla MATCH pub generated_paths_x: String, // ⚡ Bufor tylko dla MISMATCH pub generated_code: String, // ⚡ Bufor na wygenerowany kod + pub stats_m: TreeStats, // ⚡ NOWOŚĆ: Statystyki Match + pub stats_x: TreeStats, // ⚡ NOWOŚĆ: Statystyki Mismatch pub ui_scale: f32, } impl CargoPlotApp { pub fn new(args: CliArgs) -> Self { + let default_out = args.out_path.clone().unwrap_or_default(); Self { args, active_tab: Tab::Settings, active_paths_tab: PathsTab::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: String::new(), + stats_m: TreeStats::default(), + stats_x: TreeStats::default(), ui_scale: 1.0, } } diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index 40b2c77..835f637 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -98,9 +98,17 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { egui::ComboBox::from_label(if is_pl { "Sortowanie" } else { "Sorting" }) .selected_text(format!("{:?}", app.args.sort)) .show_ui(ui, |ui| { + // ⚡ PEŁNA LISTA SORTOWAŃ 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"); }); @@ -119,6 +127,26 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.checkbox(&mut app.args.no_root, if is_pl { "Ukryj ROOT w drzewie" } else { "Hide ROOT in tree" }); }); + // ⚡ NOWOŚĆ: ŚCIEŻKA WYNIKOWA (Output path) + ui.add_space(10.0); + ui.horizontal(|ui| { + ui.label(if is_pl { "💾 Ścieżka zapisu (Output):" } else { "💾 Output path:" }); + if ui.text_edit_singleline(&mut app.out_path_input).changed() { + app.args.out_path = if app.out_path_input.trim().is_empty() { + None + } else { + Some(app.out_path_input.trim().to_string()) + }; + } + if ui.button(if is_pl { "Wybierz folder..." } else { "Browse folder..." }).clicked() { + if let Some(folder) = rfd::FileDialog::new().pick_folder() { + let path = folder.to_string_lossy().replace('\\', "/"); + app.out_path_input = path.clone(); + app.args.out_path = Some(path); + } + } + }); + ui.add_space(30.0); // 6. STOPKA From d472df4ffe3bbd84e536f4425b54d4141797eb50 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Fri, 20 Mar 2026 20:14:33 +0100 Subject: [PATCH 36/45] (fix gui) --- src/interfaces/gui.rs | 22 ++++-- src/interfaces/gui/code.rs | 129 +++++++++++++++++++++++---------- src/interfaces/gui/paths.rs | 57 +++++++++------ src/interfaces/gui/settings.rs | 23 ++++-- 4 files changed, 153 insertions(+), 78 deletions(-) diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs index 7db5a46..4f88684 100644 --- a/src/interfaces/gui.rs +++ b/src/interfaces/gui.rs @@ -11,6 +11,10 @@ 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 file_count: usize, @@ -25,28 +29,32 @@ pub struct CargoPlotApp { pub args: CliArgs, pub active_tab: Tab, pub active_paths_tab: PathsTab, + pub active_code_tab: CodeTab, // ⚡ Dodane pole: aktywna zakładka Kodu pub new_pattern_input: String, pub out_path_input: String, - pub generated_paths_m: String, // ⚡ Bufor tylko dla MATCH - pub generated_paths_x: String, // ⚡ Bufor tylko dla MISMATCH - pub generated_code: String, // ⚡ Bufor na wygenerowany kod - pub stats_m: TreeStats, // ⚡ NOWOŚĆ: Statystyki Match - pub stats_x: TreeStats, // ⚡ NOWOŚĆ: Statystyki Mismatch + pub generated_paths_m: String, + pub generated_paths_x: String, + pub generated_code_m: String, // ⚡ Dodane pole: kod MATCH + pub generated_code_x: String, // ⚡ Dodane pole: kod MISMATCH + pub stats_m: TreeStats, + pub stats_x: TreeStats, pub ui_scale: f32, } impl CargoPlotApp { pub fn new(args: CliArgs) -> Self { - let default_out = args.out_path.clone().unwrap_or_default(); + let default_out = args.out_path.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: 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, diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs index d687318..6a0f89f 100644 --- a/src/interfaces/gui/code.rs +++ b/src/interfaces/gui/code.rs @@ -1,5 +1,5 @@ use eframe::egui; -use crate::interfaces::gui::CargoPlotApp; +use crate::interfaces::gui::{CargoPlotApp, CodeTab}; use cargo_plot::i18n::Lang; use cargo_plot::execute; use cargo_plot::core::path_matcher::stats::ShowMode; @@ -12,18 +12,14 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.horizontal(|ui| { if ui.button(if is_pl { "🔄 Generuj kod (Cache)" } else { "🔄 Generate code" }).clicked() { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - let show_mode = match (app.args.include, app.args.exclude) { - (true, false) => ShowMode::Include, - (false, true) => ShowMode::Exclude, - _ => ShowMode::Context, - }; + // Wymuszamy pełny skan (Context), żeby mieć oba wyniki od razu let stats = execute::execute( &app.args.enter_path, &app.args.patterns, !app.args.ignore_case, app.args.sort.into(), - show_mode, + ShowMode::Context, app.args.view.into(), app.args.no_root, false, @@ -32,57 +28,112 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { |_| {}, |_| {}, ); - let tree_text = stats.render_output(app.args.view.into(), show_mode, false, false); - let mut content = format!("```plaintext\n{}\n```\n\n", tree_text); - let base_dir = std::path::Path::new(&app.args.enter_path); - let mut counter = 1; - // Ręcznie budujemy podgląd kodu dla GUI + // --- BUDOWA BUFORA MATCH (-m) --- + 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.push_str(&format!("### {:03}: `{}`\n\n```rust\n{}\n```\n\n", counter, p_str, txt)); - } - Err(_) => { - content.push_str(&format!("### {:03}: `{}`\n\n> *(Pominięto binarkę)*\n\n", counter, p_str)); - } + 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> *(Pominięto binarkę)*\n\n", counter_m, p_str)), + } + counter_m += 1; + } + app.generated_code_m = content_m; + + // --- BUDOWA BUFORA MISMATCH (-x) --- + 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> *(Pominięto binarkę)*\n\n", counter_x, p_str)), } - counter += 1; + counter_x += 1; } - app.generated_code = content; + app.generated_code_x = content_x; } ui.add_space(15.0); - ui.checkbox(&mut app.args.by, if is_pl { "Dodaj stopkę (--by) przy zapisie" } else { "Add footer (--by) on save" }); + ui.checkbox(&mut app.args.by, if is_pl { "Dodaj stopkę (--by)" } else { "Add footer (--by)" }); ui.add_space(15.0); - if ui.button(if is_pl { "💾 Zapisz do pliku" } else { "💾 Save to file" }).clicked() { - let tag = TimeTag::now(); - let filepath = format!("./cache_{}.md", tag); - - // Prosty zapis tego, co użytkownik ma w polu tekstowym - let mut final_text = app.generated_code.clone(); - if app.args.by { - final_text.push_str(&format!("\n\n---\n**Wersja raportu:** {}\n---", tag)); + // ⚡ Helper rozwiązujący folder zapisu ze zmiennej out_code + let resolve_dir = |val: &Option| -> String { + match val { + Some(v) if v == "AUTO" => "./other/".to_string(), + Some(v) => { + let mut p = v.replace('\\', "/"); + if !p.ends_with('/') { p.push('/'); } + p + } + None => "./".to_string(), } - + }; + + // ⚡ ZAPIS DLA -m + if ui.button(if is_pl { "💾 Zapisz (-m)" } else { "💾 Save (-m)" }).clicked() { + let tag = TimeTag::now(); + let filepath = format!("{}plot-archive_{}_M.md", resolve_dir(&app.args.out_code), tag); + let mut final_text = app.generated_code_m.clone(); + if app.args.by { final_text.push_str(&format!("\n\n---\n**Wersja raportu:** {}\n---", tag)); } + let _ = std::fs::write(&filepath, final_text); + } + + ui.add_space(5.0); + + // ⚡ ZAPIS DLA -x + if ui.button(if is_pl { "💾 Zapisz (-x)" } else { "💾 Save (-x)" }).clicked() { + let tag = TimeTag::now(); + let filepath = format!("{}plot-archive_{}_X.md", resolve_dir(&app.args.out_code), tag); + let mut final_text = app.generated_code_x.clone(); + if app.args.by { final_text.push_str(&format!("\n\n---\n**Wersja raportu:** {}\n---", tag)); } let _ = std::fs::write(&filepath, final_text); } }); ui.separator(); - // 2. POLE TEKSTOWE / NOTATNIK - egui::ScrollArea::both().show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(&mut app.generated_code) - .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY) - .desired_rows(30), - ); + // 2. DOLNA BELKA (ZAKŁADKI) + egui::TopBottomPanel::bottom("code_subtabs").show_inside(ui, |ui| { + ui.add_space(8.0); + ui.horizontal(|ui| { + if app.active_code_tab == CodeTab::Match { + ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); + } + + let m_text = egui::RichText::new("✔ (-m) MATCH").size(18.0).strong().color(egui::Color32::from_rgb(138, 90, 255)); + let x_text = egui::RichText::new("✖ (-x) MISMATCH").size(18.0).strong().color(egui::Color32::from_rgb(255, 80, 100)); + + ui.selectable_value(&mut app.active_code_tab, CodeTab::Match, m_text); + ui.add_space(20.0); + ui.selectable_value(&mut app.active_code_tab, CodeTab::Mismatch, x_text); + }); + ui.add_space(8.0); + }); + + // 3. POLE TEKSTOWE / NOTATNIK + egui::CentralPanel::default().show_inside(ui, |ui| { + egui::ScrollArea::both().show(ui, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + let text_buffer = match app.active_code_tab { + CodeTab::Match => &mut app.generated_code_m, + CodeTab::Mismatch => &mut app.generated_code_x, + }; + + ui.add( + egui::TextEdit::multiline(text_buffer) + .font(egui::TextStyle::Monospace) + .desired_width(f32::INFINITY) + ); + }); }); } \ No newline at end of file diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs index 4597166..0a11c3a 100644 --- a/src/interfaces/gui/paths.rs +++ b/src/interfaces/gui/paths.rs @@ -13,7 +13,6 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { if ui.button(if is_pl { "🔄 Generuj / Regeneruj" } else { "🔄 Generate" }).clicked() { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - // Skanujemy dysk tylko raz, wyciągając pełny kontekst let stats = execute::execute( &app.args.enter_path, &app.args.patterns, @@ -28,49 +27,62 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { |_| {}, |_| {}, ); - // ⚡ Od razu zapisujemy oba wyniki do niezależnych buforów app.generated_paths_m = stats.render_output(app.args.view.into(), ShowMode::Include, false, false); app.generated_paths_x = stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); } ui.add_space(15.0); - ui.checkbox(&mut app.args.by, if is_pl { "Dodaj stopkę (--by) przy zapisie" } else { "Add footer (--by) on save" }); + ui.checkbox(&mut app.args.by, if is_pl { "Dodaj stopkę (--by)" } else { "Add footer (--by)" }); ui.add_space(15.0); - if ui.button(if is_pl { "💾 Zapisz do pliku" } else { "💾 Save to file" }).clicked() { + // ⚡ Helper rozwiązujący folder zapisu ze zmiennej out_path + let resolve_dir = |val: &Option| -> String { + match val { + Some(v) if v == "AUTO" => "./other/".to_string(), // Jeśli AUTO, wrzuć do ./other/ + Some(v) => { + let mut p = v.replace('\\', "/"); + if !p.ends_with('/') { p.push('/'); } + p + } + None => "./".to_string(), // Domyślnie główny folder projektu + } + }; + + // ⚡ ZAPIS DLA -m + if ui.button(if is_pl { "💾 Zapisz (-m)" } else { "💾 Save (-m)" }).clicked() { let tag = TimeTag::now(); - let filepath = format!("./paths_{}.md", tag); + let filepath = format!("{}plot-address_{}_M.md", resolve_dir(&app.args.out_path), tag); let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - - // ⚡ Zapisujemy tylko ten bufor, na który użytkownik aktualnie patrzy! - let current_text = if app.active_paths_tab == PathsTab::Match { - &app.generated_paths_m - } else { - &app.generated_paths_x - }; + cargo_plot::core::save::SaveFile::paths(&app.generated_paths_m, &filepath, &tag, app.args.by, &i18n); + } + + ui.add_space(5.0); - cargo_plot::core::save::SaveFile::paths(current_text, &filepath, &tag, app.args.by, &i18n); + // ⚡ ZAPIS DLA -x + if ui.button(if is_pl { "💾 Zapisz (-x)" } else { "💾 Save (-x)" }).clicked() { + let tag = TimeTag::now(); + let filepath = format!("{}plot-address_{}_X.md", resolve_dir(&app.args.out_path), tag); + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + cargo_plot::core::save::SaveFile::paths(&app.generated_paths_x, &filepath, &tag, app.args.by, &i18n); } }); ui.separator(); - // 2. DOLNA BELKA (ZAKŁADKI) - Przypinamy ją do dołu ekranu + // 2. DOLNA BELKA (ZAKŁADKI) egui::TopBottomPanel::bottom("paths_subtabs").show_inside(ui, |ui| { ui.add_space(8.0); ui.horizontal(|ui| { - // ⚡ ZMIENIAMY TŁO TYLKO DLA LEWEGO GUZIKA (MATCH) if app.active_paths_tab == PathsTab::Match { - ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); // Złoty + ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); } - // Jeśli aktywny jest Mismatch, egui użyje swojego domyślnego, nienaruszonego tła! let m_text = egui::RichText::new("✔ (-m) MATCH") - .size(18.0).strong().color(egui::Color32::from_rgb(138, 90, 255)); // Fiolet + .size(18.0).strong().color(egui::Color32::from_rgb(138, 90, 255)); let x_text = egui::RichText::new("✖ (-x) MISMATCH") - .size(18.0).strong().color(egui::Color32::from_rgb(255, 80, 100)); // Czerwień + .size(18.0).strong().color(egui::Color32::from_rgb(255, 80, 100)); ui.selectable_value(&mut app.active_paths_tab, PathsTab::Match, m_text); ui.add_space(20.0); @@ -79,13 +91,11 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add_space(8.0); }); - // 3. ŚRODKOWA PRZESTRZEŃ (NOTATNIK) - Wypełnia całą resztę miejsca między górą a dołem + // 3. NOTATNIK egui::CentralPanel::default().show_inside(ui, |ui| { egui::ScrollArea::both().show(ui, |ui| { - // ⚡ Wyłączamy łamanie wierszy - włącza się poziomy scroll! ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - // Wybieramy odpowiedni bufor tekstowy do edycji i podglądu let text_buffer = match app.active_paths_tab { PathsTab::Match => &mut app.generated_paths_m, PathsTab::Mismatch => &mut app.generated_paths_x, @@ -94,8 +104,7 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add( egui::TextEdit::multiline(text_buffer) .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY) // Rozciąga na max szerokość - // desired_rows() usunięte - teraz bierze całą wolną wysokość okna! + .desired_width(f32::INFINITY) ); }); }); diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index 835f637..a3d5da0 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -130,19 +130,26 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // ⚡ NOWOŚĆ: ŚCIEŻKA WYNIKOWA (Output path) ui.add_space(10.0); ui.horizontal(|ui| { - ui.label(if is_pl { "💾 Ścieżka zapisu (Output):" } else { "💾 Output path:" }); + ui.label(if is_pl { "💾 Folder zapisu (Output):" } else { "💾 Output folder:" }); + + // Używamy `out_path_input` z gui.rs jako wspólnego bufora tekstowego if ui.text_edit_singleline(&mut app.out_path_input).changed() { - app.args.out_path = if app.out_path_input.trim().is_empty() { - None - } else { - Some(app.out_path_input.trim().to_string()) - }; + let trimmed = app.out_path_input.trim(); + let path_val = if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }; + // ⚡ Przypisujemy ten sam wpisany folder do obu flag w CLI + app.args.out_path = path_val.clone(); + app.args.out_code = path_val; } + if ui.button(if is_pl { "Wybierz folder..." } else { "Browse folder..." }).clicked() { if let Some(folder) = rfd::FileDialog::new().pick_folder() { - let path = folder.to_string_lossy().replace('\\', "/"); + let mut path = folder.to_string_lossy().replace('\\', "/"); + if !path.ends_with('/') { path.push('/'); } // Gwarantujemy, że to zawsze będzie traktowane jak folder + app.out_path_input = path.clone(); - app.args.out_path = Some(path); + // ⚡ Przypisujemy wybrany folder do obu flag + app.args.out_path = Some(path.clone()); + app.args.out_code = Some(path); } } }); From 2167a57a619d6de7912c557f1e2b081e30df0232 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Fri, 20 Mar 2026 23:14:02 +0100 Subject: [PATCH 37/45] (update: system save) --- src/core/save.rs | 45 ++----- src/interfaces/cli/args.rs | 209 +++++++++++++++++++++++++-------- src/interfaces/cli/engine.rs | 153 ++++++++++++------------ src/interfaces/gui.rs | 2 +- src/interfaces/gui/code.rs | 19 ++- src/interfaces/gui/i18n.rs | 77 ++++++++++++ src/interfaces/gui/paths.rs | 11 +- src/interfaces/gui/settings.rs | 126 ++++++++++---------- src/interfaces/tui/i18n.rs | 21 ++-- src/interfaces/tui/menu.rs | 36 +++--- src/interfaces/tui/state.rs | 10 +- 11 files changed, 443 insertions(+), 266 deletions(-) create mode 100644 src/interfaces/gui/i18n.rs diff --git a/src/core/save.rs b/src/core/save.rs index b478868..98a9394 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -6,43 +6,18 @@ use std::path::Path; pub struct SaveFile; impl SaveFile { - fn generate_by_section(tag: &str, typ: &str, i18n: &I18n) -> String { - let raw_args: Vec = std::env::args().collect(); - let mut formatted_args = Vec::new(); - - // 1. Obsługa początku komendy (estetyka: zamiana ścieżki na "cargo plot") - formatted_args.push("cargo".to_string()); - - // Jeśli nie wywołaliśmy tego przez cargo (np. bezpośrednio binarkę), - // a w argumentach nie ma jeszcze "plot", dodajmy go dla czytelności raportu. - if !raw_args.iter().any(|a| a == "plot") { - formatted_args.push("plot".to_string()); - } - - // 2. Przetwarzanie reszty argumentów (pomijamy arg[0], bo to ścieżka do binarki) - for arg in raw_args.into_iter().skip(1) { - if arg.starts_with('-') || arg == "plot" || arg == "--" { - // Flagi i słowa kluczowe zostawiamy gołe - formatted_args.push(arg); - } else { - // Ścieżki, wartości i wzorce owijamy w cudzysłowy - formatted_args.push(format!("\"{}\"", arg.replace('\"', "\\\""))); - } - } - - let command = formatted_args.join(" "); - + // ⚡ Upubliczniamy funkcję, żeby kod w `code.rs` mógł wygenerować stopkę + pub fn generate_by_section(tag: &str, typ: &str, i18n: &I18n, cmd: &str) -> String { format!( "\n\n---\n---\n\n{}\n\n{}\n\n```bash\n{}\n```\n\n{}\n\n{}\n\n{}\n\n---\n", i18n.by_title(typ), i18n.by_cmd(), - command, + cmd, // ⚡ Używa czystej, przetworzonej komendy! i18n.by_instructions(), i18n.by_link(), i18n.by_version(tag) ) } - /// 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); @@ -65,9 +40,9 @@ impl SaveFile { } } /// Formatowanie i zapis samego widoku struktury (ścieżek) - pub fn paths(content: &str, filepath: &str, tag: &str, add_by: bool, i18n: &I18n) { + pub fn paths(content: &str, filepath: &str, tag: &str, add_by: bool, i18n: &I18n, cmd: &str) { let by_section = if add_by { - Self::generate_by_section(tag, "paths", i18n) + Self::generate_by_section(tag, "paths", i18n, cmd) } else { String::new() }; @@ -97,16 +72,10 @@ impl SaveFile { /// 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, + tree_text: &str, paths: &[String], base_dir: &str, filepath: &str, tag: &str, add_by: bool, i18n: &I18n, cmd: &str, ) { let by_section = if add_by { - Self::generate_by_section(tag, "codes", i18n) + Self::generate_by_section(tag, "codes", i18n, cmd) } else { String::new() }; diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 0928b8f..c923119 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -3,8 +3,8 @@ 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. -/// Oszukuje clap'a, mówiąc mu: "Główny program nazywa się 'cargo', a 'plot' to jego subkomenda". #[derive(Parser, Debug)] #[command(name = "cargo", bin_name = "cargo")] pub enum CargoCli { @@ -13,78 +13,105 @@ pub enum CargoCli { Plot(CliArgs), } -/// [POL]: Nasze docelowe argumenty CLI. Zauważ, że teraz to jest `Args`, a nie `Parser`. +/// [ENG]: Command line arguments for cargo-plot. +/// [POL]: Argumenty wiersza poleceń dla cargo-plot. #[derive(Args, Debug, Clone)] -#[command(author, version, about = "Zaawansowany skaner struktury plików", long_about = None)] +#[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. + /// [ENG]: 📂 Input path to scan. + /// [POL]: 📂 Ścieżka wejściowa do skanowania. #[arg(short = 'd', long = "dir", default_value = ".")] pub enter_path: String, - /// [ENG]: Match patterns. - /// [POL]: Wzorce dopasowań. - #[arg(short = 'p', long = "pat", required_unless_present = "gui")] + /// [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]: Results sorting strategy. - /// [POL]: Strategia sortowania wyników. + /// [ENG]: ✔️ Treat patterns as match (include) rules. + /// [POL]: ✔️ Traktuj wzorce jako zasady dopasowania (włącz). + #[arg(short = 'm', long = "pat-match")] + pub include: bool, + + /// [ENG]: ❌ Treat patterns as mismatch (exclude) rules. + /// [POL]: ❌ Traktuj wzorce jako zasady odrzucenia (wyklucz). + #[arg(short = 'x', long = "pat-mismatch")] + 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, - /// [POL]: Wybiera format wyświetlania wyników (drzewo, lista, siatka). + /// [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]: Display only matched paths. - /// [POL]: Wyświetlaj tylko dopasowane ścieżki. - #[arg(short = 'm', long = "on-match")] - pub include: bool, - - /// [ENG]: Display only rejected paths. - /// [POL]: Wyświetlaj tylko odrzucone ścieżki. - #[arg(short = 'x', long = "on-mismatch")] - pub exclude: bool, + /// [ENG]: 📝 Save the paths structure to a file. + /// [POL]: 📝 Zapisuje strukturę ścieżek do pliku. + #[arg(long = "save-address")] + pub save_address: bool, - // ⚡ FLAGA ZAPISU ŚCIEŻEK (MARKDOWN) - /// [POL]: Opcjonalna ścieżka do pliku, w którym zostanie zapisany wynik. - #[arg(short = 'o', long = "out-paths", num_args = 0..=1, default_missing_value = "AUTO")] - pub out_path: Option, + /// [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, - // ⚡ NOWA FLAGA ZAPISU KODU (MARKDOWN) - /// [POL]: Opcjonalna ścieżka do pliku z kodem (cache). Samo -c wygeneruje domyślną ścieżkę w ./other/ - #[arg(short = 'c', long = "out-cache", num_args = 0..=1, default_missing_value = "AUTO")] - pub out_code: Option, - - // ⚡ FLAGA BY (STOPKA) - /// [POL]: Dodaje sekcję informacyjną z wywołaną komendą na dole pliku. - #[arg(short = 'b', long = "by", default_value_t = false)] + /// [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]: Ignore case. - /// [POL]: Ignoruj wielkość liter. - #[arg(long = "ignore-case")] - pub ignore_case: bool, - - /// [POL]: Ukrywa główny folder (root) w widoku drzewa. - #[arg(long = "treeview-no-root", default_value_t = false)] + /// [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, - /// [POL]: Wyświetla dodatkowe informacje, statystyki i nagłówki (tryb gadatliwy). - #[arg(short = 'i', long = "info", default_value_t = false)] + /// [ENG]: ℹ️ Display summary statistics and headers. + /// [POL]: ℹ️ Wyświetla statystyki podsumowujące i nagłówki. + #[arg(short = 'i', long = "info")] pub info: bool, - /// [POL]: Uruchamia aplikację natychmiast w trybie graficznym (GUI). - #[arg(short = 'g', long = "gui", default_value_t = false)] + /// [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, - /// [POL]: Wyłącza renderowanie ikon/emoji (przydatne w czystych terminalach lub GUI). - #[arg(long = "no-emoji", default_value_t = false)] - pub no_emoji: bool, - - /// [POL]: Wymusza język interfejsu (pl / en). Domyślnie pobiera z systemu. + /// [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)] @@ -109,6 +136,12 @@ pub enum CliSortStrategy { ZaDirMerge, } +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)] +pub enum CliUnitSystem { + Dec, + Bin, +} + impl From for SortStrategy { fn from(val: CliSortStrategy) -> Self { match val { @@ -136,3 +169,83 @@ impl From for ViewMode { } } } + +impl CliArgs { + /// [ENG]: Reconstructs a clean terminal command string. + /// [POL]: Odtwarza czystą komendę terminalową. + pub fn to_command_string(&self) -> 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)); + } + } + + if !self.patterns.is_empty() { + cmd.push("-p".to_string()); + cmd.push(format!("\"{}\"", self.patterns.join(","))); + } + + if self.include { cmd.push("-m".to_string()); } + if self.exclude { 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()); + } + + if self.save_address { cmd.push("--save-address".to_string()); } + if self.save_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(" ") + } +} \ No newline at end of file diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 267cf2e..e3933c8 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -6,22 +6,28 @@ use cargo_plot::core::path_view::ViewMode; use cargo_plot::core::save::SaveFile; use cargo_plot::execute::{self, SortStrategy}; use cargo_plot::i18n::I18n; -// use cargo_plot::theme::for_path_list::get_icon_for_path; -/// [ENG]: The execution engine (Cockpit). -/// [POL]: Silnik wykonawczy (Kokpit). +// [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]: 📝 Reconstructs the command string for the footer. + // [POL]: 📝 Odtwarza ciąg komendy dla stopki. + let cmd_string = args.to_command_string(); 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]: 🎚️ 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, // Tylko flaga -m - (false, true) => ShowMode::Exclude, // Tylko flaga -x - _ => ShowMode::Context, // Brak flag (lub podane obie) = pokazujemy wszystko + (true, false) => ShowMode::Include, + (false, true) => ShowMode::Exclude, + _ => ShowMode::Context, }; + // [ENG]: 🚀 Executes the core matching logic. + // [POL]: 🚀 Wykonuje główną logikę dopasowywania. let stats = execute::execute( &args.enter_path, &args.patterns, @@ -33,95 +39,92 @@ pub fn run(args: CliArgs) { args.info, args.no_emoji, &i18n, - |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk + |_| {}, |_| {}, - // |file_stat| { - // if !args.treeview { - // println!( - // "✅ MATCH: {} {} ({} B)", - // get_icon_for_path(&file_stat.path), - // file_stat.path, - // file_stat.weight_bytes - // ); - // } - // }, - // |file_stat| { - // if !args.treeview && show_exclude { - // println!( - // "❌ REJECT: {} {} ({} B)", - // get_icon_for_path(&file_stat.path), - // file_stat.path, - // file_stat.weight_bytes - // ); - // } - // }, ); - // 2. RENDEROWANIE WYNIKÓW + // [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); - let has_out_paths = args.out_path.is_some(); - let has_out_codes = args.out_code.is_some(); - - if has_out_paths || has_out_codes { + // [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(); - let output_str_txt = stats.render_output(view_mode, show_mode, args.info, false); - - // Closure do automatycznego generowania ścieżki - let resolve_filepath = |val: &str, prefix: &str| -> String { - if val == "AUTO" { - format!("./other/{}_{}.md", prefix, tag) - } else if val.ends_with('/') || val.ends_with('\\') { - format!("{}{}_{}.md", val, prefix, tag) - } else { - let path = std::path::Path::new(val); - 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("")); + + // [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); - let parent_str = parent.to_string_lossy().replace('\\', "/"); - let ext_str = if ext.is_empty() { - String::new() - } else { - format!(".{}", ext) - }; - let stem_str = if stem.is_empty() { prefix } else { &stem }; - if parent_str.is_empty() { - format!("{}_{}{}", stem_str, tag, ext_str) - } else { - format!("{}/{}_{}{}", parent_str, stem_str, tag, ext_str) + // [ENG]: 📂 Resolves the output directory path. + // [POL]: 📂 Rozwiązuje ścieżkę katalogu wyjściowego. + let resolve_dir = |val: &Option| -> String { + match val { + Some(v) if v == "AUTO" => "./other/".to_string(), + Some(v) => { + let mut p = v.replace('\\', "/"); + if !p.ends_with('/') { p.push('/'); } + p } + None => "./".to_string(), } }; - if let Some(val) = &args.out_path { - let filepath = resolve_filepath(val, "paths"); - // ⚡ CZYSTE WYWOŁANIE: podajemy args.by - SaveFile::paths(&output_str_txt, &filepath, &tag, args.by, &i18n); + let output_dir = resolve_dir(&args.dir_out); + + // [ENG]: 📝 Saves the path structure (address). + // [POL]: 📝 Zapisuje strukturę ścieżek (adres). + if args.save_address { + if args.include || (!args.include && !args.exclude) { + let filepath = format!("{}plot-address_{}_M.md", output_dir, tag); + SaveFile::paths(&output_str_txt_m, &filepath, &tag, args.by, &i18n, &cmd_string); + } + if args.exclude || (!args.include && !args.exclude) { + let filepath = format!("{}plot-address_{}_X.md", output_dir, tag); + SaveFile::paths(&output_str_txt_x, &filepath, &tag, args.by, &i18n, &cmd_string); + } } - if let Some(val) = &args.out_code { - let filepath = resolve_filepath(val, "cache"); + // [ENG]: 📦 Saves the full file contents (archive). + // [POL]: 📦 Zapisuje pełną zawartość plików (archiwum). + if args.save_archive { if let Ok(ctx) = PathContext::resolve(&args.enter_path) { - // ⚡ CZYSTE WYWOŁANIE: podajemy args.by - SaveFile::codes( - &output_str_txt, - &stats.m_matched.paths, - &ctx.entry_absolute, - &filepath, - &tag, - args.by, - &i18n, - ); + if args.include || (!args.include && !args.exclude) { + let filepath = format!("{}plot-archive_{}_M.md", output_dir, tag); + SaveFile::codes( + &output_str_txt_m, + &stats.m_matched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_string + ); + } + if args.exclude || (!args.include && !args.exclude) { + let filepath = format!("{}plot-archive_{}_X.md", output_dir, tag); + SaveFile::codes( + &output_str_txt_x, + &stats.x_mismatched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_string + ); + } } } } - // 3. PODSUMOWANIE + // [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!("---------------------------------------"); - // ⚡ PODMIENIONO NA WYWOŁANIA Z I18N println!( "{}", i18n.cli_summary_matched(stats.m_size_matched, stats.total) @@ -133,4 +136,4 @@ pub fn run(args: CliArgs) { } else { println!("---------------------------------------"); } -} +} \ No newline at end of file diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs index 4f88684..5051c06 100644 --- a/src/interfaces/gui.rs +++ b/src/interfaces/gui.rs @@ -43,7 +43,7 @@ pub struct CargoPlotApp { impl CargoPlotApp { pub fn new(args: CliArgs) -> Self { - let default_out = args.out_path.clone().unwrap_or_default(); + let default_out = args.dir_out.clone().unwrap_or_default(); Self { args, active_tab: Tab::Settings, diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs index 6a0f89f..b4b1030 100644 --- a/src/interfaces/gui/code.rs +++ b/src/interfaces/gui/code.rs @@ -81,20 +81,29 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // ⚡ ZAPIS DLA -m if ui.button(if is_pl { "💾 Zapisz (-m)" } else { "💾 Save (-m)" }).clicked() { let tag = TimeTag::now(); - let filepath = format!("{}plot-archive_{}_M.md", resolve_dir(&app.args.out_code), tag); + let filepath = format!("{}plot-archive_{}_M.md", resolve_dir(&app.args.dir_out), tag); let mut final_text = app.generated_code_m.clone(); - if app.args.by { final_text.push_str(&format!("\n\n---\n**Wersja raportu:** {}\n---", tag)); } + + if app.args.by { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section(&tag, "codes", &i18n, &cmd_string)); + } let _ = std::fs::write(&filepath, final_text); } ui.add_space(5.0); - // ⚡ ZAPIS DLA -x if ui.button(if is_pl { "💾 Zapisz (-x)" } else { "💾 Save (-x)" }).clicked() { let tag = TimeTag::now(); - let filepath = format!("{}plot-archive_{}_X.md", resolve_dir(&app.args.out_code), tag); + let filepath = format!("{}plot-archive_{}_X.md", resolve_dir(&app.args.dir_out), tag); let mut final_text = app.generated_code_x.clone(); - if app.args.by { final_text.push_str(&format!("\n\n---\n**Wersja raportu:** {}\n---", tag)); } + + if app.args.by { + let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + let cmd_string = app.args.to_command_string(); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section(&tag, "codes", &i18n, &cmd_string)); + } let _ = std::fs::write(&filepath, final_text); } }); diff --git a/src/interfaces/gui/i18n.rs b/src/interfaces/gui/i18n.rs new file mode 100644 index 0000000..71948d1 --- /dev/null +++ b/src/interfaces/gui/i18n.rs @@ -0,0 +1,77 @@ +// [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, + FooterVersion, + FooterDownload, + FooterInstall, + FooterUninstall, +} + +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::FooterVersion => "Wersja raportu:", + GuiText::FooterDownload => "Pobierz binarkę (GitHub)", + GuiText::FooterInstall => "Instalacja:", + GuiText::FooterUninstall => "Usuwanie:", + }, + 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::FooterVersion => "Report version:", + GuiText::FooterDownload => "Download binary (GitHub)", + GuiText::FooterInstall => "Install:", + GuiText::FooterUninstall => "Uninstall:", + }, + } + } +} \ No newline at end of file diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs index 0a11c3a..d59fe6b 100644 --- a/src/interfaces/gui/paths.rs +++ b/src/interfaces/gui/paths.rs @@ -51,19 +51,20 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // ⚡ ZAPIS DLA -m if ui.button(if is_pl { "💾 Zapisz (-m)" } else { "💾 Save (-m)" }).clicked() { let tag = TimeTag::now(); - let filepath = format!("{}plot-address_{}_M.md", resolve_dir(&app.args.out_path), tag); + let filepath = format!("{}plot-address_{}_M.md", resolve_dir(&app.args.dir_out), tag); let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - cargo_plot::core::save::SaveFile::paths(&app.generated_paths_m, &filepath, &tag, app.args.by, &i18n); + let cmd_string = app.args.to_command_string(); // ⚡ + cargo_plot::core::save::SaveFile::paths(&app.generated_paths_m, &filepath, &tag, app.args.by, &i18n, &cmd_string); } ui.add_space(5.0); - // ⚡ ZAPIS DLA -x if ui.button(if is_pl { "💾 Zapisz (-x)" } else { "💾 Save (-x)" }).clicked() { let tag = TimeTag::now(); - let filepath = format!("{}plot-address_{}_X.md", resolve_dir(&app.args.out_path), tag); + let filepath = format!("{}plot-address_{}_X.md", resolve_dir(&app.args.dir_out), tag); let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - cargo_plot::core::save::SaveFile::paths(&app.generated_paths_x, &filepath, &tag, app.args.by, &i18n); + let cmd_string = app.args.to_command_string(); // ⚡ + cargo_plot::core::save::SaveFile::paths(&app.generated_paths_x, &filepath, &tag, app.args.by, &i18n, &cmd_string); } }); diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index a3d5da0..930eee9 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -17,6 +17,7 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.radio_value(&mut app.args.lang, Some(Lang::En), "English"); }); ui.separator(); + ui.add_space(10.0); // 2. WYBÓR FOLDERU (Z działającym przyciskiem okna systemowego) ui.horizontal(|ui| { @@ -30,8 +31,69 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { app.args.enter_path = folder.to_string_lossy().replace('\\', "/"); } }); + ui.add_space(10.0); + ui.separator(); + + + // ⚡ NOWOŚĆ: ŚCIEŻKA WYNIKOWA (Output path) + ui.add_space(10.0); + ui.horizontal(|ui| { + ui.label(if is_pl { "💾 Folder zapisu (Output):" } else { "💾 Output folder:" }); + + // Używamy `out_path_input` z gui.rs jako wspólnego bufora tekstowego + if ui.text_edit_singleline(&mut app.out_path_input).changed() { + let trimmed = app.out_path_input.trim(); + app.args.dir_out = if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }; + } + + if ui.button(if is_pl { "Wybierz folder..." } else { "Browse folder..." }).clicked() { + if 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); + } + } + }); + ui.add_space(10.0); ui.separator(); + // 5. WIDOK I SORTOWANIE + ui.horizontal(|ui| { + egui::ComboBox::from_label(if is_pl { "Sortowanie" } else { "Sorting" }) + .selected_text(format!("{:?}", app.args.sort)) + .show_ui(ui, |ui| { + // ⚡ PEŁNA LISTA SORTOWAŃ + 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(if is_pl { "Tryb widoku" } else { "View mode" }) + .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, if is_pl { "Ukryj ROOT w drzewie" } else { "Hide ROOT in tree" }); + }); + + ui.add_space(20.0); // 3. WZORCE DOPASOWAŃ (Z połączonymi opcjami z linii "X") ui.heading(if is_pl { "🔍 Wzorce dopasowań (Patterns)" } else { "🔍 Match Patterns" }); @@ -93,68 +155,10 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { }); ui.separator(); - // 5. WIDOK I SORTOWANIE - ui.horizontal(|ui| { - egui::ComboBox::from_label(if is_pl { "Sortowanie" } else { "Sorting" }) - .selected_text(format!("{:?}", app.args.sort)) - .show_ui(ui, |ui| { - // ⚡ PEŁNA LISTA SORTOWAŃ - 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(if is_pl { "Tryb widoku" } else { "View mode" }) - .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, if is_pl { "Ukryj ROOT w drzewie" } else { "Hide ROOT in tree" }); - }); - - // ⚡ NOWOŚĆ: ŚCIEŻKA WYNIKOWA (Output path) - ui.add_space(10.0); - ui.horizontal(|ui| { - ui.label(if is_pl { "💾 Folder zapisu (Output):" } else { "💾 Output folder:" }); - - // Używamy `out_path_input` z gui.rs jako wspólnego bufora tekstowego - if ui.text_edit_singleline(&mut app.out_path_input).changed() { - let trimmed = app.out_path_input.trim(); - let path_val = if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }; - // ⚡ Przypisujemy ten sam wpisany folder do obu flag w CLI - app.args.out_path = path_val.clone(); - app.args.out_code = path_val; - } - - if ui.button(if is_pl { "Wybierz folder..." } else { "Browse folder..." }).clicked() { - if let Some(folder) = rfd::FileDialog::new().pick_folder() { - let mut path = folder.to_string_lossy().replace('\\', "/"); - if !path.ends_with('/') { path.push('/'); } // Gwarantujemy, że to zawsze będzie traktowane jak folder - - app.out_path_input = path.clone(); - // ⚡ Przypisujemy wybrany folder do obu flag - app.args.out_path = Some(path.clone()); - app.args.out_code = Some(path); - } - } - }); + + - ui.add_space(30.0); + ui.add_space(50.0); // 6. STOPKA ui.separator(); diff --git a/src/interfaces/tui/i18n.rs b/src/interfaces/tui/i18n.rs index 5c2c26f..5b41719 100644 --- a/src/interfaces/tui/i18n.rs +++ b/src/interfaces/tui/i18n.rs @@ -36,8 +36,9 @@ pub enum Prompt { SubSelectView, SubSelectSort, SubNoRoot, - SubOutPaths, - SubOutCode, + SubDirOut, + SubSaveAddress, + SubSaveArchive, SubBy, SubOnMatch, SubOnMismatch, @@ -124,13 +125,17 @@ impl Translatable for Prompt { pol: "Ukryć główny folder? / Hide root dir?", eng: "Ukryć główny folder? / Hide root dir?", }, - Prompt::SubOutPaths => Txt { - pol: "Plik na ścieżki (puste=Brak, AUTO=domyślny) / Paths output file:", - eng: "Plik na ścieżki (puste=Brak, AUTO=domyślny) / Paths output file:", + Prompt::SubDirOut => Txt { + pol: "Folder zapisu (--dir-out) [puste=CWD, AUTO=./other/]:", + eng: "Output folder (--dir-out) [empty=CWD, AUTO=./other/]:", }, - Prompt::SubOutCode => Txt { - pol: "Plik na kod (puste=Brak, AUTO=domyślny) / Code output file:", - eng: "Plik na kod (puste=Brak, AUTO=domyślny) / Code output file:", + 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?", diff --git a/src/interfaces/tui/menu.rs b/src/interfaces/tui/menu.rs index 4edc986..e15e20f 100644 --- a/src/interfaces/tui/menu.rs +++ b/src/interfaces/tui/menu.rs @@ -49,14 +49,10 @@ pub fn menu_main(s: &mut StateTui) { !s.args.no_root ); - let out_p = s.args.out_path.as_deref().unwrap_or("NONE"); - let out_c = s.args.out_code.as_deref().unwrap_or("NONE"); + let out_p = s.args.dir_out.as_deref().unwrap_or("AUTO"); let lbl_out = format!( - "{} (paths: {}, cache: {}, by: {})", - t.fmt(Prompt::BtnOutput), - out_p, - out_c, - s.args.by + "{} (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!( @@ -267,25 +263,21 @@ fn handle_view(s: &mut StateTui, t: &T) { } fn handle_output(s: &mut StateTui, t: &T) { - let out_p: String = cliclack::input(t.raw(Prompt::SubOutPaths)) - .default_input(s.args.out_path.as_deref().unwrap_or("")) + 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.out_path = if out_p.trim().is_empty() { - None - } else { - Some(out_p.trim().to_string()) - }; + s.args.dir_out = if out_p.trim().is_empty() { None } else { Some(out_p.trim().to_string()) }; - let out_c: String = cliclack::input(t.raw(Prompt::SubOutCode)) - .default_input(s.args.out_code.as_deref().unwrap_or("")) + s.args.save_address = cliclack::confirm(t.raw(Prompt::SubSaveAddress)) + .initial_value(s.args.save_address) .interact() - .unwrap_or_default(); - s.args.out_code = if out_c.trim().is_empty() { - None - } else { - Some(out_c.trim().to_string()) - }; + .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) diff --git a/src/interfaces/tui/state.rs b/src/interfaces/tui/state.rs index b6043b6..e16df26 100644 --- a/src/interfaces/tui/state.rs +++ b/src/interfaces/tui/state.rs @@ -17,11 +17,15 @@ impl StateTui { patterns: vec![], sort: CliSortStrategy::AzFileMerge, view: CliViewMode::Tree, - include: true, // Domyślnie pokazujemy dopasowania (-m) + include: true, exclude: false, - out_path: None, - out_code: None, + 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) From d0eed6e6cfbeb7a1b0766ee41c412fbb96d01099 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Fri, 20 Mar 2026 23:26:05 +0100 Subject: [PATCH 38/45] (add: i18n to GUI) --- src/core/path_view/grid.rs | 23 ++++- src/core/path_view/list.rs | 6 +- src/core/path_view/tree.rs | 21 +++- src/core/save.rs | 9 +- src/interfaces.rs | 2 +- src/interfaces/cli/args.rs | 46 ++++++--- src/interfaces/cli/engine.rs | 70 ++++++++----- src/interfaces/gui.rs | 101 +++++++++++------- src/interfaces/gui/code.rs | 148 ++++++++++++++++++-------- src/interfaces/gui/i18n.rs | 33 +++++- src/interfaces/gui/paths.rs | 121 ++++++++++++++-------- src/interfaces/gui/settings.rs | 183 ++++++++++++++++++++------------- src/interfaces/tui/i18n.rs | 36 ++++--- src/interfaces/tui/menu.rs | 31 ++++-- src/interfaces/tui/state.rs | 2 +- 15 files changed, 557 insertions(+), 275 deletions(-) diff --git a/src/core/path_view/grid.rs b/src/core/path_view/grid.rs index 427cb84..c0a531b 100644 --- a/src/core/path_view/grid.rs +++ b/src/core/path_view/grid.rs @@ -39,7 +39,7 @@ impl PathGrid { paths_map: &BTreeMap>, base_path: &Path, sort_strategy: SortStrategy, - weight_cfg: &WeightConfig, + weight_cfg: &WeightConfig, no_emoji: bool, ) -> FileNode { let name = path @@ -64,7 +64,9 @@ impl PathGrid { 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)) + .map(|c| { + build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji) + }) .collect(); FileNode::sort_slice(&mut child_nodes, sort_strategy); @@ -100,7 +102,16 @@ impl PathGrid { 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)) + .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); @@ -116,7 +127,11 @@ impl PathGrid { name: r_name.to_string(), path: PathBuf::from(r_name), is_dir: true, - icon: if no_emoji { String::new() } else { DIR_ICON.to_string() }, + icon: if no_emoji { + String::new() + } else { + DIR_ICON.to_string() + }, weight_str: empty_weight, weight_bytes: 0, children: top_nodes, diff --git a/src/core/path_view/list.rs b/src/core/path_view/list.rs index c210e4f..1c20d23 100644 --- a/src/core/path_view/list.rs +++ b/src/core/path_view/list.rs @@ -31,7 +31,11 @@ impl PathList { name: p_str.clone(), path: absolute, is_dir, - icon: if no_emoji { String::new() } else { get_icon_for_path(p_str).to_string() }, + 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 diff --git a/src/core/path_view/tree.rs b/src/core/path_view/tree.rs index f611131..119dd25 100644 --- a/src/core/path_view/tree.rs +++ b/src/core/path_view/tree.rs @@ -64,7 +64,9 @@ impl PathTree { 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)) + .map(|c| { + build_node(c, paths_map, base_path, sort_strategy, weight_cfg, no_emoji) + }) .collect(); FileNode::sort_slice(&mut child_nodes, sort_strategy); @@ -101,7 +103,16 @@ impl PathTree { 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)) + .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); @@ -117,7 +128,11 @@ impl PathTree { name: r_name.to_string(), path: PathBuf::from(r_name), is_dir: true, - icon: if no_emoji { String::new() } else { DIR_ICON.to_string() }, + icon: if no_emoji { + String::new() + } else { + DIR_ICON.to_string() + }, weight_str: empty_weight, weight_bytes: 0, children: top_nodes, diff --git a/src/core/save.rs b/src/core/save.rs index 98a9394..dfcdbd4 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -72,7 +72,14 @@ impl SaveFile { /// 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, + tree_text: &str, + paths: &[String], + base_dir: &str, + filepath: &str, + tag: &str, + add_by: bool, + i18n: &I18n, + cmd: &str, ) { let by_section = if add_by { Self::generate_by_section(tag, "codes", i18n, cmd) diff --git a/src/interfaces.rs b/src/interfaces.rs index 5bc0411..171421d 100644 --- a/src/interfaces.rs +++ b/src/interfaces.rs @@ -2,5 +2,5 @@ // [POL]: Warstwa interakcji z użytkownikiem (Porty i Adaptery). pub mod cli; +pub mod gui; pub mod tui; -pub mod gui; \ No newline at end of file diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index c923119..2ae6b59 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -193,9 +193,15 @@ impl CliArgs { cmd.push(format!("\"{}\"", self.patterns.join(","))); } - if self.include { cmd.push("-m".to_string()); } - if self.exclude { cmd.push("-x".to_string()); } - if self.ignore_case { cmd.push("-c".to_string()); } + if self.include { + cmd.push("-m".to_string()); + } + if self.exclude { + 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 { @@ -225,19 +231,33 @@ impl CliArgs { cmd.push(view_str.to_string()); } - if self.save_address { cmd.push("--save-address".to_string()); } - if self.save_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.save_address { + cmd.push("--save-address".to_string()); + } + if self.save_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 { @@ -248,4 +268,4 @@ impl CliArgs { cmd.join(" ") } -} \ No newline at end of file +} diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index e3933c8..cebb43c 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -21,9 +21,9 @@ pub fn run(args: CliArgs) { // [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, + (true, false) => ShowMode::Include, + (false, true) => ShowMode::Exclude, + _ => ShowMode::Context, }; // [ENG]: 🚀 Executes the core matching logic. @@ -39,7 +39,7 @@ pub fn run(args: CliArgs) { args.info, args.no_emoji, &i18n, - |_| {}, + |_| {}, |_| {}, ); @@ -52,7 +52,7 @@ pub fn run(args: CliArgs) { // [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); @@ -62,13 +62,15 @@ pub fn run(args: CliArgs) { // [POL]: 📂 Rozwiązuje ścieżkę katalogu wyjściowego. let resolve_dir = |val: &Option| -> String { match val { - Some(v) if v == "AUTO" => "./other/".to_string(), + Some(v) if v == "AUTO" => "./other/".to_string(), Some(v) => { let mut p = v.replace('\\', "/"); - if !p.ends_with('/') { p.push('/'); } + if !p.ends_with('/') { + p.push('/'); + } p } - None => "./".to_string(), + None => "./".to_string(), } }; @@ -79,11 +81,25 @@ pub fn run(args: CliArgs) { if args.save_address { if args.include || (!args.include && !args.exclude) { let filepath = format!("{}plot-address_{}_M.md", output_dir, tag); - SaveFile::paths(&output_str_txt_m, &filepath, &tag, args.by, &i18n, &cmd_string); + SaveFile::paths( + &output_str_txt_m, + &filepath, + &tag, + args.by, + &i18n, + &cmd_string, + ); } if args.exclude || (!args.include && !args.exclude) { let filepath = format!("{}plot-address_{}_X.md", output_dir, tag); - SaveFile::paths(&output_str_txt_x, &filepath, &tag, args.by, &i18n, &cmd_string); + SaveFile::paths( + &output_str_txt_x, + &filepath, + &tag, + args.by, + &i18n, + &cmd_string, + ); } } @@ -94,27 +110,27 @@ pub fn run(args: CliArgs) { if args.include || (!args.include && !args.exclude) { let filepath = format!("{}plot-archive_{}_M.md", output_dir, tag); SaveFile::codes( - &output_str_txt_m, - &stats.m_matched.paths, - &ctx.entry_absolute, - &filepath, - &tag, - args.by, - &i18n, - &cmd_string + &output_str_txt_m, + &stats.m_matched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_string, ); } if args.exclude || (!args.include && !args.exclude) { let filepath = format!("{}plot-archive_{}_X.md", output_dir, tag); SaveFile::codes( - &output_str_txt_x, - &stats.x_mismatched.paths, - &ctx.entry_absolute, - &filepath, - &tag, - args.by, - &i18n, - &cmd_string + &output_str_txt_x, + &stats.x_mismatched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_string, ); } } @@ -136,4 +152,4 @@ pub fn run(args: CliArgs) { } else { println!("---------------------------------------"); } -} \ No newline at end of file +} diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs index 5051c06..f811f2d 100644 --- a/src/interfaces/gui.rs +++ b/src/interfaces/gui.rs @@ -1,19 +1,30 @@ -pub mod settings; -pub mod paths; pub mod code; +pub mod i18n; +pub mod paths; +pub mod settings; -use eframe::egui; use crate::interfaces::cli::args::CliArgs; +use eframe::egui; #[derive(PartialEq)] -pub enum Tab { Settings, Paths, Code } +pub enum Tab { + Settings, + Paths, + Code, +} #[derive(PartialEq)] -pub enum PathsTab { Match, Mismatch } +pub enum PathsTab { + Match, + Mismatch, +} // ⚡ Dodana zakładka dla karty "Kod" #[derive(PartialEq)] -pub enum CodeTab { Match, Mismatch } +pub enum CodeTab { + Match, + Mismatch, +} #[derive(Default, Clone)] pub struct TreeStats { @@ -29,15 +40,15 @@ pub struct CargoPlotApp { pub args: CliArgs, pub active_tab: Tab, pub active_paths_tab: PathsTab, - pub active_code_tab: CodeTab, // ⚡ Dodane pole: aktywna zakładka Kodu + pub active_code_tab: CodeTab, // ⚡ Dodane pole: aktywna zakładka Kodu 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, // ⚡ Dodane pole: kod MATCH - pub generated_code_x: String, // ⚡ Dodane pole: kod MISMATCH - pub stats_m: TreeStats, - pub stats_x: TreeStats, + pub generated_paths_m: String, + pub generated_paths_x: String, + pub generated_code_m: String, // ⚡ Dodane pole: kod MATCH + pub generated_code_x: String, // ⚡ Dodane pole: kod MISMATCH + pub stats_m: TreeStats, + pub stats_x: TreeStats, pub ui_scale: f32, } @@ -48,13 +59,13 @@ impl CargoPlotApp { args, active_tab: Tab::Settings, active_paths_tab: PathsTab::Match, - active_code_tab: CodeTab::Match, // ⚡ Domyślnie ładujemy zakładkę 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_m: String::new(), generated_paths_x: String::new(), - generated_code_m: String::new(), // ⚡ Pusty na start - generated_code_x: String::new(), // ⚡ Pusty na start + 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, @@ -75,34 +86,47 @@ impl eframe::App for CargoPlotApp { 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() { + + 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() { + + 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; - } + + 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()); + 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), - } + 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), }); } } @@ -114,5 +138,10 @@ pub fn run_gui(args: CliArgs) { .with_title("cargo-plot"), ..Default::default() }; - eframe::run_native("cargo-plot", options, Box::new(|_cc| Ok(Box::new(CargoPlotApp::new(args))))).unwrap(); -} \ No newline at end of file + 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 index b4b1030..bab4ef9 100644 --- a/src/interfaces/gui/code.rs +++ b/src/interfaces/gui/code.rs @@ -1,60 +1,89 @@ -use eframe::egui; +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; use crate::interfaces::gui::{CargoPlotApp, CodeTab}; -use cargo_plot::i18n::Lang; -use cargo_plot::execute; -use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::addon::TimeTag; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::execute; +use eframe::egui; +// [ENG]: View function for the Code tab, managing source code extraction and preview. +// [POL]: Funkcja widoku dla karty Kod, zarządzająca ekstrakcją i podglądem kodu źródłowego. pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { - let is_pl = app.args.lang.unwrap_or(Lang::En) == Lang::Pl; + // [ENG]: Initialize translation engine. + // [POL]: Inicjalizacja silnika tłumaczeń. + let gt = GuiI18n::new(app.args.lang); - // 1. BELKA GÓRNA + // [ENG]: 1. TOP BAR - Code generation and archival save controls. + // [POL]: 1. GÓRNA BELKA - Kontrolki generowania kodu i zapisu archiwalnego. ui.horizontal(|ui| { - if ui.button(if is_pl { "🔄 Generuj kod (Cache)" } else { "🔄 Generate code" }).clicked() { + if ui.button(gt.t(GT::BtnGenerateCode)).clicked() { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - - // Wymuszamy pełny skan (Context), żeby mieć oba wyniki od razu + + // [ENG]: Execute scan in Context mode to populate both match and mismatch buffers. + // [POL]: Wykonaj skanowanie w trybie Context, aby wypełnić bufory dopasowań i odrzuceń. let stats = execute::execute( &app.args.enter_path, &app.args.patterns, !app.args.ignore_case, app.args.sort.into(), - ShowMode::Context, + ShowMode::Context, app.args.view.into(), app.args.no_root, - false, + false, true, &i18n, - |_| {}, |_| {}, + |_| {}, + |_| {}, ); - + let base_dir = std::path::Path::new(&app.args.enter_path); - // --- BUDOWA BUFORA MATCH (-m) --- + // --- [ENG]: BUILD MATCH BUFFER (-m) --- + // --- [POL]: BUDOWA BUFORA DOPASOWAŃ (-m) --- 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; } + 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> *(Pominięto binarkę)*\n\n", counter_m, p_str)), + 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; - // --- BUDOWA BUFORA MISMATCH (-x) --- + // --- [ENG]: BUILD MISMATCH BUFFER (-x) --- + // --- [POL]: BUDOWA BUFORA ODRZUCEŃ (-x) --- 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; } + 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> *(Pominięto binarkę)*\n\n", counter_x, p_str)), + 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; } @@ -62,47 +91,71 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { } ui.add_space(15.0); - ui.checkbox(&mut app.args.by, if is_pl { "Dodaj stopkę (--by)" } else { "Add footer (--by)" }); + ui.checkbox(&mut app.args.by, gt.t(GT::LabelAddFooter)); ui.add_space(15.0); - // ⚡ Helper rozwiązujący folder zapisu ze zmiennej out_code + // [ENG]: Helper to resolve output directory from app arguments. + // [POL]: Pomocnik do wyznaczania folderu zapisu z argumentów aplikacji. let resolve_dir = |val: &Option| -> String { match val { Some(v) if v == "AUTO" => "./other/".to_string(), Some(v) => { let mut p = v.replace('\\', "/"); - if !p.ends_with('/') { p.push('/'); } + if !p.ends_with('/') { + p.push('/'); + } p } None => "./".to_string(), } }; - // ⚡ ZAPIS DLA -m - if ui.button(if is_pl { "💾 Zapisz (-m)" } else { "💾 Save (-m)" }).clicked() { + // [ENG]: Save archival code for MATCH results (-m). + // [POL]: Zapis archiwalnego kodu dla wyników MATCH (-m). + 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), tag); + let filepath = format!( + "{}plot-archive_{}_M.md", + resolve_dir(&app.args.dir_out), + tag + ); let mut final_text = app.generated_code_m.clone(); - - if app.args.by { + + if app.args.by { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); let cmd_string = app.args.to_command_string(); - final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section(&tag, "codes", &i18n, &cmd_string)); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( + &tag, + "codes", + &i18n, + &cmd_string, + )); } let _ = std::fs::write(&filepath, final_text); } ui.add_space(5.0); - if ui.button(if is_pl { "💾 Zapisz (-x)" } else { "💾 Save (-x)" }).clicked() { + // [ENG]: Save archival code for MISMATCH results (-x). + // [POL]: Zapis archiwalnego kodu dla wyników MISMATCH (-x). + 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), tag); + let filepath = format!( + "{}plot-archive_{}_X.md", + resolve_dir(&app.args.dir_out), + tag + ); let mut final_text = app.generated_code_x.clone(); - - if app.args.by { + + if app.args.by { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); let cmd_string = app.args.to_command_string(); - final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section(&tag, "codes", &i18n, &cmd_string)); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( + &tag, + "codes", + &i18n, + &cmd_string, + )); } let _ = std::fs::write(&filepath, final_text); } @@ -110,16 +163,24 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.separator(); - // 2. DOLNA BELKA (ZAKŁADKI) + // [ENG]: 2. BOTTOM BAR - Sub-tabs for switching between Match and Mismatch code views. + // [POL]: 2. DOLNA BELKA - Zakładki do przełączania między widokiem kodu Match i Mismatch. egui::TopBottomPanel::bottom("code_subtabs").show_inside(ui, |ui| { ui.add_space(8.0); ui.horizontal(|ui| { if app.active_code_tab == CodeTab::Match { - ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); + ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); } - - let m_text = egui::RichText::new("✔ (-m) MATCH").size(18.0).strong().color(egui::Color32::from_rgb(138, 90, 255)); - let x_text = egui::RichText::new("✖ (-x) MISMATCH").size(18.0).strong().color(egui::Color32::from_rgb(255, 80, 100)); + + let m_text = egui::RichText::new(gt.t(GT::TabMatch)) + .size(18.0) + .strong() + .color(egui::Color32::from_rgb(138, 90, 255)); + + let x_text = egui::RichText::new(gt.t(GT::TabMismatch)) + .size(18.0) + .strong() + .color(egui::Color32::from_rgb(255, 80, 100)); ui.selectable_value(&mut app.active_code_tab, CodeTab::Match, m_text); ui.add_space(20.0); @@ -128,7 +189,8 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add_space(8.0); }); - // 3. POLE TEKSTOWE / NOTATNIK + // [ENG]: 3. MAIN CONTENT AREA - Scrollable editor showing extracted file contents. + // [POL]: 3. GŁÓWNY OBSZAR TREŚCI - Przewijalny edytor pokazujący wyekstrahowaną zawartość plików. egui::CentralPanel::default().show_inside(ui, |ui| { egui::ScrollArea::both().show(ui, |ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); @@ -141,8 +203,8 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add( egui::TextEdit::multiline(text_buffer) .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY) + .desired_width(f32::INFINITY), ); }); }); -} \ No newline at end of file +} diff --git a/src/interfaces/gui/i18n.rs b/src/interfaces/gui/i18n.rs index 71948d1..b9a83bf 100644 --- a/src/interfaces/gui/i18n.rs +++ b/src/interfaces/gui/i18n.rs @@ -21,15 +21,24 @@ pub enum GuiText { BtnClearAll, BtnBrowse, MsgNoPatterns, - FooterVersion, 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) } + Self { + lang: lang.unwrap_or(Lang::En), + } } pub fn t(&self, text: GuiText) -> &'static str { @@ -48,10 +57,17 @@ impl GuiI18n { GuiText::BtnClearAll => "💣 Usuń wszystkie", GuiText::BtnBrowse => "Wybierz...", GuiText::MsgNoPatterns => "Brak wzorców. Dodaj przynajmniej jeden!", - GuiText::FooterVersion => "Wersja raportu:", 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:", @@ -67,11 +83,18 @@ impl GuiI18n { GuiText::BtnClearAll => "💣 Clear all", GuiText::BtnBrowse => "Browse...", GuiText::MsgNoPatterns => "No patterns. Add at least one!", - GuiText::FooterVersion => "Report version:", 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)*", }, } } -} \ No newline at end of file +} diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs index d59fe6b..0d531d1 100644 --- a/src/interfaces/gui/paths.rs +++ b/src/interfaces/gui/paths.rs @@ -1,89 +1,127 @@ -use eframe::egui; +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; use crate::interfaces::gui::{CargoPlotApp, PathsTab}; -use cargo_plot::i18n::Lang; -use cargo_plot::execute; -use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::addon::TimeTag; +use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::execute; +use eframe::egui; pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { - let is_pl = app.args.lang.unwrap_or(Lang::En) == Lang::Pl; + // [ENG]: Initialize translation engine. + // [POL]: Inicjalizacja silnika tłumaczeń. + let gt = GuiI18n::new(app.args.lang); - // 1. GÓRNA BELKA (Generowanie i Zapis) + // [ENG]: 1. TOP BAR - Generation and Saving controls. + // [POL]: 1. GÓRNA BELKA - Kontrolki generowania i zapisu. ui.horizontal(|ui| { - if ui.button(if is_pl { "🔄 Generuj / Regeneruj" } else { "🔄 Generate" }).clicked() { + if ui.button(gt.t(GT::BtnGenerate)).clicked() { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - + let stats = execute::execute( &app.args.enter_path, &app.args.patterns, !app.args.ignore_case, app.args.sort.into(), - ShowMode::Context, + ShowMode::Context, app.args.view.into(), app.args.no_root, - false, + false, true, &i18n, - |_| {}, |_| {}, + |_| {}, + |_| {}, ); - - app.generated_paths_m = stats.render_output(app.args.view.into(), ShowMode::Include, false, false); - app.generated_paths_x = stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); + + app.generated_paths_m = + stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + app.generated_paths_x = + stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); } ui.add_space(15.0); - ui.checkbox(&mut app.args.by, if is_pl { "Dodaj stopkę (--by)" } else { "Add footer (--by)" }); + ui.checkbox(&mut app.args.by, gt.t(GT::LabelAddFooter)); ui.add_space(15.0); - // ⚡ Helper rozwiązujący folder zapisu ze zmiennej out_path + // [ENG]: Helper to resolve output directory from app arguments. + // [POL]: Pomocnik do wyznaczania folderu zapisu z argumentów aplikacji. let resolve_dir = |val: &Option| -> String { match val { - Some(v) if v == "AUTO" => "./other/".to_string(), // Jeśli AUTO, wrzuć do ./other/ + Some(v) if v == "AUTO" => "./other/".to_string(), Some(v) => { let mut p = v.replace('\\', "/"); - if !p.ends_with('/') { p.push('/'); } + if !p.ends_with('/') { + p.push('/'); + } p } - None => "./".to_string(), // Domyślnie główny folder projektu + None => "./".to_string(), } }; - // ⚡ ZAPIS DLA -m - if ui.button(if is_pl { "💾 Zapisz (-m)" } else { "💾 Save (-m)" }).clicked() { + // [ENG]: Save MATCH results (-m). + // [POL]: Zapis wyników MATCH (-m). + 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), tag); + let filepath = format!( + "{}plot-address_{}_M.md", + resolve_dir(&app.args.dir_out), + tag + ); let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - let cmd_string = app.args.to_command_string(); // ⚡ - cargo_plot::core::save::SaveFile::paths(&app.generated_paths_m, &filepath, &tag, app.args.by, &i18n, &cmd_string); + let cmd_string = app.args.to_command_string(); + cargo_plot::core::save::SaveFile::paths( + &app.generated_paths_m, + &filepath, + &tag, + app.args.by, + &i18n, + &cmd_string, + ); } ui.add_space(5.0); - if ui.button(if is_pl { "💾 Zapisz (-x)" } else { "💾 Save (-x)" }).clicked() { + // [ENG]: Save MISMATCH results (-x). + // [POL]: Zapis wyników MISMATCH (-x). + 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), tag); + let filepath = format!( + "{}plot-address_{}_X.md", + resolve_dir(&app.args.dir_out), + tag + ); let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - let cmd_string = app.args.to_command_string(); // ⚡ - cargo_plot::core::save::SaveFile::paths(&app.generated_paths_x, &filepath, &tag, app.args.by, &i18n, &cmd_string); + let cmd_string = app.args.to_command_string(); + cargo_plot::core::save::SaveFile::paths( + &app.generated_paths_x, + &filepath, + &tag, + app.args.by, + &i18n, + &cmd_string, + ); } }); ui.separator(); - // 2. DOLNA BELKA (ZAKŁADKI) + // [ENG]: 2. BOTTOM BAR - Sub-tabs for switching between Match and Mismatch views. + // [POL]: 2. DOLNA BELKA - Zakładki do przełączania między widokiem Match i Mismatch. egui::TopBottomPanel::bottom("paths_subtabs").show_inside(ui, |ui| { ui.add_space(8.0); ui.horizontal(|ui| { - if app.active_paths_tab == PathsTab::Match { - ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); + ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); } - - let m_text = egui::RichText::new("✔ (-m) MATCH") - .size(18.0).strong().color(egui::Color32::from_rgb(138, 90, 255)); - - let x_text = egui::RichText::new("✖ (-x) MISMATCH") - .size(18.0).strong().color(egui::Color32::from_rgb(255, 80, 100)); + + let m_text = egui::RichText::new(gt.t(GT::TabMatch)) + .size(18.0) + .strong() + .color(egui::Color32::from_rgb(138, 90, 255)); + + let x_text = egui::RichText::new(gt.t(GT::TabMismatch)) + .size(18.0) + .strong() + .color(egui::Color32::from_rgb(255, 80, 100)); ui.selectable_value(&mut app.active_paths_tab, PathsTab::Match, m_text); ui.add_space(20.0); @@ -92,11 +130,12 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add_space(8.0); }); - // 3. NOTATNIK + // [ENG]: 3. MAIN CONTENT AREA - Scrollable notepad showing generated path data. + // [POL]: 3. GŁÓWNY OBSZAR TREŚCI - Przewijalny notatnik z wygenerowanymi danymi ścieżek. egui::CentralPanel::default().show_inside(ui, |ui| { egui::ScrollArea::both().show(ui, |ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - + let text_buffer = match app.active_paths_tab { PathsTab::Match => &mut app.generated_paths_m, PathsTab::Mismatch => &mut app.generated_paths_x, @@ -105,8 +144,8 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add( egui::TextEdit::multiline(text_buffer) .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY) + .desired_width(f32::INFINITY), ); }); }); -} \ No newline at end of file +} diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index 930eee9..033ed83 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -1,56 +1,64 @@ -use eframe::egui; +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 crate::interfaces::cli::args::{CliSortStrategy, CliViewMode}; +use eframe::egui; pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { - // Sprawdzamy, czy wybrany jest język polski (dla dynamicznych tłumaczeń w locie) - let is_pl = app.args.lang.unwrap_or(Lang::En) == Lang::Pl; + // [ENG]: Initialize the GUI translation engine based on current settings. + // [POL]: Inicjalizacja silnika tłumaczeń GUI na podstawie aktualnych ustawień. + let gt = GuiI18n::new(app.args.lang); egui::ScrollArea::vertical().show(ui, |ui| { ui.add_space(10.0); - // 1. WYBÓR JĘZYKA (Teraz dynamicznie aktualizuje resztę interfejsu!) + // [ENG]: 1. LANGUAGE SELECTION - Dynamically updates the entire UI. + // [POL]: 1. WYBÓR JĘZYKA - Dynamicznie aktualizuje cały interfejs. ui.horizontal(|ui| { - ui.label(if is_pl { "🌍 Język:" } else { "🌍 Language:" }); + 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.separator(); ui.add_space(10.0); - // 2. WYBÓR FOLDERU (Z działającym przyciskiem okna systemowego) + // [ENG]: 2. SCAN FOLDER SELECTION - Uses native system dialog. + // [POL]: 2. WYBÓR FOLDERU SKANOWANIA - Używa natywnego okna systemowego. ui.horizontal(|ui| { - ui.label(if is_pl { "📂 Ścieżka skanowania:" } else { "📂 Scan path:" }); + ui.label(gt.t(GT::LabelScanPath)); ui.text_edit_singleline(&mut app.args.enter_path); - - // ⚡ NATYWNE OKNO WYBORU FOLDERU - if ui.button(if is_pl { "Wybierz..." } else { "Browse..." }).clicked() - && let Some(folder) = rfd::FileDialog::new().pick_folder() { - // Aktualizujemy ścieżkę i ujednolicamy ukośniki - app.args.enter_path = folder.to_string_lossy().replace('\\', "/"); - } + + 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_space(10.0); ui.separator(); - - // ⚡ NOWOŚĆ: ŚCIEŻKA WYNIKOWA (Output path) + // [ENG]: 3. OUTPUT FOLDER SELECTION - Common path for paths and archive saves. + // [POL]: 3. WYBÓR FOLDERU WYNIKOWEGO - Wspólna ścieżka dla zapisu ścieżek i archiwum. ui.add_space(10.0); ui.horizontal(|ui| { - ui.label(if is_pl { "💾 Folder zapisu (Output):" } else { "💾 Output folder:" }); - - // Używamy `out_path_input` z gui.rs jako wspólnego bufora tekstowego + ui.label(gt.t(GT::LabelOutFolder)); + if ui.text_edit_singleline(&mut app.out_path_input).changed() { let trimmed = app.out_path_input.trim(); - app.args.dir_out = if trimmed.is_empty() { None } else { Some(trimmed.to_string()) }; + app.args.dir_out = if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + }; } - - if ui.button(if is_pl { "Wybierz folder..." } else { "Browse folder..." }).clicked() { + + if ui.button(gt.t(GT::BtnBrowse)).clicked() { if let Some(folder) = rfd::FileDialog::new().pick_folder() { let mut path = folder.to_string_lossy().replace('\\', "/"); - if !path.ends_with('/') { path.push('/'); } - + if !path.ends_with('/') { + path.push('/'); + } + app.out_path_input = path.clone(); app.args.dir_out = Some(path); } @@ -59,16 +67,32 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add_space(10.0); ui.separator(); - // 5. WIDOK I SORTOWANIE + // [ENG]: 4. VIEW AND SORTING - Controls the structure of the generated report. + // [POL]: 4. WIDOK I SORTOWANIE - Kontroluje strukturę generowanego raportu. ui.horizontal(|ui| { - egui::ComboBox::from_label(if is_pl { "Sortowanie" } else { "Sorting" }) + egui::ComboBox::from_label(gt.t(GT::LabelSorting)) .selected_text(format!("{:?}", app.args.sort)) .show_ui(ui, |ui| { - // ⚡ PEŁNA LISTA SORTOWAŃ - 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::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"); @@ -80,7 +104,7 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add_space(15.0); - egui::ComboBox::from_label(if is_pl { "Tryb widoku" } else { "View mode" }) + 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"); @@ -89,39 +113,34 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { }); ui.add_space(15.0); - - ui.checkbox(&mut app.args.no_root, if is_pl { "Ukryj ROOT w drzewie" } else { "Hide ROOT in tree" }); + ui.checkbox(&mut app.args.no_root, gt.t(GT::LabelNoRoot)); }); - + ui.add_space(20.0); - // 3. WZORCE DOPASOWAŃ (Z połączonymi opcjami z linii "X") - ui.heading(if is_pl { "🔍 Wzorce dopasowań (Patterns)" } else { "🔍 Match Patterns" }); - - // Trzy opcje logiczne przytulone do wzorców - //ui.horizontal(|ui| { - - //ui.checkbox(&mut app.args.include, if is_pl { "✅ Pokaż dopasowane (m)" } else { "✅ Show matched (m)" }); - //ui.checkbox(&mut app.args.exclude, if is_pl { "❌ Pokaż odrzucone (x)" } else { "❌ Show rejected (x)" }); - //}); + + // [ENG]: 5. MATCH PATTERNS - Pattern management with real-time list interaction. + // [POL]: 5. WZORCE DOPASOWAŃ - Zarządzanie wzorcami z interaktywną listą. + ui.heading(gt.t(GT::HeadingPatterns)); ui.add_space(5.0); - // Pole dodawania nowego wzorca ui.horizontal(|ui| { - ui.checkbox(&mut app.args.ignore_case, if is_pl { "🔠 Ignoruj wielkość liter" } else { "🔠 Ignore case" }); - ui.label(if is_pl { "Nowy:" } else { "New:" }); + ui.checkbox(&mut app.args.ignore_case, gt.t(GT::LabelIgnoreCase)); + ui.label(gt.t(GT::LabelNewPattern)); let response = ui.text_edit_singleline(&mut app.new_pattern_input); - let btn_clicked = ui.button(if is_pl { "➕ Dodaj wzorzec" } else { "➕ Add pattern" }).clicked(); - - if (btn_clicked || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))) - && !app.new_pattern_input.trim().is_empty() + let btn_clicked = ui.button(gt.t(GT::BtnAddPattern)).clicked(); + + if (btn_clicked + || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))) + && !app.new_pattern_input.trim().is_empty() { - app.args.patterns.push(app.new_pattern_input.trim().to_string()); + app.args + .patterns + .push(app.new_pattern_input.trim().to_string()); app.new_pattern_input.clear(); response.request_focus(); } }); - // 4. LISTA DODANYCH WZORCÓW ui.add_space(5.0); egui::Frame::group(ui.style()).show(ui, |ui| { ui.set_min_height(100.0); @@ -132,35 +151,52 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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); } + 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 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(if is_pl { "💣 Usuń wszystkie" } else { "💣 Clear all" }).clicked() { + if ui.button(gt.t(GT::BtnClearAll)).clicked() { app.args.patterns.clear(); } } else { - let empty_msg = if is_pl { "Brak wzorców. Dodaj przynajmniej jeden!" } else { "No patterns. Add at least one!" }; - ui.label(egui::RichText::new(empty_msg).italics().weak()); + ui.label( + egui::RichText::new(gt.t(GT::MsgNoPatterns)) + .italics() + .weak(), + ); } }); ui.separator(); - - - ui.add_space(50.0); - - // 6. STOPKA + + // [ENG]: 6. FOOTER - Versioning, links and installation instructions. + // [POL]: 6. STOPKA - Wersjonowanie, linki i instrukcje instalacji. ui.separator(); ui.add_space(10.0); ui.horizontal(|ui| { @@ -168,16 +204,19 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.separator(); ui.hyperlink_to("Crates.io", "https://crates.io/crates/cargo-plot"); ui.separator(); - ui.hyperlink_to(if is_pl { "Pobierz binarkę (GitHub)" } else { "Download binary (GitHub)" }, "https://github.com/j-Cis/cargo-plot/releases"); + 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(if is_pl { "Instalacja:" } else { "Install:" }).weak()); + ui.label(egui::RichText::new(gt.t(GT::FooterInstall)).weak()); ui.code("cargo install cargo-plot"); ui.separator(); - ui.label(egui::RichText::new(if is_pl { "Usuwanie:" } else { "Uninstall:" }).weak()); + ui.label(egui::RichText::new(gt.t(GT::FooterUninstall)).weak()); ui.code("cargo uninstall cargo-plot"); }); ui.add_space(10.0); }); -} \ No newline at end of file +} diff --git a/src/interfaces/tui/i18n.rs b/src/interfaces/tui/i18n.rs index 5b41719..62b71b2 100644 --- a/src/interfaces/tui/i18n.rs +++ b/src/interfaces/tui/i18n.rs @@ -46,9 +46,13 @@ pub enum Prompt { BtnCliMode, InputCliCommand, SuccessCliParse, - BtnHelp, - HelpPause, - SubHelpHeader, HelpPatternsBtn, HelpFlagsBtn, HelpTextPatterns, HelpTextFlags, + BtnHelp, + HelpPause, + SubHelpHeader, + HelpPatternsBtn, + HelpFlagsBtn, + HelpTextPatterns, + HelpTextFlags, BtnGui, } @@ -165,21 +169,21 @@ impl Translatable for Prompt { pol: "Wczytano konfigurację!", eng: "Configuration loaded!", }, - Prompt::BtnHelp => Txt { - pol: "❓ Pomoc (Wzorce i Flagi)", - eng: "❓ Help (Patterns & Flags)" + Prompt::BtnHelp => Txt { + pol: "❓ Pomoc (Wzorce i Flagi)", + eng: "❓ Help (Patterns & Flags)", }, Prompt::SubHelpHeader => Txt { pol: "Wybierz temat pomocy:", - eng: "Choose help topic:" + eng: "Choose help topic:", }, Prompt::HelpPatternsBtn => Txt { pol: "Składnia Wzorców", - eng: "Patterns Syntax" + eng: "Patterns Syntax", }, Prompt::HelpFlagsBtn => Txt { pol: "Opis Flag i Opcji", - eng: "Flags & Options Description" + eng: "Flags & Options Description", }, Prompt::HelpTextPatterns => Txt { pol: "=== WZORCE DOPASOWAŃ === @@ -194,7 +198,7 @@ $ - 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) @@ -206,7 +210,7 @@ $ - 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 (+)" +!@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) === @@ -222,7 +226,7 @@ $ - Orphan: matches ONLY when file/dir pair is missing -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) @@ -235,11 +239,11 @@ $ - Orphan: matches ONLY when file/dir pair is missing -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" +--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::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)", diff --git a/src/interfaces/tui/menu.rs b/src/interfaces/tui/menu.rs index e15e20f..6508bde 100644 --- a/src/interfaces/tui/menu.rs +++ b/src/interfaces/tui/menu.rs @@ -25,7 +25,6 @@ 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); @@ -52,7 +51,11 @@ pub fn menu_main(s: &mut StateTui) { 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 + t.fmt(Prompt::BtnOutput), + out_p, + s.args.save_address, + s.args.save_archive, + s.args.by ); let lbl_filt = format!( @@ -64,7 +67,9 @@ pub fn menu_main(s: &mut StateTui) { ); // ⚡ BUDOWA MENU - let links_hint = style("crates.io/crates/cargo-plot | github.com/j-Cis/cargo-plot").dim().to_string(); + 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), "") @@ -162,9 +167,10 @@ pub fn menu_main(s: &mut StateTui) { .item(0, t.raw(Prompt::BtnExit), "") .interact() .unwrap_or(0); - + if help_choice == 1 { - cliclack::note("📖 WZORCE / PATTERNS", t.raw(Prompt::HelpTextPatterns)).unwrap(); + 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() @@ -176,7 +182,7 @@ pub fn menu_main(s: &mut StateTui) { .interact() .unwrap_or_default(); } - }, + } Ok(Action::Run) => { if s.args.patterns.is_empty() { cliclack::log::warning(t.raw(Prompt::WarnNoPatterns)).unwrap(); @@ -189,12 +195,12 @@ pub fn menu_main(s: &mut StateTui) { 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; + return; } Ok(Action::Exit) | Err(_) => { cliclack::outro(t.raw(Prompt::ExitBye)).unwrap(); @@ -202,7 +208,6 @@ pub fn menu_main(s: &mut StateTui) { } } cliclack::clear_screen().unwrap(); - } } @@ -267,7 +272,11 @@ fn handle_output(s: &mut StateTui, t: &T) { .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.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) diff --git a/src/interfaces/tui/state.rs b/src/interfaces/tui/state.rs index e16df26..be79254 100644 --- a/src/interfaces/tui/state.rs +++ b/src/interfaces/tui/state.rs @@ -17,7 +17,7 @@ impl StateTui { patterns: vec![], sort: CliSortStrategy::AzFileMerge, view: CliViewMode::Tree, - include: true, + include: true, exclude: false, dir_out: None, save_address: false, From 3ccaabada180ca22c343d982ccff649d5c024a0f Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sat, 21 Mar 2026 01:27:49 +0100 Subject: [PATCH 39/45] (update: gui) --- .gitignore | 3 +- src/i18n.rs | 2 + src/interfaces/cli.rs | 28 ++- src/interfaces/cli/args.rs | 62 ++--- src/interfaces/cli/engine.rs | 72 ++---- src/interfaces/gui.rs | 1 + src/interfaces/gui/code.rs | 239 ++++++------------- src/interfaces/gui/paths.rs | 172 ++++---------- src/interfaces/gui/settings.rs | 412 +++++++++++++++++---------------- src/interfaces/gui/shared.rs | 86 +++++++ src/main.rs | 27 +-- 11 files changed, 488 insertions(+), 616 deletions(-) create mode 100644 src/interfaces/gui/shared.rs diff --git a/.gitignore b/.gitignore index e185046..3e37f54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /target/ *.exe *.lock -/new/ -/other/ \ No newline at end of file +/.cargo-plot/ \ No newline at end of file diff --git a/src/i18n.rs b/src/i18n.rs index 242df3a..c20ae9f 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -157,6 +157,8 @@ impl I18n { } } + + // ===================================================================== // 4. TUI - INTERAKTYWNY PANEL // ===================================================================== diff --git a/src/interfaces/cli.rs b/src/interfaces/cli.rs index bece8ee..c8003cf 100644 --- a/src/interfaces/cli.rs +++ b/src/interfaces/cli.rs @@ -4,32 +4,36 @@ pub mod engine; use self::args::CargoCli; use clap::Parser; -// [ENG]: Main entry point for the CLI interface. -// [POL]: Główny punkt wejścia dla interfejsu CLI. +// [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() { - // [POL]: Pobieramy surowe argumenty bezpośrednio z systemu. 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. - // We insert it manually so the parser matches the Cargo plugin structure. // [POL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. - // Wstawiamy go ręcznie, aby parser pasował do struktury wtyczki Cargo. if args.len() > 1 && args[1] != "plot" { args.insert(1, "plot".to_string()); } - // [ENG]: Now parse from the modified list. - // [POL]: Teraz parsujemy ze zmodyfikowanej listy. + // [ENG]: Parse from the modified list. + // [POL]: Parsowanie ze zmodyfikowanej listy. let CargoCli::Plot(flags) = CargoCli::parse_from(args); - // [ENG]: Transfer control to our execution engine. - // [POL]: Przekazanie kontroli do naszego silnika wykonawczego. + // [ENG]: Transfer control based on parsed flags. + // [POL]: Przekazanie kontroli na podstawie sparsowanych flag. if flags.gui { - // Jeśli podano -g, od razu ładujemy okienko ze sparsowaną konfiguracją crate::interfaces::gui::run_gui(flags); + } else if flags.tui { + crate::interfaces::tui::run_tui(); } else { - // Jeśli nie, uruchamiamy standardowy silnik generujący raport w terminalu engine::run(flags); } -} +} \ No newline at end of file diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index 2ae6b59..de4aa40 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -35,12 +35,12 @@ pub struct CliArgs { /// [ENG]: ✔️ Treat patterns as match (include) rules. /// [POL]: ✔️ Traktuj wzorce jako zasady dopasowania (włącz). - #[arg(short = 'm', long = "pat-match")] + #[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")] + #[arg(short = 'x', long = "pat-mismatch", required_unless_present_any = ["include", "gui", "tui"])] pub exclude: bool, /// [ENG]: 🔠 Ignore case sensitivity in patterns. @@ -173,7 +173,7 @@ impl From for ViewMode { impl CliArgs { /// [ENG]: Reconstructs a clean terminal command string. /// [POL]: Odtwarza czystą komendę terminalową. - pub fn to_command_string(&self) -> String { + 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() { @@ -185,23 +185,24 @@ impl CliArgs { 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() { - cmd.push("-p".to_string()); - cmd.push(format!("\"{}\"", self.patterns.join(","))); + for pattern in &self.patterns { + cmd.push("-p".to_string()); + cmd.push(format!("\"{}\"", pattern)); + } } - if self.include { - cmd.push("-m".to_string()); - } - if self.exclude { - cmd.push("-x".to_string()); - } - if self.ignore_case { - cmd.push("-c".to_string()); - } + // ⚡ 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 { @@ -231,33 +232,20 @@ impl CliArgs { cmd.push(view_str.to_string()); } - if self.save_address { - cmd.push("--save-address".to_string()); - } - if self.save_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()); - } - + // ⚡ 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 { diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index cebb43c..1ad8233 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -12,7 +12,6 @@ use cargo_plot::i18n::I18n; pub fn run(args: CliArgs) { // [ENG]: 📝 Reconstructs the command string for the footer. // [POL]: 📝 Odtwarza ciąg komendy dla stopki. - let cmd_string = args.to_command_string(); let i18n = I18n::new(args.lang); let is_case_sensitive = !args.ignore_case; let sort_strategy: SortStrategy = args.sort.into(); @@ -58,48 +57,35 @@ pub fn run(args: CliArgs) { 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. - // [POL]: 📂 Rozwiązuje ścieżkę katalogu wyjściowego. - let resolve_dir = |val: &Option| -> String { - match val { - Some(v) if v == "AUTO" => "./other/".to_string(), - Some(v) => { - let mut p = v.replace('\\', "/"); - if !p.ends_with('/') { - p.push('/'); - } - p - } - None => "./".to_string(), + // [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().map_or(true, |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); + 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.include && !args.exclude) { let filepath = format!("{}plot-address_{}_M.md", output_dir, tag); - SaveFile::paths( - &output_str_txt_m, - &filepath, - &tag, - args.by, - &i18n, - &cmd_string, - ); + let cmd_m = args.to_command_string(true, false, true, false); // ⚡ address = true + SaveFile::paths(&output_str_txt_m, &filepath, &tag, args.by, &i18n, &cmd_m); } if args.exclude || (!args.include && !args.exclude) { let filepath = format!("{}plot-address_{}_X.md", output_dir, tag); - SaveFile::paths( - &output_str_txt_x, - &filepath, - &tag, - args.by, - &i18n, - &cmd_string, - ); + let cmd_x = args.to_command_string(false, true, true, false); // ⚡ address = true + SaveFile::paths(&output_str_txt_x, &filepath, &tag, args.by, &i18n, &cmd_x); } } @@ -109,29 +95,13 @@ pub fn run(args: CliArgs) { if let Ok(ctx) = PathContext::resolve(&args.enter_path) { if args.include || (!args.include && !args.exclude) { let filepath = format!("{}plot-archive_{}_M.md", output_dir, tag); - SaveFile::codes( - &output_str_txt_m, - &stats.m_matched.paths, - &ctx.entry_absolute, - &filepath, - &tag, - args.by, - &i18n, - &cmd_string, - ); + let cmd_m = args.to_command_string(true, false, false, true); // ⚡ archive = true + SaveFile::codes(&output_str_txt_m, &stats.m_matched.paths, &ctx.entry_absolute, &filepath, &tag, args.by, &i18n, &cmd_m); } if args.exclude || (!args.include && !args.exclude) { let filepath = format!("{}plot-archive_{}_X.md", output_dir, tag); - SaveFile::codes( - &output_str_txt_x, - &stats.x_mismatched.paths, - &ctx.entry_absolute, - &filepath, - &tag, - args.by, - &i18n, - &cmd_string, - ); + let cmd_x = args.to_command_string(false, true, false, true); // ⚡ archive = true + SaveFile::codes(&output_str_txt_x, &stats.x_mismatched.paths, &ctx.entry_absolute, &filepath, &tag, args.by, &i18n, &cmd_x); } } } diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs index f811f2d..2c35619 100644 --- a/src/interfaces/gui.rs +++ b/src/interfaces/gui.rs @@ -2,6 +2,7 @@ pub mod code; pub mod i18n; pub mod paths; pub mod settings; +pub mod shared; use crate::interfaces::cli::args::CliArgs; use eframe::egui; diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs index bab4ef9..1e736d9 100644 --- a/src/interfaces/gui/code.rs +++ b/src/interfaces/gui/code.rs @@ -1,210 +1,103 @@ use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; use crate::interfaces::gui::{CargoPlotApp, CodeTab}; +use crate::interfaces::gui::shared::{resolve_dir, draw_tabs, draw_footer, draw_editor}; use cargo_plot::addon::TimeTag; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::execute; use eframe::egui; -// [ENG]: View function for the Code tab, managing source code extraction and preview. -// [POL]: Funkcja widoku dla karty Kod, zarządzająca ekstrakcją i podglądem kodu źródłowego. pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { - // [ENG]: Initialize translation engine. - // [POL]: Inicjalizacja silnika tłumaczeń. let gt = GuiI18n::new(app.args.lang); - // [ENG]: 1. TOP BAR - Code generation and archival save controls. - // [POL]: 1. GÓRNA BELKA - Kontrolki generowania kodu i zapisu archiwalnego. + 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(); + ui.horizontal(|ui| { if ui.button(gt.t(GT::BtnGenerateCode)).clicked() { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + + // ⚡ OPTYMALIZACJA: Generowanie skanowania i odczytów plików tylko dla żądanej sekcji. + let show_mode = if is_match { ShowMode::Include } else { ShowMode::Exclude }; - // [ENG]: Execute scan in Context mode to populate both match and mismatch buffers. - // [POL]: Wykonaj skanowanie w trybie Context, aby wypełnić bufory dopasowań i odrzuceń. let stats = execute::execute( - &app.args.enter_path, - &app.args.patterns, - !app.args.ignore_case, - app.args.sort.into(), - ShowMode::Context, - app.args.view.into(), - app.args.no_root, - false, - true, - &i18n, - |_| {}, - |_| {}, + &app.args.enter_path, &app.args.patterns, !app.args.ignore_case, app.args.sort.into(), + show_mode, app.args.view.into(), app.args.no_root, false, true, &i18n, |_| {}, |_| {}, ); let base_dir = std::path::Path::new(&app.args.enter_path); - // --- [ENG]: BUILD MATCH BUFFER (-m) --- - // --- [POL]: BUDOWA BUFORA DOPASOWAŃ (-m) --- - 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; - - // --- [ENG]: BUILD MISMATCH BUFFER (-x) --- - // --- [POL]: BUDOWA BUFORA ODRZUCEŃ (-x) --- - 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; + 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; } - 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) - )), + 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; } - counter_x += 1; + app.generated_code_x = content_x; } - 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]: Helper to resolve output directory from app arguments. - // [POL]: Pomocnik do wyznaczania folderu zapisu z argumentów aplikacji. - let resolve_dir = |val: &Option| -> String { - match val { - Some(v) if v == "AUTO" => "./other/".to_string(), - Some(v) => { - let mut p = v.replace('\\', "/"); - if !p.ends_with('/') { - p.push('/'); - } - p + 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, "codes", &i18n, &cmd_string)); } - None => "./".to_string(), + let _ = std::fs::write(&filepath, final_text); } - }; - - // [ENG]: Save archival code for MATCH results (-m). - // [POL]: Zapis archiwalnego kodu dla wyników MATCH (-m). - 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), - 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(); - final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( - &tag, - "codes", - &i18n, - &cmd_string, - )); - } - let _ = std::fs::write(&filepath, final_text); - } - - ui.add_space(5.0); - - // [ENG]: Save archival code for MISMATCH results (-x). - // [POL]: Zapis archiwalnego kodu dla wyników MISMATCH (-x). - 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), - 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(); - final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( - &tag, - "codes", - &i18n, - &cmd_string, - )); + } 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, "codes", &i18n, &cmd_string)); + } + let _ = std::fs::write(&filepath, final_text); } - let _ = std::fs::write(&filepath, final_text); } }); ui.separator(); - // [ENG]: 2. BOTTOM BAR - Sub-tabs for switching between Match and Mismatch code views. - // [POL]: 2. DOLNA BELKA - Zakładki do przełączania między widokiem kodu Match i Mismatch. - egui::TopBottomPanel::bottom("code_subtabs").show_inside(ui, |ui| { - ui.add_space(8.0); - ui.horizontal(|ui| { - if app.active_code_tab == CodeTab::Match { - ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); - } - - let m_text = egui::RichText::new(gt.t(GT::TabMatch)) - .size(18.0) - .strong() - .color(egui::Color32::from_rgb(138, 90, 255)); + draw_footer(ui, "code_stats_footer"); - let x_text = egui::RichText::new(gt.t(GT::TabMismatch)) - .size(18.0) - .strong() - .color(egui::Color32::from_rgb(255, 80, 100)); - - ui.selectable_value(&mut app.active_code_tab, CodeTab::Match, m_text); - ui.add_space(20.0); - ui.selectable_value(&mut app.active_code_tab, CodeTab::Mismatch, x_text); - }); - ui.add_space(8.0); - }); - - // [ENG]: 3. MAIN CONTENT AREA - Scrollable editor showing extracted file contents. - // [POL]: 3. GŁÓWNY OBSZAR TREŚCI - Przewijalny edytor pokazujący wyekstrahowaną zawartość plików. - egui::CentralPanel::default().show_inside(ui, |ui| { - egui::ScrollArea::both().show(ui, |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - - let text_buffer = match app.active_code_tab { - CodeTab::Match => &mut app.generated_code_m, - CodeTab::Mismatch => &mut app.generated_code_x, - }; - - ui.add( - egui::TextEdit::multiline(text_buffer) - .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY), - ); - }); - }); -} + 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); +} \ No newline at end of file diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs index 0d531d1..4277ea3 100644 --- a/src/interfaces/gui/paths.rs +++ b/src/interfaces/gui/paths.rs @@ -1,151 +1,79 @@ use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; use crate::interfaces::gui::{CargoPlotApp, PathsTab}; +use crate::interfaces::gui::shared::{resolve_dir, draw_tabs, draw_footer, draw_editor}; use cargo_plot::addon::TimeTag; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::execute; use eframe::egui; pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { - // [ENG]: Initialize translation engine. - // [POL]: Inicjalizacja silnika tłumaczeń. let gt = GuiI18n::new(app.args.lang); - // [ENG]: 1. TOP BAR - Generation and Saving controls. - // [POL]: 1. GÓRNA BELKA - Kontrolki generowania i zapisu. + // [ENG]: 1. TOP TABS - Shared 50/50 layout. + // [POL]: 1. GÓRNE ZAKŁADKI - Współdzielony układ 50/50. + 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 - Dynamic save and isolated generation. + // [POL]: 2. PASEK AKCJI - Dynamiczny zapis i izolowane generowanie. ui.horizontal(|ui| { if ui.button(gt.t(GT::BtnGenerate)).clicked() { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); + + // ⚡ OPTYMALIZACJA: Generujemy tylko to, czego w danej chwili potrzebujesz! + let show_mode = if is_match { ShowMode::Include } else { ShowMode::Exclude }; let stats = execute::execute( - &app.args.enter_path, - &app.args.patterns, - !app.args.ignore_case, - app.args.sort.into(), - ShowMode::Context, - app.args.view.into(), - app.args.no_root, - false, - true, - &i18n, - |_| {}, - |_| {}, + &app.args.enter_path, &app.args.patterns, !app.args.ignore_case, app.args.sort.into(), + show_mode, // ⚡ Oszczędzamy procesor, używając precyzyjnego trybu + app.args.view.into(), app.args.no_root, false, true, &i18n, |_| {}, |_| {}, ); - app.generated_paths_m = - stats.render_output(app.args.view.into(), ShowMode::Include, false, false); - app.generated_paths_x = - stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); + 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(15.0); ui.checkbox(&mut app.args.by, gt.t(GT::LabelAddFooter)); ui.add_space(15.0); - // [ENG]: Helper to resolve output directory from app arguments. - // [POL]: Pomocnik do wyznaczania folderu zapisu z argumentów aplikacji. - let resolve_dir = |val: &Option| -> String { - match val { - Some(v) if v == "AUTO" => "./other/".to_string(), - Some(v) => { - let mut p = v.replace('\\', "/"); - if !p.ends_with('/') { - p.push('/'); - } - p - } - None => "./".to_string(), + // ⚡ Wyświetla tylko przycisk zapisu odpowiadający otwartej zakładce + 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); + } + } 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); } - }; - - // [ENG]: Save MATCH results (-m). - // [POL]: Zapis wyników MATCH (-m). - 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), - tag - ); - let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - let cmd_string = app.args.to_command_string(); - cargo_plot::core::save::SaveFile::paths( - &app.generated_paths_m, - &filepath, - &tag, - app.args.by, - &i18n, - &cmd_string, - ); - } - - ui.add_space(5.0); - - // [ENG]: Save MISMATCH results (-x). - // [POL]: Zapis wyników MISMATCH (-x). - 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), - tag - ); - let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - let cmd_string = app.args.to_command_string(); - cargo_plot::core::save::SaveFile::paths( - &app.generated_paths_x, - &filepath, - &tag, - app.args.by, - &i18n, - &cmd_string, - ); } }); ui.separator(); - // [ENG]: 2. BOTTOM BAR - Sub-tabs for switching between Match and Mismatch views. - // [POL]: 2. DOLNA BELKA - Zakładki do przełączania między widokiem Match i Mismatch. - egui::TopBottomPanel::bottom("paths_subtabs").show_inside(ui, |ui| { - ui.add_space(8.0); - ui.horizontal(|ui| { - if app.active_paths_tab == PathsTab::Match { - ui.visuals_mut().selection.bg_fill = egui::Color32::from_rgb(255, 215, 0); - } - - let m_text = egui::RichText::new(gt.t(GT::TabMatch)) - .size(18.0) - .strong() - .color(egui::Color32::from_rgb(138, 90, 255)); - - let x_text = egui::RichText::new(gt.t(GT::TabMismatch)) - .size(18.0) - .strong() - .color(egui::Color32::from_rgb(255, 80, 100)); - - ui.selectable_value(&mut app.active_paths_tab, PathsTab::Match, m_text); - ui.add_space(20.0); - ui.selectable_value(&mut app.active_paths_tab, PathsTab::Mismatch, x_text); - }); - ui.add_space(8.0); - }); - - // [ENG]: 3. MAIN CONTENT AREA - Scrollable notepad showing generated path data. - // [POL]: 3. GŁÓWNY OBSZAR TREŚCI - Przewijalny notatnik z wygenerowanymi danymi ścieżek. - egui::CentralPanel::default().show_inside(ui, |ui| { - egui::ScrollArea::both().show(ui, |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - - let text_buffer = match app.active_paths_tab { - PathsTab::Match => &mut app.generated_paths_m, - PathsTab::Mismatch => &mut app.generated_paths_x, - }; - - ui.add( - egui::TextEdit::multiline(text_buffer) - .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY), - ); - }); - }); -} + // [ENG]: 3. FOOTER - Shared statistics block. + // [POL]: 3. STOPKA - Współdzielony blok statystyk. + draw_footer(ui, "paths_stats_footer"); + + // [ENG]: 4. MAIN EDITOR - Shared notepad UI. + // [POL]: 4. GŁÓWNY EDYTOR - Współdzielony interfejs notatnika. + 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); +} \ No newline at end of file diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index 033ed83..95c4fd9 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -5,218 +5,232 @@ use cargo_plot::i18n::Lang; use eframe::egui; pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { - // [ENG]: Initialize the GUI translation engine based on current settings. - // [POL]: Inicjalizacja silnika tłumaczeń GUI na podstawie aktualnych ustawień. + // [ENG]: Initialize the GUI translation engine. + // [POL]: Inicjalizacja silnika tłumaczeń GUI. let gt = GuiI18n::new(app.args.lang); - egui::ScrollArea::vertical().show(ui, |ui| { - ui.add_space(10.0); - - // [ENG]: 1. LANGUAGE SELECTION - Dynamically updates the entire UI. - // [POL]: 1. WYBÓR JĘZYKA - Dynamicznie aktualizuje cały interfejs. - 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.separator(); - ui.add_space(10.0); - - // [ENG]: 2. SCAN FOLDER SELECTION - Uses native system dialog. - // [POL]: 2. WYBÓR FOLDERU SKANOWANIA - Używa natywnego okna systemowego. - ui.horizontal(|ui| { - ui.label(gt.t(GT::LabelScanPath)); - ui.text_edit_singleline(&mut app.args.enter_path); - - 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('\\', "/"); - } + // [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-beta").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); }); - ui.add_space(10.0); - ui.separator(); - - // [ENG]: 3. OUTPUT FOLDER SELECTION - Common path for paths and archive saves. - // [POL]: 3. WYBÓR FOLDERU WYNIKOWEGO - Wspólna ścieżka dla zapisu ścieżek i archiwum. - ui.add_space(10.0); - ui.horizontal(|ui| { - ui.label(gt.t(GT::LabelOutFolder)); - - if ui.text_edit_singleline(&mut app.out_path_input).changed() { - let trimmed = app.out_path_input.trim(); - app.args.dir_out = if trimmed.is_empty() { - None - } else { - Some(trimmed.to_string()) - }; - } - - if ui.button(gt.t(GT::BtnBrowse)).clicked() { - if 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); - } - } - }); - ui.add_space(10.0); - ui.separator(); - - // [ENG]: 4. VIEW AND SORTING - Controls the structure of the generated report. - // [POL]: 4. WIDOK I SORTOWANIE - Kontroluje strukturę generowanego raportu. - 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"); + // [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() { + if 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(20.0); + + // [ENG]: Match Patterns Section. + // [POL]: Sekcja wzorców dopasowań. + ui.heading(gt.t(GT::HeadingPatterns)); 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.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); + }); + } - ui.add_space(15.0); - ui.checkbox(&mut app.args.no_root, gt.t(GT::LabelNoRoot)); - }); - - ui.add_space(20.0); - - // [ENG]: 5. MATCH PATTERNS - Pattern management with real-time list interaction. - // [POL]: 5. WZORCE DOPASOWAŃ - Zarządzanie wzorcami z interaktywną listą. - ui.heading(gt.t(GT::HeadingPatterns)); - ui.add_space(5.0); - - ui.horizontal(|ui| { - ui.checkbox(&mut app.args.ignore_case, gt.t(GT::LabelIgnoreCase)); - ui.label(gt.t(GT::LabelNewPattern)); - let response = ui.text_edit_singleline(&mut app.new_pattern_input); - let btn_clicked = ui.button(gt.t(GT::BtnAddPattern)).clicked(); - - if (btn_clicked - || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))) - && !app.new_pattern_input.trim().is_empty() - { - app.args - .patterns - .push(app.new_pattern_input.trim().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(100.0); - - let mut move_up = None; - let mut move_down = None; - let mut remove = None; + 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); } - for (i, pat) in app.args.patterns.iter().enumerate() { - ui.horizontal(|ui| { - if ui.button("🗑").clicked() { - remove = Some(i); + if !app.args.patterns.is_empty() { + ui.separator(); + if ui.button(gt.t(GT::BtnClearAll)).clicked() { + app.args.patterns.clear(); } - 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()); } - } else { - ui.label( - egui::RichText::new(gt.t(GT::MsgNoPatterns)) - .italics() - .weak(), - ); - } - }); - ui.separator(); + }); - ui.add_space(50.0); - - // [ENG]: 6. FOOTER - Versioning, links and installation instructions. - // [POL]: 6. STOPKA - Wersjonowanie, linki i instrukcje instalacji. - ui.separator(); - ui.add_space(10.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("📦 cargo-plot v0.2.0-beta").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(20.0); }); - ui.add_space(10.0); }); -} +} \ No newline at end of file diff --git a/src/interfaces/gui/shared.rs b/src/interfaces/gui/shared.rs new file mode 100644 index 0000000..c8a5df9 --- /dev/null +++ b/src/interfaces/gui/shared.rs @@ -0,0 +1,86 @@ +use eframe::egui; +use crate::interfaces::gui::i18n::{GuiI18n, GuiText as GT}; + +// [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().map_or(true, |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 na przyszłe statystyki. +pub fn draw_footer(ui: &mut egui::Ui, panel_id: &'static str) { + egui::TopBottomPanel::bottom(panel_id).show_inside(ui, |ui| { + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.label("📝 Txt: 0 (0 B)"); ui.separator(); + ui.label("📦 Bin: 0 (0 B)"); ui.separator(); + ui.label("🚫 Err: 0 (0 B)"); ui.separator(); + ui.label("🕳️ Empty: 0"); ui.separator(); + ui.label("🎯 Matched: 0 / 0"); + }); + 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), + ); + }); + }); +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a1e27ee..8c4aafb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,16 @@ -// [ENG]: Main entry point switching between interactive TUI and automated CLI. -// [POL]: Główny punkt wejścia przełączający między interaktywnym TUI a automatycznym CLI. +// [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)] -use std::env; mod interfaces; fn main() { - // Rejestrujemy pusty handler Ctrl+C. - // Dzięki temu system nie zabije programu natychmiast, a `cliclack` - // przejmie sygnał i bezpiecznie wyjdzie z prompta. + // [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"); - let args: Vec = env::args().collect(); - - // [POL]: Uruchom TUI tylko jeśli: - // 1. Brak argumentów (tylko nazwa pliku binarnego) -> len == 1 - // 2. Wywołanie subkomendy bez flag (cargo-plot plot) -> len == 2 && args[1] == "plot" - let is_tui = args.len() == 1 || (args.len() == 2 && args[1] == "plot"); - - if is_tui { - interfaces::tui::run_tui(); - return; // ⚡ ODKOMENTOWANE: Zapobiega odpaleniu CLI po wyjściu z TUI - } - - // Wszystko inne (w tym --help) trafia do parsera CLI + // [ENG]: Pass execution directly to the CLI parser and router. + // [POL]: Przekazanie wykonania bezpośrednio do parsera i routera CLI. interfaces::cli::run_cli(); -} +} \ No newline at end of file From a63502238dc66c28306b1fb3ce39e4dc56d2ef13 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sat, 21 Mar 2026 01:42:18 +0100 Subject: [PATCH 40/45] (fix: foot) --- src/core/save.rs | 63 +++++++++++++----- src/i18n.rs | 50 +++++++------- src/interfaces/cli.rs | 2 +- src/interfaces/cli/args.rs | 54 +++++++++++---- src/interfaces/cli/engine.rs | 64 ++++++++++++++---- src/interfaces/gui/code.rs | 100 ++++++++++++++++++++++------ src/interfaces/gui/paths.rs | 73 +++++++++++++++----- src/interfaces/gui/settings.rs | 117 ++++++++++++++++++++++++--------- src/interfaces/gui/shared.rs | 58 +++++++++++----- src/main.rs | 2 +- 10 files changed, 428 insertions(+), 155 deletions(-) diff --git a/src/core/save.rs b/src/core/save.rs index dfcdbd4..f4f63ac 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -6,18 +6,38 @@ use std::path::Path; pub struct SaveFile; impl SaveFile { - // ⚡ Upubliczniamy funkcję, żeby kod w `code.rs` mógł wygenerować stopkę - pub fn generate_by_section(tag: &str, typ: &str, i18n: &I18n, cmd: &str) -> String { - format!( - "\n\n---\n---\n\n{}\n\n{}\n\n```bash\n{}\n```\n\n{}\n\n{}\n\n{}\n\n---\n", - i18n.by_title(typ), - i18n.by_cmd(), - cmd, // ⚡ Używa czystej, przetworzonej komendy! - i18n.by_instructions(), - i18n.by_link(), - i18n.by_version(tag) - ) + // ⚡ 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-beta` |\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); @@ -39,20 +59,28 @@ impl SaveFile { 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) { + 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, "paths", i18n, cmd) + Self::generate_by_section(tag, enter_path, i18n, cmd) } else { String::new() }; - let internal_tag = if add_by { "" } else { tag }; // Zapobiega dublowaniu tagu + let internal_tag = if add_by { "" } else { tag }; let file_name = Path::new(filepath) .file_name() .unwrap_or_default() .to_string_lossy(); - // ⚡ DODAJE NAGŁÓWEK H1 NA POCZĄTKU let markdown_content = format!( "# {}\n\n```plaintext\n{}\n```\n\n{}{}", file_name, content, internal_tag, by_section @@ -80,13 +108,14 @@ impl SaveFile { add_by: bool, i18n: &I18n, cmd: &str, + enter_path: &str, ) { let by_section = if add_by { - Self::generate_by_section(tag, "codes", i18n, cmd) + Self::generate_by_section(tag, enter_path, i18n, cmd) } else { String::new() }; - let internal_tag = if add_by { "" } else { tag }; // Zapobiega dublowaniu tagu + let internal_tag = if add_by { "" } else { tag }; let file_name = Path::new(filepath) .file_name() .unwrap_or_default() diff --git a/src/i18n.rs b/src/i18n.rs index c20ae9f..f3d4d6c 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -68,40 +68,41 @@ impl I18n { Lang::En => "> *(Read error / file is not UTF-8)*", } } - pub fn by_title(&self, typ: &str) -> String { + + 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 => format!("## Command - Query ({typ})"), - Lang::En => format!("## Command - Query ({typ})"), + Lang::Pl => "Folder", + _ => "Input", } } - pub fn by_cmd(&self) -> &'static str { + pub fn footer_cmd(&self) -> &str { match self.lang { - Lang::Pl => "**Wywołana komenda:**", - Lang::En => "**Executed command:**", + Lang::Pl => "Komenda", + _ => "Command", } } - pub fn by_instructions(&self) -> &'static str { + pub fn footer_tag(&self) -> &str { match self.lang { - Lang::Pl => { - "**Krótka instrukcja flag:**\n- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`)\n- `-p, --pat ...` : Wzorce dopasowań (wymagane)\n- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`)\n- `-v, --view ` : Widok wyników (`tree`, `list`, `grid`)\n- `-m, --on-match` : Pokaż tylko dopasowane ścieżki\n- `-x, --on-mismatch` : Pokaż tylko odrzucone ścieżki\n- `-o, --out-paths [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`)\n- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`)\n- `-i, --info` : Tryb gadatliwy w terminalu\n- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku\n- `--ignore-case` : Ignoruj wielkość liter we wzorcach\n- `--treeview-no-root` : Ukryj główny folder w widoku drzewa" - } - Lang::En => { - "**Short flags manual:**\n- `-d, --dir ` : Input path to scan (default: `.`)\n- `-p, --pat ...` : Match patterns (required)\n- `-s, --sort ` : Sorting strategy (e.g. `az-file-merge`)\n- `-v, --view ` : Results view (`tree`, `list`, `grid`)\n- `-m, --on-match` : Show only matched paths\n- `-x, --on-mismatch` : Show only rejected paths\n- `-o, --out-paths [PATH]` : Save paths to file (AUTO: `./other/`)\n- `-c, --out-cache [PATH]` : Save code to file (AUTO: `./other/`)\n- `-i, --info` : Verbose terminal mode\n- `-b, --by` : Add info section at end of file\n- `--ignore-case` : Ignore case in patterns\n- `--treeview-no-root` : Hide root directory in tree view" - } + Lang::Pl => "Tag", + _ => "TimeTag", } } - pub fn by_link(&self) -> &'static str { + pub fn footer_links(&self) -> &str { match self.lang { - Lang::Pl => { - "[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot)" - } - Lang::En => "[📊 Check `cargo-plot` on crates.io](https://crates.io/crates/cargo-plot)", + Lang::Pl => "Linki", + _ => "Links", } } - pub fn by_version(&self, tag: &str) -> String { + pub fn footer_help(&self) -> &str { match self.lang { - Lang::Pl => format!("**Wersja raportu:** {tag}"), - Lang::En => format!("**Report version:** {tag}"), + Lang::Pl => "Pomoc", + _ => "Help", } } @@ -156,11 +157,4 @@ impl I18n { Lang::En => format!("📊 Summary: Rejected {} of {} paths.", count, total), } } - - - - // ===================================================================== - // 4. TUI - INTERAKTYWNY PANEL - // ===================================================================== - // (Zostawiamy tu miejsce na przyszłość) } diff --git a/src/interfaces/cli.rs b/src/interfaces/cli.rs index c8003cf..b298b53 100644 --- a/src/interfaces/cli.rs +++ b/src/interfaces/cli.rs @@ -36,4 +36,4 @@ pub fn run_cli() { } else { engine::run(flags); } -} \ No newline at end of file +} diff --git a/src/interfaces/cli/args.rs b/src/interfaces/cli/args.rs index de4aa40..58ba9a7 100644 --- a/src/interfaces/cli/args.rs +++ b/src/interfaces/cli/args.rs @@ -173,7 +173,13 @@ impl From for ViewMode { 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 { + 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() { @@ -199,10 +205,16 @@ impl CliArgs { } // ⚡ 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 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 { @@ -233,19 +245,33 @@ impl CliArgs { } // ⚡ 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.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 { diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 1ad8233..adc2daa 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -60,14 +60,20 @@ pub fn run(args: CliArgs) { // [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().map_or(true, |v| v.trim().is_empty() || v == "AUTO"); + let is_auto = val + .as_ref() + .map_or(true, |v| v.trim().is_empty() || v == "AUTO"); if is_auto { let mut b = base_path.replace('\\', "/"); - if !b.ends_with('/') { b.push('/'); } + 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('/'); } + if !p.ends_with('/') { + p.push('/'); + } p } }; @@ -79,13 +85,29 @@ pub fn run(args: CliArgs) { if args.save_address { if args.include || (!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); // ⚡ address = true - SaveFile::paths(&output_str_txt_m, &filepath, &tag, args.by, &i18n, &cmd_m); + 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 && !args.exclude) { let filepath = format!("{}plot-address_{}_X.md", output_dir, tag); - let cmd_x = args.to_command_string(false, true, true, false); // ⚡ address = true - SaveFile::paths(&output_str_txt_x, &filepath, &tag, args.by, &i18n, &cmd_x); + 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, + ); } } @@ -95,13 +117,33 @@ pub fn run(args: CliArgs) { if let Ok(ctx) = PathContext::resolve(&args.enter_path) { if args.include || (!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); // ⚡ archive = true - SaveFile::codes(&output_str_txt_m, &stats.m_matched.paths, &ctx.entry_absolute, &filepath, &tag, args.by, &i18n, &cmd_m); + 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 && !args.exclude) { let filepath = format!("{}plot-archive_{}_X.md", output_dir, tag); - let cmd_x = args.to_command_string(false, true, false, true); // ⚡ archive = true - SaveFile::codes(&output_str_txt_x, &stats.x_mismatched.paths, &ctx.entry_absolute, &filepath, &tag, args.by, &i18n, &cmd_x); + 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, + ); } } } diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs index 1e736d9..386559c 100644 --- a/src/interfaces/gui/code.rs +++ b/src/interfaces/gui/code.rs @@ -1,6 +1,6 @@ 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}; -use crate::interfaces::gui::shared::{resolve_dir, draw_tabs, draw_footer, draw_editor}; use cargo_plot::addon::TimeTag; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::execute; @@ -11,48 +11,88 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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 }; + app.active_code_tab = if is_match { + CodeTab::Match + } else { + CodeTab::Mismatch + }; ui.separator(); ui.horizontal(|ui| { if ui.button(gt.t(GT::BtnGenerateCode)).clicked() { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - + // ⚡ OPTYMALIZACJA: Generowanie skanowania i odczytów plików tylko dla żądanej sekcji. - let show_mode = if is_match { ShowMode::Include } else { ShowMode::Exclude }; + let show_mode = if is_match { + ShowMode::Include + } else { + ShowMode::Exclude + }; 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(), app.args.no_root, false, true, &i18n, |_| {}, |_| {}, + &app.args.enter_path, + &app.args.patterns, + !app.args.ignore_case, + app.args.sort.into(), + show_mode, + app.args.view.into(), + app.args.no_root, + false, + true, + &i18n, + |_| {}, + |_| {}, ); let base_dir = std::path::Path::new(&app.args.enter_path); if is_match { - let tree_m = stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + 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; } + 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))), + 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 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; } + 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))), + 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; } @@ -67,24 +107,42 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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 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 { + 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, "codes", &i18n, &cmd_string)); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( + &tag, + &app.args.enter_path, + &i18n, + &cmd_string, + )); // ⚡ DODANE } 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 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 { + 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, "codes", &i18n, &cmd_string)); + final_text.push_str(&cargo_plot::core::save::SaveFile::generate_by_section( + &tag, + &app.args.enter_path, + &i18n, + &cmd_string, + )); // ⚡ DODANE } let _ = std::fs::write(&filepath, final_text); } @@ -100,4 +158,4 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { CodeTab::Mismatch => &mut app.generated_code_x, }; draw_editor(ui, text_buffer); -} \ No newline at end of file +} diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs index 4277ea3..e2b802d 100644 --- a/src/interfaces/gui/paths.rs +++ b/src/interfaces/gui/paths.rs @@ -1,6 +1,6 @@ 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}; -use crate::interfaces::gui::shared::{resolve_dir, draw_tabs, draw_footer, draw_editor}; use cargo_plot::addon::TimeTag; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::execute; @@ -13,7 +13,11 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // [POL]: 1. GÓRNE ZAKŁADKI - Współdzielony układ 50/50. 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 }; + app.active_paths_tab = if is_match { + PathsTab::Match + } else { + PathsTab::Mismatch + }; ui.separator(); @@ -22,20 +26,35 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.horizontal(|ui| { if ui.button(gt.t(GT::BtnGenerate)).clicked() { let i18n = cargo_plot::i18n::I18n::new(app.args.lang); - + // ⚡ OPTYMALIZACJA: Generujemy tylko to, czego w danej chwili potrzebujesz! - let show_mode = if is_match { ShowMode::Include } else { ShowMode::Exclude }; + let show_mode = if is_match { + ShowMode::Include + } else { + ShowMode::Exclude + }; let stats = execute::execute( - &app.args.enter_path, &app.args.patterns, !app.args.ignore_case, app.args.sort.into(), + &app.args.enter_path, + &app.args.patterns, + !app.args.ignore_case, + app.args.sort.into(), show_mode, // ⚡ Oszczędzamy procesor, używając precyzyjnego trybu - app.args.view.into(), app.args.no_root, false, true, &i18n, |_| {}, |_| {}, + app.args.view.into(), + app.args.no_root, + false, + true, + &i18n, + |_| {}, + |_| {}, ); if is_match { - app.generated_paths_m = stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + 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); + app.generated_paths_x = + stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); } } @@ -47,18 +66,42 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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 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); + 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 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); + 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, + ); } } }); @@ -76,4 +119,4 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { PathsTab::Mismatch => &mut app.generated_paths_x, }; draw_editor(ui, text_buffer); -} \ No newline at end of file +} diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index 95c4fd9..136ac9b 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -17,15 +17,18 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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-beta").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.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()); @@ -61,7 +64,7 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { egui::Grid::new("path_settings_grid") .num_columns(2) .spacing([10.0, 10.0]) - .min_col_width(120.0) + .min_col_width(120.0) .show(ui, |ui| { // Row 1: Scan path ui.label(gt.t(GT::LabelScanPath)); @@ -71,7 +74,10 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { { app.args.enter_path = folder.to_string_lossy().replace('\\', "/"); } - ui.add_sized(ui.available_size(), egui::TextEdit::singleline(&mut app.args.enter_path)); + ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut app.args.enter_path), + ); }); ui.end_row(); @@ -81,16 +87,25 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { if ui.button(gt.t(GT::BtnBrowse)).clicked() { if let Some(folder) = rfd::FileDialog::new().pick_folder() { let mut path = folder.to_string_lossy().replace('\\', "/"); - if !path.ends_with('/') { path.push('/'); } + 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)); + + 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()) }; + app.args.dir_out = if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + }; } }); ui.end_row(); @@ -106,10 +121,26 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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::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"); @@ -143,12 +174,16 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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)); + 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)))) + 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(); @@ -157,18 +192,18 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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('\'')) + 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('\'') { @@ -181,7 +216,7 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // 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()); } @@ -201,7 +236,7 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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()); + ui.set_min_width(ui.available_width()); let mut move_up = None; let mut move_down = None; @@ -209,16 +244,32 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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); } + 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 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(); @@ -226,11 +277,15 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { app.args.patterns.clear(); } } else { - ui.label(egui::RichText::new(gt.t(GT::MsgNoPatterns)).italics().weak()); + ui.label( + egui::RichText::new(gt.t(GT::MsgNoPatterns)) + .italics() + .weak(), + ); } }); - ui.add_space(20.0); + ui.add_space(20.0); }); }); -} \ No newline at end of file +} diff --git a/src/interfaces/gui/shared.rs b/src/interfaces/gui/shared.rs index c8a5df9..464c8f7 100644 --- a/src/interfaces/gui/shared.rs +++ b/src/interfaces/gui/shared.rs @@ -1,17 +1,23 @@ -use eframe::egui; 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().map_or(true, |v| v.trim().is_empty() || v == "AUTO"); + let is_auto = val + .as_ref() + .map_or(true, |v| v.trim().is_empty() || v == "AUTO"); if is_auto { let mut b = base_path.replace('\\', "/"); - if !b.ends_with('/') { b.push('/'); } + 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('/'); } + if !p.ends_with('/') { + p.push('/'); + } p } } @@ -29,12 +35,20 @@ pub fn draw_tabs(ui: &mut egui::Ui, gt: &GuiI18n, is_match: &mut bool) { 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) + [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; } + if m_btn.clicked() { + *is_match = true; + } ui.add_space(8.0); @@ -47,10 +61,18 @@ pub fn draw_tabs(ui: &mut egui::Ui, gt: &GuiI18n, is_match: &mut bool) { } 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) + [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; } + if x_btn.clicked() { + *is_match = false; + } }); } @@ -60,10 +82,14 @@ pub fn draw_footer(ui: &mut egui::Ui, panel_id: &'static str) { egui::TopBottomPanel::bottom(panel_id).show_inside(ui, |ui| { ui.add_space(5.0); ui.horizontal(|ui| { - ui.label("📝 Txt: 0 (0 B)"); ui.separator(); - ui.label("📦 Bin: 0 (0 B)"); ui.separator(); - ui.label("🚫 Err: 0 (0 B)"); ui.separator(); - ui.label("🕳️ Empty: 0"); ui.separator(); + ui.label("📝 Txt: 0 (0 B)"); + ui.separator(); + ui.label("📦 Bin: 0 (0 B)"); + ui.separator(); + ui.label("🚫 Err: 0 (0 B)"); + ui.separator(); + ui.label("🕳️ Empty: 0"); + ui.separator(); ui.label("🎯 Matched: 0 / 0"); }); ui.add_space(5.0); @@ -83,4 +109,4 @@ pub fn draw_editor(ui: &mut egui::Ui, text_buffer: &mut String) { ); }); }); -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 8c4aafb..947ac4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,4 +13,4 @@ fn main() { // [ENG]: Pass execution directly to the CLI parser and router. // [POL]: Przekazanie wykonania bezpośrednio do parsera i routera CLI. interfaces::cli::run_cli(); -} \ No newline at end of file +} From b6dcc59eb0b6817ea3d98b6a99b44e43c798022e Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sat, 21 Mar 2026 02:08:46 +0100 Subject: [PATCH 41/45] (on finish) --- src/core/save.rs | 2 +- src/interfaces/cli/engine.rs | 72 +++++++++------------------------- src/interfaces/gui.rs | 21 +++++----- src/interfaces/gui/code.rs | 4 +- src/interfaces/gui/paths.rs | 53 ++++++++++++++++++------- src/interfaces/gui/settings.rs | 5 +-- src/interfaces/gui/shared.rs | 33 ++++++++++------ 7 files changed, 97 insertions(+), 93 deletions(-) diff --git a/src/core/save.rs b/src/core/save.rs index f4f63ac..7808bb6 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -194,7 +194,7 @@ impl SaveFile { /// [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. -fn is_blacklisted_extension(ext: &str) -> bool { +pub fn is_blacklisted_extension(ext: &str) -> bool { let e = ext.to_lowercase(); matches!( diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index adc2daa..7ba2496 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -62,7 +62,7 @@ pub fn run(args: CliArgs) { let resolve_dir = |val: &Option, base_path: &str| -> String { let is_auto = val .as_ref() - .map_or(true, |v| v.trim().is_empty() || v == "AUTO"); + .is_none_or(|v| v.trim().is_empty() || v == "AUTO"); if is_auto { let mut b = base_path.replace('\\', "/"); if !b.ends_with('/') { @@ -83,68 +83,34 @@ pub fn run(args: CliArgs) { // [ENG]: 📝 Saves the path structure (address). // [POL]: 📝 Zapisuje strukturę ścieżek (adres). if args.save_address { - if args.include || (!args.include && !args.exclude) { + if args.include || !args.exclude { + // (kod pozostaje bez zmian) 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, - ); + SaveFile::paths(&output_str_txt_m, &filepath, &tag, args.by, &i18n, &cmd_m, &args.enter_path); } - if args.exclude || (!args.include && !args.exclude) { + if args.exclude || !args.include { + // (kod pozostaje bez zmian) 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, - ); + 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 { - if let Ok(ctx) = PathContext::resolve(&args.enter_path) { - if args.include || (!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 && !args.exclude) { - 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, - ); - } + if args.save_archive && let Ok(ctx) = PathContext::resolve(&args.enter_path) { + if args.include || !args.exclude { + // (kod pozostaje bez zmian) + 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 { + // (kod pozostaje bez zmian) + 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); } } } diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs index 2c35619..4ba307f 100644 --- a/src/interfaces/gui.rs +++ b/src/interfaces/gui.rs @@ -29,27 +29,30 @@ pub enum CodeTab { #[derive(Default, Clone)] pub struct TreeStats { - pub file_count: usize, - pub total_weight: u64, - pub text_count: usize, - pub text_weight: u64, + 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, // ⚡ Dodane pole: aktywna zakładka Kodu + 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, // ⚡ Dodane pole: kod MATCH - pub generated_code_x: String, // ⚡ Dodane pole: kod MISMATCH - pub stats_m: TreeStats, - pub stats_x: TreeStats, + pub generated_code_m: String, + pub generated_code_x: String, + pub stats_m: TreeStats, + pub stats_x: TreeStats, pub ui_scale: f32, } diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs index 386559c..cea2ea3 100644 --- a/src/interfaces/gui/code.rs +++ b/src/interfaces/gui/code.rs @@ -151,7 +151,9 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.separator(); - draw_footer(ui, "code_stats_footer"); + // ⚡ Przekazujemy odpowiednie statystyki zależnie od wybranej zakładki + let current_stats = if is_match { &app.stats_m } else { &app.stats_x }; + draw_footer(ui, "code_stats_footer", current_stats); let text_buffer = match app.active_code_tab { CodeTab::Match => &mut app.generated_code_m, diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs index e2b802d..b63f176 100644 --- a/src/interfaces/gui/paths.rs +++ b/src/interfaces/gui/paths.rs @@ -5,6 +5,8 @@ use cargo_plot::addon::TimeTag; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::execute; use eframe::egui; +use cargo_plot::core::save::is_blacklisted_extension; +use crate::interfaces::gui::TreeStats; pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { let gt = GuiI18n::new(app.args.lang); @@ -34,21 +36,43 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ShowMode::Exclude }; + let mut st_m = TreeStats::default(); + let mut st_x = TreeStats::default(); + let stats = execute::execute( - &app.args.enter_path, - &app.args.patterns, - !app.args.ignore_case, - app.args.sort.into(), - show_mode, // ⚡ Oszczędzamy procesor, używając precyzyjnego trybu - app.args.view.into(), - app.args.no_root, - false, - true, - &i18n, - |_| {}, - |_| {}, + &app.args.enter_path, &app.args.patterns, !app.args.ignore_case, app.args.sort.into(), + show_mode, app.args.view.into(), app.args.no_root, false, true, &i18n, + |f_stats| { + if f_stats.weight_bytes == 0 { st_m.empty_count += 1; } + if !f_stats.path.ends_with('/') { + let ext = f_stats.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_stats.weight_bytes; + } else { + st_m.txt_count += 1; st_m.txt_weight += f_stats.weight_bytes; + } + } + }, + |f_stats| { + if f_stats.weight_bytes == 0 { st_x.empty_count += 1; } + if !f_stats.path.ends_with('/') { + let ext = f_stats.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_stats.weight_bytes; + } else { + st_x.txt_count += 1; st_x.txt_weight += f_stats.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; + + if is_match { app.stats_m = st_m; } else { app.stats_x = st_x; } + if is_match { app.generated_paths_m = stats.render_output(app.args.view.into(), ShowMode::Include, false, false); @@ -110,8 +134,9 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // [ENG]: 3. FOOTER - Shared statistics block. // [POL]: 3. STOPKA - Współdzielony blok statystyk. - draw_footer(ui, "paths_stats_footer"); - + 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 - Shared notepad UI. // [POL]: 4. GŁÓWNY EDYTOR - Współdzielony interfejs notatnika. let text_buffer = match app.active_paths_tab { diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index 136ac9b..98b23e2 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -84,8 +84,8 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // 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() { - if let Some(folder) = rfd::FileDialog::new().pick_folder() { + 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('/'); @@ -93,7 +93,6 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { app.out_path_input = path.clone(); app.args.dir_out = Some(path); } - } let txt_response = ui.add_sized( ui.available_size(), diff --git a/src/interfaces/gui/shared.rs b/src/interfaces/gui/shared.rs index 464c8f7..db39338 100644 --- a/src/interfaces/gui/shared.rs +++ b/src/interfaces/gui/shared.rs @@ -6,7 +6,7 @@ use eframe::egui; pub fn resolve_dir(val: &Option, base_path: &str) -> String { let is_auto = val .as_ref() - .map_or(true, |v| v.trim().is_empty() || v == "AUTO"); + .is_none_or(|v| v.trim().is_empty() || v == "AUTO"); if is_auto { let mut b = base_path.replace('\\', "/"); if !b.ends_with('/') { @@ -77,20 +77,29 @@ pub fn draw_tabs(ui: &mut egui::Ui, gt: &GuiI18n, is_match: &mut bool) { } // [ENG]: UI component: Statistics footer placeholder. -// [POL]: Komponent UI: Stopka na przyszłe statystyki. -pub fn draw_footer(ui: &mut egui::Ui, panel_id: &'static str) { +// [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("📝 Txt: 0 (0 B)"); - ui.separator(); - ui.label("📦 Bin: 0 (0 B)"); - ui.separator(); - ui.label("🚫 Err: 0 (0 B)"); - ui.separator(); - ui.label("🕳️ Empty: 0"); - ui.separator(); - ui.label("🎯 Matched: 0 / 0"); + 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); }); From 5aba453e18b4e8ea82b6e2f1cabe5909f4f9d229 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sat, 21 Mar 2026 02:45:59 +0100 Subject: [PATCH 42/45] (add: new flag `-a` & `-u` --- src/core/file_stats/weight.rs | 1 + src/execute.rs | 104 ++++++++-------------- src/interfaces/cli/engine.rs | 34 +++++--- src/interfaces/gui/code.rs | 138 ++++++++++++++--------------- src/interfaces/gui/paths.rs | 154 ++++++++++++++++----------------- src/interfaces/gui/settings.rs | 2 + 6 files changed, 208 insertions(+), 225 deletions(-) diff --git a/src/core/file_stats/weight.rs b/src/core/file_stats/weight.rs index 297e01c..9f9c335 100644 --- a/src/core/file_stats/weight.rs +++ b/src/core/file_stats/weight.rs @@ -44,6 +44,7 @@ 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 { return get_dir_size(path); } diff --git a/src/execute.rs b/src/execute.rs index 8d08b6d..f368716 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -7,8 +7,8 @@ use crate::core::path_view::{PathGrid, PathList, PathTree, ViewMode}; use crate::core::patterns_expand::PatternContext; use std::path::Path; -/// [POL]: Egzekutor operujący na wielu wzorcach (wersja po rozwinięciu klamer/tokenizacji). -/// [ENG]: Executor operating on multiple patterns (post brace expansion/tokenisation). +/// [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], @@ -16,6 +16,7 @@ pub fn execute( 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, @@ -24,149 +25,118 @@ pub fn execute( mut on_mismatch: OnMismatch, ) -> MatchStats where - // OnMatch: FnMut(&str), - // OnMismatch: FnMut(&str), - // ⚡ Teraz callbacki oczekują bogatego obiektu, a nie tylko tekstu OnMatch: FnMut(&FileStats), OnMismatch: FnMut(&FileStats), { - // 1. Inicjalizacja kontekstów + // [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); }); - // 2. Logowanie stanu początkowego + // [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!("{}", i18n.cli_patterns_raw(&format!("{:?}", pattern_ctx.raw))); + println!("{}", i18n.cli_patterns_tok(&format!("{:?}", pattern_ctx.tok))); println!("---------------------------------------"); } else { println!("---------------------------------------"); } - // 3. Budowa silników dopasowujących (Generał) - let matchers = - PathMatchers::new(&pattern_ctx.tok, is_case_sensitive).expect("Błąd kompilacji wzorców"); + // [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"); - // 4. Skanowanie dysku (Getter) - // [POL]: Ładujemy dane do rejestru z rdzenia + // [ENG]: 4. Scan disk. + // [POL]: 4. Skanowanie dysku. let paths_store = PathStore::scan(&path_ctx.entry_absolute); - // [POL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) let paths_set = paths_store.get_index(); - let entry_abs = path_ctx.entry_absolute.clone(); - // 6. Zwracamy statystyki do Engine'u + + // [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| { - // Pośrednik pobiera statystyki let stats = FileStats::fetch(raw_path, &entry_abs); on_match(&stats); }, |raw_path| { - // Pośrednik pobiera statystyki let stats = FileStats::fetch(raw_path, &entry_abs); on_mismatch(&stats); }, ); - // 7. ⚡ MAGIA BUDOWANIA WIDOKÓW - let weight_cfg = WeightConfig::default(); + + + + + + + + + + + // [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()) + Path::new(&path_ctx.entry_absolute).file_name().and_then(|n| n.to_str()) }; - // Pomocnicze flagi do budowania (żeby kod w match był krótki) let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; - // ⚡ Czysty match dla widoków (Grid, Tree, List) 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, + &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, + &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, + &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, + &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, + &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.x_mismatched.paths, &path_ctx.entry_absolute, sort_strategy, &weight_cfg, no_emoji, )); } } } stats -} +} \ No newline at end of file diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 7ba2496..5aed822 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,5 +1,6 @@ -use crate::interfaces::cli::args::CliArgs; +use crate::interfaces::cli::args::{CliArgs, CliUnitSystem}; use cargo_plot::addon::TimeTag; +use cargo_plot::core::file_stats::weight::{WeightConfig, UnitSystem}; use cargo_plot::core::path_matcher::stats::ShowMode; use cargo_plot::core::path_store::PathContext; use cargo_plot::core::path_view::ViewMode; @@ -7,16 +8,28 @@ 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. +/// [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]: 📝 Reconstructs the command string for the footer. - // [POL]: 📝 Odtwarza ciąg komendy dla stopki. + // [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) { @@ -25,8 +38,8 @@ pub fn run(args: CliArgs) { _ => ShowMode::Context, }; - // [ENG]: 🚀 Executes the core matching logic. - // [POL]: 🚀 Wykonuje główną logikę dopasowywania. + // [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, @@ -34,6 +47,7 @@ pub fn run(args: CliArgs) { sort_strategy, show_mode, view_mode, + weight_cfg, // ⚡ WSTRZYKNIĘTE args.no_root, args.info, args.no_emoji, @@ -84,13 +98,11 @@ pub fn run(args: CliArgs) { // [POL]: 📝 Zapisuje strukturę ścieżek (adres). if args.save_address { if args.include || !args.exclude { - // (kod pozostaje bez zmian) 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 { - // (kod pozostaje bez zmian) 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); @@ -101,13 +113,11 @@ pub fn run(args: CliArgs) { // [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 { - // (kod pozostaje bez zmian) 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 { - // (kod pozostaje bez zmian) 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); @@ -130,4 +140,4 @@ pub fn run(args: CliArgs) { } else { println!("---------------------------------------"); } -} +} \ No newline at end of file diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs index cea2ea3..219361f 100644 --- a/src/interfaces/gui/code.rs +++ b/src/interfaces/gui/code.rs @@ -1,35 +1,47 @@ 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}; +use crate::interfaces::gui::{CargoPlotApp, CodeTab, TreeStats}; use cargo_plot::addon::TimeTag; use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::file_stats::weight::{WeightConfig, UnitSystem}; +use cargo_plot::core::save::is_blacklisted_extension; +use cargo_plot::core::file_stats::FileStats; 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 - }; + 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 }; - // ⚡ OPTYMALIZACJA: Generowanie skanowania i odczytów plików tylko dla żądanej sekcji. - 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, @@ -37,62 +49,65 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { app.args.sort.into(), show_mode, app.args.view.into(), + weight_cfg, app.args.no_root, false, - true, + 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 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; - } + 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) - )), + 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 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; - } + 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) - )), + 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; } @@ -104,45 +119,29 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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 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, - )); // ⚡ DODANE + 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 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, - )); // ⚡ DODANE + 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); } @@ -151,13 +150,16 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.separator(); - // ⚡ Przekazujemy odpowiednie statystyki zależnie od wybranej zakładki + // [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); -} +} \ No newline at end of file diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs index b63f176..faaed21 100644 --- a/src/interfaces/gui/paths.rs +++ b/src/interfaces/gui/paths.rs @@ -1,147 +1,145 @@ 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}; +use crate::interfaces::gui::{CargoPlotApp, PathsTab, TreeStats}; use cargo_plot::addon::TimeTag; use cargo_plot::core::path_matcher::stats::ShowMode; +use cargo_plot::core::file_stats::weight::{WeightConfig, UnitSystem}; +use cargo_plot::core::save::is_blacklisted_extension; +use cargo_plot::core::file_stats::FileStats; use cargo_plot::execute; use eframe::egui; -use cargo_plot::core::save::is_blacklisted_extension; -use crate::interfaces::gui::TreeStats; +/// [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 - Shared 50/50 layout. - // [POL]: 1. GÓRNE ZAKŁADKI - Współdzielony układ 50/50. + // [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 - }; + app.active_paths_tab = if is_match { PathsTab::Match } else { PathsTab::Mismatch }; ui.separator(); - // [ENG]: 2. ACTION BAR - Dynamic save and isolated generation. - // [POL]: 2. PASEK AKCJI - Dynamiczny zapis i izolowane generowanie. + // [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 }; - // ⚡ OPTYMALIZACJA: Generujemy tylko to, czego w danej chwili potrzebujesz! - 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(), app.args.no_root, false, true, &i18n, - |f_stats| { - if f_stats.weight_bytes == 0 { st_m.empty_count += 1; } - if !f_stats.path.ends_with('/') { - let ext = f_stats.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_stats.weight_bytes; - } else { - st_m.txt_count += 1; st_m.txt_weight += f_stats.weight_bytes; - } + &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_stats| { - if f_stats.weight_bytes == 0 { st_x.empty_count += 1; } - if !f_stats.path.ends_with('/') { - let ext = f_stats.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_stats.weight_bytes; - } else { - st_x.txt_count += 1; st_x.txt_weight += f_stats.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; - if is_match { app.stats_m = st_m; } else { app.stats_x = st_x; } + 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); + 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); + app.generated_paths_x = stats.render_output(app.args.view.into(), ShowMode::Exclude, false, false); } } - ui.add_space(15.0); + ui.add_space(10.0); ui.checkbox(&mut app.args.by, gt.t(GT::LabelAddFooter)); + ui.add_space(15.0); - // ⚡ Wyświetla tylko przycisk zapisu odpowiadający otwartej zakładce + // [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 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, - ); + 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 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, - ); + 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 - Shared statistics block. - // [POL]: 3. STOPKA - Współdzielony blok statystyk. + // [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 - Shared notepad UI. - // [POL]: 4. GŁÓWNY EDYTOR - Współdzielony interfejs notatnika. + + // [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); -} +} \ No newline at end of file diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index 98b23e2..dab6d3d 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -161,6 +161,8 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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); From 28c69d78f12496a8b1c28f8917d1fa56c6db0fe9 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sat, 21 Mar 2026 02:59:41 +0100 Subject: [PATCH 43/45] fix --- src/core/path_view/grid.rs | 20 ++++++++++++++------ src/core/path_view/tree.rs | 20 ++++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/core/path_view/grid.rs b/src/core/path_view/grid.rs index c0a531b..2236753 100644 --- a/src/core/path_view/grid.rs +++ b/src/core/path_view/grid.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use super::node::FileNode; -use crate::core::file_stats::weight::{self, UnitSystem, WeightConfig}; +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}; @@ -116,13 +116,21 @@ impl PathGrid { 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 { - let empty_weight = if weight_cfg.system != UnitSystem::None { - " ".repeat(7 + weight_cfg.precision) + // [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 { - String::new() + // [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), @@ -132,8 +140,8 @@ impl PathGrid { } else { DIR_ICON.to_string() }, - weight_str: empty_weight, - weight_bytes: 0, + weight_str: root_weight_str, + weight_bytes: root_bytes, children: top_nodes, }] } else { diff --git a/src/core/path_view/tree.rs b/src/core/path_view/tree.rs index 119dd25..3cdcf67 100644 --- a/src/core/path_view/tree.rs +++ b/src/core/path_view/tree.rs @@ -4,7 +4,7 @@ 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, UnitSystem, WeightConfig}; +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 { @@ -117,13 +117,21 @@ impl PathTree { 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 { - let empty_weight = if weight_cfg.system != UnitSystem::None { - " ".repeat(7 + weight_cfg.precision) + // [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 { - String::new() + // [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), @@ -133,8 +141,8 @@ impl PathTree { } else { DIR_ICON.to_string() }, - weight_str: empty_weight, - weight_bytes: 0, + weight_str: root_weight_str, + weight_bytes: root_bytes, children: top_nodes, }] } else { From cf2650086251988a009295d1a45d23e6d63dd58b Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sat, 21 Mar 2026 03:26:56 +0100 Subject: [PATCH 44/45] (finish) --- src/execute.rs | 69 ++++++++++++------ src/interfaces/cli/engine.rs | 54 +++++++++++--- src/interfaces/gui.rs | 10 +-- src/interfaces/gui/code.rs | 128 ++++++++++++++++++++++++++------- src/interfaces/gui/paths.rs | 120 ++++++++++++++++++++++++------- src/interfaces/gui/settings.rs | 15 ++-- src/interfaces/gui/shared.rs | 58 +++++++++++---- 7 files changed, 349 insertions(+), 105 deletions(-) diff --git a/src/execute.rs b/src/execute.rs index f368716..021f47f 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -44,8 +44,14 @@ where 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!( + "{}", + i18n.cli_patterns_raw(&format!("{:?}", pattern_ctx.raw)) + ); + println!( + "{}", + i18n.cli_patterns_tok(&format!("{:?}", pattern_ctx.tok)) + ); println!("---------------------------------------"); } else { println!("---------------------------------------"); @@ -53,7 +59,8 @@ where // [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"); + let matchers = + PathMatchers::new(&pattern_ctx.tok, is_case_sensitive).expect("Błąd kompilacji wzorców"); // [ENG]: 4. Scan disk. // [POL]: 4. Skanowanie dysku. @@ -78,22 +85,14 @@ where }, ); - - - - - - - - - - // [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()) + 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; @@ -103,40 +102,68 @@ where 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, + &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, + &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, + &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, + &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, + &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.x_mismatched.paths, + &path_ctx.entry_absolute, + sort_strategy, + &weight_cfg, + no_emoji, )); } } } stats -} \ No newline at end of file +} diff --git a/src/interfaces/cli/engine.rs b/src/interfaces/cli/engine.rs index 5aed822..318ccbb 100644 --- a/src/interfaces/cli/engine.rs +++ b/src/interfaces/cli/engine.rs @@ -1,6 +1,6 @@ use crate::interfaces::cli::args::{CliArgs, CliUnitSystem}; use cargo_plot::addon::TimeTag; -use cargo_plot::core::file_stats::weight::{WeightConfig, UnitSystem}; +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; @@ -26,7 +26,7 @@ pub fn run(args: CliArgs) { CliUnitSystem::Dec => UnitSystem::Decimal, }, // [POL]: Jeśli 'all' (-a) jest true, liczymy fizyczną wagę z dysku dla folderów. - dir_sum_included: !args.all, + dir_sum_included: !args.all, ..WeightConfig::default() }; @@ -100,27 +100,65 @@ pub fn run(args: CliArgs) { 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); + 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); + 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.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); + 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); + SaveFile::codes( + &output_str_txt_x, + &stats.x_mismatched.paths, + &ctx.entry_absolute, + &filepath, + &tag, + args.by, + &i18n, + &cmd_x, + &args.enter_path, + ); } } } @@ -140,4 +178,4 @@ pub fn run(args: CliArgs) { } else { println!("---------------------------------------"); } -} \ No newline at end of file +} diff --git a/src/interfaces/gui.rs b/src/interfaces/gui.rs index 4ba307f..f9b272f 100644 --- a/src/interfaces/gui.rs +++ b/src/interfaces/gui.rs @@ -44,15 +44,15 @@ pub struct CargoPlotApp { pub args: CliArgs, pub active_tab: Tab, pub active_paths_tab: PathsTab, - pub active_code_tab: CodeTab, + 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 generated_code_m: String, + pub generated_code_x: String, + pub stats_m: TreeStats, + pub stats_x: TreeStats, pub ui_scale: f32, } diff --git a/src/interfaces/gui/code.rs b/src/interfaces/gui/code.rs index 219361f..ff80b9c 100644 --- a/src/interfaces/gui/code.rs +++ b/src/interfaces/gui/code.rs @@ -2,10 +2,10 @@ 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::file_stats::weight::{WeightConfig, UnitSystem}; use cargo_plot::core::save::is_blacklisted_extension; -use cargo_plot::core::file_stats::FileStats; use cargo_plot::execute; use eframe::egui; @@ -18,7 +18,11 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // [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 }; + app.active_code_tab = if is_match { + CodeTab::Match + } else { + CodeTab::Mismatch + }; ui.separator(); @@ -27,12 +31,20 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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 }; + 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 }, + system: if app.args.unit == crate::interfaces::cli::args::CliUnitSystem::Bin { + UnitSystem::Binary + } else { + UnitSystem::Decimal + }, dir_sum_included: !app.args.all, ..WeightConfig::default() }; @@ -55,19 +67,43 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { app.args.no_emoji, &i18n, |f: &FileStats| { - if f.weight_bytes == 0 { st_m.empty_count += 1; } + 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; } + 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.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; } + 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; + } } }, ); @@ -85,29 +121,51 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // [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 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; } + 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))), + 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 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; } + 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))), + 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; } @@ -124,24 +182,42 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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 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)); + 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 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)); + 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); } @@ -162,4 +238,4 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { CodeTab::Mismatch => &mut app.generated_code_x, }; draw_editor(ui, text_buffer); -} \ No newline at end of file +} diff --git a/src/interfaces/gui/paths.rs b/src/interfaces/gui/paths.rs index faaed21..d80d6eb 100644 --- a/src/interfaces/gui/paths.rs +++ b/src/interfaces/gui/paths.rs @@ -2,10 +2,10 @@ 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::file_stats::weight::{WeightConfig, UnitSystem}; use cargo_plot::core::save::is_blacklisted_extension; -use cargo_plot::core::file_stats::FileStats; use cargo_plot::execute; use eframe::egui; @@ -18,7 +18,11 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // [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 }; + app.active_paths_tab = if is_match { + PathsTab::Match + } else { + PathsTab::Mismatch + }; ui.separator(); @@ -29,12 +33,20 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // [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 }; + 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 }, + system: if app.args.unit == crate::interfaces::cli::args::CliUnitSystem::Bin { + UnitSystem::Binary + } else { + UnitSystem::Decimal + }, dir_sum_included: !app.args.all, ..WeightConfig::default() }; @@ -57,19 +69,43 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { app.args.no_emoji, &i18n, |f: &FileStats| { - if f.weight_bytes == 0 { st_m.empty_count += 1; } + 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; } + 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.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; } + 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; + } } }, ); @@ -85,24 +121,34 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { app.stats_x = st_x; if is_match { - app.generated_paths_m = stats.render_output(app.args.view.into(), ShowMode::Include, false, false); + 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); + 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 }; + + 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); @@ -112,18 +158,42 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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 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); + 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 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); + cargo_plot::core::save::SaveFile::paths( + &app.generated_paths_x, + &filepath, + &tag, + app.args.by, + &i18n, + &cmd_string, + &app.args.enter_path, + ); } } }); @@ -133,7 +203,7 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { // [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); + draw_footer(ui, "paths_stats_footer", current_stats); // [ENG]: 4. MAIN EDITOR - Generated content area. // [POL]: 4. GŁÓWNY EDYTOR - Obszar wygenerowanej treści. @@ -142,4 +212,4 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { PathsTab::Mismatch => &mut app.generated_paths_x, }; draw_editor(ui, text_buffer); -} \ No newline at end of file +} diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index dab6d3d..24e8399 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -85,14 +85,15 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { 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 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(), diff --git a/src/interfaces/gui/shared.rs b/src/interfaces/gui/shared.rs index db39338..dd0a7bb 100644 --- a/src/interfaces/gui/shared.rs +++ b/src/interfaces/gui/shared.rs @@ -78,28 +78,60 @@ pub fn draw_tabs(ui: &mut egui::Ui, gt: &GuiI18n, is_match: &mut bool) { // [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) { +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) } + 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(); + 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("🚫 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.label(format!("🕳️ Empty: {}", stats.empty_count)); + ui.separator(); + ui.label(format!( + "🎯 Matched: {} / {}", + stats.matched_count, stats.total_count + )); }); ui.add_space(5.0); }); From b94200aff3581f5ab4d6dd12d955341452b50327 Mon Sep 17 00:00:00 2001 From: GEPIDEN Date: Sat, 21 Mar 2026 03:54:39 +0100 Subject: [PATCH 45/45] cargo-plot v0.2.0 --- CHANGELOG.md | 129 + Cargo.toml | 4 +- ...lpha-1_2026Q1D076W12_Tue17Mar_122807021.md | 3033 ---------- ...-0-2-0_2026Q1D080W12_Sat21Mar_033235486.md | 5265 +++++++++++++++++ README.md | 920 +-- src/core/save.rs | 2 +- src/interfaces/gui/settings.rs | 2 +- 7 files changed, 5470 insertions(+), 3885 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_122807021.md create mode 100644 CargoPlot-0-2-0_2026Q1D080W12_Sat21Mar_033235486.md 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 2d36891..3794c56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-plot" -version = "0.2.0-beta" +version = "0.2.0" authors = ["Jan Roman Cisowski „j-Cis”"] edition = "2024" rust-version = "1.94.0" @@ -22,7 +22,7 @@ chrono = "0.4.44" walkdir = "2.5.0" regex = "1.12.3" clap = { version = "4.5.60", features = ["derive"] } -cliclack = "0.4.1" +cliclack = "0.5.0" colored = "3.1.1" console = "0.16.3" ctrlc = "3.5.2" diff --git a/CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_122807021.md b/CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_122807021.md deleted file mode 100644 index 073798e..0000000 --- a/CargoPlot-0-2-0-alpha-1_2026Q1D076W12_Tue17Mar_122807021.md +++ /dev/null @@ -1,3033 +0,0 @@ -```plaintext -✅ - └──┬ 📂 cargo-plot-2 ./cargo-plot-2/ -[ kB 1.373] ├──• ⚙️ Cargo.toml ./Cargo.toml -[ kB 95.32] └──┬ 📂 src ./src/ -[ B 70.00] ├──• 🦀 addon.rs ./src/addon.rs -[ kB 2.490] ├──┬ 📂 addon ./src/addon/ -[ kB 2.490] │ └──• 🦀 time_tag.rs ./src/addon/time_tag.rs -[ B 132.0] ├──• 🦀 core.rs ./src/core.rs -[ kB 62.87] ├──┬ 📂 core ./src/core/ -[ B 561.0] │ ├──• 🦀 by.rs ./src/core/by.rs -[ kB 1.144] │ ├──• 🦀 file_stats.rs ./src/core/file_stats.rs -[ kB 3.156] │ ├──┬ 📂 file_stats ./src/core/file_stats/ -[ kB 3.156] │ │ └──• 🦀 weight.rs ./src/core/file_stats/weight.rs -[ B 285.0] │ ├──• 🦀 path_matcher.rs ./src/core/path_matcher.rs -[ kB 23.55] │ ├──┬ 📂 path_matcher ./src/core/path_matcher/ -[ kB 15.00] │ │ ├──• 🦀 matcher.rs ./src/core/path_matcher/matcher.rs -[ kB 4.610] │ │ ├──• 🦀 sort.rs ./src/core/path_matcher/sort.rs -[ kB 3.938] │ │ └──• 🦀 stats.rs ./src/core/path_matcher/stats.rs -[ B 101.0] │ ├──• 🦀 path_store.rs ./src/core/path_store.rs -[ kB 4.234] │ ├──┬ 📂 path_store ./src/core/path_store/ -[ kB 2.119] │ │ ├──• 🦀 context.rs ./src/core/path_store/context.rs -[ kB 2.115] │ │ └──• 🦀 store.rs ./src/core/path_store/store.rs -[ B 313.0] │ ├──• 🦀 path_view.rs ./src/core/path_view.rs -[ kB 22.08] │ ├──┬ 📂 path_view ./src/core/path_view/ -[ kB 9.936] │ │ ├──• 🦀 grid.rs ./src/core/path_view/grid.rs -[ kB 2.560] │ │ ├──• 🦀 list.rs ./src/core/path_view/list.rs -[ kB 2.589] │ │ ├──• 🦀 node.rs ./src/core/path_view/node.rs -[ kB 7.001] │ │ └──• 🦀 tree.rs ./src/core/path_view/tree.rs -[ kB 1.724] │ ├──• 🦀 patterns_expand.rs ./src/core/patterns_expand.rs -[ kB 5.713] │ └──• 🦀 save.rs ./src/core/save.rs -[ kB 5.698] ├──• 🦀 execute.rs ./src/execute.rs -[ kB 7.397] ├──• 🦀 i18n.rs ./src/i18n.rs -[ B 148.0] ├──• 🦀 interfaces.rs ./src/interfaces.rs -[ kB 11.03] ├──┬ 📂 interfaces ./src/interfaces/ -[ kB 1.104] │ ├──• 🦀 cli.rs ./src/interfaces/cli.rs -[ kB 9.929] │ └──┬ 📂 cli ./src/interfaces/cli/ -[ kB 4.564] │ ├──• 🦀 args.rs ./src/interfaces/cli/args.rs -[ kB 5.365] │ └──• 🦀 engine.rs ./src/interfaces/cli/engine.rs -[ B 75.00] ├──• 🦀 lib.rs ./src/lib.rs -[ kB 1.105] ├──• 🦀 main.rs ./src/main.rs -[ B 79.00] ├──• 🦀 output.rs ./src/output.rs -[ B 46.00] ├──• 🦀 theme.rs ./src/theme.rs -[ kB 4.183] └──┬ 📂 theme ./src/theme/ -[ B 837.0] ├──• 🦀 for_path_list.rs ./src/theme/for_path_list.rs -[ kB 3.346] └──• 🦀 for_path_tree.rs ./src/theme/for_path_tree.rs -``` - -### 001: `./Cargo.toml` - -```toml -[package] -name = "cargo-plot" -version = "0.2.0-alpha.1" -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.4.1" -colored = "3.1.1" -console = "0.16.3" -ctrlc = "3.5.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 by; -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/by.rs` - -```rust -use super::super::i18n::I18n; -use std::env; - -pub struct BySection; - -impl BySection { - #[must_use] - pub fn generate(tag: &str, typ: &str, i18n: &I18n) -> String { - let args: Vec = env::args().collect(); - let command = args.join(" "); - - format!( - "\n\n---\n---\n\n{}\n\n{}\n\n```bash\n{}\n```\n\n{}\n\n{}\n\n{}\n\n---\n", - i18n.by_title(typ), - i18n.by_cmd(), - command, - i18n.by_instructions(), - i18n.by_link(), - i18n.by_version(tag) - ) - } -} - -``` - -### 006: `./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, - } - } -} - -``` - -### 007: `./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(); - } - - 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}] ") -} - -``` - -### 008: `./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}; - -``` - -### 009: `./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 - } -} - -``` - -### 010: `./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 - } -} - -``` - -### 011: `./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 - } -} - -``` - -### 012: `./src/core/path_store.rs` - -```rust -pub mod context; -pub mod store; - -pub use self::context::PathContext; -pub use self::store::PathStore; - -``` - -### 013: `./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, - }) - } -} - -``` - -### 014: `./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() - } -} - -``` - -### 015: `./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, -} - -``` - -### 016: `./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, UnitSystem, 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>, - ) -> 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, - ) -> 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 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)) - .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)) - .collect(); - - FileNode::sort_slice(&mut top_nodes, sort_strategy); - - let final_roots = if let Some(r_name) = root_name { - let empty_weight = if weight_cfg.system != UnitSystem::None { - " ".repeat(7 + weight_cfg.precision) - } else { - String::new() - }; - - vec![FileNode { - name: r_name.to_string(), - path: PathBuf::from(r_name), - is_dir: true, - icon: DIR_ICON.to_string(), - weight_str: empty_weight, - weight_bytes: 0, - 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 - } -} - -``` - -### 017: `./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, - ) -> 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: 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 - } -} - -``` - -### 018: `./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 - } -} - -``` - -### 019: `./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, UnitSystem, 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>, - ) -> 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, - ) -> 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 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)) - .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)) - .collect(); - - FileNode::sort_slice(&mut top_nodes, sort_strategy); - - let final_roots = if let Some(r_name) = root_name { - let empty_weight = if weight_cfg.system != UnitSystem::None { - " ".repeat(7 + weight_cfg.precision) - } else { - String::new() - }; - - vec![FileNode { - name: r_name.to_string(), - path: PathBuf::from(r_name), - is_dir: true, - icon: DIR_ICON.to_string(), - weight_str: empty_weight, - weight_bytes: 0, - 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 - } -} - -``` - -### 020: `./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()] - } -} - -``` - -### 021: `./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 { - /// 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, by_section: &str, i18n: &I18n) { - let markdown_content = format!("```plaintext\n{}\n```\n\n{}{}", content, 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, - by_section: &str, - i18n: &I18n, - ) { - let mut content = String::new(); - - // 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{}{}", 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. -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" - ) -} - -``` - -### 022: `./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; - -/// [POL]: Egzekutor operujący na wielu wzorcach (wersja po rozwinięciu klamer/tokenizacji). -/// [ENG]: Executor operating on multiple patterns (post brace expansion/tokenisation). -pub fn execute( - enter_path: &str, - patterns: &[String], - is_case_sensitive: bool, - sort_strategy: SortStrategy, - show_mode: ShowMode, - view_mode: ViewMode, - no_root: bool, - print_info: bool, - i18n: &crate::i18n::I18n, - mut on_match: OnMatch, - mut on_mismatch: OnMismatch, -) -> MatchStats -where - // OnMatch: FnMut(&str), - // OnMismatch: FnMut(&str), - // ⚡ Teraz callbacki oczekują bogatego obiektu, a nie tylko tekstu - OnMatch: FnMut(&FileStats), - OnMismatch: FnMut(&FileStats), -{ - // 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); - }); - - // 2. Logowanie stanu początkowego - 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!("---------------------------------------"); - } - - // 3. Budowa silników dopasowujących (Generał) - let matchers = - PathMatchers::new(&pattern_ctx.tok, is_case_sensitive).expect("Błąd kompilacji wzorców"); - - // 4. Skanowanie dysku (Getter) - // [POL]: Ładujemy dane do rejestru z rdzenia - let paths_store = PathStore::scan(&path_ctx.entry_absolute); - // [POL]: Wyciągamy PULĘ ŚCIEŻEK (Encyklopedię) - let paths_set = paths_store.get_index(); - - let entry_abs = path_ctx.entry_absolute.clone(); - // 6. Zwracamy statystyki do Engine'u - let mut stats = matchers.evaluate( - &paths_store.list, - &paths_set, - sort_strategy, - show_mode, - |raw_path| { - // Pośrednik pobiera statystyki - let stats = FileStats::fetch(raw_path, &entry_abs); - on_match(&stats); - }, - |raw_path| { - // Pośrednik pobiera statystyki - let stats = FileStats::fetch(raw_path, &entry_abs); - on_mismatch(&stats); - }, - ); - - // 7. ⚡ MAGIA BUDOWANIA WIDOKÓW - let weight_cfg = WeightConfig::default(); - let root_name = if no_root { - None - } else { - Path::new(&path_ctx.entry_absolute) - .file_name() - .and_then(|n| n.to_str()) - }; - - // Pomocnicze flagi do budowania (żeby kod w match był krótki) - let do_include = show_mode == ShowMode::Include || show_mode == ShowMode::Context; - let do_exclude = show_mode == ShowMode::Exclude || show_mode == ShowMode::Context; - - // ⚡ Czysty match dla widoków (Grid, Tree, List) - 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, - )); - } - if do_exclude { - stats.x_mismatched.grid = Some(PathGrid::build( - &stats.x_mismatched.paths, - &path_ctx.entry_absolute, - sort_strategy, - &weight_cfg, - root_name, - )); - } - } - 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, - )); - } - if do_exclude { - stats.x_mismatched.tree = Some(PathTree::build( - &stats.x_mismatched.paths, - &path_ctx.entry_absolute, - sort_strategy, - &weight_cfg, - root_name, - )); - } - } - ViewMode::List => { - if do_include { - stats.m_matched.list = Some(PathList::build( - &stats.m_matched.paths, - &path_ctx.entry_absolute, - sort_strategy, - &weight_cfg, - )); - } - if do_exclude { - stats.x_mismatched.list = Some(PathList::build( - &stats.x_mismatched.paths, - &path_ctx.entry_absolute, - sort_strategy, - &weight_cfg, - )); - } - } - } - - stats -} - -``` - -### 023: `./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 by_title(&self, typ: &str) -> String { - match self.lang { - Lang::Pl => format!("## Command - Query ({typ})"), - Lang::En => format!("## Command - Query ({typ})"), - } - } - pub fn by_cmd(&self) -> &'static str { - match self.lang { - Lang::Pl => "**Wywołana komenda:**", - Lang::En => "**Executed command:**", - } - } - pub fn by_instructions(&self) -> &'static str { - match self.lang { - Lang::Pl => { - "**Krótka instrukcja flag:**\n- `-d, --dir ` : Ścieżka wejściowa do skanowania (domyślnie: `.`)\n- `-p, --pat ...` : Wzorce dopasowań (wymagane)\n- `-s, --sort ` : Strategia sortowania (np. `az-file-merge`)\n- `-v, --view ` : Widok wyników (`tree`, `list`, `grid`)\n- `-m, --on-match` : Pokaż tylko dopasowane ścieżki\n- `-x, --on-mismatch` : Pokaż tylko odrzucone ścieżki\n- `-o, --out-paths [PATH]` : Zapisz ścieżki do pliku (AUTO: `./other/`)\n- `-c, --out-cache [PATH]` : Zapisz kod do pliku (AUTO: `./other/`)\n- `-i, --info` : Tryb gadatliwy w terminalu\n- `-b, --by` : Dodaj sekcję informacyjną na końcu pliku\n- `--ignore-case` : Ignoruj wielkość liter we wzorcach\n- `--treeview-no-root` : Ukryj główny folder w widoku drzewa" - } - Lang::En => { - "**Short flags manual:**\n- `-d, --dir ` : Input path to scan (default: `.`)\n- `-p, --pat ...` : Match patterns (required)\n- `-s, --sort ` : Sorting strategy (e.g. `az-file-merge`)\n- `-v, --view ` : Results view (`tree`, `list`, `grid`)\n- `-m, --on-match` : Show only matched paths\n- `-x, --on-mismatch` : Show only rejected paths\n- `-o, --out-paths [PATH]` : Save paths to file (AUTO: `./other/`)\n- `-c, --out-cache [PATH]` : Save code to file (AUTO: `./other/`)\n- `-i, --info` : Verbose terminal mode\n- `-b, --by` : Add info section at end of file\n- `--ignore-case` : Ignore case in patterns\n- `--treeview-no-root` : Hide root directory in tree view" - } - } - } - pub fn by_link(&self) -> &'static str { - match self.lang { - Lang::Pl => { - "[📊 Sprawdź `cargo-plot` na crates.io](https://crates.io/crates/cargo-plot)" - } - Lang::En => "[📊 Check `cargo-plot` on crates.io](https://crates.io/crates/cargo-plot)", - } - } - pub fn by_version(&self, tag: &str) -> String { - match self.lang { - Lang::Pl => format!("**Wersja raportu:** {tag}"), - Lang::En => format!("**Report version:** {tag}"), - } - } - - // ===================================================================== - // 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), - } - } - - // ===================================================================== - // 4. TUI - INTERAKTYWNY PANEL - // ===================================================================== - // (Zostawiamy tu miejsce na przyszłość) -} - -``` - -### 024: `./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 tui; - -``` - -### 025: `./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. -// [POL]: Główny punkt wejścia dla interfejsu CLI. -pub fn run_cli() { - // [POL]: Pobieramy surowe argumenty bezpośrednio z systemu. - let args_os = std::env::args(); - let mut args: Vec = args_os.collect(); - - // [ENG]: Injection trick: If run via 'cargo run -- -d...', 'plot' is missing. - // We insert it manually so the parser matches the Cargo plugin structure. - // [POL]: Trik z wstrzyknięciem: Jeśli uruchomiono przez 'cargo run -- -d...', brakuje 'plot'. - // Wstawiamy go ręcznie, aby parser pasował do struktury wtyczki Cargo. - if args.len() > 1 && args[1] != "plot" { - args.insert(1, "plot".to_string()); - } - - // [ENG]: Now parse from the modified list. - // [POL]: Teraz parsujemy ze zmodyfikowanej listy. - let CargoCli::Plot(flags) = CargoCli::parse_from(args); - - // [ENG]: Transfer control to our execution engine. - // [POL]: Przekazanie kontroli do naszego silnika wykonawczego. - engine::run(flags); -} - -``` - -### 026: `./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}; - -/// [POL]: Główny wrapper dla wtyczki Cargo. -/// Oszukuje clap'a, mówiąc mu: "Główny program nazywa się 'cargo', a 'plot' to jego subkomenda". -#[derive(Parser, Debug)] -#[command(name = "cargo", bin_name = "cargo")] -pub enum CargoCli { - /// [ENG]: Cargo plot subcommand. - /// [POL]: Podkomenda cargo plot. - Plot(CliArgs), -} - -/// [POL]: Nasze docelowe argumenty CLI. Zauważ, że teraz to jest `Args`, a nie `Parser`. -#[derive(Args, Debug)] -#[command(author, version, about = "Zaawansowany skaner struktury plików", 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]: Match patterns. - /// [POL]: Wzorce dopasowań. - #[arg(short = 'p', long = "pat", required = true)] - pub patterns: Vec, - - /// [ENG]: Results sorting strategy. - /// [POL]: Strategia sortowania wyników. - #[arg(short = 's', long = "sort", value_enum, default_value_t = CliSortStrategy::AzFileMerge)] - pub sort: CliSortStrategy, - - /// [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]: Display only matched paths. - /// [POL]: Wyświetlaj tylko dopasowane ścieżki. - #[arg(short = 'm', long = "on-match")] - pub include: bool, - - /// [ENG]: Display only rejected paths. - /// [POL]: Wyświetlaj tylko odrzucone ścieżki. - #[arg(short = 'x', long = "on-mismatch")] - pub exclude: bool, - - // ⚡ FLAGA ZAPISU ŚCIEŻEK (MARKDOWN) - /// [POL]: Opcjonalna ścieżka do pliku, w którym zostanie zapisany wynik. - #[arg(short = 'o', long = "out-paths", num_args = 0..=1, default_missing_value = "AUTO")] - pub out_path: Option, - - // ⚡ NOWA FLAGA ZAPISU KODU (MARKDOWN) - /// [POL]: Opcjonalna ścieżka do pliku z kodem (cache). Samo -c wygeneruje domyślną ścieżkę w ./other/ - #[arg(short = 'c', long = "out-cache", num_args = 0..=1, default_missing_value = "AUTO")] - pub out_code: Option, - - // ⚡ FLAGA BY (STOPKA) - /// [POL]: Dodaje sekcję informacyjną z wywołaną komendą na dole pliku. - #[arg(short = 'b', long = "by", default_value_t = false)] - pub by: bool, - - /// [ENG]: Ignore case. - /// [POL]: Ignoruj wielkość liter. - #[arg(long = "ignore-case")] - pub ignore_case: bool, - - /// [POL]: Ukrywa główny folder (root) w widoku drzewa. - #[arg(long = "treeview-no-root", default_value_t = false)] - pub no_root: bool, - - /// [POL]: Wyświetla dodatkowe informacje, statystyki i nagłówki (tryb gadatliwy). - #[arg(short = 'i', long = "info", default_value_t = false)] - pub info: bool, - - /// [POL]: Wymusza język interfejsu (pl / en). Domyślnie pobiera z systemu. - #[arg(long, value_enum)] - pub lang: Option, -} - -#[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] -pub enum CliViewMode { - Tree, - List, - Grid, -} - -#[derive(Debug, Clone, Copy, ValueEnum)] -pub enum CliSortStrategy { - None, - Az, - Za, - AzFile, - ZaFile, - AzDir, - ZaDir, - AzFileMerge, - ZaFileMerge, - AzDirMerge, - ZaDirMerge, -} - -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, - } - } -} - -``` - -### 027: `./src/interfaces/cli/engine.rs` - -```rust -use crate::interfaces::cli::args::CliArgs; -use cargo_plot::addon::TimeTag; -use cargo_plot::core::by::BySection; -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; -// use cargo_plot::theme::for_path_list::get_icon_for_path; - -/// [ENG]: The execution engine (Cockpit). -/// [POL]: Silnik wykonawczy (Kokpit). -pub fn run(args: CliArgs) { - 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(); - - let show_mode = match (args.include, args.exclude) { - (true, false) => ShowMode::Include, // Tylko flaga -m - (false, true) => ShowMode::Exclude, // Tylko flaga -x - _ => ShowMode::Context, // Brak flag (lub podane obie) = pokazujemy wszystko - }; - - let stats = execute::execute( - &args.enter_path, - &args.patterns, - is_case_sensitive, - sort_strategy, - show_mode, - view_mode, - args.no_root, - args.info, - &i18n, - |_| {}, // Closure są puste, bo renderujemy PO zebraniu statystyk - |_| {}, - // |file_stat| { - // if !args.treeview { - // println!( - // "✅ MATCH: {} {} ({} B)", - // get_icon_for_path(&file_stat.path), - // file_stat.path, - // file_stat.weight_bytes - // ); - // } - // }, - // |file_stat| { - // if !args.treeview && show_exclude { - // println!( - // "❌ REJECT: {} {} ({} B)", - // get_icon_for_path(&file_stat.path), - // file_stat.path, - // file_stat.weight_bytes - // ); - // } - // }, - ); - - // 2. RENDEROWANIE WYNIKÓW - let output_str_cli = stats.render_output(view_mode, show_mode, args.info, true); - print!("{}", output_str_cli); - - let has_out_paths = args.out_path.is_some(); - let has_out_codes = args.out_code.is_some(); - - if has_out_paths || has_out_codes { - let tag = TimeTag::now(); - let internal_tag = if args.by { "" } else { &tag }; - - // let by_content = if args.by { - // BySection::generate(&tag) - // } else { - // String::new() - // }; - - let output_str_txt = stats.render_output(view_mode, show_mode, args.info, false); - - // Closure do automatycznego generowania ścieżki - let resolve_filepath = |val: &str, prefix: &str| -> String { - if val == "AUTO" { - format!("./other/{}_{}.md", prefix, tag) - } else if val.ends_with('/') || val.ends_with('\\') { - format!("{}{}_{}.md", val, prefix, tag) - } else { - let path = std::path::Path::new(val); - 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 parent_str = parent.to_string_lossy().replace('\\', "/"); - let ext_str = if ext.is_empty() { - String::new() - } else { - format!(".{}", ext) - }; - let stem_str = if stem.is_empty() { prefix } else { &stem }; - if parent_str.is_empty() { - format!("{}_{}{}", stem_str, tag, ext_str) - } else { - format!("{}/{}_{}{}", parent_str, stem_str, tag, ext_str) - } - } - }; - - if let Some(val) = &args.out_path { - let filepath = resolve_filepath(val, "paths"); - let by_content = if args.by { - BySection::generate(&tag, "paths", &i18n) - } else { - String::new() - }; - SaveFile::paths(&output_str_txt, &filepath, internal_tag, &by_content, &i18n); - } - - if let Some(val) = &args.out_code { - let filepath = resolve_filepath(val, "cache"); - if let Ok(ctx) = PathContext::resolve(&args.enter_path) { - let by_content = if args.by { - BySection::generate(&tag, "codes", &i18n) - } else { - String::new() - }; - SaveFile::codes( - &output_str_txt, - &stats.m_matched.paths, - &ctx.entry_absolute, - &filepath, - internal_tag, - &by_content, - &i18n, - ); - } - } - } - - // 3. PODSUMOWANIE - if args.info { - println!("---------------------------------------"); - // ⚡ PODMIENIONO NA WYWOŁANIA Z I18N - println!("{}", i18n.cli_summary_matched(stats.m_size_matched, stats.total)); - println!("{}", i18n.cli_summary_rejected(stats.x_size_mismatched, stats.total)); - } else { - println!("---------------------------------------"); - } -} - -``` - -### 028: `./src/lib.rs` - -```rust -pub mod addon; -pub mod core; -pub mod execute; -pub mod i18n; -pub mod theme; - -``` - -### 029: `./src/main.rs` - -```rust -// [ENG]: Main entry point switching between interactive TUI and automated CLI. -// [POL]: Główny punkt wejścia przełączający między interaktywnym TUI a automatycznym CLI. - -#![allow(clippy::pedantic, clippy::struct_excessive_bools)] - -use std::env; -mod interfaces; - -fn main() { - // Rejestrujemy pusty handler Ctrl+C. - // Dzięki temu system nie zabije programu natychmiast, a `cliclack` - // przejmie sygnał i bezpiecznie wyjdzie z prompta. - ctrlc::set_handler(move || {}).expect("Błąd podczas ustawiania handlera Ctrl+C"); - - let args: Vec = env::args().collect(); - - // [POL]: Uruchom TUI tylko jeśli: - // 1. Brak argumentów (tylko nazwa pliku binarnego) -> len == 1 - // 2. Wywołanie subkomendy bez flag (cargo-plot plot) -> len == 2 && args[1] == "plot" - let is_tui = args.len() == 1 || (args.len() == 2 && args[1] == "plot"); - - if is_tui { - interfaces::tui::run_tui(); - return; // ⚡ ODKOMENTOWANE: Zapobiega odpaleniu CLI po wyjściu z TUI - } - - // Wszystko inne (w tym --help) trafia do parsera CLI - interfaces::cli::run_cli(); -} - -``` - -### 030: `./src/output.rs` - -```rust -pub mod save_path; -pub mod save_code; -pub mod generator; -//pub use save_path -``` - -### 031: `./src/theme.rs` - -```rust -pub mod for_path_list; -pub mod for_path_tree; - -``` - -### 032: `./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 - } -} - -``` - -### 033: `./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(), - } - } -} - -``` - - - - - ---- ---- - -## Command - Query (codes) - -**Executed command:** - -```bash -target\debug\cargo-plot.exe -d ./ -p ./src/+ -p !@tui{.rs,/}+ -p ./Cargo.toml -s az-file-merge -v grid -m -c -o -b -i --lang en -``` - -**Short flags manual:** -- `-d, --dir ` : Input path to scan (default: `.`) -- `-p, --pat ...` : Match patterns (required) -- `-s, --sort ` : Sorting strategy (e.g. `az-file-merge`) -- `-v, --view ` : Results view (`tree`, `list`, `grid`) -- `-m, --on-match` : Show only matched paths -- `-x, --on-mismatch` : Show only rejected paths -- `-o, --out-paths [PATH]` : Save paths to file (AUTO: `./other/`) -- `-c, --out-cache [PATH]` : Save code to file (AUTO: `./other/`) -- `-i, --info` : Verbose terminal mode -- `-b, --by` : Add info section at end of file -- `--ignore-case` : Ignore case in patterns -- `--treeview-no-root` : Hide root directory in tree view - -[📊 Check `cargo-plot` on crates.io](https://crates.io/crates/cargo-plot) - -**Report version:** 2026Q1D076W12_Tue17Mar_122807021 - ---- 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/src/core/save.rs b/src/core/save.rs index 7808bb6..e8923de 100644 --- a/src/core/save.rs +++ b/src/core/save.rs @@ -13,7 +13,7 @@ impl SaveFile { f.push_str("> | Property | Value |\n"); f.push_str("> | ---: | :--- |\n"); f.push_str(&format!( - "> | **{}** | `cargo-plot v0.2.0-beta` |\n", + "> | **{}** | `cargo-plot v0.2.0` |\n", i18n.footer_tool() )); f.push_str(&format!( diff --git a/src/interfaces/gui/settings.rs b/src/interfaces/gui/settings.rs index 24e8399..16bd55b 100644 --- a/src/interfaces/gui/settings.rs +++ b/src/interfaces/gui/settings.rs @@ -19,7 +19,7 @@ pub fn show(ui: &mut egui::Ui, app: &mut CargoPlotApp) { ui.add_space(10.0); ui.horizontal(|ui| { - ui.label(egui::RichText::new("📦 cargo-plot v0.2.0-beta").strong()); + 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();