From d35acdeb5587340f3f9ff0b8b55f8dc1fd137f80 Mon Sep 17 00:00:00 2001 From: cinco euzebio Date: Sat, 28 Feb 2026 20:15:27 -0300 Subject: [PATCH] refactor: extract business logic into lib.rs Move scan_from_root, scan_all_roots, get_projects, show_cache_status and launch_tmux_session from main.rs into a new src/lib.rs, making them pub so they are testable independently of the binary entrypoint. main.rs is now a thin entrypoint that imports from tmuxido:: and keeps only select_project_with_fzf (interactive subprocess, not unit-testable). Add tempfile = "3" to [dev-dependencies] in preparation for tests. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + src/lib.rs | 162 ++++++++++++++++++++++++++++++ src/main.rs | 161 +----------------------------- 4 files changed, 442 insertions(+), 161 deletions(-) create mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2b48d96..40d4f01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,28 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "getrandom" version = "0.2.16" @@ -175,6 +197,28 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -187,6 +231,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "indexmap" version = "2.12.0" @@ -194,7 +244,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -209,6 +261,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.177" @@ -225,12 +283,30 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -243,6 +319,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -261,13 +347,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] @@ -278,11 +370,24 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom", + "getrandom 0.2.16", "libredox", "thiserror 2.0.17", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "ryu" version = "1.0.20" @@ -298,6 +403,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -376,6 +487,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -426,6 +550,7 @@ dependencies = [ "serde", "serde_json", "shellexpand", + "tempfile", "toml", "walkdir", ] @@ -477,6 +602,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -499,6 +630,58 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -671,3 +854,91 @@ checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] diff --git a/Cargo.toml b/Cargo.toml index e818f1d..4a8cf24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ name = "tmuxido" version = "0.1.0" edition = "2024" +[dev-dependencies] +tempfile = "3" + [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c63048b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,162 @@ +pub mod cache; +pub mod config; +pub mod session; + +use anyhow::Result; +use cache::ProjectCache; +use config::Config; +use session::{SessionConfig, TmuxSession}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::time::UNIX_EPOCH; +use walkdir::WalkDir; + +pub fn show_cache_status(config: &Config) -> Result<()> { + if !config.cache_enabled { + println!("Cache is disabled in configuration"); + return Ok(()); + } + + if let Some(cache) = ProjectCache::load()? { + let age_seconds = cache.age_in_seconds(); + let age_hours = age_seconds / 3600; + let age_minutes = (age_seconds % 3600) / 60; + + println!("Cache status:"); + println!(" Location: {}", ProjectCache::cache_path()?.display()); + println!(" Projects cached: {}", cache.projects.len()); + println!(" Directories tracked: {}", cache.dir_mtimes.len()); + println!(" Last updated: {}h {}m ago", age_hours, age_minutes); + } else { + println!("No cache found"); + println!(" Run without --cache-status to create it"); + } + + Ok(()) +} + +pub fn get_projects(config: &Config, force_refresh: bool) -> Result> { + if !config.cache_enabled || force_refresh { + let (projects, fingerprints) = scan_all_roots(config)?; + let cache = ProjectCache::new(projects.clone(), fingerprints); + cache.save()?; + eprintln!("Cache updated with {} projects", projects.len()); + return Ok(projects); + } + + if let Some(mut cache) = ProjectCache::load()? { + // Cache no formato antigo (sem dir_mtimes) → atualizar com rescan completo + if cache.dir_mtimes.is_empty() { + eprintln!("Upgrading cache, scanning for projects..."); + let (projects, fingerprints) = scan_all_roots(config)?; + let new_cache = ProjectCache::new(projects.clone(), fingerprints); + new_cache.save()?; + eprintln!("Cache updated with {} projects", projects.len()); + return Ok(projects); + } + + let changed = cache.validate_and_update(&|root| scan_from_root(root, config))?; + if changed { + cache.save()?; + eprintln!( + "Cache updated incrementally ({} projects)", + cache.projects.len() + ); + } else { + eprintln!("Using cached projects ({} projects)", cache.projects.len()); + } + return Ok(cache.projects); + } + + // Sem cache ainda — scan completo inicial + eprintln!("No cache found, scanning for projects..."); + let (projects, fingerprints) = scan_all_roots(config)?; + let cache = ProjectCache::new(projects.clone(), fingerprints); + cache.save()?; + eprintln!("Cache updated with {} projects", projects.len()); + Ok(projects) +} + +pub fn scan_all_roots(config: &Config) -> Result<(Vec, HashMap)> { + let mut all_projects = Vec::new(); + let mut all_fingerprints = HashMap::new(); + + for path_str in &config.paths { + let path = PathBuf::from(shellexpand::tilde(path_str).to_string()); + + if !path.exists() { + eprintln!("Warning: Path does not exist: {}", path.display()); + continue; + } + + eprintln!("Scanning: {}", path.display()); + + let (projects, fingerprints) = scan_from_root(&path, config)?; + all_projects.extend(projects); + all_fingerprints.extend(fingerprints); + } + + all_projects.sort(); + all_projects.dedup(); + + Ok((all_projects, all_fingerprints)) +} + +pub fn scan_from_root( + root: &Path, + config: &Config, +) -> Result<(Vec, HashMap)> { + let mut projects = Vec::new(); + let mut fingerprints = HashMap::new(); + + for entry in WalkDir::new(root) + .max_depth(config.max_depth) + .follow_links(false) + .into_iter() + .filter_entry(|e| { + e.file_name() + .to_str() + .map(|s| !s.starts_with('.') || s == ".git") + .unwrap_or(false) + }) + { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + + if entry.file_type().is_dir() { + if entry.file_name() == ".git" { + // Projeto encontrado + if let Some(parent) = entry.path().parent() { + projects.push(parent.to_path_buf()); + } + } else { + // Registrar mtime para detecção de mudanças futuras + if let Ok(metadata) = entry.metadata() + && let Ok(modified) = metadata.modified() + { + let mtime = modified + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + fingerprints.insert(entry.path().to_path_buf(), mtime); + } + } + } + } + + Ok((projects, fingerprints)) +} + +pub fn launch_tmux_session(selected: &Path, config: &Config) -> Result<()> { + // Try to load project-specific config, fallback to global default + let session_config = SessionConfig::load_from_project(selected)? + .unwrap_or_else(|| config.default_session.clone()); + + // Create tmux session + let tmux_session = TmuxSession::new(selected); + tmux_session.create(&session_config)?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index b12b1cf..f629a88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,10 @@ -mod cache; -mod config; -mod session; - use anyhow::{Context, Result}; -use cache::ProjectCache; use clap::Parser; -use config::Config; -use session::{SessionConfig, TmuxSession}; -use std::collections::HashMap; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::{Command, Stdio}; -use std::time::UNIX_EPOCH; -use walkdir::WalkDir; +use tmuxido::config::Config; +use tmuxido::{get_projects, launch_tmux_session, show_cache_status}; #[derive(Parser, Debug)] #[command( @@ -74,141 +66,6 @@ fn main() -> Result<()> { Ok(()) } -fn show_cache_status(config: &Config) -> Result<()> { - if !config.cache_enabled { - println!("Cache is disabled in configuration"); - return Ok(()); - } - - if let Some(cache) = ProjectCache::load()? { - let age_seconds = cache.age_in_seconds(); - let age_hours = age_seconds / 3600; - let age_minutes = (age_seconds % 3600) / 60; - - println!("Cache status:"); - println!(" Location: {}", ProjectCache::cache_path()?.display()); - println!(" Projects cached: {}", cache.projects.len()); - println!(" Directories tracked: {}", cache.dir_mtimes.len()); - println!(" Last updated: {}h {}m ago", age_hours, age_minutes); - } else { - println!("No cache found"); - println!(" Run without --cache-status to create it"); - } - - Ok(()) -} - -fn get_projects(config: &Config, force_refresh: bool) -> Result> { - if !config.cache_enabled || force_refresh { - let (projects, fingerprints) = scan_all_roots(config)?; - let cache = ProjectCache::new(projects.clone(), fingerprints); - cache.save()?; - eprintln!("Cache updated with {} projects", projects.len()); - return Ok(projects); - } - - if let Some(mut cache) = ProjectCache::load()? { - // Cache no formato antigo (sem dir_mtimes) → atualizar com rescan completo - if cache.dir_mtimes.is_empty() { - eprintln!("Upgrading cache, scanning for projects..."); - let (projects, fingerprints) = scan_all_roots(config)?; - let new_cache = ProjectCache::new(projects.clone(), fingerprints); - new_cache.save()?; - eprintln!("Cache updated with {} projects", projects.len()); - return Ok(projects); - } - - let changed = cache.validate_and_update(&|root| scan_from_root(root, config))?; - if changed { - cache.save()?; - eprintln!( - "Cache updated incrementally ({} projects)", - cache.projects.len() - ); - } else { - eprintln!("Using cached projects ({} projects)", cache.projects.len()); - } - return Ok(cache.projects); - } - - // Sem cache ainda — scan completo inicial - eprintln!("No cache found, scanning for projects..."); - let (projects, fingerprints) = scan_all_roots(config)?; - let cache = ProjectCache::new(projects.clone(), fingerprints); - cache.save()?; - eprintln!("Cache updated with {} projects", projects.len()); - Ok(projects) -} - -fn scan_all_roots(config: &Config) -> Result<(Vec, HashMap)> { - let mut all_projects = Vec::new(); - let mut all_fingerprints = HashMap::new(); - - for path_str in &config.paths { - let path = PathBuf::from(shellexpand::tilde(path_str).to_string()); - - if !path.exists() { - eprintln!("Warning: Path does not exist: {}", path.display()); - continue; - } - - eprintln!("Scanning: {}", path.display()); - - let (projects, fingerprints) = scan_from_root(&path, config)?; - all_projects.extend(projects); - all_fingerprints.extend(fingerprints); - } - - all_projects.sort(); - all_projects.dedup(); - - Ok((all_projects, all_fingerprints)) -} - -fn scan_from_root(root: &Path, config: &Config) -> Result<(Vec, HashMap)> { - let mut projects = Vec::new(); - let mut fingerprints = HashMap::new(); - - for entry in WalkDir::new(root) - .max_depth(config.max_depth) - .follow_links(false) - .into_iter() - .filter_entry(|e| { - e.file_name() - .to_str() - .map(|s| !s.starts_with('.') || s == ".git") - .unwrap_or(false) - }) - { - let entry = match entry { - Ok(e) => e, - Err(_) => continue, - }; - - if entry.file_type().is_dir() { - if entry.file_name() == ".git" { - // Projeto encontrado - if let Some(parent) = entry.path().parent() { - projects.push(parent.to_path_buf()); - } - } else { - // Registrar mtime para detecção de mudanças futuras - if let Ok(metadata) = entry.metadata() { - if let Ok(modified) = metadata.modified() { - let mtime = modified - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - fingerprints.insert(entry.path().to_path_buf(), mtime); - } - } - } - } - } - - Ok((projects, fingerprints)) -} - fn select_project_with_fzf(projects: &[PathBuf]) -> Result { let mut child = Command::new("fzf") .stdin(Stdio::piped()) @@ -237,15 +94,3 @@ fn select_project_with_fzf(projects: &[PathBuf]) -> Result { Ok(PathBuf::from(selected)) } - -fn launch_tmux_session(selected: &Path, config: &Config) -> Result<()> { - // Try to load project-specific config, fallback to global default - let session_config = SessionConfig::load_from_project(selected)? - .unwrap_or_else(|| config.default_session.clone()); - - // Create tmux session - let tmux_session = TmuxSession::new(selected); - tmux_session.create(&session_config)?; - - Ok(()) -}