# AnsiWrapper cares about Ansi Colour Code \e[... class AnsiWrapper TAB_TO_SPACES = 2 ANSI_REGEX = /\e\[[0-9;]*m/ # TODO: support for \x1b and \033 ANSI_RESET = "\e[0m".freeze def self.wrap(text, length, prefix = '', continuation = prefix) if visible_length(prefix) != visible_length(continuation) raise "continuation <#{continuation.inspect}> should have the same length as prefix <#{prefix.inspect}>" end return unless text text = text.gsub("\t", ' ' * TAB_TO_SPACES) lines = split_text_to_lines(text, length - visible_length(prefix)) lines = inject_continuation_and_ansi_colors_to_lines(lines, prefix, continuation) lines.join("\n") end private_class_method def self.inject_continuation_and_ansi_colors_to_lines(lines, prefix, continuation) last_ansi = '' lines.each_with_index.map do |line, index| current = index.zero? ? prefix : continuation current += last_ansi unless last_ansi.empty? || last_ansi == ANSI_RESET current += line last_ansi = scan_for_actual_ansi(line, last_ansi) current += ANSI_RESET if last_ansi.empty? || last_ansi != ANSI_RESET current end end private_class_method def self.scan_for_actual_ansi(line, last_ansi) line.scan(ANSI_REGEX).each do |match| ansi_code = match.to_s if ansi_code == ANSI_RESET last_ansi = ANSI_RESET else last_ansi += ansi_code end end last_ansi end private_class_method def self.split_text_to_lines(text, length) lines = text.split("\n") sublines = lines.map do |line| visible_length(line) > length ? visible_split(line, length) : [line] end sublines.flatten end private_class_method def self.visible_length(line) raise 'line should not contain carriage return character!' if line.match "\n" ansi_code_length = line.scan(ANSI_REGEX).map(&:length).sum line.length - ansi_code_length end # TODO: might be refactored with less complexity private_class_method def self.visible_split(line, length, stack = '') # rubocop:disable Metrics/AbcSize,Metrics/MethodLength before, ansi_code, after = line.partition(ANSI_REGEX) stack_length = visible_length(stack) visible_length = before.length + stack_length if visible_length == length ["#{stack}#{before}#{ansi_code}"] + visible_split(after, length) elsif visible_length > length first_line = stack + before[0...length - stack_length] tail = before[length - stack_length..] + ansi_code + after [first_line] + visible_split(tail, length) elsif ansi_code.length.positive? visible_split(after, length, "#{stack}#{before}#{ansi_code}") else ["#{stack}#{before}#{ansi_code}"] end end end