use std::{io::Write, path::PathBuf, sync::mpsc::Receiver};
use thousands::Separable;
use ulid::Ulid;
use crate::{
models::{DispatchedRequest, DispatchedResponse, HttpMethod, Request, ResponseBody},
TabCtx,
};
pub struct Tab {
pub id: Ulid,
request: Request,
ctx: TabCtx,
pending_response: Option<Receiver<DispatchedResponse>>,
response: Option<DispatchedResponse>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub(crate) struct PersistedTab {
pub id: Ulid,
pub request: Request,
}
impl Tab {
pub fn new(ctx: TabCtx) -> Self {
Self {
id: Ulid::new(),
ctx,
request: <_>::default(),
response: <_>::default(),
pending_response: None,
}
}
pub fn title(&self) -> String {
let url = &self.request.url;
let url = url
.trim_start_matches("http://")
.trim_start_matches("https://");
let url = url.trim_start_matches("www.");
let url = url.split('/').next().unwrap_or(url);
format!("{} {}", self.request.method.as_str(), url)
}
pub fn request(&self) -> &Request {
&self.request
}
pub fn file_name(&self) -> PathBuf {
PathBuf::from(format!("{}.httpmand", self.id.to_string()))
}
pub fn new_from_file(path: &std::path::Path, ctx: TabCtx) -> anyhow::Result<Self> {
let file = std::fs::File::open(path)?;
let persisted: PersistedTab = rmp_serde::from_read(file)?;
Ok(Self::new_from_persisted(persisted, ctx))
}
pub fn save_to_file(&self, path: &std::path::Path) -> anyhow::Result<()> {
let mut file = std::fs::File::create(path)?;
let bytes = rmp_serde::to_vec(&self.into_persisted())?;
file.write_all(&bytes)?;
Ok(())
}
fn new_from_persisted(p: PersistedTab, ctx: TabCtx) -> Self {
Self {
id: p.id,
request: p.request,
ctx,
pending_response: <_>::default(),
response: <_>::default(),
}
}
pub fn new_bitcoin_api_example(ctx: TabCtx) -> Self {
let mut request = Request::default();
request.method = HttpMethod::Get;
request.url = "https://api.coingecko.com/api/v3/coins/bitcoin".to_string();
request
.headers
.push(("Accept".to_string(), "application/json".to_string()));
Self {
id: Ulid::new(),
request,
ctx,
pending_response: None,
response: None,
}
}
pub fn new_image_example(ctx: TabCtx) -> Self {
let mut request = Request::default();
request.method = HttpMethod::Get;
request.url = "https://picsum.photos/800".to_string();
Self {
id: Ulid::new(),
request,
ctx,
pending_response: None,
response: None,
}
}
fn into_persisted(&self) -> PersistedTab {
PersistedTab {
id: self.id,
request: self.request.clone(),
}
}
pub fn render(&mut self, ui: &mut egui::Ui) -> anyhow::Result<()> {
ui.vertical(|ui| {
let request = &mut self.request;
render_method_and_url(ui, request);
ui.separator();
ui.label("Headers");
render_headers(request, ui);
if request.method.can_have_body() {
ui.separator();
ui.label("Body");
egui::TextEdit::multiline(&mut request.body)
.code_editor()
.show(ui);
ui.separator();
}
if self.pending_response.is_some() {
let _ = self.fetch_response(ui);
}
});
self.render_reponse(ui);
Ok(())
}
pub fn follow_redirects(&self) -> bool {
self.request.follow_redirects
}
pub fn set_follow_redirects(&mut self, follow: bool) {
self.request.follow_redirects = follow;
}
pub fn is_sending(&self) -> bool {
self.pending_response.is_some()
}
pub fn send_request_public(&mut self) {
self.send_request();
}
fn render_reponse(&mut self, ui: &mut egui::Ui) {
match &self.response {
Some(DispatchedResponse::Success(http)) => {
ui.separator();
ui.label(format!("HTTP {}", http.status_code));
ui.spacing();
egui::ScrollArea::vertical().hscroll(true).show(ui, |ui| {
ui.vertical_centered_justified(|ui| {
egui::CollapsingHeader::new(format!("Headers ({})", http.headers.len()))
.default_open(false)
.show(ui, |ui| {
for (k, v) in http.headers.iter() {
ui.horizontal(|ui| {
ui.label(k);
ui.label(v);
});
}
});
ui.spacing();
let content_length = http.content_length().unwrap_or(0);
egui::CollapsingHeader::new(format!(
"Body ({} bytes)",
content_length.separate_with_commas()
))
.default_open(true)
.show(ui, |ui| {
match &http.body {
ResponseBody::Text(text) => {
ui.monospace(text);
}
ResponseBody::Json {
value: json,
length: _,
} => {
egui_json_tree::JsonTree::new(
format!("{}_response", self.id),
json,
)
.default_expand(egui_json_tree::DefaultExpand::ToLevel(0))
.show(ui);
}
ResponseBody::Image(f) => {
let path = format!("file://{}", f.to_string_lossy());
ui.vertical_centered_justified(|ui| ui.image(path));
}
ResponseBody::Binary(_f) => {
if ui
.button(format!("Saved {} bytes", content_length))
.clicked()
{
}
}
ResponseBody::Empty => {
if http.is_redirect() {
if ui.button("Rety and follow redirect").clicked() {
}
} else {
ui.label("Empty response");
}
}
};
});
});
});
}
Some(DispatchedResponse::Error(_e)) => {}
None => {}
}
}
fn fetch_response(&mut self, ui: &mut egui::Ui) -> anyhow::Result<()> {
if let Some(rx) = &self.pending_response {
if let Ok(response) = rx.try_recv() {
self.response = Some(response);
self.pending_response = None;
self.ctx
.storage_read()?
.save_tab(self)
.expect("save tab failed");
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(999));
}
}
Ok(())
}
fn send_request(&mut self) {
let (sender, receiver) = std::sync::mpsc::channel::<DispatchedResponse>();
let dispatched_request = DispatchedRequest {
respone_sender: sender,
request: self.request.clone(),
};
self.pending_response = Some(receiver);
self.ctx
.request_dispatcher
.send(dispatched_request)
.unwrap();
_ = self
.ctx
.storage_read()
.and_then(|storage| storage.save_tab(self))
.map_err(|e| eprintln!("Failed to save tab after sending request: {}", e));
}
}
fn render_method_and_url(ui: &mut egui::Ui, request: &mut Request) {
ui.horizontal(|ui| {
let mut selected_method = request.method;
egui::ComboBox::from_label(" ")
.selected_text(selected_method.as_str())
.show_ui(ui, |ui| {
for method in HttpMethod::iter() {
ui.selectable_value(&mut selected_method, method, method.as_str());
}
});
request.method = selected_method;
ui.text_edit_singleline(&mut request.url);
});
}
fn render_headers(request: &mut Request, ui: &mut egui::Ui) {
let mut idx = 0;
let mut to_remove = None;
ui.vertical(|ui| {
for (k, v) in request.headers.iter_mut() {
ui.horizontal_top(|ui| {
ui.style_mut().wrap = Some(true);
ui.text_edit_singleline(k);
ui.text_edit_singleline(v);
if ui.link("Delete").clicked() {
to_remove = Some(idx);
}
});
idx += 1;
}
});
if let Some(idx) = to_remove {
request.headers.remove(idx);
}
if ui.link("+ New").clicked() {
request.headers.push(("".to_string(), "".to_string()));
}
}