Commit Graph
850 Commits
Author SHA1 Message Date
Pranav 0fed26a14f Sandbox: a crate that runs a helper program confined (Landlock and seccomp on Linux, sandbox-exec on macOS)
## Stack

The Python cells work is split by layer. It merges in this order, each with a merge commit rather than a squash. Nobody can switch Python on before #3481.

1. #3475 depends on `main`
2. #3476 depends on `main` **(this pull request)**
3. #3477 depends on [#3475](https://github.com/storytold/gridcraft/pull/3475) and [#3476](https://github.com/storytold/gridcraft/pull/3476)
4. #3478 depends on [#3477](https://github.com/storytold/gridcraft/pull/3477)
5. #3479 depends on [#3478](https://github.com/storytold/gridcraft/pull/3478)
6. #3480 depends on [#3479](https://github.com/storytold/gridcraft/pull/3479)
7. #3481 depends on [#3480](https://github.com/storytold/gridcraft/pull/3480)

## Summary

`crates/sandbox` runs a program that can read only the paths its policy lists, write nothing except `/dev/null`, start no process, and use no network. An application re-executes itself with a launcher flag, closes every inherited descriptor above standard error, applies the confinement, checks that it holds, and only then runs the program. If any of that fails, the program does not run.

## Changes

- **`crates/sandbox`:** the policy, the Linux backend (Landlock, and seccomp that denies all sockets, with an x32 filter), the macOS backend (`/usr/bin/sandbox-exec` with a deny-default profile), the probes and a canary.
- **`apps/gridcraft`, `apps/gridcraft-cli`:** the launcher flag comes first in `main`, before any window or log.
- **`gridcraft-cli sandbox-check`:** says whether this system confines programs, and can run a confined self-test.
- **`docs/sandbox.md`:** what is refused and how.
- **`nix`:** a new dependency on Linux and macOS, for closing descriptors without `unsafe`.

## Compatibility

Windows and FreeBSD have no backend here. There the crate reports that it can't confine, and nothing that needs it runs. On Linux a launch needs Landlock ABI 3, which is Linux 6.2 and later, so an older kernel is treated the same way: this excludes, for example, Debian 12's kernel (6.1) and Ubuntu 22.04's original kernel (5.15). Nothing changes for a process started without the launcher flag.

## Review fixes

- **File metadata on Linux.** Landlock has no right for a file's permissions, owner, extended attributes, times or flags, and the seccomp filter allows what it doesn't name, so a confined program could change them on any file its user owns, outside the policy too. The filter now refuses the `chmod`, `chown`, `setxattr`, `removexattr` and timestamp families with their `*at` variants, by path and by descriptor, and any `ioctl` whose number says it hands the kernel data, as `FS_IOC_SETFLAGS` and `FS_IOC_FSSETXATTR` do on a descriptor opened only to read. A filter allows what it doesn't name, so a call a later kernel adds is allowed until it is named; the newest named is `file_setattr` (Linux 6.17).
- **Truncation on Linux.** Landlock covers truncation only from ABI 3, so on ABI 1 or 2 `os.truncate(path, 0)` emptied any file its user can write, and `open` with `O_RDONLY | O_TRUNC` emptied one it may read. A launch now requires that right. A filter can't take its place: `openat2` hides its flags from seccomp, and refusing `O_TRUNC` would refuse opening `/dev/null` to write, as `open(os.devnull, "w")` does.
- **Standard streams.** They stay open with the rights they were opened with, so a stream that is a file can be written and truncated, and one that is a socket can still send. The page and `launch_command` say so, and that a parent gives the program pipes.
- **The enforcement check, again.** It now also tries to change the canary's permissions and to truncate it, asking for what it already has, so nothing changes if one works.
- **Other processes on Linux.** On a kernel without Landlock's signal scope, a confined program could signal any process of the same user, and `kill(-1, 9)` would reach them all. The seccomp filter now refuses signals to any process but the program itself, whose ID `exec` keeps; changes to the limits, priority or scheduling of any other process; `pidfd_open`, `pidfd_send_signal`, `kcmp`, `get_robust_list`, `migrate_pages` and `move_pages`; every System V shared memory, message queue and semaphore call and every POSIX message queue call, which no path rule covers; and `syslog`.
- **The terminal on Linux.** A program started from a shell could type into its terminal. `TIOCSTI` and `TIOCLINUX` are refused, compared in 32 bits as the kernel reads them, so high bits set on the argument don't get past the rule.
- **The enforcement check.** It now also tries signal 0 on the launcher's parent, which delivers nothing, so `sandbox-check` reports a gap rather than assuming none. The launcher tries the same signal before it confines itself and refuses if that fails, and the check refuses if its parent has changed since, since a parent that refuses anyone would prove nothing. A launcher's arguments must include the current executable, which that check starts; otherwise the file rules refused the start and the check proved nothing about new processes.
- **A refused launch.** The launcher read its arguments only after closing every inherited descriptor, so a refused launch had already closed them. It reads them first.
- **A kernel without Landlock.** The refusal says so in that case too, which the tests that skip on such a machine look for. Only the first ABI's rights being refused, or `ENOSYS` or `EOPNOTSUPP` from creating the ruleset, says so. Any other failure, such as `EINVAL` or too many layers, is reported as what it is and doesn't skip the tests.
- **Folders in the exec list on macOS.** A folder got a rule that matches only the folder, so a launcher under it failed to start where the same policy works on Linux. It now covers what is in it, in both rules.
- **The sandbox page** names only what this layer has: only `gridcraft-cli sandbox-check` starts the launcher here.

## Testing

All 10 steps of `cargo xtask ci` pass on macOS (arm64), and `cargo clippy` passes for this crate for both Linux architectures. The gate on ubuntu-24.04 is the hosted one on this pull request, and it executes the Linux backend through the real binary, including `a_confined_program_leaves_other_processes_alone` and `a_confined_program_changes_no_file`, in which a confined Python tries each refused call on a file inside the policy, one outside it and a descriptor, and the test then compares contents, permissions and modification time.

A separate run on the fork, on Linux 7.0 with Landlock ABI 8, covered the same commit's code with parts of the fix taken out. Without the metadata rules, the launch check refuses with `change the canary's permissions: it worked`. Without those rules or the new checks, `a_confined_program_changes_no_file` fails with `permissions of ...: it worked`. With the Landlock rights limited to ABI 2, as on an older kernel, it fails with `truncate ...: it worked`. A kernel that is actually older than ABI 3, or that has no Landlock at all, has not been run. On macOS, terminal input was checked by hand on a pseudo-terminal: `TIOCSTI` is refused when confined and works when not.

The commits group the change by area for reading. Only the last commit of the branch is expected to build.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
  * Added sandbox support for running commands with policy-limited file access and restrictions on network and process operations. The sandbox is available on supported Linux and macOS systems; launches are refused if confinement cannot be established or verified.
  * Added the `sandbox-check` command to check sandbox support and enforcement, or run a specified program in the sandbox. It reports when sandboxing is unsupported.
* **Documentation**
  * Added guidance on sandbox behavior, platform support, access restrictions, launch checks, and `sandbox-check` usage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 07:31:42 +01:00
FredAppsandFredApps a36e4987c1 i18n(uk): translate Chart as Діаграма (#3703)
Use the shared craft-i18n Ukrainian term, distinct from Diagram (Схема).
Also translate the remaining Chart-labelled strings that were still English.

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-10 17:07:10 +01:00
FredAppsandFredApps 457bc7e374 Naming: use Diagram and Decorative Text in the feature catalog (#3622)
* Naming: use Diagram and Decorative Text in the feature catalog

* Naming: align the Chinese decorative text term

* test(naming): guard neutral display labels

* Naming: Diagram no longer repeats the chart word in de, uk and ro

German Schema and Ukrainian Схема match craft-i18n#211; Romanian Schemă
avoids GridCraft's own chart word Diagramă. A catalog test checks every
shipped language translates both labels and that Diagram differs from
Chart/Charts. The display-name guard now handles escaped char literals
and any #[cfg(test)] item, asserts it scanned catalog.rs and a minimum
of files and literals, and tests word boundaries. The catalog test
asserts each ID appears once. Docs carry timestamps and revision history.

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-10 16:13:13 +01:00
Harrison Booth 50a9e9ede4 XLS: formulas open as legacy formulas with Excel's implicit @ (#410) (#1367)
.xls formulas are read as legacy formulas with Excel's implicit @ (apply_legacy via read_xls_with), one-cell Ctrl+Shift+Enter arrays keep their range, @ of a 2-D range meets the formula's row and column, and defined-name analyses are shared across the formulas of a pass and replayed exactly.

Closes #410
2026-11-10 15:05:02 +01:00
RockerHXandFredApps ec12eba83e UI.1265: align CJK fallback text with the Latin baseline (#3081)
* UI.1265: align CJK fallback text with the Latin baseline

Use epaint's existing baseline alignment for system CJK fallback faces
and embedded faces tagged with CJK scripts. Mixed CJK, Latin and digits
then share a baseline in cells, editors and window titles.

Add regression coverage for zoom, font styles, wrapping, fixed-height
clipping, vertical alignment, editor height and title truncation.

Refs #1265, #2621.

* UI.1265: assert title baselines and document visual validation

* UI.1265: add baseline comparison and review validation

* UI.1265: adapt tooltip regression after upstream merge

The upstream shortcut update uses the runtime OS to choose tooltip text.
Use the harness OS for the existing split-button test expectation too.

Validation: cargo xtask ci; all 10 steps pass; 3,002 tests passed, 1 ignored, no exclusions.

* UI.1265: propagate CJK fallback to installed fonts

* UI.1265: align fallback baselines across cells

* UI.1265: align wrapped and unwrapped cell text

* UI.1265: preserve blank lines in wrapped cell alignment

* UI.1265: preserve centered fallback placement

* UI.1265: find CJK cell text by its visible bounds

Use the overlap of text ink, clip rect and cell rect to locate painted text
instead of testing a fixed offset from the galley origin. Baseline-aligned
fallbacks can move that origin above the cell on Ubuntu.

Keep the existing baseline and wrapping assertions unchanged.

Validation: cargo xtask ci on macOS; all 3 cjk_baseline tests on Ubuntu 24.04 x86_64.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 12:44:19 +01:00
ca39b5ab9f XLSX: legacy formulas (no dynamic-array cm) open with Excel's implicit @ and calculate like Excel (#417)
* XLSX: legacy formulas (no dynamic-array cm) open with Excel's implicit @ and calculate like Excel; implied @ saved back without cm

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* XLSX: long operator chains analysed in full and unfinished legacy analysis leaves the formula unchanged (never saved as SINGLE); @ of a union keeps its parentheses; IF's condition intersected inside SUMPRODUCT

* XLSX: a legacy union already in parentheses saves back without a second pair (never as SINGLE); a name's definition analysed as an array throughout, inserting no @

* XLSX: drop the unreachable keep-unions retry; an @ of a bare union saves as a dynamic formula

* XLSX: an @ keeps parentheses the reader would not put back (redundant ones, a union's)

* XLSX: a redundant @ keeps a legacy formula legacy; IFERROR and IFNA intersected inside SUMPRODUCT; file parentheses and unfinished analyses saved as read

An edit could leave an implied @ on a single cell: deleting rows 2:3 under the legacy SQRT(A1:A3), read as SQRT(@A1:A3), gives SQRT(@A1:A1), and a name redefined to one cell does the same to SQRT(@Rng). The writer kept an @ only when re-analysing the formula without it put the same @ back, which it no longer does for one cell, so the formula was saved as SQRT(_xlfn.SINGLE(A1:A1)) with cm, readable only by dynamic-array Excel. The writer now analyses the formula with its own @ in place: an @ is accepted when the analysis implies it at that position or when its operand is a single value there (such an @ changes nothing), and any @ inserted elsewhere or any other @ still makes the formula dynamic. SQRT(@A1:A1) saves as SQRT(A1:A1), SQRT(@A1) as SQRT(A1) and @SUM(A1:A3) as SUM(A1:A3), all without cm; SUMPRODUCT(@SQRT(A1:A3)) stays dynamic because SQRT(A1:A3) is an array there.

Legacy Excel intersects IFERROR's and IFNA's arguments inside SUMPRODUCT unless the formula is entered with Ctrl+Shift+Enter, like IF's condition: =SUMPRODUCT(IFERROR(VALUE(C19:C21),0)) returns one row's value or #VALUE!, and the accepted fix is CSE or N(+range) (https://stackoverflow.com/questions/59228707). They now get the same single-value parameter class as IF's condition (Param::Single), so the legacy SUMPRODUCT(IFERROR(A1:A3/A1:A3,0)) reads as SUMPRODUCT(IFERROR(@A1:A3/@A1:A3,0)), and a typed SUMPRODUCT(IFERROR(VALUE(A1:A3),0)), which dynamic-array Excel evaluates as an array, is saved with cm. IF's result arguments are left as arrays: in https://stackoverflow.com/questions/29682328, a non-CSE =SUMPRODUCT(--(B4=IF(NOT($C$4:$C$11),$B$4:$B$11))) in row 4 returns 2 (both "apple" rows of B4:B11 counted, so the result branch was not intersected), 0 in a row where C is TRUE and #VALUE! outside rows 4:11 (the condition was intersected).

The reader's @ in front of a whole-formula array result took no extra parentheses when the file already had them, so ({1,2}+1) and {1,2}+1 both read as @({1,2}+1), and the writer dropped the file's parentheses. An operand already in parentheses now gets a second pair, so ({1,2}+1) saves as written.

A legacy formula the analysis could not finish (node budget, nesting) was read unchanged, evaluated as a dynamic-array formula and saved with cm, so Excel spilled it. Names were analysed again at every use, so a 399-term name used six times exhausted the 4096-node budget; their kinds are now remembered per formula, and the budget is 16384 nodes. The nesting limit is 128, so Excel's 64 levels of nested functions are analysed. A formula that still exceeds the limits is saved as it was read, without cm.

* XLSX: an implied @ is dropped and any other @ of a single value kept as _xlfn.SINGLE in a legacy formula (as Excel writes it); a legacy formula the analysis cannot finish or whose semantics it does not know opens with a whole-formula @ and never spills; analysis depth follows the parser's limit

* XLSX: a legacy union given to a function's single-value parameter keeps its implied @, so it calculates to #VALUE! instead of spilling and saves as read; checked on a calculated workbook

* XLSX: only a one-cell range an @ applies to counts as one an edit shrank; a reading past the parser's nesting limit leaves the formula as read, and the writer saves the text a formula it cannot analyse was read from

* XLSX: a formula nested past the parser's limit for its reading is left as read (documented: it may spill); test covers it

* XLSX: a name's definition counts its one-cell ranges as collapsed only when it is itself a reference, so CHOOSE's index or IF's condition in it keeps the name legacy

* XLSX: an @ on a name or one-cell range counts as implied when ranges an edit may have shrunk, anywhere the name returns them (CHOOSE's choices, parentheses), make it so; deleting rows keeps such formulas legacy

* XLSX: the collapsed-range retry of an @ runs only when the first pass reached a one-cell range written as a range (tracked through cached names)

* docs: describe unfinished formula export fallback

* docs: the unfinished-analysis fallback differs for formulas with an explicit @

* Preserve main formula analyzer fixes

* Order file-format revision history

---------

Co-authored-by: neilcawse <8085959+neilcawse@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Local contributor <contributor@localhost>
2026-11-10 12:43:07 +01:00
e9addf52fe Arabic bidi layout and cell reading direction in the grid and editors (#947)
* M12.AR4: preserve Arabic logical text in bidi display and editing

* M12.AR4: drop the stale "no bidi" note from the RTL sheet status

* M12.AR4: open the Arabic editors and place the caret through key and pointer events in tests

* M12.AR4: skip the conditional-format lookup in the formula bar unless a rule sets reading order

* M12.AR4: test that the caret after typed Arabic is drawn at the text's left end (#1070)

* M12.AR4: pass the theme body font in the reading-order style test

* M12.AR4: adapt the Arabic UI tests to egui 0.37 (row direction, editor ids, Ctrl+End to the logical end)

* M12.AR4: read the active sheet through Workbook::active_sheet() in the formula bar

main made `Workbook::active_sheet` an `ActiveSheet` (shared between windows on one
workbook); the formula bar's reading-order lookup takes the index from the accessor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR4: keep explicit Left on wrapped right-to-left cells and trailing spaces in the editor

Wrapped cell text lays out every row from the left when the painted alignment is Left, instead of aligning right-to-left rows to their start. The cell and formula bar editor keep trailing whitespace in their layout so a right-to-left entry ending in a space keeps the caret inside the text.

* M12.AR4: keep Right and General on right-to-left sheets aligned right in wrapped cells

* M12.AR4: keep right-aligned Arabic with a marked first letter inside its cell

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:55:43 +01:00
cda767b49b Arabic font fallback in cells, the formula bar and the web (#931)
* M12.AR2: Arabic font fallback in cells and the interface (#229)

* M12.AR2: reserve the preferred Arabic file only once it has loaded

load_fallback treated fontconfig's Arabic file as already loaded even
when load_preferred_arabic could not load it, so a system Arabic face
from the same file was skipped and the chains could end up with no
Arabic system face. The file is now reserved only when the preferred
face is in the font set; the candidate loop moves to load_faces so a
test can drive it with a fixture file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR2: say where fontconfig's Arabic choice applies

fontconfig's Arabic face leads the Arabic fallbacks, so it draws Arabic
where the UI or cell font has none. A UI font with Arabic of its own,
such as DejaVu Sans in the Flatpak, still draws interface Arabic (#229),
which is left for a follow-up. The docs and the test's doc comment no
longer say the choice comes first in every chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:54:55 +01:00
c35234e5f5 Number formats: honour the B1/B2 calendar prefixes (Hijri dates for B2) (#1918)
* M12.AR6: honour the B1/B2 calendar prefixes in number formats (Hijri for B2)

* M12.AR6: match Excel's B1/B2 behaviour measured in review

Changes from a reviewer's Excel run (TEXT and Range.NumberFormat):
- A [$-...] locale tag before B2 keeps the date Gregorian (prefix still
  hidden); B2[$-401] stays Hijri.
- Any other unquoted B and digit (B3dd, dd/mm/yyyyB2, dd B2 mm), and B2
  before a number format (B20.00), make the code invalid: TEXT gives
  #VALUE!, a cell shows General, as for a code over 255 characters.
- Under B2, ddd and dddd show the Arabic weekday name (full name for both).
- Serial 60 has no Hijri date: TEXT gives #VALUE!, a cell shows ####.
- Serial 0 shows as serial 1 (29/08/1317).

Also fixes a break from merging main: days_from_civil now returns 0 for
day 0 and 29 February 1900, which sent serials 0 and 60 to 1970
(23/10/1389) under B2. The Hijri day count no longer passes those dates.

Docs: localization-parity states the behaviour, including the one
measured case not matched (mmmm next to dddd prints the weekday in Excel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR6: Hijri leap year in cycle year 15, not 16, as Excel shows

A reviewer compared Excel's B2yyyy-mm-dd on 43,512 serials with this
formula: it disagreed by one day on 1,372, all in cycle years 15 and 16,
and moving the leap year from 16 to 15 left no disagreement. The leap
years are now 2, 5, 7, 10, 13, 15, 18, 21, 24, 26 and 29 (still 10,631
days a cycle).

The count of leap years before a year becomes (4 + 11y) div 30; it
differs from the old (3 + 11y) div 30 only when y = 16 (mod 30), which is
exactly the move from 16 to 15. The inverse's year estimate can now be one
year off at a boundary and is corrected by one step.

Tests: Excel values for 2 March 2005 (1426-01-21) and 29 January 2006
(1426-12-29) plus the 1425/1426 year boundaries they imply, the reviewer's
other anchors, and exhaustive checks: year, month and 30-year cycle
lengths for Hijri years 1 to 10,000; day -> Hijri -> day round trip and
day-by-day succession for every day from 1 Muharram 1 AH to one cycle past
31 December 9999; every 1900-system serial 1 to 2,958,465 (except 60).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR6: match B1/B2 details checked in Excel 16.0 build 20527

Compared GridCraft with Excel 16.0 (build 20527, en-US) through COM: TEXT
with B2dd/mm/yyyy on every serial from 1 to 2,958,465 (no difference), and
about 1,750 format/value pairs through both a cell's NumberFormat and TEXT.
Fixed what differed and is in scope:

- Two-word Hijri month names use a no-break space (U+00A0), as Excel does.
- B1 before a number format is rejected like B2 (B10.00, B1#,##0).
- Prefix runs: a repeated prefix is accepted (B2B2, B2b2, [Red]B2B2, B1B1);
  a mixed one (B1B2, B2B1), more digits (B12, B21, B02), a prefix after a
  tag or space (B2[Red]B2, B2 B2) and an escaped \B2 are rejected.
- Serial 60 under B2 is #VALUE! for any date or clock part (hh:mm, ss,
  AM/PM), not only date parts; [h] and literal-only sections still show.

Regression tests use the Excel values (all 12 month and 7 weekday names,
the accepted and rejected codes, serial 60). Not changed, listed in the
docs: Excel's weekday/month-name mix-ups when ddd(d) and mmm(m) share a B2
section, weekday-only B2 sections (real weekday for serials 0-59, #VALUE!
for the last days of 9999), and Buddhist-year b/bb/bbbb tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:54:05 +01:00
004523c286 AutoFit: estimate Arabic text width from measured glyph widths (#1933)
* M12.AR12: estimate Arabic text width from measured glyph widths in AutoFit

* M12.AR12: treat U+0897–089F as marks and U+08E2 as visible; size the honorific ligatures (review)

* M12.AR12: size the remaining honorific ligatures like their siblings (review)

U+FD4C-FD4F (Unicode 14) and U+FDC8-FDCF (FDCF Unicode 14, FDC8-FDCE
Unicode 17) are honorific word ligatures, not letters, but fell into
the 0.48 em letter arm. Give them the 1.6 em of the measured
U+FD40-FD4B honorifics and assert each range's endpoints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR12: size Arabic-Indic digits like ASCII digits and the remaining honorific ligatures (review)

Arabic-Indic (U+0660-0669) and Extended Arabic-Indic (U+06F0-06F9)
digits measure 0.53-0.57 em in Noto Sans Arabic, Arial, Tahoma and
Times New Roman, the same as each font's ASCII digits, so they get
the ASCII digit estimate (0.56) instead of the letter estimate.
Arabic punctuation measures 0.17-0.50 em, so it keeps 0.48 rather
than falling back to 1 em.

U+FBC3-FBD2 and U+FD90-FD91 are honorific word ligatures like
U+FD40-FD4F and get the same 1.6 em.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-09 15:52:55 +01:00
517a4b9256 Sort: order Arabic text by its letters, not its vowel marks or digit script (#1940)
* M12.AR7: sort Arabic text by its letters, not its vowel marks or digit script

* M12.AR7: order marked Arabic text the way Excel does

Checked compare_text against Excel 16.0 build 20527 (en-US) via COM:
Range.Sort, =SORT() and the < / = operators over ~300 strings. Fixes the
mismatches inside this change's scope:

- The tie-break compared raw code points, so an RLM-prefixed word sorted
  after every vowelled form of it, "‏12" after "١٢", and a tatweel
  form after a shadda form. Ignorable characters are now skipped at the
  tie-break too.
- Marks are compared letter by letter (an unmarked letter first), so a
  mark on an earlier letter outranks the digit or letter after it, as in
  Excel ("ب١" < "بَ1", "بپ" < "بَپ").
- Marks rank in Excel's measured order (sukun before shadda), and the
  Quranic marks Excel skips (U+0610–061A, U+06D8–06ED except 06DD/06DE/
  06DF/06E0/06E5/06E6/06E9) are skipped by letter order.
- Excel also ignores bidi embeddings/overrides/isolates, BOM, soft hyphen,
  CGJ and word joiner; they now are ignored too (ZWSP is not, as in Excel).

Equality is unchanged. Regression tests use the orders Excel produced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M12.AR7: compare the mark level of Arabic text without allocating

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-09 15:49:26 +01:00
vangel76andClaude Sonnet 5.5 762be28e29 Merge upstream/main into fix/math-macro-defs-and-matrix-cells
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 18:38:03 +01:00
FredApps 6b19e445f4 fix(chart): pass series index to data-table keys (#1150) 2026-11-07 17:27:25 +01:00
Harrison BoothandClaude Sonnet 5.5 292b784f02 fix(calc): apply "Set precision as displayed" in calculation (#1366)
* ft-121.7.9: apply Set precision as displayed in calculation (#1343)

With the option on, a formula result (and each element of a spill) is
rounded to its cell's number format before it is stored and before other
formulas read it. Numbers typed, pasted or reformatted while it is on are
rounded at once, and turning it on through formulas.calculationOptions
{precisionAsDisplayed} rounds every number constant, as Excel does.

The rounding is NumberFormat::round_as_displayed in the numfmt crate,
which shares the renderer's digit logic (fixed, scientific, fraction).
General, date/time, text, digitless and conditional formats keep the full
value, as observed in Excel through COM. Nothing changes when the flag is
off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ft-121.7.9: round conditional formats by the sign's section under precision as displayed (#1343)

Excel does round formats with a condition when "Set precision as
displayed" is on; the PR kept them at full value. Observed through COM
with synthetic values (and in gridcraft-corpus
excel/precision-displayed/pad-on-conditional-sections.xlsx): conditions
are ignored and the sign picks the section, the first for positives and
the second for negatives. `[<1]0.00;0` shows 2+1/3 as 2 but stores 2.33,
and stores -10/3 as -3.

Two more observed details: with a condition and a single number section,
negatives keep the full value (`[<1]0.00`), unless an `@` section follows
(`[<1]0.00;@` rounds them by the first section); and a completely empty
section passes to the next one (`0.0;;0.000` stores -2.333 as -2.333,
`;0.0` stores 2.333 as 2.3), for formats with or without a condition.

Display is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* engine: recalculate spill anchors when a spill cell is reformatted under precision as displayed

round_edited_as_displayed only looked at stored-cell changes, but a spill
destination lives in Sheet::spill, so reformatting it (or its row or column) left
the anchor out of the changed set and the element unrounded. Add each spill anchor
whose range holds a changed cell or a changed row/column style to the changed set.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* numfmt: drop a cast the fraction merge made redundant

* precision as displayed: keep locked constants on protected sheets, round new sheets, fix manual-mode comment

* precision as displayed: name the input sheet in the new-sheet histogram test

* precision as displayed: use six values in the new-sheet histogram test

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:24:49 +01:00
Pranav 4e43ad8868 UI: RefEdit follow-ups: a dialog keeps the sheet it was opened on, a click on the selected cell picks, quoted sheet names in Solver (#3180)
* UI: a dialog's reference boxes belong to the sheet it was opened on, recorded when it opens rather than at the first pick

* UI: a click on the cell already selected writes it into the reference box being picked into

* Solver: references split into areas only at commas outside quoted sheet names, through one splitter shared with the reference boxes

* Solver: a constraint splits at its relation only outside quoted sheet names, in constraint text and in a model loaded from cells

* Solver: Save Model counts a constraint's areas outside quoted sheet names, and reads a number on the right side as one value whatever its separators

* UI: a Ctrl+click that follows a link is no pick of the cell already selected

The press on the grid tells the reference box being picked into that a
cell was selected only in the branches that select: Shift+click, Ctrl+click
that adds a cell, and a plain click. A Ctrl+click on a link's text follows
the link and selects nothing, so a link back to the selected cell no longer
replaces the box's text.

* UI: while a reference box picks, a second quick click on a cell picks it

A click on the same cell within the double-click interval started editing
the cell and skipped the pick, also when the earlier click came before the
box was clicked into. While a reference box is the one grid selections
fill, the double-click handling is skipped and the click picks.

* Docs: reference boxes take a second quick click as a pick; a followed link is none

* UI: while a reference box picks, a click on a link or checkbox picks its cell

A link was followed (Go To moved the selection into the box, and a web
link's question replaced the dialog) and a checkbox was toggled. While a
reference box is the one grid selections fill, a click only picks the cell
clicked, with or without Ctrl. The test of the Ctrl+click on a link back to
the selected cell becomes part of a test of links on the same sheet, a web
link and a checkbox.
2026-11-05 13:20:51 +01:00
Francisco Herreraandalosslessdev 1fb191571b Charts: draw every doughnut series as a ring (#1827)
* Charts: draw every doughnut series as a ring (#1827)

* Charts: submit doughnut hole size and first slice angle when the drag ends (#1827)

---------

Co-authored-by: alosslessdev <alosslessdev@users.noreply.github.com>
2026-11-05 05:13:35 +01:00
prxnvt b9595ccdec Merge origin/main into pr/form-controls
Conflicts:
- crates/xlsx/src/read.rs: main's names.unique() kept with this branch's
  (sheet, read_controls) result from read_sheet.
- crates/ui-egui/tests/it/keytips_generated.rs: regenerated with
  cargo xtask keytips from the merged keytips.json (413 sequences).
- docs/file-format-parity.md, docs/ui-parity.md: header and revision
  history keep main's entries and this branch's form-controls entries.
2026-11-04 13:01:54 -05:00
vangel76andClaude Sonnet 5.5 472132b158 math: refuse macro definitions and cap matrix cells
RaTeX counts macro expansions (1,000) but not how large they grow, so a
definition whose argument doubles on every call keeps the expander busy
without bound. User macros have no use in an equation, so check_caps now
refuses the definers (def, gdef, edef, xdef, let, futurelet, global, long,
newcommand, renewcommand, providecommand) before RaTeX sees the source. The
scan reads control words, verbatim text and comments the way RaTeX's lexer
does.

A matrix is laid out and written as rows times the widest row, so one long row
in a tall matrix multiplied the output. check_caps now counts the padded cells
of every environment from the source and refuses more than 2,500 in total;
to_omml checks the parsed grid as well.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 19:50:27 +01:00
Brandon Thomas 166234eff0 Merge PR #3268 2026-11-03 09:55:39 -05:00
Brandon Thomas 6c386f79ed Merge PR #3252 2026-11-03 09:55:39 -05:00
Brandon Thomas 5ae7ecfab9 Merge PR #3244 2026-11-03 09:55:39 -05:00
Brandon Thomas 0081e4f29e Merge PR #3239 2026-11-03 09:55:38 -05:00
Brandon Thomas ebf762a03a Merge PR #3254 2026-11-03 09:55:38 -05:00
Brandon Thomas 3bdfa6ed71 Merge PR #3253 2026-11-03 09:55:38 -05:00
Brandon Thomas d906bcbb6f Merge PR #3223 2026-11-03 09:55:38 -05:00
Brandon Thomas d7b53fcc20 Merge PR #3267 2026-11-03 09:55:37 -05:00
Brandon Thomas 31156f7949 Merge PR #3251 2026-11-03 09:55:37 -05:00
Brandon Thomas 502fac89fa Merge PR #3237 2026-11-03 09:55:37 -05:00
Brandon Thomas 56cc52eb39 Merge PR #3250 2026-11-03 09:55:37 -05:00
Brandon Thomas 85f09f05d6 Merge PR #3240 2026-11-03 09:55:36 -05:00
Brandon Thomas cb4b7af1e3 Merge PR #3229 2026-11-03 09:55:36 -05:00
Brandon Thomas 0670337908 Merge PR #3231 2026-11-03 09:55:36 -05:00
Brandon Thomas 2e27cbbf05 Merge PR #3226 2026-11-03 09:55:36 -05:00
Brandon Thomas 0a12da6e52 Merge PR #3221 2026-11-03 09:55:36 -05:00
Brandon Thomas affae49e7c Merge PR #3216 2026-11-03 09:55:36 -05:00
Brandon Thomas 89d658406e Merge PR #3238 2026-11-03 09:55:36 -05:00
Brandon Thomas a10d220cea Merge PR #3234 2026-11-03 09:55:35 -05:00
Daniele MoriandClaude Opus 5.5 38a7067ca2 Options: highlight the active cell's row, as a global preference
On a wide table the eye loses its place following a row from the active
cell. File > Options > General gets "Highlight the active cell's row", off
by default: a tint across the whole row of the active cell, over the fills
and under the text, in Normal and Page Layout views. It is a preference for
every sheet and workbook, so nothing goes into the file, the workbook does
not become dirty and there is no undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:59:41 +01:00
Daniele MoriandClaude Opus 5.5 f4f69beec2 Point mode: dragging over row or column headers extends the reference
A press on a header while a formula waits for a reference inserted C:C, but the
drag that followed did nothing, so dragging from column C to E left C:C. The drag
now follows the pointer, as it does over cells, in either direction and for rows
too, keeping the sheet qualifier, the earlier arguments of a Ctrl press and the
far end of a Shift press.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:45:53 +01:00
Hector VillarrealandClaude Opus 5.5 2bdec6c0a1 #941: a window hidden while parked drops its key tip request; parity row names the badge-less keys
Addresses CodeRabbit review on #3250:
- "Clear parked keytip requests when their window becomes hidden": a
  window hidden from another window (View › Hide while it isn't in
  front) kept the request it had parked, and showing it again opened
  the control. follow_active_window now drops the waiting request of
  every parked window that is hidden.
- "Correct the no-badge description in the parity table": only Alt+I,
  D, V and T (no ribbon tab) paint no badges.

Test: legacy_keytips::a_window_hidden_from_another_window_drops_its_parked_request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:44:40 -06:00
Hector VillarrealandClaude Opus 5.5 3c72d5f7aa #3224: Find and Replace within the selected range
edit.find and edit.replace take within: selection, which searches only
the selected cells; Find Next then moves the active cell through them
and keeps the range selected, as Excel does. The Find and Replace
dialog uses it whenever more than one cell is selected (Excel searches
a selected range; one selected cell still means the whole sheet).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 03:05:18 -06:00
adifiedio e56a722bb4 Clipboard: refuse oversized pasted pictures before copying their pixels; document paste as desktop-only (#699) 2026-11-03 14:25:14 +05:30
Hector VillarrealandClaude Opus 5.5 94b4f6a3a2 #1481: open SYLK (.slk) files
A small reader for the SYLK text format: cell values (numbers, text,
booleans, errors), formulas converted from R1C1 to A1 as Excel does
(relative/absolute references, ranges, whole rows and columns, shared
formulas) with their cached results, number formats (P / F;P) and
column widths (F;W). Import only; saving as .slk says so. The sheet is
named after the file, and damaged records are skipped with a warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:51:39 -06:00
adifiedio ac27137cd8 Clipboard: paste a picture from the system clipboard at the active cell (#699)
Cmd/Ctrl+V dropped egui's PasteImage event, and the ribbon/menu Paste only read
text, so screenshots and images copied in a browser reported "Nothing to paste".
Both now encode the pixels as PNG (bounded to 16384 px per side, 64 Mpx) and run
insert.picture as a floating picture anchored at the active cell, as Excel does.
Text on the clipboard still wins. New host service: Services::paste_image (arboard).

Fixes #699, #3018, #3182.
2026-11-03 14:14:34 +05:30
Hector VillarrealandClaude Opus 5.5 dea1359c7e #1246: drag the formula bar's bottom edge to resize it
The bottom edge of the formula bar is a resize handle: dragging it sets
the height in whole lines (up to half the window; one line collapses
the bar). Expanding again (the chevron, Ctrl+Shift+U) returns to the
dragged height instead of a fixed five lines. The drag previews live
and commits once through view.expandFormulaBar {lines}, which is also
scriptable; the height is kept with the UI preferences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:41:57 -06:00
Hector VillarrealandClaude Opus 5.5 3c0749e685 #941: Excel 2003 menu access keys (Alt+I, R; Alt+D, S; ...)
Adds 43 legacy sequences to keytips.json (Insert, Edit, Format, Data,
View, Tools menus and File > Page Setup), each running the command or
dialog the modern key tip already uses. Alt+I, D, V and T are implicit
levels with no ribbon tab; badges are not painted under them, as in
Excel. The generator checks Options (a Backstage page) by its page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:31:45 -06:00
Hector VillarrealandClaude Opus 5.5 f9edafa800 #2120: a linked cell's menu edits, opens or removes its link
Right-clicking a cell with a link only offered "Link…", whose dialog was
titled Insert Hyperlink, so changing a link wasn't obvious. As in Excel,
the cell menu of a linked cell now offers Edit Hyperlink… (the same
dialog, filled with the link and titled Edit Hyperlink), Open Hyperlink
(as clicking the link: a reference selects its cell, a web or e-mail
address asks first) and Remove Hyperlink (edit.clearHyperlinks). A cell
without a link keeps "Link…".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:59:12 -06:00
Hector VillarrealandClaude Opus 5.5 c35ab0f4f4 #1482: every Paste Special option has its Excel access key
Paste Special only knew the letters F, V, T, C and E. Excel gives every
option one: A (All), N (Validation), X (All except borders), W (Column
widths), R (Formulas and number formats), U (Values and number formats);
O, D, S, M, I for the operations; B for Skip blanks; and L for the
Paste Link button, which pastes at once. One table now maps each letter
to what it does; Enter still pastes with the chosen options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:41:55 -06:00
Hector VillarrealandClaude Opus 5.5 846cc09d67 #3101: show or hide the sheet tabs and scroll bars of a workbook
Excel can hide the sheet tabs, the horizontal scroll bar and the
vertical scroll bar of a workbook window (File › Options › Advanced ›
Display options for this workbook), saved with the workbook. GridCraft
already read and wrote these settings (#3057) but always showed all
three, and had no command or option for them.

New commands view.showSheetTabs, view.showHorizontalScrollBar and
view.showVerticalScrollBar {on?} (toggling without `on`, undoable,
saved with the workbook), and a "Display options for this workbook"
section in File › Options. The tab bar follows them: without the tabs
the scroll bar takes the bar, without the scroll bar the tabs do, and
with neither the bar isn't shown; without the vertical scroll bar the
grid takes its width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:37:34 -06:00
Hector VillarrealandClaude Opus 5.5 f0557ec443 #3089: keep "remove personal information on save" and honour it
Excel's per-workbook privacy setting (Trust Center › Privacy Options ›
Remove personal information from file properties on save, set by the
Document Inspector) is saved as <workbookPr filterPrivacy="1"/>, and
while it is on Excel leaves the author and company empty on save.
GridCraft dropped the attribute, so a cleaned workbook came back with
the setting off, and it wrote the author and company back.

Workbook.remove_personal_info now keeps the setting (read and written
as filterPrivacy), and while it is on the writer leaves dc:creator and
Company out. Other properties are written as before. Note authors and
lastModifiedBy are the rest of the issue (the latter overlaps #881).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:25:29 -06:00