pim

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }