✨ feat: add first-run setup choice prompt
When no config file exists, ask the user whether to run the interactive wizard or apply sensible defaults immediately. - Add `SetupChoice` enum and `parse_setup_choice_input` (pure, tested) - Add `render_setup_choice_prompt` and `render_default_config_saved` UI helpers - Extract `Config::write_default_config` and `Config::run_wizard` from `ensure_config_exists` for clarity; routing now driven by the user's choice - 9 new tests covering all branches of `parse_setup_choice_input` and the default-config write path
This commit is contained in:
parent
2da5715a34
commit
ee2059986c
127
src/config.rs
127
src/config.rs
@ -96,50 +96,72 @@ impl Config {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Run interactive configuration wizard
|
// Ask whether to run the interactive wizard or apply sensible defaults
|
||||||
let paths = Self::prompt_for_paths()?;
|
let raw = ui::render_setup_choice_prompt()?;
|
||||||
let max_depth = Self::prompt_for_max_depth()?;
|
match ui::parse_setup_choice_input(&raw) {
|
||||||
let cache_enabled = Self::prompt_for_cache_enabled()?;
|
ui::SetupChoice::Default => {
|
||||||
let cache_ttl_hours = if cache_enabled {
|
Self::write_default_config(&config_path)?;
|
||||||
Self::prompt_for_cache_ttl()?
|
ui::render_default_config_saved(&config_path.display().to_string());
|
||||||
} else {
|
}
|
||||||
24
|
ui::SetupChoice::Wizard => {
|
||||||
};
|
Self::run_wizard(&config_path)?;
|
||||||
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,
|
|
||||||
cache_enabled,
|
|
||||||
cache_ttl_hours,
|
|
||||||
update_check_interval_hours: default_update_check_interval_hours(),
|
|
||||||
default_session: SessionConfig { windows },
|
|
||||||
};
|
|
||||||
|
|
||||||
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())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Offer to set up a keyboard shortcut (best-effort, non-fatal)
|
|
||||||
if let Err(e) = crate::shortcut::setup_shortcut_wizard() {
|
|
||||||
eprintln!("Warning: shortcut setup failed: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offer to install .desktop entry + icon (best-effort, non-fatal)
|
|
||||||
if let Err(e) = crate::shortcut::setup_desktop_integration_wizard() {
|
|
||||||
eprintln!("Warning: desktop integration failed: {}", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config_path)
|
Ok(config_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write the built-in default config to `config_path` without any prompts.
|
||||||
|
fn write_default_config(config_path: &std::path::Path) -> Result<()> {
|
||||||
|
let config = Self::default_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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the full interactive configuration wizard and offer shortcut / desktop setup at the end.
|
||||||
|
fn run_wizard(config_path: &std::path::Path) -> Result<()> {
|
||||||
|
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,
|
||||||
|
max_depth,
|
||||||
|
cache_enabled,
|
||||||
|
cache_ttl_hours,
|
||||||
|
update_check_interval_hours: default_update_check_interval_hours(),
|
||||||
|
default_session: SessionConfig { windows },
|
||||||
|
};
|
||||||
|
|
||||||
|
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()))?;
|
||||||
|
|
||||||
|
// Offer to set up a keyboard shortcut (best-effort, non-fatal)
|
||||||
|
if let Err(e) = crate::shortcut::setup_shortcut_wizard() {
|
||||||
|
eprintln!("Warning: shortcut setup failed: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offer to install .desktop entry + icon (best-effort, non-fatal)
|
||||||
|
if let Err(e) = crate::shortcut::setup_desktop_integration_wizard() {
|
||||||
|
eprintln!("Warning: desktop integration failed: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn prompt_for_paths() -> Result<Vec<String>> {
|
fn prompt_for_paths() -> Result<Vec<String>> {
|
||||||
// Render styled welcome banner
|
// Render styled welcome banner
|
||||||
ui::render_welcome_banner();
|
ui::render_welcome_banner();
|
||||||
@ -377,6 +399,35 @@ mod tests {
|
|||||||
assert_eq!(ui::parse_layout_input("invalid"), None);
|
assert_eq!(ui::parse_layout_input("invalid"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_write_default_config_to_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let config_path = dir.path().join("tmuxido.toml");
|
||||||
|
|
||||||
|
Config::write_default_config(&config_path).unwrap();
|
||||||
|
|
||||||
|
assert!(config_path.exists());
|
||||||
|
let content = std::fs::read_to_string(&config_path).unwrap();
|
||||||
|
let loaded: Config = toml::from_str(&content).unwrap();
|
||||||
|
assert!(!loaded.paths.is_empty());
|
||||||
|
assert_eq!(loaded.max_depth, 5);
|
||||||
|
assert!(loaded.cache_enabled);
|
||||||
|
assert_eq!(loaded.cache_ttl_hours, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_write_valid_toml_in_default_config() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let config_path = dir.path().join("tmuxido.toml");
|
||||||
|
|
||||||
|
Config::write_default_config(&config_path).unwrap();
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&config_path).unwrap();
|
||||||
|
// Must parse cleanly
|
||||||
|
let result: Result<Config, _> = toml::from_str(&content);
|
||||||
|
assert!(result.is_ok(), "Default config must be valid TOML");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_parse_config_with_windows_and_panes() {
|
fn should_parse_config_with_windows_and_panes() {
|
||||||
let toml_str = r#"
|
let toml_str = r#"
|
||||||
|
|||||||
131
src/ui.rs
131
src/ui.rs
@ -613,6 +613,91 @@ pub fn render_desktop_integration_success(result: &crate::shortcut::DesktopInsta
|
|||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Choices offered when no configuration file is found on first run
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum SetupChoice {
|
||||||
|
Wizard,
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the first-run setup choice input.
|
||||||
|
///
|
||||||
|
/// Accepts:
|
||||||
|
/// - `""`, `" "`, `"1"`, `"w"`, `"wizard"` → `Wizard` (default)
|
||||||
|
/// - `"2"`, `"d"`, `"default"` → `Default`
|
||||||
|
/// - anything else falls back to `Wizard`
|
||||||
|
pub fn parse_setup_choice_input(input: &str) -> SetupChoice {
|
||||||
|
match input.trim().to_lowercase().as_str() {
|
||||||
|
"2" | "d" | "default" => SetupChoice::Default,
|
||||||
|
_ => SetupChoice::Wizard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the first-run prompt asking whether to run the wizard or use defaults.
|
||||||
|
/// Returns the raw user input.
|
||||||
|
pub fn render_setup_choice_prompt() -> Result<String> {
|
||||||
|
let prompt_style = Style::new().bold(true).foreground(color_green());
|
||||||
|
let hint_style = Style::new().italic(true).foreground(color_dark_gray());
|
||||||
|
let title_style = Style::new().bold(true).foreground(color_blue());
|
||||||
|
let option_style = Style::new().foreground(color_purple());
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{}", title_style.render(" 🚀 Welcome to tmuxido!"));
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
hint_style.render(" No configuration found. How would you like to get started?")
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
option_style
|
||||||
|
.render("1. Run setup wizard — configure paths, cache, and windows interactively")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
option_style.render("2. Use default config — start immediately with sensible defaults")
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
print!(" {} ", prompt_style.render("❯ Choose (1/2):"));
|
||||||
|
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 confirmation message after the default config is written.
|
||||||
|
pub fn render_default_config_saved(config_path: &str) {
|
||||||
|
let success_style = Style::new().bold(true).foreground(color_green());
|
||||||
|
let info_style = Style::new().foreground(color_dark_gray());
|
||||||
|
let path_style = Style::new().bold(true).foreground(color_blue());
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
success_style.render(" ✅ Default configuration saved!")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" {} {}",
|
||||||
|
info_style.render("Config:"),
|
||||||
|
path_style.render(config_path)
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
info_style.render(
|
||||||
|
" ⚙️ Edit it anytime to customise your setup, or run 'tmuxido --setup-shortcut'."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/// Parse comma-separated list into Vec<String>, filtering empty items
|
/// Parse comma-separated list into Vec<String>, filtering empty items
|
||||||
pub fn parse_comma_separated_list(input: &str) -> Vec<String> {
|
pub fn parse_comma_separated_list(input: &str) -> Vec<String> {
|
||||||
input
|
input
|
||||||
@ -882,4 +967,50 @@ mod tests {
|
|||||||
};
|
};
|
||||||
render_desktop_integration_success(&result);
|
render_desktop_integration_success(&result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- SetupChoice / parse_setup_choice_input ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_wizard_for_empty_input() {
|
||||||
|
assert_eq!(parse_setup_choice_input(""), SetupChoice::Wizard);
|
||||||
|
assert_eq!(parse_setup_choice_input(" "), SetupChoice::Wizard);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_wizard_for_option_1() {
|
||||||
|
assert_eq!(parse_setup_choice_input("1"), SetupChoice::Wizard);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_wizard_for_w_aliases() {
|
||||||
|
assert_eq!(parse_setup_choice_input("w"), SetupChoice::Wizard);
|
||||||
|
assert_eq!(parse_setup_choice_input("wizard"), SetupChoice::Wizard);
|
||||||
|
assert_eq!(parse_setup_choice_input("W"), SetupChoice::Wizard);
|
||||||
|
assert_eq!(parse_setup_choice_input("WIZARD"), SetupChoice::Wizard);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_default_for_option_2() {
|
||||||
|
assert_eq!(parse_setup_choice_input("2"), SetupChoice::Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_default_for_d_aliases() {
|
||||||
|
assert_eq!(parse_setup_choice_input("d"), SetupChoice::Default);
|
||||||
|
assert_eq!(parse_setup_choice_input("default"), SetupChoice::Default);
|
||||||
|
assert_eq!(parse_setup_choice_input("D"), SetupChoice::Default);
|
||||||
|
assert_eq!(parse_setup_choice_input("DEFAULT"), SetupChoice::Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_return_wizard_for_unknown_input() {
|
||||||
|
assert_eq!(parse_setup_choice_input("banana"), SetupChoice::Wizard);
|
||||||
|
assert_eq!(parse_setup_choice_input("3"), SetupChoice::Wizard);
|
||||||
|
assert_eq!(parse_setup_choice_input("yes"), SetupChoice::Wizard);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_default_config_saved_should_not_panic() {
|
||||||
|
render_default_config_saved("/home/user/.config/tmuxido/tmuxido.toml");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user