cat / 012
clone

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

Rename the project to 012

Dami committed 7e22118
The module, command, binary, window title and docs are now 012, and the
native file extension is .012 to match.
commit
7e221183c15d0512b5ca8d84f79510fc196515eb
parent
66b0e8df33fac0fb85375a51e656f953a52df740
author
Dami <dami.etoile@gmail.com>
37 files changed, +87 -87
MMakefile6+++---
MREADME.md8++++----
MROADMAP.md8++++----
Rcmd/one23/main.go → cmd/012/main.go10+++++-----
Me2e/app_test.go8++++----
Me2e/format_test.go4++--
Me2e/go.mod2+-
Me2e/harness_test.go14+++++++-------
Me2e/screens_test.go6+++---
Me2e/shell_test.go4++--
Me2e/testdata/screens/about.html2+-
Me2e/testdata/screens/menu-narrow.html2+-
Mgo.mod2+-
Minternal/sheet/file.go2+-
Minternal/ui/clipboard.go2+-
Minternal/ui/commands.go4++--
Minternal/ui/context.go2+-
Minternal/ui/context_test.go2+-
Minternal/ui/edit.go2+-
Minternal/ui/entry.go2+-
Minternal/ui/format.go2+-
Minternal/ui/format_test.go2+-
Minternal/ui/help.go8++++----
Minternal/ui/help_test.go2+-
Minternal/ui/menu.go2+-
Minternal/ui/model.go2+-
Minternal/ui/model_test.go10+++++-----
Minternal/ui/mouse.go2+-
Minternal/ui/mouse_test.go2+-
Minternal/ui/panel.go6+++---
Minternal/ui/selection.go2+-
Minternal/ui/shell_test.go2+-
Minternal/ui/theme.go2+-
Minternal/ui/view.go2+-
Moracle/format_test.go12++++++------
Moracle/go.mod6+++---
Moracle/oracle_test.go18+++++++++---------
M Makefile +3 -3
M README.md +4 -4
M ROADMAP.md +4 -4
@@ -1,6 +1,6 @@
11 # Roadmap: past 1-2-322 3-Principles: inside the grid one23 behaves like Google Sheets; 1-2-3 is the3+Principles: inside the grid 012 behaves like Google Sheets; 1-2-3 is the44 visual identity around it. The main binary stays pure Go (`CGO_ENABLED=0`), and every feature ships with unit55 tests, a libghostty e2e test and reviewed golden screens. Visual and UX66 quality keep pace with features: see [docs/UX.md](docs/UX.md).
@@ -11,9 +11,9 @@ quality keep pace with features: see [docs/UX.md](docs/UX.md).
1111 |---|---|---|1212 | 1 | The engine does what Sheets users reach for daily, and every change can be undone | Undo/redo (Ctrl+Z/Ctrl+Y) covers every mutation; copy, cut and paste (Ctrl+C/X/V, paste values) and insert/delete rows and columns adjust relative and `$absolute` references; number formats (Format menu, auto-detected `$` and `%` on entry) and named ranges work and persist; Sheets' everyday functions, dates and finance functions match excelize on a differential test suite |1313 | 2 | Finding and using features is faster than in any terminal spreadsheet | A Sheets-structured menu bar (File Edit View Insert Format Data Help) and a searchable command palette reach every command with its key shown; formulas autocomplete with signature hints; precedents and dependents are visible and jumpable; rows and columns freeze; find/replace and filter work; fill down (Ctrl+D) and series fill work; an optional vim keymap exists |14-| 3 | one23 uses the terminal it's running in to the fullest, and degrades gracefully | Copy and paste ranges through the system clipboard; charts render as images in Ghostty/kitty and as text elsewhere; URLs are clickable; error cells are marked beyond color; cursor shape reflects the mode; the theme follows light and dark |15-| 4 | Data moves in and out of one23 without friction or cgo | CSV/TSV, XLSX, SQLite, Parquet and Lotus .wk1 import, with export where it makes sense; large imports show progress; the binary still builds with `CGO_ENABLED=0` |16-| 5 | one23 goes where 1-2-3 couldn't | Multiple sheets with cross-sheet references; derived frequency and pivot sheets; recorded and Starlark macros; an SSH server mode; opt-in decimal arithmetic; VHS demo tapes in CI |14+| 3 | 012 uses the terminal it's running in to the fullest, and degrades gracefully | Copy and paste ranges through the system clipboard; charts render as images in Ghostty/kitty and as text elsewhere; URLs are clickable; error cells are marked beyond color; cursor shape reflects the mode; the theme follows light and dark |15+| 4 | Data moves in and out of 012 without friction or cgo | CSV/TSV, XLSX, SQLite, Parquet and Lotus .wk1 import, with export where it makes sense; large imports show progress; the binary still builds with `CGO_ENABLED=0` |16+| 5 | 012 goes where 1-2-3 couldn't | Multiple sheets with cross-sheet references; derived frequency and pivot sheets; recorded and Starlark macros; an SSH server mode; opt-in decimal arithmetic; VHS demo tapes in CI |1717 1818 ## Phase 1: engine foundations (unblocks everything else)1919
R cmd/one23/main.go → cmd/012/main.go +5 -5
M e2e/app_test.go +4 -4
M e2e/format_test.go +2 -2
M e2e/go.mod +1 -1
M e2e/harness_test.go +7 -7
@@ -1,4 +1,4 @@
1-// Package e2e runs the real one23 binary on a pseudo-terminal and renders1+// Package e2e runs the real 012 binary on a pseudo-terminal and renders22 // its output with libghostty-vt, Ghostty's terminal emulator core. Tests33 // assert on what a user would actually see: screen text, cursor position,44 // window title and screen mode. Keystrokes are encoded by libghostty from
@@ -22,16 +22,16 @@ import (
2222 var binPath string2323 2424 func TestMain(m *testing.M) {25- dir, err := os.MkdirTemp("", "one23-e2e")25+ dir, err := os.MkdirTemp("", "012-e2e")2626 if err != nil {2727 panic(err)2828 }29- binPath = filepath.Join(dir, "one23")30- build := exec.Command("go", "build", "-o", binPath, "./cmd/one23")29+ binPath = filepath.Join(dir, "012")30+ build := exec.Command("go", "build", "-o", binPath, "./cmd/012")3131 build.Dir = ".."3232 build.Env = append(os.Environ(), "CGO_ENABLED=0")3333 if out, err := build.CombinedOutput(); err != nil {34- fmt.Fprintf(os.Stderr, "building one23: %v\n%s", err, out)34+ fmt.Fprintf(os.Stderr, "building 012: %v\n%s", err, out)3535 os.Exit(1)3636 }3737 code := m.Run()
@@ -41,7 +41,7 @@ func TestMain(m *testing.M) {
4141 4242 const waitTimeout = 5 * time.Second4343 44-// session is one running instance of one23 attached to a virtual terminal.44+// session is one running instance of 012 attached to a virtual terminal.4545 type session struct {4646 t *testing.T4747 dir string
@@ -68,7 +68,7 @@ type options struct {
6868 light bool // use the light reference palette6969 }7070 71-// start launches one23 in dir (a fresh temp dir if empty) with args.71+// start launches 012 in dir (a fresh temp dir if empty) with args.7272 func start(t *testing.T, dir string, args ...string) *session {7373 t.Helper()7474 return startWith(t, options{dir: dir}, args...)
M e2e/screens_test.go +3 -3
M e2e/shell_test.go +2 -2
M e2e/testdata/screens/about.html +1 -1
@@ -1,6 +1,6 @@
11 <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="background:var(--vt-palette-6)"> </span><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>22 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A1</span><span style="background:var(--vt-palette-8)"> </span>3-one23 devel: Lotus 1-2-3 looks, Google Sheets keys. <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> Close3+012 devel: Lotus 1-2-3 looks, Google Sheets keys. <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> Close44 <span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>55 <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span>66 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span>
M e2e/testdata/screens/menu-narrow.html +1 -1
@@ -4,7 +4,7 @@
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="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Keyboard shortcuts <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>55 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> Household budget 202<span style="color:var(--vt-palette-8)">│</span> Function list <span style="color:var(--vt-palette-8)">│</span>66 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">├───────────────────────────────┤</span>7-<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> Rent 1450 <span style="color:var(--vt-palette-8)">│</span> About one23 <span style="color:var(--vt-palette-8)">│</span>7+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> Rent 1450 <span style="color:var(--vt-palette-8)">│</span> About 012 <span style="color:var(--vt-palette-8)">│</span>88 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> Groceries 612.4 <span style="color:var(--vt-palette-8)">└───────────────────────────────┘</span>99 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> Transit 961010 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Savings r <span style="color:var(--vt-palette-1)">#DIV/0!</span>
M go.mod +1 -1
M internal/sheet/file.go +1 -1
M internal/ui/clipboard.go +1 -1
M internal/ui/commands.go +2 -2
M internal/ui/context.go +1 -1
M internal/ui/context_test.go +1 -1
M internal/ui/edit.go +1 -1
M internal/ui/entry.go +1 -1
M internal/ui/format.go +1 -1
M internal/ui/format_test.go +1 -1
M internal/ui/help.go +4 -4
M internal/ui/help_test.go +1 -1
M internal/ui/menu.go +1 -1
M internal/ui/model.go +1 -1
M internal/ui/model_test.go +5 -5
M internal/ui/mouse.go +1 -1
M internal/ui/mouse_test.go +1 -1
M internal/ui/panel.go +3 -3
M internal/ui/selection.go +1 -1
M internal/ui/shell_test.go +1 -1
M internal/ui/theme.go +1 -1
M internal/ui/view.go +1 -1
M oracle/format_test.go +6 -6
@@ -7,7 +7,7 @@ import (
77 88 "github.com/xuri/excelize/v2"99 10- "one23/internal/sheet"10+ "012/internal/sheet"1111 )1212 1313 // patterns are number formats: Sheets' Format > Number presets and
@@ -21,10 +21,10 @@ var patterns = []string{
2121 2222 var values = []float64{0, 1, -1, 0.5, 1234.5678, -1234.5678, 0.125, 1e6, 46291, 46291.6041666667, 1.0423611111}2323 24-// skippedFormats are pattern and value pairs where one23 deliberately24+// skippedFormats are pattern and value pairs where 012 deliberately2525 // differs, or excelize departs from Excel.2626 var skippedFormats = map[string]string{27- "#,##0, -1": "one23 drops the sign of a number that rounds to zero; excelize shows -0",27+ "#,##0, -1": "012 drops the sign of a number that rounds to zero; excelize shows -0",2828 "0.0# 46291.6041666667": "excelize keeps a trailing zero under #",2929 "[h]:mm:ss 1e+06": "excelize overflows",3030 }
@@ -35,7 +35,7 @@ func skipReason(pat string, v float64) string {
3535 case isDateOrTime(pat) && v < 0:3636 return "dates before 1899-12-30 are negative serials in Sheets; Excel has none"3737 case isDate(pat) && v < 61:38- return "Excel counts a fictitious 1900-02-29, so its dates before March 1900 are a day off; Sheets and one23 don't"38+ return "Excel counts a fictitious 1900-02-29, so its dates before March 1900 are a day off; Sheets and 012 don't"3939 }4040 return skippedFormats[fmt.Sprintf("%s %v", pat, v)]4141 }
@@ -62,10 +62,10 @@ func TestFormatsAgainstExcelize(t *testing.T) {
6262 t.Fatal(err)6363 }6464 ours := sheet.FormatPattern(v, pat)65- // one23 shows AM and PM in capitals, as Sheets' Time format does;65+ // 012 shows AM and PM in capitals, as Sheets' Time format does;6666 // excelize copies the case of the pattern's am/pm.6767 if ours != theirs && !(strings.Contains(pat, "am/pm") && strings.EqualFold(ours, theirs)) {68- t.Errorf("%q of %v: one23 %q, excelize %q", pat, v, ours, theirs)68+ t.Errorf("%q of %v: 012 %q, excelize %q", pat, v, ours, theirs)6969 continue7070 }7171 matched++
M oracle/go.mod +3 -3
M oracle/oracle_test.go +9 -9
@@ -1,4 +1,4 @@
1-// Package oracle checks one23's formula engine against excelize's1+// Package oracle checks 012's formula engine against excelize's22 // calculation engine: the same data and formulas go into both, and the33 // results must agree. It is a separate module so the main binary never44 // depends on excelize. Run it with make oracle.
@@ -12,7 +12,7 @@ import (
1212 1313 "github.com/xuri/excelize/v2"1414 15- "one23/internal/sheet"15+ "012/internal/sheet"1616 )1717 1818 // fixture is the data both engines read, typed as a user would.
@@ -107,10 +107,10 @@ var formulas = []string{
107107 `=RATE(48, -200, 8000)`, `=NPV(0.1, H1:H4)`, `=IRR(H1:H4)`, `=IRR(C1:C5)`,108108 }109109 110-// skipped are formulas where Google Sheets, which one23 follows, and110+// skipped are formulas where Google Sheets, which 012 follows, and111111 // Excel disagree, or where excelize departs from Excel. Each says why.112112 var skipped = map[string]string{113- // Sheets and Excel disagree; one23 follows Sheets.113+ // Sheets and Excel disagree; 012 follows Sheets.114114 `=ROUND(-2.5)`: "Sheets makes ROUND's places optional; Excel requires them",115115 116116 // excelize departs from Excel (and Sheets) here.
@@ -140,7 +140,7 @@ var skipped = map[string]string{
140140 `=NPV(0.1, H1:H4)`: "excelize reads only the first cell of a range",141141 }142142 143-// toExcel translates a one23 formula to Excel syntax: 1-2-3 style @SUM143+// toExcel translates a 012 formula to Excel syntax: 1-2-3 style @SUM144144 // and A1..B2 ranges become SUM and A1:B2, and the leading = goes.145145 func toExcel(f string) string {146146 f = strings.TrimPrefix(f, "=")
@@ -177,7 +177,7 @@ func TestToExcel(t *testing.T) {
177177 }178178 }179179 180-// build loads the fixture into one23, then copies the values one23180+// build loads the fixture into 012, then copies the values 012181181 // computed into an excelize workbook, so both start from the same data.182182 func build(t *testing.T) (*sheet.Sheet, *excelize.File) {183183 t.Helper()
@@ -225,7 +225,7 @@ func TestAgainstExcelize(t *testing.T) {
225225 continue226226 }227227 if err := s.Set(addr(cell), f); err != nil {228- t.Errorf("one23 rejects %s: %v", f, err)228+ t.Errorf("012 rejects %s: %v", f, err)229229 continue230230 }231231 ours := s.Value(addr(cell))
@@ -240,12 +240,12 @@ func TestAgainstExcelize(t *testing.T) {
240240 matched++241241 continue242242 }243- t.Errorf("%s: one23 %s, excelize %q", f, show(ours), theirs)243+ t.Errorf("%s: 012 %s, excelize %q", f, show(ours), theirs)244244 }245245 t.Logf("%d of %d formulas match excelize, %d skipped", matched, len(formulas)-len(skipped), len(skipped))246246 }247247 248-// agree compares a one23 value with excelize's text result: numbers to248+// agree compares a 012 value with excelize's text result: numbers to249249 // about 12 significant digits, booleans as TRUE/FALSE, errors by code.250250 func agree(v sheet.Value, x string) bool {251251 switch v.Kind {