cat / 012
clone

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

Move the formats table to formats.go and describe each format fully there

Dami committed 3945e7f
Each row now also holds what the UI shows for the format (its noun,
Download menu title and description) and its traits (holds several
sheets, is a database of tables), exposed as Kind methods, so the UI
can derive its lists instead of switching on kinds.
commit
3945e7f2ef42ca3e7f094e63bf28101c28c14ee1
parent
0040bb1c716ae3f7daff9e90e3e670f86f098fd8
author
Dami <dami.etoile@gmail.com>
3 files changed, +191 -108
Minternal/fileio/fileio.go111+---------------------
Ainternal/fileio/formats.go151++++++++++++++++++++++++++++++
Ainternal/fileio/formats_test.go37+++++++
M internal/fileio/fileio.go +3 -108
@@ -3,6 +3,7 @@
33 // 1-2-3 worksheets. Everything is pure Go. The sheet package knows none44 // of these formats: importers build a sheet through its public API55 // (Load, SetColWidth, RecalcAll) and exporters read a Snapshot of one.6+// Each format is a row of the table in formats.go and a file of its own.67 package fileio78 89 import (
@@ -21,111 +22,6 @@ import (
2122 "github.com/FelineStateMachine/012/internal/telemetry"2223 )2324 24-// Kind is an external file format.25-type Kind int26-27-const (28- CSV Kind = iota + 129- TSV30- XLSX31- SQLite32- Parquet33- WK134-)35-36-// A fileFormat is everything about a Kind: its names, its extensions, and37-// the functions reading and writing it. Adding a format is a file with38-// its importer (and exporter) and a row here.39-type fileFormat struct {40- kind Kind41- name string // short name, e.g. in "Export as CSV"42- label string // what the file is, for the import picker43- exts []string // recognized extensions, the first one written44- // book is set for formats that hold named sheets of their own; a45- // file of any other kind names its one sheet after the file.46- book bool47- read importer48- // write is nil for formats 012 only imports.49- write exporter50-}51-52-// An importer reads a file into a new workbook, returning the sheet to53-// show. It checks ctx between rows, and updates opt.Progress.54-type importer func(ctx context.Context, name string, opt Options) (*Result, error)55-56-// An exporter writes a snapshot to a file.57-type exporter func(ctx context.Context, name string, snap *Snapshot, opt ExportOptions) (*ExportResult, error)58-59-// kinds are the formats, in Kind order, which is menu order.60-var kinds = []fileFormat{61- {CSV, "CSV", "Comma-separated values", []string{".csv"}, false, importCSV, exportCSV},62- {TSV, "TSV", "Tab-separated values", []string{".tsv", ".tab"}, false, importTSV, exportTSV},63- {XLSX, "XLSX", "Excel workbook", []string{".xlsx", ".xlsm"}, true, importXLSX, exportXLSX},64- {SQLite, "SQLite", "SQLite database", []string{".sqlite", ".sqlite3", ".db"}, false, importSQLite, exportSQLite},65- {Parquet, "Parquet", "Parquet file", []string{".parquet"}, false, importParquet, nil},66- {WK1, "WK1", "Lotus 1-2-3 worksheet", []string{".wk1", ".wks"}, false, importWK1, nil},67-}68-69-// format returns the kind's format, or nil for an unknown kind.70-func (k Kind) format() *fileFormat {71- if k < 1 || int(k) > len(kinds) {72- return nil73- }74- return &kinds[k-1]75-}76-77-// Kinds lists every format, in menu order.78-func Kinds() []Kind {79- out := make([]Kind, len(kinds))80- for i, k := range kinds {81- out[i] = k.kind82- }83- return out84-}85-86-// String is the format's short name, e.g. "XLSX".87-func (k Kind) String() string {88- if f := k.format(); f != nil {89- return f.name90- }91- return "unknown"92-}93-94-// Label says what a file of this kind is, e.g. "Excel workbook".95-func (k Kind) Label() string {96- if f := k.format(); f != nil {97- return f.label98- }99- return ""100-}101-102-// Ext is the extension written for this kind, e.g. ".xlsx".103-func (k Kind) Ext() string {104- if f := k.format(); f != nil {105- return f.exts[0]106- }107- return ""108-}109-110-// CanExport reports whether sheets can be written in this format.111-func (k Kind) CanExport() bool {112- f := k.format()113- return f != nil && f.write != nil114-}115-116-// KindOf recognizes a file's format by its extension.117-func KindOf(name string) (Kind, bool) {118- ext := strings.ToLower(filepath.Ext(name))119- for _, k := range kinds {120- for _, e := range k.exts {121- if e == ext {122- return k.kind, true123- }124- }125- }126- return 0, false127-}128-12925 // Progress reports how far an import has got. The importer updates it13026 // from its goroutine; the UI reads it on a timer.13127 type Progress struct {
@@ -222,13 +118,12 @@ func Import(ctx context.Context, name string, opt Options) (*Result, error) {
222118 }223119 224120 func importKind(ctx context.Context, name string, k Kind, opt Options) (*Result, error) {225- f := k.format()226- r, err := f.read(ctx, name, opt)121+ r, err := k.format().read(ctx, name, opt)227122 if err != nil {228123 return nil, err229124 }230125 // A file of one table becomes one sheet named after it, as in Sheets.231- if book := r.Sheet.Book(); !f.book && book.Len() == 1 {126+ if book := r.Sheet.Book(); !k.HoldsSheets() && book.Len() == 1 {232127 base := strings.TrimSuffix(filepath.Base(name), filepath.Ext(name))233128 if opt.Table != "" {234129 base = opt.Table
A internal/fileio/formats.go +151 -0
@@ -0,0 +1,151 @@
1+package fileio2+3+import (4+ "context"5+ "path/filepath"6+ "strings"7+)8+9+// Kind is an external file format.10+type Kind int11+12+const (13+ CSV Kind = iota + 114+ TSV15+ XLSX16+ SQLite17+ Parquet18+ WK119+)20+21+// A fileFormat is everything about a Kind: its names, its extensions,22+// how the UI offers it, and the functions reading and writing it. Import,23+// Export, KindOf and every list of formats the UI shows come from the24+// kinds table, so adding a format is a file with its importer (and25+// exporter) and a row there.26+type fileFormat struct {27+ kind Kind28+ name string // short name, e.g. in "Download as CSV"29+ noun string // what people call it in a sentence, e.g. "Excel"30+ label string // what the file is, for the import picker31+ exts []string // recognized extensions, the first one written32+ read importer33+34+ // Exporting; write is nil for formats 012 only imports.35+ write exporter36+ menu string // the Download menu's title, without the extension37+ about string // what downloading writes, for the command's description38+39+ // book is set for formats that hold named sheets of their own:40+ // every sheet is exported, and an imported file keeps its sheets'41+ // names. A file of any other kind has one sheet, named after it.42+ book bool43+ // tables is set for databases: an import picks a table, and an44+ // export writes the sheet or the selection as a named table.45+ tables bool46+}47+48+// An importer reads a file into a new workbook, returning the sheet to49+// show. It checks ctx between rows and updates opt.Progress.50+type importer func(ctx context.Context, name string, opt Options) (*Result, error)51+52+// An exporter writes a snapshot to a file.53+type exporter func(ctx context.Context, name string, snap *Snapshot, opt ExportOptions) (*ExportResult, error)54+55+// kinds are the formats, in Kind order, which is menu order.56+var kinds = []fileFormat{{57+ kind: CSV, name: "CSV", noun: "CSV", label: "Comma-separated values", exts: []string{".csv"},58+ read: importCSV, write: exportCSV, menu: "Comma-separated values",59+ about: "Save the values as shown, as comma-separated values (.csv)",60+}, {61+ kind: TSV, name: "TSV", noun: "TSV", label: "Tab-separated values", exts: []string{".tsv", ".tab"},62+ read: importTSV, write: exportTSV, menu: "Tab-separated values",63+ about: "Save the values as shown, as tab-separated values (.tsv)",64+}, {65+ kind: XLSX, name: "XLSX", noun: "Excel", label: "Excel workbook", exts: []string{".xlsx", ".xlsm"},66+ read: importXLSX, write: exportXLSX, menu: "Microsoft Excel",67+ about: "Save as an Excel workbook (.xlsx) with formulas, formats and widths",68+ book: true,69+}, {70+ kind: SQLite, name: "SQLite", noun: "SQLite", label: "SQLite database", exts: []string{".sqlite", ".sqlite3", ".db"},71+ read: importSQLite, write: exportSQLite, menu: "SQLite database",72+ about: "Save the sheet, or the selection, as a table in a SQLite database",73+ tables: true,74+}, {75+ kind: Parquet, name: "Parquet", noun: "Parquet", label: "Parquet file", exts: []string{".parquet"},76+ read: importParquet,77+}, {78+ kind: WK1, name: "WK1", noun: "1-2-3", label: "Lotus 1-2-3 worksheet", exts: []string{".wk1", ".wks"},79+ read: importWK1,80+}}81+82+// unknownFormat is what an invalid Kind reports.83+var unknownFormat = fileFormat{name: "unknown", exts: []string{""}}84+85+// format returns the kind's row of the table.86+func (k Kind) format() *fileFormat {87+ if k < 1 || int(k) > len(kinds) {88+ return &unknownFormat89+ }90+ return &kinds[k-1]91+}92+93+// Kinds lists every format, in menu order.94+func Kinds() []Kind {95+ out := make([]Kind, len(kinds))96+ for i, k := range kinds {97+ out[i] = k.kind98+ }99+ return out100+}101+102+// KindOf recognizes a file's format by its extension.103+func KindOf(name string) (Kind, bool) {104+ ext := strings.ToLower(filepath.Ext(name))105+ for _, k := range kinds {106+ for _, e := range k.exts {107+ if e == ext {108+ return k.kind, true109+ }110+ }111+ }112+ return 0, false113+}114+115+// String is the format's short name, e.g. "XLSX".116+func (k Kind) String() string { return k.format().name }117+118+// Noun is what the format is called in a sentence, e.g. "Excel" in119+// "Import a CSV, TSV, Excel ... file".120+func (k Kind) Noun() string { return k.format().noun }121+122+// Label says what a file of this kind is, e.g. "Excel workbook".123+func (k Kind) Label() string { return k.format().label }124+125+// Ext is the extension written for this kind, e.g. ".xlsx".126+func (k Kind) Ext() string { return k.format().exts[0] }127+128+// CanExport reports whether sheets can be written in this format.129+func (k Kind) CanExport() bool { return k.format().write != nil }130+131+// MenuTitle is the format's entry in the Download menu, e.g. "Microsoft132+// Excel (.xlsx)"; empty for formats that can't be exported.133+func (k Kind) MenuTitle() string {134+ if !k.CanExport() {135+ return ""136+ }137+ return k.format().menu + " (" + k.Ext() + ")"138+}139+140+// About says what downloading in this format writes; empty for formats141+// that can't be exported.142+func (k Kind) About() string { return k.format().about }143+144+// HoldsSheets reports whether a file of this kind holds several named145+// sheets, so a download writes the whole workbook (see SnapBook).146+func (k Kind) HoldsSheets() bool { return k.format().book }147+148+// HasTables reports whether a file of this kind is a database of tables:149+// importing picks one (see Tables), and a download writes the sheet or150+// the selection as a named table (ExportOptions.Table).151+func (k Kind) HasTables() bool { return k.format().tables }
A internal/fileio/formats_test.go +37 -0