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 / nbview / keys.go
keys.go 8.3K
1package nbview23import (4 "strconv"5 "strings"67 tea "charm.land/bubbletea/v2"89 "src.rest/cat/012/internal/notebook"10)1112// Keys. Command mode moves between cells itself (Up and Down, j and k,13// Home and End, PgUp and PgDn) and runs the commands Keys binds for the14// rest; a key it doesn't know is the UI's (menus, other sheets, Save).15// Edit mode types into the cell, with EditKeys' commands (Esc, running)16// and Tab completing.1718// Editing reports whether a cell is being edited.19func (v *View) Editing() bool { return v.edit.on }2021// FullOpen reports whether an output is shown full-screen.22func (v *View) FullOpen() bool { return v.full != nil }2324// Pending is the first key of a pair waiting for its second: "d" or25// "0".26func (v *View) Pending() string { return v.pending }2728// Key takes a key, reporting whether the notebook took it.29func (v *View) Key(k tea.KeyPressMsg) (tea.Cmd, bool) {30 switch {31 case v.full != nil:32 if v.full.Key(k) {33 v.full = nil34 }35 return nil, true36 case v.edit.on:37 return v.editKey(k)38 }39 return v.commandKey(k)40}4142func (v *View) commandKey(k tea.KeyPressMsg) (tea.Cmd, bool) {43 key := k.String()44 if p := v.pending; p != "" {45 v.pending = ""46 if id, ok := v.Keys[p+" "+key]; ok {47 return v.h.Run(id), true48 }49 }50 if key == "esc" {51 return nil, true // command mode is the notebook's outermost level52 }53 if v.move(key) {54 return nil, true55 }56 for pair := range v.Keys {57 if first, _, ok := strings.Cut(pair, " "); ok && first == key {58 v.pending = key59 return nil, true60 }61 }62 if id, ok := v.Keys[key]; ok {63 return v.h.Run(id), true64 }65 return nil, false66}6768// move moves the selection for a movement key: Up and Down (j and k)69// scroll a selected output's window before they leave it, and with70// Shift (or J and K) select the cells passed over, as JupyterLab does.71func (v *View) move(key string) bool {72 n := len(v.h.Cells())73 switch key {74 case "up", "k":75 if !v.scrollOut(-1) {76 v.step(-1)77 }78 case "down", "j":79 if !v.scrollOut(1) {80 v.step(1)81 }82 case "shift+up", "K":83 v.extend(-1)84 case "shift+down", "J":85 v.extend(1)86 case "home", "ctrl+home":87 v.Select(0, false)88 case "end", "ctrl+end":89 v.Select(n-1, false)90 case "pgup":91 v.top = max(v.top-v.height+1, 0)92 v.selectAtTop()93 case "pgdown":94 v.top += v.height - 195 v.selectAtTop()96 default:97 return false98 }99 return true100}101102// step moves the selection one place: from a cell to its output, if it103// shows one, and from an output to the next cell. It selects one cell.104func (v *View) step(d int) {105 cells := v.h.Cells()106 if len(cells) == 0 {107 return108 }109 v.anchor = -1110 hasOut := func(i int) bool { return v.outHeight(cells[i]) > 0 }111 switch {112 case d > 0 && !v.onOut && hasOut(v.sel):113 v.onOut = true114 case d > 0 && v.sel < len(cells)-1:115 v.sel, v.onOut = v.sel+1, false116 case d < 0 && v.onOut:117 v.onOut = false118 case d < 0 && v.sel > 0:119 v.sel--120 v.onOut = hasOut(v.sel)121 }122 v.follow()123}124125// extend moves the active cell one cell, keeping the cells passed over126// selected with it.127func (v *View) extend(d int) {128 n := len(v.h.Cells())129 if n == 0 || v.sel+d < 0 || v.sel+d >= n {130 return131 }132 if v.anchor < 0 {133 v.anchor = v.sel134 }135 v.sel += d136 v.onOut = false137 v.clamp()138 v.follow()139}140141// scrollOut scrolls the selected output's window d rows, reporting142// whether it could.143func (v *View) scrollOut(d int) bool {144 c, ok := v.Cell()145 if !ok || !v.onOut {146 return false147 }148 return v.scrollBy(c, d)149}150151// scrollBy scrolls cell c's output window d rows, as far as it goes,152// reporting whether it moved.153func (v *View) scrollBy(c notebook.Cell, d int) bool {154 f := v.foldOf(c.ID)155 sh := v.shown(c)156 switch {157 case f.hidden || f.whole && !sh.entered():158 return false159 case sh.isGrid():160 return sh.grid.Scroll(d)161 }162 limit := v.shown(c).maxScroll(false, v.outWidth())163 to := min(max(f.scroll+d, 0), limit)164 if to == f.scroll {165 return false166 }167 v.foldFor(c.ID).scroll = to168 return true169}170171// selectAtTop selects the first cell whose top shows after a page172// moved the view.173func (v *View) selectAtTop() {174 cells := v.h.Cells()175 v.top = max(min(v.top, v.total()-v.height), 0)176 at := 0177 v.anchor = -1178 for i, c := range cells {179 if at >= v.top {180 v.sel, v.onOut = i, false181 return182 }183 at += v.blockOf(i, c).lines()184 }185 v.sel, v.onOut = max(len(cells)-1, 0), false186}187188func (v *View) editKey(k tea.KeyPressMsg) (tea.Cmd, bool) {189 key := k.String()190 e := &v.edit191 if len(e.comp) > 0 {192 switch key {193 case "tab", "down", "ctrl+n":194 e.compSel = (e.compSel + 1) % len(e.comp)195 return nil, true196 case "shift+tab", "up", "ctrl+p":197 e.compSel = (e.compSel + len(e.comp) - 1) % len(e.comp)198 return nil, true199 case "enter":200 v.complete()201 return v.changed(), true202 case "esc":203 e.comp = nil204 return nil, true205 }206 e.comp = nil207 }208 if id, ok := v.EditKeys[key]; ok {209 return v.h.Run(id), true210 }211 if key == "tab" {212 return v.askCompletions(), true213 }214 before := e.text()215 if !e.area.Key(k, v.content()) {216 return nil, true // a key edit mode doesn't use does nothing217 }218 v.follow()219 if e.text() != before {220 return v.changed(), true221 }222 return v.rest(), true223}224225// Paste types text into the cell being edited.226func (v *View) Paste(text string) tea.Cmd {227 if !v.edit.on {228 return nil229 }230 v.edit.area.InsertText(text)231 v.follow()232 return v.changed()233}234235// StartEdit edits the selected cell, the caret at the end of its source.236func (v *View) StartEdit() tea.Cmd {237 c, ok := v.Cell()238 if !ok {239 return nil240 }241 v.full, v.onOut, v.anchor = nil, false, -1242 v.edit.cancel()243 v.edit = editor{on: true, id: c.ID}244 v.edit.area.SetText(c.Source)245 v.follow()246 return v.changed()247}248249// StopEdit leaves edit mode, keeping what was typed.250func (v *View) StopEdit() {251 if !v.edit.on {252 return253 }254 id, text := v.edit.id, v.edit.text()255 v.edit.cancel()256 v.edit = editor{}257 for _, c := range v.h.Cells() {258 if c.ID == id && c.Source != text {259 v.h.Edit(id, text)260 }261 }262 v.follow()263}264265// Commit keeps what's typed as the cell's source, still editing.266func (v *View) Commit() {267 if !v.edit.on {268 return269 }270 for _, c := range v.h.Cells() {271 if c.ID == v.edit.id && c.Source != v.edit.text() {272 v.h.Edit(c.ID, v.edit.text())273 }274 }275}276277// OpenFull shows the selected cell's output full-screen.278func (v *View) OpenFull() bool {279 c, ok := v.Cell()280 if !ok {281 return false282 }283 sh := v.shown(c)284 if sh.kind == outNone {285 return false286 }287 title := "Output"288 if name := c.Name(); name != "" {289 title = name290 }291 if o := v.h.Output(c.ID); o != nil && o.Count > 0 {292 title += " [" + itoa(o.Count) + "]"293 }294 v.full = newFull(title, sh, v.width, v.height)295 return true296}297298// CloseFull goes back from a full-screen output.299func (v *View) CloseFull() { v.full = nil }300301// Full is the output shown full-screen, or nil.302func (v *View) Full() *Full { return v.full }303304// Head is what the formula bar says of the selection: the cell's name305// or number, and what it reads, or what its output holds.306func (v *View) Head() (name, text string) {307 c, ok := v.Cell()308 if !ok {309 return "empty", ""310 }311 i, _ := v.Selected()312 name = "cell " + itoa(i+1)313 if n := c.Name(); n != "" {314 name = n315 }316 if c.Kind == notebook.Note {317 return name, "Markdown"318 }319 if v.onOut {320 return name, v.outputSummary(c)321 }322 src := c.Parse()323 var reads []string324 for _, r := range src.Refs() {325 reads = append(reads, "$"+r)326 }327 if src.ReadsSelection() {328 reads = append(reads, "$selection")329 }330 _, _, ranges := src.Command()331 for _, r := range ranges {332 reads = append(reads, "$sheet."+r.Ref)333 }334 text = "a nushell pipeline"335 if len(reads) > 0 {336 text = "reads " + strings.Join(reads, ", ")337 }338 var sets []string339 for _, a := range src.Assigned() {340 if a != c.Name() {341 sets = append(sets, "$"+a)342 }343 }344 if len(sets) > 0 {345 text += "; sets " + strings.Join(sets, ", ")346 }347 if n := c.Name(); n != "" {348 text += "; read as $" + n + ", nu." + n + " in formulas"349 }350 return name, text351}352353// outputSummary says what an output holds.354func (v *View) outputSummary(c notebook.Cell) string {355 sh := v.shown(c)356 switch sh.kind {357 case outTable:358 return "a table: " + more(sh.total, "row") + ", " + more(sh.cols, "column")359 case outRecord:360 return "a record: " + more(sh.total, "field")361 case outList:362 return "a list: " + more(sh.total, "item")363 case outText:364 return "text: " + more(sh.total, "line")365 case outError:366 return "an error"367 case outUnsaved:368 return "not saved in the file; run the cell to see it"369 }370 return "a value"371}372373func itoa(n int) string { return strconv.Itoa(n) }