Line 0 is now the menu bar (File Edit View Format Help, accelerator letters underlined, menus with no registered commands hidden) with the mode indicator on the right. Line 1 is the formula bar: a fixed-width name box showing the active cell or the selected range, then the cell's contents; entries are typed here with the terminal cursor and still show in the cell. Line 2 is the context line for prompts, formula errors and key hints, with prompt keys or the file list on the right. Commands can say when they are unavailable, and shortcuts list Ctrl combinations first, as Sheets shows them.
- commit
- 71a8e6cb649f80b4d32a6a9bccc58bbfb6f07a7f
- parent
- 3fe4c54f48a0c208a55d7d4ba37383bd2da71039
- author
- Dami <dami.etoile@gmail.com>
@@ -10,8 +10,44 @@ import (
1010 ghostty "go.mitchellh.com/libghostty"1111 )1212 13-// Screen layout: panel lines 0-2, column header 3, grid row 1 on line 4.14-const gridRow1 = 413+// Screen layout: menu bar 0, formula bar 1 (an 11-wide name box and a14+// space, then the contents), context line 2, column header 3, grid row 115+// on line 4.16+const (17+ barLine = 118+ barX = 1219+ contextLine = 220+ gridRow1 = 421+)22+23+// waitForBar waits for the formula bar to show name in the name box and24+// text after it.25+func (s *session) waitForBar(name, text string) {26+ s.t.Helper()27+ s.eventually(fmt.Sprintf("formula bar %s %q", name, text), func() bool {28+ l := s.line(barLine)29+ return len(l) >= barX && strings.TrimSpace(l[:barX]) == name && l[barX:] == text30+ })31+}32+33+// waitForName waits for the name box to show name.34+func (s *session) waitForName(name string) {35+ s.t.Helper()36+ s.eventually(fmt.Sprintf("name box %s", name), func() bool {37+ l := s.line(barLine)38+ return strings.TrimSpace(l[:min(barX, len(l))]) == name39+ })40+}41+42+// waitForEntry waits for the formula bar to show text, whatever the name43+// box says; used while typing.44+func (s *session) waitForEntry(text string) {45+ s.t.Helper()46+ s.eventually(fmt.Sprintf("formula bar %q", text), func() bool {47+ l := s.line(barLine)48+ return len(l) >= barX && l[barX:] == text49+ })50+}1551 1652 // numRow is how a grid row of numbers in default-width (10) columns looks:1753 // a 6-wide row header, then each number right-aligned with one column of@@ -28,7 +64,7 @@ func TestFormulaRecalc(t *testing.T) {
2864 s := start(t, "")2965 s.keys("10", "<enter>", "20", "<enter>", "=SUM(A1:A2)", "<enter>")3066 s.keys("<up>")31- s.waitFor("A3 =SUM(A1:A2)")67+ s.waitForBar("A3", "=SUM(A1:A2)")3268 s.waitForLine(gridRow1+2, numRow(3, "30"))3369 3470 // Changing a precedent recalculates the total on screen.@@ -40,11 +76,11 @@ func TestPointModeBuildsFormula(t *testing.T) {
4076 s := start(t, "")4177 s.keys("4", "<tab>", "6", "<tab>", "=", "<left>", "<left>")4278 s.waitFor("POINT")43- s.waitForLine(1, "=A1")79+ s.waitForBar("C1", "=A1")4480 s.keys("*", "<left>", "<enter>")4581 // Enter after a run of Tabs returns to the starting column, as in Sheets.4682 s.keys("<up>", "<right>", "<right>")47- s.waitFor("C1 =A1*B1")83+ s.waitForBar("C1", "=A1*B1")4884 s.waitForLine(gridRow1, numRow(1, "4", "6", "24"))4985 }5086 @@ -76,7 +112,7 @@ func TestInvalidFormulaCursor(t *testing.T) {
76112 // The real terminal cursor sits on the edit line at the error.77113 s.eventually("cursor at end of edit line", func() bool {78114 x, y := s.cursor()79- return x == len("=SUM(A1") && y == 1115+ return x == barX+len("=SUM(A1") && y == barLine80116 })81117 }82118 @@ -139,7 +175,7 @@ func TestCopyPasteAdjustsReferences(t *testing.T) {
139175 s.eventually("system clipboard", func() bool { return clip.get() == "20" })140176 s.keys("<down>", "<ctrl+v>")141177 s.waitFor("Pasted 1 cell at B2")142- s.waitFor("B2 =A2*10")178+ s.waitForBar("B2", "=A2*10")143179 s.waitForLine(gridRow1+1, numRow(2, "3", "30"))144180 145181 // Esc clears the marker; the paste stays.@@ -150,7 +186,7 @@ func TestCopyPasteAdjustsReferences(t *testing.T) {
150186 s.keys("<ctrl+home>", "<ctrl+x>", "<down>", "<down>", "<down>", "<ctrl+v>")151187 s.waitFor("Moved A1 to A4")152188 s.keys("<ctrl+home>", "<right>")153- s.waitFor("B1 =A4*10")189+ s.waitForBar("B1", "=A4*10")154190 s.waitForLine(gridRow1+3, numRow(4, "2"))155191 }156192 @@ -174,7 +210,7 @@ func TestInsertRowRewritesFormulas(t *testing.T) {
174210 s.keys("<up>", "<up>", "<shift+space>", "<ctrl+alt+=>")175211 s.waitForLine(gridRow1+3, numRow(4, "3"))176212 s.keys("<down>", "<down>")177- s.waitFor("A4 =SUM(A1:A3)")213+ s.waitForBar("A4", "=SUM(A1:A3)")178214 s.keys("<up>", "<up>", "5", "<enter>")179215 s.waitForLine(gridRow1+3, numRow(4, "8"))180216 s.keys("<ctrl+z>", "<ctrl+z>")@@ -193,7 +229,7 @@ func TestFillAndAbsoluteReferences(t *testing.T) {
193229 s := start(t, "")194230 s.keys("2", "<enter>", "3", "<enter>", "4", "<enter>", "<ctrl+home>", "<right>")195231 s.keys("=A1*A1", "<left>", "<left>", "<f4>")196- s.waitForLine(1, "=A1*$A$1")232+ s.waitForEntry("=A1*$A$1")197233 s.keys("<enter>", "<up>", "<shift+down>", "<shift+down>", "<ctrl+d>")198234 s.waitForLine(gridRow1+2, numRow(3, "4", "8"))199235 s.keys("<right>", "<shift+down>", "=B1+1", "<ctrl+enter>")@@ -89,7 +89,7 @@ func colX(c int) int { return 6 + c*10 + 2 }
8989 func TestMouseClickDragAndShiftClick(t *testing.T) {9090 s := start(t, "")9191 s.click(colX(1), gridRow1+2, 0)92- s.waitFor(" B3 ")92+ s.waitForName("B3")9393 s.drag([2]int{colX(0), gridRow1}, [2]int{colX(1), gridRow1 + 1}, [2]int{colX(2), gridRow1 + 2})9494 s.eventually("drag selection", func() bool { return strings.Contains(s.line(29), "A1:C3") })9595 s.click(colX(3), gridRow1+4, ghostty.ModShift)@@ -108,17 +108,17 @@ func TestMouseClickWhileTypingAndFormulaReference(t *testing.T) {
108108 s := start(t, "")109109 s.keys("40", "<enter>", "2", "<enter>", "=")110110 s.click(colX(0), gridRow1, 0)111- s.waitForLine(1, "=A1")111+ s.waitForEntry("=A1")112112 s.keys("+")113113 s.click(colX(0), gridRow1+1, 0)114- s.waitForLine(1, "=A1+A2")114+ s.waitForEntry("=A1+A2")115115 s.keys("<enter>")116116 s.waitForLine(gridRow1+2, numRow(3, "42"))117117 118118 // Clicking another cell while typing plain text accepts it there.119119 s.keys("note")120120 s.click(colX(2), gridRow1, 0)121- s.waitFor(" C1 ")121+ s.waitForName("C1")122122 s.waitForLine(gridRow1+3, " 4 note")123123 }124124 @@ -143,5 +143,5 @@ func TestMouseDoubleClickEdits(t *testing.T) {
143143 s.click(colX(0), gridRow1, 0)144144 s.click(colX(0), gridRow1, 0)145145 s.waitFor("EDIT")146- s.waitForLine(1, "hello")146+ s.waitForEntry("hello")147147 }@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> B7 </span> =SUM(B3:B5) <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-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">H</span>elp <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="color:var(--vt-palette-0);background:var(--vt-palette-7)"> B7 </span> =SUM(B3:B5)33 44 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 23 </span>2828 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 24 </span>2929 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 25 </span>30-untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-0)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-0)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5) <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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5)33 44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5) <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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B1:B8192 </span> =SUM(B3:B5)33 44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-4)"> 1 </span> Household<span style="color:var(--vt-palette-0);background:var(--vt-palette-4)"> budget 20</span>26@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C2 </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-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">H</span>elp <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="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C2 </span>33 Copied B3:B5 <span style="font-weight:bold">Ctrl+V</span> <span style="color:var(--vt-palette-0)">paste </span><span style="font-weight:bold">Ctrl+Shift+V</span> <span style="color:var(--vt-palette-0)">paste values </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-0)">clear</span>44 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> A B </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> C </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@ Copied B3:B5 <span style="font-weight:bold">Ctrl+V</span> <span style="color:v
2727 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 23 </span>2828 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 24 </span>2929 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 25 </span>30-untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-0)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-0)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> A3 </span> Rent <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-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">H</span>elp <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="color:var(--vt-palette-0);background:var(--vt-palette-7)"> A3:B5 </span> Rent33 Cut A3:B5 <span style="font-weight:bold">Ctrl+V</span> <span style="color:var(--vt-palette-0)">move here </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-0)">cancel</span>44 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> A </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-4)"> B </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 1 </span> Household budget 2026@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A3 </span> Rent <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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A3:B5 </span> Rent33 Cut A3:B5 <span style="font-weight:bold">Ctrl+V</span> <span style="color:var(--vt-palette-8)">move here </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-8)">cancel</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-4)"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C2 </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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C2 </span>33 Copied B3:B5 <span style="font-weight:bold">Ctrl+V</span> <span style="color:var(--vt-palette-8)">paste </span><span style="font-weight:bold">Ctrl+Shift+V</span> <span style="color:var(--vt-palette-8)">paste values </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-8)">clear</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A B </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> C </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@ Copied B3:B5 <span style="font-weight:bold">Ctrl+V</span> <span style="color:v
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A1 </span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> EDIT</span><span style="background:var(--vt-palette-6)"> </span>2-=SUM(A11+<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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> EDIT</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A1 </span> =SUM(A133 <span style="color:var(--vt-palette-3)">Expected , or ) in SUM</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> A </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> 1 </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)"> =SUM(A1 </span>@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B8 </span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> ENTER</span><span style="background:var(--vt-palette-6)"> </span>2-=B7*121+<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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> ENTER</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B8 </span> =B7*1233 <span style="font-weight:bold">Enter</span> <span style="color:var(--vt-palette-8)">accept </span><span style="font-weight:bold">Tab</span> <span style="color:var(--vt-palette-8)">accept and go right </span><span style="font-weight:bold">Arrows after an operator</span> <span style="color:var(--vt-palette-8)">pick cells </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-8)">cancel</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A1 </span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> ERROR</span><span style="background:var(--vt-palette-6)"> </span>2-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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> ERROR</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A1 </span>33 44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> A </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> 1 </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)"> </span>@@ -7,12 +7,12 @@
77 Mouse Click, drag or Shift+click to select; click headers88 for whole columns or rows; double-click to edit99 10- Backspace / Del Clear Ctrl+O Open10+ Del / Backspace Clear Ctrl+O Open1111 Ctrl+C Copy Ctrl+V Paste1212 Ctrl+X Cut Ctrl+Shift+V Paste values only1313 Ctrl+Alt+- Delete rows or columns Ctrl+Q Quit1414 Esc Deselect Ctrl+Shift+Z / Ctrl+Y Redo15- Enter / F2 Edit cell Ctrl+S Save15+ F2 / Enter Edit cell Ctrl+S Save1616 Ctrl+D Fill down Ctrl+A Select all1717 Ctrl+R Fill right Ctrl+Space Select columns1818 Ctrl+G / F5 Go to Shift+Space Select rows@@ -7,12 +7,12 @@
77 Mouse Click, drag or Shift+click to select; click headers88 for whole columns or rows; double-click to edit99 10- Backspace / Del Clear Ctrl+O Open10+ Del / Backspace Clear Ctrl+O Open1111 Ctrl+C Copy Ctrl+V Paste1212 Ctrl+X Cut Ctrl+Shift+V Paste values only1313 Ctrl+Alt+- Delete rows or columns Ctrl+Q Quit1414 Esc Deselect Ctrl+Shift+Z / Ctrl+Y Redo15- Enter / F2 Edit cell Ctrl+S Save15+ F2 / Enter Edit cell Ctrl+S Save1616 Ctrl+D Fill down Ctrl+A Select all1717 Ctrl+R Fill right Ctrl+Space Select columns1818 Ctrl+G / F5 Go to Shift+Space Select rows@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5) <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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5)33 44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-6);background:var(--vt-palette-8);font-weight:bold"> A ▐</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,6 +1,6 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5) <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> MENU</span><span style="background:var(--vt-palette-6)"> </span>2-<span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">File</span> Edit Format3-New Open Save As Quit1+<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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5)3+<span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">File</span> Edit Format <span style="color:var(--vt-palette-8)">New Open Save As Quit</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 202666 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 2 </span>@@ -27,4 +27,4 @@ New Open Save As Quit
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B8 </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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B8 </span>33 Copied B7 <span style="font-weight:bold">Ctrl+V</span> <span style="color:var(--vt-palette-8)">paste </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-8)">clear</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -13,4 +13,4 @@ Copied B7 <span style="font-weight:bold">Ctrl+V</span> <span style="color:var(
1313 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 9 </span>1414 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 10 </span>1515 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 11 </span>16-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file16+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5) <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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5)33 44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -13,4 +13,4 @@
1313 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 9 </span>1414 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 10 </span>1515 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 11 </span>16-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file16+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C7 </span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> POINT</span><span style="background:var(--vt-palette-6)"> </span>2-=AVERAGE(<span style="color:var(--vt-palette-15);background:var(--vt-palette-4)">B3:B5</span>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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> POINT</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C7 </span> =AVERAGE(<span style="color:var(--vt-palette-15);background:var(--vt-palette-4)">B3:B5</span>33 <span style="font-weight:bold">Arrows</span> <span style="color:var(--vt-palette-0)">pick a cell </span><span style="font-weight:bold">Shift+arrows</span> <span style="color:var(--vt-palette-0)">pick a range </span><span style="font-weight:bold">Enter</span> <span style="color:var(--vt-palette-0)">accept </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-0)">back</span>44 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 23 </span>2828 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 24 </span>2929 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 25 </span>30-untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-0)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-0)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C7 </span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> POINT</span><span style="background:var(--vt-palette-6)"> </span>2-=AVERAGE(<span style="color:var(--vt-palette-0);background:var(--vt-palette-4)">B3:B5</span>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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> POINT</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C7 </span> =AVERAGE(<span style="color:var(--vt-palette-0);background:var(--vt-palette-4)">B3:B5</span>33 <span style="font-weight:bold">Arrows</span> <span style="color:var(--vt-palette-8)">pick a cell </span><span style="font-weight:bold">Shift+arrows</span> <span style="color:var(--vt-palette-8)">pick a range </span><span style="font-weight:bold">Enter</span> <span style="color:var(--vt-palette-8)">accept </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-8)">back</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,6 +1,6 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A1 </span> Household budget 2026 <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> WIDTH</span><span style="background:var(--vt-palette-6)"> </span>2-Column width (1-240): 133-<span style="font-weight:bold">Left/Right</span> <span style="color:var(--vt-palette-8)">adjust </span><span style="font-weight:bold">Enter</span> <span style="color:var(--vt-palette-8)">apply </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-8)">cancel</span>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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> WIDTH</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A1 </span> Household budget 20263+Column width (1-240): 13 <span style="font-weight:bold">Left/Right</span> <span style="color:var(--vt-palette-8)">adjust </span><span style="font-weight:bold">Enter</span> <span style="color:var(--vt-palette-8)">apply </span><span style="font-weight:bold">Esc</span> <span style="color:var(--vt-palette-8)">cancel</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> A </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> 1 </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)"> Household bu</span>dget 202666 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 2 </span>@@ -27,4 +27,4 @@ Column width (1-240): 13
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,6 +1,6 @@
1-<pre class="screen"><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> B7 </span> =SUM(B3:B5) <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> MENU</span><span style="background:var(--vt-palette-6)"> </span>2-Cancel <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Quit without saving</span>3-You have unsaved changes. Close one23 anyway?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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> B7 </span> =SUM(B3:B5)3+Cancel <span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Quit without saving</span> <span style="color:var(--vt-palette-0)">You have unsaved changes. Close one23 anyway?</span>44 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 1 </span> Household budget 202666 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 2 </span>@@ -27,4 +27,4 @@ You have unsaved changes. Close one23 anyway?
2727 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 23 </span>2828 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 24 </span>2929 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 25 </span>30-untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-0)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-0)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file@@ -1,6 +1,6 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5) <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> MENU</span><span style="background:var(--vt-palette-6)"> </span>2-Cancel <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Quit without saving</span>3-You have unsaved changes. Close one23 anyway?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">H</span>elp <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5)3+Cancel <span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Quit without saving</span> <span style="color:var(--vt-palette-8)">You have unsaved changes. Close one23 anyway?</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 202666 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 2 </span>@@ -27,4 +27,4 @@ You have unsaved changes. Close one23 anyway?
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A1 </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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A1 </span>33 44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> A </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> 1 </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)"> </span>@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> B7 </span> =SUM(B3:B5) <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-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">H</span>elp <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="color:var(--vt-palette-0);background:var(--vt-palette-7)"> B7 </span> =SUM(B3:B5)33 <span style="font-weight:bold">Column A</span><span style="color:var(--vt-palette-0)"> width </span>16<span style="color:var(--vt-palette-0)"> double-click the border to fit</span>44 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> A </span><span style="color:var(--vt-palette-6);background:var(--vt-palette-7);font-weight:bold">▐</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C D E F G H </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 23 </span>2828 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 24 </span>2929 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 25 </span>30-untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-0)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-0)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-0)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-0)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-0)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5) <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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B7 </span> =SUM(B3:B5)33 <span style="font-weight:bold">Column A</span><span style="color:var(--vt-palette-8)"> width </span>16<span style="color:var(--vt-palette-8)"> double-click the border to fit</span>44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-6);background:var(--vt-palette-8);font-weight:bold">▐</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -27,4 +27,4 @@
2727 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 23 </span>2828 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 24 </span>2929 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 25 </span>30-untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">F1</span> <span style="color:var(--vt-palette-8)">help </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file30+untitled<span style="color:var(--vt-palette-8)"> modified</span> <span style="font-weight:bold">Ctrl+/</span> <span style="color:var(--vt-palette-8)">shortcuts </span><span style="font-weight:bold">F10</span> <span style="color:var(--vt-palette-8)">menu </span><span style="font-weight:bold">Ctrl+Q</span> <span style="color:var(--vt-palette-8)">quit</span></pre>\ No newline at end of file@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> B3 </span> 1450 <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-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">H</span>elp <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="color:var(--vt-palette-0);background:var(--vt-palette-7)"> B3:B5 </span> 145033 44 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-0);background:var(--vt-palette-7)"> 1 </span> Household budget 2026@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B3 </span> 1450 <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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B3:B5 </span> 145033 44 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -1,5 +1,5 @@
1-<pre class="screen"><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B5 </span> 96 <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-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">H</span>elp <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="color:var(--vt-palette-15);background:var(--vt-palette-8)"> B5:B6 </span> 9633 Undid: clear B5:B644 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> A </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold"> B </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> C D E F G H I </span>55 <span style="color:var(--vt-palette-15);background:var(--vt-palette-8)"> 1 </span> Household budget 2026@@ -1,7 +1,9 @@
11 package ui22 33 import (4+ "cmp"45 "slices"6+ "strings"57 68 tea "charm.land/bubbletea/v2"79 @@ -16,6 +18,15 @@ type command struct {
1618 title string // shown in menus, the palette and help, e.g. "Save"1719 desc string // one line, shown on the third panel line1820 run func(m *Model) tea.Cmd21+22+ // enabled, when set, reports whether the command can run right now.23+ // Menus and the palette show unavailable commands dimmed.24+ enabled func(m *Model) bool25+}26+27+// available reports whether the command can run in m's current state.28+func (c *command) available(m *Model) bool {29+ return c.enabled == nil || c.enabled(m)1930 }2031 2132 var commands = map[string]*command{}@@ -50,7 +61,9 @@ var keymap = map[string]string{
5061 "ctrl+q": "quit",5162 }5263 53-// keysFor returns the shortcuts bound to a command, sorted.64+// keysFor returns the shortcuts bound to a command, the one to show first65+// leading: Ctrl combinations (what Sheets shows), then function keys, then66+// Alt combinations and named keys.5467 func keysFor(id string) []string {5568 var keys []string5669 for k, c := range keymap {@@ -58,10 +71,35 @@ func keysFor(id string) []string {
5871 keys = append(keys, k)5972 }6073 }61- slices.Sort(keys)74+ slices.SortFunc(keys, func(a, b string) int {75+ return cmp.Or(cmp.Compare(keyRank(a), keyRank(b)), cmp.Compare(a, b))76+ })6277 return keys6378 }6479 80+func keyRank(k string) int {81+ switch {82+ case strings.HasPrefix(k, "ctrl+"):83+ return 084+ case len(k) >= 2 && k[0] == 'f' && k[1] >= '0' && k[1] <= '9':85+ return 186+ case strings.HasPrefix(k, "alt+"):87+ return 288+ case k == "delete":89+ return 390+ }91+ return 492+}93+94+// shortcut is the key shown next to a command in menus and the palette,95+// e.g. "Ctrl+S", or "" if it has none.96+func shortcut(id string) string {97+ if keys := keysFor(id); len(keys) > 0 {98+ return keyLabel(keys[0])99+ }100+ return ""101+}102+65103 // runCommand runs a registered command by ID.66104 func (m *Model) runCommand(id string) tea.Cmd {67105 c, ok := commands[id]@@ -17,41 +17,41 @@ import (
1717 // menuItem is one entry in the slash menu. An item either opens a submenu1818 // (items) or runs a registered command (cmd). Descriptions come from the1919 // command unless the item overrides them.20-type menuItem struct {20+type slashItem struct {2121 name string2222 desc string2323 descFn func(m *Model) string // overrides desc when set24- items []menuItem24+ items []slashItem2525 cmd string2626 }2727 2828 type menuLevel struct {29- items []menuItem29+ items []slashItem3030 sel int3131 }3232 3333 // rootMenu is interim: the Sheets-style menu bar replaces it.34-var rootMenu = []menuItem{35- {name: "File", items: []menuItem{34+var rootMenu = []slashItem{35+ {name: "File", items: []slashItem{3636 {name: "New", descFn: unsavedWarning("Start a new, empty sheet"), cmd: "file.new"},3737 {name: "Open", cmd: "file.open"},3838 {name: "Save", cmd: "file.save"},3939 {name: "As", desc: "Save the sheet under a new name", cmd: "file.saveas"},4040 {name: "Quit", cmd: "quit"},4141 }},42- {name: "Edit", items: []menuItem{42+ {name: "Edit", items: []slashItem{4343 {name: "Clear", cmd: "clear"},4444 {name: "Go to", cmd: "goto"},4545 {name: "Select all", cmd: "select.all"},4646 }},47- {name: "Format", items: []menuItem{47+ {name: "Format", items: []slashItem{4848 {name: "Column width", cmd: "column.width"},4949 {name: "Reset column width", cmd: "column.reset"},5050 }},5151 }5252 5353 // quitConfirm is shown by Quit when there are unsaved changes.54-var quitConfirm = []menuItem{54+var quitConfirm = []slashItem{5555 {name: "Cancel", desc: "Keep working"},5656 {name: "Quit without saving", descFn: unsavedWarning("Close one23"), cmd: "quit.force"},5757 }@@ -69,7 +69,7 @@ func unsavedWarning(desc string) func(m *Model) string {
6969 }7070 }7171 72-func (it menuItem) description(m *Model) string {72+func (it slashItem) description(m *Model) string {7373 switch {7474 case it.descFn != nil:7575 return it.descFn(m)@@ -87,6 +87,20 @@ func (it menuItem) description(m *Model) string {
8787 return ""8888 }8989 90+// slashMenuLine shows the open menu level and the highlighted item's91+// description.92+func (m *Model) slashMenuLine() string {93+ lvl := m.menu[len(m.menu)-1]94+ parts := make([]string, len(lvl.items))95+ for i, it := range lvl.items {96+ parts[i] = it.name97+ if i == lvl.sel {98+ parts[i] = m.th.menuSelected.Render(it.name)99+ }100+ }101+ return strings.Join(parts, " ") + " " + m.th.muted.Render(lvl.items[lvl.sel].description(m))102+}103+90104 func (m *Model) openMenu() {91105 m.mode = modeMenu92106 m.menu = []menuLevel{{items: rootMenu}}@@ -145,7 +159,7 @@ func (m *Model) menuKey(k tea.KeyPressMsg) tea.Cmd {
145159 return nil146160 }147161 148-func (m *Model) choose(it menuItem) tea.Cmd {162+func (m *Model) choose(it slashItem) tea.Cmd {149163 if it.items != nil {150164 m.menu = append(m.menu, menuLevel{items: it.items})151165 return nil@@ -0,0 +1,150 @@
1+package ui2+3+import (4+ "strings"5+)6+7+// The menu bar follows Google Sheets: File Edit View Insert Format Data8+// Help, each opened with Alt and its accelerator letter (Alt+O for Format,9+// as in Sheets). Items name registered commands; an item whose command10+// isn't registered is hidden, and so is a menu or submenu left empty, so11+// menus fill in as features land.12+13+// menuItem is one line of a dropdown: a command, a submenu or a separator.14+type menuItem struct {15+ title string // defaults to the command's title16+ cmd string17+ items []menuItem // a submenu18+ sep bool19+}20+21+// sep separates groups of items.22+var sep = menuItem{sep: true}23+24+// menuDef is a menu bar title and its dropdown.25+type menuDef struct {26+ title string27+ accel byte // lowercase accelerator letter, marked in the title28+ items []menuItem29+}30+31+var menuBar = []menuDef{32+ {title: "File", accel: 'f', items: []menuItem{33+ {cmd: "file.new"}, {cmd: "file.open"}, {cmd: "file.save"}, {cmd: "file.saveas"}, sep,34+ {cmd: "quit"},35+ }},36+ {title: "Edit", accel: 'e', items: []menuItem{37+ {cmd: "edit.undo"}, {cmd: "edit.redo"}, sep,38+ {cmd: "edit.cut"}, {cmd: "edit.copy"}, {cmd: "edit.paste"}, {cmd: "edit.paste_values", title: "Paste values only"}, sep,39+ {cmd: "edit.fill_down", title: "Fill down"}, {cmd: "edit.fill_right", title: "Fill right"}, sep,40+ {cmd: "clear"}, {cmd: "select.all"}, {cmd: "goto"}, sep,41+ {cmd: "delete.row", title: "Delete row"}, {cmd: "delete.col", title: "Delete column"},42+ }},43+ {title: "View", accel: 'v', items: []menuItem{44+ {cmd: "palette", title: "Command palette"}, {cmd: "help"},45+ }},46+ {title: "Insert", accel: 'i', items: []menuItem{47+ {cmd: "insert.row_above", title: "Row above"}, {cmd: "insert.row_below", title: "Row below"}, sep,48+ {cmd: "insert.col_left", title: "Column left"}, {cmd: "insert.col_right", title: "Column right"},49+ }},50+ {title: "Format", accel: 'o', items: []menuItem{51+ {title: "Number", items: []menuItem{52+ {cmd: "format.automatic", title: "Automatic"}, {cmd: "format.plain_text", title: "Plain text"}, sep,53+ {cmd: "format.number", title: "Number"}, {cmd: "format.percent", title: "Percent"}, {cmd: "format.scientific", title: "Scientific"}, sep,54+ {cmd: "format.accounting", title: "Accounting"}, {cmd: "format.financial", title: "Financial"},55+ {cmd: "format.currency", title: "Currency"}, {cmd: "format.currency_rounded", title: "Currency rounded"}, sep,56+ {cmd: "format.date", title: "Date"}, {cmd: "format.time", title: "Time"}, {cmd: "format.datetime", title: "Date time"}, {cmd: "format.duration", title: "Duration"},57+ }},58+ {cmd: "format.decimals_more", title: "Increase decimal places"}, {cmd: "format.decimals_less", title: "Decrease decimal places"}, sep,59+ {cmd: "format.bold", title: "Bold"}, {cmd: "format.italic", title: "Italic"}, {cmd: "format.underline", title: "Underline"}, {cmd: "format.strikethrough", title: "Strikethrough"}, sep,60+ {cmd: "format.align_left", title: "Align left"}, {cmd: "format.align_center", title: "Align center"}, {cmd: "format.align_right", title: "Align right"}, sep,61+ {cmd: "column.width"}, {cmd: "column.reset"}, sep,62+ {cmd: "format.clear", title: "Clear formatting"},63+ }},64+ // Data: sort, filter and named ranges will go here.65+ {title: "Data", accel: 'd'},66+ {title: "Help", accel: 'h', items: []menuItem{67+ {cmd: "palette", title: "Search the menus"}, {cmd: "help"}, {cmd: "help.functions"}, sep,68+ {cmd: "help.about"},69+ }},70+}71+72+// label is the text an item shows.73+func (it menuItem) label() string {74+ if it.title != "" {75+ return it.title76+ }77+ if c, ok := commands[it.cmd]; ok {78+ return c.title79+ }80+ return it.cmd81+}82+83+// visibleItems drops items whose command isn't registered and submenus84+// left empty, then tidies the separators around what's left.85+func visibleItems(items []menuItem) []menuItem {86+ var out []menuItem87+ for _, it := range items {88+ switch {89+ case it.sep:90+ if len(out) > 0 && !out[len(out)-1].sep {91+ out = append(out, it)92+ }93+ case it.items != nil:94+ if sub := visibleItems(it.items); len(sub) > 0 {95+ it.items = sub96+ out = append(out, it)97+ }98+ case commands[it.cmd] != nil:99+ out = append(out, it)100+ }101+ }102+ if len(out) > 0 && out[len(out)-1].sep {103+ out = out[:len(out)-1]104+ }105+ return out106+}107+108+// barMenu is a menu bar title as laid out on screen.109+type barMenu struct {110+ def *menuDef111+ x, w int // the title's span, including one column of padding each side112+}113+114+// barMenus lays out the menus that have visible items. Titles are115+// separated by two spaces, as elsewhere in the UI.116+func barMenus() []barMenu {117+ var out []barMenu118+ x := 0119+ for i := range menuBar {120+ d := &menuBar[i]121+ if len(visibleItems(d.items)) == 0 {122+ continue123+ }124+ w := len(d.title) + 2125+ out = append(out, barMenu{def: d, x: x, w: w})126+ x += w127+ }128+ return out129+}130+131+// menuBarTitles renders the menu bar with each accelerator letter marked132+// and the open menu, if any, highlighted.133+func (m *Model) menuBarTitles() string {134+ var b strings.Builder135+ for i, bm := range barMenus() {136+ style, accel := m.th.menuBar, m.th.menuAccel137+ if i == m.openBarMenu() {138+ style, accel = m.th.menuSelected, m.th.menuAccelSelected139+ }140+ t := bm.def.title141+ k := strings.IndexByte(strings.ToLower(t), bm.def.accel)142+ b.WriteString(style.Render(" "+t[:k]) + accel.Render(t[k:k+1]) + style.Render(t[k+1:]+" "))143+ }144+ return b.String()145+}146+147+// openBarMenu is the index in barMenus of the open dropdown, or -1.148+func (m *Model) openBarMenu() int {149+ return -1150+}@@ -29,13 +29,17 @@ func (m mode) String() string {
2929 return [...]string{"READY", "ENTER", "EDIT", "POINT", "MENU", "", "HELP", "ERROR"}[m]3030 }3131 32-// Layout: three control panel lines, the column header, the grid, and a33-// status line.32+// Layout: three control panel lines (menu bar, formula bar, context33+// line), the column header, the grid, and a status line.3434 const (35- panelLines = 336- headerLine = panelLines37- gridTop = panelLines + 138- rowHdrW = 635+ menuLine = 0 // menu bar on the left, mode indicator on the right36+ formulaLine = 1 // name box, then the cell's contents or the entry37+ contextLine = 2 // prompts, key hints and formula errors38+ panelLines = 339+ headerLine = panelLines40+ gridTop = panelLines + 141+ rowHdrW = 642+ nameBoxW = 11 // fits most ranges, e.g. "AA100:AB200", without jumping3943 )4044 4145 // doubleClick is the longest gap between two clicks that edits a cell.@@ -115,6 +115,11 @@ func line(m *Model, i int) string {
115115 return strings.TrimRight(strings.Split(screen(m), "\n")[i], " ")116116 }117117 118+// bar is the formula bar's text after the name box.119+func bar(m *Model) string {120+ return strings.TrimSpace(ansi.Cut(line(m, formulaLine), formulaBarTextX(), m.width))121+}122+118123 func addr(s string) sheet.Addr {119124 a, _ := sheet.ParseAddr(s)120125 return a@@ -137,8 +142,8 @@ func TestEnterCommitsAndMovesDown(t *testing.T) {
137142 t.Errorf("A3 = %v", got)138143 }139144 press(t, m, "<up>")140- if !strings.HasPrefix(line(m, 0), " A3 =SUM(A1:A2)") {141- t.Errorf("formula bar %q", line(m, 0))145+ if l := line(m, formulaLine); !strings.HasPrefix(l, " A3 ") || bar(m) != "=SUM(A1:A2)" {146+ t.Errorf("formula bar %q", l)142147 }143148 }144149 @@ -181,12 +186,12 @@ func TestPointMode(t *testing.T) {
181186 m := newModel()182187 press(t, m, "1", "<enter>", "2", "<enter>")183188 press(t, m, "=SUM(", "<up>", "<up>")184- if m.mode != modePoint || line(m, 1) != "=SUM(A1" {185- t.Fatalf("mode %v, edit line %q", m.mode, line(m, 1))189+ if m.mode != modePoint || bar(m) != "=SUM(A1" {190+ t.Fatalf("mode %v, edit line %q", m.mode, bar(m))186191 }187192 press(t, m, "<shift+down>")188- if line(m, 1) != "=SUM(A1:A2" {189- t.Fatalf("edit line %q", line(m, 1))193+ if bar(m) != "=SUM(A1:A2" {194+ t.Fatalf("edit line %q", bar(m))190195 }191196 press(t, m, ")", "<enter>")192197 if input(m, "A3") != "=SUM(A1:A2)" || m.sheet.Value(addr("A3")).Num != 3 || m.cur != addr("A4") {@@ -344,8 +349,8 @@ func TestWheelDoesNotSnapBack(t *testing.T) {
344349 func TestMenuColumnWidthOnSelection(t *testing.T) {345350 m := newModel()346351 press(t, m, "<shift+right>", "<f10>")347- if line(m, 1) != "File Edit Format" {348- t.Fatalf("menu line %q", line(m, 1))352+ if l := line(m, contextLine); !strings.HasPrefix(l, "File Edit Format") {353+ t.Fatalf("menu line %q", l)349354 }350355 press(t, m, "f") // two items start with F: cycles to Format351356 press(t, m, "<enter>", "c", "15", "<enter>")@@ -448,7 +453,7 @@ func TestHelpListsShortcuts(t *testing.T) {
448453 m := newModel()449454 press(t, m, "<f1>")450455 s := screen(m)451- for _, want := range []string{"Ctrl+S", "Save", "Backspace / Del", "Ctrl+Shift+V", "Paste values only", "SUM"} {456+ for _, want := range []string{"Ctrl+S", "Save", "Backspace", "Del", "Ctrl+Shift+V", "Paste values only", "SUM"} {452457 if !strings.Contains(s, want) {453458 t.Errorf("help missing %q", want)454459 }@@ -39,10 +39,11 @@ type hit struct {
3939 4040 // formulaBarAt returns where the active cell's contents are drawn in READY4141 // mode: the screen line and the x of the first character.42-func (m *Model) formulaBarAt() (line, x int) { return 0, rowHdrW }42+func (m *Model) formulaBarAt() (line, x int) { return formulaLine, formulaBarTextX() }4343 44-// editLineAt returns where the entry being typed is drawn.45-func (m *Model) editLineAt() (line, x int) { return 1, 0 }44+// editLineAt returns where the entry being typed is drawn: in place, in the45+// formula bar.46+func (m *Model) editLineAt() (line, x int) { return formulaLine, formulaBarTextX() }4647 4748 // hitTest maps a screen position to what's there.4849 func (m *Model) hitTest(x, y int) hit {@@ -11,6 +11,15 @@ import (
1111 1212 func mouseAt(x, y int) tea.Mouse { return tea.Mouse{X: x, Y: y, Button: tea.MouseLeft} }1313 14+// barText is the text in the formula bar after the name box.15+func barText(m *Model) string {16+ l := line(m, formulaLine)17+ if len(l) <= formulaBarTextX() {18+ return ""19+ }20+ return strings.TrimSpace(l[formulaBarTextX():])21+}22+1423 func TestClickWhileTypingAccepts(t *testing.T) {1524 m := newModel()1625 press(t, m, "hello")@@ -24,15 +33,15 @@ func TestClickInsertsReferenceIntoFormula(t *testing.T) {
2433 m := newModel()2534 press(t, m, "5", "<enter>", "7", "<enter>", "=")2635 click(m, cellX(0), gridTop, 0)27- if m.mode != modePoint || line(m, 1) != "=A1" {28- t.Fatalf("mode %v line %q", m.mode, line(m, 1))36+ if m.mode != modePoint || barText(m) != "=A1" {37+ t.Fatalf("mode %v formula bar %q", m.mode, barText(m))2938 }3039 // Dragging makes it a range.3140 send(m, tea.MouseClickMsg(mouseAt(cellX(0), gridTop)))3241 send(m, tea.MouseMotionMsg(mouseAt(cellX(0), gridTop+1)))3342 send(m, tea.MouseReleaseMsg(mouseAt(cellX(0), gridTop+1)))34- if line(m, 1) != "=A1:A2" {35- t.Fatalf("line %q", line(m, 1))43+ if barText(m) != "=A1:A2" {44+ t.Fatalf("formula bar %q", barText(m))3645 }3746 press(t, m, "*2", "<enter>")3847 if got := m.sheet.Cell(addr("A3")); got == nil || got.Input != "=A1:A2*2" {@@ -118,7 +127,7 @@ func TestPointerShapes(t *testing.T) {
118127 {cellX(1), gridTop + 1, "cell"},119128 {rowHdrW + sheet.DefaultWidth - 1, headerLine, "col-resize"},120129 {cellX(1), headerLine, "pointer"},121- {rowHdrW + 1, 0, "text"},130+ {formulaBarTextX() + 1, formulaLine, "text"},122131 {1, m.height - 1, "default"},123132 } {124133 send(m, tea.MouseMotionMsg{X: tt.x, Y: tt.y})@@ -0,0 +1,209 @@
1+package ui2+3+import (4+ "math"5+ "path/filepath"6+ "strconv"7+ "strings"8+9+ tea "charm.land/bubbletea/v2"10+ "github.com/charmbracelet/x/ansi"11+12+ "one23/internal/sheet"13+)14+15+// The control panel is three lines: the menu bar with the mode indicator,16+// the formula bar, and the context line. Below the grid, the status line17+// shows the file and selection statistics.18+19+// View implements tea.Model.20+func (m *Model) View() tea.View {21+ var lines []string22+ if m.mode == modeHelp {23+ lines = m.helpLines()24+ } else {25+ lines = append(lines, m.menuBarLine(), m.formulaBar(), m.contextLineText(), m.headerRow())26+ for i := range m.visibleRows() {27+ lines = append(lines, m.gridRow(m.top+i))28+ }29+ lines = append(lines, m.statusLine())30+ }31+ for i, l := range lines {32+ lines[i] = ansi.Truncate(l, m.width, "")33+ }34+35+ v := tea.NewView(strings.Join(lines, "\n"))36+ v.AltScreen = true37+ v.MouseMode = tea.MouseModeAllMotion // hover feedback; see mouse.go38+ v.WindowTitle = "one23 - " + m.displayName()39+ if m.changed {40+ v.WindowTitle += " (modified)"41+ }42+ if x, y, ok := m.cursorPos(); ok {43+ v.Cursor = tea.NewCursor(x, y)44+ v.Cursor.Shape = tea.CursorBar45+ }46+ return v47+}48+49+func (m *Model) displayName() string {50+ if m.filename == "" {51+ return "untitled"52+ }53+ return filepath.Base(m.filename)54+}55+56+// indicator is the mode shown at the top right.57+func (m *Model) indicator() string {58+ if m.mode == modePrompt {59+ return m.prompt.indicator60+ }61+ return m.mode.String()62+}63+64+// menuBarLine is the menu bar with the mode indicator on the right.65+func (m *Model) menuBarLine() string {66+ left := m.menuBarTitles()67+ ind := m.th.indicator.Render(" " + m.indicator() + " ")68+ gap := m.width - ansi.StringWidth(left) - ansi.StringWidth(ind)69+ return left + strings.Repeat(" ", max(gap, 1)) + ind70+}71+72+// formulaBarTextX is the screen column where the formula bar's text (the73+// cell's contents or the entry being typed) starts, after the name box.74+func formulaBarTextX() int { return nameBoxW + 1 }75+76+// formulaBar shows the name box (the active cell, or the selected range)77+// and the active cell's contents as typed. While typing, the entry is78+// edited here with the terminal cursor, and shown in the cell too.79+func (m *Model) formulaBar() string {80+ name := m.cur.String()81+ if m.hasRange() && m.mode == modeReady {82+ name = m.selection().String()83+ }84+ box := m.th.header.Render(padRight(" "+name, nameBoxW)) + " "85+ switch m.mode {86+ case modeEnter, modeEdit:87+ return box + string(m.buf)88+ case modePoint:89+ return box + m.pointPrefix + m.th.selection.Render(m.point.text()) + m.pointSuffix90+ }91+ if c := m.sheet.Cell(m.cur); c != nil {92+ return box + c.Input93+ }94+ return box95+}96+97+// contextLineText says what's going on: a prompt, a formula error, or the98+// keys that apply in the current mode.99+func (m *Model) contextLineText() string {100+ var left, right string101+ switch {102+ case m.drag == dragResize:103+ left = m.th.key.Render("Column "+sheet.ColName(m.resizeCol)) + m.th.muted.Render(" width ") +104+ strconv.Itoa(m.sheet.ColWidth(m.resizeCol)) + m.th.muted.Render(" double-click the border to fit")105+ case m.hint != "":106+ left = m.th.warning.Render(m.hint)107+ case m.mode == modeReady:108+ left = m.readyLine()109+ case m.mode == modeMenu:110+ left = m.slashMenuLine()111+ case m.mode == modePrompt:112+ left, right = m.promptLine()113+ case m.mode == modePoint:114+ left = m.keyHints("Arrows", "pick a cell", "Shift+arrows", "pick a range", "Enter", "accept", "Esc", "back")115+ case m.mode == modeEnter && m.isFormula():116+ left = m.keyHints("Enter", "accept", "Tab", "accept and go right", "Arrows after an operator", "pick cells", "Esc", "cancel")117+ case m.mode == modeEnter:118+ left = m.keyHints("Enter", "accept", "Tab", "accept and go right", "Arrows", "accept and move", "Esc", "cancel")119+ case m.mode == modeEdit:120+ left = m.keyHints("Enter", "accept", "Left/Right", "move the caret", "Esc", "cancel")121+ }122+ return m.spread(left, right)123+}124+125+// spread puts right at the right edge after left, dropping it if there126+// isn't room for both.127+func (m *Model) spread(left, right string) string {128+ gap := m.width - ansi.StringWidth(left) - ansi.StringWidth(right)129+ if right == "" || gap < 3 {130+ return left131+ }132+ return left + strings.Repeat(" ", gap) + right133+}134+135+// promptLine is an open prompt, e.g. "Save as: budget.o23", and the keys136+// or choices that go with it.137+func (m *Model) promptLine() (left, right string) {138+ switch {139+ case m.pointing():140+ return m.promptPrefix() + m.th.selection.Render(m.point.text()),141+ m.keyHints("Arrows", "move", "Shift+arrows", "extend", "Enter", "apply", "Esc", "cancel")142+ case len(m.files) > 0:143+ return m.promptPrefix() + string(m.buf), m.th.muted.Render(strings.Join(m.files, " "))144+ case m.prompt.kind == promptWidth:145+ return m.promptPrefix() + string(m.buf), m.keyHints("Left/Right", "adjust", "Enter", "apply", "Esc", "cancel")146+ }147+ return m.promptPrefix() + string(m.buf), m.keyHints("Enter", "apply", "Esc", "cancel")148+}149+150+func (m *Model) promptPrefix() string {151+ return m.prompt.label + " "152+}153+154+// keyHints renders key and description pairs, e.g. "Enter accept".155+func (m *Model) keyHints(pairs ...string) string {156+ parts := make([]string, 0, len(pairs)/2)157+ for i := 0; i+1 < len(pairs); i += 2 {158+ parts = append(parts, m.th.key.Render(pairs[i])+" "+m.th.muted.Render(pairs[i+1]))159+ }160+ return strings.Join(parts, m.th.muted.Render(" "))161+}162+163+// cursorPos returns where the terminal cursor goes: in the formula bar164+// while typing an entry, on the context line in a text prompt.165+func (m *Model) cursorPos() (x, y int, ok bool) {166+ switch {167+ case m.mode == modeEnter, m.mode == modeEdit:168+ return formulaBarTextX() + ansi.StringWidth(string(m.buf[:m.bufPos])), formulaLine, true169+ case m.mode == modePrompt && !m.pointing():170+ return ansi.StringWidth(m.promptPrefix() + string(m.buf[:m.bufPos])), contextLine, true171+ }172+ return 0, 0, false173+}174+175+func (m *Model) statusLine() string {176+ if m.mode == modeError {177+ return m.th.error.Render(m.errMsg) + m.th.muted.Render(" press any key")178+ }179+ left := m.displayName()180+ if m.changed {181+ left += m.th.muted.Render(" modified")182+ }183+ if m.sheet.Circular {184+ left += " " + m.th.warning.Render("Circular reference")185+ }186+ var right string187+ if m.hasRange() && m.mode == modeReady {188+ r := m.selection()189+ st := m.sheet.RangeStats(r)190+ parts := []string{m.th.key.Render(r.String())}191+ if st.Nums > 0 {192+ parts = append(parts,193+ m.th.muted.Render("Sum ")+fmtStat(st.Sum),194+ m.th.muted.Render("Avg ")+fmtStat(st.Sum/float64(st.Nums)))195+ }196+ parts = append(parts, m.th.muted.Render("Count ")+strconv.Itoa(st.Count))197+ right = strings.Join(parts, " ")198+ } else {199+ right = m.keyHints(shortcut("help"), "shortcuts", shortcut("menu"), "menu", shortcut("quit"), "quit")200+ }201+ gap := max(m.width-ansi.StringWidth(left)-ansi.StringWidth(right), 1)202+ return left + strings.Repeat(" ", gap) + right203+}204+205+// fmtStat formats a status line statistic with at most two decimals, as206+// Sheets does.207+func fmtStat(v float64) string {208+ return strconv.FormatFloat(math.Round(v*100)/100, 'f', -1, 64)209+}@@ -0,0 +1,31 @@
1+package ui2+3+import (4+ "strings"5+ "testing"6+)7+8+func TestControlPanelLayout(t *testing.T) {9+ m := newModel()10+ if l := line(m, menuLine); !strings.HasPrefix(l, " File Edit") || !strings.HasSuffix(l, "READY") {11+ t.Errorf("menu bar %q", l)12+ }13+ press(t, m, "<shift+down>", "<shift+right>")14+ if l := line(m, formulaLine); strings.TrimSpace(l) != "A1:B2" {15+ t.Errorf("name box should show the selection: %q", l)16+ }17+ press(t, m, "<esc>", "=1+")18+ if x, y, _ := m.cursorPos(); y != formulaLine || x != formulaBarTextX()+3 {19+ t.Errorf("entry cursor at %d,%d", x, y)20+ }21+ if !strings.Contains(line(m, contextLine), "Enter accept") {22+ t.Errorf("context line %q", line(m, contextLine))23+ }24+ press(t, m, "<esc>", "<ctrl+g>")25+ if l := line(m, contextLine); !strings.HasPrefix(l, "Go to: A1") || !strings.HasSuffix(l, "Esc cancel") {26+ t.Errorf("prompt line %q", l)27+ }28+ if x, y, _ := m.cursorPos(); y != contextLine || x != len("Go to: A1") {29+ t.Errorf("prompt cursor at %d,%d", x, y)30+ }31+}@@ -13,15 +13,14 @@ import (
1313 // lose contrast.1414 type theme struct {1515 indicator lipgloss.Style // mode indicator, top right16- header lipgloss.Style // column letters and row numbers16+ header lipgloss.Style // column letters, row numbers and the name box1717 headerActive lipgloss.Style // header of the focused row and column1818 headerSel lipgloss.Style // headers of selected rows and columns1919 headerHover lipgloss.Style // a header under the mouse2020 handle lipgloss.Style // a column resize handle being hovered or dragged2121 pointer lipgloss.Style // the cell pointer2222 selection lipgloss.Style // a range being pointed at23- menuSelected lipgloss.Style // highlighted menu item24- hint lipgloss.Style // guidance on the third panel line23+ hint lipgloss.Style // guidance on the context line2524 warning lipgloss.Style // recoverable problems, e.g. a formula error2625 error lipgloss.Style // ERROR mode message2726 muted lipgloss.Style // secondary text: key hints, file lists@@ -31,16 +30,28 @@ type theme struct {
3130 // a dashed underline across every cell, layered on the cell's own3231 // style, with its own text color where the cell has none.3332 copied lipgloss.Style33+34+ // Chrome: the menu bar, dropdowns, the palette and dialogs.35+ menuBar lipgloss.Style // menu bar titles36+ menuAccel lipgloss.Style // a title's accelerator letter37+ menuSelected lipgloss.Style // open title, highlighted item or button38+ menuAccelSelected lipgloss.Style // accelerator letter of the open title39+ border lipgloss.Style // box borders and separators40+ title lipgloss.Style // box titles and group headings41+ disabled lipgloss.Style // items that can't run right now42+ match lipgloss.Style // characters matched by a search43+ matchSelected lipgloss.Style // matched characters in the highlighted row44+ button lipgloss.Style // dialog buttons without focus3445 }3546 3647 func newTheme(dark bool) theme {3748 // Contrast was checked against the reference palettes in e2e: text on3849 // colored backgrounds stays at or above roughly 4.5:1.3950 headerBg, headerFg := lipgloss.BrightBlack, lipgloss.BrightWhite40- selFg, muted := lipgloss.Black, lipgloss.BrightBlack51+ selFg, muted, match := lipgloss.Black, lipgloss.BrightBlack, lipgloss.Yellow4152 if !dark {4253 headerBg, headerFg = lipgloss.White, lipgloss.Black43- selFg, muted = lipgloss.BrightWhite, lipgloss.Black54+ selFg, muted, match = lipgloss.BrightWhite, lipgloss.Black, lipgloss.Blue4455 }4556 accent := lipgloss.NewStyle().Background(lipgloss.Cyan).Foreground(lipgloss.Black)4657 return theme{@@ -52,7 +63,6 @@ func newTheme(dark bool) theme {
5263 handle: lipgloss.NewStyle().Background(headerBg).Foreground(lipgloss.Cyan).Bold(true),5364 pointer: accent,5465 selection: lipgloss.NewStyle().Background(lipgloss.Blue).Foreground(selFg),55- menuSelected: accent.Bold(true),5666 hint: lipgloss.NewStyle().Foreground(muted),5767 warning: lipgloss.NewStyle().Foreground(lipgloss.Yellow),5868 error: lipgloss.NewStyle().Foreground(lipgloss.BrightRed).Bold(true),@@ -61,5 +71,16 @@ func newTheme(dark bool) theme {
6171 errorCell: lipgloss.NewStyle().Foreground(lipgloss.Red),6272 copied: lipgloss.NewStyle().Foreground(lipgloss.Magenta).6373 UnderlineStyle(lipgloss.UnderlineDashed).UnderlineSpaces(true),74+75+ menuBar: lipgloss.NewStyle(),76+ menuAccel: lipgloss.NewStyle().Underline(true),77+ menuSelected: accent,78+ menuAccelSelected: accent.Underline(true),79+ border: lipgloss.NewStyle().Foreground(lipgloss.BrightBlack),80+ title: lipgloss.NewStyle().Bold(true),81+ disabled: lipgloss.NewStyle().Foreground(lipgloss.BrightBlack), // exempt from contrast, like Sheets82+ match: lipgloss.NewStyle().Foreground(match).Bold(true),83+ matchSelected: accent.Bold(true).Underline(true),84+ button: lipgloss.NewStyle().Background(headerBg).Foreground(headerFg),6485 }6586 }@@ -2,161 +2,16 @@ package ui
22 33 import (44 "fmt"5- "math"6- "path/filepath"75 "slices"86 "strconv"97 "strings"108 11- tea "charm.land/bubbletea/v2"129 "charm.land/lipgloss/v2"1310 "github.com/charmbracelet/x/ansi"1411 1512 "one23/internal/sheet"1613 )1714 18-// View implements tea.Model.19-func (m *Model) View() tea.View {20- var lines []string21- if m.mode == modeHelp {22- lines = m.helpLines()23- } else {24- lines = append(lines, m.panelLine1(), m.panelLine2(), m.panelLine3(), m.headerRow())25- for i := range m.visibleRows() {26- lines = append(lines, m.gridRow(m.top+i))27- }28- lines = append(lines, m.statusLine())29- }30- for i, l := range lines {31- lines[i] = ansi.Truncate(l, m.width, "")32- }33-34- v := tea.NewView(strings.Join(lines, "\n"))35- v.AltScreen = true36- v.MouseMode = tea.MouseModeAllMotion // hover feedback; see mouse.go37- v.WindowTitle = "one23 - " + m.displayName()38- if m.changed {39- v.WindowTitle += " (modified)"40- }41- if x, ok := m.cursorX(); ok {42- line, lx := m.editLineAt()43- v.Cursor = tea.NewCursor(lx+x, line)44- v.Cursor.Shape = tea.CursorBar45- }46- return v47-}48-49-func (m *Model) displayName() string {50- if m.filename == "" {51- return "untitled"52- }53- return filepath.Base(m.filename)54-}55-56-// panelLine1 is the formula bar: the active cell's address and contents as57-// typed, with the mode indicator on the right.58-func (m *Model) panelLine1() string {59- left := m.th.header.Render(padRight(" "+m.cur.String(), rowHdrW-1)) + " "60- if c := m.sheet.Cell(m.cur); c != nil {61- left += c.Input62- }63- ind := m.mode.String()64- if m.mode == modePrompt {65- ind = m.prompt.indicator66- }67- ind = m.th.indicator.Render(" " + ind + " ")68- gap := m.width - ansi.StringWidth(left) - ansi.StringWidth(ind)69- if gap < 1 {70- left = ansi.Truncate(left, m.width-ansi.StringWidth(ind)-2, "…")71- gap = m.width - ansi.StringWidth(left) - ansi.StringWidth(ind)72- }73- return left + strings.Repeat(" ", max(gap, 1)) + ind74-}75-76-// panelLine2 is the edit line, the menu, or a prompt.77-func (m *Model) panelLine2() string {78- switch m.mode {79- case modeEnter, modeEdit:80- return string(m.buf)81- case modePoint:82- return m.pointPrefix + m.th.selection.Render(m.point.text()) + m.pointSuffix83- case modeMenu:84- lvl := m.menu[len(m.menu)-1]85- parts := make([]string, len(lvl.items))86- for i, it := range lvl.items {87- parts[i] = it.name88- if i == lvl.sel {89- parts[i] = m.th.menuSelected.Render(it.name)90- }91- }92- return strings.Join(parts, " ")93- case modePrompt:94- if m.pointing() {95- return m.promptPrefix() + m.th.selection.Render(m.point.text())96- }97- return m.promptPrefix() + string(m.buf)98- }99- return ""100-}101-102-func (m *Model) promptPrefix() string {103- return m.prompt.label + " "104-}105-106-// panelLine3 explains the current state: the highlighted menu item, a107-// formula error, or which keys do what.108-func (m *Model) panelLine3() string {109- switch {110- case m.drag == dragResize:111- return m.th.key.Render("Column "+sheet.ColName(m.resizeCol)) + m.th.muted.Render(" width ") +112- strconv.Itoa(m.sheet.ColWidth(m.resizeCol)) + m.th.muted.Render(" double-click the border to fit")113- case m.hint != "":114- return m.th.warning.Render(m.hint)115- case m.mode == modeMenu:116- lvl := m.menu[len(m.menu)-1]117- return lvl.items[lvl.sel].description(m)118- case m.mode == modePrompt && len(m.files) > 0:119- return m.th.muted.Render(strings.Join(m.files, " "))120- case m.mode == modePrompt && m.prompt.kind == promptWidth:121- return m.keyHints("Left/Right", "adjust", "Enter", "apply", "Esc", "cancel")122- case m.pointing():123- return m.keyHints("Arrows", "move", "Shift+arrows", "extend", "Enter", "apply", "Esc", "cancel")124- case m.mode == modePrompt:125- return m.keyHints("Enter", "apply", "Esc", "cancel")126- case m.mode == modePoint:127- return m.keyHints("Arrows", "pick a cell", "Shift+arrows", "pick a range", "Enter", "accept", "Esc", "back")128- case m.mode == modeEnter && m.isFormula():129- return m.keyHints("Enter", "accept", "Tab", "accept and go right", "Arrows after an operator", "pick cells", "Esc", "cancel")130- case m.mode == modeEnter:131- return m.keyHints("Enter", "accept", "Tab", "accept and go right", "Arrows", "accept and move", "Esc", "cancel")132- case m.mode == modeEdit:133- return m.keyHints("Enter", "accept", "Left/Right", "move the caret", "Esc", "cancel")134- case m.mode == modeReady:135- return m.readyLine()136- }137- return ""138-}139-140-// keyHints renders key and description pairs, e.g. "Enter accept".141-func (m *Model) keyHints(pairs ...string) string {142- parts := make([]string, 0, len(pairs)/2)143- for i := 0; i+1 < len(pairs); i += 2 {144- parts = append(parts, m.th.key.Render(pairs[i])+" "+m.th.muted.Render(pairs[i+1]))145- }146- return strings.Join(parts, m.th.muted.Render(" "))147-}148-149-// cursorX returns where the terminal cursor goes on the edit line.150-func (m *Model) cursorX() (int, bool) {151- switch {152- case m.mode == modeEnter, m.mode == modeEdit:153- return ansi.StringWidth(string(m.buf[:m.bufPos])), true154- case m.mode == modePrompt && !m.pointing():155- return ansi.StringWidth(m.promptPrefix() + string(m.buf[:m.bufPos])), true156- }157- return 0, false158-}159-16015 // active is the cell drawn as the cell pointer: the pointer while16116 // pointing, otherwise the active cell (which stays put while a selection is16217 // extended, as in Sheets).@@ -316,42 +171,6 @@ func (m *Model) rowText(row int) []string {
316171 return out317172 }318173 319-func (m *Model) statusLine() string {320- if m.mode == modeError {321- return m.th.error.Render(m.errMsg) + m.th.muted.Render(" press any key")322- }323- left := m.displayName()324- if m.changed {325- left += m.th.muted.Render(" modified")326- }327- if m.sheet.Circular {328- left += " " + m.th.warning.Render("Circular reference")329- }330- var right string331- if m.hasRange() && m.mode == modeReady {332- r := m.selection()333- st := m.sheet.RangeStats(r)334- parts := []string{m.th.key.Render(r.String())}335- if st.Nums > 0 {336- parts = append(parts,337- m.th.muted.Render("Sum ")+fmtStat(st.Sum),338- m.th.muted.Render("Avg ")+fmtStat(st.Sum/float64(st.Nums)))339- }340- parts = append(parts, m.th.muted.Render("Count ")+strconv.Itoa(st.Count))341- right = strings.Join(parts, " ")342- } else {343- right = m.keyHints("F1", "help", "F10", "menu", "Ctrl+Q", "quit")344- }345- gap := max(m.width-ansi.StringWidth(left)-ansi.StringWidth(right), 1)346- return left + strings.Repeat(" ", gap) + right347-}348-349-// fmtStat formats a status line statistic with at most two decimals, as350-// Sheets does.351-func fmtStat(v float64) string {352- return strconv.FormatFloat(math.Round(v*100)/100, 'f', -1, 64)353-}354-355174 func (m *Model) helpLines() []string {356175 lines := []string{357176 m.th.indicator.Render(" HELP ") + " Keyboard shortcuts " + m.th.muted.Render("(press any key to return)"),