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 / download.go
download.go 5.3K
1package ui23import (4 "context"5 "fmt"6 "os"7 "path/filepath"8 "strings"910 tea "charm.land/bubbletea/v2"1112 "src.rest/cat/012/internal/fileio"13 "src.rest/cat/012/internal/sheet"14 "src.rest/cat/012/internal/telemetry"15)1617// File > Download: the workbook, or for formats with tables the selection,18// written in the background in any format fileio exports, asking before19// replacing a file or table.2021func init() {22 for _, k := range fileio.Kinds() {23 if !k.CanExport() {24 continue25 }26 register(&command{27 id: downloadID(k),28 title: "Download as " + k.String(),29 desc: k.About(),30 run: func(m *Model) tea.Cmd { return m.openDownload(k) },31 macro: macroNever,32 })33 }34}3536// downloadID is the command downloading in format k.37func downloadID(k fileio.Kind) string { return "file.download." + strings.ToLower(k.String()) }3839// downloadItems is the File > Download menu: every format 012 exports.40func downloadItems() []menuItem {41 var items []menuItem42 for _, k := range fileio.Kinds() {43 if k.CanExport() {44 items = append(items, menuItem{cmd: downloadID(k), title: k.MenuTitle()})45 }46 }47 return items48}4950type exportedMsg struct {51 name string52 kind fileio.Kind53 res *fileio.ExportResult54 err error55}5657// displayBase is the name downloads and saves start from: the sheet's58// file or the imported file, without its extension.59func (m *Model) displayBase() string {60 name := m.filename61 if name == "" {62 name = m.xfer.Source63 }64 if name == "" {65 return "SHEET1"66 }67 return strings.TrimSuffix(name, filepath.Ext(name))68}6970// openDownload asks where to download the sheet as k.71func (m *Model) openDownload(k fileio.Kind) tea.Cmd {72 if _, ok := m.sheet.UsedRange(); !ok {73 m.note = "Nothing to download: the sheet is empty"74 return nil75 }76 r := sheet.Rect{}77 label := "Download as " + k.String() + ":"78 if k.HasTables() && m.hasRange() {79 r = m.selection()80 label = "Download " + r.String() + " as " + k.String() + ":"81 }82 m.openText(label, m.displayBase()+k.Ext(), func(m *Model, text string) tea.Cmd {83 return m.downloadFile(k, r, text)84 })85 return nil86}8788// downloadFile downloads r (the whole sheet when zero) as k to a file89// named for Download or :w, asking before replacing a file or table.90func (m *Model) downloadFile(k fileio.Kind, r sheet.Rect, text string) tea.Cmd {91 if text == "" {92 return nil93 }94 name := text95 if filepath.Ext(name) == "" {96 name += k.Ext()97 }98 path, ok := m.path("download to", name)99 if !ok {100 return nil101 }102 if k.HasTables() {103 m.openTableName(name, path, k, r)104 return nil105 }106 return m.confirmReplace(filepath.Base(name)+" exists.", func(m *Model) tea.Cmd {107 return m.download(name, path, k, r, "")108 }, exists(path))109}110111func exists(name string) bool {112 _, err := os.Stat(name)113 return err == nil114}115116// openTableName asks for the table to write in a database of kind k,117// name at path on disk.118func (m *Model) openTableName(name, path string, k fileio.Kind, r sheet.Rect) {119 m.openText("Table in "+filepath.Base(name)+":", fileio.TableName(filepath.Base(m.displayBase())), func(m *Model, table string) tea.Cmd {120 if table == "" {121 return nil122 }123 has := false124 if exists(path) {125 ts, err := fileio.Tables(context.Background(), path)126 if err != nil {127 m.fail(fmt.Sprintf("Couldn't open %s: %v", filepath.Base(name), err))128 return nil129 }130 for _, t := range ts {131 has = has || strings.EqualFold(t.Name, table)132 }133 }134 return m.confirmReplace("Table "+table+" exists in "+filepath.Base(name)+".", func(m *Model) tea.Cmd {135 return m.download(name, path, k, r, table)136 }, has)137 })138}139140// confirmReplace runs do, first asking on the context line when it141// would replace something.142func (m *Model) confirmReplace(msg string, do func(*Model) tea.Cmd, replaces bool) tea.Cmd {143 if !replaces {144 return do(m)145 }146 m.ask(question{147 msg: msg,148 warn: true,149 choices: []choice{150 {key: "enter", label: "Replace", run: do},151 {key: "esc", label: "Cancel", run: func(m *Model) tea.Cmd {152 m.quitAfterSave = false // cancelling :wq's save cancels its quit153 return nil154 }},155 },156 })157 return nil158}159160// download snapshots the sheet and writes it, as name at path on disk,161// in the background.162func (m *Model) download(name, path string, k fileio.Kind, r sheet.Rect, table string) tea.Cmd {163 snap := fileio.Snap(m.sheet, r, filepath.Base(m.displayBase()))164 if k.HoldsSheets() && r == (sheet.Rect{}) {165 snap = fileio.SnapBook(m.sheet) // every sheet, as Sheets' .xlsx download166 }167 ctx := telemetry.WithParent(context.Background(), m.spans.Parent())168 return func() tea.Msg {169 res, err := fileio.Export(ctx, path, k, snap, fileio.ExportOptions{Table: table})170 return exportedMsg{name: name, kind: k, res: res, err: err}171 }172}173174// saveImported is Save for a sheet imported from another format: save it175// as a .012 file (keeping formulas and formatting), or export it back.176func (m *Model) saveImported() tea.Cmd {177 k := m.xfer.Kind178 choices := []choice{{key: "enter", label: "Save as " + filepath.Base(m.displayBase()) + sheet.FileExt, run: (*Model).openSave}}179 if k.CanExport() {180 choices = append(choices, choice{key: "e", label: "Download as " + k.String(), run: func(m *Model) tea.Cmd { return m.openDownload(k) }})181 }182 choices = append(choices, choice{key: "esc", label: "Cancel", run: func(m *Model) tea.Cmd {183 m.quitAfterSave = false184 return nil185 }})186 m.ask(question{msg: filepath.Base(m.xfer.Source) + " was imported.", choices: choices})187 return nil188}