diff --git a/src/config.rs b/src/config.rs index db2ff6b..f781f5d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -90,15 +90,26 @@ impl Config { ) })?; - // Prompt user for paths interactively + // Run interactive configuration wizard let paths = Self::prompt_for_paths()?; + let max_depth = Self::prompt_for_max_depth()?; + let cache_enabled = Self::prompt_for_cache_enabled()?; + let cache_ttl_hours = if cache_enabled { + Self::prompt_for_cache_ttl()? + } else { + 24 + }; + let windows = Self::prompt_for_windows()?; + + // Render styled success message before moving windows + ui::render_config_created(&paths, max_depth, cache_enabled, cache_ttl_hours, &windows); let config = Config { paths: paths.clone(), - max_depth: 5, - cache_enabled: true, - cache_ttl_hours: 24, - default_session: default_session_config(), + max_depth, + cache_enabled, + cache_ttl_hours, + default_session: SessionConfig { windows }, }; let toml_string = @@ -107,9 +118,6 @@ impl Config { fs::write(&config_path, toml_string).with_context(|| { format!("Failed to write config file: {}", config_path.display()) })?; - - // Render styled success message - ui::render_config_created(&paths); } Ok(config_path) @@ -137,6 +145,107 @@ impl Config { } } + fn prompt_for_max_depth() -> Result { + ui::render_section_header("Scan Settings"); + let input = ui::render_max_depth_prompt()?; + + if input.is_empty() { + return Ok(5); + } + + match input.parse::() { + Ok(n) if n > 0 => Ok(n), + _ => { + eprintln!("Invalid value, using default: 5"); + Ok(5) + } + } + } + + fn prompt_for_cache_enabled() -> Result { + ui::render_section_header("Cache Settings"); + let input = ui::render_cache_enabled_prompt()?; + + if input.is_empty() || input == "y" || input == "yes" { + Ok(true) + } else if input == "n" || input == "no" { + Ok(false) + } else { + eprintln!("Invalid value, using default: yes"); + Ok(true) + } + } + + fn prompt_for_cache_ttl() -> Result { + let input = ui::render_cache_ttl_prompt()?; + + if input.is_empty() { + return Ok(24); + } + + match input.parse::() { + Ok(n) if n > 0 => Ok(n), + _ => { + eprintln!("Invalid value, using default: 24"); + Ok(24) + } + } + } + + fn prompt_for_windows() -> Result> { + ui::render_section_header("Default Session"); + let input = ui::render_windows_prompt()?; + + let window_names: Vec = input + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + let names = if window_names.is_empty() { + vec!["editor".to_string(), "terminal".to_string()] + } else { + window_names + }; + + // Configure panes for each window + let mut windows = Vec::new(); + for name in names { + let panes = Self::prompt_for_panes(&name)?; + windows.push(crate::session::Window { + name, + panes, + layout: None, + }); + } + + Ok(windows) + } + + fn prompt_for_panes(window_name: &str) -> Result> { + let input = ui::render_panes_prompt(window_name)?; + + let pane_names: Vec = input + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + + if pane_names.is_empty() { + // Single pane, no commands + return Ok(vec![]); + } + + // Ask for commands for each pane + let mut panes = Vec::new(); + for pane_name in pane_names { + let command = ui::render_pane_command_prompt(&pane_name)?; + panes.push(command); + } + + Ok(panes) + } + fn parse_paths_input(input: &str) -> Vec { input .trim() diff --git a/src/ui.rs b/src/ui.rs index 7320fbb..8482239 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,3 +1,4 @@ +use crate::session::Window; use anyhow::{Context, Result}; use lipgloss::{Color, Style}; use std::io::{self, Write}; @@ -72,26 +73,84 @@ pub fn render_paths_prompt() -> Result { Ok(input.trim().to_string()) } -/// Renders a success message after config is created -pub fn render_config_created(paths: &[String]) { +/// Renders a success message after config is created with all settings +pub fn render_config_created( + paths: &[String], + max_depth: usize, + cache_enabled: bool, + cache_ttl_hours: u64, + windows: &[Window], +) { let success_style = Style::new().bold(true).foreground(color_green()); - + let label_style = Style::new().foreground(color_light_gray()); + let value_style = Style::new().bold(true).foreground(color_blue()); let path_style = Style::new().foreground(color_blue()); - + let window_style = Style::new().foreground(color_purple()); let info_style = Style::new().foreground(color_dark_gray()); + let bool_enabled_style = Style::new().bold(true).foreground(color_green()); + let bool_disabled_style = Style::new().bold(true).foreground(color_orange()); println!(); println!("{}", success_style.render(" ✅ Configuration saved!")); println!(); - println!("{}", info_style.render(" 📂 Watching directories:")); + + // Project discovery section + println!("{}", label_style.render(" 📁 Project Discovery:")); + println!( + " {} {} {}", + label_style.render("Max scan depth:"), + value_style.render(&max_depth.to_string()), + label_style.render("levels") + ); + println!(); + + // Paths + println!("{}", label_style.render(" 📂 Directories:")); for path in paths { println!(" {}", path_style.render(&format!("• {}", path))); } println!(); + + // Cache settings + println!("{}", label_style.render(" 💾 Cache Settings:")); + let cache_status = if cache_enabled { + bool_enabled_style.render("enabled") + } else { + bool_disabled_style.render("disabled") + }; + println!(" {} {}", label_style.render("Status:"), cache_status); + if cache_enabled { + println!( + " {} {} {}", + label_style.render("TTL:"), + value_style.render(&cache_ttl_hours.to_string()), + label_style.render("hours") + ); + } + println!(); + + // Default session + println!("{}", label_style.render(" 🪟 Default Windows:")); + for window in windows { + println!(" {}", window_style.render(&format!("◦ {}", window.name))); + if !window.panes.is_empty() { + for (i, pane) in window.panes.iter().enumerate() { + let pane_display = if pane.is_empty() { + format!(" └─ pane {} (shell)", i + 1) + } else { + format!(" └─ pane {}: {}", i + 1, pane) + }; + println!("{}", info_style.render(&pane_display)); + } + } + } + println!(); + println!( "{}", - info_style - .render(" ⚙️ You can edit ~/.config/tmuxido/tmuxido.toml later to add more paths.") + info_style.render( + " ⚙️ You can edit ~/.config/tmuxido/tmuxido.toml anytime to change these settings." + ) ); println!(); } @@ -106,3 +165,164 @@ pub fn render_fallback_message() { warning_style.render(" ⚠️ No paths provided. Using default: ~/Projects") ); } + +/// Renders a section header for grouping related settings +pub fn render_section_header(title: &str) { + let header_style = Style::new().bold(true).foreground(color_purple()); + + println!(); + println!("{}", header_style.render(&format!(" 📋 {}", title))); +} + +/// Renders a prompt for max_depth with instructions +pub fn render_max_depth_prompt() -> Result { + let prompt_style = Style::new().bold(true).foreground(color_green()); + let hint_style = Style::new().italic(true).foreground(color_dark_gray()); + + println!( + "{}", + hint_style.render(" How many levels deep should tmuxido search for git repositories?") + ); + println!( + "{}", + hint_style.render(" Higher values = deeper search, but slower. Default: 5") + ); + print!(" {} ", prompt_style.render("❯ Max depth:")); + 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 prompt for cache_enabled with instructions +pub fn render_cache_enabled_prompt() -> Result { + let prompt_style = Style::new().bold(true).foreground(color_green()); + let hint_style = Style::new().italic(true).foreground(color_dark_gray()); + + println!( + "{}", + hint_style.render(" Enable caching to speed up project discovery?") + ); + println!( + "{}", + hint_style.render(" Cache avoids rescanning unchanged directories. Default: yes (y)") + ); + print!(" {} ", prompt_style.render("❯ Enable cache? (y/n):")); + 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_lowercase()) +} + +/// Renders a prompt for cache_ttl_hours with instructions +pub fn render_cache_ttl_prompt() -> Result { + let prompt_style = Style::new().bold(true).foreground(color_green()); + let hint_style = Style::new().italic(true).foreground(color_dark_gray()); + + println!( + "{}", + hint_style.render(" How long should the cache remain valid (in hours)?") + ); + println!( + "{}", + hint_style.render(" After this time, tmuxido will rescan your directories. Default: 24") + ); + print!(" {} ", prompt_style.render("❯ Cache TTL (hours):")); + 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 prompt for default session windows with instructions +pub fn render_windows_prompt() -> Result { + let prompt_style = Style::new().bold(true).foreground(color_green()); + let hint_style = Style::new().italic(true).foreground(color_dark_gray()); + + println!( + "{}", + hint_style.render(" What windows should be created by default in new tmux sessions?") + ); + println!( + "{}", + hint_style.render(" Enter window names separated by commas. Default: editor, terminal") + ); + println!( + "{}", + hint_style.render(" 💡 Tip: Common choices are 'editor', 'terminal', 'server', 'logs'") + ); + print!(" {} ", prompt_style.render("❯ Window names:")); + 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 prompt asking for panes in a specific window +pub fn render_panes_prompt(window_name: &str) -> Result { + let prompt_style = Style::new().bold(true).foreground(color_green()); + let hint_style = Style::new().italic(true).foreground(color_dark_gray()); + let window_style = Style::new().bold(true).foreground(color_purple()); + + println!(); + println!(" Configuring window: {}", window_style.render(window_name)); + println!( + "{}", + hint_style + .render(" Enter pane names separated by commas, or leave empty for a single pane.") + ); + println!("{}", hint_style.render(" 💡 Example: code, logs, tests")); + print!(" {} ", prompt_style.render("❯ Pane names:")); + 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 prompt for a pane command +pub fn render_pane_command_prompt(pane_name: &str) -> Result { + let prompt_style = Style::new().bold(true).foreground(color_green()); + let hint_style = Style::new().italic(true).foreground(color_dark_gray()); + let pane_style = Style::new().foreground(color_blue()); + + println!( + "{}", + hint_style.render(&format!( + " What command should run in pane '{}' on startup?", + pane_style.render(pane_name) + )) + ); + println!( + "{}", + hint_style.render(" Leave empty to run the default shell, or enter a command like 'nvim', 'npm run dev'") + ); + print!(" {} ", prompt_style.render("❯ Command:")); + 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()) +}