use anyhow::Result;
use std::io::{stdout, Write};
#[derive(Debug, Clone)]
pub struct ComponentAreas {
pub terminal_size: (u16, u16),
pub status_bar: ComponentArea,
pub task_panel: ComponentArea,
pub chat_area: ComponentArea,
pub input_field: ComponentArea,
pub suggestions: ComponentArea,
}
#[derive(Debug, Clone)]
pub struct ComponentArea {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
pub visible: bool,
}
impl ComponentArea {
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
Self {
x,
y,
width,
height,
visible: true,
}
}
pub fn is_valid(&self, terminal_size: (u16, u16)) -> bool {
self.x + self.width <= terminal_size.0 && self.y + self.height <= terminal_size.1
}
}
impl ComponentAreas {
pub fn calculate(terminal_size: (u16, u16)) -> Self {
let (cols, rows) = terminal_size;
let mut current_y = 0u16;
let status_bar = ComponentArea::new(0, current_y, cols, 1);
current_y += 2;
let task_panel_height = if rows >= 20 {
6
} else if rows >= 15 {
4
} else {
0
};
let task_panel = if task_panel_height > 0 {
let area = ComponentArea::new(0, current_y, cols, task_panel_height);
current_y += task_panel_height + 1; area
} else {
ComponentArea::new(0, 0, 0, 0) };
let remaining_rows = rows.saturating_sub(current_y).saturating_sub(4); let chat_height = remaining_rows.max(2); let chat_area = ComponentArea::new(0, current_y, cols, chat_height);
current_y += chat_height;
let suggestions_height = 2;
let suggestions = ComponentArea::new(0, current_y, cols, suggestions_height);
current_y += suggestions_height;
let input_field = ComponentArea::new(0, current_y, cols, 1);
Self {
terminal_size,
status_bar,
task_panel: ComponentArea {
visible: task_panel_height > 0,
..task_panel
},
chat_area,
input_field,
suggestions: ComponentArea {
visible: false,
..suggestions
}, }
}
fn minimal_layout(terminal_size: (u16, u16)) -> Self {
let (cols, rows) = terminal_size;
let status_bar = ComponentArea::new(0, 0, cols, 1);
let input_field = ComponentArea::new(0, rows.saturating_sub(2), cols, 2);
let task_panel = ComponentArea::new(0, 0, 0, 0);
let chat_area = ComponentArea::new(0, 1, cols, rows.saturating_sub(3));
let suggestions = ComponentArea::new(0, 0, 0, 0);
Self {
terminal_size,
status_bar,
task_panel: ComponentArea {
visible: false,
..task_panel
},
chat_area,
input_field,
suggestions: ComponentArea {
visible: false,
..suggestions
},
}
}
pub fn update_for_size(&mut self, new_size: (u16, u16)) {
*self = Self::calculate(new_size);
}
}
pub struct TerminalRenderer {
areas: ComponentAreas,
}
impl TerminalRenderer {
pub fn new() -> Result<Self> {
Ok(Self {
areas: ComponentAreas::calculate((80, 24)), })
}
pub fn render_component_in_place<F>(
&mut self,
_area: &ComponentArea,
render_fn: F,
) -> Result<()>
where
F: FnOnce(&mut std::io::Stdout, &ComponentArea) -> Result<()>,
{
let dummy_area = ComponentArea::new(0, 0, 80, 10);
render_fn(&mut stdout(), &dummy_area)?;
stdout().flush()?;
Ok(())
}
pub fn areas(&self) -> &ComponentAreas {
&self.areas
}
pub fn handle_resize(&mut self) -> Result<bool> {
Ok(false)
}
}
pub fn wrap_text_smart(text: &str, width: usize) -> Vec<String> {
if width < 10 {
return text
.chars()
.collect::<Vec<char>>()
.chunks(width)
.map(|chunk| chunk.iter().collect())
.collect();
}
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
if word.len() > width {
if !current_line.is_empty() {
lines.push(current_line.clone());
current_line.clear();
}
let chars: Vec<char> = word.chars().collect();
for chunk in chars.chunks(width) {
lines.push(chunk.iter().collect());
}
continue;
}
let space_needed = if current_line.is_empty() { 0 } else { 1 }; if current_line.len() + space_needed + word.len() > width {
if !current_line.is_empty() {
lines.push(current_line.clone());
current_line.clear();
}
}
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
}
if !current_line.is_empty() {
lines.push(current_line);
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
pub fn truncate_with_ellipsis(text: &str, max_width: usize) -> String {
if text.len() <= max_width {
return text.to_string();
}
if max_width <= 3 {
return "...".chars().take(max_width).collect();
}
let mut result = text.chars().take(max_width - 3).collect::<String>();
result.push_str("...");
result
}
pub fn format_colored_text(text: &str, color: crossterm::style::Color) -> String {
let color_code = match color {
crossterm::style::Color::Red => "31",
crossterm::style::Color::Green => "32",
crossterm::style::Color::Yellow => "33",
crossterm::style::Color::Blue => "34",
crossterm::style::Color::Magenta => "35",
crossterm::style::Color::Cyan => "36",
crossterm::style::Color::White => "37",
crossterm::style::Color::Grey => "90",
_ => "37", };
format!("\x1b[{}m{}\x1b[0m", color_code, text)
}
pub fn render_without_scroll<F>(render_fn: F) -> Result<()>
where
F: FnOnce() -> Result<()>,
{
render_fn()?;
stdout().flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_areas_calculation() {
let areas = ComponentAreas::calculate((100, 30));
assert_eq!(areas.terminal_size, (100, 30));
assert_eq!(areas.status_bar.height, 1);
assert!(areas.task_panel.height > 0);
assert!(areas.input_field.height >= 3);
assert!(areas.chat_area.height > 0);
}
#[test]
fn test_minimal_layout() {
let areas = ComponentAreas::calculate((50, 8));
assert!(!areas.task_panel.visible);
assert!(!areas.suggestions.visible);
assert!(areas.status_bar.visible);
assert!(areas.input_field.visible);
}
#[test]
fn test_text_wrapping() {
let wrapped = wrap_text_smart("This is a long line of text that should be wrapped", 20);
assert!(wrapped.len() > 1);
for line in &wrapped {
assert!(line.len() <= 20);
}
}
#[test]
fn test_text_truncation() {
let truncated = truncate_with_ellipsis("This is a very long string", 15);
assert_eq!(truncated.len(), 15);
assert!(truncated.ends_with("..."));
}
}