New docs/themes.md; docs/jev.md's setup is now set-key, the credential store and jev-api-key-command; architecture explains the config registry, the credential store choice and scheme rendering; extending lists options and roles as registries; testing and UX cover config isolation and full-width bars.
- commit
- 1429607a4a37a3b6429d93ee60906a4eb42633a0
- parent
- 710b5e6b52f1f4666a35faf4cc4f4c1e9bf68fcc
- author
- Dami <dami.etoile@gmail.com>
| M | README.md | 8 | ++- |
| M | docs/ | 4 | +- |
| M | docs/ | 7 | ++- |
| M | docs/ | 66 | ++++++++++++++++++- |
| M | docs/ | 2 | +- |
| M | docs/ | 13 | +++- |
| M | docs/ | 64 | ++++++++++++++++-- |
| M | docs/ | 4 | + |
| M | docs/ | 7 | ++- |
| A | docs/ | 104 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 2 | +- |
@@ -26,7 +26,7 @@ Requires Go 1.27. Or from a clone: `make build` puts the binary in `bin/012`.
2626 ```2727 2828 F1 shows every shortcut, F10 or Alt+letter opens the menus, and Ctrl+K29-searches every command.29+searches every command. `012 config edit` opens the settings file.3030 3131 ## What it does3232 @@ -55,6 +55,11 @@ searches every command.
5555 - **Made for terminals.** Mouse with hover and resize handles, hyperlinks,5656 light and dark themes that follow the terminal, desktop notifications,5757 menus and a command palette styled like terminal software, not a GUI.58+- **Themes and a config file.** Your terminal's colors by default, or any59+ of 349 color schemes (or your own Ghostty theme) with solid menu and60+ status bars; one Ghostty-style config file for every setting, and the61+ JEV API key kept in the OS keychain ([config](docs/config.md),62+ [themes](docs/themes.md)).5863 5964 ## Demos6065 @@ -79,6 +84,7 @@ kitty, Ghostty and WezTerm they are real images.
7984 - [Charts, links and the terminal](docs/charts.md)8085 - [Files](docs/files.md)8186 - [JEV functions](docs/jev.md)87+- [Configuration](docs/config.md) and [themes](docs/themes.md)8288 - [Architecture](docs/architecture.md), [UX bar](docs/UX.md) and [testing](docs/testing.md)8389 - [Limits](docs/limits.md): how big a sheet 012 handles and where it slows down8490 - [Observability](docs/observability.md): event logs, DuckDB, and a local Collector, ClickHouse and Grafana stack@@ -8,7 +8,9 @@
88 - [Working with data](data.md): selection, copy and fill, undo, freeze, sort, filter, find99 - [Charts, links and the terminal](charts.md): charts, hyperlinks, terminal features used1010 - [Files](files.md): the `.012` format, import and export11-- [JEV functions](jev.md): asking a hosted model from formulas11+- [JEV functions](jev.md): asking a hosted model from formulas, and storing the API key12+- [Configuration](config.md): the config file, `012 config` and every option13+- [Themes](themes.md): terminal color schemes, built in or your own1214 1315 **Working on 012**1416 @@ -45,7 +45,12 @@ follow-up task.
4545 `lipgloss.NewStyle()` in views. Need a new role? Add it to `theme` with a4646 comment saying what it's for, in both the dark and light variants.4747 - Use the 16 ANSI colors so the user's terminal palette applies. No48- hard-coded RGB.48+ hard-coded RGB. Color schemes ([themes.md](themes.md)) are data: they49+ map the same roles to their colors and correct contrast, and50+ `TestEveryThemeReadable` must pass for a new role.51+- Bars (menu bar, formula bar, context line, column headers, status line)52+ get their background from their row role and must fill the full width,53+ at odd widths and under overlays; draw them with `theme.Fill`.4954 - Text on a colored background needs about 4.5:1 contrast against both5055 reference palettes (`e2e/palette_test.go`). Check it in the gallery.5156 - Never rely on color alone: state also shows as text (mode indicator,@@ -4,17 +4,19 @@
44 Lip Gloss v2.55 66 ```7-cmd/012 entry point: flags, JEV setup, opening or importing a file7+cmd/012 entry point: flags and config, 012 config, JEV setup, opening or importing a file8+internal/config the config file and the registry of options89 internal/sheet the engine: cells, evaluation, functions, recalculation, undo, files910 internal/formula the formula language: references, lexer, parser, printer, rewriting1011 internal/numfmt number formats, rounding, General, date serials1112 internal/fileio import and export: CSV, TSV, XLSX, SQLite, Parquet, Lotus .wk11213 internal/chart chart layout, text rendering and kitty image encoding13-internal/jev JEV configuration, answer cache and TypeSafe client14+internal/jev the API key's resolution, answer cache and TypeSafe client15+internal/keyring the OS credential store the API key lives in1416 internal/telemetry opt-in JSON log and OTLP export of spans, events and frame stats1517 internal/stress synthetic worst-case sheets for the -tags stress benchmarks1618 internal/ui the Bubble Tea model: modes, menus, overlays, rendering17- theme style roles and the widgets drawn with them (frames, key chips)19+ theme style roles, color schemes and the widgets drawn with them (frames, key chips)1820 rowtext laying out a row of cell text across the columns on screen1921 formula reading the formula being typed (F4, the word and call at the caret)2022 e2e/ end-to-end tests through libghostty (separate module, cgo)@@ -203,6 +205,64 @@ saved with sheets. Work is bounded by the chart's size, not its data: only
203205 the categories that fit are drawn, pie slices are found by binary search,204206 and a pie image supersamples only pixels on a slice edge or the rim.205207 208+## Configuration and secrets209+210+`internal/config` reads one Ghostty-style file (`key = value`, `#`211+comments, `config-file` includes) from `$XDG_CONFIG_HOME/012` or the OS212+config directory; the same directory holds `themes/` and 012 serve's host213+key. Every option is one entry in `config.Options`: name, type, default,214+environment variables, flag, whether Reload config applies it, and a215+description. Parsing and validation, the flags `cmd/012` accepts, `012216+config`'s listing and default file, and [config.md](config.md)'s217+reference (checked by a test) all come from that table. Values are layered defaults < file < environment < flags, each218+remembering its source; problems are warnings, never fatal. The UI gets a219+`ui.Settings` (the config, a reload function, the credential store, a JEV220+connector) through `Model.Configure`, so tests hand it a temporary config221+and a store in memory.222+223+The JEV API key is never in the config. `jev.ResolveKey` takes it from224+`TYPESAFE_API_KEY`, then the credential store, then225+`jev-api-key-command` (split into words and run without a shell, with a226+timeout, only when a sheet first asks JEV something). The base URL comes227+from the config or environment only and must be https unless it's228+loopback, so a file next to a sheet can't redirect the key.229+230+`internal/keyring` is the credential store, chosen for the least231+dependency weight that is still correct on each OS, with `CGO_ENABLED=0`:232+233+| OS | Store | How |234+|---|---|---|235+| macOS | Keychain | `/usr/bin/security`. Writing uses `security -i`, which reads its command from stdin, with the key hex-encoded (`-X`), so it's never in any process's arguments. |236+| Linux, BSD | Secret Service (GNOME Keyring, KWallet, KeePassXC) | libsecret's `secret-tool`, which reads the secret to store from stdin. |237+| Windows | Credential Manager | `CredReadW`, `CredWriteW`, `CredDeleteW` from advapi32 through `golang.org/x/sys/windows`, already in the module graph. |238+239+The alternatives were a keyring module (zalando/go-keyring, which does240+the same on macOS but brings godbus for Linux, or 99designs/keyring,241+which brings several backends and needs cgo for the macOS Keychain) or242+cgo bindings to Security.framework and libsecret, which would end the243+pure-Go build. Two244+small command-line programs and one system DLL cover the three OSes with245+no new module. The stores share a `Store` interface; tests use a246+recording runner or `keyring.Memory`, and the e2e binary is built with247+`-tags fakekeyring`, whose store is a file under `XDG_CONFIG_HOME`, so no248+test reaches a real keychain.249+250+## Themes251+252+`theme.Theme` is a set of style roles, each defined on the 16 ANSI colors253+and the default background and foreground (`theme.New`). The terminal254+theme sends them as ANSI numbers, so the terminal's palette applies. A255+color scheme (`theme.Palette`: VHS's embedded catalog, or a Ghostty,256+kitty or VHS JSON file in `themes/`) is drawn by `theme.FromPalette`,257+which maps each ANSI color to the scheme's, adds full-width bands for the258+bars and a screen background, and corrects contrast (see259+[themes.md](themes.md)). Bands are applied by `theme.Fill`, one pass over260+a rendered line's escape sequences that sets the band's colors at the261+start and after each reset and pads to the width; `View` fills the bar262+lines before overlays are composited and the whole screen after, so bars263+reach the edge under menus too. Charts take their colors from the scheme264+when there is one, and from the terminal's palette otherwise.265+206266 ## Telemetry207267 208268 `internal/telemetry` is off unless asked for. When on, spans, events and@@ -99,7 +99,7 @@ Send a desktop notification (OSC 9) when JEV answers or an import finishes while
9999 100100 #### `jev-api-key-command`101101 102-A command that prints the TypeSafe API key, used when TYPESAFE_API_KEY isn't set and the credential store has no key, e.g. `op read op://Private/TypeSafe/credential` or `pass show typesafe`. It runs without a shell, for up to 10 seconds; for pipes, write `sh -c '...'` yourself. The key itself never goes in this file.102+A command that prints the TypeSafe API key, used when TYPESAFE_API_KEY isn't set and the credential store has no key, e.g. `op read op://Private/TypeSafe/credential` or `pass show typesafe`. It runs the first time a sheet asks JEV something, without a shell, for up to 10 seconds, and its first line of output is the key; for pipes, write `sh -c '...'` yourself. The key itself never goes in this file.103103 104104 | | |105105 |---|---|@@ -18,10 +18,15 @@ shows it is derived from the table rather than listing it again.
1818 | `ui.command` | an action: id, title, description, run, enabled, checked | key bindings, menu bar, context menus, palette, shortcuts help |1919 | `fileio` formats (`formats.go`) | a format: name, extensions, labels, traits, importer, exporter | `Import`, `Export`, detection, import picker, File > Download, command line |2020 | `chart.types`, with `sheet.ChartTypes` | a type: name and order (sheet, saved in files), a layout drawing text and image (chart) | `chart.Draw`, `chart.Image`, chart editor, Insert > Chart |21-| theme roles | a role: dark and light styles on the 16 ANSI colors | every style in the UI |22-23-Adding a function, command, format or chart type means adding an entry24-(and its file), not editing switch statements elsewhere. When a list has to21+| theme roles (`theme.Theme`) | a role: dark and light styles on the 16 ANSI colors, with a contrast minimum for schemes (`minContrast`) | every style in the UI, drawn in the terminal's palette or any color scheme (`FromPalette`); `TestEveryThemeReadable` checks each role under all 349 schemes |22+| `config.Options` | an option: name, type, default, environment variables, flag, live or not, description, check | parsing and warnings, flags, `012 config` and its default file, [config.md](config.md), Reload config |23+24+Adding a function, command, format, chart type, option or role means25+adding an entry (and its file), not editing switch statements elsewhere.26+A new process-wide setting is a `config.Options` entry read with27+`Config.String`, `Bool`, `Int` or `Duration`; one that belongs to a28+workbook goes in the file instead (pattern 9). A new role goes in29+`theme.Theme` with its ANSI colors in `New`; schemes pick it up. When a list has to2530 be kept in sync by hand, turn it into a registry.2631 2732 ### 2. Components own their state@@ -16,11 +16,60 @@ background (cells show `Loading…`), are cached by question, and the context
1616 line shows the confidence for the selected cell. Data > Ask JEV again1717 re-asks the selection.1818 19-Set `TYPESAFE_API_KEY` in the environment or a `.env` file in the current20-directory or next to the sheet; `TYPESAFE_BASE_URL` and21-`TYPESAFE_DEFAULT_MODEL` are optional. Without a key the functions show22-`#N/A` and say why. `JEV_LIVE_TEST=1 go test ./internal/jev -run TestLive`23-checks the real service.19+Without a key the functions show `#N/A` and say how to add one.20+`JEV_LIVE_TEST=1 go test ./internal/jev -run TestLive` checks the real21+service.22+23+## Setup24+25+The API key never goes in a file 012 reads or on a command line. Store it26+once in your operating system's credential store:27+28+```sh29+012 config set-key # asks for the key without echoing it30+op read op://Private/TypeSafe/credential | 012 config set-key # or pipe it in31+012 config delete-key # remove it32+```33+34+or in the app, File > Settings > JEV API key, which asks on the context35+line with the key masked and turns JEV on at once. `012 config` says where36+the key would come from, without showing it.37+38+012 looks for the key in this order:39+40+1. **`TYPESAFE_API_KEY`** in the environment, for CI, containers and41+ one-off runs.42+2. **The credential store**: the macOS Keychain, the Windows Credential43+ Manager, or the Secret Service on Linux and the BSDs (GNOME Keyring,44+ KWallet, KeePassXC; this needs `secret-tool`, in the `libsecret-tools`45+ package on Debian and Ubuntu). The item is service `012`, account46+ `jev-api-key`. `jev-credential-store = false` in the config skips it.47+3. **`jev-api-key-command`** in the config: a command that prints the key,48+ for password managers:49+50+ ```51+ jev-api-key-command = op read op://Private/TypeSafe/credential52+ jev-api-key-command = pass show typesafe53+ jev-api-key-command = sh -c 'gpg -dq ~/.secrets/typesafe.gpg | head -1'54+ ```55+56+ It runs the first time a sheet asks JEV something, not at every57+ start, for at most 10 seconds, and the first line it prints is the key.58+ It runs without a shell: the value is split into words, with quotes59+ grouping words and a backslash escaping the next character, and60+ nothing is expanded (`$HOME`, `~`, `*`, pipes and `;` are passed on61+ literally). For a pipeline, write `sh -c '...'` yourself.62+63+The service's address and model come from the config file or the64+environment only: `jev-base-url` (or `TYPESAFE_BASE_URL`), which must be65+https unless it's this machine, and `jev-model` (or66+`TYPESAFE_DEFAULT_MODEL`). See [config.md](config.md).67+68+**`.env` files are no longer read.** A `.env` next to a downloaded sheet69+could set `TYPESAFE_BASE_URL` and send your key to someone else's server.70+If the directory 012 starts in has a `.env` with `TYPESAFE_API_KEY`, the71+context line suggests `012 config set-key`; 012 never reads the key from72+it.2473 2574 ## How requests behave2675 @@ -33,7 +82,8 @@ checks the real service.
3382 than two labels, more than the service's 255 labels or 10 score levels)3483 show as errors at once and are never sent.3584 - A failed request shows `#ERROR!`, and the context line says why.36-- 012 never logs or saves the API key; `.env` files are read for37- `TYPESAFE_*` settings only.85+- 012 never logs, shows or saves the API key anywhere but the credential86+ store, and never passes it as a command-line argument: the macOS87+ Keychain and the Secret Service get it on stdin.3888 3989 The client is [typesafe-go](https://github.com/FelineStateMachine/typesafe-go).@@ -25,6 +25,10 @@ O12_LOG_LEVEL=debug # also one event per frame
2525 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 012 budget.012 # the same2626 ```2727 28+Or keep them in the [config file](config.md) as `log-file`, `log-level`29+and `otlp-endpoint`; flags win over the environment, which wins over the30+file.31+2832 The two can be on together or separately. Stdout belongs to the2933 terminal UI, so events go to the file, appended one JSON object per3034 line, and/or to the OTLP endpoint (see [OTLP](#otlp) below). With@@ -26,7 +26,12 @@ by libghostty itself from the modes the app turned on, so they are the bytes
2626 Ghostty would send. Tests assert on what a user would see: screen text,2727 cursor position, window title, hyperlinks, which screen is active after2828 quitting. A fake TypeSafe server answers JEV questions, and the harness29-clears `TYPESAFE_API_KEY` so tests never reach the real service.29+clears `TYPESAFE_API_KEY` so tests never reach the real service. Every30+session gets its own config directory (`XDG_CONFIG_HOME`, with an31+optional config file), and the binary is built with `-tags fakekeyring`,32+whose credential store is a file there, so tests never read your config33+or touch your keychain. Unit tests do the same with `ui.Settings` and34+`keyring.Memory`.3035 3136 `make e2e` builds libghostty-vt from source with Zig into `.deps/` on first3237 use. It is its own Go module so cgo never reaches the main binary.@@ -0,0 +1,104 @@
1+# Themes2+3+A theme is a terminal color scheme: the 16 ANSI colors plus a background,4+a foreground and a selection color, the same shape Ghostty, kitty, iTerm2,5+Alacritty and VHS themes have. 012 has no theme format of its own to learn:6+any scheme works, and so does any Ghostty theme file.7+8+```9+# ~/.config/012/config10+theme = terminal # the default11+theme = Dracula # one scheme12+theme = light:Catppuccin Latte,dark:Catppuccin Mocha # follow the terminal13+```14+15+- **`terminal`** (the default) draws with the terminal's own palette, as16+ ANSI color numbers, with a dark and a light variant picked from the17+ terminal's background. It looks the way your terminal is set up.18+- **Any other name** is a color scheme. 012 draws its roles in the19+ scheme's colors (true color), fills the whole screen with the scheme's20+ background, and draws the menu bar, the column headers and the status21+ line as solid bands, in the manner of 1-2-3 and classic terminal22+ programs. The terminal's own palette is never changed.23+- **`light:A,dark:B`** picks A on a light terminal and B on a dark one,24+ and switches when the terminal does (for example with the system's25+ appearance, in terminals that report it).26+27+File > Settings > Theme opens a picker: type to search the names, or28+"dark" or "light"; the highlighted theme is drawn live, Enter keeps it and29+writes `theme = ...` to the config file, and Esc goes back to the theme you30+had. With a `light:`/`dark:` pair, the picker changes the one in use.31+`012 config themes` lists every theme, with a `*` on the current one.32+33+## Built-in schemes34+35+012 embeds the 348 schemes of [VHS](https://github.com/charmbracelet/vhs)36+(Dracula, Nord, Gruvbox, Catppuccin, Solarized, Tokyo Night, One Dark,37+Rose Pine and hundreds more), so names are the ones VHS's `Set Theme`38+takes, plus **1-2-3 Classic**: CGA colors on a blue screen. Names match39+without regard to case, and then without regard to spaces, hyphens and40+underscores, so `tokyo-night` finds `TokyoNight`.41+42+## Your own schemes43+44+Put a file in the `themes` directory next to the config file45+(`~/.config/012/themes/` on Linux; `012 config path` shows where the46+config is) and use its file name as the theme. A file with a built-in's47+name replaces it. Three formats are read:48+49+- **Ghostty** (copy any Ghostty theme unchanged):50+51+ ```52+ palette = 0=#1d1f2153+ palette = 1=#cc666654+ # ... through 1555+ background = #1d1f2156+ foreground = #c5c8c657+ selection-background = #373b4158+ ```59+60+- **kitty**: `color0 #1d1f21` through `color15`, `background`,61+ `foreground` and `selection_background`.62+- **VHS JSON**: one scheme or a list, with `black` ... `brightWhite`,63+ `background`, `foreground`, `selection` and `meta.isDark` (a `.json`64+ file; `purple` and `selectionBackground` are accepted too).65+66+A scheme needs a background and a foreground; ANSI colors it leaves out67+are xterm's. Other keys (fonts, cursor colors) are ignored.68+69+## How roles get their colors70+71+Every role in `internal/ui/theme` (the cell pointer, selection, headers,72+menus, errors, links, chart series and so on) is defined on the ANSI73+colors and the background and foreground: the pointer is black on cyan,74+headers are bright white on bright black (black on white on light75+terminals), errors are red. The terminal theme sends those color numbers;76+a scheme replaces each with its own color. Then, so every scheme reads77+well:78+79+- the selection uses the scheme's selection color, when it has one;80+- the menu bar and status line are the background moved 12% toward the81+ text color; the column header row is filled with the header color;82+- a background role too close to the screen's background is moved apart;83+- text below its contrast minimum against its background (WCAG 4.5:1 for84+ text, 3:1 for hints, lines and chart axes, 2:1 for unavailable items)85+ is moved toward the scheme's text color, or toward black or white, just86+ far enough to reach it.87+88+A test runs every built-in scheme through this and checks those minimums89+for cell text, text on the bars, the selection, the headers and every90+other role (`TestEveryThemeReadable`). Charts, as text and as kitty91+images, use the scheme's colors too.92+93+The roles for the bars are `MenuBarRow`, `FormulaBarRow`, `ContextRow`,94+`ColumnHeaderRow` and `StatusBarRow` (a background and a text color for95+the whole line, out to the terminal's edge, under overlays too), with96+`RowHeader` for the row numbers and `Screen` for everything else. The97+terminal theme leaves them empty, so it looks as it always has.98+99+## Credits100+101+The built-in schemes are VHS's `themes.json` (MIT License, Copyright (c)102+2022-2023 Charmbracelet, Inc.; see [NOTICE](../NOTICE)), which credits103+each scheme's authors in its `meta.credits`. The contrast correction is104+adapted from the theme package of puzzletea, by the same author.@@ -91,7 +91,7 @@ var Options = []Option{
9191 {Name: "jev-api-key-command", Kind: Command, Group: GroupJEV,9292 Desc: "A command that prints the TypeSafe API key, used when TYPESAFE_API_KEY isn't set and the " +9393 "credential store has no key, e.g. `op read op://Private/TypeSafe/credential` or " +94- "`pass show typesafe`. It runs without a shell, for up to 10 seconds; for pipes, write " +94+ "`pass show typesafe`. It runs the first time a sheet asks JEV something, without a shell, for up to 10 seconds, and its first line of output is the key; for pipes, write " +9595 "`sh -c '...'` yourself. The key itself never goes in this file."},9696 {Name: "jev-credential-store", Kind: Bool, Group: GroupJEV, Default: "true", Env: []string{"O12_JEV_CREDENTIAL_STORE"},9797 Desc: "Look for the API key in the OS credential store (macOS Keychain, Windows Credential Manager, " +