1package ui23import (4 "strings"56 tea "charm.land/bubbletea/v2"7 "charm.land/lipgloss/v2"8 "github.com/charmbracelet/x/ansi"910 "src.rest/cat/012/internal/sheet"11 "src.rest/cat/012/internal/ui/overlay"12 "src.rest/cat/012/internal/ui/theme"13)1415// The context line explains the active cell's error, or why a linked16// source's tab can't show its rows, in one line. An explanation longer17// than the line is cut where a sentence ends when one fits, else with …,18// and an F1 chip at the right says how to read the rest: F1 there opens19// the whole explanation in a box (beside the cell, as a note shows on20// hover), and anywhere else the keyboard shortcuts, as F1 at a word of a21// notebook's cell does.2223func init() {24 register(&command{id: "help.here", macro: macroNever, title: "Help here",25 desc: "Explain the active cell's error in full, or else show the keyboard shortcuts",26 run: func(m *Model) tea.Cmd {27 if b := m.explained(); b != nil {28 m.openOverlay(b)29 return nil30 }31 return m.runCommand("help")32 }})33 keymap["f1"] = "help.here"34}3536// explained is the box explaining what the context line explains, or37// nil when it explains nothing.38func (m *Model) explained() *errorBox {39 if m.srcView() != nil {40 if title, why := m.sourceTrouble(); why != "" {41 return &errorBox{m: m, title: title, why: why, beside: false}42 }43 return nil44 }45 if v, why := m.errorWhy(); why != "" {46 return &errorBox{m: m, title: v.Str + " in " + m.cur.String(), why: why, at: m.cur, beside: true}47 }48 return nil49}5051// errorWhy is the active cell's error and why it shows it, "" when it52// shows none or there's nothing to say.53func (m *Model) errorWhy() (sheet.Value, string) {54 if m.srcView() != nil || m.nbView() != nil {55 return sheet.Value{}, ""56 }57 v := m.sheet.Value(m.cur)58 if v.Kind != sheet.Error {59 return v, ""60 }61 return v, m.sheet.ExplainError(m.cur)62}6364// errorLine explains the active cell's error on the context line, e.g.65// "#DIV/0! Division by zero in B3/0", and when that is too long for the66// line, the part that fits and an F1 chip for the rest.67func (m *Model) errorLine() (string, string) {68 v, why := m.errorWhy()69 if why == "" {70 return "", ""71 }72 return m.explainLine(m.th.ErrorCell.Render(v.Str)+" ", why, m.th.Muted)73}7475// explainLine is head then why in style, on one line: the part of why76// that fits and an F1 chip for the rest when it doesn't.77func (m *Model) explainLine(head, why string, style lipgloss.Style) (string, string) {78 if ansi.StringWidth(head)+ansi.StringWidth(why) <= m.width {79 return head + style.Render(why), ""80 }81 chip := m.th.KeyHints("F1", "more")82 room := m.width - ansi.StringWidth(head) - ansi.StringWidth(chip) - 383 return head + style.Render(cutExplanation(why, room)), chip84}8586// spread puts right at the right edge after left, dropping it if there87// isn't room for both.88func (m *Model) spread(left, right string) string {89 gap := m.width - ansi.StringWidth(left) - ansi.StringWidth(right)90 if right == "" || gap < 3 {91 return left92 }93 return left + strings.Repeat(" ", gap) + right94}9596// cutExplanation is the start of why in at most room columns: its97// sentences that fit, when at least one does, followed by …; else as98// much of it as fits.99func cutExplanation(why string, room int) string {100 if room < 2 {101 return ""102 }103 end := -1104 for i := 0; i < len(why); i++ {105 if (why[i] == '.' || why[i] == ';') && i+1 < len(why) && why[i+1] == ' ' && ansi.StringWidth(why[:i+1])+2 <= room {106 end = i + 1107 }108 }109 if end > 0 {110 return why[:end] + " …"111 }112 return ansi.Truncate(why, room, "…")113}114115// errorBox is an error's whole explanation in a box over the grid,116// beside its cell or under the context line; any key closes it.117type errorBox struct {118 m *Model119 title string120 why string121 at sheet.Addr122 beside bool // beside the cell at, else at the grid's top left123}124125const errorBoxID = "error"126127// errorBoxWidth is the widest the explanation's lines run, for reading.128const errorBoxWidth = 48129130func (b *errorBox) Indicator() string { return "HELP" }131132// Layout wraps the explanation to the box's width and places the box133// as a note's (besideCell), or at the grid's top left on a source's tab.134func (b *errorBox) Layout() []overlay.Box {135 m := b.m136 inner := min(errorBoxWidth, m.width-2)137 lines := strings.Split(ansi.Wordwrap(b.why, inner-2, ""), "\n")138 rows := make([]string, len(lines))139 for i, line := range lines {140 rows[i] = m.th.MenuBar.Render(theme.PadRight(" "+strings.TrimRight(line, " "), inner))141 }142 framed := m.th.Frame(inner, b.title, "", rows)143 if !b.beside {144 return []overlay.Box{{ID: errorBoxID, X: 0, Y: gridTop, Lines: framed}}145 }146 return []overlay.Box{m.besideCell(errorBoxID, b.at, framed)}147}148149// Key closes the box, and a key that moves or runs a command does so150// too, as it would have without the box.151func (b *errorBox) Key(k tea.KeyPressMsg) tea.Cmd {152 b.m.closeOverlay()153 switch k.String() {154 case "esc", "enter", "f1":155 return nil156 }157 return b.m.handleKey(k)158}159160// Mouse closes the box on a click anywhere.161func (b *errorBox) Mouse(e overlay.MouseEvent) tea.Cmd {162 if e.Kind == overlay.MousePress {163 b.m.closeOverlay()164 }165 return nil166}167168func (b *errorBox) Status() (string, string) {169 return "The whole explanation; any key closes it", b.m.th.KeyHints("Esc", "close")170}