1package nbview23import (4 "strings"56 "github.com/charmbracelet/x/ansi"78 "src.rest/cat/012/internal/notebook"9)1011// The toolbar over the cells, as Jupyter's: run, stop, restart, run12// all; add, cut, copy, paste; the active cell's kind; and at the right13// how running stands, as Jupyter's kernel status. Each button runs its14// command when clicked and shows its key as a chip where there's room:15// the chips go first, from the right, then the words, the icons stay.1617// Back is what ToolbarAt says of the button that leaves a full-screen18// output, which has no command.19const Back = "back"2021const backButton = "◀ Back"2223// button is a toolbar button.24type button struct {25 icon, label, cmd string26 on func(v *View, k Kernel) bool27}2829func always(*View, Kernel) bool { return true }30func hasCell(v *View, _ Kernel) bool { return len(v.h.Cells()) > 0 }31func busy(_ *View, k Kernel) bool { return k.Busy || k.Waiting > 0 }32func canPaste(_ *View, k Kernel) bool { return k.Clip > 0 }33func canRun(v *View, k Kernel) bool { return hasCell(v, k) && k.Off == "" }34func separator(*View, Kernel) bool { return false }35func (b button) sep() bool { return b.cmd == "" }36func (b button) text(level int, chip string) string {37 s := b.icon38 if level > 0 && b.label != "" {39 s += " " + b.label40 }41 if level > 1 && chip != "" {42 s += " " + chip43 }44 return s45}4647// buttons are the toolbar's, a blank cmd between groups; the kind's48// label is the active cell's.49func (v *View) buttons() []button {50 kind := "Code ▾"51 if c, ok := v.Cell(); ok && c.Kind == notebook.Note {52 kind = "Markdown ▾"53 }54 return []button{55 {"▶", "Run", "nb.run_next", canRun}, {"■", "Stop", "nb.stop", busy}, {"↻", "Restart", "nb.restart", hasCell},56 {"▶▶", "Run all", "nb.run_all", canRun}, {"│", "", "", separator},57 {"+", "Add", "nb.insert_below", always}, {"✂", "Cut", "nb.cut", hasCell}, {"⧉", "Copy", "nb.copy", hasCell},58 {"⎘", "Paste", "nb.paste", canPaste}, {"│", "", "", separator},59 {kind, "", "nb.kind", hasCell},60 }61}6263// toolbarGap is the space between buttons.64const toolbarGap = 26566// layout fits the buttons in width, beside the status: how much of each67// shows (2 with its key, 1 with its word, 0 its icon alone).68func (v *View) layout(bs []button, width int) []int {69 levels := make([]int, len(bs))70 chips := make([]string, len(bs))71 for i, b := range bs {72 levels[i] = 273 chips[i] = keyFor(v.Keys, b.cmd)74 }75 fits := func() bool {76 w := 177 for i, b := range bs {78 w += ansi.StringWidth(b.text(levels[i], chips[i])) + 2*boolInt(levels[i] > 1 && chips[i] != "") + toolbarGap79 }80 return w <= width81 }82 for level := 2; level > 0 && !fits(); level-- {83 for i := len(bs) - 1; i >= 0 && !fits(); i-- {84 if levels[i] == level {85 levels[i] = level - 186 }87 }88 }89 return levels90}9192// Toolbar draws the toolbar at width: the full-screen output's title93// while one is open.94func (v *View) Toolbar(width int) string {95 th := v.h.Theme()96 if v.full != nil {97 return " " + backButton + " " + th.Chip("Esc") + " " + th.Title.Render(v.full.Title) + th.Muted.Render(", full-screen")98 }99 k := v.h.Kernel()100 status := v.status(k)101 bs := v.buttons()102 levels := v.layout(bs, width-ansi.StringWidth(status)-2)103 var b strings.Builder104 b.WriteString(" ")105 for i, bt := range bs {106 if i > 0 {107 b.WriteString(strings.Repeat(" ", toolbarGap))108 }109 b.WriteString(v.drawButton(bt, levels[i], k))110 }111 left := b.String()112 gap := width - ansi.StringWidth(left) - ansi.StringWidth(status) - 1113 if gap < 1 {114 return left115 }116 return left + strings.Repeat(" ", gap) + status117}118119// drawButton draws a button as much as level says: a disabled one120// dimmed, a separator as a rule.121func (v *View) drawButton(bt button, level int, k Kernel) string {122 th := v.h.Theme()123 if bt.sep() {124 return th.Border.Render(bt.icon)125 }126 chip := keyFor(v.Keys, bt.cmd)127 if !bt.on(v, k) {128 return th.Disabled.Render(bt.text(min(level, 1), ""))129 }130 s := bt.text(min(level, 1), "")131 if level > 1 && chip != "" {132 s += " " + th.Chip(chip)133 }134 return s135}136137// status is how running stands: nu ○ idle, nu ● busy, nu ⊘ off, after138// reactive when the notebook is.139func (v *View) status(k Kernel) string {140 th := v.h.Theme()141 var s string142 switch {143 case k.Off != "":144 s = th.Warning.Render("nu ⊘ off")145 case k.Live > 0 && (k.Busy || k.Waiting > 0):146 s = th.Hint.Render("nu ● busy, " + itoa(k.Live) + " live")147 case k.Live > 0:148 s = th.Hint.Render("nu ● " + itoa(k.Live) + " live")149 case k.Busy && k.Waiting > 0:150 s = th.Hint.Render("nu ● busy, " + itoa(k.Waiting) + " waiting")151 case k.Busy || k.Waiting > 0:152 s = th.Hint.Render("nu ● busy")153 default:154 s = th.Muted.Render("nu ○ idle")155 }156 if k.Reactive {157 s = th.Muted.Render("reactive") + " " + s158 }159 return s160}161162// ToolbarAt is the command of the button at column x of the toolbar,163// or "".164func (v *View) ToolbarAt(x, width int) string {165 if v.full != nil {166 if x >= 1 && x <= ansi.StringWidth(backButton) {167 return Back168 }169 return ""170 }171 k := v.h.Kernel()172 bs := v.buttons()173 levels := v.layout(bs, width-ansi.StringWidth(v.status(k))-2)174 at := 1175 for i, bt := range bs {176 if i > 0 {177 at += toolbarGap178 }179 w := ansi.StringWidth(v.drawButton(bt, levels[i], k))180 if x >= at && x < at+w && !bt.sep() && bt.on(v, k) {181 return bt.cmd182 }183 at += w184 }185 return ""186}