1package ui23import (4 "cmp"5 "slices"6 "strconv"7 "strings"89 tea "charm.land/bubbletea/v2"1011 "src.rest/cat/012/internal/fileio"12 "src.rest/cat/012/internal/sheet"13 "src.rest/cat/012/internal/ui/cmdline"14)1516// The command line (: with vim keys, or from the palette) takes vim's17// file commands (:w, :q, :wq, :x, :e file), a cell, range, named range or18// row number to go to (:B12, :Sheet2!A1, :40), or any registered command19// by its ID or title (:edit.fill_down, :fill down). Completions come from20// the command registry as you type, in a box under the context line, so21// there's no second list of commands to keep up.2223func init() {24 register(&command{id: "vim.command", macro: macroNever, title: "Command line",25 desc: "Type a command: a cell to go to (B12), w, q, wq, e file, or any command by name",26 run: func(m *Model) tea.Cmd {27 m.openOverlay(cmdline.New(m.host()))28 return nil29 }})30}3132// commandIDs are the registered commands' IDs, by title.33func commandIDs() []string {34 ids := make([]string, 0, len(commands))35 for id := range commands {36 ids = append(ids, id)37 }38 slices.SortFunc(ids, func(a, b string) int {39 return cmp.Or(cmp.Compare(commands[a].title, commands[b].title), cmp.Compare(a, b))40 })41 return ids42}4344// runCmdLine carries out a command line and reports whether it was one.45func (m *Model) runCmdLine(text string) (tea.Cmd, bool) {46 word, arg, _ := strings.Cut(text, " ")47 arg = strings.TrimSpace(arg)48 bang := strings.HasSuffix(word, "!")49 switch strings.TrimSuffix(word, "!") {50 case "w":51 return m.writeTo(arg, bang), true52 case "q":53 if bang {54 return m.exit(), true55 }56 return m.runCommand("quit"), true57 case "wq", "x":58 if word == "x" && !m.changed && arg == "" {59 return m.exit(), true60 }61 m.quitAfterSave = true62 return m.writeTo(arg, bang), true63 case "e":64 return m.editFile(arg, bang), true65 }66 back := m.vim.back67 m.jumped() // going to a cell or row is a jump '' comes back from68 if n, err := strconv.Atoi(text); err == nil {69 m.clearSelection()70 m.cur.Row = m.visibleRow(clamp(n-1, 0, sheet.MaxRows-1))71 return nil, true72 }73 if m.gotoText(text) {74 return nil, true75 }76 m.vim.back = back77 id, ok := commandNamed(text)78 if !ok {79 return nil, false80 }81 if c := commands[id]; !c.available(m) {82 m.note = c.title + " isn't available now"83 return nil, true84 }85 return m.runCommand(id), true86}8788// commandNamed finds a command by its ID or title, in any case.89func commandNamed(text string) (string, bool) {90 ids := commandIDs()91 for _, id := range ids {92 if strings.EqualFold(id, text) {93 return id, true94 }95 }96 for _, id := range ids {97 if strings.EqualFold(commands[id].title, text) {98 return id, true99 }100 }101 return "", false102}103104// writeTo is :w. Without a name it saves, as File > Save; with one it105// saves as that name, as File > Save as, or downloads when the name is106// another format's (:w out.csv), as File > Download. It goes the menus'107// ways, so files are named, checked and replaced the same. Forced (:w!),108// it answers their questions as Overwrite and Replace would: it writes109// over the open file even if it changed on disk since it was opened, and110// over a file of the name given.111func (m *Model) writeTo(name string, force bool) tea.Cmd {112 if name == "" {113 if force && m.filename != "" {114 return m.saveAs(m.filename, false)115 }116 return m.runCommand("file.save")117 }118 k, ok := fileio.ExportKindOf(name)119 switch {120 case !ok && force:121 return m.saveAs(withExt(name), false)122 case !ok:123 return m.saveAsFile(name)124 case !k.CanExport():125 m.quitAfterSave = false126 m.fail("012 can't write " + k.String() + " files")127 return nil128 }129 m.quitAfterSave = false // a download isn't the sheet's file130 if force && !k.HasTables() {131 if path, ok := m.path("download to", name); ok {132 return m.download(name, path, k, sheet.Rect{}, "")133 }134 return nil135 }136 return m.downloadFile(k, sheet.Rect{}, name)137}138139// editFile is :e name: open a sheet or import a file, refusing to drop140// unsaved changes unless forced with :e!.141func (m *Model) editFile(name string, force bool) tea.Cmd {142 switch {143 case name == "":144 return m.runCommand("file.open")145 case m.changed && !force:146 m.fail("Unsaved changes: :w saves them, :e! " + name + " discards them")147 return nil148 }149 return m.openFile(name)150}