diff --git a/documentation/pages/configuration.md b/documentation/pages/configuration.md index b2420b41..ab3aea60 100644 --- a/documentation/pages/configuration.md +++ b/documentation/pages/configuration.md @@ -23,6 +23,21 @@ Used to specify the default theme. Values can be located through Amp's theme mod It's handy for temporarily changing to a lighter theme when working outdoors, or vice-versa. +### Transparent Background + +**:lucide-tag: v0.8.0+** + +```yaml +transparent_background: false +``` + +When set to `true`, Amp avoids rendering the theme's background color, instead +relying on your terminal emulator's background. This may be helpful if you want +Amp to more closely match your terminal theme, or if you want to use a +transparent background but your terminal renders Amp's content as opaque. +Cursor-line highlighting is always rendered, in addition to (admittedly rare) +token-specific background colors. + ### Tab Width ```yaml diff --git a/src/models/application/preferences/default.yml b/src/models/application/preferences/default.yml index d667db30..6f898aec 100644 --- a/src/models/application/preferences/default.yml +++ b/src/models/application/preferences/default.yml @@ -1,4 +1,5 @@ theme: solarized_dark +transparent_background: false tab_width: 2 soft_tabs: true line_length_guide: 80 diff --git a/src/models/application/preferences/mod.rs b/src/models/application/preferences/mod.rs index 432e4a99..5d5cc60f 100644 --- a/src/models/application/preferences/mod.rs +++ b/src/models/application/preferences/mod.rs @@ -32,6 +32,7 @@ const SOFT_TABS_KEY: &str = "soft_tabs"; const SYNTAX_PATH: &str = "syntaxes"; const TAB_WIDTH_KEY: &str = "tab_width"; const THEME_KEY: &str = "theme"; +const TRANSPARENT_BACKGROUND_KEY: &str = "transparent_background"; const THEME_PATH: &str = "themes"; const TYPES_KEY: &str = "types"; const TYPES_SYNTAX_KEY: &str = "syntax"; @@ -163,6 +164,23 @@ impl Preferences { self.theme = Some(theme.into()); } + pub fn transparent_background(&self) -> bool { + self.data + .as_ref() + .and_then(|data| { + if let Yaml::Boolean(value) = data[TRANSPARENT_BACKGROUND_KEY] { + Some(value) + } else { + None + } + }) + .unwrap_or_else(|| { + self.default[TRANSPARENT_BACKGROUND_KEY] + .as_bool() + .expect("Couldn't find default transparent background setting!") + }) + } + pub fn tab_width(&self, path: Option<&PathBuf>) -> usize { self.data .as_ref() @@ -504,6 +522,21 @@ mod tests { assert_eq!(preferences.theme(), "solarized_dark"); } + #[test] + fn preferences_returns_user_defined_transparent_background() { + let data = YamlLoader::load_from_str("transparent_background: true").unwrap(); + let preferences = Preferences::new(data.into_iter().nth(0)); + + assert!(preferences.transparent_background()); + } + + #[test] + fn preferences_returns_default_transparent_background_when_user_defined_data_not_found() { + let preferences = Preferences::new(None); + + assert!(!preferences.transparent_background()); + } + #[test] fn tab_width_returns_user_defined_data() { let data = YamlLoader::load_from_str("tab_width: 12").unwrap(); diff --git a/src/view/buffer/renderer.rs b/src/view/buffer/renderer.rs index 4fd3dd2a..c522ef75 100644 --- a/src/view/buffer/renderer.rs +++ b/src/view/buffer/renderer.rs @@ -2,7 +2,7 @@ use crate::errors::*; use crate::models::application::Preferences; use crate::view::buffer::line_numbers::*; use crate::view::buffer::{LexemeMapper, MappedLexeme, RenderState}; -use crate::view::color::to_rgb_color; +use crate::view::color::{theme_background, theme_line_highlight, to_rgb_color}; use crate::view::terminal::{Cell, Terminal, TerminalBuffer}; use crate::view::{Colors, RGBColor, Style, RENDER_CACHE_FREQUENCY}; use scribe::buffer::{Buffer, Position, Range}; @@ -142,7 +142,7 @@ impl<'a, 'p> BufferRenderer<'a, 'p> { } } - fn current_char_style(&self, token_color: RGBColor) -> (Style, Colors) { + fn current_char_style(&self, token_fg: RGBColor, token_bg: RGBColor) -> (Style, Colors) { let (style, colors) = match self.highlights { Some(highlight_ranges) => { for range in highlight_ranges { @@ -159,24 +159,34 @@ impl<'a, 'p> BufferRenderer<'a, 'p> { // We aren't inside one of the highlighted areas. // Fall back to other styling considerations. - if self.on_cursor_line() { - (Style::Default, Colors::CustomFocusedForeground(token_color)) - } else { - (Style::Default, Colors::CustomForeground(token_color)) - } - } - None => { - if self.on_cursor_line() { - (Style::Default, Colors::CustomFocusedForeground(token_color)) - } else { - (Style::Default, Colors::CustomForeground(token_color)) - } + (Style::Default, self.token_colors(token_fg, token_bg)) } + None => (Style::Default, self.token_colors(token_fg, token_bg)), }; (style, colors) } + fn token_colors(&self, token_fg: RGBColor, token_bg: RGBColor) -> Colors { + let theme_bg = theme_background(self.theme); + + // Theme-driven token background highlighting; applied + // regardless of transparency preference (rare) + if token_bg != theme_bg { + return Colors::Custom(token_fg, token_bg); + } + + if self.on_cursor_line() { + return Colors::Custom(token_fg, theme_line_highlight(self.theme)); + } + + if self.preferences.transparent_background() { + Colors::CustomForeground(token_fg) + } else { + Colors::Custom(token_fg, token_bg) + } + } + fn print_lexeme>>(&mut self, lexeme: L) { for character in lexeme.into().graphemes(true) { // Ignore newline characters. @@ -187,8 +197,9 @@ impl<'a, 'p> BufferRenderer<'a, 'p> { self.set_cursor(); // Determine the style we'll use to print. - let token_color = to_rgb_color(self.current_style.foreground); - let (style, color) = self.current_char_style(token_color); + let token_fg = to_rgb_color(self.current_style.foreground); + let token_bg = to_rgb_color(self.current_style.background); + let (style, color) = self.current_char_style(token_fg, token_bg); if self.preferences.line_wrapping() && self.screen_position.offset == self.terminal.width() @@ -425,8 +436,10 @@ fn has_trailing_newline(line: &str) -> bool { mod tests { use super::{BufferRenderer, LexemeMapper, MappedLexeme}; use crate::models::application::Preferences; + use crate::view::color::{theme_line_highlight, to_rgb_color}; use crate::view::terminal::*; - use scribe::buffer::Position; + use crate::view::{Colors, Style}; + use scribe::buffer::{Position, Range}; use scribe::util::LineIterator; use scribe::{Buffer, Workspace}; use std::cell::RefCell; @@ -845,4 +858,221 @@ mod tests { expected_content ); } + + fn single_char_cell(buffer: &TerminalBuffer<'_>) -> (Style, Colors) { + buffer + .iter() + .find(|(_, cell)| cell.content == "x") + .map(|(_, cell)| (cell.style, cell.colors)) + .unwrap() + } + + #[test] + fn render_uses_theme_background_when_off_cursor_line() { + let mut workspace = Workspace::new(Path::new(".")).unwrap(); + let mut buffer = Buffer::new(); + buffer.insert("x"); + *buffer.cursor = Position { line: 1, offset: 0 }; + workspace.add_buffer(buffer); + + let terminal = build_terminal().unwrap(); + let mut terminal_buffer = TerminalBuffer::new(terminal.width(), terminal.height()); + let theme_set = ThemeSet::load_defaults(); + let theme = &theme_set.themes["base16-ocean.dark"]; + let preferences = Preferences::new(None); + let render_cache = Rc::new(RefCell::new(HashMap::new())); + let data = workspace.current_buffer.as_ref().unwrap().data(); + let lines = LineIterator::new(&data); + + BufferRenderer::new( + workspace.current_buffer.as_ref().unwrap(), + None, + 0, + &**terminal, + theme, + &preferences, + &render_cache, + &workspace.syntax_set, + &mut terminal_buffer, + ) + .render(lines, None) + .unwrap(); + + assert_eq!( + single_char_cell(&terminal_buffer), + ( + Style::Default, + Colors::Custom( + to_rgb_color(theme.settings.foreground.unwrap()), + to_rgb_color(theme.settings.background.unwrap()) + ), + ) + ); + } + + #[test] + fn render_uses_terminal_background_when_off_cursor_line_with_transparency_enabled() { + let mut workspace = Workspace::new(Path::new(".")).unwrap(); + let mut buffer = Buffer::new(); + buffer.insert("x"); + *buffer.cursor = Position { line: 1, offset: 0 }; + workspace.add_buffer(buffer); + + let terminal = build_terminal().unwrap(); + let mut terminal_buffer = TerminalBuffer::new(terminal.width(), terminal.height()); + let theme_set = ThemeSet::load_defaults(); + let theme = &theme_set.themes["base16-ocean.dark"]; + let data = YamlLoader::load_from_str("transparent_background: true").unwrap(); + let preferences = Preferences::new(data.into_iter().nth(0)); + let render_cache = Rc::new(RefCell::new(HashMap::new())); + let data = workspace.current_buffer.as_ref().unwrap().data(); + let lines = LineIterator::new(&data); + + BufferRenderer::new( + workspace.current_buffer.as_ref().unwrap(), + None, + 0, + &**terminal, + theme, + &preferences, + &render_cache, + &workspace.syntax_set, + &mut terminal_buffer, + ) + .render(lines, None) + .unwrap(); + + assert_eq!( + single_char_cell(&terminal_buffer), + ( + Style::Default, + Colors::CustomForeground(to_rgb_color(theme.settings.foreground.unwrap())), + ) + ); + } + + #[test] + fn render_uses_line_highlight_when_on_cursor_line() { + let mut workspace = Workspace::new(Path::new(".")).unwrap(); + let mut buffer = Buffer::new(); + buffer.insert("x"); + workspace.add_buffer(buffer); + + let terminal = build_terminal().unwrap(); + let mut terminal_buffer = TerminalBuffer::new(terminal.width(), terminal.height()); + let theme_set = ThemeSet::load_defaults(); + let theme = &theme_set.themes["base16-ocean.dark"]; + let preferences = Preferences::new(None); + let render_cache = Rc::new(RefCell::new(HashMap::new())); + let data = workspace.current_buffer.as_ref().unwrap().data(); + let lines = LineIterator::new(&data); + + BufferRenderer::new( + workspace.current_buffer.as_ref().unwrap(), + None, + 0, + &**terminal, + theme, + &preferences, + &render_cache, + &workspace.syntax_set, + &mut terminal_buffer, + ) + .render(lines, None) + .unwrap(); + + assert_eq!( + single_char_cell(&terminal_buffer), + ( + Style::Default, + Colors::Custom( + to_rgb_color(theme.settings.foreground.unwrap()), + theme_line_highlight(theme), + ) + ) + ); + } + + #[test] + fn render_uses_line_highlight_when_on_cursor_line_with_transparency_enabled() { + let mut workspace = Workspace::new(Path::new(".")).unwrap(); + let mut buffer = Buffer::new(); + buffer.insert("x"); + workspace.add_buffer(buffer); + + let terminal = build_terminal().unwrap(); + let mut terminal_buffer = TerminalBuffer::new(terminal.width(), terminal.height()); + let theme_set = ThemeSet::load_defaults(); + let theme = &theme_set.themes["base16-ocean.dark"]; + let data = YamlLoader::load_from_str("transparent_background: true").unwrap(); + let preferences = Preferences::new(data.into_iter().nth(0)); + let render_cache = Rc::new(RefCell::new(HashMap::new())); + let data = workspace.current_buffer.as_ref().unwrap().data(); + let lines = LineIterator::new(&data); + + BufferRenderer::new( + workspace.current_buffer.as_ref().unwrap(), + None, + 0, + &**terminal, + theme, + &preferences, + &render_cache, + &workspace.syntax_set, + &mut terminal_buffer, + ) + .render(lines, None) + .unwrap(); + + assert_eq!( + single_char_cell(&terminal_buffer), + ( + Style::Default, + Colors::Custom( + to_rgb_color(theme.settings.foreground.unwrap()), + theme_line_highlight(theme), + ) + ) + ); + } + + #[test] + fn render_selection_overrides_token_colors() { + let mut workspace = Workspace::new(Path::new(".")).unwrap(); + let mut buffer = Buffer::new(); + buffer.insert("x"); + workspace.add_buffer(buffer); + + let terminal = build_terminal().unwrap(); + let mut terminal_buffer = TerminalBuffer::new(terminal.width(), terminal.height()); + let theme_set = ThemeSet::load_defaults(); + let theme = &theme_set.themes["base16-ocean.dark"]; + let preferences = Preferences::new(None); + let render_cache = Rc::new(RefCell::new(HashMap::new())); + let highlights = vec![Range::new( + Position { line: 0, offset: 0 }, + Position { line: 0, offset: 1 }, + )]; + let data = workspace.current_buffer.as_ref().unwrap().data(); + let lines = LineIterator::new(&data); + + BufferRenderer::new( + workspace.current_buffer.as_ref().unwrap(), + Some(&highlights), + 0, + &**terminal, + theme, + &preferences, + &render_cache, + &workspace.syntax_set, + &mut terminal_buffer, + ) + .render(lines, None) + .unwrap(); + + assert_eq!( + single_char_cell(&terminal_buffer), + (Style::Bold, Colors::SelectMode) + ); + } } diff --git a/src/view/color/map.rs b/src/view/color/map.rs index 7b4294c7..8b47305f 100644 --- a/src/view/color/map.rs +++ b/src/view/color/map.rs @@ -3,31 +3,23 @@ use crate::view::color::{Colors, RGBColor}; use syntect::highlighting::Theme; pub trait ColorMap { - fn map_colors(&self, colors: Colors) -> Colors; + fn map_colors(&self, colors: Colors, transparent_background: bool) -> Colors; } impl ColorMap for Theme { - fn map_colors(&self, colors: Colors) -> Colors { - let fg = self - .settings - .foreground - .map(to_rgb_color) - .unwrap_or(RGBColor(255, 255, 255)); - - let bg = self - .settings - .background - .map(to_rgb_color) - .unwrap_or(RGBColor(0, 0, 0)); - - let alt_bg = self - .settings - .line_highlight - .map(to_rgb_color) - .unwrap_or(RGBColor(55, 55, 55)); + fn map_colors(&self, colors: Colors, transparent_background: bool) -> Colors { + let fg = theme_foreground(self); + let bg = theme_background(self); + let alt_bg = theme_line_highlight(self); match colors { - Colors::Default => Colors::CustomForeground(fg), + Colors::Default => { + if transparent_background { + Colors::CustomForeground(fg) + } else { + Colors::Custom(fg, bg) + } + } Colors::Focused => Colors::Custom(fg, alt_bg), Colors::Inverted => Colors::Custom(bg, fg), Colors::Insert => Colors::Custom(RGBColor(255, 255, 255), RGBColor(0, 180, 0)), @@ -37,9 +29,124 @@ impl ColorMap for Theme { Colors::PathMode => Colors::Custom(RGBColor(255, 255, 255), RGBColor(255, 20, 147)), Colors::SearchMode => Colors::Custom(RGBColor(255, 255, 255), RGBColor(120, 0, 120)), Colors::SelectMode => Colors::Custom(RGBColor(255, 255, 255), RGBColor(0, 120, 160)), - Colors::CustomForeground(custom_fg) => Colors::CustomForeground(custom_fg), + Colors::CustomForeground(custom_fg) => { + if transparent_background { + Colors::CustomForeground(custom_fg) + } else { + Colors::Custom(custom_fg, bg) + } + } Colors::CustomFocusedForeground(custom_fg) => Colors::Custom(custom_fg, alt_bg), Colors::Custom(custom_fg, custom_bg) => Colors::Custom(custom_fg, custom_bg), } } } + +pub fn theme_foreground(theme: &Theme) -> RGBColor { + theme + .settings + .foreground + .map(to_rgb_color) + .unwrap_or(RGBColor(255, 255, 255)) +} + +pub fn theme_background(theme: &Theme) -> RGBColor { + theme + .settings + .background + .map(to_rgb_color) + .unwrap_or(RGBColor(0, 0, 0)) +} + +pub fn theme_line_highlight(theme: &Theme) -> RGBColor { + theme + .settings + .line_highlight + .map(to_rgb_color) + .unwrap_or(RGBColor(55, 55, 55)) +} + +#[cfg(test)] +mod tests { + use super::{theme_background, theme_line_highlight, ColorMap}; + use crate::view::{Colors, RGBColor}; + use syntect::highlighting::{Color, Theme, ThemeSettings}; + + fn theme() -> Theme { + Theme { + name: Some(String::from("Test Theme")), + settings: ThemeSettings { + foreground: Some(Color { + r: 0x11, + g: 0x22, + b: 0x33, + a: 0xFF, + }), + background: Some(Color { + r: 0x22, + g: 0x33, + b: 0x44, + a: 0xFF, + }), + line_highlight: Some(Color { + r: 0x33, + g: 0x44, + b: 0x55, + a: 0xFF, + }), + ..ThemeSettings::default() + }, + ..Theme::default() + } + } + + #[test] + fn map_colors_uses_theme_background_for_default_colors() { + let theme = theme(); + + assert_eq!( + theme.map_colors(Colors::Default, false), + Colors::Custom(RGBColor(0x11, 0x22, 0x33), theme_background(&theme)) + ); + } + + #[test] + fn map_colors_uses_theme_background_for_custom_foreground() { + let theme = theme(); + + assert_eq!( + theme.map_colors(Colors::CustomForeground(RGBColor(1, 2, 3)), false), + Colors::Custom(RGBColor(1, 2, 3), theme_background(&theme)) + ); + } + + #[test] + fn map_colors_uses_line_highlight_for_focused_foreground() { + let theme = theme(); + + assert_eq!( + theme.map_colors(Colors::CustomFocusedForeground(RGBColor(1, 2, 3)), false), + Colors::Custom(RGBColor(1, 2, 3), theme_line_highlight(&theme)) + ); + } + + #[test] + fn map_colors_uses_terminal_background_for_default_colors_when_transparency_enabled() { + let theme = theme(); + + assert_eq!( + theme.map_colors(Colors::Default, true), + Colors::CustomForeground(RGBColor(0x11, 0x22, 0x33)) + ); + } + + #[test] + fn map_colors_uses_terminal_background_for_custom_foreground_when_transparency_enabled() { + let theme = theme(); + + assert_eq!( + theme.map_colors(Colors::CustomForeground(RGBColor(1, 2, 3)), true), + Colors::CustomForeground(RGBColor(1, 2, 3)) + ); + } +} diff --git a/src/view/color/mod.rs b/src/view/color/mod.rs index bd884867..5692d950 100644 --- a/src/view/color/mod.rs +++ b/src/view/color/mod.rs @@ -7,7 +7,7 @@ pub use self::colors::Colors; // Define and export a trait for mapping // convenience Colors to printable equivalents. mod map; -pub use self::map::ColorMap; +pub use self::map::{theme_background, theme_line_highlight, ColorMap}; // Re-export external RGB/RGBA types. pub use self::termion::color::Rgb as RGBColor; diff --git a/src/view/presenter.rs b/src/view/presenter.rs index e4182eed..1904248a 100644 --- a/src/view/presenter.rs +++ b/src/view/presenter.rs @@ -76,12 +76,13 @@ impl<'p> Presenter<'p> { pub fn present(&mut self) -> Result<()> { debug!("rendering terminal buffer to terminal"); + let transparent_background = self.view.preferences.borrow().transparent_background(); for (position, cell) in self.terminal_buffer.iter() { self.view.terminal.print( &position, cell.style, - self.theme.map_colors(cell.colors), + self.theme.map_colors(cell.colors, transparent_background), &cell.content, )?; }