Skip to content

fix(gui): dimmed Radio Audio Outputs rows look dimmed. Principle VIII. - #6388

Merged
rfoust merged 2 commits into
aethersdr:mainfrom
on8st:fix/radio-audio-outputs-dimmed-look
Oct 12, 2026
Merged

rfoust merged 2 commits into
aethersdr:mainfrom
on8st:fix/radio-audio-outputs-dimmed-look

Conversation

@on8st

@on8st on8st commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Refs #5482, #6353

This is the follow-up to the two forward notes in the approving review of #6353.

Limits, first

  • macOS, offscreen platform only (Qt 6.12.0). The pixels are from QWidget::grab() in a test. Nobody looked at the dialog on a screen, and there is no screenshot.
  • Two themes rendered: Default Dark and Default Light. No user theme.
  • The Line Out row's styling is touched; its logic is not. That row is @jcmerg's (fix(audio): the title-bar mute reaches the radio's line out only where it has one. Principle VIII. #6260). His predicate, his reason text and his applyLineoutAvailability lambda are unchanged. Only the three stylesheets of the row gain a :disabled rule, the same one the Headphone row gets, so the two rows keep agreeing.
  • The Front Speaker row is not touched. It is never disabled.
  • A dimmed Mute looks the same engaged or not (from this PR's review, c0bfd883). An engaged Mute on an unavailable row took the unavailable text on the engaged red background. It now takes the row's own unchecked background (color.background.1), so the engaged state is not shown while the row is unavailable. §4a names no look for a control that is unavailable and also has state, so whether the state should stay visible is the maintainer's call. The checked state is still on the widget for accessibility clients.
  • Contrast is what the token gives. color.control.unavailable is 2.09:1 (dark) and 1.77:1 (light) by the style guide's own table (§4a). This PR uses the token and does not change it.
  • The Line Out reason sentence is not hoisted. It exists in one place only (src/gui/RadioSetupDialog.cpp:4774-4775 at 9fabcbf8); no other source file and no test repeats it.
  • Refs, not Fixes: closing M0 item 3 follow-up: the title-bar headphone slider is a second unconverted site for mixer headphone gain/mute #5482 stays your call.

What was wrong

At 9fabcbf8, in Radio Setup → Audio → "Radio Audio Outputs", a disabled row shows it on the slider only. The other three widgets of each row carry their own stylesheet, and none has a :disabled rule:

widget stylesheet applied at (Line Out / Headphone)
caption label kLabelStyleTemplate, QLabel { color: {{color.text.primary}}; … } (src/gui/RadioSetupDialogCommon.h:31-32) src/gui/RadioSetupDialog.cpp:4740 / :4794
value label kValueStyleTemplate, QLabel { color: {{color.accent.bright}}; … } (RadioSetupDialog.cpp:129-130) :4745 / :4799
Mute button inline QPushButton { … color: {{color.text.primary}}; … } plus :checked :4751-4753 / :4805-4807
slider none of its own :4742 / :4796

An explicit color with no :disabled rule also applies when the widget is disabled, so the label, the value and the Mute button render the same in both states. The review of #6353 measured 0 differing pixels on them; the test here reproduces that (below).

The two headphone reason sentences were written out three times: src/gui/TitleBar.cpp:1297-1302, src/gui/RadioSetupDialog.cpp:4826-4834, and tests/radio_setup_headphone_output_gate_test.cpp:134-139.

The change

The look. A disabled label, value or Mute button of the two rows takes color.control.unavailable, the token §4a of docs/style/theme-style-guide.md names for the unavailable state:

QLabel:disabled      { color: {{color.control.unavailable}}; }
QPushButton:disabled { color: {{color.control.unavailable}}; }

This is the pattern already in the tree for the TX timing fields (#6166, RadioSetupDialog.cpp:2565-2567, QLineEdit:disabled) and for WfmApplet.cpp:58. The rules go through ThemeManager::applyStyleSheet, so they follow a theme switch like their neighbours. No colour literal is added. The enabled look is unchanged: the rule is appended to the existing template, and only for these six widgets. makeValueLabel takes an optional extra rule so kValueStyleTemplate keeps its single reference. The shared applyLabelStyle is not changed, so no other page of the dialog moves.

The wording. src/gui/MixerControlAvailability.h now holds the two sentences next to the predicate, as kHeadphoneLevelUnavailableReason and kHeadphoneMuteUnavailableReason. They are constexpr char[], so the header stays free of Qt. TitleBar::setHeadphoneAvailable, the Radio Setup lambda and both tests read them from there. The text itself is unchanged, character for character.

No predicate, no capability field, no default and no user-visible string changes. docs/user/docs/audio-settings.md already says the controls "are dimmed with the reason in the tooltip", which stays true.

Tests

tests/radio_setup_headphone_output_gate_test.cpp gains dimmedRowsLookUnavailableInBothThemes (offscreen, no socket). For each of Default Dark and Default Light it builds the dialog on the HL2 capabilities (both rows dimmed) and on the Demo (both rows live), and for the label, value and Mute button of each row:

  • renders the widget's own region with grab() and counts differing pixels, exactly, no tolerance. It requires more than 0 between dimmed and live;
  • first asserts the two renders are comparable: same size, same value text, same mute state. So the enabled state is the only difference;
  • control 1: the same widget rendered twice differs in 0 pixels;
  • control 2: after a disconnect the same HL2 widgets are live again and render pixel-identical to the Demo's, and different from their own dimmed render;
  • requires the resolved stylesheet to carry the colour color.control.unavailable has in the active theme.

With the change, differing pixels (measured at 6c83ac5a, the same diff before it was rebased onto current main; the test that asserts them passes at the head), dimmed vs live, identical in both themes:

region Headphone Line Out
label 1329 / 6840 852 / 6840
value 410 / 2280 410 / 2280
Mute 512 / 3800 512 / 3800

Fail-before. Named mutation: this commit with both :disabled rule strings emptied, which is main's styling. Full build and ctest. All twelve regions read 0 differing pixels, as in the review:

PIXELS Default Dark | Headphone: label: dimmed vs live 0 / 6840 (same widget twice: 0)
PIXELS Default Dark | Headphone: value: dimmed vs live 0 / 2280 (same widget twice: 0)
PIXELS Default Dark | Headphone: mute: dimmed vs live 0 / 3800 (same widget twice: 0)
PIXELS Default Dark | Line Out: label: dimmed vs live 0 / 6840 (same widget twice: 0)
PIXELS Default Dark | Line Out: value: dimmed vs live 0 / 2280 (same widget twice: 0)
PIXELS Default Dark | Line Out: mute: dimmed vs live 0 / 3800 (same widget twice: 0)
(the same six lines for Default Light)
FAIL!  : RadioSetupHeadphoneOutputGateTest::dimmedRowsLookUnavailableInBothThemes() 'failures.isEmpty()' returned FALSE. (Default Dark | Headphone: label: dimmed renders as live
Default Dark | Headphone: label: not the unavailable token #3a4a5a
…
Totals: 5 passed, 1 failed, 0 skipped, 0 blacklisted

The test could not be run on unmodified main as it stands, because it reads the sentences from the header. That is why the fail-before is a mutation and not "the test alone".

The wording. radio_setup_headphone_output_gate_test compares the row against the header's two constants. tests/mixer_control_availability_test.cpp gains two rows: the title bar's mute and slider descriptions equal the header's sentences. Its existing rows still check the content independently ("Unavailable:", "no headphone output", "speaker button", "master volume").

ctest

macOS (Apple Silicon), Qt 6.12.0, ENABLE_ASR=OFF, full suite at -j8, failures rerun serially, on c0bfd883 (this PR's head):

Static checks, run locally: tools/check_a11y.py on RadioSetupDialog.cpp 9 findings before and 9 after, TitleBar.cpp 0; tools/audit_colours.py summary identical before and after; gen_touchpoint_manifest.py --check and docs/gen_reference.py --check up to date; the tools/check_*.py --strict set passes (check_codeql_coverage.py needs a build directory and was not run).

73, ON8ST

🤖 Generated with Claude Code

Radio Setup -> Audio -> Radio Audio Outputs: the label, value and Mute
button of a disabled Headphone or Line Out row carry stylesheets with no
:disabled rule, so only the slider shows the row is unavailable. They take
color.control.unavailable on :disabled, the pattern of the TX timing
fields. The enabled look, the predicates and the Line Out logic are
unchanged.

The two headphone reason sentences move next to the predicate in
MixerControlAvailability.h; the title bar, the Radio Setup row and both
tests read them from there.

Refs aethersdr#5482, aethersdr#6353

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st
on8st requested a review from a team as a code owner October 11, 2026 20:39

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

Yes, for the part it claims. #5482 is the title-bar headphone capability gate; #6353 landed the dimming and the reasons, and its acceptance item "visibly dimmed" was only half-true — the slider dimmed, the label, value and Mute button did not. This closes that gap for both rows of Radio Audio Outputs and hoists the two reason sentences to one owner. The Refs, not Fixes, is right: #5482's own "out of scope, deliberately" item (labelling whose output each slider is) is untouched and still needs a maintainer ruling, so #5482 should not close on this.

I verified every substantive claim in the body I could check statically, and all of them held — details in "What I tried to break".

Scope

File What it changes Claimed? Verdict
src/gui/MixerControlAvailability.h Adds the two inline constexpr char[] reason sentences beside the predicate Yes In scope
src/gui/RadioSetupDialog.cpp makeValueLabel gains an optional extraRules; two :disabled rule constants; six widgets in the Headphone and Line Out rows take them; the headphone lambda reads the header constants Yes In scope
src/gui/TitleBar.cpp MixerControlAvailability.h include; two inline literals replaced by the header constants Yes (the hoist) In scope
tests/mixer_control_availability_test.cpp Two checks: the title bar's descriptions equal the header's sentences Yes In scope
tests/radio_setup_headphone_output_gate_test.cpp connectDemo() extracted from case 5; dimmedRowsLookUnavailableInBothThemes Yes In scope
tests/tests.cmake Comment text on the existing registration block only — no target, source, environment or TIMEOUT change Yes In scope

Everything in the diff is explained by the issue and the PR body. Checked specifically for the usual smuggling shapes and found none: no new public surface (the two constants are internal UI text, no protocol verb, no settings key, no capability field), no removed guard, no changed default, no CHANGELOG.md entry, no build-config or CI churn, and the one refactor (connectDemo) is a verbatim extraction of case 5's body.

On the CodeGuard list: the harness handed over a 56-file audit list and ~718 findings, but only three of those files are in this PR at all, and none of the quoted hits is in the diff — the appimage.yml / macos-dmg.yml / static-checks.yml lines are a stale wide snapshot of untouched files. I ran the required security-audit subagent against the PR head; it reported no findings in this PR's diff, and I confirmed its reasoning on the one shape that would matter here: the two new QSS fragments (RadioSetupDialog.cpp:146-149) are compile-time literals concatenated with other literals, so no radio-, model- or operator-supplied string ever reaches applyStyleSheet() on these paths — there is no stylesheet-injection vector. The two reason constants reach only setToolTip/setAccessibleDescription, with no format string and no arg(). The CS-VLN-CMDI-BACKTICK family hits on MixerControlAvailability.h:8 are the known false positive: a // comment Markdown-quoting the Flex verb `mixer headphone|lineout ...`. That is a clean result for this diff, not a clean bill for the 53 files the PR does not touch.

Socket tests: none added, none removed. The new case uses the existing never-dialled Demo pattern (QHostAddress::LocalHost, port 4992, connectToRadio on SimBackend, which the file's header comment already documents) and the injected NoCommandPlaneBackend. No QTcpServer/QUdpSocket/bind/listen/connectToHost, and no fake radio, amplifier or tuner peer.

Blockers

None.

Nits

All non-blocking.

  • src/gui/RadioSetupDialog.cpp:4763-4764 and :4820-4821 — :checked and :disabled collide on a muted unavailable row.

    "QPushButton:checked { background: #8b0000; color: {{color.text.primary}}; }"
    + kUnavailableButtonRule
    

    Both selectors carry one pseudo-class, so they tie on specificity and the appended :disabled rule wins on color only because it comes last. background: #8b0000 is not overridden. So a row whose Mute is engaged when the radio turns out to have no headphone output renders dark-red-on-#3a4a5a — the "engaged" background with unavailable text, which reads as broken rather than unavailable. Reachable whenever m_model->headphoneMute() (or lineoutMute()) is true on a no-command-plane radio; #8b0000 is pre-existing, not this PR's. The new test cannot catch it: it asserts QCOMPARE(dim.mute->isChecked(), live.mute->isChecked()) and both default to unchecked, so the checked+disabled combination is never rendered. If you want it closed, an explicit rule beats relying on source order:

    static const QString kUnavailableButtonRule =
        "QPushButton:disabled { color: {{color.control.unavailable}}; }"
        "QPushButton:checked:disabled { background: {{color.background.1}}; "
        "color: {{color.control.unavailable}}; }";

    (I have not built that; the background token is a guess at the right neutral and is the maintainer's call.)

  • tests/radio_setup_headphone_output_gate_test.cpp:177-178 with its caller at :334 — a size mismatch is reported as the wrong defect. differingPixels returns -1 for differing sizes or a null image, and the caller folds that into if (diff <= 0) failures << ": dimmed renders as live". If the two dialogs' widgets ever differ in geometry, the failure message names the opposite problem. Cheap:

                    if (diff < 0)
                        failures << where + QStringLiteral(": regions differ in size");
                    else if (diff == 0)
                        failures << where + QStringLiteral(": dimmed renders as live");
  • tests/radio_setup_headphone_output_gate_test.cpp:358 — the theme restore is skipped on an early return. QVERIFY2 inside the loop returns from the slot, leaving the process on Default Light and AppSettings["ActiveTheme"] written to it. Harmless today because this is the last slot, but it is a trap for whoever adds the next one; a small RAII guard around startTheme would make it unconditional.

  • Per-PR CI does not execute this test, so the pixel assertions are one-host evidence. ci.yml has no unfiltered ctest step — every -R there is the frozen .github/ci-test-gate.txt set (cross_needle_meter_test, mac_nr_filter_test, the DV group, asr_gpu_probe_test), and radio_setup_headphone_output_gate_test is in none of them. So the five green checks prove the new code compiles on Linux, macOS and Windows; the first real run of dimmedRowsLookUnavailableInBothThemes on another platform is full-suite.yml after merge. The body is honest about "macOS, offscreen platform only", so this is a note on where the risk lands rather than a missing disclosure — and the one assertion I would watch there is the zero-tolerance cross-instance one (see below), not the relative ones.

  • The Line Out reason sentence stays an inline literal at src/gui/RadioSetupDialog.cpp:4785, so the header's "Every site that dims the pair, and the tests, read them from here" is true of the headphone pair only. The body discloses this and I verified the claim behind it: git grep finds that sentence in exactly one place, no other source file and no test. Fine as scoped; worth a sentence in the header comment if the two rows ever diverge.

What I tried to break

Everything below is reasoned from the PR head checkout at ee179f67; I have no build and no test run, so nothing here was reproduced at runtime.

  • Every factual claim in the body. The sentences really are character-identical — TitleBar's two removed literals and the header constants match byte for byte, and the dialog's old prefix + suffix concatenation reconstructs each one exactly. kValueStyleTemplate still has exactly one reference (makeValueLabel), as its own comment demands. applyLabelStyle is unchanged, and the two inlined replacements are faithful: applyLabelStyle(w) is applyStyleSheet(w, kLabelStyleTemplate) and nothing else (RadioSetupDialogCommon.h:43-46), so no other page of the dialog moves. The cited precedents exist: RadioSetupDialog.cpp:2575 (TX timing QLineEdit:disabled, #6166) and WfmApplet.cpp:58.
  • Is this a preference restyle? No — docs/style/theme-style-guide.md §4a names color.control.unavailable as the token for the unavailable state, in a table, and the PR quotes that section's contrast figures (2.09:1 dark, 1.77:1 light) correctly rather than inventing better ones, while adding no colour literal and not touching the token. §4a's own text says colour alone cannot carry the distinction, which is why accessibleDescription is mandatory — and that channel landed in #6353 and is unchanged here. This is a fix toward documented canon, not taste.
  • Why not ControlAvailabilityRegistry? That registry exists for exactly this doctrine (registerWidget(widget, reason, predicate) → token treatment via setWidgetForegroundToken + accessible description), so hand-rolling a third copy looked like a seam-in-the-wrong-place finding. It is not: the registry's predicate is (bool connected, const RadioCapabilities&), and command-plane reachability is not a RadioCapabilities field. The file already states that twice, at :2585 and :5275 — "By hand, not ControlAvailabilityRegistry: command-plane reachability is not in RadioCapabilities." The PR is consistent with the established exception.
  • Would the new test pass against the unfixed code? No, and I checked why rather than taking the mutation table on faith. On main the three widgets carry an explicit color: with no :disabled rule, which Qt applies in both states — so dimmed and live render identically and diff <= 0 fires on all twelve regions, which is exactly the zero-pixel measurement the #6353 review reported. The author's fail-before is a mutation rather than a run on main (the test now reads the header constants, which main lacks) and the body says so.
  • Does the test rely on something CI will not have? This was my main blocker candidate: the new case is the first in this file to call setActiveTheme, and a missing theme would make it red on the only lane that runs it. It holds up — resources/resources.qrc:36-38 ships default-dark.json and default-light.json, tests.cmake:3746 already compiles ${THEME_TEST_RESOURCES} into this target, and ThemeManager's constructor runs scanAvailableThemes() then activates the saved theme or "Default Dark", so activeTheme() is never empty and the final restore cannot be handed an empty name.
  • Lifetime and leakage across the theme loop. RadioModel, both dialogs, parts and dimmedImages are all declared inside the for (theme) body, so iteration 2 holds no pointer into iteration 1's destroyed widgets. The re-enable path is the pattern case 3 already uses (backend->connected = false; emit connectionStateChanged(false)), and I confirmed it actually works: RadioModel::isConnected() falls through to m_backend->isConnected() (RadioModel.cpp:2794) with no cached flag, and applyHeadphoneAvailability/applyLineoutAvailability are both connected to connectionStateChanged, so the widgets really do re-enable.
  • Cross-platform fragility of pixel equality — mostly a non-issue, because the dimmed-vs-live and render-twice comparisons are relative within one host, so font and rasterisation differences cancel; ci.yml:822-826 records that offscreen rendering was verified bit-identical across four platforms for cross_needle_meter_test. The one genuinely strict claim is vsDemo == 0 at :352-353: a re-enabled HL2 widget must render pixel-identical to a separate dialog's widget. The author guarded the obvious divergences (same size, same value text, same mute state, both asserted before the comparison). The residual mechanism I could not rule out is window-activation state leaking into the paint — two show()n dialogs, and Fusion draws focused/default buttons differently — but Mute is checkable with no focus by default, so I rate this low and did not raise it above the CI note.
  • Sibling rows left behind. The Front Speaker row at :4875 carries the same :checked rule and no :disabled rule, but it is never gated by an availability predicate, so it cannot be disabled; the body says so and the code agrees. No other Radio Audio Outputs widget is dimmed.
  • Could not check: the rendered appearance on a real screen, the test's wall-clock against the existing TIMEOUT 60 (the case adds two dialog constructions and a third Demo connect with a 5 s QTRY; the body reports the suite green on the author's Mac, which is the only evidence either of us has), and the two forward notes from #6353's review, which are PR review text — I relied on the body's account of them.

Recommendation

Approve with nits. This is a narrow, canon-anchored fix with an unusually honest body: every claim I could test statically held, the token and pattern come from the style guide and an in-tree precedent, the hoist is byte-for-byte, and the test genuinely fails against the unfixed styling rather than asserting the implementation back to itself. The four nits are all cheap and none of them breaks a user. Concrete next step: if you want the :checked:disabled background closed, it is a two-line addition to kUnavailableButtonRule and belongs here; otherwise the diff < 0 message split and the theme-restore guard can ride along or follow, and it is worth deciding whether radio_setup_headphone_output_gate_test running only in full-suite.yml is acceptable for a pixel-exact test or whether this one should be bumped to CI's gate list.

Thanks for the detailed limits section and the named mutation — the measured pixel table and the explicit "nobody looked at the dialog on a screen" made this much faster to review than it would otherwise have been.


🤖 aethersdr-agent · cost: $14.8948 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (727 finding(s))

  • [MEDIUM] CG-PATH-001 — Potential path traversal in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:205
  • [MEDIUM] CG-PATH-001 — Potential path traversal in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:417
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:77
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:387
  • [CRITICAL] CS-VLN-CMDI-SUBSHELL — Vulnerability: CS-VLN-CMDI-SUBSHELL in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:501
  • [CRITICAL] CS-VLN-CMDI-SUBSHELL — Vulnerability: CS-VLN-CMDI-SUBSHELL in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:503
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:132
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:257
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:304
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:334
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:366
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:451
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:182
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:209
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:215
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:415
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:499
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/appimage.yml /tmp/aetherclaude/pr-6388/.github/workflows/appimage.yml:512
  • [MEDIUM] CG-PATH-001 — Potential path traversal in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:117
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:444
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:539
  • [CRITICAL] CS-VLN-CMDI-SUBSHELL — Vulnerability: CS-VLN-CMDI-SUBSHELL in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:757
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:22
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:129
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:307
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:433
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:507
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:113
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:668
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/macos-dmg.yml /tmp/aetherclaude/pr-6388/.github/workflows/macos-dmg.yml:766
  • [CRITICAL] CS-VLN-CMDI-SUBSHELL — Vulnerability: CS-VLN-CMDI-SUBSHELL in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:479
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:14
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:36
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:81
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:172
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:306
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:329
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:337
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:352
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6388/.github/workflows/static-checks.yml:382

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $15.2694 · model: claude-opus-5

Review of aethersdr#6388, nit 1: :checked and :disabled tie on specificity, so a
Mute engaged on an unavailable Radio Audio Outputs row kept the engaged
background under unavailable text. QPushButton:checked:disabled now sets
the row's own unchecked background, color.background.1, and the
unavailable token. A control renders in one of three states
(theme-style-guide.md, Three-state controls), and this one is
unavailable: engaged or not, it renders the same.

The test engages both rows' Mute in both themes with the toggled signal
blocked, so no model setter runs and commandDropped stays at 0. The
dimmed button has no #8b0000 pixel, the live one does, and the dimmed
button renders as it does not engaged.

Nit 2: a size mismatch reports "regions differ in size", not "dimmed
renders as live". Nit 3: an RAII guard restores the starting theme on
every exit from the slot. The header comment says the Line Out reason is
not one of the hoisted sentences.

Refs aethersdr#5482, aethersdr#6353

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st

on8st commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Fixed in c0bfd883, a new commit on top (no rebase).

  • An engaged Mute on an unavailable row no longer shows the engaged red. Measured first: 665 of 950 pixels were exact #8b0000. There is now an explicit QPushButton:checked:disabled rule. Its background is not a guess: it is the token the same button's base rule uses for the unchecked state, color.background.1, which is the one you named.
    This is a choice, and yours to overrule: a dimmed Mute now renders pixel-identically engaged or not. §4a gives the unavailable state one look and says nothing about a control that is unavailable and also has state, so I kept none. The checked state is still on the widget for accessibility clients.
  • The test renders that state, in both themes, for both rows: zero #8b0000 pixels on the dimmed engaged Mute, 665 on the live one as the positive control, and the dimmed pair identical. The Mute is checked under a QSignalBlocker, and the test asserts that no command was built or dropped and that both models' mute state is unchanged. With the rule dropped the slot fails on 8 lines.
  • diff < 0 now reports "regions differ in size"; diff == 0 reports "dimmed renders as live".
  • The theme is restored by a RAII guard, also on an early return.
  • The header comment says the Line Out reason is not among the hoisted sentences.

On the gate list: I have not asked for this test to join the per-PR set. That is the maintainer's call; the body says the pixel figures are one host.

Full ctest at c0bfd883 on macOS: 763 of 765. The two that fail are known on this Mac: #6008, with the dialog page minimum still 784, and vkamp_connection_test with only its two known checks (#5254).

73, ON8ST

@rfoust rfoust self-assigned this Oct 12, 2026

@rfoust rfoust left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The follow-up to #6353 addresses the remaining visible-dimming gap in the Headphone and Line Out rows and centralizes the unchanged headphone reason text. No correctness blockers found. The operator explicitly accepted, as maintainer, the checked+disabled Mute presentation: checked state remains stored but unavailable Mute buttons render identically whether checked or unchecked. That design question is settled for this review.

Scope:

File/group Claimed? Verdict
MixerControlAvailability.h and TitleBar.cpp Shared reason text In scope; wording unchanged
RadioSetupDialog.cpp Disabled styling In scope; predicates, command routing and enabled styles unchanged
Two test files Rendering and wording coverage In scope
tests.cmake Coverage comment In scope; registration unchanged

No unrelated changes, new settings, changed defaults, removed guards, or CHANGELOG entry. Existing user documentation already describes unavailable output controls as dimmed with a reason.

Blockers: none.

Non-blocking nit: add the explicit QSignalBlocker include in the rendering test (inline suggestion).

Verification: reconstructed merge of head c0bfd88 with main f6cd464; ARM64 macOS build with Qt 6.12 and RADE enabled, host/system processors and executable verified ARM64, no RNNoise x86 sources. Both radio_setup_headphone_output_gate_test and mixer_control_availability_test passed. Removed both disabled-style rule strings: the rendering case failed, all 12 unchecked regions rendered identically to enabled controls, and disabled checked Mute buttons retained 665 red pixels. Restored the PR code and both tests passed again. Registration, CI gate, touchpoint manifest and generated reference checks passed. No model-invocable automated review skill was available; performed a second manual lifecycle/failure-path pass and verified prior bot findings rather than adopting them.

The running offscreen app was driven only against DEMO-0001 through a dedicated authenticated socket and isolated settings. whoami reported PID 5337, identity pr-6388-review, txAllowed false; get radio reported AetherSDR Demo, DEMO-0001, transmitting false. Opened Audio settings, inspected captures in Default Dark and Default Light after a live theme switch, closed/reopened the dialog, and asserted all eight output-row widgets remained enabled. Closed the owned instance afterwards. Disabled rows were exercised by injected-backend tests; Demo has a command plane. No live hardware, custom themes, or Linux/Windows runtime rendering was tested.

The attacks that held: exact wording preservation; checked-state rendering without mutating model state; disconnect restoration of the same widgets; theme changes and dialog recreation. The five published checks passed on this head, but their merge base is older than the local tested merge. Main subsequently advanced to aa0dd0e in unrelated files; that newer merge was inspected for overlap but not rebuilt. No unresolved review threads were returned.

Approve with the non-blocking include nit. The maintainer has accepted the only outstanding visual choice.

Comment thread tests/radio_setup_headphone_output_gate_test.cpp
@rfoust
rfoust merged commit 1b77f49 into aethersdr:main Oct 12, 2026
5 checks passed
rfoust added a commit that referenced this pull request Oct 12, 2026
…VIII. (#6403)

Refs #6387, #6388

Answers rfoust's three optional notes on the approving reviews of #6387
and
#6388.

## Limits

- One documentation sentence, one comment and five include lines. No
  behaviour change: no statement, declaration or test registration is
touched, and the five headers were already reached through `<QtTest>`
and
  `<QApplication>`.
- Built and run on one Mac (macOS, Qt 6.12.0). Per-PR CI compiles these
tests but does not run them. The Linux rule the sentence describes is
not
run on Linux here; it is exercised by
`audio_device_preference_policy_test`
  on this host.
- The documentation now names the five ID patterns used by the Linux
hot-plug
filter. The source matches substrings or a prefix, including custom
names;
  it does not establish any audio server's default device names.
- The include pass covers the Qt classes #6387 and #6388 named first in
the
files they touched. Transitive includes that were there before them are
  listed below and left alone.

## Change

1. #6387, `docs/user/docs/audio-settings.md:28`:

   > this reads as applying to all audio-server virtual devices, but the
> exception depends on five ID patterns. A custom-named virtual device
   > outside those patterns restores live.

The reviewer's suggested sentence was the starting point. The follow-up
commit names all five matching ID patterns, including monitor sources
and
custom names. `isLinuxTransientAudioDeviceId` leaves those ids out of
the
arrival list, so a saved device matching one is never seen arriving; at
   startup the saved id is looked up among all enumerable devices.
2. #6387, `src/core/AudioDevicePreferencePolicy.h:54`:

> this comment should describe test portability without claiming every
   > lane runs it.

   The suggested wording is taken as written.
3. #6388, `tests/radio_setup_headphone_output_gate_test.cpp:38`:

   > Include it directly rather than relying on QtTest to supply it
   > transitively, per the repository explicit-include convention.

`<QSignalBlocker>` is included. The same pass over both PRs found four
   more Qt classes #6388 named first in that file without their header:
`QColor`, `QCoreApplication`, `QList`, `QStringList`. They are included
   too.

Found by the same pass and not changed, because the use predates both
PRs:
`QString` and `QWidget` in that test; `QString` in
`mixer_control_availability_test.cpp`, `RadioSetupDialog.cpp` and
`TitleBar.cpp`; `QByteArray` and `QList` in `MainWindow.cpp`. The other
files of #6387 include what they name.

## Evidence

Full ctest before the documentation-only follow-up, at `ac0dd8f6`,
`-j8`:
770 of 772 passed (4 skipped: the
`nr_stereo_independence_*` set), 2 failed:

- `peripheral_auth_dialog_interaction_test`: "dialog 960x680, viewport
658,
page minimum 784" (#6008); fails on clean main on this Mac, also on the
  serial rerun. This diff touches no dialog file.
- `vkamp_connection_test`: one of its two known checks, "commands after
a
disconnect are dropped, not queued" (#5254), at `-j8`; it passed on the
  serial rerun.

`radio_setup_headphone_output_gate_test`,
`audio_device_preference_policy_test` and
`mixer_control_availability_test`
passed. `tools/docs/gen_reference.py --check`,
`check_test_registration.py
--strict` and `gen_touchpoint_manifest.py --check` pass. At the
follow-up
commit `f7dd95c9`, `npm ci && npm run build` in `docs/user/` passed, and
the
source checks remained green.

73, ON8ST

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Robbie Foust <rfoust@gmail.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.

2 participants