Skip to content

Commit

Permalink
feature flag for ansi support
Browse files Browse the repository at this point in the history
  • Loading branch information
blueforesticarus committed Nov 12, 2022
1 parent bd6a12e commit dab6194
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 15 deletions.
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ path = "examples/readme_table_no_tty.rs"
name = "readme_table"
path = "examples/readme_table.rs"

[[example]]
name = "inner_style"
path = "examples/inner_style.rs"
required-features = ["ansi"]

[features]
default = ["tty"]
tty = ["crossterm"]
ansi = ["console"]
# This flag is for library debugging only!
debug = []
# This feature is used to expose internal functionality for integration testing.
Expand All @@ -44,7 +50,7 @@ crossterm = { version = "0.25", optional = true }
strum = "0.24"
strum_macros = "0.24"
unicode-width = "0.1"
console = "0.15.1"
console = { version = "0.15.1", optional = true }

[dev-dependencies]
criterion = "0.4"
Expand Down
7 changes: 6 additions & 1 deletion src/cell.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#[cfg(feature = "tty")]
use crossterm::style::{Attribute, Color};

use crate::{style::CellAlignment, utils::fix_style_in_split_str};
use crate::style::CellAlignment;

#[cfg(feature = "ansi")]
use crate::utils::fix_style_in_split_str;

/// A stylable table cell with content.
#[derive(Clone, Debug)]
Expand All @@ -27,9 +30,11 @@ impl Cell {
#[allow(clippy::needless_pass_by_value)]
pub fn new<T: ToString>(content: T) -> Self {
let content = content.to_string();
#[cfg_attr(not(feature = "ansi"), allow(unused_mut))]
let mut split_content: Vec<String> = content.split('\n').map(ToString::to_string).collect();

// corrects ansi codes so style is terminated and resumed around the split
#[cfg(feature = "ansi")]
fix_style_in_split_str(&mut split_content);

Self {
Expand Down
7 changes: 5 additions & 2 deletions src/row.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::slice::Iter;

use crate::cell::{Cell, Cells};
use crate::{
cell::{Cell, Cells},
utils::formatting::content_split::measure_text_width,
};

/// Each row contains [Cells](crate::Cell) and can be added to a [Table](crate::Table).
#[derive(Clone, Debug, Default)]
Expand Down Expand Up @@ -64,7 +67,7 @@ impl Row {
.map(|string| {
//let width = console::measure_text_width(string);
//println!("{} {} {}", width, &string, string.escape_debug());
console::measure_text_width(string)
measure_text_width(string)
})
.max()
.unwrap_or(0)
Expand Down
19 changes: 12 additions & 7 deletions src/utils/formatting/content_format.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use console::strip_ansi_codes;
#[cfg(feature = "tty")]
use crossterm::style::{style, Stylize};
use unicode_width::UnicodeWidthStr;

use super::content_split::measure_text_width;
use super::content_split::split_line;

use crate::cell::Cell;
use crate::row::Row;
use crate::style::CellAlignment;
use crate::table::Table;
use crate::utils::ColumnDisplayInfo;

#[cfg(feature = "ansi")]
use console::strip_ansi_codes;

pub fn delimiter(cell: &Cell, info: &ColumnDisplayInfo, table: &Table) -> char {
// Determine, which delimiter should be used
if let Some(delimiter) = cell.delimiter {
Expand Down Expand Up @@ -88,7 +92,7 @@ pub fn format_row(
// 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 console::measure_text_width(line) > info.content_width.into() {
if measure_text_width(line) > info.content_width.into() {
let mut splitted = split_line(line, info, delimiter);
cell_lines.append(&mut splitted);
} else {
Expand All @@ -108,9 +112,11 @@ pub fn format_row(
.expect("We know it's this long.");

// we are truncate the line, so we might cuttoff a ansi code
let stripped = strip_ansi_codes(last_line).to_string();
last_line.clear();
last_line.push_str(&stripped);
#[cfg(feature = "ansi")]
{
let stripped = strip_ansi_codes(last_line).to_string();
*last_line = stripped;
}

// Don't do anything if the collumn is smaller then 6 characters
let width: usize = info.content_width.into();
Expand Down Expand Up @@ -183,8 +189,7 @@ pub fn format_row(
#[allow(unused_variables)]
fn align_line(table: &Table, info: &ColumnDisplayInfo, cell: &Cell, mut line: String) -> String {
let content_width = info.content_width;
let remaining: usize =
usize::from(content_width).saturating_sub(console::measure_text_width(&line));
let remaining: usize = usize::from(content_width).saturating_sub(measure_text_width(&line));

// Apply the styling before aligning the line, if the user requests it.
// That way non-delimiter whitespaces won't have stuff like underlines.
Expand Down
63 changes: 59 additions & 4 deletions src/utils/formatting/content_split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,19 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

use crate::utils::ColumnDisplayInfo;

/// returns printed length of string
/// if ansi feature enabled, takes into account escape codes
pub fn measure_text_width(s: &str) -> usize {
#[cfg(feature = "ansi")]
let width = console::measure_text_width(s);

#[cfg(not(feature = "ansi"))]
let width = s.width();
width
}

/// Split the line by the given deliminator without breaking ansi codes that contain the delimiter
#[cfg(feature = "ansi")]
pub fn ansi_aware_split(line: &str, delimiter: char) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();
let mut current_line = String::default();
Expand Down Expand Up @@ -49,15 +61,22 @@ pub fn split_line(line: &str, info: &ColumnDisplayInfo, delimiter: char) -> Vec<
// Split the line by the given deliminator and turn the content into a stack.
// Also clone it and convert it into a Vec<String>. Otherwise we get some burrowing problems
// due to early drops of borrowed values that need to be inserted into `Vec<&str>`
#[cfg(not(feature = "ansi"))]
let mut elements = line
.split(delimiter)
.map(ToString::to_string)
.collect::<Vec<String>>();

#[cfg(feature = "ansi")]
let mut elements = ansi_aware_split(line, delimiter);

// Reverse it, since we want to push/pop without reversing the text.
elements.reverse();

let mut current_line = String::new();
while let Some(next) = elements.pop() {
let current_length = console::measure_text_width(&current_line);
let next_length = console::measure_text_width(&next);
let current_length = measure_text_width(&current_line);
let next_length = measure_text_width(&next);

// Some helper variables
// The length of the current line when combining it with the next element
Expand Down Expand Up @@ -176,20 +195,55 @@ const MIN_FREE_CHARS: usize = 2;
/// Otherwise, we simply return the current line and basically don't do anything.
fn check_if_full(lines: &mut Vec<String>, content_width: usize, current_line: String) -> String {
// Already complete the current line, if there isn't space for more than two chars
if console::measure_text_width(&current_line) > content_width.saturating_sub(MIN_FREE_CHARS) {
if measure_text_width(&current_line) > content_width.saturating_sub(MIN_FREE_CHARS) {
lines.push(current_line);
return String::new();
}

current_line
}

#[cfg(feature = "ansi")]
const ANSI_RESET: &str = "\u{1b}[0m";

/// Splits a long word at a given character width. Inserting the needed ansi codes to preserve style.
/// Splits a long word at a given character width.
/// This needs some special logic, as we have to take multi-character UTF-8 symbols into account.
/// When simply splitting at a certain char position, we might end up with a string that's has a
/// wider display width than allowed.
#[cfg(not(feature = "ansi"))]
fn split_long_word(allowed_width: usize, word: &str) -> (String, String) {
let mut current_width = 0;
let mut splitted = String::new();

let mut char_iter = word.chars().peekable();
// Check if the string might be too long, one character at a time.
// Peek into the next char and check the exit condition.
// That is, pushing the next character would result in the string being too long.
while let Some(c) = char_iter.peek() {
if (current_width + c.width().unwrap_or(1)) > allowed_width {
break;
}

// We can unwrap, as we just checked that a suitable character is next in line.
let c = char_iter.next().unwrap();

// We default to 1 char, if the character length cannot be determined.
// The user has to live with this, if they decide to add control characters or some fancy
// stuff into their tables. This is considered undefined behavior and we try to handle this
// to the best of our capabilities.
let character_width = c.width().unwrap_or(1);

current_width += character_width;
splitted.push(c);
}

// Collect the remaining characters.
let remaining = char_iter.collect();
(splitted, remaining)
}

/// Splits a long word at a given character width. Inserting the needed ansi codes to preserve style.
#[cfg(feature = "ansi")]
fn split_long_word(allowed_width: usize, word: &str) -> (String, String) {
// A buffer for the first half of the split str, which will take up at most `allowed_len` characters when printed to the terminal.
let mut head = String::with_capacity(word.len());
Expand Down Expand Up @@ -271,6 +325,7 @@ fn split_long_word(allowed_width: usize, word: &str) -> (String, String) {
/// Fixes ansi escape codes in a split string
/// 1. Adds reset code to the end of each substring if needed.
/// 2. Keeps track of previous substring's escape codes and inserts them in later substrings to continue style
#[cfg(feature = "ansi")]
pub fn fix_style_in_split_str(words: &mut [String]) {
let mut escapes: Vec<String> = Vec::new();

Expand Down
2 changes: 2 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use crate::{Column, Table};
use arrangement::arrange_content;
use formatting::borders::draw_borders;
use formatting::content_format::format_content;

#[cfg(feature = "ansi")]
pub use formatting::content_split::fix_style_in_split_str;

/// This struct is ONLY used when table.to_string() is called.
Expand Down

0 comments on commit dab6194

Please sign in to comment.