tazuna 0.1.0

TUI tool for managing multiple Claude Code sessions in parallel
Documentation
//! TUI rendering for TUI application.
//!
//! Contains render method for drawing UI components.

use ratatui::{
    Frame,
    layout::{Constraint, Layout},
};

use crate::tui::popup::{
    ActionSelectPopup, ConfirmPermissionsPopup, ConfirmQuitPopup, ErrorPopup, IssueItem,
    IssueLoadingPopup, PopupSizing, SessionItem, SessionTerminatedPopup, WorkspacePopup,
};
use crate::tui::status_bar::StatusBar;
use crate::tui::tabs::{StatusIndicator, TabBar, TabItem};
use crate::tui::terminal::TerminalView;

use super::state::AppState;
use super::tuiapp::TuiApp;

impl TuiApp {
    // Layout slot indices for main view
    const SLOT_TAB_BAR: usize = 0;
    const SLOT_TERMINAL: usize = 1;
    const SLOT_STATUS_BAR: usize = 2;

    /// Render UI
    pub fn render(&mut self, frame: &mut Frame) {
        let area = frame.area();

        // Layout: Tab bar | Terminal | Status bar
        let areas = Layout::vertical([
            Constraint::Length(1), // SLOT_TAB_BAR
            Constraint::Min(1),    // SLOT_TERMINAL
            Constraint::Length(1), // SLOT_STATUS_BAR
        ])
        .split(area);

        self.render_tab_bar(frame, areas[Self::SLOT_TAB_BAR]);
        self.render_terminal(frame, areas[Self::SLOT_TERMINAL]);
        self.render_status_bar(frame, areas[Self::SLOT_STATUS_BAR]);
        self.render_popup(frame, area);
    }

    fn render_tab_bar(&self, frame: &mut Frame, area: ratatui::layout::Rect) {
        let tabs: Vec<TabItem> = self
            .sessions
            .iter()
            .map(|s| TabItem::new(&s.name, StatusIndicator::from(&s.status)))
            .collect();
        frame.render_widget(TabBar::new(&tabs, self.active_idx), area);
    }

    fn render_terminal(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
        if self.sessions.is_empty() {
            frame.render_widget(crate::tui::welcome::WelcomeScreen::new(), area);
            return;
        }

        let Some(session) = self.sessions.get(self.active_idx) else {
            return;
        };
        let offset = self.scroll_offsets.get(&session.id).copied().unwrap_or(0);

        if let Some(parser) = self.terminal_buffers.get_mut(&session.id) {
            parser.set_scrollback(offset);
            frame.render_widget(TerminalView::new(parser), area);
            parser.set_scrollback(0);
        }
    }

    fn render_status_bar(&self, frame: &mut Frame, area: ratatui::layout::Rect) {
        let mut status_bar =
            StatusBar::new(self.sessions.len()).notifications(self.pending_count());

        if let Some(cost) = self.today_cost {
            status_bar = status_bar.today_cost(cost);
        }

        if let Some(session) = self.sessions.get(self.active_idx) {
            status_bar = status_bar.active_session(&session.name);
            if let Some(branch) = &session.branch {
                status_bar = status_bar.active_branch(branch);
            }
        }
        frame.render_widget(status_bar, area);
    }

    fn render_popup(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
        match self.state {
            AppState::WorkspacePopup => self.render_workspace_popup(frame, area),
            AppState::ConfirmQuit => self.render_confirm_quit(frame, area),
            AppState::ErrorPopup { ref message, .. } => {
                let popup = ErrorPopup::new(message);
                let popup_area = popup.compute_area(area);
                frame.render_widget(popup, popup_area);
            }
            AppState::SessionTerminatedPopup {
                session_id,
                exit_code,
            } => {
                self.render_terminated_popup(frame, area, session_id, exit_code);
            }
            AppState::IssueLoading {
                issue_number,
                ref phase,
            } => {
                let popup = IssueLoadingPopup::new(issue_number, phase.message());
                let popup_area = popup.compute_area(area);
                frame.render_stateful_widget(popup, popup_area, &mut self.issue_loading_throbber);
            }
            AppState::ActionSelectPopup {
                issue_number,
                ref choices,
            } => {
                let popup = ActionSelectPopup::new(issue_number, choices);
                let popup_area = popup.compute_area(area);
                frame.render_stateful_widget(popup, popup_area, &mut self.action_select_state);
            }
            AppState::ConfirmPermissions {
                ref branch,
                selected_yes,
                ..
            } => {
                let popup = if selected_yes {
                    ConfirmPermissionsPopup::new(branch).select_yes()
                } else {
                    ConfirmPermissionsPopup::new(branch).select_no()
                };
                let popup_area = popup.compute_area(area);
                frame.render_widget(popup, popup_area);
            }
            AppState::Normal => {}
        }
    }

    fn render_workspace_popup(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
        let session_items: Vec<SessionItem> = self
            .sessions
            .iter()
            .map(|s| {
                SessionItem::new(&s.name, StatusIndicator::from(&s.status))
                    .branch(s.branch.clone())
                    .pending(self.is_pending(&s.id))
            })
            .collect();
        let issues: Vec<_> = self
            .issues
            .iter()
            .map(|i| IssueItem::new(i.number, &i.title, &i.labels))
            .collect();
        let worktrees = self.worktrees.clone();
        let input = self.popup_state.input.clone();
        let cursor = self.popup_state.cursor;
        let section = self.popup_state.section;
        let popup =
            WorkspacePopup::new(&session_items, &worktrees, &issues, &input, cursor, section);
        let popup_area = popup.compute_area(area);
        frame.render_stateful_widget(popup, popup_area, &mut self.popup_state);
    }

    fn render_confirm_quit(&self, frame: &mut Frame, area: ratatui::layout::Rect) {
        let popup = if self.quit_selected_yes {
            ConfirmQuitPopup::new().select_yes()
        } else {
            ConfirmQuitPopup::new()
        };
        let popup_area = popup.compute_area(area);
        frame.render_widget(popup, popup_area);
    }

    fn render_terminated_popup(
        &self,
        frame: &mut Frame,
        area: ratatui::layout::Rect,
        session_id: crate::session::SessionId,
        exit_code: Option<i32>,
    ) {
        let session_name = self
            .sessions
            .iter()
            .find(|s| s.id == session_id)
            .map_or("Session", |s| &s.name);
        let popup = SessionTerminatedPopup::new(session_name, exit_code);
        let popup_area = popup.compute_area(area);
        frame.render_widget(popup, popup_area);
    }
}