From 2cd87a50ee29486092eb7b45c8bae582c6a0055a Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Tue, 6 Jun 2023 23:04:14 +0200 Subject: [PATCH 1/7] OutputType: PagerCfg and oneshot_write Use new, smaller PagerCfg instead of the full Config, as for pager output only 3 variables are relevant. oneshot_write() can be used to write paginated output, usually before exiting. --- src/features/navigate.rs | 6 ++-- src/main.rs | 3 +- src/subcommands/show_colors.rs | 3 +- src/subcommands/show_syntax_themes.rs | 2 +- src/subcommands/show_themes.rs | 9 ++++-- src/utils/bat/output.rs | 42 +++++++++++++++++++++++++-- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/features/navigate.rs b/src/features/navigate.rs index d04f808c1..71f9802b8 100644 --- a/src/features/navigate.rs +++ b/src/features/navigate.rs @@ -3,8 +3,8 @@ use std::io::Write; use std::io::{Error, ErrorKind}; use std::path::PathBuf; -use crate::config::Config; use crate::features::OptionValueFunction; +use crate::utils::bat::output::PagerCfg; pub fn make_feature() -> Vec<(String, OptionValueFunction)> { builtin_feature!([ @@ -68,7 +68,9 @@ pub fn make_navigate_regex( // current implementation, no writes to the delta less history file are propagated back to the real // history file so, for example, a (non-navigate) search performed in the delta less process will // not be stored in history. -pub fn copy_less_hist_file_and_append_navigate_regex(config: &Config) -> std::io::Result { +pub fn copy_less_hist_file_and_append_navigate_regex( + config: &PagerCfg, +) -> std::io::Result { let delta_less_hist_file = get_delta_less_hist_file()?; let initial_contents = ".less-history-file:\n".to_string(); let mut contents = if let Some(hist_file) = get_less_hist_file() { diff --git a/src/main.rs b/src/main.rs index 925e7dc4e..78c1a25bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,8 +121,9 @@ fn run_app() -> std::io::Result { return Ok(0); } + let pager_cfg = (&config).into(); let mut output_type = - OutputType::from_mode(&env, config.paging_mode, config.pager.clone(), &config).unwrap(); + OutputType::from_mode(&env, config.paging_mode, config.pager.clone(), &pager_cfg).unwrap(); let mut writer = output_type.handle().unwrap(); if let (Some(minus_file), Some(plus_file)) = (&config.minus_file, &config.plus_file) { diff --git a/src/subcommands/show_colors.rs b/src/subcommands/show_colors.rs index 9d0428c75..a7f5927ce 100644 --- a/src/subcommands/show_colors.rs +++ b/src/subcommands/show_colors.rs @@ -17,9 +17,10 @@ pub fn show_colors() -> std::io::Result<()> { let env = DeltaEnv::default(); let opt = cli::Opt::from_args_and_git_config(&env, assets); let config = config::Config::from(opt); + let pagercfg = (&config).into(); let mut output_type = - OutputType::from_mode(&env, PagingMode::QuitIfOneScreen, None, &config).unwrap(); + OutputType::from_mode(&env, PagingMode::QuitIfOneScreen, None, &pagercfg).unwrap(); let writer = output_type.handle().unwrap(); let mut painter = paint::Painter::new(writer, &config); diff --git a/src/subcommands/show_syntax_themes.rs b/src/subcommands/show_syntax_themes.rs index 118805935..6af8acd18 100644 --- a/src/subcommands/show_syntax_themes.rs +++ b/src/subcommands/show_syntax_themes.rs @@ -16,7 +16,7 @@ pub fn show_syntax_themes() -> std::io::Result<()> { &env, PagingMode::QuitIfOneScreen, None, - &config::Config::from(cli::Opt::parse()), + &config::Config::from(cli::Opt::parse()).into(), ) .unwrap(); let mut writer = output_type.handle().unwrap(); diff --git a/src/subcommands/show_themes.rs b/src/subcommands/show_themes.rs index 86cae34d9..70987fc71 100644 --- a/src/subcommands/show_themes.rs +++ b/src/subcommands/show_themes.rs @@ -40,8 +40,13 @@ pub fn show_themes(dark: bool, light: bool, computed_theme_is_light: bool) -> st &["delta", "--navigate", "--show-themes"], git_config, ); - let mut output_type = - OutputType::from_mode(&env, PagingMode::Always, None, &config::Config::from(opt)).unwrap(); + let mut output_type = OutputType::from_mode( + &env, + PagingMode::Always, + None, + &config::Config::from(opt).into(), + ) + .unwrap(); let title_style = ansi_term::Style::new().bold(); let writer = output_type.handle().unwrap(); diff --git a/src/utils/bat/output.rs b/src/utils/bat/output.rs index 91fb5ba71..e93fe2ca9 100644 --- a/src/utils/bat/output.rs +++ b/src/utils/bat/output.rs @@ -13,6 +13,28 @@ use crate::env::DeltaEnv; use crate::fatal; use crate::features::navigate; +#[derive(Debug, Default)] +pub struct PagerCfg { + pub navigate: bool, + pub show_themes: bool, + pub navigate_regex: Option, +} + +impl From<&config::Config> for PagerCfg { + fn from(cfg: &config::Config) -> Self { + PagerCfg { + navigate: cfg.navigate, + show_themes: cfg.show_themes, + navigate_regex: cfg.navigate_regex.clone(), + } + } +} +impl From for PagerCfg { + fn from(cfg: config::Config) -> Self { + (&cfg).into() + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[allow(dead_code)] pub enum PagingMode { @@ -30,11 +52,25 @@ pub enum OutputType { } impl OutputType { + /// Create a pager and write all data into it. Waits until the pager exits. + /// The expectation is that the program will exit afterwards. + pub fn oneshot_write(data: String) -> io::Result<()> { + let mut output_type = OutputType::from_mode( + &DeltaEnv::init(), + PagingMode::QuitIfOneScreen, + None, + &PagerCfg::default(), + ) + .unwrap(); + let mut writer = output_type.handle().unwrap(); + write!(&mut writer, "{}", data) + } + pub fn from_mode( env: &DeltaEnv, mode: PagingMode, pager: Option, - config: &config::Config, + config: &PagerCfg, ) -> Result { use self::PagingMode::*; Ok(match mode { @@ -49,7 +85,7 @@ impl OutputType { env: &DeltaEnv, quit_if_one_screen: bool, pager_from_config: Option, - config: &config::Config, + config: &PagerCfg, ) -> Result { let mut replace_arguments_to_less = false; @@ -127,7 +163,7 @@ fn _make_process_from_less_path( args: &[String], replace_arguments_to_less: bool, quit_if_one_screen: bool, - config: &config::Config, + config: &PagerCfg, ) -> Option { if let Ok(less_path) = grep_cli::resolve_binary(less_path) { let mut p = Command::new(less_path); From 19872ffecabadc802302574e490ce4c40c0fabf7 Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Wed, 7 Jun 2023 22:51:17 +0200 Subject: [PATCH 2/7] Make drop impl of OutputType more prominent --- src/utils/bat/output.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/utils/bat/output.rs b/src/utils/bat/output.rs index e93fe2ca9..c3f4a04ad 100644 --- a/src/utils/bat/output.rs +++ b/src/utils/bat/output.rs @@ -51,6 +51,14 @@ pub enum OutputType { Stdout(io::Stdout), } +impl Drop for OutputType { + fn drop(&mut self) { + if let OutputType::Pager(ref mut command) = *self { + let _ = command.wait(); + } + } +} + impl OutputType { /// Create a pager and write all data into it. Waits until the pager exits. /// The expectation is that the program will exit afterwards. @@ -241,11 +249,3 @@ delta is not an appropriate value for $PAGER \ None } } - -impl Drop for OutputType { - fn drop(&mut self) { - if let OutputType::Pager(ref mut command) = *self { - let _ = command.wait(); - } - } -} From 1284a76fbfd6a91c8dd67511d486f12259ceec1c Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Mon, 29 Jul 2024 22:51:47 +0200 Subject: [PATCH 3/7] Add wrap function for --help output Unicode and somewhat ANSI aware, can add indentation (skippable) and can also skip wrapping lines by using configurable magic prefixes. --- src/utils/helpwrap.rs | 348 ++++++++++++++++++++++++++++++++++++++++++ src/utils/mod.rs | 1 + 2 files changed, 349 insertions(+) create mode 100644 src/utils/helpwrap.rs diff --git a/src/utils/helpwrap.rs b/src/utils/helpwrap.rs new file mode 100644 index 000000000..24191d2b6 --- /dev/null +++ b/src/utils/helpwrap.rs @@ -0,0 +1,348 @@ +#![allow(clippy::comparison_to_empty)] // no_indent != "", instead of !no_indent.is_empty() + +use crate::ansi::measure_text_width; + +/// Wrap `text` at spaces ('` `') to fit into `width`. If `indent_with` is non-empty, indent +/// each line with this string. If a line from `text` starts with `no_indent`, do not indent. +/// If a line starts with `no_wrap`, do not wrap (empty `no_indent`/`no_wrap` have no effect). +/// If both "magic prefix" markers are used, `no_indent` must be first. +/// Takes unicode and ANSI into account when calculating width, but won't wrap ANSI correctly. +/// Removes trailing spaces. Leading spaces or enumerations with '- ' continue the indentation on +/// the wrapped line. +/// Example: +/// ``` +/// let wrapped = wrap("ab cd ef\n!NI!123\n|AB CD EF GH\n!NI!|123 456 789", 7, "_", "!NI!", "|"); +/// assert_eq!(wrapped, "\ +/// _ab cd\n\ +/// _ef\n\ +/// 123\n\ +/// _AB CD EF GH\n\ +/// 123 456 789\n\ +/// "); +/// ``` +pub fn wrap(text: &str, width: usize, indent_with: &str, no_indent: &str, no_wrap: &str) -> String { + let mut result = String::with_capacity(text.len()); + let indent_len = measure_text_width(indent_with); + + for line in text.lines() { + let line = line.trim_end_matches(' '); + + let (line, indent) = + if let (Some(line), true) = (line.strip_prefix(no_indent), no_indent != "") { + (line, "") + } else { + result.push_str(indent_with); + (line, indent_with) + }; + + if let (Some(line), true) = (line.strip_prefix(no_wrap), no_wrap != "") { + result.push_str(line); + } else { + // `"foo bar end".split_inclusive(' ')` => `["foo ", "bar ", " ", " ", "end"]` + let mut wordit = line.split_inclusive(' '); + let mut curr_len = indent_len; + + if let Some(word) = wordit.next() { + result.push_str(word); + curr_len += measure_text_width(word); + } + + while let Some(mut word) = wordit.next() { + let word_len = measure_text_width(word); + if curr_len + word_len == width + 1 && word.ends_with(' ') { + // If just ' ' is over the limit, let the next word trigger the overflow. + } else if curr_len + word_len > width { + // Remove any trailing whitespace: + let pos = result.trim_end_matches(' ').len(); + result.truncate(pos); + + result.push('\n'); + + // Do not count spaces, skip until next proper word is found. + if word == " " { + for nextword in wordit.by_ref() { + word = nextword; + if word != " " { + break; + } + } + } + + // Re-calculates indent for each wrapped line. Could be done only once, maybe + // after an early return which just uses .len() (works for fullwidth chars). + + // If line started with spaces, indent by that much again. + let (indent, space_pos) = + if let Some(space_prefix_len) = line.find(|c: char| c != ' ') { + ( + format!("{}{}", indent, " ".repeat(space_prefix_len)), + space_prefix_len, + ) + } else { + debug_assert!(false, "line.trim_end_matches() missing?"); + (indent.to_string(), 0) + }; + + // If line started with '- ', treat it as a bullet point and increase indentation + let indent = if line[space_pos..].starts_with("- ") { + format!("{}{}", indent, " ") + } else { + indent + }; + + result.push_str(&indent); + curr_len = measure_text_width(&indent); + } + curr_len += word_len; + result.push_str(word); + } + } + let pos = result.trim_end_matches(' ').len(); + result.truncate(pos); + result.push('\n'); + } + + #[cfg(test)] + if result.find("no-sanity").is_none() { + // sanity check + let stripped_input = text + .replace(" ", "") + .replace("\n", "") + .replace(no_wrap, "") + .replace(no_indent, ""); + let stripped_output = result + .replace(" ", "") + .replace("\n", "") + .replace(indent_with, ""); + assert_eq!(stripped_input, stripped_output); + } + + result +} + +#[cfg(test)] +mod test { + use super::*; + use insta::assert_snapshot; + + #[test] + fn simple_ascii_can_not_split() { + let input = "000 123456789 abcdefghijklmnopqrstuvwxyz ok"; + let result = wrap(input, 5, "", "", ""); + assert_snapshot!(result, @r###" + 000 + 123456789 + abcdefghijklmnopqrstuvwxyz + ok + "###); + } + + #[test] + fn simple_ascii_just_whitespace() { + let input = " \n \n \n \n \n \n"; + let result = wrap(input, 3, "__", "", ""); + assert_snapshot!(result, @r###" + __ + __ + __ + __ + __ + __ + "###); + let result = wrap(input, 3, "", "", ""); + assert_eq!(result, "\n\n\n\n\n\n"); + } + + #[test] + fn simple_ascii_can_not_split_plus_whitespace() { + let input = "000 123456789 abcdefghijklmnopqrstuvwxyz ok"; + let result = wrap(input, 5, "", "", ""); + assert_snapshot!(result, @r###" + 000 + 123456789 + abcdefghijklmnopqrstuvwxyz + ok + "###); + } + + #[test] + fn simple_ascii_keep_leading_input_indent() { + let input = "abc\n Def ghi jkl mno pqr stuv xyz\n Abc def ghijklm\nok"; + let result = wrap(input, 10, "_", "", ""); + assert_snapshot!(result, @r###" + _abc + _ Def ghi + _ jkl mno + _ pqr + _ stuv + _ xyz + _ Abc + _ def + _ ghijklm + _ok + "###); + } + + #[test] + fn simple_ascii_indent_and_bullet_points() { + let input = "- ABC ABC abc\n def ghi - jkl\n - 1 22 3 4 55 6 7 8 9\n - 1 22 3 4 55 6 7 8 9\n!- 0 0 0 0 0 0 0 \n"; + let result = wrap(input, 10, "", "!", ""); + assert_snapshot!(result, @r###" + - ABC ABC + abc + def ghi + - jkl + - 1 22 3 + 4 55 6 + 7 8 9 + - 1 22 + 3 4 + 55 6 + 7 8 + 9 + - 0 0 0 0 + 0 0 0 + "###); + } + + #[test] + fn simple_ascii_all_overlong_after_indent() { + let input = "0000 1111 2222"; + let result = wrap(input, 5, "__", "", ""); + assert_snapshot!(result, @r###" + __0000 + __1111 + __2222 + "###); + } + + #[test] + fn simple_ascii_one_line() { + let input = "123 456 789 abc def ghi jkl mno pqr stu vwx yz"; + let result = wrap(input, 10, "__", "", ""); + assert_snapshot!(result, @r###" + __123 456 + __789 abc + __def ghi + __jkl mno + __pqr stu + __vwx yz + "###); + } + + #[test] + fn simple_ascii_trailing_space() { + let input = "123 \n\n \n 456 \n a b \n\n"; + let result = wrap(input, 10, " ", "", ""); + assert_eq!(result, " 123\n\n\n 456\n a\n b\n\n"); + } + + #[test] + fn simple_ascii_two_lines() { + let input = "123 456 789 abc def\nghi jkl mno pqr stu vwx yz\n1234 567 89 876 54321\n"; + let result = wrap(input, 10, "__", "", ""); + assert_snapshot!(result, @r###" + __123 456 + __789 abc + __def + __ghi jkl + __mno pqr + __stu vwx + __yz + __1234 567 + __89 876 + __54321 + "###); + } + + #[test] + fn simple_ascii_no_indent() { + let input = "123 456 789\n!!abc def ghi jkl mno pqr\nstu vwx yz\n\n"; + let result = wrap(input, 10, "__", "!!", ""); + assert_snapshot!(result, @r###" + __123 456 + __789 + abc def + ghi jkl + mno pqr + __stu vwx + __yz + __ + "###); + } + + #[test] + fn simple_ascii_no_wrap() { + let input = "123 456 789\n|abc def ghi jkl mno pqr\nstu vwx yz\n|W\nA B C D E F G H I\n"; + let result = wrap(input, 10, "__", "!!", "|"); + assert_snapshot!(result, @r###" + __123 456 + __789 + __abc def ghi jkl mno pqr + __stu vwx + __yz + __W + __A B C D + __E F G H + __I + "###); + } + + #[test] + fn simple_ascii_no_both() { + let input = "123 456 789\n!!|abc def ghi jkl mno pqr\nstu vwx yz\n|W\nA B C D E F G H I\n"; + let result = wrap(input, 10, "__", "!!", "|"); + assert_snapshot!(result, @r###" + __123 456 + __789 + abc def ghi jkl mno pqr + __stu vwx + __yz + __W + __A B C D + __E F G H + __I + "###); + } + + #[test] + fn simple_ascii_no_both_wrong_order() { + let input = "!!|abc def ghi jkl\n|!!ABC DEF GHI JKL + no-sanity\n"; + let result = wrap(input, 7, "__", "!!", "|"); + assert_snapshot!(result, @r###" + abc def ghi jkl + __!!ABC DEF GHI JKL + no-sanity + "###); + let wrapped = wrap( + "ab cd ef\n!NI!123\n|AB CD EF GH\n!NI!|123 456 789", + 6, + "_", + "!NI!", + "|", + ); + assert_snapshot!(wrapped, @r###" + _ab cd + _ef + 123 + _AB CD EF GH + 123 456 789 + "###); + } + + #[test] + fn simple_ascii_much_whitespace() { + let input = "123 456 789\nabc def ghi jkl mno pqr \nstu vwx yz"; + let result = wrap(input, 10, "__", "!!", "|"); + assert_snapshot!(result, @r###" + __123 + __456 + __789 + __abc + __def ghi + __jkl mno + __pqr + __stu + __vwx yz + "###); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index fa8427b66..2d952fe32 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,5 +1,6 @@ #[cfg(not(tarpaulin_include))] pub mod bat; +pub mod helpwrap; pub mod path; pub mod process; pub mod regex_replacement; From 727b281e10906ad960ec46a96ffd00a119d0ea76 Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Mon, 5 Jun 2023 22:21:07 +0200 Subject: [PATCH 4/7] Wrap --help output and use pager Tell clap to not wrap output, append `after_long_help()` (with matching ansi codes in this section), then manually wrap the result. The help output is now paginated if writing to a terminal. All code paths flow back to main(), so an invoked pager is properly waited for (by the OutputType drop impl). --- Cargo.toml | 1 - src/ansi/mod.rs | 2 + src/cli.rs | 450 +++++++++++++++++++++------------ src/main.rs | 9 + src/subcommands/show_colors.rs | 7 +- 5 files changed, 304 insertions(+), 165 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65bb72331..8bb6b33ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ clap = { version = "4.3.14", features = [ "help", "usage", "error-context", - "wrap_help", ] } console = "0.15.0" ctrlc = "3.2.5" diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs index 856c39022..5c3b11b88 100644 --- a/src/ansi/mod.rs +++ b/src/ansi/mod.rs @@ -12,8 +12,10 @@ use iterator::{AnsiElementIterator, Element}; pub const ANSI_CSI_CLEAR_TO_EOL: &str = "\x1b[0K"; pub const ANSI_CSI_CLEAR_TO_BOL: &str = "\x1b[1K"; +pub const ANSI_SGR_BOLD: &str = "\x1b[1m"; pub const ANSI_SGR_RESET: &str = "\x1b[0m"; pub const ANSI_SGR_REVERSE: &str = "\x1b[7m"; +pub const ANSI_SGR_UNDERLINE: &str = "\x1b[4m"; pub fn strip_ansi_codes(s: &str) -> String { strip_ansi_codes_from_strings_iterator(ansi_strings_iterator(s)) diff --git a/src/cli.rs b/src/cli.rs index 28d196280..2809179a7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,12 +3,14 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use bat::assets::HighlightingAssets; -use clap::{ColorChoice, CommandFactory, FromArgMatches, Parser, ValueEnum, ValueHint}; +use clap::{ArgMatches, ColorChoice, CommandFactory, FromArgMatches, Parser, ValueEnum, ValueHint}; use clap_complete::Shell; +use console::Term; use lazy_static::lazy_static; use syntect::highlighting::Theme as SyntaxTheme; use syntect::parsing::SyntaxSet; +use crate::ansi::{ANSI_SGR_BOLD, ANSI_SGR_RESET, ANSI_SGR_UNDERLINE}; use crate::config::delta_unreachable; use crate::env::DeltaEnv; use crate::git_config::GitConfig; @@ -16,170 +18,17 @@ use crate::options; use crate::utils; use crate::utils::bat::output::PagingMode; +const TERM_FALLBACK_WIDTH: usize = 79; + #[derive(Parser)] #[command( name = "delta", about = "A viewer for git and diff output", version, color = ColorChoice::Always, - term_width(0), - after_long_help = "\ -GIT CONFIG ----------- - -By default, delta takes settings from a section named \"delta\" in git config files, if one is present. The git config file to use for delta options will usually be ~/.gitconfig, but delta follows the rules given in https://git-scm.com/docs/git-config#FILES. Most delta options can be given in a git config file, using the usual option names but without the initial '--'. An example is - -[delta] - line-numbers = true - zero-style = dim syntax - -FEATURES --------- -A feature is a named collection of delta options in git config. An example is: - -[delta \"my-delta-feature\"] - syntax-theme = Dracula - plus-style = bold syntax \"#002800\" - -To activate those options, you would use: - -delta --features my-delta-feature - -A feature name may not contain whitespace. You can activate multiple features: - -[delta] - features = my-highlight-styles-colors-feature my-line-number-styles-feature - -If more than one feature sets the same option, the last one wins. - -If an option is present in the [delta] section, then features are not considered at all. - -If you want an option to be fully overridable by a feature and also have a non default value when no features are used, then you need to define a \"default\" feature and include it in the main delta configuration. - -For instance: - -[delta] -feature = default-feature - -[delta \"default-feature\"] -width = 123 - -At this point, you can override features set in the command line or in the environment variables and the \"last one wins\" rules will apply as expected. - -STYLES ------- - -All options that have a name like --*-style work the same way. It is very similar to how colors/styles are specified in a gitconfig file: https://git-scm.com/docs/git-config#Documentation/git-config.txt-color - -Here is an example: - ---minus-style 'red bold ul \"#ffeeee\"' - -That means: For removed lines, set the foreground (text) color to 'red', make it bold and underlined, and set the background color to '#ffeeee'. - -See the COLORS section below for how to specify a color. In addition to real colors, there are 4 special color names: 'auto', 'normal', 'raw', and 'syntax'. - -Here is an example of using special color names together with a single attribute: - ---minus-style 'syntax bold auto' - -That means: For removed lines, syntax-highlight the text, and make it bold, and do whatever delta normally does for the background. - -The available attributes are: 'blink', 'bold', 'dim', 'hidden', 'italic', 'reverse', 'strike', and 'ul' (or 'underline'). - -The attribute 'omit' is supported by commit-style, file-style, and hunk-header-style, meaning to remove the element entirely from the output. - -A complete description of the style string syntax follows: - -- If the input that delta is receiving already has colors, and you want delta to output those colors unchanged, then use the special style string 'raw'. Otherwise, delta will strip any colors from its input. - -- A style string consists of 0, 1, or 2 colors, together with an arbitrary number of style attributes, all separated by spaces. - -- The first color is the foreground (text) color. The second color is the background color. Attributes can go in any position. - -- This means that in order to specify a background color you must also specify a foreground (text) color. - -- If you want delta to choose one of the colors automatically, then use the special color 'auto'. This can be used for both foreground and background. - -- If you want the foreground/background color to be your terminal's foreground/background color, then use the special color 'normal'. - -- If you want the foreground text to be syntax-highlighted according to its language, then use the special foreground color 'syntax'. This can only be used for the foreground (text). - -- The minimal style specification is the empty string ''. This means: do not apply any colors or styling to the element in question. - -COLORS ------- - -There are four ways to specify a color (this section applies to foreground and background colors within a style string): - -1. CSS color name - - Any of the 140 color names used in CSS: https://www.w3schools.com/colors/colors_groups.asp - -2. RGB hex code - - An example of using an RGB hex code is: - --file-style=\"#0e7c0e\" - -3. ANSI color name - - There are 8 ANSI color names: - black, red, green, yellow, blue, magenta, cyan, white. - - In addition, all of them have a bright form: - brightblack, brightred, brightgreen, brightyellow, brightblue, brightmagenta, brightcyan, brightwhite. - - An example of using an ANSI color name is: - --file-style=\"green\" - - Unlike RGB hex codes, ANSI color names are just names: you can choose the exact color that each name corresponds to in the settings of your terminal application (the application you use to enter commands at a shell prompt). This means that if you use ANSI color names, and you change the color theme used by your terminal, then delta's colors will respond automatically, without needing to change the delta command line. - - \"purple\" is accepted as a synonym for \"magenta\". Color names and codes are case-insensitive. - -4. ANSI color number - - An example of using an ANSI color number is: - --file-style=28 - - There are 256 ANSI color numbers: 0-255. The first 16 are the same as the colors described in the \"ANSI color name\" section above. See https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit. Specifying colors like this is useful if your terminal only supports 256 colors (i.e. doesn\'t support 24-bit color). - - -LINE NUMBERS ------------- - -To display line numbers, use --line-numbers. - -Line numbers are displayed in two columns. Here's what it looks like by default: - - 1 ⋮ 1 │ unchanged line - 2 ⋮ │ removed line - ⋮ 2 │ added line - -In that output, the line numbers for the old (minus) version of the file appear in the left column, and the line numbers for the new (plus) version of the file appear in the right column. In an unchanged (zero) line, both columns contain a line number. - -The following options allow the line number display to be customized: - ---line-numbers-left-format: Change the contents of the left column ---line-numbers-right-format: Change the contents of the right column ---line-numbers-left-style: Change the style applied to the left column ---line-numbers-right-style: Change the style applied to the right column ---line-numbers-minus-style: Change the style applied to line numbers in minus lines ---line-numbers-zero-style: Change the style applied to line numbers in unchanged lines ---line-numbers-plus-style: Change the style applied to line numbers in plus lines - -Options --line-numbers-left-format and --line-numbers-right-format allow you to change the contents of the line number columns. Their values are arbitrary format strings, which are allowed to contain the placeholders {nm} for the line number associated with the old version of the file and {np} for the line number associated with the new version of the file. The placeholders support a subset of the string formatting syntax documented here: https://doc.rust-lang.org/std/fmt/#formatting-parameters. Specifically, you can use the alignment and width syntax. - -For example, the default value of --line-numbers-left-format is '{nm:^4}⋮'. This means that the left column should display the minus line number (nm), center-aligned, padded with spaces to a width of 4 characters, followed by a unicode dividing-line character (⋮). - -Similarly, the default value of --line-numbers-right-format is '{np:^4}│'. This means that the right column should display the plus line number (np), center-aligned, padded with spaces to a width of 4 characters, followed by a unicode dividing-line character (│). - -Use '<' for left-align, '^' for center-align, and '>' for right-align. - - -If something isn't working correctly, or you have a feature request, please open an issue at https://github.com/dandavison/delta/issues. - -For a short help summary, please use delta -h. -" + // output is wrapped by delta later: + term_width = usize::MAX, + max_term_width = usize::MAX, )] pub struct Opt { #[arg(long = "blame-code-style", value_name = "STYLE")] @@ -1113,7 +962,7 @@ pub struct Opt { /// First file to be compared when delta is being used in diff mode /// - /// `delta file_1 file_2` is equivalent to `diff -u file_1 file_2 | delta`. + /// `delta file1 file2` is equivalent to `diff -u file1 file2 | delta`. pub minus_file: Option, /// Second file to be compared when delta is being used in diff mode. @@ -1129,6 +978,186 @@ pub struct Opt { pub env: DeltaEnv, } +fn get_after_long_help(is_term: bool, no_indent: &str, no_wrap: &str) -> String { + let i0 = no_indent; + let l = no_wrap; + + #[allow(non_snake_case)] + // header and underline + let (H_, _H, u_, _u) = if is_term { + ( + format!("{ANSI_SGR_BOLD}{ANSI_SGR_UNDERLINE}"), + ANSI_SGR_RESET, + ANSI_SGR_UNDERLINE, + ANSI_SGR_RESET, + ) + } else { + ("".to_string(), "", "", "") + }; + + format!( + r##" + +{i0}{H_}Git config{_H} + +By default, delta takes settings from a section named "delta" in git config files, if one is present. The git config file to use for delta options will usually be ~/.gitconfig, but delta follows the rules given in https://git-scm.com/docs/git-config#FILES. Most delta options can be given in a git config file, using the usual option names but without the initial '--'. An example is + +[delta] + line-numbers = true + zero-style = dim syntax + + +{i0}{H_}Features{_H} + +A feature is a named collection of delta options in git config. An example is: + +[delta "my-delta-feature"] + syntax-theme = Dracula + plus-style = bold syntax "#002800" + +To activate those options, you would use: + +delta --features my-delta-feature + +A feature name may not contain whitespace. You can activate multiple features: + +{l}[delta] +{l} features = my-highlight-styles-colors-feature my-line-number-styles-feature + +If more than one feature sets the same option, the last one wins. + +If an option is present in the [delta] section, then features are not considered at all. + +If you want an option to be fully overridable by a feature and also have a non default value when no features are used, then you need to define a "default" feature and include it in the main delta configuration. + +For instance: + +[delta] + feature = default-feature + +[delta "default-feature"] + width = 123 + +At this point, you can override features set in the command line or in the environment variables and the "last one wins" rules will apply as expected. + + +{i0}{H_}Styles{_H} + +All options that have a name like --*-style work the same way. It is very similar to how colors/styles are specified in a gitconfig file: https://git-scm.com/docs/git-config#Documentation/git-config.txt-color + +Here is an example: + +--minus-style 'red bold ul "#ffeeee"' + +That means: For removed lines, set the foreground (text) color to 'red', make it bold and underlined, and set the background color to '#ffeeee'. + +See the {u_}Colors{_u} section below for how to specify a color. In addition to real colors, there are 4 special color names: 'auto', 'normal', 'raw', and 'syntax'. + +Here is an example of using special color names together with a single attribute: + +--minus-style 'syntax bold auto' + +That means: For removed lines, syntax-highlight the text, and make it bold, and do whatever delta normally does for the background. + +The available attributes are: 'blink', 'bold', 'dim', 'hidden', 'italic', 'reverse', 'strike', and 'ul' (or 'underline'). + +The attribute 'omit' is supported by commit-style, file-style, and hunk-header-style, meaning to remove the element entirely from the output. + +A complete description of the style string syntax follows: + +- If the input that delta is receiving already has colors, and you want delta to output those colors unchanged, then use the special style string 'raw'. Otherwise, delta will strip any colors from its input. + +- A style string consists of 0, 1, or 2 colors, together with an arbitrary number of style attributes, all separated by spaces. + +- The first color is the foreground (text) color. The second color is the background color. Attributes can go in any position. + +- This means that in order to specify a background color you must also specify a foreground (text) color. + +- If you want delta to choose one of the colors automatically, then use the special color 'auto'. This can be used for both foreground and background. + +- If you want the foreground/background color to be your terminal's foreground/background color, then use the special color 'normal'. + +- If you want the foreground text to be syntax-highlighted according to its language, then use the special foreground color 'syntax'. This can only be used for the foreground (text). + +- The minimal style specification is the empty string ''. This means: do not apply any colors or styling to the element in question. + + +{i0}{H_}Colors{_H} + +There are four ways to specify a color (this section applies to foreground and background colors within a style string): + +1. CSS color name + + Any of the 140 color names used in CSS: https://www.w3schools.com/colors/colors_groups.asp + +2. RGB hex code + + An example of using an RGB hex code is: + --file-style="#0e7c0e" + +3. ANSI color name + + There are 8 ANSI color names: + black, red, green, yellow, blue, magenta, cyan, white. + + In addition, all of them have a bright form: + brightblack, brightred, brightgreen, brightyellow, brightblue, brightmagenta, brightcyan, brightwhite. + + An example of using an ANSI color name is: + --file-style="green" + + Unlike RGB hex codes, ANSI color names are just names: you can choose the exact color that each name corresponds to in the settings of your terminal application (the application you use to enter commands at a shell prompt). This means that if you use ANSI color names, and you change the color theme used by your terminal, then delta's colors will respond automatically, without needing to change the delta command line. + + "purple" is accepted as a synonym for "magenta". Color names and codes are case-insensitive. + +4. ANSI color number + + An example of using an ANSI color number is: + --file-style=28 + + There are 256 ANSI color numbers: 0-255. The first 16 are the same as the colors described in the "ANSI color name" section above. See https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit. Specifying colors like this is useful if your terminal only supports 256 colors (i.e. doesn't support 24-bit color). + + +{i0}{H_}Line Numbers{_H} + +To display line numbers, use --line-numbers. + +Line numbers are displayed in two columns. Here's what it looks like by default: + + 1 ⋮ 1 │ unchanged line + 2 ⋮ │ removed line + ⋮ 2 │ added line + +In that output, the line numbers for the old (minus) version of the file appear in the left column, and the line numbers for the new (plus) version of the file appear in the right column. In an unchanged (zero) line, both columns contain a line number. + +The following options allow the line number display to be customized: + +--line-numbers-left-format: Change the contents of the left column +--line-numbers-right-format: Change the contents of the right column +--line-numbers-left-style: Change the style applied to the left column +--line-numbers-right-style: Change the style applied to the right column +--line-numbers-minus-style: Change the style applied to line numbers in minus lines +--line-numbers-zero-style: Change the style applied to line numbers in unchanged lines +--line-numbers-plus-style: Change the style applied to line numbers in plus lines + +Options --line-numbers-left-format and --line-numbers-right-format allow you to change the contents of the line number columns. Their values are arbitrary format strings, which are allowed to contain the placeholders {{nm}} for the line number associated with the old version of the file and {{np}} for the line number associated with the new version of the file. The placeholders support a subset of the string formatting syntax documented here: https://doc.rust-lang.org/std/fmt/#formatting-parameters. Specifically, you can use the alignment and width syntax. + +For example, the default value of --line-numbers-left-format is '{{nm:^4}}⋮'. This means that the left column should display the minus line number (nm), center-aligned, padded with spaces to a width of 4 characters, followed by a unicode dividing-line character (⋮). + +Similarly, the default value of --line-numbers-right-format is '{{np:^4}}│'. This means that the right column should display the plus line number (np), center-aligned, padded with spaces to a width of 4 characters, followed by a unicode dividing-line character (│). + +Use '<' for left-align, '^' for center-align, and '>' for right-align. + + +{i0}{H_}Support{_H} + +If something isn't working correctly, or you have a feature request, please open an issue at https://github.com/dandavison/delta/issues. + +For a short help summary, please use delta -h. +"## + ) +} + #[derive(Default, Clone, Debug)] pub struct ComputedValues { pub available_terminal_width: usize, @@ -1168,9 +1197,93 @@ pub enum DetectDarkLight { Never, } +// Which call path to take +#[derive(Debug)] +pub enum Call { + Delta(T), + Help(String), + Version(String), +} + +// Custom conversion because a) generic TryFrom is not possible and +// b) the Delta(T) variant can't be converted. +impl Call { + fn try_convert(self) -> Option> { + use Call::*; + match self { + Delta(_) => None, + Help(help) => Some(Help(help)), + Version(ver) => Some(Version(ver)), + } + } +} + impl Opt { - pub fn from_args_and_git_config(env: &DeltaEnv, assets: HighlightingAssets) -> Self { - let matches = Self::command().get_matches(); + fn handle_help_and_version(args: &[OsString]) -> Call { + match Self::command().try_get_matches_from(args) { + Err(e) if e.kind() == clap::error::ErrorKind::DisplayVersion => { + let version = Self::command().render_version(); + Call::Version(version) + } + Err(e) if e.kind() == clap::error::ErrorKind::DisplayHelp => { + let term = Term::stdout(); + // find out if short or long version of --help was used: + let long_help = !args.iter().any(|arg| arg == "-h"); + let help_clap = if long_help { + Self::command().render_long_help() + } else { + Self::command().render_help() + }; + + let is_term = term.is_term(); + let (help_clap, wrap_width) = if term.is_term() { + ( + help_clap.ansi().to_string(), + utils::workarounds::windows_msys2_width_fix(term.size(), &term), + ) + } else { + (help_clap.to_string(), TERM_FALLBACK_WIDTH) + }; + + // Stop wrapping for very narrow terminals, and leave a 2 wide margin on the right. + let wrap_width = + wrap_width.clamp(TERM_FALLBACK_WIDTH - 22, TERM_FALLBACK_WIDTH + 22) - 2; + + let mut help = utils::helpwrap::wrap(&help_clap, wrap_width, "", "", ""); + if long_help { + let indent_with = " "; + let no_indent = ":no_Indent:"; + let no_wrap = ":no_Wrap:"; + let after_help = utils::helpwrap::wrap( + &get_after_long_help(is_term, no_indent, no_wrap), + wrap_width, + indent_with, + no_indent, + no_wrap, + ); + help.push_str(&after_help); + } + + Call::Help(help) + } + Err(e) => { + e.exit(); + } + Ok(matches) => Call::Delta(matches), + } + } + + pub fn from_args_and_git_config(env: &DeltaEnv, assets: HighlightingAssets) -> Call { + let args = std::env::args_os().collect::>(); + + let matches = match Self::handle_help_and_version(&args) { + Call::Delta(t) => t, + msg => { + return msg + .try_convert() + .unwrap_or_else(|| panic!("Call<_> conversion failed")) + } + }; let mut final_config = if *matches.get_one::("no_gitconfig").unwrap_or(&false) { None @@ -1185,7 +1298,12 @@ impl Opt { } } - Self::from_clap_and_git_config(env, matches, final_config, assets) + Call::Delta(Self::from_clap_and_git_config( + env, + matches, + final_config, + assets, + )) } pub fn from_iter_and_git_config( @@ -1256,3 +1374,9 @@ lazy_static! { .into_iter() .collect(); } + +// Call::Help(format!( +// "foo\nbar\nbatz\n{}\n{}", +// help.replace("Options:", "well well\n\nOptions:"), +// h2 +// )) diff --git a/src/main.rs b/src/main.rs index 78c1a25bf..a0df151ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ use std::process; use bytelines::ByteLinesReader; +use crate::cli::Call; use crate::delta::delta; use crate::utils::bat::assets::list_languages; use crate::utils::bat::output::OutputType; @@ -78,6 +79,14 @@ fn run_app() -> std::io::Result { let env = env::DeltaEnv::init(); let opt = cli::Opt::from_args_and_git_config(&env, assets); + let opt = match opt { + Call::Help(msg) | Call::Version(msg) => { + OutputType::oneshot_write(msg)?; + return Ok(0); + } + Call::Delta(opt) => opt, + }; + let subcommand_result = if let Some(shell) = opt.generate_completion { Some(subcommands::generate_completion::generate_completion_file( shell, diff --git a/src/subcommands/show_colors.rs b/src/subcommands/show_colors.rs index a7f5927ce..b67c22997 100644 --- a/src/subcommands/show_colors.rs +++ b/src/subcommands/show_colors.rs @@ -15,7 +15,12 @@ pub fn show_colors() -> std::io::Result<()> { let assets = utils::bat::assets::load_highlighting_assets(); let env = DeltaEnv::default(); - let opt = cli::Opt::from_args_and_git_config(&env, assets); + + let opt = match cli::Opt::from_args_and_git_config(&env, assets) { + cli::Call::Delta(opt) => opt, + _ => panic!("non-Delta Call variant should not occur here"), + }; + let config = config::Config::from(opt); let pagercfg = (&config).into(); From 1a275fd3fd170e2b0f3f71a7a3f3c847e93f2dd6 Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Tue, 30 Jul 2024 00:08:18 +0200 Subject: [PATCH 5/7] Use angle bracket around , regenerate help output markdown --- manual/src/full---help-output.md | 617 +++++++++++++++++++++++-------- src/cli.rs | 26 +- 2 files changed, 467 insertions(+), 176 deletions(-) diff --git a/manual/src/full---help-output.md b/manual/src/full---help-output.md index 16116a1dc..b3095dc55 100644 --- a/manual/src/full---help-output.md +++ b/manual/src/full---help-output.md @@ -9,7 +9,7 @@ Arguments: [MINUS_FILE] First file to be compared when delta is being used in diff mode - `delta file_1 file_2` is equivalent to `diff -u file_1 file_2 | delta`. + `delta file1 file2` is equivalent to `diff -u file1 file2 | delta`. [PLUS_FILE] Second file to be compared when delta is being used in diff mode @@ -18,24 +18,39 @@ Options: --blame-code-style