~hime/linetest

4a1c17e24b73eca349a6973b10da10bdc35100f1 — drbawb 4 years ago c48bcbb
implement basic buffer management

- ascii keys append to a buffer
- arrows (left/right) navigate around buffer
- terminal width controls a sliding span for displaying that buffer
4 files changed, 275 insertions(+), 427 deletions(-)

M Cargo.lock
M Cargo.toml
A src/app.rs
M src/main.rs
M Cargo.lock => Cargo.lock +5 -340
@@ 13,53 13,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034"

[[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.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518"

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


@@ 75,23 34,6 @@ 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.6"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 117,55 59,6 @@ dependencies = [
]

[[package]]
name = "dirs"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
dependencies = [
 "libc",
 "redox_users",
 "winapi",
]

[[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.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
dependencies = [
 "libc",
 "redox_users",
 "winapi",
]

[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"

[[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 = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 178,23 71,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701"

[[package]]
name = "linefeed"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28715d08e35c6c074f9ae6b2e6a2420bac75d050c66ecd669d7d5b98e2caa036"
dependencies = [
 "dirs 1.0.5",
 "mortal",
 "winapi",
]

[[package]]
name = "linetest"
version = "0.1.0"
dependencies = [
 "anyhow",
 "crossterm",
 "linefeed",
 "unicode-segmentation",
 "unicode-width",
]

[[package]]


@@ 216,12 99,6 @@ 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.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 246,45 123,6 @@ dependencies = [
]

[[package]]
name = "mortal"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998fd6a991497275567703b6f435e27958b633878ec991f5734b96dd46675e9f"
dependencies = [
 "bitflags",
 "libc",
 "nix",
 "smallstr",
 "terminfo",
 "unicode-normalization",
 "unicode-width",
 "winapi",
]

[[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 = "nom"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
dependencies = [
 "memchr",
 "version_check",
]

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


@@ 318,130 156,12 @@ dependencies = [
]

[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
 "phf_shared",
]

[[package]]
name = "phf_codegen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
 "phf_generator",
 "phf_shared",
]

[[package]]
name = "phf_generator"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [
 "phf_shared",
 "rand",
]

[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
 "siphasher",
]

[[package]]
name = "ppv-lite86"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"

[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
 "getrandom",
 "libc",
 "rand_chacha",
 "rand_core",
 "rand_hc",
 "rand_pcg",
]

[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
 "ppv-lite86",
 "rand_core",
]

[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
 "getrandom",
]

[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
 "rand_core",
]

[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
 "rand_core",
]

[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"

[[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 = "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 = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 469,21 189,6 @@ dependencies = [
]

[[package]]
name = "siphasher"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7"

[[package]]
name = "smallstr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f"
dependencies = [
 "smallvec",
]

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


@@ 502,32 207,10 @@ dependencies = [
]

[[package]]
name = "terminfo"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e"
dependencies = [
 "dirs 2.0.2",
 "fnv",
 "nom",
 "phf",
 "phf_codegen",
]

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

[[package]]
name = "unicode-normalization"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977"
dependencies = [
 "tinyvec",
]
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"

[[package]]
name = "unicode-width"


@@ 536,24 219,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"

[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"

[[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.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +2 -1
@@ 9,4 9,5 @@ edition = "2018"
[dependencies]
anyhow = "1.0"
crossterm = "0.17"
linefeed = "0.6"
unicode-segmentation = "1.0"
unicode-width = "0.1"
\ No newline at end of file

A src/app.rs => src/app.rs +243 -0
@@ 0,0 1,243 @@
use crossterm::{
	cursor,
	event::{self, KeyEvent, KeyModifiers},
	execute,
	style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
	terminal::{self, Clear, ClearType},
	ExecutableCommand, QueueableCommand, Result,
};

use std::cmp;
use std::io::{self, Write};

struct InputBuffer {
    pos: usize,
    start: usize,
    end: usize,

    buffer: String,
}

impl InputBuffer {
    pub fn new() -> Self {
        Self {
            // TODO: should probably init w/ some size ...
            pos: 0,
            start: 0,
            end: 0,

            buffer: String::new(),
        }
    }

    pub fn insert(&mut self, ch: char) {
        self.buffer.push(ch);
        self.advance();
    }

    pub fn backspace(&mut self) {
        self.buffer.pop();
        self.retreat();
    }

    pub fn cursor_left(&mut self) {
        if self.pos <= 0 { return; }
        
        self.pos -= 1;
        
        if self.pos < self.start  {
            self.start -= 1;
            self.end -= 1;
        }
    }

    pub fn cursor_right(&mut self) {
        if self.pos >= self.buffer.len() { return; }

        self.pos += 1;
        
        if self.pos > self.end {
            self.start += 1;
            self.end += 1;
        }
    }

    fn advance(&mut self) {
        self.pos += 1;

        if self.pos > self.end {
            // move viewport forwards
            self.start += 1;
            self.end += 1;
        }
    }

    fn retreat(&mut self) {
        if self.pos <= 0 { return; } // ... no.

        self.pos -= 1;

        if self.start > 0 {
            self.start -= 1;
            self.end -= 1;
        }
    }

    fn span_len(&self) -> usize {
        self.end - self.start
    }

    pub fn render(&mut self, stdout: &mut io::Stdout, col: u16, row: u16, width: u16) -> anyhow::Result<()> {

        // calculate some info about our prompt & buffer size
        let prompt = "> ";
        let width = width as usize; // TODO: what if usize < u16?
        let width_with_prompt = width - 1 - prompt.len();

        // adjust viewspan to match current terminal width (less the prompt)
        if self.span_len() > width_with_prompt {
            // shrink the view span
            self.end -= self.span_len() - width_with_prompt;
        } else if self.span_len() < width_with_prompt {
            // grow the view span
            self.end += width_with_prompt - self.span_len();
        }

        // draw status info on line 2
        let buf_info = format!(
            "start: {}, end: {}, pos: {}, spanlen: {}, buflen: {}",
            self.start, 
            self.end,
            self.pos,
            self.span_len(),
            self.buffer.len()
        );
        stdout
            .queue(cursor::MoveTo(0, 1))?
            .queue(Clear(ClearType::CurrentLine))?
            .queue(Print(&buf_info))?;

        // clear the line & draw our prompt
        stdout
            .queue(cursor::MoveTo(col, row))?
            .queue(Clear(ClearType::CurrentLine))?
            .queue(Print(prompt.to_string()))?;

        // draw the current viewport
        let relative_cursor_pos = (self.pos - self.start) + prompt.len() + 1;
        stdout
            .queue(Print(&self.buffer[self.start..]))?
            .queue(cursor::MoveToColumn(relative_cursor_pos as u16))?;

        Ok(stdout.flush()?)
    }

}

pub struct Shell {
    term_cols: u16,
    term_rows: u16,

    buffer: InputBuffer,
    lines: Vec<String>,
    is_running: bool,
}

impl Shell {
	pub fn new() -> anyhow::Result<Self> {
        let (cols, rows) = terminal::size()?;

		Ok(Self {
            term_cols: cols,
            term_rows: rows,
            
            buffer: InputBuffer::new(),
            lines: vec![],
            is_running: true,
		})
    }

	pub fn present(&mut self) -> anyhow::Result<()> {
		let mut stdout = io::stdout();
        
        // save the input cursor position
        stdout
            //.queue(cursor::SavePosition)?
            .queue(cursor::Hide)?;

        // draw title line
        stdout
            .queue(cursor::MoveTo(0, 0))?
            .queue(Clear(ClearType::CurrentLine))?
            .queue(Print(self.status_line()))?;

		// redraw current buffer lines except for prompt
        let mut text_spans = self.lines.iter();
		for i in 2..(self.term_rows - 1) {
			stdout
				.queue(cursor::MoveTo(0, i))?
				.queue(Clear(ClearType::CurrentLine))?;

			if let Some(span) = text_spans.next() {
				stdout.queue(Print(span.clone()))?;
			}
        }
        
        self.buffer.render(&mut stdout, 0, self.term_rows, self.term_cols)?;

        // restore cursor position
        stdout
            //.queue(cursor::RestorePosition)?
            .queue(cursor::Show)?;
		Ok(stdout.flush()?)
    }
    
    pub fn event_keydown(&mut self, event: event::KeyEvent) {
        match event {
            KeyEvent { code, modifiers } if modifiers.contains(KeyModifiers::CONTROL) => match code {
                event::KeyCode::Char('q') => { self.is_running = false; },
                _ => {},
            },

            KeyEvent { code, modifiers } => match code {
                event::KeyCode::Char(ch) if modifiers.contains(KeyModifiers::SHIFT) => {

                    self.buffer.insert(ch.to_ascii_uppercase());
                },

                event::KeyCode::Char(ch) => {
                    self.buffer.insert(ch);
                },

                event::KeyCode::Backspace => {
                    self.buffer.backspace();
                },

                event::KeyCode::Left => {
                    self.buffer.cursor_left();
                },

                event::KeyCode::Right => {
                    self.buffer.cursor_right();
                },

                _ => {},
            },

            _ => {},
        }
    }

    pub fn event_resize(&mut self, cols: u16, rows: u16) {
        self.term_cols = cols;
        self.term_rows = rows;
    }

    pub fn is_running(&self) -> bool { self.is_running }



    fn status_line(&self) -> String {
        format!("linetest - rows [{}] cols [{}]", self.term_rows, self.term_cols)
    }
}
\ No newline at end of file

M src/main.rs => src/main.rs +25 -86
@@ 1,117 1,56 @@
use crossterm::{
	cursor,
	event,
	event::{self, Event},
	execute,
	style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
	terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
	ExecutableCommand, QueueableCommand, Result,
};

use linefeed::{Interface, ReadResult};

use std::io::{stdout, Write};
use std::thread;
use std::time::Duration;

struct App {
	lines: Vec<String>,
}

impl App {
	pub fn new() -> Self {
		Self {
			lines: vec![],
		}
	}

	pub fn present(&mut self) -> anyhow::Result<()> {
		let mut stdout = stdout();
		stdout.queue(cursor::SavePosition)?;


		// redraw lines except for prompt
		let (cols, rows) = terminal::size()?;
		let mut text_spans = self.lines.iter();
		for i in 0..(rows - 1) {
			stdout
				.queue(cursor::MoveTo(0, i))?
				.queue(terminal::Clear(terminal::ClearType::CurrentLine))?;

			if let Some(span) = text_spans.next() {
				stdout.queue(Print(span.clone()))?;
			}
		}

		stdout.queue(cursor::RestorePosition)?;
		Ok(stdout.flush()?)
	}
}
mod app;


fn main() -> anyhow::Result<()> {

	//crossterm::terminal::enable_raw_mode()?;

	stdout()
		.execute(EnterAlternateScreen)?
		.execute(SetForegroundColor(Color::Blue))?
		.execute(SetBackgroundColor(Color::Red))?
		.execute(Print("oh my word"))?
		.execute(ResetColor)?;
	crossterm::terminal::enable_raw_mode()?;
	
	let mut shell = app::Shell::new()?;
	let poll_hz = Duration::from_millis(1000 / 120);

	let (cols, rows) = terminal::size()?;
	while shell.is_running() {
		shell.present()?;
		if !event::poll(poll_hz)? { continue }

	let mut app = App::new();
		match event::read()? {
			Event::Key(key_evt) => {
				shell.event_keydown(key_evt);
			},

	let mut reader = Interface::new("app")?;
	reader.set_prompt("my-app > ")?;
	reader.ignore_signal(linefeed::terminal::Signal::Interrupt);
			Event::Resize(cols, rows) => { 
				shell.event_resize(cols, rows);
			},

	let step = Some(Duration::from_millis(1000 / 60));
			_ => {},
		}

	//while let ReadResult::Input(input) = reader.read_line_step(step)? {
	loop {
		match reader.read_line_step(step)? {
			Some(ReadResult::Input(input)) => {
				if input == "quit" { break }

				reader.add_history(input.clone());
	}

				app.lines.push(input.clone());
				app.present()?;
			},

			_ => { app.present()?; continue; }
		}
	}
	//loop {
	//	if event::poll(Duration::from_millis(1000 / 60))? {
	//		match event::read()? {
	//			event::Event::Key(event) => { 
	//				if event.code == event::KeyCode::Enter {
	//					break;
	//				}
	//			},

	//			_ => { /* dont care */ },
	//		}
	//	}
	//}

	stdout()
		.execute(LeaveAlternateScreen)?;
		.execute(LeaveAlternateScreen)?
		.execute(cursor::Show)?;
	crossterm::terminal::disable_raw_mode()?;
	Ok(())
}





//	let mut reader = Interface::new("my-application")?;
//	reader.set_prompt("cmd > ")?;
//
//	while let ReadResult::Input(input) = reader.read_line()? {
//		println!("got input {:?}", input);
//
//		if input.starts_with("quit") { break }
//	}
//
//	println!("goodbye"); Ok(())
}