cat / 012
clone

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

cat/module-path
012 / internal / ui / importplace.go
importplace.go 5.2K
1package ui23import (4 "path/filepath"5 "strings"67 tea "charm.land/bubbletea/v2"89 "src.rest/cat/012/internal/fileio"10 "src.rest/cat/012/internal/sheet"11 "src.rest/cat/012/internal/ui/picker"12 "src.rest/cat/012/internal/ui/transfer"13)1415// Where File > Import puts what it reads, as Sheets' Import location:16// into new sheets after the sheet shown, in place of it (keeping its17// charts), or in place of the whole spreadsheet (what File > Open and18// the command line do). A new, empty spreadsheet is just replaced. Inserting or replacing19// a sheet is one undo step, and the file stays the one being edited.2021// The places are transfer.Book, transfer.NewSheets and transfer.Sheet.2223// pristine reports whether the spreadsheet is new and untouched, so an24// import may simply replace it.25func (m *Model) pristine() bool {26 return !m.changed && m.filename == "" && m.xfer.Source == "" && m.book().Len() == 1 &&27 m.sheet.Len() == 0 && len(m.sheet.Charts()) == 028}2930// askImportPlace imports name, asking first where it goes unless the31// spreadsheet is new and empty.32func (m *Model) askImportPlace(name string) tea.Cmd {33 if m.pristine() {34 return m.startImport(name, fileio.Options{}, transfer.Book)35 }36 k, _ := fileio.KindOf(name)37 base := filepath.Base(name)38 insert, as := "Insert new sheet", " as a new sheet"39 if k.HoldsSheets() {40 insert, as = "Insert new sheets", " as new sheets"41 }42 item := func(title, detail, desc string, run func(m *Model) tea.Cmd) picker.Item {43 return picker.Item{Title: title, Name: len(title), Detail: detail, Desc: desc, Pick: func() tea.Cmd {44 m.closeOverlay()45 return run(m)46 }}47 }48 items := []picker.Item{item(insert, "after "+m.sheet.Name(), "Add "+base+as,49 func(m *Model) tea.Cmd { return m.startImport(name, fileio.Options{}, transfer.NewSheets) })}50 if !k.HoldsSheets() {51 keeping := ", keeping its name"52 if len(m.sheet.Charts()) > 0 {53 keeping = ", keeping its name and charts"54 }55 items = append(items, item("Replace current sheet", m.sheet.Name(), "Put "+base+" in place of "+m.sheet.Name()+keeping,56 func(m *Model) tea.Cmd { return m.startImport(name, fileio.Options{}, transfer.Sheet) }))57 }58 items = append(items, item("Follow the file", "live, in a new sheet", "Add "+base+" as a new sheet linked to the file, following it as it grows or is rewritten",59 func(m *Model) tea.Cmd {60 return m.linkFile(name, func(m *Model, src sheet.LinkSource) { m.followInNewSheet(src) })61 }))62 detail := "open it instead"63 if m.changed {64 detail = "unsaved changes"65 }66 items = append(items, item("Replace spreadsheet", detail, "Open "+base+" instead, as File > Open does",67 func(m *Model) tea.Cmd { return m.confirmImport(name, fileio.Options{}) }))68 p := m.newPicker("Import "+base, "Import location", 60, items)69 p.Action = "import"70 m.openOverlay(p)71 return nil72}7374// placeImport puts an imported workbook where the import was asked to go75// and says what it did, or reports false to replace the spreadsheet.76func (m *Model) placeImport(msg transfer.ImportedMsg, what string) bool {77 res := msg.Res78 label := "import " + filepath.Base(msg.Name)79 switch msg.Place {80 case transfer.NewSheets:81 ins, err := m.book().InsertBook(res.Sheet.Book(), m.book().Index(m.sheet)+1, label)82 if err != nil {83 m.fail(err.Error())84 return true85 }86 show := res.Sheet87 if show.Hidden() {88 show = ins.Sheets[0]89 }90 m.afterSheetsChange(show, m.book().Index(m.sheet))91 m.note = "Imported " + what + " as " + sheetList(ins.Sheets) + " (" + transfer.Rows(res.Rows) + ")"92 if ins.Names > 0 {93 m.note += "; " + plural(ins.Names, "1 named range", transfer.Thousands(ins.Names)+" named ranges") + " left out, their names taken"94 }95 case transfer.Sheet:96 i := m.book().Index(m.sheet)97 fates, err := m.book().ReplaceSheet(m.sheet, res.Sheet, label)98 if err != nil {99 m.fail(err.Error())100 return true101 }102 m.afterSheetsChange(res.Sheet, i)103 m.note = "Imported " + what + " into " + res.Sheet.Name() + " (" + transfer.Rows(res.Rows) + ")" + chartFates(fates)104 default:105 return false106 }107 m.changed = true108 if len(res.Notes) > 0 {109 m.note += "; " + strings.Join(res.Notes, "; ")110 }111 return true112}113114// sheetList names sheets for a note: "Sales", "Sales and Q3", or "3115// sheets, Sales to Q3".116func sheetList(sheets []*sheet.Sheet) string {117 switch n := len(sheets); n {118 case 1:119 return sheets[0].Name()120 case 2:121 return sheets[0].Name() + " and " + sheets[1].Name()122 default:123 return transfer.Thousands(n) + " sheets, " + sheets[0].Name() + " to " + sheets[n-1].Name()124 }125}126127// chartFates tells what replacing a sheet did to its charts, for the128// note: re-pointed to the new data, kept on their range, or removed, as129// they no longer fit (undo brings them back).130func chartFates(fates []sheet.ChartFate) string {131 var kept, removed []string132 for _, f := range fates {133 switch {134 case f.Removed:135 removed = append(removed, f.Name+" ("+f.Was.String()+")")136 case f.Now != f.Was:137 kept = append(kept, f.Name+" re-pointed to "+f.Now.String())138 default:139 kept = append(kept, f.Name+" kept on "+f.Now.String())140 }141 }142 // Removals first: they matter more when the note is cut short.143 out := ""144 if len(removed) > 0 {145 out += "; removed charts: " + strings.Join(removed, ", ")146 }147 if len(kept) > 0 {148 out += "; charts: " + strings.Join(kept, ", ")149 }150 return out151}