Back to workbench
/usr/bin/kde-cli-wrappers

KDE Plasma CLI Job Trackers

Native KDE Plasma notification integrations for heavy CLI tools like FFmpeg and Whisper AI.

I love using the terminal for heavy media tasks, but I hate having to keep a terminal window open and focused just to monitor a 20-minute FFmpeg transcode or a Whisper AI transcription. I wanted these background operations to feel like native desktop tasks integrated directly into my Wayland/Plasma 6 environment.

To solve this, I wrote a suite of C++ wrappers using Qt6 and KDE Frameworks. These tools execute standard CLI commands (ffmpeg, whisper-cli) as background processes and pipe their output directly into KDE's KUiServerV2JobTracker.

The DaVinci Resolve Problem

One of the primary drivers for this project was video editing on Linux. DaVinci Resolve on Linux is notoriously strict about codecs, often refusing to read standard H.264/AAC .mp4 files or variable framerate screen recordings.

I built ff-resolve-kde to automate the fix. You pass it a video, and it silently transcodes it to an editing-friendly format (DNxHR SQ, YUV 4:2:2 10-bit with PCM audio) while showing a native progress bar on the desktop.

Intercepting FFmpeg Telemetry

The core engineering challenge across all these wrappers was progress calculation. CLI tools stream raw text to standard output. By running FFmpeg with the -progress pipe:1 flag, I configured my wrapper to intercept the data stream, parse the key=value pairs in real-time, and calculate the completion percentage based on the initial ffprobe duration.

Dolphin Context Menu Integration

To make the tools completely frictionless, I integrated them directly into KDE's file manager (Dolphin) using ServiceMenus. By right-clicking any media file, I can trigger FFmpeg encodes or Whisper AI transcriptions directly from the context menu. The custom .desktop files ensure that the resulting native Plasma notifications display the correct application icons and branding.

// Intercepting FFmpeg's stdout to update the KDE Plasma progress bar void readProgress() { while (m_process->canReadLine()) { const QString line = QString::fromUtf8(m_process->readLine()).trimmed(); const int separator = line.indexOf('='); if (separator < 0) continue; const QString key = line.left(separator); const QString value = line.mid(separator + 1); // We only care about the current timestamp being processed if (key != "out_time_us") continue; bool ok = false; const double microseconds = value.toDouble(&ok); if (!ok || m_duration <= 0) continue; // Convert microseconds to seconds, then divide by total media duration int percent = static_cast<int>((microseconds / 1000000.0) / m_duration * 100.0); // Clamp the value between 0 and 100 to prevent UI crashes setPercent(static_cast<unsigned long>(qBound(0, percent, 100))); } }