You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

78 lines
2.6 KiB

6 months ago
  1. # AnsiWrapper cares about Ansi Colour Code \e[...
  2. class AnsiWrapper
  3. TAB_TO_SPACES = 2
  4. ANSI_REGEX = /\e\[[0-9;]*m/ # TODO: support for \x1b and \033
  5. ANSI_RESET = "\e[0m".freeze
  6. def self.wrap(text, length, prefix = '', continuation = prefix)
  7. if prefix.length != continuation.length
  8. raise "continuation <#{continuation}> should have the same length as prefix <#{prefix}>"
  9. end
  10. return unless text
  11. text = text.gsub("\t", ' ' * TAB_TO_SPACES)
  12. lines = split_text_to_lines(text, length - prefix.length)
  13. lines = inject_continuation_and_ansi_colors_to_lines(lines, prefix, continuation)
  14. lines.join("\n")
  15. end
  16. private_class_method def self.inject_continuation_and_ansi_colors_to_lines(lines, prefix, continuation)
  17. last_ansi = ''
  18. lines.each_with_index.map do |line, index|
  19. current = index.zero? ? prefix : continuation
  20. current += last_ansi unless last_ansi.empty? || last_ansi == ANSI_RESET
  21. current += line
  22. last_ansi = scan_for_actual_ansi(line, last_ansi)
  23. current += ANSI_RESET if last_ansi.empty? || last_ansi != ANSI_RESET
  24. current
  25. end
  26. end
  27. private_class_method def self.scan_for_actual_ansi(line, last_ansi)
  28. line.scan(ANSI_REGEX).each do |match|
  29. ansi_code = match.to_s
  30. if ansi_code == ANSI_RESET
  31. last_ansi = ANSI_RESET
  32. else
  33. last_ansi += ansi_code
  34. end
  35. end
  36. last_ansi
  37. end
  38. private_class_method def self.split_text_to_lines(text, length)
  39. lines = text.split("\n")
  40. sublines = lines.map do |line|
  41. visible_length(line) > length ? visible_split(line, length) : [line]
  42. end
  43. sublines.flatten
  44. end
  45. private_class_method def self.visible_length(line)
  46. raise 'line should not contain carriage return character!' if line.match "\n"
  47. ansi_code_length = line.scan(ANSI_REGEX).map(&:length).sum
  48. line.length - ansi_code_length
  49. end
  50. # TODO: might be refactored with less complexity
  51. private_class_method def self.visible_split(line, length, stack = '') # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
  52. before, ansi_code, after = line.partition(ANSI_REGEX)
  53. stack_length = visible_length(stack)
  54. visible_length = before.length + stack_length
  55. if visible_length == length
  56. ["#{stack}#{before}#{ansi_code}"] + visible_split(after, length)
  57. elsif visible_length > length
  58. first_line = stack + before[0...length - stack_length]
  59. tail = before[length - stack_length..] + ansi_code + after
  60. [first_line] + visible_split(tail, length)
  61. elsif ansi_code.length.positive?
  62. visible_split(after, length, "#{stack}#{before}#{ansi_code}")
  63. else
  64. ["#{stack}#{before}#{ansi_code}"]
  65. end
  66. end
  67. end