144 lines
3.3 KiB
Go
144 lines
3.3 KiB
Go
package app
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var (
|
|
statusVolatileRE = regexp.MustCompile(`\b(?:\d+h\s*)?\d+m\s*\d+s\b|\b\d{1,2}:\d{2}(?::\d{2})?\b|\b\d+(?:\.\d+)?s\b`)
|
|
counterRE = regexp.MustCompile(`\b\d+\s*/\s*\d+\b|\b\d{1,3}%`)
|
|
spinnerGlyphRE = regexp.MustCompile(`^[\s⠁⠂⠄⡀⢀⠠⠐⠈⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏•·∙◐◓◑◒]+`)
|
|
)
|
|
|
|
func canonicalizeTerminalText(s string, maxLines int) (string, bool, int) {
|
|
s = string(stripANSIBytes(nil, []byte(s)))
|
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
|
s = carriageReturnToLines(s)
|
|
s = strings.ReplaceAll(s, "\r", "\n")
|
|
|
|
lines := strings.Split(s, "\n")
|
|
out := make([]string, 0, len(lines))
|
|
pendingBlank := false
|
|
for _, raw := range lines {
|
|
line := strings.TrimRightFunc(stripControlRunes(raw), unicode.IsSpace)
|
|
if strings.TrimSpace(line) == "" {
|
|
if len(out) > 0 {
|
|
pendingBlank = true
|
|
}
|
|
continue
|
|
}
|
|
if isBorderOnlyLine(line) {
|
|
continue
|
|
}
|
|
line = canonicalStatusLine(line)
|
|
if len(out) > 0 && out[len(out)-1] == line {
|
|
pendingBlank = false
|
|
continue
|
|
}
|
|
if pendingBlank {
|
|
out = append(out, "")
|
|
pendingBlank = false
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
|
|
if maxLines > 0 && len(out) > maxLines {
|
|
dropped := strings.Join(out[:len(out)-maxLines], "\n")
|
|
out = out[len(out)-maxLines:]
|
|
return strings.Join(out, "\n"), true, len(dropped)
|
|
}
|
|
return strings.Join(out, "\n"), false, 0
|
|
}
|
|
|
|
func carriageReturnToLines(s string) string {
|
|
var out []string
|
|
var current strings.Builder
|
|
flush := func() {
|
|
out = append(out, current.String())
|
|
current.Reset()
|
|
}
|
|
for len(s) > 0 {
|
|
r, size := utf8.DecodeRuneInString(s)
|
|
s = s[size:]
|
|
switch r {
|
|
case '\r':
|
|
current.Reset()
|
|
case '\n':
|
|
flush()
|
|
default:
|
|
current.WriteRune(r)
|
|
}
|
|
}
|
|
if current.Len() > 0 || len(out) == 0 {
|
|
flush()
|
|
}
|
|
return strings.Join(out, "\n")
|
|
}
|
|
|
|
func stripControlRunes(s string) string {
|
|
return strings.Map(func(r rune) rune {
|
|
if r == '\t' || r == '\n' {
|
|
return r
|
|
}
|
|
if unicode.IsControl(r) {
|
|
return -1
|
|
}
|
|
return r
|
|
}, s)
|
|
}
|
|
|
|
func isBorderOnlyLine(s string) bool {
|
|
trimmed := strings.TrimSpace(s)
|
|
if trimmed == "" {
|
|
return false
|
|
}
|
|
seenBox := false
|
|
for _, r := range trimmed {
|
|
if r >= 0x2500 && r <= 0x257f {
|
|
seenBox = true
|
|
continue
|
|
}
|
|
switch r {
|
|
case ' ', '\t', '-', '_', '=', '+', '|', ':', '.', '\'', '"', '`', '*':
|
|
continue
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return seenBox
|
|
}
|
|
|
|
func canonicalStatusLine(s string) string {
|
|
if !looksStatusLike(s) {
|
|
return s
|
|
}
|
|
leading := len(s) - len(strings.TrimLeftFunc(s, unicode.IsSpace))
|
|
prefix := s[:leading]
|
|
body := s[leading:]
|
|
body = spinnerGlyphRE.ReplaceAllString(body, "")
|
|
body = statusVolatileRE.ReplaceAllString(body, "[time]")
|
|
body = counterRE.ReplaceAllString(body, "[count]")
|
|
return prefix + strings.TrimRightFunc(body, unicode.IsSpace)
|
|
}
|
|
|
|
func looksStatusLike(s string) bool {
|
|
lower := strings.ToLower(s)
|
|
for _, token := range []string{
|
|
"status", "running", "remaining", "progress", "loading",
|
|
"building", "installing", "downloading", "waiting", "working",
|
|
} {
|
|
if strings.Contains(lower, token) {
|
|
return true
|
|
}
|
|
}
|
|
trimmed := strings.TrimSpace(s)
|
|
if trimmed == "" {
|
|
return false
|
|
}
|
|
r, _ := utf8.DecodeRuneInString(trimmed)
|
|
return strings.ContainsRune("⠁⠂⠄⡀⢀⠠⠐⠈⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏•·∙◐◓◑◒", r)
|
|
}
|