diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..42a591c --- /dev/null +++ b/src/command.rs @@ -0,0 +1,104 @@ +use std::io::{self, BufRead, ErrorKind, Read, Seek, Write}; +use thiserror::Error; + +#[derive(Debug, Copy, Default, Clone, PartialEq, Eq)] +pub enum Encoding { + #[default] + UTF8Strict, + UTF8Lossy, + Bytes, +} + +#[derive(Debug, Copy, Default, Clone, PartialEq, Eq)] +pub enum LineEnding { + Keep, + StripLF, + + #[default] + StripCRLF, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ReadOptions { + pub encoding: Encoding, + pub ending: LineEnding, + pub max_read: u64, +} + +impl Default for ReadOptions { + #[inline(always)] + fn default() -> Self { + Self { + encoding: Default::default(), + ending: Default::default(), + max_read: 2048, + } + } +} + +pub struct Command { + pub line: String, +} + +#[derive(Debug, Error)] +pub enum CommandReadError { + #[error("An IO Error occurred while attempting to retrieve the next command.")] + IOError(#[from] io::Error), + + #[error( + "The maximum line length limit was encountered while attempting to read the next command." + )] + MaxCommandLengthError(String), + + #[error("The command input was closed.")] + InputClosed, +} + +#[inline] +fn normalize_line_ending(v: &mut String, ending: LineEnding) { + if v.ends_with('\n') { + match ending { + LineEnding::StripLF => { + v.pop(); + } + LineEnding::StripCRLF => { + v.pop(); + if v.ends_with('\r') { + v.pop(); + } + } + _ => { + println!("No match? {}", v.ends_with('\n')); + } + } + } +} + +/// Read a single line of `source` into `sink` using the provided ReadOptions. +pub fn read_command(opts: ReadOptions) -> Result, CommandReadError> { + match opts.encoding { + Encoding::UTF8Strict => {} + encode => unimplemented!( + "Encoding support for {:?} has not been implemented yet!", + encode + ), + } + + let mut line = String::with_capacity(8); + let stdin = io::stdin().lock(); + let n = stdin.take(opts.max_read).read_line(&mut line)?; + if n == 0 { + return Err(CommandReadError::InputClosed); + } + + if n == (opts.max_read as usize) && !line.ends_with('\n') { + return Err(CommandReadError::MaxCommandLengthError(line)); + } + + normalize_line_ending(&mut line, opts.ending); + if line.is_empty() { + Ok(None) + } else { + Ok(Some(line)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 295c2a5..f70cb45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +pub mod command; +pub mod prompt; + use strum::Display; // Terminal Colors diff --git a/src/main.rs b/src/main.rs index e7a11a9..d20bafd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,32 @@ -fn main() { - println!("Hello, world!"); +use std::io::Write; + +use cacoon_lib::{command::{read_command, CommandReadError, ReadOptions}, prompt::write_prompt}; +use anyhow::Result; + +fn main() -> Result<()> { + let mut stdout = std::io::stdout().lock(); + + let prompt = "Cacoon -> "; + let response = "Haha, you said: ".as_bytes(); + + loop { + write_prompt(prompt)?; + match read_command(ReadOptions::default()) { + Ok(Some(command)) => { + // Laugh at the user + stdout.write_all(response)?; + stdout.write_all(command.as_bytes())?; + stdout.write_all(b"\n")?; + stdout.flush()?; + } + Err(CommandReadError::InputClosed) => { + stdout.write_all(b"\nGoodbye.\n")?; + stdout.flush()?; + return Ok(()); + }, + other => { + other?; + }, + } + } } diff --git a/src/prompt.rs b/src/prompt.rs new file mode 100644 index 0000000..30cf6e7 --- /dev/null +++ b/src/prompt.rs @@ -0,0 +1,14 @@ +use std::io::{self, stdout, Write}; + +/// Writes the provided prompt string into the provided output. +pub fn write_prompt_into(prompt: &str, output: &mut W) -> io::Result<()> { + output.write_all(prompt.as_bytes())?; + output.flush()?; + Ok(()) +} + +/// Writes the provided prompt to stdout. +pub fn write_prompt(prompt: &str, ) -> io::Result<()> { + let mut stdout = stdout().lock(); + write_prompt_into(prompt, &mut stdout) +}