elements.go

  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}