From 8b0f589486e617c4fd52d870839a94d6e78f7379 Mon Sep 17 00:00:00 2001 From: David Knaack Date: Wed, 5 Aug 2020 19:16:59 +0200 Subject: [PATCH] fix(explain): align table correctly (#1482) * fix(explain): align table correctly * iterate over lines directly * calculate desc_width with the actual space available * custom unicode-aware textwrapping * fix clippy error * better width estimination * explain +6 * move padding width into a constant --- Cargo.lock | 12 +------- Cargo.toml | 1 - src/print.rs | 86 +++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 36caa95d..03bb1918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,7 +195,7 @@ dependencies = [ "atty", "bitflags", "strsim", - "textwrap 0.11.0", + "textwrap", "unicode-width", "vec_map", ] @@ -1110,7 +1110,6 @@ dependencies = [ "sysinfo", "tempfile", "term_size", - "textwrap 0.12.1", "toml", "unicode-segmentation", "unicode-width", @@ -1202,15 +1201,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "textwrap" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" -dependencies = [ - "unicode-width", -] - [[package]] name = "thread_local" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 41bc3117..34f26c65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,6 @@ os_info = "2.0.7" urlencoding = "1.1.1" open = "1.4.0" unicode-width = "0.1.8" -textwrap = "0.12.1" term_size = "0.3.2" quick-xml = "0.18.1" diff --git a/src/print.rs b/src/print.rs index 5491a694..48d6e7de 100644 --- a/src/print.rs +++ b/src/print.rs @@ -4,6 +4,7 @@ use rayon::prelude::*; use std::collections::BTreeSet; use std::fmt::{self, Debug, Write as FmtWrite}; use std::io::{self, Write}; +use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthChar; use crate::configs::PROMPT_ORDER; @@ -104,50 +105,72 @@ pub fn explain(args: ArgMatches) { let value = module.get_segments().join(""); ModuleInfo { value: ansi_term::ANSIStrings(&module.ansi_strings()).to_string(), - value_len: value.chars().count() + count_wide_chars(&value), + value_len: better_width(value.as_str()), desc: module.get_description().to_owned(), } }) .collect::>(); - let mut max_ansi_module_width = 0; - let mut max_module_width = 0; + let max_module_width = modules.iter().map(|i| i.value_len).max().unwrap_or(0); - for info in &modules { - max_ansi_module_width = std::cmp::max( - max_ansi_module_width, - info.value.chars().count() + count_wide_chars(&info.value), - ); - max_module_width = std::cmp::max(max_module_width, info.value_len); - } + // In addition to the module width itself there are also 6 padding characters in each line. + // Overall a line looks like this: " {module name} - {description}". + const PADDING_WIDTH: usize = 6; let desc_width = term_size::dimensions() .map(|(w, _)| w) - .map(|width| width - std::cmp::min(width, max_ansi_module_width)); + // Add padding length to module length to avoid text overflow. This line also assures desc_width >= 0. + .map(|width| width - std::cmp::min(width, max_module_width + PADDING_WIDTH)); println!("\n Here's a breakdown of your prompt:"); for info in modules { - let wide_chars = count_wide_chars(&info.value); - if let Some(desc_width) = desc_width { - let wrapped = textwrap::fill(&info.desc, desc_width); - let mut lines = wrapped.split('\n'); - println!( - " {:width$} - {}", + // Custom Textwrapping! + let mut current_pos = 0; + let mut escaping = false; + // Print info + print!( + " {}{} - ", info.value, - lines.next().unwrap(), - width = max_ansi_module_width - wide_chars + " ".repeat(max_module_width - info.value_len) ); + for g in info.desc.graphemes(true) { + // Handle ANSI escape sequnces + if g == "\x1B" { + escaping = true; + } + if escaping { + print!("{}", g); + escaping = !("a" <= g && "z" >= g || "A" <= g && "Z" >= g); + continue; + } - for line in lines { - println!("{}{}", " ".repeat(max_module_width + 6), line.trim()); + // Handle normal wrapping + current_pos += grapheme_width(g); + // Wrap when hitting max width or newline + if g == "\n" || current_pos > desc_width { + // trim spaces on linebreak + if g == " " && desc_width > 1 { + continue; + } + + print!("\n{}", " ".repeat(max_module_width + PADDING_WIDTH)); + if g == "\n" { + current_pos = 0; + continue; + } + + current_pos = 1; + } + print!("{}", g); } + println!(); } else { println!( - " {:width$} - {}", + " {}{} - {}", info.value, + " ".repeat(max_module_width - info.value_len), info.desc, - width = max_ansi_module_width - wide_chars ); }; } @@ -266,6 +289,19 @@ fn should_add_implicit_custom_module( .unwrap_or(false) } -fn count_wide_chars(value: &str) -> usize { - value.chars().filter(|c| c.width().unwrap_or(0) > 1).count() +fn better_width(s: &str) -> usize { + s.graphemes(true).map(grapheme_width).sum() +} + +// Assume that graphemes have width of the first character in the grapheme +fn grapheme_width(g: &str) -> usize { + g.chars().next().and_then(|i| i.width()).unwrap_or(0) +} + +#[test] +fn test_grapheme_aware_better_width() { + // UnicodeWidthStr::width would return 8 + assert_eq!(2, better_width("👩‍👩‍👦‍👦")); + assert_eq!(1, better_width("Ü")); + assert_eq!(11, better_width("normal text")); }