openapi-tui 0.10.2

This TUI allows you to list and browse APIs described by the openapi specification.
use color_eyre::eyre::Result;
use ratatui::{
  prelude::*,
  widgets::{block::*, *},
};

use crate::{
  action::Action,
  panes::Pane,
  state::{OperationItemType, State},
  tui::Frame,
};

pub struct ApisPane {
  focused: bool,
  focused_border_style: Style,
  current_operation_index: usize,
}

impl ApisPane {
  pub fn new(focused: bool, focused_border_style: Style) -> Self {
    Self { focused, focused_border_style, current_operation_index: 0 }
  }

  fn border_style(&self) -> Style {
    match self.focused {
      true => self.focused_border_style,
      false => Style::default(),
    }
  }

  fn border_type(&self) -> BorderType {
    match self.focused {
      true => BorderType::Thick,
      false => BorderType::Plain,
    }
  }

  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 ApisPane {
  fn height_constraint(&self) -> Constraint {
    match self.focused {
      true => Constraint::Fill(3),
      false => Constraint::Fill(3),
    }
  }

  fn update(&mut self, action: Action, state: &mut State) -> Result<Option<Action>> {
    match action {
      Action::Down => {
        let operations_len = state.operations_len();
        if operations_len > 0 {
          self.current_operation_index = self.current_operation_index.saturating_add(1) % operations_len;
        }
        state.active_operation_index = self.current_operation_index;
        return Ok(Some(Action::Update));
      },
      Action::Up => {
        let operations_len = state.operations_len();
        if operations_len > 0 {
          self.current_operation_index =
            self.current_operation_index.saturating_add(operations_len.saturating_sub(1)) % operations_len;
        }
        state.active_operation_index = self.current_operation_index;
        return Ok(Some(Action::Update));
      },
      Action::Focus => {
        self.focused = true;
        static STATUS_LINE: &str = "[j,k → movement] [ENTER → request]";
        return Ok(Some(Action::TimedStatusLine(STATUS_LINE.into(), 3)));
      },
      Action::UnFocus => {
        self.focused = false;
      },
      Action::Submit => {},
      Action::Update => {
        self.current_operation_index = state.active_operation_index;
      },
      _ => {},
    }

    Ok(None)
  }

  fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<()> {
    let items = state.openapi_operations.iter().filter_map(|operation_item| {
      if let Some(active_tag) = &state.active_tag_name {
        if !operation_item.has_tag(active_tag) {
          return None;
        }
      }
      if !operation_item.path.contains(state.active_filter.as_str()) {
        return None;
      }
      Some(Line::from(vec![
        Span::styled(
          format!(
            " {:7}",
            match operation_item.r#type {
              OperationItemType::Path => operation_item.method.as_str(),
              OperationItemType::Webhook => "EVENT",
            }
          ),
          match operation_item.r#type {
            OperationItemType::Path => Self::method_color(operation_item.method.as_str()),
            OperationItemType::Webhook => Color::LightMagenta,
          },
        ),
        Span::styled(format!(" {:7}", operation_item.path), Color::White),
      ]))
    });

    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(Some(self.current_operation_index));

    frame.render_stateful_widget(list, area, &mut list_state);
    let active_tag = format!("[{}]", state.active_tag_name.clone().unwrap_or(String::from("ALL")));
    frame.render_widget(
      Block::default()
        .title("APIs")
        .borders(Borders::ALL)
        .border_style(self.border_style())
        .border_type(self.border_type())
        .title_bottom(
          Line::from(format!(
            "{} of {}",
            self.current_operation_index.saturating_add(1).min(state.operations_len()),
            state.operations_len()
          ))
          .right_aligned(),
        )
        .title(Line::styled(active_tag, Style::default().add_modifier(Modifier::ITALIC)).right_aligned()),
      area,
    );
    Ok(())
  }
}