#[derive(Debug, PartialEq)]
pub struct Layout {
pub columns: Vec<u32>,
}
impl Layout {
pub fn num_cols(&self) -> usize {
self.columns.len()
}
pub fn total_panes(&self) -> u32 {
self.columns.iter().sum()
}
}
pub fn parse_layout(arg: &str) -> Result<Layout, String> {
if arg.contains(',') {
return parse_custom_layout(arg);
}
if arg.contains('x') {
return parse_grid_spec(arg);
}
parse_numeric(arg)
}
fn parse_custom_layout(arg: &str) -> Result<Layout, String> {
let parts: Vec<&str> = arg.split(',').collect();
let mut columns = Vec::with_capacity(parts.len());
for part in &parts {
if part.is_empty() {
return Err(format!("Invalid custom layout: '{}'", arg));
}
let rows = part
.parse::<u32>()
.map_err(|_| format!("Invalid custom layout: '{}'", arg))?;
if rows == 0 {
return Err(format!(
"Each column must have >= 1 row, got 0 in: '{}'",
arg
));
}
columns.push(rows);
}
let layout = Layout { columns };
if layout.total_panes() < 2 {
return Err("Total panes must be >= 2".to_string());
}
Ok(layout)
}
fn parse_grid_spec(arg: &str) -> Result<Layout, String> {
let (cols_str, rows_str) = arg
.split_once('x')
.ok_or_else(|| format!("Invalid grid format: '{}'", arg))?;
let cols = cols_str
.parse::<u32>()
.map_err(|_| format!("Invalid grid format: '{}'", arg))?;
let rows = rows_str
.parse::<u32>()
.map_err(|_| format!("Invalid grid format: '{}'", arg))?;
if cols == 0 || rows == 0 {
return Err(format!("Grid dimensions must be >= 1, got: '{}'", arg));
}
if cols * rows < 2 {
return Err("Total panes must be >= 2".to_string());
}
Ok(Layout {
columns: vec![rows; cols as usize],
})
}
fn parse_numeric(arg: &str) -> Result<Layout, String> {
let n = arg.parse::<u32>().map_err(|_| {
format!(
"Invalid argument: '{}'. Expected a number, grid spec, or custom layout (e.g. 4, 2x3, 1,3)",
arg
)
})?;
if n < 2 {
return Err("Number of panes must be >= 2".to_string());
}
let sqrt = (n as f64).sqrt().ceil() as u32;
let mut cols = sqrt;
while n % cols != 0 {
cols += 1;
}
let rows = n / cols;
let (cols, rows) = if cols >= rows {
(cols, rows)
} else {
(rows, cols)
};
Ok(Layout {
columns: vec![rows; cols as usize],
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_layout_valid_numeric() {
let cases: &[(&str, &[u32])] = &[
("2", &[1, 1]),
("3", &[1, 1, 1]),
("4", &[2, 2]),
("5", &[1, 1, 1, 1, 1]), ("6", &[2, 2, 2]),
("9", &[3, 3, 3]),
];
for (input, expected_columns) in cases {
assert_eq!(
parse_layout(input),
Ok(Layout {
columns: expected_columns.to_vec()
}),
"input: {}",
input
);
}
}
#[test]
fn parse_layout_valid_grid() {
let cases: &[(&str, &[u32])] = &[("2x3", &[3, 3]), ("3x2", &[2, 2, 2]), ("1x4", &[4])];
for (input, expected_columns) in cases {
assert_eq!(
parse_layout(input),
Ok(Layout {
columns: expected_columns.to_vec()
}),
"input: {}",
input
);
}
}
#[test]
fn parse_layout_valid_custom() {
let cases: &[(&str, &[u32])] = &[("1,3", &[1, 3]), ("2,1,3", &[2, 1, 3]), ("1,1", &[1, 1])];
for (input, expected_columns) in cases {
assert_eq!(
parse_layout(input),
Ok(Layout {
columns: expected_columns.to_vec()
}),
"input: {}",
input
);
}
}
#[test]
fn parse_layout_invalid_cases() {
let cases = [
"abc", "0", "1", "0x3", "2x0", "1x1", "axb", ",3", "3,", "1,,3", "0,3", "1,0", "a,b",
];
for input in cases {
assert!(parse_layout(input).is_err(), "input: {}", input);
}
}
#[test]
fn parse_layout_grid_and_custom_equivalence() {
let custom = parse_layout("2,2").unwrap();
let grid = parse_layout("2x2").unwrap();
assert_eq!(custom, grid);
}
#[test]
fn layout_num_cols() {
let cases: &[(&[u32], usize)] = &[(&[1, 3], 2), (&[2, 1, 3], 3), (&[4], 1)];
for (columns, expected) in cases {
let layout = Layout {
columns: columns.to_vec(),
};
assert_eq!(layout.num_cols(), *expected);
}
}
#[test]
fn layout_total_panes() {
let cases: &[(&[u32], u32)] = &[(&[1, 3], 4), (&[2, 1, 3], 6), (&[4], 4), (&[2, 2], 4)];
for (columns, expected) in cases {
let layout = Layout {
columns: columns.to_vec(),
};
assert_eq!(layout.total_panes(), *expected);
}
}
}