diff --git a/Cargo.lock b/Cargo.lock index bcdc8b0..aefa939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,12 +58,33 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "cfg-if" version = "1.0.4" @@ -116,6 +137,64 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "dirs" version = "5.0.1" @@ -158,6 +237,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -174,6 +262,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" version = "2.3.0" @@ -269,9 +363,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libredox" @@ -289,6 +383,33 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "lipgloss" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c1d116ae421d84dfea8bacb5d5fcce330d8b3f03a4867cd1e4860eecd94fb4" +dependencies = [ + "crossterm", + "palette", + "strip-ansi-escapes", + "unicode-width", +] + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -301,6 +422,27 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -319,6 +461,95 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -353,6 +584,30 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -375,6 +630,15 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.3" @@ -403,6 +667,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.27" @@ -470,6 +740,58 @@ dependencies = [ "dirs 6.0.0", ] +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -547,6 +869,7 @@ dependencies = [ "anyhow", "clap", "dirs 5.0.1", + "lipgloss", "serde", "serde_json", "shellexpand", @@ -602,6 +925,18 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -614,6 +949,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -682,6 +1026,22 @@ dependencies = [ "semver", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -691,6 +1051,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 8ff527e..0603ea5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ walkdir = "2.4" anyhow = "1.0" shellexpand = "3.1" clap = { version = "4.5", features = ["derive"] } +lipgloss = "0.1" diff --git a/src/config.rs b/src/config.rs index 941fc7a..db2ff6b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ use std::fs; use std::path::PathBuf; use crate::session::SessionConfig; +use crate::ui; #[derive(Debug, Deserialize, Serialize)] pub struct Config { @@ -89,20 +90,62 @@ impl Config { ) })?; - let default_config = Self::default_config(); - let toml_string = toml::to_string_pretty(&default_config) - .context("Failed to serialize default config")?; + // Prompt user for paths interactively + let paths = Self::prompt_for_paths()?; + + let config = Config { + paths: paths.clone(), + max_depth: 5, + cache_enabled: true, + cache_ttl_hours: 24, + default_session: default_session_config(), + }; + + let toml_string = + toml::to_string_pretty(&config).context("Failed to serialize config")?; fs::write(&config_path, toml_string).with_context(|| { format!("Failed to write config file: {}", config_path.display()) })?; - eprintln!("Created default config at: {}", config_path.display()); + // Render styled success message + ui::render_config_created(&paths); } Ok(config_path) } + fn prompt_for_paths() -> Result> { + // Render styled welcome banner + ui::render_welcome_banner(); + + // Get input with styled prompt + let input = ui::render_paths_prompt()?; + let paths = Self::parse_paths_input(&input); + + if paths.is_empty() { + ui::render_fallback_message(); + Ok(vec![ + dirs::home_dir() + .unwrap_or_default() + .join("Projects") + .to_string_lossy() + .to_string(), + ]) + } else { + Ok(paths) + } + } + + fn parse_paths_input(input: &str) -> Vec { + input + .trim() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + } + fn default_config() -> Self { Config { paths: vec![ @@ -153,4 +196,46 @@ mod tests { let result: Result = toml::from_str("not valid toml ]][["); assert!(result.is_err()); } + + #[test] + fn should_parse_single_path() { + let input = "~/Projects"; + let paths = Config::parse_paths_input(input); + assert_eq!(paths, vec!["~/Projects"]); + } + + #[test] + fn should_parse_multiple_paths_with_commas() { + let input = "~/Projects, ~/work, ~/repos"; + let paths = Config::parse_paths_input(input); + assert_eq!(paths, vec!["~/Projects", "~/work", "~/repos"]); + } + + #[test] + fn should_trim_whitespace_from_paths() { + let input = " ~/Projects , ~/work "; + let paths = Config::parse_paths_input(input); + assert_eq!(paths, vec!["~/Projects", "~/work"]); + } + + #[test] + fn should_return_empty_vec_for_empty_input() { + let input = ""; + let paths = Config::parse_paths_input(input); + assert!(paths.is_empty()); + } + + #[test] + fn should_return_empty_vec_for_whitespace_only() { + let input = " "; + let paths = Config::parse_paths_input(input); + assert!(paths.is_empty()); + } + + #[test] + fn should_handle_empty_parts_between_commas() { + let input = "~/Projects,,~/work"; + let paths = Config::parse_paths_input(input); + assert_eq!(paths, vec!["~/Projects", "~/work"]); + } } diff --git a/src/lib.rs b/src/lib.rs index d1ba0a9..5e72580 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod config; pub mod deps; pub mod self_update; pub mod session; +pub mod ui; use anyhow::Result; use cache::ProjectCache; diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..7320fbb --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,108 @@ +use anyhow::{Context, Result}; +use lipgloss::{Color, Style}; +use std::io::{self, Write}; + +// Tokyo Night theme colors (as RGB tuples) +fn color_blue() -> Color { + Color::from_rgb(122, 162, 247) +} // #7AA2F7 +fn color_purple() -> Color { + Color::from_rgb(187, 154, 247) +} // #BB9AF7 +fn color_light_gray() -> Color { + Color::from_rgb(169, 177, 214) +} // #A9B1D6 +fn color_dark_gray() -> Color { + Color::from_rgb(86, 95, 137) +} // #565F89 +fn color_green() -> Color { + Color::from_rgb(158, 206, 106) +} // #9ECE6A +fn color_orange() -> Color { + Color::from_rgb(224, 175, 104) +} // #E0AF68 + +/// Renders a styled welcome screen for first-time setup +pub fn render_welcome_banner() { + let title_style = Style::new().bold(true).foreground(color_blue()); + + let subtitle_style = Style::new().foreground(color_purple()); + + let text_style = Style::new().foreground(color_light_gray()); + + let hint_style = Style::new().italic(true).foreground(color_dark_gray()); + + println!(); + println!("{}", title_style.render(" 🚀 Welcome to tmuxido!")); + println!(); + println!( + "{}", + subtitle_style.render(" 📁 Let's set up your project directories") + ); + println!(); + println!( + "{}", + text_style.render(" Please specify where tmuxido should look for your projects.") + ); + println!(); + println!( + "{}", + text_style.render(" You can add multiple paths separated by commas:") + ); + println!(); + println!( + "{}", + hint_style.render(" 💡 Example: ~/Projects, ~/work, ~/personal/repos") + ); + println!(); +} + +/// Renders a prompt asking for paths +pub fn render_paths_prompt() -> Result { + let prompt_style = Style::new().bold(true).foreground(color_green()); + + print!(" {} ", prompt_style.render("❯ Paths:")); + io::stdout().flush().context("Failed to flush stdout")?; + + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .context("Failed to read input")?; + + Ok(input.trim().to_string()) +} + +/// Renders a success message after config is created +pub fn render_config_created(paths: &[String]) { + let success_style = Style::new().bold(true).foreground(color_green()); + + let path_style = Style::new().foreground(color_blue()); + + let info_style = Style::new().foreground(color_dark_gray()); + + println!(); + println!("{}", success_style.render(" ✅ Configuration saved!")); + println!(); + println!("{}", info_style.render(" 📂 Watching directories:")); + for path in paths { + println!(" {}", path_style.render(&format!("• {}", path))); + } + println!(); + println!( + "{}", + info_style + .render(" ⚙️ You can edit ~/.config/tmuxido/tmuxido.toml later to add more paths.") + ); + println!(); +} + +/// Renders a warning when user provides no input (fallback to default) +pub fn render_fallback_message() { + let warning_style = Style::new().italic(true).foreground(color_orange()); + + println!(); + println!( + "{}", + warning_style.render(" ⚠️ No paths provided. Using default: ~/Projects") + ); +}