embers-client 0.1.0

Client rendering, input handling, configuration, and scripting support for Embers.
#![allow(dead_code)]

use embers_client::ClientState;
use embers_core::{
    ActivityState, BufferId, FloatGeometry, FloatingId, NodeId, PtySize, SessionId, SplitDirection,
};
use embers_protocol::{
    BufferHistoryScope, BufferRecord, BufferRecordKind, BufferRecordState, BufferViewRecord,
    FloatingRecord, NodeRecord, NodeRecordKind, SessionRecord, SessionSnapshot, SplitRecord,
    TabRecord, TabsRecord, VisibleSnapshotResponse,
};

pub const SESSION_ID: SessionId = SessionId(1);
pub const ROOT_TABS_ID: NodeId = NodeId(10);
pub const HIDDEN_ROOT_LEAF_ID: NodeId = NodeId(11);
pub const ROOT_SPLIT_ID: NodeId = NodeId(20);
pub const LEFT_LEAF_ID: NodeId = NodeId(21);
pub const NESTED_TABS_ID: NodeId = NodeId(30);
pub const HIDDEN_NESTED_LEAF_ID: NodeId = NodeId(31);
pub const FOCUSED_LEAF_ID: NodeId = NodeId(32);
pub const FOCUSED_BUFFER_ID: BufferId = BufferId(4);
pub const FLOATING_ID: FloatingId = FloatingId(90);
pub const FLOATING_SPLIT_ID: NodeId = NodeId(40);
pub const FLOATING_TOP_LEAF_ID: NodeId = NodeId(41);
pub const FLOATING_BOTTOM_LEAF_ID: NodeId = NodeId(42);
pub const ROOT_BUFFER_LEAF_ID: NodeId = NodeId(50);
pub const ROOT_ONLY_SPLIT_ID: NodeId = NodeId(60);
pub const ROOT_SPLIT_LEFT_LEAF_ID: NodeId = NodeId(61);
pub const ROOT_SPLIT_RIGHT_LEAF_ID: NodeId = NodeId(62);

pub fn demo_state() -> ClientState {
    let mut state = ClientState::default();
    state.apply_session_snapshot(demo_snapshot(None));
    for snapshot in demo_snapshots() {
        state.apply_buffer_snapshot(snapshot);
    }
    state
}

pub fn floating_focused_state() -> ClientState {
    let mut state = ClientState::default();
    state.apply_session_snapshot(demo_snapshot(Some((FLOATING_ID, FLOATING_TOP_LEAF_ID))));
    for snapshot in demo_snapshots() {
        state.apply_buffer_snapshot(snapshot);
    }
    state
}

pub fn root_focus_state() -> ClientState {
    let mut state = ClientState::default();
    state.apply_session_snapshot(demo_snapshot(None));
    for snapshot in demo_snapshots() {
        state.apply_buffer_snapshot(snapshot);
    }
    if let Some(session) = state.sessions.get_mut(&SESSION_ID) {
        session.focused_floating_id = None;
        session.focused_leaf_id = Some(LEFT_LEAF_ID);
    }
    state
}

pub fn root_buffer_state() -> ClientState {
    let mut state = ClientState::default();
    state.apply_session_snapshot(root_buffer_snapshot());
    state.apply_buffer_snapshot(snapshot(7, ["root buffer", "extra line"]));
    state
}

pub fn root_split_state() -> ClientState {
    let mut state = ClientState::default();
    state.apply_session_snapshot(root_split_snapshot());
    state.apply_buffer_snapshot(snapshot(7, ["left root pane"]));
    state.apply_buffer_snapshot(snapshot(8, ["right root pane"]));
    state
}

fn demo_snapshot(focused_floating: Option<(FloatingId, NodeId)>) -> SessionSnapshot {
    let (focused_floating_id, focused_leaf_id) = focused_floating
        .map(|(floating_id, leaf_id)| (Some(floating_id), Some(leaf_id)))
        .unwrap_or((None, Some(FOCUSED_LEAF_ID)));

    SessionSnapshot {
        session: SessionRecord {
            id: SESSION_ID,
            name: "demo".to_owned(),
            root_node_id: ROOT_TABS_ID,
            floating_ids: vec![FLOATING_ID],
            focused_leaf_id,
            focused_floating_id,
            zoomed_node_id: None,
        },
        nodes: vec![
            NodeRecord {
                id: ROOT_TABS_ID,
                session_id: SESSION_ID,
                parent_id: None,
                kind: NodeRecordKind::Tabs,
                buffer_view: None,
                split: None,
                tabs: Some(TabsRecord {
                    active: 1,
                    tabs: vec![
                        TabRecord {
                            title: "shell".to_owned(),
                            child_id: HIDDEN_ROOT_LEAF_ID,
                        },
                        TabRecord {
                            title: "workspace".to_owned(),
                            child_id: ROOT_SPLIT_ID,
                        },
                    ],
                }),
            },
            buffer_view_node(HIDDEN_ROOT_LEAF_ID, Some(ROOT_TABS_ID), BufferId(1)),
            NodeRecord {
                id: ROOT_SPLIT_ID,
                session_id: SESSION_ID,
                parent_id: Some(ROOT_TABS_ID),
                kind: NodeRecordKind::Split,
                buffer_view: None,
                split: Some(SplitRecord {
                    direction: SplitDirection::Vertical,
                    child_ids: vec![LEFT_LEAF_ID, NESTED_TABS_ID],
                    sizes: vec![1, 2],
                }),
                tabs: None,
            },
            buffer_view_node(LEFT_LEAF_ID, Some(ROOT_SPLIT_ID), BufferId(2)),
            NodeRecord {
                id: NESTED_TABS_ID,
                session_id: SESSION_ID,
                parent_id: Some(ROOT_SPLIT_ID),
                kind: NodeRecordKind::Tabs,
                buffer_view: None,
                split: None,
                tabs: Some(TabsRecord {
                    active: 1,
                    tabs: vec![
                        TabRecord {
                            title: "build".to_owned(),
                            child_id: HIDDEN_NESTED_LEAF_ID,
                        },
                        TabRecord {
                            title: "logs-long-title".to_owned(),
                            child_id: FOCUSED_LEAF_ID,
                        },
                    ],
                }),
            },
            buffer_view_node(HIDDEN_NESTED_LEAF_ID, Some(NESTED_TABS_ID), BufferId(3)),
            buffer_view_node(FOCUSED_LEAF_ID, Some(NESTED_TABS_ID), FOCUSED_BUFFER_ID),
            NodeRecord {
                id: FLOATING_SPLIT_ID,
                session_id: SESSION_ID,
                parent_id: None,
                kind: NodeRecordKind::Split,
                buffer_view: None,
                split: Some(SplitRecord {
                    direction: SplitDirection::Horizontal,
                    child_ids: vec![FLOATING_TOP_LEAF_ID, FLOATING_BOTTOM_LEAF_ID],
                    sizes: vec![1, 1],
                }),
                tabs: None,
            },
            buffer_view_node(FLOATING_TOP_LEAF_ID, Some(FLOATING_SPLIT_ID), BufferId(5)),
            buffer_view_node(
                FLOATING_BOTTOM_LEAF_ID,
                Some(FLOATING_SPLIT_ID),
                BufferId(6),
            ),
        ],
        buffers: vec![
            buffer(1, Some(HIDDEN_ROOT_LEAF_ID), "shell", ActivityState::Idle),
            buffer(2, Some(LEFT_LEAF_ID), "editor", ActivityState::Activity),
            buffer(3, Some(HIDDEN_NESTED_LEAF_ID), "build", ActivityState::Bell),
            buffer(
                4,
                Some(FOCUSED_LEAF_ID),
                "logs-long-title",
                ActivityState::Idle,
            ),
            buffer(
                5,
                Some(FLOATING_TOP_LEAF_ID),
                "popup-top",
                ActivityState::Idle,
            ),
            buffer(
                6,
                Some(FLOATING_BOTTOM_LEAF_ID),
                "popup-bottom",
                ActivityState::Idle,
            ),
        ],
        floating: vec![FloatingRecord {
            id: FLOATING_ID,
            session_id: SESSION_ID,
            root_node_id: FLOATING_SPLIT_ID,
            title: Some("popup".to_owned()),
            geometry: FloatGeometry::new(14, 5, 20, 7),
            focused: focused_floating_id == Some(FLOATING_ID),
            visible: true,
            close_on_empty: true,
        }],
    }
}

fn root_buffer_snapshot() -> SessionSnapshot {
    SessionSnapshot {
        session: SessionRecord {
            id: SESSION_ID,
            name: "root-buffer".to_owned(),
            root_node_id: ROOT_BUFFER_LEAF_ID,
            floating_ids: Vec::new(),
            focused_leaf_id: Some(ROOT_BUFFER_LEAF_ID),
            focused_floating_id: None,
            zoomed_node_id: None,
        },
        nodes: vec![buffer_view_node(ROOT_BUFFER_LEAF_ID, None, BufferId(7))],
        buffers: vec![buffer(
            7,
            Some(ROOT_BUFFER_LEAF_ID),
            "root-buffer",
            ActivityState::Idle,
        )],
        floating: Vec::new(),
    }
}

fn root_split_snapshot() -> SessionSnapshot {
    SessionSnapshot {
        session: SessionRecord {
            id: SESSION_ID,
            name: "root-split".to_owned(),
            root_node_id: ROOT_ONLY_SPLIT_ID,
            floating_ids: Vec::new(),
            focused_leaf_id: Some(ROOT_SPLIT_RIGHT_LEAF_ID),
            focused_floating_id: None,
            zoomed_node_id: None,
        },
        nodes: vec![
            NodeRecord {
                id: ROOT_ONLY_SPLIT_ID,
                session_id: SESSION_ID,
                parent_id: None,
                kind: NodeRecordKind::Split,
                buffer_view: None,
                split: Some(SplitRecord {
                    direction: SplitDirection::Vertical,
                    child_ids: vec![ROOT_SPLIT_LEFT_LEAF_ID, ROOT_SPLIT_RIGHT_LEAF_ID],
                    sizes: vec![1, 1],
                }),
                tabs: None,
            },
            buffer_view_node(
                ROOT_SPLIT_LEFT_LEAF_ID,
                Some(ROOT_ONLY_SPLIT_ID),
                BufferId(7),
            ),
            buffer_view_node(
                ROOT_SPLIT_RIGHT_LEAF_ID,
                Some(ROOT_ONLY_SPLIT_ID),
                BufferId(8),
            ),
        ],
        buffers: vec![
            buffer(
                7,
                Some(ROOT_SPLIT_LEFT_LEAF_ID),
                "root-left",
                ActivityState::Idle,
            ),
            buffer(
                8,
                Some(ROOT_SPLIT_RIGHT_LEAF_ID),
                "root-right",
                ActivityState::Activity,
            ),
        ],
        floating: Vec::new(),
    }
}

fn buffer_view_node(id: NodeId, parent_id: Option<NodeId>, buffer_id: BufferId) -> NodeRecord {
    NodeRecord {
        id,
        session_id: SESSION_ID,
        parent_id,
        kind: NodeRecordKind::BufferView,
        buffer_view: Some(BufferViewRecord {
            buffer_id,
            focused: false,
            zoomed: false,
            follow_output: true,
            last_render_size: PtySize::new(80, 24),
        }),
        split: None,
        tabs: None,
    }
}

fn buffer(
    id: u64,
    attachment_node_id: Option<NodeId>,
    title: &str,
    activity: ActivityState,
) -> BufferRecord {
    BufferRecord {
        id: BufferId(id),
        title: title.to_owned(),
        command: vec!["/bin/sh".to_owned()],
        cwd: Some("/tmp".to_owned()),
        kind: BufferRecordKind::Pty,
        pid: None,
        env: Default::default(),
        state: BufferRecordState::Running,
        attachment_node_id,
        read_only: false,
        helper_source_buffer_id: None,
        helper_scope: None::<BufferHistoryScope>,
        pty_size: PtySize::new(80, 24),
        activity,
        last_snapshot_seq: 0,
        exit_code: None,
        pipe: None,
    }
}

fn demo_snapshots() -> Vec<VisibleSnapshotResponse> {
    vec![
        snapshot(2, ["left pane", "line two", "line three"]),
        snapshot(4, ["logs visible", "second row", "third row"]),
        snapshot(5, ["popup top"]),
        snapshot(6, ["popup bottom"]),
    ]
}

fn snapshot<const N: usize>(buffer_id: u64, lines: [&str; N]) -> VisibleSnapshotResponse {
    VisibleSnapshotResponse {
        request_id: embers_core::RequestId(0),
        buffer_id: BufferId(buffer_id),
        sequence: 1,
        size: PtySize::new(80, 24),
        lines: lines.into_iter().map(str::to_owned).collect(),
        title: None,
        cwd: None,
        viewport_top_line: 0,
        total_lines: 24,
        alternate_screen: false,
        mouse_reporting: false,
        focus_reporting: false,
        bracketed_paste: false,
        cursor: None,
    }
}