package tui import ( "fmt" "net/url" "os" "strings" "time" "unicode" "github.com/johannSo/codey/internal/llm" ) // human renders a token count the way a status bar wants to read it. func human(n int) string { switch { case n >= 1_000_000: return fmt.Sprintf("%.1fM", float64(n)/1e6) case n >= 1_000: return fmt.Sprintf("%.1fk", float64(n)/1e3) } return fmt.Sprint(n) } // elapsed renders how long something ran, in the coarsest unit that still // says something useful. Under a minute is the common answer and keeps a // decimal, because the difference between 3.2s and 3.9s is one a person // notices; past that the tenths stop mattering and the minutes start. func elapsed(d time.Duration) string { switch { case d < 0: return "0.0s" case d < time.Minute: return fmt.Sprintf("%.1fs", d.Seconds()) case d < time.Hour: return fmt.Sprintf("%dm %ds", int(d/time.Minute), int(d%time.Minute/time.Second)) } return fmt.Sprintf("%dh %dm", int(d/time.Hour), int(d%time.Hour/time.Minute)) } // prettyModel turns an API model id into the name a person uses for it: // claude-opus-5 becomes "Opus 5", claude-haiku-4-5-20251001 becomes // "Haiku 4.5". An id that does not fit the shape is returned unchanged, which // matters because this also runs against gateways serving models we have // never heard of. func prettyModel(id string) string { s := strings.TrimSuffix(strings.TrimPrefix(id, "claude-"), "-latest") // Anything bracketed is a variant tag, not part of the name: opus-5[1m]. tag := "" if i := strings.IndexByte(s, '['); i >= 0 { tag, s = s[i:], s[:i] } parts := strings.Split(s, "-") if len(parts) < 2 { return id } // A trailing eight-digit date stamp is a build, not a version. if last := parts[len(parts)-1]; len(last) == 8 && allDigits(last) { parts = parts[:len(parts)-1] } family, rest := parts[0], parts[1:] if family == "" || !isAlpha(family) { return id } name := strings.ToUpper(family[:1]) + family[1:] var version []string for _, p := range rest { if !allDigits(p) { return id } version = append(version, p) } if len(version) > 0 { name += " " + strings.Join(version, ".") } return name + tag } // endpointLabel names where the prompts are going. The default endpoint gets // a short name; anything else shows its host, because "somewhere that is not // Anthropic" is the single most important thing the header can say. func endpointLabel(c *llm.Client) string { if c == nil { return "" } if c.IsDefaultEndpoint() { return "anthropic" } if u, err := url.Parse(c.BaseURL); err == nil && u.Host != "" { return u.Host } return c.BaseURL } // camel renders a snake_case tool name as the transcript shows it: read_file // becomes ReadFile. It is a display transform only — approvals and errors // still quote the real name. func camel(name string) string { parts := strings.Split(name, "_") for i, p := range parts { if p == "" { continue } parts[i] = strings.ToUpper(p[:1]) + p[1:] } return strings.Join(parts, "") } func allDigits(s string) bool { if s == "" { return false } for _, r := range s { if !unicode.IsDigit(r) { return false } } return true } func isAlpha(s string) bool { for _, r := range s { if !unicode.IsLetter(r) { return false } } return s != "" } func maxInt(a, b int) int { if a > b { return a } return b } func minInt(a, b int) int { if a < b { return a } return b } // shortenHome writes a path the way a person says it. The working root is // shown in the welcome block, where "~/Work/thing" carries the same // information as the absolute path in a third of the columns. func shortenHome(path string) string { home, err := os.UserHomeDir() if err != nil || home == "" || path == home { return path } if strings.HasPrefix(path, home+string(os.PathSeparator)) { return "~" + path[len(home):] } return path }