tim_cli/
lib.rs

1use std::fs::{File};
2use std::io::{stdout, Write, Seek, ErrorKind};
3use crossterm::{
4    execute,
5    terminal::{Clear, ClearType, DisableLineWrap, EnableLineWrap},
6    cursor::MoveTo,
7    event::{Event, read, KeyCode, KeyEventKind, KeyModifiers},
8    style::{Color, ResetColor, SetBackgroundColor, SetForegroundColor},
9};
10
11mod t_file;
12use t_file::TFile;
13
14mod t_event;
15use t_event::{InsertEvent, DeleteEvent, Direction, MoveEvent};
16
17mod t_file_explorer;
18use t_file_explorer::TFileExplorer;
19
20
21pub struct Config
22{
23    pub file_path: String,
24    pub file_explorer: bool,
25    pub create_file: bool,
26    pub delete_file: bool,
27    pub rename_file: bool,
28    pub dark: bool,
29    pub light: bool,
30    pub new_file_name: String,
31    pub help: bool,
32    pub keybinds: bool,
33}
34
35
36const MODIFIERS: [&str; 10] = ["-c", "--create", "-d", "--delete", "-r", "--rename", "-b", "--dark", "-l", "--light"];
37
38
39impl Config
40{
41    pub fn default() -> Config
42    {
43        Config {file_path: String::new(),
44                file_explorer: false,
45                create_file: false,
46                delete_file: false,
47                rename_file: false,
48                dark: false,
49                light: false,
50                new_file_name: String::new(),
51                help: false,
52                keybinds: false,
53            }
54    }
55
56    pub fn build(args: &[String]) -> Result<Config, &'static str>
57    {
58        if args.len() <= 1
59        {
60            return Err("Not enough arguments");
61        }
62
63        if args.len() == 2
64        {
65            if args[1] == "--files" || args[1] == "-f"
66            {
67                let mut c = Config::default();
68                c.file_explorer = true;
69                return Ok(c);
70            }
71            if args[1] == "--help" || args[1] == "-h"
72            {
73                let mut c = Config::default();
74                c.help = true;
75                return Ok(c);
76            }
77            if args[1] == "--keybinds" || args[1] == "-k"
78            {
79                let mut c = Config::default();
80                c.keybinds = true;
81                return Ok(c);
82            }
83
84            let mut c = Config::default();
85            c.file_path = args[1].clone();
86            return Ok(c);
87        }
88
89        if args.len() == 3
90        {
91            if !MODIFIERS.contains(&args[2].as_str())
92            {
93                return Err("Invalid modifier");
94            }
95
96            let file_path = args[1].clone();
97
98            if MODIFIERS[0..2].contains(&args[2].as_str())
99            {
100                let mut c = Config::default();
101                c.file_path = file_path;
102                c.create_file = true;
103                return Ok(c);
104            }
105            if MODIFIERS[2..4].contains(&args[2].as_str())
106            {
107                let mut c = Config::default();
108                c.file_path = file_path;
109                c.delete_file = true;
110                return Ok(c);
111            }
112            if MODIFIERS[4..6].contains(&args[2].as_str())
113            {
114                let mut c = Config::default();
115                c.file_path = file_path;
116                c.rename_file = true;
117                return Ok(c);
118            }
119            if MODIFIERS[6..8].contains(&args[2].as_str())
120            {
121                let mut c = Config::default();
122                c.file_path = file_path;
123                c.dark = true;
124                return Ok(c);
125            }
126
127            let mut c = Config::default();
128            c.file_path = file_path;
129            c.light = true;
130            return Ok(c);
131        }
132
133        if args.len() == 4
134        {
135            if !MODIFIERS[4..6].contains(&args[2].as_str())
136            {
137                return Err("Too many arguments");
138            }
139
140            let mut c = Config::default();
141            c.file_path = args[1].clone();
142            c.new_file_name = args[3].clone();
143            c.create_file = true;
144            return Ok(c);
145        }
146
147        Err("Too many arguments")
148    }
149}
150
151
152pub fn run(config: Config) -> Result<(), &'static str>
153{
154    if config.help
155    {
156        print!(r#"Command line text editor like vim. But tim.
157
158Usage: tim <FILE_PATH> [OPTIONS]
159
160Options:
161    -c, --create        Creates but doesn't open file
162    -d, --delete        Deletes file
163    -r, --rename [NAME] Renames file to [NAME] or user inputted
164    -b, --dark          White on black
165    -l, --light         Black on white
166
167Usage: tim [OPTIONS]
168
169Options:
170    -f, --files         Opens a file explorer to pick a file to open
171    -h, --help          Shows commands
172    -k, --keybinds      Shows keybinds/controls
173
174"#);
175        Ok(())
176    }
177    else if config.keybinds
178    {
179        print!(r#"Text Editor:
180    Esc, End, Delete, Ctrl-S => Exit
181    Arrow Keys => Move Cursor
182    Ctrl-Z => Undo
183
184File Explorer:
185    Esc, End, Delete, Ctrl-S => Exit
186    Arrow Keys => Move Cursor
187    Enter, Space => Select
188    Backspace => Parent Directory
189
190"#);
191        Ok(())
192    }
193    else if config.file_explorer
194    {
195        file_explorer()
196    }
197    else if config.create_file
198    {
199        create_file(config.file_path.as_str())
200    }
201    else if config.delete_file
202    {
203        delete_file(config.file_path.as_str())
204    }
205    else if config.rename_file
206    {
207        rename_file(config.file_path.as_str(), config.new_file_name)
208    }
209    else
210    {
211        if config.dark
212        {
213            execute!(
214                stdout(),
215                SetForegroundColor(Color::White),
216                SetBackgroundColor(Color::Black),
217            ).unwrap();
218        }
219        else if config.light
220        {
221            execute!(
222                stdout(),
223                SetForegroundColor(Color::Black),
224                SetBackgroundColor(Color::White),
225            ).unwrap();
226        }
227        text_editor(config.file_path.as_str())
228    }
229}
230
231
232fn create_file(path: &str) -> Result<(), &'static str>
233{
234    match File::create_new(path)
235    {
236        Ok(_) => Ok(()),
237        Err(err) => {
238            match err.kind()
239            {
240                ErrorKind::AlreadyExists => Err("File already exists."),
241                _ => Err("Cannot create file."),
242            }
243        },
244    }
245}
246
247
248fn delete_file(path: &str) -> Result<(), &'static str>
249{
250    match std::fs::remove_file(path)
251    {
252        Ok(_) => Ok(()),
253        Err(err) => {
254            match err.kind()
255            {
256                ErrorKind::NotFound => Err("File doesn't exist."),
257                _ => Err("Cannot delete file."),
258            }
259        },
260    }
261}
262
263
264fn rename_file(path: &str, mut name: String) -> Result<(), &'static str>
265{
266    if name.is_empty()
267    {
268        println!("Enter a new name for the file:");
269
270        name = String::new();
271        match std::io::stdin().read_line(&mut name)
272        {
273            Ok(_) => {},
274            Err(_) => {
275                return Err("Error reading name input.");
276            },
277        }
278        println!();
279    }
280
281    match std::fs::rename(path, name.trim())
282    {
283        Ok(_) => Ok(()),
284        Err(err) => {
285            match err.kind()
286            {
287                ErrorKind::NotFound => Err("File doesn't exist."),
288                _ => { println!("{}", err); Err("Cannot rename file.") },
289            }
290        },
291    }
292}
293
294
295fn file_explorer() -> Result<(), &'static str>
296{
297    let mut t_file_explorer = TFileExplorer::new();
298    let selected_path: &str;
299
300    crossterm::terminal::enable_raw_mode().unwrap();
301
302    execute!(
303        stdout(),
304        DisableLineWrap,
305    ).unwrap();
306
307    t_file_explorer.clear_screen().unwrap();
308
309    loop
310    {
311        match read().unwrap()
312        {
313            Event::Key(event) => {
314                if event.kind == KeyEventKind::Press
315                {
316                    match event.code
317                    {
318                        KeyCode::Esc | KeyCode::End | KeyCode::Delete => { selected_path = ""; break; },
319                        KeyCode::Char('s') =>
320                            {
321                                if event.modifiers == KeyModifiers::CONTROL
322                                {
323                                    selected_path = "";
324                                    break;
325                                }
326                            },
327
328                        KeyCode::Up => { t_file_explorer.move_up().unwrap(); },
329                        KeyCode::Down => { t_file_explorer.move_down().unwrap(); },
330                        KeyCode::Enter | KeyCode::Char(' ') => {
331                            match t_file_explorer.select()
332                            {
333                                None => { t_file_explorer.make_paths(); },
334                                Some(path) => { selected_path = path; break; },
335                            };
336                        },
337                        KeyCode::Backspace => { t_file_explorer.back().unwrap(); },
338                        _ => {},
339                    }
340                }
341            },
342            _ => {},
343        }
344    }
345
346    execute!(
347        stdout(),
348        EnableLineWrap,
349        Clear(ClearType::All),
350        Clear(ClearType::Purge),
351        MoveTo(0, 0),
352    ).unwrap();
353
354    crossterm::terminal::disable_raw_mode().unwrap();
355
356    if selected_path.is_empty()
357    {
358        Ok(())
359    }
360    else
361    {
362        text_editor(selected_path)
363    }
364}
365
366
367fn text_editor(path: &str) -> Result<(), &'static str>
368{
369    let mut t_file;
370    match open_file(path)
371    {
372        Ok(f) => t_file = f,
373        Err(err) => return Err(err),
374    }
375
376    crossterm::terminal::enable_raw_mode().unwrap();
377
378    t_file.clear_screen().unwrap();
379
380    loop
381    {
382        match read().unwrap()
383        {
384            Event::Key(event) => {
385                if event.kind == KeyEventKind::Press
386                {
387                    match event.code
388                    {
389                        KeyCode::Esc | KeyCode::End | KeyCode::Delete => break,
390                        KeyCode::Char('s') =>
391                            {
392                                if event.modifiers == KeyModifiers::CONTROL
393                                {
394                                    break;
395                                }
396                                t_file.add_event(InsertEvent(event.code.to_string()));
397                            },
398
399                        KeyCode::Up => { t_file.add_event(MoveEvent(Direction::Up, 0)); },
400                        KeyCode::Down => { t_file.add_event(MoveEvent(Direction::Down, 0)); },
401                        KeyCode::Left => { t_file.add_event(MoveEvent(Direction::Left, 0)); },
402                        KeyCode::Right => { t_file.add_event(MoveEvent(Direction::Right, 0)); },
403
404                        KeyCode::Enter => { t_file.add_event(InsertEvent(String::from("\n"))); },
405                        KeyCode::Backspace => { t_file.add_event(DeleteEvent(1, String::new())); },
406
407                        KeyCode::Char('z') =>
408                            {
409                                if event.modifiers == KeyModifiers::CONTROL
410                                {
411                                    t_file.undo();
412                                }
413                                else
414                                {
415                                    t_file.add_event(InsertEvent(event.code.to_string()));
416                                }
417                            },
418
419                        KeyCode::Tab => { t_file.add_event(InsertEvent(String::from("    "))); },
420                        KeyCode::Char(' ') => { t_file.add_event(InsertEvent(String::from(" "))); },
421                        _ => {
422                            if event.code.to_string().len() == 1
423                            {
424                                t_file.add_event(InsertEvent(event.code.to_string()));
425                            }
426                        },
427                    }
428                }
429            },
430            _ => {},
431        }
432    }
433
434    execute!(
435        stdout(),
436        ResetColor,
437        Clear(ClearType::All),
438        Clear(ClearType::Purge),
439        MoveTo(0, 0),
440    ).unwrap();
441
442    crossterm::terminal::disable_raw_mode().unwrap();
443
444    t_file.file.set_len(0).unwrap();
445    t_file.file.rewind().unwrap();
446    t_file.file.write(t_file.content.as_ref()).unwrap();
447
448    Ok(())
449}
450
451
452fn open_file(file_path: &str) -> Result<TFile, &'static str>
453{
454    match File::options().write(true).read(true).create(true).open(file_path)
455    {
456        Ok(f) => match TFile::build(f) {
457            Ok(t_file) => Ok(t_file),
458            Err(err) => Err(err),
459        },
460        Err(err) => {
461            match err.kind()
462            {
463                _ => Err("File cannot be opened"),
464            }
465        },
466    }
467}