embers-server 0.1.0

Headless session, layout, persistence, and PTY runtime server for Embers.
use embers_core::{SessionId, SplitDirection};
use embers_server::{BufferAttachment, Node, ServerState, SplitNode, TabsNode};

fn root_tabs(state: &ServerState, session_id: SessionId) -> TabsNode {
    let root_id = state.root_tabs(session_id).expect("session has root tabs");
    match state.node(root_id).expect("root node exists") {
        Node::Tabs(tabs) => tabs.clone(),
        other => panic!("expected root tabs node, got {other:?}"),
    }
}

fn split_node(state: &ServerState, node_id: embers_core::NodeId) -> SplitNode {
    match state.node(node_id).expect("split node exists") {
        Node::Split(split) => split.clone(),
        other => panic!("expected split node, got {other:?}"),
    }
}

#[test]
fn repeated_nested_splits_preserve_leaf_order_and_focus() {
    let mut state = ServerState::new();
    let session_id = state.create_session("main");

    let first_buffer = state.create_buffer("one", vec!["/bin/sh".to_owned()], None);
    state
        .add_root_tab_from_buffer(session_id, "one", first_buffer)
        .expect("add initial tab");
    let first_leaf = state
        .session(session_id)
        .expect("session exists")
        .focused_leaf
        .expect("initial tab focuses first leaf");

    let second_buffer = state.create_buffer("two", vec!["/bin/sh".to_owned()], None);
    let outer_split = state
        .split_leaf_with_new_buffer(first_leaf, SplitDirection::Vertical, second_buffer)
        .expect("split first leaf");
    let second_leaf = state
        .session(session_id)
        .expect("session exists")
        .focused_leaf
        .expect("split focuses new leaf");

    let third_buffer = state.create_buffer("three", vec!["/bin/sh".to_owned()], None);
    let inner_split = state
        .split_leaf_with_new_buffer(second_leaf, SplitDirection::Horizontal, third_buffer)
        .expect("split nested leaf");
    let third_leaf = state
        .session(session_id)
        .expect("session exists")
        .focused_leaf
        .expect("nested split focuses newest leaf");

    let tabs = root_tabs(&state, session_id);
    assert_eq!(tabs.tabs.len(), 1);
    assert_eq!(tabs.tabs[0].child, outer_split);

    let outer = split_node(&state, outer_split);
    assert_eq!(outer.direction, SplitDirection::Vertical);
    assert_eq!(outer.children, vec![first_leaf, inner_split]);
    assert_eq!(outer.sizes, vec![1, 1]);

    let inner = split_node(&state, inner_split);
    assert_eq!(inner.direction, SplitDirection::Horizontal);
    assert_eq!(inner.children, vec![second_leaf, third_leaf]);
    assert_eq!(inner.sizes, vec![1, 1]);

    assert_eq!(
        state
            .visible_session_leaves(session_id)
            .expect("visible leaves"),
        vec![first_leaf, second_leaf, third_leaf]
    );

    state.validate().expect("nested splits remain valid");
}

#[test]
fn resize_updates_split_weights_and_rejects_invalid_sizes() {
    let mut state = ServerState::new();
    let session_id = state.create_session("main");

    let first_buffer = state.create_buffer("one", vec!["/bin/sh".to_owned()], None);
    state
        .add_root_tab_from_buffer(session_id, "one", first_buffer)
        .expect("add initial tab");
    let first_leaf = state
        .session(session_id)
        .expect("session exists")
        .focused_leaf
        .expect("initial tab focuses first leaf");

    let second_buffer = state.create_buffer("two", vec!["/bin/sh".to_owned()], None);
    let split_id = state
        .split_leaf_with_new_buffer(first_leaf, SplitDirection::Vertical, second_buffer)
        .expect("split leaf");

    state
        .resize_split_children(split_id, vec![3, 2])
        .expect("resize split");
    assert_eq!(split_node(&state, split_id).sizes, vec![3, 2]);

    assert!(state.resize_split_children(split_id, vec![5]).is_err());
    assert!(state.resize_split_children(split_id, vec![5, 0]).is_err());

    state.validate().expect("resized split remains valid");
}

#[test]
fn closing_leaf_normalizes_split_and_detaches_buffer() {
    let mut state = ServerState::new();
    let session_id = state.create_session("main");

    let first_buffer = state.create_buffer("one", vec!["/bin/sh".to_owned()], None);
    state
        .add_root_tab_from_buffer(session_id, "one", first_buffer)
        .expect("add initial tab");
    let first_leaf = state
        .session(session_id)
        .expect("session exists")
        .focused_leaf
        .expect("initial tab focuses first leaf");

    let second_buffer = state.create_buffer("two", vec!["/bin/sh".to_owned()], None);
    state
        .split_leaf_with_new_buffer(first_leaf, SplitDirection::Horizontal, second_buffer)
        .expect("split leaf");
    let second_leaf = state
        .session(session_id)
        .expect("session exists")
        .focused_leaf
        .expect("split focuses new leaf");

    state.close_node(second_leaf).expect("close focused leaf");

    assert_eq!(
        state.session(session_id).expect("session exists").root_node,
        first_leaf
    );
    assert_eq!(
        state.node_parent(first_leaf).expect("first leaf parent"),
        None
    );
    assert_eq!(
        state
            .session(session_id)
            .expect("session exists")
            .focused_leaf,
        Some(first_leaf)
    );
    assert!(matches!(
        &state
            .buffer(second_buffer)
            .expect("buffer exists")
            .attachment,
        BufferAttachment::Detached
    ));
    assert_eq!(
        state
            .visible_session_leaves(session_id)
            .expect("visible leaves"),
        vec![first_leaf]
    );

    state.validate().expect("close normalizes split");
}