1package ui23import (4 "strings"56 tea "charm.land/bubbletea/v2"78 "src.rest/cat/012/internal/sheet"9)1011// Format commands follow Sheets' Format menu and shortcuts. They apply to12// the whole selection through Sheet methods, each one undo step with a13// label such as "bold B2:B5", and confirm what changed on the third panel14// line.1516// numberFormats are Format > Number, with Sheets' examples as17// descriptions.18var numberFormats = []struct {19 id, title, desc string20 f sheet.Format21}{22 {"format.automatic", "Automatic", "Show numbers as typed, or as formulas infer", sheet.Format{}},23 {"format.plain_text", "Plain text", "Keep entries exactly as typed, even numbers and formulas", sheet.Preset(sheet.FmtText)},24 {"format.number", "Number", "Number with thousands separators: 1,000.12", sheet.Preset(sheet.FmtNumber)},25 {"format.percent", "Percent", "Percent: 10.12%", sheet.Preset(sheet.FmtPercent)},26 {"format.scientific", "Scientific", "Scientific notation: 1.01E+03", sheet.Preset(sheet.FmtScientific)},27 {"format.accounting", "Accounting", "Accounting, $ at the left and negatives in parentheses: $ (1,000.12)", sheet.Preset(sheet.FmtAccounting)},28 {"format.financial", "Financial", "Financial, negatives in parentheses: (1,000.12)", sheet.Preset(sheet.FmtFinancial)},29 {"format.currency", "Currency", "Currency: $1,000.12", sheet.Preset(sheet.FmtCurrency)},30 {"format.currency_rounded", "Currency rounded", "Currency without cents: $1,000", sheet.Format{Kind: sheet.FmtCurrency}},31 {"format.date", "Date", "Date: 9/26/2026", sheet.Preset(sheet.FmtDate)},32 {"format.time", "Time", "Time: 3:59:00 PM", sheet.Preset(sheet.FmtTime)},33 {"format.datetime", "Date time", "Date and time: 9/26/2026 15:59:00", sheet.Preset(sheet.FmtDateTime)},34 {"format.duration", "Duration", "Elapsed hours, minutes and seconds: 24:01:00", sheet.Preset(sheet.FmtDuration)},35 {"format.size", "Size", "A count of bytes, as nushell shows file sizes: 1.6 kB", sheet.Preset(sheet.FmtSize)},36}3738// textStyles toggle like Sheets: on for the whole selection unless the39// active cell already has it.40var textStyles = []struct {41 id, title, desc string42 get func(*sheet.Style) *bool43}{44 {"format.bold", "Bold", "Make text bold, or plain again", func(s *sheet.Style) *bool { return &s.Bold }},45 {"format.italic", "Italic", "Make text italic, or upright again", func(s *sheet.Style) *bool { return &s.Italic }},46 {"format.underline", "Underline", "Underline text, or remove the underline", func(s *sheet.Style) *bool { return &s.Underline }},47 {"format.strikethrough", "Strikethrough", "Strike through text, or remove the line", func(s *sheet.Style) *bool { return &s.Strikethrough }},48}4950var alignments = []struct {51 id, title, desc string52 a sheet.Align53}{54 {"format.align_left", "Left", "Align to the left", sheet.AlignLeft},55 {"format.align_center", "Center", "Center horizontally", sheet.AlignCenter},56 {"format.align_right", "Right", "Align to the right", sheet.AlignRight},57}5859func init() {60 for _, nf := range numberFormats {61 f, title := nf.f, nf.title62 register(&command{id: nf.id, title: title, desc: nf.desc, edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {63 r := m.selection()64 m.sheet.SetFormat(r, f)65 m.formatted(title + " format")66 return nil67 }})68 }69 for _, ts := range textStyles {70 get, title := ts.get, ts.title71 register(&command{id: ts.id, title: title, desc: ts.desc, edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {72 cur := m.sheet.CellStyle(m.cur)73 on := !*get(&cur)74 r := m.selection()75 label := strings.ToLower(title) + " " + r.String()76 if !on {77 label = "remove " + strings.ToLower(title) + " from " + r.String()78 }79 m.sheet.Batch(sheet.Change{Label: label, Focus: r}, func() error {80 m.sheet.SetStyle(r, func(s *sheet.Style) { *get(s) = on })81 return nil82 })83 m.formatted(title + onOff(on))84 return nil85 }})86 }87 for _, al := range alignments {88 a, title := al.a, al.title89 register(&command{id: al.id, title: title, desc: al.desc, edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {90 r := m.selection()91 m.sheet.Batch(sheet.Change{Label: "align " + r.String() + " " + strings.ToLower(title), Focus: r}, func() error {92 m.sheet.SetStyle(r, func(s *sheet.Style) { s.Align = a })93 return nil94 })95 m.formatted("Aligned " + strings.ToLower(title))96 return nil97 }})98 }99 register(100 &command{id: "format.decimals_more", title: "Increase decimal places", desc: "Show one more decimal place", edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {101 m.sheet.AdjustDecimals(m.selection(), 1)102 m.formatted("One more decimal place")103 return nil104 }},105 &command{id: "format.decimals_less", title: "Decrease decimal places", desc: "Show one less decimal place", edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {106 m.sheet.AdjustDecimals(m.selection(), -1)107 m.formatted("One less decimal place")108 return nil109 }},110 &command{id: "format.clear", title: "Clear formatting", desc: "Reset number formats and text styles, keeping contents", edits: (*Model).selection, keepsSpills: true, run: func(m *Model) tea.Cmd {111 m.sheet.ClearFormatting(m.selection())112 m.formatted("Formatting cleared")113 return nil114 }},115 )116117 // Sheets' shortcuts. Ctrl+Shift+digit needs a terminal that reports118 // it (the kitty keyboard protocol, as in Ghostty, kitty or WezTerm).119 for key, id := range map[string]string{120 "ctrl+shift+1": "format.number",121 "ctrl+shift+2": "format.time",122 "ctrl+shift+3": "format.date",123 "ctrl+shift+4": "format.currency",124 "ctrl+shift+5": "format.percent",125 "ctrl+shift+6": "format.scientific",126 "ctrl+b": "format.bold",127 "ctrl+i": "format.italic",128 "ctrl+u": "format.underline",129 "alt+shift+5": "format.strikethrough",130 "ctrl+shift+l": "format.align_left",131 "ctrl+shift+e": "format.align_center",132 "ctrl+shift+r": "format.align_right",133 "ctrl+\\": "format.clear",134 } {135 keymap[key] = id136 }137}138139// keyAliases maps other ways terminals report a shortcut to the form in140// keymap: without the kitty protocol, or with a shifted character,141// Ctrl+Shift+1 arrives as ctrl+! and Alt+Shift+5 as alt+%.142var keyAliases = func() map[string]string {143 a := map[string]string{"alt+%": "alt+shift+5", "alt+shift+%": "alt+shift+5"}144 for i, sym := range "!@#$^&" { // Sheets' border keys, Alt+Shift+1 to 4, 6 and 7145 d := string("123467"[i])146 a["alt+"+string(sym)] = "alt+shift+" + d147 a["alt+shift+"+string(sym)] = "alt+shift+" + d148 }149 for i, sym := range "!@#$%^" {150 d := string(rune('1' + i))151 a["ctrl+"+string(sym)] = "ctrl+shift+" + d152 a["ctrl+shift+"+string(sym)] = "ctrl+shift+" + d153 }154 for _, l := range "ler" {155 a["ctrl+"+strings.ToUpper(string(l))] = "ctrl+shift+" + string(l)156 }157 return a158}()159160// canonicalKey resolves keyAliases.161func canonicalKey(k string) string {162 if c, ok := keyAliases[k]; ok {163 return c164 }165 return k166}167168// formatted marks the sheet changed and says what happened, e.g. "Bold169// on for B2:B5", on the third panel line.170func (m *Model) formatted(what string) {171 m.changed = true172 m.note = what + " for " + m.selection().String()173}174175func onOff(on bool) string {176 if on {177 return " on"178 }179 return " off"180}