zitadel-tui 0.1.8

A terminal UI for managing Zitadel resources
use std::io;

use anyhow::Result;
use crossterm::{
    event::{self, Event, KeyCode, KeyEventKind},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::backend::CrosstermBackend;

use crate::{
    conductor::TuiConductor,
    tui::{draw, App, AppCommand},
};

pub async fn run_tui(mut conductor: TuiConductor) -> Result<()> {
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen)?;
    let terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout))?;
    let result = run_app(terminal, &mut conductor).await;
    disable_raw_mode()?;
    execute!(io::stdout(), LeaveAlternateScreen)?;
    result
}

async fn run_app(
    mut terminal: ratatui::Terminal<CrosstermBackend<io::Stdout>>,
    conductor: &mut TuiConductor,
) -> Result<()> {
    let mut app = App::from_bootstrap(conductor.bootstrap_state());

    loop {
        terminal.draw(|frame| draw(frame, &app))?;

        let Some(key_code) = read_key_press(std::time::Duration::from_millis(100))? else {
            continue;
        };

        let command = app.handle_key(key_code);
        if !handle_app_command(command, &mut app, conductor).await? {
            break;
        }
    }

    Ok(())
}

fn read_key_press(timeout: std::time::Duration) -> Result<Option<KeyCode>> {
    if !event::poll(timeout)? {
        return Ok(None);
    }

    let Event::Key(key) = event::read()? else {
        return Ok(None);
    };

    if key.kind != KeyEventKind::Press {
        return Ok(None);
    }

    Ok(Some(key.code))
}

async fn handle_app_command(
    command: AppCommand,
    app: &mut App,
    conductor: &mut TuiConductor,
) -> Result<bool> {
    match command {
        AppCommand::Noop => Ok(true),
        AppCommand::Quit => Ok(false),
        AppCommand::BeginAction {
            resource,
            action_index,
            selected_record,
        } => {
            let mode = conductor.begin_action(resource, action_index, selected_record.as_ref());
            app.set_canvas_mode(mode);
            Ok(true)
        }
        AppCommand::SubmitForm(form) => {
            let next = conductor.submit_form(&form).await;
            app.set_canvas_mode(next);
            app.sync_runtime(conductor.bootstrap_state());
            Ok(true)
        }
        AppCommand::Confirm(pending) => {
            let next = conductor.confirm(pending).await;
            app.set_canvas_mode(next);
            app.sync_runtime(conductor.bootstrap_state());
            Ok(true)
        }
    }
}