openapi-tui 0.10.2

This TUI allows you to list and browse APIs described by the openapi specification.
use std::ops::Not;

use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
use ratatui::{
  prelude::*,
  widgets::{block::*, *},
};

use crate::{
  action::Action,
  panes::Pane,
  state::{InputMode, OperationItem, State},
  tui::{EventResponse, Frame},
};

#[derive(Default)]
struct OperationHistoryItem {
  operation_id: String,
  method: String,
  path: String,
}

#[derive(Default)]
pub struct HistoryPane {
  history: Vec<OperationHistoryItem>,
  history_item_index: Option<usize>,
}

impl HistoryPane {
  pub fn new(operation_ids: Vec<&OperationItem>) -> Self {
    let history = operation_ids
      .iter()
      .filter_map(|opertation_item| {
        opertation_item.operation.operation_id.as_ref().map(|operation_id| OperationHistoryItem {
          operation_id: operation_id.clone(),
          method: opertation_item.method.clone(),
          path: opertation_item.path.clone(),
        })
      })
      .collect::<Vec<OperationHistoryItem>>();
    let history_item_index = history.is_empty().not().then_some(0);
    Self { history, history_item_index }
  }

  fn method_color(method: &str) -> Color {
    match method {
      "GET" => Color::LightCyan,
      "POST" => Color::LightBlue,
      "PUT" => Color::LightYellow,
      "DELETE" => Color::LightRed,
      _ => Color::Gray,
    }
  }
}

impl Pane for HistoryPane {
  fn height_constraint(&self) -> Constraint {
    Constraint::Fill(3)
  }

  fn handle_key_events(
    &mut self,
    key: crossterm::event::KeyEvent,
    state: &mut State,
  ) -> Result<Option<crate::tui::EventResponse<crate::action::Action>>> {
    match state.input_mode {
      InputMode::Normal => {
        let response = match key.code {
          KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => EventResponse::Stop(Action::Down),
          KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => EventResponse::Stop(Action::Up),
          KeyCode::Esc => EventResponse::Stop(Action::CloseHistory),
          KeyCode::Enter => {
            if let Some(item_index) = self.history_item_index {
              EventResponse::Stop(Action::NewCall(self.history.get(item_index).map(|item| item.operation_id.clone())))
            } else {
              return Ok(Some(EventResponse::Stop(Action::Noop)));
            }
          },
          _ => {
            return Ok(Some(EventResponse::Stop(Action::Noop)));
          },
        };
        Ok(Some(response))
      },
      InputMode::Insert => Ok(Some(EventResponse::Stop(Action::Noop))),
      InputMode::Command => Ok(Some(EventResponse::Stop(Action::Noop))),
    }
  }

  fn update(&mut self, action: Action, _state: &mut State) -> Result<Option<Action>> {
    match action {
      Action::Down => {
        let history_len = self.history.len();
        if history_len > 0 {
          self.history_item_index = self.history_item_index.map(|item_idx| item_idx.saturating_add(1) % history_len);
        } else {
          self.history_item_index = None;
        }
        return Ok(Some(Action::Update));
      },
      Action::Up => {
        let history_len = self.history.len();
        if history_len > 0 {
          self.history_item_index = self
            .history_item_index
            .map(|item_idx| item_idx.saturating_add(history_len.saturating_sub(1)) % history_len);
        } else {
          self.history_item_index = None;
        }
        return Ok(Some(Action::Update));
      },
      _ => {},
    }
    Ok(None)
  }

  fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, _state: &State) -> Result<()> {
    frame.render_widget(Clear, area);
    let items = self.history.iter().map(|item| {
      Line::from(vec![
        Span::styled(format!(" {:7}", item.method), Self::method_color(item.method.as_str())),
        Span::from(item.path.clone()),
      ])
    });
    let list = List::new(items)
      .block(Block::default().borders(Borders::ALL))
      .highlight_symbol(symbols::scrollbar::HORIZONTAL.end)
      .highlight_spacing(HighlightSpacing::Always)
      .highlight_style(Style::default().add_modifier(Modifier::BOLD));
    let mut list_state = ListState::default().with_selected(self.history_item_index);

    frame.render_stateful_widget(list, area, &mut list_state);
    frame.render_widget(Block::default().borders(Borders::ALL).title("Request History").style(Style::default()), area);
    Ok(())
  }
}