Skip to content
Closed
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
6 changes: 2 additions & 4 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"
rfd = "0.15.2"
reqwest = "0.12.12"
tokio = { version = "1", features = ["full"] }

# gui options are optional
iced = { version = "0.13.1", optional = true, features = ["advanced"] }
iced = { version = "0.13.1", optional = true, features = ["advanced", "tokio"] }
iced_highlighter = { version = "0.13.0", optional = true }

230 changes: 209 additions & 21 deletions crates/crabapi/src/gui/iced/mod.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
// internal mods
mod default_styles;

use http::{HeaderMap, HeaderName};
use std::collections::HashMap;
// dependencies
use crate::core::requests;
use crate::core::requests::{Method, constants, send_requests, validators};
use http::{HeaderMap, HeaderName};
use iced;
use iced::widget::text_editor::{Action, Content};
use iced::widget::{Button, Row, Text, TextInput, scrollable, text_editor};
use iced::widget::{button, column, container, pick_list, row};
use iced::widget::{Button, Row, Space, Text, TextInput};
use iced::widget::{button, column, container, pick_list, radio, row, scrollable, text_editor};
use iced::{Alignment, Center, Element, Length, Task};
use iced_highlighter::Highlighter;
use reqwest::{Body, Client};
// internal dependencies
use crate::core::requests::{Method, constants, send_requests, validators};
use std::collections::HashMap;
use std::io;
use std::path::PathBuf;
use std::sync::Arc;

pub fn init() {
iced::run(GUI::title, GUI::update, GUI::view).unwrap()
Expand All @@ -30,6 +31,23 @@ enum Message {
SendRequest,
ResponseBodyChanged(String),
ResponseBodyText(Action),
BodyTypeChanged(BodyType),
BodyContentChanged(text_editor::Action),
BodyContentOpenFile,
BodyContentFileOpened(Result<(PathBuf, Arc<String>), FileOpenDialogError>),
}

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

#[derive(Debug, Clone)]
pub enum FileOpenDialogError {
DialogClosed,
IoError(io::ErrorKind),
}

#[derive(Debug)]
Expand All @@ -42,6 +60,10 @@ struct GUI {
url_input_valid: bool,
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>>,
}

impl GUI {
Expand All @@ -54,6 +76,10 @@ impl GUI {
url_input_valid: false,
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 @@ -140,6 +166,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(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 FileOpenDialogError::IoError(kind) = error {
println!("Error kind: {:?}", kind);
}
}
}
Task::none()
}
}
}

Expand All @@ -148,25 +201,20 @@ 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: Response
let response_row = self.view_response();

column![
request_row,
container(headers_column)
.width(Length::Fill)
.padding(default_styles::padding()),
container(response_row)
.align_x(Center)
.width(Length::Fill)
.padding(default_styles::padding()),
]
.into()
column![request_row, headers_row, body_row, response_row].into()
}

fn view_request(&self) -> Element<Message> {
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();
Expand All @@ -175,11 +223,17 @@ impl GUI {

let request_row = Self::view_request_row_setup(row![method_input, url_input, send_button]);

request_row.into()
column![title_row, request_row].into()
}

// VIEW REQUEST - GENERAL

fn view_request_title() -> Element<'static, Message> {
Text::new("Request")
.size(default_styles::input_size())
.into()
}

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)
Expand Down Expand Up @@ -229,6 +283,13 @@ impl GUI {
// VIEW REQUEST - HEADERS

fn view_request_headers(&self) -> Element<Message> {
container(self.view_request_headers_inner())
.width(Length::Fill)
.padding(default_styles::padding())
.into()
}

fn view_request_headers_inner(&self) -> Element<Message> {
let headers_title = Self::view_request_headers_title();

let headers_column = self.view_request_headers_column();
Expand All @@ -241,7 +302,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<Message> {
Expand Down Expand Up @@ -280,7 +343,109 @@ impl GUI {
.into()
}

// VIEW REQUEST - BODY

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()
}

// VIEW RESPONSE

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)
Expand All @@ -300,3 +465,26 @@ impl Default for GUI {
GUI::new()
}
}

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
}

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))
}