1package mcp23import (4 "context"5 "fmt"6 "path/filepath"7 "strings"8 "sync"910 "src.rest/cat/012/internal/diff"11 "src.rest/cat/012/internal/fileio"12 "src.rest/cat/012/internal/headless"13 "src.rest/cat/012/internal/sheet"14)1516// Backend is where the tools' workbook lives. The tools only read it,17// work on a copy thrown away, or change it through the workbook's one18// mutation path (Batch), so the file backend here and a live session's19// (a workbook open on the screen, changed as a participant's operations)20// can stand behind the same tools.21type Backend interface {22 // Name is what the workbook is called, for titles: its file's name.23 Name() string24 // View runs fn on the workbook as it is, which fn must not change.25 View(ctx context.Context, fn func(*sheet.Workbook) error) error26 // Scratch runs fn on a copy of the workbook that is thrown away.27 Scratch(ctx context.Context, fn func(*sheet.Workbook) error) error28 // Change runs fn on the workbook inside one Batch, labelled label,29 // and returns what it changed. The change is kept unless fn fails or30 // dryRun is set; a failed change leaves the workbook as it was.31 Change(ctx context.Context, label string, dryRun bool, fn func(*sheet.Workbook) error) ([]diff.Change, error)32}3334// FileBackend is a workbook file, opened afresh for every call so edits35// made meanwhile by others (the screen, git, 012 set) are seen, and36// saved atomically after each change, as 012 set saves it.37type FileBackend struct {38 Path string39 // Prepare, when set, runs on the workbook once opened, before fn:40 // what the server's flags allow (answering JEV functions).41 Prepare func(context.Context, *headless.File) error42 // Save writes a changed workbook; nil is File.Save.43 Save func(*headless.File) error4445 mu sync.Mutex // one call at a time, so changes don't overlap46}4748// Name is the file's name without its folder.49func (b *FileBackend) Name() string {50 return strings.TrimSuffix(filepath.Base(b.Path), filepath.Ext(b.Path))51}5253// open opens the file, a missing one as an empty workbook saved on the54// first change, as 012 set creates it.55func (b *FileBackend) open(ctx context.Context) (*headless.File, error) {56 f, err := headless.Open(b.Path, true)57 if err != nil {58 return nil, err59 }60 if b.Prepare != nil {61 if err := b.Prepare(ctx, f); err != nil {62 return nil, err63 }64 }65 return f, nil66}6768func (b *FileBackend) View(ctx context.Context, fn func(*sheet.Workbook) error) error {69 b.mu.Lock()70 defer b.mu.Unlock()71 f, err := b.open(ctx)72 if err != nil {73 return err74 }75 return fn(f.Book)76}7778// Scratch is View: every call opens its own copy of the file.79func (b *FileBackend) Scratch(ctx context.Context, fn func(*sheet.Workbook) error) error {80 return b.View(ctx, fn)81}8283func (b *FileBackend) Change(ctx context.Context, label string, dryRun bool, fn func(*sheet.Workbook) error) ([]diff.Change, error) {84 b.mu.Lock()85 defer b.mu.Unlock()86 f, err := b.open(ctx)87 if err != nil {88 return nil, err89 }90 w := f.Book91 if err := w.Batch(sheet.Change{Label: label, Sheet: w.Sheet(w.Active())}, func() error { return fn(w) }); err != nil {92 return nil, err93 }94 changes, err := f.Changes()95 if err != nil || dryRun || len(changes) == 0 {96 return changes, err97 }98 if b.Save != nil {99 return changes, b.Save(f)100 }101 return changes, f.Save()102}103104// ImportBackend is a file in a format 012 imports (CSV, XLSX, SQLite105// and the rest), read afresh for every call as the app opens it. It is106// read only: 012 writes a workbook as a .012 file, which create_workbook107// makes from it.108type ImportBackend struct {109 Path string110 Prepare func(context.Context, *headless.File) error111}112113// Name is the file's name without its folder or extension.114func (b *ImportBackend) Name() string {115 return strings.TrimSuffix(filepath.Base(b.Path), filepath.Ext(b.Path))116}117118func (b *ImportBackend) View(ctx context.Context, fn func(*sheet.Workbook) error) error {119 w, err := importBook(ctx, b.Path)120 if err != nil {121 return err122 }123 if b.Prepare != nil {124 if err := b.Prepare(ctx, &headless.File{Path: b.Path, Book: w}); err != nil {125 return err126 }127 }128 return fn(w)129}130131// Scratch is View: every call imports its own copy.132func (b *ImportBackend) Scratch(ctx context.Context, fn func(*sheet.Workbook) error) error {133 return b.View(ctx, fn)134}135136func (b *ImportBackend) Change(context.Context, string, bool, func(*sheet.Workbook) error) ([]diff.Change, error) {137 return nil, fmt.Errorf("%s is read only: 012 saves workbooks as %s files; create_workbook with from set to it makes one to change", filepath.Base(b.Path), sheet.FileExt)138}139140// importBook reads a file 012 imports as a workbook.141func importBook(ctx context.Context, path string) (*sheet.Workbook, error) {142 res, err := fileio.Import(ctx, path, fileio.Options{})143 if err != nil {144 return nil, fmt.Errorf("%s: %w", filepath.Base(path), err)145 }146 return res.Sheet.Book(), nil147}