kobectl 0.32.0-rc.1

kobe — CLI for the kobe cluster-pool operator: lease, inspect and manage instant CI/dev Kubernetes clusters
use anyhow::Result;
use crossterm::ExecutableCommand;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use crossterm::terminal::{
    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Layout};
use ratatui::prelude::*;
use ratatui::style::{Modifier, Style};
use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph};
use std::io::stdout;

pub(crate) struct PickerItem {
    pub primary: String,
    pub secondary: String,
}

pub(crate) fn run_picker(title: &str, help: &str, items: &[PickerItem]) -> Result<usize> {
    let _guard = TerminalGuard::enter()?;
    let backend = CrosstermBackend::new(stdout());
    let mut terminal = Terminal::new(backend)?;
    let mut state = ListState::default().with_selected(Some(0));

    loop {
        terminal.draw(|frame| {
            let areas =
                Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).split(frame.area());

            frame.render_widget(Paragraph::new(help.to_string()), areas[0]);

            let rendered_items: Vec<ListItem> = items
                .iter()
                .map(|item| {
                    ListItem::new(vec![
                        Line::from(item.primary.clone()),
                        Line::from(item.secondary.clone()),
                    ])
                })
                .collect();

            let list = List::new(rendered_items)
                .block(Block::default().borders(Borders::ALL).title(title))
                .highlight_style(Style::default().add_modifier(Modifier::REVERSED))
                .highlight_symbol("");

            frame.render_stateful_widget(list, areas[1], &mut state);
        })?;

        if let Event::Key(key) = event::read()? {
            if key.kind != KeyEventKind::Press {
                continue;
            }

            let selected = state.selected().unwrap_or(0);
            match key.code {
                KeyCode::Up => {
                    state.select(Some(selected.saturating_sub(1)));
                }
                KeyCode::Down => {
                    state.select(Some((selected + 1).min(items.len().saturating_sub(1))));
                }
                KeyCode::Enter => return Ok(selected),
                KeyCode::Esc | KeyCode::Char('q') => anyhow::bail!("Selection cancelled"),
                _ => {}
            }
        }
    }
}

struct TerminalGuard;

impl TerminalGuard {
    fn enter() -> Result<Self> {
        enable_raw_mode()?;
        stdout().execute(EnterAlternateScreen)?;
        Ok(Self)
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = disable_raw_mode();
        let _ = stdout().execute(LeaveAlternateScreen);
    }
}