use std::collections::BTreeMap;
use std::fmt;
use tear_types::{
ControlError, Direction, LayoutPlan, LiveSession, MultiplexerControl, PaneId, PaneSlot,
SessionSource, SplitOrientation, WindowId,
};
use crate::definition::{DefinitionError, SessionDefinition, SessionOrigin};
#[derive(Debug)]
pub enum InstantiateError {
Invalid(DefinitionError),
Control(ControlError),
}
impl fmt::Display for InstantiateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid(e) => write!(f, "invalid session definition: {e:?}"),
Self::Control(e) => write!(f, "backend error during instantiation: {e}"),
}
}
}
impl std::error::Error for InstantiateError {}
impl From<ControlError> for InstantiateError {
fn from(e: ControlError) -> Self {
Self::Control(e)
}
}
fn source_for(origin: &SessionOrigin) -> SessionSource {
match origin {
SessionOrigin::Project | SessionOrigin::Adhoc { .. } => SessionSource::Human,
SessionOrigin::Authored => SessionSource::Named("defsession".into()),
}
}
fn split_direction(orientation: SplitOrientation) -> Direction {
match orientation {
SplitOrientation::Vertical => Direction::Right,
SplitOrientation::Horizontal => Direction::Below,
}
}
fn realize_into(
plan: &LayoutPlan,
occupant: PaneId,
specs: &BTreeMap<PaneSlot, tear_types::SpawnSpec>,
backend: &dyn MultiplexerControl,
out: &mut BTreeMap<PaneSlot, PaneId>,
) -> Result<(), InstantiateError> {
match plan {
LayoutPlan::Leaf { slot } => {
out.insert(*slot, occupant);
Ok(())
}
LayoutPlan::Split {
orientation, a, b, ..
} => {
let b_first = b.leftmost_slot();
let b_shell = specs[&b_first].shell.clone();
let new_pane = backend.split_pane(occupant, split_direction(*orientation), &b_shell)?;
realize_into(a, occupant, specs, backend, out)?;
realize_into(b, new_pane, specs, backend, out)?;
Ok(())
}
}
}
fn realize_window(
window: &tear_types::WindowPlan,
wid: WindowId,
specs: &BTreeMap<PaneSlot, tear_types::SpawnSpec>,
backend: &dyn MultiplexerControl,
) -> Result<(), InstantiateError> {
let first_pane = backend.get_window(wid)?.1.active_pane;
let mut slot_to_pane = BTreeMap::new();
realize_into(&window.layout, first_pane, specs, backend, &mut slot_to_pane)?;
if let Some(&active) = slot_to_pane.get(&window.active_slot) {
backend.select_pane(active)?;
}
Ok(())
}
pub fn instantiate(
def: &SessionDefinition,
backend: &dyn MultiplexerControl,
) -> Result<LiveSession, InstantiateError> {
def.validate().map_err(InstantiateError::Invalid)?;
let source = source_for(&def.origin);
let display = def.display_name();
let mut session_id = None;
let mut first_window = None;
for (i, window) in def.windows.iter().enumerate() {
let leftmost = window.layout.leftmost_slot();
let first_shell = def.pane_specs[&leftmost].shell.clone();
let wid = if i == 0 {
let sid = backend.new_session_with_source_and_size(
&display,
&first_shell,
source.clone(),
(80, 24),
)?;
session_id = Some(sid);
let w = backend.get_session(sid)?.active_window;
first_window = Some(w);
w
} else {
backend.new_window(session_id.expect("first window set"), &window.name, &first_shell)?
};
realize_window(window, wid, &def.pane_specs, backend)?;
}
if let Some(w) = first_window {
backend.select_window(w)?;
}
let sid = session_id.expect("a validated definition has ≥1 window");
let session = backend.get_session(sid)?;
Ok(LiveSession::new(def.def_id, session))
}
pub fn reinstantiate(
def: &SessionDefinition,
backend: &dyn MultiplexerControl,
) -> Result<LiveSession, InstantiateError> {
instantiate(def, backend)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::NameStyle;
use tear_core::inproc::InProcess;
use tear_types::WindowPlan;
fn specs(pairs: &[(u32, &str)]) -> BTreeMap<PaneSlot, tear_types::SpawnSpec> {
pairs
.iter()
.map(|&(s, sh)| (PaneSlot(s), tear_types::SpawnSpec::shell(PaneSlot(s), sh)))
.collect()
}
#[test]
fn instantiate_single_pane_definition_spawns_one_pane_linked_to_its_def() {
let backend = InProcess::new();
let def = SessionDefinition::single_pane("/code/pleme-io/mado", "/bin/sh", NameStyle::Emoji, 0);
let live = instantiate(&def, &backend).unwrap();
assert_eq!(live.definition, def.def_id);
let w = &live.session.windows[&live.session.active_window];
assert_eq!(w.layout.pane_count(), 1);
assert_eq!(live.durability, tear_types::Durability::ProcessBound);
}
#[test]
fn instantiate_two_pane_plan_builds_the_exact_tree() {
let backend = InProcess::new();
let def = SessionDefinition {
def_id: tear_types::DefinitionId::from_project(std::path::Path::new("/x")),
origin: SessionOrigin::Project,
name_seed: 0,
name_style: NameStyle::Emoji,
theme: None,
custom_name: None,
project_root: "/x".into(),
windows: vec![WindowPlan {
name: "work".into(),
layout: LayoutPlan::split(
SplitOrientation::Vertical,
LayoutPlan::leaf(PaneSlot(0)),
LayoutPlan::leaf(PaneSlot(1)),
),
active_slot: PaneSlot(1),
}],
pane_specs: specs(&[(0, "/bin/sh"), (1, "/bin/sh")]),
visits: 1,
last_seen: 0,
tags: Vec::new(),
};
let live = instantiate(&def, &backend).unwrap();
let w = &live.session.windows[&live.session.active_window];
assert_eq!(w.layout.pane_count(), 2);
match &w.layout {
tear_types::LayoutNode::Split { orientation, .. } => {
assert_eq!(*orientation, SplitOrientation::Vertical);
}
tear_types::LayoutNode::Leaf { .. } => panic!("expected a split"),
}
let panes = w.layout.panes();
assert_eq!(w.active_pane, panes[1]);
live.session
.windows
.values()
.for_each(|win| win.layout.validate().unwrap());
}
#[test]
fn instantiate_nested_three_pane_plan_matches_structure() {
let backend = InProcess::new();
let def = SessionDefinition {
def_id: tear_types::DefinitionId::from_project(std::path::Path::new("/y")),
origin: SessionOrigin::Project,
name_seed: 0,
name_style: NameStyle::Emoji,
theme: None,
custom_name: None,
project_root: "/y".into(),
windows: vec![WindowPlan {
name: "work".into(),
layout: LayoutPlan::split(
SplitOrientation::Vertical,
LayoutPlan::leaf(PaneSlot(0)),
LayoutPlan::split(
SplitOrientation::Horizontal,
LayoutPlan::leaf(PaneSlot(1)),
LayoutPlan::leaf(PaneSlot(2)),
),
),
active_slot: PaneSlot(0),
}],
pane_specs: specs(&[(0, "/bin/sh"), (1, "/bin/sh"), (2, "/bin/sh")]),
visits: 1,
last_seen: 0,
tags: Vec::new(),
};
let live = instantiate(&def, &backend).unwrap();
let w = &live.session.windows[&live.session.active_window];
assert_eq!(w.layout.pane_count(), 3);
match &w.layout {
tear_types::LayoutNode::Split { orientation, b, .. } => {
assert_eq!(*orientation, SplitOrientation::Vertical);
assert!(matches!(
b.as_ref(),
tear_types::LayoutNode::Split { orientation: SplitOrientation::Horizontal, .. }
));
}
tear_types::LayoutNode::Leaf { .. } => panic!("expected a split"),
}
w.layout.validate().unwrap();
}
#[test]
fn reinstantiate_yields_a_fresh_incarnation_of_the_same_definition() {
let backend = InProcess::new();
let def = SessionDefinition::single_pane("/code/pleme-io/tear", "/bin/sh", NameStyle::Emoji, 0);
let first = instantiate(&def, &backend).unwrap();
let second = reinstantiate(&def, &backend).unwrap();
assert_eq!(first.definition, second.definition);
assert_ne!(first.instance(), second.instance());
}
fn shape(n: &tear_types::LayoutNode, out: &mut String) {
match n {
tear_types::LayoutNode::Leaf { .. } => out.push('L'),
tear_types::LayoutNode::Split { orientation, a, b, .. } => {
out.push('(');
out.push(if *orientation == SplitOrientation::Vertical {
'V'
} else {
'H'
});
shape(a, out);
shape(b, out);
out.push(')');
}
}
}
#[test]
fn capture_then_instantiate_reproduces_the_layout_structure() {
use crate::definition::SessionDefinition;
let backend = InProcess::new();
let sid = backend.new_session("orig", "/bin/sh").unwrap();
let w = backend.get_session(sid).unwrap().active_window;
let p0 = backend.get_session(sid).unwrap().windows[&w].active_pane;
let p1 = backend.split_pane(p0, Direction::Right, "/bin/sh").unwrap();
backend.split_pane(p1, Direction::Below, "/bin/sh").unwrap();
let original = backend.get_session(sid).unwrap();
let mut orig_shape = String::new();
shape(&original.windows[&w].layout, &mut orig_shape);
let def = SessionDefinition::from_live(&original, "/code/x", 0);
let backend2 = InProcess::new();
let live = instantiate(&def, &backend2).unwrap();
let new_w = &live.session.windows[&live.session.active_window];
let mut new_shape = String::new();
shape(&new_w.layout, &mut new_shape);
assert_eq!(new_shape, orig_shape, "captured layout structure differs");
assert_eq!(new_w.layout.pane_count(), 3);
}
#[test]
fn instantiate_rejects_invalid_definition_before_spawning() {
let backend = InProcess::new();
let mut def = SessionDefinition::single_pane("/x", "/bin/sh", NameStyle::Emoji, 0);
def.windows.push(WindowPlan::single("orphan", PaneSlot(42)));
let err = instantiate(&def, &backend).unwrap_err();
assert!(matches!(err, InstantiateError::Invalid(DefinitionError::MissingSpec(_))));
assert!(backend.list_sessions().unwrap().is_empty());
}
}