Skip to main content

stynx_code_tui/layout/
main_layout.rs

1use ratatui::layout::{Constraint, Layout, Rect};
2
3pub struct LayoutResult {
4    /// Top of the left column — the tool stream.
5    pub tool_history: Option<Rect>,
6    /// Bottom of the left column — model / session info.
7    pub tool_info: Option<Rect>,
8    pub messages: Rect,
9    pub thinking: Option<Rect>,
10    pub delegate: Option<Rect>,
11    pub summary: Option<Rect>,
12    pub input: Rect,
13    /// Fallback bottom bar — only used when the terminal is too narrow for the
14    /// left sidebar (otherwise model info lives in `tool_info`).
15    pub footer: Option<Rect>,
16}
17
18pub struct MainLayout;
19
20impl MainLayout {
21    pub const TOOL_HISTORY_WIDTH: u16 = 44;
22    pub const MIN_MAIN_WIDTH: u16 = 60;
23    /// Height reserved for the model-info section at the bottom of the sidebar.
24    const INFO_HEIGHT: u16 = 9;
25
26    pub fn split(
27        area: Rect,
28        input_lines: usize,
29        thinking_lines: usize,
30        delegate_lines: usize,
31        has_summary: bool,
32    ) -> LayoutResult {
33        let (tool_col, main) = if area.width > Self::TOOL_HISTORY_WIDTH + Self::MIN_MAIN_WIDTH {
34            let chunks = Layout::horizontal([
35                Constraint::Length(Self::TOOL_HISTORY_WIDTH),
36                Constraint::Min(1),
37            ])
38            .split(area);
39            (Some(chunks[0]), chunks[1])
40        } else {
41            (None, area)
42        };
43
44        // Split the left column: tool stream on top, model info on the bottom.
45        let (tool_history, tool_info) = match tool_col {
46            Some(col) => {
47                let info_h = Self::INFO_HEIGHT.min(col.height.saturating_sub(3));
48                if info_h >= 4 {
49                    let parts = Layout::vertical([
50                        Constraint::Min(1),
51                        Constraint::Length(info_h),
52                    ])
53                    .split(col);
54                    (Some(parts[0]), Some(parts[1]))
55                } else {
56                    (Some(col), None)
57                }
58            }
59            None => (None, None),
60        };
61
62        let content = input_lines.clamp(1, 8) as u16;
63        let input_h = (content + 2).min(main.height.saturating_sub(4));
64        let thinking_h: u16 = if thinking_lines == 0 {
65            0
66        } else {
67            (1 + thinking_lines.min(12)) as u16
68        };
69        let delegate_h: u16 = delegate_lines.min(4) as u16;
70        // Show a bottom bar only when there's no sidebar to host the model info.
71        let footer_h: u16 = if tool_info.is_some() { 0 } else { 1 };
72
73        let mut constraints: Vec<Constraint> = vec![Constraint::Min(1)];
74        if thinking_h > 0 { constraints.push(Constraint::Length(thinking_h)); }
75        if delegate_h > 0 { constraints.push(Constraint::Length(delegate_h)); }
76        if has_summary { constraints.push(Constraint::Length(1)); }
77        constraints.push(Constraint::Length(input_h));
78        if footer_h > 0 { constraints.push(Constraint::Length(footer_h)); }
79
80        let rows = Layout::vertical(constraints).split(main);
81        let mut idx = 0;
82        let messages = rows[idx]; idx += 1;
83        let thinking = if thinking_h > 0 { let r = Some(rows[idx]); idx += 1; r } else { None };
84        let delegate = if delegate_h > 0 { let r = Some(rows[idx]); idx += 1; r } else { None };
85        let summary = if has_summary { let r = Some(rows[idx]); idx += 1; r } else { None };
86        let input = rows[idx]; idx += 1;
87        let footer = if footer_h > 0 { Some(rows[idx]) } else { None };
88
89        LayoutResult { tool_history, tool_info, messages, thinking, delegate, summary, input, footer }
90    }
91}