diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a933e8..15eddfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed +## Fix + +- Fix string width calculation with ANSI escape sequences by using ansi-str instead of console::measure_text_width(). +- Fix typos. +- Fix compiler warnings in tests/all/property_test.rs + - ## [7.1.0] - 2023-10-21 @@ -28,7 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fix a panic when working with extreme paddings, where `(padding.left + padding.right) > u16::MAX`. - Fix a panic when working with extremely long content, where `(content_width + padding) > u16::MAX`. -- Properly enforce lower boundery constraints. +- Properly enforce lower boundary constraints. Previously, "normal" columns were allocated before lower boundaries were respected. This could lead to scenarios, where the table would grow beyond the specified size, when there was a lower boundary. - Fix calculation of column widths for empty columns. @@ -44,7 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Breaking - The `Color` and `Attribute` enum are no longer re-exported from crossterm by default. - Previously, when updating comfy-table, crossterm needed to be upgraded as well, since the compile would otherwise fail due to type incompatibilies. + Previously, when updating comfy-table, crossterm needed to be upgraded as well, since the compile would otherwise fail due to type incompatibilities. To fix this, these enums are now mirrored and internally mapped to their crossterm equivalents, which allows us to safely bump crossterm whenever a new version is released. This change will only affect you if your projects explicitly use crossterm and comfy-table at the same time **and** feed crossterm's native types into comfy-table. @@ -78,7 +84,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed -- Disable unneded crossterm `bracketed-paste` feature. +- Disable unneeded crossterm `bracketed-paste` feature. ## [6.1.2] - 2022-10-27 @@ -172,7 +178,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The `tty` feature flag is enabled by default. Implemented by [roee88](https://github.com/roee88) in [#47](https://github.com/Nukesor/comfy-table/pull/47). - ## [4.1.0] - 2021-08-09 ### Added @@ -243,6 +248,7 @@ pub enum Width { ``` Instead of the old + ``` enum ColumnConstraints { ..., @@ -263,7 +269,6 @@ enum ColumnConstraints { Check the docs on the trait implementations for Cell, Row and Cells - Add the `Cells` type, to allow super generic `Iterator -> Row` conversions. - ## [2.1.0] - 2021-01-26 ### Added @@ -271,7 +276,6 @@ enum ColumnConstraints { - `DynamicFullWidth` arrangement. This mode is basically the same as the `Dynamic` arrangement mode, but it will always use the full available width, even if there isn't enough content to fill the space. - ## [2.0.0] - 2021-01-16 ### Added @@ -279,15 +283,15 @@ enum ColumnConstraints { **Dynamic arrangement** A new logic to optimize space usage after splitting content has been added.\ -If there is a lot of unused space after the content has been arranged, this space will now be redistributed ot the remaining columns. +If there is a lot of unused space after the content has been arranged, this space will now be redistributed to the remaining columns. Or it will be removed if there are no other columns. **This is considered a breaking change, since this can result in different table layouts!!** This process is far from perfect, but the behavior is better than before. - Old behavior: + ``` +-----------------------------------+-----------------------------------+------+ | Header1 | Header2 | Head | @@ -298,6 +302,7 @@ Old behavior: ``` New behavior: + ``` +-----------------------------------------+-----------------------------+------+ | Header1 | Header2 | Head | @@ -308,6 +313,7 @@ New behavior: ``` Old behavior: + ``` +------------------------------------------------+ | Header1 | @@ -318,6 +324,7 @@ Old behavior: ``` New behavior: + ``` +-------------------------------+ | Header1 | @@ -353,7 +360,7 @@ New behavior: ### Added -- New ColumConstraint for hiding columns +- New ColumnConstraint for hiding columns ## [1.2.0] - 2020-10-27 diff --git a/Cargo.toml b/Cargo.toml index 711bed4..d6ad7ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ tty = ["crossterm"] # - Text formatting still works, even if you roll your own ANSI escape sequences. # - Rainbow text # - Makes comfy-table 30-50% slower -custom_styling = ["console"] +custom_styling = ["ansi-str", "console"] # With this flag, comfy_table re-exposes crossterm's "Attribute" and "Color" enum. # By default, a mirrored type is exposed, which internally maps to the crossterm type. # @@ -69,6 +69,7 @@ integration_test = [] strum = "0.26" strum_macros = "0.26" unicode-width = "0.1" +ansi-str = { version = "0.8", optional = true } console = { version = "0.15", optional = true } [dev-dependencies] diff --git a/src/column.rs b/src/column.rs index 742fac9..6fb8c5f 100644 --- a/src/column.rs +++ b/src/column.rs @@ -98,7 +98,7 @@ impl Column { self } - /// Returns wheather the columns is hidden via [ColumnConstraint::Hidden]. + /// Returns weather the columns is hidden via [ColumnConstraint::Hidden]. pub fn is_hidden(&self) -> bool { matches!(self.constraint, Some(ColumnConstraint::Hidden)) } diff --git a/src/style/column.rs b/src/style/column.rs index 10e4bd6..cad6097 100644 --- a/src/style/column.rs +++ b/src/style/column.rs @@ -21,7 +21,7 @@ pub enum ColumnConstraint { /// If the column has longer content and is allowed to grow, the column may take more space. LowerBoundary(Width), /// Specify a upper boundary, either fixed or as percentage of the total width. - /// A column with this constriant will be at most as wide as specified. + /// A column with this constraint will be at most as wide as specified. /// The column may be smaller than that width. UpperBoundary(Width), /// Specify both, an upper and a lower boundary. diff --git a/src/utils/arrangement/dynamic.rs b/src/utils/arrangement/dynamic.rs index a69a0b6..0a299d3 100644 --- a/src/utils/arrangement/dynamic.rs +++ b/src/utils/arrangement/dynamic.rs @@ -455,22 +455,22 @@ fn longest_line_after_split(average_space: usize, column: &Column, table: &Table let delimiter = delimiter(table, column, cell); // Create a temporary ColumnDisplayInfo with the average space as width. - // That way we can simulate how the splitted text will look like. + // That way we can simulate how the split text will look like. let info = ColumnDisplayInfo::new(column, average_space.try_into().unwrap_or(u16::MAX)); // Iterate over each line and split it into multiple lines, if necessary. // Newlines added by the user will be preserved. for line in cell.content.iter() { if line.width() > average_space { - let mut splitted = split_line(line, &info, delimiter); + let mut parts = split_line(line, &info, delimiter); #[cfg(feature = "debug")] println!( "dynamic::longest_line_after_split: Splitting line with width {}. Original:\n {}\nSplitted:\n {:?}", - line.width(), line, splitted + line.width(), line, parts ); - column_lines.append(&mut splitted); + column_lines.append(&mut parts); } else { column_lines.push(line.into()); } @@ -499,7 +499,7 @@ fn use_full_width(infos: &mut DisplayInfos, remaining_width: usize) { } // Calculate the amount of average remaining space per column. - // Since we do integer division, there is most likely a little bit of non equally-divisable space. + // Since we do integer division, there is most likely a little bit of non equally-divisible space. // We then try to distribute it as fair as possible (from left to right). let average_space = remaining_width / visible_columns; let mut excess = remaining_width - (average_space * visible_columns); @@ -510,7 +510,7 @@ fn use_full_width(infos: &mut DisplayInfos, remaining_width: usize) { continue; } - // Distribute the non-divisable excess from left-to right until nothing is left. + // Distribute the non-divisible excess from left-to right until nothing is left. let width = if excess > 0 { excess -= 1; (average_space + 1).try_into().unwrap_or(u16::MAX) @@ -535,7 +535,7 @@ fn distribute_remaining_space( remaining_columns: usize, ) { // Calculate the amount of average remaining space per column. - // Since we do integer division, there is most likely a little bit of non equally-divisable space. + // Since we do integer division, there is most likely a little bit of non equally-divisible space. // We then try to distribute it as fair as possible (from left to right). let average_space = remaining_width / remaining_columns; let mut excess = remaining_width - (average_space * remaining_columns); @@ -546,7 +546,7 @@ fn distribute_remaining_space( continue; } - // Distribute the non-divisable excess from left-to right until nothing is left. + // Distribute the non-divisible excess from left-to right until nothing is left. let width = if excess > 0 { excess -= 1; (average_space + 1).try_into().unwrap_or(u16::MAX) diff --git a/src/utils/formatting/content_format.rs b/src/utils/formatting/content_format.rs index 93e8ee1..552b95b 100644 --- a/src/utils/formatting/content_format.rs +++ b/src/utils/formatting/content_format.rs @@ -72,7 +72,7 @@ pub fn format_row( cell_iter.next(); continue; } - // Each cell is devided into several lines devided by newline + // Each cell is divided into several lines divided by newline // Every line that's too long will be split into multiple lines let mut cell_lines = Vec::new(); @@ -92,8 +92,8 @@ pub fn format_row( // Newlines added by the user will be preserved. for line in cell.content.iter() { if measure_text_width(line) > info.content_width.into() { - let mut splitted = split_line(line, info, delimiter); - cell_lines.append(&mut splitted); + let mut parts = split_line(line, info, delimiter); + cell_lines.append(&mut parts); } else { cell_lines.push(line.into()); } @@ -117,7 +117,7 @@ pub fn format_row( *last_line = stripped; } - // Don't do anything if the collumn is smaller then 6 characters + // Don't do anything if the column is smaller then 6 characters let width: usize = info.content_width.into(); if width >= 6 { // Truncate the line if '...' doesn't fit diff --git a/src/utils/formatting/content_split/custom_styling.rs b/src/utils/formatting/content_split/custom_styling.rs index 2dfbae2..7fa9755 100644 --- a/src/utils/formatting/content_split/custom_styling.rs +++ b/src/utils/formatting/content_split/custom_styling.rs @@ -1,3 +1,4 @@ +use ansi_str::AnsiStr; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; const ANSI_RESET: &str = "\u{1b}[0m"; @@ -5,7 +6,7 @@ const ANSI_RESET: &str = "\u{1b}[0m"; /// Returns printed length of string, takes into account escape codes #[inline(always)] pub fn measure_text_width(s: &str) -> usize { - console::measure_text_width(s) + s.ansi_strip().width() } /// Split the line by the given deliminator without breaking ansi codes that contain the delimiter @@ -13,7 +14,7 @@ pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec { let mut lines: Vec = Vec::new(); let mut current_line = String::default(); - // Iterate over line, spliting text with delimiter + // Iterate over line, splitting text with delimiter let iter = console::AnsiCodeIterator::new(line); for (str_slice, is_esc) in iter { if is_esc { @@ -55,7 +56,7 @@ pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) { let mut escapes = Vec::new(); // Iterate over segments of the input string, each segment is either a singe escape code or block of text containing no escape codes. - // Add text and escape codes to the head buffer, keeping track of printable length and what ansi codes are active, untill there is no more room in allowed_width. + // Add text and escape codes to the head buffer, keeping track of printable length and what ansi codes are active, until there is no more room in allowed_width. // If the str was split at a point with active escape-codes, add the ansi reset code to the end of head, and the list of active escape codes to the beginning of tail. let mut iter = console::AnsiCodeIterator::new(word); for (str_slice, is_esc) in iter.by_ref() { @@ -158,6 +159,8 @@ pub fn fix_style_in_split_str(words: &mut [String]) { #[cfg(test)] mod test { + use unicode_width::UnicodeWidthStr; + #[test] fn ansi_aware_split_test() { use super::split_line_by_delimiter; @@ -175,4 +178,15 @@ mod test { ] ) } + + #[test] + fn measure_text_width_osc8_test() { + use super::measure_text_width; + + let text = "\x1b]8;;https://github.com\x1b\\This is a link\x1b]8;;\x1b"; + let width = measure_text_width(text); + + assert_eq!(text.width(), 41); + assert_eq!(width, 14); + } } diff --git a/src/utils/formatting/content_split/mod.rs b/src/utils/formatting/content_split/mod.rs index 09f957c..aa70042 100644 --- a/src/utils/formatting/content_split/mod.rs +++ b/src/utils/formatting/content_split/mod.rs @@ -39,7 +39,7 @@ pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec< // Some helper variables // The length of the current line when combining it with the next element - // Add 1 for the delimiter if we are on a non-emtpy line. + // Add 1 for the delimiter if we are on a non-empty line. let mut added_length = next_length + current_length; if !current_line.is_empty() { added_length += 1; diff --git a/src/utils/formatting/content_split/normal.rs b/src/utils/formatting/content_split/normal.rs index a030b60..ea510c9 100644 --- a/src/utils/formatting/content_split/normal.rs +++ b/src/utils/formatting/content_split/normal.rs @@ -20,7 +20,7 @@ pub fn split_line_by_delimiter(line: &str, delimiter: char) -> Vec { /// wider display width than allowed. pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) { let mut current_width = 0; - let mut splitted = String::new(); + let mut parts = String::new(); let mut char_iter = word.chars().peekable(); // Check if the string might be too long, one character at a time. @@ -41,10 +41,10 @@ pub fn split_long_word(allowed_width: usize, word: &str) -> (String, String) { let character_width = c.width().unwrap_or(1); current_width += character_width; - splitted.push(c); + parts.push(c); } // Collect the remaining characters. let remaining = char_iter.collect(); - (splitted, remaining) + (parts, remaining) } diff --git a/tests/all/constraints_test.rs b/tests/all/constraints_test.rs index 36884dc..56f5286 100644 --- a/tests/all/constraints_test.rs +++ b/tests/all/constraints_test.rs @@ -142,7 +142,7 @@ fn unnecessary_max_min_constraints() { #[test] /// The user can specify constraints that result in bigger width than actually provided -/// This is allowed, but results in a wider table than acutally aimed for. +/// This is allowed, but results in a wider table than actually aimed for. /// Anyway we still try to fit everything as good as possible, which of course breaks stuff. fn constraints_bigger_than_table_width() { let mut table = get_constraint_table(); diff --git a/tests/all/content_arrangement_test.rs b/tests/all/content_arrangement_test.rs index fc427a5..6140daa 100644 --- a/tests/all/content_arrangement_test.rs +++ b/tests/all/content_arrangement_test.rs @@ -8,7 +8,7 @@ use comfy_table::{ContentArrangement, Row, Table}; use super::assert_table_line_width; -/// Test the robustnes of the dynamic table arangement. +/// Test the robustness of the dynamic table arrangement. #[test] fn simple_dynamic_table() { let mut table = Table::new(); @@ -229,7 +229,7 @@ fn dynamic_full_width() { /// Test that a table is displayed in its full width, if the `table.width` is set to the exact /// width the table has, if it's fully expanded. /// -/// The same should be the case for values that're larget than this width. +/// The same should be the case for values that are larger than this width. #[test] fn dynamic_exact_width() { let header = vec!["a\n---\ni64", "b\n---\ni64", "b_squared\n---\nf64"]; diff --git a/tests/all/custom_delimiter_test.rs b/tests/all/custom_delimiter_test.rs index a33693a..6efd4bf 100644 --- a/tests/all/custom_delimiter_test.rs +++ b/tests/all/custom_delimiter_test.rs @@ -4,7 +4,7 @@ use comfy_table::*; #[test] /// Create a table with a custom delimiter on Table, Column and Cell level. -/// The first column should be splitted with the table's delimiter. +/// The first column should be split with the table's delimiter. /// The first cell of the second column should be split with the custom column delimiter /// The second cell of the second column should be split with the custom cell delimiter fn full_custom_delimiters() { diff --git a/tests/all/property_test.rs b/tests/all/property_test.rs index b38b20f..609c63b 100644 --- a/tests/all/property_test.rs +++ b/tests/all/property_test.rs @@ -58,7 +58,7 @@ prop_compose! { /// Returns all data needed to build the final table. /// 1. A matrix of cells Row[Column[Cell]]. -/// 2. Constriants for all columns. +/// 2. Constraints for all columns. /// 3. The alignment for each cell. /// 3. The alignment for each column. fn columns_and_rows() -> impl Strategy< @@ -203,7 +203,7 @@ proptest! { // A line can be a bit longer than u16::MAX due to formatting and borders. let actual: u16 = line_length.try_into().unwrap_or(u16::MAX); - if actual > expected_max.into() { + if actual > expected_max { return build_error( &formatted, &format!("Expected table to be smaller than line length!\n\ @@ -315,11 +315,7 @@ fn enforce_constraints( .column_iter() .map(|col| col.constraint().cloned()) .filter(|constraint| { - if let Some(ColumnConstraint::Hidden) = constraint { - false - } else { - true - } + !matches!(constraint, Some(ColumnConstraint::Hidden)) }) .collect(); @@ -357,7 +353,7 @@ fn enforce_constraints( ColumnConstraint::ContentWidth => continue, // Absolute width is defined. ColumnConstraint::Absolute(absolute) => { - let mut expected = absolute_width(&table, absolute); + let mut expected = absolute_width(table, absolute); // The minimal amount of chars per column (with default padding) // is 3 chars. 2 padding + 1 char content. if expected < 3 { @@ -375,7 +371,7 @@ fn enforce_constraints( } } ColumnConstraint::LowerBoundary(lower) => { - let expected_lower = absolute_width(&table, lower); + let expected_lower = absolute_width(table, lower); if actual < expected_lower.into() { return build_error( &formatted, @@ -388,7 +384,7 @@ fn enforce_constraints( } } ColumnConstraint::UpperBoundary(upper) => { - let mut expected_upper = absolute_width(&table, upper); + let mut expected_upper = absolute_width(table, upper); // The minimal amount of chars per column (with default padding) // is 3 chars. 2 padding + 1 char content. if expected_upper < 3 { @@ -407,8 +403,8 @@ fn enforce_constraints( } } ColumnConstraint::Boundaries { lower, upper } => { - let expected_lower = absolute_width(&table, lower); - let mut expected_upper = absolute_width(&table, upper); + let expected_lower = absolute_width(table, lower); + let mut expected_upper = absolute_width(table, upper); // The minimal amount of chars per column (with default padding) // is 3 chars. 2 padding + 1 char content. if expected_upper < 3 {