├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── screencap.gif └── src ├── app.rs ├── commands.rs ├── file_ops.rs ├── main.rs └── ui.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | .vscode 4 | .DS_Store -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "bitflags" 5 | version = "1.2.1" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "cassowary" 10 | version = "0.3.0" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [[package]] 14 | name = "cfg-if" 15 | version = "0.1.10" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | 18 | [[package]] 19 | name = "either" 20 | version = "1.5.3" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | 23 | [[package]] 24 | name = "itertools" 25 | version = "0.8.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | dependencies = [ 28 | "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", 29 | ] 30 | 31 | [[package]] 32 | name = "libc" 33 | version = "0.2.65" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | 36 | [[package]] 37 | name = "log" 38 | version = "0.4.8" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | dependencies = [ 41 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 42 | ] 43 | 44 | [[package]] 45 | name = "numtoa" 46 | version = "0.1.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | 49 | [[package]] 50 | name = "redox_syscall" 51 | version = "0.1.56" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | 54 | [[package]] 55 | name = "redox_termios" 56 | version = "0.1.1" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | dependencies = [ 59 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", 60 | ] 61 | 62 | [[package]] 63 | name = "termion" 64 | version = "1.5.3" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | dependencies = [ 67 | "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", 68 | "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", 70 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 71 | ] 72 | 73 | [[package]] 74 | name = "tfex" 75 | version = "0.1.0" 76 | dependencies = [ 77 | "termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", 78 | "tui 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", 79 | ] 80 | 81 | [[package]] 82 | name = "tui" 83 | version = "0.6.2" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | dependencies = [ 86 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 87 | "cassowary 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", 89 | "itertools 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 90 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 91 | "termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", 92 | "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 93 | "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 94 | ] 95 | 96 | [[package]] 97 | name = "unicode-segmentation" 98 | version = "1.6.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | 101 | [[package]] 102 | name = "unicode-width" 103 | version = "0.1.6" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | 106 | [metadata] 107 | "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 108 | "checksum cassowary 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 109 | "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 110 | "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 111 | "checksum itertools 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "87fa75c9dea7b07be3138c49abbb83fd4bea199b5cdc76f9804458edc5da0d6e" 112 | "checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" 113 | "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 114 | "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 115 | "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 116 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 117 | "checksum termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330" 118 | "checksum tui 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b422ff4986065d33272b587907654f918a3fe8702786a8110bf68dede0d8ee" 119 | "checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 120 | "checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20" 121 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tfex" 3 | version = "0.1.0" 4 | authors = ["porksausages"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | tui = "0.6.2" 11 | termion = "1.5" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Demo 2 | 3 | # Overview 4 | tfex-rs is a simple \[t\]erminal \[f\]ile \[ex\]plorer written in Rust. It's not very useful in it's current state, and probably never will be. It was written for fun/practice rather than to be actually used. 5 | 6 | # Controls 7 | | Key | Command | 8 | | --- | ------- | 9 | | h | Move selection left | 10 | | j | Move selection down | 11 | | k | Move selection up | 12 | | l | Move selection right | 13 | | c | Copy file | 14 | | x | Cut file | 15 | | v | Paste file | 16 | | : | Enter command mode | 17 | | Esc | Exit command mode | 18 | | Enter | Open folder or execute command | 19 | | Backspace | Move up one directory | 20 | | q | Quit | 21 | 22 | # Working Commands 23 | | Long | Short | Description | 24 | |------|-------|-------------| 25 | | :rename [new name]| :ren | Renames the selected file or directory | 26 | | :delete | :del | Deletes the selected file or directory **[Dangerous - will delete all directory contents too. This is irreversible]**| 27 | | :directory [name]| :dir | Creates a new directory | 28 | 29 | 30 | # Installation 31 | tfx-rs should definitely work on macOS. It'll *probably* work on Linux, and almost definitely won't work on Windows. 32 | * Install rustup (https://rustup.rs) 33 | * Clone this repository (`git clone https://github.com/PorkSausages/tfex-rs.git`) 34 | * Run `cargo install --path /path/to/cloned/repository/` 35 | * Launch by running `tfex` -------------------------------------------------------------------------------- /screencap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PorkSausages/tfex-rs/df4d69f5fc712077550e29387cc4f0edc2631345/screencap.gif -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::io::Stdout; 2 | use std::path; 3 | use std::path::PathBuf; 4 | 5 | use tui::backend::TermionBackend; 6 | use tui::Terminal; 7 | 8 | use termion::raw::RawTerminal; 9 | 10 | use crate::commands; 11 | use crate::file_ops; 12 | use crate::file_ops::DirectoryItem; 13 | 14 | pub struct App<'a> { 15 | pub current_directory: path::PathBuf, 16 | pub terminal: &'a mut Terminal>>, 17 | pub mode: Mode, 18 | pub selection_index: Option, 19 | pub directory_contents: Vec, 20 | pub command_buffer: Vec, 21 | pub error: Option, 22 | pub buffered_file_name: Option, 23 | pub window_height: u16, 24 | 25 | file_buffer: Option>, 26 | max_file_selection: usize, 27 | } 28 | 29 | impl<'a> App<'a> { 30 | pub fn new(terminal: &'a mut Terminal>>) -> App<'a> { 31 | let current_dir = path::PathBuf::from("/"); 32 | let window_height = terminal.size().unwrap().height - 5; //borders + command window height add up to 5 33 | 34 | let mut app = App { 35 | current_directory: current_dir, 36 | terminal, 37 | mode: Mode::Browse, 38 | selection_index: Some(0), 39 | max_file_selection: 0, 40 | directory_contents: Vec::new(), 41 | command_buffer: Vec::new(), 42 | file_buffer: None, 43 | buffered_file_name: None, 44 | error: None, 45 | window_height: window_height, 46 | }; 47 | 48 | if let Err(error) = app.populate_files() { 49 | panic!(format!( 50 | "Error opening {:?}: {:?}", 51 | app.current_directory, 52 | error.kind() 53 | )); 54 | } 55 | 56 | app 57 | } 58 | 59 | pub fn move_selection_down(&mut self) { 60 | if let Some(selection_index) = self.selection_index { 61 | if selection_index < self.max_file_selection - 1 { 62 | self.selection_index = Some(selection_index + 1); 63 | } 64 | } 65 | } 66 | 67 | pub fn move_selection_up(&mut self) { 68 | if let Some(selection_index) = self.selection_index { 69 | if selection_index > 0 { 70 | self.selection_index = Some(selection_index - 1); 71 | } 72 | } 73 | } 74 | 75 | pub fn move_selection_left(&mut self) { 76 | if let Some(selection_index) = self.selection_index { 77 | if selection_index >= self.window_height as usize { 78 | self.selection_index = Some(selection_index - self.window_height as usize); 79 | } else { 80 | self.selection_index = Some(0); 81 | } 82 | } 83 | } 84 | 85 | pub fn move_selection_right(&mut self) { 86 | if let Some(selection_index) = self.selection_index { 87 | if selection_index + self.window_height as usize <= self.directory_contents.len() - 1 { 88 | self.selection_index = Some(selection_index + self.window_height as usize); 89 | } else { 90 | self.selection_index = Some(self.directory_contents.len() - 1); 91 | } 92 | } 93 | } 94 | 95 | pub fn update_window_height(&mut self) { 96 | self.window_height = self.terminal.size().unwrap().height - 5; //borders + command window height add up to 5 97 | } 98 | 99 | pub fn populate_files(&mut self) -> Result<(), std::io::Error> { 100 | let mut files = file_ops::get_files_for_current_directory(&self)?; 101 | 102 | files.sort(); 103 | 104 | self.directory_contents = files; 105 | self.max_file_selection = self.directory_contents.len(); 106 | 107 | if self.max_file_selection == 0 { 108 | self.selection_index = None; 109 | } 110 | 111 | Ok(()) 112 | } 113 | 114 | pub fn change_mode(&mut self, mode: Mode) { 115 | self.mode = mode; 116 | } 117 | 118 | pub fn open_folder(&mut self) { 119 | if let Some(selection_index) = self.selection_index { 120 | if let DirectoryItem::Directory(path) = &self.directory_contents[selection_index] { 121 | let previous_dir = self.current_directory.clone(); 122 | 123 | self.current_directory = PathBuf::from(path); 124 | 125 | if let Err(err) = self.populate_files() { 126 | self.current_directory = previous_dir; 127 | self.error = Some(err.to_string()); 128 | } else { 129 | self.selection_index = Some(0); 130 | } 131 | } 132 | } 133 | } 134 | 135 | pub fn move_up_directory(&mut self) -> Result<(), std::io::Error> { 136 | let current_dir = self.current_directory.to_str().unwrap(); 137 | 138 | if current_dir != "/" { 139 | let mut prev_dir_split: Vec<&str> = current_dir.split("/").collect(); 140 | prev_dir_split.remove(prev_dir_split.len() - 1); 141 | let mut new_dir_string = prev_dir_split.join("/"); 142 | if new_dir_string == "" { 143 | new_dir_string.push_str("/"); 144 | } 145 | 146 | self.current_directory = PathBuf::from(new_dir_string); 147 | self.selection_index = Some(0); 148 | self.populate_files()?; 149 | } 150 | 151 | Ok(()) 152 | } 153 | 154 | pub fn add_to_command_buffer(&mut self, character: char) { 155 | self.command_buffer.push(character); 156 | } 157 | 158 | pub fn execute_command(&mut self) { 159 | let command_string = self.get_command_buffer_as_string(); 160 | self.command_buffer = Vec::new(); 161 | commands::process_command(command_string, self); 162 | 163 | self.change_mode(Mode::Browse); 164 | } 165 | 166 | pub fn get_command_buffer_as_string(&self) -> String { 167 | let mut command_string = String::new(); 168 | for c in &self.command_buffer { 169 | command_string.push(*c); 170 | } 171 | 172 | command_string 173 | } 174 | 175 | pub fn get_selected_file_path(&self) -> Option { 176 | if self.selection_index != None { 177 | let dir_item = self.directory_contents[self.selection_index.unwrap()].clone(); 178 | match dir_item { 179 | DirectoryItem::Directory(path) | DirectoryItem::File((path, _)) => Some(path), 180 | } 181 | } else { 182 | None 183 | } 184 | } 185 | 186 | pub fn load_selected_into_file_buffer(&mut self) { 187 | let result = file_ops::read_file(self); 188 | self.file_buffer = result.0; 189 | self.buffered_file_name = result.1; 190 | } 191 | 192 | pub fn get_buffered_file(&self) -> (Option>, Option) { 193 | (self.file_buffer.clone(), self.buffered_file_name.clone()) 194 | } 195 | 196 | pub fn write_buffered_file(&mut self) { 197 | let result = file_ops::write_file(self); 198 | if let Ok(_) = result { 199 | self.buffered_file_name = None; 200 | self.file_buffer = None; 201 | } 202 | } 203 | } 204 | 205 | #[derive(PartialEq)] 206 | pub enum Mode { 207 | Browse, 208 | Command, 209 | _Select, 210 | } 211 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use crate::file_ops; 3 | 4 | pub fn process_command(command_string: String, app: &mut App) { 5 | //split command buffer 6 | let split_command: Vec = command_string 7 | .trim_start_matches(":") 8 | .split_ascii_whitespace() 9 | .map(|f| f.to_string()) 10 | .collect(); 11 | 12 | let current_dir = &app.current_directory.to_str().unwrap(); 13 | 14 | match split_command[0].to_ascii_uppercase().as_ref() { 15 | "RENAME" | "REN" => app.error = file_ops::rename_file(&split_command, current_dir, &app), 16 | "DELETE" | "DEL" => { 17 | app.error = { 18 | let result = file_ops::delete_file(&app); 19 | app.move_selection_up(); 20 | result 21 | } 22 | } 23 | "DIRECTORY" | "DIR" => app.error = file_ops::create_directory(&split_command, current_dir), 24 | _ => app.error = Some(String::from("Not a command")), 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/file_ops.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::fs::{read_dir, File}; 3 | use std::io::prelude::*; 4 | use std::path::PathBuf; 5 | 6 | use crate::app; 7 | 8 | #[derive(Eq, PartialEq, PartialOrd, Ord, Clone)] 9 | pub enum DirectoryItem { 10 | File((String, u64)), 11 | Directory(String), 12 | } 13 | 14 | pub fn get_files_for_current_directory( 15 | app: &app::App, 16 | ) -> Result, std::io::Error> { 17 | //Get list, unwrap, and convert results to &Path 18 | let dir_items: Vec = match read_dir(app.current_directory.as_path()) { 19 | Ok(val) => val.map(|f| f.unwrap().path()).collect(), 20 | Err(err) => return Err(err), 21 | }; 22 | 23 | //Convert items to DirectoryItem 24 | let mut files: Vec = Vec::new(); 25 | for item in dir_items { 26 | let file = File::open(&item); 27 | 28 | let file_size: u64 = match file { 29 | Ok(file) => (file.metadata().unwrap().len() as f64 / 1000.00).ceil() as u64, 30 | Err(_) => 0, 31 | }; 32 | 33 | if item.is_file() { 34 | let file = DirectoryItem::File((String::from(item.to_str().unwrap()), file_size)); 35 | files.push(file); 36 | } else { 37 | let file = DirectoryItem::Directory(String::from(item.to_str().unwrap())); 38 | files.push(file); 39 | } 40 | } 41 | 42 | Ok(files) 43 | } 44 | 45 | pub fn rename_file(command: &Vec, current_dir: &str, app: &app::App) -> Option { 46 | if command.len() > 1 && app.selection_index != None { 47 | //put new file name back together after originally splitting on whitespace 48 | let new_name_split = &command[1..command.len()]; 49 | let mut concat = String::new(); 50 | for s in new_name_split { 51 | concat.push_str(format!("{} ", s).as_str()); 52 | } 53 | let new_name = concat.trim_end(); 54 | 55 | let current_name = app.get_selected_file_path().unwrap(); 56 | 57 | match fs::rename(current_name, format!("{}/{}", current_dir, new_name)) { 58 | Ok(_) => None, 59 | Err(err) => Some(String::from(err.to_string())), 60 | } 61 | } else { 62 | Some(String::from("Wrong number of arguments supplied")) 63 | } 64 | } 65 | 66 | pub fn delete_file(app: &app::App) -> Option { 67 | if app.selection_index != None { 68 | let selection_index = app.selection_index.unwrap(); 69 | 70 | let result = match &app.directory_contents[selection_index] { 71 | DirectoryItem::Directory(path) => fs::remove_dir_all(path), 72 | DirectoryItem::File((path, _)) => fs::remove_file(path), 73 | }; 74 | 75 | match result { 76 | Ok(_) => None, 77 | Err(err) => Some(String::from(err.to_string())), 78 | } 79 | } else { 80 | Some(String::from("Nothing to delete")) 81 | } 82 | } 83 | 84 | pub fn read_file(app: &mut app::App) -> (Option>, Option) { 85 | let file_path = app.get_selected_file_path(); 86 | if let Some(path) = file_path { 87 | //read the file 88 | let mut file = File::open(&path).unwrap(); 89 | let mut buffer: Vec = Vec::new(); 90 | 91 | //get old filename and store it 92 | let split_path: Vec = path.split("/").map(|s| s.to_string()).collect(); 93 | 94 | let result = file.read_to_end(&mut buffer); 95 | match result { 96 | Ok(_) => (Some(buffer), Some(split_path.last().unwrap().to_string())), 97 | Err(err) => { 98 | app.error = Some(err.to_string()); 99 | (None, None) 100 | } 101 | } 102 | } else { 103 | (None, None) 104 | } 105 | } 106 | 107 | pub fn write_file(app: &mut app::App) -> Result<(), std::io::Error> { 108 | let buffered_file = app.get_buffered_file(); 109 | if buffered_file != (None, None) { 110 | let mut file = File::create(format!( 111 | "{}/{}", 112 | app.current_directory.to_str().unwrap(), 113 | buffered_file.1.clone().unwrap().as_str() 114 | ))?; 115 | 116 | let result = file.write(&buffered_file.0.unwrap()); 117 | 118 | if let Err(err) = result { 119 | app.error = Some(err.to_string()); 120 | Err(err) 121 | } else { 122 | Ok(()) 123 | } 124 | } else { 125 | Ok(()) 126 | } 127 | } 128 | 129 | pub fn create_directory(command: &Vec, current_directory: &str) -> Option { 130 | if command.len() > 1 { 131 | //put new file name back together after originally splitting on whitespace 132 | let new_name_split = &command[1..command.len()]; 133 | let mut concat = String::new(); 134 | for s in new_name_split { 135 | concat.push_str(format!("{} ", s).as_str()); 136 | } 137 | let new_name = concat.trim_end(); 138 | 139 | let result = fs::create_dir(String::from(format!("{}/{}", current_directory, new_name))); 140 | 141 | match result { 142 | Ok(_) => None, 143 | Err(err) => Some(err.to_string()), 144 | } 145 | } else { 146 | Some(String::from("Wrong number of arguments supplied")) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{io, thread, time}; 2 | 3 | use termion::input::TermRead; 4 | use termion::raw::IntoRawMode; 5 | 6 | use tui::backend::TermionBackend; 7 | use tui::Terminal; 8 | 9 | mod app; 10 | mod commands; 11 | mod file_ops; 12 | mod ui; 13 | 14 | use app::App; 15 | 16 | fn main() -> Result<(), io::Error> { 17 | //Initialize terminal 18 | let stdout = io::stdout().into_raw_mode()?; 19 | let backend = TermionBackend::new(stdout); 20 | let mut terminal = Terminal::new(backend)?; 21 | 22 | terminal.clear()?; 23 | 24 | //Initialize input 25 | let mut stdin = termion::async_stdin().keys(); 26 | 27 | //Initialize App state 28 | let mut app = App::new(&mut terminal); 29 | 30 | //Main application loop 31 | loop { 32 | app.update_window_height(); 33 | //Handle input 34 | let input = stdin.next(); 35 | if let Some(Ok(key)) = input { 36 | if app.mode == app::Mode::Browse { 37 | match key { 38 | termion::event::Key::Char('q') => break, 39 | termion::event::Key::Char('j') => app.move_selection_down(), 40 | termion::event::Key::Char('k') => app.move_selection_up(), 41 | termion::event::Key::Char('h') => app.move_selection_left(), 42 | termion::event::Key::Char('l') => app.move_selection_right(), 43 | termion::event::Key::Char('\n') => app.open_folder(), 44 | termion::event::Key::Char(':') => app.change_mode(app::Mode::Command), 45 | termion::event::Key::Backspace => app.move_up_directory()?, 46 | termion::event::Key::Char('c') => app.load_selected_into_file_buffer(), 47 | termion::event::Key::Char('x') => { 48 | app.load_selected_into_file_buffer(); 49 | file_ops::delete_file(&app); 50 | } 51 | termion::event::Key::Char('v') => app.write_buffered_file(), 52 | _ => {} 53 | } 54 | } 55 | 56 | if app.mode == app::Mode::Command { 57 | if let termion::event::Key::Char(chr) = key { 58 | if chr != '\n' { 59 | app.add_to_command_buffer(chr); 60 | } else { 61 | app.execute_command(); 62 | } 63 | } 64 | if key == termion::event::Key::Esc { 65 | app.change_mode(app::Mode::Browse); 66 | app.command_buffer = Vec::new(); 67 | } 68 | if key == termion::event::Key::Backspace { 69 | if app.command_buffer.len() > 1 { 70 | app.command_buffer.truncate(app.command_buffer.len() - 1); 71 | } 72 | } 73 | } 74 | } 75 | 76 | app.populate_files()?; 77 | ui::draw(&mut app)?; 78 | thread::sleep(time::Duration::from_millis(50)); 79 | } 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::PathBuf; 3 | use std::thread; 4 | 5 | use tui::backend::Backend; 6 | use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; 7 | use tui::style::{Color, Modifier, Style}; 8 | use tui::widgets::{Block, Borders, Paragraph, Text, Widget}; 9 | use tui::Frame; 10 | 11 | use crate::app::App; 12 | use crate::file_ops; 13 | 14 | pub fn draw(app: &mut App) -> Result<(), io::Error> { 15 | let command_string = app.get_command_buffer_as_string(); 16 | let mut reset_error = false; 17 | 18 | let App { 19 | current_directory, 20 | terminal, 21 | directory_contents, 22 | selection_index, 23 | error, 24 | .. 25 | } = app; 26 | 27 | terminal.hide_cursor()?; 28 | 29 | terminal.draw(|mut f| { 30 | let chunks = Layout::default() 31 | .direction(Direction::Vertical) 32 | .constraints([Constraint::Min(3), Constraint::Length(3)].as_ref()) 33 | .split(f.size()); 34 | 35 | draw_file_list( 36 | &mut f, 37 | chunks[0], 38 | directory_contents, 39 | selection_index, 40 | current_directory, 41 | ); 42 | 43 | //Error & command box drawing 44 | if let Some(err) = error { 45 | draw_error(&mut f, chunks[1], err); 46 | reset_error = true; 47 | } else { 48 | draw_command_buffer(&mut f, chunks[1], command_string); 49 | } 50 | })?; 51 | 52 | if reset_error { 53 | thread::sleep(std::time::Duration::from_secs(1)); 54 | app.error = None; 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | pub fn draw_file_list( 61 | frame: &mut Frame, 62 | area: Rect, 63 | files: &Vec, 64 | selected_file: &Option, 65 | current_dir: &PathBuf, 66 | ) { 67 | let mut names: Vec = Vec::new(); 68 | let mut sizes: Vec = Vec::new(); 69 | let inner_rect = Rect::new(area.x + 1, area.y + 1, area.width - 1, area.height - 1); //Shrinking the area by 1 in every direction for the text columns, as border is drawn separately 70 | 71 | //Draw the border 72 | Block::default() 73 | .borders(Borders::ALL) 74 | .title(format!("Contents─{}", current_dir.to_str().unwrap()).as_ref()) 75 | .render(frame, area); 76 | 77 | if files.len() != 0 { 78 | //Convert DirectoryItems to Text 79 | for file in files { 80 | match file { 81 | file_ops::DirectoryItem::File((path, size)) => { 82 | let split: Vec<&str> = path.split('/').collect(); 83 | let string = String::from(format!("📄 {}\n", split[split.len() - 1 as usize])); 84 | names.push(Text::raw(string)); 85 | sizes.push(Text::raw(format!("{}KB\n", size.to_string()))); 86 | } 87 | file_ops::DirectoryItem::Directory(path) => { 88 | let split: Vec<&str> = path.split('/').collect(); 89 | let string = String::from(format!("📁 {}\n", split[split.len() - 1 as usize])); 90 | names.push(Text::raw(string)); 91 | sizes.push(Text::raw("\n")); 92 | } 93 | } 94 | } 95 | 96 | //Highlight selected file 97 | if let Some(selection_index) = selected_file { 98 | //Get name of selected file 99 | let selected = match &mut names[*selection_index] { 100 | Text::Raw(value) => value, 101 | _ => "", 102 | } 103 | .to_string(); 104 | 105 | //Replace name of selected file with bold name 106 | names.insert( 107 | *selection_index, 108 | Text::styled( 109 | selected, 110 | Style::default() 111 | .modifier(Modifier::BOLD) 112 | .fg(Color::Indexed(2)), 113 | ), 114 | ); 115 | names.remove(selection_index + 1); 116 | } 117 | 118 | //Figure out number of columns and their spacing 119 | let columns: u16 = (names.len() as f32 / (area.height - 2) as f32).ceil() as u16; 120 | let column_size: u16 = 100 / columns; 121 | let mut constraints: Vec = Vec::new(); 122 | 123 | //Create the constraints 124 | for _ in 1..=columns as u32 { 125 | constraints.push(Constraint::Percentage(column_size)); 126 | } 127 | 128 | //Create the chunks 129 | let chunks = Layout::default() 130 | .direction(Direction::Horizontal) 131 | .constraints(constraints) 132 | .split(inner_rect); 133 | 134 | for i in 0..=columns - 1 { 135 | let height: usize = (area.height - 2) as usize; // -2 to account for the border 136 | let from: usize = (i as usize * height) as usize; 137 | let mut to: usize = (i as usize * height) + (height); 138 | 139 | if to >= names.len() { 140 | to = names.len(); 141 | } 142 | 143 | let names_iter = names[from..to].iter(); 144 | let sizes_iter = sizes[from..to].iter(); 145 | 146 | Paragraph::new(names_iter) 147 | .wrap(false) 148 | .render(frame, chunks[i as usize]); 149 | 150 | Paragraph::new(sizes_iter) 151 | .alignment(Alignment::Right) 152 | .wrap(false) 153 | .render( 154 | frame, 155 | Rect { 156 | //create new Rect that doesn't overlap the border 157 | height: chunks[i as usize].height, 158 | width: chunks[i as usize].width - 2, 159 | x: chunks[i as usize].x, 160 | y: chunks[i as usize].y, 161 | }, 162 | ); 163 | } 164 | } 165 | } 166 | 167 | pub fn draw_command_buffer(frame: &mut Frame, area: Rect, command_string: String) { 168 | let text: Vec = vec![Text::raw(command_string)]; 169 | 170 | Paragraph::new(text.iter()) 171 | .block(Block::default().title("Command").borders(Borders::ALL)) 172 | .render(frame, area); 173 | } 174 | 175 | pub fn draw_error(frame: &mut Frame, area: Rect, error: &String) { 176 | let text: Vec = vec![Text::styled( 177 | error.to_string(), 178 | Style::default().fg(Color::Red), 179 | )]; 180 | 181 | Paragraph::new(text.iter()) 182 | .block( 183 | Block::default() 184 | .title("Error") 185 | .borders(Borders::ALL) 186 | .style(Style::default().fg(Color::Red)), 187 | ) 188 | .render(frame, area); 189 | } 190 | --------------------------------------------------------------------------------