| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- package common
- import (
- "cmp"
- "fmt"
- "image/color"
- "strings"
- "charm.land/lipgloss/v2"
- "github.com/charmbracelet/crush/internal/home"
- "github.com/charmbracelet/crush/internal/ui/styles"
- "github.com/charmbracelet/x/ansi"
- )
- // PrettyPath formats a file path with home directory shortening and applies
- // muted styling.
- func PrettyPath(t *styles.Styles, path string, width int) string {
- formatted := home.Short(path)
- return t.Muted.Width(width).Render(formatted)
- }
- // ModelContextInfo contains token usage and cost information for a model.
- type ModelContextInfo struct {
- ContextUsed int64
- ModelContext int64
- Cost float64
- }
- // ModelInfo renders model information including name, provider, reasoning
- // settings, and optional context usage/cost.
- func ModelInfo(t *styles.Styles, modelName, providerName, reasoningInfo string, context *ModelContextInfo, width int) string {
- modelIcon := t.Subtle.Render(styles.ModelIcon)
- modelName = t.Base.Render(modelName)
- // Build first line with model name and optionally provider on the same line
- var firstLine string
- if providerName != "" {
- providerInfo := t.Muted.Render(fmt.Sprintf("via %s", providerName))
- modelWithProvider := fmt.Sprintf("%s %s %s", modelIcon, modelName, providerInfo)
- // Check if it fits on one line
- if lipgloss.Width(modelWithProvider) <= width {
- firstLine = modelWithProvider
- } else {
- // If it doesn't fit, put provider on next line
- firstLine = fmt.Sprintf("%s %s", modelIcon, modelName)
- }
- } else {
- firstLine = fmt.Sprintf("%s %s", modelIcon, modelName)
- }
- parts := []string{firstLine}
- // If provider didn't fit on first line, add it as second line
- if providerName != "" && !strings.Contains(firstLine, "via") {
- providerInfo := fmt.Sprintf("via %s", providerName)
- parts = append(parts, t.Muted.PaddingLeft(2).Render(providerInfo))
- }
- if reasoningInfo != "" {
- parts = append(parts, t.Subtle.PaddingLeft(2).Render(reasoningInfo))
- }
- if context != nil {
- formattedInfo := formatTokensAndCost(t, context.ContextUsed, context.ModelContext, context.Cost)
- parts = append(parts, lipgloss.NewStyle().PaddingLeft(2).Render(formattedInfo))
- }
- return lipgloss.NewStyle().Width(width).Render(
- lipgloss.JoinVertical(lipgloss.Left, parts...),
- )
- }
- // formatTokensAndCost formats token usage and cost with appropriate units
- // (K/M) and percentage of context window.
- func formatTokensAndCost(t *styles.Styles, tokens, contextWindow int64, cost float64) string {
- var formattedTokens string
- switch {
- case tokens >= 1_000_000:
- formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
- case tokens >= 1_000:
- formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
- default:
- formattedTokens = fmt.Sprintf("%d", tokens)
- }
- if strings.HasSuffix(formattedTokens, ".0K") {
- formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
- }
- if strings.HasSuffix(formattedTokens, ".0M") {
- formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
- }
- percentage := (float64(tokens) / float64(contextWindow)) * 100
- formattedCost := t.Muted.Render(fmt.Sprintf("$%.2f", cost))
- formattedTokens = t.Subtle.Render(fmt.Sprintf("(%s)", formattedTokens))
- formattedPercentage := t.Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
- formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
- if percentage > 80 {
- formattedTokens = fmt.Sprintf("%s %s", styles.LSPWarningIcon, formattedTokens)
- }
- return fmt.Sprintf("%s %s", formattedTokens, formattedCost)
- }
- // StatusOpts defines options for rendering a status line with icon, title,
- // description, and optional extra content.
- type StatusOpts struct {
- Icon string // if empty no icon will be shown
- Title string
- TitleColor color.Color
- Description string
- DescriptionColor color.Color
- ExtraContent string // additional content to append after the description
- }
- // Status renders a status line with icon, title, description, and extra
- // content. The description is truncated if it exceeds the available width.
- func Status(t *styles.Styles, opts StatusOpts, width int) string {
- icon := opts.Icon
- title := opts.Title
- description := opts.Description
- titleColor := cmp.Or(opts.TitleColor, t.Muted.GetForeground())
- descriptionColor := cmp.Or(opts.DescriptionColor, t.Subtle.GetForeground())
- title = t.Base.Foreground(titleColor).Render(title)
- if description != "" {
- extraContentWidth := lipgloss.Width(opts.ExtraContent)
- if extraContentWidth > 0 {
- extraContentWidth += 1
- }
- description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContentWidth, "…")
- description = t.Base.Foreground(descriptionColor).Render(description)
- }
- var content []string
- if icon != "" {
- content = append(content, icon)
- }
- content = append(content, title)
- if description != "" {
- content = append(content, description)
- }
- if opts.ExtraContent != "" {
- content = append(content, opts.ExtraContent)
- }
- return strings.Join(content, " ")
- }
- // Section renders a section header with a title and a horizontal line filling
- // the remaining width.
- func Section(t *styles.Styles, text string, width int, info ...string) string {
- char := styles.SectionSeparator
- length := lipgloss.Width(text) + 1
- remainingWidth := width - length
- var infoText string
- if len(info) > 0 {
- infoText = strings.Join(info, " ")
- if len(infoText) > 0 {
- infoText = " " + infoText
- remainingWidth -= lipgloss.Width(infoText)
- }
- }
- text = t.Section.Title.Render(text)
- if remainingWidth > 0 {
- text = text + " " + t.Section.Line.Render(strings.Repeat(char, remainingWidth)) + infoText
- }
- return text
- }
- // DialogTitle renders a dialog title with a decorative line filling the
- // remaining width.
- func DialogTitle(t *styles.Styles, title string, width int, fromColor, toColor color.Color) string {
- char := "╱"
- length := lipgloss.Width(title) + 1
- remainingWidth := width - length
- if remainingWidth > 0 {
- lines := strings.Repeat(char, remainingWidth)
- lines = styles.ApplyForegroundGrad(t, lines, fromColor, toColor)
- title = title + " " + lines
- }
- return title
- }
|