vkit 0.1.2

Fast Rust dev CLI: manage Node ports, run scripts, install & sync VS Code / Cursor extensions.
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};

use crate::theme;

/// 列表光标行高亮符号(所有选择列表统一用 `❯ `)。
pub const LIST_CURSOR: &str = "";

/// 列表光标行高亮样式:橙色(highlight)前景加粗,作用于整行。
///
/// 各工具的选择列表统一用它作为 `List::highlight_style`,保证光标行观感一致。
pub fn list_cursor_style() -> Style {
    Style::default()
        .fg(theme::highlight())
        .add_modifier(Modifier::BOLD)
}

/// 复选框标记:勾选 `[x]`(selected 黄)、未勾选 `[ ]`(muted)。
pub fn checkbox(checked: bool) -> Span<'static> {
    let (mark, color) = if checked {
        ("[x] ", theme::selected())
    } else {
        ("[ ] ", theme::muted())
    };
    Span::styled(mark.to_string(), Style::default().fg(color))
}

/// 列表项标题样式:勾选 / 选中用 selected 黄,未选中弱化为 muted。
pub fn item_title_style(checked: bool) -> Style {
    if checked {
        Style::default().fg(theme::selected())
    } else {
        Style::default().fg(theme::muted())
    }
}

/// 列表项描述 / 次要信息样式:muted 且更弱(DIM)。
pub fn item_desc_style() -> Style {
    Style::default()
        .fg(theme::muted())
        .add_modifier(Modifier::DIM)
}

/// 状态行图标:成功 `✓`(success 绿)、失败 `✗`(warning)。
pub fn status_icon(ok: bool) -> Span<'static> {
    let (icon, color) = if ok {
        ("", theme::success())
    } else {
        ("", theme::warning())
    };
    Span::styled(icon.to_string(), Style::default().fg(color))
}

/// 列表光标环形移动:在第一个按上跳到最后一个,在最后一个按下回到第一个。
///
/// 返回 `None` 表示列表为空。`delta` 通常是 `-1`(上)或 `1`(下)。
pub fn wrapping_index(current: usize, delta: isize, len: usize) -> Option<usize> {
    if len == 0 {
        return None;
    }
    let len_i = len as isize;
    Some((current as isize + delta).rem_euclid(len_i) as usize)
}

/// 公共输入框组件:所有工具(port / run / vsix)共用同一套输入 / 搜索样式。
///
/// - 空值时显示 `placeholder`(弱化色);聚焦时光标 `▏` 在最前,形成「幽灵提示」效果。
/// - 有值时显示正文色,聚焦时光标 `▏` 在末尾。
/// - 聚焦时边框为 accent 高亮,否则弱化。标题统一为 accent 加粗。
pub fn input_paragraph(
    value: &str,
    placeholder: &str,
    active: bool,
    title: &str,
) -> Paragraph<'static> {
    let cursor = Span::styled("".to_string(), Style::default().fg(theme::text()));
    let mut spans: Vec<Span<'static>> = Vec::new();

    if value.is_empty() {
        if active {
            spans.push(cursor);
        }
        spans.push(Span::styled(
            placeholder.to_string(),
            Style::default().fg(theme::muted()),
        ));
    } else {
        spans.push(Span::styled(
            value.to_string(),
            Style::default().fg(theme::text()),
        ));
        if active {
            spans.push(cursor);
        }
    }

    let border = if active {
        theme::accent()
    } else {
        theme::muted()
    };
    let block = Block::default()
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(border))
        .title(Span::styled(
            title.to_string(),
            Style::default()
                .fg(theme::accent())
                .add_modifier(Modifier::BOLD),
        ));

    Paragraph::new(Line::from(spans)).block(block)
}

/// 公共按键提示组件:keycap 键帽风格 `▏ key ▏ label   …`。
///
/// 键名带深色底块(像键盘按键),后跟弱化的动作标签,条目间留空隙。
/// dashboard 与各工具共用,保证键位提示风格一致。
pub fn key_hint_line(items: &[(&str, &str)]) -> Line<'static> {
    let key_style = Style::default()
        .fg(theme::text())
        .bg(theme::surface())
        .add_modifier(Modifier::BOLD);
    let label_style = Style::default().fg(theme::muted());

    let mut spans: Vec<Span<'static>> = Vec::with_capacity(items.len() * 3);
    for (index, (key, label)) in items.iter().enumerate() {
        if index > 0 {
            spans.push(Span::raw("   "));
        }
        spans.push(Span::styled(format!(" {key} "), key_style));
        spans.push(Span::styled(format!(" {label}"), label_style));
    }

    Line::from(spans)
}