Rename SheetCraft to GridCraft everywhere (crates, apps, bundle ids, packaging, docs)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-06 01:39:08 -04:00
parent 2618a233bf
commit 488e12de31
193 changed files with 1557 additions and 1566 deletions

View File

@ -2,7 +2,7 @@
# (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter.
#
# With `package: true` (release.yml calls it that way, or a manual run) it instead builds the
# release binaries and uploads `sheetcraft-<version>-freebsd-x86_64.tar.gz` as the
# release binaries and uploads `gridcraft-<version>-freebsd-x86_64.tar.gz` as the
# `release-freebsd` artifact (packaging/freebsd/package.sh), which release.yml attaches to the
# draft GitHub Release.
name: FreeBSD
@ -51,10 +51,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 300
env:
SHEETCRAFT_VERSION: ${{ inputs.version }}
SHEETCRAFT_VERSION_OVERRIDE: ${{ inputs.version_override && '1' || '' }}
SHEETCRAFT_PACKAGE: ${{ inputs.package && '1' || '' }}
SHEETCRAFT_BUILD_SHA: ${{ github.sha }}
GRIDCRAFT_VERSION: ${{ inputs.version }}
GRIDCRAFT_VERSION_OVERRIDE: ${{ inputs.version_override && '1' || '' }}
GRIDCRAFT_PACKAGE: ${{ inputs.package && '1' || '' }}
GRIDCRAFT_BUILD_SHA: ${{ github.sha }}
steps:
- uses: actions/checkout@v4
# Optional font input (docs: storytold/craft-fonts docs/integration.md); releases embed it.
@ -71,7 +71,7 @@ jobs:
usesh: true
cpu: 4
mem: 12288
envs: "SHEETCRAFT_VERSION SHEETCRAFT_VERSION_OVERRIDE SHEETCRAFT_PACKAGE SHEETCRAFT_BUILD_SHA"
envs: "GRIDCRAFT_VERSION GRIDCRAFT_VERSION_OVERRIDE GRIDCRAFT_PACKAGE GRIDCRAFT_BUILD_SHA"
# Only the packaging run brings files back (dist/release). The target dir lives outside
# the synced checkout, because copying it out of the VM fails on large builds.
copyback: ${{ inputs.package }}
@ -82,11 +82,11 @@ jobs:
. "$HOME/.cargo/env"
# The checkout is synced in under another owner; let tests that call git read it.
git config --global --add safe.directory "*"
export CARGO_TARGET_DIR=/var/tmp/sheetcraft-target CARGO_PROFILE_DEV_DEBUG=0
export CARGO_TARGET_DIR=/var/tmp/gridcraft-target CARGO_PROFILE_DEV_DEBUG=0
rustc --version
if [ -n "$SHEETCRAFT_PACKAGE" ]; then
if [ -n "$SHEETCRAFT_VERSION_OVERRIDE" ]; then
cargo run -q -p xtask --no-default-features -- version set "$SHEETCRAFT_VERSION"
if [ -n "$GRIDCRAFT_PACKAGE" ]; then
if [ -n "$GRIDCRAFT_VERSION_OVERRIDE" ]; then
cargo run -q -p xtask --no-default-features -- version set "$GRIDCRAFT_VERSION"
fi
CRAFT_FONTS_DIR="$PWD/craft-fonts" CRAFT_FONTS_REQUIRED=1 bash packaging/freebsd/package.sh
else

View File

@ -46,12 +46,12 @@ jobs:
}
- name: XML, desktop entry, AppStream, Flatpak manifest
run: |
xmllint --noout packaging/windows/sheetcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml*
xmllint --noout packaging/windows/gridcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml*
# Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon.
if grep -oE '<Icon Id="[^"]*"' packaging/windows/sheetcraft.wxs | grep -vE '\.(ico|exe)"$'; then
if grep -oE '<Icon Id="[^"]*"' packaging/windows/gridcraft.wxs | grep -vE '\.(ico|exe)"$'; then
echo "::error::Icon Id must end in .ico or .exe"; exit 1
fi
desktop-file-validate packaging/linux/ai.storyteller.sheetcraft.desktop
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.sheetcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.sheetcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.sheetcraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.sheetcraft.yml')); assert m['id'] == 'ai.storyteller.sheetcraft' and m['command'] == 'sheetcraft'"
desktop-file-validate packaging/linux/ai.storyteller.gridcraft.desktop
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.gridcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.gridcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.gridcraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.gridcraft.yml')); assert m['id'] == 'ai.storyteller.gridcraft' and m['command'] == 'gridcraft'"

View File

@ -1,5 +1,5 @@
# Release pipeline: every push to `release` builds signed installers for every platform and
# creates or updates the draft GitHub Release "SheetCraft v<version>". See docs/releasing.md.
# creates or updates the draft GitHub Release "GridCraft v<version>". See docs/releasing.md.
# Copied from DesignCraft (craftrules release/playbook.md); keep the two in step.
#
# All jobs run in the `release` environment (restricted to the `release` branch), which holds
@ -29,7 +29,7 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
SHEETCRAFT_BUILD_SHA: ${{ github.sha }}
GRIDCRAFT_BUILD_SHA: ${{ github.sha }}
INPUT_VERSION: ${{ inputs.version }}
jobs:
@ -59,7 +59,7 @@ jobs:
echo "date=$(date -u +%Y-%m-%d)"
echo "prerelease=$pre"
} >>"$GITHUB_OUTPUT"
echo "### SheetCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
echo "### GridCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
macos:
name: macOS universal
@ -67,8 +67,8 @@ jobs:
runs-on: macos-15
environment: release
env:
SHEETCRAFT_VERSION: ${{ needs.version.outputs.version }}
SHEETCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
GRIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
GRIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps:
- uses: actions/checkout@v4
# Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it.
@ -86,7 +86,7 @@ jobs:
key: release-macos-universal
- name: Apply version override
if: inputs.version != ''
run: cargo run -q -p xtask --no-default-features -- version set "$SHEETCRAFT_VERSION"
run: cargo run -q -p xtask --no-default-features -- version set "$GRIDCRAFT_VERSION"
- name: Import signing certificate
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@ -126,8 +126,8 @@ jobs:
# .github/workflows/windows-arm64.yml installs and runs it on ARM64 hardware.
- { arch: arm64, target: aarch64-pc-windows-msvc }
env:
SHEETCRAFT_VERSION: ${{ needs.version.outputs.version }}
SHEETCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
GRIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
GRIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
steps:
- uses: actions/checkout@v4
# Optional font input (docs: storytold/craft-fonts docs/integration.md); releases always embed it.
@ -146,7 +146,7 @@ jobs:
- name: Apply version override
if: inputs.version != ''
shell: bash
run: cargo run -q -p xtask --no-default-features -- version set "$SHEETCRAFT_VERSION"
run: cargo run -q -p xtask --no-default-features -- version set "$GRIDCRAFT_VERSION"
- name: Install WiX
shell: pwsh
run: dotnet tool install --global wix --version 5.0.2
@ -184,8 +184,8 @@ jobs:
- { arch: x86_64, runner: ubuntu-22.04, deb: amd64 }
- { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 }
env:
SHEETCRAFT_VERSION: ${{ needs.version.outputs.version }}
SHEETCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
GRIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
GRIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
NFPM_VERSION: 2.47.0
steps:
- uses: actions/checkout@v4
@ -211,7 +211,7 @@ jobs:
key: release-linux-${{ matrix.arch }}
- name: Apply version override
if: inputs.version != ''
run: cargo run -q -p xtask --no-default-features -- version set "$SHEETCRAFT_VERSION"
run: cargo run -q -p xtask --no-default-features -- version set "$GRIDCRAFT_VERSION"
- name: Build and package (AppImage, deb, rpm, tar.gz)
env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
@ -220,9 +220,9 @@ jobs:
- name: Check the packages
run: |
dpkg-deb --info dist/release/*.deb
dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/sheetcraft$|metainfo|applications'
ldd target/release/sheetcraft
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.sheetcraft.yml')); assert m['id'] == 'ai.storyteller.sheetcraft'"
dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/gridcraft$|metainfo|applications'
ldd target/release/gridcraft
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.gridcraft.yml')); assert m['id'] == 'ai.storyteller.gridcraft'"
for appimage in dist/release/*.AppImage; do
chmod +x "$appimage"
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version
@ -239,8 +239,8 @@ jobs:
runs-on: ubuntu-latest
environment: release
env:
SHEETCRAFT_VERSION: ${{ needs.version.outputs.version }}
SHEETCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
GRIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
GRIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
TRUNK_VERSION: 0.21.14
steps:
- uses: actions/checkout@v4
@ -264,7 +264,7 @@ jobs:
trunk --version
- name: Apply version override
if: inputs.version != ''
run: cargo run -q -p xtask --no-default-features -- version set "$SHEETCRAFT_VERSION"
run: cargo run -q -p xtask --no-default-features -- version set "$GRIDCRAFT_VERSION"
- name: Build and zip
env:
CRAFT_FONTS_DIR: ${{ github.workspace }}/craft-fonts
@ -277,7 +277,7 @@ jobs:
if-no-files-found: error
# FreeBSD has no hosted runners: freebsd.yml builds in a FreeBSD VM and, with package: true,
# uploads the release-freebsd artifact (sheetcraft-<v>-freebsd-x86_64.tar.gz). It needs no
# uploads the release-freebsd artifact (gridcraft-<v>-freebsd-x86_64.tar.gz). It needs no
# secrets, so the called workflow runs without the release environment.
freebsd:
name: FreeBSD
@ -314,7 +314,7 @@ jobs:
working-directory: artifacts
run: |
tag="v$VERSION"
title="SheetCraft v$VERSION"
title="GridCraft v$VERSION"
pre=()
if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi
if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then

View File

@ -53,7 +53,7 @@ jobs:
run: ./packaging/windows/package.ps1 -Arch arm64
- uses: actions/upload-artifact@v4
with:
name: sheetcraft-windows-arm64
name: gridcraft-windows-arm64
path: dist/arm64/*
if-no-files-found: error
retention-days: 7
@ -66,7 +66,7 @@ jobs:
steps:
- uses: actions/download-artifact@v4
with:
name: sheetcraft-windows-arm64
name: gridcraft-windows-arm64
path: dist
- name: Install the MSI, check it, run it, uninstall it
shell: pwsh
@ -75,8 +75,8 @@ jobs:
$msi = (Get-ChildItem dist/*.msi | Select-Object -First 1).FullName
$p = Start-Process msiexec.exe -ArgumentList '/i', "`"$msi`"", '/qn', '/l*v', 'install.log' -Wait -PassThru
if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 60; throw "msiexec /i exited $($p.ExitCode)" }
$dir = Join-Path $env:ProgramFiles 'SheetCraft'
foreach ($exe in 'sheetcraft.exe', 'sheetcraft-cli.exe') {
$dir = Join-Path $env:ProgramFiles 'GridCraft'
foreach ($exe in 'gridcraft.exe', 'gridcraft-cli.exe') {
$path = Join-Path $dir $exe
if (-not (Test-Path $path)) { throw "$exe not installed in $dir" }
$bytes = [System.IO.File]::ReadAllBytes($path)
@ -85,11 +85,11 @@ jobs:
Write-Output "ok $path (ARM64)"
}
# The installed CLI runs natively on ARM64.
$cli = Join-Path $dir 'sheetcraft-cli.exe'
$cli = Join-Path $dir 'gridcraft-cli.exe'
$out = & $cli --version 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) { Write-Output $out; throw "sheetcraft-cli --version exited $LASTEXITCODE" }
Write-Output "ok sheetcraft-cli --version: $($out.Trim())"
if ($LASTEXITCODE -ne 0) { Write-Output $out; throw "gridcraft-cli --version exited $LASTEXITCODE" }
Write-Output "ok gridcraft-cli --version: $($out.Trim())"
$p = Start-Process msiexec.exe -ArgumentList '/x', "`"$msi`"", '/qn' -Wait -PassThru
if ($p.ExitCode -ne 0) { throw "msiexec /x exited $($p.ExitCode)" }
if (Test-Path (Join-Path $dir 'sheetcraft.exe')) { throw 'uninstall left sheetcraft.exe behind' }
if (Test-Path (Join-Path $dir 'gridcraft.exe')) { throw 'uninstall left gridcraft.exe behind' }
Write-Output 'ok uninstalled'

View File

@ -1,6 +1,6 @@
# SheetCraft — instructions for agents
# GridCraft — instructions for agents
SheetCraft is a clean-room, open-source, Rust-native spreadsheet targeting Microsoft Excel parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../designcraft` (InDesign-class, the closest reference), `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../printcraft` (Acrobat), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../effectcraft` (After Effects).
GridCraft is a clean-room, open-source, Rust-native spreadsheet targeting Microsoft Excel parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../designcraft` (InDesign-class, the closest reference), `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../printcraft` (Acrobat), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../effectcraft` (After Effects).
Standards and learnings shared across the crafting apps live in `../../craftrules` (checked out next to the craft apps, or `storytold/craftrules`). Read its `AGENTS.md` at the start of a session, follow its standards, and contribute reusable learnings back there. Never code: repos don't share code.
@ -12,7 +12,7 @@ Standards and learnings shared across the crafting apps live in `../../craftrule
## Assets and iconography — absolute rule
**No Microsoft, Adobe, Avid, Autodesk (or any other vendor's) iconography, images, artwork, fonts, templates, themes or sample files — ever.** This is the most important rule in this repository; breaking it is a serious failure.
- Every asset in the repo is original work by SheetCraft contributors, or public domain / CC0, or open source / Creative Commons that allows redistribution (or licensed open source by the contributor who created it).
- Every asset in the repo is original work by GridCraft contributors, or public domain / CC0, or open source / Creative Commons that allows redistribution (or licensed open source by the contributor who created it).
- Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md) (author, source, licence). `cargo xtask assets` enforces it. Adding an asset without a row is a bug.
- Prefer art generated in code: the UI icon set (`crates/ui-egui/src/icons.rs`), table and cell style palettes, themes and sample workbooks are drawn/defined in code and original.
- Never copy Office's icons, ribbon artwork, theme XML, table-style definitions, templates, clip art or wording beyond feature names. Excel feature *names* (menu labels, function names) are fine.
@ -20,7 +20,7 @@ Standards and learnings shared across the crafting apps live in `../../craftrule
- The ArtCraft brand files in `docs/brand/` are ArtCraft trademarks under `docs/brand/LICENSE-brand.txt`, the one exception.
## Never crash
People trust SheetCraft with their numbers; a crash loses their work. **This outranks feature work.** Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
People trust GridCraft with their numbers; a crash loses their work. **This outranks feature work.** Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
- **Errors are `Result<T, E>`** through the crate's error type and `?`.
- **Input-derived numbers are hostile** (files, formulas, commands, MCP/control params): `get()` instead of indexing, checked/saturating arithmetic, cap input-sized allocations (huge ranges, `REPT`, `SEQUENCE`, whole-column references).
@ -41,10 +41,10 @@ People trust SheetCraft with their numbers; a crash loses their work. **This out
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, assets, layers, wasm). One task id per commit (`M2.1: borders gallery`).
## Running and looking at the app
- `cargo run --release -p sheetcraft -- --sample sales --control 7979` (sample workbook + control channel).
- `cargo run --release -p gridcraft -- --sample sales --control 7979` (sample workbook + control channel).
- Drive it: JSON lines on `127.0.0.1:7979`, e.g. `{"id":1,"method":"engine.execute","params":{"command":"cell.set","params":{"cell":"B2","input":"=SUM(A1:A9)"}}}` then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods: `crates/ui-egui/src/control.rs`, docs: `docs/control-protocol.md`.
- **Offscreen UI render** (no window): `cargo run --release -p sheetcraft-cli -- snapshot --sample sales out.png`.
- Headless: `sheetcraft-cli eval '=SUM(1,2,3)'`, `sheetcraft-cli run --in book.xlsx --cmd 'home.bold={"range":"A1:C1"}' --out book.xlsx`, `sheetcraft-cli mcp`.
- **Offscreen UI render** (no window): `cargo run --release -p gridcraft-cli -- snapshot --sample sales out.png`.
- Headless: `gridcraft-cli eval '=SUM(1,2,3)'`, `gridcraft-cli run --in book.xlsx --cmd 'home.bold={"range":"A1:C1"}' --out book.xlsx`, `gridcraft-cli mcp`.
- **For UI work, look at the result** (snapshot PNG) and compare with `plan/excel/observed-ui.md`.
- Shell gotcha: `mv`/`cp` are aliased interactive here — use `/bin/mv -f` / `/bin/cp -f`. macOS has no `timeout`; use `perl -e 'alarm 60; exec @ARGV' cmd`.
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done.

View File

@ -2,7 +2,7 @@
Every non-code asset in this repository (images, icons, fonts, example files, presets) is listed here with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an asset file is missing from this table.
**Policy (mandatory):** SheetCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, artwork, fonts, templates, themes, table styles or sample files**. Every asset is original work by SheetCraft contributors or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution). Screenshots of Microsoft software are never committed. Font files are not added here: shared fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (`CRAFT_FONTS_DIR`). The one exception is the ArtCraft brand files in `docs/brand/`: ArtCraft trademarks, not open source, used under `docs/brand/LICENSE-brand.txt`.
**Policy (mandatory):** GridCraft contains **no Microsoft, Adobe, Avid or Autodesk iconography, images, artwork, fonts, templates, themes, table styles or sample files**. Every asset is original work by GridCraft contributors or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution). Screenshots of Microsoft software are never committed. Font files are not added here: shared fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (`CRAFT_FONTS_DIR`). The one exception is the ArtCraft brand files in `docs/brand/`: ArtCraft trademarks, not open source, used under `docs/brand/LICENSE-brand.txt`.
Generated-in-code art is original and has no file to list: the UI icon set (`crates/ui-egui/src/icons.rs`), themes (`crates/engine/src/cmd/view.rs`), cell styles (`crates/engine/src/cmd/format.rs`), table styles (`crates/engine/src/tables.rs`), chart palettes and the sample workbooks (`crates/engine/src/sample.rs`).
@ -17,23 +17,23 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra
| `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/artcraft-mark-black.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | Trademark |
| `docs/brand/LICENSE-brand.txt` | (licence text) | craftrules | — | |
| `assets/app-icon/sheetcraft.svg` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | PLACEHOLDER app icon (ledger page); replaced by the owner's creature art later |
| `assets/app-icon/sheetcraft-1024.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, rendered from sheetcraft.svg |
| `assets/app-icon/sheetcraft-macos-512.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, macOS grid render |
| `assets/app-icon/sheetcraft.icns` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, macOS bundle icon |
| `assets/app-icon/sheetcraft.ico` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Windows exe icon |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.sheetcraft.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.sheetcraft.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.sheetcraft.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.sheetcraft.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.sheetcraft.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.sheetcraft.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.sheetcraft.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.sheetcraft.png` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.sheetcraft.svg` | SheetCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, copy of sheetcraft.svg |
| `assets/app-icon/README.md` | SheetCraft contributors | original | MIT OR Apache-2.0 | Icon documentation |
| `assets/app-icon/gridcraft.svg` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | PLACEHOLDER app icon (ledger page); replaced by the owner's creature art later |
| `assets/app-icon/gridcraft-1024.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, rendered from gridcraft.svg |
| `assets/app-icon/gridcraft-macos-512.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, macOS grid render |
| `assets/app-icon/gridcraft.icns` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, macOS bundle icon |
| `assets/app-icon/gridcraft.ico` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Windows exe icon |
| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.gridcraft.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.gridcraft.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.gridcraft.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.gridcraft.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.gridcraft.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.gridcraft.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.gridcraft.png` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, Linux hicolor render |
| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.gridcraft.svg` | GridCraft contributors | original (hand-written SVG; `packaging/icons.sh`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | Placeholder icon, copy of gridcraft.svg |
| `assets/app-icon/README.md` | GridCraft contributors | original | MIT OR Apache-2.0 | Icon documentation |
| `assets/app-icon/LICENSE.txt` | (licence text) | original | — | Licence of the app icon files |
| `docs/images/hero-sales.png` | SheetCraft contributors | screenshot of SheetCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/budget.png` | SheetCraft contributors | screenshot of SheetCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/grades-formulas.png` | SheetCraft contributors | screenshot of SheetCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/dark-insert.png` | SheetCraft contributors | screenshot of SheetCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/hero-sales.png` | GridCraft contributors | screenshot of GridCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/budget.png` | GridCraft contributors | screenshot of GridCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/grades-formulas.png` | GridCraft contributors | screenshot of GridCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |
| `docs/images/dark-insert.png` | GridCraft contributors | screenshot of GridCraft rendered offscreen (`crates/ui-egui/examples/snapshot.rs`) showing an original sample workbook | MIT OR Apache-2.0 | README screenshot; no third-party content |

View File

@ -1,6 +1,6 @@
# SheetCraft — instructions for agents
# GridCraft — instructions for agents
SheetCraft is a clean-room, open-source, Rust-native spreadsheet targeting Microsoft Excel parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../designcraft` (InDesign-class, the closest reference), `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../printcraft` (Acrobat), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../effectcraft` (After Effects).
GridCraft is a clean-room, open-source, Rust-native spreadsheet targeting Microsoft Excel parity — and superiority (speed, openness, agent control). It runs natively on macOS, Windows, Linux and BSD, and on the web via WASM. Siblings with the same conventions: `../designcraft` (InDesign-class, the closest reference), `../photocraft` (Photoshop), `../vectorcraft` (Illustrator), `../printcraft` (Acrobat), `../filmcraft` (Premiere), `../lightcraft` (Lightroom), `../effectcraft` (After Effects).
Standards and learnings shared across the crafting apps live in `../../craftrules` (checked out next to the craft apps, or `storytold/craftrules`). Read its `AGENTS.md` at the start of a session, follow its standards, and contribute reusable learnings back there. Never code: repos don't share code.
@ -12,7 +12,7 @@ Standards and learnings shared across the crafting apps live in `../../craftrule
## Assets and iconography — absolute rule
**No Microsoft, Adobe, Avid, Autodesk (or any other vendor's) iconography, images, artwork, fonts, templates, themes or sample files — ever.** This is the most important rule in this repository; breaking it is a serious failure.
- Every asset in the repo is original work by SheetCraft contributors, or public domain / CC0, or open source / Creative Commons that allows redistribution (or licensed open source by the contributor who created it).
- Every asset in the repo is original work by GridCraft contributors, or public domain / CC0, or open source / Creative Commons that allows redistribution (or licensed open source by the contributor who created it).
- Every asset file has a row in [`ATTRIBUTION.md`](ATTRIBUTION.md) (author, source, licence). `cargo xtask assets` enforces it. Adding an asset without a row is a bug.
- Prefer art generated in code: the UI icon set (`crates/ui-egui/src/icons.rs`), table and cell style palettes, themes and sample workbooks are drawn/defined in code and original.
- Never copy Office's icons, ribbon artwork, theme XML, table-style definitions, templates, clip art or wording beyond feature names. Excel feature *names* (menu labels, function names) are fine.
@ -20,7 +20,7 @@ Standards and learnings shared across the crafting apps live in `../../craftrule
- The ArtCraft brand files in `docs/brand/` are ArtCraft trademarks under `docs/brand/LICENSE-brand.txt`, the one exception.
## Never crash
People trust SheetCraft with their numbers; a crash loses their work. **This outranks feature work.** Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
People trust GridCraft with their numbers; a crash loses their work. **This outranks feature work.** Standard: [`craftrules/standards/never-crash.md`](https://github.com/storytold/craftrules/blob/main/standards/never-crash.md).
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
- **Errors are `Result<T, E>`** through the crate's error type and `?`.
- **Input-derived numbers are hostile** (files, formulas, commands, MCP/control params): `get()` instead of indexing, checked/saturating arithmetic, cap input-sized allocations (huge ranges, `REPT`, `SEQUENCE`, whole-column references).
@ -41,10 +41,10 @@ People trust SheetCraft with their numbers; a crash loses their work. **This out
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, assets, layers, wasm). One task id per commit (`M2.1: borders gallery`).
## Running and looking at the app
- `cargo run --release -p sheetcraft -- --sample sales --control 7979` (sample workbook + control channel).
- `cargo run --release -p gridcraft -- --sample sales --control 7979` (sample workbook + control channel).
- Drive it: JSON lines on `127.0.0.1:7979`, e.g. `{"id":1,"method":"engine.execute","params":{"command":"cell.set","params":{"cell":"B2","input":"=SUM(A1:A9)"}}}` then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods: `crates/ui-egui/src/control.rs`, docs: `docs/control-protocol.md`.
- **Offscreen UI render** (no window): `cargo run --release -p sheetcraft-cli -- snapshot --sample sales out.png`.
- Headless: `sheetcraft-cli eval '=SUM(1,2,3)'`, `sheetcraft-cli run --in book.xlsx --cmd 'home.bold={"range":"A1:C1"}' --out book.xlsx`, `sheetcraft-cli mcp`.
- **Offscreen UI render** (no window): `cargo run --release -p gridcraft-cli -- snapshot --sample sales out.png`.
- Headless: `gridcraft-cli eval '=SUM(1,2,3)'`, `gridcraft-cli run --in book.xlsx --cmd 'home.bold={"range":"A1:C1"}' --out book.xlsx`, `gridcraft-cli mcp`.
- **For UI work, look at the result** (snapshot PNG) and compare with `plan/excel/observed-ui.md`.
- Shell gotcha: `mv`/`cp` are aliased interactive here — use `/bin/mv -f` / `/bin/cp -f`. macOS has no `timeout`; use `perl -e 'alarm 60; exec @ARGV' cmd`.
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; delete your target dir when done.

340
Cargo.lock generated
View File

@ -1390,6 +1390,175 @@ dependencies = [
"windows",
]
[[package]]
name = "gridcraft"
version = "0.1.0"
dependencies = [
"eframe",
"egui",
"gridcraft-engine",
"gridcraft-ui-egui",
"image",
"log",
"muda",
"rfd",
"serde_json",
"winresource",
]
[[package]]
name = "gridcraft-calc"
version = "0.1.0"
dependencies = [
"gridcraft-core",
"gridcraft-formula",
"gridcraft-functions",
"gridcraft-model",
"gridcraft-numfmt",
"log",
]
[[package]]
name = "gridcraft-chart"
version = "0.1.0"
dependencies = [
"gridcraft-calc",
"gridcraft-core",
"gridcraft-model",
"gridcraft-numfmt",
"serde",
]
[[package]]
name = "gridcraft-cli"
version = "0.1.0"
dependencies = [
"gridcraft-engine",
"gridcraft-mcp",
"log",
"serde_json",
]
[[package]]
name = "gridcraft-core"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]]
name = "gridcraft-engine"
version = "0.1.0"
dependencies = [
"gridcraft-calc",
"gridcraft-chart",
"gridcraft-core",
"gridcraft-formula",
"gridcraft-functions",
"gridcraft-model",
"gridcraft-numfmt",
"gridcraft-pdf",
"gridcraft-xlsx",
"image",
"log",
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]]
name = "gridcraft-formula"
version = "0.1.0"
dependencies = [
"gridcraft-core",
"serde",
"thiserror 2.0.21",
]
[[package]]
name = "gridcraft-functions"
version = "0.1.0"
dependencies = [
"gridcraft-core",
"regex",
]
[[package]]
name = "gridcraft-mcp"
version = "0.1.0"
dependencies = [
"gridcraft-engine",
"log",
"serde_json",
]
[[package]]
name = "gridcraft-model"
version = "0.1.0"
dependencies = [
"gridcraft-core",
"gridcraft-formula",
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]]
name = "gridcraft-numfmt"
version = "0.1.0"
dependencies = [
"gridcraft-core",
]
[[package]]
name = "gridcraft-pdf"
version = "0.1.0"
[[package]]
name = "gridcraft-ui-egui"
version = "0.1.0"
dependencies = [
"egui",
"egui_kittest",
"gridcraft-chart",
"gridcraft-engine",
"image",
"log",
"serde",
"serde_json",
]
[[package]]
name = "gridcraft-web"
version = "0.1.0"
dependencies = [
"eframe",
"egui",
"gridcraft-engine",
"gridcraft-ui-egui",
"js-sys",
"log",
"rfd",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "gridcraft-xlsx"
version = "0.1.0"
dependencies = [
"gridcraft-core",
"gridcraft-formula",
"gridcraft-model",
"quick-xml 0.38.4",
"thiserror 2.0.21",
"zip",
]
[[package]]
name = "guillotiere"
version = "0.7.0"
@ -2833,175 +3002,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "sheetcraft"
version = "0.1.0"
dependencies = [
"eframe",
"egui",
"image",
"log",
"muda",
"rfd",
"serde_json",
"sheetcraft-engine",
"sheetcraft-ui-egui",
"winresource",
]
[[package]]
name = "sheetcraft-calc"
version = "0.1.0"
dependencies = [
"log",
"sheetcraft-core",
"sheetcraft-formula",
"sheetcraft-functions",
"sheetcraft-model",
"sheetcraft-numfmt",
]
[[package]]
name = "sheetcraft-chart"
version = "0.1.0"
dependencies = [
"serde",
"sheetcraft-calc",
"sheetcraft-core",
"sheetcraft-model",
"sheetcraft-numfmt",
]
[[package]]
name = "sheetcraft-cli"
version = "0.1.0"
dependencies = [
"log",
"serde_json",
"sheetcraft-engine",
"sheetcraft-mcp",
]
[[package]]
name = "sheetcraft-core"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]]
name = "sheetcraft-engine"
version = "0.1.0"
dependencies = [
"image",
"log",
"serde",
"serde_json",
"sheetcraft-calc",
"sheetcraft-chart",
"sheetcraft-core",
"sheetcraft-formula",
"sheetcraft-functions",
"sheetcraft-model",
"sheetcraft-numfmt",
"sheetcraft-pdf",
"sheetcraft-xlsx",
"thiserror 2.0.21",
]
[[package]]
name = "sheetcraft-formula"
version = "0.1.0"
dependencies = [
"serde",
"sheetcraft-core",
"thiserror 2.0.21",
]
[[package]]
name = "sheetcraft-functions"
version = "0.1.0"
dependencies = [
"regex",
"sheetcraft-core",
]
[[package]]
name = "sheetcraft-mcp"
version = "0.1.0"
dependencies = [
"log",
"serde_json",
"sheetcraft-engine",
]
[[package]]
name = "sheetcraft-model"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"sheetcraft-core",
"sheetcraft-formula",
"thiserror 2.0.21",
]
[[package]]
name = "sheetcraft-numfmt"
version = "0.1.0"
dependencies = [
"sheetcraft-core",
]
[[package]]
name = "sheetcraft-pdf"
version = "0.1.0"
[[package]]
name = "sheetcraft-ui-egui"
version = "0.1.0"
dependencies = [
"egui",
"egui_kittest",
"image",
"log",
"serde",
"serde_json",
"sheetcraft-chart",
"sheetcraft-engine",
]
[[package]]
name = "sheetcraft-web"
version = "0.1.0"
dependencies = [
"eframe",
"egui",
"js-sys",
"log",
"rfd",
"serde_json",
"sheetcraft-engine",
"sheetcraft-ui-egui",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "sheetcraft-xlsx"
version = "0.1.0"
dependencies = [
"quick-xml 0.38.4",
"sheetcraft-core",
"sheetcraft-formula",
"sheetcraft-model",
"thiserror 2.0.21",
"zip",
]
[[package]]
name = "shlex"
version = "2.0.1"
@ -4288,8 +4288,8 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
name = "xtask"
version = "0.1.0"
dependencies = [
"gridcraft-engine",
"serde_json",
"sheetcraft-engine",
]
[[package]]

View File

@ -1,29 +1,29 @@
[workspace]
resolver = "3"
members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/sheetcraft", "apps/sheetcraft-cli", "xtask"]
default-members = ["crates/*", "apps/gridcraft", "apps/gridcraft-cli", "xtask"]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.90"
repository = "https://github.com/storytold/sheetcraft"
repository = "https://github.com/storytold/gridcraft"
[workspace.dependencies]
# internal
sheetcraft-core = { path = "crates/core" }
sheetcraft-numfmt = { path = "crates/numfmt" }
sheetcraft-formula = { path = "crates/formula" }
sheetcraft-functions = { path = "crates/functions" }
sheetcraft-model = { path = "crates/model" }
sheetcraft-calc = { path = "crates/calc" }
sheetcraft-xlsx = { path = "crates/xlsx" }
sheetcraft-chart = { path = "crates/chart" }
sheetcraft-engine = { path = "crates/engine" }
sheetcraft-ui-egui = { path = "crates/ui-egui" }
sheetcraft-mcp = { path = "crates/mcp" }
sheetcraft-pdf = { path = "crates/pdf" }
gridcraft-core = { path = "crates/core" }
gridcraft-numfmt = { path = "crates/numfmt" }
gridcraft-formula = { path = "crates/formula" }
gridcraft-functions = { path = "crates/functions" }
gridcraft-model = { path = "crates/model" }
gridcraft-calc = { path = "crates/calc" }
gridcraft-xlsx = { path = "crates/xlsx" }
gridcraft-chart = { path = "crates/chart" }
gridcraft-engine = { path = "crates/engine" }
gridcraft-ui-egui = { path = "crates/ui-egui" }
gridcraft-mcp = { path = "crates/mcp" }
gridcraft-pdf = { path = "crates/pdf" }
# foundation
thiserror = "2"

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 ArtCraft Team and the SheetCraft contributors
Copyright (c) 2026 ArtCraft Team and the GridCraft contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

6
NOTICE
View File

@ -1,5 +1,5 @@
SheetCraft
Copyright (c) 2026 ArtCraft Team and the SheetCraft contributors.
GridCraft
Copyright (c) 2026 ArtCraft Team and the GridCraft contributors.
Licensed under the MIT License or the Apache License, Version 2.0, at your option
(LICENSE-MIT, LICENSE-APACHE).
@ -12,5 +12,5 @@ Third-party material
This product bundles third-party material under its own licenses, listed below. The per-file
list (author, source, license) is in ATTRIBUTION.md.
None: every bundled asset is original to SheetCraft. Builds made with craft-fonts
None: every bundled asset is original to GridCraft. Builds made with craft-fonts
(CRAFT_FONTS_DIR) embed the fonts listed in ATTRIBUTION.md under the SIL Open Font License 1.1.

View File

@ -7,7 +7,7 @@
</a>
</p>
<h1 align="center">SheetCraft</h1>
<h1 align="center">GridCraft</h1>
<p align="center">
<b>Spreadsheets and data; an open-source, clean-room reimplementation of Microsoft Excel, rebuilt in pure Rust.</b>
@ -31,7 +31,7 @@
</p>
<p align="center">
<a href="https://getartcraft.com/apps/sheetcraft"><b>SheetCraft on getartcraft.com</b></a> ·
<a href="https://getartcraft.com/apps/gridcraft"><b>GridCraft on getartcraft.com</b></a> ·
<a href="https://getartcraft.com/">ArtCraft</a> ·
<a href="https://getartcraft.com/apps">All Crafting Apps</a>
</p>
@ -39,8 +39,8 @@
<br>
<p align="center">
<img src="docs/images/hero-sales.png" alt="SheetCraft showing a quarterly sales dashboard: a styled table with banded rows, filter buttons, a totals row, data bars, arrow icons and sparklines, next to a clustered column chart and a pie chart" width="100%">
<br><sub><b>Quarterly Sales Dashboard</b>: a table with a totals row, conditional formatting, sparklines and two live charts — built entirely from SheetCraft commands (<code>File → New → Sample</code>).</sub>
<img src="docs/images/hero-sales.png" alt="GridCraft showing a quarterly sales dashboard: a styled table with banded rows, filter buttons, a totals row, data bars, arrow icons and sparklines, next to a clustered column chart and a pie chart" width="100%">
<br><sub><b>Quarterly Sales Dashboard</b>: a table with a totals row, conditional formatting, sparklines and two live charts — built entirely from GridCraft commands (<code>File → New → Sample</code>).</sub>
</p>
> [!NOTE]
@ -50,7 +50,7 @@
<p align="center">
<a href="#a-look-around">A look around</a> ·
<a href="#why-sheetcraft">Why SheetCraft</a> ·
<a href="#why-gridcraft">Why GridCraft</a> ·
<a href="#quick-start">Quick start</a> ·
<a href="#for-agents-mcp-cli-and-the-control-channel">For agents</a> ·
<a href="#architecture">Architecture</a> ·
@ -72,11 +72,11 @@
</tr>
</table>
## Why SheetCraft
## Why GridCraft
- **Familiar.** Excel's ribbon, formula bar, Name Box, sheet tabs, keyboard shortcuts, fill
handle, point-mode formula entry with coloured references, Format Cells, Paste Special,
conditional formatting, tables, charts and more. If you know Excel, you already know SheetCraft.
conditional formatting, tables, charts and more. If you know Excel, you already know GridCraft.
- **Compatible.** XLSX is the native format: styles, themes, formulas (including dynamic arrays),
tables, conditional formats, validation, comments, hyperlinks, charts, pictures, sparklines and
print settings round-trip. CSV and TSV too.
@ -94,18 +94,18 @@
## Quick start
```sh
cargo run --release -p sheetcraft # desktop app
cargo run --release -p sheetcraft -- --sample sales # open a sample (sales, budget, grades)
cargo run --release -p sheetcraft -- book.xlsx # open a workbook
cargo run --release -p sheetcraft -- --sample sales --control 7979 # + JSON control channel
cargo run --release -p gridcraft # desktop app
cargo run --release -p gridcraft -- --sample sales # open a sample (sales, budget, grades)
cargo run --release -p gridcraft -- book.xlsx # open a workbook
cargo run --release -p gridcraft -- --sample sales --control 7979 # + JSON control channel
cargo xtask ci # fmt, clippy, tests, assets, layering, wasm
```
### Web
```sh
cd apps/sheetcraft-web && trunk build --release # → dist/web (serve it with any static server)
cd apps/sheetcraft-web && trunk serve --release # http://127.0.0.1:8771 (?sample=sales opens a sample)
cd apps/gridcraft-web && trunk build --release # → dist/web (serve it with any static server)
cd apps/gridcraft-web && trunk serve --release # http://127.0.0.1:8771 (?sample=sales opens a sample)
```
You need [trunk](https://trunkrs.dev) and the `wasm32-unknown-unknown` target. Open uses the
@ -114,23 +114,23 @@ browser's file picker (dropping files works too) and Save downloads the workbook
## For agents: MCP, CLI and the control channel
```sh
claude mcp add sheetcraft -- sheetcraft-cli mcp # headless MCP server
sheetcraft-cli mcp --connect 7979 # MCP bridged to a running window
sheetcraft-cli eval '=XLOOKUP("b",{"a","b"},{1,2})' # → 2
sheetcraft-cli run --sample budget --cmd 'home.bold={"range":"B4:F4"}' --out budget.xlsx
sheetcraft-cli cat budget.xlsx --range B4:F15 # aligned text table
sheetcraft-cli commands --search chart # every command and its params
claude mcp add gridcraft -- gridcraft-cli mcp # headless MCP server
gridcraft-cli mcp --connect 7979 # MCP bridged to a running window
gridcraft-cli eval '=XLOOKUP("b",{"a","b"},{1,2})' # → 2
gridcraft-cli run --sample budget --cmd 'home.bold={"range":"B4:F4"}' --out budget.xlsx
gridcraft-cli cat budget.xlsx --range B4:F15 # aligned text table
gridcraft-cli commands --search chart # every command and its params
```
The MCP server offers tools such as `read_range`, `write_range`, `set_cell`, `evaluate_formula`,
`format_range`, `insert_chart`, `create_table`, `sort_range`, `filter` and `execute_command`
(which reaches every one of SheetCraft's commands). The desktop app's control channel adds real
(which reaches every one of GridCraft's commands). The desktop app's control channel adds real
pointer and keyboard input, dialogs and screenshots. See [`docs/mcp.md`](docs/mcp.md),
[`docs/cli.md`](docs/cli.md) and [`docs/control-protocol.md`](docs/control-protocol.md).
## Architecture
SheetCraft is an engine-first Cargo workspace with enforced layering (`cargo xtask layers`). The
GridCraft is an engine-first Cargo workspace with enforced layering (`cargo xtask layers`). The
egui frontend is a separate crate, so the UI can be swapped without touching the engine.
| Layer | Crates |
@ -141,7 +141,7 @@ egui frontend is a separate crate, so the UI can be swapped without touching the
| L3 | `chart` (toolkit-free chart layout and rendering) |
| L5 | `engine` (session, commands, history, clipboard, fill, sort, filter, file I/O) |
| L6 | `ui-egui` (Excel-style UI, control channel) · `mcp` (MCP server) |
| L7 | `apps/sheetcraft`, `apps/sheetcraft-cli`, `apps/sheetcraft-web` |
| L7 | `apps/gridcraft`, `apps/gridcraft-cli`, `apps/gridcraft-web` |
- **Contributor and agent rules** (clean-room, asset policy, never-crash, quality gates): [`AGENTS.md`](AGENTS.md)
- **Bundled assets:** each one is listed with its licence in [`ATTRIBUTION.md`](ATTRIBUTION.md)
@ -149,14 +149,14 @@ egui frontend is a separate crate, so the UI can be swapped without touching the
## Roadmap
SheetCraft is pre-alpha and moving fast. Today it covers 60% of Excel's ribbon and menu
GridCraft is pre-alpha and moving fast. Today it covers 60% of Excel's ribbon and menu
commands ([`docs/parity.md`](docs/parity.md)) and about 93% of its worksheet functions. PivotTables,
printing and PDF, page layout view, deeper chart formatting and scripting come next. The plan
and estimates are in [ROADMAP.md](ROADMAP.md).
## The Crafting Apps
SheetCraft is one of the **Crafting Apps**: free, open-source creative tools from the
GridCraft is one of the **Crafting Apps**: free, open-source creative tools from the
[ArtCraft](https://getartcraft.com/) team, each written from scratch in Rust and each able to
stand on its own.
@ -169,7 +169,7 @@ stand on its own.
| <img src="https://raw.githubusercontent.com/storytold/printcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.printcraft.png" alt="" width="32" height="32"> | **PrintCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/printcraft) | [Website](https://getartcraft.com/apps/printcraft) |
| <img src="https://raw.githubusercontent.com/storytold/effectcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.effectcraft.png" alt="" width="32" height="32"> | **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
| <img src="https://raw.githubusercontent.com/storytold/designcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png" alt="" width="32" height="32"> | **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
| <img src="https://raw.githubusercontent.com/storytold/sheetcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.sheetcraft.png" alt="" width="32" height="32"> | **SheetCraft** | **Spreadsheets and data · you are here** | [GitHub](https://github.com/storytold/sheetcraft) | [Website](https://getartcraft.com/apps/sheetcraft) |
| <img src="https://raw.githubusercontent.com/storytold/gridcraft/main/assets/app-icon/hicolor/64x64/apps/ai.storyteller.gridcraft.png" alt="" width="32" height="32"> | **GridCraft** | **Spreadsheets and data · you are here** | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control.
@ -192,27 +192,27 @@ And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video stud
<a href="https://discord.gg/artcraft"><b>discord.gg/artcraft</b></a> ·
<a href="https://getartcraft.com/">getartcraft.com</a> ·
<a href="https://getartcraft.com/apps">The Crafting Apps</a> ·
<a href="https://getartcraft.com/apps/sheetcraft">SheetCraft</a>
<a href="https://getartcraft.com/apps/gridcraft">GridCraft</a>
</p>
## License and credits
SheetCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
Copyright (c) 2026 ArtCraft Team and the SheetCraft contributors. Required notices are in [NOTICE](NOTICE).
GridCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
Copyright (c) 2026 ArtCraft Team and the GridCraft contributors. Required notices are in [NOTICE](NOTICE).
Bundled fonts, icons, images and other assets keep their own open licenses; each one is listed
with its author, source and license in [ATTRIBUTION.md](ATTRIBUTION.md).
All UI icons are drawn in code and are original; the sample workbooks and every screenshot in
this README were made with SheetCraft itself. SheetCraft bundles no fonts: it uses the fonts
this README were made with GridCraft itself. GridCraft bundles no fonts: it uses the fonts
installed on your system.
The ArtCraft name, wordmark and logos in [`docs/brand/`](docs/brand/) are trademarks of the
ArtCraft Team and are not covered by this license. They may be used only unmodified, and only as
part of this repository and SheetCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
part of this repository and GridCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt).
Forks and modified versions must remove them.
<sub>Microsoft, Excel and Microsoft 365 are trademarks or registered trademarks of Microsoft Corporation in the United States and/or other countries. SheetCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Microsoft Corporation; these names are used only to describe the workflows it is compatible with.</sub>
<sub>Microsoft, Excel and Microsoft 365 are trademarks or registered trademarks of Microsoft Corporation in the United States and/or other countries. GridCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Microsoft Corporation; these names are used only to describe the workflows it is compatible with.</sub>
<br>

View File

@ -1,6 +1,6 @@
# SheetCraft roadmap
# GridCraft roadmap
SheetCraft aims at full Microsoft Excel parity — and to be better: faster, open (XLSX is the
GridCraft aims at full Microsoft Excel parity — and to be better: faster, open (XLSX is the
native format), fully scriptable by agents (MCP + control channel + CLI), and available on the
web.
@ -81,4 +81,4 @@ work** (≈ 9–10 days running continuously with 3–4 parallel agents), broken
- `cargo xtask parity` — catalog coverage (feature names from Excel's ribbon and menus).
- `cargo test --workspace` — engine, formula, function, XLSX and UI behaviour.
- `cargo run --release -p sheetcraft-ui-egui --example snapshot -- --sample sales out.png` — look at the UI offscreen.
- `cargo run --release -p gridcraft-ui-egui --example snapshot -- --sample sales out.png` — look at the UI offscreen.

View File

@ -1,20 +1,20 @@
[package]
name = "sheetcraft-cli"
name = "gridcraft-cli"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "SheetCraft command line: inspect, convert, evaluate, script and serve MCP without a window"
description = "GridCraft command line: inspect, convert, evaluate, script and serve MCP without a window"
publish = false
[lints]
workspace = true
[[bin]]
name = "sheetcraft-cli"
name = "gridcraft-cli"
path = "src/main.rs"
[dependencies]
sheetcraft-engine = { workspace = true }
sheetcraft-mcp = { workspace = true }
gridcraft-engine = { workspace = true }
gridcraft-mcp = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }

View File

@ -1,23 +1,23 @@
//! Headless SheetCraft.
//! Headless GridCraft.
//!
//! ```text
//! sheetcraft-cli info <file> [--json]
//! sheetcraft-cli convert <in> <out> [--sheet NAME]
//! sheetcraft-cli eval <formula> [--in FILE] [--sheet S] [--cell A1] [--json]
//! sheetcraft-cli cat <file> [--range A1:D20] [--sheet S] [--formulas] [--csv]
//! sheetcraft-cli run [--in FILE | --sample NAME] [--cmd 'id={json}']... [--script FILE.jsonl] [--out FILE] [--print RANGE] [--quiet]
//! sheetcraft-cli commands [--json] [--search X]
//! sheetcraft-cli functions [--json] [--search X] [--category C]
//! sheetcraft-cli mcp [--connect PORT] [--in FILE | --sample NAME]
//! sheetcraft-cli send <port> <method> [json]
//! sheetcraft-cli version | --version
//! gridcraft-cli info <file> [--json]
//! gridcraft-cli convert <in> <out> [--sheet NAME]
//! gridcraft-cli eval <formula> [--in FILE] [--sheet S] [--cell A1] [--json]
//! gridcraft-cli cat <file> [--range A1:D20] [--sheet S] [--formulas] [--csv]
//! gridcraft-cli run [--in FILE | --sample NAME] [--cmd 'id={json}']... [--script FILE.jsonl] [--out FILE] [--print RANGE] [--quiet]
//! gridcraft-cli commands [--json] [--search X]
//! gridcraft-cli functions [--json] [--search X] [--category C]
//! gridcraft-cli mcp [--connect PORT] [--in FILE | --sample NAME]
//! gridcraft-cli send <port> <method> [json]
//! gridcraft-cli version | --version
//! ```
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
use std::process::ExitCode;
/// `println!` that ends the program quietly when stdout is closed (`sheetcraft-cli commands |
/// `println!` that ends the program quietly when stdout is closed (`gridcraft-cli commands |
/// head`) instead of panicking on a broken pipe.
macro_rules! outln {
($($arg:tt)*) => {{
@ -28,21 +28,21 @@ macro_rules! outln {
}};
}
use gridcraft_engine::Session;
use serde_json::{Value, json};
use sheetcraft_engine::Session;
const USAGE: &str = "usage:
sheetcraft-cli info <file> [--json] sheets, used ranges, tables, charts, names
sheetcraft-cli convert <in> <out> [--sheet NAME] xlsx/csv/tsv/json/html by extension
sheetcraft-cli eval <formula> [--in FILE] [--sheet S] [--cell A1] [--json]
sheetcraft-cli cat <file> [--range A1:D20] [--sheet S] [--formulas] [--csv]
sheetcraft-cli run [--in FILE | --sample NAME] [--cmd 'id={json}']... [--script FILE.jsonl]
gridcraft-cli info <file> [--json] sheets, used ranges, tables, charts, names
gridcraft-cli convert <in> <out> [--sheet NAME] xlsx/csv/tsv/json/html by extension
gridcraft-cli eval <formula> [--in FILE] [--sheet S] [--cell A1] [--json]
gridcraft-cli cat <file> [--range A1:D20] [--sheet S] [--formulas] [--csv]
gridcraft-cli run [--in FILE | --sample NAME] [--cmd 'id={json}']... [--script FILE.jsonl]
[--out FILE] [--print RANGE] [--quiet]
sheetcraft-cli commands [--json] [--search X]
sheetcraft-cli functions [--json] [--search X] [--category C]
sheetcraft-cli mcp [--connect PORT] [--in FILE | --sample NAME]
sheetcraft-cli send <port> <method> [json]
sheetcraft-cli version";
gridcraft-cli commands [--json] [--search X]
gridcraft-cli functions [--json] [--search X] [--category C]
gridcraft-cli mcp [--connect PORT] [--in FILE | --sample NAME]
gridcraft-cli send <port> <method> [json]
gridcraft-cli version";
/// stdout went away. A reader that stopped early (a closed pipe) ends the program quietly; any
/// other write error is reported.
@ -50,7 +50,7 @@ fn stdout_failed(e: std::io::Error) -> ! {
if e.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(0);
}
eprintln!("sheetcraft-cli: can't write to stdout: {e}");
eprintln!("gridcraft-cli: can't write to stdout: {e}");
std::process::exit(1);
}
@ -59,7 +59,7 @@ fn main() -> ExitCode {
let rest = args.get(1..).unwrap_or_default();
let r = match args.first().map(String::as_str) {
Some("--version" | "-V" | "version") => {
outln!("sheetcraft-cli {}", env!("CARGO_PKG_VERSION"));
outln!("gridcraft-cli {}", env!("CARGO_PKG_VERSION"));
Ok(())
}
Some("--help" | "-h" | "help") => {
@ -81,7 +81,7 @@ fn main() -> ExitCode {
match r {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("sheetcraft-cli: {e}");
eprintln!("gridcraft-cli: {e}");
ExitCode::FAILURE
}
}
@ -258,14 +258,14 @@ fn convert(args: &[String]) -> Result<(), String> {
let input = a.pos(0, "<in>")?;
let out = a.pos(1, "<out>")?;
a.no_extra(2)?;
if sheetcraft_engine::io::FileKind::from_path(out).is_none() {
if gridcraft_engine::io::FileKind::from_path(out).is_none() {
return Err(format!("{out}: unknown output format (use .xlsx, .csv, .tsv, .json or .html)"));
}
let mut s = open(input)?;
activate_sheet(&mut s, a.opt("sheet"))?;
let d = s.doc().map_err(|e| e.to_string())?;
let bytes = sheetcraft_engine::io::save_bytes(&d.wb, out).map_err(|e| e.to_string())?;
sheetcraft_engine::io::write_file(out, &bytes).map_err(|e| e.to_string())?;
let bytes = gridcraft_engine::io::save_bytes(&d.wb, out).map_err(|e| e.to_string())?;
gridcraft_engine::io::write_file(out, &bytes).map_err(|e| e.to_string())?;
eprintln!("wrote {out} ({} bytes)", bytes.len());
Ok(())
}
@ -480,9 +480,9 @@ fn functions(args: &[String]) -> Result<(), String> {
}
/// `mcp` (headless, in-process engine) or `mcp --connect PORT|HOST:PORT` (drive a running app
/// started with `sheetcraft --control PORT`). JSON-RPC on stdin/stdout; logs on stderr.
/// started with `gridcraft --control PORT`). JSON-RPC on stdin/stdout; logs on stderr.
fn mcp(args: &[String]) -> Result<(), String> {
use sheetcraft_mcp::{Backend, Headless, Remote, Server, control_addr};
use gridcraft_mcp::{Backend, Headless, Remote, Server, control_addr};
let a = Args::parse(args, &["connect", "in", "sample"], &[])?;
a.no_extra(0)?;
let backend: Box<dyn Backend> = match a.opt("connect") {
@ -490,7 +490,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
let addr = control_addr(c);
Box::new(
Remote::connect(&addr)
.map_err(|e| format!("cannot connect to the SheetCraft app at {addr}: {e} (start it with `sheetcraft --control PORT`)"))?,
.map_err(|e| format!("cannot connect to the GridCraft app at {addr}: {e} (start it with `gridcraft --control PORT`)"))?,
)
}
None => {
@ -506,7 +506,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
Box::new(Headless::with_session(session))
}
};
eprintln!("sheetcraft-cli: MCP server on stdio ({})", backend.describe());
eprintln!("gridcraft-cli: MCP server on stdio ({})", backend.describe());
let stdin = std::io::stdin();
let stdout = std::io::stdout();
Server::new(backend).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
@ -514,7 +514,7 @@ fn mcp(args: &[String]) -> Result<(), String> {
/// `send PORT METHOD [JSON]`: one control-channel request to a running app.
fn send(args: &[String]) -> Result<(), String> {
use sheetcraft_mcp::{Backend, Remote, control_addr};
use gridcraft_mcp::{Backend, Remote, control_addr};
let port = args.first().ok_or_else(|| format!("missing <port>\n{USAGE}"))?;
let method = args.get(1).ok_or_else(|| format!("missing <method>\n{USAGE}"))?;
let params: Value = match args.get(2) {
@ -526,7 +526,7 @@ fn send(args: &[String]) -> Result<(), String> {
}
let addr = control_addr(port);
let mut r = Remote::connect(&addr)
.map_err(|e| format!("cannot connect to the SheetCraft app at {addr}: {e} (start it with `sheetcraft --control {port}`)"))?;
.map_err(|e| format!("cannot connect to the GridCraft app at {addr}: {e} (start it with `gridcraft --control {port}`)"))?;
let v = r.call(method, params)?;
outln!("{}", pretty(&v));
Ok(())

View File

@ -5,29 +5,29 @@ use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
fn bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_sheetcraft-cli"))
Command::new(env!("CARGO_BIN_EXE_gridcraft-cli"))
}
fn run(args: &[&str]) -> Output {
bin().args(args).output().expect("run sheetcraft-cli")
bin().args(args).output().expect("run gridcraft-cli")
}
fn ok(args: &[&str]) -> String {
let o = run(args);
assert!(o.status.success(), "sheetcraft-cli {args:?} failed: {}", String::from_utf8_lossy(&o.stderr));
assert!(o.status.success(), "gridcraft-cli {args:?} failed: {}", String::from_utf8_lossy(&o.stderr));
String::from_utf8(o.stdout).unwrap()
}
fn tmpdir(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("sheetcraft-cli-test-{}-{name}", std::process::id()));
let d = std::env::temp_dir().join(format!("gridcraft-cli-test-{}-{name}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn version_and_usage() {
assert_eq!(ok(&["--version"]).trim(), format!("sheetcraft-cli {}", env!("CARGO_PKG_VERSION")));
assert_eq!(ok(&["version"]).trim(), format!("sheetcraft-cli {}", env!("CARGO_PKG_VERSION")));
assert_eq!(ok(&["--version"]).trim(), format!("gridcraft-cli {}", env!("CARGO_PKG_VERSION")));
assert_eq!(ok(&["version"]).trim(), format!("gridcraft-cli {}", env!("CARGO_PKG_VERSION")));
let o = run(&["frobnicate"]);
assert!(!o.status.success());
assert!(String::from_utf8_lossy(&o.stderr).contains("unknown subcommand"));
@ -147,6 +147,6 @@ fn mcp_over_stdio() {
let text = String::from_utf8(out.stdout).unwrap();
let lines: Vec<serde_json::Value> = text.lines().map(|l| serde_json::from_str(l).unwrap()).collect();
assert_eq!(lines.len(), 2, "{text}");
assert_eq!(lines[0]["result"]["serverInfo"]["name"], "sheetcraft");
assert_eq!(lines[0]["result"]["serverInfo"]["name"], "gridcraft");
assert!(lines[1]["result"]["content"][0]["text"].as_str().unwrap().contains("42"));
}

View File

@ -1,6 +1,6 @@
[package]
name = "sheetcraft-web"
description = "SheetCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
name = "gridcraft-web"
description = "GridCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
version.workspace = true
edition.workspace = true
license.workspace = true
@ -10,12 +10,12 @@ publish = false
workspace = true
[[bin]]
name = "sheetcraft-web"
name = "gridcraft-web"
path = "src/main.rs"
[dependencies]
sheetcraft-engine = { workspace = true }
sheetcraft-ui-egui = { workspace = true }
gridcraft-engine = { workspace = true }
gridcraft-ui-egui = { workspace = true }
egui = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }

View File

@ -1,4 +1,4 @@
# Build with: trunk build --release (from apps/sheetcraft-web)
# Build with: trunk build --release (from apps/gridcraft-web)
[build]
target = "index.html"
dist = "../../dist/web"

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>GridCraft</title>
<link data-trunk rel="rust" data-bin="gridcraft-web" data-wasm-opt="z" data-weak-refs />
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #f3f3f3; }
#gridcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#gridcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #555; font: 14px system-ui, sans-serif; pointer-events: none; }
</style>
</head>
<body>
<canvas id="gridcraft_canvas"></canvas>
<div id="gridcraft_loading">Loading GridCraft&hellip;</div>
</body>
</html>

View File

@ -1,6 +1,6 @@
//! SheetCraft in the browser.
//! GridCraft in the browser.
//!
//! Runs the same [`sheetcraft_ui_egui::SheetApp`] as the desktop app through eframe's web runner
//! Runs the same [`gridcraft_ui_egui::SheetApp`] as the desktop app through eframe's web runner
//! (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` from
//! this directory (output in `dist/web`).
//!
@ -21,5 +21,5 @@ fn main() {
#[cfg(not(target_arch = "wasm32"))]
fn main() {
eprintln!("sheetcraft-web only runs in the browser: build it with `trunk build --release` in apps/sheetcraft-web");
eprintln!("gridcraft-web only runs in the browser: build it with `trunk build --release` in apps/gridcraft-web");
}

View File

@ -1,11 +1,11 @@
//! The browser shell: web `Services`, drag-and-drop, and the eframe web runner.
use sheetcraft_engine::Session;
use sheetcraft_ui_egui::{Inbox, Services, SheetApp};
use gridcraft_engine::Session;
use gridcraft_ui_egui::{Inbox, Services, SheetApp};
use wasm_bindgen::JsCast as _;
const CANVAS_ID: &str = "sheetcraft_canvas";
const LOADING_ID: &str = "sheetcraft_loading";
const CANVAS_ID: &str = "gridcraft_canvas";
const LOADING_ID: &str = "gridcraft_loading";
pub fn start() {
eframe::WebLogger::init(log::LevelFilter::Info).ok();
@ -46,7 +46,7 @@ pub fn start() {
if let Some(el) = document.get_element_by_id(LOADING_ID) {
match result {
Ok(()) => el.remove(),
Err(e) => el.set_inner_html(&format!("<p>SheetCraft failed to start: {e:?}</p><p>A browser with WebGPU or WebGL2 is required.</p>")),
Err(e) => el.set_inner_html(&format!("<p>GridCraft failed to start: {e:?}</p><p>A browser with WebGPU or WebGL2 is required.</p>")),
}
}
});

View File

@ -1,18 +1,18 @@
[package]
name = "sheetcraft"
name = "gridcraft"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
default-run = "sheetcraft"
description = "SheetCraft desktop app"
default-run = "gridcraft"
description = "GridCraft desktop app"
[lints]
workspace = true
[dependencies]
sheetcraft-engine = { workspace = true }
sheetcraft-ui-egui = { workspace = true }
gridcraft-engine = { workspace = true }
gridcraft-ui-egui = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] }
egui = { workspace = true }
serde_json = { workspace = true }
@ -20,7 +20,7 @@ log = { workspace = true }
image = { workspace = true }
[build-dependencies]
# Icon + VERSIONINFO for sheetcraft.exe; build.rs only uses it when the target is Windows.
# Icon + VERSIONINFO for gridcraft.exe; build.rs only uses it when the target is Windows.
winresource = "0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

30
apps/gridcraft/build.rs Normal file
View File

@ -0,0 +1,30 @@
//! Windows only: embed the app icon and version info (VERSIONINFO) into `gridcraft.exe`, so it
//! shows in Explorer, the taskbar, the Start menu and Alt-Tab.
//!
//! On every other target this does nothing. A missing resource compiler is a warning, so a
//! cross-compile from macOS or Linux still links, unless `GRIDCRAFT_REQUIRE_WINRES=1` turns it
//! into an error (for release builds).
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=../../assets/app-icon/gridcraft.ico");
println!("cargo:rerun-if-env-changed=GRIDCRAFT_REQUIRE_WINRES");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
let mut res = winresource::WindowsResource::new();
res.set_icon("../../assets/app-icon/gridcraft.ico")
.set("ProductName", "GridCraft")
.set("FileDescription", "GridCraft spreadsheet")
.set("CompanyName", "Learning Machines LLC")
.set("LegalCopyright", "Copyright (c) the GridCraft authors. MIT OR Apache-2.0.")
.set("OriginalFilename", "gridcraft.exe")
.set("InternalName", "gridcraft");
if let Err(e) = res.compile() {
if std::env::var_os("GRIDCRAFT_REQUIRE_WINRES").is_some() {
eprintln!("embedding Windows resources failed: {e}");
std::process::exit(1);
}
println!("cargo:warning=gridcraft.exe built without icon/version resources: {e}");
}
}

View File

@ -6,8 +6,8 @@ use std::net::{TcpListener, TcpStream};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration;
use gridcraft_ui_egui::ControlRequest;
use serde_json::{Value as Json, json};
use sheetcraft_ui_egui::ControlRequest;
const MAX_LINE: usize = 64 * 1024 * 1024;
@ -21,7 +21,7 @@ pub fn start(port: u16, ctx: egui::Context) -> std::io::Result<Receiver<ControlR
let _ = std::thread::Builder::new().name("control-conn".into()).spawn(move || serve(stream, tx, ctx));
}
})?;
eprintln!("SheetCraft control channel listening on 127.0.0.1:{port}");
eprintln!("GridCraft control channel listening on 127.0.0.1:{port}");
Ok(rx)
}

View File

@ -1,10 +1,10 @@
//! SheetCraft desktop app.
//! GridCraft desktop app.
//!
//! Usage: `sheetcraft [--control <port>] [--sample <name>] [files…]`
//! Usage: `gridcraft [--control <port>] [--sample <name>] [files…]`
//!
//! `--control <port>` (or `SHEETCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control
//! `--control <port>` (or `GRIDCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control
//! server: `{"id":1,"method":"ui.inspect","params":{}}` → `{"id":1,"ok":true,"result":…}`.
//! See `sheetcraft_ui_egui::control` and `docs/control-protocol.md` for the methods.
//! See `gridcraft_ui_egui::control` and `docs/control-protocol.md` for the methods.
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
@ -12,9 +12,9 @@ mod control_server;
#[cfg(target_os = "macos")]
mod native_menu;
use gridcraft_engine::Session;
use gridcraft_ui_egui::{Services, SheetApp};
use serde_json::json;
use sheetcraft_engine::Session;
use sheetcraft_ui_egui::{Services, SheetApp};
struct App(SheetApp, #[cfg(target_os = "macos")] Option<native_menu::NativeMenu>);
@ -22,7 +22,7 @@ impl eframe::App for App {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
#[cfg(target_os = "macos")]
{
if self.1.is_none() && std::env::var_os("SHEETCRAFT_NO_NATIVE_MENU").is_none() {
if self.1.is_none() && std::env::var_os("GRIDCRAFT_NO_NATIVE_MENU").is_none() {
self.1 = Some(native_menu::NativeMenu::install(ctx));
}
if let Some(m) = &self.1 {
@ -50,19 +50,19 @@ impl eframe::App for App {
fn config_dir() -> Option<std::path::PathBuf> {
if cfg!(target_os = "macos") {
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/SheetCraft"))
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/GridCraft"))
} else if cfg!(windows) {
std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("SheetCraft"))
std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("GridCraft"))
} else {
std::env::var_os("XDG_CONFIG_HOME")
.map(std::path::PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config")))
.map(|c| c.join("sheetcraft"))
.map(|c| c.join("gridcraft"))
}
}
fn load_prefs(app: &mut SheetApp) {
if std::env::var_os("SHEETCRAFT_NO_PREFS").is_some() {
if std::env::var_os("GRIDCRAFT_NO_PREFS").is_some() {
return;
}
let Some(dir) = config_dir() else { return };
@ -79,7 +79,7 @@ fn load_prefs(app: &mut SheetApp) {
}
fn save_prefs(app: &SheetApp) {
if std::env::var_os("SHEETCRAFT_NO_PREFS").is_some() {
if std::env::var_os("GRIDCRAFT_NO_PREFS").is_some() {
return;
}
let Some(dir) = config_dir() else { return };
@ -127,7 +127,7 @@ fn services() -> Services {
}
fn icon() -> Option<egui::IconData> {
let bytes = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.sheetcraft.png");
let bytes = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.gridcraft.png");
let img = image::load_from_memory(bytes).ok()?.to_rgba8();
Some(egui::IconData { width: img.width(), height: img.height(), rgba: img.into_raw() })
}
@ -135,10 +135,10 @@ fn icon() -> Option<egui::IconData> {
fn main() -> eframe::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("SheetCraft {}", env!("CARGO_PKG_VERSION"));
println!("GridCraft {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
let mut control_port: Option<u16> = std::env::var("SHEETCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
let mut control_port: Option<u16> = std::env::var("GRIDCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
let mut sample: Option<String> = None;
let mut files: Vec<String> = Vec::new();
let mut i = 0;
@ -175,10 +175,10 @@ fn main() -> eframe::Result<()> {
let control_port = control_port;
load_prefs(&mut app);
let mut viewport = egui::ViewportBuilder::default()
.with_title("SheetCraft")
.with_title("GridCraft")
.with_inner_size([1440.0, 900.0])
.with_min_inner_size([640.0, 420.0])
.with_app_id("ai.storyteller.sheetcraft")
.with_app_id("ai.storyteller.gridcraft")
.with_drag_and_drop(true);
if cfg!(target_os = "macos") {
viewport = viewport.with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false);
@ -188,7 +188,7 @@ fn main() -> eframe::Result<()> {
}
let options = eframe::NativeOptions { viewport, ..Default::default() };
eframe::run_native(
"SheetCraft",
"GridCraft",
options,
Box::new(move |cc| {
SheetApp::setup_context(&cc.egui_ctx, app.ui.dark);

View File

@ -4,10 +4,10 @@
use std::str::FromStr;
use std::sync::mpsc::{Receiver, channel};
use gridcraft_ui_egui::SheetApp;
use muda::accelerator::Accelerator;
use muda::{Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
use serde_json::{Value, json};
use sheetcraft_ui_egui::SheetApp;
type Entry = (&'static str, &'static str, Option<&'static str>);
@ -170,7 +170,7 @@ fn tree() -> Vec<(&'static str, Vec<Entry>)> {
"Help",
vec![
("Agent Control (MCP)…", "dialog:agents", None),
("SheetCraft on getartcraft.com", "url:https://getartcraft.com/apps/sheetcraft", None),
("GridCraft on getartcraft.com", "url:https://getartcraft.com/apps/gridcraft", None),
("Join the ArtCraft Discord…", "url:https://discord.gg/artcraft", None),
],
),
@ -185,8 +185,8 @@ pub struct NativeMenu {
impl NativeMenu {
pub fn install(ctx: &egui::Context) -> NativeMenu {
let menu = Menu::new();
let app_menu = Submenu::new("SheetCraft", true);
let about = MenuItem::with_id("url:https://getartcraft.com/apps/sheetcraft", "About SheetCraft", true, None);
let app_menu = Submenu::new("GridCraft", true);
let about = MenuItem::with_id("url:https://getartcraft.com/apps/gridcraft", "About GridCraft", true, None);
let _ = app_menu.append_items(&[
&about,
&PredefinedMenuItem::separator(),

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>SheetCraft</title>
<link data-trunk rel="rust" data-bin="sheetcraft-web" data-wasm-opt="z" data-weak-refs />
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #f3f3f3; }
#sheetcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#sheetcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #555; font: 14px system-ui, sans-serif; pointer-events: none; }
</style>
</head>
<body>
<canvas id="sheetcraft_canvas"></canvas>
<div id="sheetcraft_loading">Loading SheetCraft&hellip;</div>
</body>
</html>

View File

@ -1,30 +0,0 @@
//! Windows only: embed the app icon and version info (VERSIONINFO) into `sheetcraft.exe`, so it
//! shows in Explorer, the taskbar, the Start menu and Alt-Tab.
//!
//! On every other target this does nothing. A missing resource compiler is a warning, so a
//! cross-compile from macOS or Linux still links, unless `SHEETCRAFT_REQUIRE_WINRES=1` turns it
//! into an error (for release builds).
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=../../assets/app-icon/sheetcraft.ico");
println!("cargo:rerun-if-env-changed=SHEETCRAFT_REQUIRE_WINRES");
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
let mut res = winresource::WindowsResource::new();
res.set_icon("../../assets/app-icon/sheetcraft.ico")
.set("ProductName", "SheetCraft")
.set("FileDescription", "SheetCraft spreadsheet")
.set("CompanyName", "Learning Machines LLC")
.set("LegalCopyright", "Copyright (c) the SheetCraft authors. MIT OR Apache-2.0.")
.set("OriginalFilename", "sheetcraft.exe")
.set("InternalName", "sheetcraft");
if let Err(e) = res.compile() {
if std::env::var_os("SHEETCRAFT_REQUIRE_WINRES").is_some() {
eprintln!("embedding Windows resources failed: {e}");
std::process::exit(1);
}
println!("cargo:warning=sheetcraft.exe built without icon/version resources: {e}");
}
}

View File

@ -1,8 +1,8 @@
SheetCraft app icon (sheetcraft.svg and every file derived from it in this directory:
sheetcraft-1024.png, sheetcraft-macos-512.png, sheetcraft.icns, sheetcraft.ico, hicolor/).
GridCraft app icon (gridcraft.svg and every file derived from it in this directory:
gridcraft-1024.png, gridcraft-macos-512.png, gridcraft.icns, gridcraft.ico, hicolor/).
PLACEHOLDER artwork: original work written for this project by SheetCraft contributors (simple
hand-written SVG geometry). No third-party material. Licensed like the rest of SheetCraft, under
PLACEHOLDER artwork: original work written for this project by GridCraft contributors (simple
hand-written SVG geometry). No third-party material. Licensed like the rest of GridCraft, under
either of
* Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)

View File

@ -1,13 +1,13 @@
# SheetCraft app icon (PLACEHOLDER)
# GridCraft app icon (PLACEHOLDER)
**This icon is temporary.** Every Crafting App's icon is an engraved creature portrait that the
product owner makes (craftrules `standards/icon-design.md`); SheetCraft's hasn't been made yet.
product owner makes (craftrules `standards/icon-design.md`); GridCraft's hasn't been made yet.
Until it is, this placeholder stands in: a simple ledger page with a header row, a row-number
column, a selected cell with its fill handle and a double rule under a total, on a full-bleed
SheetCraft-green tile. It is original geometry written by hand for this repository, deliberately
GridCraft-green tile. It is original geometry written by hand for this repository, deliberately
not modelled on any vendor's spreadsheet logo (no letters, no "X", no overlapping panels).
When the owner's art arrives, replace `sheetcraft.svg`, run `packaging/icons.sh`, update this
When the owner's art arrives, replace `gridcraft.svg`, run `packaging/icons.sh`, update this
README (provenance, creature) and the rows in `ATTRIBUTION.md`, and drop the word placeholder.
## Palette
@ -18,35 +18,35 @@ Exactly three colours:
|---|---|---|
| Ink | `#0b0b0c` | Line work and contour |
| Paper | `#efe9dc` | The page |
| SheetCraft green (app colour, **proposed**) | `#1f9d55` | The full-bleed field and the header cells |
| GridCraft green (app colour, **proposed**) | `#1f9d55` | The full-bleed field and the header cells |
The app colour is a proposal pending the owner's approval: SheetCraft green `#1f9d55`, with ink
The app colour is a proposal pending the owner's approval: GridCraft green `#1f9d55`, with ink
(darker) variant `#147a40` for text and accents on light backgrounds. It differs from
DesignCraft's `#7bb51c`.
## Geometry
A 512 × 512 viewBox, clipped to a rounded square with `rx=112`. Windows and Linux use the full
tile. The macOS icons (`.icns`, `sheetcraft-macos-512.png`) sit on Apple's grid: an 824/1024
tile. The macOS icons (`.icns`, `gridcraft-macos-512.png`) sit on Apple's grid: an 824/1024
body with a transparent margin.
## Files
| File | Use |
|---|---|
| `sheetcraft.svg` | Canonical artwork; also `hicolor/scalable` |
| `sheetcraft-1024.png` | 1024 px render of the full tile |
| `sheetcraft-macos-512.png` | Runtime Dock icon on macOS (for `apps/sheetcraft` to embed) |
| `sheetcraft.icns` | macOS bundle icon (`CFBundleIconFile`) |
| `sheetcraft.ico` | Windows exe icon, 16–256 px (embedded by `apps/sheetcraft/build.rs`) |
| `hicolor/<size>/apps/ai.storyteller.sheetcraft.png` | Linux icon theme, 16–512 px; the 256 px one is also the runtime window icon on Windows and Linux |
| `hicolor/scalable/apps/ai.storyteller.sheetcraft.svg` | Linux scalable icon |
| `gridcraft.svg` | Canonical artwork; also `hicolor/scalable` |
| `gridcraft-1024.png` | 1024 px render of the full tile |
| `gridcraft-macos-512.png` | Runtime Dock icon on macOS (for `apps/gridcraft` to embed) |
| `gridcraft.icns` | macOS bundle icon (`CFBundleIconFile`) |
| `gridcraft.ico` | Windows exe icon, 16–256 px (embedded by `apps/gridcraft/build.rs`) |
| `hicolor/<size>/apps/ai.storyteller.gridcraft.png` | Linux icon theme, 16–512 px; the 256 px one is also the runtime window icon on Windows and Linux |
| `hicolor/scalable/apps/ai.storyteller.gridcraft.svg` | Linux scalable icon |
| `LICENSE.txt` | MIT OR Apache-2.0 |
The app ID is `ai.storyteller.sheetcraft`. It's used for the hicolor icon names, the Wayland app
ID and `packaging/linux/ai.storyteller.sheetcraft.desktop`.
The app ID is `ai.storyteller.gridcraft`. It's used for the hicolor icon names, the Wayland app
ID and `packaging/linux/ai.storyteller.gridcraft.desktop`.
## Regenerate
Edit `sheetcraft.svg`, then run `packaging/icons.sh`. It needs `resvg`, and `iconutil` for the
Edit `gridcraft.svg`, then run `packaging/icons.sh`. It needs `resvg`, and `iconutil` for the
`.icns` (macOS only). It packs the `.ico` with `cargo xtask ico`.

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<!-- SheetCraft PLACEHOLDER icon: a ledger page with a selected cell, on SheetCraft green.
<!-- GridCraft PLACEHOLDER icon: a ledger page with a selected cell, on GridCraft green.
Original work; temporary until the owner's engraved creature portrait replaces it
(craftrules standards/icon-design.md). Palette: ink #0b0b0c, paper #efe9dc,
app colour #1f9d55 (proposed). -->

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 704 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<!-- SheetCraft PLACEHOLDER icon: a ledger page with a selected cell, on SheetCraft green.
<!-- GridCraft PLACEHOLDER icon: a ledger page with a selected cell, on GridCraft green.
Original work; temporary until the owner's engraved creature portrait replaces it
(craftrules standards/icon-design.md). Palette: ink #0b0b0c, paper #efe9dc,
app colour #1f9d55 (proposed). -->

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,17 +1,17 @@
[package]
name = "sheetcraft-calc"
name = "gridcraft-calc"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "SheetCraft calculation engine: dependency graph, recalculation, evaluator, dynamic arrays"
description = "GridCraft calculation engine: dependency graph, recalculation, evaluator, dynamic arrays"
[lints]
workspace = true
[dependencies]
sheetcraft-core = { workspace = true }
sheetcraft-formula = { workspace = true }
sheetcraft-functions = { workspace = true }
sheetcraft-numfmt = { workspace = true }
sheetcraft-model = { workspace = true }
gridcraft-core = { workspace = true }
gridcraft-formula = { workspace = true }
gridcraft-functions = { workspace = true }
gridcraft-numfmt = { workspace = true }
gridcraft-model = { workspace = true }
log = { workspace = true }

View File

@ -6,10 +6,10 @@
use std::sync::Arc;
use sheetcraft_core::{Array, CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value, compare};
use sheetcraft_formula::{BinOp, Expr, RefKind, Reference, SheetSel, StructItem, StructRef, UnOp};
use sheetcraft_functions::{Arg, Ctx};
use sheetcraft_model::Workbook;
use gridcraft_core::{Array, CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value, compare};
use gridcraft_formula::{BinOp, Expr, RefKind, Reference, SheetSel, StructItem, StructRef, UnOp};
use gridcraft_functions::{Arg, Ctx};
use gridcraft_model::Workbook;
/// Largest array a reference may expand to.
pub const MAX_CELLS: u64 = 16_000_000;
@ -73,7 +73,7 @@ struct FnCtx<'a, 'h> {
}
impl Ctx for FnCtx<'_, '_> {
fn date_system(&self) -> sheetcraft_core::DateSystem {
fn date_system(&self) -> gridcraft_core::DateSystem {
self.ev.host.workbook().date_system
}
fn now_serial(&self) -> f64 {
@ -224,7 +224,7 @@ impl<'h> Evaluator<'h> {
if let Some(def) = wb.name(bare, scope_sheet) {
let text = def.formula.clone();
let scope = def.scope.unwrap_or(self.sheet);
return match sheetcraft_formula::parse(&text) {
return match gridcraft_formula::parse(&text) {
Ok(expr) => {
let saved = self.sheet;
// Unqualified references in a name refer to the scope sheet.
@ -649,7 +649,7 @@ impl<'h> Evaluator<'h> {
Ok(c) => c,
Err(e) => return Value::Error(e),
};
match sheetcraft_numfmt::text_function(v, &code, sys) {
match gridcraft_numfmt::text_function(v, &code, sys) {
Ok(s) => Value::text(s),
Err(e) => Value::Error(e),
}
@ -685,7 +685,7 @@ impl<'h> Evaluator<'h> {
}
fn builtin(&mut self, name: &str, args: &[Expr]) -> Ev {
let Some(spec) = sheetcraft_functions::lookup(name) else {
let Some(spec) = gridcraft_functions::lookup(name) else {
// A defined name holding a LAMBDA, or a LET-bound lambda.
if let Ev::L(l) = self.name(name) {
return self.apply_lambda(&l, args);
@ -716,7 +716,7 @@ impl<'h> Evaluator<'h> {
evaluated.push(Arg { value, from_ref });
}
let mut ctx = FnCtx { ev: self };
sheetcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V)
gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V)
}
pub fn apply_lambda(&mut self, l: &Lambda, args: &[Expr]) -> Ev {
@ -942,7 +942,7 @@ impl<'h> Evaluator<'h> {
}
};
let text = if a1 { text } else { r1c1_to_a1(&text, self.at).unwrap_or(text) };
match sheetcraft_formula::parse(&text) {
match gridcraft_formula::parse(&text) {
Ok(e @ (Expr::Ref(_) | Expr::Name(_) | Expr::Struct(_))) => match self.eval(&e) {
r @ Ev::R(_) => r,
_ => err(CellError::Ref),
@ -1024,7 +1024,7 @@ impl<'h> Evaluator<'h> {
let wb = self.host.workbook();
let sheet = wb.sheet(a.sheet);
match kind.as_str() {
"address" => Ev::V(Value::text(format!("${}${}", sheetcraft_core::col_to_letters(c.col), c.row + 1))),
"address" => Ev::V(Value::text(format!("${}${}", gridcraft_core::col_to_letters(c.col), c.row + 1))),
"row" => Ev::V(Value::number(c.row as f64 + 1.0)),
"col" => Ev::V(Value::number(c.col as f64 + 1.0)),
"contents" => Ev::V(self.host.cell_value(a.sheet, c)),
@ -1115,9 +1115,9 @@ impl<'h> Evaluator<'h> {
}
if let Some(k) = k {
values.push(Value::Empty);
return Ev::V(sheetcraft_functions::aggregate_values_k(func, &values[..values.len() - 1], &k));
return Ev::V(gridcraft_functions::aggregate_values_k(func, &values[..values.len() - 1], &k));
}
Ev::V(sheetcraft_functions::aggregate_values(func, &values))
Ev::V(gridcraft_functions::aggregate_values(func, &values))
}
}
@ -1187,7 +1187,7 @@ pub fn r1c1_to_a1(s: &str, at: CellRef) -> Option<String> {
};
let (r, ra) = num(rpart, at.row)?;
let (c, ca) = num(cpart, at.col)?;
Some(format!("{}{}{}{}", if ca { "$" } else { "" }, sheetcraft_core::col_to_letters(c), if ra { "$" } else { "" }, r + 1))
Some(format!("{}{}{}{}", if ca { "$" } else { "" }, gridcraft_core::col_to_letters(c), if ra { "$" } else { "" }, r + 1))
};
let out = match body.split_once(':') {
Some((a, b)) => format!("{}:{}", conv(a)?, conv(b)?),
@ -1336,7 +1336,7 @@ pub fn precedents(wb: &Workbook, sheet: usize, e: &Expr) -> (Vec<Area>, bool) {
dynamic = true
}
Expr::Name(_) | Expr::Struct(_) => dynamic = true,
Expr::Call(n, _) if sheetcraft_functions::lookup(n).is_none() && !is_special(n) => dynamic = true,
Expr::Call(n, _) if gridcraft_functions::lookup(n).is_none() && !is_special(n) => dynamic = true,
_ => {}
});
(out, dynamic)
@ -1416,7 +1416,7 @@ pub const SPECIAL_FUNCTIONS: &[&str] = &[
];
pub fn is_known_function(n: &str) -> bool {
is_special(&n.to_ascii_uppercase()) || sheetcraft_functions::lookup(n).is_some()
is_special(&n.to_ascii_uppercase()) || gridcraft_functions::lookup(n).is_some()
}
#[allow(dead_code)]

View File

@ -1,4 +1,4 @@
//! SheetCraft's calculation engine: the evaluator (references, names, tables, LET/LAMBDA,
//! GridCraft's calculation engine: the evaluator (references, names, tables, LET/LAMBDA,
//! dynamic arrays) and dependency-driven recalculation with spills and cycle detection.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]

View File

@ -37,9 +37,9 @@ impl Hasher for FxHasher {
type HashMap<K, V> = std::collections::HashMap<K, V, BuildHasherDefault<FxHasher>>;
type HashSet<K> = std::collections::HashSet<K, BuildHasherDefault<FxHasher>>;
use sheetcraft_core::{Array, CellError, CellRef, RangeRef, Value};
use sheetcraft_formula::Expr;
use sheetcraft_model::{CalcMode, Workbook};
use gridcraft_core::{Array, CellError, CellRef, RangeRef, Value};
use gridcraft_formula::Expr;
use gridcraft_model::{CalcMode, Workbook};
use crate::eval::{Area, Evaluator, Host, precedents};
@ -403,7 +403,7 @@ impl Calc {
let t1 = prof_now();
let n = dirty.len();
self.run(wb, dirty, false);
if std::env::var_os("SHEETCRAFT_PROFILE").is_some() {
if std::env::var_os("GRIDCRAFT_PROFILE").is_some() {
eprintln!("recalc: {} seeds, closure {} cells {:.1} ms, run {:.1} ms", seeds.len(), n, t1 - t0, prof_now() - t1);
}
}
@ -478,7 +478,7 @@ impl Calc {
(host.results, host.spills, host.cycles)
};
self.rng = rng;
if std::env::var_os("SHEETCRAFT_PROFILE").is_some() {
if std::env::var_os("GRIDCRAFT_PROFILE").is_some() {
eprintln!(" eval {} formulas {:.1} ms", results.len(), prof_now() - tp);
}
let tw = prof_now();
@ -511,8 +511,8 @@ impl Calc {
if let Some(arr) = new {
let end = CellRef::new(k.1.row.saturating_add(arr.rows as u32 - 1), k.1.col.saturating_add(arr.cols as u32 - 1));
let range = RangeRef::new(k.1, end);
let blocked = end.row >= sheetcraft_core::MAX_ROWS
|| end.col >= sheetcraft_core::MAX_COLS
let blocked = end.row >= gridcraft_core::MAX_ROWS
|| end.col >= gridcraft_core::MAX_COLS
|| range.iter().any(|c| c != k.1 && (sheet.cells.has(c) || sheet.spill.contains_key(&c)))
|| sheet.merges.iter().any(|m| m.intersects(&range));
if blocked {
@ -538,7 +538,7 @@ impl Calc {
if full {
// Everything was already evaluated once; spill dependents need another pass.
}
if std::env::var_os("SHEETCRAFT_PROFILE").is_some() {
if std::env::var_os("GRIDCRAFT_PROFILE").is_some() {
eprintln!(" write-back {:.1} ms", prof_now() - tw);
}
spill_changes.sort_by_key(|(s, c)| (*s, c.row, c.col));
@ -568,7 +568,7 @@ pub fn now_serial() -> f64 {
/// validation, Evaluate Formula, Name Manager previews, the CLI `eval`).
pub fn evaluate(wb: &Workbook, sheet: usize, at: CellRef, formula: &str) -> Value {
let body = formula.strip_prefix('=').unwrap_or(formula);
let Ok(expr) = sheetcraft_formula::parse(body) else { return Value::Error(CellError::Name) };
let Ok(expr) = gridcraft_formula::parse(body) else { return Value::Error(CellError::Name) };
evaluate_expr(wb, sheet, at, &expr)
}

View File

@ -1,5 +1,5 @@
use sheetcraft_core::{CellError, CellRef, Value};
use sheetcraft_model::{Cell, Formula, Workbook};
use gridcraft_core::{CellError, CellRef, Value};
use gridcraft_model::{Cell, Formula, Workbook};
use crate::Calc;
@ -20,7 +20,7 @@ impl T {
let cell = if input.starts_with('=') {
Cell::formula(Formula::new(input))
} else {
Cell::value(sheetcraft_core::parse::parse_input(input, self.wb.date_system).value)
Cell::value(gridcraft_core::parse::parse_input(input, self.wb.date_system).value)
};
self.wb.sheet_mut(0).unwrap().cells.set(c(at), cell);
self.calc.cells_changed(&mut self.wb, &[(0, c(at))]);
@ -174,8 +174,8 @@ fn let_lambda() {
#[test]
fn names_and_sheets() {
let mut t = T::new();
t.wb.names.push(sheetcraft_model::DefinedName { name: "Rate".into(), scope: None, formula: "0.5".into(), comment: String::new(), hidden: false });
t.wb.sheets.push(std::sync::Arc::new(sheetcraft_model::Sheet::new("Data")));
t.wb.names.push(gridcraft_model::DefinedName { name: "Rate".into(), scope: None, formula: "0.5".into(), comment: String::new(), hidden: false });
t.wb.sheets.push(std::sync::Arc::new(gridcraft_model::Sheet::new("Data")));
t.wb.sheet_mut(1).unwrap().set_value(c("A1"), Value::Number(8.0));
t.calc.rebuild(&t.wb);
t.set("A1", "=Rate*Data!A1");
@ -198,7 +198,7 @@ fn subtotal_skips_hidden() {
t.set("A1", "1");
t.set("A2", "2");
t.set("A3", "3");
t.wb.sheet_mut(0).unwrap().rows.insert(1, sheetcraft_model::LineInfo { hidden: true, ..Default::default() });
t.wb.sheet_mut(0).unwrap().rows.insert(1, gridcraft_model::LineInfo { hidden: true, ..Default::default() });
t.set("B1", "=SUBTOTAL(109,A1:A3)");
t.set("B2", "=SUBTOTAL(9,A1:A3)");
assert_eq!(t.num("B1"), 4.0);

View File

@ -1,16 +1,16 @@
[package]
name = "sheetcraft-chart"
name = "gridcraft-chart"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "SheetCraft charts: data resolution, layout into toolkit-free primitives, sparklines and a small rasteriser"
description = "GridCraft charts: data resolution, layout into toolkit-free primitives, sparklines and a small rasteriser"
[lints]
workspace = true
[dependencies]
sheetcraft-core = { workspace = true }
sheetcraft-model = { workspace = true }
sheetcraft-calc = { workspace = true }
sheetcraft-numfmt = { workspace = true }
gridcraft-core = { workspace = true }
gridcraft-model = { workspace = true }
gridcraft-calc = { workspace = true }
gridcraft-numfmt = { workspace = true }
serde = { workspace = true }

View File

@ -1,7 +1,7 @@
//! Value-axis scaling ("nice numbers") and label formatting.
use sheetcraft_core::{DateSystem, Value, number_to_text};
use sheetcraft_numfmt::{NumberFormat, format_value};
use gridcraft_core::{DateSystem, Value, number_to_text};
use gridcraft_numfmt::{NumberFormat, format_value};
/// Largest magnitude we plot; bigger values are clamped so ranges stay finite.
pub(crate) const MAX_ABS: f64 = 1e300;

View File

@ -1,4 +1,4 @@
//! SheetCraft charts (layer L3, toolkit-free).
//! GridCraft charts (layer L3, toolkit-free).
//!
//! - [`resolve`] pulls series names, categories and values out of a workbook,
//! - [`render`] lays a chart out into a list of drawing primitives ([`Prim`]) that any UI
@ -6,7 +6,7 @@
//! - [`render_sparkline`] draws in-cell sparklines,
//! - [`rasterize`] turns primitives into RGBA pixels for PNG export and tests.
//!
//! The look (palette, greys, spacing) is SheetCraft's own design.
//! The look (palette, greys, spacing) is GridCraft's own design.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
@ -19,8 +19,8 @@ mod sparkline;
#[cfg(test)]
mod tests;
pub use gridcraft_model::{Chart, ChartKind, LegendPos, SparklineKind};
use serde::{Deserialize, Serialize};
pub use sheetcraft_model::{Chart, ChartKind, LegendPos, SparklineKind};
pub use axis::{Scale, format_number, nice_scale};
pub use raster::rasterize;

View File

@ -3,7 +3,7 @@
use std::collections::HashSet;
use std::f32::consts::{PI, TAU};
use sheetcraft_model::{Chart, ChartKind, LegendPos, Theme};
use gridcraft_model::{Chart, ChartKind, LegendPos, Theme};
use crate::axis::{Fmt, Scale, clean, fixed_scale, nice_scale};
use crate::resolve::{MAX_POINTS, MAX_SERIES, series_color};

View File

@ -1,9 +1,9 @@
//! Pulls chart data (names, categories, values, formats, colours) out of a workbook.
use sheetcraft_calc::evaluate;
use sheetcraft_core::{CellRef, RangeRef, Value, number_to_text};
use sheetcraft_model::{Chart, ChartKind, Theme, Workbook, style::apply_tint};
use sheetcraft_numfmt::{NumberFormat, format_value};
use gridcraft_calc::evaluate;
use gridcraft_core::{CellRef, RangeRef, Value, number_to_text};
use gridcraft_model::{Chart, ChartKind, Theme, Workbook, style::apply_tint};
use gridcraft_numfmt::{NumberFormat, format_value};
use crate::{ChartData, Rgba, SeriesData};

View File

@ -1,6 +1,6 @@
//! In-cell sparklines.
use sheetcraft_model::SparklineKind;
use gridcraft_model::SparklineKind;
use crate::render::{DOWN, finalize};
use crate::resolve::MAX_POINTS;

View File

@ -1,7 +1,7 @@
use std::sync::Arc;
use sheetcraft_core::{CellRef, Value};
use sheetcraft_model::{Anchor, Chart, ChartKind, Color, LegendPos, Series, SparklineKind, Style, Workbook};
use gridcraft_core::{CellRef, Value};
use gridcraft_model::{Anchor, Chart, ChartKind, Color, LegendPos, Series, SparklineKind, Style, Workbook};
use crate::*;
@ -200,7 +200,7 @@ fn axis_labels_use_number_format() {
#[test]
fn resolve_from_workbook() {
let mut wb = Workbook::new();
let cur = wb.styles.intern(Style { num_fmt: sheetcraft_model::NumFmt(Arc::from("$#,##0")), ..Style::default() });
let cur = wb.styles.intern(Style { num_fmt: gridcraft_model::NumFmt(Arc::from("$#,##0")), ..Style::default() });
{
let s = wb.sheet_mut(0).unwrap();
s.name = "Sales".into();
@ -307,7 +307,7 @@ fn resolve_from_workbook() {
#[test]
fn palette_beyond_six_series() {
let theme = sheetcraft_model::Theme::default();
let theme = gridcraft_model::Theme::default();
let p = default_palette(&theme, 14);
assert_eq!(p.len(), 14);
assert_ne!(p[0], p[6]);

View File

@ -1,9 +1,9 @@
[package]
name = "sheetcraft-core"
name = "gridcraft-core"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "SheetCraft core types: cell addresses, values, errors, date serials"
description = "GridCraft core types: cell addresses, values, errors, date serials"
[lints]
workspace = true

View File

@ -1,4 +1,4 @@
//! SheetCraft core types: cell addresses, values and Excel's coercion rules, error values,
//! GridCraft core types: cell addresses, values and Excel's coercion rules, error values,
//! date serials and typed-input parsing. No dependencies on the rest of the workspace.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]

View File

@ -1,25 +1,25 @@
[package]
name = "sheetcraft-engine"
name = "gridcraft-engine"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "SheetCraft engine: sessions, commands, history, clipboard, fill, sort, filter"
description = "GridCraft engine: sessions, commands, history, clipboard, fill, sort, filter"
[lints]
workspace = true
[dependencies]
sheetcraft-core = { workspace = true }
sheetcraft-numfmt = { workspace = true }
sheetcraft-formula = { workspace = true }
sheetcraft-functions = { workspace = true }
sheetcraft-model = { workspace = true }
sheetcraft-calc = { workspace = true }
gridcraft-core = { workspace = true }
gridcraft-numfmt = { workspace = true }
gridcraft-formula = { workspace = true }
gridcraft-functions = { workspace = true }
gridcraft-model = { workspace = true }
gridcraft-calc = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
log = { workspace = true }
sheetcraft-xlsx = { workspace = true }
sheetcraft-chart = { workspace = true }
sheetcraft-pdf = { workspace = true }
gridcraft-xlsx = { workspace = true }
gridcraft-chart = { workspace = true }
gridcraft-pdf = { workspace = true }
image = { workspace = true }

View File

@ -1,10 +1,10 @@
//! Performance scenarios on large workbooks (release build):
//! `cargo run --release -p sheetcraft-engine --example perf -- [rows]`
//! `cargo run --release -p gridcraft-engine --example perf -- [rows]`
use std::time::Instant;
use gridcraft_engine::Session;
use serde_json::json;
use sheetcraft_engine::Session;
fn main() {
let rows: usize = std::env::args().nth(1).and_then(|s| s.parse().ok()).unwrap_or(100_000);

View File

@ -3,8 +3,8 @@
use std::collections::HashMap;
use sheetcraft_core::{CellRef, RangeRef, Value, compare};
use sheetcraft_model::*;
use gridcraft_core::{CellRef, RangeRef, Value, compare};
use gridcraft_model::*;
/// The conditional look of one cell.
#[derive(Clone, Debug, Default, PartialEq)]
@ -77,7 +77,7 @@ fn threshold(wb: &Workbook, sheet: usize, at: CellRef, st: &RuleStats, k: &CfVal
CfValueKind::Number(n) => *n,
CfValueKind::Percent(p) => min + (max - min) * p / 100.0,
CfValueKind::Percentile(p) => percentile(&st.sorted, *p),
CfValueKind::Formula(f) => sheetcraft_calc::evaluate(wb, sheet, at, f).to_number().unwrap_or(0.0),
CfValueKind::Formula(f) => gridcraft_calc::evaluate(wb, sheet, at, f).to_number().unwrap_or(0.0),
}
}
@ -128,7 +128,7 @@ impl CfCache {
let text = v.display();
let matched_style: Option<&Style> = match &cf.rule {
CfRule::CellIs { op, a, b, style } => {
let eval = |f: &str| sheetcraft_calc::evaluate(wb, sheet, c, &relative(f, first, c));
let eval = |f: &str| gridcraft_calc::evaluate(wb, sheet, c, &relative(f, first, c));
let av = eval(a);
let ok = match op {
CfOperator::Between | CfOperator::NotBetween => {
@ -147,7 +147,7 @@ impl CfCache {
(ok && !v.is_empty()).then_some(style.as_ref())
}
CfRule::Expression { formula, style } => {
sheetcraft_calc::evaluate(wb, sheet, c, &relative(formula, first, c)).to_bool().unwrap_or(false).then_some(style.as_ref())
gridcraft_calc::evaluate(wb, sheet, c, &relative(formula, first, c)).to_bool().unwrap_or(false).then_some(style.as_ref())
}
CfRule::ContainsText { text: t, style } => text.to_lowercase().contains(&t.to_lowercase()).then_some(style.as_ref()),
CfRule::NotContainsText { text: t, style } => (!text.to_lowercase().contains(&t.to_lowercase())).then_some(style.as_ref()),
@ -186,13 +186,13 @@ impl CfCache {
},
CfRule::TimePeriod { period, style } => match v.as_f64() {
Some(n) => {
let today = sheetcraft_calc::now_serial().floor();
let today = gridcraft_calc::now_serial().floor();
let d = n.floor();
let wd = sheetcraft_core::date::datetime_from_serial(wb.date_system, today).map(|x| x.weekday as f64).unwrap_or(0.0);
let wd = gridcraft_core::date::datetime_from_serial(wb.date_system, today).map(|x| x.weekday as f64).unwrap_or(0.0);
let week_start = today - wd;
let (ty, tm) =
sheetcraft_core::date::datetime_from_serial(wb.date_system, today).map(|x| (x.year, x.month)).unwrap_or((2026, 1));
let ym = sheetcraft_core::date::datetime_from_serial(wb.date_system, d).map(|x| (x.year, x.month));
gridcraft_core::date::datetime_from_serial(wb.date_system, today).map(|x| (x.year, x.month)).unwrap_or((2026, 1));
let ym = gridcraft_core::date::datetime_from_serial(wb.date_system, d).map(|x| (x.year, x.month));
let month_off = |k: i32| {
let m = tm as i32 + k;
let y = ty + (m - 1).div_euclid(12);
@ -299,12 +299,10 @@ fn relative(f: &str, first: CellRef, c: CellRef) -> String {
if first == c {
return f.to_string();
}
match sheetcraft_formula::parse(f.trim_start_matches('=')) {
Ok(e) => sheetcraft_formula::print(&sheetcraft_formula::adjust::shift_relative(
e,
c.row as i64 - first.row as i64,
c.col as i64 - first.col as i64,
)),
match gridcraft_formula::parse(f.trim_start_matches('=')) {
Ok(e) => {
gridcraft_formula::print(&gridcraft_formula::adjust::shift_relative(e, c.row as i64 - first.row as i64, c.col as i64 - first.col as i64))
}
Err(_) => f.to_string(),
}
}

View File

@ -3,9 +3,9 @@
use std::cmp::Ordering;
use std::sync::Arc;
use gridcraft_core::{CellRef, RangeRef, Value, sort_compare};
use gridcraft_model::*;
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, RangeRef, Value, sort_compare};
use sheetcraft_model::*;
use super::*;
use crate::selection::current_region;
@ -93,7 +93,7 @@ pub fn specs() -> Vec<CommandSpec> {
fn col_param(v: Option<&Json>, base: u32) -> Option<u32> {
match v? {
Json::String(s) => sheetcraft_core::letters_to_col(s.trim_start_matches('$')),
Json::String(s) => gridcraft_core::letters_to_col(s.trim_start_matches('$')),
Json::Number(n) => n.as_u64().map(|o| base + o as u32),
_ => None,
}
@ -274,7 +274,7 @@ fn do_sort(s: &mut Session, r: RangeRef, header: bool, keys: Vec<SortKey>, by_co
&& let Some(f) = &c.formula
&& let Some(e) = f.expr()
{
let shifted = sheetcraft_formula::adjust::shift_relative(e, dst.row as i64 - src.row as i64, dst.col as i64 - src.col as i64);
let shifted = gridcraft_formula::adjust::shift_relative(e, dst.row as i64 - src.row as i64, dst.col as i64 - src.col as i64);
c.formula = Some(Arc::new(Formula::from_expr(shifted)));
}
moved.push((dst, cell));
@ -401,10 +401,10 @@ fn filter_by(s: &mut Session, p: &Json) -> Result<Json> {
/// Does a value pass a custom comparison (`>`, `=*abc*`, `<>x`…)?
fn custom_ok(v: &Value, text: &str, op: &str, crit: &str) -> bool {
let num = sheetcraft_core::parse::parse_number_text(crit);
let num = gridcraft_core::parse::parse_number_text(crit);
let cmp = match (v.as_f64(), num) {
(Some(a), Some(b)) => a.partial_cmp(&b),
_ => Some(sheetcraft_core::compare_text(text, crit)),
_ => Some(gridcraft_core::compare_text(text, crit)),
};
let wild = || super::edit::wildcard(&text.to_lowercase(), &crit.to_lowercase());
match op {
@ -781,14 +781,12 @@ pub fn list_items(wb: &Workbook, sheet: usize, dv: &Validation) -> Vec<String> {
let f = dv.f1.trim();
if f.starts_with('"')
|| (!f.contains('!')
&& sheetcraft_formula::parse(f)
.map(|e| !matches!(e, sheetcraft_formula::Expr::Ref(_) | sheetcraft_formula::Expr::Name(_)))
.unwrap_or(true))
&& gridcraft_formula::parse(f).map(|e| !matches!(e, gridcraft_formula::Expr::Ref(_) | gridcraft_formula::Expr::Name(_))).unwrap_or(true))
{
return f.trim_matches('"').split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect();
}
let at = dv.ranges.first().map(|r| r.start).unwrap_or_default();
match sheetcraft_calc::evaluate(wb, sheet, at, f) {
match gridcraft_calc::evaluate(wb, sheet, at, f) {
Value::Array(a) => a.data.iter().filter(|v| !v.is_empty()).map(Value::display).collect(),
v if !v.is_empty() && !v.is_error() => vec![v.display()],
_ => vec![],
@ -805,11 +803,11 @@ pub fn check_validation(wb: &Workbook, sheet: usize, at: CellRef, input: &str) -
if input.is_empty() && dv.allow_blank {
return None;
}
let v = sheetcraft_core::parse::parse_input(input, wb.date_system).value;
let num = |f: &str| sheetcraft_calc::evaluate(wb, sheet, at, f).to_number().ok();
let v = gridcraft_core::parse::parse_input(input, wb.date_system).value;
let num = |f: &str| gridcraft_calc::evaluate(wb, sheet, at, f).to_number().ok();
let ok = match dv.kind {
ValidationKind::List => list_items(wb, sheet, dv).iter().any(|x| x.eq_ignore_ascii_case(input)),
ValidationKind::Custom => sheetcraft_calc::evaluate(wb, sheet, at, &dv.f1).to_bool().unwrap_or(false),
ValidationKind::Custom => gridcraft_calc::evaluate(wb, sheet, at, &dv.f1).to_bool().unwrap_or(false),
ValidationKind::TextLength => {
let n = input.chars().count() as f64;
compare_op(dv.op, n, num(&dv.f1), dv.f2.as_deref().and_then(num))

View File

@ -1,8 +1,8 @@
//! Draw tab: pen and eraser tools, ink strokes, Ink to Shape.
use gridcraft_core::CellRef;
use gridcraft_model::*;
use serde_json::{Value as Json, json};
use sheetcraft_core::CellRef;
use sheetcraft_model::*;
use super::*;

View File

@ -1,8 +1,8 @@
//! Cell entry, selection, undo/redo, clipboard, clear, fill, find & replace.
use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value};
use gridcraft_model::{Cell, Formula, StyleId};
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value};
use sheetcraft_model::{Cell, Formula, StyleId};
use super::*;
use crate::selection::{current_region, jump};
@ -120,7 +120,7 @@ pub fn specs() -> Vec<CommandSpec> {
/// What typing `input` into a cell produces (constant or formula, plus an automatic number
/// format). Errors when the formula can't be parsed.
pub(crate) fn input_to_cell(input: &str, old: Option<&Cell>, wb: &mut sheetcraft_model::Workbook) -> Result<Option<Cell>> {
pub(crate) fn input_to_cell(input: &str, old: Option<&Cell>, wb: &mut gridcraft_model::Workbook) -> Result<Option<Cell>> {
let style = old.map(|c| c.style).unwrap_or_default();
if input.is_empty() {
let c = Cell { value: Value::Empty, formula: None, style };
@ -133,19 +133,19 @@ pub(crate) fn input_to_cell(input: &str, old: Option<&Cell>, wb: &mut sheetcraft
let is_formula = input.starts_with('=')
|| (input.len() > 1
&& (input.starts_with('+') || input.starts_with('-'))
&& sheetcraft_core::parse::parse_number_text(input).is_none()
&& gridcraft_core::parse::parse_number_text(input).is_none()
&& input.chars().nth(1).is_some_and(|c| c.is_ascii_alphabetic() || c == '('));
if is_formula {
let body = input.strip_prefix('=').unwrap_or(input);
// Excel closes missing parentheses for you.
let mut text = body.to_string();
let mut parsed = sheetcraft_formula::parse(&text);
let mut parsed = gridcraft_formula::parse(&text);
for _ in 0..8 {
if parsed.is_ok() {
break;
}
text.push(')');
parsed = sheetcraft_formula::parse(&text);
parsed = gridcraft_formula::parse(&text);
}
let expr = parsed.map_err(|e| EngineError::Other(format!("There's a problem with this formula: {e}")))?;
let mut cell = Cell::formula(Formula::from_expr(expr));
@ -168,17 +168,17 @@ pub(crate) fn input_to_cell(input: &str, old: Option<&Cell>, wb: &mut sheetcraft
None
};
if let Some(code) = auto {
cell.style = wb.styles.derive(style, |s| s.num_fmt = sheetcraft_model::NumFmt::new(code));
cell.style = wb.styles.derive(style, |s| s.num_fmt = gridcraft_model::NumFmt::new(code));
}
}
return Ok(Some(cell));
}
let parsed = sheetcraft_core::parse::parse_input(input, wb.date_system);
let parsed = gridcraft_core::parse::parse_input(input, wb.date_system);
let mut style = style;
if let Some(code) = parsed.format {
let cur = wb.styles.get(style).num_fmt.as_str().to_string();
if cur == "General" {
style = wb.styles.derive(style, |s| s.num_fmt = sheetcraft_model::NumFmt::new(code));
style = wb.styles.derive(style, |s| s.num_fmt = gridcraft_model::NumFmt::new(code));
}
}
// Wrap text automatically when the input has line breaks (Alt+Enter).
@ -308,7 +308,7 @@ fn range_fill(s: &mut Session, p: &Json) -> Result<Json> {
if let Some(f) = &cell.formula
&& let Some(e) = f.expr()
{
let shifted = sheetcraft_formula::adjust::shift_relative(e, at.row as i64 - origin.row as i64, at.col as i64 - origin.col as i64);
let shifted = gridcraft_formula::adjust::shift_relative(e, at.row as i64 - origin.row as i64, at.col as i64 - origin.col as i64);
cell.formula = Some(std::sync::Arc::new(Formula::from_expr(shifted)));
}
cell.style = cx.wb.sheet(sheet).and_then(|sh| sh.cell(at)).map(|c| c.style).unwrap_or(cell.style);
@ -777,7 +777,7 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
edit(s, |cx| {
// Styles from another workbook must be re-interned.
let mut style_map = std::collections::HashMap::new();
let mut map_style = |id: StyleId, wb: &mut sheetcraft_model::Workbook| -> StyleId {
let mut map_style = |id: StyleId, wb: &mut gridcraft_model::Workbook| -> StyleId {
if same_doc {
return id;
}
@ -817,14 +817,14 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
return Some(std::sync::Arc::new(Formula::from_expr(e)));
}
let moved =
sheetcraft_formula::adjust::shift_relative(e, dest.row as i64 - sc.row as i64, dest.col as i64 - sc.col as i64);
gridcraft_formula::adjust::shift_relative(e, dest.row as i64 - sc.row as i64, dest.col as i64 - sc.col as i64);
Some(std::sync::Arc::new(Formula::from_expr(moved)))
};
if link {
let sheet_prefix = if dest_sheet == clip.sheet && same_doc {
String::new()
} else {
format!("{}!", sheetcraft_formula::quote_sheet(&src_sheet_name))
format!("{}!", gridcraft_formula::quote_sheet(&src_sheet_name))
};
new.formula = Some(std::sync::Arc::new(Formula::new(&format!("={sheet_prefix}{}", sc.a1()))));
new.value = Value::Empty;
@ -878,7 +878,7 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
"multiply" => Value::number(x * y),
"divide" => {
if y == 0.0 {
Value::Error(sheetcraft_core::CellError::Div0)
Value::Error(gridcraft_core::CellError::Div0)
} else {
Value::number(x / y)
}
@ -930,7 +930,7 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
// References elsewhere to the moved block now point to its new place.
let dest_name = cx.wb.sheet(dest_sheet).map(|s| s.name.clone()).unwrap_or_default();
if dest_sheet == clip.sheet {
let e = sheetcraft_formula::adjust::Edit::Move { from: src, to_row: at.row, to_col: at.col };
let e = gridcraft_formula::adjust::Edit::Move { from: src, to_row: at.row, to_col: at.col };
rewrite_all_formulas(&mut cx.wb, &src_sheet_name, &e);
}
let _ = dest_name;
@ -947,7 +947,7 @@ fn paste_special(s: &mut Session, p: &Json) -> Result<Json> {
}
/// Applies a reference adjustment to every formula in the workbook (and names, CF, DV).
pub(crate) fn rewrite_all_formulas(wb: &mut sheetcraft_model::Workbook, target: &str, e: &sheetcraft_formula::adjust::Edit) {
pub(crate) fn rewrite_all_formulas(wb: &mut gridcraft_model::Workbook, target: &str, e: &gridcraft_formula::adjust::Edit) {
for si in 0..wb.sheets.len() {
let host = wb.sheets.get(si).map(|s| s.name.clone()).unwrap_or_default();
let keys: Vec<(CellRef, std::sync::Arc<Formula>)> =
@ -958,8 +958,8 @@ pub(crate) fn rewrite_all_formulas(wb: &mut sheetcraft_model::Workbook, target:
let Some(sheet) = wb.sheet_mut(si) else { continue };
for (c, f) in keys {
let Some(expr) = f.expr() else { continue };
let new = sheetcraft_formula::adjust::adjust(expr, &host, target, e);
let text = sheetcraft_formula::print(&new);
let new = gridcraft_formula::adjust::adjust(expr, &host, target, e);
let text = gridcraft_formula::print(&new);
if text != f.text
&& let Some(cell) = sheet.cells.get_mut(c)
{
@ -970,10 +970,10 @@ pub(crate) fn rewrite_all_formulas(wb: &mut sheetcraft_model::Workbook, target:
}
}
for n in wb.names.iter_mut() {
if let Ok(expr) = sheetcraft_formula::parse(&n.formula) {
if let Ok(expr) = gridcraft_formula::parse(&n.formula) {
// Names are workbook-level: unqualified refs don't occur; qualified ones adjust.
let new = sheetcraft_formula::adjust::adjust(expr, "\u{0}", target, e);
n.formula = sheetcraft_formula::print(&new);
let new = gridcraft_formula::adjust::adjust(expr, "\u{0}", target, e);
n.formula = gridcraft_formula::print(&new);
}
}
}

View File

@ -1,9 +1,9 @@
//! More ribbon commands: Chart Design and Format, object arrangement, workbook views, table
//! tools, Get Data, scripts (Automate), cell checkboxes, theme colours and fonts.
use gridcraft_core::{CellRef, RangeRef, Value};
use gridcraft_model::*;
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, RangeRef, Value};
use sheetcraft_model::*;
use super::*;
@ -224,7 +224,7 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
new_cell_style
),
cmd!(query "home.addIns", "Add-ins", ["Home", "Add-ins"], None, "{} → SheetCraft extends through MCP and scripts instead of add-ins", always, |_, _| Ok(json!({"addIns": [], "hint": "Use sheetcraft-cli mcp or Automate › scripts"}))),
cmd!(query "home.addIns", "Add-ins", ["Home", "Add-ins"], None, "{} → GridCraft extends through MCP and scripts instead of add-ins", always, |_, _| Ok(json!({"addIns": [], "hint": "Use gridcraft-cli mcp or Automate › scripts"}))),
cmd!(query "formulas.recentlyUsed", "Recently Used", ["Formulas", "Function Library"], None, "{} → recently used functions in this workbook", has_doc, recently_used),
cmd!(query "formulas.financial", "Financial", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Financial"}))),
cmd!(query "formulas.logical", "Logical", ["Formulas", "Function Library"], None, "{}", always, |s, _| s.execute("formulas.functions", json!({"category": "Logical"}))),
@ -356,7 +356,7 @@ fn select_data(s: &mut Session, p: &Json) -> Result<Json> {
let src = d.wb.sheet(src_si).ok_or(EngineError::NoDocument)?;
let by_rows = bool_param(p, "byRows").unwrap_or(r.width() > r.height() + 1);
let series = super::insert::series_from_range(src, r, by_rows);
let source = format!("{}!{}", sheetcraft_formula::quote_sheet(&src.name), r.a1());
let source = format!("{}!{}", gridcraft_formula::quote_sheet(&src.name), r.a1());
with_chart(s, p, move |c| {
c.series = series;
c.source = Some(source);
@ -703,8 +703,8 @@ fn from_csv(s: &mut Session, p: &Json) -> Result<Json> {
let delim = str_param(p, "delimiter")
.and_then(|d| d.chars().next())
.unwrap_or(if text.lines().next().is_some_and(|l| l.contains('\t')) { '\t' } else { ',' });
let opts = sheetcraft_xlsx::CsvOptions { delimiter: delim as u8, ..Default::default() };
let wb = sheetcraft_xlsx::read_csv(&bytes, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
let opts = gridcraft_xlsx::CsvOptions { delimiter: delim as u8, ..Default::default() };
let wb = gridcraft_xlsx::read_csv(&bytes, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
let sh = wb.sheet(0).ok_or(EngineError::NoDocument)?;
let Some(u) = sh.used_range() else { return Err(EngineError::Other("The file is empty.".into())) };
(u.start.row..=u.end.row)
@ -933,7 +933,7 @@ fn recently_used(s: &mut Session, _: &Json) -> Result<Json> {
continue;
}
let input = p.get("input").and_then(Json::as_str).unwrap_or("");
if let Ok(e) = sheetcraft_formula::parse(input.trim_start_matches('=')) {
if let Ok(e) = gridcraft_formula::parse(input.trim_start_matches('=')) {
for f in e.functions() {
if !seen.iter().any(|x| x == f) {
seen.push(f.to_string());

View File

@ -1,8 +1,8 @@
//! Formatting: Home › Font, Alignment, Number, Styles, Cells › Format.
use gridcraft_core::{CellRef, RangeRef};
use gridcraft_model::*;
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, RangeRef};
use sheetcraft_model::*;
use super::*;
@ -558,7 +558,7 @@ fn merge(s: &mut Session, p: &Json, how: &str) -> Result<Json> {
let others: Vec<CellRef> = sh.cells.iter_range(*r).map(|(c, _)| c).filter(|c| *c != r.start).collect();
for c in others {
if let Some(mut cell) = sh.cells.get(c).cloned() {
cell.value = sheetcraft_core::Value::Empty;
cell.value = gridcraft_core::Value::Empty;
cell.formula = None;
sh.cells.set(c, cell);
}
@ -610,10 +610,10 @@ fn set_fmt(s: &mut Session, p: &Json, code: &str) -> Result<Json> {
}
/// Adds or removes one decimal place in the number format (Increase/Decrease Decimal).
pub fn adjust_decimals(code: &str, value: &sheetcraft_core::Value, delta: i32) -> String {
pub fn adjust_decimals(code: &str, value: &gridcraft_core::Value, delta: i32) -> String {
if code == "General" {
// Start from the displayed number of decimals.
let text = value.as_f64().map(sheetcraft_core::number_to_text).unwrap_or_default();
let text = value.as_f64().map(gridcraft_core::number_to_text).unwrap_or_default();
let dec = text.split_once('.').map_or(0, |(_, d)| d.len()) as i32;
let n = (dec + delta).max(0) as usize;
return if n == 0 { "0".into() } else { format!("0.{}", "0".repeat(n)) };
@ -1067,7 +1067,7 @@ mod tests {
#[test]
fn decimal_steps() {
let v = sheetcraft_core::Value::Number(1.5);
let v = gridcraft_core::Value::Number(1.5);
assert_eq!(adjust_decimals("General", &v, 1), "0.00");
assert_eq!(adjust_decimals("General", &v, -1), "0");
assert_eq!(adjust_decimals("0", &v, 1), "0.0");

View File

@ -1,8 +1,8 @@
//! Formulas tab: AutoSum, Insert Function, names, auditing, calculation.
use gridcraft_core::{CellRef, RangeRef};
use gridcraft_model::{CalcMode, DefinedName};
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, RangeRef};
use sheetcraft_model::{CalcMode, DefinedName};
use super::*;
@ -144,7 +144,7 @@ fn insert_function(s: &mut Session, p: &Json) -> Result<Json> {
}
pub fn function_list() -> Vec<Json> {
let mut v: Vec<Json> = sheetcraft_functions::all()
let mut v: Vec<Json> = gridcraft_functions::all()
.iter()
.map(|f| json!({"name": f.name, "category": format!("{:?}", f.category), "signature": f.signature, "description": f.description}))
.collect();
@ -245,12 +245,12 @@ fn define_name(s: &mut Session, p: &Json) -> Result<Json> {
None => {
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
let r = d.selection.current();
let abs = |c: CellRef| format!("${}${}", sheetcraft_core::col_to_letters(c.col), c.row + 1);
let abs = |c: CellRef| format!("${}${}", gridcraft_core::col_to_letters(c.col), c.row + 1);
let body = if r.is_single() { abs(r.start) } else { format!("{}:{}", abs(r.start), abs(r.end)) };
format!("{}!{}", sheetcraft_formula::quote_sheet(&sh.name), body)
format!("{}!{}", gridcraft_formula::quote_sheet(&sh.name), body)
}
};
if sheetcraft_formula::parse(&refers).is_err() {
if gridcraft_formula::parse(&refers).is_err() {
return Err(EngineError::Other("There's a problem with this formula.".into()));
}
let comment = str_param(p, "comment").unwrap_or("").to_string();
@ -284,7 +284,7 @@ fn name_manager(s: &mut Session, _: &Json) -> Result<Json> {
.filter(|n| !n.hidden)
.map(|n| {
let sheet = n.scope.unwrap_or(d.wb.active_sheet);
let v = sheetcraft_calc::evaluate(&d.wb, sheet, CellRef::default(), &n.formula);
let v = gridcraft_calc::evaluate(&d.wb, sheet, CellRef::default(), &n.formula);
json!({"name": n.name, "refersTo": format!("={}", n.formula), "scope": n.scope.and_then(|i| d.wb.sheet(i)).map(|s| s.name.clone()).unwrap_or_else(|| "Workbook".into()), "value": crate::cmd::inspect::value_json(&v), "comment": n.comment})
})
.collect();
@ -297,13 +297,13 @@ fn create_from_selection(s: &mut Session, p: &Json) -> Result<Json> {
let left = bool_param(p, "left").unwrap_or(false);
let d = s.doc()?;
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
let q = sheetcraft_formula::quote_sheet(&sh.name);
let q = gridcraft_formula::quote_sheet(&sh.name);
let abs = |r: RangeRef| {
format!(
"{q}!${}${}:${}${}",
sheetcraft_core::col_to_letters(r.start.col),
gridcraft_core::col_to_letters(r.start.col),
r.start.row + 1,
sheetcraft_core::col_to_letters(r.end.col),
gridcraft_core::col_to_letters(r.end.col),
r.end.row + 1
)
};
@ -405,20 +405,20 @@ fn evaluate_formula(s: &mut Session, p: &Json) -> Result<Json> {
.ok_or_else(|| EngineError::Other("The cell has no formula.".into()))?
}
};
let expr = sheetcraft_formula::parse(&text).map_err(|e| EngineError::Other(e.to_string()))?;
let expr = gridcraft_formula::parse(&text).map_err(|e| EngineError::Other(e.to_string()))?;
// Steps: each sub-expression (innermost first) with its value.
let mut steps = Vec::new();
let mut nodes = Vec::new();
expr.walk(&mut |e| {
if matches!(e, sheetcraft_formula::Expr::Call(..) | sheetcraft_formula::Expr::Binary(..) | sheetcraft_formula::Expr::Ref(_)) {
if matches!(e, gridcraft_formula::Expr::Call(..) | gridcraft_formula::Expr::Binary(..) | gridcraft_formula::Expr::Ref(_)) {
nodes.push(e.clone());
}
});
for e in nodes.into_iter().rev().take(200) {
let v = sheetcraft_calc::recalc::evaluate_expr(&d.wb, sheet, at, &e);
steps.push(json!({"expression": sheetcraft_formula::print(&e), "value": crate::cmd::inspect::value_json(&v)}));
let v = gridcraft_calc::recalc::evaluate_expr(&d.wb, sheet, at, &e);
steps.push(json!({"expression": gridcraft_formula::print(&e), "value": crate::cmd::inspect::value_json(&v)}));
}
let result = sheetcraft_calc::recalc::evaluate_expr(&d.wb, sheet, at, &expr);
let result = gridcraft_calc::recalc::evaluate_expr(&d.wb, sheet, at, &expr);
Ok(json!({"formula": format!("={text}"), "steps": steps, "result": crate::cmd::inspect::value_json(&result)}))
}
@ -426,7 +426,7 @@ fn evaluate(s: &mut Session, p: &Json) -> Result<Json> {
let d = s.doc()?;
let f = str_param(p, "formula").ok_or_else(|| bad("formulas.evaluate", "missing `formula`"))?;
let at = cell_param(p, "cell").unwrap_or(d.selection.active);
let v = sheetcraft_calc::evaluate(&d.wb, d.wb.active_sheet, at, f);
let v = gridcraft_calc::evaluate(&d.wb, d.wb.active_sheet, at, f);
Ok(crate::cmd::inspect::value_json(&v))
}

View File

@ -1,9 +1,9 @@
//! Insert tab: tables, charts, sparklines, pictures, shapes, hyperlinks, comments and notes,
//! conditional formatting (Home › Styles) and symbols.
use gridcraft_core::{CellRef, RangeRef};
use gridcraft_model::*;
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, RangeRef};
use sheetcraft_model::*;
use super::*;
use crate::selection::current_region;
@ -221,7 +221,7 @@ fn insert_table(s: &mut Session, p: &Json) -> Result<Json> {
let sh = cx.sheet_mut(sheet)?;
sh.cells.shift_rows_in_cols(r.start.col, r.end.col, r.start.row, 1);
for (i, c) in (r.start.col..=r.end.col).enumerate() {
sh.set_value(CellRef::new(r.start.row, c), sheetcraft_core::Value::text(format!("Column{}", i + 1)));
sh.set_value(CellRef::new(r.start.row, c), gridcraft_core::Value::text(format!("Column{}", i + 1)));
}
cx.structural = true;
RangeRef::new(r.start, CellRef::new(r.end.row + 1, r.end.col))
@ -240,7 +240,7 @@ fn insert_table(s: &mut Session, p: &Json) -> Result<Json> {
k += 1;
}
// Headers are text.
sh.set_value(CellRef::new(r.start.row, c), sheetcraft_core::Value::text(n.as_str()));
sh.set_value(CellRef::new(r.start.row, c), gridcraft_core::Value::text(n.as_str()));
names.push(n);
}
sh.tables.push(Table {
@ -334,9 +334,9 @@ fn write_totals(sh: &mut Sheet, ti: usize) {
if let Some(code) = col.totals.subtotal_code() {
let f = format!("=SUBTOTAL({code},{}[{}])", t.name, col.name);
sh.cells
.set(c, Cell { formula: Some(std::sync::Arc::new(Formula::new(&f))), value: sheetcraft_core::Value::Empty, style: sh.style_id(c) });
.set(c, Cell { formula: Some(std::sync::Arc::new(Formula::new(&f))), value: gridcraft_core::Value::Empty, style: sh.style_id(c) });
} else if let Some(l) = &col.totals_label {
sh.set_value(c, sheetcraft_core::Value::text(l.as_str()));
sh.set_value(c, gridcraft_core::Value::text(l.as_str()));
} else {
sh.cells.remove(c);
}
@ -388,7 +388,7 @@ fn table_rename(s: &mut Session, p: &Json) -> Result<Json> {
if name.is_empty()
|| name.contains(' ')
|| name.chars().next().is_some_and(|c| c.is_ascii_digit())
|| sheetcraft_core::CellRef::parse(&name).is_some()
|| gridcraft_core::CellRef::parse(&name).is_some()
{
return Err(EngineError::Other("The name that you entered is not valid.".into()));
}
@ -408,11 +408,11 @@ fn table_rename(s: &mut Session, p: &Json) -> Result<Json> {
for (c, f) in keys {
let Some(e) = f.expr() else { continue };
let ne = e.map(&mut |x| match x {
sheetcraft_formula::Expr::Struct(mut st) if st.table.eq_ignore_ascii_case(&old) => {
gridcraft_formula::Expr::Struct(mut st) if st.table.eq_ignore_ascii_case(&old) => {
st.table = name.clone();
sheetcraft_formula::Expr::Struct(st)
gridcraft_formula::Expr::Struct(st)
}
sheetcraft_formula::Expr::Name(n) if n.eq_ignore_ascii_case(&old) => sheetcraft_formula::Expr::Name(name.clone()),
gridcraft_formula::Expr::Name(n) if n.eq_ignore_ascii_case(&old) => gridcraft_formula::Expr::Name(name.clone()),
o => o,
});
if let Some(cell) = sh.cells.get_mut(c) {
@ -480,17 +480,17 @@ pub fn chart_kind(t: &str, sub: &str) -> ChartKind {
/// Builds series from a block: first row/column as names/categories when they're text.
pub fn series_from_range(sh: &Sheet, r: RangeRef, by_rows: bool) -> Vec<Series> {
let q = sheetcraft_formula::quote_sheet(&sh.name);
let q = gridcraft_formula::quote_sheet(&sh.name);
let abs = |r: RangeRef| {
format!(
"{q}!${}${}:${}${}",
sheetcraft_core::col_to_letters(r.start.col),
gridcraft_core::col_to_letters(r.start.col),
r.start.row + 1,
sheetcraft_core::col_to_letters(r.end.col),
gridcraft_core::col_to_letters(r.end.col),
r.end.row + 1
)
};
let abs1 = |c: CellRef| format!("{q}!${}${}", sheetcraft_core::col_to_letters(c.col), c.row + 1);
let abs1 = |c: CellRef| format!("{q}!${}${}", gridcraft_core::col_to_letters(c.col), c.row + 1);
let is_text = |c: CellRef| sh.value(c).is_text() || sh.value(c).is_empty();
let header_row = r.height() > 1
&& (r.start.col..=r.end.col).any(|c| sh.value(CellRef::new(r.start.row, c)).is_text())
@ -552,7 +552,7 @@ fn insert_chart(s: &mut Session, p: &Json) -> Result<Json> {
let at = cell_param(p, "at").unwrap_or_else(|| CellRef::new(r.start.row, r.end.col + 2));
let title = str_param(p, "title").map(str::to_string).or_else(|| {
if series.len() == 1 {
series[0].name.as_ref().map(|n| sheetcraft_calc::evaluate(&d.wb, sheet, at, n).display())
series[0].name.as_ref().map(|n| gridcraft_calc::evaluate(&d.wb, sheet, at, n).display())
} else {
Some("Chart Title".into())
}
@ -575,7 +575,7 @@ fn insert_chart(s: &mut Session, p: &Json) -> Result<Json> {
style: 1,
x_title: None,
y_title: None,
source: Some(format!("{}!{}", sheetcraft_formula::quote_sheet(&sh.name), r.a1())),
source: Some(format!("{}!{}", gridcraft_formula::quote_sheet(&sh.name), r.a1())),
by_rows,
};
edit(s, |cx| {
@ -846,7 +846,7 @@ fn insert_link(s: &mut Session, p: &Json) -> Result<Json> {
});
let sh = cx.sheet_mut(sheet)?;
if let Some(t) = text.clone().or_else(|| sh.value(at).is_empty().then(|| target.clone())) {
sh.cells.set(at, Cell { value: sheetcraft_core::Value::text(t.as_str()), formula: None, style: link_style });
sh.cells.set(at, Cell { value: gridcraft_core::Value::text(t.as_str()), formula: None, style: link_style });
} else {
sh.set_style(at, link_style);
}
@ -996,7 +996,7 @@ fn add_cf(s: &mut Session, p: &Json) -> Result<Json> {
let a = rule.get("value").map(super::edit::json_to_input).unwrap_or_default();
let b = rule.get("value2").map(super::edit::json_to_input);
let quote = |v: String| {
if sheetcraft_core::parse::parse_number_text(&v).is_some() || v.starts_with('=') {
if gridcraft_core::parse::parse_number_text(&v).is_some() || v.starts_with('=') {
v.trim_start_matches('=').to_string()
} else {
format!("\"{}\"", v.replace('"', "\"\""))

View File

@ -1,7 +1,7 @@
//! Read-only commands for agents: inspect the workbook, read ranges, cell details.
use gridcraft_core::{CellRef, RangeRef, Value};
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, RangeRef, Value};
use super::*;

View File

@ -18,11 +18,11 @@ pub mod spelling;
pub mod view;
pub mod whatif;
use gridcraft_calc::Key;
use gridcraft_core::{CellRef, RangeRef};
use gridcraft_model::Workbook;
use serde::Serialize;
use serde_json::Value as Json;
use sheetcraft_calc::Key;
use sheetcraft_core::{CellRef, RangeRef};
use sheetcraft_model::Workbook;
use crate::{DocState, EngineError, Result, Selection, Session};
@ -217,7 +217,7 @@ pub(crate) fn edit<R>(s: &mut Session, f: impl FnOnce(&mut Ctx) -> Result<R>) ->
}
pub(crate) fn commit<R>(d: &mut DocState, f: impl FnOnce(&mut Ctx) -> Result<R>) -> Result<R> {
let prof = std::env::var_os("SHEETCRAFT_PROFILE").is_some();
let prof = std::env::var_os("GRIDCRAFT_PROFILE").is_some();
let t0 = std::time::Instant::now();
let mut sel = d.selection.clone();
let mut ctx = Ctx { wb: (*d.wb).clone(), changed: Vec::new(), structural: false, sel: &mut sel, fit_rows: Vec::new() };
@ -259,9 +259,9 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {
}
let default = sh.default_row_height;
// Fast path: rows of default-size, unwrapped, single-line content keep the default height.
let plain = sh.cells.row(row, 0, sheetcraft_core::MAX_COLS - 1).all(|(_, cell)| {
let plain = sh.cells.row(row, 0, gridcraft_core::MAX_COLS - 1).all(|(_, cell)| {
let st = wb.styles.get(cell.style);
st.font.size <= sheetcraft_model::DEFAULT_FONT_SIZE && !st.align.wrap && !cell.value.as_text().is_some_and(|t| t.contains('\n'))
st.font.size <= gridcraft_model::DEFAULT_FONT_SIZE && !st.align.wrap && !cell.value.as_text().is_some_and(|t| t.contains('\n'))
});
if plain {
if sh.rows.get(&row).is_some_and(|i| i.size.is_some())
@ -270,7 +270,7 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {
{
{
e.size = None;
if *e == sheetcraft_model::LineInfo::default() {
if *e == gridcraft_model::LineInfo::default() {
shm.rows.remove(&row);
}
}
@ -278,10 +278,10 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {
return;
}
let mut need: f32 = default;
for (c, cell) in sh.cells.row(row, 0, sheetcraft_core::MAX_COLS - 1) {
for (c, cell) in sh.cells.row(row, 0, gridcraft_core::MAX_COLS - 1) {
let st = wb.styles.get(cell.style);
let size = st.font.size;
let line = (size * 1.8).round().max(default * size / sheetcraft_model::DEFAULT_FONT_SIZE);
let line = (size * 1.8).round().max(default * size / gridcraft_model::DEFAULT_FONT_SIZE);
let mut lines = 1.0f32;
if !cell.value.is_empty() || cell.formula.is_some() {
let text = crate::display::cell_text(wb, sh, CellRef::new(row, c));
@ -295,7 +295,7 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {
} else {
explicit
};
} else if st.font.size <= sheetcraft_model::DEFAULT_FONT_SIZE {
} else if st.font.size <= gridcraft_model::DEFAULT_FONT_SIZE {
continue;
}
let h = if lines > 1.0 { (size * 96.0 / 72.0 * 1.22 * lines + 5.0).ceil() } else { line };
@ -304,7 +304,7 @@ pub fn auto_row_height(wb: &mut Workbook, si: usize, row: u32) {
let Some(shm) = wb.sheet_mut(si) else { return };
let e = shm.rows.entry(row).or_default();
e.size = if (need - default).abs() < 0.5 { None } else { Some(need) };
if *e == sheetcraft_model::LineInfo::default() {
if *e == gridcraft_model::LineInfo::default() {
shm.rows.remove(&row);
}
}
@ -313,7 +313,7 @@ impl Ctx<'_> {
pub fn sheet_index(&self) -> usize {
self.wb.active_sheet
}
pub fn sheet_mut(&mut self, i: usize) -> Result<&mut sheetcraft_model::Sheet> {
pub fn sheet_mut(&mut self, i: usize) -> Result<&mut gridcraft_model::Sheet> {
self.wb.sheet_mut(i).ok_or_else(|| EngineError::Other("no such sheet".into()))
}
pub fn touch(&mut self, sheet: usize, c: CellRef) {

View File

@ -6,9 +6,9 @@
use std::sync::Arc;
use gridcraft_core::{CellRef, RangeRef};
use gridcraft_model::*;
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, RangeRef};
use sheetcraft_model::*;
use super::*;
use crate::pivot as pv;
@ -1280,8 +1280,8 @@ fn recommended(s: &mut Session, p: &Json) -> Result<Json> {
#[cfg(test)]
mod tests {
use gridcraft_core::{CellRef, Value};
use serde_json::json;
use sheetcraft_core::{CellRef, Value};
use crate::Session;
@ -1316,7 +1316,7 @@ mod tests {
fn text(s: &Session, a: &str) -> String {
val(s, a).display()
}
fn pt(s: &Session) -> sheetcraft_model::PivotTable {
fn pt(s: &Session) -> gridcraft_model::PivotTable {
s.doc().unwrap().wb.active().unwrap().pivots.last().unwrap().clone()
}
/// Finds the row (0-based) whose first report column holds `label`.

View File

@ -3,16 +3,16 @@
//! Sheets are paginated like Excel's default "down, then over": the print area (or the used
//! range plus drawing objects) is split into bands of rows and columns that fit the printable
//! area at the page scale, honouring manual breaks, repeated title rows/columns, headings, Fit
//! To and centring. Each page is drawn into a PDF with `sheetcraft-pdf`: fills, gridlines,
//! To and centring. Each page is drawn into a PDF with `gridcraft-pdf`: fills, gridlines,
//! borders, formatted text (fonts mapped to the standard 14 PDF fonts), merged cells, wrapping,
//! overflow into empty neighbours, conditional formats, table styles, charts, images, shapes and
//! sparklines. Headers and footers support `&L`/`&C`/`&R` sections and the `&P &N &D &T &A &F &Z`
//! codes.
use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value};
use gridcraft_model::*;
use gridcraft_pdf::{Font, Page, PdfDoc, Rgb};
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value};
use sheetcraft_model::*;
use sheetcraft_pdf::{Font, Page, PdfDoc, Rgb};
use super::*;
@ -464,8 +464,8 @@ fn rgb_of(c: Color, wb: &Workbook) -> Option<Rgb> {
c.resolve(&wb.theme)
}
fn numfmt_rgb(c: sheetcraft_numfmt::FormatColor) -> Rgb {
use sheetcraft_numfmt::FormatColor as F;
fn numfmt_rgb(c: gridcraft_numfmt::FormatColor) -> Rgb {
use gridcraft_numfmt::FormatColor as F;
match c {
F::Black | F::Indexed(_) => [0, 0, 0],
F::Blue => [0, 0, 255],
@ -539,9 +539,9 @@ fn cell_look(wb: &Workbook, si: usize, sh: &Sheet, c: CellRef, cf: &mut Option<c
/// PDF measurement for chart layout.
struct PdfMeasure;
impl sheetcraft_chart::Measure for PdfMeasure {
impl gridcraft_chart::Measure for PdfMeasure {
fn text_width(&self, text: &str, size: f32, bold: bool) -> f32 {
sheetcraft_pdf::text_width(if bold { Font::HelveticaBold } else { Font::Helvetica }, size, text)
gridcraft_pdf::text_width(if bold { Font::HelveticaBold } else { Font::Helvetica }, size, text)
}
}
@ -563,8 +563,8 @@ impl Xf {
}
/// Draws chart primitives onto a page.
fn draw_prims(pg: &mut Page, prims: &[sheetcraft_chart::Prim], xf: Xf) {
use sheetcraft_chart::{HAlign as H, Prim, VAlign as V};
fn draw_prims(pg: &mut Page, prims: &[gridcraft_chart::Prim], xf: Xf) {
use gridcraft_chart::{HAlign as H, Prim, VAlign as V};
let k = xf.k;
for p in prims {
match p {
@ -603,7 +603,7 @@ fn draw_prims(pg: &mut Page, prims: &[sheetcraft_chart::Prim], xf: Xf) {
Prim::Text { x, y, text, size, color, bold, align, valign, rotation } => {
let font = if *bold { Font::HelveticaBold } else { Font::Helvetica };
let size = size * k;
let w = sheetcraft_pdf::text_width(font, size, text);
let w = gridcraft_pdf::text_width(font, size, text);
let dx = match align {
H::Left => 0.0,
H::Center => -w / 2.0,
@ -631,18 +631,18 @@ fn wrap_lines(text: &str, font: Font, size: f32, width: f32) -> Vec<String> {
let mut line = String::new();
for word in para.split(' ') {
let cand = if line.is_empty() { word.to_string() } else { format!("{line} {word}") };
if line.is_empty() || sheetcraft_pdf::text_width(font, size, &cand) <= width {
if line.is_empty() || gridcraft_pdf::text_width(font, size, &cand) <= width {
line = cand;
} else {
out.push(std::mem::take(&mut line));
line = word.to_string();
}
// Break a single over-long word by characters.
while sheetcraft_pdf::text_width(font, size, &line) > width && line.chars().count() > 1 {
while gridcraft_pdf::text_width(font, size, &line) > width && line.chars().count() > 1 {
let mut cut = String::new();
let mut rest = String::new();
for ch in line.chars() {
if rest.is_empty() && sheetcraft_pdf::text_width(font, size, &format!("{cut}{ch}")) <= width || cut.is_empty() {
if rest.is_empty() && gridcraft_pdf::text_width(font, size, &format!("{cut}{ch}")) <= width || cut.is_empty() {
cut.push(ch);
} else {
rest.push(ch);
@ -887,9 +887,9 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
} else if code == "General"
&& let Value::Number(n) = v
{
let digit = sheetcraft_pdf::text_width(font, size, "0").max(0.1);
let digit = gridcraft_pdf::text_width(font, size, "0").max(0.1);
let max_chars = (avail / digit).floor().max(1.0) as usize;
sheetcraft_numfmt::format_general_fit(n, max_chars.min(11)).unwrap_or_else(|| "#".repeat(max_chars.min(255)))
gridcraft_numfmt::format_general_fit(n, max_chars.min(11)).unwrap_or_else(|| "#".repeat(max_chars.min(255)))
} else {
crate::display::cell_text(wb, sh, c)
};
@ -899,15 +899,15 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
if text.is_empty() {
return;
}
let mut tw = sheetcraft_pdf::text_width(font, size, &text);
let mut tw = gridcraft_pdf::text_width(font, size, &text);
if st.align.shrink && !wrap && tw > avail && tw > 0.0 {
size = (size * avail / tw).max(1.0);
tw = sheetcraft_pdf::text_width(font, size, &text);
tw = gridcraft_pdf::text_width(font, size, &text);
}
if is_num && !wrap && tw > avail + 0.01 && !sh.show_formulas {
let hash = sheetcraft_pdf::text_width(font, size, "#").max(0.1);
let hash = gridcraft_pdf::text_width(font, size, "#").max(0.1);
text = "#".repeat(((avail / hash).floor().max(1.0) as usize).min(255));
tw = sheetcraft_pdf::text_width(font, size, &text);
tw = gridcraft_pdf::text_width(font, size, &text);
}
let h_align = match st.align.h {
HAlign::General => {
@ -984,7 +984,7 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
let mut yy = y + pad + size * 0.8;
for ch in chars {
let s = ch.to_string();
let cw = sheetcraft_pdf::text_width(font, size, &s);
let cw = gridcraft_pdf::text_width(font, size, &s);
pg.text(x + (w - cw) / 2.0, yy, size, font, color, &s);
yy += line_h;
if yy > y + h + line_h {
@ -995,7 +995,7 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
return;
}
for (i, line) in lines.iter().enumerate() {
let lw = sheetcraft_pdf::text_width(font, size, line);
let lw = gridcraft_pdf::text_width(font, size, line);
let lx = match h_align {
HAlign::Right => x + w - pad - lw - indent,
HAlign::Center => x + (w - lw) / 2.0,
@ -1053,14 +1053,14 @@ fn draw_objects(pg: &mut Page, wb: &Workbook, si: usize, sh: &Sheet, b: &Block)
if !block.contains(sp.cell) {
continue;
}
let vals: Vec<Option<f64>> = match sheetcraft_calc::evaluate(wb, si, sp.cell, &sp.source) {
let vals: Vec<Option<f64>> = match gridcraft_calc::evaluate(wb, si, sp.cell, &sp.source) {
Value::Array(a) => a.iter().take(10_000).map(Value::as_f64).collect(),
v => vec![v.as_f64()],
};
let color = rgb_of(sp.color, wb).unwrap_or([0x15, 0x60, 0x82]);
let (x, y, w, h) = b.rect(RangeRef::cell(sp.cell));
let (wpx, hpx) = (w / b.k, h / b.k);
let prims = sheetcraft_chart::render_sparkline(sp.kind, &vals, [color[0], color[1], color[2], 255], sp.markers, wpx, hpx);
let prims = gridcraft_chart::render_sparkline(sp.kind, &vals, [color[0], color[1], color[2], 255], sp.markers, wpx, hpx);
draw_prims(pg, &prims, Xf { ox: x, oy: y, k: b.k });
}
for im in &sh.images {
@ -1113,7 +1113,7 @@ fn draw_objects(pg: &mut Page, wb: &Workbook, si: usize, sh: &Sheet, b: &Block)
let lines = wrap_lines(&shp.text, Font::Helvetica, size, (w - 4.0 * b.k).max(size));
let mut yy = y + 2.0 * b.k + size * 0.8;
for l in lines.iter().take(500) {
let lw = sheetcraft_pdf::text_width(Font::Helvetica, size, l);
let lw = gridcraft_pdf::text_width(Font::Helvetica, size, l);
let lx = if shp.kind == ShapeKind::TextBox { x + 2.0 * b.k } else { x + (w - lw) / 2.0 };
pg.text(lx, yy, size, Font::Helvetica, [0, 0, 0], l);
yy += size * 1.2;
@ -1126,8 +1126,8 @@ fn draw_objects(pg: &mut Page, wb: &Workbook, si: usize, sh: &Sheet, b: &Block)
continue;
}
let (x, y, _, _) = place(r);
let data = sheetcraft_chart::resolve(wb, si, ch);
let prims = sheetcraft_chart::render(ch, &data, r.2 as f32, r.3 as f32, &PdfMeasure);
let data = gridcraft_chart::resolve(wb, si, ch);
let prims = gridcraft_chart::render(ch, &data, r.2 as f32, r.3 as f32, &PdfMeasure);
draw_prims(pg, &prims, Xf { ox: x, oy: y, k: b.k });
}
pg.restore();
@ -1147,8 +1147,8 @@ fn draw_headings(pg: &mut Page, b: &Block, col_heads: bool, row_heads: bool, at:
}
pg.fill_rect(x0, y, x1 - x0, size.1, fill);
pg.rect_stroke(x0, y, x1 - x0, size.1, lw, line);
let t = sheetcraft_core::col_to_letters(col);
let tw = sheetcraft_pdf::text_width(Font::Helvetica, fsize, &t);
let t = gridcraft_core::col_to_letters(col);
let tw = gridcraft_pdf::text_width(Font::Helvetica, fsize, &t);
pg.text(x0 + (x1 - x0 - tw) / 2.0, y + size.1 / 2.0 + fsize * 0.35, fsize, Font::Helvetica, [0x40, 0x40, 0x40], &t);
}
}
@ -1162,7 +1162,7 @@ fn draw_headings(pg: &mut Page, b: &Block, col_heads: bool, row_heads: bool, at:
pg.fill_rect(x, y0, size.0, y1 - y0, fill);
pg.rect_stroke(x, y0, size.0, y1 - y0, lw, line);
let t = (row + 1).to_string();
let tw = sheetcraft_pdf::text_width(Font::Helvetica, fsize, &t);
let tw = gridcraft_pdf::text_width(Font::Helvetica, fsize, &t);
pg.text(x + (size.0 - tw) / 2.0, y0 + (y1 - y0) / 2.0 + fsize * 0.35, fsize, Font::Helvetica, [0x40, 0x40, 0x40], &t);
}
}
@ -1182,11 +1182,11 @@ fn draw_header_footer(pg: &mut Page, lay: &SheetLayout, cx: &HfContext) {
continue;
}
let size = sec.size.unwrap_or(10.0).clamp(1.0, 72.0);
let font = Font::styled(sheetcraft_pdf::Family::Sans, sec.bold, sec.italic);
let font = Font::styled(gridcraft_pdf::Family::Sans, sec.bold, sec.italic);
let lines: Vec<&str> = sec.text.split('\n').take(20).collect();
let n = lines.len() as f32;
for (j, line) in lines.iter().enumerate() {
let lw = sheetcraft_pdf::text_width(font, size, line);
let lw = gridcraft_pdf::text_width(font, size, line);
let x = match i {
0 => l,
1 => (pw - lw) / 2.0,
@ -1241,7 +1241,7 @@ pub fn render_pdf(wb: &Workbook, layouts: &[SheetLayout], file: &str, path: &str
doc.info.title = if wb.props.title.is_empty() { file.to_string() } else { wb.props.title.clone() };
doc.info.author = wb.props.author.clone();
let total: usize = layouts.iter().map(|l| l.pages.len()).sum();
let now = sheetcraft_calc::now_serial();
let now = gridcraft_calc::now_serial();
let date = crate::display::format(&Value::Number(now.floor()), "m/d/yyyy", wb).text;
let time = crate::display::format(&Value::Number(now), "h:mm AM/PM", wb).text;
let mut page_no = 0usize;
@ -1349,7 +1349,7 @@ fn print_preview(s: &mut Session, p: &Json) -> Result<Json> {
"sheet": name,
"range": r.a1(),
"rows": format!("{}:{}", rb.0 + 1, rb.1 + 1),
"columns": format!("{}:{}", sheetcraft_core::col_to_letters(cb.0), sheetcraft_core::col_to_letters(cb.1)),
"columns": format!("{}:{}", gridcraft_core::col_to_letters(cb.0), gridcraft_core::col_to_letters(cb.1)),
"scale": (lay.scale * 1000.0).round() / 10.0,
"paper": lay.settings.paper,
"orientation": if lay.settings.orientation == Orientation::Landscape { "landscape" } else { "portrait" },
@ -1441,7 +1441,7 @@ fn page_setup(s: &mut Session, p: &Json) -> Result<Json> {
let mut v: Vec<u32> = a
.iter()
.filter_map(|x| match x {
Json::String(t) => sheetcraft_core::letters_to_col(t.trim().trim_start_matches('$')),
Json::String(t) => gridcraft_core::letters_to_col(t.trim().trim_start_matches('$')),
Json::Number(n) => n.as_u64().filter(|c| *c >= 2 && *c <= MAX_COLS as u64).map(|c| c as u32 - 1),
_ => None,
})
@ -1628,7 +1628,7 @@ mod tests {
fn export_writes_file() {
let mut s = s();
s.execute("cell.set", json!({"cell": "B2", "input": "hello"})).unwrap();
let dir = std::env::temp_dir().join(format!("sheetcraft-print-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("gridcraft-print-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("out.pdf");
let r = s.execute("file.exportPdf", json!({"path": path.to_str().unwrap()})).unwrap();
@ -1758,7 +1758,7 @@ mod tests {
assert_eq!(bands(0, 1_000_000, |_| 10.0, |_| 5.0, &[], 3).len(), 3);
let w = wrap_lines("the quick brown fox jumps", Font::Helvetica, 10.0, 50.0);
assert!(w.len() >= 2);
assert!(w.iter().all(|l| sheetcraft_pdf::text_width(Font::Helvetica, 10.0, l) <= 50.0 || !l.contains(' ')));
assert!(w.iter().all(|l| gridcraft_pdf::text_width(Font::Helvetica, 10.0, l) <= 50.0 || !l.contains(' ')));
let w = wrap_lines("Supercalifragilistic", Font::Helvetica, 10.0, 20.0);
assert!(w.len() > 2);
}

View File

@ -1,7 +1,7 @@
//! Review tab: protection, spelling (basic), workbook statistics.
use gridcraft_model::{SheetProtection, password_hash};
use serde_json::{Value as Json, json};
use sheetcraft_model::{SheetProtection, password_hash};
use super::*;

View File

@ -2,10 +2,10 @@
use std::sync::Arc;
use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
use gridcraft_formula::adjust::{Axis, Edit};
use gridcraft_model::{Sheet, Visibility};
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
use sheetcraft_formula::adjust::{Axis, Edit};
use sheetcraft_model::{Sheet, Visibility};
use super::*;
@ -76,15 +76,15 @@ fn shift_sheet_features(sh: &mut Sheet, axis: Axis, at: u32, count: u32, insert:
};
let map_range = |r: RangeRef| -> Option<RangeRef> {
let e = if insert { Edit::Insert { axis, at, count } } else { Edit::Delete { axis, at, count } };
let expr = sheetcraft_formula::Expr::Ref(sheetcraft_formula::Reference {
sheet: sheetcraft_formula::SheetSel::Current,
kind: sheetcraft_formula::RefKind::Range(
sheetcraft_formula::Anchor { row: r.start.row, col: r.start.col, row_abs: false, col_abs: false },
sheetcraft_formula::Anchor { row: r.end.row, col: r.end.col, row_abs: false, col_abs: false },
let expr = gridcraft_formula::Expr::Ref(gridcraft_formula::Reference {
sheet: gridcraft_formula::SheetSel::Current,
kind: gridcraft_formula::RefKind::Range(
gridcraft_formula::Anchor { row: r.start.row, col: r.start.col, row_abs: false, col_abs: false },
gridcraft_formula::Anchor { row: r.end.row, col: r.end.col, row_abs: false, col_abs: false },
),
});
match sheetcraft_formula::adjust::adjust(expr, "S", "S", &e) {
sheetcraft_formula::Expr::Ref(r) => Some(r.range()),
match gridcraft_formula::adjust::adjust(expr, "S", "S", &e) {
gridcraft_formula::Expr::Ref(r) => Some(r.range()),
_ => None,
}
};
@ -122,7 +122,7 @@ fn shift_sheet_features(sh: &mut Sheet, axis: Axis, at: u32, count: u32, insert:
let name = format!("Column{}", t.columns.len() + 1);
t.columns.insert(
(idx + k as usize).min(t.columns.len()),
sheetcraft_model::TableColumn { name, totals: Default::default(), totals_label: None, formula: None },
gridcraft_model::TableColumn { name, totals: Default::default(), totals_label: None, formula: None },
);
}
}
@ -172,14 +172,14 @@ fn shift_sheet_features(sh: &mut Sheet, axis: Axis, at: u32, count: u32, insert:
}
/// Rewrites every formula reference (and chart series) for a structural edit on `target`.
fn rewrite(wb: &mut sheetcraft_model::Workbook, target: &str, e: &Edit) {
fn rewrite(wb: &mut gridcraft_model::Workbook, target: &str, e: &Edit) {
super::edit::rewrite_all_formulas(wb, target, e);
for si in 0..wb.sheets.len() {
let host = wb.sheets.get(si).map(|s| s.name.clone()).unwrap_or_default();
let Some(sh) = wb.sheet_mut(si) else { continue };
let fix = |text: &str| -> String {
match sheetcraft_formula::parse(text) {
Ok(expr) => sheetcraft_formula::print(&sheetcraft_formula::adjust::adjust(expr, &host, target, e)),
match gridcraft_formula::parse(text) {
Ok(expr) => gridcraft_formula::print(&gridcraft_formula::adjust::adjust(expr, &host, target, e)),
Err(_) => text.to_string(),
}
};
@ -222,7 +222,7 @@ fn insert_lines(s: &mut Session, p: &Json, axis: Axis) -> Result<Json> {
if let Some(info) = prev {
for i in at..at + count {
let map = if axis == Axis::Rows { &mut sh.rows } else { &mut sh.cols };
map.insert(i, sheetcraft_model::LineInfo { hidden: false, outline: 0, collapsed: false, ..info });
map.insert(i, gridcraft_model::LineInfo { hidden: false, outline: 0, collapsed: false, ..info });
}
}
}
@ -373,14 +373,14 @@ fn delete_sheet(s: &mut Session, p: &Json) -> Result<Json> {
edit_doc(d, |cx| {
cx.wb.sheets.remove(i);
for si in 0..cx.wb.sheets.len() {
let keys: Vec<(CellRef, Arc<sheetcraft_model::Formula>)> =
let keys: Vec<(CellRef, Arc<gridcraft_model::Formula>)> =
cx.wb.sheets.get(si).map(|s| s.cells.iter().filter_map(|(c, x)| x.formula.clone().map(|f| (c, f))).collect()).unwrap_or_default();
let Some(sh) = cx.wb.sheet_mut(si) else { continue };
for (c, f) in keys {
if let Some(e) = f.expr() {
let ne = sheetcraft_formula::adjust::delete_sheet(e, &name);
let ne = gridcraft_formula::adjust::delete_sheet(e, &name);
if let Some(cell) = sh.cells.get_mut(c) {
cell.formula = Some(Arc::new(sheetcraft_model::Formula::from_expr(ne)));
cell.formula = Some(Arc::new(gridcraft_model::Formula::from_expr(ne)));
}
}
}
@ -414,23 +414,23 @@ fn rename_sheet(s: &mut Session, p: &Json) -> Result<Json> {
let old = cx.wb.sheet(i).map(|s| s.name.clone()).unwrap_or_default();
cx.sheet_mut(i)?.name = name.clone();
for si in 0..cx.wb.sheets.len() {
let keys: Vec<(CellRef, Arc<sheetcraft_model::Formula>)> =
let keys: Vec<(CellRef, Arc<gridcraft_model::Formula>)> =
cx.wb.sheets.get(si).map(|s| s.cells.iter().filter_map(|(c, x)| x.formula.clone().map(|f| (c, f))).collect()).unwrap_or_default();
let Some(sh) = cx.wb.sheet_mut(si) else { continue };
for (c, f) in keys {
if let Some(e) = f.expr() {
let ne = sheetcraft_formula::adjust::rename_sheet(e, &old, &name);
let text = sheetcraft_formula::print(&ne);
let ne = gridcraft_formula::adjust::rename_sheet(e, &old, &name);
let text = gridcraft_formula::print(&ne);
if text != f.text
&& let Some(cell) = sh.cells.get_mut(c)
{
cell.formula = Some(Arc::new(sheetcraft_model::Formula::from_expr(ne)));
cell.formula = Some(Arc::new(gridcraft_model::Formula::from_expr(ne)));
}
}
}
let fix = |t: &str| {
sheetcraft_formula::parse(t)
.map(|e| sheetcraft_formula::print(&sheetcraft_formula::adjust::rename_sheet(e, &old, &name)))
gridcraft_formula::parse(t)
.map(|e| gridcraft_formula::print(&gridcraft_formula::adjust::rename_sheet(e, &old, &name)))
.unwrap_or_else(|_| t.to_string())
};
for ch in sh.charts.iter_mut() {
@ -442,8 +442,8 @@ fn rename_sheet(s: &mut Session, p: &Json) -> Result<Json> {
}
}
for n in cx.wb.names.iter_mut() {
if let Ok(e) = sheetcraft_formula::parse(&n.formula) {
n.formula = sheetcraft_formula::print(&sheetcraft_formula::adjust::rename_sheet(e, &old, &name));
if let Ok(e) = gridcraft_formula::parse(&n.formula) {
n.formula = gridcraft_formula::print(&gridcraft_formula::adjust::rename_sheet(e, &old, &name));
}
}
cx.structural = true;
@ -482,7 +482,7 @@ fn tab_color(s: &mut Session, p: &Json) -> Result<Json> {
let i = target_sheet(s, p)?;
let c = super::format::color_param(p.get("color"));
edit(s, |cx| {
cx.sheet_mut(i)?.tab_color = c.filter(|c| *c != sheetcraft_model::Color::Auto);
cx.sheet_mut(i)?.tab_color = c.filter(|c| *c != gridcraft_model::Color::Auto);
Ok(Json::Null)
})
}

View File

@ -4,8 +4,8 @@
use std::collections::HashSet;
use std::sync::OnceLock;
use gridcraft_core::{RangeRef, Value};
use serde_json::{Value as Json, json};
use sheetcraft_core::{RangeRef, Value};
use super::*;
@ -110,7 +110,7 @@ fn suggestions(w: &str) -> Vec<String> {
fn spelling(s: &mut Session, p: &Json) -> Result<Json> {
if dictionary().is_empty() {
return Err(EngineError::Other("No dictionary is available on this system (SheetCraft uses the system word list).".into()));
return Err(EngineError::Other("No dictionary is available on this system (GridCraft uses the system word list).".into()));
}
let si = target_sheet(s, p)?;
let d = s.doc()?;

View File

@ -1,8 +1,8 @@
//! View tab (sheet view state that's saved with the file) and Page Layout settings.
use gridcraft_core::{CellRef, RangeRef};
use gridcraft_model::Orientation;
use serde_json::{Value as Json, json};
use sheetcraft_core::{CellRef, RangeRef};
use sheetcraft_model::Orientation;
use super::*;
@ -295,7 +295,7 @@ fn header_footer(s: &mut Session, p: &Json) -> Result<Json> {
})
}
/// SheetCraft's own theme set (original palettes).
/// GridCraft's own theme set (original palettes).
pub fn themes() -> Vec<(&'static str, [u32; 12])> {
vec![
("Craft", [0xFFFFFF, 0x000000, 0xE8E8E8, 0x0E2841, 0x156082, 0xE97132, 0x196B24, 0x0F9ED5, 0xA02B93, 0x4EA72E, 0x467886, 0x96607D]),

View File

@ -8,11 +8,11 @@
use std::collections::{HashMap, HashSet};
use gridcraft_calc::Calc;
use gridcraft_core::{CellRef, RangeRef, Value};
use gridcraft_model::{CalcMode, Cell, DefinedName, Sheet, StyleId, Workbook};
use serde::{Deserialize, Serialize};
use serde_json::{Value as Json, json};
use sheetcraft_calc::Calc;
use sheetcraft_core::{CellRef, RangeRef, Value};
use sheetcraft_model::{CalcMode, Cell, DefinedName, Sheet, StyleId, Workbook};
use super::*;
@ -162,7 +162,7 @@ fn json_value(v: &Json, wb: &Workbook) -> Value {
Json::Bool(b) => Value::Bool(*b),
Json::Number(n) => n.as_f64().map(Value::number).unwrap_or_default(),
Json::String(s) if s.is_empty() => Value::Empty,
Json::String(s) => sheetcraft_core::parse::parse_input(s, wb.date_system).value,
Json::String(s) => gridcraft_core::parse::parse_input(s, wb.date_system).value,
other => Value::text(other.to_string()),
}
}
@ -309,7 +309,7 @@ fn goal_seek(s: &mut Session, p: &Json) -> Result<Json> {
let (ssi, set) = cell_arg(s, p, "set", CMD)?;
let (csi, chg) = cell_arg(s, p, "changing", CMD)?;
let target = f64_param(p, "to")
.or_else(|| str_param(p, "to").and_then(sheetcraft_core::parse::parse_number_text))
.or_else(|| str_param(p, "to").and_then(gridcraft_core::parse::parse_number_text))
.filter(|v| v.is_finite())
.ok_or_else(|| bad(CMD, "`to` must be a number"))?;
let d = s.doc()?;
@ -403,7 +403,7 @@ fn store_scenarios(wb: &mut Workbook, list: &[Scenario]) {
.map(|c| format!("\"{}\"", c.iter().collect::<String>().replace('"', "\"\"")))
.collect::<Vec<_>>()
.join("&"),
comment: "SheetCraft Scenario Manager data".into(),
comment: "GridCraft Scenario Manager data".into(),
hidden: true,
});
}
@ -584,7 +584,7 @@ fn scenario_summary(s: &mut Session, p: &Json) -> Result<Json> {
for (i, c) in changing.iter().enumerate() {
let fmt = changing_fmts.get(i).cloned().unwrap_or_default();
let st = cx.wb.styles.derive(StyleId::DEFAULT, |st| st.num_fmt = fmt);
put(&mut sheet, row, 1, Value::text(format!("${}${}", sheetcraft_core::col_to_letters(c.col), c.row + 1)), StyleId::DEFAULT);
put(&mut sheet, row, 1, Value::text(format!("${}${}", gridcraft_core::col_to_letters(c.col), c.row + 1)), StyleId::DEFAULT);
let cur = current.get(i).cloned().unwrap_or_default();
put(&mut sheet, row, 2, cur.clone(), st);
for (k, m) in per.iter().enumerate() {
@ -598,7 +598,7 @@ fn scenario_summary(s: &mut Session, p: &Json) -> Result<Json> {
for (i, c) in results.iter().enumerate() {
let fmt = result_fmts.get(i).cloned().unwrap_or_default();
let st = cx.wb.styles.derive(StyleId::DEFAULT, |st| st.num_fmt = fmt);
put(&mut sheet, row, 1, Value::text(format!("${}${}", sheetcraft_core::col_to_letters(c.col), c.row + 1)), StyleId::DEFAULT);
put(&mut sheet, row, 1, Value::text(format!("${}${}", gridcraft_core::col_to_letters(c.col), c.row + 1)), StyleId::DEFAULT);
put(&mut sheet, row, 2, current_results.get(i).cloned().unwrap_or_default(), st);
for (k, r) in scenario_results.iter().enumerate() {
put(&mut sheet, row, 3 + k as u32, r.get(i).cloned().unwrap_or_default(), st);
@ -613,8 +613,8 @@ fn scenario_summary(s: &mut Session, p: &Json) -> Result<Json> {
Value::text("Notes: Current Values column represents values of changing cells at time Scenario Summary Report was created."),
StyleId::DEFAULT,
);
sheet.cols.insert(1, sheetcraft_model::LineInfo { size: Some(110.0), ..Default::default() });
sheet.cols.insert(2, sheetcraft_model::LineInfo { size: Some(100.0), ..Default::default() });
sheet.cols.insert(1, gridcraft_model::LineInfo { size: Some(110.0), ..Default::default() });
sheet.cols.insert(2, gridcraft_model::LineInfo { size: Some(100.0), ..Default::default() });
cx.wb.sheets.push(std::sync::Arc::new(sheet));
cx.structural = true;
Ok(cx.wb.sheets.len() - 1)
@ -710,7 +710,7 @@ enum Cond {
/// Column offset in the list, comparison operator and operand text.
Compare(u32, Value),
/// A computed criterion: formula relative to the first data row, evaluated at `at`.
Computed(sheetcraft_formula::Expr, CellRef),
Computed(gridcraft_formula::Expr, CellRef),
}
/// Splits `">=10"` into (`">="`, `"10"`); text without an operator means "begins with".
@ -741,7 +741,7 @@ pub(crate) fn criterion_matches(v: &Value, text: &str, crit: &Value) -> bool {
_ => true,
};
}
let num = sheetcraft_core::parse::parse_number_text(operand);
let num = gridcraft_core::parse::parse_number_text(operand);
let lower_text = text.to_lowercase();
let lower_op = operand.to_lowercase();
use std::cmp::Ordering;
@ -750,7 +750,7 @@ pub(crate) fn criterion_matches(v: &Value, text: &str, crit: &Value) -> bool {
(Value::Number(_), None) => None,
(Value::Empty, _) => None,
(_, Some(_)) if op != "begins" => None,
_ => Some(sheetcraft_core::compare_text(text, operand)),
_ => Some(gridcraft_core::compare_text(text, operand)),
};
let wild = |pat: &str| super::edit::wildcard(&lower_text, pat);
match op {
@ -845,8 +845,8 @@ fn advanced_filter(s: &mut Session, p: &Json) -> Result<Json> {
criterion_matches(&v, &text, crit)
}
Cond::Computed(e, at) => {
let shifted = sheetcraft_formula::adjust::shift_relative(e.clone(), row as i64 - first_data as i64, 0);
let v = sheetcraft_calc::recalc::evaluate_expr(wb, csi, *at, &shifted);
let shifted = gridcraft_formula::adjust::shift_relative(e.clone(), row as i64 - first_data as i64, 0);
let v = gridcraft_calc::recalc::evaluate_expr(wb, csi, *at, &shifted);
matches!(v.scalar().to_bool(), Ok(true))
}
})
@ -913,7 +913,7 @@ fn advanced_filter(s: &mut Session, p: &Json) -> Result<Json> {
if *k {
if let Some(e) = shm.rows.get_mut(row) {
e.hidden = false;
if *e == sheetcraft_model::LineInfo::default() {
if *e == gridcraft_model::LineInfo::default() {
shm.rows.remove(row);
}
}
@ -964,7 +964,7 @@ impl Agg {
"countNums" | "countnums" => Value::number(self.nums as f64),
"average" => {
if self.nums == 0 {
Value::Error(sheetcraft_core::CellError::Div0)
Value::Error(gridcraft_core::CellError::Div0)
} else {
Value::number(self.sum / self.nums as f64)
}
@ -1077,8 +1077,8 @@ fn consolidate(s: &mut Session, p: &Json) -> Result<Json> {
}
}
let (out_r0, out_c0) = (dest.row + top as u32, dest.col + left as u32);
if out_r0 as u64 + row_keys.len() as u64 > sheetcraft_core::MAX_ROWS as u64
|| out_c0 as u64 + col_keys.len() as u64 > sheetcraft_core::MAX_COLS as u64
if out_r0 as u64 + row_keys.len() as u64 > gridcraft_core::MAX_ROWS as u64
|| out_c0 as u64 + col_keys.len() as u64 > gridcraft_core::MAX_COLS as u64
{
return Err(EngineError::Other("The consolidated data doesn't fit below and right of the destination.".into()));
}
@ -1121,8 +1121,8 @@ fn consolidate(s: &mut Session, p: &Json) -> Result<Json> {
#[cfg(test)]
mod tests {
use gridcraft_core::{CellRef, Value};
use serde_json::json;
use sheetcraft_core::{CellRef, Value};
use crate::Session;

View File

@ -3,9 +3,9 @@
use std::collections::HashMap;
use std::sync::Mutex;
use sheetcraft_core::{CellRef, RangeRef, Value};
use sheetcraft_model::{Sheet, Workbook};
use sheetcraft_numfmt::{Formatted, NumberFormat, format_value};
use gridcraft_core::{CellRef, RangeRef, Value};
use gridcraft_model::{Sheet, Workbook};
use gridcraft_numfmt::{Formatted, NumberFormat, format_value};
/// Parsed number formats are cached by code.
fn parsed(code: &str) -> NumberFormat {

View File

@ -3,9 +3,9 @@
use std::sync::Arc;
use sheetcraft_core::date::{MONTHS, WEEKDAYS, datetime_from_serial, serial_from_ymd};
use sheetcraft_core::{CellRef, RangeRef, Value};
use sheetcraft_model::{Cell, Formula};
use gridcraft_core::date::{MONTHS, WEEKDAYS, datetime_from_serial, serial_from_ymd};
use gridcraft_core::{CellRef, RangeRef, Value};
use gridcraft_model::{Cell, Formula};
use crate::Result;
use crate::cmd::Ctx;
@ -87,7 +87,7 @@ pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: F
let style = if mode == FillMode::ValuesOnly { sh.cell(*t).map(|c| c.style).unwrap_or_default() } else { cell.style };
if let Some(f) = &cell.formula {
if let Some(e) = f.expr() {
let shifted = sheetcraft_formula::adjust::shift_relative(e, t.row as i64 - sc.row as i64, t.col as i64 - sc.col as i64);
let shifted = gridcraft_formula::adjust::shift_relative(e, t.row as i64 - sc.row as i64, t.col as i64 - sc.col as i64);
cell.formula = Some(Arc::new(Formula::from_expr(shifted)));
cell.value = Value::Empty;
}
@ -151,7 +151,7 @@ enum Pattern {
}
impl Pattern {
fn detect(cells: &[(CellRef, Option<Cell>)], custom: &[Vec<String>], sys: sheetcraft_core::DateSystem, fmt: &str) -> Pattern {
fn detect(cells: &[(CellRef, Option<Cell>)], custom: &[Vec<String>], sys: gridcraft_core::DateSystem, fmt: &str) -> Pattern {
let vals: Vec<Value> = cells.iter().map(|(_, c)| c.as_ref().map(|c| c.value.clone()).unwrap_or_default()).collect();
if cells.iter().any(|(_, c)| c.as_ref().is_some_and(|c| c.formula.is_some())) {
return Pattern::Copy;
@ -258,9 +258,9 @@ impl Pattern {
}
Pattern::Months { last, first, months } => {
let base = if forward { *last } else { *first };
let d = datetime_from_serial(sheetcraft_core::DateSystem::D1900, base)?;
let d = datetime_from_serial(gridcraft_core::DateSystem::D1900, base)?;
let m = d.month as i64 + months * k * sign;
let s = serial_from_ymd(sheetcraft_core::DateSystem::D1900, d.year as i64, m, d.day as i64)?;
let s = serial_from_ymd(gridcraft_core::DateSystem::D1900, d.year as i64, m, d.day as i64)?;
Some(Value::number(s))
}
}
@ -363,7 +363,7 @@ pub fn flash_fill(cx: &mut Ctx, sheet: usize, at: CellRef) -> Result<usize> {
top -= 1;
}
let mut bottom = at.row;
while bottom + 1 < sheetcraft_core::MAX_ROWS && !sh.value(CellRef::new(bottom + 1, src_col)).is_empty() && bottom - top < 1_000_000 {
while bottom + 1 < gridcraft_core::MAX_ROWS && !sh.value(CellRef::new(bottom + 1, src_col)).is_empty() && bottom - top < 1_000_000 {
bottom += 1;
}
// Header row: skip a first row whose target is text and source looks like a header? Keep simple.

View File

@ -2,12 +2,12 @@
use std::fmt::Write as _;
use sheetcraft_core::CellRef;
use sheetcraft_model::Workbook;
use gridcraft_core::CellRef;
use gridcraft_model::Workbook;
use crate::{EngineError, Result};
/// File formats SheetCraft reads or writes.
/// File formats GridCraft reads or writes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FileKind {
Xlsx,
@ -35,14 +35,14 @@ impl FileKind {
/// don't say.
pub fn open_bytes(name: &str, bytes: &[u8]) -> Result<(Workbook, Vec<String>)> {
let kind = FileKind::from_path(name);
let sniffed = sheetcraft_xlsx::sniff(bytes);
if sniffed == sheetcraft_xlsx::Format::Xlsx || kind == Some(FileKind::Xlsx) {
let (wb, report) = sheetcraft_xlsx::read_xlsx(bytes).map_err(|e| EngineError::Other(format!("We can't open '{name}': {e}")))?;
let sniffed = gridcraft_xlsx::sniff(bytes);
if sniffed == gridcraft_xlsx::Format::Xlsx || kind == Some(FileKind::Xlsx) {
let (wb, report) = gridcraft_xlsx::read_xlsx(bytes).map_err(|e| EngineError::Other(format!("We can't open '{name}': {e}")))?;
return Ok((wb, report.warnings));
}
match kind {
Some(FileKind::Json) => {
let mut wb: Workbook = serde_json::from_slice(bytes).map_err(|e| EngineError::Other(format!("not a SheetCraft JSON workbook: {e}")))?;
let mut wb: Workbook = serde_json::from_slice(bytes).map_err(|e| EngineError::Other(format!("not a GridCraft JSON workbook: {e}")))?;
wb.styles.rebuild_index();
if wb.sheets.is_empty() {
wb = Workbook::new();
@ -50,14 +50,14 @@ pub fn open_bytes(name: &str, bytes: &[u8]) -> Result<(Workbook, Vec<String>)> {
Ok((wb, vec![]))
}
Some(FileKind::Tsv) => {
let opts = sheetcraft_xlsx::CsvOptions { delimiter: b'\t', ..Default::default() };
let mut wb = sheetcraft_xlsx::read_csv(bytes, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
let opts = gridcraft_xlsx::CsvOptions { delimiter: b'\t', ..Default::default() };
let mut wb = gridcraft_xlsx::read_csv(bytes, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
rename_first_sheet(&mut wb, name);
Ok((wb, vec![]))
}
_ => {
let opts = sheetcraft_xlsx::CsvOptions { delimiter: 0, ..Default::default() };
let mut wb = sheetcraft_xlsx::read_csv(bytes, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
let opts = gridcraft_xlsx::CsvOptions { delimiter: 0, ..Default::default() };
let mut wb = gridcraft_xlsx::read_csv(bytes, &opts).map_err(|e| EngineError::Other(e.to_string()))?;
rename_first_sheet(&mut wb, name);
Ok((wb, vec![]))
}
@ -80,9 +80,9 @@ fn rename_first_sheet(wb: &mut Workbook, path: &str) {
pub fn save_bytes(wb: &Workbook, path: &str) -> Result<Vec<u8>> {
let sheet = wb.active_sheet;
match FileKind::from_path(path).unwrap_or(FileKind::Xlsx) {
FileKind::Xlsx => sheetcraft_xlsx::write_xlsx(wb).map_err(|e| EngineError::Other(e.to_string())),
FileKind::Csv => Ok(wb.sheet(sheet).map(|sh| sheetcraft_xlsx::write_csv(sh, wb, b',')).unwrap_or_default()),
FileKind::Tsv => Ok(wb.sheet(sheet).map(|sh| sheetcraft_xlsx::write_csv(sh, wb, b'\t')).unwrap_or_default()),
FileKind::Xlsx => gridcraft_xlsx::write_xlsx(wb).map_err(|e| EngineError::Other(e.to_string())),
FileKind::Csv => Ok(wb.sheet(sheet).map(|sh| gridcraft_xlsx::write_csv(sh, wb, b',')).unwrap_or_default()),
FileKind::Tsv => Ok(wb.sheet(sheet).map(|sh| gridcraft_xlsx::write_csv(sh, wb, b'\t')).unwrap_or_default()),
FileKind::Json => serde_json::to_vec_pretty(wb).map_err(|e| EngineError::Other(e.to_string())),
FileKind::Html => Ok(to_html(wb, sheet).into_bytes()),
}
@ -123,16 +123,16 @@ pub fn to_html(wb: &Workbook, sheet: usize) -> String {
if let Some(h) = st.font.color.hex(&wb.theme) {
let _ = write!(css, "color:{h};");
}
if st.fill.pattern != sheetcraft_model::PatternType::None
if st.fill.pattern != gridcraft_model::PatternType::None
&& let Some(h) = st.fill.fg.hex(&wb.theme)
{
let _ = write!(css, "background:{h};");
}
let v = sh.value(c);
let align = match st.align.h {
sheetcraft_model::HAlign::Center => "center",
sheetcraft_model::HAlign::Right => "right",
sheetcraft_model::HAlign::Left => "left",
gridcraft_model::HAlign::Center => "center",
gridcraft_model::HAlign::Right => "right",
gridcraft_model::HAlign::Left => "left",
_ if v.is_number() => "right",
_ => "left",
};

View File

@ -1,4 +1,4 @@
//! The SheetCraft engine façade.
//! The GridCraft engine façade.
//!
//! Every user-visible action is a command with a stable id (`cell.set`, `home.bold`,
//! `data.sortAscending`…) and JSON parameters. The egui UI, the CLI, the control channel and MCP
@ -19,19 +19,19 @@ pub mod tables;
use std::sync::Arc;
use gridcraft_calc::Calc;
use gridcraft_core::{CellRef, RangeRef};
use gridcraft_model::Workbook;
use serde::Serialize;
use serde_json::Value as Json;
use sheetcraft_calc::Calc;
use sheetcraft_core::{CellRef, RangeRef};
use sheetcraft_model::Workbook;
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
pub use gridcraft_calc as calc;
pub use gridcraft_core as core;
pub use gridcraft_formula as formula;
pub use gridcraft_model as model;
pub use gridcraft_numfmt as numfmt;
pub use selection::Selection;
pub use sheetcraft_calc as calc;
pub use sheetcraft_core as core;
pub use sheetcraft_formula as formula;
pub use sheetcraft_model as model;
pub use sheetcraft_numfmt as numfmt;
#[derive(Debug, thiserror::Error)]
pub enum EngineError {
@ -157,9 +157,9 @@ impl Default for Prefs {
Prefs {
enter_direction: "down".into(),
autocomplete: true,
user_name: "SheetCraft User".into(),
default_font: sheetcraft_model::DEFAULT_FONT.into(),
default_font_size: sheetcraft_model::DEFAULT_FONT_SIZE,
user_name: "GridCraft User".into(),
default_font: gridcraft_model::DEFAULT_FONT.into(),
default_font_size: gridcraft_model::DEFAULT_FONT_SIZE,
sheets_in_new_workbook: 1,
r1c1: false,
user_dictionary: vec![],
@ -248,7 +248,7 @@ impl Session {
let mut wb = Workbook::new();
for _ in 1..self.prefs.sheets_in_new_workbook.clamp(1, 255) {
let name = wb.next_sheet_name();
wb.sheets.push(Arc::new(sheetcraft_model::Sheet::new(name)));
wb.sheets.push(Arc::new(gridcraft_model::Sheet::new(name)));
}
self.add_document(DocState::new(wb, None, format!("Book{n}")))
}
@ -314,7 +314,7 @@ impl Session {
}
/// Convenience: the active workbook's active sheet.
pub fn active_sheet(s: &Session) -> Option<&sheetcraft_model::Sheet> {
pub fn active_sheet(s: &Session) -> Option<&gridcraft_model::Sheet> {
s.active().and_then(|d| d.wb.active())
}

View File

@ -8,9 +8,9 @@
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use sheetcraft_core::date::datetime_from_serial;
use sheetcraft_core::{CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value};
use sheetcraft_model::*;
use gridcraft_core::date::datetime_from_serial;
use gridcraft_core::{CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value};
use gridcraft_model::*;
/// Largest source block read (cells).
pub const MAX_SOURCE_CELLS: u64 = 10_000_000;
@ -67,7 +67,7 @@ pub fn quote_sheet(name: &str) -> String {
/// Absolute reference text for a range on a sheet: `Sheet1!$A$1:$E$20`.
pub fn range_text(sheet_name: &str, r: RangeRef) -> String {
let abs = |c: CellRef| format!("${}${}", sheetcraft_core::col_to_letters(c.col), c.row as u64 + 1);
let abs = |c: CellRef| format!("${}${}", gridcraft_core::col_to_letters(c.col), c.row as u64 + 1);
format!("{}!{}:{}", quote_sheet(sheet_name), abs(r.start), abs(r.end))
}

View File

@ -3,8 +3,8 @@
use std::sync::Arc;
use gridcraft_model::{Sheet, Workbook};
use serde_json::json;
use sheetcraft_model::{Sheet, Workbook};
use crate::{DocState, Session};
@ -39,7 +39,7 @@ pub fn build(name: &str) -> Option<Workbook> {
let mut wb = (*s.doc().ok()?.wb).clone();
wb.active_sheet = 0;
if let Some(sh) = wb.sheet_mut(0) {
sh.view_active = sheetcraft_core::CellRef::default();
sh.view_active = gridcraft_core::CellRef::default();
}
Some(wb)
}
@ -100,7 +100,7 @@ fn sales(s: &mut Session) -> bool {
if let Some(sh) = wb.sheet_mut(0)
&& let Some(c) = sh.charts.last_mut()
{
c.series = vec![sheetcraft_model::Series {
c.series = vec![gridcraft_model::Series {
name: Some("Sales!$G$4".into()),
categories: Some("Sales!$B$5:$B$10".into()),
values: "Sales!$G$5:$G$10".into(),

View File

@ -1,8 +1,8 @@
//! Cell selection: one or more ranges with an active cell.
use gridcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
use gridcraft_model::Sheet;
use serde::{Deserialize, Serialize};
use sheetcraft_core::{CellRef, MAX_COLS, MAX_ROWS, RangeRef};
use sheetcraft_model::Sheet;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Selection {
@ -212,7 +212,7 @@ pub fn current_region(sheet: &Sheet, c: CellRef) -> RangeRef {
#[cfg(test)]
mod tests {
use super::*;
use sheetcraft_core::Value;
use gridcraft_core::Value;
#[test]
fn jumps() {

View File

@ -1,8 +1,8 @@
//! Table styles: how a cell inside a table looks (our own palettes, keyed by the familiar
//! `TableStyleLight1…21 / Medium1…28 / Dark1…11` names so files round-trip).
use sheetcraft_core::CellRef;
use sheetcraft_model::*;
use gridcraft_core::CellRef;
use gridcraft_model::*;
/// (family, number) from a style name, e.g. `TableStyleMedium2` → ("Medium", 2).
pub fn parse_style_name(name: &str) -> (&'static str, u32) {

View File

@ -1,5 +1,5 @@
use gridcraft_core::{CellRef, Value};
use serde_json::json;
use sheetcraft_core::{CellRef, Value};
use crate::Session;
@ -182,7 +182,7 @@ fn ink_strokes_and_ink_to_shape() {
s.execute("draw.stroke", json!({"points": pts})).unwrap();
let sh = s.doc().unwrap().wb.active().unwrap().clone();
assert_eq!(sh.shapes.len(), 1);
assert_eq!(sh.shapes[0].kind, sheetcraft_model::ShapeKind::Ink);
assert_eq!(sh.shapes[0].kind, gridcraft_model::ShapeKind::Ink);
let r = s.execute("draw.inkToShape", json!({})).unwrap();
assert_eq!(r["kind"], "Rectangle");
// An open stroke becomes a line.

View File

@ -1,14 +1,14 @@
[package]
name = "sheetcraft-formula"
name = "gridcraft-formula"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "SheetCraft formula language: lexer, parser, printer and reference adjustment"
description = "GridCraft formula language: lexer, parser, printer and reference adjustment"
[lints]
workspace = true
[dependencies]
sheetcraft-core = { workspace = true }
gridcraft-core = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }

View File

@ -1,7 +1,7 @@
//! Reference adjustment: copying formulas, inserting/deleting rows and columns, moving cells,
//! renaming and deleting sheets.
use sheetcraft_core::{CellError, MAX_COLS, MAX_ROWS, RangeRef};
use gridcraft_core::{CellError, MAX_COLS, MAX_ROWS, RangeRef};
use crate::ast::*;

View File

@ -2,8 +2,8 @@
use std::sync::Arc;
use gridcraft_core::{CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef};
use serde::{Deserialize, Serialize};
use sheetcraft_core::{CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef};
/// One end of a reference, with `$` anchors.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]

View File

@ -2,7 +2,7 @@
//!
//! Token spans are byte offsets into the source, which the editor uses to colour references.
use sheetcraft_core::CellError;
use gridcraft_core::CellError;
#[derive(Clone, Debug, PartialEq)]
pub enum Tok {

View File

@ -2,8 +2,8 @@
use std::sync::Arc;
use sheetcraft_core::addr::{letters_to_col, parse_a1_prefix};
use sheetcraft_core::{CellError, MAX_ROWS};
use gridcraft_core::addr::{letters_to_col, parse_a1_prefix};
use gridcraft_core::{CellError, MAX_ROWS};
use crate::ast::*;
use crate::lexer::{Tok, Token, tokenize};

View File

@ -2,7 +2,7 @@
use std::fmt::Write;
use sheetcraft_core::{CellRef, col_to_letters, number_to_text};
use gridcraft_core::{CellRef, col_to_letters, number_to_text};
use crate::ast::*;
@ -16,7 +16,7 @@ pub fn needs_quotes(name: &str) -> bool {
return true;
}
// Looks like a cell ref (`A1`, `R1C1`) or a boolean.
sheetcraft_core::CellRef::parse(name).is_some() || name.eq_ignore_ascii_case("TRUE") || name.eq_ignore_ascii_case("FALSE")
gridcraft_core::CellRef::parse(name).is_some() || name.eq_ignore_ascii_case("TRUE") || name.eq_ignore_ascii_case("FALSE")
}
pub fn quote_sheet(name: &str) -> String {

View File

@ -122,7 +122,7 @@ fn insert_delete() {
#[test]
fn moves_and_renames() {
let mv = Edit::Move { from: sheetcraft_core::RangeRef::parse("A1:B2").unwrap(), to_row: 9, to_col: 3 };
let mv = Edit::Move { from: gridcraft_core::RangeRef::parse("A1:B2").unwrap(), to_row: 9, to_col: 3 };
let e = parse("A1+B2+C3+SUM(A1:B2)").unwrap();
assert_eq!(print(&adjust(e, "S", "S", &mv)), "D10+E11+C3+SUM(D10:E11)");
let e = parse("Old!A1+'Old'!B2").unwrap();
@ -132,7 +132,7 @@ fn moves_and_renames() {
#[test]
fn r1c1() {
let e = parse("A1+$B$2+SUM(C:C)").unwrap();
assert_eq!(print_r1c1(&e, sheetcraft_core::CellRef::new(1, 1)), "R[-1]C[-1]+R2C2+SUM(C[1])");
assert_eq!(print_r1c1(&e, gridcraft_core::CellRef::new(1, 1)), "R[-1]C[-1]+R2C2+SUM(C[1])");
}
#[test]

View File

@ -1,13 +1,13 @@
[package]
name = "sheetcraft-functions"
name = "gridcraft-functions"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "SheetCraft worksheet function library: math, statistics, text, dates, lookup, financial, engineering"
description = "GridCraft worksheet function library: math, statistics, text, dates, lookup, financial, engineering"
[lints]
workspace = true
[dependencies]
sheetcraft-core = { workspace = true }
gridcraft-core = { workspace = true }
regex = { workspace = true }

View File

@ -2,8 +2,8 @@
use std::cmp::Ordering;
use sheetcraft_core::parse::parse_number_text;
use sheetcraft_core::{CellError, Value, compare_text};
use gridcraft_core::parse::parse_number_text;
use gridcraft_core::{CellError, Value, compare_text};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Op {

View File

@ -8,7 +8,7 @@
use std::borrow::Cow;
use sheetcraft_core::{Array, CellError, Value, compare_text};
use gridcraft_core::{Array, CellError, Value, compare_text};
use crate::criteria::Criterion;
use crate::util::{A, R, as_array, has, num_val};
@ -19,7 +19,7 @@ fn find_column(db: &Array, label: &str) -> Option<usize> {
let label = label.trim();
(0..db.cols).find(|&c| match db.get(0, c) {
Some(Value::Text(t)) => compare_text(t.trim(), label).is_eq(),
Some(Value::Number(n)) => sheetcraft_core::number_to_text(*n) == label,
Some(Value::Number(n)) => gridcraft_core::number_to_text(*n) == label,
_ => false,
})
}
@ -54,7 +54,7 @@ fn cell_criterion(v: &Value) -> Option<Criterion> {
let s: &str = t;
let has_op = s.starts_with(['=', '<', '>']);
let plain_text = !has_op
&& sheetcraft_core::parse::parse_number_text(s).is_none()
&& gridcraft_core::parse::parse_number_text(s).is_none()
&& !s.eq_ignore_ascii_case("TRUE")
&& !s.eq_ignore_ascii_case("FALSE")
&& CellError::parse(s).is_none();
@ -75,7 +75,7 @@ fn matching_records(db: &Array, crit: &Array) -> Vec<usize> {
let Some(cr) = cell_criterion(cell) else { continue };
let col = match crit.get(0, c) {
Some(Value::Text(t)) => find_column(db, t),
Some(Value::Number(n)) => find_column(db, &sheetcraft_core::number_to_text(*n)),
Some(Value::Number(n)) => find_column(db, &gridcraft_core::number_to_text(*n)),
_ => None,
};
conds.push((col, cr));
@ -293,7 +293,7 @@ pub(crate) fn specs() -> Vec<FnSpec> {
#[cfg(test)]
mod tests {
use crate::util::testutil::*;
use sheetcraft_core::{CellError, Value};
use gridcraft_core::{CellError, Value};
/// The classic orchard table.
fn db() -> Value {

View File

@ -1,8 +1,8 @@
//! Date and time functions.
use sheetcraft_core::date::{DateTime, datetime_from_serial, days_in_month, is_leap, serial_from_ymd};
use sheetcraft_core::parse::parse_input;
use sheetcraft_core::{CellError, DateSystem, Value};
use gridcraft_core::date::{DateTime, datetime_from_serial, days_in_month, is_leap, serial_from_ymd};
use gridcraft_core::parse::parse_input;
use gridcraft_core::{CellError, DateSystem, Value};
use crate::util::{R, S, arg, as_array, has, num, num_val, opt_bool, opt_num, scalar, text, to_int};
use crate::{Arg, Ctx, FnSpec};
@ -565,7 +565,7 @@ pub(crate) fn specs() -> Vec<FnSpec> {
#[cfg(test)]
mod tests {
use sheetcraft_core::{CellError, Value};
use gridcraft_core::{CellError, Value};
use crate::util::testutil::*;
@ -748,7 +748,7 @@ mod tests {
#[test]
fn system_1904() {
let mut ctx = TestCtx { sys: sheetcraft_core::DateSystem::D1904, ..TestCtx::default() };
let mut ctx = TestCtx { sys: gridcraft_core::DateSystem::D1904, ..TestCtx::default() };
let spec = crate::lookup("DATE").unwrap();
close(crate::call(spec, &[n(2000.0), n(1.0), n(1.0)], &mut ctx), 35064.0);
let spec = crate::lookup("WEEKDAY").unwrap();

View File

@ -9,7 +9,7 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use sheetcraft_core::{Array, CellError, Value, sort_compare};
use gridcraft_core::{Array, CellError, Value, sort_compare};
use crate::util::{A, MAX_CELLS, R, arg, array_val, as_array, has, num, opt_bool, scalar};
use crate::{Arg, Ctx, FnSpec, VAR};
@ -1030,7 +1030,7 @@ pub(crate) fn specs() -> Vec<FnSpec> {
#[cfg(test)]
mod tests {
use sheetcraft_core::{CellError, Value};
use gridcraft_core::{CellError, Value};
use crate::util::testutil::*;

View File

@ -3,7 +3,7 @@
use std::f64::consts::PI;
use sheetcraft_core::{CellError, Value, number_to_text};
use gridcraft_core::{CellError, Value, number_to_text};
use crate::special::{erf, erfc};
use crate::util::{A, R, S, flatten, has, num, num_val, opt_num, scalar, text, text_val};
@ -1123,7 +1123,7 @@ pub(crate) fn specs() -> Vec<FnSpec> {
#[cfg(test)]
mod tests {
use sheetcraft_core::{CellError, Value};
use gridcraft_core::{CellError, Value};
use super::parse_complex;
use crate::util::testutil::*;

View File

@ -3,8 +3,8 @@
//! Sign convention follows Excel: money paid out is negative, money received is positive.
//! Date arguments are serial numbers (truncated) in the 1900 date system.
use sheetcraft_core::date::{datetime_from_serial, days_in_month, serial_from_ymd};
use sheetcraft_core::{CellError, DateSystem, Value};
use gridcraft_core::date::{datetime_from_serial, days_in_month, serial_from_ymd};
use gridcraft_core::{CellError, DateSystem, Value};
use crate::util::{R, S, array_numbers, as_array, flatten, has, num, num_val, numbers, opt_bool, opt_num};
use crate::{Arg, Ctx, FnSpec, VAR};
@ -1447,7 +1447,7 @@ pub(crate) fn specs() -> Vec<FnSpec> {
mod tests {
use crate::Arg;
use crate::util::testutil::*;
use sheetcraft_core::{CellError, DateSystem, Value, date::serial_from_ymd};
use gridcraft_core::{CellError, DateSystem, Value, date::serial_from_ymd};
fn d(y: i64, m: i64, dd: i64) -> Arg {
n(serial_from_ymd(DateSystem::D1900, y, m, dd).unwrap())

Some files were not shown because too many files have changed in this diff Show More