diff --git a/Cargo.lock b/Cargo.lock index ca6f2f5..2576022 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,71 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -55,7 +120,7 @@ name = "async-workflows" version = "0.1.0" dependencies = [ "async-trait", - "cosmoflow", + "cosmoflow 0.5.1", "serde", "serde_json", "thiserror", @@ -95,6 +160,18 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "better-ls" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "cosmoflow 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "serde_json", + "tabled", +] + [[package]] name = "bitflags" version = "2.9.1" @@ -107,6 +184,12 @@ version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytes" version = "1.10.1" @@ -133,7 +216,7 @@ name = "chat-assistant" version = "0.1.0" dependencies = [ "async-trait", - "cosmoflow", + "cosmoflow 0.5.1", "reqwest", "serde", "serde_json", @@ -141,6 +224,66 @@ dependencies = [ "tokio", ] +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -182,6 +325,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "cosmoflow" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e196deee120db4123fce797613bc71b6caece6c8141b8a844194d345a98dc1b" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "uuid", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -347,6 +502,12 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "1.3.1" @@ -465,6 +626,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -598,6 +783,12 @@ dependencies = [ "serde", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.15" @@ -637,7 +828,7 @@ name = "llm-request-handler" version = "0.1.0" dependencies = [ "async-trait", - "cosmoflow", + "cosmoflow 0.5.1", "reqwest", "serde", "serde_json", @@ -743,6 +934,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl" version = "0.10.73" @@ -787,6 +984,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "papergrid" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6978128c8b51d8f4080631ceb2302ab51e32cc6e8615f735ee2f83fd269ae3f1" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -829,6 +1037,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1136,6 +1366,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1194,6 +1430,30 @@ dependencies = [ "libc", ] +[[package]] +name = "tabled" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e39a2ee1fbcd360805a771e1b300f78cc88fec7b8d3e2f71cd37bbf23e725c7d" +dependencies = [ + "papergrid", + "tabled_derive", + "testing_table", +] + +[[package]] +name = "tabled_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846" +dependencies = [ + "heck", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tempfile" version = "3.20.0" @@ -1207,6 +1467,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "testing_table" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "2.0.12" @@ -1397,12 +1666,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "unified-workflow" version = "0.1.0" dependencies = [ "async-trait", - "cosmoflow", + "cosmoflow 0.5.1", "rand", "serde", "serde_json", @@ -1433,6 +1708,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.17.0" @@ -1555,6 +1836,41 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.3" diff --git a/cookbook/better-ls/Cargo.toml b/cookbook/better-ls/Cargo.toml new file mode 100644 index 0000000..1ad090d --- /dev/null +++ b/cookbook/better-ls/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "better-ls" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "better-ls" +path = "src/main.rs" + +[dependencies] +cosmoflow = "0.5.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tabled = "0.20" +chrono = "0.4" +clap = { version = "4.0", features = ["derive"] } diff --git a/cookbook/better-ls/README.md b/cookbook/better-ls/README.md new file mode 100644 index 0000000..0c0af38 --- /dev/null +++ b/cookbook/better-ls/README.md @@ -0,0 +1,179 @@ +# CosmoFlow Better-LS + +An enhanced directory listing tool built with the **CosmoFlow** framework, showcasing CosmoFlow's core features and capabilities. + +## 🌟 CosmoFlow Features Showcase + +### 1. **Declarative Workflow Definition** +Using CosmoFlow's `flow!` macro to define clear workflows declaratively: + +```rust +let mut flow = flow! { + storage: MemoryStorage, + start: "input", + nodes: { + "input": InputNode::new(), + "ls": LsNode, + "output": OutputNode, + }, + routes: { + "input" - "list" => "ls", + "ls" - "output" => "output", + }, + terminals: { + "output" - "complete", + } +}; +``` + +### 2. **Type-Safe Data Flow** +Each node has strongly-typed preparation, execution, and post-processing phases: + +```rust +impl Node for LsNode { + type PrepResult = Args; // Type-safe preparation result + type ExecResult = Vec; // Type-safe execution result + type Error = NodeError; // Unified error handling + + fn prep(&mut self, store: &MemoryStorage, _context: &ExecutionContext) + -> Result { ... } + + fn exec(&mut self, prep_result: Self::PrepResult, _context: &ExecutionContext) + -> Result { ... } + + fn post(&mut self, store: &mut MemoryStorage, _prep_result: Self::PrepResult, + exec_result: Self::ExecResult, _context: &ExecutionContext) + -> Result { ... } +} +``` + +### 3. **Shared Storage Mechanism** +Safe data transfer between nodes through built-in shared storage: + +- `InputNode` parses command-line arguments and stores them in `"args"` +- `LsNode` retrieves arguments, processes directory, and stores file list in `"file_listing"` +- `OutputNode` retrieves data, formats output, and displays beautiful tables + +### 4. **Built-in Validation Mechanism** +CosmoFlow validates workflow integrity before execution: + +```rust +if let Err(e) = flow.validate() { + eprintln!("❌ Flow validation failed: {e}"); + return Err(e.into()); +} +``` + +### 5. **Flexible Routing System** +Define routes between nodes using concise syntax: + +```rust +routes: { + "input" - "list" => "ls", // InputNode connects to LsNode via "list" action + "ls" - "output" => "output", // LsNode connects to OutputNode via "output" action +} +``` + +### 6. **Built-in Validation and Error Handling** +CosmoFlow provides comprehensive validation and error handling mechanisms: + +```rust +// Built-in validation mechanism +if let Err(e) = flow.validate() { + eprintln!("❌ Flow validation failed: {e}"); + return Err(e.into()); +} + +// Unified error handling +if let Err(e) = flow.execute(&mut storage) { + eprintln!("❌ Execution failed: {e}"); + return Err(e.into()); +} +``` + +## 🚀 Usage Examples + +```bash +# Basic usage +cargo run + +# Show hidden files +cargo run -- -a + +# Sort by modification time +cargo run -- -t + +# Reverse sort order +cargo run -- -r + +# Specify directory +cargo run -- /path/to/directory + +# Combine options +cargo run -- -at /path/to/directory +``` + +## 📊 Output Features + +The program displays beautiful table output with the following characteristics: + +1. **Beautiful Table Format**: Generated with tabled crate featuring rounded borders +2. **Rich File Information**: Type, name, size, permissions, modification time, creation time +3. **Color Output**: Different file types distinguished by different colors +4. **Smart Sorting**: Support for sorting by name or time, with reverse sorting +5. **Human-readable Sizes**: Automatic conversion to KB, MB, GB units + +### Output Example + +``` +╭──────┬─────────────┬───────┬─────────────┬──────────────────┬──────────────────╮ +│ Type │ Name │ Size │ Permissions │ Modified │ Created │ +├──────┼─────────────┼───────┼─────────────┼──────────────────┼──────────────────┤ +│ DIR │ src │ │ rwxr-xr-x │ 2025-06-26 06:36 │ 2025-06-26 04:38 │ +│ FILE │ Cargo.toml │ 251 B │ rw-r--r-- │ 2025-06-26 06:32 │ 2025-06-26 04:38 │ +│ FILE │ README.md │ 4.2K │ rw-r--r-- │ 2025-06-26 07:15 │ 2025-06-26 05:20 │ +╰──────┴─────────────┴───────┴─────────────┴──────────────────┴──────────────────╯ +``` + +## 🎯 CosmoFlow Advantages + +- **🔒 Type Safety**: Compile-time assurance of data flow correctness +- **🔧 Modularity**: Each node is an independent, reusable component +- **📊 Observability**: Built-in execution tracking and context management +- **🚀 Performance**: Zero-cost abstractions with minimal runtime overhead +- **🔄 Extensibility**: Easy addition of new nodes and routes +- **🛡️ Error Handling**: Unified error handling mechanism +- **💾 Data Sharing**: Flexible shared storage backends + +## 🏗️ Architecture Highlights + +This demo project shows how to build a practical command-line tool using CosmoFlow while maintaining code clarity and maintainability. The workflow contains three core nodes: + +- **InputNode**: Command-line argument parsing and path validation +- **LsNode**: File system scanning and metadata collection +- **OutputNode**: Data formatting and beautiful display + +### Workflow Execution Process + +``` +InputNode → LsNode → OutputNode + ↓ ↓ ↓ +Parse Args Scan Dir Format Output +Validate Collect Display Table +Path Metadata +``` + +This concise three-node design embodies CosmoFlow's core philosophy: **Simple, Clear, Efficient**. Each node has a single responsibility, clear data flow, easy to understand and maintain. + +## 💡 CosmoFlow Design Advantages + +Through this practical case, we can see CosmoFlow's design advantages: + +1. **Declarative Definition**: Workflow structure is clear at a glance +2. **Type Safety**: Compile-time checks ensure correct data types +3. **Separation of Concerns**: Each node focuses on a single responsibility +4. **Data Sharing**: Safe data transfer through shared storage +5. **Error Handling**: Unified error handling mechanism +6. **Easy Extension**: Can easily add new nodes or modify processes + +This allows complex business logic to be decomposed into simple, reusable components, greatly improving code maintainability and testability. diff --git a/cookbook/better-ls/src/main.rs b/cookbook/better-ls/src/main.rs new file mode 100644 index 0000000..1c7b39f --- /dev/null +++ b/cookbook/better-ls/src/main.rs @@ -0,0 +1,107 @@ +pub mod nodes; +pub mod utils; + +use clap::Parser; +use cosmoflow::{ + flow::{FlowBackend, macros::flow}, + prelude::MemoryStorage, +}; +use serde::Serialize; + +use std::time::SystemTime; +use tabled::Tabled; + +use crate::nodes::{InputNode, LsNode, OutputNode}; + +/// CosmoFlow LS Command - Enhanced directory listing with metadata and table display +#[derive(Parser, Debug, Clone, Serialize, serde::Deserialize)] +#[command(name = "ls_command")] +#[command(about = "A feature-rich directory listing tool built with CosmoFlow")] +#[command(version = "1.0")] +pub struct Args { + /// Directory path to list (defaults to current directory) + #[arg(value_name = "PATH")] + pub path: Option, + + /// Show all files including hidden ones (starting with .) + #[arg(short = 'a', long = "all")] + pub all: bool, + + /// Sort by modification time (newest first) + #[arg(short = 't', long = "time")] + pub sort_time: bool, + + /// Reverse sort order + #[arg(short = 'r', long = "reverse")] + pub reverse: bool, + + #[arg(long = "human-readable", default_value = "true")] + /// Show raw byte sizes instead of human readable format + #[arg(long = "no-human-readable", action = clap::ArgAction::SetFalse)] + pub human_readable: bool, +} + +/// Represents a file or directory entry with comprehensive metadata +#[derive(Debug, Clone, Serialize, serde::Deserialize, Tabled)] +pub struct FileEntry { + #[tabled(rename = "Type")] + pub file_type: String, + #[tabled(rename = "Name")] + pub name: String, + #[tabled(rename = "Size")] + pub size: String, + #[tabled(rename = "Permissions")] + pub permissions: String, + #[tabled(rename = "Modified")] + pub modified: String, + #[tabled(rename = "Created")] + pub created: String, + // Hidden fields for sorting and filtering + #[tabled(skip)] + pub is_hidden: bool, + #[tabled(skip)] + pub modified_timestamp: SystemTime, + #[tabled(skip)] + pub size_bytes: u64, + #[tabled(skip)] + pub is_directory: bool, + #[tabled(skip)] + pub is_executable: bool, +} + +fn main() -> Result<(), Box> { + // Create our custom storage + let mut storage = MemoryStorage::new(); + + // Build the enhanced workflow using flow! macro - showcasing CosmoFlow's declarative syntax + let mut flow = flow! { + storage: MemoryStorage, + start: "input", + nodes: { + "input": InputNode::new(), + "ls": LsNode, + "output": OutputNode, + }, + routes: { + "input" - "list" => "ls", + "ls" - "output" => "output", + }, + terminals: { + "output" - "complete", + } + }; + + // Validate the flow - demonstrates CosmoFlow's built-in validation + if let Err(e) = flow.validate() { + eprintln!("❌ Flow validation failed: {e}"); + return Err(e.into()); + } + + // Execute the flow - showcasing CosmoFlow's execution engine + if let Err(e) = flow.execute(&mut storage) { + eprintln!("❌ Execution failed: {e}"); + return Err(e.into()); + } + + Ok(()) +} diff --git a/cookbook/better-ls/src/nodes.rs b/cookbook/better-ls/src/nodes.rs new file mode 100644 index 0000000..d0f0312 --- /dev/null +++ b/cookbook/better-ls/src/nodes.rs @@ -0,0 +1,336 @@ +use std::{fs, path::Path, time::SystemTime}; + +use clap::Parser; +use cosmoflow::{Action, ExecutionContext, Node, NodeError, SharedStore, prelude::MemoryStorage}; +use tabled::{Table, settings::Color}; + +use crate::{ + Args, FileEntry, + utils::{format_permissions, format_size, format_time}, +}; + +/// Input node that processes command line arguments using clap +pub struct InputNode { + args: Args, +} + +impl InputNode { + pub fn new() -> Self { + Self { + args: Args::parse(), + } + } +} + +impl Default for InputNode { + fn default() -> Self { + Self::new() + } +} + +impl Node for InputNode { + type PrepResult = Args; + type ExecResult = Args; + type Error = NodeError; + + fn prep( + &mut self, + _store: &MemoryStorage, + _context: &ExecutionContext, + ) -> Result { + // Return the parsed arguments + Ok(self.args.clone()) + } + + fn exec( + &mut self, + prep_result: Self::PrepResult, + _context: &ExecutionContext, + ) -> Result { + // Get directory path from arguments or default to current directory + let target_path = prep_result.path.as_deref().unwrap_or("."); + let path = Path::new(target_path); + + // Validate that the path exists and is a directory + if !path.exists() { + return Err(NodeError::ValidationError(format!( + "Path does not exist: {target_path}" + ))); + } + + if !path.is_dir() { + return Err(NodeError::ValidationError(format!( + "Path is not a directory: {target_path}" + ))); + } + + Ok(prep_result) + } + + fn post( + &mut self, + store: &mut MemoryStorage, + _prep_result: Self::PrepResult, + exec_result: Self::ExecResult, + _context: &ExecutionContext, + ) -> Result { + // Store the parsed arguments + store + .set("args".to_string(), exec_result) + .map_err(|e| NodeError::StorageError(e.to_string()))?; + + Ok(Action::simple("list")) + } + + fn name(&self) -> &str { + "InputNode" + } +} + +/// LS node that lists directory contents +pub struct LsNode; + +impl Node for LsNode { + type PrepResult = Args; + type ExecResult = Vec; + type Error = NodeError; + + fn prep( + &mut self, + store: &MemoryStorage, + _context: &ExecutionContext, + ) -> Result { + // Get the parsed arguments from storage + let args: Args = store + .get("args") + .map_err(|e| NodeError::StorageError(e.to_string()))? + .ok_or_else(|| NodeError::ValidationError("Arguments not found".to_string()))?; + + Ok(args) + } + + fn exec( + &mut self, + prep_result: Self::PrepResult, + _context: &ExecutionContext, + ) -> Result { + let target_path = prep_result.path.as_deref().unwrap_or("."); + let path = Path::new(target_path); + + // Read directory contents + let entries = fs::read_dir(path) + .map_err(|e| NodeError::ExecutionError(format!("Failed to read directory: {e}")))?; + + let mut file_entries = Vec::new(); + + for entry in entries { + let entry = entry + .map_err(|e| NodeError::ExecutionError(format!("Failed to read entry: {e}")))?; + + let metadata = entry + .metadata() + .map_err(|e| NodeError::ExecutionError(format!("Failed to read metadata: {e}")))?; + + let name = entry.file_name().to_string_lossy().to_string(); + + // Check if file is hidden (starts with .) + let is_hidden = name.starts_with('.'); + + // Skip hidden files unless --all flag is set + if is_hidden && !prep_result.all { + continue; + } + + let is_directory = metadata.is_dir(); + + // Check if file is executable + let is_executable = { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + !is_directory && (metadata.permissions().mode() & 0o111) != 0 + } + #[cfg(not(unix))] + { + !is_directory + && (name.ends_with(".exe") + || name.ends_with(".bat") + || name.ends_with(".cmd")) + } + }; + + // File type without color + let file_type = if is_directory { "DIR" } else { "FILE" }.to_string(); + + // Store original name without color + let name = name.clone(); + + let size_bytes = if is_directory { 0 } else { metadata.len() }; + + // Format size without color + let size = if is_directory { + "".to_string() + } else if prep_result.human_readable { + format_size(metadata.len()) + } else { + format!("{} B", metadata.len()) + }; + + // Enhanced permissions representation + let permissions = format_permissions(&metadata); + + // Get timestamps + let modified_timestamp = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH); + let created_timestamp = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH); + + // Format timestamps + let modified = format_time(modified_timestamp); + let created = format_time(created_timestamp); + + file_entries.push(FileEntry { + file_type, + name, + size, + permissions, + modified, + created, + is_hidden, + modified_timestamp, + size_bytes, + is_directory, + is_executable, + }); + } + + // Apply sorting based on options + if prep_result.sort_time { + // Sort by modification time + file_entries.sort_by(|a, b| { + if prep_result.reverse { + a.modified_timestamp.cmp(&b.modified_timestamp) + } else { + b.modified_timestamp.cmp(&a.modified_timestamp) + } + }); + } else { + // Default sort: directories first, then files, both alphabetically + file_entries.sort_by(|a, b| { + let a_is_dir = a.file_type.contains("DIR"); + let b_is_dir = b.file_type.contains("DIR"); + let ordering = match (a_is_dir, b_is_dir) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.name.cmp(&b.name), + }; + + if prep_result.reverse { + ordering.reverse() + } else { + ordering + } + }); + } + + Ok(file_entries) + } + + fn post( + &mut self, + store: &mut MemoryStorage, + _prep_result: Self::PrepResult, + exec_result: Self::ExecResult, + _context: &ExecutionContext, + ) -> Result { + // Store the file listing + store + .set("file_listing".to_string(), exec_result) + .map_err(|e| NodeError::StorageError(e.to_string()))?; + + Ok(Action::simple("output")) + } + + fn name(&self) -> &str { + "LsNode" + } +} + +/// Output node that formats and displays the results +pub struct OutputNode; + +impl Node for OutputNode { + type PrepResult = (Args, Vec); + type ExecResult = String; + type Error = NodeError; + + fn prep( + &mut self, + store: &MemoryStorage, + _context: &ExecutionContext, + ) -> Result { + // Get both the args and file listing + let args: Args = store + .get("args") + .map_err(|e| NodeError::StorageError(e.to_string()))? + .ok_or_else(|| NodeError::ValidationError("Arguments not found".to_string()))?; + + let file_listing: Vec = store + .get("file_listing") + .map_err(|e| NodeError::StorageError(e.to_string()))? + .ok_or_else(|| NodeError::ValidationError("File listing not found".to_string()))?; + + Ok((args, file_listing)) + } + + fn exec( + &mut self, + (_args, file_listing): Self::PrepResult, + _context: &ExecutionContext, + ) -> Result { + let mut output = String::new(); + + if file_listing.is_empty() { + output.push_str("Directory is empty\n"); + } else { + use tabled::settings::{Alignment, Style, object::Columns}; + + // Create table with proper alignment and colors using tabled's Color enum + let table_string = { + Table::new(file_listing) + .with(Style::rounded()) + .modify(tabled::settings::object::Rows::new(1..), Alignment::left()) + .modify(Columns::new(2..=2), Alignment::right()) + // Apply colors to specific columns + .modify(Columns::new(0..=0), Color::FG_BLUE) // Type column - blue for DIR + .modify(Columns::new(1..=1), Color::FG_GREEN) // Name column - green + .modify(Columns::new(2..=2), Color::FG_YELLOW) // Size column - yellow + .to_string() + }; + + output.push_str(&table_string); + } + + Ok(output) + } + + fn post( + &mut self, + store: &mut MemoryStorage, + _prep_result: Self::PrepResult, + exec_result: Self::ExecResult, + _context: &ExecutionContext, + ) -> Result { + // Store formatted output for potential reuse - demonstrating CosmoFlow's data flow + store + .set("formatted_output".to_string(), exec_result.clone()) + .map_err(|e| NodeError::StorageError(e.to_string()))?; + + // Display the formatted output + println!("{exec_result}"); + + Ok(Action::simple("complete")) + } + + fn name(&self) -> &str { + "OutputNode" + } +} diff --git a/cookbook/better-ls/src/utils.rs b/cookbook/better-ls/src/utils.rs new file mode 100644 index 0000000..7e016d3 --- /dev/null +++ b/cookbook/better-ls/src/utils.rs @@ -0,0 +1,64 @@ +use std::time::SystemTime; + +/// Format file size in human readable format +pub fn format_size(size: u64) -> String { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; + let mut size = size as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < UNITS.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + if unit_index == 0 { + format!("{} {}", size as u64, UNITS[unit_index]) + } else { + format!("{:.1} {}", size, UNITS[unit_index]) + } +} + +/// Format file permissions +pub fn format_permissions(metadata: &std::fs::Metadata) -> String { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = metadata.permissions().mode(); + let user = if mode & 0o400 != 0 { "r" } else { "-" }; + let user = format!("{}{}", user, if mode & 0o200 != 0 { "w" } else { "-" }); + let user = format!("{}{}", user, if mode & 0o100 != 0 { "x" } else { "-" }); + + let group = if mode & 0o040 != 0 { "r" } else { "-" }; + let group = format!("{}{}", group, if mode & 0o020 != 0 { "w" } else { "-" }); + let group = format!("{}{}", group, if mode & 0o010 != 0 { "x" } else { "-" }); + + let other = if mode & 0o004 != 0 { "r" } else { "-" }; + let other = format!("{}{}", other, if mode & 0o002 != 0 { "w" } else { "-" }); + let other = format!("{}{}", other, if mode & 0o001 != 0 { "x" } else { "-" }); + + format!("{user}{group}{other}") + } + #[cfg(not(unix))] + { + if metadata.permissions().readonly() { + "r--r--r--".to_string() + } else { + "rw-rw-rw-".to_string() + } + } +} + +/// Format system time to human readable string +pub fn format_time(time: SystemTime) -> String { + use std::time::UNIX_EPOCH; + + match time.duration_since(UNIX_EPOCH) { + Ok(duration) => { + let secs = duration.as_secs(); + let datetime = chrono::DateTime::from_timestamp(secs as i64, 0) + .unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap()); + datetime.format("%Y-%m-%d %H:%M").to_string() + } + Err(_) => "Unknown".to_string(), + } +}