///|
/// ANSI 扫描状态:0 可见文本,1 CSI 序列中,2 OSC 序列中,3 刚读到 ESC。
const SCAN_TEXT : Int32 = 0
///|
const SCAN_CSI : Int32 = 1
///|
const SCAN_OSC : Int32 = 2
///|
const SCAN_ESC : Int32 = 3
///|
/// CSI 序列(`ESC [ ... 字母`)以字母收尾。
fn is_csi_end(c : Char) -> Bool {
let code = c.to_int()
(code >= 65 && code <= 90) || (code >= 97 && code <= 122)
}
///|
/// OSC 序列(`ESC ] ... BEL`,超链接用的那种)以 BEL 收尾。
fn is_osc_end(c : Char) -> Bool {
c.to_int() == 7
}
///|
/// 推进扫描状态。返回 (新状态, 该字符是否属于转义序列)。
///
/// CSI 与 OSC 必须分开判断:两者的收尾规则不同,混用会让 OSC 里的 URL 字母
/// 被当成 CSI 的收尾,序列后半截就泄漏成可见文本、宽度跟着算多。
fn scan_ansi(state : Int32, c : Char) -> (Int32, Bool) {
match state {
SCAN_ESC =>
if c == '[' {
(SCAN_CSI, true)
} else if c == ']' {
(SCAN_OSC, true)
} else {
// 不是 CSI / OSC,按两字符转义整个吃掉。
(SCAN_TEXT, true)
}
SCAN_CSI => if is_csi_end(c) { (SCAN_TEXT, true) } else { (SCAN_CSI, true) }
SCAN_OSC => if is_osc_end(c) { (SCAN_TEXT, true) } else { (SCAN_OSC, true) }
_ => if c == @clicolor.ESC { (SCAN_ESC, true) } else { (SCAN_TEXT, false) }
}
}
///|
/// 去掉文本里的 ANSI 转义序列,只留下可见字符。宽度计算与截断都先经它。
pub fn strip_ansi(text : String) -> String {
let buf = StringBuilder()
let mut state = SCAN_TEXT
for c in text {
let (next, in_escape) = scan_ansi(state, c)
state = next
if !in_escape {
buf.write_string(c.to_string())
}
}
buf.to_string()
}
///|
/// 单行文本的显示宽度(终端列数)。ANSI 序列不计宽,全角字符计 2 列。
pub fn display_width(text : String) -> Int32 {
@unicodewidth.str_width(strip_ansi(text))
}
///|
/// 多行文本的显示宽度,取最长的一行。
pub fn display_width_multiline(text : String) -> Int32 {
let mut widest = 0
for line in strip_ansi(text).split("\n") {
let w = @unicodewidth.str_width(line)
if w > widest {
widest = w
}
}
widest
}
///|
fn char_width_of(c : Char) -> Int32 {
match @unicodewidth.char_width(c) {
Some(w) => w
None => 0
}
}
///|
/// `count` 个空格。
fn spaces(count : Int32) -> String {
repeat_str(" ", count)
}
///|
/// 把 `text` 重复 `count` 次。
fn repeat_str(text : String, count : Int32) -> String {
let buf = StringBuilder()
for _ in 0.. String {
let len = display_width(text)
if len >= width {
return text
}
let fill = width - len
match align {
Left => text + spaces(fill)
Right => spaces(fill) + text
Center => {
// 与 cli-table3 一致:基数富余量偏右,左侧多留一列。
let right = (fill + 1) / 2
spaces(fill - right) + text + spaces(right)
}
}
}
///|
/// 按显示宽度取前缀。ANSI 序列整体保留且不计宽,所以带颜色的内容截断后
/// 颜色不会串到后面的文字上。
fn take_width(text : String, width : Int32) -> String {
let buf = StringBuilder()
let mut used = 0
let mut state = SCAN_TEXT
for c in text {
let (next, in_escape) = scan_ansi(state, c)
state = next
if in_escape {
buf.write_string(c.to_string())
continue
}
let w = char_width_of(c)
if used + w > width {
break
}
buf.write_string(c.to_string())
used += w
}
buf.to_string()
}
///|
/// 截断到给定显示宽度,超出部分换成省略符。放得下就原样返回。
pub fn truncate_to(text : String, width : Int32, symbol : String) -> String {
if display_width(text) <= width {
return text
}
let symbol_width = display_width(symbol)
let keep = if width > symbol_width { width - symbol_width } else { 0 }
let mut ret = take_width(text, keep)
ret += symbol
// 截断可能把 OSC 8 超链接的收尾序列切掉,补一个,免得后面的文字也被当成链接。
if text.contains(@clicolor.OSC8_PREFIX) &&
!ret.contains(@clicolor.OSC8_SUFFIX) {
ret += @clicolor.OSC8_SUFFIX
}
ret
}
///|
/// 把内容按 `\n` 切成若干行。
fn split_lines(text : String) -> Array[String] {
text.split("\n").map(fn(line) { line.to_owned() }).to_array()
}
///|
/// 按显示宽度断行。`on_word_boundary` 为真时优先在空白处断,否则按列硬切。
pub fn wrap_text(
text : String,
width : Int32,
on_word_boundary : Bool,
) -> Array[String] {
let out : Array[String] = []
if width <= 0 {
return out
}
for line in split_lines(text) {
if on_word_boundary {
out.append(wrap_by_word(line, width))
} else {
out.append(wrap_by_char(line, width))
}
}
out
}
///|
/// 按列硬切,忽略词边界。
fn wrap_by_char(line : String, width : Int32) -> Array[String] {
let out : Array[String] = []
let mut rest = line
while display_width(rest) > width {
let head = take_width(rest, width)
out.push(head)
rest = drop_width(rest, width)
}
out.push(rest)
out
}
///|
/// 丢掉前 `width` 列,返回剩余部分。
fn drop_width(text : String, width : Int32) -> String {
let seen = take_width(text, width)
text[seen.length():].to_owned()
}
///|
/// 在空白处断行。单个词长于一列宽时退化为按列硬切,避免无限循环。
fn wrap_by_word(line : String, width : Int32) -> Array[String] {
let out : Array[String] = []
let mut current = ""
for word in line.split(" ") {
let word = word.to_owned()
if current == "" {
current = word
} else if display_width(current) + 1 + display_width(word) <= width {
current = current + " " + word
} else {
out.push(current)
current = word
}
while display_width(current) > width {
out.push(take_width(current, width))
current = drop_width(current, width)
}
}
out.push(current)
out
}