feat(theme): Phase 2 mass migration — 59 files, ~1000 hex → token substitutions#3102
Merged
Conversation
…stitutions
PR 1 of 5 in the wrap-up sweep. Adds the migration tool and applies
it to every src/gui/ file with inline-stylesheet hex literals matching
the Phase 2 canonical taxonomy (docs/theming/canonical-tokens.md).
What's in this commit:
tools/migrate_colours.py — static rewrite tool that:
1. Walks src/ for .cpp/.h files
2. Locates setStyleSheet(...) calls with string-literal arguments
(handles C++ adjacent-literal concatenation and QStringLiteral
raw strings)
3. Substitutes every hex matching the canonical-token table with
a {{token.name}} placeholder
4. Wraps the call: setStyleSheet(STR) →
setStyleSheet(AetherSDR::ThemeManager::instance().resolve(STR))
so resolve() expands the tokens at apply time
5. Inserts #include "core/ThemeManager.h" into the top-of-file
unconditional include block (skips conditional-compile blocks
so the include doesn't land inside #ifdef Q_OS_WIN where it
wouldn't compile on other platforms)
src/gui/<59 files>.cpp — applied tool output. Hex literals in
setStyleSheet template strings replaced with {{token}} placeholders;
call sites wrapped in resolve(). No semantic change to the rendered
UI — resolved colours match the previous hardcoded values within
sub-perceptual canonicalisation shifts (where close variants
collapsed onto the same token).
Audit impact:
Before: 3641 total colour references
After: 2627 (-1014, -28%)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 2 mass migration in this PR rewrote setStyleSheet sites in CwxPanel.cpp and HelpDialog.cpp to route through ThemeManager::resolve(). Two test executables link those .cpp files directly but didn't list ThemeManager.cpp + its logging deps, so they failed at link with: undefined reference to AetherSDR::ThemeManager::instance() undefined reference to AetherSDR::ThemeManager::resolve(QString const&) Fix: add ThemeManager.cpp + AppSettings.cpp + LogManager.cpp + AsyncLogWriter.cpp to both test target source lists. Same recurring class of bug already fixed for container_*_test in #3090 — bot/PR authors miss the downstream test-target link surface when a header change adds a new function call across translation-unit boundaries. Verified locally: both targets link clean, both tests pass under QT_QPA_PLATFORM=offscreen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5cdeb55 to
ac65d49
Compare
…x entries Post-migration audit revealed 106 unique colours within ΔRGB ≤ 4 of an existing canonical token — but the migration tool's scope is setStyleSheet strings only, so most of those (73 of 106) are paint-code QColor literals that Phase 3 will address. The 33 colours that DO appear inside setStyleSheet template strings get added to CANONICAL_TOKENS with their nearest existing canonical token: background.0 (13 entries: #0b1220 / #06111c / #09111b / ... all ≤4 RGB) background.1 (7 entries: #1e3040 / #24384e / ...) text.secondary (6 entries: #9fb0c0 / #7f93a7 / ...) text.primary (2 entries: #e7f1fb / #d6e4f2) background.3 / background.tx / meter.bar.fill / text.label / accent.warning (1 each) Re-ran migrate_colours.py: 5 additional files updated, 10 additional setStyleSheet hex substitutions absorbed. Total post-migration audit goes from 2627 refs → 2617 refs (-10). Modest, but each entry is a real previously-uncanonicalised site. Also documents the near-duplicate cluster for future migration passes — if Phase 3's QColor-literal migration finds the same hex values, this table already maps them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876
added a commit
that referenced
this pull request
May 25, 2026
…base hex (#3110) Post-#3102 user testing surfaced visible darkening of applet backgrounds, title bar, and status bar. Root cause: my canonical taxonomy picked design-aesthetic values for two of the highest-impact tokens instead of the actual dominant codebase hex: color.background.0 was #0a0e14 → now #0f0f1a (84 refs to literal #0f0f1a — the QWidget base colour in Theme.h's app-wide stylesheet; the old token value was ΔRGB 12 darker, perceptible against dark UI) color.text.primary was #e6f0fa → now #c8d8e8 (367 refs — the most-used colour in the whole codebase by far; the old token value was visibly lighter) Both files updated in lockstep: - resources/themes/default-dark.json (v1.2 → v1.3, baked into the Qt resource bundle) - src/core/ThemeManager.cpp::seedBuiltinDefaults() (the compiled-in fallback when no theme file loads) No call-site changes needed — every site that resolves through ThemeManager::color() or the {{color.text.primary}} / {{color.background.0}} template placeholders automatically picks up the new values. Lesson for the taxonomy doc: canonical token values must come from the **dominant codebase hex** at sites that use the token, not from the designer's first instinct. Phase 4 Light theme can revisit the aesthetic tuning; Phase 2's job is bit-identical-where-possible preservation of the v26.5.3 look. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
ten9876
added a commit
that referenced
this pull request
May 25, 2026
…dget (#3113) ## Summary **PR 3 of 5** in the Phase 2/3 wrap-up. Adds a paint-code QColor migration tool and applies it to \`src/gui/SpectrumWidget.cpp\` — the main panadapter, the visible-most paint surface in AetherSDR. ## The tool: \`tools/migrate_paint_colours.py\` Sibling to \`migrate_colours.py\` (the setStyleSheet migration tool from #3102). Handles all three QColor constructor forms: \`\`\` QColor(0xAA, 0xBB, 0xCC) → ThemeManager::instance().color("token") QColor(0xAA, 0xBB, 0xCC, alpha) → AetherSDR::theme::withAlpha("token", alpha) QColor("#aabbcc") → ThemeManager::instance().color("token") \`\`\` Uses the same \`CANONICAL_TOKENS\` table as the setStyleSheet migration tool — colours not in the table are left alone (logged for the cleanup pass). ## New helper: \`AetherSDR::theme::withAlpha\` Added to [core/ThemeManager.h](src/core/ThemeManager.h) so alpha-bearing migrations stay readable: \`\`\`cpp namespace theme { inline QColor withAlpha(const QString& token, int alpha) { QColor c = ThemeManager::instance().color(token); c.setAlpha(alpha); return c; } } \`\`\` Without this, every alpha-bearing paint-code site would inline an ugly lambda. ## Applied to SpectrumWidget - **33 QColor literals migrated** to theme tokens (out of 70 in the file) - **37 skipped** — spectrum-specific accent shades, beacon markers, slice indicators, etc. that aren't in the canonical table yet The migrated 33 cover the main panadapter chrome (backgrounds, axes, body text, common accents). The skipped 37 are spectrum-domain colours that need dedicated tokens (PR 5 cleanup will extend the canonical table for them). ## Verified locally - [x] Linux build clean (68 link steps, no errors) - [x] No regressions flagged by compiler - [x] tools/migrate_paint_colours.py is dry-run by default (--apply to write) ## Why this is incremental The main panadapter now responds to theme changes for the 33 migrated sites. When Phase 4 ships the Default Light theme, the panadapter's overall chrome will adapt; the 37 spectrum-domain literals stay as-is until PR 5 adds their canonical tokens. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open the app, confirm panadapter renders identically (the 33 migrated literals resolve to the same hex values as before via the canonical table) 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
ten9876
added a commit
that referenced
this pull request
May 25, 2026
…migration, canonical table closeout (#3117) ## Summary **PR 5 of 5** in the Phase 2/3 wrap-up — closes the mechanical migration sweep. 159 \`QColor\` literals migrated across 11 widget files via the now-mature \`migrate_paint_colours.py\` tool, plus a closing 50-entry canonical-table expansion. ## Files migrated (11) | File | Literals | Notes | |---|---|---| | \`SpectrumWidget\` | 40 | Residual paint code from #3113 | | \`ClientChainWidget\` | 20 | | | \`StripChainWidget\` | 20 | | | \`ClientRxChainWidget\` | 16 | | | \`StripRxChainWidget\` | 16 | | | \`WaveformWidget\` | 15 | rx + strip-mode share the same palette | | \`StripWaveform\` | 15 | | | \`KeyboardMapWidget\` | 8 | 10 \`categoryColor()\` returns left as-is (see Skips) | | \`ClientCompThresholdFader\` | 5 | Meter gradient stops deferred from #3116 | | \`ClientCompEditorCanvas\` | 3 | | | \`ClientGateCurveWidget\` | 1 | Translucent cyan fill deferred from #3116 | ## Canonical-table additions — 50 new close-mappings Three logical groups in \`tools/migrate_colours.py\`, each entry annotated with its sibling canonical value and ΔRGB so the rationale is auditable later: \`\`\`python # Spectrum paint code (25) \"#80d0ff\": \"color.accent.bright\", # peak hold light cyan \"#ff5858\": \"color.accent.danger\", # bright alert red \"#ffc040\": \"color.accent.warning\", # warning amber, ΔRGB≈0 from #ffb84d # … 22 more # Chain / waveform (11) \"#587890\": \"color.text.label\", # waveform centerline \"#ffd166\": \"color.accent.warning\", # peak-hold, ΔRGB≈1 from #ffd070 # … 9 more # Threshold fader / gate gradient stops (6) \"#2f9e6a\": \"color.accent.success\", # comp meter lo green \"#f2362a\": \"color.accent.danger\", # comp meter peak red \"#50b4dc\": \"color.accent.dim\", # gate curve translucent cyan # … 3 more \`\`\` ## Build verified clean - [x] AetherSDR builds clean - [x] All 308 targets build clean (every test target) - Two warnings surface (\`sliceColor\` / \`effectiveGridStepMhz\` unused) — both pre-existing on \`main\`, confirmed via local stash bisect ## Intentional skips (deferred to Phase 4) 1. **10 \`KeyboardMapWidget::categoryColor()\` returns** — semantically distinct hues for Frequency / TX / Audio / DSP / etc. categories. No shared visual language applies; converting would homogenize them and defeat the categorization. Left as raw \`QColor\`. 2. **1 \`ClientCompMeter\` no-go-zone tint (\`#3a1810\`)** — unique meter visual, no canonical equivalent worth absorbing. 3. **Slice indicator runtime theming** — \`SliceColorManager\` still reads from the compile-time \`kSliceColors[]\` table in \`SliceColors.h\`. Default-dark.json already has \`color.slice.a\` through \`.h\` tokens, but wiring \`SliceColorManager\` to read them requires touching \`SpectrumWidget\` overlay rendering + \`VfoWidget\` badges + \`RxApplet\`. Warrants its own PR. 4. **Waterfall colormap runtime theming** — \`color.waterfall.colormap\` gradient token is already shipped in default-dark.json (validating gradient-token support added in Phase 2), but the \`SpectrumWidget\` waterfall renderer hard-codes its colormap stop set. Conversion warrants its own PR. ## Phase 2/3 closeout This closes the **5-PR Phase 2/3 wrap-up** as constrained. Recap: | PR | What | Status | |---|---|---| | #3102 | Mass migration + token taxonomy | merged | | #3106 | MeterSlider pilot (Phase 3 PR 2/5) | merged | | #3113 | Paint-color tool + SpectrumWidget (PR 3/5) | merged | | #3116 | Comp/gate/curve widgets (PR 4/5) | merged | | **#XXXX** | **Spectrum + chain + waveform closeout (PR 5/5)** | **this PR** | Phase 4 picks up: default-light theme + slice indicator runtime theming + waterfall colormap runtime theming. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open the app and confirm the spectrum + chain + waveform widgets render to-within-ΔRGB-of-50 of current main (the 159 migrated literals resolve to canonical token values that fall within audited deviation thresholds) 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
K5PTB
added a commit
to K5PTB/AetherSDR
that referenced
this pull request
May 25, 2026
…ethersdr#2962) Implements a native SmartCAT-compatible TCP CAT server, enabling band, mode, and frequency sync with N1MM, fldigi, WSJT-X, MacLoggerDX, and other logging/digital-mode software via the industry-standard CAT protocol. ## Why CAT instead of rigctld N1MM does not speak the Hamlib NET rigctl protocol; it requires a CAT port (TS-2000 or FlexCAT dialect). This made a full CAT implementation necessary for N1MM compatibility. Not1MM (a separate Linux-only product) uses rigctld — that is covered by aethersdr#2975. ## Protocol coverage - Full Kenwood TS-2000 command set per the manual appendix - Full FlexCAT command set per SmartCAT User Guide v4.1.5, except commands not applicable to a Flex radio (see PR body for gap list) - Two TCP ports; each port has a companion PTY that accepts the same commands — operators with existing SmartCAT/SmartDAX configurations can keep them unchanged ## Integration testing Tested against real hardware (FlexRadio 6500, firmware 4.2.18): | App | Connection | Result | |--------------|-----------|--------| | fldigi | TCP + PTY | ✓ | | WSJT-X | TCP + PTY | ✓ | | MacLoggerDX | TCP | ✓ | | N1MM+ | TCP | ✓ (CAT sync; CW keying not possible — see below) | Primary platform: macOS. Also tested on Raspberry Pi 5 / Raspberry Pi OS Trixie. ### N1MM CW keying — known limitation N1MM keys CW by toggling RTS/DTR on a real serial COM port. Virtual serial ports (PTYs) are not exposed as COM ports in Windows, so RTS/DTR keying cannot reach AetherSDR. No workaround exists without a WinKeyer-compatible keyer emulator; documented here so operators don't waste time chasing it. ## Test suites - `CAT_TS-2000_test`: 15 sections, 103 checks (92 always-on + 11 PTT/CW gated) - `CAT_Flex_test`: 16 sections, 129 checks (105 always-on + 24 PTT/CW gated) PTT and CW keyer tests are **skipped by default** to avoid unintended transmissions. Enable with `--ptt` and `--cw` when a radio is connected and the operator is ready to transmit. Firmware TX race noted during CW testing: the radio briefly reports TX=0 via CAT while the PA is still hot after CW completes. Safety watches added to the test suite; documented as known hardware behaviour. ## CAT applet redesign Configuration moved to a pop-out window (port number, dialect, VFO A/B slice assignment per port) rather than the right panel. Inspired by SmartSDR for macOS but more intuitive. The pattern — space-consuming config in a pop-out, status summary in the applet — is worth adopting for other applets. UI details: dynamic PTY-path column sizing; "Flex" dialect label (was "FlexCAT"); VFO column widths; column stretch. ## Security - Applied the PTY symlink fix (same pattern as GHSA-qxhr-cwrc-pvrm, aethersdr#3027) to CatPort: per-user symlink directories, atomic rename instead of unlink+symlink. ## Code quality - UAF audit: SmartCatProtocol and the new RigctlProtocol additions are both clean — no `[this]` captures in queued lambdas; all dispatch is synchronous on the main thread. - ThemeManager: `CatControlApplet` was included in the Phase 2 mass migration (aethersdr#3102). Because this branch replaces the entire applet, that migration did not carry forward. We applied `ThemeManager::applyStyleSheet()` throughout — both the docked panel and the pop-out window — so all widgets auto-restyle on `themeChanged`, consistent with the rest of the codebase. One intentional hardcode remains: the `:checked` background `#006040` (CAT active state); there is no `color.background.success` token analogous to `color.background.tx`. Adding one would complete the conversion and is worth proposing as a follow-up. Closes aethersdr#2962 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
K5PTB
added a commit
to K5PTB/AetherSDR
that referenced
this pull request
May 25, 2026
…ethersdr#2962) Implements a native SmartCAT-compatible TCP CAT server, enabling band, mode, and frequency sync with N1MM, fldigi, WSJT-X, MacLoggerDX, and other logging/digital-mode software via the industry-standard CAT protocol. ## Why CAT instead of rigctld N1MM does not speak the Hamlib NET rigctl protocol; it requires a CAT port (TS-2000 or FlexCAT dialect). This made a full CAT implementation necessary for N1MM compatibility. Not1MM (a separate Linux-only product) uses rigctld — that is covered by aethersdr#2975. ## Protocol coverage - Full Kenwood TS-2000 command set per the manual appendix - Full FlexCAT command set per SmartCAT User Guide v4.1.5, with the exception of ZZPA/ZZPE (panadapter data streaming — out of scope) - Two TCP ports by default: 4532 (rigctld) and 5001 (FlexCAT); up to 6 more ports can be configured. Each TCP port has a companion PTY that accepts the same commands and responds identically. PTY paths are shown in the pop-out applet and can be copied to the clipboard with a right-click. ## Integration testing Tested against real hardware (FlexRadio 6500, firmware 4.2.18): | App | Connection | Result | |--------------|-----------|--------| | fldigi | TCP + PTY | ✓ | | WSJT-X | TCP + PTY | ✓ | | MacLoggerDX | PTY (TS-2000 only) | ✓ | | N1MM+ | TCP | ✓ (CAT sync; CW keying not possible — see below) | Primary platform: macOS. Also tested on Raspberry Pi 5 / Raspberry Pi OS Trixie. ### N1MM CW keying — known limitation N1MM keys CW by toggling RTS/DTR on a real serial COM port. Virtual serial ports (PTYs) are not exposed as COM ports in Windows, so RTS/DTR keying cannot reach AetherSDR. No workaround exists without a WinKeyer-compatible keyer emulator; documented so operators don't waste time chasing it. ## Test suites - `CAT_TS-2000_test`: 15 sections, 103 checks (92 always-on + 11 PTT/CW gated) - `CAT_Flex_test`: 16 sections, 129 checks (105 always-on + 24 PTT/CW gated) PTT and CW keyer tests are **skipped by default** to avoid unintended transmissions. Enable with `--ptt` and `--cw` when a radio is connected and the operator is ready to transmit. Firmware TX race noted during CW testing: the radio briefly reports TX=0 via CAT while the PA is still hot after CW completes. Safety watches added to the test suite; documented as known hardware behaviour. ## CAT applet redesign Inspired by SmartSDR for macOS. The space-consuming configuration belongs in a pop-out, not the applet panel. PTY paths for running ports are shown in the table and can be copied to the clipboard with a right-click — handy when configuring external apps. ## Security - Applied the PTY symlink fix (same pattern as GHSA-qxhr-cwrc-pvrm, aethersdr#3027) to CatPort: per-user symlink directories, atomic rename instead of unlink+symlink. ## Code quality - UAF audit: SmartCatProtocol and new RigctlProtocol additions both clean — no [this] captures in queued lambdas; all dispatch synchronous on main thread - ThemeManager: CatControlApplet was included in the Phase 2 mass migration (aethersdr#3102). Because this branch replaces the entire applet, that migration did not carry forward. Applied ThemeManager::applyStyleSheet() throughout — both the docked panel and the pop-out window — so all widgets auto-restyle on themeChanged, consistent with the rest of the codebase. One intentional hardcode remains: the :checked background #006040 (CAT active state); there is no color.background.success token analogous to color.background.tx. Adding one would complete the conversion and is worth proposing as a follow-up. Closes aethersdr#2962 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ten9876
pushed a commit
that referenced
this pull request
May 25, 2026
…3131) ## CAT Protocol Coverage - Full Kenwood TS-2000 command set, referencing the manual appendix, except commands not applicable to a FlexRadio - Full FlexCAT command set per SmartCAT User Guide v4.1.5, with the exception of ZZPA/ZZPE (panadapter data streaming — out of scope for a CAT server) - Two TCP ports by default: 4532 (rigctld) and 5001 (FlexCAT). Up to 6 more ports can be configured/enabled; each port has a companion PTY that accepts the same commands and responds identically. PTY paths are shown in the pop-out applet and can be copied to the clipboard with a right-click — handy when configuring external apps. ## Why CAT in addition to rigctld Some external apps do not support rigctld, but expect to send TS-2000 CAT commands, with extensions by FlexRadio. AetherSDR now speaks both protocols, giving operators the flexibility to use whichever their software requires — setups that currently use SmartCAT can work directly with AetherSDR without change. TCI is preferred, but this supports both legacy apps and legacy hams. ## CAT applet redesign Inspired by SmartSDR for macOS. The space-consuming configuration only appears when popped out, avoiding clutter of the right panel. ## Test suites - **`CAT_TS-2000_test`**: 15 sections, 103 checks (92 always-on + 11 PTT/CW gated) - **`CAT_Flex_test`**: 16 sections, 129 checks (105 always-on + 24 PTT/CW gated) PTT and CW keyer tests are **skipped by default** to avoid unintended transmissions. Enable with `--ptt` and `--cw` when a radio is connected and the operator is ready to transmit. A firmware TX race was noted during CW testing: the radio briefly reports TX=0 via CAT while the PA is still hot after CW completes. Safety watches are added to the test suite; documented as known hardware behaviour. ## Integration testing Tested against real hardware (FlexRadio 6500, firmware 4.2.18). Primary platform: macOS. Also tested on Raspberry Pi 5 / Raspberry Pi OS Trixie. | App | Connection | Result | |---|---|---| | fldigi | TCP + PTY | ✓ | | WSJT-X | TCP + PTY | ✓ | | MacLoggerDX | PTY (TS-2000 only) | ✓ | | N1MM+ | TCP | ✓ (CAT sync; CW keying not possible — see below) | ### N1MM CW keying — known limitation N1MM keys CW by toggling RTS/DTR on a real serial COM port. Virtual serial ports (PTYs) are not exposed as COM ports in Windows, so RTS/DTR keying cannot reach AetherSDR. No workaround exists without a WinKeyer-compatible keyer emulator. Documented here so operators don't waste time chasing it. ## Security Applied the PTY symlink fix (same pattern as GHSA-qxhr-cwrc-pvrm, #3027) to `CatPort`: per-user symlink directories, atomic rename instead of unlink+symlink. ## Code quality **UAF audit:** `SmartCatProtocol` and the new `RigctlProtocol` additions are both clean — no `[this]` captures in queued lambdas; all dispatch is synchronous on the main thread. **ThemeManager:** `CatControlApplet` was included in the Phase 2 mass migration (#3102). Because this branch replaces the entire applet, that migration did not carry forward. We applied `ThemeManager::applyStyleSheet()` throughout — both the docked panel and the pop-out — so all widgets auto-restyle on `themeChanged`, consistent with the rest of the codebase. One intentional hardcode remains: the `:checked` background `#006040` (CAT active state). There is no `color.background.success` token analogous to `color.background.tx`; adding one would complete the conversion and is worth proposing as a follow-up. Closes #2962 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
ten9876
added a commit
to mvanhorn/AetherSDR
that referenced
this pull request
Jun 14, 2026
…le + clearly inactive (aethersdr#3484/aethersdr#2389) Refines the aethersdr#3547 approach per maintainer direction. The original fix drew the inactive-slice passband/filter-edge affordances in the slice's full-brightness colour, which fixed visibility (aethersdr#3484) but erased the active/inactive distinction — reintroducing the TX-into-wrong-slice confusion @jensenpat designed the dim against (aethersdr#2389). Now colour, not brightness, signals inactive: an inactive slice's bandwidth renders at full brightness in the neutral secondary colour (`color.text.secondary`), while the active slice keeps its own colour. So the passband is clearly visible AND obviously not the active/TX slice. Focus affordances (VFO centre line, triangle, RIT/XIT) keep the dimmed `col`. Uses the existing theme token (re-themes; no hardcoded hex, per the aethersdr#3102 token migration) — the same `color.text.secondary` already used for the other neutral markers in this widget. Co-authored-by: mvanhorn <mvanhorn@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merged
9 tasks
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…stitutions (aethersdr#3102) ## Summary **PR 1 of 5** in the Phase 2 wrap-up sweep. Adds a Python migration tool and applies it to every \`src/gui/\` file with inline-stylesheet hex literals matching the [canonical token taxonomy](docs/theming/canonical-tokens.md). ## Impact | Metric | Before | After | Δ | |---|---|---|---| | Total colour references | 3641 | 2627 | **-1014 (-28%)** | | setStyleSheet call sites | 1456 | 1456 | unchanged | | Files touched | — | **59** | | ## The tool: \`tools/migrate_colours.py\` Static rewrite tool that: 1. Walks \`src/\` for .cpp/.h files 2. Locates \`setStyleSheet(...)\` calls with string-literal arguments (handles C++ adjacent-literal concatenation + \`QStringLiteral(R\"(...)\")\` raw strings) 3. Substitutes every hex matching the canonical-token table with a \`{{token.name}}\` placeholder 4. Wraps the call: \`setStyleSheet(STR)\` → \`setStyleSheet(AetherSDR::ThemeManager::instance().resolve(STR))\` so resolve() expands the tokens at apply time 5. Inserts \`#include \"core/ThemeManager.h\"\` into the top-of-file unconditional include block (skips conditional-compile blocks so the include doesn't land inside \`#ifdef Q_OS_WIN\` where it wouldn't compile on other platforms) Pass \`--apply\` to write changes; default is dry-run with stats. ## What the tool produced 59 files with hex literals replaced by tokens. Example diff from \`SwrSweepLicenseDialog.cpp\`: \`\`\`diff -setStyleSheet(\"QDialog { background: #0f0f1a; color: #c8d8e8; }\"); +setStyleSheet(AetherSDR::ThemeManager::instance().resolve( + \"QDialog { background: {{color.background.0}}; color: {{color.text.primary}}; }\")); \`\`\` Top 10 files by substitution count: \`\`\` 182 subs / 80 calls src/gui/RadioSetupDialog.cpp 164 subs / 82 calls src/gui/DxClusterDialog.cpp 63 subs / 27 calls src/gui/MainWindow.cpp 37 subs / 8 calls src/gui/StripFinalOutputPanel.cpp 37 subs / 22 calls src/gui/VfoWidget.cpp 30 subs / 16 calls src/gui/PanadapterApplet.cpp 29 subs / 20 calls src/gui/NetworkDiagnosticsDialog.cpp 28 subs / 15 calls src/gui/TitleBar.cpp 27 subs / 13 calls src/gui/CwxPanel.cpp 25 subs / 13 calls src/gui/RxApplet.cpp \`\`\` ## Tradeoffs documented inline - Tool replaces hex inside \`setStyleSheet\` calls but does **not** convert them to \`applyStyleSheet\`. Each call still goes through \`resolve()\` so tokens expand, but the widget is NOT registered in the reverse-map. Live re-theme on themeChanged works for the app-wide base stylesheet (Theme.h via applyAppTheme) and for ComboStyle, but inline overrides won't auto-re-apply on theme switch. PR 5 (cleanup) addresses this where it matters; the long-tail decorative overrides are fine being static. - Some one-off hex (\`#406080\`, \`#00f0ff\`, \`#cc2222\`) weren't in the canonical table and were left alone. PR 5 cleanup handles them. ## Review strategy The tool's transformation is deterministic — review is *\"did the tool do the right thing\"* rather than line-by-line. Spot-check 5-10 random files; the rest follow the same pattern. Same risk profile as a \`clang-format\` sweep. ## Verified locally - [x] Linux build clean (633 files, 0 errors) - [x] AetherSDR binary launches without crash (offscreen QPA smoke test) - [x] No visible diff expected today — resolved colours match the previous hardcoded values within sub-perceptual canonicalisation shifts ## Sequence This is PR 1 of 5. Remaining: - **PR 2**: Phase 3 part 1 — Meters family paint code - **PR 3**: Phase 3 part 2 — Spectrum/panadapter/curves paint code - **PR 4**: Phase 3 part 3 — Waterfall colormap + slice indicators (gradient consumer) - **PR 5**: Cleanup — long-tail hex, missing applyStyleSheet conversions, audit verification 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…base hex (aethersdr#3110) Post-aethersdr#3102 user testing surfaced visible darkening of applet backgrounds, title bar, and status bar. Root cause: my canonical taxonomy picked design-aesthetic values for two of the highest-impact tokens instead of the actual dominant codebase hex: color.background.0 was #0a0e14 → now #0f0f1a (84 refs to literal #0f0f1a — the QWidget base colour in Theme.h's app-wide stylesheet; the old token value was ΔRGB 12 darker, perceptible against dark UI) color.text.primary was #e6f0fa → now #c8d8e8 (367 refs — the most-used colour in the whole codebase by far; the old token value was visibly lighter) Both files updated in lockstep: - resources/themes/default-dark.json (v1.2 → v1.3, baked into the Qt resource bundle) - src/core/ThemeManager.cpp::seedBuiltinDefaults() (the compiled-in fallback when no theme file loads) No call-site changes needed — every site that resolves through ThemeManager::color() or the {{color.text.primary}} / {{color.background.0}} template placeholders automatically picks up the new values. Lesson for the taxonomy doc: canonical token values must come from the **dominant codebase hex** at sites that use the token, not from the designer's first instinct. Phase 4 Light theme can revisit the aesthetic tuning; Phase 2's job is bit-identical-where-possible preservation of the v26.5.3 look. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…dget (aethersdr#3113) ## Summary **PR 3 of 5** in the Phase 2/3 wrap-up. Adds a paint-code QColor migration tool and applies it to \`src/gui/SpectrumWidget.cpp\` — the main panadapter, the visible-most paint surface in AetherSDR. ## The tool: \`tools/migrate_paint_colours.py\` Sibling to \`migrate_colours.py\` (the setStyleSheet migration tool from aethersdr#3102). Handles all three QColor constructor forms: \`\`\` QColor(0xAA, 0xBB, 0xCC) → ThemeManager::instance().color("token") QColor(0xAA, 0xBB, 0xCC, alpha) → AetherSDR::theme::withAlpha("token", alpha) QColor("#aabbcc") → ThemeManager::instance().color("token") \`\`\` Uses the same \`CANONICAL_TOKENS\` table as the setStyleSheet migration tool — colours not in the table are left alone (logged for the cleanup pass). ## New helper: \`AetherSDR::theme::withAlpha\` Added to [core/ThemeManager.h](src/core/ThemeManager.h) so alpha-bearing migrations stay readable: \`\`\`cpp namespace theme { inline QColor withAlpha(const QString& token, int alpha) { QColor c = ThemeManager::instance().color(token); c.setAlpha(alpha); return c; } } \`\`\` Without this, every alpha-bearing paint-code site would inline an ugly lambda. ## Applied to SpectrumWidget - **33 QColor literals migrated** to theme tokens (out of 70 in the file) - **37 skipped** — spectrum-specific accent shades, beacon markers, slice indicators, etc. that aren't in the canonical table yet The migrated 33 cover the main panadapter chrome (backgrounds, axes, body text, common accents). The skipped 37 are spectrum-domain colours that need dedicated tokens (PR 5 cleanup will extend the canonical table for them). ## Verified locally - [x] Linux build clean (68 link steps, no errors) - [x] No regressions flagged by compiler - [x] tools/migrate_paint_colours.py is dry-run by default (--apply to write) ## Why this is incremental The main panadapter now responds to theme changes for the 33 migrated sites. When Phase 4 ships the Default Light theme, the panadapter's overall chrome will adapt; the 37 spectrum-domain literals stay as-is until PR 5 adds their canonical tokens. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open the app, confirm panadapter renders identically (the 33 migrated literals resolve to the same hex values as before via the canonical table) 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…migration, canonical table closeout (aethersdr#3117) ## Summary **PR 5 of 5** in the Phase 2/3 wrap-up — closes the mechanical migration sweep. 159 \`QColor\` literals migrated across 11 widget files via the now-mature \`migrate_paint_colours.py\` tool, plus a closing 50-entry canonical-table expansion. ## Files migrated (11) | File | Literals | Notes | |---|---|---| | \`SpectrumWidget\` | 40 | Residual paint code from aethersdr#3113 | | \`ClientChainWidget\` | 20 | | | \`StripChainWidget\` | 20 | | | \`ClientRxChainWidget\` | 16 | | | \`StripRxChainWidget\` | 16 | | | \`WaveformWidget\` | 15 | rx + strip-mode share the same palette | | \`StripWaveform\` | 15 | | | \`KeyboardMapWidget\` | 8 | 10 \`categoryColor()\` returns left as-is (see Skips) | | \`ClientCompThresholdFader\` | 5 | Meter gradient stops deferred from aethersdr#3116 | | \`ClientCompEditorCanvas\` | 3 | | | \`ClientGateCurveWidget\` | 1 | Translucent cyan fill deferred from aethersdr#3116 | ## Canonical-table additions — 50 new close-mappings Three logical groups in \`tools/migrate_colours.py\`, each entry annotated with its sibling canonical value and ΔRGB so the rationale is auditable later: \`\`\`python # Spectrum paint code (25) \"#80d0ff\": \"color.accent.bright\", # peak hold light cyan \"#ff5858\": \"color.accent.danger\", # bright alert red \"#ffc040\": \"color.accent.warning\", # warning amber, ΔRGB≈0 from #ffb84d # … 22 more # Chain / waveform (11) \"#587890\": \"color.text.label\", # waveform centerline \"#ffd166\": \"color.accent.warning\", # peak-hold, ΔRGB≈1 from #ffd070 # … 9 more # Threshold fader / gate gradient stops (6) \"#2f9e6a\": \"color.accent.success\", # comp meter lo green \"#f2362a\": \"color.accent.danger\", # comp meter peak red \"#50b4dc\": \"color.accent.dim\", # gate curve translucent cyan # … 3 more \`\`\` ## Build verified clean - [x] AetherSDR builds clean - [x] All 308 targets build clean (every test target) - Two warnings surface (\`sliceColor\` / \`effectiveGridStepMhz\` unused) — both pre-existing on \`main\`, confirmed via local stash bisect ## Intentional skips (deferred to Phase 4) 1. **10 \`KeyboardMapWidget::categoryColor()\` returns** — semantically distinct hues for Frequency / TX / Audio / DSP / etc. categories. No shared visual language applies; converting would homogenize them and defeat the categorization. Left as raw \`QColor\`. 2. **1 \`ClientCompMeter\` no-go-zone tint (\`#3a1810\`)** — unique meter visual, no canonical equivalent worth absorbing. 3. **Slice indicator runtime theming** — \`SliceColorManager\` still reads from the compile-time \`kSliceColors[]\` table in \`SliceColors.h\`. Default-dark.json already has \`color.slice.a\` through \`.h\` tokens, but wiring \`SliceColorManager\` to read them requires touching \`SpectrumWidget\` overlay rendering + \`VfoWidget\` badges + \`RxApplet\`. Warrants its own PR. 4. **Waterfall colormap runtime theming** — \`color.waterfall.colormap\` gradient token is already shipped in default-dark.json (validating gradient-token support added in Phase 2), but the \`SpectrumWidget\` waterfall renderer hard-codes its colormap stop set. Conversion warrants its own PR. ## Phase 2/3 closeout This closes the **5-PR Phase 2/3 wrap-up** as constrained. Recap: | PR | What | Status | |---|---|---| | aethersdr#3102 | Mass migration + token taxonomy | merged | | aethersdr#3106 | MeterSlider pilot (Phase 3 PR 2/5) | merged | | aethersdr#3113 | Paint-color tool + SpectrumWidget (PR 3/5) | merged | | aethersdr#3116 | Comp/gate/curve widgets (PR 4/5) | merged | | **#XXXX** | **Spectrum + chain + waveform closeout (PR 5/5)** | **this PR** | Phase 4 picks up: default-light theme + slice indicator runtime theming + waterfall colormap runtime theming. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open the app and confirm the spectrum + chain + waveform widgets render to-within-ΔRGB-of-50 of current main (the 159 migrated literals resolve to canonical token values that fall within audited deviation thresholds) 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…ethersdr#3131) ## CAT Protocol Coverage - Full Kenwood TS-2000 command set, referencing the manual appendix, except commands not applicable to a FlexRadio - Full FlexCAT command set per SmartCAT User Guide v4.1.5, with the exception of ZZPA/ZZPE (panadapter data streaming — out of scope for a CAT server) - Two TCP ports by default: 4532 (rigctld) and 5001 (FlexCAT). Up to 6 more ports can be configured/enabled; each port has a companion PTY that accepts the same commands and responds identically. PTY paths are shown in the pop-out applet and can be copied to the clipboard with a right-click — handy when configuring external apps. ## Why CAT in addition to rigctld Some external apps do not support rigctld, but expect to send TS-2000 CAT commands, with extensions by FlexRadio. AetherSDR now speaks both protocols, giving operators the flexibility to use whichever their software requires — setups that currently use SmartCAT can work directly with AetherSDR without change. TCI is preferred, but this supports both legacy apps and legacy hams. ## CAT applet redesign Inspired by SmartSDR for macOS. The space-consuming configuration only appears when popped out, avoiding clutter of the right panel. ## Test suites - **`CAT_TS-2000_test`**: 15 sections, 103 checks (92 always-on + 11 PTT/CW gated) - **`CAT_Flex_test`**: 16 sections, 129 checks (105 always-on + 24 PTT/CW gated) PTT and CW keyer tests are **skipped by default** to avoid unintended transmissions. Enable with `--ptt` and `--cw` when a radio is connected and the operator is ready to transmit. A firmware TX race was noted during CW testing: the radio briefly reports TX=0 via CAT while the PA is still hot after CW completes. Safety watches are added to the test suite; documented as known hardware behaviour. ## Integration testing Tested against real hardware (FlexRadio 6500, firmware 4.2.18). Primary platform: macOS. Also tested on Raspberry Pi 5 / Raspberry Pi OS Trixie. | App | Connection | Result | |---|---|---| | fldigi | TCP + PTY | ✓ | | WSJT-X | TCP + PTY | ✓ | | MacLoggerDX | PTY (TS-2000 only) | ✓ | | N1MM+ | TCP | ✓ (CAT sync; CW keying not possible — see below) | ### N1MM CW keying — known limitation N1MM keys CW by toggling RTS/DTR on a real serial COM port. Virtual serial ports (PTYs) are not exposed as COM ports in Windows, so RTS/DTR keying cannot reach AetherSDR. No workaround exists without a WinKeyer-compatible keyer emulator. Documented here so operators don't waste time chasing it. ## Security Applied the PTY symlink fix (same pattern as GHSA-qxhr-cwrc-pvrm, aethersdr#3027) to `CatPort`: per-user symlink directories, atomic rename instead of unlink+symlink. ## Code quality **UAF audit:** `SmartCatProtocol` and the new `RigctlProtocol` additions are both clean — no `[this]` captures in queued lambdas; all dispatch is synchronous on the main thread. **ThemeManager:** `CatControlApplet` was included in the Phase 2 mass migration (aethersdr#3102). Because this branch replaces the entire applet, that migration did not carry forward. We applied `ThemeManager::applyStyleSheet()` throughout — both the docked panel and the pop-out — so all widgets auto-restyle on `themeChanged`, consistent with the rest of the codebase. One intentional hardcode remains: the `:checked` background `#006040` (CAT active state). There is no `color.background.success` token analogous to `color.background.tx`; adding one would complete the conversion and is worth proposing as a follow-up. Closes aethersdr#2962 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR 1 of 5 in the Phase 2 wrap-up sweep. Adds a Python migration tool and applies it to every `src/gui/` file with inline-stylesheet hex literals matching the canonical token taxonomy.
Impact
The tool: `tools/migrate_colours.py`
Static rewrite tool that:
Pass `--apply` to write changes; default is dry-run with stats.
What the tool produced
59 files with hex literals replaced by tokens. Example diff from `SwrSweepLicenseDialog.cpp`:
```diff
-setStyleSheet("QDialog { background: #0f0f1a; color: #c8d8e8; }");
+setStyleSheet(AetherSDR::ThemeManager::instance().resolve(
```
Top 10 files by substitution count:
```
182 subs / 80 calls src/gui/RadioSetupDialog.cpp
164 subs / 82 calls src/gui/DxClusterDialog.cpp
63 subs / 27 calls src/gui/MainWindow.cpp
37 subs / 8 calls src/gui/StripFinalOutputPanel.cpp
37 subs / 22 calls src/gui/VfoWidget.cpp
30 subs / 16 calls src/gui/PanadapterApplet.cpp
29 subs / 20 calls src/gui/NetworkDiagnosticsDialog.cpp
28 subs / 15 calls src/gui/TitleBar.cpp
27 subs / 13 calls src/gui/CwxPanel.cpp
25 subs / 13 calls src/gui/RxApplet.cpp
```
Tradeoffs documented inline
Tool replaces hex inside `setStyleSheet` calls but does not convert them to `applyStyleSheet`. Each call still goes through `resolve()` so tokens expand, but the widget is NOT registered in the reverse-map. Live re-theme on themeChanged works for the app-wide base stylesheet (Theme.h via applyAppTheme) and for ComboStyle, but inline overrides won't auto-re-apply on theme switch. PR 5 (cleanup) addresses this where it matters; the long-tail decorative overrides are fine being static.
Some one-off hex (`#406080`, `#00f0ff`, `#cc2222`) weren't in the canonical table and were left alone. PR 5 cleanup handles them.
Review strategy
The tool's transformation is deterministic — review is "did the tool do the right thing" rather than line-by-line. Spot-check 5-10 random files; the rest follow the same pattern. Same risk profile as a `clang-format` sweep.
Verified locally
Sequence
This is PR 1 of 5. Remaining:
73, Jeremy KK7GWY & Claude (AI dev partner)
🤖 Generated with Claude Code