cat / 012
clone

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

main
012 / internal / sheet / linked.go
linked.go 7.9K
1package sheet23import (4 "errors"5 "path/filepath"6 "strconv"7 "strings"8 "time"9)1011// Linked files: regions (region.go) whose rows come from a file the UI12// follows as it grows or is rewritten. Its anchor is its table's first13// cell, the file's first row, which stays; the rows under it are the14// file's, all of them up to max-cells, or the last Window of them. Its rows arrive only as live operations15// (live.go) and are never part of the undo state (see region.go for16// why), so a linked file is read again whenever its cells may no longer17// be the file's: undo put it back, it moved, or a cell blocking it was18// cleared.1920// LinkSource is what a linked file reads, as the file keeps it.21type LinkSource struct {22 // Path is the file, as the UI resolves it: relative to the23 // workbook's folder when it can be (see RebaseLinks).24 Path string25 // Format is the name of the file's format ("CSV", "NUON"), or "" to26 // tell it by the extension.27 Format string28 // Table is a SQLite table to read, or Query a query to run.29 Table, Query string30 // Window keeps the last Window rows under the header, dropping older31 // ones, as tail -f does; 0 keeps every row, up to max-cells.32 Window int33 // Paged links the file as a source (source.go): read in place, a34 // window of rows at a time, on a tab of its own, rather than into35 // cells. Order is how its tab sorts and filters the rows, nil for36 // the source's own order; it is replaced whole, never changed.37 Paged bool38 Order *SourceOrder39}4041// liveMeta is what the rows arriving have made of a linked file, kept42// beside its region and never undone.43type liveMeta struct {44 data, dropped int // rows arrived since the last reset, rows the window dropped45 updated time.Time46 err, note string47 paused bool48 stale bool // to be read again, whole49 reread bool // to be emptied and read again once the change ends50 fitted bool // its columns were widened to its text once51}5253// LinkedRegion is a linked file as the UI shows it: where it is, what54// it reads, and how the reading goes.55type LinkedRegion struct {56 // Name names the region: in LiveOps, in formulas (nu.name) and to57 // commands ($name).58 Name string59 Sheet *Sheet60 Anchor Addr61 // Area is the cells it covers, the anchor alone while it has none.62 Area Rect63 Source LinkSource64 // Rows counts the data rows it shows, under the header; Dropped the65 // older rows the window let go.66 Rows, Dropped int67 // Updated is when the file was last read, zero until it is.68 Updated time.Time69 // Err says why the file can't be read, or its rows can't be shown,70 // "" when all is well; Note says what was left out.71 Err, Note string72 // Paused is set while the region isn't following its file.73 Paused bool74 // Stale is set when its cells may no longer be the file's: the file75 // is to be read again, whole.76 Stale bool77}7879// ErrLinkedEdit is what typing into a linked file's cell says.80var ErrLinkedEdit = errors.New("That cell shows part of a linked file: unlink it (Data > Linked file > Unlink) to edit it")8182// linkedInfo describes the linked file r on s.83func (s *Sheet) linkedInfo(r Region) LinkedRegion {84 me := s.meta(nameKey(r.Name))85 err := me.err86 if me.why != "" {87 err = me.why88 }89 return LinkedRegion{Name: r.Name, Sheet: s, Anchor: r.At, Area: s.covered(r), Source: r.File,90 Rows: max(me.rows-1, 0), Dropped: me.dropped, Updated: me.updated, Err: err, Note: me.note,91 Paused: me.paused, Stale: me.stale || me.reread}92}9394// LinkedAt returns the linked file covering the cell at a.95func (s *Sheet) LinkedAt(a Addr) (LinkedRegion, bool) {96 if r, ok := s.RegionAt(a); ok && r.Linked() {97 return s.linkedInfo(r), true98 }99 return LinkedRegion{}, false100}101102// HasLinked reports whether the sheet holds a linked file.103func (s *Sheet) HasLinked() bool {104 for _, r := range s.regions.list {105 if r.Linked() {106 return true107 }108 }109 return false110}111112// LinkedRegions returns the sheet's linked files, in the order they113// were made.114func (s *Sheet) LinkedRegions() []LinkedRegion {115 var out []LinkedRegion116 for _, r := range s.regions.list {117 if r.Linked() {118 out = append(out, s.linkedInfo(r))119 }120 }121 return out122}123124// LinkedRegions returns every live sheet's linked files, sheet by sheet.125func (w *Workbook) LinkedRegions() []LinkedRegion {126 var out []LinkedRegion127 for _, s := range w.sheets {128 out = append(out, s.LinkedRegions()...)129 }130 return out131}132133// LinkedRegion returns the linked file name names.134func (w *Workbook) LinkedRegion(name string) (LinkedRegion, bool) {135 if s, r, ok := w.Region(name); ok && r.Linked() {136 return s.linkedInfo(r), true137 }138 return LinkedRegion{}, false139}140141// errLinkOver is why a linked file can't start where it's asked.142var errLinkOver = errors.New("A linked file needs an empty cell to start in")143144// AddLinked makes a linked file at a reading src, named after the file,145// as one undo step, and returns its name. It starts empty and stale: the146// UI reads the file and sends its rows.147func (s *Sheet) AddLinked(a Addr, src LinkSource) (string, error) {148 name := s.wb.linkName(src.Path)149 if err := s.AddRegion(Region{Name: name, At: a, File: src}); err != nil {150 return "", err151 }152 return name, nil153}154155// linkName is a free region name for the file at path: its name without156// the extension, made a nushell variable's name (app_log), numbered when157// taken (app_log_2).158func (w *Workbook) linkName(path string) string {159 base := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))160 var b strings.Builder161 for i := range len(base) {162 if c := base[i]; isLetter(c) || isDigit(c) || c == '_' {163 b.WriteByte(c)164 } else if b.Len() > 0 && !strings.HasSuffix(b.String(), "_") {165 b.WriteByte('_')166 }167 }168 name := strings.Trim(b.String(), "_")169 if len(name) > 48 {170 name = name[:48]171 }172 if ValidRegionName(name) != nil {173 name = "file_" + name174 }175 for n := 2; w.checkRegionName(name, false) != nil; n++ {176 name = strings.TrimSuffix(name, "_"+strconv.Itoa(n-1)) + "_" + strconv.Itoa(n)177 }178 return name179}180181// SetLinkSource changes what a linked file reads (its window, say), as182// one undo step; it is read again.183func (w *Workbook) SetLinkSource(name string, src LinkSource) error {184 s, r, ok := w.Region(name)185 if !ok || !r.Linked() {186 return ErrNoRegion187 }188 k := nameKey(r.Name)189 s.meta(k).reread = true190 s.change("change link "+r.Name, s.covered(r), func() {191 st := s.regionsCopy()192 st.list[s.regionIndex(k)].File = src193 s.putRegions(st)194 })195 return nil196}197198// Unlink turns a linked file into the values it shows, as ordinary199// cells, as one undo step: FreezeRegion.200func (w *Workbook) Unlink(name string) error {201 s, r, ok := w.Region(name)202 if !ok || !r.Linked() {203 return ErrNoRegion204 }205 return s.FreezeRegion(r.Name)206}207208// PauseLinked stops a linked file following its file, or has it follow209// again. It isn't an edit: what follows the file reads it.210func (w *Workbook) PauseLinked(name string, paused bool) {211 if s, r, ok := w.Region(name); ok && r.Linked() {212 s.meta(nameKey(r.Name)).paused = paused213 }214}215216// ReloadLinked marks a linked file stale, so the UI reads it again,217// whole.218func (w *Workbook) ReloadLinked(name string) {219 if s, r, ok := w.Region(name); ok && r.Linked() {220 s.meta(nameKey(r.Name)).stale = true221 }222}223224// RebaseLinks rewrites the path of every linked file with fn, as the UI225// does when the workbook is saved in another folder, in the undo226// history too, so undo puts back regions reading the same files. It227// isn't an edit: the regions read the same files.228func (w *Workbook) RebaseLinks(fn func(string) string) {229 seen := map[*Region]bool{}230 rebase := func(list []Region) {231 if len(list) == 0 || seen[&list[0]] {232 return // lists share their arrays with the steps that kept them233 }234 seen[&list[0]] = true235 for i, r := range list {236 if r.File.Path != "" {237 list[i].File.Path = fn(r.File.Path)238 }239 }240 }241 for _, s := range w.sheets {242 rebase(s.regions.list)243 }244 for _, st := range append(w.hist.undo, w.hist.redo...) {245 for _, rs := range st.regions {246 rebase(rs.list)247 }248 }249}