Sheets
#Sheets
This repo holds two things, sharing one lineage of IronCalc work:
lib/—@vektorapp/spreadsheet, a Solid spreadsheet library built on IronCalc's engine and a port of its canvas grid renderer. vektor installs it from this repo as a git dependency, so its build indist/is committed. Seelib/README.md.app/— Sheets, the desktop editor described below.
The two fixes in app/patches/ (pixel-smooth scrolling, bounded text wrapping)
are re-implemented in the library's port; see lib/grid/README.md.
#Sheets
A minimal, offline desktop table editor. It embeds the IronCalc spreadsheet UI (WebAssembly engine + React grid) inside a Tauri shell, and reads/writes real .xlsx files. Single file at a time — no accounts, no cloud, no sharing.
#Features
- Open / edit / save
.xlsxworkbooks,.csvfiles, and IronCalc's native.icformat. - Formulas, formatting, multiple sheets — the full IronCalc grid.
- Native menu bar — File ▸ New / Open… / Save / Save As… with
⌘N / ⌘O / ⌘S / ⌘⇧S. - File ▸ Export ▸ Excel Workbook (
.xlsx) or CSV (.csv, active sheet) — a copy, without changing the file you're editing. - Open files by dragging them onto the window, or by double-clicking an
.xlsx/.csv/.icin Finder (see Install on macOS). - Fully offline; nothing leaves your machine.
#How it works
IronCalc splits across the JS/Rust boundary:
- Frontend (
app/src/) — React + Vite. The IronCalc WASM engine edits the workbook in memory. It only understands IronCalc's native.icbyte format. - Backend (
app/src-tauri/) — Rust. All.xlsx⇄.icconversion happens here via theironcalccrate, exposed through three commands (open_file,save_file,save_file_as) that also drive the native file dialogs. CSV is parsed/rendered here too (see below).
#CSV
Opening a .csv builds a one-sheet workbook: the separator is sniffed (,, ;
or tab), quoted fields follow RFC 4180, and non-UTF-8 files fall back to
Latin-1. Fields go in through set_user_input, so numbers, dates and =
formulas are recognised exactly as if typed into the grid.
Saving a document whose path ends in .csv writes CSV back — the active sheet
only, as displayed values (formulas become their results, formatting is lost).
Use Save As… with .xlsx to keep everything.
#The vendored IronCalc packages
IronCalc's .ic format is bitcode-serialized and is only compatible between a WASM build and the Rust crate when both come from the same commit. The published npm packages don't line up with any released crate, so this repo vendors WASM + UI built from source at tag v0.7.1 (matching ironcalc = "0.7.1" in app/src-tauri/Cargo.toml):
app/vendor/ironcalc-wasm/—@ironcalc/wasm,wasm-pack --target webapp/vendor/ironcalc-workbook/—@ironcalc/workbook, the React UI (vite build)
The UI is built with the local patches in app/patches/ applied on top of the tag: the grid scrolls smoothly by the pixel instead of jumping a whole row/column, and it no longer re-wraps every visible cell's text on every frame. See app/patches/README.md.
To rebuild them (e.g. to bump the pinned version), run task vendor (see app/scripts/build-ironcalc.sh) then bun install to refresh the copies under node_modules.
#Prerequisites
- Bun and Rust (Tauri needs a native toolchain).
mise installprovides Bun + Task; Rust is expected system-wide.- To rebuild the vendored packages you also need
wasm-pack, thewasm32-unknown-unknowntarget, andpython3.
#Develop
task dev # or: bun run tauri dev
First run compiles the Rust backend (a few minutes); afterwards the frontend hot-reloads.
#Build
The library builds separately, and its output is committed:
task build:lib # vite + tsc -> dist/, which is in git
task build:app # or, from app/: bun run tauri build -> native bundle in app/src-tauri/target/release/bundle
task web runs just the Vite frontend in a browser, but file open/save need the Tauri shell.
#Install on macOS
task install # build, copy to /Applications, sign, register file associations
Sheets then appears under Open With for .xlsx, .csv and .ic, and double-clicking
one opens it (Get Info ▸ Open with ▸ Change All… to make it the default).
#Why double-clicking a downloaded .xlsx fails
tauri build leaves the bundle linker-signed only — codesign -dvvv reports
Info.plist=not bound and Sealed Resources=none. app/scripts/sign-macos.sh
re-signs it ad-hoc, which binds Info.plist and seals resources; task build
and task install run it automatically, and task sign re-runs it alone.
That makes the bundle assessable, but ad-hoc is not trusted:
$ spctl -a -vvv -t exec /Applications/Sheets.app /Applications/Sheets.app: rejected
macOS will not launch a Gatekeeper-rejected app to open a quarantined
document — any file that arrived via a browser, Mail or Slack carries
com.apple.quarantine. Finder reports this as:
Apple could not verify "….xlsx" is free of malware that may harm your Mac.
The block happens inside LaunchServices before Sheets starts, so it cannot be
handled in app code. Notarization is the only fix. (spctl --add, the old
per-app whitelist, was removed in macOS 15 — see man spctl; the documented
replacement is an MDM SystemPolicyRule profile.) Opening the same file from
File ▸ Open inside a running Sheets is unaffected, since no LaunchServices
assessment is involved.
#Signing + notarizing properly
Requires a paid Apple Developer Program membership ($99/yr). A free Apple ID issues only "Apple Development" certificates, which cannot be notarized.
-
Get a Developer ID certificate. Xcode ▸ Settings ▸ Accounts ▸ (your Apple ID) ▸ Manage Certificates ▸ + ▸ Developer ID Application. Then confirm it landed in the login keychain:
security find-identity -v -p codesigning # 1) ABC123… "Developer ID Application: Your Name (TEAMID)"
-
Store notarization credentials once. Create an app-specific password at appleid.apple.com ▸ Sign-In and Security ▸ App-Specific Passwords.
xcrun notarytool store-credentials "sheets" \ --apple-id "you@example.com" --team-id TEAMID --password "xxxx-xxxx-xxxx-xxxx"
-
Build. Tauri signs the app and, when the
APPLE_*notarization variables are present, submits it to Apple and staples the ticket — all before the.dmgis assembled.export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)" export APPLE_ID="you@example.com" export APPLE_PASSWORD="xxxx-xxxx-xxxx-xxxx" # the app-specific password export APPLE_TEAM_ID="TEAMID" task install
With
APPLE_SIGNING_IDENTITYset,sign-macos.shdeliberately does not re-sign — that would invalidate the stapled notarization ticket. It only refreshes the LaunchServices registration and prints the resulting state. -
Verify. The build prints both of these; they should read:
Gatekeeper: accepted source=Notarized Developer ID Stapled: The validate action worked!
Notarization is what makes the hardened runtime mandatory, which is why the
build passes --options runtime with app/src-tauri/entitlements.plist.
#Licence
The library in lib/ is MIT (see LICENSE). IronCalc, which both
halves build on, is dual-licensed MIT OR Apache-2.0.