cat / 012
clone

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

Merge main

Dami committed 35f0f36
commit
35f0f368feac91fef17b7dd52ae8f0ed5d7558cd
parent
2dd41e8ba6f12602e9e15e54e360404c027ab50d
parent
7029177e1daee9a7603d1922c96e393cd07dad70
author
Dami <dami.etoile@gmail.com>
kind
chore risk score 2.4 of 3, Jev's estimate the message may not describe the diff
131 files changed, +1128 -243
MMakefile16+-
MNOTICE14+-
Mdemos/arrays.tape1+
Mdemos/charts.tape5+-
Mdemos/decimal.tape1+
Mdemos/find-replace.tape1+
Mdemos/follow-log.tape1+
Mdemos/formats.tape1+
Mdemos/freeze-sort-filter.tape1+
Mdemos/import.tape1+
Mdemos/jev.tape1+
Mdemos/layout.tape1+
Ademos/lib/font-page40+++++
Mdemos/lib/setup.tape11+-
Mdemos/lib/ttyd6+-
Mdemos/live.tape1+
Mdemos/locale.tape1+
Mdemos/macros.tape1+
Mdemos/notebook.tape1+
Mdemos/nu-recipe.tape1+
Mdemos/pipeline.tape1+
Mdemos/pivot.tape1+
Mdemos/rules.tape1+
Mdemos/selection.tape1+
Mdemos/share.tape1+
Mdemos/sheet.tape1+
Mdemos/sources.tape1+
Mdemos/themes.tape1+
Mdocs/contributing/site.md80+++++++++-
Mdocs/contributing/testing.md9+-
Mdocs/files/README.md2+-
Mdocs/media/arrays.gifbin
Mdocs/media/charts.gifbin
Mdocs/media/decimal-on.pngbin
Mdocs/media/evaluate-dark.pngbin
Mdocs/media/evaluate-light.pngbin
Mdocs/media/find-replace.gifbin
Mdocs/media/first-steps.gifbin
Mdocs/media/follow-log.gifbin
Mdocs/media/formats-budget.pngbin
Mdocs/media/freeze-sort-filter.gifbin
Mdocs/media/import-picker.pngbin
Mdocs/media/jev.gifbin
Mdocs/media/layout-dark.pngbin
Mdocs/media/layout-light.pngbin
Mdocs/media/layout.gifbin
Mdocs/media/live-ask-dark.pngbin
Mdocs/media/live-ask-light.pngbin
Mdocs/media/live-review-dark.pngbin
Mdocs/media/live-review-light.pngbin
Mdocs/media/live-suggestion-dark.pngbin
Mdocs/media/live-suggestion-light.pngbin
Mdocs/media/live.gifbin
Mdocs/media/locale-de.pngbin
Mdocs/media/macros.gifbin
Mdocs/media/menus-palette.gifbin
Mdocs/media/notebook-cells-dark.pngbin
Mdocs/media/notebook-cells-light.pngbin
Mdocs/media/notebook-grid-dark.pngbin
Mdocs/media/notebook-grid-light.pngbin
Mdocs/media/notebook-nu-error-dark.pngbin
Mdocs/media/notebook-nu-error-light.pngbin
Mdocs/media/notebook-nu-help-dark.pngbin
Mdocs/media/notebook-nu-help-light.pngbin
Mdocs/media/notebook-nu-hover-dark.pngbin
Mdocs/media/notebook-nu-hover-light.pngbin
Mdocs/media/notebook.gifbin
Mdocs/media/nu-recipe.gifbin
Mdocs/media/palette-dark.pngbin
Mdocs/media/palette-light.pngbin
Mdocs/media/pipeline.gifbin
Mdocs/media/pivot.gifbin
Mdocs/media/rules-bars-dark.pngbin
Mdocs/media/rules-bars-light.pngbin
Mdocs/media/rules-panel-dark.pngbin
Mdocs/media/rules-panel-light.pngbin
Mdocs/media/rules.gifbin
Mdocs/media/selection-stats.pngbin
Mdocs/media/share-presence-dark.pngbin
Mdocs/media/share-presence-light.pngbin
Mdocs/media/share.gifbin
Mdocs/media/sheet.gifbin
Mdocs/media/source-formulas-dark.pngbin
Mdocs/media/source-formulas-light.pngbin
Mdocs/media/source-formulas-why-dark.pngbin
Mdocs/media/source-formulas-why-light.pngbin
Mdocs/media/source-tab-dark.pngbin
Mdocs/media/source-tab-light.pngbin
Mdocs/media/sources.gifbin
Mdocs/media/table-dark.pngbin
Mdocs/media/table-light.pngbin
Mdocs/media/theme-classic.pngbin
Mdocs/media/trace-view-dark.pngbin
Mdocs/media/trace-view-light.pngbin
Mdocs/media/tracing.gifbin
Mdocs/media/vim.gifbin
Mdocs/sheets/charts.md3+-
Me2e/stillcells_test.go48------
Me2e/stilldraw_test.go89+++++++-----
Me2e/stills_test.go74++++++----
Ae2e/stillthemes_test.go227++++++++++++++++++++++++++++++
Ae2e/testdata/fonts/JetBrainsMono-Bold.ttfbin
Ae2e/testdata/fonts/JetBrainsMono-BoldItalic.ttfbin
Ae2e/testdata/fonts/JetBrainsMono-Italic.ttfbin
Ae2e/testdata/fonts/JetBrainsMono-Regular.ttfbin
Ae2e/testdata/fonts/OFL.txt93++++++++++++
Minternal/fileio/fonts/OFL.txt4+-
Dinternal/fileio/fonts/ibm-plex-mono-latin-400-italic.woff2bin
Dinternal/fileio/fonts/ibm-plex-mono-latin-400-normal.woff2bin
Dinternal/fileio/fonts/ibm-plex-mono-latin-600-normal.woff2bin
Ainternal/fileio/fonts/jetbrains-mono-latin-400-italic.woff2bin
Ainternal/fileio/fonts/jetbrains-mono-latin-400-normal.woff2bin
Ainternal/fileio/fonts/jetbrains-mono-latin-600-normal.woff2bin
Minternal/fileio/html_test.go2+-
Minternal/fileio/htmlstyle.go14+-
Ainternal/sheet/testdata/fixtures/v0.8.0/plain.01210+
Ainternal/sheet/testdata/fixtures/v0.8.0/single.01221+++
Ainternal/sheet/testdata/fixtures/v0.8.0/workbook.012185++++++++++++++++++++++++
Minternal/ui/theme/palette.go10+-
Ainternal/ui/theme/picture_test.go43++++++
Minternal/ui/theme/scheme_test.go3+
Ascripts/doccheck/families.go127+++++++++++++++++
Mscripts/doccheck/main.go4+
Mwebsite/docusaurus.config.ts29+++-
Mwebsite/package-lock.json18+--
Mwebsite/package.json3+-
Mwebsite/sidebars.ts24+++-
Mwebsite/src/css/custom.css52++++++-
Mwebsite/src/pages/index.module.css38+----
Mwebsite/src/pages/index.tsx43+++---
Mwebsite/static/img/logo.svg2+-
M Makefile +11 -5
@@ -123,8 +123,12 @@ oracle:
123123 # frame held). Needs vhs 0.12+ (go install124124 # github.com/charmbracelet/vhs@latest), ttyd and ffmpeg. The JEV demo125125 # talks to demos/fakejev, started here, never the real service.126-# Charts are drawn as text, as the tapes and README describe; VHS127-# records text, not images. DEMOS=jev renders just one.126+# VHS's ttyd has sixel on, so charts are recorded as the sixel images127+# 012 draws. DEMOS=jev renders just one. Each tape runs128+# 012 with its theme set to the tape's Set Theme (setup.tape's unless it129+# sets one), and in JetBrains Mono, served to VHS's browser in ttyd's130+# page from e2e/testdata/fonts, so it needn't be installed131+# (docs/contributing/site.md#font-and-colors).128132 DEMOS ?= $(basename $(notdir $(wildcard demos/*.tape)))129133 FAKEJEV_ADDR := 127.0.0.1:8799130134
@@ -133,9 +137,11 @@ demos: build
133137 CGO_ENABLED=0 go build -o bin/fakejev ./demos/fakejev134138 mkdir -p demos/out/stills135139 bin/fakejev -addr $(FAKEJEV_ADDR) & pid=$$!; trap "kill $$pid" EXIT; \136- export DEMOS_TTYD="$$(command -v ttyd)" PATH="$(CURDIR)/demos/lib:$$PATH"; \137- export XDG_CONFIG_HOME="$$(mktemp -d)" O12_JEV_CREDENTIAL_STORE=false O12_THEME= O12_LOCALE=en-US; \138- cd demos && for d in $(DEMOS); do echo "vhs $$d.tape"; vhs -q $$d.tape || exit 1; done140+ export DEMOS_TTYD="$$(command -v ttyd)" PATH="$(CURDIR)/demos/lib:$$PATH" DEMOS_PAGE="$(CURDIR)/demos/out/ttyd.html"; \141+ demos/lib/font-page "$$DEMOS_PAGE" e2e/testdata/fonts || exit 1; \142+ export XDG_CONFIG_HOME="$$(mktemp -d)" O12_JEV_CREDENTIAL_STORE=false O12_LOCALE=en-US; \143+ cd demos && for d in $(DEMOS); do theme="$$(grep -h '^Set Theme' lib/setup.tape $$d.tape | tail -1 | cut -d'"' -f2)"; \144+ echo "vhs $$d.tape ($$theme)"; O12_THEME="$$theme" vhs -q $$d.tape || exit 1; done139145 mkdir -p demos/out/media140146 for d in $(DEMOS); do ffmpeg -v error -y -i demos/out/$$d.gif -filter_complex \141147 "fps=12,tpad=stop_mode=clone:stop_duration=2,split[a][b];[a]palettegen=max_colors=64:stats_mode=diff[p];[b][p]paletteuse=dither=none:diff_mode=rectangle" \
M NOTICE +9 -5
@@ -30,14 +30,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3030 SOFTWARE.3131 3232 -------------------------------------------------------------------------------33+e2e/testdata/fonts/*.ttf3334 internal/fileio/fonts/*.woff23435 35-IBM Plex Mono (regular, semibold and italic, Latin subset), embedded in the36-web pages 012 writes, as packaged by Fontsource37-(https://fontsource.org/fonts/ibm-plex-mono, @fontsource/ibm-plex-mono). The38-license is also in internal/fileio/fonts/OFL.txt.36+JetBrains Mono, the font of the docs' pictures of 012 and of the web pages37+012 writes: regular, bold, italic and bold italic, unchanged from release38+2.304 (https://github.com/JetBrains/JetBrainsMono), in e2e/testdata/fonts;39+regular, semibold and italic, Latin subset, as packaged by Fontsource40+(https://fontsource.org/fonts/jetbrains-mono, @fontsource/jetbrains-mono41+5.3.0), embedded in the web pages. The license is also in42+e2e/testdata/fonts/OFL.txt and internal/fileio/fonts/OFL.txt.3943 40-Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ThinItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLight.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Light.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-LightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Regular.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Italic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Medium.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-MediumItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Bold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-BoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved.44+Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)4145 4246 This Font Software is licensed under the SIL Open Font License, Version 1.1.4347 This license is copied below, and is also available with a FAQ at:
M demos/arrays.tape +1 -0
M demos/charts.tape +3 -2
M demos/decimal.tape +1 -0
M demos/find-replace.tape +1 -0
M demos/follow-log.tape +1 -0
M demos/formats.tape +1 -0
M demos/freeze-sort-filter.tape +1 -0
M demos/import.tape +1 -0
M demos/jev.tape +1 -0
M demos/layout.tape +1 -0
A demos/lib/font-page +40 -0
@@ -0,0 +1,40 @@
1+#!/bin/sh2+# font-page OUT FONTDIR: writes ttyd's own page to OUT with JetBrains3+# Mono added as a web font from FONTDIR's TTFs (e2e/testdata/fonts), for4+# demos/lib/ttyd to serve with ttyd -I. VHS's browser draws with the5+# fonts the system or the page gives it, so with the font in the page6+# the recordings are drawn in it whether or not it is installed. The7+# page comes from the ttyd in DEMOS_TTYD, so it always matches it.8+set -eu9+out=$1 fonts=$210+log=$(mktemp)11+"${DEMOS_TTYD:?set DEMOS_TTYD to the real ttyd}" -p 0 -i 127.0.0.1 true 2>"$log" &12+pid=$!13+trap 'kill $pid 2>/dev/null || true; rm -f "$log"' EXIT14+port=15+for _ in $(seq 50); do16+ port=$(sed -n 's/.*Listening on port: \([0-9]*\).*/\1/p' "$log")17+ [ -n "$port" ] && break18+ sleep 0.119+done20+[ -n "$port" ] || { echo "font-page: ttyd didn't start"; cat "$log"; exit 1; }21+page=$(curl -sf "http://127.0.0.1:$port/")22+case $page in *"</head>"*) ;; *) echo "font-page: no </head> in ttyd's page"; exit 1 ;; esac23+24+face() { # weight style file25+ printf '@font-face{font-family:"JetBrains Mono";font-weight:%s;font-style:%s;src:url(data:font/ttf;base64,' "$1" "$2"26+ base64 <"$fonts/$3" | tr -d '\n'27+ printf ') format("truetype")}'28+}29+{30+ printf '%s' "${page%%</head>*}"31+ printf '<style>'32+ face 400 normal JetBrainsMono-Regular.ttf33+ face 700 normal JetBrainsMono-Bold.ttf34+ face 400 italic JetBrainsMono-Italic.ttf35+ face 700 italic JetBrainsMono-BoldItalic.ttf36+ printf '</style>'37+ # Load every style now, before VHS measures the font.38+ printf '<script>["16px","bold 16px","italic 16px","italic bold 16px"].forEach(function(f){document.fonts.load(f+" \\"JetBrains Mono\\"")})</script>'39+ printf '</head>%s' "${page#*</head>}"40+} >"$out"
M demos/lib/setup.tape +7 -4
M demos/lib/ttyd +5 -1
M demos/live.tape +1 -0
M demos/locale.tape +1 -0
M demos/macros.tape +1 -0
M demos/notebook.tape +1 -0
M demos/nu-recipe.tape +1 -0
M demos/pipeline.tape +1 -0
M demos/pivot.tape +1 -0
M demos/rules.tape +1 -0
M demos/selection.tape +1 -0
M demos/share.tape +1 -0
M demos/sheet.tape +1 -0
M demos/sources.tape +1 -0
M demos/themes.tape +1 -0
M docs/contributing/site.md +70 -10
@@ -88,8 +88,9 @@ the site's first commit for every page there has been.
8888 `.md` files are CommonMark, so `<`, `{` and HTML comments are text and8989 markup rather than MDX. A page that needs components would be `.mdx`.9090 - Each folder's `README.md` is its category's page (`/docs/sheets/`),91- and the sidebar is generated from the tree: `_category_.json` for92- folders, `title` and `sidebar_position` for pages91+ and each section (top-level folder) has a sidebar of its own92+ (`website/sidebars.ts`), generated from its folder: `_category_.json`93+ for folders, `title` and `sidebar_position` for pages9394 ([Docs](testing.md#docs) has the rules the tree follows).9495 - Images in `docs/media/` are bundled from the pages that show them;9596 an image marked `#gh-dark-mode-only` or `#gh-light-mode-only` shows in
@@ -114,10 +115,16 @@ The landing page (`website/src/pages/index.tsx`) is drawn as a 012
114115 screen: a control panel with the mode indicator, the recording in an115116 overlay frame, the features as cells of a sheet (the one under the116117 pointer shows its entry in the control panel), and a status line. The118+navbar lists the docs' sections by folder, each opening its section's119+sidebar, so the sidebar lists that section's pages rather than the120+sections again (`website/docusaurus.config.ts`); the control panel's menu bar121+lists the same sections, so on the landing page the navbar leaves them122+to it. The117123 colors in `website/src/css/custom.css` are the reference palettes of the118124 golden screens, given the roles the app gives them: cyan where you are,119-bright black header bands, blue links and selection. Headings and chrome120-are IBM Plex Mono, body text IBM Plex Sans, both bundled with the site.125+bright black header bands, blue links and selection. Code, headings and126+chrome are JetBrains Mono, bundled with the site, the font of every127+[picture of 012](#font-and-colors); body text is the system's own sans.121128 122129 ## Diagrams123130
@@ -157,17 +164,19 @@ grid's column headers).
157164 158165 A still is a [golden screen](testing.md#golden-screens) drawn as a PNG.159166 Each is an entry of `docScreens` in `e2e/stills_test.go`: its name,160-the screen whose setup it records, and a terminal size that reads on a161-page (80 columns, unless the state needs more). `make screens` records162-it on a dark and a light terminal and draws both into `docs/media`:167+the screen whose setup it records, a terminal size that reads on a168+page (80 columns, unless the state needs more) and the169+[family](#font-and-colors) of the pages that show it. `make screens`170+records it on a dark and a light terminal and draws both into171+`docs/media`, in the family's dark and light schemes:163172 164173 ```go165-{name: "evaluate", from: "evaluate", cols: 80, rows: 12},174+{name: "evaluate", from: "evaluate", cols: 80, rows: 12, family: "Catppuccin"},166175 // docs/media/evaluate-dark.png and docs/media/evaluate-light.png167176 ```168177 169-The drawing is Go (`e2e/stilldraw_test.go`): the golden's cells in the170-[reference palettes](testing.md#golden-screens), text in Go Mono, and178+The drawing is Go (`e2e/stilldraw_test.go`): the golden's cells in its179+page's [color schemes](#font-and-colors), text in JetBrains Mono, and171180 box drawing, blocks, braille and the chrome's symbols drawn as a172181 terminal draws them, so frames join and every machine draws the same173182 picture. Each PNG records which golden it was drawn from, and
@@ -192,3 +201,54 @@ one color mode; `custom.css` shows the site's the same way:
192201 ([Demo recordings](testing.md#demo-recordings)).193202 - `scripts/doccheck` fails on a still no page shows, or one shown194203 without its other color mode.204+205+## Font and colors206+207+Every picture of 012 is set in one font, JetBrains Mono: the208+recordings, the stills, and the site's code and terminal frames. It is209+VHS's and Ghostty's default, so the pictures look like the terminals210+people use; it reads well small, its italic isn't cursive, and the SIL211+Open Font License lets it be bundled. The site's comes from212+`@fontsource/jetbrains-mono`, and the stills and recordings use the213+TTFs in `e2e/testdata/fonts` (licence in `NOTICE`). VHS's browser draws214+with the fonts the system or the page gives it, so `make demos` serves215+ttyd's own page with the font added (`demos/lib/font-page`) and the216+recordings need nothing installed. Box drawing and blocks are drawn as217+shapes in both, by xterm.js in the recordings and by the stills'218+drawing, so frames join whatever the font.219+220+The pictures' colors are a few terminal color schemes people know, in221+families of a dark and a light scheme, so the docs don't all look alike222+while each picture stays plainly 012. Each page is in one family:223+224+| Family | Dark | Light | Pages |225+|---|---|---|---|226+| Catppuccin | `Catppuccin Mocha` | `Catppuccin Latte` | `getting-started/`, `contributing/`, `reference/`, `formulas/tracing.md` |227+| Tokyo Night | `tokyonight` | `tokyonight-day` | `nushell/` |228+| Rosé Pine | `rose-pine` | `rose-pine-dawn` | `sheets/rules.md`, `sheets/formatting.md`, `sheets/tables.md` |229+| GitHub | `GitHub Dark` | `Github` | `sheets/` |230+| Gruvbox | `GruvboxDark` | `Gruvbox Light` | `files/`, `formulas/` |231+| Nord | `nord` | `nord-light` | `agents/`, `terminal/` |232+233+- A page is in the family that names it, or else the one naming its234+ deepest folder. Its stills are drawn in the family's dark and light235+ schemes, and its recordings use either one (`Set Theme` in the tape,236+ or `demos/lib/setup.tape`'s). The README and the landing page, a tour237+ of every topic, show each recording as its own page does.238+- `scripts/doccheck` fails on a page showing a picture in no family,239+ and on a still or a tape in a family other than its pages'.240+- A family is a restrained, widely used scheme with both a dark and a241+ light variant: no neon or high-saturation schemes. Its schemes are242+ named as in 012's built-in list (`internal/ui/theme/themes.json`,243+ VHS's own list, so `Set Theme` takes the same names), and244+ `TestPictureSchemes` (`internal/ui/theme`) holds each to readable245+ contrast: its text on its background at least 4.5:1, and 012's roles246+ drawn in it (header bands, the selection, the pointer, muted text)247+ at their minimums.248+- The recordings run 012 with its theme set to the tape's scheme249+ (`make demos` sets `O12_THEME` from `Set Theme`), so 012's roles get250+ the scheme's colors with their contrast held, as for anyone who picks251+ that scheme. A still draws its golden's palette colors in the scheme252+ and holds each cell to the same minimums (4.5:1 for text, 3:1 for253+ lines and muted text, 2:1 for faint text), moving the text toward the254+ scheme's text color as 012 does, so it stays drawn from the golden.
M docs/contributing/testing.md +5 -4
@@ -213,7 +213,9 @@ service one question of each kind, using `TYPESAFE_API_KEY`.
213213 recordings and stills the README and the docs show. `make demos` renders214214 all of them (or `make demos DEMOS=jev` for one) into `demos/out/`: full215215 GIFs, PNG stills of key moments in `demos/out/stills/`, and smaller GIFs216-in `demos/out/media/`. It needs vhs 0.12+, ttyd and ffmpeg. The JEV tape216+in `demos/out/media/`. It needs vhs 0.12+, ttyd and ffmpeg, and installs217+the pictures' font if it's missing; each tape's color scheme follows the218+pages that show it ([Font and colors](site.md#font-and-colors)). The JEV tape217219 talks to `demos/fakejev`, a local stand-in the target starts on218220 127.0.0.1, never the real service. On macOS, `demos/lib/ttyd` wraps ttyd219221 so Alt+letter reaches the app. VHS has no mouse commands, so the tapes
@@ -221,9 +223,8 @@ use the keyboard, and they reach commands through the palette (Ctrl+K)
221223 rather than counting menu items, so a new menu item doesn't break them.222224 Tapes that change settings set them for the run (`O12_KEYMAP=vim`)223225 rather than writing the config file the other tapes share. VHS's ttyd224-has sixel on, so 012 draws charts as sixel images, but VHS records only225-xterm.js's text layer: the recordings show the text chart each image226-covers ([Charts](../sheets/charts.md)).226+has sixel on, so 012 draws charts as sixel images and the recordings227+show them ([Charts](../sheets/charts.md)).227228 The nushell tapes need `nu`: `demos/lib/nu.tape` starts a session228229 without your config files, with a fixed prompt and 012 on the PATH, and229230 their data is in `demos/data`, so each recording comes out the same.
M docs/files/README.md +1 -1
M docs/media/arrays.gif +0 -0

Binary file.

M docs/media/charts.gif +0 -0

Binary file.

M docs/media/decimal-on.png +0 -0

Binary file.

M docs/media/evaluate-dark.png +0 -0

Binary file.

M docs/media/evaluate-light.png +0 -0

Binary file.

M docs/media/find-replace.gif +0 -0

Binary file.

M docs/media/first-steps.gif +0 -0

Binary file.

M docs/media/follow-log.gif +0 -0

Binary file.

M docs/media/formats-budget.png +0 -0

Binary file.

M docs/media/freeze-sort-filter.gif +0 -0

Binary file.

M docs/media/import-picker.png +0 -0

Binary file.

M docs/media/jev.gif +0 -0

Binary file.

M docs/media/layout-dark.png +0 -0

Binary file.

M docs/media/layout-light.png +0 -0

Binary file.

M docs/media/layout.gif +0 -0

Binary file.

M docs/media/live-ask-dark.png +0 -0

Binary file.

M docs/media/live-ask-light.png +0 -0

Binary file.

M docs/media/live-review-dark.png +0 -0

Binary file.

M docs/media/live-review-light.png +0 -0

Binary file.

M docs/media/live-suggestion-dark.png +0 -0

Binary file.

M docs/media/live-suggestion-light.png +0 -0

Binary file.

M docs/media/live.gif +0 -0

Binary file.

M docs/media/locale-de.png +0 -0

Binary file.

M docs/media/macros.gif +0 -0

Binary file.

M docs/media/menus-palette.gif +0 -0

Binary file.

M docs/media/notebook-cells-dark.png +0 -0

Binary file.

M docs/media/notebook-cells-light.png +0 -0

Binary file.

M docs/media/notebook-grid-dark.png +0 -0

Binary file.

M docs/media/notebook-grid-light.png +0 -0

Binary file.

M docs/media/notebook-nu-error-dark.png +0 -0

Binary file.

M docs/media/notebook-nu-error-light.png +0 -0

Binary file.

M docs/media/notebook-nu-help-dark.png +0 -0

Binary file.

M docs/media/notebook-nu-help-light.png +0 -0

Binary file.

M docs/media/notebook-nu-hover-dark.png +0 -0

Binary file.

M docs/media/notebook-nu-hover-light.png +0 -0

Binary file.

M docs/media/notebook.gif +0 -0

Binary file.

M docs/media/nu-recipe.gif +0 -0

Binary file.

M docs/media/palette-dark.png +0 -0

Binary file.

M docs/media/palette-light.png +0 -0

Binary file.

M docs/media/pipeline.gif +0 -0

Binary file.

M docs/media/pivot.gif +0 -0

Binary file.

M docs/media/rules-bars-dark.png +0 -0

Binary file.

M docs/media/rules-bars-light.png +0 -0

Binary file.

M docs/media/rules-panel-dark.png +0 -0

Binary file.

M docs/media/rules-panel-light.png +0 -0

Binary file.

M docs/media/rules.gif +0 -0

Binary file.

M docs/media/selection-stats.png +0 -0

Binary file.

M docs/media/share-presence-dark.png +0 -0

Binary file.

M docs/media/share-presence-light.png +0 -0

Binary file.

M docs/media/share.gif +0 -0

Binary file.

M docs/media/sheet.gif +0 -0

Binary file.

M docs/media/source-formulas-dark.png +0 -0

Binary file.

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

Binary file.

M docs/media/source-formulas-why-dark.png +0 -0

Binary file.

M docs/media/source-formulas-why-light.png +0 -0

Binary file.

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

Binary file.

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

Binary file.

M docs/media/sources.gif +0 -0

Binary file.

M docs/media/table-dark.png +0 -0

Binary file.

M docs/media/table-light.png +0 -0

Binary file.

M docs/media/theme-classic.png +0 -0

Binary file.

M docs/media/trace-view-dark.png +0 -0

Binary file.

M docs/media/trace-view-light.png +0 -0

Binary file.

M docs/media/tracing.gif +0 -0

Binary file.

M docs/media/vim.gif +0 -0

Binary file.

M docs/sheets/charts.md +1 -2
M e2e/stillcells_test.go +0 -48
M e2e/stilldraw_test.go +54 -35
@@ -6,6 +6,8 @@ import (
66 "image/color"77 "image/draw"88 "math"9+ "os"10+ "path/filepath"911 1012 "golang.org/x/image/font"1113 "golang.org/x/image/font/gofont/gomono"
@@ -17,11 +19,12 @@ import (
1719 "golang.org/x/image/vector"1820 )1921 20-// Drawing a screen's cells as a picture of a terminal: text in Go Mono21-// (bundled with golang.org/x/image, so every machine draws the same22-// letters), and box drawing, blocks, braille and the symbols 012 draws23-// its chrome with drawn as shapes, as terminals draw them, so lines join24-// across cells.22+// Drawing a screen's cells as a picture of a terminal: text in23+// JetBrains Mono, the font of the recordings and the site (testdata/fonts,24+// so every machine draws the same letters), with Go Mono for the few25+// characters it has none of, and box drawing, blocks, braille and the26+// symbols 012 draws its chrome with drawn as shapes, as terminals draw27+// them, so lines join across cells.2528 2629 const (2730 stillFontSize = 26 // pixels; stills are drawn at twice the size they show
@@ -29,46 +32,63 @@ const (
2932 stillRadius = 18 // the window's corners3033 )3134 32-// stillFaces are Go Mono's four styles at the still's size.35+// stillFaces are the font's four styles at the still's size, regular,36+// bold, italic and bold italic, and Go Mono's for what the font lacks.3337 type stillFaces struct {34- regular, bold, italic, boldItalic font.Face35- cw, ch, ascent int38+ faces, fallback [4]font.Face39+ cw, ch, ascent int3640 }3741 42+// fontFiles are the styles' files in testdata/fonts.43+var fontFiles = [4]string{"JetBrainsMono-Regular.ttf", "JetBrainsMono-Bold.ttf", "JetBrainsMono-Italic.ttf", "JetBrainsMono-BoldItalic.ttf"}44+3845 func loadFaces() (*stillFaces, error) {3946 var fs stillFaces40- for _, f := range []struct {41- ttf []byte42- dst *font.Face43- }{{gomono.TTF, &fs.regular}, {gomonobold.TTF, &fs.bold}, {gomonoitalic.TTF, &fs.italic}, {gomonobolditalic.TTF, &fs.boldItalic}} {44- parsed, err := opentype.Parse(f.ttf)47+ goMono := [4][]byte{gomono.TTF, gomonobold.TTF, gomonoitalic.TTF, gomonobolditalic.TTF}48+ for i, name := range fontFiles {49+ ttf, err := os.ReadFile(filepath.Join("testdata", "fonts", name))4550 if err != nil {4651 return nil, err4752 }48- face, err := opentype.NewFace(parsed, &opentype.FaceOptions{Size: stillFontSize, DPI: 72, Hinting: font.HintingNone})49- if err != nil {53+ if fs.faces[i], err = newFace(ttf); err != nil {54+ return nil, fmt.Errorf("%s: %w", name, err)55+ }56+ if fs.fallback[i], err = newFace(goMono[i]); err != nil {5057 return nil, err5158 }52- *f.dst = face5359 }54- adv, _ := fs.regular.GlyphAdvance('0')55- m := fs.regular.Metrics()60+ adv, _ := fs.faces[0].GlyphAdvance('0')61+ m := fs.faces[0].Metrics()5662 fs.cw = adv.Round()5763 fs.ch = int(math.Round(stillFontSize * 1.3))5864 fs.ascent = (fs.ch-(m.Ascent+m.Descent).Round())/2 + m.Ascent.Round()5965 return &fs, nil6066 }6167 68+func newFace(ttf []byte) (font.Face, error) {69+ parsed, err := opentype.Parse(ttf)70+ if err != nil {71+ return nil, err72+ }73+ return opentype.NewFace(parsed, &opentype.FaceOptions{Size: stillFontSize, DPI: 72, Hinting: font.HintingNone})74+}75+76+// face is the face c's rune is drawn in: its style in the font, or in Go77+// Mono when the font has no glyph for it; nil when neither has.6278 func (fs *stillFaces) face(c stillCell) font.Face {63- switch {64- case c.bold && c.italic:65- return fs.boldItalic66- case c.bold:67- return fs.bold68- case c.italic:69- return fs.italic70- }71- return fs.regular79+ i := 080+ if c.bold {81+ i++82+ }83+ if c.italic {84+ i += 285+ }86+ for _, f := range []font.Face{fs.faces[i], fs.fallback[i]} {87+ if _, ok := f.GlyphAdvance(c.r); ok {88+ return f89+ }90+ }91+ return nil7292 }7393 7494 // drawStill draws grid as a terminal window in palette p. It returns the
@@ -112,19 +132,15 @@ func drawCell(img *image.NRGBA, row []stillCell, x int, origin image.Point, p st
112132 if c.r == 0 && c.filled {113133 return true // the second half of a wide character, drawn with the first114134 }115- bg := p.color(c.bg, inkBg)135+ fg, bg := p.inks(c)116136 if c.bg.kind != inkNone {117137 draw.Draw(img, image.Rect(origin.X, origin.Y, origin.X+fs.cw, origin.Y+fs.ch), image.NewUniform(bg), image.Point{}, draw.Src)118138 }119- fg := p.color(c.fg, inkFg)120- if c.faint {121- fg = blend(bg, fg, 0.6)122- }123139 ok := true124140 if c.r != 0 && c.r != ' ' && c.r != ' ' {125141 ok = drawGlyph(img, c, r, fg, fs)126142 }127- lines(img, c, r, fg, p, fs)143+ lines(img, c, r, fg, bg, p, fs)128144 return ok129145 }130146
@@ -135,7 +151,7 @@ func drawGlyph(img *image.NRGBA, c stillCell, r image.Rectangle, fg color.RGBA,
135151 return true136152 }137153 face := fs.face(c)138- if _, ok := face.GlyphAdvance(c.r); !ok {154+ if face == nil {139155 return false140156 }141157 adv, _ := face.GlyphAdvance(c.r)
@@ -147,7 +163,7 @@ func drawGlyph(img *image.NRGBA, c stillCell, r image.Rectangle, fg color.RGBA,
147163 }148164 149165 // lines draws c's underline and strikethrough.150-func lines(img *image.NRGBA, c stillCell, r image.Rectangle, fg color.RGBA, p stillPalette, fs *stillFaces) {166+func lines(img *image.NRGBA, c stillCell, r image.Rectangle, fg, bg color.RGBA, p stillPalette, fs *stillFaces) {151167 t := lineWidth(fs.cw)152168 if c.strike {153169 y := r.Min.Y + fs.ascent - stillFontSize*3/10
@@ -159,6 +175,9 @@ func lines(img *image.NRGBA, c stillCell, r image.Rectangle, fg color.RGBA, p st
159175 col := fg160176 if c.ulInk.kind != inkNone {161177 col = p.color(c.ulInk, inkFg)178+ if c.ulInk.kind == inkPalette {179+ col = readable(col, bg, rgba(p.fg), 3)180+ }162181 }163182 y := r.Min.Y + fs.ascent + t*2164183 switch c.underline {
M e2e/stills_test.go +46 -28
@@ -18,39 +18,42 @@ import (
1818 // Stills are pictures of 012 for the docs, drawn from golden screens:1919 // each docScreen is recorded again from another screen's setup, on a2020 // terminal the size a page shows well, dark and light, and drawn in its21-// reference palette to docs/media/<name>-dark.png and <name>-light.png.22-// `make screens` records and draws them; `make e2e` fails when a23-// still wasn't drawn from its golden as it is.21+// family's dark and light schemes (stillthemes_test.go) to22+// docs/media/<name>-dark.png and <name>-light.png. The family is the one23+// the pages showing it are in (scripts/doccheck checks it). `make24+// screens` records and draws them; `make e2e` fails when a still wasn't25+// drawn from its golden as it is.2426 type docScreen struct {2527 name, from string // docs/media/<name>-*.png; the screen whose setup it records2628 cols, rows uint1629+ family string // a row of the families table in docs/contributing/site.md2730 }2831 2932 var docScreens = []docScreen{30- {name: "trace-view", from: "trace-view", cols: 80, rows: 14},31- {name: "evaluate", from: "evaluate", cols: 80, rows: 12},32- {name: "rules-bars", from: "rules-bars", cols: 80, rows: 12},33- {name: "rules-panel", from: "rules-panel", cols: 100, rows: 12},34- {name: "table", from: "table", cols: 80, rows: 13},35- {name: "layout", from: "layout", cols: 80, rows: 28},36- {name: "palette", from: "palette", cols: 80, rows: 18},37- {name: "notebook-cells", from: "notebook-cells", cols: 80, rows: 26},38- {name: "notebook-nu-error", from: "notebook-nu-error", cols: 80, rows: 16},39- {name: "notebook-nu-hover", from: "notebook-nu-hover", cols: 100, rows: 16},40- {name: "notebook-nu-help", from: "notebook-nu-help", cols: 100, rows: 24},41- {name: "notebook-grid", from: "notebook-grid", cols: 80, rows: 24},42- {name: "share-presence", from: "share-presence", cols: 80, rows: 13},43- {name: "live-suggestion", from: "live-suggestion", cols: 80, rows: 12},44- {name: "live-review", from: "live-review", cols: 100, rows: 12},45- {name: "live-ask", from: "live-ask", cols: 80, rows: 10},46- {name: "source-tab", from: "source-tab", cols: 80, rows: 20},47- {name: "source-formulas", from: "source-formulas", cols: 80, rows: 14},48- {name: "source-formulas-why", from: "source-formulas-why", cols: 80, rows: 19},33+ {name: "trace-view", from: "trace-view", cols: 80, rows: 14, family: "Catppuccin"},34+ {name: "evaluate", from: "evaluate", cols: 80, rows: 12, family: "Catppuccin"},35+ {name: "rules-bars", from: "rules-bars", cols: 80, rows: 12, family: "Rosé Pine"},36+ {name: "rules-panel", from: "rules-panel", cols: 100, rows: 12, family: "Rosé Pine"},37+ {name: "table", from: "table", cols: 80, rows: 13, family: "Rosé Pine"},38+ {name: "layout", from: "layout", cols: 80, rows: 28, family: "Rosé Pine"},39+ {name: "palette", from: "palette", cols: 80, rows: 18, family: "Catppuccin"},40+ {name: "notebook-cells", from: "notebook-cells", cols: 80, rows: 26, family: "Tokyo Night"},41+ {name: "notebook-nu-error", from: "notebook-nu-error", cols: 80, rows: 16, family: "Tokyo Night"},42+ {name: "notebook-nu-hover", from: "notebook-nu-hover", cols: 100, rows: 16, family: "Tokyo Night"},43+ {name: "notebook-nu-help", from: "notebook-nu-help", cols: 100, rows: 24, family: "Tokyo Night"},44+ {name: "notebook-grid", from: "notebook-grid", cols: 80, rows: 24, family: "Tokyo Night"},45+ {name: "share-presence", from: "share-presence", cols: 80, rows: 13, family: "Nord"},46+ {name: "live-suggestion", from: "live-suggestion", cols: 80, rows: 12, family: "Nord"},47+ {name: "live-review", from: "live-review", cols: 100, rows: 12, family: "Nord"},48+ {name: "live-ask", from: "live-ask", cols: 80, rows: 10, family: "Nord"},49+ {name: "source-tab", from: "source-tab", cols: 80, rows: 20, family: "Gruvbox"},50+ {name: "source-formulas", from: "source-formulas", cols: 80, rows: 14, family: "Gruvbox"},51+ {name: "source-formulas-why", from: "source-formulas-why", cols: 80, rows: 19, family: "Gruvbox"},4952 }5053 5154 // stillsVersion changes when the drawing does, so every still is5255 // drawn again.53-const stillsVersion = "1"56+const stillsVersion = "2"5457 5558 // addDocScreens adds the docs' screens, docs-<name> and5659 // docs-<name>-light, to the golden screens once every screen they're
@@ -79,18 +82,33 @@ func TestStills(t *testing.T) {
7982 if err != nil {8083 t.Fatal(err)8184 }85+ fams, err := pictureFamilies()86+ if err != nil {87+ t.Fatal(err)88+ }89+ schemes, err := loadSchemes()90+ if err != nil {91+ t.Fatal(err)92+ }8293 for _, d := range docScreens {83- for _, v := range []struct {84- suffix string85- p stillPalette86- }{{"dark", darkStill}, {"light", lightStill}} {94+ fam, ok := fams[d.family]95+ if !ok {96+ t.Errorf("still %s: no family %q in %s", d.name, d.family, familiesDoc)97+ continue98+ }99+ for _, v := range []struct{ suffix, scheme string }{{"dark", fam.dark}, {"light", fam.light}} {100+ p, ok := schemes[v.scheme]101+ if !ok {102+ t.Errorf("family %s: no scheme %q in %s", d.family, v.scheme, schemesFile)103+ continue104+ }87105 screen := "docs-" + d.name88106 if v.suffix == "light" {89107 screen += "-light"90108 }91109 t.Run(d.name+"-"+v.suffix, func(t *testing.T) {92110 path := filepath.Join("..", "docs", "media", d.name+"-"+v.suffix+".png")93- checkStill(t, screen, int(d.cols), int(d.rows), v.p, faces, path)111+ checkStill(t, screen, int(d.cols), int(d.rows), p, faces, path)94112 })95113 }96114 }
A e2e/stillthemes_test.go +227 -0
@@ -0,0 +1,227 @@
1+package e2e2+3+import (4+ "encoding/json"5+ "fmt"6+ "image/color"7+ "math"8+ "os"9+ "regexp"10+ "strconv"11+ "strings"12+)13+14+// Stills are drawn in a terminal color scheme: the golden's palette15+// indexes in the scheme's colors, so a still stays tied to its golden16+// whatever scheme shows it. The schemes are 012's built-in ones17+// (internal/ui/theme/themes.json, the file VHS's Set Theme reads too),18+// and the families the docs use are the table in19+// docs/contributing/site.md, which the tapes follow as well.20+21+const (22+ schemesFile = "../internal/ui/theme/themes.json"23+ familiesDoc = "../docs/contributing/site.md"24+)25+26+// stillPalette is a terminal's colors: a scheme's 16 ANSI colors, text27+// and background.28+type stillPalette struct {29+ name string30+ ansi [16]uint3231+ fg, bg uint3232+}33+34+// family is a row of the families table: a dark scheme and a light one.35+type family struct{ dark, light string }36+37+// familyRow is a row of the table: | Family | `dark` | `light` | pages |.38+var familyRow = regexp.MustCompile("(?m)^\\| ([^|`]+?) \\| `([^`]+)` \\| `([^`]+)` \\|")39+40+// pictureFamilies reads the families table from the docs.41+func pictureFamilies() (map[string]family, error) {42+ src, err := os.ReadFile(familiesDoc)43+ if err != nil {44+ return nil, err45+ }46+ fams := map[string]family{}47+ for _, m := range familyRow.FindAllStringSubmatch(string(src), -1) {48+ fams[m[1]] = family{dark: m[2], light: m[3]}49+ }50+ if len(fams) == 0 {51+ return nil, fmt.Errorf("%s: no families table", familiesDoc)52+ }53+ return fams, nil54+}55+56+// loadSchemes reads the built-in schemes by name.57+func loadSchemes() (map[string]stillPalette, error) {58+ src, err := os.ReadFile(schemesFile)59+ if err != nil {60+ return nil, err61+ }62+ var raw []map[string]any63+ if err := json.Unmarshal(src, &raw); err != nil {64+ return nil, err65+ }66+ slots := [16]string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",67+ "brightBlack", "brightRed", "brightGreen", "brightYellow", "brightBlue", "brightMagenta", "brightCyan", "brightWhite"}68+ out := map[string]stillPalette{}69+ for _, s := range raw {70+ name, _ := s["name"].(string)71+ p := stillPalette{name: name}72+ ok := hexInto(s["foreground"], &p.fg) && hexInto(s["background"], &p.bg)73+ for i, k := range slots {74+ ok = ok && hexInto(s[k], &p.ansi[i])75+ }76+ if ok {77+ out[name] = p78+ }79+ }80+ return out, nil81+}82+83+func hexInto(v any, dst *uint32) bool {84+ s, _ := v.(string)85+ n, err := strconv.ParseUint(strings.TrimPrefix(s, "#"), 16, 32)86+ if err != nil || len(s) != 7 {87+ return false88+ }89+ *dst = uint32(n)90+ return true91+}92+93+// color resolves an ink; def is what inkNone means in its slot.94+func (p stillPalette) color(k ink, def inkKind) color.RGBA {95+ if k.kind == inkNone {96+ k.kind = def97+ }98+ switch k.kind {99+ case inkFg:100+ return rgba(p.fg)101+ case inkPalette:102+ return rgba(p.xterm(k.palette))103+ case inkRGB:104+ return k.rgb105+ }106+ return rgba(p.bg)107+}108+109+// xterm is palette entry n: the 16 ANSI colors, then xterm's 6x6x6 cube110+// and gray ramp.111+func (p stillPalette) xterm(n int) uint32 {112+ switch {113+ case n < 16:114+ return p.ansi[n]115+ case n < 232:116+ n -= 16117+ level := func(v int) uint32 {118+ if v == 0 {119+ return 0120+ }121+ return uint32(55 + 40*v)122+ }123+ return level(n/36)<<16 | level(n/6%6)<<8 | level(n%6)124+ }125+ g := uint32(8 + 10*(n-232))126+ return g<<16 | g<<8 | g127+}128+129+// inks are a cell's colors in p, held to the contrast 012's schemes hold130+// their roles to (internal/ui/theme's FromPalette), since a scheme's131+// colors weren't chosen for 012's: a background from the palette stands132+// apart from the screen, and text moves toward the scheme's text color,133+// or black or white, until it reaches its minimum. Colors the user chose134+// as RGB are drawn as they are.135+func (p stillPalette) inks(c stillCell) (fg, bg color.RGBA) {136+ text, screen := rgba(p.fg), rgba(p.bg)137+ bg = p.color(c.bg, inkBg)138+ if c.bg.kind == inkPalette {139+ bg = distinct(bg, screen, text)140+ }141+ fg = p.color(c.fg, inkFg)142+ if c.faint {143+ fg = blend(bg, fg, 0.6)144+ }145+ if c.fg.kind != inkRGB && c.bg.kind != inkRGB {146+ fg = readable(fg, bg, text, minContrast(c))147+ }148+ return fg, bg149+}150+151+// minContrast is a cell's minimum, as theme's: 4.5:1 for text, 3:1 for152+// lines and 012's muted gray (hints, borders), 2:1 for faint text.153+func minContrast(c stillCell) float64 {154+ switch {155+ case c.faint:156+ return 2157+ case c.r >= 0x2500 && c.r <= 0x28ff, c.fg.kind == inkPalette && c.fg.palette == 8:158+ return 3159+ }160+ return 4.5161+}162+163+// The contrast arithmetic is internal/ui/theme's (contrast.go), which164+// this module can't import: WCAG 2 contrast, and a color that falls165+// short moved just far enough.166+167+func luminance(c color.RGBA) float64 {168+ lin := func(v uint8) float64 {169+ f := float64(v) / 0xff170+ if f <= 0.04045 {171+ return f / 12.92172+ }173+ return math.Pow((f+0.055)/1.055, 2.4)174+ }175+ return 0.2126*lin(c.R) + 0.7152*lin(c.G) + 0.0722*lin(c.B)176+}177+178+func contrast(a, b color.RGBA) float64 {179+ la, lb := luminance(a), luminance(b)180+ if la < lb {181+ la, lb = lb, la182+ }183+ return (la + 0.05) / (lb + 0.05)184+}185+186+// readable is fg, or fg moved toward toward, or failing that black or187+// white, until it has min contrast with bg.188+func readable(fg, bg, toward color.RGBA, min float64) color.RGBA {189+ if contrast(fg, bg) >= min {190+ return fg191+ }192+ if contrast(toward, bg) >= min {193+ return blendUntil(fg, toward, func(c color.RGBA) bool { return contrast(c, bg) >= min })194+ }195+ end := color.RGBA{0xff, 0xff, 0xff, 0xff}196+ if luminance(fg) <= luminance(bg) {197+ end = color.RGBA{0, 0, 0, 0xff}198+ }199+ if contrast(end, bg) < min {200+ end = color.RGBA{0xff - end.R, 0xff - end.G, 0xff - end.B, 0xff}201+ }202+ return blendUntil(fg, end, func(c color.RGBA) bool { return contrast(c, bg) >= min })203+}204+205+// distinct is bg, or bg moved toward toward until it stands apart from206+// the screen (1.2:1).207+func distinct(bg, screen, toward color.RGBA) color.RGBA {208+ if contrast(bg, screen) >= 1.2 {209+ return bg210+ }211+ return blendUntil(bg, toward, func(c color.RGBA) bool { return contrast(c, screen) >= 1.2 })212+}213+214+// blendUntil is the color nearest from on the way to end for which ok215+// holds; ok(end) must.216+func blendUntil(from, end color.RGBA, ok func(color.RGBA) bool) color.RGBA {217+ lo, hi := 0.0, 1.0218+ for range 24 {219+ mid := (lo + hi) / 2220+ if ok(blend(from, end, mid)) {221+ hi = mid222+ } else {223+ lo = mid224+ }225+ }226+ return blend(from, end, hi)227+}
A e2e/testdata/fonts/JetBrainsMono-Bold.ttf +0 -0

Binary file.

A e2e/testdata/fonts/JetBrainsMono-BoldItalic.ttf +0 -0

Binary file.

A e2e/testdata/fonts/JetBrainsMono-Italic.ttf +0 -0

Binary file.

A e2e/testdata/fonts/JetBrainsMono-Regular.ttf +0 -0

Binary file.

A e2e/testdata/fonts/OFL.txt +93 -0
@@ -0,0 +1,93 @@
1+Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)2+3+This Font Software is licensed under the SIL Open Font License, Version 1.1.4+This license is copied below, and is also available with a FAQ at:5+https://scripts.sil.org/OFL6+7+8+-----------------------------------------------------------9+SIL OPEN FONT LICENSE Version 1.1 - 26 February 200710+-----------------------------------------------------------11+12+PREAMBLE13+The goals of the Open Font License (OFL) are to stimulate worldwide14+development of collaborative font projects, to support the font creation15+efforts of academic and linguistic communities, and to provide a free and16+open framework in which fonts may be shared and improved in partnership17+with others.18+19+The OFL allows the licensed fonts to be used, studied, modified and20+redistributed freely as long as they are not sold by themselves. The21+fonts, including any derivative works, can be bundled, embedded,22+redistributed and/or sold with any software provided that any reserved23+names are not used by derivative works. The fonts and derivatives,24+however, cannot be released under any other type of license. The25+requirement for fonts to remain under this license does not apply26+to any document created using the fonts or their derivatives.27+28+DEFINITIONS29+"Font Software" refers to the set of files released by the Copyright30+Holder(s) under this license and clearly marked as such. This may31+include source files, build scripts and documentation.32+33+"Reserved Font Name" refers to any names specified as such after the34+copyright statement(s).35+36+"Original Version" refers to the collection of Font Software components as37+distributed by the Copyright Holder(s).38+39+"Modified Version" refers to any derivative made by adding to, deleting,40+or substituting -- in part or in whole -- any of the components of the41+Original Version, by changing formats or by porting the Font Software to a42+new environment.43+44+"Author" refers to any designer, engineer, programmer, technical45+writer or other person who contributed to the Font Software.46+47+PERMISSION & CONDITIONS48+Permission is hereby granted, free of charge, to any person obtaining49+a copy of the Font Software, to use, study, copy, merge, embed, modify,50+redistribute, and sell modified and unmodified copies of the Font51+Software, subject to the following conditions:52+53+1) Neither the Font Software nor any of its individual components,54+in Original or Modified Versions, may be sold by itself.55+56+2) Original or Modified Versions of the Font Software may be bundled,57+redistributed and/or sold with any software, provided that each copy58+contains the above copyright notice and this license. These can be59+included either as stand-alone text files, human-readable headers or60+in the appropriate machine-readable metadata fields within text or61+binary files as long as those fields can be easily viewed by the user.62+63+3) No Modified Version of the Font Software may use the Reserved Font64+Name(s) unless explicit written permission is granted by the corresponding65+Copyright Holder. This restriction only applies to the primary font name as66+presented to the users.67+68+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font69+Software shall not be used to promote, endorse or advertise any70+Modified Version, except to acknowledge the contribution(s) of the71+Copyright Holder(s) and the Author(s) or with their explicit written72+permission.73+74+5) The Font Software, modified or unmodified, in part or in whole,75+must be distributed entirely under this license, and must not be76+distributed under any other license. The requirement for fonts to77+remain under this license does not apply to any document created78+using the Font Software.79+80+TERMINATION81+This license becomes null and void if any of the above conditions are82+not met.83+84+DISCLAIMER85+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,86+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF87+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT88+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE89+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,90+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL91+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING92+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM93+OTHER DEALINGS IN THE FONT SOFTWARE.
M internal/fileio/fonts/OFL.txt +2 -2
D internal/fileio/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0

Binary file.

D internal/fileio/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0

Binary file.

D internal/fileio/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0

Binary file.

A internal/fileio/fonts/jetbrains-mono-latin-400-italic.woff2 +0 -0

Binary file.

A internal/fileio/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0

Binary file.

A internal/fileio/fonts/jetbrains-mono-latin-600-normal.woff2 +0 -0

Binary file.

M internal/fileio/html_test.go +1 -1
M internal/fileio/htmlstyle.go +7 -7
@@ -16,7 +16,7 @@ import (
1616 // reference palettes the golden screens use (e2e/palette_test.go),1717 // mapped to the roles the terminal theme gives them1818 // (internal/ui/theme), dark or light as the reader's system prefers,19-// in IBM Plex Mono, the docs site's font, embedded so the page needs19+// in JetBrains Mono, the docs site's font, embedded so the page needs2020 // nothing else. One cell is htmlCellW by htmlCellH pixels, the size of2121 // a cell of an SVG chart, so charts sit on the grid as on the screen.2222
@@ -103,12 +103,12 @@ func luminance(c uint32) float64 {
103103 var htmlCSS = sync.OnceValue(func() string {104104 var b strings.Builder105105 for _, f := range []struct{ file, weight, style string }{106- {"ibm-plex-mono-latin-400-normal.woff2", "400", "normal"},107- {"ibm-plex-mono-latin-600-normal.woff2", "600", "normal"},108- {"ibm-plex-mono-latin-400-italic.woff2", "400", "italic"},106+ {"jetbrains-mono-latin-400-normal.woff2", "400", "normal"},107+ {"jetbrains-mono-latin-600-normal.woff2", "600", "normal"},108+ {"jetbrains-mono-latin-400-italic.woff2", "400", "italic"},109109 } {110110 data, _ := fonts.ReadFile("fonts/" + f.file)111- fmt.Fprintf(&b, "@font-face{font-family:'IBM Plex Mono';font-weight:%s;font-style:%s;src:url(data:font/woff2;base64,%s) format('woff2')}\n",111+ fmt.Fprintf(&b, "@font-face{font-family:'JetBrains Mono';font-weight:%s;font-style:%s;src:url(data:font/woff2;base64,%s) format('woff2')}\n",112112 f.weight, f.style, base64.StdEncoding.EncodeToString(data))113113 }114114 dark, light := htmlPalettes[0].vars(), htmlPalettes[1].vars()
@@ -125,7 +125,7 @@ func HTMLStyle() string { return htmlCSS() }
125125 // htmlRules are the page's rules, after its fonts and palettes.126126 const htmlRules = `*{box-sizing:border-box}127127 html,body{margin:0;background:var(--bg);color:var(--fg)}128-body{font:15px/21px 'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace;font-variant-ligatures:none}128+body{font:15px/21px 'JetBrains Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace;font-variant-ligatures:none}129129 .o12{min-width:100%;width:max-content}130130 .panel{position:sticky;left:0;top:0;z-index:3;background:var(--bg);white-space:pre;width:100vw;max-width:100%}131131 .panel>div{height:21px;display:flex;gap:9px;overflow:hidden}
@@ -160,7 +160,7 @@ figure.chart{position:absolute;margin:0;border:1px solid var(--axis);background:
160160 figure.chart figcaption{position:absolute;top:-11px;left:9px;background:var(--bg);padding:0 9px;font-weight:600;white-space:pre}161161 figure.chart .range{position:absolute;bottom:-11px;right:9px;background:var(--bg);padding:0 9px;color:var(--muted);white-space:pre}162162 figure.solo{position:relative;display:inline-block;margin:21px 18px}163-svg.chart{display:block;overflow:visible;font:15px 'IBM Plex Mono',ui-monospace,monospace}163+svg.chart{display:block;overflow:visible;font:15px 'JetBrains Mono',ui-monospace,monospace}164164 .ct-ax{fill:var(--axis);color:var(--axis)}.ct-lb{fill:var(--muted);color:var(--muted)}.ct-mu{fill:var(--muted);color:var(--muted)}165165 .cs-box{stroke:currentColor;stroke-width:1}.cs-gl{stroke:var(--grid);stroke-width:1}.cs-gap{stroke:var(--bg);stroke-width:1.5}166166 .ct-s0,.cf-0{fill:var(--s0)}.ct-s1,.cf-1{fill:var(--s1)}.ct-s2,.cf-2{fill:var(--s2)}.ct-s3,.cf-3{fill:var(--s3)}.ct-s4,.cf-4{fill:var(--s4)}.ct-s5,.cf-5{fill:var(--s5)}
A internal/sheet/testdata/fixtures/v0.8.0/plain.012 +10 -0
A internal/sheet/testdata/fixtures/v0.8.0/single.012 +21 -0
A internal/sheet/testdata/fixtures/v0.8.0/workbook.012 +185 -0
@@ -0,0 +1,185 @@
1+{2+ "version": 7,3+ "names": {"Lost": "#REF!", "Rate": "Lists!B1", "Units": "Sales!C2:C9"},4+ "macroOrigin": "fixture-machine",5+ "macros": [6+ {"name": "Bold header", "key": "1", "api": 1, "source": "select(\"A1:E1\")\nrun(\"format.bold\")\n"},7+ {"name": "Fill totals", "api": 1, "source": "set(\"F2\", \"=SUM(C2:C9)\")\n"}8+ ],9+ "sheets": [10+ {11+ "name": "Sales",12+ "widths": {"A": 12, "E": 14},13+ "heights": {"1": 2, "12": 3},14+ "lines": {"A:XFD": {"italic":true}, "D:D": {"format":"currency","decimals":2}, "1:1": {"bold":true,"align":"center","borders":{"bottom":"double"}}},15+ "freeze": {"rows":1,"cols":1},16+ "filter": {"range":"A1:E9","columns":{"B":{"hidden":["West"]},"C":{"condition":"gt","value":"2"}}},17+ "protected": [{"range":"E2:E9","description":"Totals"}],18+ "merges": ["A11:C11","A12:A13"],19+ "cells": {20+ "A1": "Date",21+ "B1": "Region",22+ "C1": "Units",23+ "D1": "Price",24+ "E1": "Total",25+ "G1": "Share",26+ "H1": "Regions",27+ "I1": "Scaled",28+ "J1": {"input":"007","text":true},29+ "A2": {"input":"2026-01-05","format":"date"},30+ "B2": "North",31+ "C2": "3",32+ "D2": "12.5",33+ "E2": {"input":"=C2*D2","format":"currency","decimals":2,"own":true},34+ "G2": {"input":"=C2/SUM(Units)","format":"percent","decimals":1},35+ "H2": "=SORT(UNIQUE(B2:B9))",36+ "I2": "=LET(r, Rate, MAP(C2:C4, LAMBDA(x, x*r)))",37+ "J2": {"input":"=1+1","text":false,"format":"text"},38+ "A3": {"input":"2026-01-12","format":"date"},39+ "B3": {"input":"South","note":"Includes returns"},40+ "C3": "5",41+ "D3": "9.75",42+ "E3": "=C3*D3",43+ "G3": {"input":"=C3/SUM(Units)","format":"custom","pattern":"0.00%;[Red]-0.00%"},44+ "A4": {"input":"2026-02-02","format":"date"},45+ "B4": "West",46+ "C4": "1",47+ "D4": "40",48+ "E4": "=C4*D4",49+ "A5": {"input":"2026-02-16","format":"date"},50+ "B5": "North",51+ "C5": "7",52+ "D5": "12.5",53+ "E5": "=C5*D5",54+ "A6": {"input":"2026-03-01","format":"date"},55+ "B6": "East",56+ "C6": {"input":"2","bold":true,"italic":true,"underline":true,"strikethrough":true},57+ "D6": "22",58+ "E6": "=C6*D6",59+ "A7": {"input":"2026-03-09","format":"date"},60+ "B7": "South",61+ "C7": {"input":"4","align":"left","valign":"top"},62+ "D7": "9.75",63+ "E7": "=C7*D7",64+ "A8": {"input":"2026-03-20","format":"date"},65+ "B8": "East",66+ "C8": {"input":"6","borders":{"top":"thin","bottom":"double","left":"thick","right":"thin","bottomColor":"red","leftColor":"blue"}},67+ "D8": "22",68+ "E8": "=C8*D8",69+ "A9": {"input":"2026-03-31","format":"date"},70+ "B9": "West",71+ "C9": "8",72+ "D9": "40",73+ "E9": "=C9*D9",74+ "A11": {"input":"A long title merged across three columns","align":"center","wrap":"wrap"},75+ "A12": {"input":"Tall","valign":"middle"},76+ "B12": {"input":"clipped text that does not spill over","wrap":"clip"},77+ "C12": "TRUE",78+ "D12": "=IFERROR(1/0, \"none\")",79+ "E12": {"input":"=Lists!B1*2","format":"number","decimals":3},80+ "F12": "'00123",81+ "F13": "=HYPERLINK(\"https://example.com\", \"site\")"82+ },83+ "charts": [84+ {"type":"column","data":"A1:C9","at":"K2","width":40,"height":12,"header":true,"labels":true,"title":"Units by date"},85+ {"type":"line","data":"C1:E9","at":"K16","width":36,"height":10,"header":true,"stack":"stacked","trend":true,"min":0,"max":400,"gridlines":false,"legend":"none"},86+ {"type":"pie","data":"B1:C5","at":"K28","width":30,"height":10,"header":true,"labels":true,"legend":"right"},87+ {"type":"scatter","data":"C1:D9","at":"K40","width":30,"height":10,"header":true,"trend":true,"log":true},88+ {"type":"bar","data":"B1:C9","at":"K52","width":30,"height":10,"header":true,"stack":"percent"},89+ {"type":"area","data":"C1:D9","at":"K64","width":30,"height":10,"byRow":true,"header":true}90+ ],91+ "conditionalFormats": [92+ {"ranges":"C2:C9","condition":"gt","values":["5"],"text":"red","bold":true},93+ {"ranges":"B2:B9","condition":"contains","values":["th"],"fill":"yellow","italic":true},94+ {"ranges":"A2:A9","condition":"date_is","values":["this week"],"underline":true},95+ {"ranges":"E2:E9","condition":"formula","values":["=$C2\u003e4"],"text":"green","strikethrough":true},96+ {"ranges":"C2:C9","condition":"between","values":["2","4"],"fill":"cyan"},97+ {"ranges":"C2:C9","condition":"top","values":["3"],"fill":"magenta"},98+ {"ranges":"D2:D9","condition":"duplicate","text":"blue"},99+ {"ranges":"C2:C9","condition":"above_average","bold":true},100+ {"ranges":"D2:D9","scale":[{"type":"min","color":"green"},{"type":"percentile","value":"50","color":"yellow"},{"type":"max","color":"red"}]},101+ {"ranges":"E2:E9","dataBar":{"color":"blue","min":{"type":"min"},"max":{"type":"num","value":"400"},"barOnly":true}},102+ {"ranges":"C2:C9,D2:D9","iconSet":{"icons":"arrows","points":[{"type":"percent","value":"33"},{"type":"percent","value":"67"}],"reverse":true}}103+ ],104+ "validations": [105+ {"ranges":"B2:B9","criteria":"list","items":["North","South","East","West"],"help":"Pick a region","display":"chip"},106+ {"ranges":"C2:C9","criteria":"number","condition":"between","values":["0","80"],"reject":true},107+ {"ranges":"A2:A9","criteria":"date","condition":"gt","values":["2025-12-31"]},108+ {"ranges":"C12","criteria":"checkbox","items":["TRUE","FALSE"]},109+ {"ranges":"F2:F9","criteria":"range","source":"Lists!A1:A4","display":"plain"},110+ {"ranges":"G5:G9","criteria":"length","condition":"le","values":["10"]},111+ {"ranges":"H12","criteria":"formula","values":["=H12\u003c\u003e\"\""]}112+ ]113+ },114+ {115+ "name": "Lists",116+ "hidden": true,117+ "protected": [{"sheet":true}],118+ "tables": [{"name":"Targets","range":"D1:E3","columns":["Region","Target"],"banded":true,"header":true}],119+ "cells": {120+ "A1": "North",121+ "B1": {"input":"1.5","note":"Rate used by Sales!I2"},122+ "D1": "Region",123+ "E1": "Target",124+ "F1": "=SUM(Targets[Target])+ROWS(Targets[#All])",125+ "A2": "South",126+ "D2": "North",127+ "E2": "20",128+ "F2": "=Targets[@Target]*2",129+ "A3": "East",130+ "D3": "South",131+ "E3": "=E2/2",132+ "F3": "=COUNTA(Targets[[#Headers],[Region]:[Target]])",133+ "A4": "West"134+ }135+ },136+ {137+ "name": "Pivot Table 1",138+ "cells": {},139+ "pivot": {"source":"Sales!A1:E9","rows":[{"column":"B"},{"column":"A","order":"desc","sortBy":1}],"columns":[{"column":"C"}],"values":[{"column":"E","summarize":"sum"},{"column":"C","summarize":"counta","showAs":"percent_of_total","name":"Share"}],"filters":[{"column":"B","hidden":["West"]}],"rowTotals":true,"columnTotals":false}140+ },141+ {142+ "name": "Data",143+ "widths": {"A": 20},144+ "cells": {145+ "A20": "=SUM(nu.files)",146+ "A21": "=SUM(trips[fare])",147+ "A22": "=COUNTIFS(events[kind],\"click\",events[ms],\"\u003e100\")"148+ },149+ "regions": [150+ {"name":"files","at":"A1","output":true},151+ {"name":"big","at":"A8","output":true},152+ {"name":"app","at":"F2","path":"logs/app.csv","window":500},153+ {"name":"db","at":"F20","path":"data.sqlite","format":"SQLite","table":"orders"},154+ {"name":"q","at":"M20","path":"data.sqlite","format":"SQLite","query":"select * from orders limit 5"}155+ ]156+ },157+ {158+ "name": "trips",159+ "cells": {},160+ "regions": [161+ {"name":"trips","at":"A1","path":"trips.parquet","paged":true,"order":{"sort":[{"column":"C","desc":true}],"filter":[{"column":"B","condition":"eq","value":"card"},{"column":"D","condition":"gt","value":"10"}]}}162+ ]163+ },164+ {165+ "name": "events",166+ "cells": {},167+ "regions": [168+ {"name":"events","at":"A1","path":"events.sqlite","format":"SQLite","query":"select * from events where day \u003e 0","paged":true}169+ ]170+ },171+ {172+ "name": "Notebook",173+ "cells": {},174+ "tab": "notebook",175+ "reactive": true,176+ "notebookCells": [177+ {"kind":"note","source":"# Files\nWhat the folder holds."},178+ {"source":"files = ls","output":"[[name, size]; [go.mod, 812b], [README.md, 4kb]]"},179+ {"source":"big = $files | where size \u003e 1kb","output":"[[name, size]; [README.md, 4kb]]","note":"2 rows left out"},180+ {"source":"$files | nope","error":"Command `nope` not found","detail":"help: check the spelling"},181+ {"source":"ls **/*","unsaved":true}182+ ]183+ }184+ ]185+}
M internal/ui/theme/palette.go +6 -4
A internal/ui/theme/picture_test.go +43 -0
M internal/ui/theme/scheme_test.go +3 -0
A scripts/doccheck/families.go +127 -0
@@ -0,0 +1,127 @@
1+package main2+3+import (4+ "os"5+ "path"6+ "path/filepath"7+ "regexp"8+ "slices"9+ "strings"10+)11+12+// The pictures' color schemes: docs/contributing/site.md has a table of13+// families, each a dark and a light scheme and the pages in it. Every14+// page that shows a picture is in one family, and so is every picture15+// it shows: a still's docScreens entry names its family, and a tape's16+// Set Theme is one of the family's two schemes.17+18+const familiesDoc = "docs/contributing/site.md"19+20+var (21+ // familyRow is a row of the table: | Family | `dark` | `light` | pages |.22+ familyRow = regexp.MustCompile("(?m)^\\| ([^|`]+?) \\| `([^`]+)` \\| `([^`]+)` \\| (.+) \\|$")23+ setTheme = regexp.MustCompile(`(?m)^Set Theme\s+"([^"]+)"`)24+ // stillFamily is a docScreens entry's name and family.25+ stillFamily = regexp.MustCompile(`(?m)^\s*\{name: "([^"]+)", from:.*family: "([^"]+)"\}`)26+)27+28+type family struct {29+ name, dark, light string30+ pages []string // pages and folders, relative to docs/31+}32+33+// readFamilies reads the families table.34+func readFamilies() ([]family, error) {35+ src, err := os.ReadFile(familiesDoc)36+ if err != nil {37+ return nil, err38+ }39+ var out []family40+ for _, m := range familyRow.FindAllStringSubmatch(string(src), -1) {41+ f := family{name: m[1], dark: m[2], light: m[3]}42+ for _, p := range strings.Split(m[4], ",") {43+ f.pages = append(f.pages, strings.Trim(strings.TrimSpace(p), "`"))44+ }45+ out = append(out, f)46+ }47+ return out, nil48+}49+50+// familyOf is the family of a page under docs/: the one naming the page,51+// or else the deepest folder holding it; nil when none does.52+func familyOf(fams []family, page string) *family {53+ rel := strings.TrimPrefix(page, "docs/")54+ var best *family55+ bestLen := -156+ for i, f := range fams {57+ for _, p := range f.pages {58+ if (p == rel || strings.HasSuffix(p, "/") && strings.HasPrefix(rel, p)) && len(p) > bestLen {59+ best, bestLen = &fams[i], len(p)60+ }61+ }62+ }63+ return best64+}65+66+// checkFamilies reports pictures in a family other than the pages67+// showing them, and pages showing pictures in no family. shown maps a68+// media path to the docs showing it.69+func checkFamilies(shown map[string]map[string]string) []string {70+ fams, err := readFamilies()71+ if err != nil {72+ return []string{familiesDoc + ": " + err.Error()}73+ }74+ if len(fams) == 0 {75+ return []string{familiesDoc + ": no table of the pictures' families"}76+ }77+ // check reports each docs page showing media whose family doesn't78+ // pass ok.79+ var out []string80+ check := func(source string, media []string, ok func(*family) bool, want string) {81+ for _, m := range media {82+ for _, page := range sortedKeys(shown["docs/media/"+m]) {83+ if !strings.HasPrefix(page, "docs/") {84+ continue // the README and the landing page show every topic's85+ }86+ switch f := familyOf(fams, page); {87+ case f == nil:88+ out = append(out, page+": shows "+m+" but is in no family of "+familiesDoc)89+ case !ok(f):90+ out = append(out, source+": "+want+", but "+page+", which shows "+m+", is in the family "+f.name)91+ }92+ }93+ }94+ }95+ setup, _ := os.ReadFile("demos/lib/setup.tape")96+ tapes, _ := filepath.Glob("demos/*.tape")97+ for _, t := range tapes {98+ src, err := os.ReadFile(t)99+ if err != nil {100+ continue101+ }102+ themes := setTheme.FindAllStringSubmatch(string(setup)+string(src), -1)103+ if len(themes) == 0 {104+ out = append(out, t+": no Set Theme, here or in demos/lib/setup.tape")105+ continue106+ }107+ theme := themes[len(themes)-1][1]108+ var made []string109+ for _, re := range []*regexp.Regexp{tapeOutput, tapeScreenshot} {110+ for _, m := range re.FindAllStringSubmatch(string(src), -1) {111+ made = append(made, path.Base(m[1]))112+ }113+ }114+ check(t, made, func(f *family) bool { return theme == f.dark || theme == f.light }, "its theme is "+theme)115+ }116+ if src, err := os.ReadFile(stillsSource); err == nil {117+ for _, m := range stillFamily.FindAllStringSubmatch(string(src), -1) {118+ name, fam := m[1], m[2]119+ if !slices.ContainsFunc(fams, func(f family) bool { return f.name == fam }) {120+ out = append(out, stillsSource+": the still "+name+" is in the family "+fam+", which "+familiesDoc+" doesn't list")121+ }122+ check(stillsSource, []string{name + "-dark.png", name + "-light.png"},123+ func(f *family) bool { return f.name == fam }, "the still "+name+" is in the family "+fam)124+ }125+ }126+ return out127+}
M scripts/doccheck/main.go +4 -0
M website/docusaurus.config.ts +25 -4
@@ -12,6 +12,17 @@ const repo = 'https://github.com/FelineStateMachine/012';
1212 const repoDir = path.resolve(__dirname, '..');1313 const docsDir = path.join(repoDir, 'docs');1414 15+// The navbar's sections: a label and the docs folder it opens.16+const sections: [string, string][] = [17+ ['Getting started', 'getting-started'],18+ ['Sheets', 'sheets'],19+ ['Formulas', 'formulas'],20+ ['Files', 'files'],21+ ['Nushell', 'nushell'],22+ ['Agents', 'agents'],23+ ['Terminal', 'terminal'],24+];25+1526 // The site is served at the root of its own host; SITE_URL names that1627 // host for canonical links and the sitemap.1728 const url = process.env.SITE_URL || 'https://012.dev.site';
@@ -89,11 +100,21 @@ const config: Config = {
89100 navbar: {90101 // The logomark alone: it already reads 012, so no title beside it.91102 logo: {alt: '012', src: 'img/logo.svg'},103+ // The docs' sections, each opening its own sidebar (sidebars.ts);104+ // the landing page's control panel lists the same sections.92105 items: [93- {type: 'docSidebar', sidebarId: 'docs', position: 'left', label: 'Docs'},94- {to: '/docs/reference/keys/', label: 'Keys', position: 'left'},95- {to: '/docs/reference/functions/', label: 'Functions', position: 'left'},96- {href: `${repo}/blob/main/ROADMAP.md`, label: 'Roadmap', position: 'right'},106+ ...sections.map(([label, dir]) => ({107+ type: 'docSidebar' as const,108+ sidebarId: dir,109+ label,110+ position: 'left' as const,111+ })),112+ {113+ type: 'docSidebar',114+ sidebarId: 'reference',115+ label: 'Reference',116+ position: 'left',117+ },97118 {href: repo, label: 'GitHub', position: 'right'},98119 // The mode indicator, as at the right end of 012's first line.99120 {type: 'html', position: 'right', value: '<span class="mode-chip" aria-hidden="true">READY</span>'},
M website/package-lock.json +4 -14
@@ -11,8 +11,7 @@
1111 "@docusaurus/preset-classic": "3.10.2",1212 "@docusaurus/theme-mermaid": "3.10.2",1313 "@easyops-cn/docusaurus-search-local": "0.55.3",14- "@fontsource/ibm-plex-mono": "5.3.0",15- "@fontsource/ibm-plex-sans": "5.3.0",14+ "@fontsource/jetbrains-mono": "5.3.0",1615 "@mdx-js/react": "3.1.1",1716 "clsx": "2.1.1",1817 "prism-react-renderer": "2.4.1",
@@ -4985,19 +4984,10 @@
49854984 "node": ">=18"49864985 }49874986 },4988- "node_modules/@fontsource/ibm-plex-mono": {4987+ "node_modules/@fontsource/jetbrains-mono": {49894988 "version": "5.3.0",4990- "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",4991- "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",4992- "license": "OFL-1.1",4993- "funding": {4994- "url": "https://github.com/sponsors/ayuhito"4995- }4996- },4997- "node_modules/@fontsource/ibm-plex-sans": {4998- "version": "5.3.0",4999- "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-sans/-/ibm-plex-sans-5.3.0.tgz",5000- "integrity": "sha512-CbE4CbbEEZJX860XyUiRpsksXIQR8Rp2XDva2VO53NJox9tVNtusrysd2x5YkUEY3ErQ66W1IiiQL8/wihhw5w==",4989+ "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz",4990+ "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==",50014991 "license": "OFL-1.1",50024992 "funding": {50034993 "url": "https://github.com/sponsors/ayuhito"
M website/package.json +1 -2
M website/sidebars.ts +19 -5
M website/src/css/custom.css +42 -10
@@ -5,14 +5,10 @@
55 * where you are (the pointer, the mode indicator), bright black draws the66 * header bands, blue is links and the selection.77 */8-@import '@fontsource/ibm-plex-mono/400.css';9-@import '@fontsource/ibm-plex-mono/500.css';10-@import '@fontsource/ibm-plex-mono/600.css';11-@import '@fontsource/ibm-plex-mono/400-italic.css';12-@import '@fontsource/ibm-plex-sans/400.css';13-@import '@fontsource/ibm-plex-sans/500.css';14-@import '@fontsource/ibm-plex-sans/600.css';15-@import '@fontsource/ibm-plex-sans/400-italic.css';8+@import '@fontsource/jetbrains-mono/400.css';9+@import '@fontsource/jetbrains-mono/500.css';10+@import '@fontsource/jetbrains-mono/600.css';11+@import '@fontsource/jetbrains-mono/400-italic.css';1612 1713 :root {1814 /* Light reference palette */
@@ -43,8 +39,8 @@
4339 --role-link: var(--ansi-blue);4440 --role-panel-bg: #f0f0f0;4541 46- --font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;47- --font-sans: 'IBM Plex Sans', -apple-system, 'Segoe UI', sans-serif;42+ --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;43+ --font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;4844 4945 --ifm-color-primary: #4271ae;5046 --ifm-color-primary-dark: #3b669d;
@@ -291,6 +287,42 @@ html:has([data-control-panel]) .navbar .mode-chip {
291287 display: none;292288 }293289 290+/* The sections fit beside the search box down to the width the navbar291+ collapses at: links a little tighter, and the indicator only where292+ there's room for it too. */293+.navbar__items .navbar__item {294+ padding-left: 0.5rem;295+ padding-right: 0.5rem;296+ white-space: nowrap;297+}298+299+@media (max-width: 1279px) {300+ .navbar .mode-chip {301+ display: none;302+ }303+304+ .navbar__items .navbar__item {305+ padding-left: 0.35rem;306+ padding-right: 0.35rem;307+ }308+309+ .navbar__link {310+ font-size: 0.82rem;311+ }312+313+ .navbar__search-input {314+ width: 10rem;315+ }316+}317+318+/* The landing page's control panel lists the sections in its menu bar,319+ so the navbar leaves them to it wherever both would show. */320+@media (min-width: 997px) {321+ html:has([data-control-panel]) .navbar__items:not(.navbar__items--right) > .navbar__item {322+ display: none;323+ }324+}325+294326 .mode-chip {295327 font-family: var(--font-mono);296328 font-weight: 600;
M website/src/pages/index.module.css +11 -27
M website/src/pages/index.tsx +22 -21
@@ -16,9 +16,11 @@ type Cell = {
1616 to: string;1717 };1818 19+// The control panel at rest: the pointer on A1, holding a label, written20+// with the apostrophe 012 shows before text.1921 const home: Cell = {2022 addr: 'A1',21- entry: "'012",23+ entry: "'Start here",2224 hint: 'Hover a cell, or Tab through them. F1 opens the guides.',2325 title: '',2426 body: '',
@@ -76,14 +78,17 @@ const cells: Cell[] = [
7678 },7779 ];7880 81+// The menu bar lists the navbar's sections, in its order: on this page82+// it stands in for them (custom.css hides the navbar's at widths where83+// both would show).7984 const menu: {label: string; to: string}[] = [80- {label: 'Docs', to: '/docs/'},81- {label: 'Install', to: '/docs/getting-started/install/'},82- {label: 'Formulas', to: '/docs/formulas/'},85+ {label: 'Getting started', to: '/docs/getting-started/'},8386 {label: 'Sheets', to: '/docs/sheets/'},87+ {label: 'Formulas', to: '/docs/formulas/'},8488 {label: 'Files', to: '/docs/files/'},85- {label: 'Terminal', to: '/docs/terminal/'},8689 {label: 'Nushell', to: '/docs/nushell/'},90+ {label: 'Agents', to: '/docs/agents/'},91+ {label: 'Terminal', to: '/docs/terminal/'},8792 {label: 'Reference', to: '/docs/reference/'},8893 ];8994
@@ -120,18 +125,25 @@ function ControlPanel({cell, pointing}: {cell: Cell; pointing: boolean}) {
120125 function Hero() {121126 return (122127 <section className={styles.hero}>128+ <figure className={styles.frame}>129+ <figcaption className={styles.frameTitle}>first-steps.tape</figcaption>130+ <img131+ src={firstSteps}132+ alt="Typing a small budget, pointing at cells in a formula, and watching totals recalculate"133+ width={1000}134+ height={600}135+ />136+ <span className={styles.frameCount}>Catppuccin Mocha</span>137+ </figure>123138 <div className={styles.heroText}>124- <h1 className={styles.title}>125- 012<span className={styles.titleCursor} aria-hidden="true" />126- </h1>127- <p className={styles.lede}>A spreadsheet for the terminal.</p>139+ <h1 className={styles.srOnly}>012</h1>128140 <p className={styles.sub}>129141 The control panel, the mode indicator and the character grid of Lotus 1-2-3. Inside the grid, Google130142 Sheets: typing replaces a cell, <code>=</code> starts a formula, Enter and Tab move you on, and Sheets'131143 shortcuts do what you expect. One pure-Go binary.132144 </p>133145 <pre className={styles.install} aria-label="Install">134- <span className={styles.prompt}>$ </span>go install github.com/FelineStateMachine/012/cmd/012@latest146+ <span className={styles.prompt}>$ </span>curl -fsSL https://012.dev.site/install.sh | sh135147 </pre>136148 <div className={styles.actions}>137149 <Link className={clsx(styles.action, styles.actionPrimary)} to="/docs/getting-started/">
@@ -145,16 +157,6 @@ function Hero() {
145157 </span>146158 </div>147159 </div>148- <figure className={styles.frame}>149- <figcaption className={styles.frameTitle}>first-steps.tape</figcaption>150- <img151- src={firstSteps}152- alt="Typing a small budget, pointing at cells in a formula, and watching totals recalculate"153- width={1000}154- height={600}155- />156- <span className={styles.frameCount}>Catppuccin Mocha</span>157- </figure>158160 </section>159161 );160162 }
@@ -226,7 +228,6 @@ export default function Home(): ReactNode {
226228 const [cell, setCell] = useState<Cell | null>(null);227229 return (228230 <Layout229- title="A spreadsheet for the terminal"230231 description="012 is a terminal spreadsheet with the look of Lotus 1-2-3 and the behavior of Google Sheets.">231232 <main className={styles.page}>232233 <ControlPanel cell={cell ?? home} pointing={cell !== null} />
M website/static/img/logo.svg +1 -1