/* * Copyright (c) 2023 Marceline Cramer * Copyright (c) 2023 Emma Tebibyte * 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::{ env::args, fs::File, io::{stdout, Read, Stdout, Write}, os::fd::FromRawFd, }; use crossterm::{ cursor, event::{read, Event, KeyCode, KeyEvent}, terminal, ExecutableCommand, Result, }; use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE}; mod buffer; mod theme; use buffer::Buffer; use theme::StyleStore; #[derive(Copy, Clone, Debug, Default)] pub struct Cursor { pub column: usize, pub line: usize, } #[derive(Clone, Debug, Default)] struct NormalState { pub error: Option, } #[derive(Clone, Debug, Default)] struct CommandState { pub buf: String, pub cursor: usize, } #[derive(Copy, Clone, Debug)] struct InsertState { append: bool, } #[derive(Clone, Debug)] enum Mode { Normal(NormalState), Command(CommandState), Visual, Insert(InsertState), } impl Default for Mode { fn default() -> Self { Mode::Normal(Default::default()) } } 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)] pub enum Direction { Left, Down, Up, Right, } struct State { pub style_store: StyleStore, pub buffer: Buffer, pub cursor: Cursor, pub scroll: Cursor, pub size: (usize, usize), pub mode: Mode, pub quit: bool, } impl State { pub fn from_str(text: &str) -> Result { let (cols, rows) = terminal::size()?; Ok(Self { style_store: StyleStore::default(), buffer: Buffer::from_str(text), cursor: Cursor::default(), scroll: Cursor::default(), size: (cols as usize, rows as usize), mode: Mode::default(), quit: false, }) } pub fn draw(&mut self, out: &mut impl Write) -> Result<()> { // begin update let (cols, rows) = terminal::size()?; out.execute(terminal::BeginSynchronizedUpdate)?; out.execute(terminal::Clear(terminal::ClearType::All))?; // draw status line let mut set_cursor_pos = None; let mut show_status_bar = false; match &self.mode { Mode::Command(CommandState { buf, cursor }) => { let col = *cursor as u16 + 1; let row = rows - 1; out.execute(cursor::MoveTo(0, row))?; write!(out, ":{}", buf)?; set_cursor_pos = Some((col, row)); show_status_bar = true; } Mode::Normal(NormalState { error: Some(error) }) => { let error_style = self.style_store.get_scope("error"); out.execute(cursor::MoveTo(0, rows - 1))?; error_style.print_styled(out, error)?; show_status_bar = true; } _ => {} } // draw buffer let buffer_rows = if show_status_bar { rows - 1 } else { rows }; let lr_width = self.buffer .draw(&mut self.style_store, cols, buffer_rows, self.scroll, out)?; // 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.execute(cursor::MoveTo(cursor_pos.0, cursor_pos.1))?; out.execute(self.mode.cursor_style())?; // finish update out.execute(terminal::EndSynchronizedUpdate)?; Ok(()) } pub fn on_event(&mut self, event: Event) { match &self.mode { Mode::Normal(state) => self.on_normal_event(event, state.clone()), Mode::Command(state) => self.on_command_event(event, state.clone()), Mode::Visual => self.on_visual_event(event), Mode::Insert(state) => self.on_insert_event(event, state.clone()), } } fn on_normal_event(&mut self, event: Event, mut state: NormalState) { // reset the error from the last event state.error = None; match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Char('i') => { let state = InsertState { append: false }; self.mode = Mode::Insert(state); } KeyCode::Char('a') => { let state = InsertState { append: true }; self.move_cursor(Direction::Right); self.mode = Mode::Insert(state); } KeyCode::Char('o') => { let state = InsertState { append: false }; self.cursor.line += 1; self.cursor.column = 0; self.buffer.insert_char(self.cursor, '\n'); self.mode = Mode::Insert(state); } KeyCode::Char(':') => { self.mode = Mode::Command(Default::default()); } KeyCode::Char('v') => { self.mode = Mode::Visual; } code => self.on_any_key(code), }, event => self.on_any_event(event), } match self.mode { Mode::Normal(_) => self.mode = Mode::Normal(state), _ => {} } } fn on_command_event(&mut self, event: Event, mut state: CommandState) { match event { Event::Key(KeyEvent { code, .. }) => 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 result = self.execute_command(&state.buf); let error = result.err(); self.mode = Mode::Normal(NormalState { error }); return; } code => return self.on_any_key(code), }, event => return self.on_any_event(event), } self.mode = Mode::Command(state); } fn on_visual_event(&mut self, event: Event) { match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Esc => { self.mode = Mode::default(); } code => self.on_any_key(code), }, event => self.on_any_event(event), } } fn on_insert_event(&mut self, event: Event, state: InsertState) { match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Char(c) => { self.buffer.insert_char(self.cursor, c); self.move_cursor(Direction::Right) } KeyCode::Backspace => { self.move_cursor(Direction::Left); self.buffer.remove(self.cursor); } KeyCode::Delete => { self.buffer.remove(self.cursor); } KeyCode::Enter => { self.buffer.insert_char(self.cursor, '\n'); self.cursor.line += 1; self.cursor.column = 0; } KeyCode::Esc => { if state.append { self.move_cursor(Direction::Left); } self.mode = Mode::default(); } code => self.on_any_key(code), }, event => self.on_any_event(event), } } fn on_any_event(&mut self, event: Event) { match event { Event::Resize(cols, rows) => { self.size = (cols as usize, rows as usize); } Event::Key(KeyEvent { code, .. }) => self.on_any_key(code), _ => {} } } fn on_any_key(&mut self, code: KeyCode) { match code { KeyCode::Esc => self.mode = Mode::default(), 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 execute_command(&mut self, command: &str) -> std::result::Result<(), String> { match command { "q" => self.quit = true, command => return Err(format!("unrecognized command {:?}", command)), } Ok(()) } fn move_cursor(&mut self, direction: Direction) { self.buffer.move_cursor(&mut self.cursor, direction, true); 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; } } } fn screen_main(stdout: &mut Stdout, mut state: State) -> Result<()> { while !state.quit { state.draw(stdout)?; let event = read()?; state.on_event(event); } Ok(()) } fn main() -> Result<()> { let argv = args().collect::>(); let mut buf = Vec::new(); match argv.get(1).map(|s| s.as_str()) { Some("-") | None => { let stdin = 0; // get stdin as a file descriptor if unsafe { libc::isatty(stdin) } == 0 { unsafe { File::from_raw_fd(stdin) } } else { File::open("/dev/null").unwrap() } } Some(path) => std::fs::File::open(path).unwrap_or_else(|_| { eprintln!("{}: {}: No such file or directory.", argv[0], argv[1]); exit(EX_UNAVAILABLE); }), } .read_to_end(&mut buf) .unwrap(); let text = String::from_utf8(buf).unwrap_or_else(|_| { eprintln!( "{}: {}: File contents are not valid UTF-8.", argv[0], argv[1] ); exit(EX_DATAERR); }); let state = State::from_str(&text)?; let mut stdout = stdout(); terminal::enable_raw_mode()?; stdout.execute(terminal::EnterAlternateScreen)?; let result = screen_main(&mut stdout, state); stdout.execute(terminal::LeaveAlternateScreen)?; terminal::disable_raw_mode()?; result }