cat / 012
clone

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

main
012 / internal / ui / nbcommands.go
nbcommands.go 7.2K
1package ui23import (4 "strconv"5 "strings"67 tea "charm.land/bubbletea/v2"89 "src.rest/cat/012/internal/notebook"10 "src.rest/cat/012/internal/ui/picker"11)1213// The commands that change a notebook's cells and how their outputs14// show, and finding a cell: each acts on the cells selected (nbcells.go).1516// registerCellCommands registers the commands that change cells.17func registerCellCommands(onCell, onCode func(*Model) bool) {18 onTab := func(m *Model) bool { return m.nbView() != nil }19 isKind := func(k notebook.Kind) func(m *Model) bool {20 return func(m *Model) bool { c, ok := m.nbCell(); return ok && c.Kind == k }21 }22 register(23 &command{id: "nb.insert_above", macro: macroNever, title: "Add cell above", desc: "Add a code cell above the selected ones",24 enabled: onTab, run: func(m *Model) tea.Cmd { return m.addCell(0, false) }},25 &command{id: "nb.insert_below", macro: macroNever, title: "Add cell below", desc: "Add a code cell below the selected ones",26 enabled: onTab, run: func(m *Model) tea.Cmd { return m.addCell(1, false) }},27 &command{id: "nb.add_code", macro: macroNever, title: "New code cell", desc: "Add a code cell below the selected ones and edit it",28 enabled: onTab, run: func(m *Model) tea.Cmd { return m.addCell(1, true) }},29 &command{id: "nb.delete", macro: macroNever, title: "Delete cells", desc: "Delete the selected cells; z or Undo brings them back",30 enabled: onCell, run: func(m *Model) tea.Cmd { m.deleteCells(); return nil }},31 &command{id: "nb.move_up", macro: macroNever, title: "Move cells up", desc: "Move the selected cells above the cell over them",32 enabled: func(m *Model) bool { from, _ := m.nbRangeIf(); return from > 0 }, run: func(m *Model) tea.Cmd { m.moveCells(-1); return nil }},33 &command{id: "nb.move_down", macro: macroNever, title: "Move cells down", desc: "Move the selected cells below the cell under them",34 enabled: func(m *Model) bool { _, to := m.nbRangeIf(); return to >= 0 && to < len(m.sheet.NotebookCells())-1 },35 run: func(m *Model) tea.Cmd { m.moveCells(1); return nil }},36 &command{id: "nb.to_note", macro: macroNever, title: "Markdown", desc: "Make the selected cells notes, written in Markdown",37 enabled: onCell, checked: isKind(notebook.Note), run: func(m *Model) tea.Cmd { m.setKind(notebook.Note); return nil }},38 &command{id: "nb.to_code", macro: macroNever, title: "Code", desc: "Make the selected cells code, nushell pipelines",39 enabled: onCell, checked: isKind(notebook.Code), run: func(m *Model) tea.Cmd { m.setKind(notebook.Code); return nil }},40 &command{id: "nb.name", macro: macroNever, title: "Name cell", desc: "Name the cell's output, which later cells read as $name and formulas as nu.name",41 enabled: onCode, run: (*Model).openCellName},42 &command{id: "nb.copy", macro: macroNever, title: "Copy cells", desc: "Copy the selected cells, to paste with v or V",43 enabled: onCell, run: func(m *Model) tea.Cmd { m.copyCells(false); return nil }},44 &command{id: "nb.cut", macro: macroNever, title: "Cut cells", desc: "Cut the selected cells, to paste with v or V",45 enabled: onCell, run: func(m *Model) tea.Cmd { m.copyCells(true); return nil }},46 &command{id: "nb.paste", macro: macroNever, title: "Paste cells below", desc: "Paste the cells copied below the selected ones",47 enabled: func(m *Model) bool { return m.nbView() != nil && len(m.nb.clip) > 0 }, run: func(m *Model) tea.Cmd { m.pasteCells(false); return nil }},48 &command{id: "nb.paste_above", macro: macroNever, title: "Paste cells above", desc: "Paste the cells copied above the selected ones",49 enabled: func(m *Model) bool { return m.nbView() != nil && len(m.nb.clip) > 0 }, run: func(m *Model) tea.Cmd { m.pasteCells(true); return nil }},50 &command{id: "nb.goto", macro: macroNever, title: "Go to cell", desc: "Jump to a cell by its number, name, code or heading",51 enabled: onCell, run: func(m *Model) tea.Cmd { m.openCellPicker(false); return nil }},52 &command{id: "nb.toc", macro: macroNever, title: "Table of contents", desc: "Jump to a heading of the notebook's notes",53 enabled: onTab, run: func(m *Model) tea.Cmd { m.openCellPicker(true); return nil }},54 )55}5657// registerOutputCommands registers the commands for outputs.58func registerOutputCommands(onCode func(*Model) bool) {59 register(60 &command{id: "nb.toggle_output", macro: macroNever, title: "Hide output", desc: "Fold the selected cells' outputs to a line, or show them again",61 enabled: onCode, checked: func(m *Model) bool { c, ok := m.nbCell(); return ok && m.nbView().Hidden(c.ID) },62 run: func(m *Model) tea.Cmd { m.nbView().ToggleHidden(); return nil }},63 &command{id: "nb.toggle_whole", macro: macroNever, title: "Show whole output", desc: "Show every row of the selected cells' outputs, or scroll them in a window again",64 enabled: onCode, checked: func(m *Model) bool { c, ok := m.nbCell(); return ok && m.nbView().Whole(c.ID) },65 run: func(m *Model) tea.Cmd { m.nbView().ToggleWhole(); return nil }},66 &command{id: "nb.clear_output", macro: macroNever, title: "Clear output", desc: "Clear the selected cells' outputs",67 enabled: onCode, run: func(m *Model) tea.Cmd { m.clearCellOutputs(); return nil }},68 &command{id: "nb.open_output", macro: macroNever, title: "Open output", desc: "Show the selected cell's output full-screen, a table as a grid to sort, filter and copy from",69 enabled: onCode, run: func(m *Model) tea.Cmd {70 v := m.nbView()71 switch {72 case !v.OpenFull():73 m.note = "The cell has no output to open"74 case v.Full().Grid() != nil:75 m.enterGrid(v.Full().Grid().(*outGrid))76 }77 return nil78 }},79 &command{id: "nb.send", macro: macroNever, title: "Send to sheet", desc: "Put the cell's output on a sheet, kept up to date whenever the cell runs",80 enabled: onCode, run: (*Model).sendCell},81 &command{id: "region.freeze", title: "Freeze output", desc: "Turn the output sent here into plain values you can edit; the cell no longer updates it",82 enabled: onOutputRegion, run: func(m *Model) tea.Cmd { return m.freezeOutput() }},83 &command{id: "region.delete", title: "Remove output", desc: "Remove the output sent here and its values from the sheet",84 enabled: onOutputRegion, run: func(m *Model) tea.Cmd { return m.deleteOutput() }},85 )86}8788// nbRangeIf is the selected cells of the notebook shown, or -1, -1 on a89// sheet.90func (m *Model) nbRangeIf() (from, to int) {91 v := m.nbView()92 if v == nil || len(m.sheet.NotebookCells()) == 0 {93 return -1, -194 }95 return v.Range()96}9798// openCellPicker opens a picker of the notebook's cells to jump to, or99// of its headings alone, a table of contents.100func (m *Model) openCellPicker(toc bool) {101 v := m.nbView()102 var items []picker.Item103 for _, e := range v.Outline(!toc) {104 title := e.Title105 detail := "cell " + strconv.Itoa(e.Cell+1)106 if e.Level > 0 {107 title = strings.Repeat(" ", e.Level-1) + title108 }109 i := e.Cell110 items = append(items, picker.Item{Title: title, Name: len(title), Detail: detail, Desc: "Go to " + detail,111 Pick: func() tea.Cmd { m.closeOverlay(); v.Select(i, false); return nil }})112 }113 title, empty := "Go to cell", "a cell's number, name or code"114 if toc {115 title, empty = "Table of contents", "a heading"116 if len(items) == 0 {117 m.note = "The notes have no headings: # starts one"118 return119 }120 }121 p := m.newPicker(title, empty, 60, items)122 p.Action = "go"123 m.openOverlay(p)124}