cat / 012
clone

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

Move terminal and JEV handling onto the types that own them

Dami committed cd1b078
Probing the terminal, handling its replies, syncing chart images,
notifications and releasing the terminal become methods of terminal;
sending JEV questions and describing their progress become methods of
jevRunner (nil when JEV is off). Update routes to them, and a JEV answer
is handled in one answerJEV step.
commit
cd1b078290d5f134d5223fcf288ba73f8f271312
parent
2260941271530e8306e69627d5239a0c4f35a85d
author
Dami <dami.etoile@gmail.com>
10 files changed, +88 -82
Minternal/ui/charts.go2+-
Minternal/ui/charts_test.go6+-
Minternal/ui/graphics.go83++++++++++++++++--------------
Minternal/ui/jev.go47++++++++++-------
Minternal/ui/links_test.go6+-
Minternal/ui/model.go14++---
Minternal/ui/mouse.go2+-
Minternal/ui/panel.go6+-
Minternal/ui/stress_test.go2+-
Minternal/ui/transfer.go2+-
M internal/ui/charts.go +1 -1
M internal/ui/charts_test.go +3 -3
M internal/ui/graphics.go +43 -40
@@ -13,7 +13,9 @@ import (
1313 "github.com/charmbracelet/x/ansi"1414 1515 "github.com/FelineStateMachine/012/internal/chart"16+ "github.com/FelineStateMachine/012/internal/sheet"1617 "github.com/FelineStateMachine/012/internal/telemetry"18+ "github.com/FelineStateMachine/012/internal/ui/theme"1719 )1820 1921 // terminal is what 012 knows about the terminal it runs in, learned from
@@ -46,8 +48,8 @@ func (t *terminal) wrap(seq string) string {
4648 4749 // probes are the startup queries: kitty graphics support, and live4850 // light and dark changes (mode 2031).49-func (m *Model) probes() tea.Cmd {50- return tea.Raw(m.term.wrap(chart.Query()) + ansi.SetModeLightDark)51+func (t *terminal) probes() tea.Cmd {52+ return tea.Raw(t.wrap(chart.Query()) + ansi.SetModeLightDark)5153 }5254 5355 // firstImageID is the image id of the first chart. Placeholders name
@@ -58,12 +60,12 @@ const firstImageID = 16
5860 // maxImages caps how many charts become images; the rest stay text.5961 const maxImages = 256 - firstImageID6062 61-// handleTerminal handles replies to queries and terminal events.62-func (m *Model) handleTerminal(msg tea.Msg) tea.Cmd {63+// handle handles replies to queries and terminal events.64+func (t *terminal) handle(msg tea.Msg) tea.Cmd {6365 switch msg := msg.(type) {6466 case uv.KittyGraphicsEvent:65- if msg.Options.ID == chart.QueryID && string(msg.Payload) == "OK" && !m.term.kitty {66- m.term.kitty = true67+ if msg.Options.ID == chart.QueryID && string(msg.Payload) == "OK" && !t.kitty {68+ t.kitty = true6769 // Ask for the cell size (for image proportions) and the6870 // palette colors charts draw with.6971 var q strings.Builder
@@ -75,20 +77,20 @@ func (m *Model) handleTerminal(msg tea.Msg) tea.Cmd {
7577 }7678 case uv.CellSizeEvent:7779 if msg.Width > 0 && msg.Height > 0 {78- m.term.cellW, m.term.cellH = msg.Width, msg.Height80+ t.cellW, t.cellH = msg.Width, msg.Height7981 }8082 case uv.UnknownOscEvent:8183 if i, c, ok := parsePaletteReply(string(msg)); ok {82- m.term.palette[i] = c84+ t.palette[i] = c8385 }8486 case uv.DarkColorSchemeEvent, uv.LightColorSchemeEvent:8587 // The system switched between light and dark. The terminal's8688 // background decides the theme, so ask for it again.8789 return tea.RequestBackgroundColor8890 case tea.FocusMsg:89- m.term.blurred = false91+ t.blurred = false9092 case tea.BlurMsg:91- m.term.blurred = true93+ t.blurred = true9294 }9395 return nil9496 }
@@ -127,62 +129,63 @@ func (t *terminal) color(i int) color.RGBA {
127129 }128130 129131 // chartPalette is the theme's series colors as the terminal draws them.130-func (m *Model) chartPalette() chart.Palette {132+func (t *terminal) chartPalette(th *theme.Theme) chart.Palette {131133 var p chart.Palette132- for i, idx := range m.th.SeriesANSI {133- p.Series[i] = m.term.color(idx)134+ for i, idx := range th.SeriesANSI {135+ p.Series[i] = t.color(idx)134136 }135- p.Grid = m.term.color(8) // bright black, like the text axes137+ p.Grid = t.color(8) // bright black, like the text axes136138 p.Grid.A = 90137139 return p138140 }139141 140142 // chartOptions are the drawing options for charts on this terminal.141-func (m *Model) chartOptions() chart.Options {142- return chart.Options{Image: m.term.kitty, CellW: m.term.cellW, CellH: m.term.cellH}143+func (t *terminal) chartOptions() chart.Options {144+ return chart.Options{Image: t.kitty, CellW: t.cellW, CellH: t.cellH}143145 }144146 145147 // syncImages sends the images of charts that changed since they were last146-// sent, and frees those of deleted charts. Update calls it after every147-// message, so images follow edits, recalculation, resizing and the theme.148-func (m *Model) syncImages() tea.Cmd {149- if !m.term.kitty {148+// sent, and frees those of deleted charts; shown gives the charts as149+// drawn, asked for only on terminals that show images. Update calls it150+// after every message, so images follow edits, recalculation, resizing151+// and the theme.152+func (t *terminal) syncImages(s *sheet.Sheet, shown func() []sheet.Chart, th *theme.Theme) tea.Cmd {153+ if !t.kitty {150154 return nil151155 }152156 var out strings.Builder153- charts := m.displayCharts()154- o := m.chartOptions()155- pal := m.chartPalette()157+ o := t.chartOptions()158+ pal := t.chartPalette(th)156159 live := map[int]bool{}157- for i, c := range charts {160+ for i, c := range shown() {158161 if i >= maxImages {159162 break160163 }161164 id := firstImageID + i162165 live[id] = true163166 w, h := chartInner(c)164- d := m.sheet.ChartData(c)167+ d := s.ChartData(c)165168 key := fmt.Sprintf("%v %v %d %d %v %v", c.Type, d, w, h, o, pal)166- if m.term.sent[id] == key {169+ if t.sent[id] == key {167170 continue168171 }169- m.term.sent[id] = key172+ t.sent[id] = key170173 span := telemetry.Start("chart.image", slog.String("type", c.Type.String()), slog.Int("w", w), slog.Int("h", h))171174 g := chart.Draw(c.Type, d, w, h, o)172175 img := chart.Image(c.Type, d, w, h, o, pal)173176 if img == nil || g.Plot.Empty() {174177 span.End()175- out.WriteString(chart.Delete(id, m.term.wrap))178+ out.WriteString(chart.Delete(id, t.wrap))176179 continue177180 }178- sent := chart.Transmit(id, img, g.Plot.Dx(), g.Plot.Dy(), m.term.wrap)181+ sent := chart.Transmit(id, img, g.Plot.Dx(), g.Plot.Dy(), t.wrap)179182 span.End(slog.Int("bytes", len(sent)))180183 out.WriteString(sent)181184 }182- for id := range m.term.sent {185+ for id := range t.sent {183186 if !live[id] {184- delete(m.term.sent, id)185- out.WriteString(chart.Delete(id, m.term.wrap))187+ delete(t.sent, id)188+ out.WriteString(chart.Delete(id, t.wrap))186189 }187190 }188191 if out.Len() == 0 {
@@ -191,21 +194,21 @@ func (m *Model) syncImages() tea.Cmd {
191194 return tea.Raw(out.String())192195 }193196 194-// releaseTerminal undoes the startup modes and frees the chart images.195-func (m *Model) releaseTerminal() string {197+// release undoes the startup modes and frees the chart images.198+func (t *terminal) release() string {196199 var b strings.Builder197200 b.WriteString(ansi.ResetModeLightDark)198- for id := range m.term.sent {199- b.WriteString(chart.Delete(id, m.term.wrap))201+ for id := range t.sent {202+ b.WriteString(chart.Delete(id, t.wrap))200203 }201204 return b.String()202205 }203206 204-// notifyDone tells the user a long job finished, with a desktop207+// notify tells the user a long job finished, with a desktop205208 // notification (OSC 9) when the terminal window isn't focused. When it is,206209 // the screen already shows the result.207-func (m *Model) notifyDone(msg string) tea.Cmd {208- if !m.term.blurred {210+func (t *terminal) notify(msg string) tea.Cmd {211+ if !t.blurred {209212 return nil210213 }211214 msg = strings.Map(func(r rune) rune {
@@ -214,5 +217,5 @@ func (m *Model) notifyDone(msg string) tea.Cmd {
214217 }215218 return r216219 }, msg)217- return tea.Raw(m.term.wrap(ansi.Notify("012: " + msg)))220+ return tea.Raw(t.wrap(ansi.Notify("012: " + msg)))218221 }
M internal/ui/jev.go +27 -20
@@ -11,6 +11,7 @@ import (
1111 "github.com/FelineStateMachine/012/internal/jev"1212 "github.com/FelineStateMachine/012/internal/sheet"1313 "github.com/FelineStateMachine/012/internal/telemetry"14+ "github.com/FelineStateMachine/012/internal/ui/theme"1415 )1516 1617 // JEV functions are answered in the background: the engine queues
@@ -68,19 +69,19 @@ func plural(n int, one, many string) string {
6869 return many6970 }7071 71-// sendJEV starts queued questions, keeping at most jevParallel in flight72+// send starts queued questions, keeping at most jevParallel in flight7273 // so a filled-down column doesn't open hundreds of connections at once.73-func (m *Model) sendJEV() tea.Cmd {74- if m.jev == nil {74+func (j *jevRunner) send() tea.Cmd {75+ if j == nil {7576 return nil7677 }77- inFlight, queued := m.jev.cache.Busy()78+ inFlight, queued := j.cache.Busy()7879 telemetry.Set("jev_in_flight", int64(inFlight))7980 telemetry.Set("jev_queued", int64(queued))80- calls := m.jev.cache.Take(jevParallel - inFlight)81+ calls := j.cache.Take(jevParallel - inFlight)8182 cmds := make([]tea.Cmd, len(calls))8283 for i, call := range calls {83- client := m.jev.client84+ client := j.client8485 cmds[i] = func() tea.Msg {8586 ctx, cancel := context.WithTimeout(context.Background(), jevTimeout)8687 defer cancel()
@@ -97,21 +98,27 @@ func (m *Model) sendJEV() tea.Cmd {
9798 return tea.Batch(cmds...)9899 }99100 100-// handleJEVAnswer stores an answer and recalculates the JEV cells.101-func (m *Model) handleJEVAnswer(msg jevAnswerMsg) {101+// answerJEV stores an answer and recalculates the JEV cells, telling102+// the user when the last question is answered.103+func (m *Model) answerJEV(msg jevAnswerMsg) tea.Cmd {102104 if m.jev == nil {103- return105+ return nil104106 }107+ busy := m.jev.busy() != ""105108 m.jev.cache.Store(msg.call, msg.answer)106109 m.sheet.RecalcVolatile()110+ if busy && m.jev.busy() == "" {111+ return m.term.notify("JEV finished answering in " + m.displayName())112+ }113+ return nil107114 }108115 109-// jevBusy describes questions still being answered, for the status line.110-func (m *Model) jevBusy() string {111- if m.jev == nil {116+// busy describes questions still being answered, for the status line.117+func (j *jevRunner) busy() string {118+ if j == nil {112119 return ""113120 }114- inFlight, queued := m.jev.cache.Busy()121+ inFlight, queued := j.cache.Busy()115122 switch {116123 case inFlight+queued == 0:117124 return ""
@@ -121,18 +128,18 @@ func (m *Model) jevBusy() string {
121128 return fmt.Sprintf("JEV answering %d, %d waiting", inFlight, queued)122129 }123130 124-// jevLine explains the JEV answer in the active cell, for the context line.125-func (m *Model) jevLine() string {126- calls := m.sheet.RemoteCalls(m.cur)131+// line explains the JEV answers of calls, the active cell's, for the132+// context line.133+func (j *jevRunner) line(th *theme.Theme, calls []sheet.RemoteCall) string {127134 if len(calls) == 0 {128135 return ""129136 }130- if m.jev == nil {131- return m.th.Warning.Render("JEV functions need TYPESAFE_API_KEY, in the environment or a .env file")137+ if j == nil {138+ return th.Warning.Render("JEV functions need TYPESAFE_API_KEY, in the environment or a .env file")132139 }133140 var parts []string134141 for _, c := range calls {135- a, ok := m.jev.cache.Answer(c)142+ a, ok := j.cache.Answer(c)136143 if !ok {137144 parts = append(parts, "JEV: asking…")138145 continue
@@ -143,5 +150,5 @@ func (m *Model) jevLine() string {
143150 for _, p := range parts[1:] {144151 line += " " + p145152 }146- return m.th.Muted.Render(line)153+ return th.Muted.Render(line)147154 }
M internal/ui/links_test.go +3 -3
M internal/ui/model.go +5 -9
@@ -92,8 +92,8 @@ func New(s *sheet.Sheet, filename string) *Model {
9292 // Init implements tea.Model. It asks the terminal for its background color9393 // so the theme can adapt to light terminals.9494 func (m *Model) Init() tea.Cmd {95- // sendJEV starts any questions queued while loading the file.96- return tea.Batch(tea.RequestBackgroundColor, tea.Raw(shiftEscapeOn), m.probes(), m.sendJEV(), m.startupCmd())95+ // jev.send starts any questions queued while loading the file.96+ return tea.Batch(tea.RequestBackgroundColor, tea.Raw(shiftEscapeOn), m.term.probes(), m.jev.send(), m.startupCmd())9797 }9898 9999 // Update implements tea.Model.
@@ -145,15 +145,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145145 m.prompt.files = msg146146 }147147 case jevAnswerMsg:148- busy := m.jevBusy() != ""149- m.handleJEVAnswer(msg)150- if busy && m.jevBusy() == "" {151- cmd = m.notifyDone("JEV finished answering in " + m.displayName())152- }148+ cmd = m.answerJEV(msg)153149 case importedMsg, importTickMsg, exportedMsg:154150 cmd = m.handleTransfer(msg)155151 default:156- cmd = m.handleTerminal(msg)152+ cmd = m.term.handle(msg)157153 }158154 // Scroll only when the focus moves, so the mouse wheel can look around159155 // without the view snapping back, as in Sheets.
@@ -164,7 +160,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
164160 m.clampView()165161 // Any edit may have queued JEV questions.166162 // Chart images follow any change, see graphics.go.167- return m, tea.Batch(cmd, m.sendJEV(), m.syncImages())163+ return m, tea.Batch(cmd, m.jev.send(), m.term.syncImages(m.sheet, m.displayCharts, &m.th))168164 }169165 170166 // beginUpdate prepares for an input event and returns the sheet's state
M internal/ui/mouse.go +1 -1
M internal/ui/panel.go +3 -3
M internal/ui/stress_test.go +1 -1
M internal/ui/transfer.go +1 -1