use std::sync::{mpsc::Sender, Arc, RwLock};
use eframe::egui;
use models::DispatchedRequest;
use request_list::RequestList;
use settings::AppSettings;
use storage::{Storage, TabAction};
use tab::Tab;
use tokio::task::JoinHandle;
use ulid::Ulid;
mod http_client;
mod models;
mod request_list;
mod settings;
mod storage;
mod tab;
pub type RequestDispatcher = Sender<DispatchedRequest>;
#[tokio::main]
async fn main() -> Result<(), eframe::Error> {
env_logger::init();
let (tx, rx) = std::sync::mpsc::channel::<DispatchedRequest>();
let (refresh_tx, refresh_rx) = std::sync::mpsc::channel::<()>();
let http_requester = tokio::spawn(async move {
while let Ok(incoming) = rx.recv() {
let to_send = http_client::handle_http_request(incoming.request)
.await
.map(models::DispatchedResponse::Success)
.map_err(models::DispatchedResponse::Error)
.unwrap_or_else(|e| e);
_ = incoming.respone_sender.send(to_send);
_ = refresh_tx.send(());
}
});
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1024.0, 768.0])
.with_close_button(true),
..Default::default()
};
_ = eframe::run_native(
"httpman",
options,
Box::new(move |ctx| {
egui_extras::install_image_loaders(&ctx.egui_ctx);
let ctxx = ctx.egui_ctx.clone();
let refresher = tokio::spawn(async move {
loop {
if refresh_rx.try_recv().is_ok() {
ctxx.request_repaint_after(std::time::Duration::from_millis(100));
} else {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
}
});
Box::new(HttprsApp::new(tx, refresher))
}),
);
tokio::try_join!(http_requester).expect("finish http_requester");
Ok(())
}
struct HttprsApp {
tabs: Vec<Tab>,
request_list: RequestList,
settings: AppSettings,
tab_ctx: TabCtx,
rename_dialog: Option<RenameDialog>,
}
#[derive(Debug)]
struct RenameDialog {
folder_path: std::path::PathBuf,
new_name: String,
}
#[derive(Clone)]
pub(crate) struct TabCtx {
request_dispatcher: RequestDispatcher,
storage: Arc<RwLock<Storage>>,
_refresh_bg: Arc<JoinHandle<()>>,
}
impl HttprsApp {
pub fn new(tx: RequestDispatcher, refresh_bg: JoinHandle<()>) -> Self {
let storage = Arc::new(RwLock::new(
Storage::new().expect("initialise storage failed"),
));
let tab_ctx = TabCtx {
request_dispatcher: tx.clone(),
storage: storage.clone(),
_refresh_bg: Arc::new(refresh_bg),
};
{
let storage_ref = storage.read().expect("read storage");
if storage_ref.is_empty() {
drop(storage_ref); let storage_write = storage.write().expect("write storage");
_ = storage_write
.create_example_data(&tab_ctx)
.map_err(|e| eprintln!("Failed to create example data: {}", e));
}
}
let mut tabs = storage
.read()
.expect("read storage")
.load_active_tabs(tab_ctx.clone())
.expect("load older tabs")
.flat_map(|t| t.ok())
.collect::<Vec<_>>();
if tabs.is_empty() {
tabs.push(Tab::new_bitcoin_api_example(tab_ctx.clone()));
tabs.push(Tab::new_image_example(tab_ctx.clone()));
}
let mut settings = storage.read().expect("read storage").load_settings();
settings.active_tab_index =
std::cmp::min(settings.active_tab_index, tabs.len().saturating_sub(1));
let request_list = RequestList::new();
Self {
tabs,
settings,
tab_ctx,
request_list,
rename_dialog: None,
}
}
fn save_settings(&self) {
_ = self
.tab_ctx
.storage_read()
.and_then(|storage| storage.save_settings(&self.settings))
.map_err(|e| eprintln!("Failed to save settings: {}", e));
}
fn handle_tab_action(&mut self, action: TabAction) {
match action {
TabAction::LoadFile { file_path } => {
if let Ok(tab) = self
.tab_ctx
.storage_read()
.and_then(|storage| storage.load_saved_tab(&file_path, self.tab_ctx.clone()))
{
self.tabs.push(tab);
self.settings.active_tab_index = self.tabs.len() - 1;
self.save_settings();
}
}
TabAction::CreateFolder { parent_path } => {
let folder_name = format!("New_Folder_{}", Ulid::new());
let folder_path = parent_path.join(&folder_name);
if let Err(e) = std::fs::create_dir_all(&folder_path) {
eprintln!("Failed to create folder: {}", e);
return;
}
{
let mut storage = self.tab_ctx.storage_write().expect("get storage");
_ = storage.refresh_list(&self.tab_ctx);
}
}
TabAction::CreateFile { folder_path } => {
let new_tab = Tab::new(self.tab_ctx.clone());
let file_name = format!("request_{}", new_tab.id);
let file_path = folder_path.join(format!("{}.httpmand", file_name));
if let Err(e) = new_tab.save_to_file(&file_path) {
eprintln!("Failed to create file: {}", e);
return;
}
self.tabs.push(new_tab);
self.settings.active_tab_index = self.tabs.len() - 1;
self.save_settings();
{
let mut storage = self.tab_ctx.storage_write().expect("get storage");
_ = storage.refresh_list(&self.tab_ctx);
}
}
TabAction::RenameFolder {
folder_path,
current_name,
} => {
self.rename_dialog = Some(RenameDialog {
folder_path,
new_name: current_name,
});
}
}
}
}
impl eframe::App for HttprsApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::SidePanel::left("file_tree_panel")
.resizable(true)
.default_width(self.settings.tab_bar_width)
.width_range(150.0..=400.0)
.show(ctx, |ui| {
let current_width = ui.available_width();
if (current_width - self.settings.tab_bar_width).abs() > 1.0 {
self.settings.tab_bar_width = current_width;
self.save_settings();
}
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 10.0);
ui.heading("httpman");
ui.separator();
ui.label("Saved Requests");
ui.separator();
if let Ok(Some(action)) = self.request_list.render(ui, &self.tab_ctx) {
self.handle_tab_action(action);
}
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 10.0);
ui.horizontal_wrapped(|ui| {
let mut remove_tab = None;
let mut new_active_tab = None;
for (tab_idx, tab) in self.tabs.iter().enumerate() {
let is_active = tab_idx == self.settings.active_tab_index;
let tab_btn =
ui.selectable_label(is_active, format!("{}. {}", tab_idx + 1, tab.title()));
tab_btn.context_menu(|ui| {
if ui.button("Close").clicked() {
remove_tab = Some(tab_idx);
}
if ui.button("Save As...").clicked() {
if let Some(tab) = self.tabs.get(tab_idx) {
let name = format!("request_{}", tab.id);
_ = self
.tab_ctx
.storage_read()
.and_then(|storage| storage.save_tab_to_saved(tab, &name))
.map_err(|e| eprintln!("Failed to save tab: {}", e));
{
let mut storage =
self.tab_ctx.storage_write().expect("get storage");
_ = storage.refresh_list(&self.tab_ctx);
}
}
ui.close_menu();
}
ui.separator();
if ui.button("Copy as cURL").clicked() {
if let Some(tab) = self.tabs.get(tab_idx) {
let curl_command = tab.request().to_curl();
ui.output_mut(|o| o.copied_text = curl_command);
}
ui.close_menu();
}
if ui.button("Copy as HTTP").clicked() {
if let Some(tab) = self.tabs.get(tab_idx) {
let http_format = tab.request().to_http_format();
ui.output_mut(|o| o.copied_text = http_format);
}
ui.close_menu();
}
});
if tab_btn.clicked() {
new_active_tab = Some(tab_idx);
} else if tab_btn.double_clicked() || tab_btn.middle_clicked() {
remove_tab = Some(tab_idx);
}
}
if ui.button("+").clicked() {
self.tabs.push(Tab::new(self.tab_ctx.clone()));
new_active_tab = Some(self.tabs.len() - 1);
}
if let Some(tab_idx) = new_active_tab {
self.settings.active_tab_index = tab_idx;
self.save_settings();
}
if let Some(tab_idx) = remove_tab {
if let Some(tab) = self.tabs.get(tab_idx) {
_ = self
.tab_ctx
.storage_read()
.and_then(|storage| storage.delete_tab(tab))
.map_err(|e| eprintln!("Failed to delete tab from storage: {}", e));
}
self.tabs.remove(tab_idx);
if self.settings.active_tab_index >= tab_idx {
if self.settings.active_tab_index == 0 {
self.settings.active_tab_index = self.tabs.len().saturating_sub(1);
} else {
self.settings.active_tab_index =
std::cmp::max(self.settings.active_tab_index - 1, 0);
}
}
self.save_settings();
}
});
ui.separator();
if let Some(active_tab) = self.tabs.get_mut(self.settings.active_tab_index) {
_ = active_tab.render(ui);
} else {
ui.centered_and_justified(|ui| {
ui.label("No tabs open. Click '+' to create a new tab.");
});
}
});
egui::TopBottomPanel::bottom("request_controls")
.resizable(false)
.min_height(40.0)
.show(ctx, |ui| {
ui.add_space(8.0); ui.horizontal(|ui| {
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 10.0);
if let Some(active_tab) = self.tabs.get_mut(self.settings.active_tab_index) {
if active_tab.is_sending() {
ui.add_enabled(false, egui::Button::new("Sending..."));
ui.spinner();
} else if ui.button("🚀 Send Request").clicked() {
active_tab.send_request_public();
}
ui.separator();
let mut follow_redirects = active_tab.follow_redirects();
if ui
.checkbox(&mut follow_redirects, "Follow redirects")
.changed()
{
active_tab.set_follow_redirects(follow_redirects);
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(format!(
"Tab {}/{}",
self.settings.active_tab_index + 1,
self.tabs.len()
));
});
} else {
ui.label("No active tab");
}
});
});
let mut close_rename_dialog = false;
let mut perform_rename = None;
if let Some(rename_dialog) = &mut self.rename_dialog {
egui::Window::new("Rename Folder")
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
ui.label("New folder name:");
ui.text_edit_singleline(&mut rename_dialog.new_name);
ui.horizontal(|ui| {
if ui.button("Rename").clicked() {
perform_rename = Some((
rename_dialog.folder_path.clone(),
rename_dialog.new_name.clone(),
));
close_rename_dialog = true;
}
if ui.button("Cancel").clicked() {
close_rename_dialog = true;
}
});
});
}
if let Some((folder_path, new_name)) = perform_rename {
_ = self
.tab_ctx
.storage_read()
.and_then(|storage| storage.rename_folder(&folder_path, &new_name))
.map_err(|e| eprintln!("Failed to rename folder: {}", e));
{
let mut storage = self.tab_ctx.storage_write().expect("get storage");
_ = storage.refresh_list(&self.tab_ctx);
}
}
if close_rename_dialog {
self.rename_dialog = None;
}
}
}
impl TabCtx {
pub fn storage_read(&self) -> anyhow::Result<std::sync::RwLockReadGuard<Storage>> {
self.storage.read().map_err(|e| anyhow::anyhow!("{e}"))
}
pub fn storage_write(&self) -> anyhow::Result<std::sync::RwLockWriteGuard<Storage>> {
self.storage.write().map_err(|e| anyhow::anyhow!("{e}"))
}
}