main.rs (14230B)
1 mod buffer; 2 mod editor; 3 4 use crossterm::{ 5 event::{self, Event, KeyCode}, 6 execute, 7 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, 8 }; 9 use ratatui::{ 10 Terminal, 11 backend::CrosstermBackend, 12 layout::{Constraint, Direction, Layout}, 13 widgets::Paragraph, 14 }; 15 use std::io::{self, Stdout, stdout}; 16 use std::path::PathBuf; 17 18 use crate::buffer::Buffer; 19 use crate::editor::{Editor, Mode}; 20 21 enum AppState { 22 Running, 23 SwapDetected(PathBuf), 24 NewFilePrompt(PathBuf), 25 UnsavedChangesPrompt, 26 Quitting, 27 } 28 29 fn main() -> io::Result<()> { 30 let mut terminal = setup_terminal()?; 31 32 let path = std::env::args().nth(1).map(PathBuf::from); 33 let mut buffer = None; 34 let mut state = AppState::Running; 35 36 if let Some(ref p) = path { 37 let swap_path = p.with_extension(format!( 38 "{}.swp", 39 p.extension().and_then(|e| e.to_str()).unwrap_or("txt") 40 )); 41 if swap_path.exists() { 42 state = AppState::SwapDetected(p.clone()); 43 } else if !p.exists() { 44 state = AppState::NewFilePrompt(p.clone()); 45 } else { 46 buffer = Some(Buffer::from_path(p.clone(), true)?); 47 } 48 } 49 50 let result = run(&mut terminal, buffer, state, path); 51 restore_terminal(&mut terminal)?; 52 result 53 } 54 55 fn setup_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> { 56 enable_raw_mode()?; 57 let mut stdout = stdout(); 58 execute!(stdout, EnterAlternateScreen)?; 59 let backend = CrosstermBackend::new(stdout); 60 Terminal::new(backend) 61 } 62 63 fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> io::Result<()> { 64 disable_raw_mode()?; 65 execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 66 terminal.show_cursor() 67 } 68 69 fn run( 70 terminal: &mut Terminal<CrosstermBackend<Stdout>>, 71 mut buffer: Option<Buffer>, 72 mut state: AppState, 73 _path: Option<PathBuf>, 74 ) -> io::Result<()> { 75 let mut editor = Editor::new(); 76 77 loop { 78 match state { 79 AppState::Quitting => { 80 if let Some(ref b) = buffer { 81 let _ = b.remove_swap_file(); 82 } 83 break; 84 } 85 AppState::SwapDetected(ref p) => { 86 terminal.draw(|f| { 87 let content = format!( 88 "Swap file detected for {:?}. \nRecover? (y: Yes, n: No/Discard)", 89 p 90 ); 91 f.render_widget(Paragraph::new(content), f.area()); 92 })?; 93 94 if let Ok(Event::Key(key)) = event::read() 95 && key.kind == event::KeyEventKind::Press 96 { 97 match key.code { 98 KeyCode::Char('y') => { 99 buffer = Some(Buffer::from_path(p.clone(), true)?); 100 state = AppState::Running; 101 } 102 KeyCode::Char('n') => { 103 buffer = Some(Buffer::from_path(p.clone(), false)?); 104 state = AppState::Running; 105 } 106 _ => {} 107 } 108 } 109 } 110 AppState::NewFilePrompt(ref p) => { 111 terminal.draw(|f| { 112 let content = format!( 113 "File {:?} does not exist. \nCreate new file? (y: Yes, n: No/Quit)", 114 p 115 ); 116 f.render_widget(Paragraph::new(content), f.area()); 117 })?; 118 119 if let Ok(Event::Key(key)) = event::read() 120 && key.kind == event::KeyEventKind::Press 121 { 122 match key.code { 123 KeyCode::Char('y') => { 124 buffer = Some(Buffer::new_empty(p.clone())); 125 state = AppState::Running; 126 } 127 KeyCode::Char('n') => { 128 state = AppState::Quitting; 129 } 130 _ => {} 131 } 132 } 133 } 134 AppState::UnsavedChangesPrompt => { 135 terminal.draw(|f| { 136 let content = "You have unsaved changes. \nSave and quit? (y: Save & Quit, n: Discard & Quit, c: Cancel)"; 137 f.render_widget(Paragraph::new(content), f.area()); 138 })?; 139 140 if let Ok(Event::Key(key)) = event::read() 141 && key.kind == event::KeyEventKind::Press 142 { 143 match key.code { 144 KeyCode::Char('y') => { 145 if let Some(ref mut b) = buffer { 146 b.save()?; 147 } 148 state = AppState::Quitting; 149 } 150 KeyCode::Char('n') => { 151 state = AppState::Quitting; 152 } 153 KeyCode::Char('c') => { 154 state = AppState::Running; 155 } 156 _ => {} 157 } 158 } 159 } 160 AppState::Running => { 161 let mut line_num_width = 0; 162 terminal.draw(|f| { 163 let chunks = Layout::default() 164 .direction(Direction::Vertical) 165 .constraints([Constraint::Min(0), Constraint::Length(1)]) 166 .split(f.area()); 167 168 let mut display_content = String::new(); 169 if let Some(ref b) = buffer { 170 let view_height = chunks[0].height as usize; 171 let total_lines = b.total_lines(); 172 line_num_width = total_lines.to_string().len().max(1); 173 174 let start = editor.scroll_offset; 175 let end = (start + view_height).min(total_lines.max(1)); 176 177 for i in start..end { 178 if let Ok(line) = b.read_line(i) { 179 let line_content = line.replace(['\n', '\r'], ""); 180 let line_display = format!( 181 " {:>width$} | {}\n", 182 i + 1, 183 line_content, 184 width = line_num_width 185 ); 186 display_content.push_str(&line_display); 187 } 188 } 189 } 190 191 f.render_widget(Paragraph::new(display_content), chunks[0]); 192 193 let mode_str = match editor.mode { 194 Mode::Normal => "NORMAL", 195 Mode::Insert => "INSERT", 196 }; 197 198 let line_count = buffer.as_ref().map(|b| b.total_lines()).unwrap_or(0); 199 let modified_flag = if buffer.as_ref().map(|b| b.is_modified()).unwrap_or(false) 200 { 201 " [modified]" 202 } else { 203 "" 204 }; 205 206 let key_hint = match editor.mode { 207 Mode::Normal => "'w': Save, 'u': Undo, 'q': Quit, 'h/j/k/l': Move", 208 Mode::Insert => "'ESC': Exit Insert mode", 209 }; 210 211 let status = format!( 212 "Mode: {}{} | Lines: {} | Cursor: {}:{} | {}", 213 mode_str, 214 modified_flag, 215 line_count, 216 editor.cursor_line + 1, 217 editor.cursor_column + 1, 218 key_hint 219 ); 220 221 f.render_widget(Paragraph::new(status), chunks[1]); 222 })?; 223 224 if let Some(ref b) = buffer { 225 let line = b.read_line(editor.cursor_line).unwrap_or_default(); 226 let line_len = line.replace(['\n', '\r'], "").len(); 227 let max_column = if editor.mode == Mode::Normal { 228 line_len.saturating_sub(1) 229 } else { 230 line_len 231 }; 232 if editor.cursor_column > max_column { 233 editor.cursor_column = max_column; 234 } 235 236 let x = (2 + line_num_width + 3 + editor.cursor_column) as u16; 237 let y = (editor.cursor_line - editor.scroll_offset) as u16; 238 terminal.set_cursor_position((x, y))?; 239 } 240 terminal.show_cursor()?; 241 242 if let Event::Key(key) = event::read()? { 243 if key.kind != event::KeyEventKind::Press { 244 continue; 245 } 246 247 match editor.mode { 248 Mode::Normal => match key.code { 249 KeyCode::Char('q') => { 250 if buffer.as_ref().map(|b| b.is_modified()).unwrap_or(false) { 251 state = AppState::UnsavedChangesPrompt; 252 } else { 253 state = AppState::Quitting; 254 } 255 } 256 KeyCode::Char('w') => { 257 if let Some(ref mut b) = buffer { 258 b.save()?; 259 } 260 } 261 KeyCode::Char('u') => { 262 if let Some(ref mut b) = buffer { 263 b.rollback()?; 264 let line = b.read_line(editor.cursor_line).unwrap_or_default(); 265 let line_len = line.replace(['\n', '\r'], "").len(); 266 if editor.cursor_column >= line_len { 267 editor.cursor_column = line_len.saturating_sub(1); 268 } 269 } 270 } 271 KeyCode::Char('i') => editor.mode = Mode::Insert, 272 KeyCode::Char('h') => editor.move_cursor_left(), 273 KeyCode::Char('l') => { 274 if let Some(ref b) = buffer { 275 let line = b.read_line(editor.cursor_line).unwrap_or_default(); 276 let line_len = line.replace(['\n', '\r'], "").len(); 277 editor.move_cursor_right(line_len.saturating_sub(1)); 278 } 279 } 280 KeyCode::Char('j') => { 281 if let Some(ref b) = buffer { 282 let total = b.total_lines().max(1); 283 editor.move_cursor_down(total); 284 if editor.cursor_line 285 >= editor.scroll_offset + terminal.size()?.height as usize 286 - 1 287 { 288 editor.scroll_down(total, 1); 289 } 290 } 291 } 292 KeyCode::Char('k') if buffer.is_some() => { 293 editor.move_cursor_up(); 294 if editor.cursor_line < editor.scroll_offset { 295 editor.scroll_up(); 296 } 297 } 298 _ => {} 299 }, 300 Mode::Insert => { 301 if key.code == KeyCode::Esc { 302 editor.mode = Mode::Normal; 303 if let Some(ref mut b) = buffer { 304 b.sync_to_swap()?; 305 306 let line = b.read_line(editor.cursor_line).unwrap_or_default(); 307 let line_len = line.replace(['\n', '\r'], "").len(); 308 if editor.cursor_column >= line_len && line_len > 0 { 309 editor.cursor_column = line_len - 1; 310 } 311 } 312 } else if let KeyCode::Char(c) = key.code { 313 if let Some(ref mut b) = buffer { 314 let mut current_line = 315 b.read_line(editor.cursor_line).unwrap_or_default(); 316 current_line = current_line.replace(['\n', '\r'], ""); 317 current_line.insert(editor.cursor_column, c); 318 b.update_line(editor.cursor_line, current_line); 319 editor.cursor_column += 1; 320 } 321 } else if let (KeyCode::Backspace, Some(b)) = (key.code, &mut buffer) 322 && editor.cursor_column > 0 323 { 324 let mut current_line = 325 b.read_line(editor.cursor_line).unwrap_or_default(); 326 current_line = current_line.replace(['\n', '\r'], ""); 327 current_line.remove(editor.cursor_column - 1); 328 b.update_line(editor.cursor_line, current_line); 329 editor.cursor_column -= 1; 330 } 331 } 332 } 333 } 334 } 335 } 336 } 337 Ok(()) 338 }