cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / ui / menubar.go
menubar.go 10.3K
1package ui23import (4 "strings"56 tea "charm.land/bubbletea/v2"7)89// The menu bar follows Google Sheets: File Edit View Insert Format Data10// Help, each opened with Alt and its accelerator letter (Alt+O for Format,11// as in Sheets). Items name registered commands; an item whose command12// isn't registered is hidden, and so is a menu or submenu left empty, so13// menus fill in as features land.1415// menuItem is one line of a dropdown: a command, a submenu or a separator.16type menuItem struct {17 title string // defaults to the command's title18 cmd string19 items []menuItem // a submenu20 sep bool21}2223// sep separates groups of items.24var sep = menuItem{sep: true}2526// menuDef is a menu bar title and its dropdown.27type menuDef struct {28 title string29 accel byte // lowercase accelerator letter, marked in the title30 items []menuItem31}3233var menuBar = []menuDef{34 {title: "File", accel: 'f', items: []menuItem{35 {cmd: "file.new"}, {cmd: "file.open"}, {cmd: "file.import"}, sep,36 {cmd: "file.save"}, {cmd: "file.saveas"}, {title: "Download", items: downloadItems()}, sep,37 {cmd: "share.who"}, {cmd: "share.hand"}, sep,38 {cmd: "agent.invite"}, {cmd: "agent.review"}, {cmd: "agent.direct"}, {cmd: "agent.stop"}, sep,39 {title: "Settings", items: []menuItem{40 {cmd: "settings.decimal"}, {cmd: "settings.locale"}, {cmd: "settings.vim"}, sep,41 {cmd: "settings.theme"}, {cmd: "settings.jev_key"}, sep,42 {cmd: "settings.config_edit"}, {cmd: "settings.config_reload"},43 }}, sep,44 {cmd: "pipe.send_selection"}, {cmd: "pipe.send_sheet"}, {cmd: "pipe.quit_unsent"}, {cmd: "quit"},45 }},46 {title: "Edit", accel: 'e', items: []menuItem{47 {cmd: "edit.undo"}, {cmd: "edit.redo"}, sep,48 {cmd: "edit.cut"}, {cmd: "edit.copy"}, {cmd: "edit.paste"}, {cmd: "edit.paste_values", title: "Paste values only"}, sep,49 {cmd: "edit.fill_down", title: "Fill down"}, {cmd: "edit.fill_right", title: "Fill right"}, sep,50 {cmd: "edit.find"}, {cmd: "edit.replace"}, sep,51 {cmd: "clear"}, {cmd: "select.all"}, {cmd: "goto"}, sep,52 {cmd: "delete.row", title: "Delete row"}, {cmd: "delete.col", title: "Delete column"}, {cmd: "delete.selection"}, sep,53 {title: "Sheet", items: []menuItem{54 {cmd: "sheet.rename", title: "Rename"}, {cmd: "sheet.duplicate", title: "Duplicate"}, {cmd: "sheet.delete", title: "Delete"}, {cmd: "sheet.hide"}, sep,55 {cmd: "sheet.move_left", title: "Move left"}, {cmd: "sheet.move_right", title: "Move right"}, sep,56 {cmd: "sheet.next"}, {cmd: "sheet.prev"}, {cmd: "sheet.goto"},57 }},58 }},59 {title: "View", accel: 'v', items: []menuItem{60 {title: "Freeze", items: []menuItem{61 {cmd: "view.freeze_rows0", title: "No rows"}, {cmd: "view.freeze_rows1", title: "1 row"},62 {cmd: "view.freeze_rows2", title: "2 rows"}, {cmd: "view.freeze_rows_cur", title: "Up to current row"}, sep,63 {cmd: "view.freeze_cols0", title: "No columns"}, {cmd: "view.freeze_cols1", title: "1 column"},64 {cmd: "view.freeze_cols2", title: "2 columns"}, {cmd: "view.freeze_cols_cur", title: "Up to current column"},65 }}, sep,66 {cmd: "sheet.unhide"}, {cmd: "nb.toc"}, sep,67 {cmd: "palette", title: "Command palette"}, {cmd: "help"},68 }},69 {title: "Insert", accel: 'i', items: []menuItem{70 {cmd: "insert.row_above", title: "Row above"}, {cmd: "insert.row_below", title: "Row below"}, sep,71 {cmd: "insert.col_left", title: "Column left"}, {cmd: "insert.col_right", title: "Column right"}, sep,72 {cmd: "insert.selection"}, sep,73 {cmd: "sheet.new", title: "Sheet"}, sep,74 {cmd: "note.edit"}, {cmd: "insert.chart"}, sep,75 {cmd: "insert.checkbox"}, {cmd: "insert.dropdown"},76 }},77 {title: "Format", accel: 'o', items: []menuItem{78 {title: "Number", items: []menuItem{79 {cmd: "format.automatic", title: "Automatic"}, {cmd: "format.plain_text", title: "Plain text"}, sep,80 {cmd: "format.number", title: "Number"}, {cmd: "format.percent", title: "Percent"}, {cmd: "format.scientific", title: "Scientific"}, sep,81 {cmd: "format.accounting", title: "Accounting"}, {cmd: "format.financial", title: "Financial"},82 {cmd: "format.currency", title: "Currency"}, {cmd: "format.currency_rounded", title: "Currency rounded"}, sep,83 {cmd: "format.date", title: "Date"}, {cmd: "format.time", title: "Time"}, {cmd: "format.datetime", title: "Date time"}, {cmd: "format.duration", title: "Duration"}, sep,84 {cmd: "format.size", title: "Size"},85 }},86 {cmd: "format.decimals_more", title: "Increase decimal places"}, {cmd: "format.decimals_less", title: "Decrease decimal places"}, sep,87 {cmd: "format.bold", title: "Bold"}, {cmd: "format.italic", title: "Italic"}, {cmd: "format.underline", title: "Underline"}, {cmd: "format.strikethrough", title: "Strikethrough"}, sep,88 {title: "Alignment", items: []menuItem{89 {cmd: "format.align_left", title: "Left"}, {cmd: "format.align_center", title: "Center"}, {cmd: "format.align_right", title: "Right"}, sep,90 {cmd: "format.valign_top", title: "Top"}, {cmd: "format.valign_middle", title: "Middle"}, {cmd: "format.valign_bottom", title: "Bottom"},91 }},92 {title: "Wrapping", items: []menuItem{93 {cmd: "format.wrap_overflow"}, {cmd: "format.wrap"}, {cmd: "format.wrap_clip"},94 }},95 {title: "Borders", items: []menuItem{96 {cmd: "format.borders_all", title: "All"}, {cmd: "format.borders_outer", title: "Outer"}, {cmd: "format.borders_inner", title: "Inner"}, sep,97 {cmd: "format.border_top", title: "Top"}, {cmd: "format.border_bottom", title: "Bottom"},98 {cmd: "format.border_left", title: "Left"}, {cmd: "format.border_right", title: "Right"}, sep,99 {cmd: "format.borders_clear", title: "None"}, sep,100 {cmd: "format.border_thin"}, {cmd: "format.border_thick"}, {cmd: "format.border_double"}, {cmd: "format.border_color"},101 }},102 {title: "Merge cells", items: []menuItem{103 {cmd: "format.merge_all"}, {cmd: "format.merge_horizontal"}, {cmd: "format.merge_vertical"}, sep,104 {cmd: "format.unmerge"},105 }}, sep,106 {cmd: "column.width"}, {cmd: "column.reset"}, {cmd: "row.height"}, {cmd: "row.fit"}, sep,107 {cmd: "table.create"}, sep,108 {cmd: "format.conditional"}, {cmd: "format.conditional_clear"}, sep,109 {cmd: "format.clear", title: "Clear formatting"},110 }},111 {title: "Data", accel: 'd', items: []menuItem{112 {title: "Sort sheet", items: []menuItem{113 {cmd: "data.sort_sheet_az", title: "A to Z"}, {cmd: "data.sort_sheet_za", title: "Z to A"},114 }},115 {title: "Sort range", items: []menuItem{116 {cmd: "data.sort_range_az", title: "A to Z by the active column"}, {cmd: "data.sort_range_za", title: "Z to A by the active column"}, sep,117 {cmd: "data.sort_range", title: "Advanced range sorting options"},118 }}, sep,119 {cmd: "data.filter"}, {cmd: "data.filter_column"}, {cmd: "data.filter_remove"}, sep,120 {cmd: "data.pivot"}, {cmd: "data.pivot_edit"}, {cmd: "data.frequency"}, sep,121 {cmd: "data.named_ranges"}, {cmd: "data.define_name"}, {title: "Table", items: tableItems}, {cmd: "data.protect"},122 {cmd: "data.validation"}, {cmd: "data.validation_clear"}, sep,123 {title: "Formula tracing", items: []menuItem{124 {cmd: "data.precedents"}, {cmd: "data.dependents"}, {cmd: "data.trace_list"}, sep,125 {cmd: "view.trace"}, {cmd: "data.evaluate"},126 }}, sep,127 {title: "Macros", items: macroItems}, {title: "Shell", items: notebookItems}, {title: "Linked file", items: linkedItems}, sep,128 {cmd: "jev.refresh"},129 }},130 {title: "Help", accel: 'h', items: []menuItem{131 {cmd: "palette", title: "Search the menus"}, {cmd: "help"}, {cmd: "help.functions"}, sep,132 {cmd: "help.about"},133 }},134}135136// label is the text an item shows.137func (it menuItem) label() string {138 if it.title != "" {139 return it.title140 }141 if c, ok := commands[it.cmd]; ok {142 return c.title143 }144 return it.cmd145}146147// visibleItems drops items whose command isn't registered and submenus148// left empty, then tidies the separators around what's left.149func visibleItems(items []menuItem) []menuItem {150 var out []menuItem151 for _, it := range items {152 switch {153 case it.sep:154 if len(out) > 0 && !out[len(out)-1].sep {155 out = append(out, it)156 }157 case it.items != nil:158 if sub := visibleItems(it.items); len(sub) > 0 {159 it.items = sub160 out = append(out, it)161 }162 case commands[it.cmd] != nil:163 out = append(out, it)164 }165 }166 if len(out) > 0 && out[len(out)-1].sep {167 out = out[:len(out)-1]168 }169 return out170}171172// barMenu is a menu bar title as laid out on screen.173type barMenu struct {174 def *menuDef175 x, w int // the title's span, including one column of padding each side176}177178// anyVisible reports whether visibleItems would keep any of items,179// without making the list: the menu bar asks every frame.180func anyVisible(items []menuItem) bool {181 for _, it := range items {182 if it.items != nil && anyVisible(it.items) || !it.sep && it.items == nil && commands[it.cmd] != nil {183 return true184 }185 }186 return false187}188189// barMenus lays out the menus that have visible items. Titles are190// separated by two spaces, as elsewhere in the UI.191func barMenus() []barMenu {192 var out []barMenu193 x := 0194 for i := range menuBar {195 d := &menuBar[i]196 if !anyVisible(d.items) {197 continue198 }199 w := len(d.title) + 2200 out = append(out, barMenu{def: d, x: x, w: w})201 x += w202 }203 return out204}205206// menuBarTitles renders the menu bar with each accelerator letter marked207// and the open menu, if any, highlighted.208func (m *Model) menuBarTitles() string {209 var b strings.Builder210 for i, bm := range barMenus() {211 style, accel := m.th.MenuBar, m.th.MenuAccel212 if i == m.openBarMenu() {213 style, accel = m.th.MenuSelected, m.th.MenuAccelSelected214 }215 t := bm.def.title216 k := strings.IndexByte(strings.ToLower(t), bm.def.accel)217 b.WriteString(style.Render(" "+t[:k]) + accel.Render(t[k:k+1]) + style.Render(t[k+1:]+" "))218 }219 return b.String()220}221222// openBarMenu is the index in barMenus of the open dropdown, or -1.223func (m *Model) openBarMenu() int {224 if o, ok := m.overlay.(*menuOverlay); ok {225 return o.bar226 }227 return -1228}229230// barMenuAt returns the index in barMenus of the title at column x, or -1.231func barMenuAt(x int) int {232 for i, bm := range barMenus() {233 if x >= bm.x && x < bm.x+bm.w {234 return i235 }236 }237 return -1238}239240// barMenuFor returns the index in barMenus of the menu Alt+key opens, or241// -1 if key isn't one.242func barMenuFor(key string) int {243 letter, ok := strings.CutPrefix(key, "alt+")244 if !ok || len(letter) != 1 {245 return -1246 }247 for i, bm := range barMenus() {248 if bm.def.accel == letter[0] {249 return i250 }251 }252 return -1253}254255func init() {256 register(&command{id: "menu", macro: macroNever, title: "Menu", desc: "Open the menu bar", run: func(m *Model) tea.Cmd {257 m.showBarMenu(0)258 return nil259 }})260 keymap["f10"] = "menu"261}