fileeditor/
fileeditor.rs

1// https://github.com/ingobeans/banano
2
3use cool_rust_input::{
4    set_terminal_line, CoolInput, CustomInputHandler, HandlerContext, InputTransform,
5    KeyPressResult,
6};
7use crossterm::cursor;
8use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers};
9use crossterm::style::{ResetColor, SetBackgroundColor};
10use crossterm::{
11    queue,
12    style::{Color, SetForegroundColor},
13};
14use std::env;
15use std::fs;
16use std::io::stdout;
17
18fn save_file(filename: &str, text: &str) {
19    fs::write(filename, text).expect("Unable to write new contents.");
20}
21pub struct FileEditorInput {
22    pub filename: String,
23    original_text: String,
24    is_new: bool,
25}
26impl FileEditorInput {
27    fn open_filename(filename: String, original_text: String, is_new: bool) -> Self {
28        FileEditorInput {
29            filename,
30            original_text,
31            is_new,
32        }
33    }
34}
35impl CustomInputHandler for FileEditorInput {
36    fn handle_key_press(&mut self, key: &Event, ctx: HandlerContext) -> KeyPressResult {
37        if let Event::Key(key_event) = key {
38            if key_event.kind == crossterm::event::KeyEventKind::Press {
39                if let KeyCode::Char(c) = key_event.code {
40                    if key_event.modifiers.contains(KeyModifiers::CONTROL) {
41                        // exit on CTRL + C
42                        if c == 'c' {
43                            return KeyPressResult::Stop;
44                        }
45                        // save on CTRL + S
46                        if c == 's' {
47                            save_file(&self.filename, &ctx.text_data.text);
48                            self.is_new = false;
49                            self.original_text = ctx.text_data.text.to_owned();
50                            return KeyPressResult::Handled;
51                        }
52                    }
53                }
54            }
55        }
56        KeyPressResult::Continue
57    }
58    fn after_draw_text(&mut self, ctx: HandlerContext) {
59        let _ = queue!(
60            stdout(),
61            SetForegroundColor(Color::Black),
62            SetBackgroundColor(Color::White)
63        );
64        let left_text = format!("BANANO v{}", env!("CARGO_PKG_VERSION"));
65        let center_text = format!("FILE: '{}'", self.filename);
66        let mut right_text = "NOT MODIFIED";
67
68        if self.original_text != ctx.text_data.text {
69            right_text = "MODIFIED";
70        }
71        if self.is_new {
72            right_text = "NEW FILE"
73        }
74
75        let bottom_text_position = (ctx.terminal_size.1 - 1) as usize;
76        let width = self.get_input_transform(ctx).size.0;
77
78        let _ = set_terminal_line(&left_text, 0, 0, true);
79        let _ = set_terminal_line(
80            &center_text,
81            (width as usize - center_text.len()) / 2,
82            0,
83            false,
84        );
85        let _ = set_terminal_line(right_text, width as usize - right_text.len(), 0, false);
86
87        let keybinds = ["^S".to_string(), "^C".to_string()];
88        let descriptions = ["Save File".to_string(), "Exit".to_string()];
89
90        let mut offset = 0;
91        for (keybind, description) in keybinds.iter().zip(descriptions) {
92            let _ = queue!(
93                stdout(),
94                SetForegroundColor(Color::Black),
95                SetBackgroundColor(Color::White)
96            );
97            let _ = set_terminal_line(keybind, offset, bottom_text_position, false);
98            offset += keybind.chars().count() + 1;
99            let _ = queue!(stdout(), ResetColor);
100            let _ = set_terminal_line(&description, offset, bottom_text_position, false);
101            offset += description.chars().count() + 1;
102        }
103    }
104    fn get_input_transform(&mut self, ctx: HandlerContext) -> InputTransform {
105        let size = (ctx.terminal_size.0, ctx.terminal_size.1 - 3);
106        let offset = (0, 2);
107        InputTransform { size, offset }
108    }
109}
110
111pub fn path_exists(path: &str) -> bool {
112    fs::metadata(path).is_ok()
113}
114
115/// A simple Y/N prompt input handler. Automatically stops on first keypress, no enter required.
116pub struct ConfirmationInputHandler {
117    pub prompt: String,
118    pub value: bool,
119}
120impl ConfirmationInputHandler {
121    pub fn prompt(prompt: &str) -> Result<bool, std::io::Error> {
122        let handler = ConfirmationInputHandler {
123            prompt: prompt.to_string(),
124            value: false,
125        };
126        let mut input = CoolInput::new(handler, 0);
127        input.listen()?;
128        Ok(input.custom_input.value)
129    }
130}
131impl CustomInputHandler for ConfirmationInputHandler {
132    fn get_input_transform(&mut self, ctx: HandlerContext) -> InputTransform {
133        let prompt_offset = self.prompt.chars().count() as u16;
134        InputTransform {
135            size: (ctx.terminal_size.0 - prompt_offset, ctx.terminal_size.1),
136            offset: (prompt_offset, 0),
137        }
138    }
139    fn after_update_cursor(&mut self, _: HandlerContext) {
140        let _ = queue!(stdout(), cursor::Hide);
141    }
142    fn after_draw_text(&mut self, _: HandlerContext) {
143        let _ = set_terminal_line(&self.prompt, 0, 0, false);
144    }
145    fn handle_key_press(&mut self, key: &Event, _: HandlerContext) -> KeyPressResult {
146        if let Event::Key(key_event) = key {
147            if key_event.kind == KeyEventKind::Press {
148                // Make CTRL + C stop
149                if let KeyCode::Char(c) = key_event.code {
150                    if c == 'c' && key_event.modifiers.contains(KeyModifiers::CONTROL) {
151                        return KeyPressResult::Stop;
152                    } else if c == 'y' || c == 'n' {
153                        self.value = c == 'y';
154                        return KeyPressResult::Stop;
155                    }
156                }
157            }
158        }
159        KeyPressResult::Handled
160    }
161}
162
163fn main() -> Result<(), std::io::Error> {
164    let args: Vec<_> = env::args().collect();
165    if args.len() != 2 {
166        println!("please specify a filename!");
167        return Ok(());
168    }
169    let filename = &args[1];
170    let mut text = String::new();
171    let mut is_new = true;
172    if path_exists(filename) {
173        text = fs::read_to_string(filename).expect("Unable to read file contents.");
174        is_new = false;
175    }
176    let mut cool_input = CoolInput::new(
177        FileEditorInput::open_filename(filename.to_string(), text.to_owned(), is_new),
178        0,
179    );
180    cool_input.text_data.text = text;
181    cool_input.listen()?;
182    if cool_input.custom_input.original_text != cool_input.text_data.text {
183        let save = ConfirmationInputHandler::prompt("Save file? [y/n]").unwrap();
184        if save {
185            save_file(filename, &cool_input.text_data.text);
186        }
187    }
188    Ok(())
189}