module CLI::UI::Truncater

Truncater truncates a string to a provided printable width.

Constants

EMOJI_RANGE

EMOJI_RANGE in particular is super inaccurate. This is best-effort. If you need this to be more accurate, we'll almost certainly accept a PR improving it.

ESC
LC_ALPHA_RANGE
LEFT_SQUARE_BRACKET
NUMERIC_RANGE
PARSE_ANSI
PARSE_ESC
PARSE_ROOT
PARSE_ZWJ
SEMICOLON
TRUNCATED
UC_ALPHA_RANGE
ZWJ

Public Class Methods

call(text, printing_width) click to toggle source
# File lib/cli/ui/truncater.rb, line 30
def call(text, printing_width)
  return text if text.size <= printing_width

  width            = 0
  mode             = PARSE_ROOT
  truncation_index = nil

  codepoints = text.codepoints
  codepoints.each.with_index do |cp, index|
    case mode
    when PARSE_ROOT
      case cp
      when ESC # non-printable, followed by some more non-printables.
        mode = PARSE_ESC
      when ZWJ # non-printable, followed by another non-printable.
        mode = PARSE_ZWJ
      else
        width += width(cp)
        if width >= printing_width
          truncation_index ||= index
          # it looks like we could break here but we still want the
          # width calculation for the rest of the characters.
        end
      end
    when PARSE_ESC
      mode = case cp
      when LEFT_SQUARE_BRACKET
        PARSE_ANSI
      else
        PARSE_ROOT
      end
    when PARSE_ANSI
      # ANSI escape codes preeeetty much have the format of:
      # \x1b[0-9;]+[A-Za-z]
      case cp
      when NUMERIC_RANGE, SEMICOLON
      when LC_ALPHA_RANGE, UC_ALPHA_RANGE
        mode = PARSE_ROOT
      else
        # unexpected. let's just go back to the root state I guess?
        mode = PARSE_ROOT
      end
    when PARSE_ZWJ
      # consume any character and consider it as having no width
      # width(x+ZWJ+y) = width(x).
      mode = PARSE_ROOT
    end
  end

  # Without the `width <= printing_width` check, we truncate
  # "foo\x1b[0m" for a width of 3, but it should not be truncated.
  # It's specifically for the case where we decided "Yes, this is the
  # point at which we'd have to add a truncation!" but it's actually
  # the end of the string.
  return text if !truncation_index || width <= printing_width

  codepoints[0...truncation_index].pack('U*') + TRUNCATED
end

Private Class Methods

width(printable_codepoint) click to toggle source
# File lib/cli/ui/truncater.rb, line 91
def width(printable_codepoint)
  case printable_codepoint
  when EMOJI_RANGE
    2
  else
    1
  end
end