Skip to content

Runtime-loaded styles.css; Linux Hamlib/QMX launcher wrapper; readable form-field text - #804

Merged
patrickrb merged 11 commits into
patrickrb:devfrom
laniol8926:runtime-styles-css-v2
Sep 8, 2026
Merged

patrickrb merged 11 commits into
patrickrb:devfrom
laniol8926:runtime-styles-css-v2

Conversation

@laniol8926

Copy link
Copy Markdown

Summary

Resubmission of #799 rebased onto current dev (that PR's base had drifted ~1540 commits and gone into conflict). Same three desktop-app changes from real testing on Linux (aarch64), all verified live against a physical QRP Labs QMX transceiver:

  • Runtime-loaded styles.css: styles.css was imported in main.tsx, so Vite bundled it into a content-hashed file that Tauri embeds into the compiled binary at build time. Confirmed directly that editing the bundled dist/styles.css after a build and relaunching the same binary had zero effect. Now a get_custom_css Tauri command 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 on first run. main.tsx awaits this and injects the result as a <style> tag before rendering. Editing that file and relaunching now picks up changes with no rebuild at all.

  • Linux launcher wrapper for a real Hamlib version conflict: rig.rs's load_hamlib() does a bare dlopen("libhamlib.so.4"), resolved against whatever the dynamic linker finds first. On a system with both a distro-packaged Hamlib (no QMX support) and a separately-built newer one (with QMX support) sharing the same soname, the distro one wins by default — confirmed live: 0/283 rigs included QMX, rig_init failed for the QMX model. desktop/linux/run-ft8af.sh sets LD_LIBRARY_PATH just for FT8AF's own process, preferring a newer Hamlib build at /usr/local/lib if one exists, with zero effect on any other software on the same machine that depends on the system Hamlib package. Falls back cleanly to the system Hamlib if no such build is present.

  • Readable form-field text: on this WebKitGTK build, several <input>/<select> fields (Display name, Band, Backend/Radio/Connection/Serial port/Baud, Audio Input/Output device, Developer waterfall FFT controls) rendered with light text against a native light background — unreadable. Fixed by scoping a color override to each individual field (via id or a placeholder-based attribute selector), not the shared select, input rule. That distinction matters: overriding color on the shared rule was tried first and reverted after real-keyboard testing showed it broke text entry entirely across the app (confirmed with xev that real KeyPress events stopped reaching the window — a WebKitGTK quirk, not a data/logic bug, and not fixed by loading the same CSS the old static way either, ruling out the runtime-loading mechanism as the cause). Scoping the same color change to one field at a time was confirmed safe across every field it was applied to. Rebased onto dev's combined rig-backend/model selector and its removal of the standalone "serial" rig backend — the id-scoping now targets that current structure (the old separate "Radio" select and legacy serial-backend block no longer exist upstream, so they're gone from this fix too).

Rebase notes (this resubmission)

  • Original branch (runtime-styles-css, PR Runtime-loaded styles.css; Linux Hamlib/QMX launcher wrapper; readable form-field text #799) was cut from dev on 2026-07-07 and had drifted ~1540 commits behind, producing merge conflicts. Re-merged onto current origin/dev (main.rs, engine.rs, App.tsx had real conflicts — dev has since landed RX input gain, the WSJT-X UDP feature, OS-location support, and a rig-backend/model selector refactor; all preserved, resolved by hand, not overwritten).
  • git diff origin/dev...HEAD is functionally identical to the original PR's diff (same 8 files, ~same insertion count).
  • Verified npx tsc --noEmit passes clean on the resolved App.tsx/ipc.ts. Rust toolchain is no longer installed on this machine (removed in an unrelated cleanup), so the Rust side is unverified locally — relying on CI plus the fact both Rust conflicts were non-overlapping additive hunks.

Test plan

  • Full release build on real aarch64 Linux hardware (npm run tauri build) — original PR, prior to rebase
  • Verified with a real screenshot test: launched the built binary, edited the on-disk styles.css to an unmistakable color with no rebuild, relaunched the same binary, confirmed the new color rendered
  • Verified the launcher wrapper via --list-rigs: rig count went from 283 → 321 entries with QMX included, and confirmed a real, live CAT connection to a physical QMX transceiver through the wrapper
  • Verified the readable-text fix is real-keyboard-safe: xev-confirmed KeyPress delivery, and manual typing into every affected field, across multiple rebuild/retest cycles
  • Confirmed the launcher wrapper is a no-op for other software on the same machine depending on the system Hamlib package
  • npx tsc --noEmit clean after the rebase's conflict resolution
  • Full release build not re-verified post-rebase (no local Rust toolchain) — relying on CI

Closes #799.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf

laniol8926 and others added 10 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
@laniol8926

Copy link
Copy Markdown
Author

@patrickrb the CI workflow runs on this PR (Desktop CI, Native FT8 tests, iOS CI, Android CI) are all sitting at action_required since it's from a fork — could you approve them when you get a chance?

@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 (74b4735).

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              @@
##                dev     #804      +/-   ##
============================================
+ 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%) ⬆️
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.

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
@patrickrb
patrickrb merged commit dd0cbd6 into patrickrb:dev Sep 8, 2026
19 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