Skip to content

feat(theme): Phase 2 mass migration — 59 files, ~1000 hex → token substitutions#3102

Merged
ten9876 merged 3 commits into
mainfrom
auto/theme-phase2-mass-migrate
May 25, 2026
Merged

feat(theme): Phase 2 mass migration — 59 files, ~1000 hex → token substitutions#3102
ten9876 merged 3 commits into
mainfrom
auto/theme-phase2-mass-migrate

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

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

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

  • Linux build clean (633 files, 0 errors)
  • AetherSDR binary launches without crash (offscreen QPA smoke test)
  • 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

@ten9876 ten9876 requested a review from a team as a code owner May 25, 2026 03:03
@ten9876 ten9876 requested a review from a team as a code owner May 25, 2026 03:24
ten9876 and others added 2 commits May 24, 2026 20:37
…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>
@ten9876 ten9876 force-pushed the auto/theme-phase2-mass-migrate branch from 5cdeb55 to ac65d49 Compare May 25, 2026 03:38
…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 ten9876 merged commit e20b5f3 into main May 25, 2026
4 checks passed
@ten9876 ten9876 deleted the auto/theme-phase2-mass-migrate branch May 25, 2026 04:46
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>
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>
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>
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>
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.

1 participant