├── .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 |
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 |
--------------------------------------------------------------------------------