From 54abb2e5793fc25f5734de4a927dac3fce022df2 Mon Sep 17 00:00:00 2001 From: Thomas Lin Pedersen Date: Thu, 23 Apr 2026 13:22:35 +0200 Subject: [PATCH 1/5] Dynamically build docs for cli during built --- Cargo.lock | 196 ++++++++++++++++- doc/vendor/SKILL.md | 497 ++++++++++++++++++++++++++++++++++++++++++++ src/Cargo.toml | 8 + src/build.rs | 380 +++++++++++++++++++++++++++++++++ src/cli.rs | 277 +++++++++++++++++++++++- 5 files changed, 1356 insertions(+), 2 deletions(-) create mode 100644 doc/vendor/SKILL.md create mode 100644 src/build.rs diff --git a/Cargo.lock b/Cargo.lock index 58958f31..aa649b2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -911,7 +911,7 @@ checksum = "e0d05af1e006a2407bedef5af410552494ce5be9090444dbbcb57258c1af3d56" dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -991,6 +991,24 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "coolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3" +dependencies = [ + "crossterm", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1080,6 +1098,45 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crokey" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a63daf06a168535c74ab97cdba3ed4fa5d4f32cb36e437dcceb83d66854b7c" +dependencies = [ + "crokey-proc_macros", + "crossterm", + "once_cell", + "serde", + "strict", +] + +[[package]] +name = "crokey-proc_macros" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847f11a14855fc490bd5d059821895c53e77eeb3c2b73ee3dded7ce77c93b231" +dependencies = [ + "crossterm", + "proc-macro2", + "quote", + "strict", + "syn 2.0.117", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1123,6 +1180,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -1232,6 +1316,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "digest" version = "0.10.7" @@ -1302,6 +1408,15 @@ dependencies = [ "libloading", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dpi" version = "0.1.2" @@ -1815,6 +1930,7 @@ dependencies = [ "serde_json", "sprintf", "tempfile", + "termimad", "thiserror 1.0.69", "toml_edit 0.22.27", "tree-sitter", @@ -2486,6 +2602,29 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +[[package]] +name = "lazy-regex" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2635,6 +2774,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -2709,6 +2854,15 @@ dependencies = [ "libc", ] +[[package]] +name = "minimad" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" +dependencies = [ + "once_cell", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2726,6 +2880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -5051,6 +5206,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -5227,6 +5393,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "strict" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" + [[package]] name = "stringprep" version = "0.1.5" @@ -5362,6 +5534,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termimad" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7301d9c2c4939c97f25376b70d3c13311f8fefdee44092fc361d2a98adc2cbb6" +dependencies = [ + "coolor", + "crokey", + "crossbeam", + "lazy-regex", + "minimad", + "serde", + "thiserror 2.0.18", + "unicode-width 0.1.14", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -5814,6 +6002,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" diff --git a/doc/vendor/SKILL.md b/doc/vendor/SKILL.md new file mode 100644 index 00000000..9397ecae --- /dev/null +++ b/doc/vendor/SKILL.md @@ -0,0 +1,497 @@ +--- +name: ggsql +description: Write ggsql queries — a grammar of graphics for SQL. Use when the user wants to create, modify, or understand a ggsql visualization query. +allowed-tools: Bash(ggsql:*) +argument-hint: "[description of desired visualization]" +metadata: + author: George Stagg (@georgestagg) + version: "1.0" +license: MIT +--- + +# ggsql Query Writer + +ggsql is a SQL extension for declarative data visualization based on Grammar of Graphics principles. It lets users combine SQL data queries with visualization specifications in a single, composable syntax. + +When the user describes a visualization they want, write a valid ggsql query. Use ONLY syntax documented below. NEVER invent clauses, settings, aesthetics, or layer types. + +## Query structure + +A ggsql query has two parts: + +1. **SQL part** (optional): Standard SQL executed on the backend. Any tables, CTEs, or SELECT results are available to the visualization. +2. **VISUALISE part** (required): Begins with `VISUALISE` (or `VISUALIZE`). Everything after this is the visualization query. + +There are two patterns for combining SQL with VISUALISE: + +### Pattern A: SELECT → VISUALISE + +The last SQL statement is a SELECT. Data flows from its result set into VISUALISE, which has no `FROM` clause. + +```ggsql +SELECT name, score_a, score_b FROM 'dataset.csv' WHERE value > 50 +VISUALISE score_a AS x, score_b AS y +[DRAW / PLACE / SCALE / FACET / PROJECT / LABEL clauses] +``` + +Works with any SQL that ends in a SELECT: bare SELECT, WITH...SELECT, UNION/INTERSECT/EXCEPT. + +### Pattern B: VISUALISE FROM + +VISUALISE provides its own data source via `FROM`. Use when referencing a table, file, CTE, or built-in dataset directly without a trailing SELECT. + +```ggsql +VISUALISE score_a AS x, score_b AS y FROM 'dataset.csv' +DRAW point +``` + +```ggsql +WITH summary AS (SELECT category, COUNT(*) AS n FROM 'dataset.csv' GROUP BY category) +VISUALISE category AS x, n AS y FROM summary +DRAW bar +``` + +## Data sources + +Data sources can appear in `VISUALISE ... FROM` or `DRAW ... MAPPING ... FROM`: + +- **Table/CTE name** (unquoted): `FROM sales`, `FROM my_cte` +- **File path** (single-quoted string): `FROM 'data.parquet'`, `FROM 'data.csv'` +- **Built-in datasets**: `FROM ggsql:penguins`, `FROM ggsql:airquality` + +## VISUALISE clause + +Marks the start of the visualization. Optionally defines global mappings inherited by all layers. + +``` +VISUALISE , ... FROM +``` + +### Mapping forms + +- **Explicit**: `column AS aesthetic` — e.g. `revenue AS y` +- **Implicit**: `column` — column name must match aesthetic name, e.g. `x` maps to `x` +- **Wildcard**: `*` — all columns with names matching aesthetics are mapped +- **Constants**: `'red' AS fill`, `42 AS size` — literal values mapped to aesthetic + +```ggsql +VISUALISE bill_len AS x, bill_dep AS y, species AS fill FROM ggsql:penguins +VISUALISE * FROM my_table +VISUALISE FROM ggsql:penguins +``` + +## DRAW clause + +Defines a layer. Multiple DRAW clauses stack layers (first = bottom, last = top). + +``` +DRAW + MAPPING , ... FROM + REMAPPING AS , ... + SETTING => , ... + FILTER + PARTITION BY , ... + ORDER BY , ... +``` + +All subclauses are optional if VISUALISE provides global mappings and data. + +### MAPPING + +Same syntax as VISUALISE mappings. Layer mappings merge with global mappings (layer takes precedence). Can include `FROM` for layer-specific data. + +- Use `null` to prevent inheriting a global mapping: `MAPPING null AS color` + +### REMAPPING + +For statistical layers (histogram, density, boxplot, violin, smooth, bar without y). Maps calculated statistics to aesthetics. Each layer documents its available stats and default remapping. + +```ggsql +DRAW histogram + MAPPING body_mass AS x + REMAPPING density AS y -- use density instead of default count +``` + +### SETTING + +Set literal aesthetic values or layer parameters. Aesthetics set here bypass scales. + +```ggsql +DRAW point + SETTING size => 5, opacity => 0.7, stroke => 'red' +``` + +**Position adjustment** is a special setting: +```ggsql +SETTING position => 'identity' -- no adjustment (default for most) +SETTING position => 'stack' -- stack (default for bar, histogram, area) +SETTING position => 'dodge' -- side by side (default for boxplot, violin) +SETTING position => 'jitter' -- random offset +``` + +### FILTER + +SQL WHERE condition applied to layer data. Content is passed to the database: +```ggsql +DRAW point + FILTER sex = 'female' AND body_mass > 4000 +``` + +### PARTITION BY + +Additional grouping columns beyond mapped discrete aesthetics: +```ggsql +DRAW line + MAPPING Day AS x, Temp AS y + PARTITION BY Month +``` + +### ORDER BY + +Controls record order (important for path layers): +```ggsql +DRAW path + ORDER BY timestamp +``` + +## PLACE clause + +Creates annotation layers with literal values only (no data mappings). Supports tuples for multiple annotations. + +``` +PLACE + SETTING => , ... +``` + +```ggsql +PLACE point SETTING x => 5, y => 10, color => 'red' +PLACE rule SETTING y => 70, linetype => 'dotted' +PLACE text SETTING x => (34, 44), y => (66, 49), label => ('Mean = 34', 'Mean = 44') +``` + +## SCALE clause + +Controls how data values are translated to aesthetic values. Sensible defaults are always provided. + +``` +SCALE FROM TO VIA + SETTING => , ... + RENAMING =>