333 lines
9.7 KiB
Rust
333 lines
9.7 KiB
Rust
|
use itertools::Itertools;
|
||
|
use std::collections::BTreeMap;
|
||
|
|
||
|
use crate::TermFrame;
|
||
|
|
||
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||
|
pub enum TermSplit {
|
||
|
Term {
|
||
|
frame: TermFrame,
|
||
|
},
|
||
|
Horizontal {
|
||
|
left: Box<TermSplit>,
|
||
|
right: Box<TermSplit>,
|
||
|
},
|
||
|
Vertical {
|
||
|
top: Box<TermSplit>,
|
||
|
bottom: Box<TermSplit>,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
impl TermSplit {
|
||
|
fn collect_term_frames(&self, into: &mut BTreeMap<TermFrame, usize>) {
|
||
|
match self {
|
||
|
TermSplit::Term { frame } => {
|
||
|
into.entry(frame.clone())
|
||
|
.and_modify(|v| *v += 1)
|
||
|
.or_insert(1);
|
||
|
}
|
||
|
TermSplit::Horizontal { left, right } => {
|
||
|
left.collect_term_frames(into);
|
||
|
right.collect_term_frames(into);
|
||
|
}
|
||
|
TermSplit::Vertical { top, bottom } => {
|
||
|
top.collect_term_frames(into);
|
||
|
bottom.collect_term_frames(into);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn validate(&self) -> Result<(), String> {
|
||
|
let mut frame_count: BTreeMap<TermFrame, usize> = BTreeMap::new();
|
||
|
self.collect_term_frames(&mut frame_count);
|
||
|
let duplicate_terminal = frame_count
|
||
|
.iter()
|
||
|
.filter_map(|(k, v)| {
|
||
|
if *v > 1 {
|
||
|
Some(format!("Terminal {}", k.0))
|
||
|
} else {
|
||
|
None
|
||
|
}
|
||
|
})
|
||
|
.join(", ");
|
||
|
if !duplicate_terminal.is_empty() {
|
||
|
Err(format!(
|
||
|
"Attempt to create layout that duplicates reference to: {}",
|
||
|
duplicate_terminal
|
||
|
))?;
|
||
|
}
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
pub fn modify_at_pathstr<F>(&mut self, pathstr: &str, mod_with: F) -> Result<(), String>
|
||
|
where
|
||
|
F: FnOnce(&mut TermSplit) -> Result<(), String>,
|
||
|
{
|
||
|
self.modify_at_pathstr_vec(&pathstr.chars().collect::<Vec<char>>(), mod_with)
|
||
|
}
|
||
|
|
||
|
fn modify_at_pathstr_vec<F>(&mut self, pathstr: &[char], mod_with: F) -> Result<(), String>
|
||
|
where
|
||
|
F: FnOnce(&mut TermSplit) -> Result<(), String>,
|
||
|
{
|
||
|
match self {
|
||
|
TermSplit::Horizontal { left, right } => match pathstr.split_first() {
|
||
|
None => mod_with(self),
|
||
|
Some(('l', path_rest)) => left.modify_at_pathstr_vec(path_rest, mod_with),
|
||
|
Some(('r', path_rest)) => right.modify_at_pathstr_vec(path_rest, mod_with),
|
||
|
Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a horizontal split", c, path_rest.iter().collect::<String>()))
|
||
|
},
|
||
|
TermSplit::Vertical { top, bottom } => match pathstr.split_first() {
|
||
|
None => mod_with(self),
|
||
|
Some(('t', path_rest)) => top.modify_at_pathstr_vec(path_rest, mod_with),
|
||
|
Some(('b', path_rest)) => bottom.modify_at_pathstr_vec(path_rest, mod_with),
|
||
|
Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::<String>()))
|
||
|
},
|
||
|
TermSplit::Term { .. } => match pathstr.split_first() {
|
||
|
None => mod_with(self),
|
||
|
Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::<String>()))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn hsplit(&mut self, pathstr: &str, new_frame: TermFrame) -> Result<(), String> {
|
||
|
let mut new = self.clone();
|
||
|
new.modify_at_pathstr(pathstr, move |n| {
|
||
|
*n = TermSplit::Horizontal {
|
||
|
left: n.clone().into(),
|
||
|
right: TermSplit::Term { frame: new_frame }.into(),
|
||
|
};
|
||
|
Ok(())
|
||
|
})?;
|
||
|
new.validate()?;
|
||
|
*self = new;
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
pub fn vsplit(&mut self, pathstr: &str, new_frame: TermFrame) -> Result<(), String> {
|
||
|
let mut new = self.clone();
|
||
|
new.modify_at_pathstr(pathstr, move |n| {
|
||
|
*n = TermSplit::Vertical {
|
||
|
top: n.clone().into(),
|
||
|
bottom: TermSplit::Term { frame: new_frame }.into(),
|
||
|
};
|
||
|
Ok(())
|
||
|
})?;
|
||
|
new.validate()?;
|
||
|
*self = new;
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
pub fn join(&mut self, pathstr: &str) -> Result<(), String> {
|
||
|
self.modify_at_pathstr(pathstr, move |n| match n {
|
||
|
TermSplit::Term { .. } => {
|
||
|
Err("Can only join vertical or horizontal splits, not a terminal".to_owned())
|
||
|
}
|
||
|
TermSplit::Horizontal { left, .. } => {
|
||
|
*n = (**left).clone();
|
||
|
Ok(())
|
||
|
}
|
||
|
TermSplit::Vertical { top, .. } => {
|
||
|
*n = (**top).clone();
|
||
|
Ok(())
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[cfg(test)]
|
||
|
mod tests {
|
||
|
use super::*;
|
||
|
|
||
|
#[test]
|
||
|
fn validate_accepts_valid() {
|
||
|
use TermSplit::*;
|
||
|
assert_eq!(
|
||
|
(Vertical {
|
||
|
top: Term {
|
||
|
frame: TermFrame(1)
|
||
|
}
|
||
|
.into(),
|
||
|
bottom: Horizontal {
|
||
|
left: Term {
|
||
|
frame: TermFrame(2)
|
||
|
}
|
||
|
.into(),
|
||
|
right: Term {
|
||
|
frame: TermFrame(3)
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into()
|
||
|
})
|
||
|
.validate(),
|
||
|
Ok(())
|
||
|
);
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn validate_rejects_duplicate() {
|
||
|
use TermSplit::*;
|
||
|
assert_eq!(
|
||
|
(Vertical {
|
||
|
top: Term {
|
||
|
frame: TermFrame(1)
|
||
|
}
|
||
|
.into(),
|
||
|
bottom: Horizontal {
|
||
|
left: Term {
|
||
|
frame: TermFrame(1)
|
||
|
}
|
||
|
.into(),
|
||
|
right: Term {
|
||
|
frame: TermFrame(3)
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into()
|
||
|
})
|
||
|
.validate(),
|
||
|
Err("Attempt to create layout that duplicates reference to: Terminal 1".to_owned())
|
||
|
);
|
||
|
assert_eq!(
|
||
|
(Vertical {
|
||
|
top: Term {
|
||
|
frame: TermFrame(42)
|
||
|
}
|
||
|
.into(),
|
||
|
bottom: Horizontal {
|
||
|
left: Term {
|
||
|
frame: TermFrame(1)
|
||
|
}
|
||
|
.into(),
|
||
|
right: Term {
|
||
|
frame: TermFrame(42)
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into()
|
||
|
})
|
||
|
.validate(),
|
||
|
Err("Attempt to create layout that duplicates reference to: Terminal 42".to_owned())
|
||
|
);
|
||
|
}
|
||
|
|
||
|
#[test]
|
||
|
fn modify_at_pathstr_works() {
|
||
|
use TermSplit::*;
|
||
|
let mut t = Term {
|
||
|
frame: TermFrame(1),
|
||
|
};
|
||
|
assert_eq!(
|
||
|
t.modify_at_pathstr("", |v| {
|
||
|
*v = Term {
|
||
|
frame: TermFrame(2),
|
||
|
};
|
||
|
Ok(())
|
||
|
}),
|
||
|
Ok(())
|
||
|
);
|
||
|
assert_eq!(
|
||
|
t,
|
||
|
Term {
|
||
|
frame: TermFrame(2)
|
||
|
}
|
||
|
);
|
||
|
|
||
|
assert_eq!(
|
||
|
t.modify_at_pathstr("tlr", |v| {
|
||
|
*v = Term {
|
||
|
frame: TermFrame(2),
|
||
|
};
|
||
|
Ok(())
|
||
|
}),
|
||
|
Err("In split path, found trailing junk tlr after addressing terminal".to_owned())
|
||
|
);
|
||
|
|
||
|
let mut t = Vertical {
|
||
|
top: Horizontal {
|
||
|
left: Horizontal {
|
||
|
left: Term {
|
||
|
frame: TermFrame(42),
|
||
|
}
|
||
|
.into(),
|
||
|
right: Term {
|
||
|
frame: TermFrame(64),
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into(),
|
||
|
right: Term {
|
||
|
frame: TermFrame(42),
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into(),
|
||
|
bottom: Vertical {
|
||
|
top: Term {
|
||
|
frame: TermFrame(43),
|
||
|
}
|
||
|
.into(),
|
||
|
bottom: Term {
|
||
|
frame: TermFrame(44),
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into(),
|
||
|
};
|
||
|
assert_eq!(
|
||
|
t.modify_at_pathstr("tlr", |v| {
|
||
|
*v = Term {
|
||
|
frame: TermFrame(2),
|
||
|
};
|
||
|
Ok(())
|
||
|
}),
|
||
|
Ok(())
|
||
|
);
|
||
|
assert_eq!(
|
||
|
t.modify_at_pathstr("bb", |v| {
|
||
|
*v = Term {
|
||
|
frame: TermFrame(3),
|
||
|
};
|
||
|
Ok(())
|
||
|
}),
|
||
|
Ok(())
|
||
|
);
|
||
|
assert_eq!(
|
||
|
t,
|
||
|
Vertical {
|
||
|
top: Horizontal {
|
||
|
left: Horizontal {
|
||
|
left: Term {
|
||
|
frame: TermFrame(42),
|
||
|
}
|
||
|
.into(),
|
||
|
right: Term {
|
||
|
frame: TermFrame(2),
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into(),
|
||
|
right: Term {
|
||
|
frame: TermFrame(42),
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into(),
|
||
|
bottom: Vertical {
|
||
|
top: Term {
|
||
|
frame: TermFrame(43),
|
||
|
}
|
||
|
.into(),
|
||
|
bottom: Term {
|
||
|
frame: TermFrame(3),
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
.into(),
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
}
|