Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
.idea/

.DS_Store
8 changes: 3 additions & 5 deletions crates/crabapi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

6 changes: 3 additions & 3 deletions crates/crabapi/src/gui/iced/default_styles.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
32 changes: 32 additions & 0 deletions crates/crabapi/src/gui/iced/file.rs
Original file line number Diff line number Diff line change
@@ -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<String>), 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<PathBuf>,
) -> Result<(PathBuf, Arc<String>), 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))
}
161 changes: 63 additions & 98 deletions crates/crabapi/src/gui/iced/mod.rs
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -27,6 +27,17 @@ enum Message {
SendRequest,
ResponseBodyChanged(String),
ResponseBodyText(Action),
BodyTypeChanged(BodyType),
BodyContentChanged(text_editor::Action),
BodyContentOpenFile,
BodyContentFileOpened(Result<(PathBuf, Arc<String>), file::FileOpenDialogError>),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BodyType {
Empty,
File,
Text,
}

#[derive(Debug, Clone)]
Expand All @@ -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<BodyType>,
body_file_path: Option<PathBuf>,
body_file_content: Option<Arc<String>>,
}

mod views;

impl GUI {
fn new() -> Self {
Self {
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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()
}
}
}

Expand Down Expand Up @@ -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();
Expand All @@ -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<Message> {
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<Message> {
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::Font> {
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<Message> {
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::<Highlighter>(
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 {
Expand Down
97 changes: 97 additions & 0 deletions crates/crabapi/src/gui/iced/views/body.rs
Original file line number Diff line number Diff line change
@@ -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<Message> {
container(self.view_request_body_inner())
.width(Length::Fill)
.padding(default_styles::padding())
.into()
}

fn view_request_body_inner(&self) -> Element<Message> {
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<Message> {
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<Message> {
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<Message> {
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<Message> {
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()
}
}
Loading