~cdv/riveted

7eb5e7062ac86a4e59c579df4ad827499a8a20a0 — Chris Vittal 5 months ago 90a3146 master
Modes?
4 files changed, 476 insertions(+), 32 deletions(-)

M Cargo.lock
M Cargo.toml
A src/cmd.rs
M src/main.rs
M Cargo.lock => Cargo.lock +183 -0
@@ 7,12 7,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b585a98a234c46fc563103e9278c9391fde1f4e6850334da895d27edb9580f62"

[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"

[[package]]
name = "arrayvec"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"

[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"

[[package]]
name = "base64"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"

[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"

[[package]]
name = "blake2b_simd"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
dependencies = [
 "arrayref",
 "arrayvec",
 "constant_time_eq",
]

[[package]]
name = "cc"
version = "1.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d"

[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 28,6 69,23 @@ dependencies = [
]

[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"

[[package]]
name = "crossbeam-utils"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
dependencies = [
 "autocfg",
 "cfg-if",
 "lazy_static",
]

[[package]]
name = "crossterm"
version = "0.17.4"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 53,6 111,28 @@ dependencies = [
]

[[package]]
name = "dirs"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
dependencies = [
 "cfg-if",
 "dirs-sys",
]

[[package]]
name = "dirs-sys"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b"
dependencies = [
 "cfg-if",
 "libc",
 "redox_users",
 "winapi 0.3.8",
]

[[package]]
name = "fehler"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 89,6 169,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"

[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
dependencies = [
 "cfg-if",
 "libc",
 "wasi",
]

[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 142,6 233,12 @@ dependencies = [
]

[[package]]
name = "memchr"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"

[[package]]
name = "mio"
version = "0.6.22"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 184,6 281,19 @@ dependencies = [
]

[[package]]
name = "nix"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363"
dependencies = [
 "bitflags",
 "cc",
 "cfg-if",
 "libc",
 "void",
]

[[package]]
name = "parking_lot"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 232,12 342,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"

[[package]]
name = "redox_users"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431"
dependencies = [
 "getrandom",
 "redox_syscall",
 "rust-argon2",
]

[[package]]
name = "riveted"
version = "0.0.1"
dependencies = [
 "crossterm",
 "fehler",
 "libriveted",
 "rustyline",
]

[[package]]
name = "rust-argon2"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017"
dependencies = [
 "base64",
 "blake2b_simd",
 "constant_time_eq",
 "crossbeam-utils",
]

[[package]]
name = "rustyline"
version = "6.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd20b28d972040c627e209eb29f19c24a71a19d661cc5a220089176e20ee202"
dependencies = [
 "cfg-if",
 "dirs",
 "libc",
 "log",
 "memchr",
 "nix",
 "scopeguard",
 "unicode-segmentation",
 "unicode-width",
 "utf8parse",
 "winapi 0.3.8",
]

[[package]]


@@ 291,12 444,42 @@ dependencies = [
]

[[package]]
name = "unicode-segmentation"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"

[[package]]
name = "unicode-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"

[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"

[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"

[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"

[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"

[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +1 -0
@@ 16,6 16,7 @@ path = "src/main.rs"
libriveted = { path = "lib", version = "0.0.1" }
crossterm = "0.17"
fehler = "1"
rustyline = "6"

[workspace]
members = [

A src/cmd.rs => src/cmd.rs +94 -0
@@ 0,0 1,94 @@
use crossterm::event::KeyModifiers;

use super::*;

impl Drop for Command<'_> {
    fn drop(&mut self) {
        assert_eq!(self.ed.mode, Mode::Command);
        self.ed.mode = Mode::Normal;
        execute!(self.ed.screen, cursor::RestorePosition).unwrap();
    }
}

#[derive(Debug)]
pub struct Command<'ed> {
    ed: &'ed mut Editor,
    coff: usize,
    cx: usize,
    buf: String,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CommandStatus {
    Ok,
    Quit,
    Error(String /* FIXME error type? */),
}

impl<'ed> Command<'ed> {
    #[throws(Box<dyn std::error::Error>)]
    pub(crate) fn new(ed: &'ed mut Editor) -> Self {
        execute!(
            ed.screen,
            cursor::SavePosition,
            cursor::MoveTo(0, ed.screenrows as u16 + 1)
        )?;

        Self {
            ed,
            cx: 0,
            coff: 0,
            buf: String::new(),
        }
    }

    #[throws(Box<dyn std::error::Error>)]
    pub fn run(mut self) -> CommandStatus {
        let mut rl = rustyline::Editor::<()>::new();
        execute!(self.ed.screen, cursor::MoveToColumn(0))?;
        self.buf = rl.readline(":")?;
        self.run_command()
    }

    /// Returns true when enter or escape is pressed
    #[throws(Box<dyn std::error::Error>)]
    fn process_event(&mut self) -> bool {
        match event::read()? {
            Event::Key(key) => self.handle_key(key),
            _ => false,
        }
    }

    fn handle_key(&mut self, key: KeyEvent) -> bool {
        use KeyCode::*;
        match key {
            KeyEvent {
                code: Char(c),
                modifiers,
            } if !modifiers.contains(KeyModifiers::CONTROL) => {
                self.insert_char(c);
                false
            }
            KeyEvent { code: Esc, .. } => {
                self.buf.clear();
                true
            }
            KeyEvent { code: Enter, .. } => true,
            _ => false,
        }
    }

    fn insert_char(&mut self, c: char) {
        let dc = c.len_utf8();
        self.buf.insert(self.cx, c);
        self.cx += dc;
    }

    fn run_command(&mut self) -> CommandStatus {
        match self.buf.trim() {
            "" => CommandStatus::Ok,
            "q" | "quit" => CommandStatus::Quit,
            cmd => CommandStatus::Error(format!("unknown command: '{}'", cmd)),
        }
    }
}

M src/main.rs => src/main.rs +198 -32
@@ 1,15 1,25 @@
use std::io::{self, prelude::*, Stdout};
use std::{
    fs::File,
    io::{self, prelude::*},
    path::PathBuf,
};

use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    execute, queue, style,
    event::{self, Event, KeyCode, KeyEvent},
    execute, style,
    terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use fehler::*;

mod cmd;

use cmd::{Command, CommandStatus};

static VERSION: &str = env!("CARGO_PKG_VERSION");

fn main() {
    let ed = match Editor::new() {
    let mut ed = match Editor::new() {
        Ok(ed) => ed,
        Err(e) => {
            eprintln!("riv: {}", e);


@@ 17,6 27,12 @@ fn main() {
        }
    };

    let mut args = std::env::args_os();

    if args.len() > 1 {
        let _ = ed.open_file(args.nth(1).unwrap());
    }

    match ed.run() {
        Err(e) => {
            eprintln!("riv: {}", e);


@@ 29,35 45,46 @@ fn main() {
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Mode {
    Normal,
    // Command,
    Command,
    // Insert,
}

#[derive(Debug)]
struct Editor {
    cx: u16,
    cy: u16,
    tcols: u16,
    trows: u16,
    screen: Stdout,
    cx: usize,
    cy: usize,
    roff: usize,
    coff: usize,
    screencols: usize,
    screenrows: usize,
    screen: io::Stdout,
    path: Option<PathBuf>,
    rows: Vec<String>,
    mode: Mode,
}

impl Editor {
    const CTRL_Q: KeyEvent = KeyEvent {
        code: KeyCode::Char('q'),
        modifiers: KeyModifiers::CONTROL,
    };
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Move {
    Right,
    Up,
    Left,
    Down,
}

impl Editor {
    #[throws(Box<dyn std::error::Error>)]
    fn new() -> Self {
        let (tcols, trows) = terminal::size()?;
        let mut ed = Self {
            cx: 0,
            cy: 0,
            tcols,
            trows: trows - 1,
            roff: 0,
            coff: 0,
            screencols: tcols as usize,
            screenrows: trows as usize - 1,
            screen: io::stdout(),
            path: None,
            rows: Vec::new(),
            mode: Mode::Normal,
        };



@@ 72,44 99,183 @@ impl Editor {
        ed
    }

    fn nrows(&self) -> usize {
        self.rows.len()
    }

    #[throws(io::Error)]
    fn open_file<P: Into<PathBuf>>(&mut self, path: P) {
        let path = path.into();
        let file = io::BufReader::new(File::open(&path)?);
        self.path = Some(path);
        for line in file.lines() {
            let line = line?;
            self.rows.push(line);
        }
    }

    #[throws(Box<dyn std::error::Error>)]
    fn run(mut self) {
        loop {
            self.refresh()?;
            if self.process_event()? {
                break;
            self.process_event()?;

            if self.mode == Mode::Command {
                match self.command()? {
                    CommandStatus::Quit => break,
                    _ => {}
                }
            }
        }
    }

    #[throws(Box<dyn std::error::Error>)]
    fn refresh(&mut self) {
        queue!(self.screen, Clear(ClearType::All), cursor::MoveTo(0, 0),)?;
        self.scroll();

        self.draw_rows()?;
        let mut buf = Vec::new();
        execute!(&mut buf, cursor::Hide, cursor::MoveTo(0, 0))?;

        queue!(self.screen, cursor::MoveTo(0, 0))?;
        self.draw_rows(&mut buf)?;

        execute!(
            &mut buf,
            cursor::MoveTo((self.cx - self.coff) as u16, (self.cy - self.roff) as u16),
            cursor::Show
        )?;
        self.screen.write_all(&buf)?;
        self.screen.flush()?;
    }

    fn scroll(&mut self) {
        if self.cy < self.roff {
            self.roff = self.cy;
        }
        if self.cy >= self.roff + self.screenrows {
            self.roff = self.cy - self.screenrows + 1;
        }
        if self.cx < self.coff {
            self.coff = self.cx;
        }
        if self.cx >= self.coff + self.screencols {
            self.coff = self.cx - self.screencols + 1;
        }
    }

    #[throws(Box<dyn std::error::Error>)]
    fn draw_rows(&mut self) {
        for _ in 0..self.trows {
            queue!(self.screen, style::Print("~\r\n"))?;
    fn draw_rows(&self, buf: &mut Vec<u8>) {
        for y in 0..self.screenrows {
            self.build_row(buf, y)?;
            execute!(buf, Clear(ClearType::UntilNewLine), style::Print("\r\n"))?;
        }
        queue!(self.screen, style::Print("%"))?;

        execute!(buf, style::Print(":"), Clear(ClearType::UntilNewLine))?;
    }

    /// Returns true if exit has been recived
    #[throws(Box<dyn std::error::Error>)]
    fn process_event(&mut self) -> bool {
        let evt = event::read()?;
        if evt == Event::Key(Self::CTRL_Q) {
            true
    fn build_row(&self, buf: &mut Vec<u8>, y: usize) {
        let ridx = y + self.roff;
        if self.nrows() == 0 && y == self.screenrows / 3 {
            let mut msg = format!("riveted editor -- version {}", VERSION);
            msg.truncate(self.screencols);
            let mut pad = (self.screencols - msg.len()) / 2;
            if pad > 0 {
                buf.push(b'~');
                pad -= 1;
            }
            write!(buf, "{:width$}{}", "", msg, width = pad)?;
        } else if ridx >= self.nrows() {
            buf.push(b'~');
        } else {
            false
            render_line(buf, &self.rows[ridx], self.coff, self.screencols)?;
        }
    }

    /// Returns true if exit has been recieved
    #[throws(Box<dyn std::error::Error>)]
    fn process_event(&mut self) {
        match event::read()? {
            Event::Key(key) => match self.mode {
                Mode::Normal => self.process_key(key)?,
                _ => unreachable!(),
            },
            _ => {}
        }
    }

    /// Returns true if exit has been recieved.
    #[throws(Box<dyn std::error::Error>)]
    fn process_key(&mut self, key: KeyEvent) {
        use KeyCode::*;
        match key {
            KeyEvent {
                code: Char(':'), ..
            } => {
                self.mode = Mode::Command;
            }
            KeyEvent { code: Home, .. } => self.cx = 0,
            KeyEvent { code: End, .. } => self.cx = self.screencols - 1,

            // Maybe FIXME, modifiers on arrow keys
            KeyEvent {
                code: c @ PageUp, ..
            }
            | KeyEvent {
                code: c @ PageDown, ..
            } => {
                let mv = if c == PageUp { Move::Up } else { Move::Down };
                for _ in 0..self.screenrows {
                    self.move_cursor(mv);
                }
            }

            KeyEvent { code: Right, .. } => self.move_cursor(Move::Right),
            KeyEvent { code: Up, .. } => self.move_cursor(Move::Up),
            KeyEvent { code: Left, .. } => self.move_cursor(Move::Left),
            KeyEvent { code: Down, .. } => self.move_cursor(Move::Down),
            _ => {}
        }
    }

    fn move_cursor(&mut self, mv: Move) {
        use Move::*;
        let row = self.rows.get(self.cy);
        match mv {
            Right => {
                if let Some(row) = row {
                    if self.cx < row.len() {
                        self.cx += 1;
                    }
                }
            }
            Up => self.cy = self.cy.saturating_sub(1),
            Left => self.cx = self.cx.saturating_sub(1),
            Down => {
                if self.cy < self.nrows() {
                    self.cy += 1;
                }
            }
        }
        let rlen = self.rows.get(self.cy).map_or(0, String::len);
        self.cx = std::cmp::min(self.cx, rlen);
    }

    #[throws(Box<dyn std::error::Error>)]
    fn command(&mut self) -> CommandStatus {
        Command::new(self)?.run()?
    }
}

#[throws(io::Error)]
fn render_line<W: Write>(w: &mut W, line: &str, coff: usize, width: usize) {
    let len = line.len().saturating_sub(coff);
    let len = std::cmp::min(len, width);
    let line = if coff > line.len() {
        ""
    } else {
        &line[coff..coff + len]
    };
    write!(w, "{}", line)?;
}

impl Drop for Editor {