1package theme23import (4 "slices"5 "strings"67 "charm.land/lipgloss/v2"8 "github.com/charmbracelet/x/ansi"9)1011// A box is framed with light box-drawing lines, which keep the crisp12// character-grid look. Rows are exactly the inner width; separator rows13// become a line joined to the frame.1415// SepRow marks a separator in the rows passed to Frame.16const SepRow = "\x00"1718// Frame draws a border around rows. A title sits in the top border and a19// footer at the right of the bottom border.20func (t *Theme) Frame(inner int, title, footer string, rows []string) []string {21 inner = max(inner, 2) // screens smaller than the box get a clipped box22 b := t.Border23 top := "┌" + strings.Repeat("─", inner) + "┐"24 if title != "" {25 tt := ansi.Truncate(" "+title+" ", inner-1, "…")26 top = b.Render("┌─") + t.Title.Render(tt) + b.Render(strings.Repeat("─", inner-1-ansi.StringWidth(tt))+"┐")27 } else {28 top = b.Render(top)29 }30 bottom := b.Render("└" + strings.Repeat("─", inner) + "┘")31 if footer != "" && ansi.StringWidth(footer)+4 <= inner {32 f := " " + footer + " "33 bottom = b.Render("└"+strings.Repeat("─", inner-1-ansi.StringWidth(f))) + t.Muted.Render(f) + b.Render("─┘")34 }35 lines := make([]string, 0, len(rows)+2)36 lines = append(lines, top)37 for _, r := range rows {38 if r == SepRow {39 lines = append(lines, b.Render("├"+strings.Repeat("─", inner)+"┤"))40 continue41 }42 lines = append(lines, b.Render("│")+r+b.Render("│"))43 }44 return append(lines, bottom)45}4647// Chip draws a key name as a key cap, e.g. "Ctrl+S".48func (t *Theme) Chip(label string) string {49 return t.KeyChip.Render(" " + label + " ")50}5152// Chips renders keys as key chips, e.g. [Ctrl+/] [F1].53func (t *Theme) Chips(keys []string) string {54 parts := make([]string, len(keys))55 for i, k := range keys {56 parts[i] = t.Chip(k)57 }58 return strings.Join(parts, " ")59}6061// KeyHints renders key and description pairs, e.g. "Enter accept", each62// key as a chip.63func (t *Theme) KeyHints(pairs ...string) string {64 parts := make([]string, 0, len(pairs)/2)65 for i := 0; i+1 < len(pairs); i += 2 {66 parts = append(parts, t.Chip(pairs[i])+" "+t.Muted.Render(pairs[i+1]))67 }68 return strings.Join(parts, " ")69}7071// Cells pads or truncates s to exactly w columns, then styles it.72func Cells(style lipgloss.Style, s string, w int) string {73 return style.Render(PadRight(ansi.Truncate(s, w, "…"), w))74}7576// HighlightMatches renders s in w columns with the bytes at idx in the hl77// style, truncating with an ellipsis.78func HighlightMatches(s string, idx []int, w int, base, hl lipgloss.Style) string {79 s = ansi.Truncate(s, w, "…")80 var b strings.Builder81 run, lit := "", false82 flush := func() {83 if lit {84 b.WriteString(hl.Render(run))85 } else {86 b.WriteString(base.Render(run))87 }88 run = ""89 }90 for i, r := range s {91 if on := slices.Contains(idx, i); on != lit {92 flush()93 lit = on94 }95 run += string(r)96 }97 flush()98 return b.String() + base.Render(strings.Repeat(" ", max(w-ansi.StringWidth(s), 0)))99}100101// KeyLabel formats a key binding for display, e.g. "ctrl+s" -> "Ctrl+S".102func KeyLabel(k string) string {103 switch k {104 case "delete":105 return "Del"106 case "backspace":107 return "Backspace"108 case "esc":109 return "Esc"110 }111 k = strings.NewReplacer("pgdown", "PgDn", "pgup", "PgUp").Replace(k)112 parts := strings.Split(k, "+")113 for i, p := range parts {114 switch {115 case len(p) == 1:116 parts[i] = strings.ToUpper(p)117 case p[0] == 'f' && len(p) <= 3 && p[1] >= '0' && p[1] <= '9':118 parts[i] = strings.ToUpper(p)119 default:120 parts[i] = strings.ToUpper(p[:1]) + p[1:]121 }122 }123 return strings.Join(parts, "+")124}125126// PadRight pads s with spaces to w columns.127func PadRight(s string, w int) string {128 return s + strings.Repeat(" ", max(w-ansi.StringWidth(s), 0))129}130131// PadLeft right-aligns s in w columns.132func PadLeft(s string, w int) string {133 return strings.Repeat(" ", max(w-ansi.StringWidth(s), 0)) + s134}135136// Center centers s in w columns, any odd space going right.137func Center(s string, w int) string {138 pad := max(w-ansi.StringWidth(s), 0)139 return strings.Repeat(" ", pad/2) + s + strings.Repeat(" ", pad-pad/2)140}