diff --git a/src/builtins/set_color.rs b/src/builtins/set_color.rs index 4f096005b..d6e442bdb 100644 --- a/src/builtins/set_color.rs +++ b/src/builtins/set_color.rs @@ -3,60 +3,11 @@ use super::prelude::*; use crate::color::Color; use crate::common::str2wcstring; -use crate::terminal::TerminalCommand::{ - EnterBoldMode, EnterDimMode, EnterItalicsMode, EnterReverseMode, EnterStandoutMode, - EnterUnderlineMode, ExitAttributeMode, -}; +use crate::terminal::TerminalCommand::ExitAttributeMode; use crate::terminal::{best_color, get_color_support, Output, Outputter}; +use crate::text_face::{TextFace, TextStyling}; -#[allow(clippy::too_many_arguments)] -fn print_modifiers( - outp: &mut Outputter, - bold: bool, - underline: bool, - italics: bool, - dim: bool, - reverse: bool, - bg: Color, -) { - if bold { - outp.write_command(EnterBoldMode); - } - - if underline { - outp.write_command(EnterUnderlineMode); - } - - if italics { - outp.write_command(EnterItalicsMode); - } - - if dim { - outp.write_command(EnterDimMode); - } - - #[allow(clippy::collapsible_if)] - if reverse { - if !outp.write_command(EnterReverseMode) { - outp.write_command(EnterStandoutMode); - } - } - if !bg.is_none() && bg.is_normal() { - outp.write_command(ExitAttributeMode); - } -} - -#[allow(clippy::too_many_arguments)] -fn print_colors( - streams: &mut IoStreams, - args: &[&wstr], - bold: bool, - underline: bool, - italics: bool, - dim: bool, - reverse: bool, - bg: Color, -) { +fn print_colors(streams: &mut IoStreams, args: &[&wstr], style: TextStyling, bg: Color) { let outp = &mut Outputter::new_buffering(); // Rebind args to named_colors if there are no args. @@ -70,18 +21,14 @@ fn print_colors( for color_name in args { if streams.out_is_terminal() { - print_modifiers(outp, bold, underline, italics, dim, reverse, bg); - let color = Color::from_wstr(color_name).unwrap_or(Color::NONE); - outp.set_color(color, Color::NONE); - if !bg.is_none() { - outp.write_color(bg, false /* not is_fg */); - } + let fg = Color::from_wstr(color_name).unwrap_or(Color::None); + outp.set_text_face(TextFace::new(fg, bg, style)); } outp.write_wstr(color_name); if streams.out_is_terminal() && !bg.is_none() { // If we have a background, stop it after the color // or it goes to the end of the line and looks ugly. - outp.write_command(ExitAttributeMode); + outp.reset_text_face(false); } outp.writech('\n'); } // conveniently, 'normal' is always the last color so we don't need to reset here @@ -114,11 +61,7 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) - } let mut bgcolor = None; - let mut bold = false; - let mut underline = false; - let mut italics = false; - let mut dim = false; - let mut reverse = false; + let mut style = TextStyling::empty(); let mut print = false; let mut w = WGetopter::new(SHORT_OPTIONS, LONG_OPTIONS, argv); @@ -132,11 +75,11 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) - builtin_print_help(parser, streams, argv[0]); return Ok(SUCCESS); } - 'o' => bold = true, - 'i' => italics = true, - 'd' => dim = true, - 'r' => reverse = true, - 'u' => underline = true, + 'o' => style |= TextStyling::BOLD, + 'i' => style |= TextStyling::ITALICS, + 'd' => style |= TextStyling::DIM, + 'r' => style |= TextStyling::REVERSE, + 'u' => style |= TextStyling::UNDERLINE, 'c' => print = true, ':' => { // We don't error here because "-b" is the only option that requires an argument, @@ -159,7 +102,7 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) - // We want to reclaim argv so grab wopt_index now. let mut wopt_index = w.wopt_index; - let mut bg = Color::from_wstr(bgcolor.unwrap_or(L!(""))).unwrap_or(Color::NONE); + let mut bg = bgcolor.and_then(Color::from_wstr).unwrap_or(Color::None); if bgcolor.is_some() && bg.is_none() { streams.err.append(wgettext_fmt!( "%ls: Unknown color '%ls'\n", @@ -174,17 +117,17 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) - // for --print-colors. Because it's not interesting in terms of display, // just skip it. if bgcolor.is_some() && bg.is_special() { - bg = Color::NONE; + bg = Color::None; } let args = &argv[wopt_index..argc]; - print_colors(streams, args, bold, underline, italics, dim, reverse, bg); + print_colors(streams, args, style, bg); return Ok(SUCCESS); } // Remaining arguments are foreground color. let mut fgcolors = Vec::new(); while wopt_index < argc { - let fg = Color::from_wstr(argv[wopt_index]).unwrap_or(Color::NONE); + let fg = Color::from_wstr(argv[wopt_index]).unwrap_or(Color::None); if fg.is_none() { streams.err.append(wgettext_fmt!( "%ls: Unknown color '%ls'\n", @@ -203,7 +146,7 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) - assert!(fgcolors.is_empty() || !fg.is_none()); let outp = &mut Outputter::new_buffering(); - print_modifiers(outp, bold, underline, italics, dim, reverse, bg); + outp.set_text_face(TextFace::new(Color::None, Color::None, style)); if bgcolor.is_some() && bg.is_normal() { outp.write_command(ExitAttributeMode); } @@ -215,7 +158,7 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) - // We need to do *something* or the lack of any output messes up // when the cartesian product here would make "foo" disappear: // $ echo (set_color foo)bar - outp.set_color(Color::RESET, Color::NONE); + outp.reset_text_face(true); } } if bgcolor.is_some() && !bg.is_normal() && !bg.is_reset() { diff --git a/src/color.rs b/src/color.rs index 7b7db1f52..94c6d161b 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,4 +1,3 @@ -use bitflags::bitflags; use std::cmp::Ordering; use crate::wchar::prelude::*; @@ -22,8 +21,9 @@ fn from_bits(bits: u32) -> Self { } } +/// A type that represents a color. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Type { +pub enum Color { // TODO: remove this? Users should probably use `Option` instead None, Named { idx: u8 }, @@ -32,55 +32,12 @@ pub enum Type { Reset, } -bitflags! { - #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] - pub struct Flags: u8 { - const DEFAULT = 0; - const BOLD = 1<<0; - const UNDERLINE = 1<<1; - const ITALICS = 1<<2; - const DIM = 1<<3; - const REVERSE = 1<<4; - } -} - -/// A type that represents a color. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Color { - pub typ: Type, - pub flags: Flags, -} - impl Color { /// The color white - pub const WHITE: Self = Self { - typ: Type::Named { idx: 7 }, - flags: Flags::DEFAULT, - }; + pub const WHITE: Self = Self::Named { idx: 7 }; /// The color black - pub const BLACK: Self = Self { - typ: Type::Named { idx: 0 }, - flags: Flags::DEFAULT, - }; - - /// The reset special color. - pub const RESET: Self = Self { - typ: Type::Reset, - flags: Flags::DEFAULT, - }; - - /// The normal special color. - pub const NORMAL: Self = Self { - typ: Type::Normal, - flags: Flags::DEFAULT, - }; - - /// The none special color. - pub const NONE: Self = Self { - typ: Type::None, - flags: Flags::DEFAULT, - }; + pub const BLACK: Self = Self::Named { idx: 0 }; /// Parse a color from a string. pub fn from_wstr(s: &wstr) -> Option { @@ -91,35 +48,32 @@ pub fn from_wstr(s: &wstr) -> Option { /// Create an RGB color. pub fn from_rgb(r: u8, g: u8, b: u8) -> Self { - Self { - typ: Type::Rgb(Color24 { r, g, b }), - flags: Flags::DEFAULT, - } + Self::Rgb(Color24 { r, g, b }) } /// Returns whether the color is the normal special color. pub const fn is_normal(self) -> bool { - matches!(self.typ, Type::Normal) + matches!(self, Self::Normal) } /// Returns whether the color is the reset special color. pub const fn is_reset(self) -> bool { - matches!(self.typ, Type::Reset) + matches!(self, Self::Reset) } /// Returns whether the color is the none special color. pub const fn is_none(self) -> bool { - matches!(self.typ, Type::None) + matches!(self, Self::None) } /// Returns whether the color is a named color (like "magenta"). pub const fn is_named(self) -> bool { - matches!(self.typ, Type::Named { .. }) + matches!(self, Self::Named { .. }) } /// Returns whether the color is specified via RGB components. pub const fn is_rgb(self) -> bool { - matches!(self.typ, Type::Rgb(_)) + matches!(self, Self::Rgb(_)) } /// Returns whether the color is special, that is, not rgb or named. @@ -127,73 +81,23 @@ pub const fn is_special(self) -> bool { !self.is_named() && !self.is_rgb() } - /// Returns whether the color is bold. - pub const fn is_bold(self) -> bool { - self.flags.contains(Flags::BOLD) - } - - /// Set whether the color is bold. - pub fn set_bold(&mut self, bold: bool) { - self.flags.set(Flags::BOLD, bold) - } - - /// Returns whether the color is underlined. - pub const fn is_underline(self) -> bool { - self.flags.contains(Flags::UNDERLINE) - } - - /// Set whether the color is underline. - pub fn set_underline(&mut self, underline: bool) { - self.flags.set(Flags::UNDERLINE, underline) - } - - /// Returns whether the color is italics. - pub const fn is_italics(self) -> bool { - self.flags.contains(Flags::ITALICS) - } - - /// Set whether the color is italics. - pub fn set_italics(&mut self, italics: bool) { - self.flags.set(Flags::ITALICS, italics) - } - - /// Returns whether the color is dim. - pub const fn is_dim(self) -> bool { - self.flags.contains(Flags::DIM) - } - - /// Set whether the color is dim. - pub fn set_dim(&mut self, dim: bool) { - self.flags.set(Flags::DIM, dim) - } - - /// Returns whether the color is reverse. - pub const fn is_reverse(self) -> bool { - self.flags.contains(Flags::REVERSE) - } - - /// Set whether the color is reverse. - pub fn set_reverse(&mut self, reverse: bool) { - self.flags.set(Flags::REVERSE, reverse) - } - pub fn is_grayscale(&self) -> bool { - match self.typ { - Type::None => true, - Type::Named { idx } => [0, 7, 8, 15, 16].contains(&idx) || (232..=255).contains(&idx), - Type::Rgb(rgb) => rgb.r == rgb.g && rgb.r == rgb.b, - Type::Normal => true, - Type::Reset => true, + match self { + Self::None => true, + Self::Named { idx } => [0, 7, 8, 15, 16].contains(idx) || (232..=255).contains(idx), + Self::Rgb(rgb) => rgb.r == rgb.g && rgb.r == rgb.b, + Self::Normal => true, + Self::Reset => true, } } /// Returns the name index for the given color. Requires that the color be named or RGB. pub fn to_name_index(self) -> u8 { // TODO: This should look for the nearest color. - match self.typ { - Type::Named { idx } => idx, - Type::Rgb(c) => term16_color_for_rgb(c), - Type::None | Type::Normal | Type::Reset => { + match self { + Self::Named { idx } => idx, + Self::Rgb(c) => term16_color_for_rgb(c), + Self::None | Self::Normal | Self::Reset => { panic!("to_name_index() called on Color that's not named or RGB") } } @@ -201,7 +105,7 @@ pub fn to_name_index(self) -> u8 { /// Returns the term256 index for the given color. Requires that the color be RGB. pub fn to_term256_index(self) -> u8 { - let Type::Rgb(c) = self.typ else { + let Self::Rgb(c) = self else { panic!("Tried to get term256 index of non-RGB color"); }; @@ -210,7 +114,7 @@ pub fn to_term256_index(self) -> u8 { /// Returns the 24 bit color for the given color. Requires that the color be RGB. pub const fn to_color24(self) -> Color24 { - let Type::Rgb(c) = self.typ else { + let Self::Rgb(c) = self else { panic!("Tried to get color24 of non-RGB color"); }; @@ -238,20 +142,13 @@ pub fn named_color_names() -> Vec<&'static wstr> { /// Try parsing a special color name like "normal". fn try_parse_special(special: &wstr) -> Option { - // TODO: this is a very hot function, may need optimization by e.g. comparing length first, - // depending on how well inlining of `simple_icase_compare` works - let typ = if simple_icase_compare(special, L!("normal")) == Ordering::Equal { - Type::Normal + if simple_icase_compare(special, L!("normal")) == Ordering::Equal { + Some(Self::Normal) } else if simple_icase_compare(special, L!("reset")) == Ordering::Equal { - Type::Reset + Some(Self::Reset) } else { - return None; - }; - - Some(Self { - typ, - flags: Flags::default(), - }) + None + } } /// Try parsing an rgb color like "#F0A030". @@ -302,11 +199,8 @@ fn try_parse_named(name: &wstr) -> Option { .binary_search_by(|c| simple_icase_compare(c.name, name)) .ok()?; - Some(Self { - typ: Type::Named { - idx: NAMED_COLORS[i].idx, - }, - flags: Flags::default(), + Some(Self::Named { + idx: NAMED_COLORS[i].idx, }) } } @@ -436,7 +330,7 @@ fn term256_color_for_rgb(color: Color24) -> u8 { #[cfg(test)] mod tests { - use crate::color::{Color, Color24, Flags, Type}; + use crate::color::{Color, Color24}; use crate::wchar::prelude::*; #[test] @@ -466,10 +360,7 @@ fn parse_rgb() { #[test] fn test_term16_color_for_rgb() { for c in 0..=u8::MAX { - let color = Color { - typ: Type::Rgb(Color24 { r: c, g: c, b: c }), - flags: Flags::DEFAULT, - }; + let color = Color::Rgb(Color24 { r: c, g: c, b: c }); let _ = color.to_name_index(); } } diff --git a/src/highlight/highlight.rs b/src/highlight/highlight.rs index a04a710ec..ab72f2f10 100644 --- a/src/highlight/highlight.rs +++ b/src/highlight/highlight.rs @@ -11,7 +11,7 @@ valid_var_name, valid_var_name_char, ASCII_MAX, EXPAND_RESERVED_BASE, EXPAND_RESERVED_END, }; use crate::complete::complete_wrap_map; -use crate::env::Environment; +use crate::env::{EnvVar, Environment}; use crate::expand::{ expand_one, expand_to_command_and_args, ExpandFlags, ExpandResultCode, PROCESS_EXPAND_SELF_STR, }; @@ -27,7 +27,8 @@ parse_util_locate_cmdsubst_range, parse_util_slice_length, MaybeParentheses, }; use crate::path::{path_as_implicit_cd, path_get_cdpath, path_get_path, paths_are_same_file}; -use crate::terminal::{parse_color, Outputter}; +use crate::terminal::{parse_text_face, Outputter}; +use crate::text_face::{TextFace, TextStyling}; use crate::threads::assert_is_background_thread; use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir}; use crate::wchar::{wstr, WString, L}; @@ -71,12 +72,14 @@ pub fn colorize(text: &wstr, colors: &[HighlightSpec], vars: &dyn Environment) - for (i, c) in text.chars().enumerate() { let color = colors[i]; if color != last_color { - outp.set_color(rv.resolve_spec(&color, false, vars), Color::NORMAL); + let mut face = rv.resolve_spec(&color, vars); + face.bg = Color::Normal; // Historical behavior. + outp.set_text_face(face); last_color = color; } outp.writech(c); } - outp.set_color(Color::NORMAL, Color::NORMAL); + outp.set_text_face(TextFace::default()); outp.contents().to_owned() } @@ -107,8 +110,7 @@ pub fn highlight_shell( /// one screen redraw. #[derive(Default)] pub struct HighlightColorResolver { - fg_cache: HashMap, - bg_cache: HashMap, + cache: HashMap, } /// highlight_color_resolver_t resolves highlight specs (like "a command") to actual RGB colors. @@ -119,82 +121,62 @@ pub fn new() -> Self { Default::default() } /// Return an RGB color for a given highlight spec. - pub fn resolve_spec( + pub(crate) fn resolve_spec( &mut self, highlight: &HighlightSpec, - is_background: bool, vars: &dyn Environment, - ) -> Color { - let cache = if is_background { - &mut self.bg_cache - } else { - &mut self.fg_cache - }; - match cache.entry(*highlight) { + ) -> TextFace { + match self.cache.entry(*highlight) { Entry::Occupied(e) => *e.get(), Entry::Vacant(e) => { - let color = Self::resolve_spec_uncached(highlight, is_background, vars); - e.insert(color); - color + let face = Self::resolve_spec_uncached(highlight, vars); + e.insert(face); + face } } } - pub fn resolve_spec_uncached( + pub(crate) fn resolve_spec_uncached( highlight: &HighlightSpec, - is_background: bool, vars: &dyn Environment, - ) -> Color { - let mut result = Color::NORMAL; - let role = if is_background { - highlight.background - } else { - highlight.foreground + ) -> TextFace { + let resolve_role = |role| { + vars.get_unless_empty(get_highlight_var_name(role)) + .or_else(|| vars.get_unless_empty(get_highlight_var_name(get_fallback(role)))) + .or_else(|| vars.get(get_highlight_var_name(HighlightRole::normal))) }; - - let var = vars - .get_unless_empty(get_highlight_var_name(role)) - .or_else(|| vars.get_unless_empty(get_highlight_var_name(get_fallback(role)))) - .or_else(|| vars.get(get_highlight_var_name(HighlightRole::normal))); - if let Some(var) = var { - result = parse_color(&var, is_background); - } + let fg_var = resolve_role(highlight.foreground); + let bg_var = resolve_role(highlight.background); + let mut result = parse_text_face_for_highlight(fg_var.as_ref(), bg_var.as_ref()); // Handle modifiers. - if !is_background && highlight.valid_path { - if let Some(var2) = vars.get(L!("fish_color_valid_path")) { - let result2 = parse_color(&var2, is_background); - if result.is_normal() { - result = result2; - } else if !result2.is_normal() { - // Valid path has an actual color, use it and merge the modifiers. - let mut rescol = result2; - rescol.set_bold(result.is_bold() || result2.is_bold()); - rescol.set_underline(result.is_underline() || result2.is_underline()); - rescol.set_italics(result.is_italics() || result2.is_italics()); - rescol.set_dim(result.is_dim() || result2.is_dim()); - rescol.set_reverse(result.is_reverse() || result2.is_reverse()); - result = rescol; - } else { - if result2.is_bold() { - result.set_bold(true) - }; - if result2.is_underline() { - result.set_underline(true) - }; - if result2.is_italics() { - result.set_italics(true) - }; - if result2.is_dim() { - result.set_dim(true) - }; - if result2.is_reverse() { - result.set_reverse(true) - }; + if highlight.valid_path { + if let Some(valid_path_var) = vars.get(L!("fish_color_valid_path")) { + // Historical behavior is to not apply background. + let valid_path_face = parse_text_face_for_highlight(Some(&valid_path_var), None); + // Apply the foreground, except if it's normal. The intention here is likely + // to only override foreground if the valid path color has an explicit foreground. + if !valid_path_face.fg.is_normal() { + result.fg = valid_path_face.fg; + } + if valid_path_face.is_bold() { + result.set_bold(true); + } + if valid_path_face.is_underline() { + result.set_underline(true); + } + if valid_path_face.is_italics() { + result.set_italics(true); + } + if valid_path_face.is_dim() { + result.set_dim(true); + } + if valid_path_face.is_reverse() { + result.set_reverse(true); } } } - if !is_background && highlight.force_underline { + if highlight.force_underline { result.set_underline(true); } @@ -202,6 +184,32 @@ pub fn resolve_spec_uncached( } } +/// Return the internal color code representing the specified color. +/// TODO: This code should be refactored to enable sharing with builtin_set_color. +/// In particular, the argument parsing still isn't fully capable. +pub(crate) fn parse_text_face_for_highlight( + fg_var: Option<&EnvVar>, + bg_var: Option<&EnvVar>, +) -> TextFace { + let parse_var = |maybe_var: Option<&EnvVar>, is_background| { + let Some(var) = maybe_var else { + return (Color::Normal, TextStyling::empty()); + }; + let (mut color, style) = parse_text_face(var, is_background); + if color.is_none() { + color = Color::Normal; + } + (color, style) + }; + let (fg, fg_style) = parse_var(fg_var, false); + let (bg, bg_style) = parse_var(bg_var, true); + TextFace { + fg, + bg, + style: fg_style | bg_style, + } +} + fn command_is_valid( cmd: &wstr, decoration: StatementDecoration, @@ -1276,7 +1284,7 @@ pub enum HighlightRole { pager_selected_description, } -/// Simply value type describing how a character should be highlighted.. +/// Simple value type describing how a character should be highlighted. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct HighlightSpec { pub foreground: HighlightRole, diff --git a/src/highlight/tests.rs b/src/highlight/tests.rs index 5715a0189..40c9d5e9e 100644 --- a/src/highlight/tests.rs +++ b/src/highlight/tests.rs @@ -695,9 +695,9 @@ fn test_trailing_spaces_after_command() { // Check that 'echo' is underlined for i in 0..4 { - let rgb = resolver.resolve_spec(&colors[i], false, vars); + let face = resolver.resolve_spec(&colors[i], vars); assert!( - rgb.is_underline(), + face.is_underline(), "Character at position {} of 'echo' should be underlined", i ); @@ -705,9 +705,9 @@ fn test_trailing_spaces_after_command() { // Check that trailing spaces are NOT underlined for i in 4..text.len() { - let rgb = resolver.resolve_spec(&colors[i], false, vars); + let face = resolver.resolve_spec(&colors[i], vars); assert!( - !rgb.is_underline(), + !face.is_underline(), "Trailing space at position {} should NOT be underlined", i ); diff --git a/src/lib.rs b/src/lib.rs index 2dbe7378a..db98027cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ pub mod signal; pub mod terminal; pub mod termsize; +pub mod text_face; pub mod threads; pub mod timer; pub mod tinyexpr; diff --git a/src/reader.rs b/src/reader.rs index d598b03e2..df81ce543 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -46,7 +46,6 @@ use crate::builtins::shared::ErrorCode; use crate::builtins::shared::STATUS_CMD_ERROR; use crate::builtins::shared::STATUS_CMD_OK; -use crate::color::Color; use crate::common::restore_term_foreground_process_group_for_exit; use crate::common::{ escape, escape_string, exit_without_destructors, get_ellipsis_char, get_obfuscation_read_char, @@ -71,7 +70,8 @@ use crate::future::IsSomeAnd; use crate::global_safety::RelaxedAtomicBool; use crate::highlight::{ - autosuggest_validate_from_history, highlight_shell, HighlightRole, HighlightSpec, + autosuggest_validate_from_history, highlight_shell, parse_text_face_for_highlight, + HighlightRole, HighlightSpec, }; use crate::history::{ history_session_id, in_private_mode, History, HistorySearch, PersistenceMode, SearchDirection, @@ -123,8 +123,7 @@ signal_check_cancel, signal_clear_cancel, signal_reset_handlers, signal_set_handlers, signal_set_handlers_once, }; -use crate::terminal::parse_color; -use crate::terminal::parse_color_maybe_none; +use crate::terminal::parse_text_face; use crate::terminal::BufferedOutputter; use crate::terminal::Output; use crate::terminal::Outputter; @@ -144,6 +143,7 @@ SYNCHRONIZED_OUTPUT_SUPPORTED, }; use crate::termsize::{termsize_invalidate_tty, termsize_last, termsize_update}; +use crate::text_face::TextFace; use crate::threads::{ assert_is_background_thread, assert_is_main_thread, iothread_service_main_with_timeout, Debounce, @@ -1660,7 +1660,10 @@ fn paint_layout(&mut self, reason: &wstr, is_final_rendering: bool) { let explicit_foreground = self .vars() .get_unless_empty(L!("fish_color_search_match")) - .is_some_and(|var| !parse_color_maybe_none(&var, false).is_none()); + .is_some_and(|var| { + let (color, _flags) = parse_text_face(&var, false); + !color.is_none() + }); for color in &mut colors[range] { if explicit_foreground { @@ -2290,9 +2293,7 @@ fn readline(&mut self, nchars: Option) -> Option { } perror("tcsetattr"); // return to previous mode } - Outputter::stdoutput() - .borrow_mut() - .set_color(Color::RESET, Color::RESET); + Outputter::stdoutput().borrow_mut().reset_text_face(true); } let result = self .rls() @@ -2694,13 +2695,13 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) { let mut outp = Outputter::stdoutput().borrow_mut(); if let Some(fish_color_cancel) = self.vars().get(L!("fish_color_cancel")) { - outp.set_color( - parse_color(&fish_color_cancel, false), - parse_color(&fish_color_cancel, true), - ); + outp.set_text_face(parse_text_face_for_highlight( + Some(&fish_color_cancel), + Some(&fish_color_cancel), + )); } outp.write_wstr(L!("^C")); - outp.set_color(Color::RESET, Color::RESET); + outp.reset_text_face(true); // We print a newline last so the prompt_sp hack doesn't get us. outp.push(b'\n'); @@ -4464,9 +4465,7 @@ fn reader_interactive_init(parser: &Parser) { /// Destroy data for interactive use. fn reader_interactive_destroy() { - Outputter::stdoutput() - .borrow_mut() - .set_color(Color::RESET, Color::RESET); + Outputter::stdoutput().borrow_mut().reset_text_face(true); } /// Return whether fish is currently unwinding the stack in preparation to exit. @@ -4530,7 +4529,7 @@ pub fn reader_write_title( out.write_command(Osc0WindowTitle(&lst)); } - out.set_color(Color::RESET, Color::RESET); + out.reset_text_face(true); if reset_cursor_position && !lst.is_empty() { // Put the cursor back at the beginning of the line (issue #2453). out.write_bytes(b"\r"); @@ -5659,7 +5658,7 @@ fn reader_run_command(parser: &Parser, cmd: &wstr) -> EvalRes { reader_write_title(cmd, parser, true); Outputter::stdoutput() .borrow_mut() - .set_color(Color::NORMAL, Color::NORMAL); + .set_text_face(TextFace::default()); term_donate(false); let time_before = Instant::now(); diff --git a/src/screen.rs b/src/screen.rs index a6d3f35ce..b5abebf0b 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -1038,9 +1038,9 @@ fn update( // Helper function to set a resolved color, using the caching resolver. let mut color_resolver = HighlightColorResolver::new(); let mut set_color = |zelf: &mut Self, c| { - let fg = color_resolver.resolve_spec(&c, false, vars); - let bg = color_resolver.resolve_spec(&c, true, vars); - zelf.outp.borrow_mut().set_color(fg, bg); + zelf.outp + .borrow_mut() + .set_text_face(color_resolver.resolve_spec(&c, vars)); }; let mut cached_layouts = LAYOUT_CACHE_SHARED.lock().unwrap(); diff --git a/src/terminal.rs b/src/terminal.rs index 18f59f327..34b33cd1c 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -1,11 +1,12 @@ // Generic output functions. -use crate::color::{self, Color, Color24}; +use crate::color::{Color, Color24}; use crate::common::ToCString; use crate::common::{self, escape_string, wcs2string, wcs2string_appending, EscapeStringStyle}; use crate::env::EnvVar; use crate::future_feature_flags::{self, FeatureFlag}; use crate::global_safety::RelaxedAtomicBool; use crate::screen::{is_dumb, only_grayscale}; +use crate::text_face::{TextFace, TextStyling}; use crate::threads::MainThread; use crate::wchar::prelude::*; use crate::FLOGF; @@ -231,13 +232,7 @@ pub(crate) fn use_terminfo() -> bool { } fn palette_color(out: &mut impl Output, foreground: bool, mut idx: u8) -> bool { - if only_grayscale() - && !(Color { - typ: color::Type::Named { idx }, - flags: color::Flags::DEFAULT, - }) - .is_grayscale() - { + if only_grayscale() && !(Color::Named { idx }).is_grayscale() { return false; } if use_terminfo() { @@ -441,8 +436,8 @@ const fn new_from_fd(fd: RawFd) -> Self { contents: Vec::new(), buffer_count: 0, fd, - last_fg: Color::NORMAL, - last_bg: Color::NORMAL, + last_fg: Color::Normal, + last_bg: Color::Normal, was_bold: false, was_underline: false, was_italics: false, @@ -492,7 +487,8 @@ pub fn write_color(&mut self, color: Color, is_fg: bool) -> bool { self.write_command(TerminalCommand::SelectRgbColor(is_fg, color.to_color24())) } - pub(crate) fn reset_color(&mut self, weird_workaround: bool) { + /// Unconditionally resets colors and text style. + pub(crate) fn reset_text_face(&mut self, weird_workaround: bool) { if weird_workaround { // If we exit attribute mode, we must first set a color, or previously colored text might // lose its color. Terminals are weird... @@ -500,8 +496,8 @@ pub(crate) fn reset_color(&mut self, weird_workaround: bool) { } use TerminalCommand::ExitAttributeMode; self.write_command(ExitAttributeMode); - self.last_fg = Color::NORMAL; - self.last_bg = Color::NORMAL; + self.last_fg = Color::Normal; + self.last_bg = Color::Normal; self.reset_modes(); } @@ -522,17 +518,18 @@ pub(crate) fn reset_color(&mut self, weird_workaround: bool) { /// - Lastly we may need to write set_a_background or set_a_foreground to set the other half of the /// color pair to what it should be. #[allow(clippy::if_same_then_else)] - pub fn set_color(&mut self, mut fg: Color, bg: Color) { + pub(crate) fn set_text_face(&mut self, face: TextFace) { + let TextFace { mut fg, bg, .. } = face; let mut bg_set = false; let mut last_bg_set = false; - let is_bold = fg.is_bold() || bg.is_bold(); - let is_underline = fg.is_underline() || bg.is_underline(); - let is_italics = fg.is_italics() || bg.is_italics(); - let is_dim = fg.is_dim() || bg.is_dim(); - let is_reverse = fg.is_reverse() || bg.is_reverse(); + let is_bold = face.is_bold(); + let is_underline = face.is_underline(); + let is_italics = face.is_italics(); + let is_dim = face.is_dim(); + let is_reverse = face.is_reverse(); if fg.is_reset() || bg.is_reset() { - self.reset_color(true); + self.reset_text_face(true); return; } use TerminalCommand::{ @@ -544,7 +541,7 @@ pub fn set_color(&mut self, mut fg: Color, bg: Color) { || (self.was_reverse && !is_reverse) { // Only way to exit bold/dim/reverse mode is a reset of all attributes. - self.reset_color(false); + self.reset_text_face(false); } if !self.last_bg.is_special() { // Background was set. @@ -571,7 +568,7 @@ pub fn set_color(&mut self, mut fg: Color, bg: Color) { } if !bg_set && last_bg_set { // Background color changed and is no longer set, so we exit bold mode. - self.reset_color(false); + self.reset_text_face(false); } if !fg.is_none() && self.last_fg != fg { @@ -579,7 +576,7 @@ pub fn set_color(&mut self, mut fg: Color, bg: Color) { write_foreground_color(self, 0); self.write_command(ExitAttributeMode); - self.last_bg = Color::NORMAL; + self.last_bg = Color::Normal; self.reset_modes(); } else { assert!(!fg.is_special()); @@ -735,11 +732,11 @@ fn write_bytes(&mut self, buf: &[u8]) { /// RgbColor::NONE if empty. pub fn best_color(candidates: &[Color], support: ColorSupport) -> Color { if candidates.is_empty() { - return Color::NONE; + return Color::None; } - let mut first_rgb = Color::NONE; - let mut first_named = Color::NONE; + let mut first_rgb = Color::None; + let mut first_named = Color::None; for color in candidates { if first_rgb.is_none() && color.is_rgb() { first_rgb = *color; @@ -763,24 +760,8 @@ pub fn best_color(candidates: &[Color], support: ColorSupport) -> Color { result } -/// Return the internal color code representing the specified color. -/// TODO: This code should be refactored to enable sharing with builtin_set_color. -/// In particular, the argument parsing still isn't fully capable. -pub fn parse_color(var: &EnvVar, is_background: bool) -> Color { - let mut result = parse_color_maybe_none(var, is_background); - if result.is_none() { - result.typ = color::Type::Normal; - } - result -} - -pub fn parse_color_maybe_none(var: &EnvVar, is_background: bool) -> Color { - let mut is_bold = false; - let mut is_underline = false; - let mut is_italics = false; - let mut is_dim = false; - let mut is_reverse = false; - +pub fn parse_text_face(var: &EnvVar, is_background: bool) -> (Color, TextStyling) { + let mut style = TextStyling::empty(); let mut candidates: Vec = Vec::new(); let prefix = L!("--background="); @@ -802,7 +783,7 @@ pub fn parse_color_maybe_none(var: &EnvVar, is_background: bool) -> Color { next_is_background = true; } else if next == "--reverse" || next == "-r" { // Reverse should be meaningful in either context - is_reverse = true; + style |= TextStyling::REVERSE; } else if next.starts_with("-b") { // Look for something like "-bred". // Yes, that length is hardcoded. @@ -810,15 +791,15 @@ pub fn parse_color_maybe_none(var: &EnvVar, is_background: bool) -> Color { } } else { if next == "--bold" || next == "-o" { - is_bold = true; + style |= TextStyling::BOLD; } else if next == "--underline" || next == "-u" { - is_underline = true; + style |= TextStyling::UNDERLINE; } else if next == "--italics" || next == "-i" { - is_italics = true; + style |= TextStyling::ITALICS; } else if next == "--dim" || next == "-d" { - is_dim = true; + style |= TextStyling::DIM; } else if next == "--reverse" || next == "-r" { - is_reverse = true; + style |= TextStyling::REVERSE; } else { color_name = Some(next.as_utfstr()); } @@ -829,13 +810,8 @@ pub fn parse_color_maybe_none(var: &EnvVar, is_background: bool) -> Color { } } - let mut result = best_color(&candidates, get_color_support()); - result.set_bold(is_bold); - result.set_underline(is_underline); - result.set_italics(is_italics); - result.set_dim(is_dim); - result.set_reverse(is_reverse); - result + let color = best_color(&candidates, get_color_support()); + (color, style) } /// The [`Term`] singleton. Initialized via a call to [`setup()`] and surfaced to the outside world via [`term()`]. diff --git a/src/text_face.rs b/src/text_face.rs new file mode 100644 index 000000000..dcfced7d3 --- /dev/null +++ b/src/text_face.rs @@ -0,0 +1,86 @@ +use bitflags::bitflags; + +use crate::color::Color; + +bitflags! { + #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] + pub struct TextStyling: u8 { + const BOLD = 1<<0; + const UNDERLINE = 1<<1; + const ITALICS = 1<<2; + const DIM = 1<<3; + const REVERSE = 1<<4; + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct TextFace { + pub(crate) fg: Color, + pub(crate) bg: Color, + pub(crate) style: TextStyling, +} + +impl Default for TextFace { + fn default() -> Self { + Self { + fg: Color::Normal, + bg: Color::Normal, + style: TextStyling::empty(), + } + } +} + +impl TextFace { + pub fn new(fg: Color, bg: Color, style: TextStyling) -> Self { + Self { fg, bg, style } + } + /// Returns whether the text face is bold. + pub const fn is_bold(self) -> bool { + self.style.contains(TextStyling::BOLD) + } + + /// Set whether the text face is bold. + pub fn set_bold(&mut self, bold: bool) { + self.style.set(TextStyling::BOLD, bold) + } + + /// Returns whether the text face is underlined. + pub const fn is_underline(self) -> bool { + self.style.contains(TextStyling::UNDERLINE) + } + + /// Set whether the text face is underline. + pub fn set_underline(&mut self, underline: bool) { + self.style.set(TextStyling::UNDERLINE, underline) + } + + /// Returns whether the text face is italics. + pub const fn is_italics(self) -> bool { + self.style.contains(TextStyling::ITALICS) + } + + /// Set whether the text face is italics. + pub fn set_italics(&mut self, italics: bool) { + self.style.set(TextStyling::ITALICS, italics) + } + + /// Returns whether the text face is dim. + pub const fn is_dim(self) -> bool { + self.style.contains(TextStyling::DIM) + } + + /// Set whether the text face is dim. + pub fn set_dim(&mut self, dim: bool) { + self.style.set(TextStyling::DIM, dim) + } + + /// Returns whether the text face has reverse foreground/background colors. + pub const fn is_reverse(self) -> bool { + self.style.contains(TextStyling::REVERSE) + } + + /// Set whether the text face has reverse foreground/background colors. + pub fn set_reverse(&mut self, reverse: bool) { + self.style.set(TextStyling::REVERSE, reverse) + } +}