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
15// PrettyPath formats a file path with home directory shortening and applies
16// muted styling.
17func PrettyPath(t *styles.Styles, path string, width int) string {
18 formatted := home.Short(path)
19 return t.Muted.Width(width).Render(formatted)
20}
21
22// ModelContextInfo contains token usage and cost information for a model.
23type ModelContextInfo struct {
24 ContextUsed int64
25 ModelContext int64
26 Cost float64
27}
28
29// ModelInfo renders model information including name, provider, reasoning
30// settings, and optional context usage/cost.
31func ModelInfo(t *styles.Styles, modelName, providerName, reasoningInfo string, context *ModelContextInfo, width int) string {
32 modelIcon := t.Subtle.Render(styles.ModelIcon)
33 modelName = t.Base.Render(modelName)
34
35 // Build first line with model name and optionally provider on the same line
36 var firstLine string
37 if providerName != "" {
38 providerInfo := t.Muted.Render(fmt.Sprintf("via %s", providerName))
39 modelWithProvider := fmt.Sprintf("%s %s %s", modelIcon, modelName, providerInfo)
40
41 // Check if it fits on one line
42 if lipgloss.Width(modelWithProvider) <= width {
43 firstLine = modelWithProvider
44 } else {
45 // If it doesn't fit, put provider on next line
46 firstLine = fmt.Sprintf("%s %s", modelIcon, modelName)
47 }
48 } else {
49 firstLine = fmt.Sprintf("%s %s", modelIcon, modelName)
50 }
51
52 parts := []string{firstLine}
53
54 // If provider didn't fit on first line, add it as second line
55 if providerName != "" && !strings.Contains(firstLine, "via") {
56 providerInfo := fmt.Sprintf("via %s", providerName)
57 parts = append(parts, t.Muted.PaddingLeft(2).Render(providerInfo))
58 }
59
60 if reasoningInfo != "" {
61 parts = append(parts, t.Subtle.PaddingLeft(2).Render(reasoningInfo))
62 }
63
64 if context != nil {
65 formattedInfo := formatTokensAndCost(t, context.ContextUsed, context.ModelContext, context.Cost)
66 parts = append(parts, lipgloss.NewStyle().PaddingLeft(2).Render(formattedInfo))
67 }
68
69 return lipgloss.NewStyle().Width(width).Render(
70 lipgloss.JoinVertical(lipgloss.Left, parts...),
71 )
72}
73
74// formatTokensAndCost formats token usage and cost with appropriate units
75// (K/M) and percentage of context window.
76func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost float64) string {
77 var formattedTokens string
78 switch {
79 case tokens >= 1_000_000:
80 formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
81 case tokens >= 1_000:
82 formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
83 default:
84 formattedTokens = fmt.Sprintf("%d", tokens)
85 }
86
87 if strings.HasSuffix(formattedTokens, ".0K") {
88 formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
89 }
90 if strings.HasSuffix(formattedTokens, ".0M") {
91 formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
92 }
93
94 percentage := (float64(tokens) / float64(contextWindow)) * 100
95
96 formattedCost := t.Muted.Render(fmt.Sprintf("$%.2f", cost))
97
98 formattedTokens = t.Subtle.Render(fmt.Sprintf("(%s)", formattedTokens))
99 formattedPercentage := t.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
100 formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
101 if percentage > 80 {
102 formattedTokens = fmt.Sprintf("%s %s", styles.WarningIcon, formattedTokens)
103 }
104
105 return fmt.Sprintf("%s %s", formattedTokens, formattedCost)
106}
107
108// StatusOpts defines options for rendering a status line with icon, title,
109// description, and optional extra content.
110type StatusOpts struct {
111 Icon string // if empty no icon will be shown
112 Title string
113 TitleColor color.Color
114 Description string
115 DescriptionColor color.Color
116 ExtraContent string // additional content to append after the description
117}
118
119// Status renders a status line with icon, title, description, and extra
120// content. The description is truncated if it exceeds the available width.
121func Status(t *styles.Styles, opts StatusOpts, width int) string {
122 icon := opts.Icon
123 title := opts.Title
124 description := opts.Description
125
126 titleColor := cmp.Or(opts.TitleColor, t.Muted.GetForeground())
127 descriptionColor := cmp.Or(opts.DescriptionColor, t.Subtle.GetForeground())
128
129 title = t.Base.Foreground(titleColor).Render(title)
130
131 if description != "" {
132 extraContentWidth := lipgloss.Width(opts.ExtraContent)
133 if extraContentWidth > 0 {
134 extraContentWidth += 1
135 }
136 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
137 description = t.Base.Foreground(descriptionColor).Render(description)
138 }
139
140 content := []string{}
141 if icon != "" {
142 content = append(content, icon)
143 }
144 content = append(content, title)
145 if description != "" {
146 content = append(content, description)
147 }
148 if opts.ExtraContent != "" {
149 content = append(content, opts.ExtraContent)
150 }
151
152 return strings.Join(content, " ")
153}
154
155// Section renders a section header with a title and a horizontal line filling
156// the remaining width.
157func Section(t *styles.Styles, text string, width int) string {
158 char := styles.SectionSeparator
159 length := lipgloss.Width(text) + 1
160 remainingWidth := width - length
161 text = t.Section.Title.Render(text)
162 if remainingWidth > 0 {
163 text = text + " " + t.Section.Line.Render(strings.Repeat(char, remainingWidth))
164 }
165 return text
166}
167
168// DialogTitle renders a dialog title with a decorative line filling the
169// remaining width.
170func DialogTitle(t *styles.Styles, title string, width int) string {
171 char := "╱"
172 length := lipgloss.Width(title) + 1
173 remainingWidth := width - length
174 if remainingWidth > 0 {
175 lines := strings.Repeat(char, remainingWidth)
176 lines = styles.ApplyForegroundGrad(t, lines, t.Primary, t.Secondary)
177 title = title + " " + lines
178 }
179 return title
180}