diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 35d5bc0..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index a8b5cd3..1ec6b6d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ Cargo.lock # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -.idea/ \ No newline at end of file +.idea/ + +.DS_Store \ No newline at end of file diff --git a/crates/crabapi/Cargo.toml b/crates/crabapi/Cargo.toml index af9f8e1..d134ca1 100644 --- a/crates/crabapi/Cargo.toml +++ b/crates/crabapi/Cargo.toml @@ -11,10 +11,8 @@ iced = ["dep:iced", "dep:iced_highlighter"] clap = "4.5.31" const_format = "0.2.34" http = "1.2.0" +iced = { version = "0.13.1", optional = true, features = ["advanced", "tokio"] } +iced_highlighter = { version = "0.13.0", optional = true } reqwest = "0.12.12" +rfd = "0.15.2" tokio = { version = "1", features = ["full"] } - -# gui options are optional -iced = { version = "0.13.1", optional = true, features = ["advanced"] } -iced_highlighter = { version = "0.13.0", optional = true } - diff --git a/crates/crabapi/src/gui/iced/default_styles.rs b/crates/crabapi/src/gui/iced/default_styles.rs index 6518622..0e7d61d 100644 --- a/crates/crabapi/src/gui/iced/default_styles.rs +++ b/crates/crabapi/src/gui/iced/default_styles.rs @@ -1,9 +1,9 @@ #[allow(unused_imports)] -use iced::{Color, Pixels, Border, Padding, border::Radius}; +use iced::{Border, Color, Padding, Pixels, border::Radius}; /// Font size for inputs and buttons pub const fn input_size_as_f32() -> f32 { - 22.0 // Slightly larger for better readability + 22.0 // Slightly larger for better readability } /// Font size wrapper @@ -13,7 +13,7 @@ pub const fn input_size() -> Pixels { /// Padding for UI elements pub const fn padding() -> Padding { - Padding::new(12.0) // Increased padding for better spacing + Padding::new(12.0) // Increased padding for better spacing } /// Spacing between elements diff --git a/crates/crabapi/src/gui/iced/file.rs b/crates/crabapi/src/gui/iced/file.rs new file mode 100644 index 0000000..f17c441 --- /dev/null +++ b/crates/crabapi/src/gui/iced/file.rs @@ -0,0 +1,32 @@ +use std::io; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub enum FileOpenDialogError { + DialogClosed, + IoError(io::ErrorKind), +} + +pub async fn open_file() -> Result<(PathBuf, Arc), FileOpenDialogError> { + let picked_file = rfd::AsyncFileDialog::new() + .set_title("Open a file...") + .pick_file() + .await + .ok_or(FileOpenDialogError::DialogClosed)?; + + load_file(picked_file).await +} + +pub async fn load_file( + path: impl Into, +) -> Result<(PathBuf, Arc), FileOpenDialogError> { + let path = path.into(); + + let contents = tokio::fs::read_to_string(&path) + .await + .map(Arc::new) + .map_err(|error| FileOpenDialogError::IoError(error.kind()))?; + + Ok((path, contents)) +} diff --git a/crates/crabapi/src/gui/iced/mod.rs b/crates/crabapi/src/gui/iced/mod.rs index 0f7597e..169ed0d 100644 --- a/crates/crabapi/src/gui/iced/mod.rs +++ b/crates/crabapi/src/gui/iced/mod.rs @@ -1,18 +1,18 @@ -// internal mods mod default_styles; +mod file; +mod views; -use http::{HeaderMap, HeaderName}; -// dependencies use crate::core::requests; +use crate::core::requests::{Method, constants, send_requests, validators}; +use http::{HeaderMap, HeaderName}; use iced; +use iced::widget::column; +use iced::widget::text_editor; use iced::widget::text_editor::{Action, Content}; -use iced::widget::{Button, Row, Text, TextInput, scrollable, text_editor}; -use iced::widget::{column, container, pick_list, row}; -use iced::{Alignment, Center, Element, Length, Task}; -use iced_highlighter::Highlighter; +use iced::{Element, Task}; use reqwest::{Body, Client}; -// internal dependencies -use crate::core::requests::{Method, constants, send_requests, validators}; +use std::path::PathBuf; +use std::sync::Arc; pub fn init() { iced::run(GUI::title, GUI::update, GUI::view).unwrap() @@ -27,6 +27,17 @@ enum Message { SendRequest, ResponseBodyChanged(String), ResponseBodyText(Action), + BodyTypeChanged(BodyType), + BodyContentChanged(text_editor::Action), + BodyContentOpenFile, + BodyContentFileOpened(Result<(PathBuf, Arc), file::FileOpenDialogError>), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum BodyType { + Empty, + File, + Text, } #[derive(Debug, Clone)] @@ -48,10 +59,12 @@ struct GUI { query_input: Vec<(String, String)>, header_input: Vec<(String, String)>, response_body: Content, + body_content: text_editor::Content, + body_type_select: Option, + body_file_path: Option, + body_file_content: Option>, } -mod views; - impl GUI { fn new() -> Self { Self { @@ -63,6 +76,10 @@ impl GUI { query_input: vec![(String::new(), String::new())], header_input: vec![(String::new(), String::new())], response_body: Content::with_text("Response body will go here..."), + body_content: text_editor::Content::default(), + body_type_select: Some(BodyType::Text), + body_file_path: None, + body_file_content: None, } } @@ -138,6 +155,33 @@ impl GUI { Task::none() } + Message::BodyTypeChanged(body_type) => { + self.body_type_select = Some(body_type); + Task::none() + } + Message::BodyContentChanged(action) => { + self.body_content.perform(action); + Task::none() + } + Message::BodyContentOpenFile => { + Task::perform(file::open_file(), Message::BodyContentFileOpened) + } + Message::BodyContentFileOpened(result) => { + match result { + Ok((path, content)) => { + self.body_file_content = Some(content); + self.body_file_path = Some(path); + } + Err(error) => { + // TODO: use tracing + println!("Error opening file: {:?}", error); + if let file::FileOpenDialogError::IoError(kind) = error { + println!("Error kind: {:?}", kind); + } + } + } + Task::none() + } } } @@ -171,7 +215,10 @@ impl GUI { let request_row = self.view_request(); // ROW: Headers - let headers_column = self.view_request_headers(); + let headers_row = self.view_request_headers(); + + // ROW: Body + let body_row = self.view_request_body(); // ROW: Queries let queries_column = self.view_request_queries(); @@ -181,95 +228,13 @@ impl GUI { column![ request_row, - container(headers_column) - .width(Length::Fill) - .padding(default_styles::padding()), - container(queries_column) - .width(Length::Fill) - .padding(default_styles::padding()), - container(response_row) - .align_x(Center) - .width(Length::Fill) - .padding(default_styles::padding()), + headers_row, + body_row, + queries_column, + response_row ] .into() } - - fn view_request(&self) -> Element { - let url_input = self.view_request_url_input(); - - let method_input = self.view_request_method_input(); - - let send_button = Self::view_request_send_button(); - - let request_row = Self::view_request_row_setup(row![method_input, url_input, send_button]); - - request_row.into() - } - - // VIEW REQUEST - GENERAL - - fn view_request_url_input(&self) -> Element { - let url_input_icon = Self::view_request_url_input_icon(self.url_input_valid); - let url_input = TextInput::new("Enter URI", &self.url_input) - .on_input(Message::UrlInputChanged) - .size(default_styles::input_size()) - .icon(url_input_icon) - .width(Length::Fill); - - url_input.into() - } - - fn view_request_url_input_icon(valid: bool) -> iced::widget::text_input::Icon { - iced::widget::text_input::Icon { - font: iced::Font::default(), - code_point: if valid { '✅' } else { '❌' }, - size: Some(default_styles::input_size()), - spacing: 0.0, - side: iced::widget::text_input::Side::Right, - } - } - - fn view_request_method_input(&self) -> Element { - pick_list( - self.methods, - self.method_selected.clone(), - Message::MethodChanged, - ) - .placeholder("Method") - .width(Length::Shrink) - .text_size(default_styles::input_size()) - .into() - } - - fn view_request_row_setup(request_row: Row<'_, Message>) -> Row<'_, Message> { - request_row - .spacing(default_styles::spacing()) - .padding(default_styles::padding()) - .align_y(Alignment::Center) - .width(Length::Fill) // Stretch the row - } - - fn view_request_send_button() -> Element<'static, Message> { - Button::new(Text::new("Send").size(default_styles::input_size())) - .on_press(Message::SendRequest) - .into() - } - - fn view_response(&self) -> Element<'_, Message> { - let label = Text::new("Response:").size(default_styles::input_size()); - let body = text_editor(&self.response_body) - .on_action(Message::ResponseBodyText) - .highlight_with::( - iced_highlighter::Settings { - theme: iced_highlighter::Theme::SolarizedDark, - token: "html".to_string(), - }, - |highlight, _theme| highlight.to_format(), - ); - column![label, scrollable(body)].into() - } - } impl Default for GUI { diff --git a/crates/crabapi/src/gui/iced/views/body.rs b/crates/crabapi/src/gui/iced/views/body.rs new file mode 100644 index 0000000..eef1b94 --- /dev/null +++ b/crates/crabapi/src/gui/iced/views/body.rs @@ -0,0 +1,97 @@ +use super::super::BodyType; +use super::GUI; +use crate::gui::iced::{Message, default_styles}; +use iced::widget::{Button, Row, Space, Text, column, container, radio, row, text_editor}; +use iced::{Center, Element, Length}; + +impl GUI { + pub fn view_request_body(&self) -> Element { + container(self.view_request_body_inner()) + .width(Length::Fill) + .padding(default_styles::padding()) + .into() + } + + fn view_request_body_inner(&self) -> Element { + let body_title = Self::view_request_body_title(); + + let radio_buttons = self.view_request_body_radio_buttons(); + + let content = self.view_request_body_content(); + + column!(body_title, radio_buttons, content,) + .spacing(default_styles::spacing()) + .into() + } + + fn view_request_body_title() -> Element<'static, Message> { + Text::new("Body").size(default_styles::input_size()).into() + } + + fn view_request_body_radio_buttons(&self) -> Row { + let empty = radio( + "Empty", + BodyType::Empty, + self.body_type_select, + Message::BodyTypeChanged, + ); + + let text = radio( + "Text", + BodyType::Text, + self.body_type_select, + Message::BodyTypeChanged, + ); + let file = radio( + "File", + BodyType::File, + self.body_type_select, + Message::BodyTypeChanged, + ); + + row![empty, text, file].spacing(default_styles::spacing()) + } + + fn view_request_body_content(&self) -> Row { + let content = match self.body_type_select { + Some(BodyType::Empty) => row![], + Some(BodyType::File) => self.view_request_body_file(), + Some(BodyType::Text) => self.view_request_body_text(), + None => row![], + }; + + content + } + + fn view_request_body_text(&self) -> Row { + row![ + text_editor(&self.body_content) + .on_action(Message::BodyContentChanged) + .placeholder("Introduce body here...") + .size(default_styles::input_size()) + ] + } + + fn view_request_body_file(&self) -> Row { + let file_name_string = format!( + "File: {}", + self.body_file_path + .as_ref() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "No file selected".to_string()) + ); + + row![ + Self::view_request_body_text_button(), + Space::new(default_styles::input_size(), default_styles::input_size()), + Text::new(file_name_string).size(default_styles::input_size()) + ] + .align_y(Center) + } + + fn view_request_body_text_button() -> Element<'static, Message> { + Button::new(Text::new("Select File").size(default_styles::input_size())) + .on_press(Message::BodyContentOpenFile) + .into() + } +} diff --git a/crates/crabapi/src/gui/iced/views/headers.rs b/crates/crabapi/src/gui/iced/views/headers.rs index 0189dcf..f2faba4 100644 --- a/crates/crabapi/src/gui/iced/views/headers.rs +++ b/crates/crabapi/src/gui/iced/views/headers.rs @@ -1,10 +1,17 @@ use super::GUI; use crate::gui::iced::{Message, TupleEvent, default_styles}; -use iced::widget::{Button, Text, TextInput, row}; +use iced::widget::{Button, Text, TextInput, container, row}; use iced::{Element, Length}; impl GUI { pub fn view_request_headers(&self) -> Element { + container(self.view_request_headers_inner()) + .width(Length::Fill) + .padding(default_styles::padding()) + .into() + } + + fn view_request_headers_inner(&self) -> Element { let headers_title = Self::view_request_headers_title(); let headers_column = self.view_request_headers_column(); @@ -17,7 +24,9 @@ impl GUI { } fn view_request_headers_title() -> Element<'static, Message> { - Text::new("Headers").size(16).into() + Text::new("Headers") + .size(default_styles::input_size()) + .into() } fn view_request_headers_column(&self) -> Element { diff --git a/crates/crabapi/src/gui/iced/views/mod.rs b/crates/crabapi/src/gui/iced/views/mod.rs index 78d9fd1..bf77993 100644 --- a/crates/crabapi/src/gui/iced/views/mod.rs +++ b/crates/crabapi/src/gui/iced/views/mod.rs @@ -1,3 +1,7 @@ use super::GUI; + +mod body; mod headers; mod queries; +mod request; +mod response; diff --git a/crates/crabapi/src/gui/iced/views/queries.rs b/crates/crabapi/src/gui/iced/views/queries.rs index b0d2af0..b98912e 100644 --- a/crates/crabapi/src/gui/iced/views/queries.rs +++ b/crates/crabapi/src/gui/iced/views/queries.rs @@ -1,23 +1,32 @@ use super::GUI; use crate::gui::iced::{Message, TupleEvent, default_styles}; -use iced::widget::{Button, Text, TextInput, row}; +use iced::widget::{Button, Text, TextInput, column, container, row}; use iced::{Element, Length}; impl GUI { pub fn view_request_queries(&self) -> Element { + container(self.view_request_queries_inner()) + .width(Length::Fill) + .padding(default_styles::padding()) + .into() + } + + fn view_request_queries_inner(&self) -> Element { let queries_title = Self::view_request_queries_title(); let queries_column = self.view_request_queries_column(); let query_add_button = Self::view_request_queries_add_button(); - iced::widget::column![queries_title, queries_column, query_add_button] + column![queries_title, queries_column, query_add_button] .spacing(default_styles::spacing()) .into() } fn view_request_queries_title() -> Element<'static, Message> { - Text::new("Queries").size(16).into() + Text::new("Queries") + .size(default_styles::input_size()) + .into() } fn view_request_queries_column(&self) -> Element { diff --git a/crates/crabapi/src/gui/iced/views/request.rs b/crates/crabapi/src/gui/iced/views/request.rs new file mode 100644 index 0000000..971af61 --- /dev/null +++ b/crates/crabapi/src/gui/iced/views/request.rs @@ -0,0 +1,72 @@ +use super::GUI; +use crate::gui::iced::{Message, default_styles}; +use iced::widget::{Button, Row, Text, TextInput, column, pick_list, row}; +use iced::{Alignment, Element, Length}; + +impl GUI { + pub fn view_request(&self) -> Element { + let title_row = Self::view_request_row_setup(row![Self::view_request_title()]); + + let url_input = self.view_request_url_input(); + + let method_input = self.view_request_method_input(); + + let send_button = Self::view_request_send_button(); + + let request_row = Self::view_request_row_setup(row![method_input, url_input, send_button]); + + column![title_row, request_row].into() + } + + fn view_request_title() -> Element<'static, Message> { + Text::new("Request") + .size(default_styles::input_size()) + .into() + } + + fn view_request_url_input(&self) -> Element { + let url_input_icon = Self::view_request_url_input_icon(self.url_input_valid); + let url_input = TextInput::new("Enter URI", &self.url_input) + .on_input(Message::UrlInputChanged) + .size(default_styles::input_size()) + .icon(url_input_icon) + .width(Length::Fill); + + url_input.into() + } + + fn view_request_url_input_icon(valid: bool) -> iced::widget::text_input::Icon { + iced::widget::text_input::Icon { + font: iced::Font::default(), + code_point: if valid { '✅' } else { '❌' }, + size: Some(default_styles::input_size()), + spacing: 0.0, + side: iced::widget::text_input::Side::Right, + } + } + + fn view_request_method_input(&self) -> Element { + pick_list( + self.methods, + self.method_selected.clone(), + Message::MethodChanged, + ) + .placeholder("Method") + .width(Length::Shrink) + .text_size(default_styles::input_size()) + .into() + } + + fn view_request_row_setup(request_row: Row<'_, Message>) -> Row<'_, Message> { + request_row + .spacing(default_styles::spacing()) + .padding(default_styles::padding()) + .align_y(Alignment::Center) + } + + fn view_request_send_button() -> Element<'static, Message> { + Button::new(Text::new("Send").size(default_styles::input_size())) + .on_press(Message::SendRequest) + .into() + } +} diff --git a/crates/crabapi/src/gui/iced/views/response.rs b/crates/crabapi/src/gui/iced/views/response.rs new file mode 100644 index 0000000..91e9876 --- /dev/null +++ b/crates/crabapi/src/gui/iced/views/response.rs @@ -0,0 +1,31 @@ +use super::GUI; +use crate::gui::iced::{Message, default_styles}; +use iced::widget::{Text, column, container, scrollable, text_editor}; +use iced::{Center, Element, Length}; +use iced_highlighter::Highlighter; + +impl GUI { + pub fn view_response(&self) -> Element<'_, Message> { + container(self.view_response_inner()) + .align_x(Center) + .width(Length::Fill) + .padding(default_styles::padding()) + .into() + } + + fn view_response_inner(&self) -> Element<'_, Message> { + let label = Text::new("Response:").size(default_styles::input_size()); + let body = text_editor(&self.response_body) + .on_action(Message::ResponseBodyText) + .highlight_with::( + iced_highlighter::Settings { + theme: iced_highlighter::Theme::SolarizedDark, + token: "html".to_string(), + }, + |highlight, _theme| highlight.to_format(), + ); + column![label, scrollable(body)] + .spacing(default_styles::spacing()) + .into() + } +}