/* * 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::io::{stdin, stdout, Read, Write}; use crossterm::{cursor, event, terminal, ExecutableCommand, Result}; use event::{Event, KeyCode, KeyEvent}; use ropey::Rope; struct Buffer { pub text: Rope, } impl Buffer { pub fn from_str(text: &str) -> Self { Self { text: Rope::from_str(text), } } pub fn draw(&self, out: &mut (impl ExecutableCommand + Write)) -> Result { let (cols, rows) = terminal::size()?; let lr_width = self.text.len_lines().ilog10() + 1; out.execute(cursor::MoveTo(0, 0))?; for (row, line) in (0..rows).zip(self.text.lines()) { write!(out, "{:width$} ", row, width = lr_width as usize)?; write!(out, "{}", line.as_str().unwrap_or("").trim_end())?; out.execute(cursor::MoveToNextLine(1))?; } Ok(lr_width + 1) } pub fn clamped_cursor(&self, cursor: Cursor) -> Cursor { Cursor { line: cursor.line, column: cursor .column .min(self.text.line(cursor.line).len_chars() - 1), } } pub fn cursor_to_char(&self, cursor: Cursor) -> usize { let cursor = self.clamped_cursor(cursor); self.text.line_to_char(cursor.line) + cursor.column } pub fn move_cursor(&self, cursor: &mut Cursor, direction: Direction, enable_linewrap: bool) { *cursor = self.clamped_cursor(*cursor); match direction { Direction::Left => { if cursor.column > 0 { cursor.column -= 1; } else if enable_linewrap && cursor.line > 0 { cursor.line -= 1; let line = self.text.line(cursor.line); cursor.column = line.len_chars() - 1; } } Direction::Down => { if cursor.line < self.text.len_lines() { cursor.line += 1; } } Direction::Up => { if cursor.line > 0 { cursor.line -= 1; } } Direction::Right => { let line = self.text.line(cursor.line); if cursor.column + 2 > line.len_chars() { if enable_linewrap && cursor.line < self.text.len_lines() { cursor.line += 1; cursor.column = 0; } } else { cursor.column += 1; } } } } } #[derive(Copy, Clone, Debug, Default)] struct Cursor { pub column: usize, pub line: usize, } #[derive(Copy, Clone, Debug, Default)] enum Mode { #[default] Normal, Command, Visual, Insert, } 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, } } } #[derive(Copy, Clone, Debug)] enum Direction { Left, Down, Up, Right, } struct State { pub buffer: Buffer, pub cursor: Cursor, pub mode: Mode, pub quit: bool, } impl State { pub fn from_str(text: &str) -> Self { Self { buffer: Buffer::from_str(text), cursor: Cursor::default(), mode: Mode::default(), quit: false, } } pub fn draw(&self, out: &mut impl Write) -> Result<()> { out.execute(terminal::BeginSynchronizedUpdate)?; out.execute(terminal::Clear(terminal::ClearType::All))?; let lr_width = self.buffer.draw(out)?; let cursor = self.buffer.clamped_cursor(self.cursor); let (col, row) = (cursor.column as u16, cursor.line as u16); let col = col + lr_width as u16; out.execute(cursor::MoveTo(col, row))?; out.execute(self.mode.cursor_style())?; out.execute(terminal::EndSynchronizedUpdate)?; Ok(()) } pub fn on_event(&mut self, event: Event) { match self.mode { Mode::Normal => self.on_normal_event(event), Mode::Command => self.on_command_event(event), Mode::Visual => self.on_visual_event(event), Mode::Insert => self.on_insert_event(event), } } fn on_normal_event(&mut self, event: Event) { match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Char('i') => { self.mode = Mode::Insert; } KeyCode::Char(':') => { self.mode = Mode::Command; } KeyCode::Char('v') => { self.mode = Mode::Visual; } KeyCode::Esc => { self.quit = true; } code => self.match_any_key(code), }, _ => {} } } fn on_command_event(&mut self, event: Event) { match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Esc => { self.mode = Mode::Normal; } code => self.match_any_key(code), }, _ => {} } } fn on_visual_event(&mut self, event: Event) { match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Esc => { self.mode = Mode::Normal; } code => self.match_any_key(code), }, _ => {} } } fn on_insert_event(&mut self, event: Event) { match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Char(c) => { let index = self.buffer.cursor_to_char(self.cursor); self.buffer.text.insert_char(index, c); self.move_cursor(Direction::Right) } KeyCode::Backspace => { self.move_cursor(Direction::Left); let index = self.buffer.cursor_to_char(self.cursor); self.buffer.text.remove(index..=index); } KeyCode::Delete => { let index = self.buffer.cursor_to_char(self.cursor); self.buffer.text.remove(index..=index); } KeyCode::Enter => { let index = self.buffer.cursor_to_char(self.cursor); self.buffer.text.insert_char(index, '\n'); self.cursor.line += 1; self.cursor.column = 0; } KeyCode::Esc => { self.mode = Mode::Normal; } code => self.match_any_key(code), }, _ => {} } } fn match_any_key(&mut self, code: KeyCode) { match code { KeyCode::Esc => self.mode = Mode::Normal, KeyCode::Char('h') | KeyCode::Left => self.move_cursor(Direction::Left), KeyCode::Char('j') | KeyCode::Down => self.move_cursor(Direction::Down), KeyCode::Char('k') | KeyCode::Up => self.move_cursor(Direction::Up), KeyCode::Char('l') | KeyCode::Right => self.move_cursor(Direction::Right), _ => {} } } fn move_cursor(&mut self, direction: Direction) { self.buffer.move_cursor(&mut self.cursor, direction, true); } } fn main() -> Result<()> { let text = include_str!("main.rs"); let mut state = State::from_str(text); let mut stdout = stdout(); terminal::enable_raw_mode()?; stdout.execute(terminal::EnterAlternateScreen)?; while !state.quit { state.draw(&mut stdout)?; let event = event::read()?; state.on_event(event); } stdout.execute(terminal::LeaveAlternateScreen)?; Ok(()) }