tmuxido/src/ui.rs
cinco euzebio ee2059986c 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
2026-03-01 20:15:30 -03:00

1017 lines
33 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::session::Window;
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<String> {
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 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!();
// 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 let Some(layout) = &window.layout {
println!(
"{}",
info_style.render(&format!(" └─ layout: {}", layout))
);
}
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 anytime to change these settings."
)
);
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")
);
}
/// 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 the layout of a window with multiple panes
pub fn render_layout_prompt(window_name: &str, pane_count: usize) -> Result<Option<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());
let label_style = Style::new().foreground(color_blue());
println!();
println!(
" Layout for window {} ({} panes):",
window_style.render(window_name),
label_style.render(&pane_count.to_string())
);
println!(
"{}",
hint_style.render(" Choose a pane layout (leave empty for no layout):")
);
println!(
"{}",
hint_style.render(" 1. main-horizontal — main pane on top, others below")
);
println!(
"{}",
hint_style.render(" 2. main-vertical — main pane on left, others on right")
);
println!(
"{}",
hint_style.render(" 3. tiled — all panes tiled equally")
);
println!(
"{}",
hint_style.render(" 4. even-horizontal — all panes side by side")
);
println!(
"{}",
hint_style.render(" 5. even-vertical — all panes stacked vertically")
);
print!(" {} ", prompt_style.render(" Layout (1-5 or name):"));
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(parse_layout_input(input.trim()))
}
/// Parse layout input: accepts number (1-5) or layout name; returns None for empty/invalid
pub fn parse_layout_input(input: &str) -> Option<String> {
match input.trim() {
"" => None,
"1" | "main-horizontal" => Some("main-horizontal".to_string()),
"2" | "main-vertical" => Some("main-vertical".to_string()),
"3" | "tiled" => Some("tiled".to_string()),
"4" | "even-horizontal" => Some("even-horizontal".to_string()),
"5" | "even-vertical" => Some("even-vertical".to_string()),
_ => None,
}
}
/// 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())
}
/// Parse max_depth input, returning None for empty/invalid (use default)
pub fn parse_max_depth_input(input: &str) -> Option<usize> {
let trimmed = input.trim();
if trimmed.is_empty() {
return None;
}
trimmed.parse::<usize>().ok().filter(|&n| n > 0)
}
/// Parse cache enabled input, returning None for empty (use default)
pub fn parse_cache_enabled_input(input: &str) -> Option<bool> {
let trimmed = input.trim().to_lowercase();
if trimmed.is_empty() {
return None;
}
match trimmed.as_str() {
"y" | "yes" => Some(true),
"n" | "no" => Some(false),
_ => None,
}
}
/// Parse cache TTL input, returning None for empty/invalid (use default)
pub fn parse_cache_ttl_input(input: &str) -> Option<u64> {
let trimmed = input.trim();
if trimmed.is_empty() {
return None;
}
trimmed.parse::<u64>().ok().filter(|&n| n > 0)
}
// ============================================================================
// Shortcut wizard UI
// ============================================================================
/// Render warning when the desktop environment could not be detected
pub fn render_shortcut_unknown_de() {
let warning_style = Style::new().italic(true).foreground(color_orange());
println!(
"{}",
warning_style.render(" Desktop environment not detected. Skipping shortcut setup.")
);
println!(
"{}",
warning_style.render(" Run 'tmuxido --setup-shortcut' later when your DE is active.")
);
}
/// Ask the user whether to set up a keyboard shortcut. Returns `true` if yes.
pub fn render_shortcut_setup_prompt() -> Result<bool> {
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(" Set up a keyboard shortcut to launch tmuxido from anywhere?")
);
print!(" {} ", prompt_style.render(" Set up shortcut? (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")?;
let trimmed = input.trim().to_lowercase();
Ok(trimmed != "n" && trimmed != "no")
}
/// Ask the user for a key combo (shows the default in brackets).
pub fn render_key_combo_prompt(default: &str) -> 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(&format!(
" Enter the key combo to launch tmuxido (default: {})",
default
))
);
println!(
"{}",
hint_style.render(" 💡 Example: Super+Shift+T, Super+Ctrl+P")
);
print!(
" {} ",
prompt_style.render(&format!(" Key combo [{}]:", default))
);
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())
}
/// Show a conflict warning and ask whether to use the suggested alternative.
/// Returns `true` if the user accepts the suggestion.
pub fn render_shortcut_conflict_prompt(
combo: &str,
taken_by: &str,
suggestion: &str,
) -> Result<bool> {
let warning_style = Style::new().foreground(color_orange());
let prompt_style = Style::new().bold(true).foreground(color_green());
let hint_style = Style::new().italic(true).foreground(color_dark_gray());
println!(
"{}",
warning_style.render(&format!(
" ⚠️ {} is already taken by: {}",
combo, taken_by
))
);
println!(
"{}",
hint_style.render(&format!(" Use {} instead?", suggestion))
);
print!(" {} ", prompt_style.render(" Use suggestion? (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")?;
let trimmed = input.trim().to_lowercase();
Ok(trimmed != "n" && trimmed != "no")
}
/// Render a success message after the shortcut has been written
pub fn render_shortcut_success(de: &str, combo: &str, details: &str, reload_hint: &str) {
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 info_style = Style::new().foreground(color_dark_gray());
println!();
println!("{}", success_style.render(" ⌨️ Shortcut configured!"));
println!(
" {} {}",
label_style.render("Combo:"),
value_style.render(combo)
);
println!(
" {} {}",
label_style.render("Desktop:"),
value_style.render(de)
);
println!(" {}", info_style.render(details));
println!();
println!(" {}", info_style.render(reload_hint));
println!();
}
/// Ask the user whether to install the .desktop entry and icon
pub fn render_desktop_integration_prompt() -> Result<bool> {
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(
" Install a .desktop entry so tmuxido appears in app launchers (Walker, Rofi, etc.)?"
)
);
println!(
"{}",
hint_style
.render(" Also downloads the 96×96 icon from GitHub (requires internet access).")
);
print!(
" {} ",
prompt_style.render(" Install desktop entry? (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")?;
let trimmed = input.trim().to_lowercase();
Ok(trimmed != "n" && trimmed != "no")
}
/// Render a success message after desktop integration is installed
pub fn render_desktop_integration_success(result: &crate::shortcut::DesktopInstallResult) {
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 warn_style = Style::new().italic(true).foreground(color_orange());
println!();
println!("{}", success_style.render(" 🖥️ Desktop entry installed!"));
println!(
" {} {}",
label_style.render(".desktop:"),
value_style.render(&result.desktop_path.display().to_string())
);
if result.icon_downloaded {
println!(
" {} {}",
label_style.render("icon:"),
value_style.render(&result.icon_path.display().to_string())
);
} else {
println!(
" {}",
warn_style.render("icon: download skipped (no network or curl unavailable)")
);
}
println!();
}
// ============================================================================
/// 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
pub fn parse_comma_separated_list(input: &str) -> Vec<String> {
input
.trim()
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_return_none_for_empty_max_depth() {
assert_eq!(parse_max_depth_input(""), None);
assert_eq!(parse_max_depth_input(" "), None);
}
#[test]
fn should_parse_valid_max_depth() {
assert_eq!(parse_max_depth_input("5"), Some(5));
assert_eq!(parse_max_depth_input("10"), Some(10));
assert_eq!(parse_max_depth_input(" 3 "), Some(3));
}
#[test]
fn should_return_none_for_invalid_max_depth() {
assert_eq!(parse_max_depth_input("0"), None);
assert_eq!(parse_max_depth_input("-1"), None);
assert_eq!(parse_max_depth_input("abc"), None);
assert_eq!(parse_max_depth_input("3.5"), None);
}
#[test]
fn should_return_none_for_empty_cache_enabled() {
assert_eq!(parse_cache_enabled_input(""), None);
assert_eq!(parse_cache_enabled_input(" "), None);
}
#[test]
fn should_parse_yes_as_true() {
assert_eq!(parse_cache_enabled_input("y"), Some(true));
assert_eq!(parse_cache_enabled_input("Y"), Some(true));
assert_eq!(parse_cache_enabled_input("yes"), Some(true));
assert_eq!(parse_cache_enabled_input("YES"), Some(true));
assert_eq!(parse_cache_enabled_input("Yes"), Some(true));
}
#[test]
fn should_parse_no_as_false() {
assert_eq!(parse_cache_enabled_input("n"), Some(false));
assert_eq!(parse_cache_enabled_input("N"), Some(false));
assert_eq!(parse_cache_enabled_input("no"), Some(false));
assert_eq!(parse_cache_enabled_input("NO"), Some(false));
assert_eq!(parse_cache_enabled_input("No"), Some(false));
}
#[test]
fn should_return_none_for_invalid_cache_input() {
assert_eq!(parse_cache_enabled_input("maybe"), None);
assert_eq!(parse_cache_enabled_input("true"), None);
assert_eq!(parse_cache_enabled_input("1"), None);
}
#[test]
fn should_return_none_for_empty_cache_ttl() {
assert_eq!(parse_cache_ttl_input(""), None);
assert_eq!(parse_cache_ttl_input(" "), None);
}
#[test]
fn should_parse_valid_cache_ttl() {
assert_eq!(parse_cache_ttl_input("24"), Some(24));
assert_eq!(parse_cache_ttl_input("12"), Some(12));
assert_eq!(parse_cache_ttl_input(" 48 "), Some(48));
}
#[test]
fn should_return_none_for_invalid_cache_ttl() {
assert_eq!(parse_cache_ttl_input("0"), None);
assert_eq!(parse_cache_ttl_input("-1"), None);
assert_eq!(parse_cache_ttl_input("abc"), None);
assert_eq!(parse_cache_ttl_input("12.5"), None);
}
#[test]
fn should_parse_empty_comma_list() {
let result = parse_comma_separated_list("");
assert!(result.is_empty());
}
#[test]
fn should_parse_single_item() {
let result = parse_comma_separated_list("editor");
assert_eq!(result, vec!["editor"]);
}
#[test]
fn should_parse_multiple_items() {
let result = parse_comma_separated_list("editor, terminal, server");
assert_eq!(result, vec!["editor", "terminal", "server"]);
}
#[test]
fn should_trim_whitespace_in_comma_list() {
let result = parse_comma_separated_list(" editor , terminal ");
assert_eq!(result, vec!["editor", "terminal"]);
}
#[test]
fn should_filter_empty_parts_in_comma_list() {
let result = parse_comma_separated_list("editor,,terminal");
assert_eq!(result, vec!["editor", "terminal"]);
}
#[test]
fn color_blue_should_return_expected_rgb() {
let color = color_blue();
// We can't easily test the internal RGB values, but we can verify it doesn't panic
let _ = color;
}
#[test]
fn color_functions_should_return_distinct_colors() {
// Verify all color functions return valid Color objects
let colors = vec![
color_blue(),
color_purple(),
color_light_gray(),
color_dark_gray(),
color_green(),
color_orange(),
];
// Just verify they don't panic and are distinct
assert_eq!(colors.len(), 6);
}
#[test]
fn render_section_header_should_not_panic() {
// This test verifies the function doesn't panic
// We can't capture stdout easily in unit tests without additional setup
render_section_header("Test Section");
}
#[test]
fn render_welcome_banner_should_not_panic() {
render_welcome_banner();
}
#[test]
fn render_fallback_message_should_not_panic() {
render_fallback_message();
}
#[test]
fn render_config_created_should_not_panic() {
let windows = vec![
Window {
name: "editor".to_string(),
panes: vec!["nvim .".to_string()],
layout: None,
},
Window {
name: "terminal".to_string(),
panes: vec![],
layout: None,
},
];
render_config_created(&vec!["~/Projects".to_string()], 5, true, 24, &windows);
}
#[test]
fn should_return_none_for_empty_layout_input() {
assert_eq!(parse_layout_input(""), None);
assert_eq!(parse_layout_input(" "), None);
}
#[test]
fn should_parse_layout_by_number() {
assert_eq!(parse_layout_input("1"), Some("main-horizontal".to_string()));
assert_eq!(parse_layout_input("2"), Some("main-vertical".to_string()));
assert_eq!(parse_layout_input("3"), Some("tiled".to_string()));
assert_eq!(parse_layout_input("4"), Some("even-horizontal".to_string()));
assert_eq!(parse_layout_input("5"), Some("even-vertical".to_string()));
}
#[test]
fn should_parse_layout_by_name() {
assert_eq!(
parse_layout_input("main-horizontal"),
Some("main-horizontal".to_string())
);
assert_eq!(
parse_layout_input("main-vertical"),
Some("main-vertical".to_string())
);
assert_eq!(parse_layout_input("tiled"), Some("tiled".to_string()));
assert_eq!(
parse_layout_input("even-horizontal"),
Some("even-horizontal".to_string())
);
assert_eq!(
parse_layout_input("even-vertical"),
Some("even-vertical".to_string())
);
}
#[test]
fn should_return_none_for_invalid_layout_input() {
assert_eq!(parse_layout_input("6"), None);
assert_eq!(parse_layout_input("0"), None);
assert_eq!(parse_layout_input("unknown"), None);
assert_eq!(parse_layout_input("horizontal"), None);
}
#[test]
fn render_config_created_with_disabled_cache_should_not_panic() {
let windows = vec![Window {
name: "editor".to_string(),
panes: vec![],
layout: None,
}];
render_config_created(&vec!["~/work".to_string()], 3, false, 24, &windows);
}
#[test]
fn render_shortcut_unknown_de_should_not_panic() {
render_shortcut_unknown_de();
}
#[test]
fn render_shortcut_success_should_not_panic() {
render_shortcut_success(
"Hyprland",
"Super+Shift+T",
"Added to ~/.config/hypr/bindings.conf",
"Reload Hyprland with Super+Shift+R to activate.",
);
}
#[test]
fn render_desktop_integration_success_should_not_panic() {
use crate::shortcut::DesktopInstallResult;
use std::path::PathBuf;
let result = DesktopInstallResult {
desktop_path: PathBuf::from("/home/user/.local/share/applications/tmuxido.desktop"),
icon_path: PathBuf::from(
"/home/user/.local/share/icons/hicolor/96x96/apps/tmuxido.png",
),
icon_downloaded: true,
};
render_desktop_integration_success(&result);
}
#[test]
fn render_desktop_integration_success_without_icon_should_not_panic() {
use crate::shortcut::DesktopInstallResult;
use std::path::PathBuf;
let result = DesktopInstallResult {
desktop_path: PathBuf::from("/home/user/.local/share/applications/tmuxido.desktop"),
icon_path: PathBuf::from(
"/home/user/.local/share/icons/hicolor/96x96/apps/tmuxido.png",
),
icon_downloaded: false,
};
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");
}
}