#![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] #[cfg(target_os = "macos")] #[macro_use] extern crate objc; use std::collections::HashMap; use std::fs::create_dir_all; use std::path::Path; use http::header::{HeaderName, USER_AGENT}; use http::{HeaderMap, HeaderValue, Method}; use reqwest::redirect::Policy; use sqlx::migrate::Migrator; use sqlx::sqlite::SqlitePoolOptions; use sqlx::{Pool, Sqlite}; use tauri::{AppHandle, State, Wry}; use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent}; use tokio::sync::Mutex; use window_ext::WindowExt; mod models; mod runtime; mod window_ext; #[derive(serde::Serialize)] pub struct CustomResponse { status: u16, body: String, url: String, method: String, elapsed: u128, elapsed2: u128, headers: HashMap, pub status_reason: Option<&'static str>, } async fn migrate_db(db_instance: &Mutex>) -> Result<(), String> { let pool = &*db_instance.lock().await; let m = Migrator::new(Path::new("./migrations")) .await .expect("Failed to load migrations"); m.run(pool).await.expect("Failed to run migrations"); println!("Migrations ran"); Ok(()) } #[tauri::command] async fn send_request( app_handle: AppHandle, db_instance: State<'_, Mutex>>, request_id: &str, ) -> Result { let pool = &*db_instance.lock().await; let req = models::get_request(request_id, pool) .await .expect("Failed to get request"); let start = std::time::Instant::now(); let mut abs_url = req.url.to_string(); if !abs_url.starts_with("http://") && !abs_url.starts_with("https://") { abs_url = format!("http://{}", req.url); } let client = reqwest::Client::builder() .redirect(Policy::none()) .build() .unwrap(); let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, HeaderValue::from_static("reqwest")); headers.insert("x-foo-bar", HeaderValue::from_static("hi mom")); headers.insert( HeaderName::from_static("x-api-key"), HeaderValue::from_static("123-123-123"), ); let m = Method::from_bytes(req.method.to_uppercase().as_bytes()).unwrap(); let builder = client.request(m, abs_url.to_string()).headers(headers); let sendable_req = match req.body { Some(b) => builder.body(b).build(), None => builder.build(), } .expect("Failed to build request"); let resp = client.execute(sendable_req).await; let p = app_handle .path_resolver() .resolve_resource("plugins/plugin.ts") .expect("failed to resolve resource"); runtime::run_plugin_sync(p.to_str().unwrap()).unwrap(); match resp { Ok(v) => { let status = v.status().as_u16() as i64; let status_reason = v.status().canonical_reason(); let headers = v .headers() .iter() .map(|(k, v)| models::HttpResponseHeader { name: k.as_str().to_string(), value: v.to_str().unwrap().to_string(), }) .collect(); let url = v.url().clone(); let body = v.text().await.expect("Failed to get body"); let elapsed = start.elapsed().as_millis() as i64; let response = models::create_response( &req.id, elapsed, url.as_str(), status, status_reason, body.as_str(), headers, pool, ) .await .expect("Failed to create response"); Ok(response) } Err(e) => { println!("Error: {}", e); Err(e.to_string()) } } } #[tauri::command] async fn create_request( workspace_id: &str, name: &str, app_handle: AppHandle, db_instance: State<'_, Mutex>>, ) -> Result { let pool = &*db_instance.lock().await; let headers = Vec::new(); let created_request = models::upsert_request(None, workspace_id, name, "GET", None, "", headers, pool) .await .expect("Failed to create request"); app_handle .emit_all("created_request", &created_request) .unwrap(); Ok(created_request.id) } #[tauri::command] async fn update_request( request: models::HttpRequest, app_handle: AppHandle, db_instance: State<'_, Mutex>>, ) -> Result<(), String> { let pool = &*db_instance.lock().await; // TODO: Figure out how to make this better let b2; let body = match request.body { Some(b) => { b2 = b; Some(b2.as_str()) } None => None, }; let updated_request = models::upsert_request( Some(request.id.as_str()), request.workspace_id.as_str(), request.name.as_str(), request.method.as_str(), body, request.url.as_str(), request.headers.0, pool, ) .await .expect("Failed to update request"); app_handle .emit_all("updated_request", updated_request) .unwrap(); Ok(()) } #[tauri::command] async fn requests( workspace_id: &str, db_instance: State<'_, Mutex>>, ) -> Result, String> { let pool = &*db_instance.lock().await; models::find_requests(workspace_id, pool) .await .map_err(|e| e.to_string()) } #[tauri::command] async fn responses( request_id: &str, db_instance: State<'_, Mutex>>, ) -> Result, String> { let pool = &*db_instance.lock().await; models::find_responses(request_id, pool) .await .map_err(|e| e.to_string()) } #[tauri::command] async fn delete_response( id: &str, db_instance: State<'_, Mutex>>, ) -> Result<(), String> { let pool = &*db_instance.lock().await; models::delete_response(id, pool) .await .map_err(|e| e.to_string()) } #[tauri::command] async fn delete_all_responses( request_id: &str, db_instance: State<'_, Mutex>>, ) -> Result<(), String> { let pool = &*db_instance.lock().await; models::delete_all_responses(request_id, pool) .await .map_err(|e| e.to_string()) } #[tauri::command] async fn workspaces( db_instance: State<'_, Mutex>>, ) -> Result, String> { let pool = &*db_instance.lock().await; let workspaces = models::find_workspaces(pool) .await .expect("Failed to find workspaces"); if workspaces.is_empty() { let workspace = models::create_workspace("Default", "This is the default workspace", pool) .await .expect("Failed to create workspace"); Ok(vec![workspace]) } else { Ok(workspaces) } } #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } fn main() { // here `"quit".to_string()` defines the menu item id, and the second parameter is the menu item label. let quit = CustomMenuItem::new("quit".to_string(), "Quit"); let tray_menu = SystemTrayMenu::new().add_item(quit); let system_tray = SystemTray::new().with_menu(tray_menu); tauri::Builder::default() .system_tray(system_tray) .setup(|app| { let win = app.get_window("main").unwrap(); win.position_traffic_lights(); Ok(()) }) .setup(|app| { let dir = app.path_resolver().app_data_dir().unwrap(); create_dir_all(dir.clone()).expect("Problem creating App directory!"); let p = dir.join("db.sqlite"); let p_string = p.to_string_lossy().replace(' ', "%20"); let url = format!("sqlite://{}?mode=rwc", p_string); println!("DB PATH: {}", p_string); tauri::async_runtime::block_on(async move { let pool = SqlitePoolOptions::new() .connect(url.as_str()) .await .expect("Failed to connect to database"); let m = Mutex::new(pool); migrate_db(&m).await.expect("Failed to migrate database"); app.manage(m); Ok(()) }) }) .on_system_tray_event(|app, event| { if let SystemTrayEvent::MenuItemClick { id, .. } = event { match id.as_str() { "quit" => { std::process::exit(0); } "hide" => { let window = app.get_window("main").unwrap(); window.hide().unwrap(); } _ => {} } } }) .on_window_event(|e| { let apply_offset = || { let win = e.window(); win.position_traffic_lights(); }; match e.event() { WindowEvent::Resized(..) => apply_offset(), WindowEvent::ThemeChanged(..) => apply_offset(), _ => {} } }) .invoke_handler(tauri::generate_handler![ greet, workspaces, requests, send_request, create_request, update_request, responses, delete_response, delete_all_responses, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }