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}