Skip to content

Commit

Permalink
feat: implementing tabbing into preview response
Browse files Browse the repository at this point in the history
  • Loading branch information
wllfaria committed Apr 23, 2024
1 parent 78e6b02 commit ab7d353
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 39 deletions.
53 changes: 44 additions & 9 deletions tui/src/components/api_explorer/api_explorer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ use ratatui::{
widgets::{Block, Clear, StatefulWidget},
Frame,
};
use std::collections::HashMap;
use std::{collections::HashMap, ops::Add};
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};

use super::{
req_uri::ReqUriState,
res_viewer::{ResViewer, ResViewerState},
res_viewer::{ResViewer, ResViewerState, ResViewerTabs},
};

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -54,6 +54,9 @@ pub struct ApiExplorer<'a> {
schema: Schema,

focus: PaneFocus,
selected_pane: Option<PaneFocus>,
preview_tab: ResViewerTabs,
raw_preview_scroll: usize,

selected_request: Option<Request>,
hovered_request: Option<RequestKind>,
Expand Down Expand Up @@ -96,6 +99,9 @@ impl<'a> ApiExplorer<'a> {
focus: PaneFocus::ReqUri,
layout,
colors,
selected_pane: None,
preview_tab: ResViewerTabs::Preview,
raw_preview_scroll: 0,

response_rx,
request_tx,
Expand Down Expand Up @@ -188,8 +194,16 @@ impl<'a> ApiExplorer<'a> {
}

fn draw_response_viewer(&mut self, frame: &mut Frame) {
let mut state =
ResViewerState::new(self.focus == PaneFocus::Preview, self.response.clone());
let mut state = ResViewerState::new(
self.focus == PaneFocus::Preview,
self.selected_pane
.as_ref()
.map(|sel| sel.eq(&PaneFocus::Preview))
.unwrap_or(false),
self.response.clone(),
self.preview_tab.clone(),
&mut self.raw_preview_scroll,
);

frame.render_stateful_widget(
ResViewer::new(self.colors),
Expand All @@ -203,6 +217,21 @@ impl<'a> ApiExplorer<'a> {
self.response = Some(res);
}
}

fn handle_preview_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result<Option<Command>> {
match key_event.code {
KeyCode::Enter => self.selected_pane = Some(PaneFocus::Preview),
KeyCode::Tab => self.preview_tab = ResViewerTabs::next(&self.preview_tab),
KeyCode::Esc => self.selected_pane = None,
KeyCode::Char('k') => {
self.raw_preview_scroll = self.raw_preview_scroll.saturating_sub(1)
}
KeyCode::Char('j') => self.raw_preview_scroll = self.raw_preview_scroll.add(1),
_ => {}
}

Ok(None)
}
}

impl Component for ApiExplorer<'_> {
Expand All @@ -225,17 +254,23 @@ impl Component for ApiExplorer<'_> {

fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result<Option<Command>> {
if let KeyCode::Tab = key_event.code {
match self.focus {
PaneFocus::Sidebar => self.focus = PaneFocus::ReqUri,
PaneFocus::ReqUri => self.focus = PaneFocus::Preview,
PaneFocus::Preview => self.focus = PaneFocus::Sidebar,
match (&self.focus, &self.selected_pane) {
(PaneFocus::Sidebar, None) => self.focus = PaneFocus::ReqUri,
(PaneFocus::ReqUri, None) => self.focus = PaneFocus::Preview,
(PaneFocus::Preview, None) => self.focus = PaneFocus::Sidebar,

(PaneFocus::Preview, Some(_)) => {
self.handle_preview_key_event(key_event)?;
}
_ => {}
}
return Ok(None);
};

match self.focus {
PaneFocus::Sidebar => self.handle_sidebar_key_event(key_event),
PaneFocus::ReqUri => self.handle_req_uri_key_event(key_event),
PaneFocus::Preview => Ok(None),
PaneFocus::Preview => self.handle_preview_key_event(key_event),
}
}
}
Expand Down
172 changes: 142 additions & 30 deletions tui/src/components/api_explorer/res_viewer.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,71 @@
use std::ops::{Add, Sub};
use reqtui::net::request_manager::ReqtuiResponse;

use ratatui::{
buffer::Buffer,
layout::Rect,
layout::{Constraint, Direction, Layout, Rect},
style::Style,
widgets::{Block, Borders, Paragraph, StatefulWidget, Widget, Wrap},
text::Line,
widgets::{Block, Borders, Paragraph, StatefulWidget, Tabs, Widget, Wrap},
};
use reqtui::net::request_manager::ReqtuiResponse;
use std::ops::{Add, Sub};

pub struct ResViewerState {
pub struct ResViewerState<'a> {
is_focused: bool,
is_selected: bool,
response: Option<ReqtuiResponse>,
curr_tab: ResViewerTabs,
raw_scroll: &'a mut usize,
}

#[derive(Debug, Clone)]
pub enum ResViewerTabs {
Preview,
Raw,
Cookies,
Headers,
}

impl ResViewerState {
pub fn new(is_focused: bool, response: Option<ReqtuiResponse>) -> Self {
impl ResViewerTabs {
pub fn next(tab: &ResViewerTabs) -> Self {
match tab {
ResViewerTabs::Preview => ResViewerTabs::Raw,
ResViewerTabs::Raw => ResViewerTabs::Cookies,
ResViewerTabs::Cookies => ResViewerTabs::Headers,
ResViewerTabs::Headers => ResViewerTabs::Preview,
}
}
}

impl From<ResViewerTabs> for usize {
fn from(value: ResViewerTabs) -> Self {
match value {
ResViewerTabs::Preview => 0,
ResViewerTabs::Raw => 1,
ResViewerTabs::Cookies => 2,
ResViewerTabs::Headers => 3,
}
}
}

pub struct ResViewerLayout {
tabs_pane: Rect,
content_pane: Rect,
}

impl<'a> ResViewerState<'a> {
pub fn new(
is_focused: bool,
is_selected: bool,
response: Option<ReqtuiResponse>,
curr_tab: ResViewerTabs,
raw_scroll: &'a mut usize,
) -> Self {
ResViewerState {
is_focused,
response,
curr_tab,
is_selected,
raw_scroll,
}
}
}
Expand All @@ -30,35 +78,99 @@ impl<'a> ResViewer<'a> {
pub fn new(colors: &'a colors::Colors) -> Self {
ResViewer { colors }
}
}

impl StatefulWidget for ResViewer<'_> {
type State = ResViewerState;

fn render(self, size: Rect, buf: &mut Buffer, state: &mut Self::State) {
let block_border = if state.is_focused {
Style::default().fg(self.colors.normal.magenta.into())
} else {
Style::default().fg(self.colors.primary.hover.into())
fn draw_container(&self, size: Rect, buf: &mut Buffer, state: &mut ResViewerState) {
let block_border = match (state.is_focused, state.is_selected) {
(true, false) => Style::default().fg(self.colors.bright.magenta.into()),
(true, true) => Style::default().fg(self.colors.bright.yellow.into()),
(_, _) => Style::default().fg(self.colors.primary.hover.into()),
};

if let Some(ref res) = state.response {
let size = Rect::new(
size.x.add(1),
size.y.add(1),
size.width.sub(2),
size.height.sub(2),
);
let preview = Paragraph::new(res.body.clone()).wrap(Wrap { trim: false });
preview.render(size, buf);
}

let block = Block::default()
.borders(Borders::ALL)
.border_style(block_border)
.title("Preview")
.title_style(Style::default().fg(self.colors.normal.white.into()));
.border_style(block_border);

block.render(size, buf);
}

fn draw_tabs(&self, buf: &mut Buffer, state: &ResViewerState, size: Rect) {
let tabs = Tabs::new(["Pretty", "Raw", "Cookies", "Headers"])
.style(Style::default().fg(self.colors.primary.hover.into()))
.select(state.curr_tab.clone().into())
.highlight_style(
Style::default()
.fg(self.colors.bright.magenta.into())
.bg(self.colors.primary.hover.into()),
);
tabs.render(size, buf);
}

fn draw_preview_tab(&self, state: &mut ResViewerState, buf: &mut Buffer, size: Rect) {
match state.curr_tab {
ResViewerTabs::Preview => {}
ResViewerTabs::Raw => self.draw_raw_response(state, buf, size),
ResViewerTabs::Cookies => {}
ResViewerTabs::Headers => {}
}
}

fn draw_raw_response(&self, state: &mut ResViewerState, buf: &mut Buffer, size: Rect) {
if let Some(ref response) = state.response {
let lines = response
.body
.chars()
.collect::<Vec<_>>()
.chunks(size.width.into())
.map(|row| Line::from(row.iter().collect::<String>()))
.collect::<Vec<_>>();

if state.raw_scroll >= &mut lines.len().sub(2) {
*state.raw_scroll = lines.len().sub(2);
}

let lines = lines
.into_iter()
.skip(*state.raw_scroll)
.take(size.height.into())
.collect::<Vec<_>>();

let raw_response = Paragraph::new(lines).wrap(Wrap { trim: true });
raw_response.render(size, buf);
}
}
}

impl<'a> StatefulWidget for ResViewer<'a> {
type State = ResViewerState<'a>;

fn render(self, size: Rect, buf: &mut Buffer, state: &mut Self::State) {
let layout = build_layout(size);

self.draw_container(size, buf, state);
self.draw_tabs(buf, state, layout.tabs_pane);
self.draw_preview_tab(state, buf, layout.content_pane);
}
}

fn build_layout(size: Rect) -> ResViewerLayout {
let size = Rect::new(
size.x.add(1),
size.y.add(1),
size.width.saturating_sub(2),
size.height.saturating_sub(2),
);

let [tabs_pane, _, content_pane] = Layout::default()
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Fill(1),
])
.direction(Direction::Vertical)
.areas(size);

ResViewerLayout {
tabs_pane,
content_pane,
}
}

0 comments on commit ab7d353

Please sign in to comment.