/***
Copyright (C) 2016-2025 Denis Arnst (Sapd)
This file is part of HeadsetControl.
HeadsetControl is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
HeadsetControl is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with HeadsetControl. If not, see .
***/
#include "argument_parser.hpp"
#include "capability_descriptors.hpp"
#include "dev.hpp"
#include "device.hpp"
#include "device_registry.hpp"
#include "devices/hid_device.hpp"
#include "feature_handlers.hpp"
#include "feature_utils.hpp"
#include "headsetcontrol.hpp"
#include "hid_utility.hpp"
#include "output.hpp"
#include "result_types.hpp"
#include "utility.hpp"
#include "version.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// ============================================================================
// Namespace imports
// ============================================================================
using headsetcontrol::DeviceRegistry;
using headsetcontrol::HIDDevice;
using headsetcontrol::make_battery_result;
using headsetcontrol::make_error;
using headsetcontrol::make_success;
// Forward declaration for C++ device registry initialization
extern "C" void init_cpp_devices();
namespace {
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
// Must be non-const: modified by signal handler for graceful shutdown
volatile sig_atomic_t g_follow_running = false;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
// ============================================================================
// Output helpers
// ============================================================================
template
void print(std::format_string fmt, Args&&... args)
{
std::cout std::optional {
if (!arg)
return cli::ParseError { "requires equalizer values", "equalizer" };
auto values = headsetcontrol::parse_float_data(*arg);
if (values.empty()) {
return cli::ParseError { "no band values specified", "equalizer" };
}
opts.equalizer = EqualizerSettings(std::move(values));
return std::nullopt; }, "Set equalizer curve", "VALUES")
// === Parametric Equalizer ===
.long_custom("parametric-equalizer", cli::ArgRequirement::Required, [&opts](std::optional arg) -> std::optional {
if (!arg)
return cli::ParseError { "requires band settings", "parametric-equalizer" };
auto peq = headsetcontrol::parse_parametric_equalizer_settings(*arg);
// Note: if any band failed to parse, it won't be in the list
// This allows partial success - valid bands are kept
opts.parametric_equalizer = std::move(peq);
return std::nullopt; }, "Set parametric EQ bands", "BANDS")
// === Microphone ===
.long_value("microphone-mute-led-brightness", opts.mic_mute_led_brightness, uint8_t(0), uint8_t(3), "Set mic mute LED brightness", "LEVEL")
.long_value("microphone-volume", opts.mic_volume, uint8_t(0), uint8_t(128), "Set microphone volume", "VOLUME")
// === Volume ===
.long_toggle("volume-limiter", opts.volume_limiter_enabled, "Toggle volume limiter")
// === Bluetooth ===
.long_toggle("bt-when-powered-on", opts.bt_when_powered_on, "Bluetooth on at power-on")
.long_value("bt-call-volume", opts.bt_call_volume, uint8_t(0), uint8_t(255), "Set bluetooth call volume", "VOLUME")
// === Output Format ===
.choice('o', "output", opts.output_format, output_formats, "Output format")
.custom('c', "short-output", cli::ArgRequirement::None, [&opts](auto) -> std::optional {
opts.output_format = OUTPUT_SHORT;
return std::nullopt; }, "Short output format")
// === Follow Mode ===
.optional_value('f', "follow", opts.follow_mode, opts.follow_seconds, 2u, 1u, 3600u, "Re-run commands periodically", "SECS")
// === Advanced ===
.flag('u', "udev", opts.print_udev_rules, "Output udev rules")
.long_flag("capabilities", opts.print_capabilities, "List device capabilities")
.long_flag("caps", opts.print_capabilities, "")
.long_flag("connected", opts.request_connected, "Check if device connected")
.long_flag("dev", opts.dev_mode, "Development mode")
// === Test Device ===
.long_custom("test-device", cli::ArgRequirement::Optional, [&opts](std::optional arg) -> std::optional {
opts.test_device = true;
if (arg && !arg->empty()) {
long val = 0;
auto [ptr, ec] = std::from_chars(arg->data(), arg->data() + arg->size(), val);
if (ec == std::errc {} && ptr == arg->data() + arg->size() && val >= 0 && val std::optional {
if (!arg)
return cli::ParseError { "requires timeout value", "timeout" };
long val = 0;
auto [ptr, ec] = std::from_chars(arg->data(), arg->data() + arg->size(), val);
if (ec != std::errc {} || ptr != arg->data() + arg->size() || val < 0 || val > 100000) {
return cli::ParseError { "invalid timeout (0-100000)", "timeout" };
}
headsetcontrol::setDeviceTimeout(static_cast(val));
return std::nullopt; }, "Set timeout in ms", "MS")
// === Readme Helper (exits immediately) ===
.long_custom("readme-helper", cli::ArgRequirement::None, [](auto) -> std::optional {
init_cpp_devices();
// Print table (inline for simplicity)
std::cout product_id;
if (cur->manufacturer_string)
dev.vendor_name = cur->manufacturer_string;
if (cur->product_string)
dev.product_name = cur->product_string;
devices.push_back(std::move(dev));
}
return devices;
}
// ============================================================================
// Feature handling
// ============================================================================
hid_device* connectForCapability(HIDConnection& conn, const HIDDevice* device, uint16_t product_id, capabilities cap)
{
auto detail = device->getCapabilityDetail(cap);
auto hid_path = headsetcontrol::get_hid_path(device->getVendorId(), product_id, detail.interface_id, detail.usagepage, detail.usageid);
if (!hid_path)
return nullptr;
return conn.open(*hid_path) ? conn.get() : nullptr;
}
// Convert FeatureOutput to FeatureResult for output formatting
FeatureResult convertToFeatureResult(const headsetcontrol::FeatureOutput& output)
{
FeatureResult result;
result.status = FEATURE_SUCCESS;
result.value = output.value;
result.message = output.message;
// Handle battery special case with extended info
if (output.battery) {
const auto& b = *output.battery;
result.value = b.level_percent;
result.status2 = static_cast(b.status);
// Copy extended battery info
if (b.voltage_mv.has_value())
result.battery_voltage_mv = b.voltage_mv;
if (b.time_to_full_min.has_value())
result.battery_time_to_full_min = b.time_to_full_min;
if (b.time_to_empty_min.has_value())
result.battery_time_to_empty_min = b.time_to_empty_min;
}
// Handle chatmix special case
if (output.chatmix.has_value()) {
result.value = output.chatmix->level;
}
// Handle sidetone special case
if (output.sidetone) {
result.value = output.sidetone->current_level;
result.sidetone_device_level = output.sidetone->device_level;
result.sidetone_level_name = output.sidetone->level_name;
}
return result;
}
// Handle a feature request via the handler registry
FeatureResult handleFeature(DiscoveredDevice& dev, capabilities cap, const FeatureParam& param)
{
// Validate parameter
if (auto error = headsetcontrol::validateFeatureParam(cap, param)) {
return make_error(-1, *error);
}
// Check device support
if (!dev.hasCapability(cap)) {
const auto& desc = headsetcontrol::getCapabilityDescriptor(cap);
return make_error(-1, std::format("This headset doesn't support {}", desc.name));
}
bool is_test = (dev.product_id == PRODUCT_TESTDEVICE);
hid_device* handle = nullptr;
// Connect to device (unless test device)
if (!is_test) {
handle = connectForCapability(dev.connection, dev.device, dev.product_id, cap);
if (!handle) {
return make_error(-1, "Could not open device");
}
}
// Execute via handler registry (no more giant switch!)
auto result = headsetcontrol::FeatureHandlerRegistry::instance().execute(
cap, dev.device, handle, param);
if (result.hasError()) {
return make_error(-1, result.error().message);
}
return convertToFeatureResult(result.value());
}
// ============================================================================
// Help output
// ============================================================================
namespace help {
// ANSI terminal formatting
namespace ansi {
constexpr std::string_view bold = "\033[1m";
constexpr std::string_view dim = "\033[2m";
constexpr std::string_view green = "\033[32m";
constexpr std::string_view reset = "\033[0m";
} // namespace ansi
// Get value hint from capability descriptor (single source of truth)
[[nodiscard]] inline std::string getValueHint(capabilities cap)
{
const auto& desc = headsetcontrol::getCapabilityDescriptor(cap);
return std::string(desc.value_hint);
}
// Option definition for help display
struct Option {
char short_opt = '\0';
std::string_view long_opt;
std::string arg; // Owns string data (for dynamic value hints from descriptors)
std::string_view description;
std::optional required_cap = std::nullopt; // Show only if device has this
bool advanced_only = false; // Show only in --help-all
};
// Section with its options
struct Section {
std::string_view title;
std::vector options;
// Fluent builder for adding options
template
Section& add(char short_opt, std::string_view long_opt, ArgT&& arg,
std::string_view desc, std::optional cap = std::nullopt, bool advanced = false)
{
options.emplace_back(short_opt, long_opt, std::string(std::forward(arg)), desc, cap, advanced);
return *this;
}
template
Section& add(std::string_view long_opt, ArgT&& arg, std::string_view desc,
std::optional cap = std::nullopt, bool advanced = false)
{
return add('\0', long_opt, std::forward(arg), desc, cap, advanced);
}
};
// Help generator class
class HelpGenerator {
public:
HelpGenerator(std::string_view program, HIDDevice* dev, bool all)
: program_name_(program)
, device_(dev)
, show_all_(all)
, caps_(dev ? dev->getCapabilities() : 0)
{
}
void generate()
{
printHeader();
printUsage();
for (const auto& section : buildSections()) {
printSection(section);
}
printExamples();
printFooter();
}
private:
std::string_view program_name_;
HIDDevice* device_;
bool show_all_;
int caps_;
static constexpr int kColumnWidth = 36;
[[nodiscard]] bool hasCapability(capabilities cap) const
{
return (caps_ & B(cap)) != 0;
}
[[nodiscard]] bool shouldShow(const Option& opt) const
{
if (opt.advanced_only && !show_all_)
return false;
if (opt.required_cap && !show_all_ && !hasCapability(*opt.required_cap))
return false;
return true;
}
[[nodiscard]] bool sectionHasVisibleOptions(const Section& section) const
{
return std::ranges::any_of(section.options, [this](const Option& opt) {
return shouldShow(opt);
});
}
void printHeader() const
{
println("{}HeadsetControl{} - Control USB gaming headsets on Linux/macOS/Windows",
ansi::bold, ansi::reset);
println("Version {} | https://github.com/Sapd/HeadsetControl", VERSION);
if (device_) {
println("\nDetected: {}{}{}", ansi::green, device_->getDeviceName(), ansi::reset);
}
}
void printUsage() const
{
println("\n{}USAGE{}", ansi::bold, ansi::reset);
println(" {} [OPTIONS]", program_name_);
println(" {} -b # Check battery", program_name_);
println(" {} -s 64 -l 1 # Set sidetone + lights on", program_name_);
}
void printOption(const Option& opt) const
{
std::string left;
if (opt.short_opt != '\0') {
left = std::format(" -{}", opt.short_opt);
if (!opt.long_opt.empty()) {
left += std::format(", --{}", opt.long_opt);
}
} else if (!opt.long_opt.empty()) {
left = std::format(" --{}", opt.long_opt);
} else {
return; // No option name
}
if (!opt.arg.empty()) {
left += std::format(" {}", opt.arg);
}
// Pad or add spacing
int padding = kColumnWidth - static_cast(left.size());
if (padding > 0) {
left.append(padding, ' ');
} else {
left += " ";
}
println("{}{}", left, opt.description);
}
void printSection(const Section& section) const
{
if (!sectionHasVisibleOptions(section))
return;
println("\n{}{}{}", ansi::bold, section.title, ansi::reset);
for (const auto& opt : section.options) {
if (shouldShow(opt)) {
printOption(opt);
}
}
// Special handling for parametric EQ types
if (section.title == "EQUALIZER" && (show_all_ || hasCapability(CAP_PARAMETRIC_EQUALIZER))) {
printParametricEqTypes();
}
}
void printParametricEqTypes() const
{
if (!show_all_ && !hasCapability(CAP_PARAMETRIC_EQUALIZER))
return;
println(" Format: FREQ,GAIN,Q,TYPE;...");
auto peq_info = device_ ? device_->getParametricEqualizerInfo() : std::nullopt;
std::string types;
for (int i = 0; i < NUM_EQ_FILTER_TYPES; i++) {
bool include = show_all_ || (peq_info && has_capability(peq_info->filter_types, static_cast(i)));
if (include) {
if (!types.empty())
types += ", ";
types += equalizer_filter_type_to_string(static_cast(i));
}
}
if (!types.empty()) {
println(" Types: {}", types);
}
}
void printExamples() const
{
println("\n{}EXAMPLES{}", ansi::bold, ansi::reset);
println(" {} -b -o json # Battery status as JSON", program_name_);
println(" {} -s 0 -l 0 # Disable sidetone and lights", program_name_);
println(" {} -e 0,2,4,2,0,-2 # Custom 6-band EQ curve", program_name_);
println(" {} -f 5 -b # Poll battery every 5 seconds", program_name_);
if (show_all_) {
println(" {} -d 1038:12ad -s 50 # Target specific device", program_name_);
}
}
void printFooter() const
{
if (!show_all_) {
println("\n{}Use --help-all to see all available options{}", ansi::dim, ansi::reset);
}
}
[[nodiscard]] std::vector buildSections() const
{
std::vector sections;
// Device selection - always shown
sections.push_back({ "DEVICE SELECTION", {} });
sections.back().add('d', "device", "VID:PID", "Select device (e.g., 1038:12ad)");
// Status
sections.push_back({ "STATUS", {} });
sections.back()
.add('b', "battery", "", "Show battery level and status", CAP_BATTERY_STATUS)
.add('m', "chatmix", "", "Show current chat-mix balance", CAP_CHATMIX_STATUS)
.add("connected", "", "Check if headset is connected", std::nullopt, true);
// Audio - value hints from capability descriptors
sections.push_back({ "AUDIO", {} });
sections.back()
.add('s', "sidetone", getValueHint(CAP_SIDETONE), "Get current level or set mic feedback (0-128)", CAP_SIDETONE)
.add("volume-limiter", getValueHint(CAP_VOLUME_LIMITER), "Enable/disable volume limiter", CAP_VOLUME_LIMITER);
// Equalizer
sections.push_back({ "EQUALIZER", {} });
sections.back()
.add('e', "equalizer", "V1,V2,...", "Set EQ curve (comma-separated dB values)", CAP_EQUALIZER);
// Dynamic preset range based on device
if (device_ && device_->getEqualizerPresetsCount() > 0) {
sections.back().add('p', "equalizer-preset",
std::format("0-{}", device_->getEqualizerPresetsCount() - 1),
"Select built-in EQ preset", CAP_EQUALIZER_PRESET);
} else {
sections.back().add('p', "equalizer-preset", "N", "Select built-in EQ preset", CAP_EQUALIZER_PRESET);
}
sections.back().add("parametric-equalizer", "BANDS", "Set parametric EQ bands", CAP_PARAMETRIC_EQUALIZER);
// Microphone - value hints from capability descriptors
sections.push_back({ "MICROPHONE", {} });
sections.back()
.add('r', "rotate-to-mute", getValueHint(CAP_ROTATE_TO_MUTE), "Mute when boom arm raised", CAP_ROTATE_TO_MUTE)
.add("microphone-mute-led-brightness", getValueHint(CAP_MICROPHONE_MUTE_LED_BRIGHTNESS), "Mute LED brightness", CAP_MICROPHONE_MUTE_LED_BRIGHTNESS)
.add("microphone-volume", getValueHint(CAP_MICROPHONE_VOLUME), "Microphone gain level", CAP_MICROPHONE_VOLUME)
.add("noise-filter", getValueHint(CAP_NOISE_FILTER), "Microphone noise filter level (0=off, 1=low, 2=high)", CAP_NOISE_FILTER);
// Lights & Audio Cues - value hints from capability descriptors
sections.push_back({ "LIGHTS & AUDIO CUES", {} });
sections.back()
.add('l', "light", getValueHint(CAP_LIGHTS), "RGB/LED lights off/on", CAP_LIGHTS)
.add('v', "voice-prompt", getValueHint(CAP_VOICE_PROMPTS), "Voice prompts off/on", CAP_VOICE_PROMPTS)
.add('n', "notificate", getValueHint(CAP_NOTIFICATION_SOUND), "Play notification sound", CAP_NOTIFICATION_SOUND);
// Power & Bluetooth - value hints from capability descriptors
sections.push_back({ "POWER & BLUETOOTH", {} });
sections.back()
.add('i', "inactive-time", getValueHint(CAP_INACTIVE_TIME), "Auto-off after N minutes (0=never)", CAP_INACTIVE_TIME)
.add("bt-when-powered-on", getValueHint(CAP_BT_WHEN_POWERED_ON), "Enable Bluetooth at power-on", CAP_BT_WHEN_POWERED_ON)
.add("bt-call-volume", getValueHint(CAP_BT_CALL_VOLUME), "Bluetooth call volume", CAP_BT_CALL_VOLUME);
// Output - always shown
sections.push_back({ "OUTPUT", {} });
sections.back()
.add('o', "output", "FORMAT", "json, yaml, env, standard")
.add("capabilities, --caps", "", "List device capabilities");
// Advanced - only in --help-all
sections.push_back({ "ADVANCED", {} });
sections.back()
.add('f', "follow", "[SECS]", "Repeat every N seconds (default: 2)", std::nullopt, true)
.add("timeout", "MS", "HID read timeout (default: 5000)", std::nullopt, true)
.add('u', "", "", "Generate udev rules for Linux", std::nullopt, true)
.add("test-device", "[N]", "Use mock device (for testing)", std::nullopt, true);
// Help - always shown
sections.push_back({ "HELP", {} });
sections.back()
.add('h', "help", "", "Show help for detected device")
.add("help-all", "", "Show all options");
return sections;
}
};
} // namespace help
void printHelp(std::string_view program_name, HIDDevice* device, bool show_all)
{
help::HelpGenerator(program_name, device, show_all).generate();
}
// ============================================================================
// Udev rules output
// ============================================================================
void printUdevRules()
{
println("ACTION!=\"add|change\", GOTO=\"headset_end\"");
println();
for (const auto& device_ptr : DeviceRegistry::instance().getAllDevices()) {
auto* device = device_ptr.get();
println("# {}", device->getDeviceName());
for (uint16_t pid : device->getProductIds()) {
println("KERNEL==\"hidraw*\", SUBSYSTEM==\"hidraw\", "
"ATTRS{{idVendor}}==\"{:04x}\", ATTRS{{idProduct}}==\"{:04x}\", TAG+=\"uaccess\"",
device->getVendorId(), pid);
}
println();
}
println("LABEL=\"headset_end\"");
}
// ============================================================================
// Feature request initialization
// ============================================================================
/**
* @brief Storage for feature request parameters
*
* This struct holds copies of feature parameters that need to persist
* for the duration of feature request processing. Using this struct
* instead of static local variables ensures proper behavior in follow mode
* and avoids const_cast issues with optional values.
*/
struct FeatureParamStorage {
int sidetone_val = 0;
int lights_val = 0;
int notification_val = 0;
int inactive_time_val = 0;
int voice_prompts_val = 0;
int rotate_to_mute_val = 0;
int equalizer_preset_val = 0;
int mic_led_val = 0;
int mic_vol_val = 0;
int volume_limiter_val = 0;
int bt_power_val = 0;
int bt_call_vol_val = 0;
int battery_req = 0;
int chatmix_req = 0;
int noise_filter_val = 0;
// Store copies of complex settings to avoid const_cast
EqualizerSettings equalizer_settings;
ParametricEqualizerSettings parametric_eq_settings;
void updateFrom(const Options& opts)
{
if (opts.sidetone_level.has_value())
sidetone_val = *opts.sidetone_level;
if (opts.lights_enabled.has_value())
lights_val = *opts.lights_enabled ? 1 : 0;
if (opts.notification_sound.has_value())
notification_val = *opts.notification_sound;
if (opts.inactive_time.has_value())
inactive_time_val = *opts.inactive_time;
if (opts.voice_prompts_enabled.has_value())
voice_prompts_val = *opts.voice_prompts_enabled ? 1 : 0;
if (opts.rotate_to_mute_enabled.has_value())
rotate_to_mute_val = *opts.rotate_to_mute_enabled ? 1 : 0;
if (opts.equalizer_preset.has_value())
equalizer_preset_val = *opts.equalizer_preset;
if (opts.mic_mute_led_brightness.has_value())
mic_led_val = *opts.mic_mute_led_brightness;
if (opts.mic_volume.has_value())
mic_vol_val = *opts.mic_volume;
if (opts.volume_limiter_enabled.has_value())
volume_limiter_val = *opts.volume_limiter_enabled ? 1 : 0;
if (opts.bt_when_powered_on.has_value())
bt_power_val = *opts.bt_when_powered_on ? 1 : 0;
if (opts.bt_call_volume.has_value())
bt_call_vol_val = *opts.bt_call_volume;
if (opts.noise_filter.has_value())
noise_filter_val = *opts.noise_filter;
battery_req = opts.request_battery ? 1 : 0;
chatmix_req = opts.request_chatmix ? 1 : 0;
// Copy complex settings (avoids const_cast)
if (opts.equalizer.has_value())
equalizer_settings = *opts.equalizer;
if (opts.parametric_equalizer.has_value())
parametric_eq_settings = *opts.parametric_equalizer;
}
};
// Global storage for feature parameters (must outlive feature requests)
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
FeatureParamStorage g_feature_params;
void initializeFeatureRequests(std::vector& devices, const Options& opts)
{
g_feature_params.updateFrom(opts);
// Build feature requests with type-safe parameters (no more void*)
std::vector requests = {
{ CAP_SIDETONE, CAPABILITYTYPE_ACTION, g_feature_params.sidetone_val, opts.sidetone_level.has_value(), {} },
{ CAP_LIGHTS, CAPABILITYTYPE_ACTION, g_feature_params.lights_val, opts.lights_enabled.has_value(), {} },
{ CAP_NOTIFICATION_SOUND, CAPABILITYTYPE_ACTION, g_feature_params.notification_val, opts.notification_sound.has_value(), {} },
{ CAP_BATTERY_STATUS, CAPABILITYTYPE_INFO, std::monostate {}, opts.request_battery, {} },
{ CAP_INACTIVE_TIME, CAPABILITYTYPE_ACTION, g_feature_params.inactive_time_val, opts.inactive_time.has_value(), {} },
{ CAP_CHATMIX_STATUS, CAPABILITYTYPE_INFO, std::monostate {}, opts.request_chatmix, {} },
{ CAP_SIDETONE_STATUS, CAPABILITYTYPE_INFO, std::monostate {}, opts.request_sidetone, {} },
{ CAP_VOICE_PROMPTS, CAPABILITYTYPE_ACTION, g_feature_params.voice_prompts_val, opts.voice_prompts_enabled.has_value(), {} },
{ CAP_ROTATE_TO_MUTE, CAPABILITYTYPE_ACTION, g_feature_params.rotate_to_mute_val, opts.rotate_to_mute_enabled.has_value(), {} },
{ CAP_EQUALIZER_PRESET, CAPABILITYTYPE_ACTION, g_feature_params.equalizer_preset_val, opts.equalizer_preset.has_value(), {} },
{ CAP_MICROPHONE_MUTE_LED_BRIGHTNESS, CAPABILITYTYPE_ACTION, g_feature_params.mic_led_val, opts.mic_mute_led_brightness.has_value(), {} },
{ CAP_MICROPHONE_VOLUME, CAPABILITYTYPE_ACTION, g_feature_params.mic_vol_val, opts.mic_volume.has_value(), {} },
{ CAP_EQUALIZER, CAPABILITYTYPE_ACTION, opts.equalizer.has_value() ? FeatureParam { g_feature_params.equalizer_settings } : FeatureParam { std::monostate {} }, opts.equalizer.has_value(), {} },
{ CAP_PARAMETRIC_EQUALIZER, CAPABILITYTYPE_ACTION, opts.parametric_equalizer.has_value() ? FeatureParam { g_feature_params.parametric_eq_settings } : FeatureParam { std::monostate {} }, opts.parametric_equalizer.has_value(), {} },
{ CAP_VOLUME_LIMITER, CAPABILITYTYPE_ACTION, g_feature_params.volume_limiter_val, opts.volume_limiter_enabled.has_value(), {} },
{ CAP_BT_WHEN_POWERED_ON, CAPABILITYTYPE_ACTION, g_feature_params.bt_power_val, opts.bt_when_powered_on.has_value(), {} },
{ CAP_BT_CALL_VOLUME, CAPABILITYTYPE_ACTION, g_feature_params.bt_call_vol_val, opts.bt_call_volume.has_value(), {} },
{ CAP_NOISE_FILTER, CAPABILITYTYPE_ACTION, g_feature_params.noise_filter_val, opts.noise_filter.has_value(), {} }
};
for (auto& dev : devices) {
dev.feature_requests = requests;
}
}
// ============================================================================
// Signal handling
// ============================================================================
void signalHandler(int) { g_follow_running = false; }
void setupSignalHandler()
{
#ifdef _WIN32
signal(SIGINT, signalHandler);
#else
struct sigaction act { };
act.sa_handler = signalHandler;
sigaction(SIGINT, &act, nullptr);
#endif
}
// ============================================================================
// Legacy output bridge
// ============================================================================
std::vector toLegacyDeviceList(std::vector& devices)
{
std::vector legacy;
legacy.reserve(devices.size());
for (size_t i = 0; i < devices.size(); i++) {
DeviceList entry {};
entry.featureRequests = devices[i].feature_requests.data();
entry.size = static_cast(devices[i].feature_requests.size());
entry.device = devices[i].device;
entry.product_id = devices[i].product_id;
entry.num_devices = static_cast(devices.size() - i);
entry.vendor_name = devices[i].vendor_name.empty() ? nullptr : devices[i].vendor_name.c_str();
entry.product_name = devices[i].product_name.empty() ? nullptr : devices[i].product_name.c_str();
legacy.push_back(entry);
}
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;
}
}
}
}
// Mark action requests as not processable when multiple devices and no filter
void handleMultiDeviceActions(std::vector& devices, const Options& opts)
{
if (devices.size() getBattery(h);
return !r.hasError() && r.value().status == BATTERY_AVAILABLE;
}
} // anonymous namespace
// ============================================================================
// Main entry point
// ============================================================================
int main(int argc, char* argv[])
{
#if defined(_WIN32) && defined(__GNUC__)
// Disable stdout/stderr buffering for MinGW builds
setvbuf(stdout, nullptr, _IONBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
#endif
Options opts;
cli::ArgumentParser parser(argv[0]);
configureParser(parser, opts);
if (auto error = parser.parse(argc, argv)) {
eprintln("Error: {}", error->format());
return 1;
}
init_cpp_devices();
// Handle immediate modes
if (opts.print_udev_rules) {
std::cerr