1// Package transfer runs imports in the background and draws their2// progress: the mode indicator says WAIT, the context line offers Esc,3// the status line counts rows with a progress bar, and the terminal4// shows its own progress indicator. It remembers where the sheet came5// from, so Save can offer to export it back. What an import's result6// does to the workbook is the UI's; this package needs no host.7package transfer89import (10 "context"11 "fmt"12 "io"13 "path/filepath"14 "strings"15 "time"1617 tea "charm.land/bubbletea/v2"18 "github.com/charmbracelet/x/ansi"1920 "src.rest/cat/012/internal/fileio"21 "src.rest/cat/012/internal/telemetry"22 "src.rest/cat/012/internal/ui/theme"23)2425// Place is where an import goes.26type Place int2728const (29 Book Place = iota // replace the spreadsheet30 NewSheets // insert new sheets31 Sheet // replace the sheet shown32)3334// Transfer is the model's import state.35type Transfer struct {36 job *job37 lastID int38 Source string // the file the sheet was imported from, if any39 Kind fileio.Kind // its format40 Startup string // a file to import as the program starts41}4243// job is an import in progress.44type job struct {45 id int46 name string47 prog *fileio.Progress48 cancel func()49 place Place50}5152// ImportedMsg is an import's result.53type ImportedMsg struct {54 ID int55 Name string56 Opt fileio.Options57 Place Place58 Res *fileio.Result59 Err error60 // Stream is set for a table read from a stream (StartReader), which61 // has no file to save back to.62 Stream bool63}6465// TickMsg refreshes the progress display.66type TickMsg struct{ id int }6768// tick is how often the progress display refreshes.69const tick = 100 * time.Millisecond7071// Busy reports whether an import is running.72func (x *Transfer) Busy() bool { return x.job != nil }7374// Start reads name, at path on disk, in the background, replacing any75// import running. Its span nests under parent.76func (x *Transfer) Start(name, path string, opt fileio.Options, place Place, parent telemetry.Parent) tea.Cmd {77 ctx, cancel := context.WithCancel(telemetry.WithParent(context.Background(), parent))78 prog := fileio.NewProgress()79 id := x.Begin(name, prog, cancel, place)80 opt.Progress = prog81 return tea.Batch(82 func() tea.Msg {83 res, err := fileio.Import(ctx, path, opt)84 return ImportedMsg{ID: id, Name: name, Opt: opt, Place: place, Res: res, Err: err}85 },86 tickCmd(id),87 )88}8990// StartReader reads a table from r in the background, as Start reads a91// file, into a sheet called name: standard input, for 012 -. Its result92// is marked Stream.93func (x *Transfer) StartReader(name string, r io.Reader, opt fileio.Options, parent telemetry.Parent) tea.Cmd {94 ctx, cancel := context.WithCancel(telemetry.WithParent(context.Background(), parent))95 prog := fileio.NewProgress()96 id := x.Begin(name, prog, cancel, Book)97 opt.Progress = prog98 return tea.Batch(99 func() tea.Msg {100 res, err := fileio.ImportReader(ctx, name, r, opt)101 return ImportedMsg{ID: id, Name: name, Opt: opt, Place: Book, Res: res, Err: err, Stream: true}102 },103 tickCmd(id),104 )105}106107// Begin tracks an import of name reporting to prog, cancelled by cancel,108// replacing any import running, and returns its ID for its ImportedMsg.109func (x *Transfer) Begin(name string, prog *fileio.Progress, cancel func(), place Place) int {110 if x.job != nil {111 x.job.cancel()112 }113 x.lastID++114 x.job = &job{id: x.lastID, name: name, prog: prog, cancel: cancel, place: place}115 return x.lastID116}117118func tickCmd(id int) tea.Cmd {119 return tea.Tick(tick, func(time.Time) tea.Msg { return TickMsg{id} })120}121122// Cancel stops the import at once and returns the name of its file; its123// result, if it still arrives, is ignored.124func (x *Transfer) Cancel() string {125 j := x.job126 j.cancel()127 x.job = nil128 return j.name129}130131// Update handles the transfer's own messages: a tick of the running132// import asks for the next one, and the running import's result ends it133// and is returned for the model to use. Stale ticks and results, from an134// import replaced or cancelled, are dropped.135func (x *Transfer) Update(msg tea.Msg) (res ImportedMsg, done bool, cmd tea.Cmd) {136 switch msg := msg.(type) {137 case TickMsg:138 if x.job != nil && x.job.id == msg.id {139 return res, false, tickCmd(msg.id)140 }141 case ImportedMsg:142 if x.job == nil || x.job.id != msg.ID {143 return res, false, nil144 }145 x.job.cancel()146 x.job = nil147 return msg, true, nil148 }149 return res, false, nil150}151152// Line is the context line during an import.153func (x *Transfer) Line(th *theme.Theme) string {154 return "Importing " + filepath.Base(x.job.name) + "… " + th.KeyHints("Esc", "cancel")155}156157// Status is the status line during an import, width wide: the file, the158// rows read, and a bar when the total is known.159func (x *Transfer) Status(th *theme.Theme, width int) (left, right string) {160 rows, frac := x.job.prog.Get()161 left = th.Key.Render("Importing " + filepath.Base(x.job.name))162 right = Rows(rows) + " read"163 if frac < 0 {164 return left, right165 }166 pct := fmt.Sprintf(" %3d%%", int(frac*100))167 w := max(0, min(width-ansi.StringWidth(left)-ansi.StringWidth(right)-len(pct)-6, 30))168 if w < 8 {169 return left, right + pct170 }171 done := int(frac * float64(w))172 return left, right + " " + th.Progress.Render(strings.Repeat("━", done)) +173 th.ProgressTodo.Render(strings.Repeat("─", w-done)) + pct174}175176// ProgressBar is the terminal's own progress indicator (OSC 9;4), shown177// in the tab or title bar where supported.178func (x *Transfer) ProgressBar() *tea.ProgressBar {179 if x.job == nil {180 return nil181 }182 if _, frac := x.job.prog.Get(); frac >= 0 {183 return tea.NewProgressBar(tea.ProgressBarDefault, int(frac*100))184 }185 return tea.NewProgressBar(tea.ProgressBarIndeterminate, 0)186}187188// Rows is n rows in words: "1 row", "4,096 rows".189func Rows(n int) string {190 if n == 1 {191 return "1 row"192 }193 return Thousands(n) + " rows"194}195196// Thousands writes n with commas between thousands.197func Thousands(n int) string {198 s := fmt.Sprint(n)199 for i := len(s) - 3; i > 0; i -= 3 {200 s = s[:i] + "," + s[i:]201 }202 return s203}