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) }