use std::fmt;
use serde::Deserialize;
use crate::layout::{LayoutNode, SplitDirection};
use crate::tab::SwapLayout;
#[derive(Debug)]
pub enum LayoutParseError {
Toml(toml::de::Error),
InvalidPaneCount { value: usize },
UnknownTemplate(String),
InvalidRatio(String),
}
impl fmt::Display for LayoutParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LayoutParseError::Toml(e) => write!(f, "TOML parse error: {e}"),
LayoutParseError::InvalidPaneCount { value } => {
write!(f, "invalid pane_count: {value}")
}
LayoutParseError::UnknownTemplate(t) => write!(f, "unknown template: {t}"),
LayoutParseError::InvalidRatio(r) => write!(f, "invalid ratio: {r}"),
}
}
}
impl std::error::Error for LayoutParseError {}
impl From<toml::de::Error> for LayoutParseError {
fn from(e: toml::de::Error) -> Self {
LayoutParseError::Toml(e)
}
}
#[derive(Debug, Deserialize)]
struct RawSwapLayout {
name: Option<String>,
pane_count: Option<usize>,
min_panes: Option<usize>,
max_panes: Option<usize>,
template: Option<String>,
direction: Option<String>,
ratio: Option<f64>,
splits: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
struct RawSwapLayoutDoc {
swap_layout: Vec<RawSwapLayout>,
}
pub fn parse_swap_layout_toml(toml_str: &str) -> Result<Vec<SwapLayout>, LayoutParseError> {
let doc: RawSwapLayoutDoc = toml::from_str(toml_str)?;
let mut layouts = Vec::new();
for (i, raw) in doc.swap_layout.into_iter().enumerate() {
let min_panes = raw.min_panes.or(raw.pane_count);
let max_panes = raw.max_panes.or(raw.pane_count);
if let Some(count) = raw.pane_count
&& count == 0
{
return Err(LayoutParseError::InvalidPaneCount { value: 0 });
}
if let Some(min) = min_panes
&& min == 0
{
return Err(LayoutParseError::InvalidPaneCount { value: 0 });
}
let name = raw.name.unwrap_or_else(|| format!("layout-{i}"));
let direction = match raw.direction.as_deref() {
Some("horizontal") => SplitDirection::Horizontal,
Some("vertical") => SplitDirection::Vertical,
None => SplitDirection::Vertical, Some(other) => {
return Err(LayoutParseError::UnknownTemplate(other.to_string()));
}
};
let ratio = if let Some(ref splits) = raw.splits {
parse_ratio_from_splits(splits)?
} else if let Some(r) = raw.ratio {
r as f32
} else if let Some(ref tmpl) = raw.template {
match tmpl.as_str() {
"vsplit" | "hsplit" => 0.5,
_ => return Err(LayoutParseError::UnknownTemplate(tmpl.clone())),
}
} else {
0.5
};
let pane_count = raw.pane_count.or(min_panes).unwrap_or(2);
let layout = build_layout_node(pane_count, direction, ratio);
layouts.push(SwapLayout {
name,
min_panes,
max_panes,
layout,
});
}
Ok(layouts)
}
fn parse_ratio_from_splits(splits: &[String]) -> Result<f32, LayoutParseError> {
if splits.len() != 2 {
return Err(LayoutParseError::InvalidRatio(format!(
"expected exactly 2 splits, got {}",
splits.len()
)));
}
let first = parse_percentage(&splits[0])?;
let _second = parse_percentage(&splits[1])?;
Ok(first)
}
fn parse_percentage(s: &str) -> Result<f32, LayoutParseError> {
let s = s.trim();
if let Some(num_str) = s.strip_suffix('%') {
num_str
.trim()
.parse::<f32>()
.map(|v| v / 100.0)
.map_err(|_| LayoutParseError::InvalidRatio(s.to_string()))
} else {
Err(LayoutParseError::InvalidRatio(s.to_string()))
}
}
fn build_layout_node(pane_count: usize, direction: SplitDirection, ratio: f32) -> LayoutNode {
if pane_count <= 1 {
LayoutNode::Leaf(100)
} else if pane_count == 2 {
LayoutNode::Split {
direction,
ratio,
first: Box::new(LayoutNode::Leaf(100)),
second: Box::new(LayoutNode::Leaf(101)),
}
} else {
let mut node = LayoutNode::Leaf(100);
for i in 1..pane_count {
let leaf_id = 100 + i as u32;
node = LayoutNode::Split {
direction,
ratio: i as f32 / (i as f32 + 1.0),
first: Box::new(node),
second: Box::new(LayoutNode::Leaf(leaf_id)),
};
}
node
}
}