1package common
2
3import (
4 "cmp"
5 "fmt"
6 "image/color"
7 "strings"
8
9 "charm.land/lipgloss/v2"
10 "github.com/charmbracelet/crush/internal/home"
11 "github.com/charmbracelet/crush/internal/ui/styles"
12 "github.com/charmbracelet/x/ansi"
13)
14
15func PrettyPath(t *styles.Styles, path string, width int) string {
16 formatted := home.Short(path)
17 return t.Muted.Width(width).Render(formatted)
18}
19
20type ModelContextInfo struct {
21 ContextUsed int64
22 ModelContext int64
23 Cost float64
24}
25
26func ModelInfo(t *styles.Styles, modelName string, reasoningInfo string, context *ModelContextInfo, width int) string {
27 modelIcon := t.Subtle.Render(styles.ModelIcon)
28 modelName = t.Base.Render(modelName)
29 modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
30
31 parts := []string{
32 modelInfo,
33 }
34
35 if reasoningInfo != "" {
36 parts = append(parts, t.Subtle.PaddingLeft(2).Render(reasoningInfo))
37 }
38
39 if context != nil {
40 parts = append(parts, formatTokensAndCost(t, context.ContextUsed, context.ModelContext, context.Cost))
41 }
42
43 return lipgloss.NewStyle().Width(width).Render(
44 lipgloss.JoinVertical(lipgloss.Left, parts...),
45 )
46}
47
48func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost float64) string {
49 var formattedTokens string
50 switch {
51 case tokens >= 1_000_000:
52 formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
53 case tokens >= 1_000:
54 formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
55 default:
56 formattedTokens = fmt.Sprintf("%d", tokens)
57 }
58
59 if strings.HasSuffix(formattedTokens, ".0K") {
60 formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
61 }
62 if strings.HasSuffix(formattedTokens, ".0M") {
63 formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
64 }
65
66 percentage := (float64(tokens) / float64(contextWindow)) * 100
67
68 formattedCost := t.Muted.Render(fmt.Sprintf("$%.2f", cost))
69
70 formattedTokens = t.Subtle.Render(fmt.Sprintf("(%s)", formattedTokens))
71 formattedPercentage := t.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
72 formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
73 if percentage > 80 {
74 formattedTokens = fmt.Sprintf("%s %s", styles.WarningIcon, formattedTokens)
75 }
76
77 return fmt.Sprintf("%s %s", formattedTokens, formattedCost)
78}
79
80type StatusOpts struct {
81 Icon string // if empty no icon will be shown
82 Title string
83 TitleColor color.Color
84 Description string
85 DescriptionColor color.Color
86 ExtraContent string // additional content to append after the description
87}
88
89func Status(t *styles.Styles, opts StatusOpts, width int) string {
90 icon := opts.Icon
91 title := opts.Title
92 description := opts.Description
93
94 titleColor := cmp.Or(opts.TitleColor, t.Muted.GetForeground())
95 descriptionColor := cmp.Or(opts.DescriptionColor, t.Subtle.GetForeground())
96
97 title = t.Base.Foreground(titleColor).Render(title)
98
99 if description != "" {
100 extraContentWidth := lipgloss.Width(opts.ExtraContent)
101 if extraContentWidth > 0 {
102 extraContentWidth += 1
103 }
104 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
105 description = t.Base.Foreground(descriptionColor).Render(description)
106 }
107
108 content := []string{}
109 if icon != "" {
110 content = append(content, icon)
111 }
112 content = append(content, title)
113 if description != "" {
114 content = append(content, description)
115 }
116 if opts.ExtraContent != "" {
117 content = append(content, opts.ExtraContent)
118 }
119
120 return strings.Join(content, " ")
121}