Compare commits

...

3 Commits

Author SHA1 Message Date
e4b9c41c22 🔖 chore: bump version to 0.7.1 and update CHANGELOG
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2026-03-01 05:07:06 -03:00
7bafa1a305 📝 docs: add ASCII art previews for each available tmux layout in README 2026-03-01 05:07:03 -03:00
04b440ddd9 🐛 fix: ask for layout in interactive wizard when window has multiple panes
Add `render_layout_prompt` and `parse_layout_input` to ui.rs so that
the first-run wizard asks the user to choose a tmux layout (1–5 or by
name) for each window that has 2 or more panes. Previously, layout was
always silently set to None.

Also update `render_config_created` to display the chosen layout in the
post-setup summary.

Closes: layout never being set during interactive setup
2026-03-01 05:07:00 -03:00
6 changed files with 198 additions and 9 deletions

View File

@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.7.1] - 2026-03-01
### Fixed
- Interactive setup wizard now asks for a tmux layout when a window has 2 or more panes
- Layout selection shown in post-wizard summary
### Changed
- README: Added ASCII art previews for each available tmux layout
## [0.7.0] - 2026-03-01 ## [0.7.0] - 2026-03-01
### Changed ### Changed

2
Cargo.lock generated
View File

@ -864,7 +864,7 @@ dependencies = [
[[package]] [[package]]
name = "tmuxido" name = "tmuxido"
version = "0.7.0" version = "0.7.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "tmuxido" name = "tmuxido"
version = "0.7.0" version = "0.7.1"
edition = "2024" edition = "2024"
[dev-dependencies] [dev-dependencies]

View File

@ -158,11 +158,61 @@ panes = []
### Available Layouts ### Available Layouts
- `main-horizontal` - Main pane on top, others below **`main-horizontal`** — Main pane on top, others below
- `main-vertical` - Main pane on left, others on right
- `tiled` - All panes tiled ```
- `even-horizontal` - All panes in horizontal row ┌──────────────────────┐
- `even-vertical` - All panes in vertical column │ │
│ main pane │
│ │
├──────────┬───────────┤
│ pane 2 │ pane 3 │
└──────────┴───────────┘
```
**`main-vertical`** — Main pane on left, others on right
```
┌─────────────┬────────┐
│ │ pane 2 │
│ main pane ├────────┤
│ │ pane 3 │
│ ├────────┤
│ │ pane 4 │
└─────────────┴────────┘
```
**`tiled`** — All panes tiled equally
```
┌───────────┬──────────┐
│ pane 1 │ pane 2 │
├───────────┼──────────┤
│ pane 3 │ pane 4 │
└───────────┴──────────┘
```
**`even-horizontal`** — All panes side by side
```
┌────────┬────────┬────────┐
│ │ │ │
│ pane 1 │ pane 2 │ pane 3 │
│ │ │ │
└────────┴────────┴────────┘
```
**`even-vertical`** — All panes stacked
```
┌──────────────────────┐
│ pane 1 │
├──────────────────────┤
│ pane 2 │
├──────────────────────┤
│ pane 3 │
└──────────────────────┘
```
### Panes ### Panes

View File

@ -181,14 +181,19 @@ impl Config {
window_names window_names
}; };
// Configure panes for each window // Configure panes and layout for each window
let mut windows = Vec::new(); let mut windows = Vec::new();
for name in names { for name in names {
let panes = Self::prompt_for_panes(&name)?; let panes = Self::prompt_for_panes(&name)?;
let layout = if panes.len() > 1 {
ui::render_layout_prompt(&name, panes.len())?
} else {
None
};
windows.push(crate::session::Window { windows.push(crate::session::Window {
name, name,
panes, panes,
layout: None, layout,
}); });
} }
@ -348,6 +353,20 @@ mod tests {
assert_eq!(result, vec!["editor", "terminal", "server"]); assert_eq!(result, vec!["editor", "terminal", "server"]);
} }
#[test]
fn should_use_ui_parse_functions_for_layout() {
assert_eq!(ui::parse_layout_input(""), None);
assert_eq!(
ui::parse_layout_input("1"),
Some("main-horizontal".to_string())
);
assert_eq!(
ui::parse_layout_input("main-vertical"),
Some("main-vertical".to_string())
);
assert_eq!(ui::parse_layout_input("invalid"), None);
}
#[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#"

111
src/ui.rs
View File

@ -133,6 +133,12 @@ pub fn render_config_created(
println!("{}", label_style.render(" 🪟 Default Windows:")); println!("{}", label_style.render(" 🪟 Default Windows:"));
for window in windows { for window in windows {
println!(" {}", window_style.render(&format!("{}", window.name))); 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() { if !window.panes.is_empty() {
for (i, pane) in window.panes.iter().enumerate() { for (i, pane) in window.panes.iter().enumerate() {
let pane_display = if pane.is_empty() { let pane_display = if pane.is_empty() {
@ -299,6 +305,67 @@ pub fn render_panes_prompt(window_name: &str) -> Result<String> {
Ok(input.trim().to_string()) 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 /// Renders a prompt for a pane command
pub fn render_pane_command_prompt(pane_name: &str) -> Result<String> { pub fn render_pane_command_prompt(pane_name: &str) -> Result<String> {
let prompt_style = Style::new().bold(true).foreground(color_green()); let prompt_style = Style::new().bold(true).foreground(color_green());
@ -531,6 +598,50 @@ mod tests {
render_config_created(&vec!["~/Projects".to_string()], 5, true, 24, &windows); 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] #[test]
fn render_config_created_with_disabled_cache_should_not_panic() { fn render_config_created_with_disabled_cache_should_not_panic() {
let windows = vec![Window { let windows = vec![Window {