cat / 012
clone

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

cat/module-path
012 / internal / ui / srcscreen.go
srcscreen.go 7.9K
1package ui23import (4 "path/filepath"5 "strconv"6 "strings"78 tea "charm.land/bubbletea/v2"9 "github.com/charmbracelet/x/ansi"1011 "src.rest/cat/012/internal/locale"12 "src.rest/cat/012/internal/sheet"13 "src.rest/cat/012/internal/ui/srcview"14 "src.rest/cat/012/internal/ui/theme"15 "src.rest/cat/012/internal/ui/transfer"16)1718// A linked source's tab on the screen: the menu bar with SOURCE as the19// mode, the formula bar naming the active cell as the source's sheet20// does and showing its value, the context line saying what the source21// is and how it's ordered, then the source's rows where a sheet's grid22// would be (package srcview). Keys move the active cell; commands that23// read a tab's data (sort, filter, pivot tables, copy) act on the24// source, and those that would change cells say it's read-only.2526// srcView is the view of the source tab shown, or nil on another.27func (m *Model) srcView() *srcview.View {28 if !m.sheet.IsSource() {29 return nil30 }31 v := m.src.views[m.sheet]32 if v == nil {33 if m.src.views == nil {34 m.src.views = map[*sheet.Sheet]*srcview.View{}35 }36 v = srcview.New(srcHost{m, m.sheet})37 m.src.views[m.sheet] = v38 }39 v.Resize(m.width, m.height-headerLine-1)40 return v41}4243// srcHost is a source tab's srcview.Host.44type srcHost struct {45 m *Model46 s *sheet.Sheet47}4849func (h srcHost) Theme() *theme.Theme { return &h.m.th }50func (h srcHost) Locale() *locale.Locale { return h.m.locale() }5152func (h srcHost) Columns() []srcview.Column {53 info, _ := h.s.Source()54 cols := make([]srcview.Column, len(info.Shape.Cols))55 for i, name := range info.Shape.Cols {56 cols[i].Name = name57 if i < len(info.Shape.Formats) {58 cols[i].Format = info.Shape.Formats[i]59 }60 }61 return cols62}6364func (h srcHost) Rows() (int64, bool) {65 if p := h.pages(); p != nil {66 return p.Rows()67 }68 return 0, false69}7071func (h srcHost) Row(i int64) (int64, []sheet.LiveCell, bool) {72 if p := h.pages(); p != nil {73 return p.Row(i)74 }75 return 0, nil, false76}7778func (h srcHost) Want(from, to int64) {79 if p := h.pages(); p != nil {80 p.Want(from, to)81 }82}8384func (h srcHost) pages() interface {85 Rows() (int64, bool)86 Row(int64) (int64, []sheet.LiveCell, bool)87 Want(int64, int64)88} {89 info, ok := h.s.Source()90 if !ok {91 return nil92 }93 if p := h.m.src.pages[info.Name]; p != nil {94 return p95 }96 return nil97}9899// sourceLines are the screen's lines on a source tab, from the menu bar100// down to the line above the status line.101func (m *Model) sourceLines(v *srcview.View) []string {102 return append([]string{m.menuBarLine(), m.formulaBar(), m.contextLineText()}, v.Draw()...)103}104105// sourceName is the active cell as the source's tab names it, and its106// value as text: C5 is the source's fourth row, under the header.107func (m *Model) sourceCell(v *srcview.View) (string, string) {108 num, cell, ok := v.Cell()109 _, col := v.Active()110 switch rows, known := (srcHost{m, m.sheet}).Rows(); {111 case !ok && known && rows > 0:112 return sheet.ColName(col) + "…", "" // its row is on its way113 case !ok:114 return "A1", ""115 }116 a := sheet.Addr{Col: col, Row: int(num) + 1}117 name := a.String()118 if a.Row >= sheet.MaxRows {119 name = sheet.ColName(col) + strconv.FormatInt(num+2, 10)120 }121 if cell.V.Kind == sheet.Empty {122 return name, ""123 }124 f := cell.F125 if f.IsZero() {126 if cols := (srcHost{m, m.sheet}).Columns(); col < len(cols) {127 f = cols[col].Format128 }129 }130 return name, sheet.FormatTextIn(cell.V, f, m.locale())131}132133// sourceBar is the formula bar on a source's tab: the active cell's134// name and value.135func (m *Model) sourceBar(v *srcview.View) string {136 name, text := m.sourceCell(v)137 return m.th.Header.Render(theme.PadRight(" "+ansi.Truncate(name, nameBoxW-1, "…"), nameBoxW)) + " " + text138}139140// sourceContext is the context line on a source tab: what it is, how141// many rows it has, and how they're ordered, or why it can't be read.142func (m *Model) sourceContext() (string, string) {143 info, _ := m.sheet.Source()144 name := sourceLabel(info)145 switch {146 case info.Err != "":147 return m.explainLine("", "! "+name+": "+info.Err, m.th.Warning)148 case !info.Known:149 return m.th.Muted.Render(sourceMark+" Opening ") + m.th.Key.Render(name) + m.th.Muted.Render("…"), ""150 }151 head := m.th.Muted.Render(sourceMark+" ") + m.th.Key.Render(name) + m.th.Muted.Render(" ")152 counts, hints := m.sourceCounts(info), m.th.KeyHints("Ctrl+↓", "last row", "Alt+D", "sort, filter")153 if ansi.StringWidth(head)+ansi.StringWidth(counts)+ansi.StringWidth(hints)+3 <= m.width {154 return head + m.th.Muted.Render(counts), hints155 }156 return m.explainLine(head, counts, m.th.Muted)157}158159// sourceTrouble is what F1 shows on a source's tab: why it can't be160// read, or its line when the context line cuts it; "" for nothing.161func (m *Model) sourceTrouble() (title, why string) {162 info, _ := m.sheet.Source()163 name := sourceLabel(info)164 switch {165 case info.Err != "":166 return name, "! " + name + ": " + info.Err167 case !info.Known:168 return "", ""169 }170 if _, chip := m.sourceContext(); chip == m.th.KeyHints("F1", "more") {171 return name, m.sourceCounts(info)172 }173 return "", ""174}175176// sourceLabel names a source's file, and its table when it has one.177func sourceLabel(info sheet.SourceInfo) string {178 name := filepath.Base(info.Source.Path)179 if info.Source.Table != "" {180 name += " " + info.Source.Table181 }182 return name183}184185// sourceMark is the glyph a source's tab shows beside its name and on186// the context line.187const sourceMark = "▦"188189// sourceCounts says how many rows the tab shows of the source's, and190// how they're ordered.191func (m *Model) sourceCounts(info sheet.SourceInfo) string {192 total := int64(info.Shape.Rows)193 out := transfer.Thousands(int(total)) + plural(int(min(total, 2)), " row", " rows") + ", " +194 transfer.Thousands(len(info.Shape.Cols)) + plural(len(info.Shape.Cols), " column", " columns")195 o, p := info.Source.Order, m.src.pages[info.Name]196 switch {197 case p != nil && p.Err() != "":198 return out + "; " + p.Err()199 case o.IsZero():200 return out201 case p == nil || p.Building():202 return out + "; sorting and filtering…"203 }204 var parts []string205 for _, s := range o.Sort {206 dir := "A to Z"207 if s.Desc {208 dir = "Z to A"209 }210 parts = append(parts, "sorted by "+m.sourceCol(info, s.Col)+" "+dir)211 }212 for _, f := range o.Filter {213 parts = append(parts, m.sourceCol(info, f.Col)+" "+strings.ToLower(f.Cond.Op.Title())+" "+f.Cond.Arg)214 }215 if len(o.Filter) > 0 {216 if n, ok := p.Rows(); ok {217 out += "; showing " + transfer.Thousands(int(n))218 }219 }220 return out + "; " + strings.Join(parts, ", ")221}222223// sourceCol names column col of a source: its letter and name.224func (m *Model) sourceCol(info sheet.SourceInfo, col int) string {225 if col < len(info.Shape.Cols) {226 return info.Shape.Cols[col]227 }228 return sheet.ColName(col)229}230231// sourceReadyKey is a key on a source tab: the view's, or else the232// UI's (menus, other sheets, commands that read the source).233func (m *Model) sourceReadyKey(v *srcview.View, k tea.KeyPressMsg) tea.Cmd {234 if v.Key(k) {235 return nil236 }237 key := k.String()238 if i := barMenuFor(key); i >= 0 {239 m.showBarMenu(i)240 return nil241 }242 if id, ok := keymap[canonicalKey(key)]; ok && commands[id].available(m) {243 return m.runCommand(id)244 }245 cmd, _ := m.runShortcut(key)246 return cmd247}248249// sourceMouse takes a click, a drag of the scrollbar or the wheel over250// the source's rows.251func (m *Model) sourceMouse(msg tea.MouseMsg) (tea.Cmd, bool) {252 v := m.srcView()253 mouse := msg.Mouse()254 if v == nil || m.mode != modeReady || m.overlay != nil || mouse.Y < headerLine || mouse.Y >= m.height-1 {255 return nil, false256 }257 hit := v.HitAt(mouse.X, mouse.Y-headerLine)258 switch msg := msg.(type) {259 case tea.MouseClickMsg:260 switch {261 case msg.Button != tea.MouseLeft:262 case hit.Bar:263 m.src.dragBar = true264 v.ScrollBar(hit.BarAt, hit.BarLines)265 case hit.Cell:266 v.MoveTo(hit.Row, hit.Col)267 }268 case tea.MouseMotionMsg:269 if m.src.dragBar {270 v.DragBar(mouse.Y - headerLine)271 }272 case tea.MouseReleaseMsg:273 m.src.dragBar = false274 case tea.MouseWheelMsg:275 switch msg.Button {276 case tea.MouseWheelUp:277 v.Scroll(-3)278 case tea.MouseWheelDown:279 v.Scroll(3)280 }281 }282 return nil, true283}