From e7169dcb44dec6b850a31e6faca79b867730a503 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 8 Feb 2025 17:04:32 -0500 Subject: [PATCH] wip: convert named colors to hex strings and fix off by one --- src/ui/cmd.rs | 72 +++++++++++++++++++++++++++++++++++++------------- src/ui/mod.rs | 10 +++---- src/ui/test.rs | 36 +++++++++++++++++++------ 3 files changed, 86 insertions(+), 32 deletions(-) diff --git a/src/ui/cmd.rs b/src/ui/cmd.rs index 2a4e092..2a8b2a1 100644 --- a/src/ui/cmd.rs +++ b/src/ui/cmd.rs @@ -7,9 +7,9 @@ pub enum Cmd<'a> { Write(Option<&'a str>), InsertRows(usize), InsertColumns(usize), - ColorRows(Option, &'a str), - ColorColumns(Option, &'a str), - ColorCell(&'a str), + ColorRows(Option, String), + ColorColumns(Option, String), + ColorCell(String), RenameSheet(Option, &'a str), NewSheet(Option<&'a str>), SelectSheet(&'a str), @@ -165,10 +165,7 @@ fn try_consume_color_cell<'cmd, 'i: 'cmd>( if input.remaining() > 0 && !is_ws(&mut input) { return Err("Invalid command: Did you mean to type `color-cell `?"); } - let arg = input.span(0..).trim(); - if arg.len() == 0 { - 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))); } @@ -330,10 +327,7 @@ fn try_consume_color_rows<'cmd, 'i: 'cmd>( return Err("Invalid command: Did you mean to type `color-rows [count] `?"); } let (idx, rest) = try_consume_usize(input.clone()); - let arg = rest.span(0..).trim(); - if arg.is_empty() { - return Err("Invalid command: `color-rows` requires a color argument"); - } + let arg = parse_color(rest.span(0..).trim())?; return Ok(Some(Cmd::ColorRows(idx, arg))); } @@ -350,19 +344,59 @@ fn try_consume_color_columns<'cmd, 'i: 'cmd>( return Err("Invalid command: Did you mean to type `color-columns [count] `?"); } let (idx, rest) = try_consume_usize(input.clone()); - let arg = rest.span(0..).trim(); - if arg.is_empty() { - return Err("Invalid command: `color-columns` requires a color argument"); - } + let arg = parse_color(rest.span(0..).trim())?; return Ok(Some(Cmd::ColorColumns(idx, arg))); } -fn try_consume_usize<'cmd, 'i: 'cmd>( - mut input: StrCursor<'i>, -) -> (Option, StrCursor<'i>) { +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) { + 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 { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bc5f916..aa02384 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -469,7 +469,7 @@ impl<'ws> Workspace<'ws> { let row_count = _count.unwrap_or(1); let row = self.book.location.row; for r in row..(row+row_count) { - self.book.set_row_style(&[("fill.bg_color", color)], self.book.current_sheet, r)?; + self.book.set_row_style(&[("fill.bg_color", &color)], self.book.current_sheet, r)?; } Ok(None) } @@ -477,7 +477,7 @@ impl<'ws> Workspace<'ws> { let col_count = _count.unwrap_or(1); let col = self.book.location.col; for c in col..(col+col_count) { - self.book.set_col_style(&[("fill.bg_color", color)], self.book.current_sheet, c)?; + self.book.set_col_style(&[("fill.bg_color", &color)], self.book.current_sheet, c)?; } Ok(None) } @@ -488,8 +488,8 @@ impl<'ws> Workspace<'ws> { sheet, row: start.row as i32, column: start.col as i32, - width: (end.col - start.col) as i32, - height: (end.row - start.row) as i32 + width: (end.col - start.col + 1) as i32, + height: (end.row - start.row + 1) as i32 } } else { let address = self.book.location.clone(); @@ -501,7 +501,7 @@ impl<'ws> Workspace<'ws> { height: 1 } }; - self.book.set_cell_style(&[("fill.bg_color", color)], &area)?; + self.book.set_cell_style(&[("fill.bg_color", &color)], &area)?; Ok(None) } Ok(None) => { diff --git a/src/ui/test.rs b/src/ui/test.rs index 5f22d0f..11fb9b5 100644 --- a/src/ui/test.rs +++ b/src/ui/test.rs @@ -2,6 +2,7 @@ use std::process::ExitCode; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use crate::ui::cmd::parse_color; use crate::ui::{Address, Modality}; use super::cmd::{parse, Cmd}; @@ -33,6 +34,10 @@ impl InputScript { self.event(construct_key_event(KeyCode::Tab)) } + pub fn enter(self) -> Self { + self.event(construct_key_event(KeyCode::Enter)) + } + pub fn modified_char(self, c: char, mods: KeyModifiers) -> Self { self.event(construct_modified_key_event(KeyCode::Char(c), mods)) } @@ -42,10 +47,6 @@ impl InputScript { self } - pub fn enter(self) -> Self { - self.event(construct_key_event(KeyCode::Enter)) - } - pub fn esc(self) -> Self { self.event(construct_key_event(KeyCode::Esc)) } @@ -267,7 +268,7 @@ fn test_cmd_color_rows_with_color() { let output = result.unwrap(); assert!(output.is_some()); let cmd = output.unwrap(); - assert_eq!(cmd, Cmd::ColorRows(None, "red")); + assert_eq!(cmd, Cmd::ColorRows(None, parse_color("red").unwrap())); } #[test] @@ -278,7 +279,7 @@ fn test_cmd_color_rows_with_idx_and_color() { let output = result.unwrap(); assert!(output.is_some()); let cmd = output.unwrap(); - assert_eq!(cmd, Cmd::ColorRows(Some(1), "red")); + assert_eq!(cmd, Cmd::ColorRows(Some(1), parse_color("red").unwrap())); } #[test] @@ -289,7 +290,7 @@ fn test_cmd_color_columns_with_color() { let output = result.unwrap(); assert!(output.is_some()); let cmd = output.unwrap(); - assert_eq!(cmd, Cmd::ColorColumns(None, "red")); + assert_eq!(cmd, Cmd::ColorColumns(None, parse_color("red").unwrap())); } #[test] @@ -300,7 +301,7 @@ fn test_cmd_color_columns_with_idx_and_color() { let output = result.unwrap(); assert!(output.is_some()); let cmd = output.unwrap(); - assert_eq!(cmd, Cmd::ColorColumns(Some(1), "red")); + assert_eq!(cmd, Cmd::ColorColumns(Some(1), parse_color("red").unwrap())); } @@ -1152,6 +1153,25 @@ fn test_extend_to_range() { assert_eq!("=B2+1".to_string(), extended_cell); } +#[test] +fn test_color_cells() { + let mut ws = new_workspace(); + script() + .char('v') + .chars("jjll") + .char(':') + .chars("color-cell red") + .enter() + .run(&mut ws) + .expect("Unable to run script"); + for ri in 1..=3 { + for ci in 1..=3 { + let style = ws.book.get_cell_style(ws.book.current_sheet, &Address { row: ri, col: ci }).expect("failed to get style"); + assert_eq!("#800000", style.fill.bg_color.expect(&format!("No background color set for {}:{}", ri, ci)).as_str()); + } + } +} + fn new_workspace<'a>() -> Workspace<'a> { Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook") }