✨ feat: add interactive setup prompt with lipgloss styling and emojis
Add styled first-time setup UI using lipgloss with Tokyo Night theme colors. The prompt now includes emojis and better visual feedback when creating the initial configuration file. - Add new ui module with styled render functions - Prompt user for project paths interactively on first run - Parse comma-separated paths with whitespace trimming - Show styled success message with configured directories - Add lipgloss dependency for terminal styling
This commit is contained in:
+89
-4
@@ -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<Vec<String>> {
|
||||
// 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<String> {
|
||||
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<Config, _> = 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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<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
|
||||
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")
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user