Skip to content

Finish the upstream sync: adopt upstream's margin API, merge the SDK fork with Free-Ink main - #32

Merged
ma-r-s merged 81 commits into
xteinkfrom
app/upsync
Sep 4, 2026
Merged

Finish the upstream sync: adopt upstream's margin API, merge the SDK fork with Free-Ink main#32
ma-r-s merged 81 commits into
xteinkfrom
app/upsync

Conversation

@ma-r-s

@ma-r-s ma-r-s commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Finishes the upstream sync that stopped on 2026-09-03 (card crosspoint-reader#133). Do not
compare this with sync/upstream-20260903 -- that branch holds the half-done
merge with conflict markers still in it and is evidence, not a candidate.

The margin decision

Adopted upstream's setContentMarginFromScreen everywhere. Mario's call:
a permanent divergence on a call every screen makes is paid at every future
sync.

The premise the card was written on turned out to be wrong, and it made the
work far smaller. The SDK comment warning that the safe area gets applied
twice is about plain setContentMargin(), not about
setContentMarginFromScreen(). From FreeInkApp.h:

marginBeyondSafeArea(m, s) = m > s ? m - s : 0
FromScreen(m) -> screen inset by  s + max(0, m - s)  =  max(m, s)   idempotent
Absolute(m)   -> screen inset by  m

max(m, s) returns an already-folded margin unchanged, and every one of these
call sites folds the safe area in already (they derive from
getScreenSafeArea()). So no unfolding was needed, and 18 of the 19
conflicting sites passed arguments already identical to upstream's -- those
resolved to a rename.

What actually moves: 9 screens that passed 0 for the side margins now inset
by the safe area (1px on the X4 Pro, 3px in the simulator's X4 profile). That
is upstream's behaviour and it makes content clear the side glass instead of
running under it. The top does not move on any screen, because
topPadding + headerHeight is far larger than the 10px top inset, so
max() returns it unchanged.

setContentMarginAbsolute survives with two callers, both fork-only:

  • toybox::absoluteChrome() passes Insets{}. FromScreen({}) is
    max(0, safe) = the safe rect, which pushes every game's chrome down by the
    top inset -- the regression docs/bezel-insets.md records finding on the
    device twice.
  • XkcdScreens.cpp passes a band that can sit on the true panel edge. On the
    X4 Pro safe.bottom is 0 so they agree, but a board on the inherited
    {9,3,3,3} default would have a deliberate full-bleed clamped by 3px.

Upstream cannot express "content rect = the full frame" at all: Screen seeds
content_ from safeRect() and both upstream calls inset from safeRect().
So this is a capability the fork needs, not a style preference -- and no screen
shared with upstream diverges any more.

The SDK

Merged Free-Ink/freeink-sdk main into the fork (16 commits, clean, no
conflicts
); the SDK's own host suite passes 2832 checks. .gitmodules named
branch x4pro-measured-insets, which has been deleted on the fork; the
pinned commit lives on fix/sd-free-space, now fast-forwarded
c444616..29221d2, and .gitmodules names it. That also fixes the
upload-pack: not our ref failure a plain git fetch --recurse-submodules hit.

Verification

Both sides of every conflicted file were checked by name, and then every
identifier present in the fork and absent from the merge was audited (103 after
filtering font tables): 102 originated upstream and were superseded by
upstream's own rewrites; the single fork-authored name was a parameter of a
legacy helper upstream deleted, whose capability survives.

Rendered and looked at in the simulator: Home, Settings, and the Games shelf.

Reviewer notes

  • The ui host suite cannot validate the margin change: it constructs
    DeviceContext with safeArea = {}, which pins the same geometry either
    way by construction. The renders are the evidence, not that suite.
  • Upstream's env:x4c / env:x4c-gh_release (X4 Classic) are deliberately
    not adopted
    -- that is new device support, a product call, not a merge
    decision. The board-name mapping for it was ported so adopting it later is
    a one-line change instead of an #error.

🤖 Generated with Claude Code

brianhuster and others added 30 commits August 18, 2026 12:16
## Summary

* **What is the goal of this PR?** Fix the sleep-screen image remaining
visible underneath the Home screen after a splashless wake.
* **What changes are included?**
* Pass a one-shot clean-initial-refresh option to HomeActivity for
splashless wakes without a retained Quick Resume frame.
* Use `HALF_REFRESH` for that first Home render instead of issuing a
second post-render refresh.
  * Preserve Quick Resume and normal Home navigation behavior.

Fix crosspoint-reader#3002.

## Scope Check

- [x] I have read SCOPE.md and ROADMAP.md.
- [x] This PR is **not** a new built-in theme.
- [x] This PR is **not** a new external network connector.
- [x] This PR is **not** an interactive app, writing tool,
RSS/news/browser, media playback, or PDF feature.
- [x] The stock firmware does not already handle this well, and no other
popular CrossPoint fork already does.
- [x] This PR does not touch `freeink-sdk/`, `lib/hal/`, the bootloader,
OTA, or recovery code.

## Additional Context

Tested in Xteink X4:
- Custom sleep image + waking up from main menu
+ Double checked that we are only refreshing once (the first fix I did
the screen flashed twice)
- Custom sleep image + waking up from book (already working, just
checking for regressions)
- Quick sleep + waking up from book (not really related to the changes
here but just to make sure there were no regressions)

---

### AI Usage

Did you use AI tools to help write this code? **YES**
Also carries a fix for lists and x4pro display variants
## Summary

* **What is the goal of this PR?**
* Make the estimated chapter-page marker (`~`) larger in the reader
status bar so it's not confused with a dash (`-`), especially on lower
PPI devices.

* **What changes are included?**
  * Render the estimate marker separately with the 10pt UI font.
* Preserve its placement in the right-side status-bar layout, including
when a clock is shown.
* Keep the marker limited to estimated chapter counts; it is not shown
for book-progress-only status bars.
* Adds a 2px right margin since it was colliding with the chapter page
number without it.

## Additional Context

* This is a visual-only status-bar change in `BaseTheme::drawStatusBar`;
it adds no heap allocation or persistent-state changes.

**X4 - Before**
<img width="493" height="640" alt="X4 Before"
src="https://github.com/user-attachments/assets/b9f471ee-71c2-4aea-9d92-b868dbb4f995"
/>

**X4 - After**
<img width="525" height="640" alt="X4 After"
src="https://github.com/user-attachments/assets/4b7a167f-2217-4219-a1b4-ca49f91998fd"
/>

**X3 - After**
<img width="598" height="640" alt="X3 After"
src="https://github.com/user-attachments/assets/f5b59c8c-66a3-4d9d-a716-8f2ce24a9678"
/>
…-reader#3186)

## Summary

* **What is the goal of this PR?** Fix Cyrillic glyphs that render fused
together in the UI, reported in crosspoint-reader#3099.
* **What changes are included?** Ubuntu Medium is converted through the
FreeType auto-hinter instead of its own bytecode; the two Medium UI
headers and the font IDs are regenerated.

At UI ppem Ubuntu Medium's bytecode grid-fits `U+0442` (т) to 11px
inside a 10px advance and lands its left bearing at **-1**, so the glyph
hangs into the one before it. In Russian "скрытые" the `ы` and `т` merge
into a single shape and the word reads as something else.
<img width="2469" height="580" alt="fix_before_after"
src="https://github.com/user-attachments/assets/bcc3ee9f-3ce6-44a4-9e6d-756f6f3695c7"
/>

The outline is not at fault — its left bearing is positive in both
weights — and Ubuntu Bold grid-fits cleanly, so only the Medium face
changes. It joins the Hebrew fallback already on the auto-hinter in
crosspoint-reader#3176.

```
U+0442   before: { 11, 11, 161, -1, ... }     after: { 10, 11, 161, +1, ... }
gap after ы:     -0.5px (overlap)                    +2.5px
```

**Stacked on crosspoint-reader#3176** — it needs the per-face `--autohint-font` flag
introduced there, so the diff shows that PR's commit too. Merge after
it.

## Scope Check

- [x] I have read SCOPE.md and ROADMAP.md.
- [x] This PR is **not** a new built-in theme.
- [x] This PR is **not** a new external network connector.
- [x] This PR is **not** an interactive app, writing tool,
RSS/news/browser, media playback, or PDF feature.
- [x] The stock firmware does not already handle this well.
- [x] This PR does not touch `freeink-sdk/`, `lib/hal/`, the bootloader,
OTA, or recovery code.

## Additional Context

Layout is untouched. The converter stores `linearHoriAdvance`, which is
unhinted, so hinting cannot move anything: **zero** of the 1031 glyph
advances change, and Latin, Cyrillic and Vietnamese strings measure
identical widths before and after. Only bitmaps and bearings move — 320
glyphs at 10pt, 269 at 12pt.

No stem drops to a 1px hairline in Latin, Greek or Cyrillic at either
size, so nothing gets thinner on e-ink. `verify-ui-noto-fonts.py` passes
(Hebrew 101.9%/95.3%, Arabic 95.3%/102.6%).

Flash 5,706,019 bytes (87.1%), RAM 16.6% — 46 bytes over crosspoint-reader#3176 alone.

Worth noting for reviewers: Ubuntu Sans 1.006 fixes this same defect
upstream (it is hinted with ttfautohint), but even its Bold renders 2px
stems at UI ppem, which would give back the weight crosspoint-reader#3096 was after. That
is why this fixes the hinting rather than bumping the font.

---

### AI Usage

Did you use AI tools to help write this code? _**YES**_
…ouch boards) (crosspoint-reader#3156)

Co-authored-by: Justin Mitchell <justin@jmitch.com>
…t-reader#3095)

## Symptom

A text-field value long enough to wrap loses a character at every wrap
point, if that point falls inside a multi-byte character. A Cyrillic
OPDS server name shows it plainly — `литературы` renders as `литера` /
`уры`, the `т` is simply gone:

![line
wrap](https://raw.githubusercontent.com/winst0niuss/crossword/assets/cursor-utf8-screenshots/docs-assets/cursor-utf8-wrap.png)

## Steps to reproduce

Reproducible on `develop` in the simulator, no special hardware or data:

1. Open a text field, e.g. `Settings → System → OPDS Servers → <server>
→ Server Name`.
2. Enter a non-ASCII value long enough to wrap onto a second line.
3. A character disappears wherever the wrap lands mid-sequence.

## Cause

`lineBreakEnd()` (`src/activities/util/KeyboardEntryActivity.cpp`)
binary-searches over **byte** indices for the widest prefix that fits,
so the returned wrap point can land inside a multi-byte character.
`render()` then slices the line at that byte and hands both halves to
the renderer as incomplete sequences. Each decodes to `U+FFFD`, and
since no built-in font carries that glyph, the character is not drawn at
all.

## Fix

Snap the wrap point back to a code point boundary. Because widths
measured mid-sequence are unreliable, the snapped line is re-measured
and stepped back whole characters until it fits, and a line always keeps
at least one whole character so the wrap loop still advances.

ASCII is unaffected — the snap is a no-op there.

## Testing

- Verified in the simulator on `develop` (character lost) and on this
branch (intact).
- Long Latin name still fills the line to the same edge as before.
- `clang-format` 21 clean.
- Not tested on hardware.

Found by @coderabbitai's review on crosspoint-reader#2840, which carried this fix
together with the cursor-block one. That PR is now split: the cursor
half is a separate PR, this is the wrapping half.
…oint-reader#3094)

## Symptom

In cursor mode, the block cursor over any non-ASCII character is drawn
one glyph too narrow and the character underneath is not rendered inside
the block. This affects every layout that has accented letters — `é` on
AzertyFr, `ü`/`ß` on QwertzDe, `ñ` on SpanishEs — and any text loaded
from outside (OPDS server names, download folder names).

**Before** — French UI, server name `Café`, cursor on `é`:


![before](https://raw.githubusercontent.com/winst0niuss/crossword/assets/cursor-utf8-screenshots/docs-assets/cursor-utf8-before.png)

**After** — same screen, same keypresses:


![after](https://raw.githubusercontent.com/winst0niuss/crossword/assets/cursor-utf8-screenshots/docs-assets/cursor-utf8-after.png)

The block should be as wide as the character and show it reversed out,
which is exactly what it already does for ASCII.

## Steps to reproduce

No special hardware or data needed — this is reproducible on `develop`
in the simulator:

1. Set the UI language to French (`Settings → System → Language`).
2. Open any text field, e.g. `Settings → System → OPDS Servers →
<server> → Server Name`.
3. Type `é` (bottom letter row on the AZERTY layout).
4. Hold **Up** to enter cursor mode, then press **Left** until the
cursor sits on the `é`.

## Cause

`cursorPos` is a byte offset that is always kept on a code point
boundary, but four places in `render()` sliced exactly **one byte** at
that offset:

| Code | Purpose |
|---|---|
| `text.substr(cursorPos, 1)` | block width |
| `displayText.substr(cursorPos, 1)` (×2) | kerning offset |
| `cursorPos + 1` | start of the trailing run |
| `char buf[2] = {text[cursorPos], '\0'}` | drawing the character in the
block |

For a two-byte character that single byte is a lone lead byte.
`utf8NextCodepoint()` (`lib/Utf8/Utf8.cpp`) validates continuation
bytes, finds none, and returns `REPLACEMENT_GLYPH` (`0xFFFD`) — so the
width is measured for the replacement glyph and the draw call paints
that instead of the letter.

Byte-identical to the old behaviour for ASCII, where one byte *is* one
code point.

## Fix

Uses the existing `utf8Next()` helper to take the whole code point in
all four places. No new helper, no behaviour change for ASCII.

The masked-password path is unaffected: `displayText` is all `*` there,
so the span is still one byte — that is why the fix computes the span
separately for `text` and `displayText`.

## Testing

- Verified in the simulator on `develop` (bug) and on this branch
(fixed) — the screenshots above are those two runs, same input script,
only the branch differs.
- ASCII behaviour unchanged (checked with a Latin server name in the
same screen).
- `clang-format` 21 clean.
- Not tested on hardware.

<details>
<summary>Full screenshots</summary>

Before:


![before-full](https://raw.githubusercontent.com/winst0niuss/crossword/assets/cursor-utf8-screenshots/docs-assets/cursor-utf8-before-full.png)

After:


![after-full](https://raw.githubusercontent.com/winst0niuss/crossword/assets/cursor-utf8-screenshots/docs-assets/cursor-utf8-after-full.png)

</details>

Split out of crosspoint-reader#2828 at @Uri-Tauber's request — this fix is independent of
where the keyboard layout tables live.

Replaces crosspoint-reader#2840, which carried this fix and the line-wrapping one
together; the wrapping half is now a separate PR.
…nt-reader#3153)

## Summary

* **What is the goal of this PR?** Fix `htmlToPlainText()` dropping the
line breaks around headings, so a dictionary definition's `<h1>`–`<h6>`
no longer run into the text that follows.
* **What changes are included?** A one-word fix in `tagBreak()`, plus a
host test for the function.

`tagBreak()` lists `h1`–`h6` among the names that produce a paragraph
break, but the tag-name scan reads *alphabetic* characters only:

```cpp
while (start < end && std::isalpha(static_cast<unsigned char>(input[start]))) start++;
```

So it sees `h1` as `h`, `equals("h1")` can never be true, and the
heading break never happens:

```
htmlToPlainText("<h1>title</h1>body")  ->  "titlebody"     (expected "title\n\nbody")
```

Scanning alphanumerics makes the listed names reachable. No other name
in that table contains a digit, so nothing else changes — `p`, `br`,
`div`, `li`, `tr` and `hr` behave exactly as before.

## Scope Check

- [x] I have read SCOPE.md and ROADMAP.md.
- [x] This PR is **not** a new built-in theme.
- [x] This PR is **not** a new external network connector.
- [x] This PR is **not** an interactive app, writing tool,
RSS/news/browser, media playback, or PDF feature.
- [x] The stock firmware does not already handle this well — this is a
defect fix in existing dictionary code.
- [x] This PR does not touch `freeink-sdk/`, `lib/hal/`, the bootloader,
OTA, or recovery code.

## Additional Context

Found while porting the dictionary feature into a fork, where the same
bug showed up in a definition whose sense headings all collided with
their text.

New test `test/html_to_plain_text` covers tag stripping, block breaks,
entity decoding, whitespace trimming and malformed markup. The heading
case fails on current `develop` with exactly the `"titlebody"` output
above, and passes after the fix — I verified both directions rather than
only the green one.

Memory / flash impact: none (`isalpha` → `isalnum`).

---

### AI Usage

Did you use AI tools to help write this code? _**YES**_ — written with
Claude Code; the diagnosis, the test cases and the before/after
verification were reviewed and run by me.
## Summary

Fixes an incorrect navigation path in the _Installing Fonts_ section of
`docs/sd-card-fonts.md`.

**Before:** `Settings > System > Manage Fonts`
**After:** `Settings > Reader > Manage Fonts`
## Summary

* **What is the goal of this PR?** Ensure Title Check runs for the
current PR head after commits are pushed.
* **What changes are included?** Add the `synchronize` activity type to
the existing `pull_request_target` workflow.

## Scope Check

CrossPoint is intentionally narrow. See
[SCOPE.md](../blob/master/SCOPE.md) and
[ROADMAP.md](../blob/master/ROADMAP.md).
Please confirm:

- [x] I have read SCOPE.md and ROADMAP.md.
- [x] This PR is **not** a new built-in theme (themes are temporarily
closed pending the move to SD-loaded themes).
- [x] This PR is **not** a new external network connector (sync engine,
cloud storage, remote file access, etc.).
- [x] This PR is **not** an interactive app, writing tool,
RSS/news/browser, media playback, or PDF feature.
- [x] The stock firmware does not already handle this well, **and** no
other popular CrossPoint fork already does
      (or, if one does, I explain why CrossPoint still needs it below).
- [x] If this PR touches `freeink-sdk/`, `lib/hal/`, the bootloader,
OTA, or recovery code, I have coordinated with
      the relevant maintainer.

**If this PR was opened against the previous (broader) scope and was
already in flight under Phase 0, link the
relevant Discussion or issue so reviewers can see the history.**

## Additional Context

Without `synchronize`, pushing commits does not trigger this workflow,
leaving Title Check without a result for the new head. GitHub documents
`synchronize` as the pull-request activity emitted when the head branch
is updated:
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target

Validated with `git diff --check` and a local YAML syntax parse.
Firmware memory and flash impact: none.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_
Co-authored-by: Julia Nguyen <julia@uxj.io>
Co-authored-by: Uri Tauber <uritaube@gmail.com>
…ss maps, Zopfli) (crosspoint-reader#3144)

Co-authored-by: Uri Tauber <uritaube@gmail.com>
…-reader#3215)

## Summary

* **What is the goal of this PR?** Fixes the battery-only deep-sleep
wake bug on the Xteink X4 Pro
(crosspoint-reader#2863). After a battery-only sleep,
the next power-button press cold-booted (~8–15 s to content) instead of
fast-waking (~1–2 s). With the serial/pogo adapter connected, external
power masked the problem.
* **What changes are included?** One change in
`HalPowerManager::startDeepSleep()` (+21 lines,
`lib/hal/HalPowerManager.cpp` only): every configured power-latch pin
(`BoardConfig::ACTIVE.power.latch0/latch1`) is driven HIGH and armed
with `gpio_hold_en()` before entering deep sleep.

Root cause: `freeink::PowerManager::deepSleep()` calls
`esp_sleep_config_gpio_isolate()`, which strips every pad without an
armed hold. The X4 Pro's master peripheral-rail enable is `power.latch0`
= GPIO1 — `holdPowerRails()` drives it HIGH at boot but arms no sleep
hold, so once isolated the latch floats LOW when external power is
removed. The fix keeps all configured power-latch pins driven and held
HIGH through sleep; peripheral rails (SD / touch / EPD reset) keep their
existing OFF-level holds from `powerDownRailsForSleep()`, so standby
current doesn't regress. The loop deliberately skips GPIO13 on the C3
Xteink boards: it *is* `power.latch0` there and is driven LOW on purpose
as the battery power-off. Generic over the profile's latch pins — also
covers the Sticky's PWR_HOLD/PWR_LOCK (GPIO45/46).

## Scope Check

CrossPoint is intentionally narrow. See
[SCOPE.md](../blob/master/SCOPE.md) and
[ROADMAP.md](../blob/master/ROADMAP.md).
Please confirm:

- [x] I have read SCOPE.md and ROADMAP.md.
- [x] This PR is **not** a new built-in theme (themes are temporarily
closed pending the move to SD-loaded themes).
- [x] This PR is **not** a new external network connector (sync engine,
cloud storage, remote file access, etc.).
- [x] This PR is **not** an interactive app, writing tool,
RSS/news/browser, media playback, or PDF feature.
- [x] The stock firmware does not already handle this well, **and** no
other popular CrossPoint fork already does
      (or, if one does, I explain why CrossPoint still needs it below).
- [x] If this PR touches `freeink-sdk/`, `lib/hal/`, the bootloader,
OTA, or recovery code, I have coordinated with
      the relevant maintainer.
→ Touches `lib/hal/` only (`HalPowerManager.cpp`); filed against crosspoint-reader#2863
where maintainer triage is already happening. No SDK changes required
(SDK pointer unchanged at `e4d3cc3`).

Stock firmware note: this restores behavior stock users get implicitly
(adapter plugged in or self-latching hardware revisions); it fixes wake
for battery-only use, which stock firmware does not handle well.

## Additional Context

* Memory / flash impact: none measurable — ~21 lines of flash, no new
RAM, no heap allocation. Runs once per sleep entry.
* Risk area to focus on: the C3 GPIO13 exclusion (battery power-off must
still drop the pin LOW); covered by keeping that block ahead of the new
loop and skipping the pin in the loop.
* Verification evidence:
  - `pio run -e x4pro` SUCCESS locally on this exact commit.
- Identical patch fully green on CI twice: upstream run 32916960939
(Build default/sticky/papermono/x4pro, unit tests, cppcheck,
clang-format) and Belphemur#3 (all checks pass incl.
CodeRabbit).
- Hardware test procedure for reviewers: flash, sleep on battery, unplug
adapter 60 s, short-press power → expect instant wake landing back on
the page (`ESP_RST_DEEPSLEEP` + `ESP_SLEEP_WAKEUP_GPIO`), not
splash/cold boot (`ESP_RST_POWERON`).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_ — root-cause
analysis and patch drafted with an AI coding agent (Hermes), reviewed
and tested by hand before submission.
Uri-Tauber and others added 23 commits August 31, 2026 11:34
## Summary

Should help with crosspoint-reader#3019. 

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
# Summary

Mixed Hebrew and English passages now keep the book's intended reading
direction, including paragraphs that begin with an English term.

suppress crosspoint-reader#3143, which was a downstream change, rather than taking care
of the root.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< PARTIALLY >**_
## Summary

I implemented four requests from crosspoint-reader#2964

- Hold Left/Right 500ms to move repeatedly through dictionary words
- Wider, taller, evenly spaced Lyra tabs
- Rotated controls automatically follow orientation on touch devices 
- “Add Server” directly from the OPDS server picker

All tested on x4 and sticky apart from the last one - I dont use OPDS
server...

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< PARTIALLY >**_
… on (crosspoint-reader#3341)

Co-authored-by: Jim Myers <jim.myers@jfmyers.dev>
Co-authored-by: Uri-Tauber <uritaube@gmail.com>
…#3355)

## Summary

* **What is the goal of this PR?**
* Restore CSS superscript and subscript styling on internal EPUB links,
including the footnote markers commonly written as `<a
style="vertical-align: super">1</a>`.

* **What changes are included?**
* Apply resolved `vertical-align: super/sub` styles when parsing
internal link elements, matching the existing generic inline-style
behavior.
* Bump `section.bin` to version 45 so cached layouts created before the
fix are automatically rebuilt.
* Add a generated EPUB fixture plus focused host tests covering both
superscript and subscript internal links. The tests verify the text
style and retained link target/ID.
## Additional Context

* The regression was limited to the parser’s internal-link branch: it
preserved the link and underline but returned before applying
`vertical-align`.
Adopts upstream's setContentMarginFromScreen everywhere and drops the
fork's setContentMarginAbsolute from every shared activity. Mario's call:
a permanent divergence on a call every screen makes is paid at every
future sync.

No unfolding was needed. marginBeyondSafeArea(m,s) = m>s ? m-s : 0, so
setContentMarginFromScreen(m) insets the screen by s+max(0,m-s) = max(m,s):
it is idempotent w.r.t. the safe area. The SDK comment that warns about
applying the safe area twice is about plain setContentMargin(), not the
FromScreen form. 18 of the 19 conflicting call sites passed arguments
already identical to upstream's, so those resolved to a rename.

The submodule now points at the SDK fork merged with Free-Ink main
(16 upstream commits, clean merge, 2832 SDK host checks pass).

Conflicts where BOTH sides' work was kept, not one side taken:
- MappedInputManager: upstream's isNavDirectionSwapped() routed through
  the fork's readButton(), which is the single place a button index is
  read and therefore the only thing the release gate can cover. Upstream's
  update() (long-press bookkeeping) landed by auto-merge in the .cpp while
  the fork defined update() inline in the .h; taking either side alone
  would have silently dropped settleReleaseGate() or duplicated the
  definition. Both behaviours are now in the one definition.
- ActivityManager: settleReleaseGate() kept ahead of upstream's new
  exclusive-storage early return; night mode keeps appliesNightMode();
  the frontlight panel keeps its Frontlight.present() guard on top of
  upstream's status-bar tap entry.
- GfxRenderer: upstream's applyPromotedRefresh() plus the fork's PanelSpan
  panel-time billing and paintclock::notePainted().
- main.cpp: the fork's dev-serial-bridge wake escape wraps upstream's
  wakeHoldVerified gate; heartbeat::begin() moved to follow upstream's
  deliberate relocation of SETTINGS.loadFromFile().
- FirmwareBoardTag: the fork defines the board macro in the header, so
  upstream's new .cpp block would have redefined it; upstream's x4c board
  was ported into the fork's header instead.
- listIcons: 18 fork icons + upstream's usb = 19, disjoint sets.
- BaseTheme: upstream's Usb keeps its enum number, Games/Apps stay last.

ROADMAP.md and the two GitHub issue templates stay deleted (3b76299).

NOT adopted: upstream's env:x4c / env:x4c-gh_release (X4 Classic). That
is new device support, a product call, not a merge decision.

Docs: bezel-insets.md updated in a follow-up commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ists

The two remaining shared-activity callers (OpdsFilter, OpdsDetail) move to
setContentMarginFromScreen. Both derive their margins from
getScreenSafeArea(), so every side is >= the safe area and max(m, safe)
returns them unchanged: no geometry moves.

setContentMarginAbsolute now has exactly two callers, both fork-only:
- toybox::absoluteChrome() passes Insets{}. FromScreen({}) = the safe rect,
  which pushes every game's chrome down by the top inset -- the regression
  docs/bezel-insets.md records finding on the device twice.
- XkcdScreens passes a band that can sit on the true panel edge. X4 Pro's
  safe.bottom is 0 so they agree there, but a board on the inherited
  {9,3,3,3} default would have a deliberate full-bleed clamped by 3px.

Upstream cannot express "content rect = full frame" at all: Screen seeds
content_ from safeRect() and both upstream calls inset from safeRect().

.gitmodules named branch x4pro-measured-insets, which has been DELETED on
the fork; the pinned commit lives on fix/sd-free-space, now fast-forwarded
to the SDK merge (c444616..29221d2) so the pointer resolves.

Docs: docs/bezel-insets.md carries the arithmetic and both exceptions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things upstream changed that the merge could not resolve by itself.

1. EpdFontData gained 7 pointer fields (split kern class maps, sparse CSR
   kerning) BETWEEN kernRightClasses and kernLeftEntryCount. The fork's 14
   generated cuts in src/apps_local/ui/fonts/ use positional initializers,
   so every field after that point was reading the wrong slot -- the build
   named it as "cannot initialize const uint16_t* with const int8_t[3551]".
   Migrated all 14 from 20 to 27 fields, NOT regenerated: regenerating a
   Toybox cut moves glyph metrics and shifts text in every app. The fork's
   cuts keep the packed class maps and the dense matrix, so the new
   pointers are null and getKerning() takes the same branch it always did.
   Field order taken from upstream's own regenerated notosans_12_bold.h.

2. The simulator ships its own HalStorage that shadows lib/hal, and
   upstream's USB Drive work added prepareForDeepSleep(), beginUsbDrive(),
   disconnectUsbDriveHost(), endUsbDrive() and usbDriveState() plus the
   UsbDriveState enum. Added sim_catchup patches for all six; parity check
   now reports 26 methods, both sides agree. The stubs answer Unsupported,
   which is the truth for a host-side simulator with no USB host attached.

3. Night mode: upstream removed Activity::appliesNightMode() in 62e84a1
   and made the polarity global, with SleepActivity forcing normal itself.
   That method came from upstream (d720891), not from this fork, so by the
   same rule as the margin API the fork takes upstream's version.

4. prewarmscope pinned the OLD prewarm shape. Upstream now buckets the scan
   by RESOLVED style and hands prewarm a deduplicated character set, so a
   style a face does not have folds into one it does instead of costing a
   second pass. The suite's own property -- one face is one prewarm however
   many styles the page draws -- still holds and still passes. Three value
   assertions were updated to the new shape after watching each go red, and
   the SdCardFont fake now folds every style to regular, which is what a
   single-face fake actually does.

Audited every identifier present in the fork and absent from the merge
(103 after filtering font tables): 102 originated upstream and were
superseded by upstream's own rewrites; the one fork-authored name,
rowSubtitle, was a parameter of the legacy BaseTheme::drawList upstream
deleted, and its capability survives through the fui list path with the
fork's tuned listWithSubtitleRowHeight values intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The simulator ships its own HalGPIO.h and BoardConfig.h that shadow lib/hal
and the SDK, and both had drifted from what src/ now calls:

- verifyPowerButtonWakeup() lost its two arguments upstream (it reads the
  settings itself now) and main.cpp calls the new form; the simulator package
  still declared the old one. Its body was already a constant true.
- BoardConfig::isX4Classic() is called by the reader; the simulator's Board
  enum has no such board, so the honest stub is a constant false.

Three fork blocks were orphaned by upstream rewrites and could not compile:

- EpubReaderActivity: resolving that conflict as "keep both sides" put
  upstream's idle prewarm back in, whose members the fork deleted on purpose
  in 0ccbfa3 after measuring that PrewarmScope threw the warmed glyphs away
  before the turn meant to use them. The fork's rationale comment stays, the
  code does not; upstream's orientation reflow is kept.
- LyraTheme::iconForName had grown a size argument for a 24px list path
  upstream has since deleted, and the per-icon headers it read (folder24.h
  and friends) are gone with it. Back to upstream's one-argument form, with
  the fork's Games/Apps shelf icons kept in the switch.
- UITheme::getNumberOfItemsPerPage lost its declaration, the BaseTheme helper
  it called, and every caller. Removed rather than propped up.

Also closed a brace: taking the fork's side of the OpdsServerList picker-mode
conflict left its `if (!pickerMode)` open, because upstream's side had no such
block and the shared trailing brace went with it.

Verified by rendering, not by exit code: Home, Settings and the Games shelf
all draw correctly in the simulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings the 16 commits that landed while this sync was being resolved,
including the tree-wide clang-format pass (fb7d8ec) and the CI gate that
now fails on formatting drift (9f24eb5). No conflicts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI fails on formatting drift since 9f24eb5. bin/clang-format-fix --check
is clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dy has

The gate caught what my tree could not: the impl patch anchored on
"bool HalStorage::freeBytes(uint64_t &out) {", a line that a LATER patch in
the same file inserts. In a warm tree freeBytes had already been applied by
an earlier build, so the anchor was there and the patch worked. In the gate's
fresh worktree mine runs first, the anchor does not exist yet, and the patch
silently does nothing -- "no longer applies" in the log, then five undefined
HalStorage symbols at link time, naming UsbDriveActivity and main rather than
anything to do with the patch.

Both patches now anchor on pristine simulator text (removeDir in the header,
HalStorage::begin in the impl) and keep that anchor in their replacement, so
the order they run in cannot decide whether they apply.

Verified the way the gate does it: wiped .pio/libdeps/simulator_x4_pro
entirely and rebuilt. All three patches report "applied", the five
definitions are in the simulator's HalStorage.cpp, and the simulator boots
to Home.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sync runbook described the stale branch as a live condition. It named
x4pro-measured-insets, which had since been deleted on the fork, so a
fetch --recurse-submodules failed outright rather than merely pointing
somewhere unhelpful. Records what it names now and what is still owed.

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

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
crossplay Ready Ready Preview Sep 4, 2026 6:07am UTC

ma-r-s and others added 2 commits September 4, 2026 00:44
Upstream's new screen hands margins that already fold the X4 Pro safe
area to plain setContentMargin(), which insets from the safe rect again:
the list started 10px lower and 1px narrower than every other settings
screen. setContentMarginFromScreen() is the call docs/bezel-insets.md
prescribes for folded margins. Inherited from upstream, found by the cold
review of the sync; worth an upstream issue.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The gate on the sync branch flagged two things of ours: the one-line
Keyboard Layouts fix needed clang-format, and site/study/tools.zip was
stale against tools_local/study after the upstream merge; regenerated
with tools_local/study/sync_site.py. The emulator staleness is CI's to
rebuild after the merge, as always.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ma-r-s
ma-r-s merged commit 6a33355 into xteink Sep 4, 2026
3 checks passed
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.