cc-switch-tui 0.2.1

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

pub(crate) fn focus_block_style(active: bool, theme: &super::theme::Theme) -> Style {
    if active {
        Style::default()
            .fg(theme.accent)
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default().fg(theme.dim)
    }
}

pub(crate) fn add_form_key_items(
    focus: FormFocus,
    editing: bool,
    selected_field: Option<ProviderAddField>,
) -> Vec<(&'static str, &'static str)> {
    let save_label: &'static str = Box::leak(crate::settings::get_save_shortcut().into_boxed_str());
    let mut keys = vec![
        ("Tab", texts::tui_key_focus()),
        (save_label, texts::tui_key_save()),
        ("Esc", texts::tui_key_close()),
    ];

    match focus {
        FormFocus::Templates => keys.extend([
            ("←→", texts::tui_key_select()),
            ("Enter", texts::tui_key_apply()),
        ]),
        FormFocus::Fields => {
            if editing {
                keys.extend([
                    ("←→", texts::tui_key_move()),
                    ("Enter", texts::tui_key_exit_edit()),
                ]);
            } else {
                let enter_action = match selected_field {
                    Some(ProviderAddField::CodexModel | ProviderAddField::GeminiModel) => {
                        texts::tui_key_fetch_model()
                    }
                    Some(
                        ProviderAddField::ClaudeModelConfig
                        | ProviderAddField::CommonSnippet
                        | ProviderAddField::OpenClawModels,
                    ) => texts::tui_key_open(),
                    Some(
                        ProviderAddField::GeminiAuthType
                        | ProviderAddField::ClaudeHideAttribution
                        | ProviderAddField::OpenClawApiProtocol
                        | ProviderAddField::OpenClawUserAgent,
                    ) => texts::tui_key_toggle(),
                    _ => texts::tui_key_edit_mode(),
                };
                keys.extend([
                    ("↑↓", texts::tui_key_select()),
                    ("Enter", enter_action),
                    ("Space", texts::tui_key_toggle()),
                ]);
            }
        }
        FormFocus::JsonPreview => {
            keys.extend([
                ("Enter", texts::tui_key_edit_mode()),
                ("↑↓", texts::tui_key_scroll()),
            ]);
        }
    }

    keys
}

pub(crate) fn render_form_template_chips(
    frame: &mut Frame<'_>,
    labels: &[&str],
    selected_idx: usize,
    active: bool,
    area: Rect,
    theme: &super::theme::Theme,
) {
    let template_block = Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Plain)
        .border_style(focus_block_style(active, theme))
        .title(texts::tui_form_templates_title());
    frame.render_widget(template_block.clone(), area);
    let template_inner = template_block.inner(area);

    let mut spans: Vec<Span<'static>> = Vec::new();
    for (idx, label) in labels.iter().enumerate() {
        let selected = idx == selected_idx;
        let style = if selected {
            active_chip_style(theme)
        } else {
            inactive_chip_style(theme)
        };
        spans.push(Span::styled(format!(" {label} "), style));
        spans.push(Span::raw(" "));
    }

    frame.render_widget(
        Paragraph::new(Line::from(spans)).wrap(Wrap { trim: false }),
        template_inner,
    );
}

pub(crate) fn visible_text_window(text: &str, cursor: usize, width: usize) -> (String, u16) {
    if width == 0 {
        return (String::new(), 0);
    }

    let chars = text.chars().collect::<Vec<_>>();
    let cursor = cursor.min(chars.len());

    let mut cum: Vec<usize> = Vec::with_capacity(chars.len() + 1);
    cum.push(0);
    for c in &chars {
        let w = UnicodeWidthChar::width(*c).unwrap_or(0);
        cum.push(cum.last().unwrap_or(&0).saturating_add(w));
    }

    let cursor_x = cum.get(cursor).copied().unwrap_or(0);
    let target = cursor_x.saturating_sub(width.saturating_sub(1));
    let mut start_idx = 0usize;
    while start_idx < cum.len() && cum[start_idx] < target {
        start_idx += 1;
    }

    let mut end_idx = start_idx;
    while end_idx < chars.len() && cum[end_idx + 1].saturating_sub(cum[start_idx]) <= width {
        end_idx += 1;
    }

    let visible = chars
        .get(start_idx..end_idx)
        .unwrap_or_default()
        .iter()
        .collect::<String>();
    let cursor_in_window = cursor_x.saturating_sub(*cum.get(start_idx).unwrap_or(&0));

    (visible, cursor_in_window.min(width) as u16)
}

pub(crate) fn render_form_json_preview(
    frame: &mut Frame<'_>,
    json_text: &str,
    scroll: usize,
    active: bool,
    area: Rect,
    theme: &super::theme::Theme,
) {
    let json_block = Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Plain)
        .border_style(focus_block_style(active, theme))
        .title(texts::tui_form_json_title());
    frame.render_widget(json_block.clone(), area);
    let json_inner = json_block.inner(area);

    let lines = json_text
        .lines()
        .map(|s| Line::raw(s.to_string()))
        .collect::<Vec<_>>();

    let height = json_inner.height as usize;
    if height == 0 {
        return;
    }
    let max_start = lines.len().saturating_sub(height);
    let start = scroll.min(max_start);
    let end = (start + height).min(lines.len());

    frame.render_widget(
        Paragraph::new(lines[start..end].to_vec()).wrap(Wrap { trim: false }),
        json_inner,
    );
}

pub(crate) fn render_form_text_preview(
    frame: &mut Frame<'_>,
    title: &str,
    text: &str,
    scroll: usize,
    active: bool,
    area: Rect,
    theme: &super::theme::Theme,
) {
    let block = Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Plain)
        .border_style(focus_block_style(active, theme))
        .title(title);
    frame.render_widget(block.clone(), area);
    let inner = block.inner(area);

    let lines = text
        .lines()
        .map(|s| Line::raw(s.to_string()))
        .collect::<Vec<_>>();

    let height = inner.height as usize;
    if height == 0 {
        return;
    }
    let max_start = lines.len().saturating_sub(height);
    let start = scroll.min(max_start);
    let end = (start + height).min(lines.len());

    frame.render_widget(
        Paragraph::new(lines[start..end].to_vec()).wrap(Wrap { trim: false }),
        inner,
    );
}

pub(crate) fn render_add_form(
    frame: &mut Frame<'_>,
    app: &App,
    data: &UiData,
    form: &FormState,
    area: Rect,
    theme: &super::theme::Theme,
) {
    match form {
        FormState::ProviderAdd(provider) => {
            render_provider_add_form(frame, app, data, provider, area, theme)
        }
        FormState::McpAdd(mcp) => render_mcp_add_form(frame, app, mcp, area, theme),
    }
}