1package ui23import (4 "strings"56 tea "charm.land/bubbletea/v2"78 "src.rest/cat/012/internal/notebook"9 "src.rest/cat/012/internal/sheet"10 "src.rest/cat/012/internal/ui/nbview"11 "src.rest/cat/012/internal/ui/overlay"12)1314// A notebook tab on the screen, as Jupyter's: the menu bar with15// NOTEBOOK or EDIT as the mode, the notebook's toolbar where a sheet's16// formula bar is, the context line (what the selection is and the keys17// that apply, or prompts and messages) drawn as a rule closing off the18// toolbar, and the cells under it where the grid would be.1920// nbBody is the screen row the notebook's cells start at, under the21// context line.22const nbBody = contextLine + 12324// openNotebook shows the workbook's notebook, making one after the sheet25// shown if it has none.26func (m *Model) openNotebook() tea.Cmd {27 w := m.book()28 s := w.Notebook()29 if s == nil {30 var err error31 if s, err = w.AddNotebook("", m.sheet); err != nil {32 m.fail(err.Error())33 return nil34 }35 m.note = "Added " + s.Name() + ": b adds a code cell, Enter edits it, Shift+Enter runs it"36 }37 m.showSheet(s)38 return m.loadWords()39}4041// OpenNotebook starts on the workbook's notebook, as 012 nu does: a new42// file's empty sheet becomes the notebook, with a code cell ready to43// type in; a workbook without one gets one.44func (m *Model) OpenNotebook() {45 w := m.book()46 s := w.Notebook()47 if s == nil {48 if first := w.Sheet(0); w.Len() == 1 && first.Len() == 0 && !w.CanUndo() && w.RenameSheet(first, "Notebook") == nil {49 first.MakeNotebook()50 s = first51 } else if s, _ = w.AddNotebook("", m.sheet); s == nil {52 return53 }54 }55 if len(s.NotebookCells()) == 0 {56 s.SetNotebookCells("add cell", []notebook.Cell{{}})57 w.ClearHistory()58 m.saved = w.StateID()59 }60 m.showSheet(s)61 m.nb.startup = true62}6364// startNotebook edits the new notebook's first cell as the program65// starts, when 012 nu asked.66func (m *Model) startNotebook() tea.Cmd {67 if !m.nb.startup {68 return nil69 }70 m.nb.startup = false71 v := m.nbView()72 if v == nil {73 return nil74 }75 cmd := m.loadWords()76 if c, ok := v.Cell(); ok && c.Source == "" && len(m.sheet.NotebookCells()) == 1 {77 return tea.Batch(cmd, v.StartEdit())78 }79 return cmd80}8182// notebookKey gives a key to the notebook shown, reporting whether it83// took it.84func (m *Model) notebookKey(k tea.KeyPressMsg) (tea.Cmd, bool) {85 v := m.nbView()86 if v == nil {87 return nil, false88 }89 m.sizeNotebook(v)90 cmd, ok := v.Key(k)91 if !ok && v.Editing() {92 v.Commit() // a key of the UI's, such as Ctrl+S, sees what's typed93 }94 return cmd, ok95}9697// sizeNotebook tells the view how much room it has.98func (m *Model) sizeNotebook(v *nbview.View) {99 v.Resize(m.width, m.height-nbBody-1)100}101102// notebookLines are the screen's lines on a notebook tab, from the103// menu bar down to the line above the status line.104func (m *Model) notebookLines(v *nbview.View) []string {105 m.sizeNotebook(v)106 lines := []string{m.menuBarLine(), m.formulaBar(), m.contextLineText()}107 return append(lines, v.Lines()...)108}109110// notebookToolbar is the toolbar, where a sheet's formula bar is.111func (m *Model) notebookToolbar(v *nbview.View) string {112 return v.Toolbar(m.width)113}114115// nbKernel is how running stands in notebook s, for its toolbar.116func (m *Model) nbKernel(s *sheet.Sheet) nbview.Kernel {117 k := nbview.Kernel{Off: m.shellOff(), Reactive: s.Reactive(), Clip: len(m.nb.clip)}118 if r := m.nb.runs.running; r != nil && r.s == s {119 k.Busy = true120 }121 for _, q := range m.nb.runs.queue {122 if q.s == s {123 k.Waiting++124 }125 }126 for _, st := range m.nb.runs.streams {127 if st.s == s {128 k.Live++129 }130 }131 return k132}133134// pasteMsg takes pasted text: into a notebook's cell being edited, and135// nowhere else on a notebook tab, or as on a sheet.136func (m *Model) pasteMsg(text string) tea.Cmd {137 if v := m.nbView(); v != nil && m.mode == modeReady && m.overlay == nil {138 return v.Paste(text)139 }140 m.handlePaste(text)141 return nil142}143144// notebookContext is the context line on a notebook tab: what the last145// action said, or the notebook's own line.146func (m *Model) notebookContext(v *nbview.View) (string, string) {147 left, right := v.ContextLine()148 switch {149 case m.warn != "":150 left, right = m.th.Warning.Render(m.warn), ""151 case m.note != "":152 left, right = m.th.Hint.Render(m.note), ""153 default:154 if g := m.gridIn(); g != nil {155 return m.gridContext(g, v), ""156 }157 }158 if v.FullOpen() {159 return left, right160 }161 return v.Rule(left, right, m.width), ""162}163164// notebookIndicator is the mode on a notebook tab.165func (m *Model) notebookIndicator(v *nbview.View) string {166 switch g := m.gridIn(); {167 case g != nil:168 return m.gridIndicator(g)169 case v.FullOpen():170 return "OUTPUT"171 case v.Editing():172 return "EDIT"173 }174 return "NOTEBOOK"175}176177// notebookCursor is where the terminal's caret goes on a notebook tab.178func (m *Model) notebookCursor(v *nbview.View) (int, int, bool) {179 if v.FullOpen() {180 return 0, 0, false181 }182 x, y, ok := v.Cursor()183 return x, nbBody + y, ok184}185186// notebookBoxes are the notebook's floating boxes: its completions.187func (m *Model) notebookBoxes() []overlay.Box {188 if v := m.nbView(); v != nil && m.overlay == nil && m.mode == modeReady {189 if m.gridIn() != nil {190 return m.gridBoxes()191 }192 return v.Boxes(nbBody)193 }194 return nil195}196197// notebookMouse takes a click on the toolbar, or a click or the wheel198// over the cells.199func (m *Model) notebookMouse(msg tea.MouseMsg) (tea.Cmd, bool) {200 v := m.nbView()201 mouse := msg.Mouse()202 if v == nil || m.mode != modeReady || m.overlay != nil {203 return nil, false204 }205 m.sizeNotebook(v)206 if cmd, ok := m.gridMouse(v, msg); ok {207 return cmd, true208 }209 if click, ok := msg.(tea.MouseClickMsg); ok && mouse.Y == formulaLine && click.Button == tea.MouseLeft {210 return m.toolbarClick(v, mouse.X), true211 }212 if mouse.Y < nbBody || mouse.Y >= m.height-1 {213 return nil, false214 }215 m.sizeNotebook(v)216 y := mouse.Y - nbBody217 switch msg := msg.(type) {218 case tea.MouseClickMsg:219 switch msg.Button {220 case tea.MouseLeft:221 return v.Click(mouse.X, y, mouse.Mod.Contains(tea.ModShift)), true222 case tea.MouseRight:223 v.RightClick(y)224 m.showContextMenu(nbCellMenu, mouse.X, mouse.Y+1)225 }226 return nil, true227 case tea.MouseWheelMsg:228 m.nb.follow = false // the user looks elsewhere while cells run229 switch msg.Button {230 case tea.MouseWheelUp:231 v.Wheel(y, -3)232 case tea.MouseWheelDown:233 v.Wheel(y, 3)234 }235 return nil, true236 }237 return nil, true238}239240// toolbarClick runs the toolbar's button at column x: the cell's kind241// opens a menu of the kinds under it.242func (m *Model) toolbarClick(v *nbview.View, x int) tea.Cmd {243 switch id := v.ToolbarAt(x, m.width); id {244 case "":245 return nil246 case nbview.Back:247 v.CloseFull()248 return nil249 case "nb.kind":250 m.showContextMenu([]menuItem{{cmd: "nb.to_code"}, {cmd: "nb.to_note"}}, x, formulaLine+1)251 return nil252 default:253 return m.runCommand(id)254 }255}256257// notebookMsg takes the notebooks' own messages.258func (m *Model) notebookMsg(msg tea.Msg) (tea.Cmd, bool) {259 switch msg := msg.(type) {260 case nbDoneMsg:261 return m.finishCell(msg), true262 case nbWordsMsg:263 m.nb.words = msg.words264 return nil, true265 case nbTickMsg:266 return m.ticked(msg), true267 case nbStreamMsg:268 return m.streamPolled(msg), true269 case nbStreamTickMsg:270 return m.streamTicked(), true271 case nbview.HelpMsg:272 return m.showWordHelp(msg), true273 }274 for _, v := range m.nb.views {275 if cmd, ok := v.Update(msg); ok {276 return cmd, true277 }278 }279 return nil, false280}281282// notebookSync is what follows any message: outputs sent again where283// they're stale, the language told what the notebook binds, and the284// highlighter asked about what was drawn.285func (m *Model) notebookSync() tea.Cmd {286 m.syncOutputs()287 if m.book().OutputsChanged() != m.nb.saved {288 m.changed = true289 }290 if v := m.nbView(); v != nil {291 m.syncLang(m.sheet, v)292 return v.Fetch()293 }294 return nil295}296297// bookOpened notes what a workbook opened with: whether it has cells of298// its own, which may come from another computer, and what converting299// it said.300func (m *Model) bookOpened() {301 w := m.book()302 for _, s := range w.Sheets() {303 m.nb.fromFile = m.nb.fromFile || len(s.NotebookCells()) > 0304 }305 if notes := w.LoadNotes(); len(notes) > 0 {306 m.note = strings.Join(notes, "; ")307 }308}309310// outputCaps are the caps on the outputs a file keeps, from the config.311func (m *Model) outputCaps() notebook.Caps {312 c := notebook.DefaultCaps313 if cfg := m.prefs.Config; cfg != nil {314 c = notebook.Caps{Cell: cfg.Int("nu-save-cell-kb") << 10, Total: cfg.Int("nu-save-notebook-kb") << 10}315 }316 return c317}318319// leftGrid notes the sheet left, when it's a grid, for $selection.320func (m *Model) leftGrid() {321 if !m.sheet.IsNotebook() {322 m.nb.grid, m.nb.sel = m.sheet, m.selection()323 }324}325326// notebookSafe reports whether a command applies on a notebook tab:327// those about files, sheets, settings, help and the notebook itself,328// not those acting on a grid's cells.329func notebookSafe(id string) bool {330 for _, p := range []string{"file.", "sheet.", "nb.", "settings.", "help", "palette", "menu", "quit", "vim.command", "macro.manage"} {331 if strings.HasPrefix(id, p) {332 return id != "sheet.duplicate"333 }334 }335 return id == "edit.undo" || id == "edit.redo"336}