virtuoso-cli 0.1.3

CLI tool to control Cadence Virtuoso from anywhere, locally or remotely
Documentation
mod input;
mod render;
mod state;
mod theme;

use crate::command_log;
use crate::error::Result;
use crossterm::event::{self, Event, KeyEventKind};
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use crossterm::ExecutableCommand;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io::stdout;
use std::time::Duration;

fn err(e: impl std::fmt::Display) -> crate::error::VirtuosoError {
    crate::error::VirtuosoError::Execution(e.to_string())
}

fn load_log(state: &mut state::TuiState) {
    if let Ok(content) = std::fs::read_to_string(command_log::log_path()) {
        state.log_lines = content.lines().map(|l| l.to_string()).collect();
        state.log_scroll = state.log_lines.len().saturating_sub(1);
    }
}

pub fn run_tui() -> Result<()> {
    let mut state = state::TuiState::new();
    let theme = theme::Theme::default();
    load_log(&mut state);

    enable_raw_mode().map_err(err)?;
    stdout().execute(EnterAlternateScreen).map_err(err)?;
    let backend = CrosstermBackend::new(stdout());
    let mut terminal = Terminal::new(backend).map_err(err)?;

    let result = run_loop(&mut terminal, &mut state, &theme);

    let _ = disable_raw_mode();
    let _ = stdout().execute(LeaveAlternateScreen);

    result
}

fn run_loop(
    terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
    state: &mut state::TuiState,
    theme: &theme::Theme,
) -> Result<()> {
    loop {
        terminal
            .draw(|frame| render::render(frame, state, theme))
            .map_err(err)?;

        if !event::poll(Duration::from_millis(500)).map_err(err)? {
            state.spinner_frame = state.spinner_frame.wrapping_add(1);
            if let Some((_, at)) = &state.status_msg {
                if at.elapsed().as_secs() >= 3 {
                    state.status_msg = None;
                }
            }
            continue;
        }

        let ev = event::read().map_err(err)?;

        if let Event::Key(key) = ev {
            if key.kind != KeyEventKind::Press {
                continue;
            }
            match input::handle_key(state, key) {
                input::EventAction::Quit => break,
                input::EventAction::Refresh => {
                    state.refresh();
                    load_log(state);
                    state.set_status("Refreshed");
                }
                input::EventAction::ShowLog => {
                    state.show_log = true;
                }
                input::EventAction::CancelJob => {
                    let idx = state.selected_job;
                    if let Some(job) = state.jobs.get_mut(idx) {
                        if job.status == crate::spectre::jobs::JobStatus::Running {
                            let _ = job.cancel();
                        }
                    }
                    if let Some(job) = state.jobs.get(idx) {
                        state.set_status(&format!("Cancelled job {}", job.id));
                    }
                }
                input::EventAction::SaveConfig => match state.save_config() {
                    Ok(_) => state.set_status("Config saved to .env"),
                    Err(e) => state.set_status(&format!("Save failed: {e}")),
                },
                input::EventAction::Continue => {}
            }
        }
    }

    Ok(())
}