From e4c4262cfe6d254fe98cc5324eee9ba6f785c96e Mon Sep 17 00:00:00 2001 From: Meerzulee <17146298+meerzulee@users.noreply.github.com> Date: Sat, 6 Dec 2025 20:01:58 +0600 Subject: [PATCH 1/3] linux: add CLI support for querying status and controlling AirPods Add command-line interface to the Linux app: - --help, --version: standard CLI options - --status, -s: show device status, battery levels - --json, -j: JSON output for scripting - --waybar, -w: Waybar custom module format - --set-noise-mode: control noise cancellation mode - --set-conversational-awareness: toggle conversational awareness - --set-adaptive-level: set adaptive noise level CLI commands communicate with running instance via IPC socket. Refactored CLI code into separate cli.cpp/cli.h for cleaner main.cpp. --- linux/CMakeLists.txt | 5 + linux/cli.cpp | 236 +++++++++++++++++++++++++++++++++++++++++++ linux/cli.h | 26 +++++ linux/main.cpp | 225 +++++++++++++++++++++++++++++++++++------ 4 files changed, 463 insertions(+), 29 deletions(-) create mode 100644 linux/cli.cpp create mode 100644 linux/cli.h diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 7f3336d6..f49ae273 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -4,6 +4,9 @@ project(linux VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Pass version to C++ code +add_compile_definitions(LIBREPODS_VERSION="${PROJECT_VERSION}") + find_package(Qt6 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus) find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) @@ -13,6 +16,8 @@ qt_standard_project_setup() qt_add_executable(librepods main.cpp + cli.cpp + cli.h logger.h media/mediacontroller.cpp media/mediacontroller.h diff --git a/linux/cli.cpp b/linux/cli.cpp new file mode 100644 index 00000000..471440b6 --- /dev/null +++ b/linux/cli.cpp @@ -0,0 +1,236 @@ +#include "cli.h" + +#include +#include +#include +#include + +#ifndef LIBREPODS_VERSION +#define LIBREPODS_VERSION "0.1" +#endif + +namespace CLI { + +QString noiseControlModeName(NoiseControlMode mode) { + switch (mode) { + case NoiseControlMode::Off: return "off"; + case NoiseControlMode::NoiseCancellation: return "noise-cancellation"; + case NoiseControlMode::Transparency: return "transparency"; + case NoiseControlMode::Adaptive: return "adaptive"; + default: return "unknown"; + } +} + +std::optional parseNoiseControlMode(const QString &name) { + QString lower = name.toLower(); + if (lower == "off" || lower == "0") return NoiseControlMode::Off; + if (lower == "noise-cancellation" || lower == "nc" || lower == "anc" || lower == "1") return NoiseControlMode::NoiseCancellation; + if (lower == "transparency" || lower == "tr" || lower == "2") return NoiseControlMode::Transparency; + if (lower == "adaptive" || lower == "3") return NoiseControlMode::Adaptive; + return std::nullopt; +} + +bool isInstanceRunning() { + QLocalSocket socket; + socket.connectToServer("app_server"); + bool running = socket.waitForConnected(300); + socket.disconnectFromServer(); + return running; +} + +QString sendIpcCommand(const QString &command, int timeout) { + QLocalSocket socket; + socket.connectToServer("app_server"); + + if (!socket.waitForConnected(500)) { + return QString(); + } + + socket.write(command.toUtf8()); + socket.flush(); + socket.waitForBytesWritten(500); + + if (socket.waitForReadyRead(timeout)) { + QString response = QString::fromUtf8(socket.readAll()); + socket.disconnectFromServer(); + return response; + } + + socket.disconnectFromServer(); + return QString(); +} + +int handleCLICommands(QApplication &app) { + app.setApplicationName("LibrePods"); + app.setApplicationVersion(LIBREPODS_VERSION); + + QCommandLineParser parser; + parser.setApplicationDescription("LibrePods - Control your AirPods on Linux"); + + // Standard options + parser.addHelpOption(); + parser.addVersionOption(); + + // Application options + QCommandLineOption debugOption(QStringList() << "debug", + "Enable debug logging output"); + parser.addOption(debugOption); + + QCommandLineOption hideOption(QStringList() << "hide", + "Start with window hidden (tray only)"); + parser.addOption(hideOption); + + // CLI query options + QCommandLineOption statusOption(QStringList() << "s" << "status", + "Show AirPods connection status and battery levels"); + parser.addOption(statusOption); + + QCommandLineOption jsonOption(QStringList() << "j" << "json", + "Output in JSON format (use with --status)"); + parser.addOption(jsonOption); + + QCommandLineOption waybarOption(QStringList() << "w" << "waybar", + "Output in Waybar custom module format"); + parser.addOption(waybarOption); + + // CLI control options + QCommandLineOption setNoiseModeOption(QStringList() << "set-noise-mode", + "Set noise control mode (off, transparency, noise-cancellation/nc/anc, adaptive)", + "mode"); + parser.addOption(setNoiseModeOption); + + QCommandLineOption setCAOption(QStringList() << "set-conversational-awareness", + "Set conversational awareness (on/off, true/false, 1/0)", + "state"); + parser.addOption(setCAOption); + + QCommandLineOption setAdaptiveLevelOption(QStringList() << "set-adaptive-level", + "Set adaptive noise level (0-100)", + "level"); + parser.addOption(setAdaptiveLevelOption); + + parser.process(app); + + bool wantsStatus = parser.isSet(statusOption); + bool wantsJson = parser.isSet(jsonOption); + bool wantsWaybar = parser.isSet(waybarOption); + QString noiseMode = parser.value(setNoiseModeOption); + QString caState = parser.value(setCAOption); + QString adaptiveLevel = parser.value(setAdaptiveLevelOption); + + // Check if this is a CLI command + bool hasStatusQuery = wantsStatus || wantsWaybar; + bool hasControlCommand = !noiseMode.isEmpty() || !caState.isEmpty() || !adaptiveLevel.isEmpty(); + bool isCLICommand = hasStatusQuery || hasControlCommand; + + if (!isCLICommand) { + // Not a CLI command, return -1 to indicate GUI should start + return -1; + } + + // Handle CLI commands + QTextStream out(stdout); + QTextStream err(stderr); + + if (!isInstanceRunning()) { + err << "Error: LibrePods is not running. Start the application first.\n"; + return 1; + } + + // Handle waybar output + if (wantsWaybar) { + QString response = sendIpcCommand("cli:status:waybar"); + + if (response.isEmpty()) { + // Output disconnected state for waybar + out << R"({"text": "󰥰 --", "tooltip": "LibrePods not running", "class": "disconnected"})" << "\n"; + return 0; + } + + out << response; + if (!response.endsWith('\n')) out << "\n"; + return 0; + } + + // Handle status query + if (wantsStatus) { + QString cmd = wantsJson ? "cli:status:json" : "cli:status:text"; + QString response = sendIpcCommand(cmd); + + if (response.isEmpty()) { + err << "Error: No response from LibrePods\n"; + return 1; + } + + out << response; + if (!response.endsWith('\n')) out << "\n"; + return 0; + } + + // Handle set noise mode + if (!noiseMode.isEmpty()) { + auto mode = parseNoiseControlMode(noiseMode); + if (!mode.has_value()) { + err << "Error: Invalid noise mode '" << noiseMode << "'\n"; + err << "Valid modes: off, transparency, noise-cancellation (nc/anc), adaptive\n"; + return 1; + } + + QString response = sendIpcCommand("cli:set-noise-mode:" + QString::number(static_cast(mode.value()))); + if (response.startsWith("OK")) { + out << "Noise control mode set to: " << noiseControlModeName(mode.value()) << "\n"; + return 0; + } else { + err << "Error: " << (response.isEmpty() ? "No response from LibrePods" : response) << "\n"; + return 1; + } + } + + // Handle set conversational awareness + if (!caState.isEmpty()) { + QString lower = caState.toLower(); + bool enabled; + if (lower == "on" || lower == "true" || lower == "1" || lower == "yes") { + enabled = true; + } else if (lower == "off" || lower == "false" || lower == "0" || lower == "no") { + enabled = false; + } else { + err << "Error: Invalid state '" << caState << "'\n"; + err << "Valid values: on/off, true/false, 1/0, yes/no\n"; + return 1; + } + + QString response = sendIpcCommand("cli:set-ca:" + QString(enabled ? "1" : "0")); + if (response.startsWith("OK")) { + out << "Conversational awareness set to: " << (enabled ? "on" : "off") << "\n"; + return 0; + } else { + err << "Error: " << (response.isEmpty() ? "No response from LibrePods" : response) << "\n"; + return 1; + } + } + + // Handle set adaptive level + if (!adaptiveLevel.isEmpty()) { + bool ok; + int level = adaptiveLevel.toInt(&ok); + if (!ok || level < 0 || level > 100) { + err << "Error: Invalid adaptive level '" << adaptiveLevel << "'\n"; + err << "Valid range: 0-100\n"; + return 1; + } + + QString response = sendIpcCommand("cli:set-adaptive-level:" + QString::number(level)); + if (response.startsWith("OK")) { + out << "Adaptive noise level set to: " << level << "\n"; + return 0; + } else { + err << "Error: " << (response.isEmpty() ? "No response from LibrePods" : response) << "\n"; + return 1; + } + } + + return 0; +} + +} // namespace CLI diff --git a/linux/cli.h b/linux/cli.h new file mode 100644 index 00000000..e75b5535 --- /dev/null +++ b/linux/cli.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include "enums.h" + +using namespace AirpodsTrayApp::Enums; + +namespace CLI { + +// Noise control mode helpers +QString noiseControlModeName(NoiseControlMode mode); +std::optional parseNoiseControlMode(const QString &name); + +// Check if another instance is running +bool isInstanceRunning(); + +// Send IPC command to running instance and get response +QString sendIpcCommand(const QString &command, int timeout = 2000); + +// Parse CLI arguments and handle CLI commands +// Returns: -1 if should continue to GUI, otherwise the exit code +int handleCLICommands(QApplication &app); + +} // namespace CLI diff --git a/linux/main.cpp b/linux/main.cpp index 63456f7f..8491c958 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -12,6 +12,9 @@ #include #include #include +#include +#include +#include #include "airpods_packets.h" #include "logger.h" @@ -26,6 +29,7 @@ #include "ble/bleutils.h" #include "QRCodeImageProvider.hpp" #include "systemsleepmonitor.hpp" +#include "cli.h" using namespace AirpodsTrayApp::Enums; @@ -134,6 +138,109 @@ class AirPodsTrayApp : public QObject { QString phoneMacStatus() const { return m_phoneMacStatus; } bool hearingAidEnabled() const { return m_deviceInfo->hearingAidEnabled(); } + QString getStatusJson() const { + QJsonObject status; + status["connected"] = areAirpodsConnected(); + + if (areAirpodsConnected() && m_deviceInfo) { + status["device_name"] = m_deviceInfo->deviceName(); + status["model"] = m_deviceInfo->modelNumber(); + status["bluetooth_address"] = m_deviceInfo->bluetoothAddress(); + + QJsonObject battery; + Battery *bat = m_deviceInfo->getBattery(); + if (bat) { + battery["left"] = bat->getLeftPodLevel(); + battery["right"] = bat->getRightPodLevel(); + battery["case"] = bat->getCaseLevel(); + battery["left_charging"] = bat->isLeftPodCharging(); + battery["right_charging"] = bat->isRightPodCharging(); + battery["case_charging"] = bat->isCaseCharging(); + } + status["battery"] = battery; + + status["noise_control_mode"] = static_cast(m_deviceInfo->noiseControlMode()); + status["noise_control_mode_name"] = CLI::noiseControlModeName(m_deviceInfo->noiseControlMode()); + status["conversational_awareness"] = m_deviceInfo->conversationalAwareness(); + status["adaptive_noise_level"] = m_deviceInfo->adaptiveNoiseLevel(); + status["one_bud_anc_mode"] = m_deviceInfo->oneBudANCMode(); + status["hearing_aid_enabled"] = m_deviceInfo->hearingAidEnabled(); + + QJsonObject ear; + EarDetection *earDet = m_deviceInfo->getEarDetection(); + if (earDet) { + ear["primary_in_ear"] = earDet->isPrimaryInEar(); + ear["secondary_in_ear"] = earDet->isSecondaryInEar(); + } + status["ear_detection"] = ear; + } + + QJsonDocument doc(status); + return doc.toJson(QJsonDocument::Compact); + } + + QString getStatusText() const { + if (!areAirpodsConnected() || !m_deviceInfo) { + return "Status: Not connected\n"; + } + + QString text; + QTextStream out(&text); + + out << "Status: Connected\n"; + out << "Device: " << m_deviceInfo->deviceName() << "\n"; + out << "Model: " << m_deviceInfo->modelNumber() << "\n"; + out << "Address: " << m_deviceInfo->bluetoothAddress() << "\n"; + + Battery *bat = m_deviceInfo->getBattery(); + if (bat) { + out << "Battery:\n"; + out << " Left: " << bat->getLeftPodLevel() << "%" << (bat->isLeftPodCharging() ? " (charging)" : "") << "\n"; + out << " Right: " << bat->getRightPodLevel() << "%" << (bat->isRightPodCharging() ? " (charging)" : "") << "\n"; + out << " Case: " << bat->getCaseLevel() << "%" << (bat->isCaseCharging() ? " (charging)" : "") << "\n"; + } + + out << "Noise Control: " << CLI::noiseControlModeName(m_deviceInfo->noiseControlMode()) << "\n"; + out << "Conversational Awareness: " << (m_deviceInfo->conversationalAwareness() ? "On" : "Off") << "\n"; + out << "Adaptive Noise Level: " << m_deviceInfo->adaptiveNoiseLevel() << "\n"; + + return text; + } + + QString getStatusWaybar() const { + QJsonObject waybar; + + if (!areAirpodsConnected() || !m_deviceInfo) { + waybar["text"] = QString::fromUtf8("󰥰 --"); + waybar["tooltip"] = "AirPods disconnected"; + waybar["class"] = "disconnected"; + } else { + Battery *bat = m_deviceInfo->getBattery(); + int left = bat ? bat->getLeftPodLevel() : 0; + int right = bat ? bat->getRightPodLevel() : 0; + int avg = (left + right) / 2; + + waybar["text"] = QString::fromUtf8("󰥰 %1%").arg(avg); + + QString tooltip; + QTextStream ts(&tooltip); + ts << m_deviceInfo->deviceName() << "\n"; + ts << "Left: " << left << "%"; + if (bat && bat->isLeftPodCharging()) ts << " ⚡"; + ts << "\n"; + ts << "Right: " << right << "%"; + if (bat && bat->isRightPodCharging()) ts << " ⚡"; + ts << "\n"; + ts << "Mode: " << CLI::noiseControlModeName(m_deviceInfo->noiseControlMode()); + + waybar["tooltip"] = tooltip; + waybar["class"] = "connected"; + } + + QJsonDocument doc(waybar); + return doc.toJson(QJsonDocument::Compact); + } + private: bool debugMode; bool isConnectedLocally = false; @@ -987,37 +1094,40 @@ private slots: int main(int argc, char *argv[]) { QApplication app(argc, argv); - QLocalServer::removeServer("app_server"); - - QFile stale("/tmp/app_server"); - if (stale.exists()) - stale.remove(); - - QLocalSocket socket_check; - socket_check.connectToServer("app_server"); + // Handle CLI commands first (returns -1 if should continue to GUI) + int cliResult = CLI::handleCLICommands(app); + if (cliResult >= 0) { + return cliResult; + } - if (socket_check.waitForConnected(300)) { + // Check if another instance is running + if (CLI::isInstanceRunning()) { LOG_INFO("Another instance already running! Reopening window..."); - socket_check.write("reopen"); - socket_check.flush(); - socket_check.waitForBytesWritten(200); - socket_check.disconnectFromServer(); + QLocalSocket reopen_socket; + reopen_socket.connectToServer("app_server"); + if (reopen_socket.waitForConnected(300)) { + reopen_socket.write("reopen"); + reopen_socket.flush(); + reopen_socket.waitForBytesWritten(200); + reopen_socket.disconnectFromServer(); + } return 0; } - app.setDesktopFileName("me.kavishdevar.librepods"); - app.setQuitOnLastWindowClosed(false); - bool debugMode = false; - bool hideOnStart = false; - for (int i = 1; i < argc; ++i) { - if (QString(argv[i]) == "--debug") - debugMode = true; + // Clean up stale socket files before starting new instance + QLocalServer::removeServer("app_server"); + QFile stale("/tmp/app_server"); + if (stale.exists()) + stale.remove(); - if (QString(argv[i]) == "--hide") - hideOnStart = true; - } + // Parse --debug and --hide flags for GUI mode + bool debugMode = app.arguments().contains("--debug"); + bool hideOnStart = app.arguments().contains("--hide"); + + app.setDesktopFileName("me.kavishdevar.librepods"); + app.setQuitOnLastWindowClosed(false); QQmlApplicationEngine engine; qmlRegisterType("me.kavishdevar.Battery", 1, 0, "Battery"); @@ -1049,12 +1159,69 @@ int main(int argc, char *argv[]) { { LOG_DEBUG("Server started, waiting for connections..."); } + QObject::connect(&server, &QLocalServer::newConnection, [&]() { QLocalSocket* socket = server.nextPendingConnection(); - // Handles Proper Connection - QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine, &trayApp]() { - QString msg = socket->readAll(); - // Check if the message is "reopen", if so, trigger onOpenApp function + + QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine, trayApp]() { + QString msg = QString::fromUtf8(socket->readAll()); + LOG_DEBUG("IPC message received: " << msg); + + // Handle CLI commands + if (msg.startsWith("cli:")) { + QString response; + + if (msg == "cli:status:json") { + response = trayApp->getStatusJson(); + } + else if (msg == "cli:status:text") { + response = trayApp->getStatusText(); + } + else if (msg == "cli:status:waybar") { + response = trayApp->getStatusWaybar(); + } + else if (msg.startsWith("cli:set-noise-mode:")) { + QString modeStr = msg.mid(QString("cli:set-noise-mode:").length()); + int mode = modeStr.toInt(); + if (!trayApp->areAirpodsConnected()) { + response = "Error: AirPods not connected"; + } else { + trayApp->setNoiseControlModeInt(mode); + response = "OK"; + } + } + else if (msg.startsWith("cli:set-ca:")) { + QString stateStr = msg.mid(QString("cli:set-ca:").length()); + bool enabled = (stateStr == "1"); + if (!trayApp->areAirpodsConnected()) { + response = "Error: AirPods not connected"; + } else { + trayApp->setConversationalAwareness(enabled); + response = "OK"; + } + } + else if (msg.startsWith("cli:set-adaptive-level:")) { + QString levelStr = msg.mid(QString("cli:set-adaptive-level:").length()); + int level = levelStr.toInt(); + if (!trayApp->areAirpodsConnected()) { + response = "Error: AirPods not connected"; + } else { + trayApp->setAdaptiveNoiseLevel(level); + response = "OK"; + } + } + else { + response = "Error: Unknown command"; + } + + socket->write(response.toUtf8()); + socket->flush(); + socket->waitForBytesWritten(500); + socket->disconnectFromServer(); + return; + } + + // Handle reopen command if (msg == "reopen") { LOG_INFO("Reopening app window"); QObject *rootObject = engine.rootObjects().first(); @@ -1072,13 +1239,12 @@ int main(int argc, char *argv[]) { } socket->disconnectFromServer(); }); - // Handles connection errors + QObject::connect(socket, &QLocalSocket::errorOccurred, [socket]() { LOG_ERROR("Failed to connect to the duplicate app instance"); LOG_DEBUG("Connection error: " << socket->errorString()); }); - // Handle server-level errors QObject::connect(&server, &QLocalServer::serverError, [&]() { LOG_ERROR("Server failed to accept a new connection"); LOG_DEBUG("Server error: " << server.errorString()); @@ -1097,6 +1263,7 @@ int main(int argc, char *argv[]) { if (stale.exists()) stale.remove(); }); + return app.exec(); } From d3b60cc5f1c32e85f600589a90308fe2f07750a0 Mon Sep 17 00:00:00 2001 From: Meerzulee <17146298+meerzulee@users.noreply.github.com> Date: Sat, 6 Dec 2025 21:13:38 +0600 Subject: [PATCH 2/3] linux: fix crash on tray Open/Settings and add Fusion style fallback - Fix segfault when calling .first() on empty rootObjects() or topLevelWindows() lists in tray menu handlers - Add Fusion as fallback Qt Quick Controls style to prevent QML load failures when platform theme modules (e.g., kvantum) are not installed - Override QT_STYLE_OVERRIDE=kvantum to Fusion before QApplication init The Fusion style is built into Qt and available on all platforms (X11, Wayland, KDE, GNOME, etc.), ensuring the app works regardless of the user's theme configuration. --- linux/main.cpp | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/linux/main.cpp b/linux/main.cpp index 8491c958..e1a5357c 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -547,8 +547,12 @@ public slots: private slots: void onTrayIconActivated() { - QQuickWindow *window = qobject_cast( - QGuiApplication::topLevelWindows().constFirst()); + const auto windows = QGuiApplication::topLevelWindows(); + if (windows.isEmpty()) { + loadMainModule(); + return; + } + QQuickWindow *window = qobject_cast(windows.constFirst()); if (window) { window->show(); @@ -559,26 +563,26 @@ private slots: void onOpenApp() { + if (parent->rootObjects().isEmpty()) { + loadMainModule(); + return; + } QObject *rootObject = parent->rootObjects().first(); if (rootObject) { QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "app")); } - else - { - loadMainModule(); - } } void onOpenSettings() { + if (parent->rootObjects().isEmpty()) { + loadMainModule(); + return; + } QObject *rootObject = parent->rootObjects().first(); if (rootObject) { QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "settings")); } - else - { - loadMainModule(); - } } void sendHandshake() { @@ -1092,6 +1096,11 @@ private slots: }; int main(int argc, char *argv[]) { + // Use Fusion style as fallback for missing theme modules + if (qgetenv("QT_STYLE_OVERRIDE").toLower() == "kvantum") + qputenv("QT_STYLE_OVERRIDE", "Fusion"); + qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion"); + QApplication app(argc, argv); // Handle CLI commands first (returns -1 if should continue to GUI) @@ -1224,13 +1233,13 @@ int main(int argc, char *argv[]) { // Handle reopen command if (msg == "reopen") { LOG_INFO("Reopening app window"); - QObject *rootObject = engine.rootObjects().first(); - if (rootObject) { - QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "app")); - } - else - { + if (engine.rootObjects().isEmpty()) { trayApp->loadMainModule(); + } else { + QObject *rootObject = engine.rootObjects().first(); + if (rootObject) { + QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "app")); + } } } else From bc25f082b84683800fbfbc391e5942a30f780064 Mon Sep 17 00:00:00 2001 From: Meerzulee <17146298+meerzulee@users.noreply.github.com> Date: Sat, 6 Dec 2025 21:57:30 +0600 Subject: [PATCH 3/3] linux: add input validation and fix memory leak in IPC handler - Add deleteLater cleanup for IPC sockets to prevent memory leaks - Validate noise mode input (0-3 range check) - Validate conversational awareness state (must be "0" or "1") - Validate adaptive level range (0-100) --- linux/main.cpp | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/linux/main.cpp b/linux/main.cpp index e1a5357c..4c1fa3fe 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -1171,6 +1171,7 @@ int main(int argc, char *argv[]) { QObject::connect(&server, &QLocalServer::newConnection, [&]() { QLocalSocket* socket = server.nextPendingConnection(); + QObject::connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater); QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine, trayApp]() { QString msg = QString::fromUtf8(socket->readAll()); @@ -1191,8 +1192,11 @@ int main(int argc, char *argv[]) { } else if (msg.startsWith("cli:set-noise-mode:")) { QString modeStr = msg.mid(QString("cli:set-noise-mode:").length()); - int mode = modeStr.toInt(); - if (!trayApp->areAirpodsConnected()) { + bool ok; + int mode = modeStr.toInt(&ok); + if (!ok || mode < 0 || mode > 3) { + response = "Error: Invalid noise mode"; + } else if (!trayApp->areAirpodsConnected()) { response = "Error: AirPods not connected"; } else { trayApp->setNoiseControlModeInt(mode); @@ -1201,18 +1205,22 @@ int main(int argc, char *argv[]) { } else if (msg.startsWith("cli:set-ca:")) { QString stateStr = msg.mid(QString("cli:set-ca:").length()); - bool enabled = (stateStr == "1"); - if (!trayApp->areAirpodsConnected()) { + if (stateStr != "0" && stateStr != "1") { + response = "Error: Invalid state"; + } else if (!trayApp->areAirpodsConnected()) { response = "Error: AirPods not connected"; } else { - trayApp->setConversationalAwareness(enabled); + trayApp->setConversationalAwareness(stateStr == "1"); response = "OK"; } } else if (msg.startsWith("cli:set-adaptive-level:")) { QString levelStr = msg.mid(QString("cli:set-adaptive-level:").length()); - int level = levelStr.toInt(); - if (!trayApp->areAirpodsConnected()) { + bool ok; + int level = levelStr.toInt(&ok); + if (!ok || level < 0 || level > 100) { + response = "Error: Invalid level (0-100)"; + } else if (!trayApp->areAirpodsConnected()) { response = "Error: AirPods not connected"; } else { trayApp->setAdaptiveNoiseLevel(level);