Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <format>
#include <iostream>
Expand Down Expand Up @@ -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_; }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1031,13 +1031,32 @@ std::vector<DeviceList> toLegacyDeviceList(std::vector<DiscoveredDevice>& 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<DiscoveredDevice>& 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;
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions cli/output/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";

// ============================================================================
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion docs/LIBRARY_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
53 changes: 53 additions & 0 deletions tests/test_cli_output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================================
Expand Down Expand Up @@ -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);
Expand Down
Loading