Skip to content

Conversation

@meerzulee
Copy link

@meerzulee meerzulee commented Dec 6, 2025

Linux: Add CLI support and fix Qt theme compatibility

Features

CLI Interface

Query status and control AirPods from the command line while the app is running in the background.

# Status queries
librepods --status              # Show connection status and battery
librepods --status --json       # JSON output
librepods --waybar              # Waybar custom module format

# Control
librepods --set-noise-mode transparency
librepods --set-noise-mode nc   # noise-cancellation
librepods --set-conversational-awareness on
librepods --set-adaptive-level 50

Waybar integration example:

  "custom/airpods": {
      "exec": "librepods --waybar",
      "interval": 10,
      "return-type": "json"
  }

Bug Fixes

Qt Theme Compatibility

  • Fix crash when clicking "Open" or "Settings" in tray menu on some systems
  • Add Fusion style fallback for systems with incomplete Qt theme modules (e.g., kvantum without kvantum-qt6)

The app now works on X11, Wayland, KDE, GNOME, and other desktop environments regardless of theme configuration.

Summary by CodeRabbit

  • New Features

    • Added a command-line interface to control LibrePods without opening the GUI.
    • Status reporting in text, JSON, and Waybar-compatible formats.
    • CLI commands to set noise control mode, conversational awareness, and adaptive noise level.
    • Detects and communicates with a running instance to apply commands or show status.
  • Bug Fixes / Improvements

    • More robust startup/instance handling and clearer CLI error/status messages.

✏️ Tip: You can customize this high-level summary in your review settings.

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.
- 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.
@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

Walkthrough

Adds a Linux CLI layer and related IPC helpers, integrates CLI into startup to reuse or start instances, exposes three status-reporting methods (JSON, text, Waybar), and defines a compile-time LIBREPODS_VERSION macro for C++ code.

Changes

Cohort / File(s) Change Summary
Build configuration
linux/CMakeLists.txt
Added add_compile_definitions(LIBREPODS_VERSION="${PROJECT_VERSION}") and included linux/cli.cpp / linux/cli.h in the librepods Qt executable target.
CLI implementation
linux/cli.h, linux/cli.cpp
New CLI namespace: noiseControlModeName() / parseNoiseControlMode(); isInstanceRunning(); sendIpcCommand(); handleCLICommands() — implements QCommandLineParser, IPC communication (QLocalSocket), validation, formatted outputs, and version fallback.
Application startup & IPC routing
linux/main.cpp
Added early CLI handling in main(), instance reuse and stale-socket cleanup, IPC command routing for status and control commands, and three new status getters: getStatusJson(), getStatusText(), getStatusWaybar().

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant App as main() / Startup
    participant IPC as QLocalSocket / Server
    participant GUI as Running GUI Instance

    User->>App: launch with CLI args
    App->>CLI: handleCLICommands(app)
    CLI->>IPC: isInstanceRunning()
    alt instance running
        CLI->>IPC: sendIpcCommand(command, timeout)
        IPC-->>CLI: response (status / ack / error)
        CLI-->>User: print formatted output / exit
    else no instance
        CLI-->>App: return -1 (continue to GUI)
        App->>App: cleanup stale socket, start IPC server
        App->>GUI: initialize GUI and event loop
        GUI-->>User: GUI started (no CLI response)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect CLI parsing and alias handling in linux/cli.cpp.
  • Verify QLocalSocket timeouts, error handling, and stale-socket removal in linux/cli.cpp and linux/main.cpp.
  • Validate JSON structure, escaping, and Waybar tooltip formatting in getStatusJson() / getStatusWaybar() (linux/main.cpp).
  • Confirm startup/control flow: early-exit semantics, flag propagation (--debug, --hide), and reopen handling.

Poem

🐰 I nibble at the socket, soft and bright,
A whisper to the server in the night.
Modes and batteries I proudly sing,
GUI wakes, or CLI takes wing —
Hopping code and carrot-light delight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Feature/linux cli' is vague and generic, using a common branch naming convention rather than a descriptive summary of the actual changes. Consider using a more descriptive title that captures the main feature, such as 'Add Linux CLI interface for status queries and AirPods control' to better convey the purpose of the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
linux/cli.cpp (1)

33-39: Centralize the local‑server name and align timeouts

isInstanceRunning() and sendIpcCommand() (and main.cpp) all hard‑code "app_server" and slightly different connect timeouts. Consider a shared constant (e.g. constexpr auto kIpcServerName = "app_server";) and, if possible, a single place defining the connection timeout, to avoid subtle drift if this ever changes.

Also applies to: 41-61

linux/cli.h (1)

3-9: Avoid using namespace in a public header

using namespace AirpodsTrayApp::Enums; in a header pulls all of that namespace’s symbols into any translation unit that includes cli.h, which can lead to surprising name clashes.

Prefer one of:

  • Qualify the type in the declarations, e.g. AirpodsTrayApp::Enums::NoiseControlMode.
  • Or add a narrow alias inside the CLI namespace, e.g. using NoiseControlMode = AirpodsTrayApp::Enums::NoiseControlMode;, and use that in the signatures.

This keeps the CLI API clean without leaking the entire enums namespace to callers.

Also applies to: 12-25

linux/main.cpp (1)

1128-1133: Optionally include <QFile> explicitly where it’s used

You use QFile stale("/tmp/app_server"); in both the startup and shutdown cleanup paths. If QFile is not already pulled in transitively via another header, it would be safer to add an explicit #include <QFile> near the top of this file to keep the dependency clear and avoid relying on indirect includes.

Also applies to: 1270-1274

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3b1db2 and d3b60cc.

📒 Files selected for processing (4)
  • linux/CMakeLists.txt (2 hunks)
  • linux/cli.cpp (1 hunks)
  • linux/cli.h (1 hunks)
  • linux/main.cpp (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
linux/cli.h (2)
linux/cli.cpp (10)
  • noiseControlModeName (14-22)
  • noiseControlModeName (14-14)
  • parseNoiseControlMode (24-31)
  • parseNoiseControlMode (24-24)
  • isInstanceRunning (33-39)
  • isInstanceRunning (33-33)
  • sendIpcCommand (41-61)
  • sendIpcCommand (41-41)
  • handleCLICommands (63-234)
  • handleCLICommands (63-63)
linux/main.cpp (7)
  • mode (291-301)
  • mode (291-291)
  • mode (302-310)
  • mode (302-302)
  • mode (1063-1063)
  • name (1068-1068)
  • app (1104-1104)
linux/cli.cpp (2)
linux/main.cpp (5)
  • mode (291-301)
  • mode (291-291)
  • mode (302-310)
  • mode (302-302)
  • mode (1063-1063)
linux/deviceinfo.hpp (1)
  • static_cast (58-58)
🪛 Clang (14.0.6)
linux/cli.h

[error] 3-3: 'QString' file not found

(clang-diagnostic-error)

🔇 Additional comments (5)
linux/cli.cpp (1)

14-31: Noise-control mode helpers look consistent and cover common aliases

The string/enum mapping (including short forms like nc, tr, numeric 0–3) looks consistent and should work well with the IPC integer protocol. Just keep the mapping in sync with any future changes to NoiseControlMode ordering.

linux/CMakeLists.txt (1)

7-9: Version macro + CLI sources wiring looks correct

Passing ${PROJECT_VERSION} via LIBREPODS_VERSION and adding cli.cpp/cli.h to the librepods target cleanly exposes the version to the CLI and ensures the new code is built with the main binary.

Also applies to: 17-47

linux/main.cpp (3)

15-17: Status helpers for JSON/text/Waybar are well factored and null‑safe

The new getStatusJson(), getStatusText(), and getStatusWaybar() methods cleanly encapsulate status reporting for CLI/IPC, with appropriate checks for connection state and null Battery/EarDetection pointers. They should be safe to call regardless of connection and give consistent output across formats.

Also applies to: 32-32, 141-180, 182-208, 210-242


548-556: Tray and menu handlers now guard against missing UI and avoid crashes

The added checks for QGuiApplication::topLevelWindows().isEmpty() and parent->rootObjects().isEmpty() before accessing windows/root objects will prevent the crashes seen when tray actions are used before QML is loaded. Falling back to loadMainModule() in those cases is a sensible behavior.

Also applies to: 564-575, 576-586


1099-1103: Theme fallback and CLI entry integration look reasonable

Switching from QT_STYLE_OVERRIDE=kvantum to Fusion and forcing QT_QUICK_CONTROLS_STYLE=Fusion should mitigate theme-plugin crashes on systems with partial Kvantum setups, and the early CLI::handleCLICommands(app) call cleanly short‑circuits into CLI mode when relevant options are present while still allowing --debug/--hide to be picked up for GUI runs.

Also applies to: 1104-1110, 1134-1140

Comment on lines +112 to +138
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;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Waybar disconnected fallback is unreachable when the app isn’t running

Right now handleCLICommands() returns early if !isInstanceRunning() (Lines 135–138), so the --waybar branch (Lines 141–153) never executes in that case and the “disconnected” JSON fallback is not emitted. For Waybar, it’s usually preferable to always print valid JSON even if the app isn’t running.

Consider restructuring to handle wantsWaybar before the instance check, e.g.:

  • If wantsWaybar, call sendIpcCommand("cli:status:waybar") directly and, on empty response, print the disconnected JSON and exit 0, regardless of isInstanceRunning().
  • For other CLI commands, keep the existing isInstanceRunning() guard and error exit.

This keeps Waybar output robust while still surfacing a clear error for the other CLI commands.

Also applies to: 140-168, 170-231

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
linux/main.cpp (1)

1172-1258: IPC CLI handler is much stronger; cli:set-adaptive-level still reports success when no change is possible

Nice improvements here:

  • Attaching deleteLater on QLocalSocket::disconnected fixes the previous per‑request socket leak.
  • CLI handlers now validate numeric inputs and check that AirPods are connected before attempting writes, which is a solid step up.

One remaining behavioral edge case (echoing the earlier review about IPC set‑* responses):

  • cli:set-adaptive-level returns "OK" whenever the level parses and areAirpodsConnected() is true, but AirPodsTrayApp::setAdaptiveNoiseLevel silently no‑ops when m_deviceInfo->adaptiveModeActive() is false or when the current level already matches. From a CLI/IPC client’s perspective, this makes it impossible to distinguish “applied” from “ignored”.

Consider having setAdaptiveNoiseLevel return a bool indicating whether a change was actually applied (and/or whether adaptiveModeActive() is true) and using that in the CLI handler to reply with "OK" vs "Error: adaptive mode inactive / no change applied". This would fully address the earlier concern about accurate IPC feedback for set‑operations.

🧹 Nitpick comments (3)
linux/main.cpp (3)

141-242: Status helpers are well‑structured; consider minor reuse/encoding tweaks

The three helpers (getStatusJson, getStatusText, getStatusWaybar) pull consistent data from DeviceInfo/Battery/EarDetection and handle nulls safely, which is good. Two optional improvements:

  • Explicitly wrap JSON output as QString::fromUtf8(doc.toJson(QJsonDocument::Compact)) to make UTF‑8 encoding explicit.
  • Extract the common battery/tooltip construction into a small helper to avoid duplication between JSON and Waybar outputs if fields evolve later.

550-555: Tray/menu reopen guards correctly prevent crashes when QML isn’t loaded

The new checks for topLevelWindows().isEmpty() and parent->rootObjects().isEmpty() before accessing windows/root objects should prevent the tray “Open” / “Settings” actions from crashing when the engine hasn’t loaded yet. As a tiny defensive improvement, you could add a Q_ASSERT(parent) or an early‑return if (!parent) return; in these slots to make the non‑null assumption explicit.

Also applies to: 566-569, 578-581


1099-1140: Fusion fallback and early CLI handling look good; one redundant removeServer call

Using Fusion when QT_STYLE_OVERRIDE=kvantum plus forcing QT_QUICK_CONTROLS_STYLE=Fusion should address missing‑theme crashes on misconfigured systems, and handling CLI commands before GUI startup is a clean separation. You now call QLocalServer::removeServer("app_server") both before isInstanceRunning/reopen (Lines 1128–1132) and again just before server.listen (Line 1160); keeping only one of these calls would simplify the flow without changing behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3b60cc and b3e6524.

📒 Files selected for processing (1)
  • linux/main.cpp (9 hunks)
🔇 Additional comments (1)
linux/main.cpp (1)

15-17: Includes for JSON, text streaming, and CLI are appropriate

Adding QJsonDocument, QJsonObject, QTextStream, and cli.h cleanly supports the new status/CLI features without unnecessary dependencies. No issues here.

Also applies to: 32-32

- 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)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
linux/main.cpp (3)

576-586: Early return after loadMainModule() may not open the Settings page.

When rootObjects().isEmpty() is true and the user clicks "Settings" in the tray menu, loadMainModule() is called but the method returns immediately without invoking the "reopen" method with "settings" argument. This means the app will show its default view rather than navigating to settings.

Consider deferring the reopen call after the module loads:

     void onOpenSettings()
     {
         if (parent->rootObjects().isEmpty()) {
             loadMainModule();
-            return;
+            // Wait for QML to load, then navigate to settings
+            QTimer::singleShot(100, this, [this]() {
+                if (!parent->rootObjects().isEmpty()) {
+                    QObject *rootObject = parent->rootObjects().first();
+                    if (rootObject) {
+                        QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "settings"));
+                    }
+                }
+            });
+            return;
         }
         QObject *rootObject = parent->rootObjects().first();

The same pattern applies to onOpenApp() at lines 564-574 if the default view isn't the "app" view.


1157-1160: Redundant QLocalServer::removeServer("app_server") call.

removeServer("app_server") is called at line 1129 during stale socket cleanup, and again at line 1160 before server.listen(). The second call is redundant since any stale socket was already removed.

     engine.addImageProvider("qrcode", new QRCodeImageProvider());
     trayApp->loadMainModule();

     QLocalServer server;
-    QLocalServer::removeServer("app_server");

     if (!server.listen("app_server"))

1193-1204: Hardcoded noise mode range may become stale if enum changes.

The validation mode > 3 hardcodes the maximum valid mode value. If NoiseControlMode enum is extended, this check could silently reject valid modes or accept invalid ones.

Consider using the enum's max value for consistency with setNoiseControlModeInt():

                 else if (msg.startsWith("cli:set-noise-mode:")) {
                     QString modeStr = msg.mid(QString("cli:set-noise-mode:").length());
                     bool ok;
                     int mode = modeStr.toInt(&ok);
-                    if (!ok || mode < 0 || mode > 3) {
+                    if (!ok || mode < 0 || mode > static_cast<int>(NoiseControlMode::Adaptive)) {
                         response = "Error: Invalid noise mode";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3e6524 and bc25f08.

📒 Files selected for processing (1)
  • linux/main.cpp (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
linux/main.cpp (1)
linux/cli.cpp (7)
  • noiseControlModeName (14-22)
  • noiseControlModeName (14-14)
  • out (132-132)
  • handleCLICommands (63-234)
  • handleCLICommands (63-63)
  • isInstanceRunning (33-39)
  • isInstanceRunning (33-33)
🔇 Additional comments (7)
linux/main.cpp (7)

15-17: LGTM!

The new includes are appropriate for JSON serialization (QJsonDocument, QJsonObject), text stream handling (QTextStream), and CLI integration (cli.h).

Also applies to: 32-32


141-180: LGTM!

The getStatusJson() implementation properly guards against null pointers for m_deviceInfo, Battery, and EarDetection objects. The JSON structure is well-organized and provides comprehensive status information.


182-208: LGTM!

The getStatusText() method provides a clean human-readable format with proper null checks and early return for disconnected state.


210-242: LGTM!

The getStatusWaybar() method provides well-structured Waybar output with appropriate fallbacks for null battery objects and includes useful tooltip information with charging indicators.


1099-1102: Fusion style applied unconditionally to Quick Controls.

Line 1102 sets QT_QUICK_CONTROLS_STYLE to "Fusion" regardless of whether the kvantum condition is met at line 1100. This means all users get Fusion style for Quick Controls, even if they have a complete theme setup.

Is this intentional? If the goal is only to fix kvantum-specific issues, you may want to wrap line 1102 inside the condition:

     if (qgetenv("QT_STYLE_OVERRIDE").toLower() == "kvantum")
+    {
         qputenv("QT_STYLE_OVERRIDE", "Fusion");
-    qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion");
+        qputenv("QT_QUICK_CONTROLS_STYLE", "Fusion");
+    }

Or if Fusion for Quick Controls is intentional for broader compatibility, the current code is fine.


1134-1139: LGTM!

The --debug and --hide flags are properly parsed and propagated. Setting quitOnLastWindowClosed(false) ensures the app continues running in the system tray when windows are closed.


1172-1174: Socket lifetime properly managed.

The deleteLater connection on disconnected signal ensures sockets are cleaned up after clients disconnect, addressing the memory leak concern from previous reviews.

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.

1 participant