worldwideportal/src/term_split.rs

333 lines
10 KiB
Rust
Raw Normal View History

2024-08-22 22:25:05 +10:00
use itertools::Itertools;
2024-08-23 23:37:58 +10:00
use std::{collections::BTreeMap, rc::Rc};
2024-08-22 22:25:05 +10:00
use crate::TermFrame;
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum TermSplit {
Term {
frame: TermFrame,
},
Horizontal {
2024-08-23 23:37:58 +10:00
left: Rc<TermSplit>,
right: Rc<TermSplit>,
2024-08-22 22:25:05 +10:00
},
Vertical {
2024-08-23 23:37:58 +10:00
top: Rc<TermSplit>,
bottom: Rc<TermSplit>,
2024-08-22 22:25:05 +10:00
},
}
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(())
}
2024-08-23 23:37:58 +10:00
pub fn modify_at_pathstr<F>(&self, pathstr: &str, mod_with: F) -> Result<Self, String>
2024-08-22 22:25:05 +10:00
where
2024-08-23 23:37:58 +10:00
F: FnOnce(&TermSplit) -> Result<Self, String>,
2024-08-22 22:25:05 +10:00
{
self.modify_at_pathstr_vec(&pathstr.chars().collect::<Vec<char>>(), mod_with)
}
2024-08-23 23:37:58 +10:00
fn modify_at_pathstr_vec<F>(&self, pathstr: &[char], mod_with: F) -> Result<Self, String>
2024-08-22 22:25:05 +10:00
where
2024-08-23 23:37:58 +10:00
F: FnOnce(&TermSplit) -> Result<Self, String>,
2024-08-22 22:25:05 +10:00
{
match self {
TermSplit::Horizontal { left, right } => match pathstr.split_first() {
None => mod_with(self),
2024-08-23 23:37:58 +10:00
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()
}),
2024-08-22 22:25:05 +10:00
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),
2024-08-23 23:37:58 +10:00
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()
}),
2024-08-22 22:25:05 +10:00
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>()))
}
}
}
2024-08-23 23:37:58 +10:00
pub fn hsplit(&self, pathstr: &str, new_frame: TermFrame) -> Result<TermSplit, String> {
let new = self.modify_at_pathstr(pathstr, move |n| {
Ok(TermSplit::Horizontal {
2024-08-22 22:25:05 +10:00
left: n.clone().into(),
right: TermSplit::Term { frame: new_frame }.into(),
2024-08-23 23:37:58 +10:00
})
2024-08-22 22:25:05 +10:00
})?;
new.validate()?;
2024-08-23 23:37:58 +10:00
Ok(new)
2024-08-22 22:25:05 +10:00
}
2024-08-23 23:37:58 +10:00
pub fn vsplit(&self, pathstr: &str, new_frame: TermFrame) -> Result<TermSplit, String> {
let new = self.modify_at_pathstr(pathstr, move |n| {
Ok(TermSplit::Vertical {
2024-08-22 22:25:05 +10:00
top: n.clone().into(),
bottom: TermSplit::Term { frame: new_frame }.into(),
2024-08-23 23:37:58 +10:00
})
2024-08-22 22:25:05 +10:00
})?;
new.validate()?;
2024-08-23 23:37:58 +10:00
Ok(new)
2024-08-22 22:25:05 +10:00
}
2024-08-23 23:37:58 +10:00
pub fn join(&self, pathstr: &str) -> Result<TermSplit, String> {
2024-08-22 22:25:05 +10:00
self.modify_at_pathstr(pathstr, move |n| match n {
TermSplit::Term { .. } => {
Err("Can only join vertical or horizontal splits, not a terminal".to_owned())
}
2024-08-23 23:37:58 +10:00
TermSplit::Horizontal { left, .. } => Ok((**left).clone()),
TermSplit::Vertical { top, .. } => Ok((**top).clone()),
2024-08-22 22:25:05 +10:00
})
}
}
#[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(),
}
);
}
}