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 crossterm::event::{KeyCode, KeyEvent};
use ratatui::prelude::*;
use tokio::sync::mpsc::UnboundedSender;

use crate::{
  action::Action,
  config::Config,
  pages::Page,
  panes::{address::AddressPane, apis::ApisPane, request::RequestPane, response::ResponsePane, tags::TagsPane, Pane},
  state::{InputMode, State},
  tui::EventResponse,
};

#[derive(Default)]
pub struct Home {
  command_tx: Option<UnboundedSender<Action>>,
  config: Config,
  panes: Vec<Box<dyn Pane>>,
  focused_pane_index: usize,
  fullscreen_pane_index: Option<usize>,
}

impl Home {
  pub fn new() -> Result<Self> {
    let focused_border_style = Style::default().fg(Color::LightGreen);

    Ok(Self {
      command_tx: None,
      config: Config::default(),
      panes: vec![
        Box::new(ApisPane::new(true, focused_border_style)),
        Box::new(TagsPane::new(false, focused_border_style)),
        Box::new(AddressPane::new(false, focused_border_style)),
        Box::new(RequestPane::new(false, focused_border_style)),
        Box::new(ResponsePane::new(false, focused_border_style)),
      ],

      focused_pane_index: 0,
      fullscreen_pane_index: None,
    })
  }
}

impl Page for Home {
  fn init(&mut self, state: &State) -> Result<()> {
    for pane in self.panes.iter_mut() {
      pane.init(state)?;
    }
    Ok(())
  }

  fn focus(&mut self) -> Result<()> {
    if let Some(command_tx) = &self.command_tx {
      const ARROW: &str = symbols::scrollbar::HORIZONTAL.end;
      let status_line =
        format!("[l,h {ARROW} pane movement] [/ {ARROW} api filter] [: {ARROW} commands] [q {ARROW} quit]");
      command_tx.send(Action::StatusLine(status_line))?;
    }
    Ok(())
  }

  fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
    self.command_tx = Some(tx);
    Ok(())
  }

  fn register_config_handler(&mut self, config: Config) -> Result<()> {
    self.config = config;
    Ok(())
  }

  fn update(&mut self, action: Action, state: &mut State) -> Result<Option<Action>> {
    let mut actions: Vec<Option<Action>> = vec![];
    match action {
      Action::Tick => {},
      Action::FocusNext => {
        let next_index = self.focused_pane_index.saturating_add(1) % self.panes.len();
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          actions.push(pane.update(Action::UnFocus, state)?);
        }
        self.focused_pane_index = next_index;
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          actions.push(pane.update(Action::Focus, state)?);
        }
      },
      Action::FocusPrev => {
        let prev_index = self.focused_pane_index.saturating_add(self.panes.len() - 1) % self.panes.len();
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          actions.push(pane.update(Action::UnFocus, state)?);
        }
        self.focused_pane_index = prev_index;
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          actions.push(pane.update(Action::Focus, state)?);
        }
      },
      Action::Update => {
        for pane in self.panes.iter_mut() {
          actions.push(pane.update(action.clone(), state)?);
        }
      },
      Action::ToggleFullScreen => {
        self.fullscreen_pane_index = self.fullscreen_pane_index.map_or(Some(self.focused_pane_index), |_| None);
      },
      Action::FocusFooter(..) => {
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          actions.push(pane.update(Action::UnFocus, state)?);
        }
      },
      Action::FooterResult(cmd, Some(args)) if cmd.eq("/") => {
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          actions.push(pane.update(Action::Focus, state)?);
        }
        state.active_operation_index = 0;
        state.active_filter = args;

        actions.push(Some(Action::Update));
      },
      Action::FooterResult(cmd, Some(args)) if cmd.eq(":") => {
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          pane.update(Action::Focus, state)?;
        }
        if args.eq("q") {
          actions.push(Some(Action::Quit));
        } else if args.eq("request") || args.eq("r") {
          actions
            .push(Some(Action::NewCall(state.active_operation().and_then(|op| op.operation.operation_id.clone()))));
        } else if args.eq("history") {
          actions.push(Some(Action::History));
        } else {
          actions.push(Some(Action::TimedStatusLine("unknown command".into(), 1)));
        }
      },
      Action::FooterResult(_cmd, None) => {
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          actions.push(pane.update(Action::Focus, state)?);
        }
      },
      _ => {
        if let Some(pane) = self.panes.get_mut(self.focused_pane_index) {
          actions.push(pane.update(action, state)?);
        }
      },
    }

    if let Some(tx) = &mut self.command_tx {
      actions.into_iter().flatten().for_each(|action| {
        tx.send(action).ok();
      });
    }
    Ok(None)
  }

  fn handle_key_events(&mut self, key: KeyEvent, state: &mut State) -> Result<Option<EventResponse<Action>>> {
    match state.input_mode {
      InputMode::Normal => {
        let response = match key.code {
          KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => EventResponse::Stop(Action::FocusNext),
          KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => EventResponse::Stop(Action::FocusPrev),
          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::Char('g') | KeyCode::Char('G') => EventResponse::Stop(Action::Go),
          KeyCode::Backspace | KeyCode::Char('b') | KeyCode::Char('B') => EventResponse::Stop(Action::Back),
          KeyCode::Enter => EventResponse::Stop(Action::NewCall(
            state.active_operation().and_then(|op| op.operation.operation_id.clone()),
          )),
          KeyCode::Char('f') | KeyCode::Char('F') => EventResponse::Stop(Action::ToggleFullScreen),
          KeyCode::Char(c) if ('1'..='9').contains(&c) => {
            EventResponse::Stop(Action::Tab(c.to_digit(10).unwrap_or(0) - 1))
          },
          KeyCode::Char(']') => EventResponse::Stop(Action::TabNext),
          KeyCode::Char('[') => EventResponse::Stop(Action::TabPrev),
          KeyCode::Char('/') => EventResponse::Stop(Action::FocusFooter("/".into(), Some(state.active_filter.clone()))),
          KeyCode::Char(':') => EventResponse::Stop(Action::FocusFooter(":".into(), None)),
          _ => {
            return Ok(None);
          },
        };
        Ok(Some(response))
      },
      InputMode::Insert => Ok(None),
      InputMode::Command => Ok(None),
    }
  }

  fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<()> {
    if let Some(fullscreen_pane_index) = self.fullscreen_pane_index {
      self.panes[fullscreen_pane_index].draw(frame, area, state)?;
    } else {
      let outer_layout = Layout::default()
        .direction(Direction::Horizontal)
        .constraints(vec![Constraint::Fill(1), Constraint::Fill(3)])
        .split(area);

      let left_panes = Layout::default()
        .direction(Direction::Vertical)
        .constraints(vec![self.panes[0].height_constraint(), self.panes[1].height_constraint()])
        .split(outer_layout[0]);

      let right_panes = Layout::default()
        .direction(Direction::Vertical)
        .constraints(vec![
          self.panes[2].height_constraint(),
          self.panes[3].height_constraint(),
          self.panes[4].height_constraint(),
        ])
        .split(outer_layout[1]);

      self.panes[0].draw(frame, left_panes[0], state)?;
      self.panes[1].draw(frame, left_panes[1], state)?;
      self.panes[2].draw(frame, right_panes[0], state)?;
      self.panes[3].draw(frame, right_panes[1], state)?;
      self.panes[4].draw(frame, right_panes[2], state)?;
    }
    Ok(())
  }
}