feat: add interactive pane and command configuration to setup wizard

Expand the configuration wizard to allow users to define panes within
each window and specify startup commands for each pane. This provides
a complete tmux session setup during initial configuration.

- Add prompts for configuring panes in each window
- Add prompts for startup commands per pane
- Show full window/pane structure in summary
- Display pane commands in the final configuration review
This commit is contained in:
Cinco Euzebio 2026-03-01 02:21:12 -03:00
parent e0da58d114
commit 61f6a9fee3
2 changed files with 344 additions and 15 deletions

View File

@ -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<usize> {
ui::render_section_header("Scan Settings");
let input = ui::render_max_depth_prompt()?;
if input.is_empty() {
return Ok(5);
}
match input.parse::<usize>() {
Ok(n) if n > 0 => Ok(n),
_ => {
eprintln!("Invalid value, using default: 5");
Ok(5)
}
}
}
fn prompt_for_cache_enabled() -> Result<bool> {
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<u64> {
let input = ui::render_cache_ttl_prompt()?;
if input.is_empty() {
return Ok(24);
}
match input.parse::<u64>() {
Ok(n) if n > 0 => Ok(n),
_ => {
eprintln!("Invalid value, using default: 24");
Ok(24)
}
}
}
fn prompt_for_windows() -> Result<Vec<crate::session::Window>> {
ui::render_section_header("Default Session");
let input = ui::render_windows_prompt()?;
let window_names: Vec<String> = 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<Vec<String>> {
let input = ui::render_panes_prompt(window_name)?;
let pane_names: Vec<String> = 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<String> {
input
.trim()

234
src/ui.rs
View File

@ -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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
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<String> {
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())
}