cc-switch-tui 0.1.3

All-in-One Assistant for Claude Code, Codex, Gemini, OpenCode, OpenClaw & Hermes
Documentation
use super::*;

pub(super) fn render_skills_installed(
    frame: &mut Frame<'_>,
    app: &App,
    data: &UiData,
    area: Rect,
    theme: &super::theme::Theme,
) {
    let outer = Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Plain)
        .border_style(pane_border_style(app, Focus::Content, theme))
        .title(texts::skills_management());
    frame.render_widget(outer.clone(), area);
    let inner = outer.inner(area);

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1),
            Constraint::Length(3),
            Constraint::Min(0),
        ])
        .split(inner);

    if app.focus == Focus::Content {
        render_key_bar_center(
            frame,
            chunks[0],
            theme,
            &[
                ("Enter", texts::tui_key_details()),
                ("gg/G", texts::tui_key_edges()),
                ("v", texts::tui_key_select()),
                ("x", texts::tui_key_toggle()),
                ("m", texts::tui_key_apps()),
                ("d", texts::tui_key_uninstall()),
                ("f", texts::tui_key_discover()),
                ("i", texts::tui_skills_action_import_existing()),
                ("s", texts::tui_skills_action_import_agent()),
            ],
        );
    }

    render_summary_bar(frame, chunks[1], theme, installed_summary(data));

    let visible = skills_installed_filtered(app, data);
    if visible.is_empty() {
        render_installed_empty_state(frame, chunks[2], theme);
        return;
    }

    let header = Row::new(vec![
        Cell::from(texts::header_name()),
        centered_cell("Claude"),
        centered_cell("Codex"),
        centered_cell("Gemini"),
        centered_cell("OpenCode"),
        centered_cell("OpenClaw"),
        centered_cell("Hermes"),
    ])
    .style(Style::default().fg(theme.dim).add_modifier(Modifier::BOLD));

    let visual_range = app.skills_visual_range(visible.len());
    let visual_style = if theme.no_color {
        Style::default().add_modifier(Modifier::REVERSED)
    } else {
        Style::default().bg(theme.surface)
    };

    let rows = visible.iter().enumerate().map(|(idx, skill)| {
        let row = Row::new(vec![
            Cell::from(skill_display_name(&skill.name, &skill.directory).to_string()),
            centered_cell(skill_marker(skill.apps.claude)),
            centered_cell(skill_marker(skill.apps.codex)),
            centered_cell(skill_marker(skill.apps.gemini)),
            centered_cell(skill_marker(skill.apps.opencode)),
            centered_cell(skill_marker(skill.apps.openclaw)),
            centered_cell(skill_marker(skill.apps.hermes)),
        ]);

        if visual_range.is_some_and(|(start, end)| (start..=end).contains(&idx)) {
            row.style(visual_style)
        } else {
            row
        }
    });

    let table = Table::new(
        rows,
        [
            Constraint::Percentage(40),
            Constraint::Length(8),
            Constraint::Length(8),
            Constraint::Length(8),
            Constraint::Length(10),
            Constraint::Length(10),
            Constraint::Length(8),
        ],
    )
    .header(header)
    .block(Block::default().borders(Borders::NONE))
    .row_highlight_style(selection_style(theme))
    .highlight_symbol(highlight_symbol(theme));

    let mut state = TableState::default();
    state.select(Some(app.skills_idx));
    frame.render_stateful_widget(table, inset_left(chunks[2], CONTENT_INSET_LEFT), &mut state);
}

fn installed_summary(data: &UiData) -> String {
    let enabled_claude = data
        .skills
        .installed
        .iter()
        .filter(|s| s.apps.claude)
        .count();
    let enabled_codex = data
        .skills
        .installed
        .iter()
        .filter(|s| s.apps.codex)
        .count();
    let enabled_gemini = data
        .skills
        .installed
        .iter()
        .filter(|s| s.apps.gemini)
        .count();
    let enabled_opencode = data
        .skills
        .installed
        .iter()
        .filter(|s| s.apps.opencode)
        .count();
    let enabled_openclaw = data
        .skills
        .installed
        .iter()
        .filter(|s| s.apps.openclaw)
        .count();
    let enabled_hermes = data
        .skills
        .installed
        .iter()
        .filter(|s| s.apps.hermes)
        .count();

    texts::tui_skills_installed_counts(
        enabled_claude,
        enabled_codex,
        enabled_gemini,
        enabled_opencode,
        enabled_openclaw,
        enabled_hermes,
    )
}

fn render_installed_empty_state(frame: &mut Frame<'_>, area: Rect, theme: &super::theme::Theme) {
    let empty_chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(0),
            Constraint::Length(7),
            Constraint::Min(0),
        ])
        .split(area);

    let icon_style = if theme.no_color {
        Style::default().add_modifier(Modifier::BOLD)
    } else {
        Style::default()
            .fg(theme.accent)
            .add_modifier(Modifier::BOLD)
    };

    let empty_lines = vec![
        Line::raw(""),
        Line::from(Span::styled("", icon_style)),
        Line::raw(""),
        Line::from(Span::styled(
            texts::tui_skills_empty_title(),
            Style::default().add_modifier(Modifier::BOLD),
        )),
        Line::raw(""),
        Line::from(Span::styled(
            texts::tui_skills_empty_subtitle(),
            Style::default().fg(theme.dim),
        )),
    ];

    frame.render_widget(
        Paragraph::new(empty_lines)
            .alignment(Alignment::Center)
            .wrap(Wrap { trim: false }),
        empty_chunks[1],
    );
}

fn skill_marker(enabled: bool) -> &'static str {
    if enabled {
        texts::tui_marker_active()
    } else {
        texts::tui_marker_inactive()
    }
}

fn centered_cell(text: impl Into<String>) -> Cell<'static> {
    Cell::from(Line::from(text.into()).alignment(Alignment::Center))
}