/* * Copyright (c) 2023 Marceline Cramer * SPDX-License-Identifier: AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ use std::{ffi::OsString, fs::OpenOptions, io::Write, path::PathBuf, sync::Arc}; use crossbeam_channel::Sender; use crossterm::{ cursor, event::{Event, KeyCode, KeyEvent}, terminal, QueueableCommand, Result, }; use parking_lot::Mutex; use ropey::Rope; use crate::{ buffer::Buffer, config::{Config, ThemeWatcher}, keybinds::{Key, Keybind}, theme::{StyleStore, StyledString}, Cursor, Direction, }; #[derive(Clone, Debug, Default)] pub struct CommandState { pub buf: String, pub cursor: usize, } #[derive(Copy, Clone, Debug)] pub struct InsertState { pub append: bool, } #[derive(Clone, Debug, Default)] pub enum Mode { #[default] Normal, Command(CommandState), Visual, Insert(InsertState), } impl Mode { pub fn cursor_style(&self) -> cursor::SetCursorStyle { use cursor::SetCursorStyle as Style; match self { Mode::Normal => Style::SteadyBlock, Mode::Visual => Style::BlinkingBlock, Mode::Insert(_) => Style::BlinkingBar, Mode::Command(_) => Style::SteadyUnderScore, } } } pub struct Rect { pub position: (usize, usize), pub size: (usize, usize), } pub struct Window { pub rect: Rect, pub contents: Vec, } impl Window { pub fn draw( &self, out: &mut impl QueueableCommand, styles: &mut StyleStore, ) -> crossterm::Result<()> { let x = self.rect.position.0 as u16; let base = *styles.get_scope("ui.window"); let width = self.rect.size.0; let clip = 0..width; let padding: String = std::iter::repeat(' ').take(width).collect(); for row in 0..self.rect.size.1 { let y = self.rect.position.1 as u16 + row as u16; out.queue(cursor::MoveTo(x, y))?; let remaining = if let Some(line) = self.contents.get(row) { line.print_sub(out, clip.clone(), base)? } else { width }; let padding = &padding[0..remaining]; base.print_styled(out, padding)?; } Ok(()) } } pub struct State { pub styles: Arc>, pub config: Config, pub buffer: Buffer, pub undo_buffers: Vec, pub redo_buffers: Vec, pub cursor: Cursor, pub file: Option, pub window: Option, pub mode: Mode, pub submode: Option, pub last_saved: Rope, pub scroll: Cursor, pub size: (usize, usize), pub quit: bool, pub error: Option, pub theme_tx: Sender, } impl State { pub fn from_str(file_name: Option, text: &str) -> Result { let styles = Arc::new(Mutex::new(StyleStore::default())); let buffer = Buffer::from_str(styles.clone(), text); let last_saved = buffer.as_ref().clone(); let (cols, rows) = terminal::size()?; let theme_tx = ThemeWatcher::spawn(styles.clone()); Ok(Self { styles, config: Default::default(), buffer, undo_buffers: Vec::new(), redo_buffers: Vec::new(), cursor: Cursor::default(), file: file_name, mode: Mode::default(), submode: None, last_saved, scroll: Cursor::default(), size: (cols as usize, rows as usize), quit: false, error: None, theme_tx, window: None, }) } pub fn draw(&mut self, out: &mut impl Write) -> Result<()> { // begin update let (cols, rows) = terminal::size()?; out.queue(terminal::BeginSynchronizedUpdate)?; out.queue(terminal::Clear(terminal::ClearType::All))?; let mut styles = self.styles.lock(); // draw status line let mut set_cursor_pos = None; let mut show_status_bar = false; if let Mode::Command(CommandState { buf, cursor }) = &self.mode { let col = *cursor as u16 + 1; let row = rows - 1; out.queue(cursor::MoveTo(0, row))?; write!(out, ":{}", buf)?; set_cursor_pos = Some((col, row)); show_status_bar = true; } else if let Some(error) = self.error.as_ref() { let error_style = styles.get_scope("error"); out.queue(cursor::MoveTo(0, rows - 1))?; error_style.print_styled(out, error)?; show_status_bar = true; } // done with styles drop(styles); // draw buffer let buffer_rows = if show_status_bar { rows - 1 } else { rows }; let lr_width = self .buffer .draw(cols, buffer_rows, self.scroll, self.cursor, out)?; // draw submode if let Some(submode) = self.submode { out.queue(cursor::MoveTo(cols - 10, rows - 1))?; write!(out, "{:?}", submode)?; } // draw open window if let Some(window) = self.window.as_ref() { let mut styles = self.styles.lock(); window.draw(out, &mut styles)?; } // draw cursor let cursor_pos = set_cursor_pos.unwrap_or_else(|| { // calculate cursor position on buffer let cursor = self.buffer.clamped_cursor(self.cursor); let col = cursor.column.saturating_sub(self.scroll.column) as u16; let row = cursor.line.saturating_sub(self.scroll.line) as u16; let col = col + lr_width as u16; (col, row) }); out.queue(cursor::MoveTo(cursor_pos.0, cursor_pos.1))?; out.queue(self.mode.cursor_style())?; // finish update out.queue(terminal::EndSynchronizedUpdate)?; out.flush()?; Ok(()) } /// Sets the current error message. pub fn set_error(&mut self, error: impl ToString) { self.error = Some(error.to_string()); } pub fn on_event(&mut self, event: Event) { // reset the error from the last event self.error = None; // close open window self.window.take(); match event { Event::Resize(cols, rows) => { self.size = (cols as usize, rows as usize); } Event::Key(KeyEvent { code, .. }) => match &self.mode { Mode::Normal | Mode::Visual => { self.on_key(code); } Mode::Command(state) => self.on_command_key(code, state.clone()), Mode::Insert(_) => { if !self.on_key(code) { if let KeyCode::Char(c) = code { self.buffer.insert_char(self.cursor, c); self.move_cursor(Direction::Right); } } } }, _ => {} } } fn on_command_key(&mut self, code: KeyCode, mut state: CommandState) { match code { KeyCode::Char(c) => { state.buf.insert(state.cursor, c); state.cursor += 1; } KeyCode::Backspace => { if state.cursor > 0 { state.cursor -= 1; state.buf.remove(state.cursor); } } KeyCode::Delete if state.cursor < state.buf.len() => { state.buf.remove(state.cursor); } KeyCode::Left if state.cursor > 0 => { state.cursor -= 1; } KeyCode::Right if state.cursor < state.buf.len() => { state.cursor += 1; } KeyCode::Enter => { // TODO add to command history let _ = self .execute_command(&state.buf) .map_err(|err| self.set_error(err)); self.mode = Mode::Normal; return; } KeyCode::Esc => { self.mode = Mode::default(); return; } _ => {} } self.mode = Mode::Command(state); } /// Processes a key press event. /// /// Returns `true` if the key was handled by a keybind, `false` otherwise. fn on_key(&mut self, key: Key) -> bool { let keybinds = match &self.mode { Mode::Normal => &self.config.keybinds.normal, Mode::Insert(_) => &self.config.keybinds.insert, Mode::Visual => &self.config.keybinds.visual, Mode::Command(_) => return false, // command mode is handled in [on_command_event] }; if let Some(submode) = self.submode { // if we're currently in a submode, try to run this submode's keybind keybinds .submodes .get(&submode) .and_then(|submode| submode.get(&key)) .cloned() .map(|keybind| self.execute_keybind(keybind)); // whether or not there is a keybind we exit the submode self.submode = None; } else if keybinds.submodes.contains_key(&key) { // enter a submode if available self.submode = Some(key); } else if let Some(keybind) = keybinds.map.get(&key) { // run the keybind if available self.execute_keybind(keybind.clone()); } else { // key is not bound; don't handle return false; } true } fn write_buffer(&mut self, file: OsString) -> Result<()> { self.last_saved = self.buffer.as_ref().clone(); let out = self.buffer.as_ref().bytes().collect::>(); let mut handle = OpenOptions::new() .create(true) .truncate(true) .write(true) .open(file)?; handle.write_all(out.as_slice())?; Ok(()) } fn write_command(&mut self, command: &str, args: &[&str]) -> std::result::Result<(), String> { let handle = match self.file.clone() { Some(handle) => handle, None => match args.get(0) { Some(part) => { let file = OsString::from(part); self.file = Some(file.clone()); file } None => { return Err(format!("{}: No file name.", command)); } }, }; self.write_buffer(handle).map_err(|err| format!("{}", err)) } fn execute_keybind(&mut self, keybind: Keybind) { match keybind { Keybind::Action(action) => action(self), Keybind::Command(command) => { let _ = self .execute_command(&command) .map_err(|err| self.set_error(err)); self.mode = Mode::Normal; return; } } } fn execute_command(&mut self, command: &str) -> std::result::Result<(), String> { let command_parts = command.split(' ').collect::>(); let command = match command_parts.get(0) { Some(command) => command, None => return Ok(()), }; let args = &command_parts[1..]; match *command { "q!" => self.quit = true, "q" => { if self.last_saved == *self.buffer.as_ref() { self.quit = true; } else { return Err("Buffer is unsaved (add ! to override)".into()); } } "w" => return self.write_command(command, args), "wq" => { self.write_command(command, args)?; self.quit = true; } command => match command.parse::() { Err(_) => return Err(format!("{}: Unrecognized command.", command)), Ok(line) if line >= self.buffer.as_ref().len_lines() => { return Err(format!("Line {} is out-of-bounds", line)) } Ok(line) => { self.cursor.line = line; self.scroll_to_cursor(); } }, } Ok(()) } pub fn move_cursor(&mut self, direction: Direction) { let wrap = self.config.move_linewrap; self.buffer.move_cursor(&mut self.cursor, direction, wrap); self.scroll_to_cursor(); } pub fn scroll_to_cursor(&mut self) { if self.cursor.column < self.scroll.column + 3 { self.scroll.column = self.cursor.column.saturating_sub(3); } else if self.cursor.column + 6 >= self.scroll.column + self.size.0 { self.scroll.column = self.cursor.column.saturating_sub(self.size.0 - 6); } if self.cursor.line < self.scroll.line { self.scroll.line = self.cursor.line; } else if self.cursor.line + 3 >= self.scroll.line + self.size.1 { self.scroll.line = self.cursor.line + 3 - self.size.1; } } /// If modified, saves the current buffer in undo history and clears the redo history. pub fn save_buffer(&mut self) { /*if let Some(last) = self.undo_buffers.last() { if *last.as_ref() == *self.buffer.as_ref() { return; } }*/ let current = self.buffer.clone(); self.undo_buffers.push(current); self.redo_buffers.clear(); } /// Pops the last undo state from the history and pushes the current state to the redo buffers. pub fn undo(&mut self) { match self.undo_buffers.pop() { None => self.set_error("Already at oldest change"), Some(last) => { let current = std::mem::replace(&mut self.buffer, last); self.redo_buffers.push(current); } } } /// Pops the next redo state from the history and pushes the current state to the undo buffers. pub fn redo(&mut self) { match self.redo_buffers.pop() { None => self.set_error("Already at newest change"), Some(next) => { let current = std::mem::replace(&mut self.buffer, next); self.undo_buffers.push(current); } } } }