use std::{io::Write, sync::Arc};
use color_eyre::eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::{
prelude::*,
widgets::{block::*, *},
};
use crate::{
action::Action,
pages::phone::{RequestBuilder, RequestPane},
panes::Pane,
state::{InputMode, OperationItem, State},
tui::{EventResponse, Frame},
};
pub struct ResponseViewer {
focused: bool,
focused_border_style: Style,
operation_item: Arc<OperationItem>,
content_types: Vec<String>,
content_type_index: usize,
}
impl ResponseViewer {
pub fn new(operation_item: Arc<OperationItem>, focused: bool, focused_border_style: Style) -> Self {
Self { operation_item, focused, focused_border_style, content_types: vec![], content_type_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,
}
}
}
impl RequestPane for ResponseViewer {}
impl RequestBuilder for ResponseViewer {
fn reqeust(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
if let Some(content_type) = self.content_types.get(self.content_type_index) {
request.header("accept", content_type)
} else {
request
}
}
}
impl Pane for ResponseViewer {
fn init(&mut self, state: &State) -> Result<()> {
self.content_types = self
.operation_item
.operation
.responses
.as_ref()
.and_then(|responses| responses.get("200"))
.and_then(|ok_response| ok_response.resolve(&state.openapi_spec).ok())
.and_then(|response| response.content)
.map(|content| content.keys().cloned().collect::<Vec<_>>())
.unwrap_or_default();
Ok(())
}
fn height_constraint(&self) -> Constraint {
match self.focused {
true => Constraint::Fill(3),
false => Constraint::Fill(1),
}
}
fn handle_key_events(&mut self, _key: KeyEvent, state: &mut State) -> Result<Option<EventResponse<Action>>> {
match state.input_mode {
InputMode::Normal => Ok(None),
InputMode::Insert => Ok(None),
InputMode::Command => Ok(None),
}
}
fn update(&mut self, action: Action, state: &mut State) -> Result<Option<Action>> {
match action {
Action::Update => {},
Action::Submit => return Ok(Some(Action::Dial)),
Action::Tab(index) if !self.content_types.is_empty() && index < self.content_types.len().try_into()? => {
self.content_type_index = index.try_into()?;
},
Action::TabNext if !self.content_types.is_empty() => {
let next_tab_index = self.content_type_index + 1;
self.content_type_index =
if next_tab_index < self.content_types.len() { next_tab_index } else { self.content_type_index };
},
Action::TabPrev if !self.content_types.is_empty() => {
self.content_type_index =
if self.content_type_index > 0 { self.content_type_index - 1 } else { self.content_type_index };
},
Action::Focus => {
self.focused = true;
},
Action::UnFocus => {
self.focused = false;
},
Action::SaveResponsePayload(filepath) => {
if let Some(response) =
self.operation_item.operation.operation_id.as_ref().and_then(|operation_id| state.responses.get(operation_id))
{
if let Err(error) =
std::fs::File::create(filepath).and_then(|mut file| file.write_all(response.body.as_bytes()))
{
return Ok(Some(Action::TimedStatusLine(format!("can't create or write file content: {error}"), 5)));
}
} else {
return Ok(Some(Action::TimedStatusLine("response is not available".into(), 5)));
}
},
_ => {},
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<()> {
let inner = area.inner(Margin { horizontal: 1, vertical: 1 });
let inner_panes = Layout::horizontal([Constraint::Fill(3), Constraint::Fill(1)]).split(inner);
let mut status_line = String::default();
if let Some(response) =
self.operation_item.operation.operation_id.as_ref().and_then(|operation_id| state.responses.get(operation_id))
{
status_line = format!(
"[{:?} {} {} {}]",
response.version,
response.status.as_str(),
symbols::DOT,
humansize::format_size(response.content_length.unwrap_or(response.body.len() as u64), humansize::DECIMAL)
);
frame.render_widget(
Paragraph::new(response.body.clone()).wrap(Wrap { trim: false }).block(
Block::default().borders(Borders::RIGHT).border_style(self.border_style()).border_type(self.border_type()),
),
inner_panes[0],
);
frame.render_widget(
List::new(
response
.headers
.iter()
.map(|(hk, hv)| {
Line::from(vec![
Span::styled(format!("{}: ", hk), Style::default().add_modifier(Modifier::BOLD)),
Span::raw(hv.to_str().unwrap_or("ERROR")),
])
})
.collect::<Vec<_>>(),
),
inner_panes[1],
);
} else {
frame.render_widget(
Paragraph::new(" No response is available. Press enter or try [send] command.").style(Style::default().dim()),
inner,
)
}
let content_types = if !self.content_types.is_empty() {
let ctype = self.content_types[self.content_type_index].clone();
let ctype_progress = if self.content_types.len() > 1 {
format!("[{}/{}]", self.content_type_index + 1, self.content_types.len())
} else {
String::default()
};
format!(": {ctype} {ctype_progress}")
} else {
String::default()
};
frame.render_widget(
Block::default()
.title(format!("Response{content_types}"))
.borders(Borders::ALL)
.border_style(self.border_style())
.border_type(self.border_type())
.title_bottom(Line::from(status_line).right_aligned()),
area,
);
Ok(())
}
}