mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-05-30 19:41:15 -03:00
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:
committed by
Johannes Altmanninger
parent
cba82a3c64
commit
a893dd10f4
@@ -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
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|||||||
@@ -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**.
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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: 无效位数"
|
||||||
|
|||||||
@@ -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:無效的小數位數"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/terminal.rs
124
src/terminal.rs
@@ -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",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
src/text_face.rs
128
src/text_face.rs
@@ -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" {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user