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, reasoning settings, and
30// optional context usage/cost.
31func ModelInfo(t *styles.Styles, modelName string, reasoningInfo string, context *ModelContextInfo, width int) string {
32 modelIcon := t.Subtle.Render(styles.ModelIcon)
33 modelName = t.Base.Render(modelName)
34 modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
35
36 parts := []string{
37 modelInfo,
38 }
39
40 if reasoningInfo != "" {
41 parts = append(parts, t.Subtle.PaddingLeft(2).Render(reasoningInfo))
42 }
43
44 if context != nil {
45 formattedInfo := formatTokensAndCost(t, context.ContextUsed, context.ModelContext, context.Cost)
46 parts = append(parts, lipgloss.NewStyle().PaddingLeft(2).Render(formattedInfo))
47 }
48
49 return lipgloss.NewStyle().Width(width).Render(
50 lipgloss.JoinVertical(lipgloss.Left, parts...),
51 )
52}
53
54// formatTokensAndCost formats token usage and cost with appropriate units
55// (K/M) and percentage of context window.
56func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost float64) string {
57 var formattedTokens string
58 switch {
59 case tokens >= 1_000_000:
60 formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
61 case tokens >= 1_000:
62 formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
63 default:
64 formattedTokens = fmt.Sprintf("%d", tokens)
65 }
66
67 if strings.HasSuffix(formattedTokens, ".0K") {
68 formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
69 }
70 if strings.HasSuffix(formattedTokens, ".0M") {
71 formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
72 }
73
74 percentage := (float64(tokens) / float64(contextWindow)) * 100
75
76 formattedCost := t.Muted.Render(fmt.Sprintf("$%.2f", cost))
77
78 formattedTokens = t.Subtle.Render(fmt.Sprintf("(%s)", formattedTokens))
79 formattedPercentage := t.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
80 formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
81 if percentage > 80 {
82 formattedTokens = fmt.Sprintf("%s %s", styles.WarningIcon, formattedTokens)
83 }
84
85 return fmt.Sprintf("%s %s", formattedTokens, formattedCost)
86}
87
88// StatusOpts defines options for rendering a status line with icon, title,
89// description, and optional extra content.
90type StatusOpts struct {
91 Icon string // if empty no icon will be shown
92 Title string
93 TitleColor color.Color
94 Description string
95 DescriptionColor color.Color
96 ExtraContent string // additional content to append after the description
97}
98
99// Status renders a status line with icon, title, description, and extra
100// content. The description is truncated if it exceeds the available width.
101func Status(t *styles.Styles, opts StatusOpts, width int) string {
102 icon := opts.Icon
103 title := opts.Title
104 description := opts.Description
105
106 titleColor := cmp.Or(opts.TitleColor, t.Muted.GetForeground())
107 descriptionColor := cmp.Or(opts.DescriptionColor, t.Subtle.GetForeground())
108
109 title = t.Base.Foreground(titleColor).Render(title)
110
111 if description != "" {
112 extraContentWidth := lipgloss.Width(opts.ExtraContent)
113 if extraContentWidth > 0 {
114 extraContentWidth += 1
115 }
116 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
117 description = t.Base.Foreground(descriptionColor).Render(description)
118 }
119
120 content := []string{}
121 if icon != "" {
122 content = append(content, icon)
123 }
124 content = append(content, title)
125 if description != "" {
126 content = append(content, description)
127 }
128 if opts.ExtraContent != "" {
129 content = append(content, opts.ExtraContent)
130 }
131
132 return strings.Join(content, " ")
133}
134
135// Section renders a section header with a title and a horizontal line filling
136// the remaining width.
137func Section(t *styles.Styles, text string, width int) string {
138 char := styles.SectionSeparator
139 length := lipgloss.Width(text) + 1
140 remainingWidth := width - length
141 text = t.Section.Title.Render(text)
142 if remainingWidth > 0 {
143 text = text + " " + t.Section.Line.Render(strings.Repeat(char, remainingWidth))
144 }
145 return text
146}