cat / 012
clone

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

Linked sources: e2e test, golden screens and stills, a filter of the condition alone, Page Down as a sheet's

Dami committed 9f59fd1
commit
9f59fd13428f87ffb12be79ac5a5400c8e173e38
parent
617ab46de68e6dd2d8acf206e5aefcc24c95f5e8
author
Dami <dami.etoile@gmail.com>
kind
test risk score 1.4 of 3, Jev's estimate
35 files changed, +861 -17
Mdocs/files/sources.md3+
Adocs/media/source-formulas-dark.pngbin
Adocs/media/source-formulas-light.pngbin
Adocs/media/source-tab-dark.pngbin
Adocs/media/source-tab-light.pngbin
Me2e/go.mod10++
Me2e/go.sum21++++
Ae2e/screensources_test.go102+++++++++++++++++++
Ae2e/sources_test.go113+++++++++++++++++++++
Me2e/stills_test.go2+
Me2e/stillshapes_test.go7+
Ae2e/testdata/screens/docs-source-formulas-light.html14+++
Ae2e/testdata/screens/docs-source-formulas.html14+++
Ae2e/testdata/screens/docs-source-tab-light.html20++++
Ae2e/testdata/screens/docs-source-tab.html20++++
Ae2e/testdata/screens/source-filter-light.html30++++++
Ae2e/testdata/screens/source-filter.html30++++++
Ae2e/testdata/screens/source-formulas-light.html30++++++
Ae2e/testdata/screens/source-formulas.html30++++++
Ae2e/testdata/screens/source-missing-light.html30++++++
Ae2e/testdata/screens/source-missing.html30++++++
Ae2e/testdata/screens/source-tab-high-contrast.html30++++++
Ae2e/testdata/screens/source-tab-light.html30++++++
Ae2e/testdata/screens/source-tab.html30++++++
Minternal/functions/stream.go2+-
Minternal/ui/commands.go6+-
Minternal/ui/filterpick/filterpick.go21++++-
Minternal/ui/filterpick/filterpick_test.go26+++++
Minternal/ui/panel.go3+-
Minternal/ui/srccmds.go20+++-
Minternal/ui/srcscreen.go15++-
Minternal/ui/srcview/draw.go13++
Minternal/ui/srcview/keys.go15++-
Minternal/ui/srcview/view.go2+-
Ainternal/ui/srcview/view_test.go159++++++++++++++++++++++++++++++
M docs/files/sources.md +3 -0
A docs/media/source-formulas-dark.png +0 -0

Binary file.

A docs/media/source-formulas-light.png +0 -0

Binary file.

A docs/media/source-tab-dark.png +0 -0

Binary file.

A docs/media/source-tab-light.png +0 -0

Binary file.

M e2e/go.mod +10 -0
M e2e/go.sum +21 -0
@@ -1,7 +1,21 @@
1+github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=2+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=13 github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=24 github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=35 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=46 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=7+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=8+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=9+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=10+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=11+github.com/parquet-go/bitpack v1.0.0 h1:AUqzlKzPPXf2bCdjfj4sTeacrUwsT7NlcYDMUQxPcQA=12+github.com/parquet-go/bitpack v1.0.0/go.mod h1:XnVk9TH+O40eOOmvpAVZ7K2ocQFrQwysLMnc6M/8lgs=13+github.com/parquet-go/jsonlite v1.0.0 h1:87QNdi56wOfsE5bdgas0vRzHPxfJgzrXGml1zZdd7VU=14+github.com/parquet-go/jsonlite v1.0.0/go.mod h1:nDjpkpL4EOtqs6NQugUsi0Rleq9sW/OtC1NnZEnxzF0=15+github.com/parquet-go/parquet-go v0.32.0 h1:NWDqTUHfrCS4cJP/Fj2HlxvqsrVedWG3sayMkf+znzM=16+github.com/parquet-go/parquet-go v0.32.0/go.mod h1:navtkAYr2LGoJVp141oXPlO/sxLvaOe3la2JEoD8+rg=17+github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=18+github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=519 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=620 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=721 github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0=
@@ -12,12 +26,15 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
1226 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=1327 github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=1428 github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=29+github.com/twpayne/go-geom v1.6.1 h1:iLE+Opv0Ihm/ABIcvQFGIiFBXd76oBIar9drAwHFhR4=30+github.com/twpayne/go-geom v1.6.1/go.mod h1:Kr+Nly6BswFsKM5sd31YaoWS5PeDDH2NftJTK7Gd028=1531 github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=1632 github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=1733 github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88=1834 github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak=1935 github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=2036 github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=37+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=2138 go.mitchellh.com/libghostty v0.0.0-20260920220152-31b65cdc24cf h1:PgfZ7mRcXk8n3+B+46oo9WxDi6xK1z6feOQEKNRWCBs=2239 go.mitchellh.com/libghostty v0.0.0-20260920220152-31b65cdc24cf/go.mod h1:sx8yDvnDrQC/PzsrFfYx7ROmH8my91nz7lOxQb6sbo0=2340 golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
@@ -28,9 +45,13 @@ golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
2845 golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=2946 golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=3047 golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=48+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=49+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=3150 golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=3251 golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=3352 golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=3453 golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=54+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=55+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=3556 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=3657 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A e2e/screensources_test.go +102 -0
@@ -0,0 +1,102 @@
1+package e2e2+3+import (4+ "path/filepath"5+ "strings"6+ "testing"7+8+ ghostty "go.mitchellh.com/libghostty"9+)10+11+// Golden screens for linked sources: two million trips on a tab of12+// their own, sorted by fare and scrolled halfway down by the scrollbar;13+// formulas over them on a sheet, one past max-cells saying why; the14+// condition a source's column is filtered by; and a source whose file15+// isn't there. Each on a light terminal too, the tab in the high-contrast16+// theme as well.17+18+func tripsFile(t *testing.T, dir string) { writeTrips(t, dir, tripRows) }19+20+// sortedTrips links trips.parquet, sorts it by fare Z to A and drags21+// the scrollbar halfway down.22+func sortedTrips(s *session) {23+ s.linkSource("trips.parquet")24+ s.waitFor("2,000,000 rows, 5 columns")25+ s.keys("<right>", "<right>")26+ s.keys("<ctrl+k>", "Sort sheet Z to A", "<enter>")27+ s.waitFor("sorted by fare Z to A")28+ s.waitFor("92.99")29+ bar := int(s.cols) - 130+ top, bottom := 7, int(s.rows)-231+ s.click(ghostty.MouseButtonLeft, bar, (top+bottom)/2)32+ s.waitFor("┃")33+ s.eventually("the rows halfway down", func() bool {34+ return strings.Contains(s.line(1), " 48") && !strings.Contains(s.screen(), " … ")35+ })36+}37+38+// tripFormulas reads the trips from a sheet: counts, sums, a lookup,39+// and a MEDIAN past max-cells (a million, in the screen's config),40+// the pointer on it.41+func tripFormulas(s *session) {42+ s.linkSource("trips.parquet")43+ s.keys("<ctrl+pgup>")44+ s.waitFor("Sheet1")45+ for _, row := range [][2]string{46+ {"Trips", "=ROWS(trips)"},47+ {"Fares", "=SUM(trips[fare])"},48+ {"By card", `=SUMIFS(trips[fare],trips[payment],"card")`},49+ {"Vouchers", `=COUNTIF(trips[payment],"voucher")`},50+ {"Longest", "=MAX(trips[miles])"},51+ {"Trip 1M", "=XLOOKUP(1000000,trips[trip],trips[fare])"},52+ {"Median", "=MEDIAN(trips[fare])"},53+ } {54+ s.keys(row[0], "<tab>", row[1], "<enter>")55+ }56+ s.waitFor("285714")57+ s.waitFor("#VALUE!")58+ s.keys("<up>", "<right>")59+ s.waitFor("MEDIAN would hold")60+}61+62+var sourceScreens = []screen{63+ {name: "source-tab", files: tripsFile, setup: sortedTrips},64+ {name: "source-formulas", opts: options{config: "max-cells = 1000000\n"}, files: tripsFile, setup: tripFormulas},65+ {name: "source-filter", files: tripsFile, setup: func(s *session) {66+ s.linkSource("trips.parquet")67+ s.waitFor("card")68+ s.keys("<right>")69+ s.keys("<ctrl+k>", "Filter by column", "<enter>")70+ s.waitFor("Filter B payment")71+ for range 7 {72+ s.keys("<down>")73+ }74+ s.keys("card")75+ s.waitFor("Text is exactly")76+ s.waitFor("card ")77+ }},78+ {name: "source-missing", files: func(t *testing.T, dir string) {79+ writeText(t, filepath.Join(dir, "rides.012"),80+ `{"version": 7, "sheets": [{"name": "Sheet1", "cells": {"A1": "=SUM(trips[fare])"}}, `+81+ `{"name": "trips", "cells": {}, "regions": [{"name": "trips", "at": "A1", "path": "trips.parquet", "paged": true}]}]}`)82+ }, args: []string{"rides.012"}, setup: func(s *session) {83+ s.waitFor("#REF!")84+ s.keys("<ctrl+pgdown>")85+ s.waitFor("! trips.parquet: the file isn't there")86+ }},87+}88+89+func init() {90+ for _, sc := range sourceScreens {91+ light := sc92+ light.name += "-light"93+ light.opts.light = true94+ screens = append(screens, sc, light)95+ if sc.name == "source-tab" {96+ hc := sc97+ hc.name += "-high-contrast"98+ hc.opts.config = highContrastConfig99+ screens = append(screens, hc)100+ }101+ }102+}
A e2e/sources_test.go +113 -0
@@ -0,0 +1,113 @@
1+package e2e2+3+import (4+ "fmt"5+ "os"6+ "path/filepath"7+ "strings"8+ "testing"9+ "time"10+11+ "github.com/parquet-go/parquet-go"12+)13+14+// trip is a row of the Parquet file the linked sources' tests read: a15+// ride, how it was paid, its fare, its miles and its day.16+type trip struct {17+ Trip int64 `parquet:"trip"`18+ Payment string `parquet:"payment,dict"`19+ Fare float64 `parquet:"fare"`20+ Miles float64 `parquet:"miles"`21+ Day int32 `parquet:"day,date"`22+}23+24+// payments are the ways trips are paid, in the order rows take them.25+var payments = []string{"card", "cash", "card", "app", "card", "cash", "voucher"}26+27+// tripAt is trip i, the same whenever it's asked for.28+func tripAt(i int) trip {29+ day := int32(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).Unix()/86400) + int32(i%270)30+ return trip{Trip: int64(i + 1), Payment: payments[i%len(payments)],31+ Fare: float64((i*7919)%9000)/100 + 3, Miles: float64((i*104729)%400) / 10, Day: day}32+}33+34+// writeTrips writes rows trips to trips.parquet in dir.35+func writeTrips(t *testing.T, dir string, rows int) {36+ t.Helper()37+ f, err := os.Create(filepath.Join(dir, "trips.parquet"))38+ if err != nil {39+ t.Fatal(err)40+ }41+ defer f.Close()42+ w := parquet.NewGenericWriter[trip](f, parquet.MaxRowsPerRowGroup(1<<18))43+ batch := make([]trip, 1<<14)44+ for i := 0; i < rows; i += len(batch) {45+ n := min(len(batch), rows-i)46+ for j := range n {47+ batch[j] = tripAt(i + j)48+ }49+ if _, err := w.Write(batch[:n]); err != nil {50+ t.Fatal(err)51+ }52+ }53+ if err := w.Close(); err != nil {54+ t.Fatal(err)55+ }56+}57+58+// tripRows is how many rows the screens' source has: past the grid's59+// last row.60+const tripRows = 2_000_00061+62+// linkSource links name as a source through the palette, and waits for63+// its rows.64+func (s *session) linkSource(name string) {65+ s.t.Helper()66+ s.keys("<ctrl+k>", "Link a source")67+ s.waitFor("Link a source")68+ s.keys("<enter>")69+ s.waitFor("Type to filter, or a path")70+ s.keys(name, "<enter>")71+ s.waitFor("Linked " + name)72+ s.keys("<down>", "<up>") // the note gives way to the source's line73+ s.waitFor("▦ " + name)74+}75+76+func TestLinkedSource(t *testing.T) {77+ dir := t.TempDir()78+ writeTrips(t, dir, tripRows)79+ s := start(t, dir)80+ s.linkSource("trips.parquet")81+ s.waitFor("2,000,000 rows, 5 columns")82+ s.waitFor("card")83+84+ // The last row, numbered past the grid's last row.85+ s.keys("<ctrl+down>")86+ s.waitFor("2000001 ")87+88+ // Sorted by fare, Z to A, in the background.89+ s.keys("<ctrl+home>", "<right>", "<right>")90+ s.keys("<ctrl+k>", "Sort sheet Z to A", "<enter>")91+ s.waitFor("sorted by fare Z to A")92+ s.eventually("the top fare", func() bool { return strings.Contains(s.line(1), "92.99") })93+94+ // A formula on another sheet reads the whole source.95+ s.keys("<ctrl+pgup>")96+ s.waitFor("Sheet1")97+ s.keys("=COUNTIF(trips[payment],\"voucher\")", "<enter>")98+ s.waitFor(fmt.Sprint(tripRows / 7))99+100+ // Saved, the workbook keeps the link, and reads the source again.101+ s.keys("<ctrl+s>")102+ s.waitFor("Save as:")103+ s.keys("rides", "<enter>")104+ s.waitFor("rides.012")105+ data, err := os.ReadFile(filepath.Join(dir, "rides.012"))106+ if err != nil || !strings.Contains(string(data), `"paged":true`) || !strings.Contains(string(data), `"version": 7`) {107+ t.Fatalf("the file: %v\n%s", err, data)108+ }109+ s.keys("<ctrl+q>")110+ s.waitExit()111+ s = start(t, dir, "rides.012")112+ s.waitFor(fmt.Sprint(tripRows / 7))113+}
M e2e/stills_test.go +2 -0
M e2e/stillshapes_test.go +7 -0
A e2e/testdata/screens/docs-source-formulas-light.html +14 -0
@@ -0,0 +1,14 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B7</span><span style="background:var(--vt-palette-7)"> </span> =MEDIAN(trips[fare])3+<span style="color:var(--vt-palette-1)">#VALUE!</span> <span style="color:var(--vt-palette-0)">MEDIAN</span> <span style="color:var(--vt-palette-0)">would</span> <span style="color:var(--vt-palette-0)">hold</span> <span style="color:var(--vt-palette-0)">2,000,000</span> <span style="color:var(--vt-palette-0)">cells</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">trips,</span> <span style="color:var(--vt-palette-0)">past</span> <span style="color:var(--vt-palette-0)">max-cells</span> <span style="color:var(--vt-palette-0)">(1,000,000):</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> Trips 20000006+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> Fares 959900007+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> By card 411385638+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> Vouchers 2857149+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> Longest 39.910+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> Trip 1M 3.8111+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">7</span><span style="background:var(--vt-palette-6)"> </span> Median <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);text-decoration:underline wavy var(--vt-palette-1)">#VALUE!</span><span style="background:var(--vt-palette-6)"> </span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span>14+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> ▦ trips <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-source-formulas.html +14 -0
@@ -0,0 +1,14 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B7</span><span style="background:var(--vt-palette-8)"> </span> =MEDIAN(trips[fare])3+<span style="color:var(--vt-palette-1)">#VALUE!</span> <span style="color:var(--vt-palette-8)">MEDIAN</span> <span style="color:var(--vt-palette-8)">would</span> <span style="color:var(--vt-palette-8)">hold</span> <span style="color:var(--vt-palette-8)">2,000,000</span> <span style="color:var(--vt-palette-8)">cells</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">trips,</span> <span style="color:var(--vt-palette-8)">past</span> <span style="color:var(--vt-palette-8)">max-cells</span> <span style="color:var(--vt-palette-8)">(1,000,000):</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> Trips 20000006+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> Fares 959900007+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> By card 411385638+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> Vouchers 2857149+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> Longest 39.910+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Trip 1M 3.8111+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">7</span><span style="background:var(--vt-palette-6)"> </span> Median <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);text-decoration:underline wavy var(--vt-palette-1)">#VALUE!</span><span style="background:var(--vt-palette-6)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> ▦ trips <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-source-tab-light.html +20 -0
@@ -0,0 +1,20 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">SOURCE</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C1912502</span><span style="background:var(--vt-palette-7)"> </span> 483+<span style="color:var(--vt-palette-0)">▦</span> <span style="font-weight:bold">trips.parquet</span> <span style="color:var(--vt-palette-0)">2,000,000</span> <span style="color:var(--vt-palette-0)">rows,</span> <span style="color:var(--vt-palette-0)">5</span> <span style="color:var(--vt-palette-0)">columns;</span> <span style="color:var(--vt-palette-0)">sorted</span> <span style="color:var(--vt-palette-0)">by</span> <span style="color:var(--vt-palette-0)">fare</span> <span style="color:var(--vt-palette-0)">Z</span> <span style="color:var(--vt-palette-0)">to</span> <span style="color:var(--vt-palette-0)">A</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">C</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">trip</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">payment</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">fare</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">miles</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">day</span><span style="text-decoration:underline"> </span>6+<span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────</span>7+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1912502</span><span style="background:var(--vt-palette-6)"> </span>1912501 card <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">48</span><span style="background:var(--vt-palette-6)"> </span> 10 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1921502</span><span style="background:var(--vt-palette-7)"> </span>1921501 card 48 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1930502</span><span style="background:var(--vt-palette-7)"> </span>1930501 cash 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1939502</span><span style="background:var(--vt-palette-7)"> </span>1939501 app 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1948502</span><span style="background:var(--vt-palette-7)"> </span>1948501 cash 48 10 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1957502</span><span style="background:var(--vt-palette-7)"> </span>1957501 voucher 48 30 1/1/2026 <span style="font-weight:bold">┃</span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1966502</span><span style="background:var(--vt-palette-7)"> </span>1966501 card 48 10 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>14+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1975502</span><span style="background:var(--vt-palette-7)"> </span>1975501 card 48 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>15+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1984502</span><span style="background:var(--vt-palette-7)"> </span>1984501 card 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>16+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1993502</span><span style="background:var(--vt-palette-7)"> </span>1993501 cash 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8823</span><span style="background:var(--vt-palette-7)"> </span> 8822 cash 47.99 10.9 7/1/2026 <span style="color:var(--vt-palette-8)">│</span>18+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17823</span><span style="background:var(--vt-palette-7)"> </span> 17822 voucher 47.99 30.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>19+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">26823</span><span style="background:var(--vt-palette-7)"> </span> 26822 card 47.99 10.9 4/2/2026 <span style="color:var(--vt-palette-8)">│</span>20+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">▦</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">trips</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-source-tab.html +20 -0
@@ -0,0 +1,20 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">SOURCE</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C1912502</span><span style="background:var(--vt-palette-8)"> </span> 483+<span style="color:var(--vt-palette-8)">▦</span> <span style="font-weight:bold">trips.parquet</span> <span style="color:var(--vt-palette-8)">2,000,000</span> <span style="color:var(--vt-palette-8)">rows,</span> <span style="color:var(--vt-palette-8)">5</span> <span style="color:var(--vt-palette-8)">columns;</span> <span style="color:var(--vt-palette-8)">sorted</span> <span style="color:var(--vt-palette-8)">by</span> <span style="color:var(--vt-palette-8)">fare</span> <span style="color:var(--vt-palette-8)">Z</span> <span style="color:var(--vt-palette-8)">to</span> <span style="color:var(--vt-palette-8)">A</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">C</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">trip</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">payment</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">fare</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">miles</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">day</span><span style="text-decoration:underline"> </span>6+<span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────</span>7+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1912502</span><span style="background:var(--vt-palette-6)"> </span>1912501 card <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">48</span><span style="background:var(--vt-palette-6)"> </span> 10 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1921502</span><span style="background:var(--vt-palette-8)"> </span>1921501 card 48 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1930502</span><span style="background:var(--vt-palette-8)"> </span>1930501 cash 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1939502</span><span style="background:var(--vt-palette-8)"> </span>1939501 app 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1948502</span><span style="background:var(--vt-palette-8)"> </span>1948501 cash 48 10 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1957502</span><span style="background:var(--vt-palette-8)"> </span>1957501 voucher 48 30 1/1/2026 <span style="font-weight:bold">┃</span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1966502</span><span style="background:var(--vt-palette-8)"> </span>1966501 card 48 10 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>14+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1975502</span><span style="background:var(--vt-palette-8)"> </span>1975501 card 48 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>15+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1984502</span><span style="background:var(--vt-palette-8)"> </span>1984501 card 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>16+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1993502</span><span style="background:var(--vt-palette-8)"> </span>1993501 cash 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8823</span><span style="background:var(--vt-palette-8)"> </span> 8822 cash 47.99 10.9 7/1/2026 <span style="color:var(--vt-palette-8)">│</span>18+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17823</span><span style="background:var(--vt-palette-8)"> </span> 17822 voucher 47.99 30.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>19+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">26823</span><span style="background:var(--vt-palette-8)"> </span> 26822 card 47.99 10.9 4/2/2026 <span style="color:var(--vt-palette-8)">│</span>20+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">▦</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">trips</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-filter-light.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">FILTER</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B2</span><span style="background:var(--vt-palette-7)"> </span> card3+4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">trip</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Filter</span> <span style="font-weight:bold">B</span> <span style="font-weight:bold">payment</span> <span style="color:var(--vt-palette-8)">────────────────┐</span>6+<span style="color:var(--vt-palette-8)">───────────────│</span> <span style="color:var(--vt-palette-0)">If</span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">‹</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Text</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">is</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">exactly</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">›</span> card <span style="color:var(--vt-palette-8)">│───────────────────────────────────────────────</span>7+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">2</span><span style="background:var(--vt-palette-6)"> </span> 1 <span style="color:var(--vt-palette-8)">└────────────────────────────────────┘</span> <span style="font-weight:bold">┃</span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> 2 cash 82.19 32.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> 3 card 71.38 25.8 1/3/2026 <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> 4 app 60.57 18.7 1/4/2026 <span style="color:var(--vt-palette-8)">│</span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> 5 card 49.76 11.6 1/5/2026 <span style="color:var(--vt-palette-8)">│</span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span> 6 cash 38.95 4.5 1/6/2026 <span style="color:var(--vt-palette-8)">│</span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span> 7 voucher 28.14 37.4 1/7/2026 <span style="color:var(--vt-palette-8)">│</span>14+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span> 8 card 17.33 30.3 1/8/2026 <span style="color:var(--vt-palette-8)">│</span>15+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">10</span><span style="background:var(--vt-palette-7)"> </span> 9 cash 6.52 23.2 1/9/2026 <span style="color:var(--vt-palette-8)">│</span>16+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">11</span><span style="background:var(--vt-palette-7)"> </span> 10 card 85.71 16.1 1/10/2026 <span style="color:var(--vt-palette-8)">│</span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">12</span><span style="background:var(--vt-palette-7)"> </span> 11 app 74.9 9 1/11/2026 <span style="color:var(--vt-palette-8)">│</span>18+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">13</span><span style="background:var(--vt-palette-7)"> </span> 12 card 64.09 1.9 1/12/2026 <span style="color:var(--vt-palette-8)">│</span>19+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">14</span><span style="background:var(--vt-palette-7)"> </span> 13 cash 53.28 34.8 1/13/2026 <span style="color:var(--vt-palette-8)">│</span>20+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">15</span><span style="background:var(--vt-palette-7)"> </span> 14 voucher 42.47 27.7 1/14/2026 <span style="color:var(--vt-palette-8)">│</span>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">16</span><span style="background:var(--vt-palette-7)"> </span> 15 card 31.66 20.6 1/15/2026 <span style="color:var(--vt-palette-8)">│</span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span> 16 cash 20.85 13.5 1/16/2026 <span style="color:var(--vt-palette-8)">│</span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span> 17 card 10.04 6.4 1/17/2026 <span style="color:var(--vt-palette-8)">│</span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span> 18 app 89.23 39.3 1/18/2026 <span style="color:var(--vt-palette-8)">│</span>25+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span> 19 card 78.42 32.2 1/19/2026 <span style="color:var(--vt-palette-8)">│</span>26+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">21</span><span style="background:var(--vt-palette-7)"> </span> 20 cash 67.61 25.1 1/20/2026 <span style="color:var(--vt-palette-8)">│</span>27+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">22</span><span style="background:var(--vt-palette-7)"> </span> 21 voucher 56.8 18 1/21/2026 <span style="color:var(--vt-palette-8)">│</span>28+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">23</span><span style="background:var(--vt-palette-7)"> </span> 22 card 45.99 10.9 1/22/2026 <span style="color:var(--vt-palette-8)">│</span>29+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">24</span><span style="background:var(--vt-palette-7)"> </span> 23 cash 35.18 3.8 1/23/2026 <span style="color:var(--vt-palette-8)">│</span>30+Rows must meet the condition <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Up/Down</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">condition</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Enter</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">apply</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">cancel</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-filter.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">FILTER</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B2</span><span style="background:var(--vt-palette-8)"> </span> card3+4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">trip</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Filter</span> <span style="font-weight:bold">B</span> <span style="font-weight:bold">payment</span> <span style="color:var(--vt-palette-8)">────────────────┐</span>6+<span style="color:var(--vt-palette-8)">───────────────│</span> <span style="color:var(--vt-palette-8)">If</span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">‹</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Text</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">is</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">exactly</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">›</span> card <span style="color:var(--vt-palette-8)">│───────────────────────────────────────────────</span>7+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">2</span><span style="background:var(--vt-palette-6)"> </span> 1 <span style="color:var(--vt-palette-8)">└────────────────────────────────────┘</span> <span style="font-weight:bold">┃</span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> 2 cash 82.19 32.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> 3 card 71.38 25.8 1/3/2026 <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> 4 app 60.57 18.7 1/4/2026 <span style="color:var(--vt-palette-8)">│</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> 5 card 49.76 11.6 1/5/2026 <span style="color:var(--vt-palette-8)">│</span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span> 6 cash 38.95 4.5 1/6/2026 <span style="color:var(--vt-palette-8)">│</span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span> 7 voucher 28.14 37.4 1/7/2026 <span style="color:var(--vt-palette-8)">│</span>14+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span> 8 card 17.33 30.3 1/8/2026 <span style="color:var(--vt-palette-8)">│</span>15+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span> 9 cash 6.52 23.2 1/9/2026 <span style="color:var(--vt-palette-8)">│</span>16+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span> 10 card 85.71 16.1 1/10/2026 <span style="color:var(--vt-palette-8)">│</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span> 11 app 74.9 9 1/11/2026 <span style="color:var(--vt-palette-8)">│</span>18+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span> 12 card 64.09 1.9 1/12/2026 <span style="color:var(--vt-palette-8)">│</span>19+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span> 13 cash 53.28 34.8 1/13/2026 <span style="color:var(--vt-palette-8)">│</span>20+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span> 14 voucher 42.47 27.7 1/14/2026 <span style="color:var(--vt-palette-8)">│</span>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span> 15 card 31.66 20.6 1/15/2026 <span style="color:var(--vt-palette-8)">│</span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span> 16 cash 20.85 13.5 1/16/2026 <span style="color:var(--vt-palette-8)">│</span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span> 17 card 10.04 6.4 1/17/2026 <span style="color:var(--vt-palette-8)">│</span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span> 18 app 89.23 39.3 1/18/2026 <span style="color:var(--vt-palette-8)">│</span>25+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span> 19 card 78.42 32.2 1/19/2026 <span style="color:var(--vt-palette-8)">│</span>26+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">21</span><span style="background:var(--vt-palette-8)"> </span> 20 cash 67.61 25.1 1/20/2026 <span style="color:var(--vt-palette-8)">│</span>27+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">22</span><span style="background:var(--vt-palette-8)"> </span> 21 voucher 56.8 18 1/21/2026 <span style="color:var(--vt-palette-8)">│</span>28+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">23</span><span style="background:var(--vt-palette-8)"> </span> 22 card 45.99 10.9 1/22/2026 <span style="color:var(--vt-palette-8)">│</span>29+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span> 23 cash 35.18 3.8 1/23/2026 <span style="color:var(--vt-palette-8)">│</span>30+Rows must meet the condition <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">condition</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Enter</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">apply</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">cancel</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-formulas-light.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B7</span><span style="background:var(--vt-palette-7)"> </span> =MEDIAN(trips[fare])3+<span style="color:var(--vt-palette-1)">#VALUE!</span> <span style="color:var(--vt-palette-0)">MEDIAN</span> <span style="color:var(--vt-palette-0)">would</span> <span style="color:var(--vt-palette-0)">hold</span> <span style="color:var(--vt-palette-0)">2,000,000</span> <span style="color:var(--vt-palette-0)">cells</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">trips,</span> <span style="color:var(--vt-palette-0)">past</span> <span style="color:var(--vt-palette-0)">max-cells</span> <span style="color:var(--vt-palette-0)">(1,000,000):</span> <span style="color:var(--vt-palette-0)">SUMIFS,</span> <span style="color:var(--vt-palette-0)">XLOOKUP</span> <span style="color:var(--vt-palette-0)">and</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">H</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">I</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> Trips 20000006+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> Fares 959900007+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> By card 411385638+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> Vouchers 2857149+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> Longest 39.910+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> Trip 1M 3.8111+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">7</span><span style="background:var(--vt-palette-6)"> </span> Median <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);text-decoration:underline wavy var(--vt-palette-1)">#VALUE!</span><span style="background:var(--vt-palette-6)"> </span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span>14+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">10</span><span style="background:var(--vt-palette-7)"> </span>15+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">11</span><span style="background:var(--vt-palette-7)"> </span>16+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">12</span><span style="background:var(--vt-palette-7)"> </span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">13</span><span style="background:var(--vt-palette-7)"> </span>18+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">14</span><span style="background:var(--vt-palette-7)"> </span>19+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">15</span><span style="background:var(--vt-palette-7)"> </span>20+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">16</span><span style="background:var(--vt-palette-7)"> </span>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17</span><span style="background:var(--vt-palette-7)"> </span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">18</span><span style="background:var(--vt-palette-7)"> </span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">19</span><span style="background:var(--vt-palette-7)"> </span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">20</span><span style="background:var(--vt-palette-7)"> </span>25+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">21</span><span style="background:var(--vt-palette-7)"> </span>26+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">22</span><span style="background:var(--vt-palette-7)"> </span>27+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">23</span><span style="background:var(--vt-palette-7)"> </span>28+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">24</span><span style="background:var(--vt-palette-7)"> </span>29+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">25</span><span style="background:var(--vt-palette-7)"> </span>30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> ▦ trips <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-formulas.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B7</span><span style="background:var(--vt-palette-8)"> </span> =MEDIAN(trips[fare])3+<span style="color:var(--vt-palette-1)">#VALUE!</span> <span style="color:var(--vt-palette-8)">MEDIAN</span> <span style="color:var(--vt-palette-8)">would</span> <span style="color:var(--vt-palette-8)">hold</span> <span style="color:var(--vt-palette-8)">2,000,000</span> <span style="color:var(--vt-palette-8)">cells</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">trips,</span> <span style="color:var(--vt-palette-8)">past</span> <span style="color:var(--vt-palette-8)">max-cells</span> <span style="color:var(--vt-palette-8)">(1,000,000):</span> <span style="color:var(--vt-palette-8)">SUMIFS,</span> <span style="color:var(--vt-palette-8)">XLOOKUP</span> <span style="color:var(--vt-palette-8)">and</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> Trips 20000006+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> Fares 959900007+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> By card 411385638+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> Vouchers 2857149+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> Longest 39.910+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Trip 1M 3.8111+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">7</span><span style="background:var(--vt-palette-6)"> </span> Median <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);text-decoration:underline wavy var(--vt-palette-1)">#VALUE!</span><span style="background:var(--vt-palette-6)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">10</span><span style="background:var(--vt-palette-8)"> </span>15+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">11</span><span style="background:var(--vt-palette-8)"> </span>16+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">12</span><span style="background:var(--vt-palette-8)"> </span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">13</span><span style="background:var(--vt-palette-8)"> </span>18+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">14</span><span style="background:var(--vt-palette-8)"> </span>19+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">15</span><span style="background:var(--vt-palette-8)"> </span>20+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">16</span><span style="background:var(--vt-palette-8)"> </span>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17</span><span style="background:var(--vt-palette-8)"> </span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">18</span><span style="background:var(--vt-palette-8)"> </span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">19</span><span style="background:var(--vt-palette-8)"> </span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">20</span><span style="background:var(--vt-palette-8)"> </span>25+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">21</span><span style="background:var(--vt-palette-8)"> </span>26+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">22</span><span style="background:var(--vt-palette-8)"> </span>27+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">23</span><span style="background:var(--vt-palette-8)"> </span>28+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">24</span><span style="background:var(--vt-palette-8)"> </span>29+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">25</span><span style="background:var(--vt-palette-8)"> </span>30+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> ▦ trips <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-missing-light.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">SOURCE</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A1</span><span style="background:var(--vt-palette-7)"> </span>3+<span style="color:var(--vt-palette-3)">!</span> <span style="color:var(--vt-palette-3)">trips.parquet:</span> <span style="color:var(--vt-palette-3)">the</span> <span style="color:var(--vt-palette-3)">file</span> <span style="color:var(--vt-palette-3)">isn&#39;t</span> <span style="color:var(--vt-palette-3)">there</span>4+<span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span>6+<span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────────────────────</span>7+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>8+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>9+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>10+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>11+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>12+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>13+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>14+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>15+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>16+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>17+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>18+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>19+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>20+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>21+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>22+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>23+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>24+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>25+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>26+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>27+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>28+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>29+<span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">┃</span>30+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">▦</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">trips</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">!</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> rides.012 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-missing.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">SOURCE</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A1</span><span style="background:var(--vt-palette-8)"> </span>3+<span style="color:var(--vt-palette-3)">!</span> <span style="color:var(--vt-palette-3)">trips.parquet:</span> <span style="color:var(--vt-palette-3)">the</span> <span style="color:var(--vt-palette-3)">file</span> <span style="color:var(--vt-palette-3)">isn&#39;t</span> <span style="color:var(--vt-palette-3)">there</span>4+<span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span>6+<span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────────────────────</span>7+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>8+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>9+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>10+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>11+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>12+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>13+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>14+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>15+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>16+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>17+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>18+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>19+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>20+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>21+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>22+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>23+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>24+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>25+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>26+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>27+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>28+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>29+<span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">┃</span>30+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">▦</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">trips</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">!</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> rides.012 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-tab-high-contrast.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">F</span><span style="color:#ffffff;background:#1f1f1f">ile</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">E</span><span style="color:#ffffff;background:#1f1f1f">dit</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">V</span><span style="color:#ffffff;background:#1f1f1f">iew</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">I</span><span style="color:#ffffff;background:#1f1f1f">nsert</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">F</span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">o</span><span style="color:#ffffff;background:#1f1f1f">rmat</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">D</span><span style="color:#ffffff;background:#1f1f1f">ata</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f;text-decoration:underline">H</span><span style="color:#ffffff;background:#1f1f1f">elp</span><span style="background:#1f1f1f"> </span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">SOURCE</span><span style="background:#40e0ff"> </span>2+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">C1867502</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span>3+<span style="color:#959595;background:#000000">▦</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">trips.parquet</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">2,000,000</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">rows,</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">5</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">columns;</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">sorted</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">by</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">fare</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">Z</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">to</span><span style="background:#000000"> </span><span style="color:#959595;background:#000000">A</span><span style="background:#000000"> </span>4+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">A</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">B</span><span style="background:#3a3a3a"> </span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">C</span><span style="background:#40e0ff"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">D</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">E</span><span style="background:#3a3a3a"> </span>5+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1</span><span style="background:#3a3a3a"> </span><span style="background:#000000;text-decoration:underline"> </span><span style="color:#40e0ff;background:#000000;font-weight:bold;text-decoration:underline">trip</span><span style="background:#000000;text-decoration:underline"> </span><span style="color:#40e0ff;background:#000000;font-weight:bold;text-decoration:underline">payment</span><span style="background:#000000;text-decoration:underline"> </span><span style="color:#40e0ff;background:#000000;font-weight:bold;text-decoration:underline">fare</span><span style="background:#000000;text-decoration:underline"> </span><span style="color:#40e0ff;background:#000000;font-weight:bold;text-decoration:underline">miles</span><span style="background:#000000;text-decoration:underline"> </span><span style="color:#40e0ff;background:#000000;font-weight:bold;text-decoration:underline">day</span><span style="background:#000000;text-decoration:underline"> </span><span style="background:#000000"> </span>6+<span style="color:#757575;background:#000000">────────────────────────────────────────────────────────────────────────────────────────────────────</span>7+<span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">1867502</span><span style="background:#40e0ff"> </span><span style="color:#ffffff;background:#000000">1867501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cash</span><span style="background:#000000"> </span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff">48</span><span style="background:#40e0ff"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">6/30/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>8+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1876502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1876501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">app</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>9+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1885502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1885501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cash</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>10+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1894502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1894501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">voucher</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">6/30/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>11+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1903502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1903501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>12+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1912502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1912501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>13+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1921502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1921501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">6/30/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>14+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1930502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1930501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cash</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>15+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1939502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1939501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">app</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>16+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1948502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1948501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cash</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">6/30/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>17+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1957502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1957501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">voucher</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1/1/2026</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000;font-weight:bold">┃</span>18+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1966502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1966501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>19+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1975502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1975501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">6/30/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>20+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1984502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1984501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>21+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">1993502</span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#000000">1993501</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cash</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">48</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>22+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">8823</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">8822</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cash</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">47.99</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10.9</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">7/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>23+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">17823</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">17822</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">voucher</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">47.99</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30.9</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1/2/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>24+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">26823</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">26822</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">47.99</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10.9</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4/2/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>25+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">35823</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">35822</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">47.99</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30.9</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">7/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>26+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">44823</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">44822</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">card</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">47.99</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10.9</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1/2/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>27+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">53823</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">53822</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cash</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">47.99</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30.9</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">4/2/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>28+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">62823</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">62822</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">app</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">47.99</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">10.9</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">7/1/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>29+<span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">71823</span><span style="background:#3a3a3a"> </span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">71822</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">cash</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">47.99</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">30.9</span><span style="background:#000000"> </span><span style="color:#ffffff;background:#000000">1/2/2026</span><span style="background:#000000"> </span><span style="color:#757575;background:#000000">│</span>30+<span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">Sheet1</span><span style="background:#1f1f1f"> </span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">▦</span><span style="background:#40e0ff"> </span><span style="color:#000000;background:#40e0ff;font-weight:bold">trips</span><span style="background:#40e0ff"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">+</span><span style="background:#1f1f1f"> </span><span style="color:#757575;background:#1f1f1f">│</span><span style="background:#1f1f1f"> </span><span style="color:#ffffff;background:#1f1f1f">untitled</span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">modified</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Ctrl+K</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">search</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">Ctrl+/</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">shortcuts</span><span style="background:#1f1f1f"> </span><span style="background:#3a3a3a"> </span><span style="color:#ffffff;background:#3a3a3a">F10</span><span style="background:#3a3a3a"> </span><span style="background:#1f1f1f"> </span><span style="color:#959595;background:#1f1f1f">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-tab-light.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">SOURCE</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C1867502</span><span style="background:var(--vt-palette-7)"> </span> 483+<span style="color:var(--vt-palette-0)">▦</span> <span style="font-weight:bold">trips.parquet</span> <span style="color:var(--vt-palette-0)">2,000,000</span> <span style="color:var(--vt-palette-0)">rows,</span> <span style="color:var(--vt-palette-0)">5</span> <span style="color:var(--vt-palette-0)">columns;</span> <span style="color:var(--vt-palette-0)">sorted</span> <span style="color:var(--vt-palette-0)">by</span> <span style="color:var(--vt-palette-0)">fare</span> <span style="color:var(--vt-palette-0)">Z</span> <span style="color:var(--vt-palette-0)">to</span> <span style="color:var(--vt-palette-0)">A</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">C</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">trip</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">payment</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">fare</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">miles</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">day</span><span style="text-decoration:underline"> </span>6+<span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────────────────────</span>7+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1867502</span><span style="background:var(--vt-palette-6)"> </span>1867501 cash <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">48</span><span style="background:var(--vt-palette-6)"> </span> 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1876502</span><span style="background:var(--vt-palette-7)"> </span>1876501 app 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1885502</span><span style="background:var(--vt-palette-7)"> </span>1885501 cash 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1894502</span><span style="background:var(--vt-palette-7)"> </span>1894501 voucher 48 10 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1903502</span><span style="background:var(--vt-palette-7)"> </span>1903501 card 48 30 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1912502</span><span style="background:var(--vt-palette-7)"> </span>1912501 card 48 10 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1921502</span><span style="background:var(--vt-palette-7)"> </span>1921501 card 48 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>14+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1930502</span><span style="background:var(--vt-palette-7)"> </span>1930501 cash 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>15+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1939502</span><span style="background:var(--vt-palette-7)"> </span>1939501 app 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>16+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1948502</span><span style="background:var(--vt-palette-7)"> </span>1948501 cash 48 10 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1957502</span><span style="background:var(--vt-palette-7)"> </span>1957501 voucher 48 30 1/1/2026 <span style="font-weight:bold">┃</span>18+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1966502</span><span style="background:var(--vt-palette-7)"> </span>1966501 card 48 10 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>19+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1975502</span><span style="background:var(--vt-palette-7)"> </span>1975501 card 48 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>20+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1984502</span><span style="background:var(--vt-palette-7)"> </span>1984501 card 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1993502</span><span style="background:var(--vt-palette-7)"> </span>1993501 cash 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8823</span><span style="background:var(--vt-palette-7)"> </span> 8822 cash 47.99 10.9 7/1/2026 <span style="color:var(--vt-palette-8)">│</span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">17823</span><span style="background:var(--vt-palette-7)"> </span> 17822 voucher 47.99 30.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">26823</span><span style="background:var(--vt-palette-7)"> </span> 26822 card 47.99 10.9 4/2/2026 <span style="color:var(--vt-palette-8)">│</span>25+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">35823</span><span style="background:var(--vt-palette-7)"> </span> 35822 card 47.99 30.9 7/1/2026 <span style="color:var(--vt-palette-8)">│</span>26+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">44823</span><span style="background:var(--vt-palette-7)"> </span> 44822 card 47.99 10.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>27+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">53823</span><span style="background:var(--vt-palette-7)"> </span> 53822 cash 47.99 30.9 4/2/2026 <span style="color:var(--vt-palette-8)">│</span>28+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">62823</span><span style="background:var(--vt-palette-7)"> </span> 62822 app 47.99 10.9 7/1/2026 <span style="color:var(--vt-palette-8)">│</span>29+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">71823</span><span style="background:var(--vt-palette-7)"> </span> 71822 cash 47.99 30.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>30+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">▦</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">trips</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/source-tab.html +30 -0
@@ -0,0 +1,30 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">SOURCE</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C1867502</span><span style="background:var(--vt-palette-8)"> </span> 483+<span style="color:var(--vt-palette-8)">▦</span> <span style="font-weight:bold">trips.parquet</span> <span style="color:var(--vt-palette-8)">2,000,000</span> <span style="color:var(--vt-palette-8)">rows,</span> <span style="color:var(--vt-palette-8)">5</span> <span style="color:var(--vt-palette-8)">columns;</span> <span style="color:var(--vt-palette-8)">sorted</span> <span style="color:var(--vt-palette-8)">by</span> <span style="color:var(--vt-palette-8)">fare</span> <span style="color:var(--vt-palette-8)">Z</span> <span style="color:var(--vt-palette-8)">to</span> <span style="color:var(--vt-palette-8)">A</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">C</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">trip</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">payment</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">fare</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">miles</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">day</span><span style="text-decoration:underline"> </span>6+<span style="color:var(--vt-palette-8)">────────────────────────────────────────────────────────────────────────────────────────────────────</span>7+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1867502</span><span style="background:var(--vt-palette-6)"> </span>1867501 cash <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">48</span><span style="background:var(--vt-palette-6)"> </span> 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1876502</span><span style="background:var(--vt-palette-8)"> </span>1876501 app 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1885502</span><span style="background:var(--vt-palette-8)"> </span>1885501 cash 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1894502</span><span style="background:var(--vt-palette-8)"> </span>1894501 voucher 48 10 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1903502</span><span style="background:var(--vt-palette-8)"> </span>1903501 card 48 30 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1912502</span><span style="background:var(--vt-palette-8)"> </span>1912501 card 48 10 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1921502</span><span style="background:var(--vt-palette-8)"> </span>1921501 card 48 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>14+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1930502</span><span style="background:var(--vt-palette-8)"> </span>1930501 cash 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>15+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1939502</span><span style="background:var(--vt-palette-8)"> </span>1939501 app 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>16+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1948502</span><span style="background:var(--vt-palette-8)"> </span>1948501 cash 48 10 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1957502</span><span style="background:var(--vt-palette-8)"> </span>1957501 voucher 48 30 1/1/2026 <span style="font-weight:bold">┃</span>18+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1966502</span><span style="background:var(--vt-palette-8)"> </span>1966501 card 48 10 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>19+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1975502</span><span style="background:var(--vt-palette-8)"> </span>1975501 card 48 30 6/30/2026 <span style="color:var(--vt-palette-8)">│</span>20+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1984502</span><span style="background:var(--vt-palette-8)"> </span>1984501 card 48 10 1/1/2026 <span style="color:var(--vt-palette-8)">│</span>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1993502</span><span style="background:var(--vt-palette-8)"> </span>1993501 cash 48 30 4/1/2026 <span style="color:var(--vt-palette-8)">│</span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8823</span><span style="background:var(--vt-palette-8)"> </span> 8822 cash 47.99 10.9 7/1/2026 <span style="color:var(--vt-palette-8)">│</span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">17823</span><span style="background:var(--vt-palette-8)"> </span> 17822 voucher 47.99 30.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">26823</span><span style="background:var(--vt-palette-8)"> </span> 26822 card 47.99 10.9 4/2/2026 <span style="color:var(--vt-palette-8)">│</span>25+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">35823</span><span style="background:var(--vt-palette-8)"> </span> 35822 card 47.99 30.9 7/1/2026 <span style="color:var(--vt-palette-8)">│</span>26+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">44823</span><span style="background:var(--vt-palette-8)"> </span> 44822 card 47.99 10.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>27+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">53823</span><span style="background:var(--vt-palette-8)"> </span> 53822 cash 47.99 30.9 4/2/2026 <span style="color:var(--vt-palette-8)">│</span>28+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">62823</span><span style="background:var(--vt-palette-8)"> </span> 62822 app 47.99 10.9 7/1/2026 <span style="color:var(--vt-palette-8)">│</span>29+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">71823</span><span style="background:var(--vt-palette-8)"> </span> 71822 cash 47.99 30.9 1/2/2026 <span style="color:var(--vt-palette-8)">│</span>30+ Sheet1 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">▦</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">trips</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file
M internal/functions/stream.go +1 -1
M internal/ui/commands.go +4 -2
M internal/ui/filterpick/filterpick.go +19 -2
@@ -48,6 +48,7 @@ type Picker struct {
4848 field int // 0 is the search, 1 the condition's value4949 fields [2]string // the text of each field5050 shown []int // values matching the search, best first51+ condOnly bool // no values to tick, only the condition: OnlyCondition5152 overlay.List // over the rows: "Select all", then shown5253 }5354
@@ -76,6 +77,14 @@ func (p *Picker) Start() {
7677 p.search()7778 }7879 80+// OnlyCondition has the picker ask for the condition alone, with no81+// values to tick, for a column whose values are too many to list (a82+// linked source's). Call it after Start.83+func (p *Picker) OnlyCondition() {84+ p.condOnly = true85+ p.focus(1)86+}87+7988 // close closes the picker without applying it.8089 func (p *Picker) close() {8190 p.h.Close()
@@ -211,7 +220,9 @@ func (p *Picker) Key(k tea.KeyPressMsg) tea.Cmd {
211220 case key == "enter":212221 p.apply()213222 case key == "tab" || key == "shift+tab":214- p.focus(1 - p.field)223+ if !p.condOnly {224+ p.focus(1 - p.field)225+ }215226 case p.field == 1 && (key == "up" || key == "down"):216227 if key == "up" {217228 p.cycle(-1)
@@ -264,7 +275,7 @@ func (p *Picker) Mouse(e overlay.MouseEvent) tea.Cmd {
264275 case x == ansi.StringWidth(p.condChip())-1:265276 p.cycle(1)266277 }267- case e.Row == 3 && e.Kind == overlay.MousePress:278+ case e.Row == 3 && e.Kind == overlay.MousePress && !p.condOnly:268279 p.focus(0)269280 case e.Row < firstRow || i >= n || i >= p.Top+p.rows():270281 case e.Kind == overlay.MouseMotion:
@@ -278,6 +289,9 @@ func (p *Picker) Mouse(e overlay.MouseEvent) tea.Cmd {
278289 279290 func (p *Picker) Status() (string, string) {280291 th := p.h.Theme()292+ if p.condOnly {293+ return "Rows must meet the condition", th.KeyHints("Up/Down", "condition", "Enter", "apply", "Esc", "cancel")294+ }281295 if p.field == 1 {282296 return "Rows must also meet the condition", th.KeyHints("Up/Down", "condition", "Tab", "values", "Enter", "apply", "Esc", "cancel")283297 }
@@ -362,6 +376,9 @@ func (p *Picker) Layout() []overlay.Box {
362376 if search == "" {363377 input += th.Muted.Render("Search values")364378 }379+ if p.condOnly {380+ return []overlay.Box{{ID: ID, X: x, Y: y, Lines: th.Frame(inner, p.title, "", []string{theme.Cells(th.MenuBar, cond, inner)})}}381+ }365382 lines := []string{theme.Cells(th.MenuBar, cond, inner), theme.SepRow, theme.Cells(th.MenuBar, input, inner), theme.SepRow}366383 for r := range rows {367384 lines = append(lines, p.row(th, p.Top+r, inner))
M internal/ui/filterpick/filterpick_test.go +26 -0
M internal/ui/panel.go +1 -2
M internal/ui/srccmds.go +17 -3
@@ -91,8 +91,21 @@ func (m *Model) sourceCommand(c *command) (tea.Cmd, bool) {
9191 return nil, false9292 }9393 94-// sourceAvailable reports whether a command runs on a source's tab.95-func sourceAvailable(id string) bool { return notebookSafe(id) || sourceTakes[id] }94+// sourceEnabled says whether a command runs on a source's tab, when95+// the tab decides it: those that would change cells don't, and those96+// acting on the source do, whatever they say of a sheet.97+func sourceEnabled(m *Model, id string) (on, decided bool) {98+ switch {99+ case notebookSafe(id), id == "data.source_all", id == "data.source_reload":100+ return false, false101+ case !sourceTakes[id]:102+ return false, true103+ case id == "data.filter_remove":104+ info, _ := m.sheet.Source()105+ return info.Source.Order != nil && len(info.Source.Order.Filter) > 0, true106+ }107+ return true, true108+}96109 97110 // copyOrder is a copy of o to change, never nil.98111 func copyOrder(o *sheet.SourceOrder) *sheet.SourceOrder {
@@ -124,7 +137,7 @@ func (m *Model) openSourceFilter(info sheet.SourceInfo, col int) {
124137 }125138 }126139 title := "Filter " + sheet.ColName(col) + " " + m.sourceCol(info, col)127- m.openValuesPicker(title, 0, nil, cond, func(cr sheet.Criteria) {140+ p := m.openValuesPicker(title, m.srcView().ColX(col), nil, cond, func(cr sheet.Criteria) {128141 o := copyOrder(info.Source.Order)129142 o.Filter = slices.DeleteFunc(o.Filter, func(f sheet.SourceFilter) bool { return f.Col == col })130143 if cr.Cond.Op != sheet.CondNone {
@@ -132,6 +145,7 @@ func (m *Model) openSourceFilter(info sheet.SourceInfo, col int) {
132145 }133146 m.setSourceOrder(info, o)134147 })148+ p.OnlyCondition()135149 }136150 137151 // sourcePivot makes a pivot table over the whole source on a new sheet,
M internal/ui/srcscreen.go +13 -2
M internal/ui/srcview/draw.go +13 -0
M internal/ui/srcview/keys.go +11 -4
M internal/ui/srcview/view.go +1 -1
A internal/ui/srcview/view_test.go +159 -0
@@ -0,0 +1,159 @@
1+package srcview2+3+import (4+ "strconv"5+ "strings"6+ "testing"7+8+ tea "charm.land/bubbletea/v2"9+ "github.com/charmbracelet/x/ansi"10+11+ "github.com/FelineStateMachine/012/internal/locale"12+ "github.com/FelineStateMachine/012/internal/sheet"13+ "github.com/FelineStateMachine/012/internal/ui/theme"14+)15+16+// fakeHost is a source of n rows, row i holding i and "r<i>", whose17+// rows from lost on are on their way.18+type fakeHost struct {19+ th theme.Theme20+ n, lost int6421+ from, to int64 // what was last wanted22+ counted bool23+ wantCalled int24+}25+26+func newHost(n int64) *fakeHost {27+ return &fakeHost{th: theme.New(true), n: n, lost: n, counted: true}28+}29+30+func (h *fakeHost) Theme() *theme.Theme { return &h.th }31+func (h *fakeHost) Locale() *locale.Locale { return locale.Canonical }32+func (h *fakeHost) Columns() []Column { return []Column{{Name: "id"}, {Name: "name"}} }33+func (h *fakeHost) Rows() (int64, bool) { return h.n, h.counted }34+35+func (h *fakeHost) Row(i int64) (int64, []sheet.LiveCell, bool) {36+ if i >= h.lost || i >= h.n {37+ return 0, nil, false38+ }39+ return i, []sheet.LiveCell{{V: sheet.Value{Kind: sheet.Number, Num: float64(i)}},40+ {V: sheet.Value{Kind: sheet.Text, Str: "r" + strconv.FormatInt(i, 10)}}}, true41+}42+43+func (h *fakeHost) Want(from, to int64) { h.from, h.to, h.wantCalled = from, to, h.wantCalled+1 }44+45+func view(n int64, width, height int) (*View, *fakeHost) {46+ h := newHost(n)47+ v := New(h)48+ v.Resize(width, height)49+ return v, h50+}51+52+func key(s string) tea.KeyPressMsg {53+ switch s {54+ case "down":55+ return tea.KeyPressMsg{Code: tea.KeyDown}56+ case "pgdown":57+ return tea.KeyPressMsg{Code: tea.KeyPgDown}58+ case "ctrl+down":59+ return tea.KeyPressMsg{Code: tea.KeyDown, Mod: tea.ModCtrl}60+ case "ctrl+home":61+ return tea.KeyPressMsg{Code: tea.KeyHome, Mod: tea.ModCtrl}62+ case "end":63+ return tea.KeyPressMsg{Code: tea.KeyEnd}64+ }65+ return tea.KeyPressMsg{Code: []rune(s)[0], Text: s}66+}67+68+func TestDrawNumbersRowsAsTheTab(t *testing.T) {69+ v, h := view(10_000_000, 60, 13)70+ lines := v.Draw()71+ if len(lines) != 13 {72+ t.Fatalf("%d lines", len(lines))73+ }74+ if l := ansi.Strip(lines[namesLine]); !strings.Contains(l, "1 id") && !strings.Contains(l, "id") {75+ t.Errorf("header row %q", l)76+ }77+ if l := ansi.Strip(lines[firstRow]); !strings.HasPrefix(strings.TrimSpace(l), "2 ") || !strings.Contains(l, "r0") {78+ t.Errorf("first row %q", l)79+ }80+ if h.from > 0 || h.to < 10 {81+ t.Errorf("wanted %d to %d", h.from, h.to)82+ }83+ // The last row is the tab's row 10,000,001, and the thumb is at the84+ // bottom of the track.85+ v.Key(key("ctrl+down"))86+ lines = v.Draw()87+ last := ansi.Strip(lines[len(lines)-1])88+ if !strings.HasPrefix(strings.TrimSpace(last), "10000001 ") || !strings.HasSuffix(last, "┃") {89+ t.Errorf("last row %q", last)90+ }91+ if first := ansi.Strip(lines[firstRow]); !strings.HasSuffix(first, "│") {92+ t.Errorf("the track at the top %q", first)93+ }94+}95+96+func TestRowsOnTheirWay(t *testing.T) {97+ v, h := view(100, 60, 10)98+ h.lost = 399+ lines := v.Draw()100+ if l := ansi.Strip(lines[firstRow+3]); !strings.Contains(l, "…") {101+ t.Errorf("a row on its way %q", l)102+ }103+ if _, _, ok := v.Cell(); !ok {104+ t.Error("the first row is here")105+ }106+ h.counted = false107+ v.Refit()108+ v.Draw()109+ if v.fitted {110+ t.Error("fitted before the rows were counted")111+ }112+}113+114+func TestKeysAndScroll(t *testing.T) {115+ v, _ := view(1000, 60, 13)116+ v.Draw()117+ v.Key(key("down"))118+ if row, _ := v.Active(); row != 1 {119+ t.Errorf("down: row %d", row)120+ }121+ v.Key(key("pgdown"))122+ if row, _ := v.Active(); row != int64(v.Lines()) || v.Top() != int64(v.Lines()-1) {123+ t.Errorf("page down: row %d top %d", row, v.Top())124+ }125+ v.Key(key("end"))126+ if _, col := v.Active(); col != 1 {127+ t.Errorf("end: column %d", col)128+ }129+ v.Key(key("ctrl+home"))130+ if row, col := v.Active(); row != 0 || col != 0 || v.Top() != 0 {131+ t.Errorf("ctrl+home: %d %d top %d", row, col, v.Top())132+ }133+ if v.Key(key("x")) {134+ t.Error("a letter is the view's")135+ }136+ v.Scroll(500)137+ if row, _ := v.Active(); row < v.Top() || row >= v.Top()+int64(v.Lines()) {138+ t.Errorf("the active row %d left the window from %d", row, v.Top())139+ }140+}141+142+func TestMouse(t *testing.T) {143+ v, _ := view(1000, 60, 13)144+ v.Draw()145+ if hit := v.HitAt(59, firstRow+2); !hit.Bar {146+ t.Errorf("the scrollbar isn't hit: %+v", hit)147+ }148+ v.DragBar(firstRow + v.Lines() - 1)149+ if last := int64(1000 - v.Lines()); v.Top() != last {150+ t.Errorf("the thumb dragged to the bottom shows from %d, want %d", v.Top(), last)151+ }152+ x := v.ColX(1)153+ if hit := v.HitAt(x, firstRow); !hit.Cell || hit.Col != 1 || hit.Row != v.Top() {154+ t.Errorf("a click on column B: %+v", hit)155+ }156+ if hit := v.HitAt(x, namesLine); hit.Cell || hit.Bar {157+ t.Errorf("the header row is hit: %+v", hit)158+ }159+}