Skip to content

Commit

Permalink
Introduce CommandPrompt to manage interactions with the command prompt
Browse files Browse the repository at this point in the history
This is a state machine, tracking inputs and maintaining properties of
the prompt. It doesn't yet handle input parsing but the scaffolding for
passing commands back to the main loop are set up.

The implementation of CommandPrompt may seem a little strange, I think.
It has a few interesting features:

* it consumes itself on each transition - meaning you can't assign the
  result of a transition to a variable and then accidentally trigger
  another state update on the original

* it emits events based on a pull model, assigning the results of the
  last input to it's `last_event` field. I implemented it this way to
  avoid the complexity of working with callbacks. On second thoughts
  maybe it'd make it harder to forget about the event by having
  `CommandPrompt::step` emit a tuple `(CommandPrompt, Event)`.

I'm probably going to pull in `nom`[1] for writing the command parser.
This may be overkill but I want excuses to use it ¯\_(ツ)_/¯

[1]: https://github.com/Geal/nom
  • Loading branch information
daniel-ferguson committed Nov 11, 2017
1 parent ad85462 commit 4a1165f
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 33 deletions.
70 changes: 37 additions & 33 deletions src/bin/hi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use termion::input::TermRead;
use termion::raw::IntoRawMode;

use hi::{byte_display, status_bar, Cursor, Frame, State};
use hi::command_prompt::{CommandMachineEvent, CommandPrompt};

fn main() {
env_logger::init().unwrap();
Expand Down Expand Up @@ -61,6 +62,8 @@ fn main() {
);
stdout.flush().unwrap();

let mut command_machine = CommandPrompt::new();

for evt in stdin.events() {
// set panel height to frame height minus status bar and command bar
let main_panel_height = frame.height - 1 - 1;
Expand Down Expand Up @@ -147,40 +150,41 @@ fn main() {
_ => {}
},
State::Prompt => match evt {
Event::Key(Key::Char(x)) => match x {
'\n' => {
cursor.x = 1;
write!(
stdout,
"{}{}",
termion::clear::CurrentLine,
termion::cursor::Hide
).unwrap();
state = State::Wait;
}
_ => {
write!(stdout, "{}", x).unwrap();
cursor.x += 1;
Event::Key(x) => {
command_machine = command_machine.step(x);
match command_machine.last_event {
CommandMachineEvent::Reset => {
cursor.x = command_machine.cursor as u16 + 2;
write!(
stdout,
"{}{}",
termion::clear::CurrentLine,
termion::cursor::Hide
).unwrap();
state = State::Wait;
}
CommandMachineEvent::Update => {
cursor.x = command_machine.cursor as u16 + 2;
write!(
stdout,
"{}{}:{}{}",
termion::cursor::Goto(1, cursor.y),
termion::clear::CurrentLine,
command_machine.text,
termion::cursor::Goto(cursor.x, cursor.y),
).unwrap();
}
CommandMachineEvent::Execute(_) => {
cursor.x = command_machine.cursor as u16 + 2;
write!(
stdout,
"{}{}",
termion::clear::CurrentLine,
termion::cursor::Hide
).unwrap();
state = State::Wait;
}
}
},
Event::Key(Key::Backspace) => if cursor.x > 2 {
cursor.x -= 1;
write!(
stdout,
"{} {}",
termion::cursor::Left(1),
termion::cursor::Left(1),
).unwrap();
},
Event::Key(Key::Ctrl('c')) => {
cursor.x = 1;
write!(
stdout,
"{}{}",
termion::clear::CurrentLine,
termion::cursor::Hide
).unwrap();
state = State::Wait;
}
e => {
let message = format!("{:?}", e);
Expand Down
123 changes: 123 additions & 0 deletions src/command_prompt/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use termion::event::Key;

pub enum CommandMachineEvent {
Reset,
Update,
Execute(Command),
}

#[derive(Debug)]
pub enum Command {
SetWidth(usize),
}

pub struct CommandPrompt {
pub text: String,
pub cursor: usize,
pub last_event: CommandMachineEvent,
}

impl CommandPrompt {
pub fn new() -> Self {
Self {
cursor: 0,
text: String::new(),
last_event: CommandMachineEvent::Reset,
}
}

pub fn step(mut self, key: Key) -> Self {
match key {
Key::Char('\n') => {
self.text.clear();
Self {
cursor: 0,
text: self.text,
last_event: CommandMachineEvent::Reset,
}
}
Key::Ctrl('c') => {
self.text.clear();
Self {
cursor: 0,
text: self.text,
last_event: CommandMachineEvent::Reset,
}
}
Key::Char(x) => {
self.text.push(x);
Self {
cursor: self.cursor + 1,
text: self.text,
last_event: CommandMachineEvent::Update,
}
}
Key::Backspace => if self.cursor > 0 {
self.text.remove(self.cursor - 1);
Self {
cursor: self.cursor - 1,
text: self.text,
last_event: CommandMachineEvent::Update,
}
} else {
Self {
cursor: self.cursor,
text: self.text,
last_event: CommandMachineEvent::Update,
}
},
_ => panic!("oh no"),
}
}
}

#[cfg(test)]
mod tests {
use super::CommandPrompt;
use termion::event::Key;

#[test]
fn it_builds_text_based_on_input() {
let command = CommandPrompt::new();
let command = command.step(Key::Char('h'));
let command = command.step(Key::Char('e'));
let command = command.step(Key::Char('l'));
let command = command.step(Key::Char('l'));
let command = command.step(Key::Char('o'));

assert_eq!(command.text, "hello");
assert_eq!(command.cursor, 5);
}

#[test]
fn it_keeps_track_of_cursor_position() {
let command = CommandPrompt::new();
let command = command.step(Key::Char('h'));
assert_eq!(command.cursor, 1);
let command = command.step(Key::Char('i'));
assert_eq!(command.cursor, 2);
}

#[test]
fn it_deletes_text_when_backspacing() {
let command = CommandPrompt::new();
let command = command.step(Key::Char('h'));
let command = command.step(Key::Char('i'));

let command = command.step(Key::Backspace);
assert_eq!(command.cursor, 1);
assert_eq!(command.text, "h");

let command = command.step(Key::Backspace);
assert_eq!(command.cursor, 0);
assert_eq!(command.text, "");
}

#[test]
fn it_does_nothing_when_backspacing_past_the_start_of_text() {
let command = CommandPrompt::new();
let command = command.step(Key::Backspace);
assert_eq!(command.cursor, 0);
assert_eq!(command.text, "");
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
extern crate log;
extern crate termion;

pub mod command_prompt;

pub struct Cursor {
pub x: u16,
pub y: u16,
Expand Down

0 comments on commit 4a1165f

Please sign in to comment.