diff --git a/src/commands/preview/mod.rs b/src/commands/preview/mod.rs index e571b78..1259ba1 100644 --- a/src/commands/preview/mod.rs +++ b/src/commands/preview/mod.rs @@ -13,8 +13,8 @@ pub struct Input { pub line: String, } -fn extract_elements(argstr: &str) -> Result<(&str, &str, &str)> { - let mut parts = argstr.split(deser::terminal::DELIMITER).skip(3); +fn extract_elements(argstr: &str, skip_columns: usize) -> Result<(&str, &str, &str)> { + let mut parts = argstr.split(deser::terminal::DELIMITER).skip(skip_columns); let tags = parts.next().context("No `tags` element provided.")?; let comment = parts.next().context("No `comment` element provided.")?; let snippet = parts.next().context("No `snippet` element provided.")?; @@ -25,7 +25,10 @@ impl Runnable for Input { fn run(&self) -> Result<()> { let line = &self.line; - let (tags, comment, snippet) = extract_elements(line)?; + // In multiline mode, only 1 column is used for rendering tags, comment an snippet with zfs. + // In legacy mode, using 3 columns for tags, comment and snippet allow permuting their order with --with-nth of fzf. + let skip_columns = if CONFIG.multiline() { 1 } else { 3 }; + let (tags, comment, snippet) = extract_elements(line, skip_columns)?; println!( "{comment} {tags} \n{snippet}", diff --git a/src/config/cli.rs b/src/config/cli.rs index c6f86f9..7a4e14f 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -36,7 +36,8 @@ use clap::{crate_version, Parser, Subcommand}; navi --fzf-overrides-var '--no-select-1' # same, but for variable selection navi --fzf-overrides '--nth 1,2' # only consider the first two columns for search navi --fzf-overrides '--no-exact' # use looser search algorithm - navi --tag-rules='git,!checkout' # show non-checkout git snippets only")] + navi --tag-rules='git,!checkout' # show non-checkout git snippets only + navi --multiline # show multiline snippets (adds support for newlines in snippet/comment)")] #[clap(version = crate_version!())] pub(super) struct ClapConfig { /// Colon-separated list of paths containing .cheat files @@ -80,6 +81,10 @@ pub(super) struct ClapConfig { #[arg(long, allow_hyphen_values = true)] pub fzf_overrides_var: Option, + /// When set, show multiline snippets in the terminal listing (split snippet/comment across lines) + #[arg(long)] + pub multiline: bool, + /// Finder application to use #[arg(long, ignore_case = true)] pub finder: Option, diff --git a/src/config/mod.rs b/src/config/mod.rs index cf7f990..2f706dc 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -169,6 +169,10 @@ impl Config { self.yaml.style.tag.width_percentage } + pub fn multiline(&self) -> bool { + self.clap.multiline || self.yaml.style.multiline + } + pub fn comment_width_percentage(&self) -> u16 { self.yaml.style.comment.width_percentage } diff --git a/src/config/yaml.rs b/src/config/yaml.rs index 943d979..7289305 100644 --- a/src/config/yaml.rs +++ b/src/config/yaml.rs @@ -38,6 +38,7 @@ pub struct Style { pub tag: ColorWidth, pub comment: ColorWidth, pub snippet: ColorWidth, + pub multiline: bool, } #[derive(Deserialize, Debug)] @@ -167,6 +168,7 @@ impl Default for Style { min_width: 45, }, snippet: Default::default(), + multiline: false, } } } diff --git a/src/deser/terminal.rs b/src/deser/terminal.rs index 3408ff1..14ffaba 100644 --- a/src/deser/terminal.rs +++ b/src/deser/terminal.rs @@ -33,21 +33,66 @@ lazy_static! { pub fn write(item: &Item) -> String { let (tag_width_percentage, comment_width_percentage, snippet_width_percentage) = *COLUMN_WIDTHS; - format!( - "{tags_short}{delimiter}{comment_short}{delimiter}{snippet_short}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}{file_index}{delimiter}\n", + let snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR); + + let printer_item = if CONFIG.multiline() { + let separator_count = max( + snippet.matches(LINE_SEPARATOR).count(), + item.comment.matches(LINE_SEPARATOR).count(), + ); + + let splitted_comment = item.comment.split(LINE_SEPARATOR).collect::>(); + let splitted_snippet = snippet.split(LINE_SEPARATOR).collect::>(); + + (0..=separator_count) + .map(|i| { + format!( + "{tags_short}{delimiter}{comment_line_i}{delimiter}{snippet_line_i}", + tags_short = style(limit_str( + if i == 0 { &item.tags } else { "" }, + tag_width_percentage + )) + .with(CONFIG.tag_color()), + comment_line_i = style(limit_str( + splitted_comment.get(i).unwrap_or(&""), + comment_width_percentage + )) + .with(CONFIG.comment_color()), + snippet_line_i = style(limit_str( + splitted_snippet.get(i).unwrap_or(&""), + snippet_width_percentage + )) + .with(CONFIG.snippet_color()), + delimiter = " ", + ) + }) + .collect::>() + .join("\n") + } else { + format!( + "{tags_short}{delimiter}{comment_short}{delimiter}{snippet_short}", tags_short = style(limit_str(&item.tags, tag_width_percentage)).with(CONFIG.tag_color()), - comment_short = style(limit_str(&item.comment, comment_width_percentage)).with(CONFIG.comment_color()), - snippet_short = style(limit_str(&fix_newlines(&item.snippet), snippet_width_percentage)).with(CONFIG.snippet_color()), + comment_short = style(limit_str(&fix_newlines(&item.comment), comment_width_percentage)) + .with(CONFIG.comment_color()), + snippet_short = style(limit_str(&fix_newlines(&item.snippet), snippet_width_percentage)) + .with(CONFIG.snippet_color()), + delimiter = DELIMITER, + ) + }; + + format!( + "{printer_item}{delimiter}{tags}{delimiter}{comment}{delimiter}{snippet}{delimiter}{file_index}{delimiter}\0", + printer_item = printer_item, tags = item.tags, comment = item.comment, delimiter = DELIMITER, - snippet = &item.snippet.trim_end_matches(LINE_SEPARATOR), + snippet = snippet, file_index = item.file_index.unwrap_or(0), ) } pub fn read(raw_snippet: &str, is_single: bool) -> Result<(&str, Item)> { - let mut lines = raw_snippet.split('\n'); + let mut lines = raw_snippet.split('\0'); let key = if is_single { "enter" } else { @@ -56,11 +101,12 @@ pub fn read(raw_snippet: &str, is_single: bool) -> Result<(&str, Item)> { .context("Key was promised but not present in `selections`")? }; + let skip_columns = if CONFIG.multiline() { 1 } else { 3 }; let mut parts = lines .next() .context("No more parts in `selections`")? .split(DELIMITER) - .skip(3); + .skip(skip_columns); let tags = parts.next().unwrap_or("").into(); let comment = parts.next().unwrap_or("").into(); diff --git a/src/finder/mod.rs b/src/finder/mod.rs index a1edee6..6a59d83 100644 --- a/src/finder/mod.rs +++ b/src/finder/mod.rs @@ -119,7 +119,8 @@ impl FinderChoice { ]); if !opts.show_all_columns { - command.args(["--with-nth", "1,2,3"]); + let with_nth = if CONFIG.multiline() { "1" } else { "1,2,3" }; + command.args(["--read0", "--print0", "--with-nth", with_nth]); } if !opts.prevent_select1 { diff --git a/src/parser.rs b/src/parser.rs index 74d7bc0..a936c62 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -225,9 +225,10 @@ impl<'a> Parser<'a> { let write_fn = self.write_fn; - self.writer + return self + .writer .write_all(write_fn(item).as_bytes()) - .context("Failed to write command to finder's stdin") + .context("Failed to write command to finder's stdin"); } pub fn read_lines( @@ -265,6 +266,7 @@ impl<'a> Parser<'a> { else if line.starts_with('%') { should_break = self.write_cmd(&item).is_err(); item.snippet = String::from(""); + item.comment = String::from(""); item.tags = without_prefix(&line); } // dependency @@ -281,9 +283,17 @@ impl<'a> Parser<'a> { } // comment else if line.starts_with('#') { - should_break = self.write_cmd(&item).is_err(); - item.snippet = String::from(""); - item.comment = without_prefix(&line); + // if current item has a snippet, write it to the finder and start a new item + if !item.snippet.is_empty() { + should_break = self.write_cmd(&item).is_err(); + item.snippet = String::from(""); + item.comment = without_prefix(&line); + } else { + if !item.comment.is_empty() { + item.comment.push_str(deser::LINE_SEPARATOR); + } + item.comment.push_str(&without_prefix(&line)); + } } // variable else if !variable_cmd.is_empty()