use super::super::render_scroll_view_persistent_state;
use super::{SESSIONS_MODAL, block_inner, draw_modal_panel, modal_text_area};
use crate::tui::{state::MissionControlState, theme::MATRIX_GREEN};
use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect, Size},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
};
use tui_scrollview::{ScrollView, ScrollbarVisibility};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SessionModalAreas {
pub(crate) help: Rect,
pub(crate) list: Rect,
pub(crate) preview: Option<Rect>,
}
pub(crate) fn sessions_modal_areas(area: Rect) -> Option<SessionModalAreas> {
let text_area = modal_text_area(area, "Sessions • Preview", SESSIONS_MODAL)?;
if text_area.height < 2 {
return None;
}
let help = Rect::new(text_area.x, text_area.y, text_area.width, 1);
let content = Rect::new(
text_area.x,
text_area.y.saturating_add(1),
text_area.width,
text_area.height.saturating_sub(1),
);
if content.height == 0 {
return None;
}
if content.width >= 50 && content.height >= 3 {
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(35),
Constraint::Length(1),
Constraint::Percentage(64),
])
.split(content);
let list = block_inner(columns[0]);
let preview = block_inner(columns[2]);
if list.width > 0 && list.height > 0 && preview.width > 0 && preview.height > 0 {
return Some(SessionModalAreas {
help,
list,
preview: Some(preview),
});
}
}
Some(SessionModalAreas {
help,
list: block_inner(content),
preview: None,
})
}
pub(crate) fn sessions_modal_list_area(area: Rect) -> Option<Rect> {
sessions_modal_areas(area).map(|areas| areas.list)
}
pub(crate) fn sessions_modal_preview_area(area: Rect) -> Option<Rect> {
sessions_modal_areas(area).and_then(|areas| areas.preview)
}
pub(in crate::tui::render) fn draw_sessions_modal(
frame: &mut Frame<'_>,
area: Rect,
state: &MissionControlState,
) {
let Some(picker) = &state.modals.session_picker else {
return;
};
let Some(text_area) = draw_modal_panel(frame, area, "Sessions • Preview", SESSIONS_MODAL)
else {
return;
};
let Some(areas) = sessions_modal_areas(area) else {
return;
};
frame.render_widget(
Paragraph::new(super::super::hotkey_line(
"[Tab] focus • [Up]/[Down] move/scroll • Mouse wheel scroll • [Enter] switch • [Esc] close",
MATRIX_GREEN.muted(false),
))
.style(MATRIX_GREEN.muted(false)),
areas.help,
);
let content_area = Rect::new(
text_area.x,
text_area.y.saturating_add(1),
text_area.width,
text_area.height.saturating_sub(1),
);
let list_block_area = if areas.preview.is_some() {
Rect::new(
areas.list.x.saturating_sub(1),
areas.list.y.saturating_sub(1),
areas.list.width.saturating_add(2),
areas.list.height.saturating_add(2),
)
} else {
content_area
};
let list_focused =
areas.preview.is_none() || picker.focus == crate::tui::state::SessionPickerFocus::List;
frame.render_widget(
Block::default()
.title("Sessions")
.borders(Borders::ALL)
.border_style(MATRIX_GREEN.border(list_focused))
.style(MATRIX_GREEN.pane()),
list_block_area,
);
draw_session_list(frame, areas.list, state);
if let Some(preview_area) = areas.preview {
let preview_block_area = Rect::new(
preview_area.x.saturating_sub(1),
preview_area.y.saturating_sub(1),
preview_area.width.saturating_add(2),
preview_area.height.saturating_add(2),
);
let preview_focused = picker.focus == crate::tui::state::SessionPickerFocus::Preview;
frame.render_widget(
Block::default()
.title("Preview")
.borders(Borders::ALL)
.border_style(MATRIX_GREEN.border(preview_focused))
.style(MATRIX_GREEN.pane()),
preview_block_area,
);
if preview_area.height > 0 && preview_area.width > 0 {
let preview_text = state.session_picker_preview_text();
let content_width = super::super::scroll_view_content_width(preview_area).max(1);
let content_height = super::super::usize_to_u16_saturating(
crate::tui::transcript::ratatui_wrapped_visual_rows(&preview_text, content_width),
);
render_scroll_view_persistent_state(
frame,
preview_area,
content_height,
Paragraph::new(preview_text)
.style(MATRIX_GREEN.pane())
.wrap(ratatui::widgets::Wrap { trim: false }),
&state.scroll_views.session_preview,
);
}
}
}
fn draw_session_list(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
if area.height == 0 || area.width == 0 {
return;
}
let Some(picker) = &state.modals.session_picker else {
return;
};
if picker.rows.is_empty() {
frame.render_widget(
Paragraph::new("No saved sessions found").style(MATRIX_GREEN.pane()),
area,
);
return;
}
let content_height =
super::super::usize_to_u16_saturating(picker.rows.len().max(usize::from(area.height)));
let content_width = super::super::scroll_view_content_width(area);
let mut view = ScrollView::new(Size::new(content_width, content_height))
.vertical_scrollbar_visibility(ScrollbarVisibility::Always)
.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
let items = picker
.rows
.iter()
.map(|row| {
let marker = if row.is_current {
"● current "
} else {
" "
};
ListItem::new(Line::from(vec![Span::raw(format!(
"{marker}{}",
row.label
))]))
})
.collect::<Vec<_>>();
let mut list_state = ListState::default().with_selected(Some(picker.selected));
view.render_stateful_widget(
List::new(items)
.style(MATRIX_GREEN.pane())
.highlight_style(MATRIX_GREEN.selection())
.highlight_symbol("> "),
Rect::new(0, 0, content_width, content_height),
&mut list_state,
);
frame.render_stateful_widget(
view,
area,
&mut *state.scroll_views.session_list.borrow_mut(),
);
}