From cfb6ce6958751da5e13683ea07f42a5dbc4eae01 Mon Sep 17 00:00:00 2001 From: sigoden Date: Sat, 11 Mar 2023 18:53:36 +0800 Subject: [PATCH] feat: support light theme (#65) There are two ways to enable the light theme: - add `light_theme: true` to config.yaml - use `AICHAT_LIGHT_THEME=true` env var --- Cargo.lock | 1 + Cargo.toml | 1 + assets/monokai-extended-light.theme.bin | Bin 0 -> 15762 bytes src/config/mod.rs | 20 ++++++++++---- src/main.rs | 13 ++++++++-- src/render/cmd.rs | 8 ++++-- src/render/markdown.rs | 11 +++++--- src/render/mod.rs | 5 ++-- src/render/repl.rs | 11 +++++--- src/repl/handler.rs | 2 ++ src/repl/init.rs | 33 +++++++++++++++++++----- 11 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 assets/monokai-extended-light.theme.bin diff --git a/Cargo.lock b/Cargo.lock index b5de47e..f399a9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,7 @@ dependencies = [ "inquire", "is-terminal", "lazy_static", + "nu-ansi-term", "parking_lot", "reedline", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index a40a16d..a144386 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ fancy-regex = "0.11.0" base64 = "0.21.0" rustc-hash = "1.1.0" bstr = "1.3.0" +nu-ansi-term = "0.46.0" [dependencies.reqwest] version = "0.11.14" diff --git a/assets/monokai-extended-light.theme.bin b/assets/monokai-extended-light.theme.bin new file mode 100644 index 0000000000000000000000000000000000000000..f9e96963c50eb664f62d86b3d6b1b4ba13d60a54 GIT binary patch literal 15762 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, @@ -75,6 +77,7 @@ impl Default for Config { proxy: None, dry_run: false, conversation_first: false, + light_theme: false, roles: vec![], role: None, conversation: None, @@ -409,11 +412,10 @@ impl Config { fn merge_env_vars(&mut self) { if let Ok(value) = env::var(get_env_name("dry_run")) { - match value.as_str() { - "1" | "true" => self.dry_run = true, - "0" | "false" => self.dry_run = false, - _ => {} - } + set_bool(&mut self.dry_run, &value); + } + if let Ok(value) = env::var(get_env_name("light_theme")) { + set_bool(&mut self.light_theme, &value); } } } @@ -479,3 +481,11 @@ fn get_env_name(key: &str) -> String { key.to_ascii_uppercase(), ) } + +fn set_bool(target: &mut bool, value: &str) { + match value { + "1" | "true" => *target = true, + "0" | "false" => *target = false, + _ => {} + } +} diff --git a/src/main.rs b/src/main.rs index 5bcdf11..fa0e64e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,10 +75,11 @@ fn start_directive( no_stream: bool, ) -> Result<()> { let highlight = config.lock().highlight && stdout().is_terminal(); + let light_theme = config.lock().light_theme; let output = if no_stream { let output = client.send_message(input)?; if highlight { - let mut markdown_render = MarkdownRender::new(); + let mut markdown_render = MarkdownRender::new(light_theme); println!("{}", markdown_render.render(&output).trim()); } else { println!("{}", output.trim()); @@ -92,7 +93,15 @@ fn start_directive( abort_clone.set_ctrlc(); }) .expect("Error setting Ctrl-C handler"); - let output = render_stream(input, &client, highlight, false, abort, wg.clone())?; + let output = render_stream( + input, + &client, + highlight, + light_theme, + false, + abort, + wg.clone(), + )?; wg.wait(); output }; diff --git a/src/render/cmd.rs b/src/render/cmd.rs index 2a25768..62a0a34 100644 --- a/src/render/cmd.rs +++ b/src/render/cmd.rs @@ -6,9 +6,13 @@ use crate::repl::{ReplyStreamEvent, SharedAbortSignal}; use anyhow::Result; use crossbeam::channel::Receiver; -pub fn cmd_render_stream(rx: Receiver, abort: SharedAbortSignal) -> Result<()> { +pub fn cmd_render_stream( + rx: Receiver, + light_theme: bool, + abort: SharedAbortSignal, +) -> Result<()> { let mut buffer = String::new(); - let mut markdown_render = MarkdownRender::new(); + let mut markdown_render = MarkdownRender::new(light_theme); loop { if abort.aborted() { return Ok(()); diff --git a/src/render/markdown.rs b/src/render/markdown.rs index affaf21..5bdca9d 100644 --- a/src/render/markdown.rs +++ b/src/render/markdown.rs @@ -7,6 +7,7 @@ use syntect::{easy::HighlightLines, parsing::SyntaxReference}; /// Monokai Extended const MD_THEME: &[u8] = include_bytes!("../../assets/monokai-extended.theme.bin"); +const MD_THEME_LIGHT: &[u8] = include_bytes!("../../assets/monokai-extended-light.theme.bin"); /// Comes from https://github.com/sharkdp/bat/raw/5e77ca37e89c873e4490b42ff556370dc5c6ba4f/assets/syntaxes.bin const SYNTAXES: &[u8] = include_bytes!("../../assets/syntaxes.bin"); @@ -29,10 +30,14 @@ pub struct MarkdownRender { } impl MarkdownRender { - pub fn new() -> Self { + pub fn new(light_theme: bool) -> Self { let syntax_set: SyntaxSet = bincode::deserialize_from(SYNTAXES).expect("invalid syntaxes binary"); - let md_theme: Theme = bincode::deserialize_from(MD_THEME).expect("invalid md_theme binary"); + let md_theme: Theme = if light_theme { + bincode::deserialize_from(MD_THEME_LIGHT).expect("invalid theme binary") + } else { + bincode::deserialize_from(MD_THEME).expect("invalid theme binary") + }; let code_color = get_code_color(&md_theme); let md_syntax = syntax_set.find_syntax_by_extension("md").unwrap().clone(); let line_type = LineType::Normal; @@ -223,7 +228,7 @@ mod tests { #[test] fn test_render() { - let render = MarkdownRender::new(); + let render = MarkdownRender::new(true); assert!(render.find_syntax("csharp").is_some()); } } diff --git a/src/render/mod.rs b/src/render/mod.rs index 4d5c6d3..d200087 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -19,6 +19,7 @@ pub fn render_stream( input: &str, client: &ChatGptClient, highlight: bool, + light_theme: bool, repl: bool, abort: SharedAbortSignal, wg: WaitGroup, @@ -28,9 +29,9 @@ pub fn render_stream( let abort_clone = abort.clone(); spawn(move || { let err = if repl { - repl_render_stream(rx, abort) + repl_render_stream(rx, light_theme, abort) } else { - cmd_render_stream(rx, abort) + cmd_render_stream(rx, light_theme, abort) }; if let Err(err) = err { let err = format!("{err:?}"); diff --git a/src/render/repl.rs b/src/render/repl.rs index d7ccbc1..04dee73 100644 --- a/src/render/repl.rs +++ b/src/render/repl.rs @@ -16,11 +16,15 @@ use std::{ }; use unicode_width::UnicodeWidthStr; -pub fn repl_render_stream(rx: Receiver, abort: SharedAbortSignal) -> Result<()> { +pub fn repl_render_stream( + rx: Receiver, + light_theme: bool, + abort: SharedAbortSignal, +) -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); - let ret = repl_render_stream_inner(rx, abort, &mut stdout); + let ret = repl_render_stream_inner(rx, light_theme, abort, &mut stdout); disable_raw_mode()?; @@ -29,13 +33,14 @@ pub fn repl_render_stream(rx: Receiver, abort: SharedAbortSign fn repl_render_stream_inner( rx: Receiver, + light_theme: bool, abort: SharedAbortSignal, writer: &mut Stdout, ) -> Result<()> { let mut last_tick = Instant::now(); let tick_rate = Duration::from_millis(100); let mut buffer = String::new(); - let mut markdown_render = MarkdownRender::new(); + let mut markdown_render = MarkdownRender::new(light_theme); let terminal_columns = terminal::size()?.0; loop { if abort.aborted() { diff --git a/src/repl/handler.rs b/src/repl/handler.rs index ce95855..a7f4b08 100644 --- a/src/repl/handler.rs +++ b/src/repl/handler.rs @@ -51,11 +51,13 @@ impl ReplCmdHandler { return Ok(()); } let highlight = self.config.lock().highlight; + let light_theme = self.config.lock().light_theme; let wg = WaitGroup::new(); let ret = render_stream( &input, &self.client, highlight, + light_theme, true, self.abort.clone(), wg.clone(), diff --git a/src/repl/init.rs b/src/repl/init.rs index 7d0c2bb..ff75eff 100644 --- a/src/repl/init.rs +++ b/src/repl/init.rs @@ -3,12 +3,17 @@ use super::REPL_COMMANDS; use crate::config::{Config, SharedConfig}; use anyhow::{Context, Result}; +use nu_ansi_term::Color; use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultValidator, Emacs, - FileBackedHistory, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, + ExampleHighlighter, FileBackedHistory, KeyCode, KeyModifiers, Keybindings, Reedline, + ReedlineEvent, ReedlineMenu, }; const MENU_NAME: &str = "completion_menu"; +const MATCH_COLOR: Color = Color::Green; +const NEUTRAL_COLOR: Color = Color::White; +const NEUTRAL_COLOR_LIGHT: Color = Color::Black; pub struct Repl { pub editor: Reedline, @@ -16,13 +21,20 @@ pub struct Repl { impl Repl { pub fn init(config: SharedConfig) -> Result { - let completer = Self::create_completer(config); + let commands: Vec = REPL_COMMANDS + .into_iter() + .map(|(v, _)| v.to_string()) + .collect(); + + let completer = Self::create_completer(config.clone(), &commands); + let highlighter = Self::create_highlighter(config, &commands); let keybindings = Self::create_keybindings(); let history = Self::create_history()?; let menu = Self::create_menu(); let edit_mode = Box::new(Emacs::new(keybindings)); let editor = Reedline::create() .with_completer(Box::new(completer)) + .with_highlighter(Box::new(highlighter)) .with_history(history) .with_menu(menu) .with_edit_mode(edit_mode) @@ -33,17 +45,24 @@ impl Repl { Ok(Self { editor }) } - fn create_completer(config: SharedConfig) -> DefaultCompleter { - let mut completion: Vec = REPL_COMMANDS - .into_iter() - .map(|(v, _)| v.to_string()) - .collect(); + fn create_completer(config: SharedConfig, commands: &[String]) -> DefaultCompleter { + let mut completion = commands.to_vec(); completion.extend(config.lock().repl_completions()); let mut completer = DefaultCompleter::with_inclusions(&['.', '-', '_']).set_min_word_len(2); completer.insert(completion.clone()); completer } + fn create_highlighter(config: SharedConfig, commands: &[String]) -> ExampleHighlighter { + let mut highlighter = ExampleHighlighter::new(commands.to_vec()); + if config.lock().light_theme { + highlighter.change_colors(MATCH_COLOR, NEUTRAL_COLOR_LIGHT, NEUTRAL_COLOR_LIGHT); + } else { + highlighter.change_colors(MATCH_COLOR, NEUTRAL_COLOR, NEUTRAL_COLOR); + } + highlighter + } + fn create_keybindings() -> Keybindings { let mut keybindings = default_emacs_keybindings(); keybindings.add_binding(