use itertools::Itertools; use std::{collections::BTreeMap, rc::Rc}; use crate::TermFrame; #[derive(PartialEq, Eq, Debug, Clone)] pub enum TermSplit { Term { frame: TermFrame, }, Horizontal { left: Rc, right: Rc, }, Vertical { top: Rc, bottom: Rc, }, } impl TermSplit { fn collect_term_frames(&self, into: &mut BTreeMap) { 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 = 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(&self, pathstr: &str, mod_with: F) -> Result where F: FnOnce(&TermSplit) -> Result, { self.modify_at_pathstr_vec(&pathstr.chars().collect::>(), mod_with) } fn modify_at_pathstr_vec(&self, pathstr: &[char], mod_with: F) -> Result where F: FnOnce(&TermSplit) -> Result, { match self { TermSplit::Horizontal { left, right } => match pathstr.split_first() { None => mod_with(self), Some(('l', path_rest)) => Ok(TermSplit::Horizontal { left: left.modify_at_pathstr_vec(path_rest, mod_with)?.into(), right: right.clone() }), Some(('r', path_rest)) => Ok(TermSplit::Horizontal { left: left.clone(), right: right.modify_at_pathstr_vec(path_rest, mod_with)?.into() }), Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a horizontal split", c, path_rest.iter().collect::())) }, TermSplit::Vertical { top, bottom } => match pathstr.split_first() { None => mod_with(self), Some(('t', path_rest)) => Ok(TermSplit::Vertical { top: top.modify_at_pathstr_vec(path_rest, mod_with)?.into(), bottom: bottom.clone() }), Some(('b', path_rest)) => Ok(TermSplit::Vertical { top: top.clone(), bottom: bottom.modify_at_pathstr_vec(path_rest, mod_with)?.into() }), Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::())) }, 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::())) } } } pub fn hsplit(&self, pathstr: &str, new_frame: TermFrame) -> Result { let new = self.modify_at_pathstr(pathstr, move |n| { Ok(TermSplit::Horizontal { left: n.clone().into(), right: TermSplit::Term { frame: new_frame }.into(), }) })?; new.validate()?; Ok(new) } pub fn vsplit(&self, pathstr: &str, new_frame: TermFrame) -> Result { let new = self.modify_at_pathstr(pathstr, move |n| { Ok(TermSplit::Vertical { top: n.clone().into(), bottom: TermSplit::Term { frame: new_frame }.into(), }) })?; new.validate()?; Ok(new) } pub fn join(&self, pathstr: &str) -> Result { 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, .. } => Ok((**left).clone()), TermSplit::Vertical { top, .. } => Ok((**top).clone()), }) } } #[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(), } ); } }