set_color: allow resetting specific attributes

Add an optional `on`/`off`` value to italics/reverse/striketrough
to allow turning of the attribute without having to use the `normal`
color, i.e. reset the whole style

Part 1/3 of #12495

Part of #12507
This commit is contained in:
Nahor
2026-03-01 13:10:37 -08:00
committed by Johannes Altmanninger
parent cba82a3c64
commit a893dd10f4
16 changed files with 347 additions and 83 deletions

View File

@@ -4,6 +4,7 @@ fish ?.?.? (released ???)
Notable improvements and fixes Notable improvements and fixes
------------------------------ ------------------------------
- New Spanish translations (:issue:`12489`). - New Spanish translations (:issue:`12489`).
- ``set_color`` is able to turn off italics, reverse mode and strikethrough individually (e.g. ``--italics=off``).
For distributors and developers For distributors and developers
------------------------------- -------------------------------

View File

@@ -48,14 +48,14 @@ The following options are available:
**-d** or **--dim** **-d** or **--dim**
Sets dim mode. Sets dim mode.
**-i** or **--italics** **-i** or **--italics**, or **-iSTATE** or **--italics=STATE**
Sets italics mode. Sets italics mode. The state can be **on** / **true** (default), or **off** / **false**
**-r** or **--reverse** **-r** or **--reverse**, or **-iSTATE** or **--reverse=STATE**
Sets reverse mode. Sets reverse mode. The state can be **on** / **true** (default), or **off** / **false**
**-s** or **--strikethrough** **-s** or **--strikethrough**, or **-sSTATE** or **--strikethrough=STATE**
Sets strikethrough mode. Sets strikethrough mode. The state can be **on** / **true** (default), or **off** / **false**
**-u** or **--underline**, or **-uSTYLE** or **--underline=STYLE** **-u** or **--underline**, or **-uSTYLE** or **--underline=STYLE**
Set the underline mode; supported styles are **single** (default), **double**, **curly**, **dotted** and **dashed**. Set the underline mode; supported styles are **single** (default), **double**, **curly**, **dotted** and **dashed**.

View File

@@ -199,6 +199,10 @@ msgstr ""
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "" msgstr ""
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "" msgstr ""

View File

@@ -199,6 +199,10 @@ msgstr ""
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "" msgstr ""
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "" msgstr ""

View File

@@ -199,6 +199,10 @@ msgstr "%s: %s: modo no válido"
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "%s: %s: nombre de modo no válido. Consulte `help %s`" msgstr "%s: %s: nombre de modo no válido. Consulte `help %s`"
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "%s: %s: escala no válida" msgstr "%s: %s: escala no válida"

View File

@@ -328,6 +328,10 @@ msgstr "%s : %s : mode invalide"
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "%s : %s : nom de mode invalide. Voir « help %s »" msgstr "%s : %s : nom de mode invalide. Voir « help %s »"
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "%s : %s : échelle invalide" msgstr "%s : %s : échelle invalide"

View File

@@ -195,6 +195,10 @@ msgstr ""
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "" msgstr ""
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "" msgstr ""

View File

@@ -200,6 +200,10 @@ msgstr ""
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "" msgstr ""
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "" msgstr ""

View File

@@ -196,6 +196,10 @@ msgstr ""
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "" msgstr ""
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "" msgstr ""

View File

@@ -220,6 +220,10 @@ msgstr "%s: %s: 无效舍入模式"
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "%s: %s: 无效模式名。参见 `help %s`" msgstr "%s: %s: 无效模式名。参见 `help %s`"
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "%s: %s: 无效位数" msgstr "%s: %s: 无效位数"

View File

@@ -193,6 +193,10 @@ msgstr "%s%s無效的模式"
msgid "%s: %s: invalid mode name. See `help %s`" msgid "%s: %s: invalid mode name. See `help %s`"
msgstr "%s%s無效的模式名稱。參見「help %s」" msgstr "%s%s無效的模式名稱。參見「help %s」"
#, c-format
msgid "%s: %s: invalid option argument: %s"
msgstr ""
#, c-format #, c-format
msgid "%s: %s: invalid scale" msgid "%s: %s: invalid scale"
msgstr "%s%s無效的小數位數" msgstr "%s%s無效的小數位數"

View File

@@ -96,6 +96,15 @@ pub fn set_color(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -
); );
return Err(STATUS_INVALID_ARGS); return Err(STATUS_INVALID_ARGS);
} }
Err(InvalidOptArg(name, value)) => {
streams.err.appendln(&wgettext_fmt!(
"%s: %s: invalid option argument: %s",
argv[0],
name,
value
));
return Err(STATUS_INVALID_ARGS);
}
Err(UnknownColor(arg)) => { Err(UnknownColor(arg)) => {
streams streams
.err .err

View File

@@ -24,7 +24,9 @@
}; };
use crate::path::{path_as_implicit_cd, path_get_cdpath, path_get_path, paths_are_same_file}; use crate::path::{path_as_implicit_cd, path_get_cdpath, path_get_path, paths_are_same_file};
use crate::terminal::Outputter; use crate::terminal::Outputter;
use crate::text_face::{SpecifiedTextFace, TextFace, TextStyling, UnderlineStyle, parse_text_face}; use crate::text_face::{
ResettableStyle, SpecifiedTextFace, TextFace, UnderlineStyle, parse_text_face,
};
use crate::threads::assert_is_background_thread; use crate::threads::assert_is_background_thread;
use crate::tokenizer::{PipeOrRedir, variable_assignment_equals_pos}; use crate::tokenizer::{PipeOrRedir, variable_assignment_equals_pos};
use fish_color::Color; use fish_color::Color;
@@ -179,7 +181,9 @@ fn resolve_spec_uncached(highlight: &HighlightSpec, vars: &dyn Environment) -> T
face.bg = bg_face.bg; face.bg = bg_face.bg;
// In case the background role is different from the foreground one, we ignore its style // In case the background role is different from the foreground one, we ignore its style
// except for reverse mode. // except for reverse mode.
face.style.reverse |= bg_face.style.is_reverse(); if face.style.reverse != ResettableStyle::On {
face.style.reverse = bg_face.style.reverse;
}
} }
// Handle modifiers. // Handle modifiers.
@@ -213,11 +217,7 @@ pub(crate) fn parse_text_face_for_highlight(var: &EnvVar) -> Option<TextFace> {
let fg = face.fg.unwrap_or(default.fg); let fg = face.fg.unwrap_or(default.fg);
let bg = face.bg.unwrap_or(default.bg); let bg = face.bg.unwrap_or(default.bg);
let underline_color = face.underline_color.unwrap_or(default.underline_color); let underline_color = face.underline_color.unwrap_or(default.underline_color);
let style = if face.style != TextStyling::unknown() { let style = default.style.union_prefer_right(face.style);
face.style
} else {
TextStyling::terminal_default()
};
TextFace { TextFace {
fg, fg,
bg, bg,
@@ -1312,7 +1312,7 @@ pub struct HighlightSpec {
mod tests { mod tests {
use super::{HighlightColorResolver, HighlightRole, HighlightSpec, highlight_shell}; use super::{HighlightColorResolver, HighlightRole, HighlightSpec, highlight_shell};
use crate::common::ScopeGuard; use crate::common::ScopeGuard;
use crate::env::{EnvMode, EnvSetMode, Environment as _}; use crate::env::{EnvMode, EnvSetMode, EnvVar, EnvVarFlags, Environment as _};
use crate::future_feature_flags::{self, FeatureFlag}; use crate::future_feature_flags::{self, FeatureFlag};
use crate::highlight::parse_text_face_for_highlight; use crate::highlight::parse_text_face_for_highlight;
use crate::operation_context::{EXPANSION_LIMIT_BACKGROUND, OperationContext}; use crate::operation_context::{EXPANSION_LIMIT_BACKGROUND, OperationContext};
@@ -1896,4 +1896,31 @@ fn test_resolve_role() {
parse_text_face_for_highlight(&vars.get(L!("fish_color_command")).unwrap()).unwrap(); parse_text_face_for_highlight(&vars.get(L!("fish_color_command")).unwrap()).unwrap();
assert_eq!(face, command_face); assert_eq!(face, command_face);
} }
#[test]
fn test_parse_text_face_for_highlight_fully_specified() {
let assert_all_set = |values: Vec<WString>| {
let var = EnvVar::new_vec(values.clone(), EnvVarFlags::empty());
let face = parse_text_face_for_highlight(&var);
assert!(
face.is_some_and(|face| face.all_set()),
"Underspecified result for {:?}\n => {:?}",
values,
face
);
};
assert_all_set(vec![L!("normal").into()]);
assert_all_set(vec![L!("green").into()]);
assert_all_set(vec![L!("--background=normal").into()]);
assert_all_set(vec![L!("--background=green").into()]);
assert_all_set(vec![L!("--underline-color=normal").into()]);
assert_all_set(vec![L!("--underline-color=green").into()]);
assert_all_set(vec![L!("--italics").into()]);
assert_all_set(vec![L!("--italics=off").into()]);
assert_all_set(vec![L!("--reverse").into()]);
assert_all_set(vec![L!("--reverse=off").into()]);
assert_all_set(vec![L!("--strikethrough").into()]);
assert_all_set(vec![L!("--strikethrough=off").into()]);
}
} }

View File

@@ -3,7 +3,7 @@
use crate::future_feature_flags::{self, FeatureFlag}; use crate::future_feature_flags::{self, FeatureFlag};
use crate::prelude::*; use crate::prelude::*;
use crate::screen::{is_dumb, only_grayscale}; use crate::screen::{is_dumb, only_grayscale};
use crate::text_face::{TextFace, TextStyling, UnderlineStyle}; use crate::text_face::{ResettableStyle, TextFace, TextStyling, UnderlineStyle};
use crate::threads::MainThread; use crate::threads::MainThread;
use bitflags::bitflags; use bitflags::bitflags;
use fish_color::{Color, Color24}; use fish_color::{Color, Color24};
@@ -54,6 +54,7 @@ pub(crate) enum SgrTerminalCommand {
EnterStrikethroughMode, EnterStrikethroughMode,
ExitItalicsMode, ExitItalicsMode,
ExitUnderlineMode, ExitUnderlineMode,
ExitReverseMode,
ExitStrikethroughMode, ExitStrikethroughMode,
// Colors // Colors
@@ -377,7 +378,7 @@ fn set_text_face_internal(
use SgrTerminalCommand::{ use SgrTerminalCommand::{
DefaultBackgroundColor, DefaultUnderlineColor, EnterBoldMode, EnterDimMode, DefaultBackgroundColor, DefaultUnderlineColor, EnterBoldMode, EnterDimMode,
EnterItalicsMode, EnterReverseMode, EnterStrikethroughMode, EnterUnderlineMode, EnterItalicsMode, EnterReverseMode, EnterStrikethroughMode, EnterUnderlineMode,
ExitItalicsMode, ExitStrikethroughMode, ExitUnderlineMode, ExitItalicsMode, ExitReverseMode, ExitStrikethroughMode, ExitUnderlineMode,
}; };
let mut style_writer = self.style_writer(); let mut style_writer = self.style_writer();
@@ -388,8 +389,10 @@ fn set_text_face_internal(
// Removes all styles that are individually resettable. // Removes all styles that are individually resettable.
let non_resettable = |mut style: TextStyling| { let non_resettable = |mut style: TextStyling| {
style.italics = false; style.italics = ResettableStyle::Unchanged;
style.underline_style = None; style.underline_style = None;
style.reverse = ResettableStyle::Unchanged;
style.strikethrough = ResettableStyle::Unchanged;
style style
}; };
let non_resettable_attributes_to_unset = non_resettable(style_writer.last().style) let non_resettable_attributes_to_unset = non_resettable(style_writer.last().style)
@@ -458,14 +461,6 @@ fn set_text_face_internal(
} }
} }
let was_italics = style_writer.last().style.is_italics();
if !style.is_italics() && was_italics && style_writer.write_command(ExitItalicsMode) {
style_writer.last().style.italics = false;
} else if style.is_italics() && !was_italics && style_writer.write_command(EnterItalicsMode)
{
style_writer.last().style.italics = true;
}
if style.is_dim() if style.is_dim()
&& !style_writer.last().style.is_dim() && !style_writer.last().style.is_dim()
&& style_writer.write_command(EnterDimMode) && style_writer.write_command(EnterDimMode)
@@ -473,25 +468,46 @@ fn set_text_face_internal(
style_writer.last().style.dim = true; style_writer.last().style.dim = true;
} }
let was_strikethrough = style_writer.last().style.is_strikethrough(); let mut current_style = style_writer.last().style;
if !style.is_strikethrough() let mut apply_resettable_style =
&& was_strikethrough |new_style: ResettableStyle,
&& style_writer.write_command(ExitStrikethroughMode) current_style: &mut ResettableStyle,
{ enter_cmd: SgrTerminalCommand,
style_writer.last().style.strikethrough = false; exit_cmd: SgrTerminalCommand| {
} else if style.is_strikethrough() if new_style == *current_style {
&& !was_strikethrough return;
&& style_writer.write_command(EnterStrikethroughMode) }
{ let cmd = match new_style {
style_writer.last().style.strikethrough = true; ResettableStyle::Unchanged => {
} return;
}
ResettableStyle::On => enter_cmd,
ResettableStyle::Off => exit_cmd,
};
if style_writer.write_command(cmd) {
*current_style = new_style;
}
};
if style.is_reverse() apply_resettable_style(
&& !style_writer.last().style.is_reverse() style.italics,
&& style_writer.write_command(EnterReverseMode) &mut current_style.italics,
{ EnterItalicsMode,
style_writer.last().style.reverse = true; ExitItalicsMode,
} );
apply_resettable_style(
style.reverse,
&mut current_style.reverse,
EnterReverseMode,
ExitReverseMode,
);
apply_resettable_style(
style.strikethrough,
&mut current_style.strikethrough,
EnterStrikethroughMode,
ExitStrikethroughMode,
);
style_writer.last().style = current_style;
} }
/// Write a wide character to the receiver. /// Write a wide character to the receiver.
@@ -655,6 +671,7 @@ pub(crate) fn write_command(&mut self, cmd: SgrTerminalCommand) -> bool {
EnterItalicsMode => self.write_param_str(1, b"3"), EnterItalicsMode => self.write_param_str(1, b"3"),
EnterUnderlineMode(style) => self.write_underline_mode(style), EnterUnderlineMode(style) => self.write_underline_mode(style),
EnterReverseMode => self.write_param_str(1, b"7"), EnterReverseMode => self.write_param_str(1, b"7"),
ExitReverseMode => self.write_param_str(1, b"27"),
EnterStrikethroughMode => self.write_param_str(1, b"9"), EnterStrikethroughMode => self.write_param_str(1, b"9"),
ExitStrikethroughMode => self.write_param_str(1, b"29"), ExitStrikethroughMode => self.write_param_str(1, b"29"),
ExitItalicsMode => self.write_param_str(1, b"23"), ExitItalicsMode => self.write_param_str(1, b"23"),
@@ -771,7 +788,9 @@ fn drop(&mut self) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use fish_color::Color24; use fish_color::{Color, Color24};
use crate::text_face::{ResettableStyle, TextFace, TextStyling};
use super::{ use super::{
Outputter, Outputter,
@@ -867,4 +886,49 @@ fn sgr_max_length() {
) )
); );
} }
#[test]
fn resettable_style_attribute() {
use ResettableStyle::{Off, On, Unchanged};
let mut outp = Outputter::new_buffering_no_assume_normal();
let mut set_attr =
|italics: ResettableStyle, reverse: ResettableStyle, strikethrough: ResettableStyle| {
let mut style = TextStyling::unknown_style();
style.italics = italics;
style.reverse = reverse;
style.strikethrough = strikethrough;
let face = TextFace::new(Color::None, Color::None, Color::None, style);
outp.set_text_face(face);
};
// `#[cfg_attr(...)]` because `#[rustfmt::skip]` triggers `error[E0658]: attributes on expressions are experimental`
#[cfg_attr(any(), rustfmt::skip)]
{
set_attr(On, Unchanged, Off);
set_attr(On, On, Unchanged);
set_attr(Unchanged, On, Unchanged);
set_attr(Unchanged, Unchanged, On);
set_attr(Off, Unchanged, On);
set_attr(Off, Off, Unchanged);
set_attr(Unchanged, Off, Unchanged);
set_attr(Unchanged, Unchanged, Off);
}
assert_eq!(
String::from_utf8_lossy(outp.contents()),
concat!(
"\u{1b}[3;29m",
"\u{1b}[7m",
"",
"\u{1b}[9m",
"\u{1b}[23m",
"\u{1b}[27m",
"",
"\u{1b}[29m",
)
);
}
} }

View File

@@ -3,11 +3,37 @@
use fish_color::Color; use fish_color::Color;
use fish_wgetopt::{ArgType, WGetopter, WOption, wopt}; use fish_wgetopt::{ArgType, WGetopter, WOption, wopt};
trait StyleSet { pub(crate) trait StyleSet {
fn union_prefer_right(self, other: Self) -> Self; fn union_prefer_right(self, other: Self) -> Self;
fn difference_prefer_empty(self, other: Self) -> Self; fn difference_prefer_empty(self, other: Self) -> Self;
} }
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) enum ResettableStyle {
#[default]
Unchanged,
Off,
On,
}
impl StyleSet for ResettableStyle {
fn union_prefer_right(self, other: Self) -> Self {
if other == Self::Unchanged {
self
} else {
other
}
}
fn difference_prefer_empty(self, other: Self) -> Self {
if other != Self::Unchanged {
Self::Unchanged
} else {
self
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) enum UnderlineStyle { pub(crate) enum UnderlineStyle {
Single, Single,
@@ -34,10 +60,10 @@ fn difference_prefer_empty(self, other: Self) -> Self {
pub(crate) struct TextStyling { pub(crate) struct TextStyling {
pub(crate) bold: bool, pub(crate) bold: bool,
pub(crate) underline_style: Option<UnderlineStyle>, pub(crate) underline_style: Option<UnderlineStyle>,
pub(crate) italics: bool, pub(crate) italics: ResettableStyle,
pub(crate) dim: bool, pub(crate) dim: bool,
pub(crate) reverse: bool, pub(crate) reverse: ResettableStyle,
pub(crate) strikethrough: bool, pub(crate) strikethrough: ResettableStyle,
} }
impl TextStyling { impl TextStyling {
@@ -45,36 +71,44 @@ pub(crate) const fn terminal_default() -> Self {
Self { Self {
bold: false, bold: false,
underline_style: None, underline_style: None,
italics: false, italics: ResettableStyle::Off,
dim: false, dim: false,
reverse: false, reverse: ResettableStyle::Off,
strikethrough: false, strikethrough: ResettableStyle::Off,
} }
} }
pub(crate) const fn unknown() -> Self { pub(crate) const fn unknown() -> Self {
Self { Self {
bold: false, bold: false,
underline_style: None, underline_style: None,
italics: false, italics: ResettableStyle::Unchanged,
dim: false, dim: false,
reverse: false, reverse: ResettableStyle::Unchanged,
strikethrough: false, strikethrough: ResettableStyle::Unchanged,
} }
} }
pub(crate) fn is_empty(&self) -> bool { pub(crate) fn is_empty(&self) -> bool {
*self == Self::unknown() *self == Self::unknown()
} }
#[cfg(test)]
pub(crate) fn all_set(&self) -> bool {
(self.italics != ResettableStyle::Unchanged)
&& (self.reverse != ResettableStyle::Unchanged)
&& (self.strikethrough != ResettableStyle::Unchanged)
}
pub(crate) fn union_prefer_right(self, other: Self) -> Self { pub(crate) fn union_prefer_right(self, other: Self) -> Self {
Self { Self {
bold: self.is_bold() || other.is_bold(), bold: self.is_bold() || other.is_bold(),
underline_style: self underline_style: self
.underline_style .underline_style
.union_prefer_right(other.underline_style), .union_prefer_right(other.underline_style),
italics: self.is_italics() || other.is_italics(), italics: self.italics.union_prefer_right(other.italics),
dim: self.is_dim() || other.is_dim(), dim: self.is_dim() || other.is_dim(),
reverse: self.is_reverse() || other.is_reverse(), reverse: self.reverse.union_prefer_right(other.reverse),
strikethrough: self.is_strikethrough() || other.is_strikethrough(), strikethrough: self.strikethrough.union_prefer_right(other.strikethrough),
} }
} }
pub(crate) fn difference_prefer_empty(self, other: Self) -> Self { pub(crate) fn difference_prefer_empty(self, other: Self) -> Self {
@@ -83,10 +117,12 @@ pub(crate) fn difference_prefer_empty(self, other: Self) -> Self {
underline_style: self underline_style: self
.underline_style .underline_style
.difference_prefer_empty(other.underline_style), .difference_prefer_empty(other.underline_style),
italics: self.is_italics() && !other.is_italics(), italics: self.italics.difference_prefer_empty(other.italics),
dim: self.is_dim() && !other.is_dim(), dim: self.is_dim() && !other.is_dim(),
reverse: self.is_reverse() && !other.is_reverse(), reverse: self.reverse.difference_prefer_empty(other.reverse),
strikethrough: self.is_strikethrough() && !other.is_strikethrough(), strikethrough: self
.strikethrough
.difference_prefer_empty(other.strikethrough),
} }
} }
@@ -105,25 +141,10 @@ pub fn inject_underline(&mut self, underline: UnderlineStyle) {
self.underline_style = Some(underline); self.underline_style = Some(underline);
} }
/// Returns whether the text face is italics.
pub const fn is_italics(self) -> bool {
self.italics
}
/// Returns whether the text face is dim. /// Returns whether the text face is dim.
pub const fn is_dim(self) -> bool { pub const fn is_dim(self) -> bool {
self.dim self.dim
} }
/// Returns whether the text face has reverse foreground/background colors.
pub const fn is_reverse(self) -> bool {
self.reverse
}
/// Returns whether the text face is strikethrough.
pub const fn is_strikethrough(self) -> bool {
self.strikethrough
}
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -160,6 +181,14 @@ pub fn new(fg: Color, bg: Color, underline_color: Color, style: TextStyling) ->
style, style,
} }
} }
#[cfg(test)]
pub(crate) fn all_set(&self) -> bool {
!self.fg.is_none()
&& !self.bg.is_none()
&& !self.underline_color.is_none()
&& self.style.all_set()
}
} }
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
@@ -211,27 +240,41 @@ pub(crate) enum ParsedArgs<'argarray, 'args> {
pub(crate) enum ParseError<'args> { pub(crate) enum ParseError<'args> {
MissingOptArg, MissingOptArg,
UnexpectedOptArg(usize), UnexpectedOptArg(usize),
InvalidOptArg(&'static wstr, &'args wstr),
UnknownColor(&'args wstr), UnknownColor(&'args wstr),
UnknownUnderlineStyle(&'args wstr), UnknownUnderlineStyle(&'args wstr),
UnknownOption(usize), UnknownOption(usize),
} }
fn parse_resettable_style<'a>(w: &WGetopter<'_, 'a, '_>) -> Result<ResettableStyle, &'a wstr> {
let Some(arg) = w.woptarg else {
return Ok(ResettableStyle::On);
};
if (arg == "off") || (arg == "false") {
Ok(ResettableStyle::Off)
} else if (arg == "on") || (arg == "true") {
Ok(ResettableStyle::On)
} else {
Err(arg)
}
}
pub(crate) fn parse_text_face_and_options<'argarray, 'args>( pub(crate) fn parse_text_face_and_options<'argarray, 'args>(
argv: &'argarray mut [&'args wstr], argv: &'argarray mut [&'args wstr],
is_builtin: bool, is_builtin: bool,
) -> Result<ParsedArgs<'argarray, 'args>, ParseError<'args>> { ) -> Result<ParsedArgs<'argarray, 'args>, ParseError<'args>> {
let builtin_extra_args = if is_builtin { 0 } else { "hc".len() }; let builtin_extra_args = if is_builtin { 0 } else { "hc".len() };
let short_options = L!("b:oidru::ch"); let short_options = L!("b:oi::dr::s::u::ch");
let short_options = &short_options[..short_options.len() - builtin_extra_args]; let short_options = &short_options[..short_options.len() - builtin_extra_args];
let long_options: &[WOption] = &[ let long_options: &[WOption] = &[
wopt(L!("background"), ArgType::RequiredArgument, 'b'), wopt(L!("background"), ArgType::RequiredArgument, 'b'),
wopt(L!("underline-color"), ArgType::RequiredArgument, '\x02'), wopt(L!("underline-color"), ArgType::RequiredArgument, '\x02'),
wopt(L!("bold"), ArgType::NoArgument, 'o'), wopt(L!("bold"), ArgType::NoArgument, 'o'),
wopt(L!("underline"), ArgType::OptionalArgument, 'u'), wopt(L!("underline"), ArgType::OptionalArgument, 'u'),
wopt(L!("italics"), ArgType::NoArgument, 'i'), wopt(L!("italics"), ArgType::OptionalArgument, 'i'),
wopt(L!("dim"), ArgType::NoArgument, 'd'), wopt(L!("dim"), ArgType::NoArgument, 'd'),
wopt(L!("strikethrough"), ArgType::NoArgument, 's'), wopt(L!("reverse"), ArgType::OptionalArgument, 'r'),
wopt(L!("reverse"), ArgType::NoArgument, 'r'), wopt(L!("strikethrough"), ArgType::OptionalArgument, 's'),
wopt(L!("theme"), ArgType::RequiredArgument, '\x01'), wopt(L!("theme"), ArgType::RequiredArgument, '\x01'),
wopt(L!("help"), ArgType::NoArgument, 'h'), wopt(L!("help"), ArgType::NoArgument, 'h'),
wopt(L!("print-colors"), ArgType::NoArgument, 'c'), wopt(L!("print-colors"), ArgType::NoArgument, 'c'),
@@ -276,10 +319,19 @@ pub(crate) fn parse_text_face_and_options<'argarray, 'args>(
return Ok(PrintHelp); return Ok(PrintHelp);
} }
'o' => style.bold = true, 'o' => style.bold = true,
'i' => style.italics = true, 'i' => {
style.italics =
parse_resettable_style(&w).map_err(|v| InvalidOptArg(L!("--italics"), v))?;
}
'd' => style.dim = true, 'd' => style.dim = true,
'r' => style.reverse = true, 'r' => {
's' => style.strikethrough = true, style.reverse =
parse_resettable_style(&w).map_err(|v| InvalidOptArg(L!("--reverse"), v))?;
}
's' => {
style.strikethrough = parse_resettable_style(&w)
.map_err(|v| InvalidOptArg(L!("--strikethrough"), v))?;
}
'u' => { 'u' => {
let arg = w.woptarg.unwrap_or(L!("single")); let arg = w.woptarg.unwrap_or(L!("single"));
style.underline_style = Some(if arg == "single" { style.underline_style = Some(if arg == "single" {

View File

@@ -39,6 +39,81 @@ string escape (set_color --background=green)
fish_term256=0 string escape (set_color --background=f00 --background=green --background=00f) fish_term256=0 string escape (set_color --background=f00 --background=green --background=00f)
# CHECK: \e\[42m # CHECK: \e\[42m
string escape (set_color --italics)
# CHECK: \e\[3m
string escape (set_color --italics=on)
# CHECK: \e\[3m
string escape (set_color --italics=true)
# CHECK: \e\[3m
string escape (set_color --italics=off)
# CHECK: \e\[23m
string escape (set_color --italics=false)
# CHECK: \e\[23m
string escape (set_color --italics=foo)
# CHECKERR: set_color: --italics: invalid option argument: foo
string escape (set_color -i)
# CHECK: \e\[3m
string escape (set_color -ion)
# CHECK: \e\[3m
string escape (set_color -itrue)
# CHECK: \e\[3m
string escape (set_color -ioff)
# CHECK: \e\[23m
string escape (set_color -ifalse)
# CHECK: \e\[23m
string escape (set_color -ifoo)
# CHECKERR: set_color: --italics: invalid option argument: foo
string escape (set_color --reverse)
# CHECK: \e\[7m
string escape (set_color --reverse=on)
# CHECK: \e\[7m
string escape (set_color --reverse=true)
# CHECK: \e\[7m
string escape (set_color --reverse=off)
# CHECK: \e\[27m
string escape (set_color --reverse=false)
# CHECK: \e\[27m
string escape (set_color --reverse=foo)
# CHECKERR: set_color: --reverse: invalid option argument: foo
string escape (set_color -r)
# CHECK: \e\[7m
string escape (set_color -ron)
# CHECK: \e\[7m
string escape (set_color -rtrue)
# CHECK: \e\[7m
string escape (set_color -roff)
# CHECK: \e\[27m
string escape (set_color -rfalse)
# CHECK: \e\[27m
string escape (set_color -rfoo)
# CHECKERR: set_color: --reverse: invalid option argument: foo
string escape (set_color --strikethrough)
# CHECK: \e\[9m
string escape (set_color --strikethrough=on)
# CHECK: \e\[9m
string escape (set_color --strikethrough=true)
# CHECK: \e\[9m
string escape (set_color --strikethrough=off)
# CHECK: \e\[29m
string escape (set_color --strikethrough=false)
# CHECK: \e\[29m
string escape (set_color --strikethrough=foo)
# CHECKERR: set_color: --strikethrough: invalid option argument: foo
string escape (set_color -s)
# CHECK: \e\[9m
string escape (set_color -son)
# CHECK: \e\[9m
string escape (set_color -strue)
# CHECK: \e\[9m
string escape (set_color -soff)
# CHECK: \e\[29m
string escape (set_color -sfalse)
# CHECK: \e\[29m
string escape (set_color -sfoo)
# CHECKERR: set_color: --strikethrough: invalid option argument: foo
string escape (set_color --underline=curly) string escape (set_color --underline=curly)
# CHECK: \e\[4:3m # CHECK: \e\[4:3m
string escape (set_color -ucurly) string escape (set_color -ucurly)
@@ -63,4 +138,4 @@ string escape (set_color --underline=dashed)
# CHECK: \e\[4:5m # CHECK: \e\[4:5m
string escape (set_color f00 --background=00f --underline-color=0f0 --bold --dim --italics --reverse --strikethrough --underline=curly) string escape (set_color f00 --background=00f --underline-color=0f0 --bold --dim --italics --reverse --strikethrough --underline=curly)
# CHECK: \e\[38\;2\;255\;0\;0\;48\;2\;0\;0\;255\;58:2::0:255:0\;1\;4:3\;3\;2\;9m\e\[7m # CHECK: \e\[38\;2\;255\;0\;0\;48\;2\;0\;0\;255\;58:2::0:255:0\;1\;4:3\;2\;3\;7m\e\[9m