diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5cd0555 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,275 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "breed" +version = "0.1.0" +dependencies = [ + "crossterm", + "ropey", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + +[[package]] +name = "libc" +version = "0.2.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ropey" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ce7a2c43a32e50d666e33c5a80251b31147bb4b49024bcab11fb6f20c671ed" +dependencies = [ + "smallvec", + "str_indices", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "str_indices" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/Cargo.toml b/Cargo.toml index f3d3b09..68cc295 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,6 @@ name = "breed" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +crossterm = "0.26" +ropey = "1.6" diff --git a/src/main.rs b/src/main.rs index e7a11a9..e78fe10 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,266 @@ -fn main() { - println!("Hello, world!"); +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(()) }