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 0000000..f9e9696 Binary files /dev/null and b/assets/monokai-extended-light.theme.bin differ diff --git a/src/config/mod.rs b/src/config/mod.rs index a670d00..96f19db 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -54,6 +54,8 @@ pub struct Config { pub dry_run: bool, /// If set ture, start a conversation immediately upon repl pub conversation_first: bool, + /// Is ligth theme + pub light_theme: bool, /// Predefined roles #[serde(skip)] pub roles: Vec, @@ -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(