1use std::io::{self, Write, BufRead};
2use std::collections::HashMap;
3use regex::Regex;
4use std::{thread, time};
5use std::fs::{self, OpenOptions, File};
6use std::path::Path;
7use std::process::Command;
8
9pub struct State {
14 menu: String,
15 commands: Vec<String>,
16 comment: String,
17 directories: HashMap<String, String>, launch_command: HashMap<String, String>, }
20
21pub fn start_splash() {
22 println!("Fast Files - File directory storage UI for fast access and sorting\n");
23}
24
25fn poll_commands(state: &mut State) -> () { let mut text_entry = String::new();
28 let mut commands: Vec<String> = Vec::new();
29
30 print!("> ");
32 io::stdout().flush().expect("Failed to flush");
33 io::stdin()
34 .read_line(&mut text_entry)
35 .expect("Failed to read line");
36
37 for word in text_entry.split_whitespace() {
39 commands.push(String::from(word));
40 }
41
42 if commands[0].len() != 1 {
44 println!("Invalid command at index 0, character length should be 1");
45 poll_commands(state);
46 }
47
48 let valid_args = &state.commands;
50
51 let _command_level1 = match &commands[0] {
53 x if valid_args.contains(x) => {
54 let _command_level2 = match commands[0].as_str() {
55 "l" => return state.directories(),
56 "o" => return state.open_directory(),
57 "n" => if &state.menu == "Directories" {
58 return state.new_directory()
59 } else if &state.menu == "LaunchCommands" {
60 return state.new_launchcommand()
61 },
62 "r" => if &state.menu == "MainMenu" {
63 return state.refresh_list()
64 } else {
65 return state.main_menu()
66 },
67 "d" => if &state.menu == "MainMenu" {
68 return state.launch_commands()
69 } else if &state.menu == "Directories" {
70 return state.delete_directory()
71 } else if &state.menu == "LaunchCommands" {
72 return state.delete_launchcommand()
73 },
74 "q" => std::process::exit(0),
75 &_ => poll_commands(state),
76 };
77 },
78 &_ => poll_commands(state),
79 };
80 }
83
84impl State {
85
86 pub fn new() -> State {
88 State {
89 menu: String::new(),
90 commands: Vec::new(),
91 comment: String::new(),
92 directories: HashMap::new(),
93 launch_command: HashMap::new(),
94 }
95 }
96
97 pub fn build(&mut self, directories: String, launchcommands: String) {
99 if let Ok(lines) = read_lines(directories) {
101 for line in lines.flatten() {
102 let mut kv_pair: Vec<String> = Vec::new();
103 for keyvalue in line.split_whitespace() {
104 kv_pair.push(keyvalue.to_string());
105 }
106 self.directories.insert(String::from(&kv_pair[0]), String::from(&kv_pair[1]));
107 }
108 }
109 if let Ok(lines) = read_lines(launchcommands) {
111 for line in lines.flatten() {
112 let mut kv_pair: Vec<String> = Vec::new();
113 for keyvalue in line.split_whitespace() {
114 kv_pair.push(keyvalue.to_string());
115 }
116 self.launch_command.insert(String::from(&kv_pair[0]), String::from(&kv_pair[1]));
117 }
118 }
119
120 let _ = File::options().write(true).create(true).open("ff_directories.txt");
121 let _ = File::options().write(true).create(true).open("ff_launchcommands.txt");
122 }
123
124 fn update(&mut self, status: &str) {
127 self.menu = String::from(status);
128 self.update_commands()
129 }
130
131 fn update_commands(&mut self) {
134 match self.menu.as_str() {
135 "MainMenu" => {
136 self.comment = String::from("Select Action\n[l] - List saved directories\n[d] - List default opening process\n[r] - Refresh saved directories\n[q] - Quit\n\n");
137 self.commands = Vec::from(["l".to_string(), "d".to_string(), "r".to_string(), "q".to_string()]);
138 },
139 "Directories" => {
140 self.comment = String::from("Select Action\n[o] - Open file endpoint/number\n[s] - Change sort (current: last modified)\n[n] - New Directory\n[d] - Delete directory\n[r] - Return to main menu\n[q] - Quit\n\n");
141 self.commands = vec!["o".to_string(), "s".to_string(), "n".to_string(), "d".to_string(), "r".to_string(), "q".to_string()];
142 },
143 "LaunchCommands" => {
144 self.comment = String::from("Select Action\n[n] - New Launch Command\n[d] - Delete Launch Command\n[r] - Return to main menu\n[q] - Quit\n\n");
145 self.commands = vec!["n".to_string(), "d".to_string(), "r".to_string(), "q".to_string()];
146 },
147 &_ => ()
148 };
149 }
150
151 fn print_commands(&self) -> &String {
153 &self.comment
154 }
155
156 fn new_directory(&mut self) {
158 let mut text_entry = String::new();
160 let mut commands: Vec<String> = Vec::new();
161
162 print!("\nPlease enter a new filepath...\n> ");
164 io::stdout().flush().expect("Failed to flush");
165 io::stdin()
166 .read_line(&mut text_entry)
167 .expect("Failed to read line");
168
169 for word in text_entry.split_whitespace() {
171 commands.push(String::from(word));
172 }
173
174 let re_endpoint = Regex::new(r"(?<endpoint>[[:word:]]+\.[[:word:]]+)").unwrap();
177 let re_ext = Regex::new(r"(?<ext>\.[[:word:]]+\w$)").unwrap();
178
179 for directory in commands.iter() {
182
183 let cap = re_endpoint.captures(directory).and_then(|cap| {
185 cap.name("endpoint").map(|endpoint| endpoint.as_str())
186 });
187 let endpoint = match cap {
188 Some(endp) => endp,
189 None => "",
190 };
191
192 let cap = re_ext.captures(directory).and_then(|cap| {
194 cap.name("ext").map(|ext| ext.as_str())
195 });
196 let extension = match cap {
197 Some(ex) => ex,
198 None => "",
199 };
200
201 self.directories.insert(String::from(directory), String::from(endpoint));
203 println!("\n{} added and saved to directories...", endpoint);
204
205 let mut registered_extensions: Vec<&str> = Vec::new();
207
208 for (reg_ext, _process) in &self.launch_command {
209 registered_extensions.push(reg_ext);
210 }
211
212 if !registered_extensions.contains(&extension) {
214 println!("New filetype discovered, what process would you like to open '{}' files with?", extension);
215
216 let mut launch_command = String::new();
218
219 print!("> ");
221 io::stdout().flush().expect("Failed to flush");
222 io::stdin()
223 .read_line(&mut launch_command)
224 .expect("Failed to read line");
225
226 let launch_command = launch_command.trim_end();
227
228 println!("\n'{}' file will now be opened with '{}' by default, use 'd' from the default opening process menu to change.", extension, launch_command);
229 self.launch_command.insert(String::from(extension), String::from(launch_command));
230
231 }
232
233 }
234
235 fs::remove_file("ff_directories.txt").unwrap();
237 let mut file = OpenOptions::new()
238 .write(true)
239 .create(true)
240 .open("ff_directories.txt")
241 .expect("Unable to open file");
242
243 for (directory, endpoint) in &self.directories { let data = format!("{directory} {endpoint}\n");
245 file.write(data.as_bytes()).expect("Unable to write to directories");
246 }
247
248 fs::remove_file("ff_launchcommands.txt").unwrap();
250 let mut file = OpenOptions::new()
251 .write(true)
252 .create(true)
253 .open("ff_launchcommands.txt")
254 .expect("Unable to open file");
255
256 for (extension, commands) in &self.launch_command { let data = format!("{extension} {commands}\n");
258 file.write(data.as_bytes()).expect("Unable to write to launchcommands");
259 }
260
261 thread::sleep(time::Duration::from_secs(2));
263 std::process::Command::new("clear").status().unwrap();
264 self.directories();
265 }
266
267 fn new_launchcommand(&mut self) {
268 let mut new_extension = String::new();
270 let mut launch_command = String::new();
271
272 print!("\nPlease enter a file extension...\n> ");
274 io::stdout().flush().expect("Failed to flush");
275 io::stdin()
276 .read_line(&mut new_extension)
277 .expect("Failed to read line");
278
279 let new_extension = new_extension.trim_end();
280
281 print!("\nWhat command would you like to open '{new_extension}' files with?\n> ");
282 io::stdout().flush().expect("Failed to flush");
283 io::stdin()
284 .read_line(&mut launch_command)
285 .expect("Failed to read line");
286
287 let launch_command = launch_command.trim_end();
288
289 println!("\n'{}' files will now be opened with '{}' by default, use 'd' from the main menu to change.", new_extension, launch_command);
290 self.launch_command.insert(String::from(new_extension), String::from(launch_command));
291
292 fs::remove_file("ff_launchcommands.txt").unwrap();
294 let mut file = OpenOptions::new()
295 .write(true)
296 .create(true)
297 .open("ff_launchcommands.txt")
298 .expect("Unable to open file");
299
300 for (extension, commands) in &self.launch_command { let data = format!("{extension} {commands}\n");
302 file.write(data.as_bytes()).expect("Unable to write to launchcommands");
303 }
304
305 thread::sleep(time::Duration::from_secs(2));
307 std::process::Command::new("clear").status().unwrap();
308 self.launch_commands();
309
310 }
311
312 fn delete_directory(&mut self) {
313
314 let mut text_entry = String::new();
316 let mut commands: Vec<String> = Vec::new();
317
318 print!("\nSelect directory number or file name to delete...\n");
320 print!("> ");
321 io::stdout().flush().expect("Failed to flush");
322 io::stdin()
323 .read_line(&mut text_entry)
324 .expect("Failed to read line");
325
326
327 for word in text_entry.split_whitespace() {
329 commands.push(String::from(word));
330 }
331
332 let mut tobe_deleted = String::new();
334
335 for command in commands.iter() {
337 for (key, value) in &self.directories {
338 if value == command {
339 println!("\nDeleteing '{}' from registry... (fast_files does not truly delete files)", command);
340 tobe_deleted = key.to_string();
341 break;
342 }
343 }
344 if tobe_deleted == "" {
345 println!("\nDirectory not found");
346 }
347 self.directories.remove(&tobe_deleted);
348 }
349
350 fs::remove_file("ff_directories.txt").unwrap();
352 let mut file = OpenOptions::new()
353 .write(true)
354 .create(true)
355 .open("ff_directories.txt")
356 .expect("Unable to open file");
357
358 for (directory, endpoint) in &self.directories { let data = format!("{directory} {endpoint}\n");
360 file.write(data.as_bytes()).expect("Unable to write to directory");
361 }
362
363 thread::sleep(time::Duration::from_secs(2));
365 std::process::Command::new("clear").status().unwrap();
366 self.directories();
367 }
368
369 fn delete_launchcommand(&mut self) {
370
371 let mut text_entry = String::new();
373 let mut commands: Vec<String> = Vec::new();
374
375 print!("\nSelect launch command to delete...\n");
377 print!("> ");
378 io::stdout().flush().expect("Failed to flush");
379 io::stdin()
380 .read_line(&mut text_entry)
381 .expect("Failed to read line");
382
383 for word in text_entry.split_whitespace() {
385 commands.push(String::from(word));
386 }
387
388 let mut tobe_deleted = String::new();
390
391 for command in commands.iter() {
393 for (key, _value) in &self.launch_command {
394 if key == command {
395 println!("\nDeleteing '{}' from registry... (fast_files does not truly delete files)", command);
396 tobe_deleted = key.to_string();
397 break;
398 }
399 }
400 if tobe_deleted == "" {
401 println!("\nDirectory not found");
402 }
403 self.launch_command.remove(&tobe_deleted);
404 }
405
406 fs::remove_file("ff_launchcommands.txt").unwrap();
408 let mut file = OpenOptions::new()
409 .write(true)
410 .create(true)
411 .open("ff_launchcommands.txt")
412 .expect("Unable to open file");
413
414 for (extension, commands) in &self.launch_command { let data = format!("{extension} {commands}\n");
416 file.write(data.as_bytes()).expect("Unable to write to launchcommands");
417 }
418
419 thread::sleep(time::Duration::from_secs(2));
421 std::process::Command::new("clear").status().unwrap();
422 self.launch_commands();
423 }
424
425 fn open_directory(&mut self) {
426 let mut ex_endpoint = String::new();
428
429 let mut ex_command = String::new();
431 let mut ex_filepath = String::new();
432
433 print!("\nPlease enter a file or file number to open...\n> ");
435 io::stdout().flush().expect("Failed to flush");
436 io::stdin()
437 .read_line(&mut ex_endpoint)
438 .expect("Failed to read line");
439
440 let ex_endpoint = ex_endpoint.trim_end();
441
442 let re_ext = Regex::new(r"(?<ext>\.[[:word:]]+)").unwrap();
444
445 let cap = re_ext.captures(ex_endpoint).and_then(|cap| {
446 cap.name("ext").map(|ext| ext.as_str())
447 });
448 let ex_extension= match cap {
449 Some(ex) => ex,
450 None => "",
451 };
452
453 for (extension, launch_command) in &self.launch_command {
455 if extension == ex_extension {
456 ex_command = launch_command.to_string();
457 }
458 }
459
460 for (filepath, endpoint) in &self.directories {
462 if endpoint == ex_endpoint {
463 ex_filepath = filepath.to_string();
464 }
465 }
466
467 println!("\nOpening {ex_endpoint} with {ex_command}...");
469 thread::sleep(time::Duration::from_secs(2));
470
471 Command::new(ex_command).arg(ex_filepath).status().expect("Couldn't open file");
473 }
474
475 fn refresh_list(&mut self) {
476 self.directories = HashMap::new();
478 self.launch_command = HashMap::new();
479
480 self.build("ff_directories.txt".to_string(), "ff_launchcommands.txt".to_string());
483
484 println!("\nRefreshing directories and launch commands from file...");
486 thread::sleep(time::Duration::from_secs(2));
487 std::process::Command::new("clear").status().unwrap();
488 print!("{}", self.print_commands());
489 poll_commands(self);
490
491 }
492
493 pub fn main_menu(&mut self) {
497 self.update("MainMenu");
498 std::process::Command::new("clear").status().unwrap();
499 start_splash();
500 print!("{}", self.print_commands());
501 poll_commands(self);
502 }
503
504 pub fn directories(&mut self) {
506 self.update("Directories");
507 std::process::Command::new("clear").status().unwrap();
508
509 println!("Listing Saved Directories...");
511
512 let mut count: i32 = 1;
513 for (_filepath, endpoint) in &self.directories {
514 println!("{}. {}", count, endpoint);
515 count += 1;
516 }
517
518 print!("\n{}", self.print_commands());
519 poll_commands(self);
520 }
521
522 pub fn launch_commands(&mut self) {
524 self.update("LaunchCommands");
525 std::process::Command::new("clear").status().unwrap();
526
527 println!("Listing Saved Opening Actions...");
529
530 let mut count: i32 = 1;
531 for (extension, launchcommand) in &self.launch_command{
532 println!("{}. {} -> {}", count, extension, launchcommand);
533 count += 1;
534 }
535
536 print!("\n{}", self.print_commands());
537 poll_commands(self);
538 }
539
540}
541
542fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
543where P: AsRef<Path>, {
544 let file = File::open(filename)?;
545 Ok(io::BufReader::new(file).lines())
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn generate_state() {
554 let mut state = State::new();
555 assert_eq!(state.menu, "", "testing menu State creation");
556 }
557
558 #[test]
559 fn generate_commands() {
560 let mut state = State::new();
561 assert_eq!(state.commands, "", "testing commands State creation");
562 }
563
564 #[test]
565 fn change_main_menu() {
566 let mut state = State::new();
567 state.directories();
568 state.main_menu();
569 assert_eq!(state.menu, "MainMenu", "testing if fn main_menu() acts correctly");
570 }
571
572 #[test]
573 fn change_directories() {
574 let mut state = State::new();
575 state.main_menu();
576 state.directories();
577 assert_eq!(state.menu, "Directories", "testing if fn directories() acts correctly");
578 }
579
580 #[test]
581 fn display_correct_commands() {
582 let mut state = State::new();
583 state.main_menu();
584 assert_eq!(state.commands,
585 "[l] - List saved directories\n[o] - Open file\n[n] - New Directory\n[r] - Refresh saved directories\n[d] - Default opening process\n[q] - Quit\n",
586 "testing if commands are selected correctly")
587 }
588}
589