class Wrapper def self.wrap(text, prefix = ' > ', length = 80) text = carriage_return_filler(text.chomp(''), length) pure = '' ansi_code = {} while (md = text.match(/\e\[\d+;?\d*m/)) pos = md.begin(0) + pure.length pure += md.pre_match text = md.post_match append_in_hash(ansi_code, pos, md.match(0)) end pure += text offset = 0 ansi_extra = {} rows = pure.length / length last_code = nil rows.times do |i| pos = (i + 1) * length last_code = last_ansi_by_range(ansi_code, last_code, offset, pos) if last_code append_in_hash(ansi_extra, pos, "\e[0m\n#{prefix}#{last_code}") elsif pos < pure.length append_in_hash(ansi_extra, pos, "\n#{prefix}") end offset = pos + 1 end ansi_extra.each_pair do |k, v| append_in_hash(ansi_code, k, v.first) end ansi_code = ansi_code.sort final = pure offset = 0 ansi_code.each do |k, v| insert_text = v.join final = final.insert(k + offset, insert_text) offset += insert_text.length end final end private def self.carriage_return_filler(text, length) lines = text.split("\n") return text if lines.count < 2 last_ansi = nil final = lines.map do |line| fill_count = length - line.length current = line result = last_ansi ? "#{last_ansi}#{line}" : line last_ansi = nil if last_ansi == "\e[0m" while (md = current.match(/\e\[\d+;?\d*m/)) last_ansi = md.to_s fill_count += last_ansi.length current = md.post_match end result = "#{result}\e[0m" if last_ansi next result if fill_count < 1 "#{result}#{' ' * fill_count}" end.join last_ansi ? "#{final}\e[0m" : final end def self.last_ansi_by_range(ansi_code, last_code, offset, pos) pos.downto(offset) do |i| next unless ansi_code.has_key?(i) result = ansi_code[i].last result = nil if result == "\e[0m" return result end last_code end def self.append_in_hash(hash, key, value) if hash.has_key?(key) hash[key] << value else hash[key] = [value] end end end