Commit Graph
1861 Commits
Author SHA1 Message Date
aiongg 6194f8241d Merge storytold/gridcraft#2522: Charts: 100% Stacked Area draws each value as a share of its category's total
# Conflicts:
#	ROADMAP.md
2026-10-25 02:54:23 +00:00
aiongg 1e50c72698 Merge storytold/gridcraft#2488: Headers and footers: printing follows the Scale with Document and Align with Page Margins check boxes
# Conflicts:
#	docs/ui-parity.md
2026-10-25 02:54:23 +00:00
aiongg b58846a549 Merge storytold/gridcraft#2482: Outline: Settings puts summary rows above and summary columns left of their detail
# Conflicts:
#	docs/ui-parity.md
2026-10-25 02:54:23 +00:00
aiongg 1713334579 Merge storytold/gridcraft#2477: Sheet tabs: drag the handle at the scrollbar's left edge to give the tabs more or less room
# Conflicts:
#	ROADMAP.md
#	docs/ui-parity.md
2026-10-25 02:54:22 +00:00
aiongg cb1fb89549 Merge storytold/gridcraft#2467: PivotTables: right-clicking a value cell offers Show Details in the cell menu
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/cmd/pivot.rs
#	docs/ui-parity.md
2026-10-25 02:46:38 +00:00
aiongg 75f7df9da6 Merge storytold/gridcraft#2461: PivotTables: Enable show details turns Show Details off for a PivotTable
# Conflicts:
#	ROADMAP.md
2026-10-25 02:46:16 +00:00
aiongg 8356b33c78 Merge storytold/gridcraft#2456: Fill handle: Fill Days, Fill Weekdays, Fill Months and Fill Years for a date
# Conflicts:
#	ROADMAP.md
#	docs/ui-parity.md
2026-10-25 02:46:16 +00:00
aiongg 2741680144 Merge storytold/gridcraft#2427: Keyboard: Ctrl+Shift+U expands and collapses the formula bar instead of underlining
# Conflicts:
#	ROADMAP.md
#	docs/ui-parity.md
2026-10-25 02:46:16 +00:00
aiongg 924d36e247 Merge storytold/gridcraft#2421: Status bar: a right-click opens a statistics menu, with Numerical Count, Minimum and Maximum added
# Conflicts:
#	ROADMAP.md
#	crates/ui-egui/src/tabs.rs
#	docs/ui-parity.md
2026-10-25 02:25:39 +00:00
aiongg ae88b40f58 Merge storytold/gridcraft#2414: Go To: lists the named ranges of every sheet and the references entered, and goes to several at once
# Conflicts:
#	docs/ui-parity.md
2026-10-25 02:25:15 +00:00
aiongg 0217dbdfd8 Merge storytold/gridcraft#2410: Formulas: F3 opens Paste Name, which inserts a defined name or pastes the list of names 2026-10-25 02:24:49 +00:00
aiongg 91a1d18c91 Merge storytold/gridcraft#2510: Status bar: Circular References names a cell only when a loop is on the active sheet 2026-10-25 02:24:49 +00:00
aiongg 58aa0f1df2 Merge storytold/gridcraft#2504: Circular references: Error Checking and the status bar quote sheet names that need quotes 2026-10-25 02:24:49 +00:00
aiongg 2660973286 Merge storytold/gridcraft#2501: Error Checking: its arrow opens a Circular References submenu listing a cell on each loop 2026-10-25 02:24:49 +00:00
aiongg 44cc84edf2 Merge storytold/gridcraft#1809: Calc: a LAMBDA that calls itself without end answers #NUM! instead of overflowing the stack 2026-10-25 02:24:49 +00:00
aiongg f1b06a9481 Merge storytold/gridcraft#2486: docs: Different First Page and Different Odd & Even Pages are no longer listed as missing 2026-10-25 02:24:49 +00:00
aiongg bb6f38d95d Merge storytold/gridcraft#2434: Tests: the engine's file-system tests each use their own temporary folder 2026-10-25 02:24:44 +00:00
dotCooCoo 0eb2f1f6f0 Charts: 100% Stacked Area draws each value as a share of its category's total
Insert > Line or Area and the All Charts list offer 100% Stacked Area,
and insert.chart and chart.set take it as type area with subtype
stacked100. Each series' area stacks on the ones before it as its
share of the category's total. When every series on the value axis is
100% stacked and no value is negative, the axis runs from 0% to 100%.
A blank cell counts as zero, and a category whose total is zero stays
on the baseline.

An XLSX area chart with percentStacked grouping now opens as 100%
stacked and is saved back as percentStacked. It used to open as a
stacked area and was saved as stacked.
2026-10-24 16:42:59 -07:00
dotCooCoo a2e4b2b1da Error Checking: the Circular References submenu scrolls
The submenu lists up to 100 cells. When the list was taller than the
window, the cells below its bottom edge could not be reached. The list
now sits in a scroll area 400 points high.
2026-10-24 15:48:55 -07:00
dotCooCoo a75224617b Status bar: Circular References names a cell only when a loop is on the active sheet
The status bar named the first circular reference in the list, even
when it was on another sheet. It now names the first one on the active
sheet, and shows only Circular References when every loop is on other
sheets, as Excel's status bar does.
2026-10-24 15:43:52 -07:00
dotCooCoo 1dd07b7f75 Circular references: Error Checking and the status bar quote sheet names that need quotes
formulas.errorChecking listed each circular reference as Sheet!A1 with
the sheet name as it is, so a sheet named Bob's Q1 gave Bob's Q1!B2,
which a formula does not accept as a reference. The list now quotes the
name with quote_sheet, as references are written elsewhere in the
engine ('Bob''s Q1'!B2), and the status bar's Circular References
label uses the same form.
2026-10-24 14:51:06 -07:00
dotCooCoo 71669dae6d Error Checking: its arrow opens a Circular References submenu listing a cell on each loop
Formulas > Error Checking is now a split button. The button opens the
Error Checking dialog as before, and its arrow opens a menu with Error
Checking… and Circular References. Circular References lists a cell on
each circular reference as Sheet!A1, up to 100, and picking one selects
it on its own sheet and scrolls the grid to it. With no circular
reference the item is unavailable.

The calculation kept only the loops its latest run found, so typing in
any cell outside a loop emptied the list, and the status bar's Circular
References with it, while the loop was still there. A partial
recalculation now keeps the loops among the formulas it does not
evaluate. In automatic calculation a loop leaves the list once an edit
breaks it. In manual calculation only the edited formulas run, so a
listed cell stays until it is edited or the workbook is recalculated.
A full recalculation or iterative calculation builds the list again.
2026-10-24 14:28:19 -07:00
dotCooCoo b745c136b2 Headers and footers: printing follows the Scale with Document and Align with Page Margins check boxes
The Header & Footer tab now has the Scale with Document and Align with
Page Margins check boxes, and pageLayout.headerFooter takes
scaleWithDoc and alignWithMargins. XLSX already kept both settings,
but nothing else read them.

With Scale with Document on, header and footer text prints at the
sheet's print scale. With it off, the text keeps its point size. With
Align with Page Margins on, the left and right sections line up with
the page's side margins. With it off, they start 0.7 inch from the
paper's edges, or a quarter of the page width on paper narrower than
2.8 inches. The Page Layout view draws headers and footers and takes
clicks on them the same way. Both settings are on by default, so a
sheet printed at a scale other than 100% now prints its header and
footer text at that scale.
2026-10-24 13:01:00 -07:00
dotCooCoo 9425358faa docs: Different First Page and Different Odd & Even Pages are no longer listed as missing
The Page Layout view row in docs/ui-parity.md listed both options as
missing. The Header & Footer tab has had both check boxes since #344, and
the first page and the even pages print and show their own header and
footer. The row now says so and lists Scale with Document and Align with
Page Margins as missing instead: XLSX keeps both settings, but no control
shows them and printing does not read them.
2026-10-24 12:16:24 -07:00
dotCooCoo 118cd15d93 Outline: Settings puts summary rows above and summary columns left of their detail
Data > Outline > Settings now has Summary rows below detail and Summary
columns to right of detail. With one cleared, each group's - / + button
sits on the line before the group, its bracket runs down to the last
detail line, and Hide Detail, Show Detail, the level buttons and
Ungroup mark or clear the collapsed flag on that line. Changing a
direction moves the flags of collapsed groups to their new summary
lines.

- model: Sheet::summary_below and summary_right (on by default, also
  for older JSON).
- xlsx: read and written as sheetPr/outlinePr summaryBelow and
  summaryRight; files that set them no longer lose them on save.
- engine: data.outlineSettings {sheet?, summaryBelow?, summaryRight?},
  undoable; outline::groups_with places the summary line.
- ui: a Settings button in the Data tab's Outline group opens the
  dialog; the outline band follows the setting.
2026-10-24 11:34:35 -07:00
dotCooCoo 010eddd25e PivotTables: the cell menu computes a report once for all its cells
The cell menu checked whether to offer Show Details by computing the
whole report again for each new active cell. pivot::detail_cells now
returns every value, subtotal and grand total cell of a report from one
computation, and the menu caches that set per document, revision, sheet
and PivotTable.
2026-10-24 11:27:58 -07:00
dotCooCoo 7c10db07d9 PivotTables: Show Details reports the turned-off option before an out-of-date report
With Enable show details off, Show Details on a value cell of a report whose
source has changed gave the out-of-date error. It now gives the turned-off
message, and a label still reports that it is not a value cell. The message
names pivot.options with enableShowDetails set to true, since there is no
PivotTable Options dialog.
2026-10-24 10:31:09 -07:00
dotCooCoo 3d41bdb956 Sheet tabs: drag the handle at the scrollbar's left edge to give the tabs more or less room
The sheet tabs took up to 60% of the tab bar and the horizontal
scrollbar the rest, and nothing between them could be dragged. The
gap at the scrollbar's left edge is now a handle: it shows the resize
cursor, and dragging it right gives the tabs more of the bar, until the
scrollbar is 100 px wide. After the window is narrowed, the scrollbar
is still 100 px wide. Dragging it left gives the scrollbar more, down
to the New sheet button. Room the tabs do not fill stays empty, and
New sheet stays after the last tab.

Each open workbook keeps its own share while it is open. The share is
not saved in the file, and a workbook starts at the default.
2026-10-24 10:01:58 -07:00
dotCooCoo 28efc9dabf Docs: list the date fill modes in the ui-parity history row without table pipes 2026-10-24 09:25:36 -07:00
dotCooCoo 3fa6cb470b PivotTables: right-clicking a value cell offers Show Details in the cell menu
The cell menu of a PivotTable's value, subtotal or grand total cell now
has a Show Details item after Sort Z to A. It runs pivot.showDetails on
the active cell, which lists the cell's source records on a new sheet,
as double-clicking the cell does. Labels and headers of a current
report, and cells outside a report, do not offer it. In a report that
is out of date every cell of the report offers the item, and running it
shows the message that asks for a refresh.

Whether the menu offers the item is cached per document, revision,
sheet and active cell.
2026-10-24 09:04:33 -07:00
dotCooCoo 30126a709a PivotTables: Enable show details turns Show Details off for a PivotTable
A PivotTable has an Enable show details option, set with the new
pivot.options command ({pivot?, enableShowDetails?: bool}). While it is
off, pivot.showDetails and double-clicking a value cell list no records:
the double-click shows a message and does not open the cell for editing.
Labels and headers still edit. Changing the option is one undo step and
does not refresh the report. pivot.fields reports the setting.

XLSX files keep it: an off option is written as enableDrill="0" on the
pivotTableDefinition, and enableDrill is read on open (on when absent).
2026-10-24 07:46:29 -07:00
dotCooCoo 46f6157800 Merge main into fix/lambda-recursion-stack 2026-10-24 07:29:00 -07:00
dotCooCoo 2ae2329090 Docs: drop the stale missing-keys row that still listed Ctrl+6
Ctrl+6 hides and shows objects, and the row above already lists the other missing keys (Ctrl+Q, Ctrl+A after a function name, Ctrl+Alt+P).
2026-10-24 07:26:48 -07:00
dotCooCoo 00edfd0690 Fill handle: Fill Days, Fill Weekdays, Fill Months and Fill Years for a date
When a fill starts from a date, the right-drag fill menu and the Auto
Fill Options button now also offer Fill Days, Fill Weekdays, Fill Months
and Fill Years. edit.autoFill takes them as the modes days, weekdays,
months and years.

A lone date steps by one unit. Two or more dates step by the difference
between the first two in that unit, or by one unit when both fall in
the same one. Fill Weekdays skips Saturday and Sunday, and a start on a
weekend moves to the next weekday first. Fill Months and Fill Years keep
the day of the month, clamped to the month's last day. A source that is
not all dates fills as a plain drag does.

Fills by months or years now keep the time of day of a date with a
time. A plain drag of dates a month apart and Series by Month or Year
used to drop it.
2026-10-24 06:56:26 -07:00
Dylan M. Taylor 3099df934e M7: verify Agile encryption's dataIntegrity before returning plaintext (#2425)
Agile decryption checked only the password verifier, which authenticates
the key — not the package. An attacker who could replace EncryptedPackage
ciphertext could change workbook content undetected ([MS-OFFCRYPTO]
2.3.4.14, the descriptor's dataIntegrity element).

The reader now requires the dataIntegrity pair, decrypts it with the
recovered package key, and checks HMAC(hash, hmacKey) over the whole
EncryptedPackage stream (StreamSize field included) against the stored
value before any plaintext is returned. Excel always writes the element
("SHOULD" in the spec); descriptors without it are refused as damaged
rather than accepted unauthenticated.

- crypto: RFC 2104/4231 HMAC test vectors, RFC-test cases for a key
  longer than the block size
- fixtures write the pair like Excel; new tests: missing/empty/ragged
  integrity fields, tampered ciphertext and descriptor, a changed
  StreamSize field
2026-10-24 07:58:13 -04:00
dotCooCoo d0fe8b6423 Tests: the engine's file-system tests each use their own temporary folder
save_fails_upfront_when_the_temp_name_is_taken,
save_keeps_the_destination_access_restrictions and
open_refuses_directories_and_unknowable_lengths in crates/engine/src/io.rs
all worked in temp_dir()/gridcraft-fs-safety and removed it at the start
and the end. cargo test runs them on parallel threads, so one test could
delete the folder while another was writing to it, and the other test
failed. Each test now uses its own folder, named with a short label and
the process id, as the gridcraft-write and gridcraft-sandbox tests in the
same file do.
2026-10-24 04:54:53 -07:00
dotCooCoo 07890b47f8 Keyboard: Ctrl+Shift+U expands and collapses the formula bar instead of underlining
Ctrl+Shift+U ran the Ctrl+U shortcut on Windows and Linux and underlined
the selection. In Excel it expands or collapses the formula bar, on
Windows and as Control+Shift+U on a Mac. It now does that on the grid
and in the cell editor or formula bar while editing, and the entry stays
open.

The new UI command view.expandFormulaBar {on?} sets the state, or
toggles it without a parameter, and returns {on}. The chevron at the end
of the formula bar is unchanged.
2026-10-24 03:55:07 -07:00
Dylan M. Taylor 10bec74810 M6: misc hardening — unbounded picture cache, stats walk, signing tmpdir, web clipboard, dash budget, fs cycle, rezip loop (#2422)
- floating_pictures: draw without retaining when eviction can't free
  enough room, so a large workbook can't pin the cache past its budget
- xtask stats: skip unreadable subtrees instead of aborting the walk
- import-cert.sh: per-run private temp dir instead of predictable
  paths in a shared root
- gridcraft-web: gate clipboard ops behind user activation
- chart raster: cap dash rendering to the stroked budget
- cli check: mark directories visited by identity on unix so hard-link
  loops can't recurse; reuse one decompression of a reused part
2026-10-24 06:33:19 -04:00
Dylan M. Taylor a005d8235f M2.9: close protection gaps a trusted script or other sheet would slip through (sec scan findings 17,20,24) (#2420)
- scripts: refuse `window.*` steps like file and link steps — activating
  another window repointed every later step at a different open document
  (finding 17).
- format: on a protected sheet, formatting may not change a cell's
  protection flags: the format-cells permission can't drop `locked` and
  then authorize content edits the password gates (finding 20).
- sheet.moveColumns: the insert/delete protection checks now carry the
  command's own `sheet` parameter, so the sheet modified is the sheet
  authorized, not whichever is active (finding 24).

Tests: unlock-via-format refused, cross-sheet move refused, window switch
refused, plus the positive controls. Engine: 462 tests pass; clippy clean.
2026-10-24 06:15:24 -04:00
Dylan M. Taylor 48d9ba264c M2.9: harden native file open/save (sec scan findings 15,22,25) (#2418)
- read_file: verify the opened file is regular and cap the bytes actually
  taken at the 2 GB limit — a workbook-named symlink to an endless device
  reported length 0 but streamed forever (finding 22).
- write_file: create the temporary file exclusively, so a planted file or
  symlink at the predictable `book.xlsx.sctmp` path makes the save fail
  instead of truncating the link's target (finding 25).
- write_file: inherit the destination's permissions, so saving an
  owner-only workbook doesn't become world-readable through the rename
  (finding 15); new files note restrictive defaults.

Tests: exclusive temp creation vs planted symlink, permissions preserved,
directory refused on open. Engine: 460 tests pass; clippy clean.
2026-10-24 05:46:35 -04:00
Dylan M. Taylor 89d8e81941 M2.9: bound print/export/autofit work in the engine (sec scan findings 18,21,27,28,30) (#2416)
- clip_html: cap wildcard/glob lookback to a 12-byte window instead of scanning
  outgoing text arbitrarily far for a semicolon (finding 18).
- print: wrap_lines binary-searches its break instead of rescanning the line
  from the start for every band (finding 21).
- autofit: per-row merge index instead of scanning every merge per row during
  autofit (finding 27) — behaviour covered by the existing autofit merge test.
- Save as Web Page: the used rectangle bounds the HTML (finding 28, #2387) —
  a tall/wide or sparse far-right sheet now errors instead of growing
  gigabytes of <td></td>.
- print: decode each embedded picture once per document instead of once per
  band that shows it (finding 30).

Engine: 459 tests pass; clippy clean.
2026-10-24 05:46:32 -04:00
Dylan M. TaylorandCodebuff 6fac430f55 M2.9: cap xlsx import work and gate hostile datetime field lengths (#2413)
Security-sweep findings from the scan at revision ac8da7d4 (storytold-
gridcraft-agent-findings.json), no longer exploitable on main at 72c1d57d.
Each change caps work or storage per sheet/part so a hostile file cannot buy
unbounded work with ordinary bytes; each has a regression test:

- finding 2 (package.rs): a part rejected by the MAX_PART size gate is
  charged its declared compressed size up front, so a loop of oversized
  parts still ends the allowance instead of re-entering the package.
- finding 4 (xml.rs): elements past the kept-node cap are consumed and
  dropped instead of kept — parse cost stays linear, memory stops growing
  with the same input (a small compressed workbook previously asked for
  gigabytes of declared-sized metadata tree).
- finding 10 (records.rs): .remaining() caches the suffix of a shared
  string's Continue parts, so crossing N Continue records is O(N) instead
  of O(N^2) in records scanned per character.
- finding 9 (xls shared formulas): the SHRFMLA master template is parsed
  once and re-emitted per dependent cell instead of decoded and reprinted
  for every referencing cell.
- findings 3, 6, 8 (sheet_read.rs): hyperlink-cell expansion, conditional-
  format kept range cells and dynamic-array (deleted-spill) checks are
  capped with one import warning each instead of the compact-workbook
  multi-gigabyte allocation or the scanned-per-cell cost.
- finding 31 (core/parse.rs): datetime recognition no longer reparses a
  prefix at every space with a time-shaped tail; an all-digit field length
  is gated before the derivation machinery runs, staying linear in the
  text.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-24 05:46:28 -04:00
dotCooCoo 7bc390d0fb Status bar: a right-click opens a statistics menu, with Numerical Count, Minimum and Maximum added
The status bar always showed Average, Count and Sum for a selection of
several cells and had no menu. A right-click on it now opens a menu with
Average, Count, Numerical Count, Minimum, Maximum and Sum, as in Excel.
Average, Count and Sum are on by default; the other three are off.

The text on the status bar can no longer be selected, so a right-click
on the mode text or a statistic opens the menu as well. The zoom and
view controls handle their own clicks as before.

The choice is a preference set with the new view.statusBar command and
kept in prefs.json. A prefs.json without it keeps the defaults.
2026-10-24 02:39:50 -07:00
Dylan M. TaylorandCodebuff f284f6a825 M2.9: budget lambda work and bound wildcard substring search (#2415)
Sweep findings from the ac8da7d4 security scan (storytold-gridcraft-agent-
findings.json), remaining on main at 72c1d57d:

- finding 14 (crates/calc/src/eval.rs:988): every lambda invocation now
  spends one charge against a per-cell work budget (16M — one per cell of
  the largest array a reference may expand to), so a hostile, deeply
  nested LAMBDA/LET formula stops at the bound a legitimately wide one
  could reach instead of growing exponentially with nesting. Exhaustion
  is #CALC!, like the existing depth cap. Metered at `call_lambda_values`
  so every path into a body is counted (direct application, MAP/REDUCE/
  SCAN/BYROW/BYCOL/MAKEARRAY, D-function criteria); the criteria
  sub-evaluator hands its spend back to the parent.
- finding 16 (crates/functions/src/criteria.rs:313): `match_from` takes a
  step budget (1<<26: an honest match against Excel's 32,767-char cell
  ceiling costs a few hundred thousand steps), and `wildcard_find` drops
  the leading star run (a no-op for a prefix match) so the first literal
  anchor skips straight to its occurrences; all suffix attempts share one
  budget. Star churn degrades to no-match instead of quadratic work.

Unit tests: mock-host exhaustion/under-budget for lambda applications
(eval::fuel_tests), nested REDUCE under budget in full recalc, wildcard
behaviour unchanged.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-24 05:07:08 -04:00
dotCooCoo 983d3d7e6a Go To: lists the named ranges of every sheet and the references entered, and goes to several at once
The Go To dialog lists the named ranges of the workbook and of every
sheet, a name scoped to another sheet as Sheet2!Name, without hidden
names, constants or formulas. After them it lists the references
entered in the dialog, newest first, and a double-click on an entry
goes there.

edit.goTo takes several names or references separated by commas
(Price, Type or B14:C22,F19:G30) and selects them all, follows a name
that refers to several areas, and goes to Sheet2!Name on Sheet2. With
record: true it keeps a reference that holds no name among the recent
ones, and with list: true it returns the names and the recent
references.
2026-10-24 01:22:19 -07:00
dotCooCoo 00b65d7e8c Merge main into fix/lambda-recursion-stack 2026-10-24 00:38:11 -07:00
dotCooCoo 3557b9f6cd Formulas: F3 opens Paste Name, which inserts a defined name or pastes the list of names
formulas.pasteName lists the names the active sheet can use (workbook
names and the sheet's own, without hidden names, in alphabetical order).
With name it puts the name at the caret of the formula being edited,
replacing selected text, or starts =Name; with list: true it writes each
name and the text of what it refers to in two columns from the active
cell. The Paste Name dialog opens from F3 when the workbook has names,
from Use in Formula's new Paste Names… item and from Alt+M, S, P. While
a dialog is open over the edit, the editor takes no keys.

Use in Formula lists the same names, and formulas.useInFormula inserts
at the caret instead of replacing the text being edited.
2026-10-23 23:47:38 -07:00
Brandon ThomasandClaude Opus 5.5 72c1d57d1e Review fixes for #2393, #2394, #2397 and #2399
- PMT: take rate / m first so (pv + fv) / m can't overflow for a tiny rate (PMT(1e-300,10,1e12)).
- Keytips probe: an empty params object keys like no params.
- Number Stored as Text check reads the same accounting parentheses as typed entry.
- Lookups: a NaN number falls back to the general comparison instead of comparing equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 02:12:07 -04:00
Brandon Thomas beda2a44dc Merge PR #2394 2026-10-24 02:10:42 -04:00
Brandon Thomas ec841a0139 Merge PR #2388 2026-10-24 02:10:42 -04:00