use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style},
text::{Line, Span},
widgets::{Clear, Paragraph, Widget},
};
use super::{PopupSizing, SizeHint};
use crate::tui::theme::{self, BlockVariant};
pub struct ErrorPopup<'a> {
message: &'a str,
}
impl<'a> ErrorPopup<'a> {
#[must_use]
pub fn new(message: &'a str) -> Self {
Self { message }
}
}
impl PopupSizing for ErrorPopup<'_> {
#[allow(clippy::cast_possible_truncation)] fn size_hint(&self) -> SizeHint {
let line_count = self.message.lines().count().max(1);
let min_height = (line_count + 5).min(20) as u16;
SizeHint::percent(0, 0)
.with_min_width(35)
.with_min_height(min_height)
}
}
impl Widget for ErrorPopup<'_> {
#[allow(clippy::cast_possible_truncation)]
fn render(self, area: Rect, buf: &mut Buffer) {
Clear.render(area, buf);
let block = theme::block(" Error ", BlockVariant::Error);
let inner = block.inner(area);
block.render(area, buf);
let line_count = self.message.lines().count().max(1) as u16;
let chunks = Layout::vertical([
Constraint::Length(1), Constraint::Length(line_count), Constraint::Length(1), Constraint::Length(1), ])
.split(inner);
Paragraph::new(self.message)
.style(Style::default().fg(Color::White))
.alignment(Alignment::Center)
.render(chunks[1], buf);
Paragraph::new("[Enter/Esc] OK")
.style(Style::default().fg(Color::Gray))
.alignment(Alignment::Center)
.render(chunks[3], buf);
}
}
pub struct SessionTerminatedPopup<'a> {
session_name: &'a str,
exit_code: Option<i32>,
}
impl<'a> SessionTerminatedPopup<'a> {
#[must_use]
pub fn new(session_name: &'a str, exit_code: Option<i32>) -> Self {
Self {
session_name,
exit_code,
}
}
}
impl PopupSizing for SessionTerminatedPopup<'_> {
fn size_hint(&self) -> SizeHint {
SizeHint::percent(0, 0)
.with_min_width(40)
.with_min_height(7)
}
}
impl Widget for SessionTerminatedPopup<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Clear.render(area, buf);
let block = theme::block(" Session Terminated ", BlockVariant::Warning);
let inner = block.inner(area);
block.render(area, buf);
let chunks = Layout::vertical([
Constraint::Length(1), Constraint::Length(2), Constraint::Length(1), Constraint::Length(1), ])
.split(inner);
let exit_text = match self.exit_code {
Some(code) => format!("Exit code: {code}"),
None => "Exit code: unknown".to_string(),
};
let content = vec![
Line::from(vec![
Span::styled("Session: ", Style::default().fg(Color::Gray)),
Span::styled(self.session_name, Style::default().fg(Color::White)),
]),
Line::from(Span::styled(exit_text, Style::default().fg(Color::Gray))),
];
Paragraph::new(content)
.alignment(Alignment::Center)
.render(chunks[1], buf);
Paragraph::new("[C] Close session [K] Keep")
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Center)
.render(chunks[3], buf);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tui::test_utils::buffer_to_text;
#[test]
fn error_popup_new() {
let popup = ErrorPopup::new("test error");
assert_eq!(popup.message, "test error");
}
#[test]
fn error_popup_renders_message() {
let popup = ErrorPopup::new("Something went wrong");
let area = Rect::new(0, 0, 40, 10);
let mut buf = Buffer::empty(area);
popup.render(area, &mut buf);
let output = buffer_to_text(&buf);
assert!(output.contains("Error"));
assert!(output.contains("Something went wrong"));
assert!(output.contains("[Enter/Esc] OK"));
}
#[test]
fn error_popup_renders_small_area() {
let popup = ErrorPopup::new("test");
let area = Rect::new(0, 0, 20, 5);
let mut buf = Buffer::empty(area);
popup.render(area, &mut buf);
let output = buffer_to_text(&buf);
assert!(output.contains("Error"));
}
#[test]
fn error_popup_renders_long_message() {
let long_msg = "This is a very long error message that should be wrapped to fit within the popup boundaries";
let popup = ErrorPopup::new(long_msg);
let area = Rect::new(0, 0, 30, 10);
let mut buf = Buffer::empty(area);
popup.render(area, &mut buf);
let output = buffer_to_text(&buf);
assert!(output.contains("Error"));
}
#[test]
fn session_terminated_popup_new() {
let popup = SessionTerminatedPopup::new("test-session", Some(0));
assert_eq!(popup.session_name, "test-session");
assert_eq!(popup.exit_code, Some(0));
}
#[test]
fn session_terminated_popup_renders() {
let popup = SessionTerminatedPopup::new("test-session", Some(0));
let area = Rect::new(0, 0, 50, 10);
let mut buf = Buffer::empty(area);
popup.render(area, &mut buf);
let output = buffer_to_text(&buf);
assert!(output.contains("Session Terminated"));
assert!(output.contains("Session:"));
assert!(output.contains("test-session"));
assert!(output.contains("Exit code: 0"));
assert!(output.contains("[C] Close session"));
assert!(output.contains("[K] Keep"));
}
#[test]
fn session_terminated_popup_renders_unknown_exit_code() {
let popup = SessionTerminatedPopup::new("test-session", None);
let area = Rect::new(0, 0, 50, 10);
let mut buf = Buffer::empty(area);
popup.render(area, &mut buf);
let output = buffer_to_text(&buf);
assert!(output.contains("Exit code: unknown"));
}
#[test]
fn session_terminated_popup_renders_small_area() {
let popup = SessionTerminatedPopup::new("test", Some(1));
let area = Rect::new(0, 0, 30, 4);
let mut buf = Buffer::empty(area);
popup.render(area, &mut buf);
let output = buffer_to_text(&buf);
assert!(output.contains("Session Terminated"));
}
mod snapshots {
use super::*;
use crate::tui::test_utils::render_to_snapshot;
use insta::assert_snapshot;
#[test]
fn error_popup_short_message() {
let popup = ErrorPopup::new("Connection failed");
assert_snapshot!(render_to_snapshot(popup, 40, 8));
}
#[test]
fn session_terminated_with_exit_code() {
let popup = SessionTerminatedPopup::new("my-session", Some(0));
assert_snapshot!(render_to_snapshot(popup, 45, 8));
}
#[test]
fn session_terminated_unknown_exit() {
let popup = SessionTerminatedPopup::new("crashed", None);
assert_snapshot!(render_to_snapshot(popup, 45, 8));
}
}
}