use super::super::theme::MissionControlTheme;
use super::{bordered_block, overlay_text_area};
use crate::tui::modal_container::{draw_modal_container, modal_area};
use ratatui::{
Frame,
layout::{Rect, Size},
widgets::{List, ListItem, ListState},
};
use std::cell::RefCell;
use tui_scrollview::ScrollView;
use super::super::state::{MissionControlState, ModalKind};
fn header_modal_close_hint(modal: ModalKind) -> &'static str {
match modal {
ModalKind::ActivityTree => "[Alt-1] Hide",
ModalKind::SessionFiles => "[Alt-2] Hide",
ModalKind::Summary => "[Alt-3] Hide",
_ => "",
}
}
pub(crate) fn header_modal_close_control_area(area: Rect, modal: ModalKind) -> Option<Rect> {
crate::tui::modal_container::close_control_area(area, Some(header_modal_close_hint(modal)))
}
pub(super) fn header_modal_block<'a>(
state: &MissionControlState,
modal: ModalKind,
title: &'a str,
hotkeys: &'a str,
) -> ratatui::widgets::Block<'a> {
let focused = state.header_modal_visible() && state.modals.focused_header_modal == Some(modal);
crate::tui::modal_container::modal_block(
ratatui::text::Line::styled(title, state.theme.title(focused)),
if !focused {
String::new()
} else if state.modals.header_modals_pinned {
hotkeys.to_owned()
} else {
format!("{hotkeys} ยท [Esc] close")
},
state.theme,
Some(header_modal_close_hint(modal)),
)
.border_style(state.theme.border(focused))
}
pub(super) fn draw_header_modals(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
let layout = super::layout_for(area, state);
for &modal in &state.modals.header_modals {
if let Some(area) = layout.header_modal_area(modal) {
match modal {
ModalKind::Summary => super::draw_summary(frame, area, state),
ModalKind::ActivityTree => super::activity::draw_tree(frame, area, state),
ModalKind::SessionFiles => super::draw_session_files(frame, area, state),
_ => {}
}
}
}
}
pub(super) fn draw_top_modal(frame: &mut Frame<'_>, area: Rect, state: &MissionControlState) {
match state.top_modal() {
Some(ModalKind::Summary | ModalKind::ActivityTree | ModalKind::SessionFiles) => {
draw_header_modals(frame, area, state);
}
Some(ModalKind::ActivityDetail) => {
super::activity::draw_detail(frame, super::activity_detail_modal_area(area), state)
}
Some(ModalKind::Settings) => {
if let Some(editor) = &state.modals.settings_editor {
crate::tui::settings_editor::draw(frame, area, editor, state.theme);
}
}
Some(ModalKind::SubagentViewer) => draw_subagent_viewer_modal(frame, area, state),
Some(ModalKind::SessionPicker) => draw_sessions_modal(frame, area, state),
Some(ModalKind::Skills) => draw_skills_modal(frame, area, state),
Some(ModalKind::Mcp) => draw_mcp_modal(frame, area, state),
Some(ModalKind::Tools) => {
if let Some(editor) = &state.modals.settings_editor {
crate::tui::settings_editor::draw(frame, area, editor, state.theme);
}
draw_tools_modal(frame, area, state);
}
Some(ModalKind::Subagents) => draw_subagents_modal(frame, area, state),
Some(ModalKind::Usage) => draw_usage_modal(frame, area, state),
Some(ModalKind::SystemPrompt) => draw_system_prompt_modal(frame, area, state),
Some(ModalKind::Rewind) => draw_rewind_modal(frame, area, state),
Some(ModalKind::ConnectProvider) => draw_connect_provider(frame, area, state),
Some(ModalKind::BashApproval) => bash_approval::draw_bash_approval(frame, area, state),
Some(ModalKind::LogoutConfirmation) => draw_logout_confirmation(frame, area, state),
Some(ModalKind::LogoutPicker) => draw_logout_picker(frame, area, state),
Some(ModalKind::ModelPicker) => draw_model_picker(frame, area, state),
Some(ModalKind::ThemePicker) => draw_theme_picker(frame, area, state),
None => {}
}
}
pub(crate) fn top_modal_close_control_area(
area: Rect,
state: &MissionControlState,
) -> Option<Rect> {
let mut hint = Some("[Esc] close");
let modal = match state.top_modal()? {
modal @ (ModalKind::Summary | ModalKind::ActivityTree | ModalKind::SessionFiles) => {
return header_modal_close_control_area(
super::layout_for(area, state).header_modal_area(modal)?,
modal,
);
}
ModalKind::Settings => {
return crate::tui::settings_editor::close_control_area(
area,
state.modals.settings_editor.as_ref()?,
);
}
ModalKind::ActivityDetail | ModalKind::SubagentViewer => modal_area(area, 60, 60),
ModalKind::SessionPicker => modal_rect(area, SESSIONS_MODAL)?,
ModalKind::SystemPrompt | ModalKind::Rewind => modal_rect(area, SYSTEM_PROMPT_MODAL)?,
ModalKind::Usage => modal_rect(area, usage::USAGE_MODAL)?,
ModalKind::ConnectProvider => {
hint = auth::connect_provider_hints(state.modals.connect_provider.as_ref()?.stage).1;
modal_rect(area, CONNECT_PROVIDER_MODAL)?
}
ModalKind::BashApproval => {
hint = Some("[Esc/d] deny");
modal_rect(area, PICKER_MODAL)?
}
ModalKind::LogoutConfirmation => {
hint = Some("[Esc] cancel");
modal_rect(area, CONFIRMATION_MODAL)?
}
ModalKind::ThemePicker => {
if matches!(
state.modals.theme_picker.as_ref()?.save_state,
crate::tui::state::ThemeSaveState::Pending
) {
return None;
}
modal_rect(area, PICKER_MODAL)?
}
ModalKind::Skills
| ModalKind::Mcp
| ModalKind::Tools
| ModalKind::Subagents
| ModalKind::LogoutPicker
| ModalKind::ModelPicker => modal_rect(area, PICKER_MODAL)?,
};
crate::tui::modal_container::close_control_area(modal, hint)
}
pub(super) mod auth;
pub(super) mod bash_approval;
pub(super) mod mcp;
pub(super) mod model;
pub(super) mod rewind;
pub(super) mod sessions;
pub(super) mod skills;
pub(super) mod subagent_viewer;
pub(super) mod subagents;
pub(super) mod system_prompt;
pub(super) mod theme;
pub(super) mod tools;
pub(super) mod usage;
pub(crate) use auth::{
connect_provider_choice_list_area, connect_provider_custom_field_visible,
connect_provider_modal_visible, connect_provider_oauth_button_areas,
connect_provider_oauth_controls_visible, connect_provider_oauth_fallback_visible,
logout_confirmation_action_row_area, logout_picker_list_area,
};
pub(super) use auth::{draw_connect_provider, draw_logout_confirmation, draw_logout_picker};
pub(super) use mcp::draw_mcp_modal;
pub(crate) use mcp::mcp_modal_list_area;
pub(super) use model::draw_model_picker;
pub(crate) use model::model_picker_list_area;
pub(super) use rewind::draw_rewind_modal;
pub(crate) use rewind::rewind_modal_text_area;
pub(super) use sessions::draw_sessions_modal;
pub(crate) use sessions::{
sessions_modal_areas, sessions_modal_list_area, sessions_modal_preview_area,
};
pub(super) use skills::draw_skills_modal;
pub(crate) use skills::{skill_row_at, skills_modal_list_area};
pub(super) use subagent_viewer::draw_subagent_viewer_modal;
pub(crate) use subagent_viewer::{
subagent_viewer_prompt_area, subagent_viewer_tab_at, subagent_viewer_timeline_area,
};
pub(super) use subagents::draw_subagents_modal;
pub(crate) use subagents::subagents_modal_list_area;
pub(super) use system_prompt::draw_system_prompt_modal;
pub(crate) use system_prompt::system_prompt_text_area;
pub(super) use theme::draw_theme_picker;
pub(crate) use theme::{theme_picker_list_area, theme_picker_query_visible};
pub(super) use tools::draw_tools_modal;
pub(crate) use tools::tools_modal_list_area;
pub(super) use usage::draw_usage_modal;
pub(crate) use usage::usage_modal_text_area;
#[derive(Debug, Clone, Copy)]
pub(in crate::tui::render) struct ModalSpec {
min_width: u16,
min_height: u16,
width_ratio: (u16, u16),
height: ModalHeight,
}
#[derive(Debug, Clone, Copy)]
pub(in crate::tui::render) enum ModalHeight {
Ratio(u16, u16),
}
pub(in crate::tui::render) const SYSTEM_PROMPT_MODAL: ModalSpec = ModalSpec {
min_width: 12,
min_height: 5,
width_ratio: (4, 5),
height: ModalHeight::Ratio(4, 5),
};
pub(super) const SESSIONS_MODAL: ModalSpec = ModalSpec {
min_width: 12,
min_height: 5,
width_ratio: (4, 5),
height: ModalHeight::Ratio(4, 5),
};
pub(in crate::tui::render) const PICKER_MODAL: ModalSpec = ModalSpec {
min_width: 12,
min_height: 5,
width_ratio: (3, 5),
height: ModalHeight::Ratio(3, 5),
};
pub(super) const CONFIRMATION_MODAL: ModalSpec = ModalSpec {
min_width: 12,
min_height: 5,
width_ratio: (3, 5),
height: ModalHeight::Ratio(3, 10),
};
pub(super) const CONNECT_PROVIDER_MODAL: ModalSpec = ModalSpec {
min_width: 60,
min_height: 18,
width_ratio: (3, 5),
height: ModalHeight::Ratio(2, 5),
};
pub(in crate::tui::render) fn modal_rect(area: Rect, spec: ModalSpec) -> Option<Rect> {
if area.width < spec.min_width || area.height < spec.min_height {
return None;
}
let width_percent =
(u32::from(spec.width_ratio.0) * 100 / u32::from(spec.width_ratio.1)) as u16;
let height_percent = match spec.height {
ModalHeight::Ratio(numerator, denominator) => {
(u32::from(numerator) * 100 / u32::from(denominator)) as u16
}
};
Some(modal_area(area, width_percent, height_percent))
}
pub(super) fn modal_text_area(area: Rect, _title: &'static str, spec: ModalSpec) -> Option<Rect> {
let modal = modal_rect(area, spec)?;
Some(overlay_text_area(bordered_block().inner(modal)))
}
pub(super) fn draw_modal_panel_with_hotkeys(
frame: &mut Frame<'_>,
area: Rect,
title: &'static str,
spec: ModalSpec,
theme: MissionControlTheme,
hotkeys: &str,
close_hint: Option<&str>,
) -> Option<Rect> {
let modal = modal_rect(area, spec)?;
let inner = draw_modal_container(frame, modal, title, hotkeys, theme, close_hint);
Some(overlay_text_area(inner))
}
pub(super) fn block_inner(area: Rect) -> Rect {
Rect::new(
area.x.saturating_add(1),
area.y.saturating_add(1),
area.width.saturating_sub(2),
area.height.saturating_sub(2),
)
}
pub(super) fn picker_list_area(text_area: Rect, header_rows: u16) -> Option<Rect> {
if text_area.width == 0 || text_area.height == 0 {
return None;
}
let list_y = text_area.y.saturating_add(header_rows);
let list_height = text_area
.y
.saturating_add(text_area.height)
.saturating_sub(list_y);
(list_height > 0).then_some(Rect::new(text_area.x, list_y, text_area.width, list_height))
}
pub(super) fn picker_modal_list_area(
area: Rect,
title: &'static str,
header_rows: u16,
) -> Option<Rect> {
let text_area = modal_text_area(area, title, PICKER_MODAL)?;
picker_list_area(text_area, header_rows)
}
pub(crate) fn settings_scope_visible(area: ratatui::layout::Rect) -> bool {
modal_text_area(area, "Settings", PICKER_MODAL)
.is_some_and(|text_area| text_area.width > 0 && text_area.height >= 2)
}
pub(super) fn render_picker_list(
frame: &mut Frame<'_>,
area: Rect,
items: Vec<ListItem<'static>>,
selected: usize,
scroll_state: &RefCell<tui_scrollview::ScrollViewState>,
theme: MissionControlTheme,
) {
if area.width == 0 || area.height == 0 {
return;
}
let content_height = super::usize_to_u16_saturating(items.len().max(usize::from(area.height)));
let content_width = super::scroll_view_content_width(area);
if content_width == 0 {
return;
}
let mut view = ScrollView::new(Size::new(content_width, content_height));
let mut list_state = ListState::default().with_selected(Some(selected));
view.render_stateful_widget(
List::new(items)
.style(theme.pane())
.highlight_style(theme.selection())
.highlight_symbol("> "),
Rect::new(0, 0, content_width, content_height),
&mut list_state,
);
let mut local_scroll_state = *scroll_state.borrow();
super::render_prepared_scroll_view_with_theme(
frame,
area,
view,
&mut local_scroll_state,
None,
theme,
);
}