tazuna 0.1.0

TUI tool for managing multiple Claude Code sessions in parallel
Documentation
//! Widget preview example - Storybook-like component browser for tazuna TUI components.
//!
//! Run with: `cargo run --example widget_preview`
//!
//! Navigation:
//! - j/k: Select component
//! - h/l: Select variant
//! - q: Quit

use std::io::{self, stdout};
use std::path::PathBuf;

use crossterm::{
    event::{self, Event, KeyCode},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
    Terminal,
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, List, ListItem, ListState, Paragraph, StatefulWidget, Widget},
};

use tazuna::tui::components::{ButtonRow, SelectableList, TextInput, button_presets};
use tazuna::tui::popup::{IssueItem, SessionItem, WorktreeItem};
use tazuna::tui::status_bar::StatusBar;
use tazuna::tui::tabs::StatusIndicator;
use tazuna::worktree::GitWorktreeStatus;

/// Render function type for component variants.
type RenderFn = Box<dyn Fn(Rect, &mut ratatui::buffer::Buffer)>;

/// Component catalog entry
struct ComponentEntry {
    name: &'static str,
    variants: Vec<(&'static str, RenderFn)>,
}

fn main() -> io::Result<()> {
    enable_raw_mode()?;
    let mut stdout = stdout();
    execute!(stdout, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let result = run_app(&mut terminal);

    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;

    result
}

fn run_app(terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>) -> io::Result<()> {
    let catalog = build_catalog();
    let mut component_idx = 0;
    let mut variant_idx = 0;
    let mut list_state = ListState::default();
    list_state.select(Some(0));

    loop {
        terminal.draw(|f| {
            let area = f.area();
            let chunks = Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Length(30), Constraint::Min(1)])
                .split(area);

            // Component list (left panel)
            render_component_list(f, chunks[0], &catalog, component_idx, &mut list_state);

            // Preview area (right panel)
            let preview_area = chunks[1];
            let preview_block = Block::default()
                .title(format!(
                    " {} - {} ",
                    catalog[component_idx].name, catalog[component_idx].variants[variant_idx].0
                ))
                .borders(Borders::ALL)
                .border_style(Style::default().fg(Color::Cyan));
            let inner = preview_block.inner(preview_area);
            f.render_widget(preview_block, preview_area);

            // Render the selected component variant
            let buf = f.buffer_mut();
            (catalog[component_idx].variants[variant_idx].1)(inner, buf);
        })?;

        if event::poll(std::time::Duration::from_millis(100))?
            && let Event::Key(key) = event::read()?
        {
            match key.code {
                KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
                KeyCode::Char('j') | KeyCode::Down => {
                    component_idx = (component_idx + 1) % catalog.len();
                    variant_idx = 0;
                    list_state.select(Some(component_idx));
                }
                KeyCode::Char('k') | KeyCode::Up => {
                    component_idx = if component_idx == 0 {
                        catalog.len() - 1
                    } else {
                        component_idx - 1
                    };
                    variant_idx = 0;
                    list_state.select(Some(component_idx));
                }
                KeyCode::Char('l') | KeyCode::Right => {
                    variant_idx = (variant_idx + 1) % catalog[component_idx].variants.len();
                }
                KeyCode::Char('h') | KeyCode::Left => {
                    let len = catalog[component_idx].variants.len();
                    variant_idx = if variant_idx == 0 {
                        len - 1
                    } else {
                        variant_idx - 1
                    };
                }
                _ => {}
            }
        }
    }
}

fn render_component_list(
    f: &mut ratatui::Frame,
    area: Rect,
    catalog: &[ComponentEntry],
    selected: usize,
    state: &mut ListState,
) {
    let items: Vec<ListItem> = catalog
        .iter()
        .enumerate()
        .map(|(i, entry)| {
            let style = if i == selected {
                Style::default()
                    .fg(Color::Cyan)
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default()
            };
            ListItem::new(Line::from(Span::styled(entry.name, style)))
        })
        .collect();

    let list = List::new(items)
        .block(
            Block::default()
                .title(" Components (j/k) ")
                .borders(Borders::ALL),
        )
        .highlight_style(Style::default().bg(Color::DarkGray))
        .highlight_symbol("> ");

    f.render_stateful_widget(list, area, state);

    // Help text at bottom
    let help =
        Paragraph::new("h/l: variants | q: quit").style(Style::default().fg(Color::DarkGray));
    let help_area = Rect::new(area.x + 1, area.y + area.height - 2, area.width - 2, 1);
    f.render_widget(help, help_area);
}

#[allow(clippy::too_many_lines)]
fn build_catalog() -> Vec<ComponentEntry> {
    vec![
        // TextInput variants
        ComponentEntry {
            name: "TextInput",
            variants: vec![
                (
                    "focused",
                    Box::new(|area, buf| {
                        let widget =
                            TextInput::new("feature/auth", 12, "Branch Name").focused(true);
                        widget.render(area, buf);
                    }),
                ),
                (
                    "unfocused",
                    Box::new(|area, buf| {
                        let widget =
                            TextInput::new("feature/auth", 12, "Branch Name").focused(false);
                        widget.render(area, buf);
                    }),
                ),
                (
                    "empty",
                    Box::new(|area, buf| {
                        let widget = TextInput::new("", 0, "Enter text...").focused(true);
                        widget.render(area, buf);
                    }),
                ),
            ],
        },
        // SelectableList variants
        ComponentEntry {
            name: "SelectableList",
            variants: vec![
                (
                    "sessions focused",
                    Box::new(|area, buf| {
                        let items = vec![
                            SessionItem::new("session-1", StatusIndicator::Running)
                                .branch(Some("feature/auth".to_string())),
                            SessionItem::new("session-2", StatusIndicator::Terminated),
                            SessionItem::new("session-3", StatusIndicator::Running).pending(true),
                        ];
                        let list =
                            SelectableList::new(&items, "Sessions (Enter: switch)").focused(true);
                        let mut state = ListState::default();
                        state.select(Some(0));
                        list.render(area, buf, &mut state);
                    }),
                ),
                (
                    "sessions unfocused",
                    Box::new(|area, buf| {
                        let items = vec![
                            SessionItem::new("session-1", StatusIndicator::Running),
                            SessionItem::new("session-2", StatusIndicator::Terminated),
                        ];
                        let list = SelectableList::new(&items, "Sessions").focused(false);
                        let mut state = ListState::default();
                        state.select(Some(0));
                        list.render(area, buf, &mut state);
                    }),
                ),
                (
                    "worktrees",
                    Box::new(|area, buf| {
                        let items = vec![
                            WorktreeItem::new(
                                "feature/auth",
                                PathBuf::from("/worktrees/abc12345"),
                                GitWorktreeStatus {
                                    dirty: true,
                                    ..Default::default()
                                },
                            ),
                            WorktreeItem::new(
                                "main",
                                PathBuf::from("/worktrees/def67890"),
                                GitWorktreeStatus {
                                    ahead: 2,
                                    behind: 1,
                                    ..Default::default()
                                },
                            ),
                        ];
                        let list = SelectableList::new(&items, "Worktrees").focused(true);
                        let mut state = ListState::default();
                        state.select(Some(0));
                        list.render(area, buf, &mut state);
                    }),
                ),
                (
                    "issues",
                    Box::new(|area, buf| {
                        let items = vec![
                            IssueItem::new(42, "Add authentication flow", "enhancement"),
                            IssueItem::new(17, "Fix crash on startup", "bug, critical"),
                            IssueItem::new(99, "Update documentation", ""),
                        ];
                        let list = SelectableList::new(&items, "Issues").focused(true);
                        let mut state = ListState::default();
                        state.select(Some(1));
                        list.render(area, buf, &mut state);
                    }),
                ),
                (
                    "empty",
                    Box::new(|area, buf| {
                        let items: Vec<SessionItem> = vec![];
                        let list = SelectableList::new(&items, "Empty List").focused(true);
                        let mut state = ListState::default();
                        list.render(area, buf, &mut state);
                    }),
                ),
            ],
        },
        // ButtonRow variants
        ComponentEntry {
            name: "ButtonRow",
            variants: vec![
                (
                    "yes selected",
                    Box::new(|area, buf| {
                        let buttons =
                            ButtonRow::new(&[button_presets::YES, button_presets::NO]).selected(0);
                        buttons.render(area, buf);
                    }),
                ),
                (
                    "no selected",
                    Box::new(|area, buf| {
                        let buttons =
                            ButtonRow::new(&[button_presets::YES, button_presets::NO]).selected(1);
                        buttons.render(area, buf);
                    }),
                ),
                (
                    "extended buttons",
                    Box::new(|area, buf| {
                        let buttons = ButtonRow::new(&[
                            button_presets::YES_UNDERSTAND,
                            button_presets::NO_CANCEL,
                        ])
                        .selected(0);
                        buttons.render(area, buf);
                    }),
                ),
            ],
        },
        // StatusBar variants
        ComponentEntry {
            name: "StatusBar",
            variants: vec![
                (
                    "minimal",
                    Box::new(|area, buf| {
                        let status = StatusBar::new(3);
                        status.render(area, buf);
                    }),
                ),
                (
                    "with session",
                    Box::new(|area, buf| {
                        let status = StatusBar::new(2).active_session("feature-auth");
                        status.render(area, buf);
                    }),
                ),
                (
                    "with branch",
                    Box::new(|area, buf| {
                        let status = StatusBar::new(2)
                            .active_session("my-session")
                            .active_branch("main");
                        status.render(area, buf);
                    }),
                ),
                (
                    "with notifications",
                    Box::new(|area, buf| {
                        let status = StatusBar::new(1).notifications(5);
                        status.render(area, buf);
                    }),
                ),
                (
                    "with cost",
                    Box::new(|area, buf| {
                        let status = StatusBar::new(1).today_cost(7.50);
                        status.render(area, buf);
                    }),
                ),
                (
                    "full",
                    Box::new(|area, buf| {
                        let status = StatusBar::new(5)
                            .active_session("dev-session")
                            .active_branch("feature/login")
                            .notifications(3)
                            .today_cost(12.34);
                        status.render(area, buf);
                    }),
                ),
            ],
        },
    ]
}