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
pull/67/head
sigoden 1 year ago committed by GitHub
parent dc09cb38d4
commit cfb6ce6958
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
Cargo.lock generated

@ -39,6 +39,7 @@ dependencies = [
"inquire", "inquire",
"is-terminal", "is-terminal",
"lazy_static", "lazy_static",
"nu-ansi-term",
"parking_lot", "parking_lot",
"reedline", "reedline",
"reqwest", "reqwest",

@ -37,6 +37,7 @@ fancy-regex = "0.11.0"
base64 = "0.21.0" base64 = "0.21.0"
rustc-hash = "1.1.0" rustc-hash = "1.1.0"
bstr = "1.3.0" bstr = "1.3.0"
nu-ansi-term = "0.46.0"
[dependencies.reqwest] [dependencies.reqwest]
version = "0.11.14" version = "0.11.14"

@ -54,6 +54,8 @@ pub struct Config {
pub dry_run: bool, pub dry_run: bool,
/// If set ture, start a conversation immediately upon repl /// If set ture, start a conversation immediately upon repl
pub conversation_first: bool, pub conversation_first: bool,
/// Is ligth theme
pub light_theme: bool,
/// Predefined roles /// Predefined roles
#[serde(skip)] #[serde(skip)]
pub roles: Vec<Role>, pub roles: Vec<Role>,
@ -75,6 +77,7 @@ impl Default for Config {
proxy: None, proxy: None,
dry_run: false, dry_run: false,
conversation_first: false, conversation_first: false,
light_theme: false,
roles: vec![], roles: vec![],
role: None, role: None,
conversation: None, conversation: None,
@ -409,11 +412,10 @@ impl Config {
fn merge_env_vars(&mut self) { fn merge_env_vars(&mut self) {
if let Ok(value) = env::var(get_env_name("dry_run")) { if let Ok(value) = env::var(get_env_name("dry_run")) {
match value.as_str() { set_bool(&mut self.dry_run, &value);
"1" | "true" => self.dry_run = true, }
"0" | "false" => self.dry_run = false, 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(), key.to_ascii_uppercase(),
) )
} }
fn set_bool(target: &mut bool, value: &str) {
match value {
"1" | "true" => *target = true,
"0" | "false" => *target = false,
_ => {}
}
}

@ -75,10 +75,11 @@ fn start_directive(
no_stream: bool, no_stream: bool,
) -> Result<()> { ) -> Result<()> {
let highlight = config.lock().highlight && stdout().is_terminal(); let highlight = config.lock().highlight && stdout().is_terminal();
let light_theme = config.lock().light_theme;
let output = if no_stream { let output = if no_stream {
let output = client.send_message(input)?; let output = client.send_message(input)?;
if highlight { if highlight {
let mut markdown_render = MarkdownRender::new(); let mut markdown_render = MarkdownRender::new(light_theme);
println!("{}", markdown_render.render(&output).trim()); println!("{}", markdown_render.render(&output).trim());
} else { } else {
println!("{}", output.trim()); println!("{}", output.trim());
@ -92,7 +93,15 @@ fn start_directive(
abort_clone.set_ctrlc(); abort_clone.set_ctrlc();
}) })
.expect("Error setting Ctrl-C handler"); .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(); wg.wait();
output output
}; };

@ -6,9 +6,13 @@ use crate::repl::{ReplyStreamEvent, SharedAbortSignal};
use anyhow::Result; use anyhow::Result;
use crossbeam::channel::Receiver; use crossbeam::channel::Receiver;
pub fn cmd_render_stream(rx: Receiver<ReplyStreamEvent>, abort: SharedAbortSignal) -> Result<()> { pub fn cmd_render_stream(
rx: Receiver<ReplyStreamEvent>,
light_theme: bool,
abort: SharedAbortSignal,
) -> Result<()> {
let mut buffer = String::new(); let mut buffer = String::new();
let mut markdown_render = MarkdownRender::new(); let mut markdown_render = MarkdownRender::new(light_theme);
loop { loop {
if abort.aborted() { if abort.aborted() {
return Ok(()); return Ok(());

@ -7,6 +7,7 @@ use syntect::{easy::HighlightLines, parsing::SyntaxReference};
/// Monokai Extended /// Monokai Extended
const MD_THEME: &[u8] = include_bytes!("../../assets/monokai-extended.theme.bin"); 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 /// Comes from https://github.com/sharkdp/bat/raw/5e77ca37e89c873e4490b42ff556370dc5c6ba4f/assets/syntaxes.bin
const SYNTAXES: &[u8] = include_bytes!("../../assets/syntaxes.bin"); const SYNTAXES: &[u8] = include_bytes!("../../assets/syntaxes.bin");
@ -29,10 +30,14 @@ pub struct MarkdownRender {
} }
impl MarkdownRender { impl MarkdownRender {
pub fn new() -> Self { pub fn new(light_theme: bool) -> Self {
let syntax_set: SyntaxSet = let syntax_set: SyntaxSet =
bincode::deserialize_from(SYNTAXES).expect("invalid syntaxes binary"); 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 code_color = get_code_color(&md_theme);
let md_syntax = syntax_set.find_syntax_by_extension("md").unwrap().clone(); let md_syntax = syntax_set.find_syntax_by_extension("md").unwrap().clone();
let line_type = LineType::Normal; let line_type = LineType::Normal;
@ -223,7 +228,7 @@ mod tests {
#[test] #[test]
fn test_render() { fn test_render() {
let render = MarkdownRender::new(); let render = MarkdownRender::new(true);
assert!(render.find_syntax("csharp").is_some()); assert!(render.find_syntax("csharp").is_some());
} }
} }

@ -19,6 +19,7 @@ pub fn render_stream(
input: &str, input: &str,
client: &ChatGptClient, client: &ChatGptClient,
highlight: bool, highlight: bool,
light_theme: bool,
repl: bool, repl: bool,
abort: SharedAbortSignal, abort: SharedAbortSignal,
wg: WaitGroup, wg: WaitGroup,
@ -28,9 +29,9 @@ pub fn render_stream(
let abort_clone = abort.clone(); let abort_clone = abort.clone();
spawn(move || { spawn(move || {
let err = if repl { let err = if repl {
repl_render_stream(rx, abort) repl_render_stream(rx, light_theme, abort)
} else { } else {
cmd_render_stream(rx, abort) cmd_render_stream(rx, light_theme, abort)
}; };
if let Err(err) = err { if let Err(err) = err {
let err = format!("{err:?}"); let err = format!("{err:?}");

@ -16,11 +16,15 @@ use std::{
}; };
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
pub fn repl_render_stream(rx: Receiver<ReplyStreamEvent>, abort: SharedAbortSignal) -> Result<()> { pub fn repl_render_stream(
rx: Receiver<ReplyStreamEvent>,
light_theme: bool,
abort: SharedAbortSignal,
) -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); 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()?; disable_raw_mode()?;
@ -29,13 +33,14 @@ pub fn repl_render_stream(rx: Receiver<ReplyStreamEvent>, abort: SharedAbortSign
fn repl_render_stream_inner( fn repl_render_stream_inner(
rx: Receiver<ReplyStreamEvent>, rx: Receiver<ReplyStreamEvent>,
light_theme: bool,
abort: SharedAbortSignal, abort: SharedAbortSignal,
writer: &mut Stdout, writer: &mut Stdout,
) -> Result<()> { ) -> Result<()> {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(100); let tick_rate = Duration::from_millis(100);
let mut buffer = String::new(); 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; let terminal_columns = terminal::size()?.0;
loop { loop {
if abort.aborted() { if abort.aborted() {

@ -51,11 +51,13 @@ impl ReplCmdHandler {
return Ok(()); return Ok(());
} }
let highlight = self.config.lock().highlight; let highlight = self.config.lock().highlight;
let light_theme = self.config.lock().light_theme;
let wg = WaitGroup::new(); let wg = WaitGroup::new();
let ret = render_stream( let ret = render_stream(
&input, &input,
&self.client, &self.client,
highlight, highlight,
light_theme,
true, true,
self.abort.clone(), self.abort.clone(),
wg.clone(), wg.clone(),

@ -3,12 +3,17 @@ use super::REPL_COMMANDS;
use crate::config::{Config, SharedConfig}; use crate::config::{Config, SharedConfig};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use nu_ansi_term::Color;
use reedline::{ use reedline::{
default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultValidator, Emacs, 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 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 struct Repl {
pub editor: Reedline, pub editor: Reedline,
@ -16,13 +21,20 @@ pub struct Repl {
impl Repl { impl Repl {
pub fn init(config: SharedConfig) -> Result<Self> { pub fn init(config: SharedConfig) -> Result<Self> {
let completer = Self::create_completer(config); let commands: Vec<String> = 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 keybindings = Self::create_keybindings();
let history = Self::create_history()?; let history = Self::create_history()?;
let menu = Self::create_menu(); let menu = Self::create_menu();
let edit_mode = Box::new(Emacs::new(keybindings)); let edit_mode = Box::new(Emacs::new(keybindings));
let editor = Reedline::create() let editor = Reedline::create()
.with_completer(Box::new(completer)) .with_completer(Box::new(completer))
.with_highlighter(Box::new(highlighter))
.with_history(history) .with_history(history)
.with_menu(menu) .with_menu(menu)
.with_edit_mode(edit_mode) .with_edit_mode(edit_mode)
@ -33,17 +45,24 @@ impl Repl {
Ok(Self { editor }) Ok(Self { editor })
} }
fn create_completer(config: SharedConfig) -> DefaultCompleter { fn create_completer(config: SharedConfig, commands: &[String]) -> DefaultCompleter {
let mut completion: Vec<String> = REPL_COMMANDS let mut completion = commands.to_vec();
.into_iter()
.map(|(v, _)| v.to_string())
.collect();
completion.extend(config.lock().repl_completions()); completion.extend(config.lock().repl_completions());
let mut completer = DefaultCompleter::with_inclusions(&['.', '-', '_']).set_min_word_len(2); let mut completer = DefaultCompleter::with_inclusions(&['.', '-', '_']).set_min_word_len(2);
completer.insert(completion.clone()); completer.insert(completion.clone());
completer 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 { fn create_keybindings() -> Keybindings {
let mut keybindings = default_emacs_keybindings(); let mut keybindings = default_emacs_keybindings();
keybindings.add_binding( keybindings.add_binding(

Loading…
Cancel
Save