mermaid_cli/render/
layout.rs1use ratatui::layout::{Constraint, Direction, Layout, Rect};
8use unicode_width::UnicodeWidthChar;
9
10use crate::domain::State;
11
12#[derive(Debug)]
14pub struct Zones {
15 pub chat: Rect,
16 pub attachments: Rect,
17 pub input: Rect,
18 pub status: Rect,
19}
20
21impl Zones {
22 pub fn for_state(area: Rect, state: &State) -> Self {
26 let input_lines = estimate_input_lines(&state.ui.input_buffer, area.width);
27 let input_height = (input_lines + 2).min(7) as u16; let attachment_height = if state.ui.attachments.is_empty() {
30 0
31 } else {
32 1
33 };
34 let status_height = if state.status.is_some() { 1 } else { 0 };
35
36 let chunks = Layout::default()
37 .direction(Direction::Vertical)
38 .constraints([
39 Constraint::Min(1),
40 Constraint::Length(attachment_height),
41 Constraint::Length(input_height),
42 Constraint::Length(status_height),
43 ])
44 .split(area);
45
46 Self {
47 chat: chunks[0],
48 attachments: chunks[1],
49 input: chunks[2],
50 status: chunks[3],
51 }
52 }
53}
54
55fn estimate_input_lines(buffer: &str, width: u16) -> usize {
60 if buffer.is_empty() {
61 return 1;
62 }
63 let inner_width = width.saturating_sub(4) as usize;
64 let mut lines = 1usize;
65 let mut col = 0usize;
66 for ch in buffer.chars() {
67 let w = ch.width().unwrap_or(0);
68 if ch == '\n' || (inner_width > 0 && col + w > inner_width) {
69 lines += 1;
70 col = if ch == '\n' { 0 } else { w };
71 } else {
72 col += w;
73 }
74 }
75 lines.min(5)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::app::Config;
82 use std::path::PathBuf;
83
84 fn state() -> State {
85 State::new(
86 Config::default(),
87 PathBuf::from("/tmp/p"),
88 "ollama/test".to_string(),
89 )
90 }
91
92 #[test]
93 fn empty_input_single_line() {
94 assert_eq!(estimate_input_lines("", 80), 1);
95 }
96
97 #[test]
98 fn linebreaks_count() {
99 assert_eq!(estimate_input_lines("a\nb\nc", 80), 3);
100 }
101
102 #[test]
103 fn wraps_at_inner_width() {
104 assert_eq!(estimate_input_lines("abcdefg", 10), 2);
106 }
107
108 #[test]
109 fn capped_at_five_lines() {
110 assert_eq!(estimate_input_lines("\n\n\n\n\n\n\n", 80), 5);
111 }
112
113 #[test]
114 fn zones_partition_area_without_overlap() {
115 let area = Rect::new(0, 0, 80, 24);
116 let zones = Zones::for_state(area, &state());
117 assert!(zones.chat.height > 0);
118 assert!(zones.input.height >= 3);
120 assert_eq!(zones.attachments.height, 0);
122 assert_eq!(zones.status.height, 0);
123 }
124
125 #[test]
126 fn status_reserves_one_row_when_present() {
127 let mut s = state();
128 s.status = Some(crate::domain::StatusLine {
129 text: "hi".to_string(),
130 kind: crate::domain::StatusKind::Info,
131 shown_at: std::time::SystemTime::now(),
132 });
133 let area = Rect::new(0, 0, 80, 24);
134 let zones = Zones::for_state(area, &s);
135 assert_eq!(zones.status.height, 1);
136 }
137}