smarkdown 0.1.0

A simple markdown tui editor, simple, fast, good!
use crate::config::Config;
use crate::editor::Buffer;

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Mode {
    Edit,
    Preview,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Prompt {
    None,
    Open(String),
    SaveAs(String),
    ConfirmQuit,
}

pub struct App {
    pub config: Config,
    pub buffer: Buffer,
    pub mode: Mode,
    pub prompt: Prompt,
    pub status_msg: Option<String>,
    pub should_quit: bool,
}

impl App {
    pub fn new(config: Config, buffer: Buffer) -> Self {
        Self {
            config,
            buffer,
            mode: Mode::Edit,
            prompt: Prompt::None,
            status_msg: None,
            should_quit: false,
        }
    }

    pub fn set_status(&mut self, msg: impl Into<String>) {
        self.status_msg = Some(msg.into());
    }

    fn toggle_mode(&mut self) {
        self.mode = match self.mode {
            Mode::Edit => Mode::Preview,
            Mode::Preview => Mode::Edit,
        };
    }
}

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::path::PathBuf;

impl App {
    pub fn handle_key(&mut self, key: KeyEvent) {
        if self.prompt != Prompt::None {
            self.handle_prompt_key(key);
            return;
        }

        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);

        match key.code {
            KeyCode::Tab if ctrl => {
                self.toggle_mode();
                return;
            }
            KeyCode::Char('s') if ctrl => {
                self.save_or_prompt();
                return;
            }
            KeyCode::Char('q') if ctrl => {
                if self.buffer.dirty {
                    self.prompt = Prompt::ConfirmQuit;
                } else {
                    self.should_quit = true;
                }
                return;
            }
            _ => {}
        }

        match self.mode {
            Mode::Edit => self.handle_edit_key(key, ctrl),
            Mode::Preview => self.handle_preview_key(key),
        }
    }

    fn handle_edit_key(&mut self, key: KeyEvent, ctrl: bool) {
        match key.code {
            KeyCode::Char('o') if ctrl => self.prompt = Prompt::Open(String::new()),
            KeyCode::Char('n') if ctrl => {
                self.buffer = Buffer::default();
                self.set_status("New note");
            }
            KeyCode::Char(c) => self.handle_char_input(c),
            KeyCode::Enter => self.buffer.insert_newline(),
            KeyCode::Backspace => self.handle_backspace(),
            KeyCode::Delete => self.buffer.delete_forward(),
            KeyCode::Left => self.buffer.move_left(),
            KeyCode::Right => self.buffer.move_right(),
            KeyCode::Up => self.buffer.move_up(),
            KeyCode::Down => self.buffer.move_down(),
            KeyCode::Home => self.buffer.cursor_col = 0,
            KeyCode::End => {
                self.buffer.cursor_col = self.buffer.lines[self.buffer.cursor_row].chars().count()
            }
            KeyCode::Tab => {
                for _ in 0..self.config.editor.tab_spaces {
                    self.buffer.insert_char(' ');
                }
            }
            _ => {}
        }
    }

    fn handle_char_input(&mut self, c: char) {
        let line = &self.buffer.lines[self.buffer.cursor_row];
        let next_char = line.chars().nth(self.buffer.cursor_col);

        if (is_closer(c) || is_symmetric(c)) && next_char == Some(c) {
            self.buffer.move_right();
            return;
        }
        if let Some(closer) = matching_closer(c) {
            self.buffer.insert_char(c);
            self.buffer.insert_char(closer);
            self.buffer.move_left();
            return;
        }
        self.buffer.insert_char(c);
    }

    fn handle_backspace(&mut self) {
        let row = self.buffer.cursor_row;
        let col = self.buffer.cursor_col;
        if col > 0 {
            let chars: Vec<char> = self.buffer.lines[row].chars().collect();
            let prev = chars[col - 1];
            let next = chars.get(col).copied();
            let is_empty_pair = match prev {
                '(' | '[' | '{' | '"' | '\'' | '`' | '*' | '_' => {
                    next == matching_closer(prev).or(Some(prev))
                }
                _ => false,
            };
            if is_empty_pair {
                self.buffer.delete_forward();
            }
        }
        self.buffer.backspace();
    }


    fn handle_preview_key(&mut self, key: KeyEvent) {
        match key.code {
            KeyCode::Up => {
                if self.buffer.scroll > 0 {
                    self.buffer.scroll -= 1;
                }
            }
            KeyCode::Down => self.buffer.scroll += 1,
            KeyCode::PageUp => {
                self.buffer.scroll = self.buffer.scroll.saturating_sub(10)
            }
            KeyCode::PageDown => self.buffer.scroll += 10,
            _ => {}
        }
    }

    fn save_or_prompt(&mut self) {
        if self.buffer.path.is_some() {
            match self.buffer.save() {
                Ok(_) => self.set_status("Saved."),
                Err(e) => self.set_status(format!("Error saving: {e}")),
            }
        } else {
            self.prompt = Prompt::SaveAs(String::new());
        }
    }

    fn handle_prompt_key(&mut self, key: KeyEvent) {
        match &mut self.prompt {
            Prompt::Open(input) | Prompt::SaveAs(input) => match key.code {
                KeyCode::Char(c) => input.push(c),
                KeyCode::Backspace => {
                    input.pop();
                }
                KeyCode::Esc => self.prompt = Prompt::None,
                KeyCode::Enter => {
                    let path = PathBuf::from(input.trim());
                    let is_open = matches!(self.prompt, Prompt::Open(_));
                    self.prompt = Prompt::None;
                    if is_open {
                        match Buffer::from_file(path) {
                            Ok(buf) => {
                                self.buffer = buf;
                                self.set_status("File opened.");
                            }
                            Err(e) => self.set_status(format!("Error opening: {e}")),
                        }
                    } else {
                        match self.buffer.save_as(path) {
                            Ok(_) => self.set_status("Saved."),
                            Err(e) => self.set_status(format!("Error saving: {e}")),
                        }
                    }
                }
                _ => {}
            },
            Prompt::ConfirmQuit => match key.code {
                KeyCode::Char('y') | KeyCode::Char('Y') => self.should_quit = true,
                _ => self.prompt = Prompt::None,
            },
            Prompt::None => {}
        }
    }
}

fn matching_closer(c: char) -> Option<char> {
    match c {
        '(' => Some(')'),
        '[' => Some(']'),
        '{' => Some('}'),
        '"' => Some('"'),
        '\'' => Some('\''),
        '`' => Some('`'),
        '*' => Some('*'),
        '_' => Some('_'),
        _ => None,
    }
}

fn is_closer(c: char) -> bool {
    matches!(c, ')' | ']' | '}')
}

fn is_symmetric(c: char) -> bool {
    matches!(c, '"' | '\'' | '`' | '*' | '_')
}