Skip to content

feat(brand): swap in the MoonTerminal lockup - #411

Merged
guyverino merged 1 commit into
mainfrom
feat/moonterminal-lockup
Sep 3, 2026
Merged

feat(brand): swap in the MoonTerminal lockup#411
guyverino merged 1 commit into
mainfrom
feat/moonterminal-lockup

Conversation

@guyverino

Copy link
Copy Markdown
Collaborator

What & why

A user reported "I cannot save the Ctrl+Left hotkey for Place Long". The hotkey was never the
problem: pressing Save produced out-of-range value for u64 type, and nothing in Settings
could be saved.

TOML has no unsigned integer. toml_edit's value serializer converts every u64 through
i64::try_from and refuses anything above i64::MAX. Moonbot cores issue strategy ids across the
whole u64 range — read_u64 straight off the wire — and two persisted fields carry one:
ServerMeta::default_alert_strategy and ManualStratState::id. A core whose pinned strategy had
the top bit set therefore aborted toml::to_string_pretty for the entire settings.toml, which is
AppConfig::save, which is every settings write in the application. Roughly half of all ids
qualify; one already in a live config reads 7394783480262116308, four fifths of the way to the
ceiling.

It left durable damage too: save_impl writes servers.enc first and settings.toml second, so
the abort landed between them with finish_pair_write never reached — and while that pending
marker exists, backup.rs refuses the daily config snapshot. The first successful save clears it.

Both fields now go through the new config::wire_id.

Notable decisions

  • Integer while it fits, string only above the ceiling. Every value any existing file holds
    fits i64, so a typical settings.toml stays byte-identical and no user's ids are rewritten
    into a shape an older build cannot read. The string form is confined to values that build could
    never have written in the first place, because writing them is precisely what was impossible.
  • A stored value that is not an id reads as 0 ("no strategy"), not as an error. Rejecting
    would quarantine the whole file to .bak and replace every core's group, market, colour and feed
    flags with defaults — the same disproportion this fix removes, pointed the other way. 0 lands
    in the field's own documented recovery: ManualStratState re-pins from the strategy NAME.
  • Divergence from strat_db's bit-cast as i64 is deliberate. There the signedness is imposed
    by the join to orders_rep.strategyid, which the core writes Delphi-signed. settings.toml has
    no such join, and a negative number in a hand-editable file is a trap.
  • The read half uses the crate's existing lenient idiom (#[serde(untagged)] enum { Number, Text, Other(IgnoredAny) }, as in layout::serde_compat) rather than a hand-written Visitor.
    IgnoredAny absorbs every remaining shape by construction, so no future TOML type can arrive at
    a missing visit_* arm and take the file down through a gap nobody closed.
  • The guard closes the class, not the two fields. every_persisted_u64_field_is_adapted_or_marked_exempt
    reads the config sources and fails when a persisted u64 carries neither the adapter nor a
    // wire-id-exempt marker. It immediately found a third persisted u64 nobody had considered —
    CoreGroup::cores — and three more in layout.rs. All are terminal-issued uids and are marked as
    such. A companion fixture test pins the guard's own ability to go red.

Known limitations

  • A settings.toml carrying the string form is unreadable to a build without this change: it
    quarantines to .bak and continues on defaults. Only a config that currently cannot be saved at
    all can contain that shape, so no working setup regresses, but a downgrade on an affected machine
    is not lossless.
  • An unreadable stored id is logged and resolves to "no strategy"; the log names the value, not the
    core or the field, because a serde adapter has no field context.
  • ServerConfig::default_alert_strategy carries the adapter although nothing encodes that struct
    today (reconcile::split sends only uid/name/key into servers.enc) — it is a public Serialize
    derive on a core-issued u64, and leaving it bare would arm the same failure for the first
    encoder that reaches it.
  • The sweep is a text scan of four config sources. A field declared through an alias other than
    CoreId, or in a config file outside that list, is still on the author's honour.

How to verify

cargo build -p moon-ui-gpui --bin moonterminal --target x86_64-pc-windows-msvc --all-targets
cargo clippy -p moon-ui-gpui --bin moonterminal --target x86_64-pc-windows-msvc --all-targets -- -D warnings
cargo test -p moon-core --target x86_64-pc-windows-msvc
cargo test -p moon-ui-gpui --target x86_64-pc-windows-msvc
  • Build: exit 0.
  • Tests: moon-core 1526 + 42 across its other targets, moon-ui-gpui 1375 + 261 (theme_contract) — 0 failed.
  • Clippy: 75 findings, all pre-existing. Two land in a file this PR touches (layout.rs:1613, :1631,
    mem_replace_option_with_some); the identical lines sit at 1610/1628 on origin/main, and this
    PR's whole layout.rs diff is three comment lines.
  • Beyond the suite: the real 24-core cfg/settings.toml from a dev machine was round-tripped through
    to_string_pretty with an id of u64::MAX injected — it serialises, reads back identically, and the
    existing 7394783480262116308 stays a bare integer. The unhappy inputs (-5, "", "7e18", 1.5,
    true, [1,2], {a=1}, a datetime) all read as 0 with the file intact.
  • FireTest not run: this change has no chart / render / window / input surface.

The header and the empty-chart placeholder drew the Moonbot wordmark. Both now
draw the MoonTerminal one, shipped as two cuts under assets/brand — dark and
light — so the scheme picks a FILE instead of patching a fill, and a custom
palette can no longer repaint the brand.

MoonWindowFrame hardcodes MoonUI's own lockup and has no variant for a foreign
one, so the main header assembles its brand cluster from the frame's drag handle
plus design::header_logo. The geometry matches brand_cluster; the rule is the
terminal's own chrome_divider rather than MoonUI's fainter border copy of it,
since the terminal now owns that seam. docs/WINDOWING.md records the exception
and its exit condition, and the windowing contract test names both the helper
and the asset path so no other screen can follow.

Every lockup size is ported through one LOGO_PORT_SCALE factor: the new artwork
is 283.23 wide against 199, and the port keeps the ICON at its drawn size rather
than the box it sat in, so only the wordmark gets longer. The header buys its
room in width (86 -> 122.4) because its strip is height-constrained; the chart
placeholder keeps its share of the pane, floor and ceiling scaled alike.

Both surfaces now take their image from a two-entry LazyLock table instead of
building one per frame: the colour scheme is the document's only input, so the
header stopped copying and hashing 6.9KB of asset on every frame and the glow
stopped re-composing 7.5KB per empty pane.
@guyverino
guyverino merged commit 3472102 into main Sep 3, 2026
6 checks passed
@guyverino
guyverino deleted the feat/moonterminal-lockup branch September 3, 2026 22:40
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