Rename SheetCraft to GridCraft everywhere (crates, apps, bundle ids, packaging, docs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
14
AGENTS.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
14
CLAUDE.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
28
Cargo.toml
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
|||
62
README.md
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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(())
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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"
|
||||
|
|
@ -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…</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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>")),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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]
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -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(),
|
||||
|
|
@ -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…</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -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 |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
|
@ -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 |
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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('"', "\"\""))
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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]),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||