The wheel is the Pager's only way to move the highlight, which is slow
and imprecise. Bring the T-Deck's keyboard navigation to it:
- With no text field focused, programmable letters move the highlight
(default I up, K down, J left, L right) and scroll (U / O). Select
and Back default to no letter, since Enter and Backspace-hold (the
wheel's click / long-press) already do them. On the Map the letters
move the highlight; W/A/X/D stay the map pan.
- The tab keys are programmable like the T-Deck's, defaulting to the
Pager's M/C/H/A/S, and the bar's labels show whatever is bound.
- Settings > Keyboard gets the same "Keyboard navigation" switch (on by
default here) and the same tap-a-row-then-press-a-key remap rows; the
T-Deck's rows move into settingsNavKeyRows() so both boards share them.
The Pager's switch only turns the letters on or off; it never empties
the focus group, which the wheel always needs.
- A remap refuses letters the Pager uses for fixed actions (Q/E slider
nudge, W/A/X/D map pan; A stays allowed as a tab key).
- In wheel edit mode on a slider, J/L preview like a turn and the
select key closes the edit, matching the wheel.
Settings: the Pager's kbd_nav and key bindings were never shown on that
board, so they held the T-Deck defaults. Schema v67 (no new field) puts
the Pager defaults in once on that board only. Two new strings,
translated in every deploy/apps/lang file; i18n_builtin.h regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
On the M9, V4-R8 and Pager the text presets grow the status-bar offset
ladder with the font (SBX), and the builder places the Bluetooth glyph
with it. The clock placement then re-aligned the clock with plain SC(),
which leaves it at its 100% spot, so at Large / Huge the Bluetooth glyph
moved left onto the time. The charging slide also used raw 100% offsets,
so plugging in or unplugging snapped the cluster back under the grown
glyphs.
- The wide-bar clock uses SBX() like the icons it sits beside.
- The charging slide uses exactly the builder's offsets (SBX plus the
battery-% overflow); the narrow V4 portrait bar keeps its raw values.
- Safety net for every board: after placing the clock, if it overlaps a
visible icon on its row (Bluetooth, DND, sleep, Wi-Fi, signal,
battery), move it just left of the leftmost one. Covers the 12-hour
clock, the Bluetooth-keyboard glyph and the centred hide-name clock.
Re-checked only when something that moves or resizes them changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
On short landscape panels (the T-Pager is 480x222) the bottom nav bar
spends a full-width strip on five icons. New Settings > Display switch
"Condense Nav", off by default, applies after a restart:
- The nav bar becomes a rail on the right edge (LV_DIR_RIGHT tabview).
Pager cells show the key letter under the icon; the Tanmatsu keeps its
coloured F-key shapes; the T-Deck / P4 key hints sit under the icons.
- Home's launcher column (Advert, Terminal, Files, Apps, Control) shows
icons only; the buttons keep their height and spacing, and the status
text, chart and info card widen into the freed space.
- The active-tab marker turns vertical beside the active cell, the
unread and update badges move to the top-right of their cells, and the
rail stays solid (and the marker visible) on the Map.
- Content that kept the bottom bar visible now stops at the rail
instead: tabContentW() subtracts the rail, and the map canvas, Settings
landing grid, chat-list rows, Mentions and the app drawer use it.
Swipe-left on the rail opens the drawer (swipe-up on the bottom bar).
Portrait layouts and the M9 are unchanged: the mode is decided once in
buildUiTree from the pref and the orientation at boot, the switch is
greyed out in portrait and absent on the M9.
TABBAR_H becomes a runtime value (0 with the rail) next to a new
TABBAR_W. The pref is a new trailing field, condense_nav (schema v66);
the schema test's layout asserts now count the v64 fields they missed.
Strings added to every deploy/apps/lang file and i18n_builtin.h
regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
A plain wheel turn moves focus, and a click sends Enter, which a slider
ignores, so no slider on the Pager (Screen timeout, Brightness, the
Control Center sliders, Map zoom) could be adjusted with the wheel; only
the hidden Q/E keys moved them.
Clicking the wheel (or Enter) on a focused slider now enters an edit
mode, shown with a bigger contrasting knob. Turns move the value with a
live preview (VALUE_CHANGED) and save nothing. Clicking again saves
(RELEASED, the same commit a touch drag ends with) and returns the wheel
to focus navigation. Back (long-press or Backspace-hold) restores the
original value without saving, as does the screen timing out or focus
leaving the slider. Q/E preview instead of saving while in edit mode.
The slider step moves out of navMoveDir() into navSliderNudge() so the
wheel and Q/E share it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
The idle power-saver (touchSleep) already ran on every touch build: the
hooks are installed in UITask::begin and main.cpp calls loopEnd() every
tick, and its gates are board-neutral. Only the Settings switch, its
callback and the amber battery tint were behind a T-Deck / V4-R8 / M9
allowlist, so on every other board (T-Pager, CrowPanel, T-Display P4,
T-Deck Pro, ...) the pref defaulted OFF with no way to turn it on --
the same bug the M9 had until 2026-09-02.
Drop the allowlist from the switch row, sleepIdleToggleCb, applyBattColor
and the status-bar re-tint. Also rejoin the lockOnScreenOffToggleCb
comment the old #if had split in two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019M5VJ43X3nUULPqVcgrAH2
Two things made a full harness run untrustworthy, and together they produced a
false accusation against a community app.
wada.fs was stubs. append threw the data away, remove always said true, and
read, write and list were not there at all — although the firmware registers
all five (LuaAppHost.cpp). Any app that writes a file and reads it back
therefore failed here while working perfectly on device. wardrive 1.2 is
exactly that app: it errors on a nil fs.read at its first log flush, long
before it renders anything, and the emoji scenario then reported the repeater
name as unrendered. Read as "1.2 regressed the beta_70 emoji fix", which it
did not. It passes untouched once fs is real.
wada.fs is now an in-memory filesystem following LuaAppHost's semantics: read
returns the data plus the file's TOTAL size so a caller can window a growing
log, an offset at or past the end gives an empty string rather than nil, a
missing file gives nil, and list is sorted so a hash order cannot make a test
flaky.
The second: the gpscompass scenario list was acting as the dispatch default,
so an app with no branch of its own was run against tests written for a
different app. Six store apps have no branch (2048, airtime, monitor, nearby,
sdktest, snake), so a full sweep came back with six red apps as its normal
state — and a real regression would not have stood out against that. Those
apps now say they have no scenarios and run none. They are untested, which is
worth saying out loud, but that is not the same as broken.
A sweep of every store app is now 8 apps passing their own scenarios, 6
reported untested, 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The M9 is the one board with no bottom bar (TABBAR_H == 0, tab_btns hidden
outright), so it never got the numeric badges the others have. beta_75 put
three glyph labels on layer_top across the bottom instead, OVER page content,
and blinked two of them at 1 Hz. With nothing else down there to read them
against, that came back reported as the navigation icons flashing.
They now carry a count and are shown only while they mean something:
* The person glyph was driven by discoveredCount(), a standing total that is
non-zero on any live mesh from the first advert heard and only ever falls
when you add or purge. It therefore blinked forever. It now counts what has
not been looked at: watermarked at boot, because a ring restored from flash
is not news, and again whenever the Discovered list is opened, with a clamp
so purge, adding to Contacts and ring eviction cannot leave the next real
arrival reading as nothing new.
* It also blinked while you were standing on Contacts with that list open.
Suppressed there, the way the mail glyph already was on the inbox tab.
* Two style writes and a z-reorder ran on every 250 ms tick regardless of
whether anything was pending, invalidating the strip for nothing.
m9NoticeSet() only touches LVGL on a real change, and takes a refront flag
so a live notice still lifts over the app drawer, which is the one overlay
notices are allowed to sit on top of.
Also: "100%" was clipped by the signal bars. The percentage is right-anchored
at SBX(22) and grows left, the bars are pinned at SBX(54), so the text gets
SBX(32) and no more; "100%" is a whole glyph wider than "99%", and the bars
are created after the label so they paint over the leading 1. Reported by
buko84 and confirmed by jd1227, both on the M9, which is a board whose text
presets grow the font and leave the geometry at 100%, so it runs out of room
first. statusPctOverflow() measures the real width and slides the bars, the
Wi-Fi glyph and the SD dot left by the overflow, capped, and gated to those
font-only-preset boards. Zero where the text already fits, so no board moves
that did not have to, and the clock, the DND moon and the BLE glyph never
move. The round panel already solved this with statusClusterStart(); this is
the same idea for the single-row ladder.
Built for the M9, the V4-R8, the Pager and the T-Deck. The T-Deck binary is
byte-identical, which is the check that the ladder change reaches nothing
outside its gate. Not run on hardware: needs an M9 tester.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dropped the fault framing. The notes describe the mechanism and the change,
not whose code it was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beta_86 put the emoji picker on the long-press menu, which covers the firmware's
own field and so covers wada.ui.input(). It does not cover an app that builds its
own buffer from key events, because there is no textarea to long-press -- and
that is exactly the case Jade asked about: their composer shows a pad-bytes
counter the firmware's field cannot, so it takes the custom path and the menu
never applies to it.
wada.ui.emoji(cb) opens the picker and hands the glyph back as UTF-8, nil if
dismissed. Same discipline as wada.ui.input's callback: one ref, one call,
released either way, refused if one is already open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported by Jade as "screen memory errors, worse the longer the app has been
open", with an app that rebuilds its screens on a 120 ms debounce.
Button and list-row callbacks live in one registry table keyed by the LVGL
object POINTER. clear() calls lv_obj_clean(), which frees every one of those
objects, and bumps s_ui_gen so stale handles go inert, but it never touched the
table. So each rebuild added an entry and kept the Lua closure, with its
upvalues, alive forever. Unbounded, and worst for exactly the apps that are
built properly: a list of twenty contacts redrawn on a timer leaks twenty
closures a go.
It was also a correctness bug in waiting. LVGL reuses freed addresses, so a new
button created WITHOUT a callback could land on an address a dead one left
behind and fire that stale closure on the next tap.
clear() now replaces the table. Every entry in it describes an object
lv_obj_clean just destroyed, so there is nothing there worth keeping.
Found by reading the host after Jade asked what the memory budget was; the
budget was never the problem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Curated from the two beta notes. Leads with the two things a user must know
before this reaches their device: do not switch a T-Display P4 to landscape on
this build (fixed in beta_86), and a T-Deck Max on beta_84 must be updated over
USB rather than from the device.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jade asked for three things for Lua apps. Two already existed and the answer is
documentation, not code: wada.sys.epoch() and wada.sys.datetime() have returned
the clock since #245, and wada.ui.input() is the firmware's own field, so it
already carries the long-press Cut/Copy/Paste/All/Sym menu, which is exactly the
"copy/paste from chats into a Lua text field" that was asked for.
The third is here. The long-press menu gains Emoji, so the picker reaches every
field in the firmware rather than only the chat composer: a Lua app's text entry,
a node name, a Wi-Fi password. The row is now sized to the panel instead of a
fixed 48 px per cell. It was already 268 px at five cells, wider than a 240 px
screen, so a sixth would have hung off the M9 and the V4 entirely.
Status bar: the right-hand ladder (battery, signal bars, Wi-Fi, Bluetooth, sleep,
clock) is a chain of fixed offsets tuned for 12 px text. The boards with
font-only UI-size presets grow the font and deliberately leave geometry at 100%,
so the labels widened and the ladder did not, and the battery percentage ran into
the signal bars (jd1227, M9, beta_85 -- the release that gave that board the
presets). SBX() scales those offsets by how much the text actually grew and
returns exactly SC() at Normal, so nobody who kept the default size sees any
change. Verified on the M9, V4-R8, Pager, T-Deck and V4.
SDK docs: corrected the app path. They said to copy the file to /apps/ on the SD
card; a card-backed device roots everything at /meshcomod, so it is
/meshcomod/apps/ there and a bare /apps/ only on boards that store internally.
Following the old wording cost gottlieb8849 an evening.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The five apps from #585 and #586 all defaulted to being seeded, which bakes
their source into the boards that cannot reach the Store. That is 40 KB of Lua,
and it took the Heltec V4 from 95.4% of flash to 96.7%: the tightest board in
the matrix paying for apps two of which it cannot use.
gen-lua-builtin.py states the rule: an app that is large, or that needs hardware
those boards do not have, earns its place rather than getting it by being in the
catalog. ProTreck (17 KB, altimeter/chrono/sensors) and Trip Odometer (GPS-only)
are both that, so they are seed:false, exactly like gpscompass and sdscan. They
remain in the Store for every board that can download.
Tetris, Breakout and Ping stay baked: no hardware needs, small, and the boards
that get seeded apps are precisely the ones that cannot fetch them otherwise.
V4 lands at 96.2%.
lua_builtin.h is regenerated here because it is tracked and the build rewrites
it. Leaving it out is how Wardrive and Nearby ended up missing from the seeded
boards after beta_68.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stacked on #585 and merged after it, as the PR asks. Two conflicts, both from
the stack base moving:
apps.json: unioned by id, so all fourteen apps survive.
harness.lua: the additive blocks came from #586. Where both sides edited the
same code, #585's is newer and wins: its ping scenarios pass cfg.gps as a
FUNCTION, which is what the harness calls (cfg.gps()); #586 still passed the
table and would have failed. Taking #586 there would have broken ping.
Then restored the two dispatch branches my first resolution dropped. The
breakout and tripodometer scenarios existed but nothing ran them, so both
apps silently fell through to the gpscompass scenarios and "failed".
All five new apps pass their own scenarios, and so do gpscompass and sdscan.
Closes#562, #563, #564, #568, #569, #570, #571, #574, #576 and adds the
Elecrow CrowPanel Advance 3.5 (#561). Carries the fix for the beta_85 P4
landscape touch, which was rotated twice: once by the driver and once by LVGL.
- New crowpanel_35 variant: display, SD, board and target files, plus board JSON
- Portrait mode is drawn rotated 180°; landscape stays at rotation 3
- Share the GT911 touch driver between Wio Tracker L2 and CrowPanel
(WioTrackerL2Touch.cpp -> src/helpers/input/LgfxGt911Touch.cpp)
- platformio envs, flasher/site entries, release and merge-bin support
- Tests for the CrowPanel SD codec and merge-bin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132MkJ7RUV32mV9F7o4PJMm
Sun/Moon rise+set from GPS lat/lon + date (Spencer 1971), moon phase 0-29.
Chronometer with lap times, countdown timer with alarm beep and preset
cycling, altimeter with GPS alt_m, min/max persistence and trend graph.
Swipe left/right to switch tabs. 4 tabs fit any screen size.
test(harness): add 7 ProTreck scenarios (tdeck, v4, no_gps, chrono,
timer, alti, cost). ALL OK 7/7.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Send a DM to any contact, measure round-trip time. Both devices
exchange their GPS coordinates; the result shows RTT, distance and
cardinal bearing when both have a fix. The app also auto-replies to
incoming pings so two devices running Ping can time each other.
Harness additions: send_dm mock, wada.geo (haversine), deliver_dm
helper, and 6 scenarios (tdeck, v4, no_contacts, round_trip,
auto_reply, cost) — all pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tracks total distance (Haversine accumulation), elapsed time, current
speed, max speed, and altitude from live GPS fixes. Start/Stop button
pauses accumulation; Reset clears the trip. The current trip persists
across app restarts via wada.store.
Also adds 6 harness scenarios (no-fix, T-Deck, moving, persist, reset,
cost) and registers the app in apps.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ball + paddle + 8×5 brick grid. Swipe left/right (or arrow keys) to
move the paddle; swipe up / Enter / Space to launch. Angle on bounce
varies by paddle-hit position. Speed increases each level; high score
persists via wada.store.
Adds 6 harness scenarios (tdeck, v4, keyboard, logic, hiscore, cost).
Worst tick ~8k instructions, 8% of the 100k budget.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detects /tetris/ in the app path and routes to 6 dedicated scenarios:
- tetris_tdeck/v4: open + swipe input on touch screens
- tetris_keyboard: arrow + enter input on keyboard boards
- tetris_logic: hard-drop to game-over, verify restart resets timer to 700 ms
- tetris_hiscore: verify high score is stored as a number after game-over
- tetris_cost: 100 ticks, worst ~6000 instructions (budget 100000)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Tetris to the app catalog. Full 7-tetromino implementation with:
- Correct piece rotations for all 7 tetrominos (I, O, T, S, Z, J, L)
- Wall kick on rotation (tries +/-1 col when center position collides)
- Hard-drop on swipe-down or Enter/Space
- Soft-drop on keyboard arrow-down
- Score scaling by level (100/300/500/800 x level per 1/2/3/4 lines)
- Speed increase every 10 lines cleared (700ms to 80ms floor)
- Next-piece preview and high score persistence via wada.store
- Game-over overlay with tap-to-restart
- Centered layout adapts to any screen width/height
Tested against the wada.* API contract (integer coords, canvas bounds,
store string/number restriction, instruction budget per tick).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The P4's larger UI scale grew the button over the swatch, which sat at a fixed
x. On the P4 the button is narrower and the swatch sits a gap past its end, in
every orientation. Other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On touch boards without a built-in keyboard, opening a chat focused the
composer and that focus raised the on-screen keys, covering the conversation.
The focus now only binds the composer (cursor shown, a Bluetooth, clip-on or
CardKB keyboard types straight in); tapping the composer brings the keys up.
T-Deck, T-Deck Pro and M9 are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the keyboard expansion arrives while the UI is portrait, a dialog counts
down 15 s and then saves landscape and restarts (the Settings orientation
switch's path); "Reboot Now" does it at once. Cancel, or taking the keyboard
off again, keeps portrait. A keyboard already attached at boot is not an
arrival, so a Cancel is not asked again on every start, and nothing is offered
under the lock screen. showConfirm() gains confirmSetMessage() to update the
open dialog's text in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GRWzzRs7oRvFRNcHUZF4c
Lock screen (all boards, #579): the default view is now drawn from parts
instead of the old 320x240 bitmap with the padlock and wordmark baked in,
which cover-scaled into a jumble on any other panel shape. WADAMESH sits at
the top, the clock below it (clear of the status bar), a smaller padlock in
the centre with the unread count to its left and a new battery readout to its
right, each centred between the padlock and the screen edge. Short panels get
a smaller wordmark and padlock. The clock's burn-in drift now moves around its
new place instead of snapping it back to y=30 every minute. T-Deck Pro keeps
its black-on-white e-paper colours; custom wallpapers are unchanged.
T-Display P4: the BOOT button (GPIO35) now works like the Wio Tracker L2's
wake button -- a press wakes or lights the lock screen, a 2 s hold locks or
unlocks -- so the P4 gets the lock screen and the "Lock when screen off"
setting. It gets no Power-off row: GPIO35 cannot wake the P4 from deep sleep.
Also: the chat list's per-row gear clears the list scrollbar on large panels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The T-Display P4 keyboard expansion (TCA8418 + its own XL9555, ported from
camillia-mt) now works. It is looked for every 1.5 s, so it can be clipped on
or off while running, and is routed like the CardKB and Bluetooth keyboards:
it types into the focused field and keeps the on-screen keyboard down. The
external-keyboard routing was gated on Bluetooth, which the P4 lacks, so it
now follows a new CAP_EXT_KEYBOARD; the Bluetooth-only parts stay on
CAP_BLE_KEYBOARD.
- Keys: Shift/Sym/Alt one-shot, Caps Lock lights the expansion LEDs, arrows
and Backspace repeat, Alt+Backspace backs out, F1-F5 open the tabs left to
right, and F11 steps the keyboard light through off/low/medium/high (saved
across reboots).
- Wire1 is shared with the ES8311 codec on other pins; P4I2c1 switches it
under a lock, and the codec's lazy bring-up now goes through it.
- With the keyboard attached the tab bar labels F1-F5 beside each icon, and
Enter on a chat bubble opens its action menu.
Also, on any keyboard build, Enter runs the command in the terminal and the
repeater admin console (the admin console's Enter hook sat on its Send button,
where nothing raised it). And on the P4: the status-bar name shrinks to fit the
bar, the Discover buttons are spaced out, contact names clear their icon in
landscape, and leaving an app returns the focus to its tile in the app drawer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keyboard/trackball cursor is a 3 px accent outline, and on an accent
fill it disappears. setSelectionGlow already swaps to the contrast
outline (white, black on Day) there, but only for a fully opaque fill;
the Map's side buttons are accent at 70% over the tiles, so they kept
the accent outline and the selected one barely stood out. Any fill that
is at least half accent now takes the contrast outline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The accent popup keeps a pointer to the text box it edits, and about 25
separate paths close the popup by hand. A path that deleted the text
box without closing it (e.g. a form closing after its send) left the
popup on screen pointing at freed memory; tapping an accent letter then
ran lv_textarea_del_char on it. That is the beta_85 T-Deck crash in
#574 (loopTask LoadProhibited at 0x3, accentBoxCellCb ->
lv_textarea_del_char), matched against the release image.
Tie the popup to its text box with an LV_EVENT_DELETE hook that clears
the pointer and hides the popup (hide only: in a layer clean the popup
may be deleted in the same pass). The cell handler also checks the text
box is still valid, and accentBoxHide no longer deletes a popup that
something else already removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The right-aligned peak readout shares the RBW/span line, and on the
narrow portrait panel it ran over that text. In portrait the RBW/span
line, chart and waterfall start one text line lower; the peak stays put
and the tall panel has the room. Landscape, the T-Deck Pro (peak on its
own line already) and the other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page offset its content by a second STATUSBAR_H to clear the tall
status bar's lower row, which overlaps the page on square panels. The
round panel's bar is already its full two rows and never doubles, so on
the P4 that only left an empty band above the Home button, in both
orientations. Start 8 px below the bar there instead.
In portrait, Flood and Zero-hop stack as two full-width buttons instead
of sharing a cramped row. Landscape and the other boards are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sheet's two-column grid of half-width buttons cut most labels off
with "..." on the narrow portrait panel. In portrait every action gets
its own full-width row; the card grows to fit and its body scrolls past
the screen height like any tall sheet. Delete stays the red bottom row.
Landscape and the other boards keep the grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The P4 is a large-screen board, so its quick-reply/emoji chips were
56 px against a 34x30 send button, all pinned to the composer's bottom
edge, where the landscape row sat under the panel's bottom corner arcs.
All three buttons are now 45 px (the average) in both orientations and
vertically centred on the text box, re-centred as it grows. That lifts
them ~9 px off the screen edge, with a floor matching the status bar's
top-corner clearance. In landscape the quick-reply button also moves
16 px in from the bottom-left corner. Replaces the 32 px the portrait
two-row change assumed from the small boards' sizes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node name and advert location save on blur, and every blur saved and
showed "Name saved" / "Position saved", edited or not. On the M9 (no
touch) moving focus past a field with the d-pad is a blur, so the
alert flashed for each field crossed.
On blur, skip the save and the alert when the field still holds what
it was filled with: the name with missing glyphs replaced, the
position as "%.6f". An edit still saves and says so. This also stops a
bare blur writing the glyph-replaced display form over the real name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chan add <name> [key], chan add #tag, chan del <slot|name> and
chan key <slot|name>, in the on-device console and remote terminal.
They reuse the Channels screen's save and delete paths, so the chats
list updates at once. No rename: chat threads are matched to channels
by name, and the Channels screen has no rename for the same reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The channel/sender line was pinned at y=30 and the message at y=60,
so a title taller than 30 px (larger UI-size presets, bigger panels)
was drawn over the message, and the rest of a tall screen stayed empty.
Centre both horizontally and centre the pair vertically with 16 px
between them; the message box takes the height its text needs, capped
to what fits and cut at a whole line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The name started a fixed 24 px after the icon, but the antenna and room
glyphs are wider than the person glyph and all three grow with the
UI-size preset, so some names touched their icon. Start the name 8 px
past the widest of the three, never closer than before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The send button sat at the composer's bottom-right, under the panel's
rounded corner. Pull it in by the same 16 px safe-area the tab bar
uses, and narrow the text box to match.
Portrait: the single row (QR | emoji | text | send) was crunched on the
284-px panel. Split it into two rows: quick-reply and emoji
right-aligned on top, text and send below. All three buttons are 32 px,
midway between the old 30-px chips and the 34-px send button.
Landscape keeps the single row, and chatComposerApplyRows re-lays it
out when the keyboard's rotate button flips orientation with a chat
open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a Lua app was open, every physical-keyboard key went to the app,
and luaAppKey always claims the key, including while the app's own
wada.ui.input prompt was open. The prompt's text box never received a
character on the T-Deck, Pager, M9 or Attaky, or from a Bluetooth
keyboard (reported with Wardrive's "Name this run").
Don't forward keys to the app while that prompt is open, in
handleHwKey and both bleKbdDispatch paths; they then type into the
bound text box like any other prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In landscape the P4's tab bar barely responded to taps, while swipes between
tabs still worked.
P4 landscape is LVGL software rotation, and LVGL 8 rotates pointer input for a
rotated display itself (indev_pointer_proc): it expects points in the
unrotated panel frame. The touch driver was also mapping points to landscape
(heltecV4CapTouchSetPointRotation), on the belief that LVGL does not, so every
point was rotated twice. In ROT_90, the rotation the Orientation button picks,
a tap at (X, Y) reached LVGL at (615 - Y, X): a tap on the bar at the bottom
landed in the middle of the screen or below it, and never on a tab button.
Swipes were unaffected because the driver detects them in its own mapped
coordinates.
The driver keeps its landscape mapping, which its swipe detection and the
direct readers (tab-bar guard, remote-mode hold) need. lvglTouchRead now
converts the point back to the panel frame before giving it to LVGL, keyed on
the driver's rotation, so the chat keyboard's rotate trick on a portrait UI
(LVGL rotated, driver not) passes through unchanged. Also corrects the comment
in UITask::begin that said LVGL does not rotate input.
Not built or tested on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R
In landscape the P4 home screen mixed its portrait column-flow layout with the
landscape right-hand button column: the TX/RX chart ran full width underneath
the buttons, and the info card was placed below the portrait launcher grid,
off the bottom of the 284-px-tall screen.
Landscape now follows the Wio Tracker L2's arrangement: status lines and a
chart sized to the remaining height on the left, clear of a right-hand column
of Advert / Terminal / Apps / Control spread evenly down the content height
(6-px gaps, each 22..46 px). The info card is portrait-only; its eight rows do
not fit landscape. The height is taken from the measured tab, since the
round-corner status bar leaves less than tabContentH() reports. Portrait is
unchanged.
Not built or tested on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbY4FsGuX2fNDpNttB6a7R