diff --git a/cli/main.cpp b/cli/main.cpp index 72559802..007cff80 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -37,8 +37,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -420,7 +420,7 @@ class HIDEnumeration { if (devices_) hid_free_enumeration(devices_); } - HIDEnumeration(const HIDEnumeration&) = delete; + HIDEnumeration(const HIDEnumeration&) = delete; HIDEnumeration& operator=(const HIDEnumeration&) = delete; hid_device_info* get() const { return devices_; } @@ -1003,7 +1003,7 @@ void setupSignalHandler() #ifdef _WIN32 signal(SIGINT, signalHandler); #else - struct sigaction act {}; + struct sigaction act { }; act.sa_handler = signalHandler; sigaction(SIGINT, &act, nullptr); #endif @@ -1031,13 +1031,32 @@ std::vector toLegacyDeviceList(std::vector& device return legacy; } +// Whether this device was asked to set something. Only counts requests that are +// still live, so it has to run after handleMultiDeviceActions has had its say. +bool hasRequestedAction(const DiscoveredDevice& device) +{ + for (const auto& req : device.feature_requests) { + if (req.type == CAPABILITYTYPE_ACTION && req.should_process) { + return true; + } + } + + return false; +} + // Enable info requests for extended output formats (JSON, YAML, ENV) +// Only for a pure query: the extra reads can cost far more than the action +// itself (Maxwell 2: -s 20 is 0.07 s, -s 20 -o json is 2.90 s). Per device, so +// an action on one headset does not silence the info reads of another. void enableExtendedInfoRequests(std::vector& devices, bool extended) { if (!extended) return; for (auto& dev : devices) { + if (hasRequestedAction(dev)) + continue; + for (auto& req : dev.feature_requests) { if (req.type == CAPABILITYTYPE_INFO && !req.should_process && dev.hasCapability(req.cap)) { req.should_process = true; @@ -1201,8 +1220,11 @@ int main(int argc, char* argv[]) // Initialize and configure feature requests initializeFeatureRequests(devices, opts); bool extended = opts.output_format == OUTPUT_YAML || opts.output_format == OUTPUT_JSON || opts.output_format == OUTPUT_ENV; - enableExtendedInfoRequests(devices, extended); + // Order matters: handleMultiDeviceActions neutralizes action requests, and + // enableExtendedInfoRequests has to see the result of that, not the requests + // as they were parsed. handleMultiDeviceActions(devices, opts); + enableExtendedInfoRequests(devices, extended); // Main loop do { diff --git a/cli/output/output.cpp b/cli/output/output.cpp index d193cf06..68b2d569 100644 --- a/cli/output/output.cpp +++ b/cli/output/output.cpp @@ -23,7 +23,9 @@ using namespace headsetcontrol::serializers; // Constants // ============================================================================ -constexpr std::string_view API_VERSION = "1.4"; +// 1.5: an invocation that performs an action no longer reports info it was not +// explicitly asked for. +constexpr std::string_view API_VERSION = "1.5"; constexpr std::string_view APP_NAME = "HeadsetControl"; // ============================================================================ @@ -179,8 +181,7 @@ void processFeatureRequest(const FeatureRequest& req, DeviceData& dev, std::stri for (const auto& preset : presets->presets) { preset_data.push_back(EqualizerPresetData { .name = preset.name, - .values = preset.values - }); + .values = preset.values }); } dev.equalizer_presets = std::move(preset_data); } diff --git a/docs/LIBRARY_USAGE.md b/docs/LIBRARY_USAGE.md index cf8afc07..419dd808 100644 --- a/docs/LIBRARY_USAGE.md +++ b/docs/LIBRARY_USAGE.md @@ -28,7 +28,7 @@ headsetcontrol -o env { "name": "HeadsetControl", "version": "3.0.0", - "api_version": "1.4", + "api_version": "1.5", "device_count": 1, "devices": [ { diff --git a/tests/test_cli_output.cpp b/tests/test_cli_output.cpp index 6ca6d16e..8c109a36 100644 --- a/tests/test_cli_output.cpp +++ b/tests/test_cli_output.cpp @@ -297,6 +297,54 @@ void testCliEnvShellSafe() std::cout << " ✓ CLI ENV output is shell-safe" << std::endl; } +// ============================================================================ +// Action + Extended Output Tests +// ============================================================================ + +void testCliActionOnlyOmitsUnrequestedInfo() +{ + std::cout << " Testing action-only invocation omits info not explicitly requested..." << std::endl; + + // -s (sidetone) is an action; battery/chatmix were not asked for, so an + // extended-format invocation should not pay for reading them. + std::string output = exec(HEADSETCONTROL_EXE " --test-device -s 20 -o json 2>&1"); + + ASSERT_CONTAINS(output, "\"actions\":", "Should have actions array"); + ASSERT_NOT_CONTAINS(output, "\"battery\":", "Battery info should be omitted when not requested"); + ASSERT_NOT_CONTAINS(output, "\"chatmix\":", "Chatmix info should be omitted when not requested"); + + std::cout << " ✓ Action-only invocation omits unrequested info" << std::endl; +} + +void testCliActionWithExplicitInfoKeepsOnlyThat() +{ + std::cout << " Testing action + explicit info request keeps only that info..." << std::endl; + + // -b is explicitly requested alongside the -s action; chatmix was not + // asked for and should still be omitted. + std::string output = exec(HEADSETCONTROL_EXE " --test-device -b -s 20 -o json 2>&1"); + + ASSERT_CONTAINS(output, "\"actions\":", "Should have actions array"); + ASSERT_CONTAINS(output, "\"battery\":", "Explicitly requested battery info should be present"); + ASSERT_NOT_CONTAINS(output, "\"chatmix\":", "Chatmix info should be omitted when not requested"); + + std::cout << " ✓ Action with explicit info request keeps only that info" << std::endl; +} + +void testCliPureQueryStillReturnsAllInfo() +{ + std::cout << " Testing pure query (no action) still returns all extended info..." << std::endl; + + // No action requested, so the extended-format default of reading every + // supported info capability still applies. + std::string output = exec(HEADSETCONTROL_EXE " --test-device -o json 2>&1"); + + ASSERT_CONTAINS(output, "\"battery\":", "Pure query should still report battery"); + ASSERT_CONTAINS(output, "\"chatmix\":", "Pure query should still report chatmix"); + + std::cout << " ✓ Pure query still returns all extended info" << std::endl; +} + // ============================================================================ // Standard Output Tests // ============================================================================ @@ -444,6 +492,11 @@ void runAllCliOutputTests() runTest("ENV Output", testCliEnvOutput); runTest("ENV Shell-Safe", testCliEnvShellSafe); + std::cout << "\n=== Action + Extended Output Tests ===" << std::endl; + runTest("Action-Only Omits Unrequested Info", testCliActionOnlyOmitsUnrequestedInfo); + runTest("Action With Explicit Info", testCliActionWithExplicitInfoKeepsOnlyThat); + runTest("Pure Query Still Returns All Info", testCliPureQueryStillReturnsAllInfo); + std::cout << "\n=== Standard Output Tests ===" << std::endl; runTest("Standard Output", testCliStandardOutput); runTest("Standard Battery Details", testCliStandardBatteryDetails);