├── .github └── workflows │ ├── clippy.yml │ └── rust.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── imgs ├── api_keys.png ├── cute.png ├── demo.gif ├── logo.gif ├── saved_request.png └── screenshot.png ├── src ├── app.rs ├── database │ ├── db.rs │ ├── mod.rs │ └── postman.rs ├── display │ ├── inputopt.rs │ ├── menuopts.rs │ └── mod.rs ├── events │ ├── event.rs │ ├── handler.rs │ └── mod.rs ├── lib.rs ├── main.rs ├── request │ ├── curl.rs │ ├── mod.rs │ └── response.rs ├── screens │ ├── auth.rs │ ├── collections.rs │ ├── cookies.rs │ ├── error.rs │ ├── headers.rs │ ├── input │ │ ├── input_screen.rs │ │ ├── mod.rs │ │ └── request_body_input.rs │ ├── method.rs │ ├── mod.rs │ ├── more_flags.rs │ ├── render.rs │ ├── request.rs │ ├── response.rs │ ├── saved_commands.rs │ ├── saved_keys.rs │ └── screen.rs └── tui_cute.rs └── test_collection.json /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Clippy check 3 | jobs: 4 | clippy_check: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - run: rustup component add clippy 9 | - uses: actions-rs/clippy-check@v1 10 | with: 11 | token: ${{ secrets.GITHUB_TOKEN }} 12 | args: --all-features 13 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build + Tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [windows-latest, ubuntu-latest, macos-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Rust 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | toolchain: stable 25 | override: true 26 | - name: Install Deps On Linux 27 | if: runner.os == 'Linux' 28 | run: | 29 | sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev 30 | - name: Build 31 | run: cargo build --verbose 32 | - name: Run tests 33 | run: cargo test --verbose 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vs 3 | testapi.txt 4 | /postman 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | All commit messages must follow the conventional commit format. For more information on this format, please visit 4 | 5 | `https://conventionalcommits.org/` 6 | 7 | 8 | ### Please use the following format for PR's: 9 | 10 | #### Please Include: 11 | 12 | - **Scope**: Feature, Bug fix, Refactor, docs, etc. 13 | 14 | - **Description**: A short description of the change and your reasoning for it. 15 | 16 | - **Related issue**: If there is no issue related, please create one :) 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "CuTE-tui" # crates.io/crates/CuTE is taken :( 3 | version = "0.1.2" 4 | authors = ["PThorpe92 "] 5 | description = "A (ratatui) TUI for HTTP requests with request + API key management" 6 | license = "GPL-3.0" 7 | edition = "2021" 8 | repository = "https://github.com/PThorpe92/CuTE" 9 | 10 | [[bin]] 11 | name = "cute" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | crossterm = "0.27.0" 16 | tui = { package = "ratatui", features = [ 17 | "crossterm", 18 | "all-widgets", 19 | "serde", 20 | "macros", 21 | ], version = "0.26.0" } 22 | tui-input = "0.8.0" 23 | tui-widget-list = "0.8.2" 24 | rusqlite = { version = "0.31.0", features = ["bundled"] } 25 | serde_json = { version = "1.0.118", features = ["std"] } 26 | serde = { version = "1.0.203", features = ["derive"] } 27 | mockito = "1.4.0" 28 | regex = "1.10.5" 29 | curl = { version = "0.4.46", features = ["http2", "ntlm"] } 30 | dirs = "5.0.1" 31 | http = "1.1.0" 32 | toml = "0.8.14" 33 | arboard = "3.4.0" 34 | log = "0.4.21" 35 | clap = "4.5.7" 36 | 37 | [profile.release] 38 | strip = "debuginfo" 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | # TUI HTTP Client with API/Auth Key Management and Request History/Storage 7 | 8 | > This project is still in active development and although it is useable, there may still be bugs and significant changes are still needed to both refactor the codebase and add new features. 9 | #### Collaboration is welcome and encouraged! There is lots of low hanging fruit 👍 and cool ideas for additional features. 10 | ![image](imgs/demo.gif) 11 | 12 | Curl TUI Environment (CuTE). HTTP client/libcurl front-end in Rust, using the awesome [ratatui](https://github.com/ratatui-org/ratatui) library designed to simplify the process of sending HTTP requests in the terminal, allowing you to store and manage your previous requests + API keys. 13 | 14 | This is a tool for when your requests are not complex enough for something like Postman, but more complicated than you would want to use `curl` CLI, or if you just don't want to remember all those commands. You can make a few requests to your back-ends for testing, set up with your API key and save the requests to be executed again later. 15 | 16 | **New**: We now support importing your `Postman` collections, so you can have easy access to your APIs without leaving the terminal. (note: `https://schema.getpostman.com/json/collection/v2.1.0/` is currently the only supported collection schema) 17 | 18 | 19 | 20 | 21 | ## Features 22 | 23 | - **Interactive TUI Interface**: Intuitive TUI interface that makes it fast and easy to construct and execute HTTP requests without leaving the terminal. 24 | 25 | - **Intuitive VIM keybindings:** Vim-like keybindings are _defaulted_. Support to change them will eventually make it into the config file. 26 | (`h` or `b` is used to go back a page, `j` and `k` move the cursor up and down. `i` for insert mode while in an input box, `enter` to submit the form and `esc` to exit insert mode) 27 | 28 | - **API Key Management**: Very simple sqlite based API key storage system. You can choose to save a Key from a request, or add/edit/delete/rename them. 29 | 30 | - **Postman Collections**: Import your postman collections to have easy access to your APIs without leaving the terminal. 31 | 32 | - **Response Visualization**: Pretty-print JSON responses in a human-readable format within the TUI, then you can choose to write the response to a file after inspecting it. You an also copy the `curl` CLI command needed to make the same request to your clipboard. 33 | 34 | - This application builds and runs on Linux, Windows and MacOS. 35 | 36 | ## Why? 37 | 38 | - Have __you__ even ran `curl --help all` ? 39 | 40 | - I made this because I needed it. As a back-end dev that is testing API's daily, Postman is great but I have enough electron apps running as it is, and I live in the terminal. 41 | 42 | 43 | ## Installation 44 | 45 | #### Prebuilt binaries for non `smelly-nerds` are available on the [Releases](https://github.com/PThorpe92/CuTE/tags) page (currently just x86-linux) 46 | 47 | ### Install with Cargo: 48 | 49 | - **Prerequisites**: Make sure you have Rust and Cargo installed on your system. 50 | 51 | 1. `cargo install CuTE-tui` 52 | 53 | 2. make sure that your `~/.cargo/bin` directory is in your PATH 54 | 55 | 3. `cute` or `cute --dump-config .` # this will put a config.toml file in your cwd. You can edit this and place it 56 | in a dir in the `~/.config/CuTE` path (see below) to customize the colors and some behavior of the application. 57 | 58 | 59 | ### Build from source: 60 | 1. **Prerequisites**: Make sure you have Rust and Cargo installed on your system. 61 | 62 | 2. **Clone the Repository**: Clone this repository to your local machine using the following command: 63 | ``` 64 | git clone https://github.com/PThorpe92/CuTE.git && cd CuTE 65 | ``` 66 | 67 | 3. **Build and Run**: Build and run the application using Cargo: 68 | ``` 69 | cargo build --release 70 | ``` 71 | 4. **Move Binary**: Move the binary to a location in your PATH of your choosing: 72 | ``` 73 | cp target/release/cute ~/.local/bin/ 74 | ``` 75 | 76 | ## Command Line Options 77 | 78 | #### cute [OPTIONAL] '--dump-config ' or '--db-path <'/PATH/to/cute.db'>' 79 | 80 | - **--dump-config**: Dumps the default config.toml file to the specified path. If no path is specified, it will output it to the current working directory. 81 | - This `config.toml` file needs to be placed in `~/.config/CuTE/{config.toml}` in order for the application to read it. 82 | - currently the config file can only specify basic colors of the application, and the path to the sqlite database. More options will be added in the future. 83 | 84 | - **--db-path**: Specify the path to the sqlite database. If no path is specified, it will default to `data_local_dir` working directory.(~/.local/share/CuTE/CuTE.db or the windows/macos equivalent) 85 | 86 | #### Menus 87 | 88 | 1. **Main Menu**: The main menu will provide options to create different types of HTTP requests and manage API keys. 89 | 90 | 2. **Request Type**: Select the type of HTTP request you would like to make. The tool supports GET, POST, PUT, PATCH, HEAD, DELETE and custom requests. 91 | 92 | 3. **API Key Management**: In the API key management section, you can add, edit, or delete API keys. Assign API keys to profiles and specific requests for easy integration. 93 | 94 | 4. **Viewing Responses**: After executing a request, the tool will display the response in a readable format within the TUI, with the option to write it out to a file. 95 | 96 | 5. **Saved Commands**: Much like the API keys, you can store and view past requests/commands for easy use later on. 97 | 98 | 99 | ## Contributing 100 | 101 | Contributions to this project are welcome and encouraged! If you encounter any bugs, have suggestions for improvements, or want to add a new feature, feel free to open an issue or submit a PR. 102 | 103 | Before contributing, please review the [Contribution Guidelines](CONTRIBUTING.md). 104 | 105 | 106 | ## License 107 | 108 | This project is licensed under the [GPL3.0 License](LICENSE). 109 | 110 | --- 111 | If you have any questions or need assistance, feel free to [reach out](p@eza.rocks) 112 | 113 | 114 | ## **Fun fact:** 115 | 116 | >This project was developed in the Maine State Prison system, where the author is currently incarcerated. I would like to bring whatever awareness possible to the importance of education and rehabilitation for those 2.2 million Americans currently incarcerated. I have a [blog post](https://pthorpe92.github.io/intro/my-story/) if you are interested in reading about my story. 117 | 118 | 119 | **Disclaimer:** This project is provided as-is, and its creators are not responsible for any misuse or potential security vulnerabilities resulting from the usage of API keys. 120 | -------------------------------------------------------------------------------- /imgs/api_keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PThorpe92/CuTE/d61ec507b3fb97b3b32ad343747419744d0b2abd/imgs/api_keys.png -------------------------------------------------------------------------------- /imgs/cute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PThorpe92/CuTE/d61ec507b3fb97b3b32ad343747419744d0b2abd/imgs/cute.png -------------------------------------------------------------------------------- /imgs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PThorpe92/CuTE/d61ec507b3fb97b3b32ad343747419744d0b2abd/imgs/demo.gif -------------------------------------------------------------------------------- /imgs/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PThorpe92/CuTE/d61ec507b3fb97b3b32ad343747419744d0b2abd/imgs/logo.gif -------------------------------------------------------------------------------- /imgs/saved_request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PThorpe92/CuTE/d61ec507b3fb97b3b32ad343747419744d0b2abd/imgs/saved_request.png -------------------------------------------------------------------------------- /imgs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PThorpe92/CuTE/d61ec507b3fb97b3b32ad343747419744d0b2abd/imgs/screenshot.png -------------------------------------------------------------------------------- /src/database/db.rs: -------------------------------------------------------------------------------- 1 | use dirs::data_local_dir; 2 | use rusqlite::{params, Connection, OpenFlags, Result}; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json; 5 | use std::env; 6 | use std::str::FromStr; 7 | use std::{ 8 | fmt::{Display, Formatter}, 9 | path::PathBuf, 10 | }; 11 | 12 | #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] 13 | pub struct SavedCommand { 14 | pub id: i32, 15 | command: String, 16 | pub description: Option, 17 | pub label: Option, 18 | curl_json: String, 19 | pub collection_id: Option, 20 | pub collection_name: Option, 21 | } 22 | 23 | #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] 24 | pub struct SavedCollection { 25 | id: i32, 26 | pub name: String, 27 | pub description: Option, 28 | } 29 | 30 | impl Display for SavedCollection { 31 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 32 | write!(f, "{}", self.name) 33 | } 34 | } 35 | 36 | #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] 37 | pub struct SavedKey { 38 | id: i32, 39 | label: Option, 40 | key: String, 41 | } 42 | 43 | #[derive(Debug)] 44 | pub struct DB { 45 | conn: Connection, 46 | } 47 | 48 | impl DB { 49 | pub fn new_test() -> Result { 50 | let conn = Connection::open_in_memory()?; 51 | conn.execute( 52 | "CREATE TABLE commands (id INTEGER PRIMARY KEY, command TEXT, label TEXT, description TEXT, curl_json TEXT, collection_id INT);", 53 | params![], 54 | )?; 55 | conn.execute( 56 | "CREATE TABLE keys (id INTEGER PRIMARY KEY, key TEXT, label TEXT);", 57 | params![], 58 | )?; 59 | conn.execute( 60 | "CREATE TABLE collections (id INTEGER PRIMARY KEY, name TEXT, description TEXT);", 61 | params![], 62 | )?; 63 | Ok(DB { conn }) 64 | } 65 | 66 | pub fn new() -> Result { 67 | let mut _path: PathBuf = PathBuf::new(); 68 | if std::env::var("CUTE_DB_PATH").is_ok() { 69 | _path = PathBuf::from_str(env::var("CUTE_DB_PATH").unwrap().as_str()).unwrap(); 70 | } else { 71 | _path = DB::get_default_path(); 72 | } 73 | if !_path.exists() { 74 | // If it doesn't exist, create it 75 | if let Err(err) = std::fs::create_dir_all(&_path) { 76 | std::fs::File::create(&_path).expect("failed to create database"); 77 | eprintln!("Failed to create CuTE directory: {}", err); 78 | } else { 79 | println!("CuTE directory created at {:?}", _path); 80 | } 81 | } 82 | 83 | let conn_result = Connection::open_with_flags( 84 | _path.join("CuTE.db"), 85 | OpenFlags::SQLITE_OPEN_READ_WRITE 86 | | OpenFlags::SQLITE_OPEN_CREATE 87 | | OpenFlags::SQLITE_OPEN_URI 88 | | OpenFlags::SQLITE_OPEN_NO_MUTEX, 89 | ); 90 | 91 | let conn = match conn_result { 92 | Ok(connection) => connection, 93 | Err(e) => { 94 | println!("CuTE Database Error: {:?}", e); 95 | return Err(e); 96 | } 97 | }; 98 | 99 | // Begin a transaction 100 | conn.execute("BEGIN;", params![])?; 101 | // collection_id needs to be nullable 102 | conn.execute( 103 | "CREATE TABLE IF NOT EXISTS commands (id INTEGER PRIMARY KEY, label TEXT, description TEXT, command TEXT, curl_json TEXT, collection_id INT);", 104 | params![], 105 | )?; 106 | 107 | conn.execute( 108 | "CREATE TABLE IF NOT EXISTS keys (id INTEGER PRIMARY KEY, key TEXT, label TEXT);", 109 | params![], 110 | )?; 111 | 112 | conn.execute( 113 | "CREATE TABLE IF NOT EXISTS collections (id INTEGER PRIMARY KEY, name TEXT, description TEXT);", 114 | params![], 115 | )?; 116 | 117 | conn.execute("COMMIT;", params![])?; 118 | 119 | Ok(DB { conn }) 120 | } 121 | 122 | pub fn rename_collection(&self, id: i32, name: &str) -> Result<(), rusqlite::Error> { 123 | let mut stmt = self 124 | .conn 125 | .prepare("UPDATE collections SET name = ? WHERE id = ?")?; 126 | stmt.execute(params![name, id])?; 127 | Ok(()) 128 | } 129 | 130 | pub fn set_collection_description( 131 | &self, 132 | id: i32, 133 | description: &str, 134 | ) -> Result<(), rusqlite::Error> { 135 | let mut stmt = self 136 | .conn 137 | .prepare("UPDATE collections SET description = ? WHERE id = ?")?; 138 | stmt.execute(params![description, id])?; 139 | Ok(()) 140 | } 141 | 142 | pub fn get_number_of_commands_in_collection(&self, id: i32) -> Result { 143 | let mut stmt = self 144 | .conn 145 | .prepare("SELECT COUNT(*) FROM commands WHERE collection_id = ?")?; 146 | let count: i32 = stmt.query_row(params![id], |row| row.get(0))?; 147 | Ok(count) 148 | } 149 | 150 | #[rustfmt::skip] 151 | pub fn add_collection(&self, name: &str, desc: &str, commands: &[SavedCommand]) -> Result<(), Box> { 152 | let mut stmt = self 153 | .conn 154 | .prepare("INSERT INTO collections (name, description) VALUES (?1, ?2)")?; 155 | let id = stmt.insert(params![name, desc])?; 156 | for command in commands { 157 | self.add_command_from_collection(&command.command, command.label.as_deref(), command.description.as_deref(), &command.curl_json, id as i32)?; 158 | } 159 | Ok(()) 160 | } 161 | 162 | pub fn get_command_by_id(&self, id: i32) -> Result { 163 | let mut stmt = self.conn.prepare( 164 | "SELECT cmd.id, cmd.command, cmd.label, cmd.description, cmd.curl_json, cmd.collection_id, col.name as collection_name FROM commands cmd LEFT JOIN collections col ON cmd.collection_id = col.id WHERE cmd.id = ?" 165 | )?; 166 | stmt.query_row(params![id], |row| { 167 | Ok(SavedCommand { 168 | id: row.get(0)?, 169 | command: row.get(1)?, 170 | label: row.get(2)?, 171 | description: row.get(3)?, 172 | curl_json: row.get(4)?, 173 | collection_id: row.get(5)?, 174 | collection_name: row.get(6)?, 175 | }) 176 | }) 177 | } 178 | 179 | pub fn get_collection_by_id(&self, id: i32) -> Result { 180 | let mut stmt = self 181 | .conn 182 | .prepare("SELECT id, name, description FROM collections WHERE id = ?") 183 | .map_err(|_| "No Collection".to_string())?; 184 | let collection = stmt 185 | .query_row(params![id], |row| { 186 | Ok(SavedCollection { 187 | id: row.get(0)?, 188 | name: row.get(1)?, 189 | description: row.get(2)?, 190 | }) 191 | }) 192 | .map_err(|_| ("No Collection".to_string()))?; 193 | Ok(collection) 194 | } 195 | 196 | pub fn create_collection(&self, name: &str) -> Result<(), rusqlite::Error> { 197 | let mut stmt = self 198 | .conn 199 | .prepare("INSERT INTO collections (name) VALUES (?1)")?; 200 | stmt.execute(params![name])?; 201 | Ok(()) 202 | } 203 | 204 | pub fn get_collections(&self) -> Result> { 205 | let mut stmt = self 206 | .conn 207 | .prepare("SELECT id, name, description FROM collections")?; 208 | let rows = stmt.query_map(params![], |row| { 209 | Ok(SavedCollection { 210 | id: row.get(0)?, 211 | name: row.get(1)?, 212 | description: row.get(2)?, 213 | }) 214 | })?; 215 | Ok(rows 216 | .into_iter() 217 | .filter_map(|row| row.ok()) 218 | .collect::>()) 219 | } 220 | 221 | pub fn get_default_path() -> PathBuf { 222 | let dir = data_local_dir().expect("Failed to get data local directory,\nPlease specify a path at $CONFIG/CuTE/config.toml\nOr with the --db_path={path/to/CuTE.db}"); 223 | dir.join("CuTE") 224 | } 225 | 226 | pub fn add_command( 227 | &self, 228 | command: &str, 229 | json_str: String, 230 | col_id: Option, 231 | ) -> Result<(), rusqlite::Error> { 232 | let mut stmt = self.conn.prepare( 233 | "INSERT INTO commands (command, curl_json, collection_id) VALUES (?1, ?2, ?3)", 234 | )?; 235 | let _ = stmt.execute(params![command, &json_str, col_id])?; 236 | Ok(()) 237 | } 238 | 239 | pub fn set_command_description( 240 | &self, 241 | id: i32, 242 | description: &str, 243 | ) -> Result, rusqlite::Error> { 244 | let mut stmt = self 245 | .conn 246 | .prepare("UPDATE commands SET description = ?1 WHERE id = ?2")?; 247 | stmt.execute(params![description, id])?; 248 | let mut stmt = self 249 | .conn 250 | .prepare("SELECT collection_id FROM commands WHERE id = ?")?; 251 | let collection_id: Option = stmt.query_row(params![id], |row| row.get(0))?; 252 | Ok(collection_id) 253 | } 254 | 255 | pub fn set_command_label(&self, id: i32, label: &str) -> Result, rusqlite::Error> { 256 | let mut stmt = self 257 | .conn 258 | .prepare("UPDATE commands SET label = ?1 WHERE id = ?2")?; 259 | stmt.execute(params![label, id])?; 260 | let mut stmt = self 261 | .conn 262 | .prepare("SELECT collection_id FROM commands WHERE id = ?")?; 263 | let collection_id: Option = stmt.query_row(params![id], |row| row.get(0))?; 264 | Ok(collection_id) 265 | } 266 | 267 | pub fn add_command_from_collection( 268 | &self, 269 | command: &str, 270 | label: Option<&str>, 271 | desc: Option<&str>, 272 | json_str: &str, 273 | collection_id: i32, 274 | ) -> Result<(), rusqlite::Error> { 275 | let mut stmt = self.conn.prepare( 276 | "INSERT INTO commands (command, label, description, curl_json, collection_id) VALUES (?1, ?2, ?3, ?4, ?5)", 277 | )?; 278 | let _ = stmt.execute(params![command, label, desc, json_str, collection_id])?; 279 | Ok(()) 280 | } 281 | 282 | pub fn delete_command(&self, id: i32) -> Result<(), rusqlite::Error> { 283 | let mut stmt = self.conn.prepare("DELETE FROM commands WHERE id = ?")?; 284 | stmt.execute([id])?; 285 | Ok(()) 286 | } 287 | 288 | pub fn key_exists(&self, key: &str) -> Result { 289 | let mut stmt = self 290 | .conn 291 | .prepare("SELECT COUNT(*) FROM keys WHERE key = ?")?; 292 | let count: i64 = stmt.query_row([&key], |row| row.get(0))?; 293 | Ok(count > 0) 294 | } 295 | 296 | pub fn command_exists(&self, command: &str) -> Result { 297 | let mut stmt = self 298 | .conn 299 | .prepare("SELECT COUNT(*) FROM commands WHERE command = ?")?; 300 | let count: i64 = stmt.query_row([&command], |row| row.get(0))?; 301 | Ok(count > 0) 302 | } 303 | 304 | pub fn get_commands(&self, id: Option) -> Result> { 305 | if let Some(id) = id { 306 | let mut stmt = self 307 | .conn 308 | .prepare("SELECT cmd.id, cmd.command, cmd.label, cmd.description, cmd.curl_json, cmd.collection_id, col.name as collection_name FROM commands cmd LEFT JOIN collections col ON cmd.collection_id = col.id WHERE cmd.collection_id = ?")?; 309 | let rows = stmt.query_map(params![id], |row| { 310 | Ok(SavedCommand { 311 | id: row.get(0)?, 312 | command: row.get(1)?, 313 | label: row.get(2)?, 314 | description: row.get(3)?, 315 | curl_json: row.get(4)?, 316 | collection_id: row.get(5)?, 317 | collection_name: row.get(6)?, 318 | }) 319 | })?; 320 | return Ok(rows.into_iter().filter_map(|row| row.ok()).collect()); 321 | } 322 | let mut stmt = self 323 | .conn 324 | .prepare("SELECT cmd.id, cmd.command, cmd.label, cmd.description, cmd.curl_json, cmd.collection_id, col.name FROM commands cmd LEFT JOIN collections col ON cmd.collection_id = col.id")?; 325 | let rows = stmt.query_map(params![], |row| { 326 | Ok(SavedCommand { 327 | id: row.get(0)?, 328 | command: row.get(1)?, 329 | label: row.get(2)?, 330 | description: row.get(3)?, 331 | curl_json: row.get(4)?, 332 | collection_id: row.get(5)?, 333 | collection_name: row.get(6)?, 334 | }) 335 | })?; 336 | let mut commands = Vec::new(); 337 | rows.for_each(|row| { 338 | commands.push(row.unwrap_or_default()); 339 | }); 340 | Ok(commands) 341 | } 342 | 343 | pub fn delete_collection(&self, id: i32) -> Result<(), rusqlite::Error> { 344 | let mut stmt = self.conn.prepare("DELETE FROM collections WHERE id = ?")?; 345 | stmt.execute([id])?; 346 | let mut stmt = self 347 | .conn 348 | .prepare("DELETE FROM commands WHERE collection_id = ?")?; 349 | stmt.execute([id])?; 350 | Ok(()) 351 | } 352 | 353 | pub fn delete_key(&self, id: i32) -> Result<()> { 354 | let mut stmt = self.conn.prepare("DELETE FROM keys WHERE id = ?")?; 355 | stmt.execute([id])?; 356 | Ok(()) 357 | } 358 | 359 | pub fn add_key(&self, key: &str) -> Result<()> { 360 | if self.key_exists(key).unwrap() { 361 | return Ok(()); 362 | } 363 | let mut stmt = self.conn.prepare("INSERT INTO keys (key) VALUES (?1)")?; 364 | let _ = stmt.execute(params![key])?; 365 | Ok(()) 366 | } 367 | 368 | pub fn set_key_label(&self, key: i32, label: &str) -> Result<()> { 369 | let mut stmt = self 370 | .conn 371 | .prepare("UPDATE keys SET label = ?1 WHERE id = ?2")?; 372 | stmt.execute(params![label, key])?; 373 | Ok(()) 374 | } 375 | 376 | pub fn get_keys(&self) -> Result> { 377 | let mut stmt = self.conn.prepare("SELECT id, key, label FROM keys")?; 378 | let rows = stmt.query_map(params![], |row| { 379 | Ok(SavedKey { 380 | id: row.get(0)?, 381 | key: row.get(1)?, 382 | label: row.get(2)?, 383 | }) 384 | })?; 385 | let mut keys = Vec::new(); 386 | for key in rows { 387 | keys.push(key?); 388 | } 389 | Ok(keys) 390 | } 391 | } 392 | 393 | impl Display for SavedCommand { 394 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 395 | write!(f, "{}", self.command) 396 | } 397 | } 398 | 399 | impl SavedCommand { 400 | // We nned to allow the user to write out the response to a file, 401 | // so at some point we may need to read it back in 402 | pub fn to_json(&self) -> Result { 403 | Ok(serde_json::to_string(&self).expect("Failed to serialize")) 404 | } 405 | 406 | pub fn new( 407 | command: &str, 408 | label: Option, 409 | description: Option, 410 | curl_json: &str, 411 | collection_id: Option, 412 | ) -> Self { 413 | SavedCommand { 414 | command: command.to_string(), 415 | label, 416 | curl_json: curl_json.to_string(), 417 | collection_id, 418 | description, 419 | ..Default::default() 420 | } 421 | } 422 | 423 | pub fn get_id(&self) -> i32 { 424 | self.id 425 | } 426 | 427 | pub fn from_json(json: &str) -> Result { 428 | Ok(serde_json::from_str(json).expect("Failed to deserialize")) 429 | } 430 | 431 | pub fn get_collection_id(&self) -> Option { 432 | self.collection_id 433 | } 434 | 435 | pub fn get_curl_json(&self) -> &str { 436 | &self.curl_json 437 | } 438 | 439 | pub fn get_command(&self) -> &str { 440 | &self.command 441 | } 442 | } 443 | 444 | impl Display for SavedKey { 445 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 446 | if self.label.is_some() { 447 | write!(f, "Label: {:?} |  : {}", self.get_label(), self.key) 448 | } else { 449 | write!(f, " : {}", self.key) 450 | } 451 | } 452 | } 453 | 454 | impl SavedKey { 455 | pub fn new(key: &str) -> Self { 456 | SavedKey { 457 | id: 0, 458 | key: key.to_string(), 459 | label: None, 460 | } 461 | } 462 | 463 | pub fn get_label(&self) -> &str { 464 | match &self.label { 465 | Some(label) => label, 466 | None => "", 467 | } 468 | } 469 | pub fn get_id(&self) -> i32 { 470 | self.id 471 | } 472 | pub fn get_key(&self) -> &str { 473 | &self.key 474 | } 475 | 476 | pub fn is_key(&self, key: &str) -> bool { 477 | self.key == key 478 | } 479 | 480 | pub fn to_json(&self) -> Result { 481 | Ok(serde_json::to_string(&self).expect("Failed to serialize")) 482 | } 483 | 484 | pub fn from_json(json: &str) -> Result { 485 | Ok(serde_json::from_str(json).expect("Failed to deserialize")) 486 | } 487 | } 488 | 489 | impl SavedCollection { 490 | pub fn get_name(&self) -> &str { 491 | &self.name 492 | } 493 | pub fn get_id(&self) -> i32 { 494 | self.id 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /src/database/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db; 2 | pub mod postman; 3 | -------------------------------------------------------------------------------- /src/database/postman.rs: -------------------------------------------------------------------------------- 1 | use super::db::SavedCommand; 2 | use crate::request::curl::{Curl, Method}; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use std::{collections::HashMap, str::FromStr}; 6 | 7 | #[derive(Serialize, Debug, Deserialize)] 8 | pub struct PostmanCollection { 9 | pub info: Info, 10 | pub item: Vec>, 11 | } 12 | 13 | impl From for Vec { 14 | fn from(collection: PostmanCollection) -> Vec { 15 | let mut saved_commands = Vec::new(); 16 | collection.item.iter().for_each(|item| { 17 | let mut curl_cmd = Curl::new_serializing(); 18 | let mut cmd_name: Option = None; 19 | let mut description: Option = None; 20 | if let Some(name) = item.get("name") { 21 | if let Some(name) = name.as_str() { 22 | cmd_name = Some(name.to_string()); 23 | } 24 | } 25 | if let Some(request) = item.get("request") { 26 | if let Some(request) = request.as_str() { 27 | // this means its a get request 28 | curl_cmd.set_url(request); 29 | curl_cmd.set_get_method(); 30 | } else if let Some(request) = request.as_object() { 31 | if let Some(desc) = request.get("description") { 32 | if let Some(desc) = desc.as_str() { 33 | description = Some(desc.to_string()); 34 | } 35 | } 36 | if let Some(url) = request.get("url") { 37 | if let Some(str_url) = url.as_str() { 38 | curl_cmd.set_url(str_url); 39 | } else if let Some(url) = url.as_object() { 40 | if let Some(raw) = url.get("raw") { 41 | if let Some(raw_str) = raw.as_str() { 42 | curl_cmd.set_url(raw_str); 43 | } 44 | } 45 | } 46 | } 47 | if let Some(method) = request.get("method") { 48 | if let Some(method) = method.as_str() { 49 | curl_cmd.set_method(Method::from_str(method).unwrap_or_default()); 50 | } 51 | } 52 | if let Some(headers) = request.get("header") { 53 | if let Some(headers) = headers.as_array() { 54 | headers.iter().for_each(|hdr| { 55 | if let Some(hdr) = hdr.as_object() { 56 | if let Some(key) = hdr.get("key") { 57 | if let Some(key) = key.as_str() { 58 | if let Some(value) = hdr.get("value") { 59 | if let Some(value) = value.as_str() { 60 | curl_cmd.add_headers(&format!( 61 | "{}: {}", 62 | key, value 63 | )); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | }); 70 | } 71 | } 72 | if let Some(body) = request.get("body") { 73 | if let Some(body) = body.as_object() { 74 | if let Some(mode) = body.get("mode") { 75 | if let Some(mode) = mode.as_str() { 76 | match mode { 77 | "formdata" => { 78 | if let Some(data) = body.get("formdata") { 79 | if let Some(data) = data.as_array() { 80 | let mut form_data = Vec::new(); 81 | data.iter().for_each(|d| { 82 | if let Some(d) = d.as_object() { 83 | if let Some(key) = d.get("key") { 84 | if let Some(key) = key.as_str() { 85 | if let Some(value) = 86 | d.get("value") 87 | { 88 | if let Some(value) = 89 | value.as_str() 90 | { 91 | form_data 92 | .push((key, value)); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | }); 99 | curl_cmd.set_request_body( 100 | &serde_json::to_string(&form_data) 101 | .unwrap_or_default(), 102 | ); 103 | } 104 | } 105 | } 106 | "urlencoded" => { 107 | if let Some(data) = body.get("urlencoded") { 108 | if let Some(data) = data.as_array() { 109 | data.iter().for_each(|d| { 110 | if let Some(d) = d.as_object() { 111 | if let Some(key) = d.get("key") { 112 | if let Some(key) = key.as_str() { 113 | if let Some(value) = 114 | d.get("value") 115 | { 116 | if let Some(value) = 117 | value.as_str() 118 | { 119 | curl_cmd.url_encode( 120 | &format!( 121 | "{}={}", 122 | key, value 123 | ), 124 | ); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | }); 131 | } 132 | } 133 | } 134 | "raw" => { 135 | if let Some(data) = body.get("raw") { 136 | if let Some(data) = data.as_str() { 137 | curl_cmd.set_request_body(data); 138 | } 139 | } 140 | } 141 | _ => {} 142 | } 143 | } 144 | } 145 | } 146 | } 147 | if let Some(cookie) = request.get("cookie") { 148 | if let Some(cookie) = cookie.as_array() { 149 | cookie.iter().for_each(|ck| { 150 | if let Some(ck) = ck.as_object() { 151 | if let Some(key) = ck.get("key") { 152 | if let Some(key) = key.as_str() { 153 | if let Some(value) = ck.get("value") { 154 | if let Some(value) = value.as_str() { 155 | curl_cmd 156 | .add_cookie(&format!("{}: {}", key, value)); 157 | } 158 | } 159 | } 160 | } 161 | } 162 | }); 163 | } 164 | } 165 | } 166 | } 167 | if cmd_name.is_none() { 168 | cmd_name = Some(String::from(curl_cmd.get_url())); 169 | } 170 | let cmd = curl_cmd.get_command_string(); 171 | let curl_json: String = serde_json::to_string(&curl_cmd).unwrap_or_default(); 172 | saved_commands.push(SavedCommand::new( 173 | &cmd, 174 | cmd_name, 175 | description, 176 | &curl_json, 177 | None, 178 | )); 179 | }); 180 | 181 | saved_commands 182 | } 183 | } 184 | 185 | #[derive(Serialize, Debug, Deserialize)] 186 | pub struct Info { 187 | pub name: String, 188 | schema: String, 189 | pub description: String, 190 | } 191 | -------------------------------------------------------------------------------- /src/display/inputopt.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | request::curl::{AuthKind, Method}, 3 | screens::Screen, 4 | }; 5 | use std::fmt::Display; 6 | 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub enum InputOpt { 9 | URL, 10 | UploadFile, 11 | Headers, 12 | Output, 13 | Verbose, 14 | RequestBody, 15 | Auth(AuthKind), 16 | VerifyPeer, 17 | Referrer, 18 | Execute, 19 | ApiKey, 20 | UnixSocket, 21 | UserAgent, 22 | MaxRedirects, 23 | CookiePath, 24 | NewCookie, 25 | CookieJar, 26 | CookieValue(String), // store the name 27 | CookieExpires(String), // store the rest 28 | FtpAccount, 29 | CaPath, 30 | CaCert, 31 | KeyLabel(i32), 32 | CmdLabel(i32), 33 | CmdDescription(i32), 34 | CollectionDescription(i32), 35 | ImportCollection, 36 | RenameCollection(i32), 37 | RequestError(String), 38 | AlertMessage(String), 39 | Method(Method), 40 | } 41 | 42 | impl InputOpt { 43 | pub fn get_return_screen(&self) -> Screen { 44 | match self { 45 | InputOpt::KeyLabel(_) => Screen::SavedKeys(None), 46 | InputOpt::CmdLabel(id) => Screen::SavedCommands { 47 | id: Some(*id), 48 | opt: None, 49 | }, 50 | InputOpt::CaPath => Screen::RequestMenu(None), 51 | InputOpt::CaCert => Screen::RequestMenu(None), 52 | InputOpt::CookiePath => Screen::RequestMenu(None), 53 | InputOpt::CookieJar => Screen::RequestMenu(None), 54 | InputOpt::CookieExpires(_) => Screen::RequestMenu(None), 55 | InputOpt::CmdDescription(id) => Screen::SavedCommands { 56 | id: Some(*id), 57 | opt: None, 58 | }, 59 | InputOpt::ApiKey => Screen::SavedKeys(None), 60 | InputOpt::UnixSocket => Screen::RequestMenu(None), 61 | InputOpt::UserAgent => Screen::RequestMenu(None), 62 | InputOpt::MaxRedirects => Screen::RequestMenu(None), 63 | InputOpt::NewCookie => Screen::RequestMenu(None), 64 | InputOpt::Referrer => Screen::RequestMenu(None), 65 | InputOpt::FtpAccount => Screen::RequestMenu(None), 66 | InputOpt::VerifyPeer => Screen::RequestMenu(None), 67 | InputOpt::Method(_) => Screen::RequestMenu(None), 68 | InputOpt::RequestError(_) => Screen::RequestMenu(None), 69 | InputOpt::AlertMessage(_) => Screen::RequestMenu(None), 70 | InputOpt::ImportCollection => Screen::SavedCollections(None), 71 | InputOpt::RenameCollection(_) => Screen::SavedCollections(None), 72 | InputOpt::Execute => Screen::Response(String::new()), 73 | InputOpt::CollectionDescription(_) => Screen::SavedCollections(None), 74 | InputOpt::URL => Screen::RequestMenu(None), 75 | InputOpt::UploadFile => Screen::RequestMenu(None), 76 | InputOpt::Headers => Screen::Headers, 77 | InputOpt::Output => Screen::Response(String::new()), 78 | InputOpt::Verbose => Screen::RequestMenu(None), 79 | InputOpt::RequestBody => Screen::RequestMenu(None), 80 | InputOpt::Auth(_) => Screen::RequestMenu(None), 81 | InputOpt::CookieValue(_) => Screen::RequestMenu(None), 82 | } 83 | } 84 | pub fn is_error(&self) -> bool { 85 | matches!(self, InputOpt::RequestError(_) | InputOpt::AlertMessage(_)) 86 | } 87 | } 88 | 89 | impl Display for InputOpt { 90 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 91 | match self { 92 | InputOpt::URL => write!(f, "| URL"), 93 | InputOpt::Headers => write!(f, "| Headers"), 94 | InputOpt::Output => write!(f, "| Output"), 95 | InputOpt::Referrer => write!(f, "| Referrer"), 96 | InputOpt::UploadFile => write!(f, "| Upload File"), 97 | InputOpt::Verbose => write!(f, "| Verbose"), 98 | InputOpt::RequestBody => write!(f, "| Request Body"), 99 | InputOpt::Auth(auth) => write!(f, "|- Authentication: {}", auth), 100 | InputOpt::Execute => write!(f, "| Execute"), 101 | InputOpt::ApiKey => write!(f, "| API Key"), 102 | InputOpt::UnixSocket => write!(f, "| Unix Socket"), 103 | InputOpt::UserAgent => write!(f, "| User Agent"), 104 | InputOpt::MaxRedirects => write!(f, "| Max Redirects"), 105 | InputOpt::NewCookie => write!(f, "| Cookie"), 106 | InputOpt::CookiePath => write!(f, "| Cookie"), 107 | InputOpt::CookieValue(_) => write!(f, "| Cookie Val"), 108 | InputOpt::CookieExpires(_) => write!(f, "| Cookie Expires"), 109 | InputOpt::CaPath => write!(f, "| Ca Path"), 110 | InputOpt::CaCert => write!(f, "| Ca Cert"), 111 | InputOpt::VerifyPeer => write!(f, "| Verify Peer DNS-Over-HTTPS"), 112 | InputOpt::FtpAccount => write!(f, "| FTP Account"), 113 | InputOpt::KeyLabel(_) => write!(f, "| Key Label"), 114 | InputOpt::ImportCollection => write!(f, "| Import Collection"), 115 | InputOpt::RenameCollection(_) => write!(f, "| Rename Collection"), 116 | InputOpt::RequestError(ref err) => write!(f, "| Error: {}", err), 117 | InputOpt::Method(method) => write!(f, "| Method: {}", method), 118 | InputOpt::CookieJar => write!(f, "| Cookie Jar"), 119 | InputOpt::AlertMessage(msg) => write!(f, "| Alert: {}", msg), 120 | InputOpt::CmdLabel(_) => write!(f, "| Command Label"), 121 | InputOpt::CmdDescription(_) => write!(f, "| Command Description"), 122 | InputOpt::CollectionDescription(_) => write!(f, "| Collection Description"), 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/display/menuopts.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * String literals for Menus/Options 3 | */ 4 | pub const SAVED_COMMANDS_PARAGRAPH: &str = 5 | "\nPress q to exit\nPress Enter to Send Request\nPress 'ESC' or 'h' to go back\n"; 6 | pub const CURL: &str = "curl"; 7 | pub const WGET: &str = "wget"; 8 | pub const CUSTOM: &str = "custom"; 9 | pub const API_KEY_PARAGRAPH: &str = 10 | "Press q to quit\nPress 'ESC' or 'h' to go back\nPress Enter for Menu\n"; 11 | pub const HTTP_REQUEST: &str = "HTTP Request"; 12 | pub const DEFAULT_MENU_PARAGRAPH: &str = 13 | "\nPress q to exit. 'h' to go back \n Press Enter to select\n keybindings to navigate"; 14 | pub const AWS_AUTH_MSG: &str = 15 | "Alert: AWS Signature V4 Authentication is using the following ENV VARs: 16 | \nAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION"; 17 | pub const AWS_AUTH_ERROR_MSG: &str = 18 | "Error: AWS Signature V4 Authentication requires the following ENV VARs: 19 | \nAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION"; 20 | pub const API_KEY_TITLE: &str = "My API Keys"; 21 | pub const METHOD_MENU_TITLE: &str = "** CuTE ** Choose a Method"; 22 | pub const SAVED_COMMANDS_TITLE: &str = "My Saved cURL Commands"; 23 | pub const DEFAULT_MENU_TITLE: &str = "** CuTE **"; 24 | pub const AUTH_MENU_TITLE: &str = "** CuTE ** Authentication Menu 󰌋"; 25 | pub const VIEW_BODY_TITLE: &str = "** CuTE ** View Response Body"; 26 | pub const INPUT_MENU_TITLE: &str = "** Press i to enter Insert mode **"; 27 | pub const ERROR_MENU_TITLE: &str = "* CuTE ** Error! *"; 28 | pub const SUCCESS_MENU_TITLE: &str = "* CuTE ** Success! *"; 29 | pub const POSTMAN_COLLECTION_TITLE: &str = "* CuTE ** Postman Collections"; 30 | pub const SUCCESS_MESSAGE: &str = "Request saved successfully"; 31 | // This padds the choices in the menu. This is the least hideous way to do this.(I think) 32 | pub const OPTION_PADDING_MAX: &str = "\n\n\n\n"; 33 | pub const OPTION_PADDING_MID: &str = "\n\n\n"; 34 | pub const OPTION_PADDING_MIN: &str = "\n\n"; 35 | pub const NEWLINE: &str = "\n"; 36 | // Yeah... if this is normal here, it f**ks up when we try to center it on the screen 37 | #[rustfmt::skip] 38 | pub const CUTE_LOGO: &str = 39 | " . . . . . . . . . . . . . . .p o w .. e r e d. . ..b.y .. 40 |  $#$#$#$#$#$#$#$` $#$#$``| #$#$#$''$#$#$#$#$#$#$#$#$#$#$ '#%#%#%#%#%#%#%##``i ** *&%&* *&%&* 41 | %%%%#``;;;;;;;` %%%%#``| %%%%#``| **`;; %%%%&*+`` **;;| %%%%%%` %%%%%%``b *%%* *&%&* *&%&* 42 | %%%%#``| *. %%%%#``| %%%%#``| ~ ` %%%%$*+` ` . %%%%%%`===#####`` ** *&%&* *&%&* 43 | %%%%#``| ` ___ %%%%#``| %%%%#``| _*_ %%%%$*+` -*- %%%%%%%%%%####`` *&%&* *&%&* 44 | %%%%#``````%%%```%%%%#`/;; %%%%#```| %%%%$*+` | %%%%%%` _____`c ** *&%&* *&%&* 45 | _*_ %%%%%%%%%%%%%%``|%%%%#=====%%%%#$`| %%%%&*+``* %%%%%%``` %%%%%#`u *%%* *&%&* *&%&* 46 | * %%%%%%%%%%%%%%`/; %%%%%%%%%%%%%%%%/ *%%%%%%**` %%%%%%$####%%%%%``r ** *&%&* *&%&* 47 | ***************l...**********$ **`. . . .***.. . . . .****************'.l *&%&* *&%&* 48 | "; 49 | #[rustfmt::skip] 50 | pub const CUTE_LOGO2: &str = " 51 | @@@@@@@. @@@ @@@ @@@@@@@ @@@@@@@@ 52 | @@@@@@@@. @@@ @@@ @@@@@@@ @@@@@@@@ 53 | !@@ ```` @@! @@@ @@! @@! 54 | !@!' !@! @!@ !@! !@! 55 | !@!' @!@ !@! @!! @!@!%! 56 | !!' !@! !!! !!! !!!!!: 57 | :!!' !!: !!! !!: !!: 58 | :!:'.. . :!: !:! :!: :!: 59 | ::: :::' ::::: :: :: :: :::: 60 | :: :: :' : : : : : :: :: 61 | "; 62 | 63 | pub const DISPLAY_OPT_VERBOSE: &str = " Verbose"; 64 | pub const DISPLAY_OPT_COMMAND_SAVED: &str = " Request will be saved  "; 65 | pub const DISPLAY_OPT_HEADERS: &str = " Response headers included 󱈝 "; 66 | pub const DISPLAY_OPT_FAIL_ON_ERROR: &str = " Fail on error  "; 67 | pub const DISPLAY_OPT_TOKEN_SAVED: &str = " Token will be saved  "; 68 | pub const DISPLAY_OPT_FOLLOW_REDIRECTS: &str = " Follow redirects 󱀀 "; 69 | pub const DISPLAY_OPT_UNRESTRICTED_AUTH: &str = "- Send auth to hosts if redirected"; 70 | pub const DISPLAY_OPT_MAX_REDIRECTS: &str = " Max redirects: "; 71 | pub const DISPLAY_OPT_UNIX_SOCKET: &str = " Unix Socket: "; 72 | pub const DISPLAY_OPT_CA_PATH: &str = " 󰄤 SSL Certificate path: "; 73 | pub const DISPLAY_OPT_AUTH: &str = " Auth: "; 74 | pub const DISPLAY_OPT_MATCH_WILDCARD: &str = " Match glob wildcard 󰛄 "; 75 | pub const DISPLAY_OPT_CERT_INFO: &str = " Request certificate info 󰄤 "; 76 | pub const DISPLAY_OPT_BODY: &str = " Request Body: "; 77 | pub const DISPLAY_OPT_UPLOAD: &str = " Upload file: "; 78 | pub const DISPLAY_OPT_REQUEST_BODY: &str = " Request Body"; 79 | pub const DISPLAY_OPT_TCP_KEEPALIVE: &str = " Enable TCP keepalive 󰗶 "; 80 | pub const DISPLAY_OPT_MAX_REC: &str = " Specify recursive depth: "; 81 | pub const DISPLAY_OPT_OUTFILE: &str = " Specify output filepath: "; 82 | pub const DISPLAY_OPT_REFERRER: &str = " Specify Referrer: "; 83 | pub const DISPLAY_OPT_COOKIE: &str = " Cookie Path: "; 84 | pub const DISPLAY_OPT_COOKIE_JAR: &str = " Cookie Jar: "; 85 | pub const DISPLAY_OPT_USERAGENT: &str = " Specify User-Agent: "; 86 | pub const DISPLAY_OPT_PROXY_TUNNEL: &str = " Enable HTTP Proxy-Tunnel 󱠾 "; 87 | pub const DISPLAY_OPT_URL: &str = " Request URL: "; 88 | pub const DISPLAY_OPT_CONTENT_HEADERS: &str = " Headers: "; 89 | pub const UPLOAD_FILEPATH_ERROR: &str = 90 | "Error: Invalid file path. Please enter an absolute path or a valid relative path."; 91 | pub const SOCKET_ERROR: &str = 92 | "Error: Invalid socket file path. Please use an absolute path or a valid relative path."; 93 | pub const PARSE_INT_ERROR: &str = "Error: Please enter a valid integer."; 94 | pub const CERT_ERROR: &str = 95 | "Error: Invalid certificate file path. Please use an absolute path or a valid relative path."; 96 | pub const HEADER_ERROR: &str = "Error: Invalid header. Please use the format \"Key:Value\"."; 97 | pub const SAVE_AUTH_ERROR: &str = 98 | "Error: You must have selected Authentication in order to save your token"; 99 | pub const VALID_COMMAND_ERROR: &str = 100 | "Error: Invalid command.\n You must add either a URL or Unix Socket to execute a command"; 101 | 102 | pub const CMD_MENU_OPTIONS: [&str; 6] = [ 103 | "Execute  ", 104 | "Add a label 󰈮 ", 105 | "Add a description 󰈮 ", 106 | "Delete  ", 107 | "Copy CLI command to clipboard 󰅎 ", 108 | "Cancel  ", 109 | ]; 110 | pub const KEY_MENU_OPTIONS: [&str; 4] = [ 111 | "Add a Label  ", 112 | "Delete  ", 113 | "Copy to Clipboard 󰅎 ", 114 | "Cancel  ", 115 | ]; 116 | pub const COLLECTION_MENU_OPTIONS: [&str; 3] = [ 117 | "Import New Postman Collection 󰖟 ", 118 | "View Collections 󱂛 ", 119 | "Cancel  ", 120 | ]; 121 | pub const ALERT_MENU_OPTIONS_KEY: [&str; 3] = 122 | ["Delete", "Copy Curl command to Clipboard", "Cancel"]; 123 | pub const MAIN_MENU_OPTIONS: [&str; 4] = [ 124 | "Build and send an HTTP request 󰖟 ", 125 | "View saved requests  ", 126 | "View or Import Postman Collections", 127 | "View Saved API keys 󱂛  ", 128 | ]; 129 | pub const COLLECTION_ALERT_MENU_OPTS: [&str; 5] = [ 130 | "View Requests in this collection", 131 | "Add a description", 132 | "Rename this collection", 133 | "Delete this collection", 134 | "Cancel", 135 | ]; 136 | pub const REQUEST_MENU_OPTIONS: [&str; 12] = [ 137 | "Add a URL 󰖟 ", 138 | "Add a file for uploads  ", 139 | "Cookie options 󰆘 ", 140 | "Authentication 󰯄 ", 141 | "Header Options  ", 142 | "Enable verbose output [-v]", 143 | "Add Request Body 󰘦 ", 144 | "Save this Request  ", 145 | "Save your API token or login information  ", 146 | "Send Request  ", 147 | "More Options  ", 148 | "Clear all options  ", 149 | ]; 150 | 151 | pub const COOKIE_MENU_OPTIONS: [&str; 5] = [ 152 | "Set Cookie file path (Use Cookies) 󰆘 ", 153 | "Set Cookie-Jar path (Storage) 󰆘 ", 154 | "Add New Cookie 󰆘 ", 155 | "Reset Cookie Session 󰆘 ", 156 | "Go back  ", 157 | ]; 158 | 159 | pub const METHOD_MENU_OPTIONS: [&str; 7] = [ 160 | "OTHER (custom command)", 161 | "GET", 162 | "POST", 163 | "PUT", 164 | "DELETE", 165 | "PATCH", 166 | "HEAD", 167 | ]; 168 | pub const HEADER_MENU_OPTIONS: [&str; 9] = [ 169 | "Add Custom Header 󰖟 ", 170 | "Add Content-Type: application/json  ", 171 | "Add Content-Type: application/xml  ", 172 | "Add Content-Type: application/X-WWW-Form-Urlencoded  ", 173 | "Add Accept: application/json  ", 174 | "Add Accept: text/html  ", 175 | "Add Accept: application/xml  ", 176 | "Enable Response Headers 󰰀 ", 177 | "Return to request menu  ", 178 | ]; 179 | pub const AUTHENTICATION_MENU_OPTIONS: [&str; 6] = [ 180 | "Basic", 181 | "Bearer Token", 182 | "Digest", 183 | "AWS SignatureV4", 184 | "Ntlm", 185 | "SPNEGO", 186 | ]; 187 | pub const MORE_FLAGS_MENU: [&str; 11] = [ 188 | "Follow Redirects 󱀀 ", 189 | "Specify Max redirects 󱀀 ", 190 | "Enable HTTP Proxy-Tunnel 󱠾 ", 191 | "Unrestricted Auth  ", 192 | "Specify Referrer 󰆽 ", 193 | "Specify SSL Certificate path 󰄤 ", 194 | "Request Certificate Info 󰄤 ", 195 | "Fail on Error  ", 196 | "Match wildcard 󰛄 ", 197 | "Specify User-Agent 󰖟 ", 198 | "Enable TCP keepalive 󰗶 ", 199 | ]; 200 | pub const RESPONSE_MENU_OPTIONS: [&str; 5] = [ 201 | "Write to file? 󱇧 ", 202 | "View response headers 󰰀 ", 203 | "View response body 󰈮 ", 204 | "Copy CLI command to clipboard 󰅎 ", 205 | "Return to main menu  ", 206 | ]; 207 | -------------------------------------------------------------------------------- /src/display/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use self::menuopts::{ 4 | DISPLAY_OPT_AUTH, DISPLAY_OPT_BODY, DISPLAY_OPT_CA_PATH, DISPLAY_OPT_CERT_INFO, 5 | DISPLAY_OPT_COMMAND_SAVED, DISPLAY_OPT_CONTENT_HEADERS, DISPLAY_OPT_COOKIE, 6 | DISPLAY_OPT_COOKIE_JAR, DISPLAY_OPT_FAIL_ON_ERROR, DISPLAY_OPT_FOLLOW_REDIRECTS, 7 | DISPLAY_OPT_HEADERS, DISPLAY_OPT_MATCH_WILDCARD, DISPLAY_OPT_MAX_REDIRECTS, 8 | DISPLAY_OPT_OUTFILE, DISPLAY_OPT_PROXY_TUNNEL, DISPLAY_OPT_REFERRER, DISPLAY_OPT_TCP_KEEPALIVE, 9 | DISPLAY_OPT_TOKEN_SAVED, DISPLAY_OPT_UNIX_SOCKET, DISPLAY_OPT_UNRESTRICTED_AUTH, 10 | DISPLAY_OPT_UPLOAD, DISPLAY_OPT_URL, DISPLAY_OPT_USERAGENT, DISPLAY_OPT_VERBOSE, 11 | }; 12 | use crate::request::curl::AuthKind; 13 | use std::fmt::{Display, Formatter}; 14 | 15 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 16 | pub enum HeaderKind { 17 | Accept(String), 18 | ContentType(String), 19 | Allow(String), 20 | Connection(String), 21 | ContentLength(String), 22 | None, 23 | } 24 | impl Display for HeaderKind { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 26 | match self { 27 | HeaderKind::Accept(kind) => write!(f, "Accept: {}", kind), 28 | HeaderKind::ContentType(kind) => write!(f, "Content-Type: {}", kind), 29 | HeaderKind::None => write!(f, ""), 30 | HeaderKind::Connection(con) => write!(f, "Connection: {}", con), 31 | HeaderKind::ContentLength(len) => write!(f, "Content-Length: {}", len), 32 | HeaderKind::Allow(allow) => write!(f, "Allow: {}", allow), 33 | } 34 | } 35 | } 36 | /* 37 | * Display - This is For Structures That Represent Display Items 38 | * Or Are Related To Display Items In Some Way 39 | */ 40 | // Input Options 41 | pub mod inputopt; 42 | 43 | // Menu Options 44 | pub mod menuopts; 45 | 46 | /// Here are the options that require us to display a box letting 47 | /// the user know that they have selected that option. 48 | #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] 49 | pub enum AppOptions { 50 | Verbose, 51 | Headers(String), 52 | URL(String), 53 | Outfile(String), 54 | SaveCommand, 55 | Response(String), 56 | Auth(AuthKind), 57 | SaveToken, 58 | UnixSocket(String), 59 | FollowRedirects, 60 | CookieJar(String), 61 | CookiePath(String), 62 | EnableHeaders, 63 | ContentHeaders(HeaderKind), 64 | FailOnError, 65 | ProxyTunnel, 66 | CaPath(String), 67 | CertInfo, 68 | UserAgent(String), 69 | Referrer(String), 70 | MatchWildcard, 71 | TcpKeepAlive, 72 | UnrestrictedAuth, 73 | MaxRedirects(usize), 74 | UploadFile(String), 75 | NewCookie(String), 76 | RequestBody(String), 77 | NewCookieSession, 78 | } 79 | 80 | impl AppOptions { 81 | pub fn get_curl_flag_value(&self) -> String { 82 | match self { 83 | Self::Verbose => "-v".to_string(), 84 | Self::Headers(ref str) => format!("-H {str}"), 85 | Self::UploadFile(ref file) => format!("-T {file}"), 86 | Self::Outfile(ref file) => format!("-o {file}"), 87 | Self::NewCookie(ref cookie) => format!("--cookie {cookie}"), 88 | Self::CookieJar(ref jar) => format!("--cookie-jar {jar}"), 89 | Self::CookiePath(ref path) => format!("--cookie {path}"), 90 | Self::Referrer(ref referrer) => format!("-e {referrer}"), 91 | Self::CaPath(ref path) => format!("--cacert {path}"), 92 | Self::MaxRedirects(ref size) => format!("--max-redirs {size}"), 93 | Self::UserAgent(ref ua) => format!("-A {ua}"), 94 | Self::RequestBody(ref body) => format!("-d {body}"), 95 | Self::NewCookieSession => "--junk-session-cookies".to_string(), 96 | Self::ProxyTunnel => "--proxy-tunnel".to_string(), 97 | Self::CertInfo => "--certinfo".to_string(), 98 | Self::FollowRedirects => "-L".to_string(), 99 | Self::UnixSocket(ref socket) => format!("--unix-socket {socket}"), 100 | Self::MatchWildcard => "-g".to_string(), 101 | Self::Auth(ref kind) => match kind { 102 | AuthKind::Basic(ref login) => { 103 | format!("-u {login}") 104 | } 105 | AuthKind::Digest(ref login) => { 106 | format!("--digest -u {login}") 107 | } 108 | AuthKind::Ntlm => "--ntlm".to_string(), 109 | AuthKind::Bearer(ref token) => format!("-H 'Authorization: Bearer {token}'"), 110 | AuthKind::AwsSigv4 => "--aws-sigv4".to_string(), 111 | AuthKind::Spnego => "--spnego".to_string(), 112 | AuthKind::None => "".to_string(), 113 | }, 114 | Self::ContentHeaders(ref kind) => match kind { 115 | HeaderKind::Accept(val) => format!("-H \"Accept: {}\"", val), 116 | HeaderKind::ContentType(val) => format!("-H \"Content-Type: {}\"", val), 117 | HeaderKind::Allow(val) => format!("-H \"Allow: {}\"", val), 118 | HeaderKind::Connection(val) => format!("-H \"Connection: {}\"", val), 119 | HeaderKind::ContentLength(val) => format!("-H \"Content-Length: {}\"", val), 120 | HeaderKind::None => "".to_string(), 121 | }, 122 | Self::UnrestrictedAuth => "--anyauth".to_string(), 123 | _ => "".to_string(), 124 | } 125 | } 126 | pub fn should_toggle(&self) -> bool { 127 | matches!( 128 | self, 129 | Self::Verbose 130 | | Self::EnableHeaders 131 | | Self::FailOnError 132 | | Self::ProxyTunnel 133 | | Self::CertInfo 134 | | Self::FollowRedirects 135 | | Self::MatchWildcard 136 | | Self::TcpKeepAlive 137 | | Self::UnrestrictedAuth 138 | ) 139 | } 140 | pub fn should_append(&self) -> bool { 141 | matches!(self, Self::Headers(_) | Self::NewCookie(_)) 142 | } 143 | pub fn replace_value(&mut self, val: String) { 144 | match self { 145 | AppOptions::ContentHeaders(ref mut kind) => match val.as_str() { 146 | "Accept" => *kind = HeaderKind::Accept(val), 147 | "Content-Type" => *kind = HeaderKind::ContentType(val), 148 | "Allow" => *kind = HeaderKind::Allow(val), 149 | "Connection" => *kind = HeaderKind::Connection(val), 150 | "Content-Length" => *kind = HeaderKind::ContentLength(val), 151 | _ => *kind = HeaderKind::None, 152 | }, 153 | AppOptions::Headers(ref mut key) => { 154 | *key = val; 155 | } 156 | AppOptions::URL(ref mut url) => { 157 | *url = val; 158 | } 159 | AppOptions::Outfile(ref mut outfile) => { 160 | *outfile = val; 161 | } 162 | AppOptions::Response(ref mut response) => { 163 | *response = val; 164 | } 165 | AppOptions::UnixSocket(ref mut socket) => { 166 | *socket = val; 167 | } 168 | AppOptions::CookiePath(ref mut cookie) => { 169 | *cookie = val; 170 | } 171 | AppOptions::CookieJar(ref mut cookie) => { 172 | *cookie = val; 173 | } 174 | AppOptions::Referrer(ref mut referrer) => { 175 | *referrer = val; 176 | } 177 | AppOptions::CaPath(ref mut ca_cert) => { 178 | *ca_cert = val; 179 | } 180 | AppOptions::MaxRedirects(ref mut max_redirects) => { 181 | *max_redirects = val.parse::().unwrap(); 182 | } 183 | AppOptions::UserAgent(ref mut ua) => { 184 | *ua = val; 185 | } 186 | AppOptions::UploadFile(ref mut file) => { 187 | *file = val; 188 | } 189 | AppOptions::RequestBody(ref mut body) => { 190 | *body = val; 191 | } 192 | _ => {} 193 | } 194 | } 195 | 196 | pub fn get_value(&self) -> String { 197 | match self { 198 | AppOptions::Verbose => String::from(DISPLAY_OPT_VERBOSE), 199 | AppOptions::URL(url) => format!("{}{}", DISPLAY_OPT_URL, url.clone()), 200 | AppOptions::Headers(val) => format!("{}{}", DISPLAY_OPT_HEADERS, val), 201 | AppOptions::Outfile(outfile) => format!("{}{}", DISPLAY_OPT_OUTFILE, outfile.clone()), 202 | AppOptions::SaveCommand => String::from(DISPLAY_OPT_COMMAND_SAVED), 203 | AppOptions::Response(response) => String::from(response), 204 | AppOptions::Auth(auth) => format!("{}{}", DISPLAY_OPT_AUTH, auth.clone()), 205 | AppOptions::SaveToken => String::from(DISPLAY_OPT_TOKEN_SAVED), 206 | AppOptions::UnixSocket(socket) => { 207 | format!("{}{}", DISPLAY_OPT_UNIX_SOCKET, socket.clone()) 208 | } 209 | AppOptions::NewCookie(cookie) => format!("{}{}", DISPLAY_OPT_COOKIE, cookie.clone()), 210 | AppOptions::EnableHeaders => DISPLAY_OPT_HEADERS.to_string(), 211 | AppOptions::FailOnError => String::from(DISPLAY_OPT_FAIL_ON_ERROR), 212 | AppOptions::ProxyTunnel => DISPLAY_OPT_PROXY_TUNNEL.to_string(), 213 | AppOptions::UserAgent(ua) => format!("{}{}", DISPLAY_OPT_USERAGENT, ua), 214 | AppOptions::MaxRedirects(max_redirects) => { 215 | format!("{}{}", DISPLAY_OPT_MAX_REDIRECTS, max_redirects) 216 | } 217 | AppOptions::NewCookieSession => String::from("New Cookie Session"), 218 | AppOptions::CookiePath(cookie) => format!("{}{}", DISPLAY_OPT_COOKIE, cookie.clone()), 219 | AppOptions::CookieJar(cookie) => { 220 | format!("{}{}", DISPLAY_OPT_COOKIE_JAR, cookie.clone()) 221 | } 222 | AppOptions::Referrer(referrer) => { 223 | format!("{}{}", DISPLAY_OPT_REFERRER, referrer.clone()) 224 | } 225 | AppOptions::CaPath(path) => format!("{}{}", DISPLAY_OPT_CA_PATH, path.clone()), 226 | AppOptions::CertInfo => DISPLAY_OPT_CERT_INFO.to_string(), 227 | AppOptions::FollowRedirects => DISPLAY_OPT_FOLLOW_REDIRECTS.to_string(), 228 | AppOptions::MatchWildcard => DISPLAY_OPT_MATCH_WILDCARD.to_string(), 229 | AppOptions::TcpKeepAlive => DISPLAY_OPT_TCP_KEEPALIVE.to_string(), 230 | AppOptions::UnrestrictedAuth => format!("{}{}", DISPLAY_OPT_UNRESTRICTED_AUTH, "󰄨"), 231 | AppOptions::UploadFile(file) => format!("{}{}", DISPLAY_OPT_UPLOAD, file.clone()), 232 | AppOptions::RequestBody(body) => format!("{}{}", DISPLAY_OPT_BODY, body.clone()), 233 | AppOptions::ContentHeaders(kind) => format!("{}{}", DISPLAY_OPT_CONTENT_HEADERS, kind), 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/events/event.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc; 2 | use std::thread; 3 | use std::time::{Duration, Instant}; 4 | 5 | use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; 6 | 7 | use crate::app::AppResult; 8 | 9 | /// Terminal events. 10 | #[derive(Clone, Copy, Debug)] 11 | pub enum Event { 12 | Tick, 13 | Key(KeyEvent), 14 | /// Mouse click/scroll. 15 | Mouse(MouseEvent), 16 | /// Terminal resize. 17 | Resize(u16, u16), 18 | } 19 | 20 | /// Terminal event handler. 21 | #[allow(dead_code)] 22 | #[derive(Debug)] 23 | pub struct EventHandler { 24 | /// Event sender channel. 25 | sender: mpsc::Sender, 26 | /// Event receiver channel. 27 | receiver: mpsc::Receiver, 28 | /// Event handler thread. 29 | handler: thread::JoinHandle<()>, 30 | } 31 | 32 | impl EventHandler { 33 | /// Constructs a new instance of [`EventHandler`]. 34 | pub fn new(tick_rate: u64) -> Self { 35 | let tick_rate = Duration::from_millis(tick_rate); 36 | let (sender, receiver) = mpsc::channel(); 37 | let handler = { 38 | let sender = sender.clone(); 39 | thread::spawn(move || { 40 | let mut last_tick = Instant::now(); 41 | loop { 42 | let timeout = tick_rate 43 | .checked_sub(last_tick.elapsed()) 44 | .unwrap_or(tick_rate); 45 | 46 | if event::poll(timeout).expect("no events available") { 47 | match event::read().expect("unable to read event") { 48 | CrosstermEvent::Key(e) => sender.send(Event::Key(e)), 49 | CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), 50 | CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), 51 | CrosstermEvent::FocusGained => {sender.send(Event::Tick)} 52 | CrosstermEvent::FocusLost => {sender.send(Event::Tick)} 53 | CrosstermEvent::Paste(_s) => sender.send(Event::Tick), 54 | } 55 | .expect("failed to send terminal event") 56 | } 57 | 58 | if last_tick.elapsed() >= tick_rate { 59 | sender.send(Event::Tick).expect("failed to send tick event"); 60 | last_tick = Instant::now(); 61 | } 62 | } 63 | }) 64 | }; 65 | Self { 66 | sender, 67 | receiver, 68 | handler, 69 | } 70 | } 71 | 72 | /// Receive the next event from the handler thread. 73 | /// 74 | /// This function will always block the current thread if 75 | /// there is no data available and it's possible for more data to be sent. 76 | pub fn next(&self) -> AppResult { 77 | Ok(self.receiver.recv()?) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/events/handler.rs: -------------------------------------------------------------------------------- 1 | use arboard::Clipboard; 2 | use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 3 | use tui_input::InputRequest; 4 | 5 | use crate::app::InputMode; 6 | use crate::app::{App, AppResult}; 7 | use crate::display::inputopt::InputOpt; 8 | use crate::screens::screen::Screen; 9 | 10 | /// Handles the key events and updates the state of [`App`]. 11 | pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> { 12 | match app.input_mode { 13 | InputMode::Normal => { 14 | match key_event.kind { 15 | KeyEventKind::Press => { 16 | match key_event.code { 17 | KeyCode::Char('q') => { 18 | app.quit(); 19 | } 20 | // Exit application on `Ctrl-C` 21 | KeyCode::Char('c') | KeyCode::Char('C') => { 22 | if key_event.modifiers == KeyModifiers::CONTROL { 23 | app.quit(); 24 | } 25 | } 26 | KeyCode::Esc => { 27 | app.go_back_screen(); // Escape Should Bring You Back 28 | if !app.input.value().is_empty() { 29 | app.input.reset(); // If we leave the page, we should clear the input buffer 30 | } 31 | } 32 | KeyCode::Up => { 33 | app.move_cursor_up(); 34 | } 35 | KeyCode::Down => { 36 | app.move_cursor_down(); 37 | } 38 | KeyCode::Enter => { 39 | app.select_item(); 40 | } 41 | KeyCode::Char('a') => { 42 | if let Screen::SavedKeys(_) = app.current_screen { 43 | app.goto_screen(&Screen::SavedKeys(Some(InputOpt::ApiKey))) 44 | } 45 | } 46 | KeyCode::Char('i') => match &app.current_screen { 47 | screen if screen.is_input_screen() => { 48 | app.input_mode = InputMode::Editing; 49 | } 50 | _ => {} 51 | }, 52 | KeyCode::Char('j') => { 53 | app.move_cursor_down(); 54 | } 55 | KeyCode::Char('k') => { 56 | app.move_cursor_up(); 57 | } 58 | KeyCode::Char('h') => { 59 | // if we are going back to method scree, clear everything 60 | app.go_back_screen(); 61 | } 62 | KeyCode::Char('b') => { 63 | app.go_back_screen(); 64 | } 65 | _ => {} 66 | } 67 | } 68 | KeyEventKind::Release => { 69 | // Release Key Event Bindings 70 | } 71 | KeyEventKind::Repeat => { 72 | // Repeat Key Event Bindings 73 | } 74 | } 75 | } 76 | InputMode::Editing => match key_event.kind { 77 | KeyEventKind::Press => match key_event.code { 78 | KeyCode::Enter => { 79 | app.messages.push(app.input.value().trim().to_string()); 80 | app.input_mode = InputMode::Normal; 81 | app.input.reset(); 82 | } 83 | KeyCode::Char(c) => { 84 | if key_event.modifiers == KeyModifiers::CONTROL && c == 'v' { 85 | if let Ok(s) = Clipboard::new().and_then(|mut ctx| ctx.get_text()) { 86 | for ch in s.trim().chars() { 87 | app.input.handle(InputRequest::InsertChar(ch)); 88 | } 89 | } 90 | } else if app.input.handle(InputRequest::InsertChar(c)).is_some() { 91 | } 92 | } 93 | KeyCode::Backspace => { 94 | if app.input.handle(InputRequest::DeletePrevChar).is_some() {} 95 | } 96 | KeyCode::Delete => if app.input.handle(InputRequest::DeleteNextChar).is_some() {}, 97 | // if ctrl + left or ctrl + right is pressed, move one word at a time in either 98 | // direction respectively 99 | KeyCode::Left => { 100 | if key_event.modifiers == KeyModifiers::CONTROL { 101 | app.input.handle(InputRequest::GoToPrevWord); 102 | } else if app.input.handle(InputRequest::GoToPrevChar).is_some() { 103 | } 104 | } 105 | KeyCode::Right => { 106 | if key_event.modifiers == KeyModifiers::CONTROL { 107 | app.input.handle(InputRequest::GoToNextWord); 108 | } else if app 109 | .input 110 | .handle(tui_input::InputRequest::GoToNextChar) 111 | .is_some() 112 | { 113 | } 114 | } 115 | KeyCode::Esc => { 116 | app.input_mode = InputMode::Normal; 117 | } 118 | _ => {} 119 | }, 120 | KeyEventKind::Release => {} 121 | KeyEventKind::Repeat => {} 122 | }, 123 | } 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /src/events/mod.rs: -------------------------------------------------------------------------------- 1 | /// Terminal events handler. 2 | pub mod event; 3 | /// Event handler. 4 | pub mod handler; 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | use std::{fmt::Display, path::PathBuf}; 3 | 4 | use crate::display::menuopts::CUTE_LOGO; 5 | 6 | use database::db::DB; 7 | use dirs::config_dir; 8 | use serde::{Deserialize, Serialize}; 9 | use tui::style::Style; 10 | 11 | // Application. 12 | pub mod app; 13 | 14 | // Database 15 | pub mod database; 16 | 17 | // Structures And Functions That Represent Screen Data 18 | pub mod screens; 19 | 20 | // Structures That Represent Display Items 21 | pub mod display; 22 | 23 | // Structures And Functions That Represent A CURL or WGET Request 24 | pub mod request; 25 | 26 | // Events & Event Handler 27 | pub mod events; 28 | 29 | pub mod tui_cute; 30 | 31 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 32 | pub struct Config { 33 | colors: Colors, 34 | logo: Option, 35 | db_path: Option, 36 | } 37 | 38 | impl Config { 39 | pub fn get_fg_color(&self) -> tui::style::Color { 40 | self.colors.get_fg() 41 | } 42 | pub fn set_db_path(&mut self, path: PathBuf) { 43 | self.db_path = Some(path); 44 | } 45 | pub fn get_bg_color(&self) -> tui::style::Color { 46 | self.colors.get_bg() 47 | } 48 | pub fn get_body_color(&self) -> tui::style::Color { 49 | self.colors.body.get_value() 50 | } 51 | pub fn get_outline_color(&self) -> tui::style::Color { 52 | self.colors.outline.get_value() 53 | } 54 | pub fn get_logo(&self) -> &str { 55 | if self.logo == Some(Logo::Default) { 56 | CUTE_LOGO 57 | } else { 58 | "" 59 | } 60 | } 61 | 62 | pub fn get_style(&self) -> Style { 63 | Style::default() 64 | .fg(self.get_fg_color()) 65 | .bg(self.get_bg_color()) 66 | } 67 | 68 | pub fn get_style_error(&self) -> Style { 69 | Style::default() 70 | .fg(tui::style::Color::Red) 71 | .bg(self.get_bg_color()) 72 | } 73 | 74 | pub fn get_default_config() -> Self { 75 | Self { 76 | colors: Colors { 77 | fg: ConfigColor::Gray, 78 | bg: ConfigColor::Black, 79 | body: ConfigColor::Yellow, 80 | outline: ConfigColor::Cyan, 81 | }, 82 | logo: Some(Logo::Default), 83 | db_path: Some(DB::get_default_path()), 84 | } 85 | } 86 | 87 | pub fn load() -> Result { 88 | if let Some(config) = config_dir() { 89 | let config = config.join("CuTE").join("config.toml"); 90 | if let Ok(config) = std::fs::read_to_string(config) { 91 | if let Ok(config) = toml::from_str::(&config) { 92 | Ok(config) 93 | } else { 94 | Err("Failed to parse config.toml".to_string()) 95 | } 96 | } else { 97 | Err("Failed to read config.toml".to_string()) 98 | } 99 | } else { 100 | Err("Failed to get config directory".to_string()) 101 | } 102 | } 103 | 104 | pub fn get_db_path(&self) -> Option { 105 | self.db_path.as_ref().cloned() 106 | } 107 | } 108 | impl Default for Config { 109 | fn default() -> Self { 110 | if let Ok(config) = Self::load() { 111 | config 112 | } else { 113 | Self::get_default_config() 114 | } 115 | } 116 | } 117 | 118 | #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] 119 | pub enum Logo { 120 | #[default] 121 | Default, 122 | None, 123 | } 124 | 125 | impl Display for Logo { 126 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 127 | match self { 128 | Logo::Default => write!(f, "{}", CUTE_LOGO), 129 | Logo::None => write!(f, ""), 130 | } 131 | } 132 | } 133 | 134 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 135 | pub struct Colors { 136 | fg: ConfigColor, 137 | bg: ConfigColor, 138 | body: ConfigColor, 139 | outline: ConfigColor, 140 | } 141 | 142 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 143 | enum ConfigColor { 144 | Red, 145 | Blue, 146 | Cyan, 147 | Magenta, 148 | Gray, 149 | Black, 150 | White, 151 | Green, 152 | Yellow, 153 | } 154 | 155 | impl Colors { 156 | pub fn get_fg(&self) -> tui::style::Color { 157 | self.fg.get_value() 158 | } 159 | pub fn get_bg(&self) -> tui::style::Color { 160 | self.bg.get_value() 161 | } 162 | } 163 | 164 | impl ConfigColor { 165 | pub fn get_value(&self) -> tui::style::Color { 166 | match self { 167 | ConfigColor::Red => tui::style::Color::Red, 168 | ConfigColor::Blue => tui::style::Color::Blue, 169 | ConfigColor::Cyan => tui::style::Color::Cyan, 170 | ConfigColor::Magenta => tui::style::Color::Magenta, 171 | ConfigColor::Gray => tui::style::Color::Gray, 172 | ConfigColor::Black => tui::style::Color::Black, 173 | ConfigColor::White => tui::style::Color::White, 174 | ConfigColor::Green => tui::style::Color::Green, 175 | ConfigColor::Yellow => tui::style::Color::Yellow, 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | #![allow(non_snake_case)] 3 | use clap::{builder::Command, Arg}; 4 | use dirs::config_dir; 5 | use std::io; 6 | use std::sync::OnceLock; 7 | use tui::backend::CrosstermBackend; 8 | use tui::Terminal; 9 | use CuTE_tui::app::{App, AppResult}; 10 | use CuTE_tui::events::{ 11 | event::{Event, EventHandler}, 12 | handler::handle_key_events, 13 | }; 14 | use CuTE_tui::{tui_cute::Tui, Config}; 15 | 16 | pub static CONFIG_PATH: OnceLock = OnceLock::new(); 17 | 18 | fn main() -> AppResult<()> { 19 | let mut app = App::new(); 20 | CONFIG_PATH.get_or_init(|| { 21 | config_dir() 22 | .unwrap() 23 | .join("CuTE/config.toml") 24 | .as_os_str() 25 | .to_string_lossy() 26 | .to_string() 27 | }); 28 | app.set_config(parse_cmdline().unwrap_or_default()); 29 | let backend = CrosstermBackend::new(io::stdout()); 30 | let terminal = Terminal::new(backend)?; 31 | let events = EventHandler::new(250); 32 | let mut tui = Tui::new(terminal, events); 33 | tui.init()?; 34 | 35 | while app.running { 36 | tui.draw(&mut app)?; 37 | if let Event::Key(key_event) = tui.events.next()? { 38 | handle_key_events(key_event, &mut app)? 39 | } 40 | } 41 | tui.exit()?; 42 | Ok(()) 43 | } 44 | 45 | fn parse_cmdline() -> Option { 46 | let args = Command::new("CuTE") 47 | .author("PThorpe92 ") 48 | .version("0.0.1") 49 | .about("Simple TUI for sending and storing HTTP requests, API keys and Postman collections") 50 | .after_help("Arguments are '--dump-config {path}' to write the default config file to the specified path, 51 | \nand '--db-path' to define a custom path to the database\nDB path can also be defined in the config file at $CONFIG/CuTE/config.toml\n 52 | or you can set the $CUTE_DB_PATH environment variable") 53 | .arg( 54 | Arg::new("db-path") 55 | .help("Define a custom path to the database") 56 | .id("db-path") 57 | .long("db-path"), // Added this line to indicate it takes a value 58 | ) 59 | .arg( 60 | Arg::new("dump-config") 61 | .help("Write the default config file to the current working directory") 62 | .id("dump-config") 63 | .long("dump-config") 64 | ).get_matches(); 65 | if args.contains_id("dump-config") { 66 | let mut config_path: String = args 67 | .get_one::("dump-config") 68 | .expect("Missing dump-config argument") 69 | .to_string(); 70 | if !config_path.contains("config.toml") { 71 | config_path.push_str("/config.toml"); 72 | } 73 | let config = CuTE_tui::Config::default(); 74 | let config = toml::to_string_pretty(&config).expect("Failed to serialize config"); 75 | std::fs::write(config_path, config).expect("Failed to write config file"); 76 | } 77 | if args.contains_id("db-path") { 78 | let db_path: String = args 79 | .get_one::("db") 80 | .expect("Missing db-path argument") 81 | .to_string(); 82 | let db_path = std::path::Path::new(&db_path); 83 | let db_path = std::fs::canonicalize(db_path).expect("Failed to canonicalize path"); 84 | let mut config = Config::default(); 85 | config.set_db_path(db_path); 86 | return Some(config); 87 | } 88 | None 89 | } 90 | -------------------------------------------------------------------------------- /src/request/curl.rs: -------------------------------------------------------------------------------- 1 | use super::ExecuteOption; 2 | use crate::database::db::DB; 3 | use crate::display::{menuopts::CURL, AppOptions, HeaderKind}; 4 | use curl::easy::{Auth, Easy2, Handler, List, WriteError}; 5 | use serde::{Deserialize, Serialize}; 6 | use std::ops::{Deref, DerefMut}; 7 | use std::{ 8 | fmt::{Display, Formatter}, 9 | io::{Read, Write}, 10 | str::FromStr, 11 | }; 12 | impl DerefMut for CurlHandler { 13 | fn deref_mut(&mut self) -> &mut Self::Target { 14 | &mut self.0 15 | } 16 | } 17 | impl Deref for CurlHandler { 18 | type Target = Easy2; 19 | fn deref(&self) -> &Self::Target { 20 | &self.0 21 | } 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct CurlHandler(Easy2); 26 | #[derive(Debug, Serialize, Deserialize, Eq, Clone, PartialEq)] 27 | pub struct Collector(Vec); 28 | impl Handler for Collector { 29 | fn write(&mut self, data: &[u8]) -> Result { 30 | self.0.extend_from_slice(data); 31 | Ok(data.len()) 32 | } 33 | } 34 | 35 | #[derive(Debug, Serialize, Deserialize)] 36 | pub struct Curl { 37 | // The libcurl interface for our command/request 38 | #[serde(skip)] 39 | curl: CurlHandler, 40 | pub method: Method, 41 | auth: AuthKind, 42 | // The final cli command string 43 | cmd: String, 44 | pub headers: Option>, 45 | url: String, 46 | pub opts: Vec, 47 | resp: Option, 48 | upload_file: Option, 49 | outfile: Option, 50 | // Whether to save the (command, auth/key) to DB after execution 51 | save: (bool, bool), 52 | ser: bool, 53 | } 54 | 55 | impl Default for CurlHandler { 56 | fn default() -> Self { 57 | Self(Easy2::new(Collector(Vec::new()))) 58 | } 59 | } 60 | 61 | #[derive(Debug, Default, Serialize, Deserialize, Eq, Clone, PartialEq)] 62 | pub enum Method { 63 | #[default] 64 | Get, 65 | Post, 66 | Put, 67 | Patch, 68 | Delete, 69 | Head, 70 | } 71 | impl Method { 72 | pub fn needs_reset(&self) -> bool { 73 | matches!(self, Method::Put | Method::Patch | Method::Post) 74 | } 75 | } 76 | impl FromStr for Method { 77 | type Err = String; 78 | fn from_str(s: &str) -> Result { 79 | match s { 80 | "GET" => Ok(Method::Get), 81 | "POST" => Ok(Method::Post), 82 | "PUT" => Ok(Method::Put), 83 | "PATCH" => Ok(Method::Patch), 84 | "DELETE" => Ok(Method::Delete), 85 | "HEAD" => Ok(Method::Head), 86 | _ => Err(String::from("GET")), 87 | } 88 | } 89 | } 90 | impl Display for Method { 91 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 92 | match self { 93 | Method::Get => write!(f, "GET"), 94 | Method::Post => write!(f, "POST"), 95 | Method::Put => write!(f, "PUT"), 96 | Method::Patch => write!(f, "PATCH"), 97 | Method::Delete => write!(f, "DELETE"), 98 | Method::Head => write!(f, "HEAD"), 99 | } 100 | } 101 | } 102 | impl Eq for Curl {} 103 | 104 | impl Clone for Curl { 105 | fn clone(&self) -> Self { 106 | let mut curl = Curl::new(); 107 | let _ = self.opts.iter().map(|x| curl.add_option(x)); 108 | curl.set_url(self.url.as_str()); 109 | 110 | match self.method { 111 | Method::Get => curl.set_get_method(), 112 | Method::Post => curl.set_post_method(), 113 | Method::Put => curl.set_put_method(), 114 | Method::Patch => curl.set_patch_method(), 115 | Method::Delete => curl.set_delete_method(), 116 | Method::Head => curl.set_head_method(), 117 | } 118 | if let Some(ref res) = self.resp { 119 | curl.set_response(res.as_str()); 120 | } 121 | 122 | if let Some(ref upload_file) = self.upload_file { 123 | curl.set_upload_file(upload_file.as_str()); 124 | } 125 | 126 | if let Some(ref outfile) = self.outfile { 127 | curl.set_outfile(outfile); 128 | } 129 | if self.cmd != CURL { 130 | // our cmd string has been built 131 | curl.cmd.clone_from(&self.cmd); 132 | } 133 | Self { 134 | curl: CurlHandler::default(), 135 | method: self.method.clone(), 136 | auth: self.auth.clone(), 137 | cmd: self.cmd.clone(), 138 | url: self.url.clone(), 139 | opts: self.opts.clone(), 140 | resp: self.resp.clone(), 141 | headers: self.headers.clone(), 142 | upload_file: self.upload_file.clone(), 143 | outfile: self.outfile.clone(), 144 | save: self.save, 145 | ser: self.ser, 146 | } 147 | } 148 | } 149 | 150 | impl PartialEq for Curl { 151 | fn eq(&self, other: &Self) -> bool { 152 | self.method == other.method 153 | && self.auth == other.auth 154 | && self.cmd == other.cmd 155 | && self.url == other.url 156 | && self.opts == other.opts 157 | && self.resp == other.resp 158 | && self.headers == other.headers 159 | && self.upload_file == other.upload_file 160 | && self.outfile == other.outfile 161 | && self.save == other.save 162 | && self.ser == other.ser 163 | } 164 | } 165 | 166 | #[derive(Debug, Serialize, Deserialize, Eq, Clone, PartialEq)] 167 | pub enum AuthKind { 168 | None, 169 | Ntlm, 170 | Basic(String), 171 | Bearer(String), 172 | Digest(String), 173 | AwsSigv4, 174 | Spnego, 175 | } 176 | 177 | impl AuthKind { 178 | pub fn has_token(&self) -> bool { 179 | matches!( 180 | self, 181 | AuthKind::Bearer(_) | AuthKind::Basic(_) | AuthKind::Digest(_) 182 | ) 183 | } 184 | pub fn get_token(&self) -> Option { 185 | match self { 186 | AuthKind::Bearer(token) => Some(token.clone()), 187 | AuthKind::Basic(login) => Some(login.clone()), 188 | AuthKind::Digest(login) => Some(login.clone()), 189 | _ => None, 190 | } 191 | } 192 | } 193 | #[rustfmt::skip] 194 | impl Display for AuthKind { 195 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 196 | match self { 197 | AuthKind::None => write!(f, "None"), 198 | AuthKind::Ntlm => write!(f, "NTLM"), 199 | AuthKind::Basic(login) => write!(f, "Basic: {}", login), 200 | AuthKind::Bearer(token) => write!(f, "Authorization: Bearer {}", token), 201 | AuthKind::Digest(login) => write!(f, "Digest Auth: {}", login), 202 | AuthKind::AwsSigv4 => write!(f, "AWS SignatureV4"), 203 | AuthKind::Spnego => write!(f, "SPNEGO Auth"), 204 | } 205 | } 206 | } 207 | 208 | impl Default for Curl { 209 | fn default() -> Self { 210 | Self { 211 | curl: CurlHandler::default(), 212 | method: Method::Get, 213 | auth: AuthKind::None, 214 | cmd: String::from(CURL), 215 | url: String::new(), 216 | opts: Vec::new(), 217 | headers: None, 218 | resp: None, 219 | upload_file: None, 220 | outfile: None, 221 | save: (false, false), 222 | ser: false, 223 | } 224 | } 225 | } 226 | impl Curl { 227 | pub fn new() -> Self { 228 | Self::default() 229 | } 230 | pub fn new_serializing() -> Self { 231 | Self { 232 | ser: true, 233 | ..Self::default() 234 | } 235 | } 236 | pub fn get_url(&self) -> &str { 237 | &self.url 238 | } 239 | pub fn get_method(&self) -> &Method { 240 | &self.method 241 | } 242 | pub fn add_basic_auth(&mut self, info: &str) { 243 | self.auth = AuthKind::Basic(String::from(info)); 244 | } 245 | fn apply_method(&mut self) { 246 | match self.method { 247 | Method::Get => self.set_get_method(), 248 | Method::Post => self.set_post_method(), 249 | Method::Put => self.set_put_method(), 250 | Method::Patch => self.set_patch_method(), 251 | Method::Delete => self.set_delete_method(), 252 | Method::Head => self.set_head_method(), 253 | } 254 | } 255 | 256 | pub fn get_response(&self) -> Option { 257 | self.resp.clone() 258 | } 259 | 260 | pub fn build_command_string(&mut self) { 261 | let mut cmd: Vec = vec![self.cmd.clone()]; 262 | cmd.push(String::from("-X")); 263 | cmd.push(self.method.to_string()); 264 | cmd.push(self.url.clone()); 265 | for flag in self.opts.iter() { 266 | cmd.push(flag.get_curl_flag_value()); 267 | } 268 | if self.headers.is_some() { 269 | self.headers.as_ref().unwrap().iter().for_each(|h| { 270 | cmd.push(String::from("-H")); 271 | cmd.push(h.clone()); 272 | }); 273 | } 274 | self.cmd = cmd.join(" ").trim().to_string(); 275 | } 276 | 277 | // this is only called after execution, we need to 278 | // find out if its been built already 279 | pub fn get_command_string(&mut self) -> String { 280 | if self.cmd == CURL { 281 | self.build_command_string(); 282 | } 283 | self.cmd.clone() 284 | } 285 | 286 | pub fn set_outfile(&mut self, outfile: &str) { 287 | self.outfile = Some(String::from(outfile)); 288 | } 289 | 290 | pub fn set_url(&mut self, url: &str) { 291 | if self.ser { 292 | // if we're serializing, we need to store the URL in the opts 293 | self.opts.push(AppOptions::URL(url.to_string())); 294 | } 295 | self.url = String::from(url.trim()); 296 | self.curl.url(url).unwrap(); 297 | } 298 | 299 | pub fn has_auth(&self) -> bool { 300 | self.auth != AuthKind::None 301 | } 302 | 303 | pub fn set_response(&mut self, response: &str) { 304 | self.resp = Some(String::from(response)); 305 | } 306 | 307 | pub fn set_cookie_path(&mut self, path: &str) { 308 | if self.ser { 309 | self.opts.push(AppOptions::CookieJar(path.to_string())); 310 | } 311 | self.curl.cookie_file(path).unwrap(); 312 | } 313 | 314 | pub fn set_cookie_jar(&mut self, path: &str) { 315 | if self.ser { 316 | self.opts.push(AppOptions::CookieJar(path.to_string())); 317 | } 318 | self.curl.cookie_jar(path).unwrap(); 319 | } 320 | pub fn reset_cookie_session(&mut self) { 321 | if self.ser { 322 | self.opts.push(AppOptions::NewCookieSession); 323 | } 324 | self.curl.cookie_session(true).unwrap(); 325 | } 326 | 327 | pub fn get_upload_file(&self) -> Option { 328 | self.upload_file.clone() 329 | } 330 | 331 | #[rustfmt::skip] 332 | pub fn execute(&mut self, mut db: Option>) -> Result<(), String> { 333 | let mut list = List::new(); 334 | curl::init(); 335 | // we do this again because if it's a patch | put and there's a 336 | // body, it will default to post 337 | self.apply_method(); 338 | let mut has_headers = self.handle_auth_exec(&mut list); 339 | if let Some(ref headers) = self.headers { 340 | headers 341 | .iter() 342 | .for_each(|h| list.append(h.as_str()).unwrap()); 343 | has_headers = true; 344 | } 345 | if self.will_save_command() { 346 | if let Some(ref mut db) = db { 347 | self.build_command_string(); 348 | let command_string = &self.get_command_string(); 349 | let command_json = serde_json::to_string(&self) 350 | .map_err(|e| format!("Error serializing command: {}", e))?; 351 | if db.add_command(command_string, command_json, None).is_err() { 352 | println!("Error saving command to DB"); 353 | } 354 | } 355 | } 356 | // Save token to DB 357 | if self.will_save_token() { 358 | if let Some(ref mut db) = db { 359 | if db 360 | .add_key(&self.auth.get_token().unwrap_or_default()) 361 | .is_err() 362 | { 363 | println!("Error saving token to DB"); 364 | } 365 | } 366 | } 367 | // Append headers if needed 368 | if has_headers { 369 | self.curl 370 | .http_headers(list) 371 | .map_err(|e| format!("Error setting headers: {:?}", e))?; 372 | } 373 | 374 | // Upload file if specified 375 | if let Some(ref upload_file) = self.upload_file { 376 | if let Ok(file) = std::fs::File::open(upload_file) { 377 | let mut buff: Vec = Vec::new(); 378 | let mut reader = std::io::BufReader::new(file); 379 | reader 380 | .read_to_end(&mut buff) 381 | .map_err(|e| format!("Error reading file: {}", e))?; 382 | 383 | // set connect only + establish connection to the URL 384 | self.curl 385 | .connect_only(true) 386 | .map_err(|e| format!("Error connecting: {:?}", e))?; 387 | 388 | // Handle upload errors 389 | self.curl 390 | .perform() 391 | .map_err(|err| format!("Error making connection: {:?}", err))?; 392 | self.curl 393 | .send(buff.as_slice()) 394 | .map_err(|e| format!("Error with upload: {}", e))?; 395 | } 396 | } 397 | 398 | // Perform the main request 399 | self.curl 400 | .perform() 401 | .map_err(|err| format!("Error: {:?}", err))?; 402 | let contents = self.curl.get_ref(); 403 | let res = String::from_utf8_lossy(&contents.0); 404 | if let Ok(json) = serde_json::from_str::(&res) { 405 | self.resp = Some(serde_json::to_string_pretty(&json).unwrap()); 406 | } else { 407 | self.resp = Some(res.to_string()); 408 | } 409 | Ok(()) 410 | } 411 | 412 | pub fn set_auth(&mut self, auth: AuthKind) { 413 | if self.ser { 414 | self.opts.push(AppOptions::Auth(auth.clone())); 415 | } 416 | match auth { 417 | AuthKind::Basic(ref info) => self.set_basic_auth(info), 418 | AuthKind::Ntlm => self.set_ntlm_auth(), 419 | AuthKind::Bearer(ref token) => self.set_bearer_auth(token), 420 | AuthKind::AwsSigv4 => self.set_aws_sigv4_auth(), 421 | AuthKind::Digest(login) => self.set_digest_auth(&login), 422 | AuthKind::Spnego => self.set_spnego_auth(), 423 | AuthKind::None => {} 424 | } 425 | } 426 | 427 | pub fn set_method(&mut self, method: Method) { 428 | match method { 429 | Method::Get => self.set_get_method(), 430 | Method::Post => self.set_post_method(), 431 | Method::Put => self.set_put_method(), 432 | Method::Patch => self.set_patch_method(), 433 | Method::Delete => self.set_delete_method(), 434 | Method::Head => self.set_head_method(), 435 | } 436 | } 437 | 438 | pub fn set_cert_info(&mut self, opt: bool) { 439 | if self.ser { 440 | self.opts.push(AppOptions::CertInfo); 441 | } 442 | self.curl.certinfo(opt).unwrap(); 443 | } 444 | 445 | pub fn set_referrer(&mut self, referrer: &str) { 446 | if self.ser { 447 | self.opts.push(AppOptions::Referrer(String::from(referrer))); 448 | } 449 | self.curl.referer(referrer).unwrap(); 450 | } 451 | 452 | pub fn set_proxy_tunnel(&mut self, opt: bool) { 453 | if self.ser { 454 | self.opts.push(AppOptions::ProxyTunnel); 455 | } 456 | self.curl.http_proxy_tunnel(opt).unwrap(); 457 | } 458 | 459 | pub fn set_verbose(&mut self, opt: bool) { 460 | if self.ser { 461 | self.opts.push(AppOptions::Verbose); 462 | } 463 | self.curl.verbose(opt).unwrap(); 464 | } 465 | 466 | pub fn set_fail_on_error(&mut self, fail: bool) { 467 | if self.ser { 468 | self.opts.push(AppOptions::FailOnError); 469 | } 470 | self.curl.fail_on_error(fail).unwrap(); 471 | } 472 | 473 | pub fn set_unix_socket(&mut self, socket: &str) { 474 | if self.ser { 475 | self.opts.push(AppOptions::UnixSocket(String::from(socket))); 476 | } 477 | self.curl.unix_socket(socket).unwrap(); 478 | } 479 | 480 | pub fn set_content_header(&mut self, kind: &HeaderKind) { 481 | if self.ser { 482 | self.opts.push(AppOptions::ContentHeaders(kind.clone())); 483 | } 484 | if let Some(ref mut headers) = self.headers { 485 | headers.push(kind.to_string()); 486 | } else { 487 | self.headers = Some(vec![kind.to_string()]); 488 | } 489 | } 490 | 491 | pub fn save_command(&mut self, opt: bool) { 492 | self.save.0 = opt; 493 | } 494 | 495 | pub fn add_headers(&mut self, headers: &str) { 496 | if self.ser { 497 | self.opts.push(AppOptions::Headers(headers.to_string())); 498 | } 499 | if self.headers.is_some() { 500 | self.headers.as_mut().unwrap().push(headers.to_string()); 501 | } else { 502 | self.headers = Some(vec![headers.to_string()]); 503 | } 504 | } 505 | 506 | pub fn save_token(&mut self, opt: bool) { 507 | self.save.1 = opt; 508 | } 509 | 510 | pub fn get_token(&self) -> Option { 511 | self.auth.get_token() 512 | } 513 | 514 | pub fn remove_headers(&mut self, headers: &str) { 515 | if self.headers.is_some() { 516 | self.headers 517 | .as_mut() 518 | .unwrap() 519 | .retain(|x| !headers.contains(x)); 520 | } 521 | } 522 | pub fn match_wildcard(&mut self, opt: bool) { 523 | if self.ser { 524 | self.opts.push(AppOptions::MatchWildcard); 525 | } 526 | self.curl.wildcard_match(opt).unwrap(); 527 | } 528 | 529 | pub fn set_unrestricted_auth(&mut self, opt: bool) { 530 | if self.ser { 531 | self.opts.push(AppOptions::UnrestrictedAuth); 532 | } 533 | self.curl.unrestricted_auth(opt).unwrap(); 534 | } 535 | 536 | pub fn set_user_agent(&mut self, ua: &str) { 537 | if self.ser { 538 | self.opts.push(AppOptions::UserAgent(ua.to_string())); 539 | } 540 | self.curl.useragent(ua).unwrap(); 541 | } 542 | 543 | pub fn set_max_redirects(&mut self, redirects: usize) { 544 | if self.ser { 545 | self.opts.push(AppOptions::MaxRedirects(redirects)); 546 | } 547 | self.curl 548 | .max_redirections(redirects as u32) 549 | .unwrap_or_default(); 550 | } 551 | 552 | pub fn set_ca_path(&mut self, ca_path: &str) { 553 | if self.ser { 554 | self.opts.push(AppOptions::CaPath(ca_path.to_string())); 555 | } 556 | self.curl.cainfo(ca_path).unwrap_or_default(); 557 | } 558 | 559 | pub fn set_tcp_keepalive(&mut self, opt: bool) { 560 | if self.ser { 561 | self.opts.push(AppOptions::TcpKeepAlive); 562 | } 563 | self.curl.tcp_keepalive(opt).unwrap_or_default(); 564 | } 565 | 566 | pub fn set_request_body(&mut self, body: &str) { 567 | if self.ser { 568 | self.opts.push(AppOptions::RequestBody(body.to_string())); 569 | } 570 | if self.opts.iter().any(|x| std::mem::discriminant(x) == std::mem::discriminant(&AppOptions::RequestBody(body.to_string()))) { 571 | self.opts.retain(|x| std::mem::discriminant(x) != std::mem::discriminant(&AppOptions::RequestBody(body.to_string()))); 572 | } 573 | self.opts.push(AppOptions::RequestBody(body.to_string())); 574 | self.curl 575 | .post_fields_copy(body.as_bytes()) 576 | .unwrap_or_default(); 577 | } 578 | 579 | pub fn set_follow_redirects(&mut self, opt: bool) { 580 | if self.ser { 581 | self.opts.push(AppOptions::FollowRedirects); 582 | } 583 | self.curl.follow_location(opt).unwrap_or_default(); 584 | } 585 | 586 | pub fn add_cookie(&mut self, cookie: &str) { 587 | if self.ser { 588 | self.opts.push(AppOptions::NewCookie(cookie.to_string())); 589 | } 590 | self.curl.cookie(cookie).unwrap_or_default(); 591 | } 592 | 593 | pub fn set_upload_file(&mut self, file: &str) { 594 | if self.ser { 595 | self.opts.push(AppOptions::UploadFile(file.to_string())); 596 | } 597 | self.upload_file = Some(file.to_string()); 598 | self.curl.upload(true).unwrap_or_default(); 599 | } 600 | 601 | pub fn write_output(&mut self) -> Result<(), std::io::Error> { 602 | match self.outfile { 603 | Some(ref mut outfile) => { 604 | let mut file = match std::fs::File::create(outfile) { 605 | Ok(f) => f, 606 | Err(e) => { 607 | eprintln!("Error creating file: {:?}", e); 608 | return Err(e); 609 | } 610 | }; 611 | let mut writer = std::io::BufWriter::new(&mut file); 612 | 613 | if let Some(resp) = &self.resp { 614 | if let Err(e) = writer.write_all(resp.as_bytes()) { 615 | eprintln!("Error writing to file: {:?}", e); 616 | return Err(e); 617 | } 618 | } 619 | 620 | Ok(()) 621 | } 622 | None => Ok(()), 623 | } 624 | } 625 | pub fn enable_response_headers(&mut self, opt: bool) { 626 | if self.ser { 627 | self.opts.push(AppOptions::EnableHeaders); 628 | } 629 | self.curl.show_header(opt).unwrap_or_default(); 630 | } 631 | 632 | fn will_save_token(&self) -> bool { 633 | // (0: save_command, 1: save_token) 634 | self.save.1 635 | } 636 | 637 | pub fn easy_from_opts(&mut self) { 638 | self.ser = true; 639 | self.build_command_string(); 640 | self.curl.url(&self.url).unwrap(); 641 | self.apply_method(); 642 | let opts = self.opts.clone(); 643 | for opt in opts.iter() { 644 | self.add_option(opt); 645 | } 646 | } 647 | 648 | pub fn set_any_auth(&mut self) { 649 | if self.ser { 650 | self.opts.push(AppOptions::Auth(AuthKind::None)); 651 | } 652 | let _ = self.curl.http_auth(&Auth::new()); 653 | } 654 | 655 | pub fn set_basic_auth(&mut self, login: &str) { 656 | if self.ser { 657 | self.opts.push(AppOptions::Auth(AuthKind::Basic(login.to_string()))); 658 | } 659 | self.auth = AuthKind::Basic(String::from(login)); 660 | } 661 | 662 | pub fn set_head_method(&mut self) { 663 | self.method = Method::Head; 664 | self.curl.nobody(true).unwrap(); 665 | } 666 | 667 | pub fn set_digest_auth(&mut self, login: &str) { 668 | if self.ser { 669 | self.opts.push(AppOptions::Auth(AuthKind::Digest(login.to_string()))); 670 | } 671 | self.auth = AuthKind::Digest(String::from(login)); 672 | } 673 | 674 | pub fn set_aws_sigv4_auth(&mut self) { 675 | if self.ser { 676 | self.opts.push(AppOptions::Auth(AuthKind::AwsSigv4)); 677 | } 678 | self.auth = AuthKind::AwsSigv4; 679 | } 680 | 681 | pub fn set_spnego_auth(&mut self) { 682 | if self.ser { 683 | self.opts.push(AppOptions::Auth(AuthKind::Spnego)); 684 | } 685 | self.auth = AuthKind::Spnego; 686 | } 687 | 688 | pub fn will_save_command(&self) -> bool { 689 | // (0: save_command, 1: save_token) 690 | self.save.0 691 | } 692 | 693 | pub fn set_get_method(&mut self) { 694 | self.method = Method::Get; 695 | self.curl.get(true).unwrap(); 696 | } 697 | 698 | pub fn set_post_method(&mut self) { 699 | self.method = Method::Post; 700 | self.curl.post(true).unwrap(); 701 | } 702 | 703 | pub fn set_put_method(&mut self) { 704 | self.method = Method::Put; 705 | self.curl.put(true).unwrap(); 706 | } 707 | 708 | pub fn set_patch_method(&mut self) { 709 | self.method = Method::Patch; 710 | self.curl.custom_request("PATCH").unwrap(); 711 | } 712 | 713 | pub fn set_delete_method(&mut self) { 714 | self.method = Method::Delete; 715 | self.curl.custom_request("DELETE").unwrap(); 716 | } 717 | 718 | pub fn set_ntlm_auth(&mut self) { 719 | self.auth = AuthKind::Ntlm; 720 | } 721 | 722 | pub fn set_bearer_auth(&mut self, token: &str) { 723 | self.auth = AuthKind::Bearer(String::from(token)); 724 | } 725 | 726 | pub fn show_headers(&mut self) { 727 | if self.ser { 728 | self.opts.push(AppOptions::EnableHeaders); 729 | } 730 | self.curl.show_header(true).unwrap(); 731 | } 732 | 733 | fn handle_auth_exec(&mut self, list: &mut List) -> bool { 734 | match &self.auth { 735 | AuthKind::None => {} 736 | AuthKind::Basic(login) => { 737 | self.curl 738 | .username(login.split(':').next().unwrap()) 739 | .unwrap(); 740 | self.curl 741 | .password(login.split(':').last().unwrap()) 742 | .unwrap(); 743 | let _ = self.curl.http_auth(Auth::new().basic(true)); 744 | } 745 | AuthKind::Bearer(ref token) => { 746 | list.append(&format!("Authorization: {token}")).unwrap(); 747 | return true; 748 | } 749 | AuthKind::Digest(login) => { 750 | self.curl 751 | .username(login.split(':').next().unwrap()) 752 | .unwrap(); 753 | self.curl 754 | .password(login.split(':').last().unwrap()) 755 | .unwrap(); 756 | let _ = self.curl.http_auth(Auth::new().digest(true)); 757 | } 758 | AuthKind::Ntlm => { 759 | let _ = self.curl.http_auth(Auth::new().ntlm(true)); 760 | } 761 | AuthKind::Spnego => { 762 | let _ = self.curl.http_auth(Auth::new().gssnegotiate(true)); 763 | } 764 | AuthKind::AwsSigv4 => { 765 | let _ = self.curl.http_auth(Auth::new().aws_sigv4(true)); 766 | } 767 | } 768 | false 769 | } 770 | 771 | pub fn url_encode(&mut self, data: &str) { 772 | let encoded = self.curl.url_encode(data.as_bytes()); 773 | self.opts.push(AppOptions::RequestBody(encoded)); 774 | } 775 | } 776 | -------------------------------------------------------------------------------- /src/request/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::display::{AppOptions, HeaderKind}; 2 | 3 | use self::curl::Curl; 4 | 5 | pub mod curl; 6 | // Response parser 7 | pub mod response; 8 | 9 | pub trait ExecuteOption { 10 | fn add_option(&mut self, opt: &AppOptions); 11 | fn remove_option(&mut self, opt: &AppOptions); 12 | } 13 | 14 | impl ExecuteOption for Curl { 15 | fn add_option(&mut self, opt: &AppOptions) { 16 | match opt { 17 | AppOptions::URL(ref url) => self.set_url(url), 18 | AppOptions::Outfile(ref file) => self.set_outfile(file), 19 | AppOptions::UploadFile(ref file) => self.set_upload_file(file), 20 | AppOptions::UnixSocket(ref file) => self.set_unix_socket(file), 21 | AppOptions::FailOnError => self.set_fail_on_error(true), 22 | AppOptions::Verbose => self.set_verbose(true), 23 | AppOptions::Response(ref resp) => self.set_response(resp), 24 | AppOptions::SaveCommand => self.save_command(true), 25 | AppOptions::SaveToken => self.save_token(true), 26 | AppOptions::FollowRedirects => self.set_follow_redirects(true), 27 | AppOptions::UnrestrictedAuth => self.set_unrestricted_auth(true), 28 | AppOptions::TcpKeepAlive => self.set_tcp_keepalive(true), 29 | AppOptions::ProxyTunnel => self.set_proxy_tunnel(true), 30 | AppOptions::CertInfo => self.set_cert_info(true), 31 | AppOptions::MatchWildcard => self.match_wildcard(true), 32 | AppOptions::CaPath(ref path) => self.set_ca_path(path), 33 | AppOptions::MaxRedirects(size) => self.set_max_redirects(*size), 34 | AppOptions::UserAgent(ref agent) => self.set_user_agent(agent), 35 | AppOptions::Referrer(ref s) => self.set_referrer(s), 36 | AppOptions::RequestBody(ref body) => self.set_request_body(body), 37 | AppOptions::CookieJar(ref jar) => self.set_cookie_jar(jar), 38 | AppOptions::CookiePath(ref path) => self.set_cookie_path(path), 39 | AppOptions::NewCookie(ref new) => self.add_cookie(new), 40 | AppOptions::NewCookieSession => self.reset_cookie_session(), 41 | AppOptions::Headers(ref headers) => self.add_headers(headers), 42 | AppOptions::Auth(auth) => self.set_auth(auth.clone()), 43 | AppOptions::EnableHeaders => self.enable_response_headers(true), 44 | AppOptions::ContentHeaders(ref headers) => self.set_content_header(headers), 45 | } 46 | } 47 | fn remove_option(&mut self, opt: &AppOptions) { 48 | match opt { 49 | AppOptions::URL(_) => self.set_url(""), 50 | AppOptions::Outfile(_) => self.set_outfile(""), 51 | AppOptions::UploadFile(_) => self.set_upload_file(""), 52 | AppOptions::UnixSocket(_) => self.set_unix_socket(""), 53 | AppOptions::FailOnError => self.set_fail_on_error(false), 54 | AppOptions::Verbose => self.set_verbose(false), 55 | AppOptions::Response(_) => self.set_response(""), 56 | AppOptions::SaveCommand => self.save_command(false), 57 | AppOptions::SaveToken => self.save_token(false), 58 | AppOptions::FollowRedirects => self.set_follow_redirects(false), 59 | AppOptions::UnrestrictedAuth => self.set_unrestricted_auth(false), 60 | AppOptions::TcpKeepAlive => self.set_tcp_keepalive(false), 61 | AppOptions::ProxyTunnel => self.set_proxy_tunnel(false), 62 | AppOptions::CertInfo => self.set_cert_info(false), 63 | AppOptions::MatchWildcard => self.match_wildcard(false), 64 | AppOptions::CaPath(_) => self.set_ca_path(""), 65 | AppOptions::MaxRedirects(_) => self.set_max_redirects(0), 66 | AppOptions::UserAgent(_) => self.set_user_agent(""), 67 | AppOptions::Referrer(_) => self.set_referrer(""), 68 | AppOptions::RequestBody(_) => self.set_request_body(""), 69 | AppOptions::CookieJar(_) => self.set_cookie_jar(""), 70 | AppOptions::CookiePath(_) => self.set_cookie_path(""), 71 | AppOptions::NewCookie(_) => self.add_cookie(""), 72 | AppOptions::NewCookieSession => self.reset_cookie_session(), 73 | AppOptions::Headers(_) => self.remove_headers(""), 74 | AppOptions::Auth(_) => self.set_auth(crate::request::curl::AuthKind::None), 75 | AppOptions::EnableHeaders => self.enable_response_headers(false), 76 | AppOptions::ContentHeaders(_) => self.set_content_header(&HeaderKind::None), 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/request/response.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub struct Response { 6 | pub status: u16, 7 | pub headers: HashMap, 8 | pub body: String, 9 | } 10 | 11 | impl Response { 12 | pub fn from_raw_string(response_str: &str) -> Result { 13 | // Split the response into lines 14 | let lines: Vec<&str> = response_str.lines().collect(); 15 | 16 | // Ensure there's at least one line in the response 17 | if lines.is_empty() { 18 | return Err("Empty response"); 19 | } 20 | 21 | // Parse the first line to extract the status code 22 | let first_line = lines[0].trim(); 23 | let status_code = first_line 24 | .split_whitespace() 25 | .nth(1) 26 | .and_then(|status| status.parse::().ok()) 27 | .ok_or("Invalid status code")?; 28 | 29 | // HashMap for the headers 30 | let mut headers = HashMap::new(); 31 | let mut body = String::new(); 32 | 33 | // Iterate through the remaining lines to extract headers and body 34 | let mut parsing_body = false; 35 | for line in &lines[1..] { 36 | if parsing_body { 37 | body.push_str(line); 38 | body.push('\n'); 39 | } else if line.is_empty() { 40 | // An empty line separates headers from the body 41 | parsing_body = true; 42 | } else { 43 | // Parse headers in key-value format (will happen first) 44 | if let Some((key, value)) = line.split_once(':') { 45 | headers.insert(key.trim().to_string(), value.trim().to_string()); 46 | } 47 | } 48 | } 49 | 50 | Ok(Response { 51 | status: status_code, 52 | headers, 53 | body, 54 | }) 55 | } 56 | 57 | pub fn get_headers(&self) -> serde_json::Value { 58 | serde_json::to_value(&self.headers).unwrap() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/screens/auth.rs: -------------------------------------------------------------------------------- 1 | use super::render::handle_screen_defaults; 2 | use crate::app::App; 3 | use crate::display::inputopt::InputOpt; 4 | use crate::display::menuopts::{AWS_AUTH_ERROR_MSG, AWS_AUTH_MSG}; 5 | use crate::display::AppOptions; 6 | use crate::request::curl::AuthKind; 7 | use crate::screens::screen::Screen; 8 | use tui::Frame; 9 | 10 | pub fn handle_authentication_screen(app: &mut App, frame: &mut Frame<'_>) { 11 | handle_screen_defaults(app, frame); 12 | if let Some(num) = app.selected { 13 | match num { 14 | 0 => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::Auth(AuthKind::Basic( 15 | "".to_string(), 16 | ))))), 17 | 1 => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::Auth( 18 | AuthKind::Bearer("".to_string()), 19 | )))), 20 | 2 => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::Auth( 21 | AuthKind::Digest("".to_string()), 22 | )))), 23 | 3 => { 24 | if varify_aws_auth() { 25 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 26 | String::from(AWS_AUTH_MSG), 27 | )))); 28 | app.add_app_option(AppOptions::Auth(AuthKind::AwsSigv4)); 29 | } else { 30 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 31 | String::from(AWS_AUTH_ERROR_MSG), 32 | )))); 33 | } 34 | } 35 | 4 => { 36 | if app.command.has_auth() { 37 | app.remove_app_option(&AppOptions::Auth(AuthKind::None)); 38 | } 39 | app.add_app_option(AppOptions::Auth(AuthKind::Spnego)); 40 | app.goto_screen(&Screen::RequestMenu(None)); 41 | } 42 | 5 => { 43 | if app.command.has_auth() { 44 | app.remove_app_option(&AppOptions::Auth(AuthKind::None)); 45 | } 46 | app.add_app_option(AppOptions::Auth(AuthKind::Ntlm)); 47 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 48 | String::from("Alert: NTLM Auth Enabled"), 49 | )))); 50 | } 51 | _ => {} 52 | } 53 | } 54 | } 55 | 56 | fn varify_aws_auth() -> bool { 57 | if std::env::var("AWS_ACCESS_KEY_ID").is_ok() 58 | && std::env::var("AWS_SECRET_ACCESS_KEY").is_ok() 59 | && std::env::var("AWS_DEFAULT_REGION").is_ok() 60 | { 61 | return true; 62 | } 63 | false 64 | } 65 | -------------------------------------------------------------------------------- /src/screens/collections.rs: -------------------------------------------------------------------------------- 1 | use super::{error_alert_box, Screen}; 2 | use crate::app::App; 3 | use crate::display::inputopt::InputOpt; 4 | use crate::display::menuopts::{ 5 | COLLECTION_ALERT_MENU_OPTS, DEFAULT_MENU_PARAGRAPH, POSTMAN_COLLECTION_TITLE, 6 | }; 7 | use crate::screens::render::handle_screen_defaults; 8 | use crate::screens::{ 9 | centered_rect, input::input_screen::handle_default_input_screen, 10 | render::render_header_paragraph, ScreenArea, 11 | }; 12 | use tui::prelude::{Constraint, Direction, Layout, Margin}; 13 | use tui::style::{Color, Modifier, Style}; 14 | use tui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; 15 | use tui::Frame; 16 | 17 | pub fn handle_collection_menu(app: &mut App, frame: &mut Frame<'_>, opt: Option) { 18 | handle_screen_defaults(app, frame); 19 | match opt { 20 | Some(InputOpt::RequestError(e)) => { 21 | error_alert_box(frame, &e); 22 | } 23 | Some(opt) => { 24 | handle_default_input_screen(app, frame, opt.clone()); 25 | } 26 | _ => {} 27 | }; 28 | match app.selected { 29 | // Import New Collection 30 | Some(0) => app.goto_screen(&Screen::SavedCollections(Some(InputOpt::ImportCollection))), 31 | // View Saved Collections 32 | Some(1) => app.goto_screen(&Screen::ViewSavedCollections), 33 | // Cancel 34 | Some(2) => { 35 | app.goto_screen(&Screen::Home); 36 | } 37 | _ => {} 38 | } 39 | } 40 | 41 | pub fn handle_collections_screen(app: &mut App, frame: &mut Frame<'_>) { 42 | let collections = app.db.as_ref().get_collections().unwrap_or_default(); 43 | let items = Some( 44 | collections 45 | .clone() 46 | .into_iter() 47 | .map(|x| x.get_name().to_string()) 48 | .collect::>(), 49 | ); 50 | let menu_options = app.current_screen.get_list(items); 51 | let area = centered_rect(frame.size(), ScreenArea::Center); 52 | let mut state = ListState::with_selected(ListState::default(), Some(app.cursor)); 53 | app.state = Some(state.clone()); 54 | app.state.as_mut().unwrap().select(Some(app.cursor)); 55 | frame.set_cursor(0, app.cursor as u16); 56 | frame.render_stateful_widget(menu_options, area, &mut state); 57 | let (paragraph, title) = (&DEFAULT_MENU_PARAGRAPH, &POSTMAN_COLLECTION_TITLE); 58 | frame.render_widget( 59 | render_header_paragraph(paragraph, title, app.config.get_style()), 60 | frame.size(), 61 | ); 62 | if let Some(selected) = app.selected { 63 | let selected = collections.get(selected).unwrap(); 64 | app.goto_screen(&Screen::ColMenu(selected.get_id())); 65 | } 66 | } 67 | 68 | pub fn handle_collection_alert_menu(app: &mut App, frame: &mut Frame<'_>, cmd: i32) { 69 | let layout = Layout::default() 70 | .direction(Direction::Vertical) 71 | .constraints( 72 | [ 73 | Constraint::Percentage(25), 74 | Constraint::Percentage(50), 75 | Constraint::Percentage(25), 76 | ] 77 | .as_ref(), 78 | ) 79 | .horizontal_margin(5) 80 | .split(frame.size()); 81 | // Render the alert box 82 | let alert_box = layout[1]; 83 | let alert_text_chunk = Block::default() 84 | .borders(Borders::ALL) 85 | .style(Style::default().bg(Color::Black).fg(Color::LightGreen)) 86 | .title("Collection Menu"); 87 | let options_box = layout[1].inner(&Margin { 88 | vertical: 1, 89 | horizontal: 1, 90 | }); 91 | let mut list_state = ListState::with_selected(ListState::default(), Some(app.cursor)); 92 | app.state = Some(list_state.clone()); 93 | let items: Vec = COLLECTION_ALERT_MENU_OPTS 94 | .iter() 95 | .map(|option| ListItem::new(*option)) 96 | .collect(); 97 | let list = List::new(items.clone()) 98 | .block(Block::default()) 99 | .highlight_style( 100 | Style::default() 101 | .bg(Color::White) 102 | .fg(Color::Black) 103 | .add_modifier(Modifier::BOLD), 104 | ) 105 | .highlight_symbol(">> "); 106 | let cmd_str = Layout::default() 107 | .direction(Direction::Vertical) 108 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) 109 | .split(alert_box)[1]; 110 | let selected = app 111 | .db 112 | .as_ref() 113 | .get_collection_by_id(cmd) 114 | .unwrap_or_default(); 115 | let count = app 116 | .db 117 | .get_number_of_commands_in_collection(cmd) 118 | .unwrap_or_default(); 119 | let paragraph = Paragraph::new(format!( 120 | "{:?}\nContains: {} {}", 121 | selected.get_name(), 122 | count, 123 | if count == 1 { "request" } else { "requests" } 124 | )) 125 | .block( 126 | Block::default() 127 | .borders(Borders::ALL) 128 | .title("Request Collection"), 129 | ) 130 | .alignment(tui::layout::Alignment::Center); 131 | frame.render_widget(paragraph, cmd_str); 132 | frame.render_widget(alert_text_chunk, alert_box); 133 | frame.render_stateful_widget(list, options_box, &mut list_state); 134 | match app.selected { 135 | // View Requests in collection 136 | Some(0) => app.goto_screen(&Screen::SavedCommands { 137 | id: Some(cmd), 138 | opt: None, 139 | }), 140 | Some(1) => app.goto_screen(&Screen::SavedCollections(Some( 141 | InputOpt::CollectionDescription(selected.get_id()), 142 | ))), 143 | // Rename Collection 144 | Some(2) => app.goto_screen(&Screen::SavedCollections(Some(InputOpt::RenameCollection( 145 | selected.get_id(), 146 | )))), 147 | // delete collection 148 | Some(3) => { 149 | if let Err(e) = app.db.as_ref().delete_collection(selected.get_id()) { 150 | app.goto_screen(&Screen::SavedCollections(Some(InputOpt::RequestError( 151 | format!("Error: {e}"), 152 | )))); 153 | } 154 | app.goto_screen(&Screen::SavedCollections(Some(InputOpt::AlertMessage( 155 | String::from("Success: collection deleted"), 156 | )))); 157 | } 158 | // cancel 159 | Some(4) => { 160 | app.goto_screen(&Screen::ViewSavedCollections); 161 | } 162 | _ => {} 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/screens/cookies.rs: -------------------------------------------------------------------------------- 1 | use tui::Frame; 2 | 3 | use crate::app::App; 4 | use crate::display::inputopt::InputOpt; 5 | use crate::display::AppOptions; 6 | 7 | use super::render::handle_screen_defaults; 8 | use super::Screen; 9 | 10 | pub fn handle_cookies_menu(app: &mut App, frame: &mut Frame<'_>) { 11 | handle_screen_defaults(app, frame); 12 | if let Some(num) = app.selected { 13 | match num { 14 | // set cookie filepath 15 | 0 => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::CookiePath))), 16 | // set cookie jar path 17 | 1 => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::CookieJar))), 18 | // new cookie 19 | 2 => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::NewCookie))), 20 | 3 => { 21 | app.add_app_option(AppOptions::NewCookieSession); 22 | app.goto_screen(&Screen::RequestMenu(None)); 23 | } 24 | 4 => app.goto_screen(&Screen::RequestMenu(None)), 25 | _ => {} 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/screens/error.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use crate::screens::{centered_rect, ScreenArea}; 3 | use tui::layout::{Constraint, Layout}; 4 | use tui::prelude::Direction; 5 | use tui::style::{Color, Style}; 6 | use tui::text::Text; 7 | use tui::widgets::{Block, Borders, ListState, Paragraph, Wrap}; 8 | use tui::Frame; 9 | 10 | fn err_box(frame: &mut Frame<'_>, error_msg: String) { 11 | let popup_layout = Layout::default() 12 | .direction(Direction::Vertical) 13 | .constraints( 14 | [ 15 | Constraint::Percentage((100 - 40) / 2), 16 | Constraint::Percentage(40), 17 | Constraint::Percentage((100 - 40) / 2), 18 | ] 19 | .as_ref(), 20 | ) 21 | .split(frame.size()); 22 | 23 | let boundbox = Layout::default() 24 | .direction(Direction::Horizontal) 25 | .constraints( 26 | [ 27 | Constraint::Percentage(25), 28 | Constraint::Percentage(50), 29 | Constraint::Percentage(25), 30 | ] 31 | .as_ref(), 32 | ) 33 | .split(popup_layout[1])[1]; 34 | 35 | let err_box_chunk = Block::default() 36 | .borders(Borders::ALL) 37 | .style(Style::default().bg(Color::Red).fg(Color::White)) 38 | .title("CuTE Error:"); 39 | frame.render_widget(err_box_chunk, boundbox); 40 | 41 | let innerbox = Layout::default() 42 | .direction(Direction::Vertical) 43 | .constraints([Constraint::Percentage(30), Constraint::Percentage(50)].as_ref()) 44 | .split(boundbox)[1]; 45 | 46 | frame.render_widget( 47 | Paragraph::new(Text::from(error_msg)) 48 | .alignment(::tui::prelude::Alignment::Center) 49 | .wrap(Wrap { trim: true }), 50 | innerbox, 51 | ); 52 | } 53 | 54 | pub fn handle_error_screen(app: &mut App, frame: &mut Frame<'_>, error_msg: String) { 55 | let area = centered_rect(frame.size(), ScreenArea::Top); 56 | let new_list = app.current_screen.get_list(None); 57 | let mut state = ListState::with_selected(ListState::default(), Some(app.cursor)); 58 | if !app.items.is_empty() { 59 | app.items.clear(); 60 | } 61 | app.items = app.current_screen.get_opts(None); 62 | app.state = Some(state.clone()); 63 | app.state.as_mut().unwrap().select(Some(app.cursor)); 64 | frame.set_cursor(0, app.cursor as u16); 65 | frame.render_stateful_widget(new_list, area, &mut state); 66 | 67 | err_box(frame, error_msg); 68 | } 69 | -------------------------------------------------------------------------------- /src/screens/headers.rs: -------------------------------------------------------------------------------- 1 | use tui::Frame; 2 | 3 | use super::render::handle_screen_defaults; 4 | use super::Screen; 5 | use crate::app::App; 6 | use crate::display::inputopt::InputOpt; 7 | use crate::display::{AppOptions, HeaderKind}; 8 | 9 | pub fn handle_headers_screen(app: &mut App, frame: &mut Frame<'_>) { 10 | handle_screen_defaults(app, frame); 11 | 12 | match app.selected { 13 | // add custom headers 14 | // "Add Content-Type: application/json  ", 15 | // "Add Content-Type: application/xml  ", 16 | // "Add Content-Type: application/X-WWW-Form-Urlencoded  ", 17 | // "Add Accept: application/json  ", 18 | // "Add Accept: text/html  ", 19 | // "Add Accept: application/xml  ", 20 | Some(0) => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::Headers))), 21 | // add content-type application/json 22 | Some(1) => { 23 | app.add_app_option(AppOptions::ContentHeaders(HeaderKind::ContentType( 24 | String::from("application/json"), 25 | ))); 26 | } 27 | // add accept application/json 28 | Some(2) => { 29 | app.add_app_option(AppOptions::ContentHeaders(HeaderKind::ContentType( 30 | String::from("application/xml"), 31 | ))); 32 | } 33 | Some(3) => { 34 | app.add_app_option(AppOptions::ContentHeaders(HeaderKind::ContentType( 35 | String::from("application/www-form-urlencoded"), 36 | ))); 37 | } 38 | // add accept application/json 39 | Some(4) => { 40 | app.add_app_option(AppOptions::ContentHeaders(HeaderKind::Accept( 41 | String::from("application/json"), 42 | ))); 43 | } 44 | Some(5) => { 45 | app.add_app_option(AppOptions::ContentHeaders(HeaderKind::ContentType( 46 | String::from("text/html"), 47 | ))); 48 | } 49 | // add accept application/json 50 | Some(6) => { 51 | app.add_app_option(AppOptions::ContentHeaders(HeaderKind::Accept( 52 | String::from("application/xml"), 53 | ))); 54 | } 55 | // accept headers in response 56 | Some(7) => { 57 | app.add_app_option(AppOptions::EnableHeaders); 58 | } 59 | // return to request menu 60 | Some(8) => app.goto_screen(&Screen::RequestMenu(None)), 61 | _ => {} 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/screens/input/input_screen.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use crate::display::menuopts::{ 3 | CERT_ERROR, HEADER_ERROR, PARSE_INT_ERROR, SOCKET_ERROR, UPLOAD_FILEPATH_ERROR, 4 | }; 5 | use crate::display::AppOptions; 6 | use crate::request::curl::AuthKind; 7 | use crate::screens::Screen; 8 | use crate::{app::InputMode, display::inputopt::InputOpt}; 9 | use std::path::Path; 10 | use tui::prelude::Line; 11 | use tui::style::Color; 12 | use tui::widgets::Paragraph; 13 | use tui::widgets::{Block, Borders}; 14 | use tui::{ 15 | prelude::{Constraint, Direction, Layout}, 16 | style::Style, 17 | text::Text, 18 | Frame, 19 | }; 20 | use tui_input::InputRequest; 21 | 22 | pub fn handle_default_input_screen(app: &mut App, frame: &mut Frame<'_>, opt: InputOpt) { 23 | let chunks = Layout::default() 24 | .direction(Direction::Vertical) 25 | .constraints(vec![ 26 | Constraint::Percentage(13), 27 | Constraint::Percentage(9), 28 | Constraint::Percentage(78), 29 | ]) 30 | .horizontal_margin(6) 31 | .split(frame.size()); 32 | // prompt needs to be _directly_ below the input box 33 | let top_box = Layout::default() 34 | .direction(Direction::Vertical) 35 | .constraints([Constraint::Percentage(40), Constraint::Percentage(60)].as_ref()) 36 | .split(chunks[1]); 37 | let width = top_box[0].width.max(3) - 3; 38 | let scroll = app.input.visual_scroll(width as usize); 39 | match opt { 40 | InputOpt::URL => { 41 | let mut url = app.command.get_url().to_string(); 42 | // if the url has been entered already we populate the input box with it 43 | // we need to prevent this from happening multiple times, without clearing the url 44 | if !url.is_empty() && app.input.cursor() == 0 { 45 | let _ = app.input.handle(InputRequest::InsertChar(' ')).is_some(); 46 | for ch in url.chars() { 47 | let _ = app.input.handle(InputRequest::InsertChar(ch)).is_some(); 48 | } 49 | url.clear(); 50 | } 51 | } 52 | InputOpt::Auth(ref kind) => { 53 | if kind.has_token() { 54 | let auth = app.command.get_token(); 55 | if auth.is_some() && app.input.value().is_empty() && app.input.cursor() == 0 { 56 | let _ = app.input.handle(InputRequest::InsertChar(' ')).is_some(); 57 | for ch in auth.unwrap().chars() { 58 | if app.input.handle(InputRequest::InsertChar(ch)).is_some() {} 59 | } 60 | } 61 | } 62 | } 63 | InputOpt::UploadFile => { 64 | let file = app.command.get_upload_file(); 65 | if file.is_some() && app.input.value().is_empty() && app.input.cursor() == 0 { 66 | let _ = app.input.handle(InputRequest::InsertChar(' ')).is_some(); 67 | for ch in file.unwrap().chars() { 68 | if app.input.handle(InputRequest::InsertChar(ch)).is_some() {} 69 | } 70 | } 71 | } 72 | InputOpt::UnixSocket => { 73 | if !app.command.get_url().is_empty() { 74 | // would only need a unix socket if we don't have a url 75 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 76 | String::from("Error: You have already entered a URL"), 77 | )))); 78 | } 79 | let socket = app.command.opts.iter().find_map(|f| { 80 | if let AppOptions::UnixSocket(s) = f { 81 | Some(s) 82 | } else { 83 | None 84 | } 85 | }); 86 | if socket.is_some() && app.input.value().is_empty() && app.input.cursor() == 0 { 87 | let _ = app.input.handle(InputRequest::InsertChar(' ')).is_some(); 88 | for ch in socket.unwrap().chars() { 89 | if app.input.handle(InputRequest::InsertChar(ch)).is_some() {} 90 | } 91 | } 92 | } 93 | InputOpt::CookiePath => { 94 | if let Some(cookie) = app.command.opts.iter().find_map(|f| { 95 | if let AppOptions::CookiePath(s) = f { 96 | Some(s) 97 | } else { 98 | None 99 | } 100 | }) { 101 | if app.input.value().is_empty() && app.input.cursor() == 0 { 102 | let _ = app.input.handle(InputRequest::InsertChar(' ')).is_some(); 103 | for ch in cookie.chars() { 104 | if app.input.handle(InputRequest::InsertChar(ch)).is_some() {} 105 | } 106 | } 107 | } 108 | } 109 | InputOpt::CookieJar => { 110 | if let Some(cookie) = app.command.opts.iter().find_map(|f| { 111 | if let AppOptions::CookieJar(s) = f { 112 | Some(s) 113 | } else { 114 | None 115 | } 116 | }) { 117 | if app.input.value().is_empty() && app.input.cursor() == 0 { 118 | let _ = app.input.handle(InputRequest::InsertChar(' ')).is_some(); 119 | for ch in cookie.chars() { 120 | if app.input.handle(InputRequest::InsertChar(ch)).is_some() {} 121 | } 122 | } 123 | } 124 | } 125 | _ => {} 126 | } 127 | let input = Paragraph::new(app.input.value()) 128 | .style(match app.input_mode { 129 | InputMode::Normal => Style::default().fg(Color::Blue), 130 | InputMode::Editing => Style::default().fg(Color::Yellow), 131 | }) 132 | .block(Block::default().borders(Borders::ALL).title("Input")); 133 | let (msg, style) = match app.input_mode { 134 | InputMode::Normal => ( 135 | Line::from("Press 'i' to start editing"), 136 | Style::default() 137 | .fg(Color::LightBlue) 138 | .add_modifier(tui::style::Modifier::BOLD), 139 | ), 140 | InputMode::Editing => ( 141 | Line::from("Press Enter to submit"), 142 | Style::default() 143 | .fg(Color::Yellow) 144 | .add_modifier(tui::style::Modifier::BOLD), 145 | ), 146 | }; 147 | let msg = Paragraph::new(msg).style(style); 148 | frame.render_widget(msg, top_box[0]); 149 | frame.render_widget(input, top_box[1]); 150 | match app.input_mode { 151 | InputMode::Normal => {} 152 | InputMode::Editing => frame.set_cursor( 153 | top_box[1].x + ((app.input.visual_cursor()).max(scroll) - scroll) as u16 + 1, 154 | top_box[1].y + 1, 155 | ), 156 | } 157 | // we have input (the user has typed something and pressed Enter while in insert mode) 158 | if !app.messages.is_empty() { 159 | app.input_mode = InputMode::Normal; 160 | // parse the input message with the opt to find out what to do with it 161 | parse_input(app.messages[0].clone(), opt, app); 162 | app.messages.remove(0); 163 | } 164 | } 165 | 166 | fn is_valid_unix_socket_path(path: &str) -> Result<(), String> { 167 | let path = Path::new(path); 168 | if path.is_absolute() || path.starts_with("~") { 169 | // Ensure it's a Unix socket file (ends with `.sock` or has no extension) 170 | if let Some(file_name) = path.file_name() { 171 | if let Some(file_name_str) = file_name.to_str() { 172 | if file_name_str.ends_with(".sock") || !file_name_str.contains('.') { 173 | return Ok(()); 174 | } 175 | } 176 | } 177 | Err(SOCKET_ERROR.to_string()) 178 | } else { 179 | Err(SOCKET_ERROR.to_string()) 180 | } 181 | } 182 | 183 | pub fn parse_input(message: String, opt: InputOpt, app: &mut App) { 184 | match opt { 185 | InputOpt::URL => { 186 | app.add_app_option(AppOptions::URL(message)); 187 | } 188 | InputOpt::ApiKey => { 189 | let _ = app.db.as_ref().add_key(&message); 190 | } 191 | InputOpt::UnixSocket => { 192 | if let Err(e) = is_valid_unix_socket_path(&message) { 193 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError(e)))); 194 | } else { 195 | app.add_app_option(AppOptions::UnixSocket(message.clone())); 196 | } 197 | } 198 | InputOpt::Headers => { 199 | if !validate_key_val(&message) { 200 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 201 | String::from(HEADER_ERROR), 202 | )))); 203 | } else { 204 | app.add_app_option(AppOptions::Headers(message.clone())); 205 | } 206 | } 207 | InputOpt::RenameCollection(ref id) => { 208 | if app.db.as_ref().rename_collection(*id, &message).is_ok() { 209 | } else { 210 | app.goto_screen(&Screen::Error("Failed to rename collection".to_string())); 211 | } 212 | } 213 | InputOpt::Output => { 214 | app.add_app_option(AppOptions::Outfile(message)); 215 | } 216 | InputOpt::CookiePath => { 217 | app.add_app_option(AppOptions::CookiePath(message)); 218 | } 219 | InputOpt::CookieJar => { 220 | app.add_app_option(AppOptions::CookieJar(message)); 221 | } 222 | InputOpt::NewCookie => { 223 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::CookieValue(message)))); 224 | } 225 | InputOpt::CmdDescription(id) => { 226 | let coll_id = app 227 | .db 228 | .set_command_description(id, &message) 229 | .unwrap_or_default(); 230 | app.goto_screen(&Screen::SavedCommands { 231 | id: coll_id, 232 | opt: Some(InputOpt::RequestError(String::from("Description Updated"))), 233 | }); 234 | } 235 | InputOpt::CollectionDescription(id) => { 236 | app.db 237 | .set_collection_description(id, &message) 238 | .unwrap_or_default(); 239 | app.goto_screen(&Screen::SavedCollections(Some(InputOpt::RequestError( 240 | String::from("Description Updated"), 241 | )))); 242 | } 243 | InputOpt::CookieValue(ref name) => { 244 | let cookie = format!("{}={};", name, message); 245 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::CookieExpires(cookie)))); 246 | } 247 | InputOpt::CookieExpires(ref cookie) => { 248 | let cookie = format!("{} {}", cookie, message); 249 | app.add_app_option(AppOptions::NewCookie(cookie)); 250 | } 251 | InputOpt::Referrer => { 252 | app.add_app_option(AppOptions::Referrer(message.clone())); 253 | } 254 | InputOpt::CaPath => { 255 | if !validate_path(&message) { 256 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 257 | String::from(CERT_ERROR), 258 | )))); 259 | } else { 260 | app.add_app_option(AppOptions::CaPath(message.clone())); 261 | } 262 | } 263 | InputOpt::UserAgent => { 264 | app.add_app_option(AppOptions::UserAgent(message.clone())); 265 | } 266 | InputOpt::MaxRedirects => { 267 | if let Ok(num) = message.parse::() { 268 | app.add_app_option(AppOptions::MaxRedirects(num)); 269 | } else { 270 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 271 | String::from(PARSE_INT_ERROR), 272 | )))); 273 | } 274 | } 275 | InputOpt::UploadFile => { 276 | if !validate_path(&message) { 277 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 278 | String::from(UPLOAD_FILEPATH_ERROR), 279 | )))); 280 | } 281 | app.add_app_option(AppOptions::UploadFile(message)); 282 | } 283 | InputOpt::Execute => { 284 | // This means they have executed the HTTP Request, and want to write to a file 285 | app.command.set_outfile(&message); 286 | if let Err(e) = app.command.write_output() { 287 | app.goto_screen(&Screen::Response(e.to_string())); 288 | } else { 289 | app.goto_screen(&Screen::Response(String::from( 290 | app.response.as_ref().unwrap_or(&String::new()).as_str(), 291 | ))); 292 | } 293 | } 294 | InputOpt::RequestBody => { 295 | // if the body is a path to a file, we need to read the file and set the body 296 | // otherwise we just set the body 297 | if Path::new(&message).exists() { 298 | match std::fs::read_to_string(&message) { 299 | Ok(body) => { 300 | app.add_app_option(AppOptions::RequestBody(body)); 301 | } 302 | Err(e) => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::AlertMessage( 303 | e.to_string(), 304 | )))), 305 | } 306 | } else { 307 | app.add_app_option(AppOptions::RequestBody(message.clone())); 308 | } 309 | } 310 | InputOpt::ImportCollection => { 311 | if let Err(e) = app.import_postman_collection(&message) { 312 | app.goto_screen(&Screen::SavedCollections(Some(InputOpt::AlertMessage( 313 | e.to_string(), 314 | )))); 315 | } else { 316 | app.goto_screen(&Screen::SavedCollections(Some(InputOpt::AlertMessage( 317 | String::from("Collection Imported"), 318 | )))); 319 | } 320 | } 321 | InputOpt::KeyLabel(id) => match app.db.set_key_label(id, &message) { 322 | Ok(_) => app.goto_screen(&Screen::SavedKeys(Some(InputOpt::AlertMessage( 323 | String::from("Label Updated"), 324 | )))), 325 | Err(e) => app.goto_screen(&Screen::SavedKeys(Some(InputOpt::RequestError(format!( 326 | "Error: {}", 327 | e 328 | ))))), 329 | }, 330 | InputOpt::CmdLabel(id) => match app.db.set_command_label(id, &message) { 331 | Ok(collection_id) => app.goto_screen(&Screen::SavedCommands { 332 | id: collection_id, 333 | opt: Some(InputOpt::AlertMessage(String::from("Label Updated"))), 334 | }), 335 | Err(e) => app.goto_screen(&Screen::SavedCommands { 336 | id: None, 337 | opt: Some(InputOpt::RequestError(format!("Error: {}", e))), 338 | }), 339 | }, 340 | InputOpt::Auth(ref auth) => { 341 | parse_auth(auth, app, &message); 342 | } 343 | _ => {} 344 | } 345 | app.goto_screen(&opt.get_return_screen()); 346 | } 347 | 348 | pub fn render_input_with_prompt<'a, T: Into>>(frame: &mut Frame<'_>, prompt: T) { 349 | // Render the input with the provided prompt 350 | let chunks = Layout::default() 351 | .direction(Direction::Vertical) 352 | .margin(2) 353 | .constraints( 354 | [ 355 | Constraint::Length(1), 356 | Constraint::Length(3), 357 | Constraint::Min(1), 358 | ] 359 | .as_ref(), 360 | ) 361 | .split(frame.size()); 362 | let message = Paragraph::new(prompt); 363 | frame.render_widget(message, chunks[0]); 364 | } 365 | 366 | fn validate_key_val(key_val: &str) -> bool { 367 | let split = key_val.split(':'); 368 | split.count() > 1 369 | } 370 | 371 | fn validate_path(path: &str) -> bool { 372 | Path::new(path).exists() 373 | } 374 | 375 | fn parse_auth(auth: &AuthKind, app: &mut App, message: &str) { 376 | app.command.set_auth(match auth { 377 | AuthKind::Basic(_) => AuthKind::Basic(String::from(message)), 378 | AuthKind::Bearer(_) => AuthKind::Bearer(String::from(message)), 379 | AuthKind::Digest(_) => AuthKind::Digest(String::from(message)), 380 | // above are the only auth options that would ever send us here 381 | _ => AuthKind::None, 382 | }); 383 | if app.command.has_auth() { 384 | app.command 385 | .opts 386 | .retain(|x| !matches!(x, AppOptions::Auth(_))); 387 | } 388 | app.command.opts.push(AppOptions::Auth(match auth { 389 | AuthKind::Basic(_) => AuthKind::Basic(String::from(message)), 390 | AuthKind::Bearer(_) => AuthKind::Bearer(String::from(message)), 391 | AuthKind::Digest(_) => AuthKind::Digest(String::from(message)), 392 | // above are the only auth options that would ever send us here 393 | _ => AuthKind::None, 394 | })); 395 | app.goto_screen(&Screen::RequestMenu(None)); 396 | } 397 | -------------------------------------------------------------------------------- /src/screens/input/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod input_screen; 2 | 3 | pub mod request_body_input; 4 | -------------------------------------------------------------------------------- /src/screens/input/request_body_input.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{App, InputMode}; 2 | use crate::display::inputopt::InputOpt; 3 | use crate::display::AppOptions; 4 | use crate::screens::{centered_rect, Screen, ScreenArea}; 5 | use tui::style::Styled; 6 | use tui::text::Line; 7 | use tui::widgets::{Block, Borders, Paragraph}; 8 | use tui::{ 9 | prelude::{Constraint, Direction, Layout}, 10 | style::{Modifier, Style}, 11 | text::Span, 12 | Frame, 13 | }; 14 | 15 | pub fn handle_req_body_input_screen(app: &mut App, frame: &mut Frame<'_>, _opt: InputOpt) { 16 | let chunks = Layout::default() 17 | .direction(Direction::Vertical) 18 | .constraints( 19 | [ 20 | Constraint::Percentage(25), 21 | Constraint::Percentage(50), 22 | Constraint::Percentage(25), 23 | ] 24 | .as_ref(), 25 | ) 26 | .split(centered_rect(frame.size(), ScreenArea::Center)); 27 | let (msg, style) = match app.input_mode { 28 | InputMode::Normal => ( 29 | vec![ 30 | Span::styled("Press 'h'", Style::default().add_modifier(Modifier::BOLD)), 31 | Span::raw("to go back..."), 32 | Span::styled("Press 'i'", Style::default().add_modifier(Modifier::BOLD)), 33 | Span::raw("to start editing."), 34 | ], 35 | Style::default(), 36 | ), 37 | InputMode::Editing => ( 38 | vec![ 39 | Span::styled("Press Esc\n", Style::default().add_modifier(Modifier::BOLD)), 40 | Span::raw(" to stop editing...\n"), 41 | Span::styled("Press Enter", Style::default().add_modifier(Modifier::BOLD)), 42 | Span::raw(" to submit."), 43 | ], 44 | Style::default(), 45 | ), 46 | }; 47 | let prompt = vec![ 48 | Line::raw("Enter your Request body Or the path to a file containing the body."), 49 | Line::raw(" "), 50 | Line::raw("Example: {\"key\", \"value\"} (no outside quotes needed)\n"), 51 | Line::raw(" "), 52 | Line::raw("a .json filepath will automatically add Content-Type Header"), 53 | Line::raw(" "), 54 | Line::raw("then press Enter to submit"), 55 | ]; 56 | if !app.command.method.needs_reset() { 57 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 58 | String::from("Error: Request Bodies are not allowed for this HTTP method"), 59 | )))); 60 | } 61 | 62 | let msg = Paragraph::new(Line::from(msg)); 63 | let prompt = Paragraph::new(prompt).set_style(style); 64 | frame.render_widget(msg, centered_rect(frame.size(), ScreenArea::Top)); 65 | frame.render_widget(&prompt, chunks[0]); 66 | 67 | let width = chunks[0].width.max(3) - 3; 68 | let scroll = app.input.visual_scroll(width as usize); 69 | if app.get_request_body().is_some_and(|s| !s.is_empty()) && app.input.value().is_empty() { 70 | let body = app.get_request_body().unwrap(); 71 | for ch in body.chars() { 72 | app.input.handle(tui_input::InputRequest::InsertChar(ch)); 73 | } 74 | app.command.set_request_body(""); 75 | } 76 | let input = Paragraph::new(app.input.value()) 77 | .wrap(tui::widgets::Wrap { trim: (true) }) 78 | .style(match app.input_mode { 79 | InputMode::Normal => app.config.get_style(), 80 | InputMode::Editing => Style::default().fg(app.config.get_outline_color()), 81 | }) 82 | .scroll((0, scroll as u16)) 83 | .block(Block::default().borders(Borders::ALL).title("Input")); 84 | frame.render_widget(input, chunks[1]); 85 | match app.input_mode { 86 | InputMode::Normal => {} 87 | InputMode::Editing => frame.set_cursor( 88 | chunks[1].x + ((app.input.visual_cursor()).max(scroll) - scroll) as u16 + 1, 89 | chunks[1].y + 1, 90 | ), 91 | } 92 | 93 | // we have input (the user has typed something and pressed Enter while in insert mode) 94 | if !app.messages.is_empty() { 95 | let input = app.messages[0].clone(); 96 | if app.messages[0].ends_with(".json") && std::path::Path::new(&input).exists() { 97 | let body = std::fs::read_to_string(input).unwrap(); 98 | app.command.set_request_body(&body); 99 | app.add_app_option(AppOptions::RequestBody(body)); 100 | app.add_app_option(AppOptions::ContentHeaders( 101 | crate::display::HeaderKind::ContentType("application/json".to_string()), 102 | )); 103 | app.goto_screen(&Screen::RequestMenu(None)); 104 | app.messages.clear(); 105 | return; 106 | } 107 | app.add_app_option(AppOptions::RequestBody(app.messages[0].clone())); 108 | app.goto_screen(&Screen::RequestMenu(None)); 109 | app.messages.clear(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/screens/method.rs: -------------------------------------------------------------------------------- 1 | use super::render::handle_screen_defaults; 2 | use crate::app::App; 3 | use crate::display::menuopts::METHOD_MENU_OPTIONS; 4 | use crate::request::curl::{Curl, Method}; 5 | use crate::screens::screen::Screen; 6 | use std::str::FromStr; 7 | use tui::Frame; 8 | 9 | pub fn handle_method_select_screen(app: &mut App, frame: &mut Frame<'_>) { 10 | app.clear_all_options(); 11 | app.set_command(Curl::new()); 12 | handle_screen_defaults(app, frame); 13 | if let Some(num) = app.selected { 14 | app.command 15 | .set_method(Method::from_str(METHOD_MENU_OPTIONS[num]).unwrap_or(Method::Get)); // safe index 16 | app.goto_screen(&Screen::RequestMenu(None)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/screens/mod.rs: -------------------------------------------------------------------------------- 1 | use ::tui::prelude::{Color, Text}; 2 | use ::tui::prelude::{Constraint, Direction, Frame, Layout, Rect}; 3 | use ::tui::style::Style; 4 | use ::tui::widgets::{Block, Borders, Paragraph, Wrap}; 5 | pub mod screen; 6 | // Method Select Screens 7 | pub mod cookies; 8 | pub mod method; 9 | // Request Select Screens 10 | pub mod request; 11 | // Response Screen 12 | pub mod more_flags; 13 | pub mod response; 14 | // All Input Type Screens 15 | pub mod input; 16 | // Auth Screen 17 | pub mod auth; 18 | pub mod render; 19 | pub mod saved_keys; 20 | pub use screen::Screen; 21 | pub mod collections; 22 | pub mod error; 23 | pub mod headers; 24 | pub mod saved_commands; 25 | pub fn error_alert_box(frame: &mut Frame<'_>, error_message: &str) -> Rect { 26 | let layout = Layout::default() 27 | .direction(Direction::Vertical) 28 | .constraints(vec![ 29 | Constraint::Length(3), // This will be the alert box 30 | Constraint::Percentage(70), // This aligns the main screen perfectly with the bottom 31 | Constraint::Percentage(27), 32 | ]) 33 | .split(frame.size()); 34 | 35 | // Render the alert box 36 | let alert_box = layout[0]; 37 | let alert_text_chunk = match error_message.starts_with("Error:") { 38 | true => Block::default() 39 | .borders(Borders::ALL) 40 | .style(Style::default().bg(Color::Red).fg(Color::White)) 41 | .title("Alert"), 42 | false => Block::default() 43 | .borders(Borders::ALL) 44 | .style(Style::default().bg(Color::LightBlue).fg(Color::White)) 45 | .title("Success"), 46 | }; 47 | frame.render_widget(alert_text_chunk, alert_box); 48 | let text = Text::styled( 49 | error_message, 50 | Style::default().fg(Color::White).bg(Color::Red), 51 | ); 52 | let alert_text_chunk = Layout::default() 53 | .direction(Direction::Horizontal) 54 | .constraints([Constraint::Percentage(100)].as_ref()) 55 | .split(alert_box)[0]; 56 | frame.render_widget( 57 | Paragraph::new(text) 58 | .alignment(::tui::prelude::Alignment::Center) 59 | .wrap(Wrap { trim: true }), 60 | alert_text_chunk, 61 | ); 62 | 63 | let main_box = layout[1]; 64 | centered_rect(main_box, ScreenArea::Top) 65 | } 66 | 67 | #[derive(Debug, Copy, Clone)] 68 | pub enum ScreenArea { 69 | Top = 0, 70 | Center = 1, 71 | Bottom = 2, 72 | } 73 | // we want to center the rectangle in the middle of the screen 74 | // but we want the padding on the bottom to also be it's own area 75 | // so we split the screen into 3 parts, the top and bottom are padding 76 | pub fn centered_rect(r: Rect, area: ScreenArea) -> Rect { 77 | let chunk = Layout::default() 78 | .direction(Direction::Horizontal) 79 | .constraints( 80 | [ 81 | Constraint::Percentage(10), 82 | Constraint::Percentage(80), 83 | Constraint::Percentage(10), 84 | ] 85 | .as_ref(), 86 | ) 87 | .vertical_margin(2) 88 | .split(r)[1]; 89 | Layout::default() 90 | .direction(Direction::Vertical) 91 | .constraints( 92 | [ 93 | Constraint::Percentage(20), 94 | Constraint::Percentage(60), 95 | Constraint::Percentage(20), 96 | ] 97 | .as_ref(), 98 | ) 99 | .split(chunk)[area as usize] 100 | } 101 | 102 | // ********************************************************************************** 103 | pub fn single_line_input_box(frame_size: Rect) -> Rect { 104 | let chunks = Layout::default() 105 | .direction(Direction::Vertical) 106 | .margin(2) 107 | .constraints( 108 | [ 109 | Constraint::Length(1), 110 | Constraint::Length(3), 111 | Constraint::Min(1), 112 | ] 113 | .as_ref(), 114 | ) 115 | .split(frame_size); 116 | chunks[0] 117 | } 118 | -------------------------------------------------------------------------------- /src/screens/more_flags.rs: -------------------------------------------------------------------------------- 1 | use tui::Frame; 2 | 3 | use crate::app::App; 4 | use crate::display::inputopt::InputOpt; 5 | use crate::display::AppOptions; 6 | use crate::screens::screen::Screen; 7 | 8 | use super::render::handle_screen_defaults; 9 | 10 | pub fn handle_more_flags_screen(app: &mut App, frame: &mut Frame<'_>) { 11 | handle_screen_defaults(app, frame); 12 | match app.selected { 13 | // follow redirects 14 | Some(0) => app.add_app_option(AppOptions::FollowRedirects), 15 | // specify max redirects 16 | Some(1) => app.goto_screen(&Screen::InputMenu(InputOpt::MaxRedirects)), 17 | // proxy tunnel 18 | Some(3) => app.add_app_option(AppOptions::ProxyTunnel), 19 | // Send auth to hosts if redirected 20 | Some(4) => app.add_app_option(AppOptions::UnrestrictedAuth), 21 | // specify referrer 22 | Some(5) => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::Referrer))), 23 | // specify ca-path 24 | Some(6) => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::CaPath))), 25 | // Request certificate info 26 | Some(7) => app.add_app_option(AppOptions::CertInfo), 27 | // fail on error 28 | Some(8) => app.add_app_option(AppOptions::FailOnError), 29 | // wildcard match 30 | Some(9) => app.add_app_option(AppOptions::MatchWildcard), 31 | // user agent 32 | Some(10) => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::UserAgent))), 33 | // enable tcp keepalive 34 | Some(11) => app.add_app_option(AppOptions::TcpKeepAlive), 35 | _ => {} 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/screens/render.rs: -------------------------------------------------------------------------------- 1 | use super::collections::handle_collection_alert_menu; 2 | use super::request::handle_request_menu_screen; 3 | use super::saved_keys::handle_key_menu; 4 | use super::*; 5 | use crate::display::inputopt::InputOpt; 6 | use crate::display::menuopts::{ 7 | API_KEY_PARAGRAPH, API_KEY_TITLE, AUTH_MENU_TITLE, DEFAULT_MENU_PARAGRAPH, DEFAULT_MENU_TITLE, 8 | ERROR_MENU_TITLE, INPUT_MENU_TITLE, POSTMAN_COLLECTION_TITLE, SAVED_COMMANDS_TITLE, 9 | SUCCESS_MENU_TITLE, VIEW_BODY_TITLE, 10 | }; 11 | use crate::display::AppOptions; 12 | use crate::{app::App, display::menuopts::SAVED_COMMANDS_PARAGRAPH}; 13 | use tui::style::Stylize; 14 | use tui::text::Line; 15 | use tui::widgets::ListState; 16 | use tui::widgets::{Block, Borders}; 17 | use tui::{ 18 | layout::Alignment, 19 | style::Style, 20 | text::Text, 21 | widgets::{BorderType, Paragraph}, 22 | Frame, 23 | }; 24 | 25 | use super::{centered_rect, Screen, ScreenArea}; 26 | 27 | /// Renders the user interface widgets. 28 | pub fn render(app: &mut App, frame: &mut Frame<'_>) { 29 | if app.response.is_none() { 30 | // Render Display Options ******************************************* 31 | // This is the box of options the user has selected so far in their current 32 | // command. This is rendered on the bottom of the screen. Each time we change 33 | // app.current_screen, this function is called so we check for any display options 34 | // that were added to app.opts in the previous screen and add them here. 35 | if app.current_screen == Screen::Home { 36 | // the home screen renders the ascii art logo 37 | let logo = Paragraph::new(app.config.get_logo()) 38 | .block(Block::default()) 39 | .style( 40 | app.config 41 | .get_style() 42 | .add_modifier(tui::style::Modifier::BOLD), 43 | ) 44 | .alignment(Alignment::Center); 45 | frame.render_widget(logo, centered_rect(frame.size(), ScreenArea::Bottom)); 46 | } 47 | let area = centered_rect(frame.size(), ScreenArea::Bottom); 48 | let opts = &app.command.opts; 49 | let display_opts = handle_display_options(opts); 50 | frame.render_widget( 51 | Paragraph::new(display_opts) 52 | .block( 53 | Block::default() 54 | .borders(Borders::ALL) 55 | .border_type(BorderType::Double) 56 | .title_style(Style::new().bold().italic()) 57 | .title("* Request Options *"), 58 | ) 59 | .fg(app.config.get_fg_color()) 60 | .bg(app.config.get_bg_color()) 61 | .style(Style::default()) 62 | .alignment(Alignment::Left), 63 | area, 64 | ); 65 | // ****************************************************************************************************** 66 | } else { 67 | let area = centered_rect(frame.size(), ScreenArea::Bottom); 68 | let response = app.response.clone().unwrap(); 69 | let paragraph = Paragraph::new(Text::from(response.as_str())) 70 | .block( 71 | Block::default() 72 | .borders(Borders::ALL) 73 | .border_type(BorderType::Double) 74 | .border_style(Style::new().bold()), 75 | ) 76 | .style(app.config.get_style()) 77 | .alignment(Alignment::Center); 78 | frame.render_widget(paragraph, area); 79 | } 80 | // We pass this off where we match on the current screen and render what we need to 81 | handle_screen(app, frame, app.current_screen.clone()); 82 | } 83 | 84 | pub fn handle_screen_defaults(app: &mut App, frame: &mut Frame<'_>) { 85 | let items = app.get_database_items(); 86 | let menu_options = app.current_screen.get_list(items); 87 | let area = centered_rect(frame.size(), ScreenArea::Center); 88 | 89 | let mut state = ListState::with_selected(ListState::default(), Some(app.cursor)); 90 | app.state = Some(state.clone()); 91 | app.state.as_mut().unwrap().select(Some(app.cursor)); 92 | frame.render_stateful_widget(menu_options, area, &mut state); 93 | let (paragraph, title) = match app.current_screen { 94 | Screen::Home => (&DEFAULT_MENU_PARAGRAPH, &DEFAULT_MENU_TITLE), 95 | Screen::SavedCommands { .. } => (&SAVED_COMMANDS_PARAGRAPH, &SAVED_COMMANDS_TITLE), 96 | Screen::Response(_) => (&DEFAULT_MENU_PARAGRAPH, &DEFAULT_MENU_TITLE), 97 | Screen::InputMenu(_) => (&DEFAULT_MENU_PARAGRAPH, &INPUT_MENU_TITLE), 98 | Screen::Authentication => (&DEFAULT_MENU_PARAGRAPH, &AUTH_MENU_TITLE), 99 | Screen::Success => (&DEFAULT_MENU_PARAGRAPH, &SUCCESS_MENU_TITLE), 100 | Screen::Error(_) => (&DEFAULT_MENU_PARAGRAPH, &ERROR_MENU_TITLE), 101 | Screen::ViewBody => (&DEFAULT_MENU_PARAGRAPH, &VIEW_BODY_TITLE), 102 | Screen::SavedKeys(_) => (&API_KEY_PARAGRAPH, &API_KEY_TITLE), 103 | Screen::HeaderAddRemove => (&DEFAULT_MENU_PARAGRAPH, &DEFAULT_MENU_TITLE), 104 | Screen::SavedCollections(_) => (&DEFAULT_MENU_PARAGRAPH, &POSTMAN_COLLECTION_TITLE), 105 | _ => (&DEFAULT_MENU_PARAGRAPH, &DEFAULT_MENU_TITLE), 106 | }; 107 | frame.render_widget( 108 | render_header_paragraph(paragraph, title, app.config.get_style()), 109 | frame.size(), 110 | ); 111 | } 112 | 113 | pub fn handle_screen(app: &mut App, frame: &mut Frame<'_>, screen: Screen) { 114 | match screen { 115 | // HOME SCREEN ********************************************************* 116 | Screen::Home => { 117 | handle_screen_defaults(app, frame); 118 | if let Some(num) = app.selected { 119 | match num { 120 | 0 => app.goto_screen(&Screen::Method), 121 | 1 => app.goto_screen(&Screen::SavedCommands { 122 | id: None, 123 | opt: None, 124 | }), 125 | 2 => app.goto_screen(&Screen::SavedCollections(None)), 126 | 3 => app.goto_screen(&Screen::SavedKeys(None)), 127 | _ => {} 128 | } 129 | } 130 | } 131 | // METHOD MENU SCREEN *************************************************** 132 | Screen::Method => method::handle_method_select_screen(app, frame), 133 | // INPUT SCREEN **************************************************** 134 | Screen::InputMenu(opt) => { 135 | input::input_screen::handle_default_input_screen(app, frame, opt.clone()); 136 | } 137 | Screen::ViewBody => { 138 | let area = centered_rect(frame.size(), ScreenArea::Center); 139 | let response = app.response.clone().unwrap_or_default(); 140 | let paragraph = Paragraph::new(Text::from(response.as_str())) 141 | .style(app.config.get_style()) 142 | .alignment(Alignment::Center); 143 | frame.render_widget(paragraph, area); 144 | } 145 | // REQUEST MENU ********************************************************* 146 | Screen::RequestMenu(e) => { 147 | handle_request_menu_screen(app, frame, e.as_ref()); 148 | } 149 | // AUTHENTICATION SCREEN ************************************************ 150 | Screen::Authentication => { 151 | auth::handle_authentication_screen(app, frame); 152 | } 153 | // SUCESSS SCREEN ******************************************************* 154 | Screen::Success => handle_screen_defaults(app, frame), 155 | // RESPONSE SCREEN ****************************************************** 156 | Screen::Response(resp) => { 157 | app.set_response(&resp); 158 | response::handle_response_screen(app, frame, resp.to_string()); 159 | } 160 | Screen::SavedCommands { id, opt } => { 161 | saved_commands::handle_saved_commands_screen(app, frame, id, opt); 162 | } 163 | Screen::Headers => { 164 | headers::handle_headers_screen(app, frame); 165 | } 166 | Screen::ColMenu(selected) => { 167 | handle_collection_alert_menu(app, frame, selected); 168 | } 169 | Screen::Error(e) => { 170 | error::handle_error_screen(app, frame, e); 171 | } 172 | Screen::MoreFlags => { 173 | more_flags::handle_more_flags_screen(app, frame); 174 | } 175 | Screen::SavedKeys(opt) => { 176 | saved_keys::handle_saved_keys_screen(app, frame, opt); 177 | } 178 | Screen::CmdMenu { id, opt } => { 179 | saved_commands::handle_alert_menu(app, frame, id, opt); 180 | } 181 | Screen::CookieOptions => { 182 | cookies::handle_cookies_menu(app, frame); 183 | } 184 | Screen::RequestBodyInput => input::request_body_input::handle_req_body_input_screen( 185 | app, 186 | frame, 187 | InputOpt::RequestBody, 188 | ), 189 | Screen::KeysMenu(cmd) => handle_key_menu(app, frame, cmd), 190 | Screen::SavedCollections(opt) => { 191 | super::collections::handle_collection_menu(app, frame, opt); 192 | } 193 | Screen::ViewSavedCollections => { 194 | super::collections::handle_collections_screen(app, frame); 195 | } 196 | _ => {} 197 | } 198 | } 199 | 200 | fn handle_display_options(opts: &[AppOptions]) -> Vec { 201 | opts.iter() 202 | .map(|x| Line::from(x.get_value())) 203 | .collect::>() 204 | } 205 | 206 | #[rustfmt::skip] 207 | pub fn render_header_paragraph(para: &'static str, title: &'static str, style: Style) -> Paragraph<'static> { 208 | Paragraph::new(para) 209 | .block( 210 | Block::default() 211 | .title(title) 212 | .title_alignment(Alignment::Center) 213 | .borders(Borders::ALL) 214 | .border_type(BorderType::Double), 215 | ) 216 | .style(style) 217 | .alignment(Alignment::Center) 218 | } 219 | -------------------------------------------------------------------------------- /src/screens/request.rs: -------------------------------------------------------------------------------- 1 | use super::input::input_screen::handle_default_input_screen; 2 | use super::render::handle_screen_defaults; 3 | use crate::app::App; 4 | use crate::display::inputopt::InputOpt; 5 | use crate::display::menuopts::{SAVE_AUTH_ERROR, VALID_COMMAND_ERROR}; 6 | use crate::display::AppOptions; 7 | use crate::screens::error_alert_box; 8 | use crate::screens::screen::Screen; 9 | use tui::Frame; 10 | 11 | pub fn handle_request_menu_screen(app: &mut App, frame: &mut Frame<'_>, opt: Option<&InputOpt>) { 12 | handle_screen_defaults(app, frame); 13 | match opt { 14 | Some(InputOpt::RequestError(e)) => { 15 | error_alert_box(frame, e); 16 | } 17 | Some(opt) => { 18 | handle_default_input_screen(app, frame, opt.clone()); 19 | } 20 | _ => {} 21 | }; 22 | match app.selected { 23 | // Add a URL, 24 | Some(0) => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::URL))), 25 | // Add file to upload 26 | Some(1) => app.goto_screen(&Screen::RequestMenu(Some(InputOpt::UploadFile))), 27 | // Cookie options 28 | Some(2) => app.goto_screen(&Screen::CookieOptions), 29 | // Auth 30 | Some(3) => app.goto_screen(&Screen::Authentication), 31 | // Headers 32 | Some(4) => app.goto_screen(&Screen::Headers), 33 | // Verbose 34 | Some(5) => app.add_app_option(AppOptions::Verbose), 35 | // Request Body 36 | Some(6) => app.goto_screen(&Screen::RequestBodyInput), 37 | // Save this command 38 | Some(7) => app.add_app_option(AppOptions::SaveCommand), 39 | // Save your token or login 40 | Some(8) => { 41 | if !app.command.has_auth() { 42 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 43 | String::from(SAVE_AUTH_ERROR), 44 | )))); 45 | return; 46 | } 47 | app.add_app_option(AppOptions::SaveToken); 48 | } 49 | // Execute command 50 | Some(9) => { 51 | if app.command.get_url().is_empty() 52 | && !app 53 | .command 54 | .opts 55 | .iter() 56 | .any(|x| *x == AppOptions::SaveCommand) 57 | { 58 | app.goto_screen(&Screen::RequestMenu(Some(InputOpt::RequestError( 59 | String::from(VALID_COMMAND_ERROR), 60 | )))); 61 | return; 62 | } 63 | match app.execute_command() { 64 | Ok(()) => { 65 | let response = app.command.get_response().unwrap_or_default(); 66 | app.set_response(&response); 67 | app.goto_screen(&Screen::Response(response)); 68 | } 69 | Err(e) => { 70 | app.goto_screen(&Screen::Error(e.to_string())); 71 | } 72 | } 73 | } 74 | // more options 75 | Some(10) => app.goto_screen(&Screen::MoreFlags), 76 | // clear options 77 | Some(11) => { 78 | app.clear_all_options(); 79 | app.goto_screen(&Screen::Method); 80 | } 81 | _ => {} 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/screens/response.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use crate::display::inputopt::InputOpt; 3 | use crate::request::response::Response; 4 | use crate::screens::{centered_rect, screen::Screen, ScreenArea}; 5 | use tui::text::Text; 6 | use tui::widgets::{ListState, Paragraph}; 7 | use tui::Frame; 8 | 9 | pub fn handle_response_screen(app: &mut App, frame: &mut Frame<'_>, resp: String) { 10 | let area = centered_rect(frame.size(), ScreenArea::Center); 11 | let new_list = app.current_screen.get_list(None); 12 | let mut state = ListState::with_selected(ListState::default(), Some(app.cursor)); 13 | if !app.items.is_empty() { 14 | app.items.clear(); 15 | } 16 | app.items = app.current_screen.get_opts(None); 17 | app.state = Some(state.clone()); 18 | app.state.as_mut().unwrap().select(Some(app.cursor)); 19 | frame.render_stateful_widget(new_list, area, &mut state); 20 | if let Some(num) = app.selected { 21 | match num { 22 | 0 => { 23 | app.goto_screen(&Screen::InputMenu(InputOpt::Execute)); 24 | } 25 | // View response headers 26 | 1 => { 27 | let area_2 = centered_rect(frame.size(), ScreenArea::Center); 28 | // Check for response error here 29 | let response = match Response::from_raw_string(resp.as_str()) { 30 | Ok(resp) => resp, 31 | Err(e) => { 32 | // Hit the error screen. 33 | app.goto_screen(&Screen::Error(String::from(e))); 34 | return; 35 | } 36 | }; 37 | let headers = response.get_headers(); 38 | let paragraph = Paragraph::new(Text::from(headers.to_string())); 39 | frame.render_widget(paragraph, area_2); 40 | //app.goto_screen(&Screen::SavedCommands); 41 | } 42 | // View response body 43 | 2 => { 44 | app.goto_screen(&Screen::ViewBody); 45 | } 46 | // Copy to clipboard 47 | 3 => { 48 | let cmd_str = app.command.get_command_string(); 49 | app.copy_to_clipboard(&cmd_str).unwrap_or_else(|e| { 50 | app.goto_screen(&Screen::Error(e)); 51 | }); 52 | app.goto_screen(&Screen::Success); 53 | } 54 | 4 => { 55 | // Return To Home 56 | app.clear_all_options(); 57 | app.goto_screen(&Screen::Home); 58 | } 59 | _ => {} 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/screens/saved_commands.rs: -------------------------------------------------------------------------------- 1 | use super::input::input_screen::handle_default_input_screen; 2 | use super::render::render_header_paragraph; 3 | use super::{centered_rect, error_alert_box, Screen, ScreenArea}; 4 | use crate::app::App; 5 | use crate::display::inputopt::InputOpt; 6 | use crate::display::menuopts::{CMD_MENU_OPTIONS, SAVED_COMMANDS_PARAGRAPH, SAVED_COMMANDS_TITLE}; 7 | use tui::prelude::{Constraint, Direction, Layout, Margin}; 8 | use tui::style::{Color, Modifier, Style}; 9 | use tui::text::{Line, Span}; 10 | 11 | use tui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}; 12 | use tui::Frame; 13 | 14 | pub fn handle_saved_commands_screen( 15 | app: &mut App, 16 | frame: &mut Frame<'_>, 17 | coll: Option, 18 | opt: Option, 19 | ) { 20 | let commands = app.db.as_ref().get_commands(coll).unwrap_or_default(); 21 | let items = Some( 22 | commands 23 | .iter() 24 | .map(|x| { 25 | format!( 26 | "Request: {} | Collection: {:?}", 27 | x.label.clone().unwrap_or(String::from("No label")), 28 | match x.collection_name.clone() { 29 | Some(name) => name, 30 | None => "No Collection".to_string(), 31 | } 32 | ) 33 | }) 34 | .collect::>(), 35 | ); 36 | let menu_options = app.current_screen.get_list(items); 37 | let area = centered_rect(frame.size(), ScreenArea::Center); 38 | let mut state = ListState::with_selected(ListState::default(), Some(app.cursor)); 39 | app.state = Some(state.clone()); 40 | app.state.as_mut().unwrap().select(Some(app.cursor)); 41 | frame.render_stateful_widget(menu_options, area, &mut state); 42 | let (paragraph, title) = (&SAVED_COMMANDS_PARAGRAPH, &SAVED_COMMANDS_TITLE); 43 | frame.render_widget( 44 | render_header_paragraph(paragraph, title, app.config.get_style()), 45 | frame.size(), 46 | ); 47 | 48 | match opt { 49 | Some(InputOpt::AlertMessage(msg)) | Some(InputOpt::RequestError(msg)) => { 50 | error_alert_box(frame, &msg); 51 | } 52 | _ => {} 53 | } 54 | if let Some(selected) = app.selected { 55 | let cmd = commands.get(selected); 56 | if let Some(cmd) = cmd { 57 | app.goto_screen(&Screen::CmdMenu { 58 | id: cmd.get_id(), 59 | opt: None, 60 | }); 61 | } else { 62 | app.goto_screen(&Screen::SavedCommands { 63 | id: None, 64 | opt: Some(InputOpt::RequestError("No commands found".to_string())), 65 | }); 66 | } 67 | } 68 | } 69 | 70 | pub fn handle_alert_menu(app: &mut App, frame: &mut Frame<'_>, cmd: i32, opt: Option) { 71 | if let Some(opt) = opt { 72 | handle_default_input_screen(app, frame, opt); 73 | } 74 | let layout = Layout::default() 75 | .direction(Direction::Vertical) 76 | .constraints( 77 | [ 78 | Constraint::Percentage(25), 79 | Constraint::Percentage(50), 80 | Constraint::Percentage(25), 81 | ] 82 | .as_ref(), 83 | ) 84 | .horizontal_margin(5) 85 | .split(frame.size()); 86 | let options_box = layout[1].inner(&Margin { 87 | vertical: 1, 88 | horizontal: 15, 89 | }); 90 | let mut list_state = ListState::with_selected(ListState::default(), Some(app.cursor)); 91 | app.state = Some(list_state.clone()); 92 | let items: Vec = CMD_MENU_OPTIONS 93 | .iter() 94 | .map(|option| ListItem::new(*option)) 95 | .collect(); 96 | let list = List::new(items) 97 | .block(Block::default()) 98 | .highlight_style( 99 | Style::default() 100 | .bg(Color::Blue) 101 | .fg(Color::Black) 102 | .add_modifier(Modifier::BOLD), 103 | ) 104 | .highlight_symbol(">> "); 105 | let cmd_str = Layout::default() 106 | .direction(Direction::Vertical) 107 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) 108 | .split(layout[1])[1]; 109 | if let Ok(command) = app.db.as_ref().get_command_by_id(cmd) { 110 | let collection_name = match app 111 | .db 112 | .as_ref() 113 | .get_collection_by_id(command.collection_id.unwrap_or(0)) 114 | { 115 | Ok(collection) => collection.name, 116 | Err(_) => "No Collection".to_string(), 117 | }; 118 | let alert_text = vec![ 119 | Line::raw("\n"), 120 | Line::default().spans(vec![ 121 | Span::styled("Label: ", Style::default().fg(Color::LightGreen)), 122 | Span::styled( 123 | command.label.clone().unwrap_or("None".to_string()), 124 | Style::default().fg(Color::White), 125 | ), 126 | ]), 127 | Line::default().spans(vec![ 128 | Span::styled("Description: ", Style::default().fg(Color::LightGreen)), 129 | Span::styled( 130 | command.description.clone().unwrap_or("None".to_string()), 131 | Style::default().fg(Color::White), 132 | ), 133 | ]), 134 | Line::default().spans(vec![ 135 | Span::styled("Collection: ", Style::default().fg(Color::LightGreen)), 136 | Span::styled(collection_name, Style::default().fg(Color::White)), 137 | ]), 138 | Line::default().spans(vec![ 139 | Span::styled("ID: ", Style::default().fg(Color::LightGreen)), 140 | Span::styled(command.id.to_string(), Style::default().fg(Color::White)), 141 | ]), 142 | ]; 143 | let alert_text = List::new(alert_text) 144 | .block( 145 | Block::default() 146 | .borders(Borders::ALL) 147 | .title("Command Details"), 148 | ) 149 | .style(Style::default().fg(Color::Blue)) 150 | .highlight_style(Style::default().fg(Color::LightGreen)); 151 | frame.render_stateful_widget(list, options_box, &mut list_state); 152 | frame.render_widget(alert_text, layout[0]); 153 | let header = Block::default().borders(Borders::ALL).title("* Request *"); 154 | frame.render_widget(header, layout[0]); 155 | let paragraph = Paragraph::new(command.get_command()) 156 | .block(Block::default().borders(Borders::ALL).title("* Command *")) 157 | .alignment(tui::layout::Alignment::Center) 158 | .centered() 159 | .wrap(Wrap::default()); 160 | frame.render_widget(paragraph, cmd_str); 161 | match app.selected { 162 | // execute saved command 163 | Some(0) => { 164 | app.execute_saved_command(command.get_curl_json()); 165 | app.goto_screen(&Screen::Response(app.response.clone().unwrap())); 166 | } 167 | // add a label 168 | Some(1) => { 169 | app.goto_screen(&Screen::CmdMenu { 170 | id: cmd, 171 | opt: Some(InputOpt::CmdLabel(cmd)), 172 | }); 173 | } 174 | // add a description 175 | Some(2) => { 176 | app.goto_screen(&Screen::CmdMenu { 177 | id: cmd, 178 | opt: Some(InputOpt::CmdDescription(cmd)), 179 | }); 180 | } 181 | // delete item 182 | Some(3) => { 183 | if let Err(e) = app.delete_item(command.get_id()) { 184 | app.goto_screen(&Screen::SavedCommands { 185 | id: None, 186 | opt: Some(InputOpt::RequestError(format!("Error: {}", e))), 187 | }); 188 | } else { 189 | app.goto_screen(&Screen::SavedCommands { 190 | id: None, 191 | opt: Some(InputOpt::AlertMessage( 192 | "Successfully deleted command".to_string(), 193 | )), 194 | }); 195 | } 196 | } 197 | // copy to clipboard 198 | Some(4) => { 199 | if let Err(e) = app.copy_to_clipboard(command.get_command()) { 200 | app.goto_screen(&Screen::Error(e.to_string())); 201 | } 202 | app.goto_screen(&Screen::SavedCommands { 203 | id: None, 204 | opt: Some(InputOpt::AlertMessage( 205 | "CLI Command copied to clipboard".to_string(), 206 | )), 207 | }); 208 | } 209 | // cancel 210 | Some(5) => { 211 | app.goto_screen(&Screen::SavedCommands { 212 | id: None, 213 | opt: None, 214 | }); 215 | } 216 | _ => {} 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/screens/saved_keys.rs: -------------------------------------------------------------------------------- 1 | use super::input::input_screen::handle_default_input_screen; 2 | use super::render::handle_screen_defaults; 3 | use super::{centered_rect, error_alert_box, Screen, ScreenArea}; 4 | use crate::app::App; 5 | use crate::display::inputopt::InputOpt; 6 | use crate::display::menuopts::KEY_MENU_OPTIONS; 7 | use tui::prelude::{Constraint, Direction, Layout, Margin}; 8 | use tui::style::{Color, Modifier, Style}; 9 | use tui::widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}; 10 | use tui::Frame; 11 | 12 | pub fn handle_saved_keys_screen(app: &mut App, frame: &mut Frame<'_>, opt: Option) { 13 | handle_screen_defaults(app, frame); 14 | match opt { 15 | Some(InputOpt::AlertMessage(msg)) | Some(InputOpt::RequestError(msg)) => { 16 | error_alert_box(frame, &msg); 17 | } 18 | Some(opt) => { 19 | handle_default_input_screen(app, frame, opt.clone()); 20 | } 21 | None => { 22 | if app.items.is_empty() { 23 | let paragraph = Paragraph::new("No Keys Found. Press 'a' to add a new key.").block( 24 | Block::default() 25 | .borders(Borders::ALL) 26 | .border_type(BorderType::Double) 27 | .border_style(Style::default().fg(Color::Red)), 28 | ); 29 | frame.render_widget(paragraph, centered_rect(frame.size(), ScreenArea::Center)) 30 | } else { 31 | let paragraph = 32 | Paragraph::new("Press 'a' to add a new key").style(Style::default()); 33 | frame.render_widget(paragraph, centered_rect(frame.size(), ScreenArea::Top)); 34 | } 35 | } 36 | }; 37 | // if we select a key, open options 38 | if let Some(cmd) = app.selected { 39 | app.goto_screen(&Screen::KeysMenu(cmd)); 40 | } 41 | } 42 | 43 | pub fn handle_key_menu(app: &mut App, frame: &mut Frame<'_>, cmd: usize) { 44 | let layout = Layout::default() 45 | .direction(Direction::Vertical) 46 | .constraints( 47 | [ 48 | Constraint::Percentage(25), 49 | Constraint::Percentage(50), 50 | Constraint::Percentage(25), 51 | ] 52 | .as_ref(), 53 | ) 54 | .horizontal_margin(5) 55 | .split(frame.size()); 56 | // Render the alert box 57 | let alert_box = layout[1]; 58 | let alert_text_chunk = Block::default() 59 | .borders(Borders::ALL) 60 | .style(Style::default().bg(Color::Black).fg(Color::LightRed)) 61 | .title("My API Keys"); 62 | let options_box = layout[1].inner(&Margin { 63 | vertical: 1, 64 | horizontal: 1, 65 | }); 66 | let mut list_state = ListState::with_selected(ListState::default(), Some(app.cursor)); 67 | app.state = Some(list_state.clone()); 68 | let items: Vec = KEY_MENU_OPTIONS 69 | .iter() 70 | .map(|option| ListItem::new(*option)) 71 | .collect(); 72 | let list = List::new(items.clone()) 73 | .block(Block::default()) 74 | .highlight_style( 75 | Style::default() 76 | .bg(Color::White) 77 | .fg(Color::Black) 78 | .add_modifier(Modifier::BOLD), 79 | ) 80 | .highlight_symbol(">> "); 81 | let cmd_str = Layout::default() 82 | .direction(Direction::Vertical) 83 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) 84 | .split(alert_box)[1]; 85 | let show_cmds = app.db.as_ref().get_keys().unwrap(); 86 | let selected = show_cmds.get(cmd).unwrap().clone(); 87 | let paragraph = Paragraph::new(format!("{:?}", selected)) 88 | .block(Block::default().borders(Borders::ALL).title("Selected Key")) 89 | .alignment(tui::layout::Alignment::Center); 90 | frame.render_widget(paragraph, cmd_str); 91 | frame.render_widget(alert_text_chunk, alert_box); 92 | frame.render_stateful_widget(list, options_box, &mut list_state); 93 | match app.selected { 94 | // Add/Edit Label 95 | Some(0) => { 96 | app.goto_screen(&Screen::SavedKeys(Some( 97 | crate::display::inputopt::InputOpt::KeyLabel(selected.get_id()), 98 | ))); 99 | } 100 | // delete item 101 | Some(1) => { 102 | if let Err(e) = app.delete_item(selected.get_id()) { 103 | app.goto_screen(&Screen::SavedKeys(Some(InputOpt::RequestError(format!( 104 | "Error: {e}" 105 | ))))); 106 | } else { 107 | app.goto_screen(&Screen::SavedKeys(Some(InputOpt::AlertMessage( 108 | String::from("Key Deleted"), 109 | )))); 110 | } 111 | } 112 | // copy to clipboard 113 | Some(2) => match app.copy_to_clipboard(selected.get_key()) { 114 | Err(e) => app.goto_screen(&Screen::SavedKeys(Some(InputOpt::RequestError(format!( 115 | "Error: {e}" 116 | ))))), 117 | 118 | Ok(_) => app.goto_screen(&Screen::SavedKeys(Some(InputOpt::AlertMessage( 119 | String::from("Key copied to clipboard"), 120 | )))), 121 | }, 122 | // cancel 123 | Some(3) => { 124 | app.goto_screen(&Screen::SavedKeys(None)); 125 | } 126 | _ => {} 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/screens/screen.rs: -------------------------------------------------------------------------------- 1 | use crate::display::inputopt::InputOpt; 2 | use crate::display::menuopts::{ 3 | AUTHENTICATION_MENU_OPTIONS, CMD_MENU_OPTIONS, COLLECTION_ALERT_MENU_OPTS, 4 | COLLECTION_MENU_OPTIONS, COOKIE_MENU_OPTIONS, HEADER_MENU_OPTIONS, KEY_MENU_OPTIONS, 5 | MAIN_MENU_OPTIONS, METHOD_MENU_OPTIONS, MORE_FLAGS_MENU, NEWLINE, OPTION_PADDING_MAX, 6 | OPTION_PADDING_MID, OPTION_PADDING_MIN, REQUEST_MENU_OPTIONS, RESPONSE_MENU_OPTIONS, 7 | }; 8 | use std::fmt::{Display, Formatter}; 9 | use tui::style::{Color, Modifier, Style}; 10 | use tui::widgets::{Block, Borders, List, ListItem}; 11 | 12 | #[derive(Debug, Default, PartialEq, Clone)] 13 | pub enum Screen { 14 | #[default] 15 | Home, 16 | Method, 17 | HeaderAddRemove, 18 | RequestMenu(Option), 19 | InputMenu(InputOpt), 20 | Response(String), 21 | SavedCollections(Option), 22 | ViewSavedCollections, 23 | Authentication, 24 | Success, 25 | SavedKeys(Option), 26 | ColMenu(i32), 27 | // takes optional collection id 28 | SavedCommands { 29 | id: Option, 30 | opt: Option, 31 | }, 32 | Error(String), 33 | ViewBody, 34 | MoreFlags, 35 | Headers, 36 | CmdMenu { 37 | id: i32, 38 | opt: Option, 39 | }, 40 | KeysMenu(usize), 41 | RequestBodyInput, 42 | CookieOptions, 43 | } 44 | 45 | impl Screen { 46 | pub fn is_input_screen(&self) -> bool { 47 | match self { 48 | Screen::RequestMenu(opt) => opt.is_some(), 49 | Screen::InputMenu(_) => true, 50 | Screen::SavedKeys(opt) => opt.is_some(), 51 | Screen::RequestBodyInput => true, 52 | Screen::SavedCollections(opt) => opt.is_some(), 53 | Screen::CmdMenu { opt, .. } => opt.is_some(), 54 | _ => false, 55 | } 56 | } 57 | } 58 | 59 | impl Display for Screen { 60 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 61 | let screen = match self { 62 | Screen::Home => "Home", 63 | Screen::Method => "Method", 64 | Screen::HeaderAddRemove => "HeaderAddRemove", 65 | Screen::RequestMenu(_) => "RequestMenu", 66 | Screen::InputMenu(_) => "InputMenu", 67 | Screen::Response(_) => "Response", 68 | Screen::Authentication => "Authentication", 69 | Screen::Success => "Success", 70 | Screen::SavedKeys(_) => "Saved Keys", 71 | Screen::SavedCommands { .. } => "My Saved Commands", 72 | Screen::Error(_) => "Error", 73 | Screen::ViewBody => "ViewBody", 74 | Screen::MoreFlags => "MoreFlags", 75 | Screen::Headers => "Headers", 76 | Screen::CmdMenu { .. } => "CmdMenu", 77 | Screen::KeysMenu(_) => "KeysMenu", 78 | Screen::RequestBodyInput => "RequestBodyInput", 79 | Screen::SavedCollections(_) => "Saved Collections", 80 | Screen::ViewSavedCollections => "View Saved Collections", 81 | Screen::ColMenu(_) => "Collection Menu", 82 | Screen::CookieOptions => "Cookie Options", 83 | }; 84 | write!(f, "{}", screen) 85 | } 86 | } 87 | 88 | fn determine_line_size(len: usize) -> &'static str { 89 | match len { 90 | len if len <= 4 => OPTION_PADDING_MAX, 91 | len if len < 8 => OPTION_PADDING_MID, 92 | _ => OPTION_PADDING_MIN, 93 | } 94 | } 95 | 96 | impl<'a> Screen { 97 | pub fn get_opts(&self, items: Option>) -> Vec> { 98 | match &self { 99 | Screen::Home => { 100 | let len = MAIN_MENU_OPTIONS.len(); 101 | MAIN_MENU_OPTIONS 102 | .iter() 103 | .map(|x| format!("{}{}", x, determine_line_size(len))) 104 | .map(ListItem::new) 105 | .collect() 106 | } 107 | Screen::Method => { 108 | let len = METHOD_MENU_OPTIONS.len(); 109 | METHOD_MENU_OPTIONS 110 | .iter() 111 | .map(|x| format!("{}{}", x, determine_line_size(len))) 112 | .map(ListItem::new) 113 | .collect() 114 | } 115 | Screen::HeaderAddRemove => { 116 | let len = METHOD_MENU_OPTIONS.len(); 117 | METHOD_MENU_OPTIONS 118 | .iter() 119 | .map(|x| format!("{}{}", x, determine_line_size(len))) 120 | .map(ListItem::new) 121 | .collect() 122 | } 123 | Screen::RequestMenu(_) => { 124 | let len = REQUEST_MENU_OPTIONS.len(); 125 | REQUEST_MENU_OPTIONS 126 | .iter() 127 | .map(|x| format!("{}{}", x, determine_line_size(len))) 128 | .map(ListItem::new) 129 | .collect() 130 | } 131 | Screen::SavedCommands { .. } => { 132 | let len = REQUEST_MENU_OPTIONS.len(); 133 | items 134 | .unwrap_or(vec!["No Saved Commands".to_string()]) 135 | .iter() 136 | .map(|c| ListItem::new(format!("{}{}", c, determine_line_size(len)))) 137 | .collect() 138 | } 139 | Screen::Response(_) => RESPONSE_MENU_OPTIONS 140 | .iter() 141 | .map(|x| format!("{}{}", x, OPTION_PADDING_MID)) 142 | .map(ListItem::new) 143 | .collect(), 144 | Screen::InputMenu(_) => { 145 | vec![ListItem::new("Input Menu").style(Style::default().fg(Color::Green))] 146 | } 147 | Screen::Headers => HEADER_MENU_OPTIONS 148 | .iter() 149 | .map(|x| format!("{}{}", x, OPTION_PADDING_MID)) 150 | .map(ListItem::new) 151 | .collect(), 152 | Screen::Authentication => { 153 | let len = AUTHENTICATION_MENU_OPTIONS.len(); 154 | AUTHENTICATION_MENU_OPTIONS 155 | .iter() 156 | .map(|x| format!("{}{}", x, determine_line_size(len))) 157 | .map(ListItem::new) 158 | .collect() 159 | } 160 | Screen::Success => { 161 | vec![ListItem::new("Success!").style(Style::default().fg(Color::Green))] 162 | } 163 | Screen::Error(_) => { 164 | vec![ListItem::new("Error!").style(Style::default().fg(Color::Red))] 165 | } 166 | Screen::ViewBody => { 167 | vec![ListItem::new("View Body").style(Style::default().fg(Color::Green))] 168 | } 169 | Screen::RequestBodyInput => { 170 | vec![ListItem::new("Request Body Input").style(Style::default().fg(Color::Green))] 171 | } 172 | Screen::CmdMenu { .. } => CMD_MENU_OPTIONS 173 | .iter() 174 | .map(|i| ListItem::new(format!("{i}{}", NEWLINE))) 175 | .collect(), 176 | Screen::ColMenu(_) => COLLECTION_ALERT_MENU_OPTS 177 | .iter() 178 | .map(|i| ListItem::new(*i)) 179 | .collect(), 180 | Screen::SavedKeys(_) => { 181 | let mut len = 0; 182 | if items.is_some() { 183 | len = items.as_ref().unwrap().len(); 184 | } 185 | items 186 | .unwrap_or(vec!["No Saved Keys".to_string()]) 187 | .iter() 188 | .map(|c| ListItem::new(format!("{}{}", c, determine_line_size(len)))) 189 | .collect() 190 | } 191 | Screen::KeysMenu(_) => KEY_MENU_OPTIONS 192 | .iter() 193 | .map(|i| ListItem::new(format!("{}{}", i, NEWLINE))) 194 | .collect(), 195 | Screen::MoreFlags => { 196 | let len = MORE_FLAGS_MENU.len(); 197 | MORE_FLAGS_MENU 198 | .iter() 199 | .map(|i| { 200 | ListItem::new(format!("{}{}", i, determine_line_size(len))) 201 | .style(Style::default().fg(Color::Red)) 202 | }) 203 | .collect() 204 | } 205 | Screen::ViewSavedCollections => items 206 | .unwrap_or(vec!["No Collections".to_string()]) 207 | .iter() 208 | .map(|c| ListItem::new(format!("{}{}", c, OPTION_PADDING_MIN))) 209 | .collect(), 210 | 211 | Screen::SavedCollections(_) => COLLECTION_MENU_OPTIONS 212 | .iter() 213 | .map(|i| ListItem::new(format!("{}{}", i, OPTION_PADDING_MAX))) 214 | .collect(), 215 | 216 | Screen::CookieOptions => COOKIE_MENU_OPTIONS 217 | .iter() 218 | .map(|c| ListItem::from(format!("{}{}", c, OPTION_PADDING_MID))) 219 | .collect(), 220 | } 221 | } 222 | 223 | pub fn get_list(&self, items: Option>) -> List { 224 | List::new(self.get_opts(items)) 225 | .block( 226 | Block::default() 227 | .title(self.to_string().clone()) 228 | .borders(Borders::ALL), 229 | ) 230 | .style(Style::default().fg(Color::White)) 231 | .highlight_style( 232 | Style::default() 233 | .add_modifier(Modifier::REVERSED) 234 | .add_modifier(Modifier::BOLD) 235 | .add_modifier(Modifier::ITALIC), 236 | ) 237 | .highlight_symbol("󱋰 ") 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/tui_cute.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::panic; 3 | 4 | use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; 5 | use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; 6 | use tui::backend::CrosstermBackend; 7 | use tui::Terminal; 8 | 9 | use crate::app::{App, AppResult}; 10 | use crate::events::event::EventHandler; 11 | use crate::screens::render::render; 12 | 13 | /// Representation of a terminal user interface. 14 | /// 15 | /// It is responsible for setting up the terminal, 16 | /// initializing the interface and handling the draw events. 17 | #[derive(Debug)] 18 | pub struct Tui { 19 | /// Interface to the Terminal. 20 | terminal: Terminal>, 21 | /// Terminal event handler. 22 | pub events: EventHandler, 23 | } 24 | 25 | impl Tui { 26 | /// Constructs a new instance of [`Tui`]. 27 | pub fn new(terminal: Terminal>, events: EventHandler) -> Self { 28 | Self { terminal, events } 29 | } 30 | 31 | /// Initializes the terminal interface. 32 | /// 33 | /// It enables the raw mode and sets terminal properties. 34 | pub fn init(&mut self) -> AppResult<()> { 35 | let mut stdout = io::stdout(); 36 | terminal::enable_raw_mode()?; 37 | crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 38 | 39 | let backend = CrosstermBackend::new(stdout); 40 | let _terminal = Terminal::new(backend)?; 41 | // Define a custom panic hook to reset the terminal properties. 42 | // This way, you won't have your terminal messed up if an unexpected error happens. 43 | let panic_hook = panic::take_hook(); 44 | panic::set_hook(Box::new(move |panic| { 45 | Self::reset().expect("failed to reset the terminal"); 46 | panic_hook(panic); 47 | })); 48 | 49 | self.terminal.hide_cursor()?; 50 | self.terminal.clear()?; 51 | Ok(()) 52 | } 53 | 54 | /// [`Draw`] the terminal interface by [`rendering`] the widgets. 55 | /// 56 | /// [`Draw`]: tui::Terminal::draw 57 | /// [`rendering`]: crate::ui:render 58 | pub fn draw(&mut self, app: &mut App) -> AppResult<()> { 59 | self.terminal.draw(|frame| render(app, frame))?; 60 | Ok(()) 61 | } 62 | 63 | /// Resets the terminal interface. 64 | /// 65 | /// This function is also used for the panic hook to revert 66 | /// the terminal properties if unexpected errors occur. 67 | fn reset() -> AppResult<()> { 68 | terminal::disable_raw_mode()?; 69 | crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)?; 70 | Ok(()) 71 | } 72 | 73 | /// Exits the terminal interface. 74 | /// 75 | /// It disables the raw mode and reverts back the terminal properties. 76 | pub fn exit(&mut self) -> AppResult<()> { 77 | Self::reset()?; 78 | self.terminal.show_cursor()?; 79 | Ok(()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "name": "Test Collection", 4 | "version": "v2.1.0", 5 | "description": "This is a demo collection.", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/" 7 | }, 8 | "item": [ 9 | { 10 | "id": "my-first-item", 11 | "name": "My First Item", 12 | "description": "This is an Item that contains a single HTTP GET request. It doesn't really do much yet!", 13 | "request": { 14 | "description": "This is a sample POST request", 15 | "url": "https://echo.getpostman.com/post", 16 | "method": "POST", 17 | "header": [ 18 | { 19 | "key": "Content-Type", 20 | "value": "application/json" 21 | }, 22 | { 23 | "key": "Host", 24 | "value": "echo.getpostman.com" 25 | } 26 | ], 27 | "body": { 28 | "mode": "urlencoded", 29 | "urlencoded": [ 30 | { 31 | "key": "my-body-variable", 32 | "value": "Something Awesome!" 33 | } 34 | ] 35 | } 36 | }, 37 | "response": [] 38 | } 39 | ] 40 | } 41 | --------------------------------------------------------------------------------