Skip to content

Linux port - #2516

Closed
ell-shad wants to merge 53 commits into
microsoft:mainfrom
ell-shad:linux-port
Closed

ell-shad wants to merge 53 commits into
microsoft:mainfrom
ell-shad:linux-port

Conversation

@ell-shad

Copy link
Copy Markdown

Fixes #.

Description of the changes:

How changes were validated:

…test smoke

Engine compiles unmodified with GCC 15 via CMake into libCalcManager.so.
Shims for MSVC-only headers (ppltasks eager-task, intsafe, winerror) and a
portable prelude replacing pch.h; upstream src/ untouched. C ABI uses UTF-8
(wchar_t is 4 bytes on Linux vs 2 on Windows/C#). Smoke test drives the real
engine: 2+2=4, clear, div0 error, sin30=0.5, history. SPEC + plan + todo.
…e+run

net10.0 console referencing Avalonia 11.3.6; dotnet build/run green.
Replaced by the real Avalonia App in Task 4.
Replaces CalcManager.Interop (C++/CX) with LibraryImport-style DllImport
using UTF-8 + GCHandle-routed callbacks (wchar_t size differs on Linux).
Resolver finds in-tree libCalcManager.so (CALCMANAGER_PATH override,
system paths for future .deb). 6 tests green against the real engine:
2+2=4, history, clear, div0 error/recovery, sin30=0.5, memory round-trip.
UnitConverter interop deferred to Task 6 with additive ABI section.
Verbatim-port dependency-free Common enums (Bindable attrs dropped, noted).
AppResources from generated en-US tables; LocalizationStringUtil %1
substitution in managed code (was kernel32 FormatMessage). LinuxLocalization
from CultureInfo/ICU (separators, grouping, digits, RTL, week, calendar).
14 tests green.
Replaces WinRT DecimalFormatter/CurrencyFormatter surface used by VMs
(grouping, fraction digits, decimal-always, per-currency digits).
Ties round half away from zero (documented WinRT RoundHalfDown delta).
9 tests green.
Replaces ApplicationData.LocalSettings. Write-through, typed via
System.Text.Json, corrupt-file tolerant. 5 tests green.
IClipboardService (in-memory + Avalonia), LinuxNetworkManager (no metered
detection on Linux), IDispatcher (immediate + Avalonia UIThread),
LinuxTraceLogger no-op keeping all VM call sites compiling. 5 tests green;
full suite 39/39, zero using Windows.
create_ex with per-token command types; history item texts/tokens/flat
replay commands per mode; display-command snapshot; memorized-numbers
cache; remove/max-size. Frozen Task 1 core untouched. Smoke asserts
[132,93,132] replay for 2+2. ctest green.
create_ex with typed expression tokens; history texts/tokens/replay
commands per mode; display-command snapshot; memorized content; radix
results; radix/precision/degree/load-mode controls. 7 tests green.
CalcCommands generated from Command.h; TokenType/RadixType verbatim;
CopyPasteManager on IClipboardService; EngineStrings/CalcUtils helpers;
NarratorTokens map; CalcEngine display/memory/history/paren events;
native refresh-memorized + is-recording. Build green.
Display adapter over engine events; History/Memory/HistoryItem VMs;
StandardCalculatorViewModel mechanical port (flat command replay,
typed tokens, no snapshots yet). 12 headless tests green: 3 modes,
programmer radixes, memory, history recall/clear, errors, paste.
CCalcEngine::InitialOneTimeOnlySetup initializes shared globals once per
load; concurrent engine instances crash. Full suite 58/58 green serially.
The Avalonia app must likewise drive the engine from the UI thread only.
Headless parity 12/12 (3 modes, programmer radixes, memory, history
recall/clear, errors, paste). UI: Standard/Scientific/Programmer panels,
docked History/Memory tabs, radix/bits/angle sync, bit-flip, keyboard map,
Ctrl+C/V. Full suite 58/58.
WinRT Calendar backed by System.Globalization (1601 floor preserved,
dual identifier spellings). DateCalculationEngine verbatim port. 10
tests green incl. upstream full-range/leap/add/subtract/OOB vectors.
Mechanical port (long-date via culture pattern, UTC today init). 7 tests
green: diff/add/subtract, same-dates, OOB, reversed-dates, resource keys.
LinuxCalendar over System.Globalization; engine verbatim port; VM port
with culture long-date; Date panel (diff/add-subtract) + nav entry.
17 tests green; app launches clean.
Native UnitConverter driven by C#-side data/display callbacks (UTF-8).
Shared utf8_util.h extracted. 14 unit_converter_* symbols; build clean.
867-line database ported (ids, names, factors, explicit temperature
conversions, region defaults). WinRT wrappers replaced by plain records;
currency behind ICurrencySource seam (Task 7); IncludeCurrency flag gates
the currency category. Build green.
P/Invoke over converter ABI with pooled loader-direction strings (native
copies after return; prior batch freed on next call — fixes use-after-free
that left ints perfect and strings garbage). 9 tests green: 12 categories,
m->ft, ft->km, C->F both ways, kg->lb, supplementary, prefs, clear.
Mechanical port onto ManagedUnitConverter + Linux services (dispatcher,
settings, formatters, clipboard seams). NetworkManager ported; currency
behind Task-7 stub. 9 headless tests green incl. temperature offsets.
Dropdowns, dual read-only fields, switch, supplementary list, pad,
currency-state bindings (dormant), 12-category nav (Currency disabled),
prefs saved on leave. App launches clean; 93/93 green.
Live endpoint api.frankfurter.dev/v1 (migrated from .app); RegionInfo
runtime metadata; recorded-JSON tests (4 green); XDG file cache.
Frankfurter ratios + RegionInfo metadata, XDG settings/cache, System.Text.Json,
status-after-finalize (fixes LoadFinished polling race upstream masks via
dispatcher). 6 loader tests green: web/cache/stale-shape/offline/ratios.
RegionInfo metadata + live ECB ratios (30 currencies verified live);
full loader port (cache/stale/week-old/offline); VM wiring + nav;
status-after-finalize race fix. 104/104 green.
Recursive-descent parser (implicit mult, right-assoc ^, functions,
pi/e/x/params, y= prefix, degrees mode); NaN convention for bad samples.
OxyPlot.Avalonia 2.1.0 referenced. 10 tests green.
Roots via verified bisection (asymptote-safe), refined extrema, parity
sampling, observed range. 5 tests green (quadratic/cubic/sine/1/x/line).
OxyPlot.Avalonia 2.1.0 binds Avalonia 0.10 -> FileNotFound under Avalonia
11; LiveCharts2 (MIT, Avalonia-11-native) renders instead. Local parser,
numeric key features, equation editor + style + settings + pad. Fixed
Rebuild self-recursion. 126/126 green; app launches clean.
…collapse

Panel-aware code-behind keyboard map (calc/converter/text separation);
mode window titles; About dialog; History dock toggle + narrow-window
auto-collapse. Build green.
Keyboard routing, titles, About, panel collapse; PORTING-NOTES.md
(30-item swap table); .deb packaging (publish + side-by-side .so,
desktop file, icon); bridge lifetime fixes; test content link.
126/126 + ctest green; zero using Windows; src/ untouched.
24 theory tests (12-category round-trips + negate gating); JPY/JOD
decimals, week-old/stale/failed currency states; nullable pragmas for
verbatim ports (0 warnings); xUnit analyzer fixes; obsolete API swap.
…ST, params, paste)

Covers prior manual-check items headlessly. Real fix: engine separators
now follow the UI locale at creation (German decimal comma verified).
Stripped engine, fixed perms, changelog + copyright + verified Depends,
lintian overrides for inherent self-contained tags (clean run). C++ tree
clang-format clean per repo .clang-format.
Async test waits, callback-gated completion asserts, extra parity tests,
headless coverage of prior manual items, docs counts + checklist triage,
packaging hardening. Truth table: 171/171 + ctest green.
…waits)

SecondInstance hung ~1/6 full-suite runs: SpinUntil-polled plain status
field vs 10s timeout while loader completed (JIT-tiering-dependent stale
read). Status now volatile-backed with release/acquire pairing; tests gate
on LoadData's exited hook. 10/10 full-suite green.
Avalonia 11.3 has no live-region notification API (verified in package
metadata), so announcement strings bind as AutomationProperties.Name on
displays and converter/date fields. Task-9 comments updated to present.
Visual review found the window did not resemble Windows Calculator:

* Mode routing: nav tags went through Enum.TryParse<ViewMode>, and
  Standard/Scientific/Programmer are ViewMode members, so Standard opened the
  unit converter. Now an explicit converter-tag set.
* Keypad collapse: the four mode panels were DockPanel siblings; only the last
  declared child fills, so the visible panel lost the fill area and buttons
  rendered content-sized in empty cells. Panels now stack in one content-host
  Grid; keypad buttons force Stretch like upstream CalcButtonStyle (Avalonia's
  Fluent Button does not).
* Unbound panels: Find<DockPanel>("ConverterPanel") went null when the element
  became a Grid, leaving date/converter/graphing with no DataContext (blank).
  Lookup is type-agnostic now.
* Keypads completed: scientific had no 0/./=/trig, programmer no C/CE. Layouts
  transcribed from the upstream operator XAMLs.
* Nav is now a LeftMinimal hamburger flyout (upstream), not a docked sidebar.
* Styling from upstream App.xaml: number/operator/accent fills and opacities,
  flat memory row, large display, light+dark variants.

New Avalonia.Headless suite (28 tests) runs the real window through Skia:
layout geometry (button vs cell fill), panel exclusivity, DataContext binding,
flyout, and per-mode pixel checks in both themes. 199 total, Debug+Release.
- RegionInfo.DisplayName followed the UI culture and the culture list contains
  chr-US, so USD showed 'Cherokee - US Dollar' / EUR 'España - Euro'. Use
  EnglishName with a deterministic neutral/English region preference.
- Programmer mode drops the big result (upstream shows only the radix lines).
- Snapshot coverage for more mode/size combinations.

206 tests green (Debug + Release), ctest green, lintian clean, .deb rebuilt.
…rogrammer missing 5

Independent audit vs the unmodified reference XAML
(src/Calculator/Views/*.xaml, ButtonId list):

  Scientific missing: Sec, Csc, Cot + all 6 inverses, Sinh/Cosh/Tanh +
  their inverses, Sech/Csch/Coth + inverses, Ceil, Cube, DMS, Degrees,
  Euler, Floor, LogBaseY, Rand, TwoPowerX, YRootX, ClearEntry.
  Programmer missing: Lsh, Nand, Nor, RolC, RorC.

The ViewModel and the native Ratpack engine already computed every one of
these correctly (sec 30deg=1.1547005383792515290182975610039,
asinh(1)=0.88137358701954302523260932497979,
e=2.7182818284590452353602874713527) - they were simply unreachable from
the UI, which a ViewModel-only suite cannot detect.

Fix is XAML-only: scientific keypad extended to 15 rows x 5 cols (all 59
reference ButtonIds reachable) inside a ScrollViewer, programmer gains the
5 missing bitwise ops.

Regression guard: tests/Calculator.Linux.Tests/Audit/KeypadCompletenessTests
walks the REAL window's visual tree per mode and fails if any reference
ButtonId is not bound to an on-screen button. Suite: 220 green.
…ort, run_all.sh

Independent audit vs unmodified reference @ d125246.
Fixed AUD-S2-001 (Scientific missing 34/59 functions, Programmer missing 5)
and AUD-S3-002 (build-deb.sh exited 2 on success).
Verified: engine is the real Ratpack rational engine (exact 0.1+0.2=0.3,
1/3*3=1, sqrt(2.25)-1.5=0, 170! beyond double range, 30-digit e);
converter data byte-identical (158/158, 105/105, 154/154); both resource
tables exact (1166/1166, 108/108); clean-checkout build green.
Reference Graphing and Currency are 100% MOCK, so those cannot be compared.
Outstanding: on-screen UI-RUN (compositor wedged), 220 keyboard bindings,
locale matrix, scaling, a11y, soak, licences.
Reference declares 71 button shortcuts (extracted to
AUDIT/evidence/reference_shortcuts.json). The port matched hardware Key
codes only - 0 uses of KeySymbol - so operators silently failed on
QWERTZ/AZERTY/non-Latin layouts, and only ~12 of 71 shortcuts existed.

MainWindow.OnKeyDown now dispatches on e.KeySymbol through three tables
transcribed from the reference: s_commonChars (operators, all modes),
s_scientificChars (29), s_programmerChars (14). Key.* retained for the
numpad and function keys.

Also: 158/158 reference golden vectors now replay green.
AUDIT/extract_golden.py pulls every TestDriver::Test case out of the
reference's own CalculatorManagerTest.cpp and ReferenceGoldenVectorTests
replays them against the port's engine - Microsoft-authored expectations
covering standard/scientific/programmer/paren/error/memory/max-digits/
order-of-operations.

Suite: 410 green.
LocaleMatrixTests (22): 15 cultures verified for decimal/group separators,
en-IN 2,2,3 grouping, RTL detection, and invariant-globalization fallback.
ar-EG correctly renders Arabic-Indic digits. No defect found.

KeyboardShortcutParityTests (42): reference's 71 shortcuts transcribed to
s_commonChars/s_scientificChars/s_programmerChars, driven through the real
window with a non-US Key code so only KeySymbol can route them.

Suite: 432 green. AUDIT/run_all.sh: ALL CHECKS GREEN.
S3-006: the flyout listed Graphing LAST, after all thirteen converters. The
reference's CategoryManifest (src/Calculator.ViewModels/Common/NavCategory.cs)
orders the calculator group Standard, Scientific, Graphing, Programmer, Date -
Graphing is THIRD. Fixed, and the pre-existing WindowLayoutTests expectation
that had recorded the wrong order was corrected to the reference sequence.

S3-007: Ctrl+Alt+1..5 (Windows' resolution of the manifest's Number1..5 access
keys) were absent, so no mode was keyboard-reachable. Added, plus plain
Alt+1..5 as the Linux mnemonic form. AccessKey was not put in XAML - Avalonia
11.3 has no such property on ListBoxItem (AVLN2000).

Also replayed three more reference test bodies verbatim, all green first time:
  ReferenceDateEngineTests    9/9  (DateCalculatorTests.cs)
  ReferenceCopyPasteTests     4/4  (CopyPasteManagerTests.cs)
  ReferenceVmGoldenVectorTests 22/22 / 114 assertions
                               (StandardCalculatorViewModelTests.cs,
                                extracted by AUDIT/extract_vm_vectors.py)

Suite: 482 green.
ReferenceNavCategoryTests (61): replays NavCategoryTests.cs -
CreateNavCategoryGroup pins Graphing at index 2 of the calculator group
(independent confirmation of S3-006), plus the stable SerializationIds
(Standard=0 ... Angle=15, Currency=16, Graphing=17) that are written to the
persisted last-used-mode setting, friendly names, the three-way group
classification and the Number1..5 VirtualKey map.

Order-dependent locale tests fixed. LocaleMatrixTests display assertions
depended on which engine was built first in the process, because the
unmodified engine holds its resource provider behind a 'const' member
installed by the first instance (CalcEngine.h:117). They passed or failed
depending on test order. Tagged [Trait(Category, IsolatedProcess)] and
AUDIT/run_all.sh now runs them in a separate process: 525 main + 18 isolated.
Not user-visible - the app builds one engine at startup and never changes
locale - and unfixable in the port without editing the engine header.

IndianGrouping_IsTwoTwoThree previously asserted 12,000, which groups
identically under en-IN and en-US and could not detect pollution; it now
asserts 1234567 -> 12,34,567 vs 1,234,567.

ui_drive.sh now locates the flyout's own X toplevel and polls for it instead
of guessing coordinates from the window origin.
S3-008: Currency mode rendered both symbols on a row BELOW the display
instead of beside their amounts. The leading symbol had no padding so its
glyph was clipped by the window edge, and the trailing symbol inherited
default left alignment inside its '*' grid column, so it appeared at the
window's horizontal centre. The reference (UnitConverter.xaml) puts each
symbol as a Grid.Column sibling of its amount, with Padding="16,0,0,0" on
the leading one and the trailing one aligned to the right-aligned Value2.

No test caught it - both symbols rendered, just in the wrong place. Found by
driving the real app. ConverterSymbolLayoutTests (3) now pins it; both
glyphs verified correct in AUDIT/evidence/screens/06-currency.png.

Also: AUDIT/license_audit.py walks project.assets.json for every project and
resolves each package's licence from its .nuspec. 44 transitive packages, all
permissive (MIT / Apache-2.0). One test-only package (xunit.abstractions)
declares only a licenseUrl and is reported for review rather than assumed.
Wired into run_all.sh as a release gate.

run_all.sh is now 9 steps: engine build, ctest, managed build, main suite,
isolated locale process, packaging, reference data parity, licence audit,
live on-screen drive.
S2-002 (security): Avalonia.FreeDesktop 11.3.6 pins Tmds.DBus.Protocol 0.21.2,
which carries CVE-2026-39959 / GHSA-xrw6-gwf8-vvr9 (CVSS 7.1 High): a peer on
the same D-Bus bus can spoof signals, exhaust file descriptors, or crash the
process. No workaround exists. Pinned the patched 0.21.3 in
Calculator.Linux.csproj; 'dotnet list package --vulnerable' is now clean.
license_audit.py gained the vulnerability gate so this cannot regress silently.

S2-003 (functionality): Scientific-mode calculations were computed correctly
but never appeared in the History pane. HistoryViewModel._currentMode is only
assigned by ReloadHistory, and ReloadHistory was never called from anywhere.
The engine keeps separate m_pStdHistory / m_pSciHistory lists, so every
scientific calculation was stored in one list and looked up in the other.
SetCalculatorType now reloads history for Standard/Scientific, matching the
reference's MainPage.xaml.cs:281,290.

ReferenceHistoryTests (21) replays the reference's HistoryTests including all
13 TestHistoryStandardOrderOfOperations cases verbatim, the three-space history
expression format, the 20-item cap with eviction, and the scientific case that
exposed S2-003.

Suite: 549 + 18 = 567 green.
Verdict narrowed from 'blocked on environment' to 'blocked on coverage'.
Sections rewritten: counts by method now include reference-test replay, live
on-screen UI-RUN, security and licence; the defect list carries all 8;
section 6 splits what is now verified from what is genuinely not started
(scaling, a11y, soak, and 8 reference C# suites still to replay); section 7 is
now what the automated on-screen pass covers versus what still needs a human
(per-button disabled-state transitions, live dark-mode repaint, Orca).
Closes two of the three previously-unstarted coverage areas.

Scaling: launched at AVALONIA_GLOBAL_SCALE_FACTOR 1.0/1.25/1.5/2.0. At 200%
everything scales proportionally, nothing clipped, all 24 standard buttons
legible. The port's layouts are all *-sized, so nothing was pinned to absolute
pixels. Captures in AUDIT/evidence/screens/scaling/.

Live dark mode: flipped color-scheme to prefer-light with the app running; it
repainted in place without a restart, confirming the theme follows the desktop
setting at runtime rather than only at startup.

AT-SPI: the session bus is reachable, which is the precondition for any screen
reader to see the app at all. The tree itself was not enumerated - pyatspi is
absent on this machine - so Orca announcement quality remains a human check.

Two of my own bugs fixed on this pass:
- ConverterSymbolLayoutTests located the currency symbol blocks by their text,
  which comes from the live rate feed, so it passed or failed depending on the
  network. It failed in the final gate run. Now located structurally.
- run_all.sh step 9 skipped the on-screen drive when DISPLAY was unset and still
  reported ALL CHECKS GREEN. A gate that skips quietly is worse than one that
  fails. DISPLAY now defaults to :0 when the socket exists, and
  AUDIT_REQUIRE_DISPLAY=1 makes the skip fatal.

Also: .gitignore now pins 'TEMP FOLDER SKILLS/' so the user's reference material
cannot be committed to the fork even by accident.

Gate is now 10 steps.
A reviewer landing on this fork sees upstream's README first, which says
'Windows Calculator' and gives no hint that a native Linux port exists here.
Rather than edit upstream's README - which would misattribute the fork to
Microsoft - this adds a root-level signpost pointing at linux/, the audit,
the exactness evidence, and the honest open items.

Upstream's README.md, LICENSE, SECURITY.md and code of conduct are untouched.
The on-screen drive step required an already-running Calculator window before
it would run. ui_drive.sh launches the application itself, so that check was
both wrong and self-defeating: it failed on a perfectly good display once
AUDIT_REQUIRE_DISPLAY=1 made the skip fatal. Now it only requires a usable
DISPLAY and xdotool.

Full gate with AUDIT_REQUIRE_DISPLAY=1: all 10 steps green, no skips.
@ell-shad ell-shad closed this Oct 11, 2026
@ell-shad
ell-shad deleted the linux-port branch October 11, 2026 12:00
@ell-shad
ell-shad restored the linux-port branch October 11, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant