//! Command mode command parsers. use slice_utils::{Measured, Peekable, Seekable, Span, StrCursor}; /// A parsed command entered in during command mode. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { Write(Option<&'a str>), InsertRows(usize), InsertColumns(usize), ColorRows(Option, String), ColorColumns(Option, String), ColorCell(String), RenameSheet(Option, &'a str), NewSheet(Option<&'a str>), SelectSheet(&'a str), Edit(&'a str), Help(Option<&'a str>), ExportCsv(&'a str), SystemPaste, Quit, } /// Parse command text into a `Cmd`. pub fn parse<'cmd, 'i: 'cmd>(input: &'i str) -> Result>, &'static str> { let cursor = StrCursor::new(input); // try consume write command. if let Some(cmd) = try_consume_write(cursor.clone())? { return Ok(Some(cmd)); } if let Some(cmd) = try_consume_new_sheet(cursor.clone())? { return Ok(Some(cmd)); } if let Some(cmd) = try_consume_select_sheet(cursor.clone())? { return Ok(Some(cmd)); } // try consume insert-row command. if let Some(cmd) = try_consume_insert_row(cursor.clone())? { return Ok(Some(cmd)); } // try consume insert-col command. if let Some(cmd) = try_consume_insert_column(cursor.clone())? { return Ok(Some(cmd)); } // Try consume export if let Some(cmd) = try_consume_export_csv(cursor.clone())? { return Ok(Some(cmd)); } // try consume edit command. if let Some(cmd) = try_consume_edit(cursor.clone())? { return Ok(Some(cmd)); } // try consume help command. if let Some(cmd) = try_consume_help(cursor.clone())? { return Ok(Some(cmd)); } // try consume quit command. if let Some(cmd) = try_consume_quit(cursor.clone())? { return Ok(Some(cmd)); } if let Some(cmd) = try_consume_rename_sheet(cursor.clone())? { return Ok(Some(cmd)); } if let Some(cmd) = try_consume_color_rows(cursor.clone())? { return Ok(Some(cmd)); } if let Some(cmd) = try_consume_color_columns(cursor.clone())? { return Ok(Some(cmd)); } if let Some(cmd) = try_consume_color_cell(cursor.clone())? { return Ok(Some(cmd)); } if let Some(cmd) = try_consume_system_paste(cursor.clone())? { return Ok(Some(cmd)); } Ok(None) } fn compare<'i>(input: StrCursor<'i>, compare: &str) -> bool { input.remaining() >= compare.len() && input.span(0..compare.len()) == compare } fn is_ws<'r, 'i: 'r>(input: &'r mut StrCursor<'i>) -> bool { match input.peek_next() { Some(b) => { if *b == (' ' as u8) || *b == ('\t' as u8) || *b == ('\n' as u8) || *b == ('\r' as u8) { input.next(); true } else { false } } _ => false, } } fn try_consume_write<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const SHORT: &'static str = "w"; const LONG: &'static str = "write"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else if compare(input.clone(), SHORT) { input.seek(SHORT.len()); // Should we check for whitespace? } else { return Ok(None); } if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `write `?"); } let arg = input.span(0..).trim(); return Ok(Some(Cmd::Write(if arg.is_empty() { None } else { Some(arg) }))); } fn try_consume_export_csv<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const LONG: &'static str = "export-csv"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else { return Ok(None); } if input.remaining() == 0 || !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `export `?"); } let arg = input.span(0..).trim(); return Ok(Some(Cmd::ExportCsv(arg))); } fn try_consume_new_sheet<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const LONG: &'static str = "new-sheet"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else { return Ok(None); } if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `new-sheet `?"); } let arg = input.span(0..).trim(); return Ok(Some(Cmd::NewSheet(if arg.is_empty() { None } else { Some(arg) }))); } fn try_consume_select_sheet<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const LONG: &'static str = "select-sheet"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else { return Ok(None); } if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `select-sheet `?"); } let arg = input.span(0..).trim(); if arg.is_empty() { return Err("Invalid command: Did you forget the sheet name? `select-sheet `?"); } return Ok(Some(Cmd::SelectSheet(arg))); } fn try_consume_color_cell<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const SHORT: &'static str = "cc"; const LONG: &'static str = "color-cell"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else if compare(input.clone(), SHORT) { input.seek(SHORT.len()); } else { return Ok(None); }; if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `color-cell `?"); } let arg = parse_color(input.span(0..).trim())?; return Ok(Some(Cmd::ColorCell(arg))); } fn try_consume_insert_row<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const SHORT: &'static str = "ir"; const LONG: &'static str = "insert-rows"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else if compare(input.clone(), SHORT) { input.seek(SHORT.len()); } else { return Ok(None); }; if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `insert-rows `?"); } let arg = input.span(0..).trim(); return Ok(Some(Cmd::InsertRows(if arg.is_empty() { 1 } else { if let Ok(count) = arg.parse() { count } else { return Err("You must pass in a non negative number for the row count"); } }))); } fn try_consume_insert_column<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const SHORT: &'static str = "ic"; const LONG: &'static str = "insert-cols"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else if compare(input.clone(), SHORT) { input.seek(SHORT.len()); } else { return Ok(None); }; if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `insert-cols `?"); } let arg = input.span(0..).trim(); return Ok(Some(Cmd::InsertColumns(if arg.is_empty() { 1 } else { if let Ok(count) = arg.parse() { count } else { return Err("You must pass in a non negative number for the column count"); } }))); } fn try_consume_edit<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const SHORT: &'static str = "e"; const LONG: &'static str = "edit"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else if compare(input.clone(), SHORT) { input.seek(SHORT.len()); } else { return Ok(None); }; if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `edit `?"); } let arg = input.span(0..).trim(); return Ok(Some(Cmd::Edit(if arg.is_empty() { return Err("You must pass in a path to edit"); } else { arg }))); } fn try_consume_help<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const SHORT: &'static str = "?"; const LONG: &'static str = "help"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else if compare(input.clone(), SHORT) { input.seek(SHORT.len()); // Should we check for whitespace? } else { return Ok(None); } if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `help `?"); } let arg = input.span(0..).trim(); return Ok(Some(Cmd::Help(if arg.is_empty() { None } else { Some(arg) }))); } fn try_consume_quit<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const SHORT: &'static str = "q"; const LONG: &'static str = "quit"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else if compare(input.clone(), SHORT) { input.seek(SHORT.len()); // Should we check for whitespace? } else { return Ok(None); } if input.remaining() > 0 { return Err("Invalid command: Quit does not take an argument"); } return Ok(Some(Cmd::Quit)); } fn try_consume_system_paste<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const LONG: &'static str = "system-paste"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else { return Ok(None); } if input.remaining() > 0 { return Err("Invalid command: system-paste does not take an argument"); } return Ok(Some(Cmd::SystemPaste)); } fn try_consume_rename_sheet<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const LONG: &'static str = "rename-sheet"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else { return Ok(None); } if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `rename-sheet [idx] `?"); } let (idx, rest) = try_consume_usize(input.clone()); let arg = rest.span(0..).trim(); if arg.is_empty() { return Err("Invalid command: `rename-sheet` requires a sheet name argument"); } return Ok(Some(Cmd::RenameSheet(idx, arg))); } fn try_consume_color_rows<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const LONG: &'static str = "color-rows"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else { return Ok(None); } if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `color-rows [count] `?"); } let (idx, rest) = try_consume_usize(input.clone()); let arg = parse_color(rest.span(0..).trim())?; return Ok(Some(Cmd::ColorRows(idx, arg))); } fn try_consume_color_columns<'cmd, 'i: 'cmd>( mut input: StrCursor<'i>, ) -> Result>, &'static str> { const LONG: &'static str = "color-columns"; if compare(input.clone(), LONG) { input.seek(LONG.len()); } else { return Ok(None); } if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `color-columns [count] `?"); } let (idx, rest) = try_consume_usize(input.clone()); let arg = parse_color(rest.span(0..).trim())?; return Ok(Some(Cmd::ColorColumns(idx, arg))); } pub(crate) fn parse_color(color: &str) -> Result { use colorsys::{Ansi256, Rgb}; if color.is_empty() { return Err("Invalid command: `color-columns` requires a color argument"); } let parsed = match color.to_lowercase().as_str() { "black" => Ansi256::new(0).as_rgb().to_hex_string(), "red" => Ansi256::new(1).as_rgb().to_hex_string(), "green" => Ansi256::new(2).as_rgb().to_hex_string(), "yellow" => Ansi256::new(3).as_rgb().to_hex_string(), "blue" => Ansi256::new(4).as_rgb().to_hex_string(), "magenta" => Ansi256::new(5).as_rgb().to_hex_string(), "cyan" => Ansi256::new(6).as_rgb().to_hex_string(), "gray" | "grey" => Ansi256::new(7).as_rgb().to_hex_string(), "darkgrey" | "darkgray" => Ansi256::new(8).as_rgb().to_hex_string(), "lightred" => Ansi256::new(9).as_rgb().to_hex_string(), "lightgreen" => Ansi256::new(10).as_rgb().to_hex_string(), "lightyellow" => Ansi256::new(11).as_rgb().to_hex_string(), "lightblue" => Ansi256::new(12).as_rgb().to_hex_string(), "lightmagenta" => Ansi256::new(13).as_rgb().to_hex_string(), "lightcyan" => Ansi256::new(14).as_rgb().to_hex_string(), "white" => Ansi256::new(15).as_rgb().to_hex_string(), candidate => { if candidate.starts_with("#") { candidate.to_string() } else if candidate.starts_with("rgb(") { if let Ok(rgb) = ::from_str(candidate) { // Note that the colorsys rgb model clamps the f64 values to no more // than 255.0 so the below casts are safe. rgb.to_hex_string() } else { return Err("Invalid color"); } } else { return Err("Invalid color"); } } }; Ok(parsed) } fn try_consume_usize<'cmd, 'i: 'cmd>(mut input: StrCursor<'i>) -> (Option, StrCursor<'i>) { let mut out = String::new(); let original_input = input.clone(); while input .peek_next() .map(|c| (*c as char).is_ascii_digit()) .unwrap_or(false) { out.push(*input.next().unwrap() as char); } if out.len() > 0 { return (Some(out.parse().unwrap()), input.clone()); } (None, original_input) }