Skip to content

Promote dev → staging (desktop runtime CSS, RX gain control, Linux launcher) - #805

Merged
patrickrb merged 12 commits into
stagingfrom
dev
Sep 8, 2026
Merged

patrickrb merged 12 commits into
stagingfrom
dev

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Promotion of dev into staging. 12 commits, all in the desktop (Tauri) app — no Android changes in this batch.

What's included

Runtime-loaded styles (PR #804)

  • styles.css moved to desktop/public/ and loaded from disk at runtime instead of being bundled at build time, so theme tweaks don't need a rebuild.

Live RX input gain

  • New RX input gain control next to the decode filter chips, wired through engine.rs / audio/input.rs with a debounced IPC call (debounce.ts + unit tests).

Text-visibility fixes

  • Scoped fix for unreadable white-on-light text in the Rig control Display name field and other select fields. Two earlier broader attempts (caret fix, global --text change) were reverted in favor of the scoped version.

Linux launcher

  • desktop/linux/run-ft8af.sh wrapper that works around a real Hamlib/QMX version conflict.

Debug helper

  • --list-audio CLI flag for enumerating audio devices.

Commits

dd0cbd6a Merge pull request #804 from laniol8926/runtime-styles-css-v2
74b47355 Address code review on the runtime-CSS / RX-gain PR
43be433e Merge remote-tracking branch 'origin/dev' into runtime-styles-css-v2
f389894d Add live RX input gain control, placed next to the decode filter chips
bff5992b Scoped black-text fix for select fields; add --list-audio debug helper
9680d653 Scoped, verified-safe fix: white text on the Rig control Display name field
28463d26 Revert "Fix unreadable input/select text: light --text color on a light native widget background"
1adc1b65 Revert "Fix invisible text caret in input/select fields"
d42c4a8b Fix invisible text caret in input/select fields
fb86646c Fix unreadable input/select text: light --text color on a light native widget background
7bb85aab Add Linux launcher wrapper working around a real Hamlib/QMX version conflict
f7f2fce4 Load styles.css from disk at runtime instead of bundling it at build time

🤖 Generated with Claude Code

https://claude.ai/code/session_01D7Z6xhcHB3XspStFrQ5puU

laniol8926 and others added 12 commits September 4, 2026 17:04
…time

Tauri embeds frontendDist into the compiled binary at build time (confirmed
directly: editing the bundled dist/styles.css post-build and relaunching the
same binary had zero effect on rendering), so any styling change previously
needed a full rebuild to take effect -- painful with a Rust/Tauri build on
constrained hardware, and heavy iteration on the stylesheet expected.

New get_custom_css command (main.rs) reads a real file from the app's data
directory (~/.local/share/FT8AF/styles.css on Linux) fresh on every launch,
seeding it from the compiled-in default (public/styles.css, via
include_str!) on first run so there's something to open and edit
immediately. main.tsx now awaits this and injects the result as a <style>
tag instead of a static Vite-bundled import, so editing that file and
relaunching the app picks up changes with no rebuild at all -- same
"resolved live, not baked in at compile time" idea already used for the
Hamlib rig list.

Verified end-to-end with a real screenshot test: launched the built binary,
edited the on-disk styles.css to a loud, unmistakable color with no rebuild,
relaunched the same binary, and confirmed the new color rendered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…onflict

rig.rs's load_hamlib() does a bare dlopen("libhamlib.so.4") -- resolved
against whatever the dynamic linker finds first. On this machine that's the
distro-packaged libhamlib.so.4 (Hamlib 4.5.4), which has no QMX support at
all (confirmed: 0/283 rigs listed included QMX, and rig_init failed for
model 2057). A separately-built Hamlib 4.7.1 already exists at
/usr/local/lib/libhamlib.so.4 with QMX support (RIG_MODEL_QRPLABS_QMX
present) -- same soname as the system package, so whichever the linker
resolves first wins for any process that doesn't override the search path.

Not fixed by upgrading the system package: other software on this machine
(CQRLOG) depends on the distro Hamlib specifically. desktop/linux/run-ft8af.sh
instead sets LD_LIBRARY_PATH just for FT8AF's own process, so it
preferentially finds the newer build without touching anything system-wide.
Falls back to the system Hamlib (previous behavior) if no /usr/local build
exists.

Verified end-to-end through the wrapper: rig list jumps from 283 to 321
entries (QRPLabs QMX now included), and the app made a real, live CAT
connection to a physical QMX (kenwood_transaction traffic, frequency set,
54ms round-trip) -- confirmed via screenshot showing "rig: qmx" instead of
"no rig".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e widget background

select and input elements set background: var(--panel) (dark), but
WebKitGTK's native rendering doesn't fully respect that for <select> --
confirmed live, the rig/band dropdown and text fields render with a light
background regardless, so the existing color: var(--text) (light,
#d7e0e8 -- meant for dark panels) made every text box's contents nearly
unreadable. Verified live via the new runtime styles.css: edited the
on-disk file, relaunched with no rebuild, confirmed all Settings-tab
inputs (Callsign, Grid, Display name, Radio) and both dropdowns
(rig/band picker, Backend, Input/Output device) now render crisp black
text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same root cause as the earlier unreadable-text fix (light --text color on
a light native widget background), but a separate CSS property: caret-color
was never set, so the blinking text cursor was effectively invisible.

This explains a real, confusing user-facing symptom: with no visible caret,
retyping a value without first clearing the field silently inserts new
characters at the actual (but invisible) cursor position instead of
replacing anything -- confirmed live, a garbled "AI5IIAI5IIAI555" ended up
saved after a few real attempts to enter a callsign, matching exactly what
duplicate-insertion-without-clearing produces. Root-caused by resetting the
field to empty and typing a single character with the caret-color fix
applied: cursor now renders as a clear, visible black line, confirmed both
in a screenshot and by the user directly on the live display.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ht native widget background"

This reverts commit fb86646.
… field

The earlier global select/input color override broke real keyboard input
entirely (reverted in fb86646/28463d2 -- see project memory:
project_ft8af_webkitgtk_input_bug.md). This is narrower and confirmed
different: targets only the one Display name <input> via its placeholder
text as a CSS attribute selector (no JSX change, no rebuild needed), not
the shared select/input rule that also matches every native <select>
dropdown.

Verified live, twice, with a real keyboard: text renders white and
readable, and typing into the field still works normally -- unlike every
variant of the global rule change, which broke input every time it was
tried. Callsign/Grid and the <select> dropdowns are untouched by this
commit and still use native-default text color.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the single-field-scoping approach validated in 9680d65 (Display
name input) to every select that was unreadable against its native light
background: Band, Rig control (Backend/Radio/Connection/Serial port/Baud),
Audio (Input/Output device), and Developer waterfall FFT
(Window function/FFT size/Averaging). Each gets its own id and a CSS rule
targeting just that id -- the shared select/input rule stays untouched,
since overriding color there is what broke real keyboard input earlier
(see project_ft8af_webkitgtk_input_bug.md).

Verified live with a real keyboard across two rebuild/retest cycles:
closed-select display text and open dropdown-list text both render black
and readable, Callsign/Grid typing still works normally, and none of the
scoped selects show any of the input-blocking behavior the global rule
change caused.

Also commits main.rs's `--list-audio` debug helper (used earlier tonight
to diagnose the audio device list, analogous to the existing --list-rigs)
-- prints cpal's enumerated input/output devices and exits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New RX gain slider (0-100%, default 100%/unity) mirrors the existing TX
gain pattern end to end: a lock-free atomic in the audio capture callback
(AudioInput::set_gain(), read fresh per sample on the realtime thread, no
locking), an EngineCommand::SetRxGain handler that persists to config and
applies live without restarting capture, a set_rx_gain Tauri command, and
an ipc.ts wrapper.

Placement: next to the All/CQ/To me filter chips on the Decode screen, not
buried in Settings -- real feedback was that this needs adjusting per-band
(noise floor varies a lot band to band) while actively watching decodes,
the same reasoning that will apply to relocating TX gain next to a future
Tune button.

Range settled at 0-100% (matching TX gain) after live testing: temporarily
widened to 0-800% to confirm the control had real effect (confirmed: mute
at 0%, a genuine clip warning near 800%, proportional movement of the raw
dBFS meter in between), but adjustment past 100% wasn't practically useful
-- the QMX's USB audio codec has no analog preamp stage (confirmed via
amixer: only a capture on/off switch, no volume control), so there's no
real gain headroom on the input side to exploit past unity. Settled on
0-100% for finer control resolution across the range that matters.

Both TX and RX gain sliders now debounce their backend IPC call (~120ms
after the last onChange) while keeping the displayed value instant --
dragging fires far more onChange events than needed, and that load
stacking on the waterfall's own frequent canvas redraws correlates with a
real WebKitGTK renderer crash observed live (the WebKitWebProcess child
disappeared entirely, leaving a blank window, while the Rust backend kept
running) -- not proven as the sole cause, but a safe mitigation regardless.

Also clarified: the waterfall's lack of visible change across the gain
range is by design (its own noise-floor-relative auto-leveling, stated in
its own code comment), and the raw "audio dB" meter reading "silent" at
low gain while decodes keep working is expected too -- it measures
broadband RMS while the decoder/waterfall work in narrow FFT bins, and FT8
is designed to decode at very low SNR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	desktop/src-tauri/src/engine.rs
#	desktop/src-tauri/src/main.rs
#	desktop/src/App.tsx
Select readability (styles.css): the per-id `color: #000` fixed the Linux
symptom but broke the other two platforms. WebKitGTK draws <select> as a
native GTK widget and ignores the author background, so --text sat on a
light control; WebView2 and WKWebView *do* honor the dark --panel
background, so black text there was black-on-#1a2129 -- and CI ships all
three. Opt selects out of native rendering with `appearance: none` so the
theme actually paints everywhere, redraw the arrow as a background image,
and pin both colors on the option list, which the platform draws outside
the page. Drops the dead #rig-radio-select selector along the way.

Stale on-disk stylesheet (main.rs): the seeded copy was frozen after the
first launch, so a later release that ships new markup would render it
unstyled with no in-app reset, and an interrupted first write left a
0-byte file that read back Ok("") and injected an empty <style> forever.
Stamp the seed with an FNV-1a hash of its own body: a file whose stamp
still matches is an untouched seed and is taken back when the compiled
default changes; an edited file, an unstamped one, or one we never wrote
is left alone. Empty or whitespace-only now re-seeds.

Unstyled fallback (main.tsx): with the bundled CSS import removed and
index.html linking nothing, the IPC read was the only source of styling
and the catch rendered anyway -- `npm run dev` in a plain browser, where
invoke rejects, came up as bare HTML. Fall back to the compiled-in copy.

RX gain: docs on SetRxGain, the engine field and set_gain still claimed
0.0-2.0 after the range was finalized at 0.0-1.0. Non-finite input now
falls back to the default instead of being clamped -- f32::clamp
propagates NaN, and the result is persisted where "NaN" parses back
cleanly, so one NaN would kill RX across restarts. The new full-scale
clamp in push_mono applies only above unity: at or below it, it could
only ever clip samples the device already delivered out of range, which
hard-clips the slightly-over-scale samples F32 backends (CoreAudio,
JACK/PipeWire) hand out on an otherwise-default install.

Linux launcher: LD_PRELOAD the one Hamlib rather than prepending
/usr/local/lib to LD_LIBRARY_PATH, which redirected every library FT8AF
resolves; and fall back to an installed ft8af on PATH so the script works
outside a source checkout, which is where its own docs send it.

Debounce: extracted to src/debounce.ts so it is testable, and cancelled
on unmount so a drag in flight cannot fire IPC after teardown.

Tests: resolve_styles/stamp round-trip and recovery cases, clamp_rx_gain
and the non-finite fallback for both gains, push_mono gain/clip/downmix
behavior, and the debounce module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D7Z6xhcHB3XspStFrQ5puU
Runtime-loaded styles.css; Linux Hamlib/QMX launcher wrapper; readable form-field text
@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.99029% with 68 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.64%. Comparing base (fb8318a) to head (dd0cbd6).
⚠️ Report is 14 commits behind head on staging.

Files with missing lines Patch % Lines
desktop/src-tauri/src/main.rs 69.09% 34 Missing ⚠️
desktop/src-tauri/src/audio/input.rs 57.14% 27 Missing ⚠️
desktop/src-tauri/src/engine.rs 78.78% 7 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##             staging     #805      +/-   ##
=============================================
+ Coverage      42.14%   42.64%   +0.49%     
+ Complexity       228      226       -2     
=============================================
  Files            270      267       -3     
  Lines          32257    32068     -189     
  Branches        3734     3664      -70     
=============================================
+ Hits           13596    13675      +79     
+ Misses         18385    18132     -253     
+ Partials         276      261      -15     
Flag Coverage Δ
desktop 63.95% <66.99%> (+0.95%) ⬆️
ios 97.71% <ø> (ø)
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
desktop/src-tauri/src/engine.rs 31.72% <78.78%> (+1.98%) ⬆️
desktop/src-tauri/src/audio/input.rs 41.22% <57.14%> (+41.22%) ⬆️
desktop/src-tauri/src/main.rs 25.16% <69.09%> (+25.16%) ⬆️

... and 13 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@patrickrb
patrickrb merged commit 97d4c5e into staging Sep 8, 2026
38 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.

2 participants