text format
This commit is contained in:
@@ -5,6 +5,90 @@ module BBS
|
|||||||
Cell = Struct.new(:char, :sgr)
|
Cell = Struct.new(:char, :sgr)
|
||||||
BLANK = Cell.new(' ', nil).freeze
|
BLANK = Cell.new(' ', nil).freeze
|
||||||
|
|
||||||
|
ANSI_ESC_RE = /\e\[[\d;]*[A-Za-z~]/.freeze
|
||||||
|
|
||||||
|
# Length of the visible portion of an ANSI-styled string.
|
||||||
|
def self.visible_width(text)
|
||||||
|
text.to_s.gsub(ANSI_ESC_RE, '').length
|
||||||
|
end
|
||||||
|
|
||||||
|
# Truncate an ANSI-styled string so its visible width is ≤ max_width.
|
||||||
|
# SGR escape sequences are preserved (they have zero visible width).
|
||||||
|
def self.clip_ansi(text, max_width)
|
||||||
|
return '' if max_width <= 0
|
||||||
|
out = +''
|
||||||
|
visible = 0
|
||||||
|
i = 0
|
||||||
|
s = text.to_s
|
||||||
|
len = s.length
|
||||||
|
while i < len
|
||||||
|
if s[i] == "\e" && i + 1 < len && s[i + 1] == '['
|
||||||
|
j = i + 2
|
||||||
|
j += 1 while j < len && !s[j].match?(/[A-Za-z~]/)
|
||||||
|
break if j >= len
|
||||||
|
out << s[i..j]
|
||||||
|
i = j + 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
break if visible >= max_width
|
||||||
|
out << s[i]
|
||||||
|
visible += 1
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
out
|
||||||
|
end
|
||||||
|
|
||||||
|
# Word-wrap an ANSI-styled string to lines of visible width ≤ width.
|
||||||
|
# SGR state is carried across wrapped lines: when the original text has
|
||||||
|
# an active color/attribute at a wrap point, the next line starts with
|
||||||
|
# that same SGR sequence so styling continues seamlessly.
|
||||||
|
def self.wrap_ansi(text, width)
|
||||||
|
return [''] if width <= 0
|
||||||
|
s = text.to_s
|
||||||
|
return [s] if visible_width(s) <= width
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
cur = +''
|
||||||
|
cur_visible = 0
|
||||||
|
active_sgr = nil
|
||||||
|
|
||||||
|
flush = lambda do
|
||||||
|
lines << cur.rstrip
|
||||||
|
cur = +''
|
||||||
|
cur_visible = 0
|
||||||
|
cur << active_sgr if active_sgr
|
||||||
|
end
|
||||||
|
|
||||||
|
s.scan(/(\e\[[\d;]*[A-Za-z~]|[^\s\e]+|\s+)/) do |(tok)|
|
||||||
|
if tok.start_with?("\e")
|
||||||
|
active_sgr = (tok == "\e[0m" || tok == "\e[m") ? nil : tok
|
||||||
|
cur << tok
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
tok_visible = tok.gsub(ANSI_ESC_RE, '').length
|
||||||
|
|
||||||
|
if tok.match?(/\A\s+\z/)
|
||||||
|
if cur_visible == 0
|
||||||
|
cur << tok
|
||||||
|
cur_visible += tok_visible
|
||||||
|
elsif cur_visible + tok_visible <= width
|
||||||
|
cur << tok
|
||||||
|
cur_visible += tok_visible
|
||||||
|
else
|
||||||
|
flush.call
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flush.call if cur_visible + tok_visible > width && cur_visible > 0
|
||||||
|
cur << tok
|
||||||
|
cur_visible += tok_visible
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
lines << cur.rstrip unless cur.empty?
|
||||||
|
lines.empty? ? [''] : lines
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :cols, :rows
|
attr_reader :cols, :rows
|
||||||
|
|
||||||
def initialize(cols, rows)
|
def initialize(cols, rows)
|
||||||
|
|||||||
@@ -445,7 +445,7 @@ module BBS
|
|||||||
line = @lines[@scroll + r]
|
line = @lines[@scroll + r]
|
||||||
next unless line
|
next unless line
|
||||||
frame.move(bounds.x, bounds.y + r)
|
frame.move(bounds.x, bounds.y + r)
|
||||||
frame.write_ansi(line[0, bounds.width].to_s, base_sgr: sgr)
|
frame.write_ansi(FrameBuffer.clip_ansi(line.to_s, bounds.width), base_sgr: sgr)
|
||||||
end
|
end
|
||||||
# right-side scrollbar
|
# right-side scrollbar
|
||||||
if @lines.size > bounds.height && bounds.width > 1
|
if @lines.size > bounds.height && bounds.width > 1
|
||||||
|
|||||||
Reference in New Issue
Block a user