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
13 changes: 8 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ This repository is intentionally kept minimal:
- `src/clipboardwriter.*`: clipboard integration, preferring KDE system clipboard support
- `src/audio/recordingnormalizer.*`: conversion to Whisper-ready mono `float32` at `16 kHz`
- `src/transcription/whispercpptranscriber.*`: in-process Whisper integration
- `src/transcription/transcriptionengine.*`: app-owned engine/session seam for backend selection and future runtime evolution
- `src/transcription/transcriptionengine.*`: app-owned engine/session seam for backend selection and future runtime evolution; the engine owns immutable runtime metadata such as backend capabilities
- `src/transcription/transcriptionworker.*`: worker object hosted on a dedicated `QThread`
- `src/transcription/transcriptiontypes.h`: normalized audio and transcription result value types
- `src/transcription/transcriptiontypes.h`: normalized audio, typed runtime error, and capability value types
- `src/config.*`: JSON config loading and defaults
- `src/app/*`: shared CLI/runtime command helpers used by the main entrypoint
- `src/control/*`: local daemon control transport, typed snapshots, and session/client APIs
Expand Down Expand Up @@ -120,6 +120,7 @@ Notes:
- Use `bash scripts/check-release-hygiene.sh` when touching publication-facing files such as `README.md`, licenses, `contrib/`, CI, or helper scripts
- Use `cmake --build "$BUILD_DIR" --target docs` when touching repo-owned public headers, Doxygen config, the Doxygen main page, or CI/docs wiring
- If install rules or licensing files change, confirm the temporary install contains the expected files under `share/licenses/mutterkey`
- If you add or change public methods in repo-owned headers, expect `cmake --build "$BUILD_DIR" --target docs` to fail until the new API is documented; treat that as part of the normal implementation loop, not follow-up polish

## Tooling Best Practices

Expand All @@ -136,6 +137,7 @@ Notes:
- Reconfigure the build directory after installing new tools so cached `find_program()` results are refreshed
- When validating inside a restricted sandbox, be ready to disable `ccache` with `CCACHE_DISABLE=1` if the cache location is read-only; that is an execution-environment issue, not a Mutterkey build failure
- Prefer fixing the code over weakening `.clang-tidy` or the Clazy check set; only relax tool config when the warning is clearly low-value for this repo
- If `clang-tidy` flags a new small enum for `performance-enum-size`, prefer an explicit narrow underlying type such as `std::uint8_t` instead of suppressing the warning
- In this Qt-heavy repo, treat `misc-include-cleaner` and `readability-redundant-access-specifiers` as low-value `clang-tidy` noise unless the underlying tool behavior improves; they conflict with Qt header-provider reality and `signals` / `slots` / `Q_SLOTS` sectioning more than they improve safety
- Prefer anonymous-namespace `Q_LOGGING_CATEGORY` for file-local logging categories; `Q_STATIC_LOGGING_CATEGORY` is not portable enough across the Qt versions this repo may build against
- Do not add broad Valgrind suppressions by default; only add narrow suppressions after reproducing stable third-party noise and keep them clearly scoped
Expand All @@ -156,8 +158,9 @@ Notes:
- Prefer narrow shared value types across subsystems; for example, consumers that only need captured audio should include `src/audio/recording.h`, not the full recorder class
- Keep JSON and other transport details at subsystem boundaries; prefer typed C++ snapshots/results once data crosses into app-owned control, tray, or service code
- Prefer dependency injection for tray-shell and control-surface code from the first implementation so headless Qt tests stay simple
- When preparing the transcription path for future runtime work, prefer app-owned engine/session seams and injected sessions over leaking concrete backend types into CLI, service, or worker orchestration
- When preparing the transcription path for future runtime work, prefer app-owned engine/session seams and injected sessions over leaking concrete backend types into CLI, service, or worker orchestration. Keep immutable capability reporting and backend metadata on the engine side, and keep the session side focused on mutable decode state, warmup, and transcription
- Prefer product-owned runtime interfaces, model/session separation, and deterministic backend selection before adding new inference backends or widening cross-platform support
- Keep backend-specific validation out of `src/config.*` when practical. Product config parsing should normalize and preserve user input, while backend support checks should live in the app-owned runtime layer near `src/transcription/*`
- Preserve the current product direction: embedded `whisper.cpp`, KDE-first, CLI/service-first

## C++ Core Guidelines Priorities
Expand Down Expand Up @@ -233,7 +236,7 @@ Typical model location:
- Treat `mutterkey-tray` as a shipped artifact once it is installed or validated in CI; keep install rules, README/setup notes, release checklist items, and workflow checks aligned with that status
- Verify with a fresh CMake build when the change affects compilation or linkage
- Run `ctest` when touching covered code in `src/config.*` or `src/audio/recordingnormalizer.*`, and extend the deterministic headless tests when practical
- When touching transcription orchestration or backend seams, prefer small headless tests with fake/injected sessions over model-dependent integration tests
- When touching transcription orchestration or backend seams, prefer small headless tests with fake/injected sessions or fake engines over model-dependent integration tests. Engine injection is the preferred seam for orchestration tests; direct session injection is still useful for narrow worker behavior
- When adding or fixing Qt GUI tests, make the `CTest` registration itself headless with `QT_QPA_PLATFORM=offscreen` so CI does not try to load `xcb`
- Prefer expanding tests around pure parsing, value normalization, and other environment-independent logic before adding KDE-session or device-heavy coverage
- Use `-DMUTTERKEY_ENABLE_ASAN=ON` and `-DMUTTERKEY_ENABLE_UBSAN=ON` for fast iteration on memory and UB bugs, and use the repo-owned Valgrind lane as the slower release-focused confirmation step
Expand All @@ -243,7 +246,7 @@ Typical model location:
- Prefer the `lint` target for a full pre-handoff analyzer pass, and use the individual analyzer targets when iterating on one class of warnings
- Run `bash scripts/run-valgrind.sh "$BUILD_DIR"` before handoff when the task is specifically about memory, ownership, lifetime, shutdown, or release hardening
- Run `bash scripts/check-release-hygiene.sh` before handoff when the task touches publication-facing files or repository metadata
- If `QT_QPA_PLATFORM=offscreen "$BUILD_DIR/mutterkey" diagnose 1` fails in a headless environment without useful output, note that limitation explicitly rather than assuming a docs-only or packaging-only change regressed runtime behavior
- If `QT_QPA_PLATFORM=offscreen "$BUILD_DIR/mutterkey" diagnose 1` fails in a headless environment after model loading or during KDE/session-dependent startup, note that limitation explicitly rather than assuming the runtime seam or docs-only change regressed behavior
- Do not leave generated artifacts in the repository tree at the end of the task
- Do not assume every workspace copy is an initialized git repository; if `git` commands fail, continue with file-based validation and mention the limitation in the final response

Expand Down
16 changes: 16 additions & 0 deletions RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ bash scripts/check-release-hygiene.sh

## Build And Test

- For the automated pre-install portion of this section, you can run:

```bash
bash scripts/run-release-checklist.sh
```

- Pass extra CMake configure arguments after `--` when you want to exercise an
accelerated release build. For example:

```bash
bash scripts/run-release-checklist.sh -- -DMUTTERKEY_ENABLE_WHISPER_CUDA=ON
```

- The script intentionally stops before install validation and still prints the
remaining manual review items that need human judgment.

- Configure a fresh out-of-tree build:

```bash
Expand Down
32 changes: 24 additions & 8 deletions docs/mainpage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
`KDE Plasma`.

This documentation is generated from the repo-owned C++ headers under `src/`.
It focuses on the application's core interfaces, ownership boundaries, and the
main daemon-mode workflow.
It focuses on the application's ownership boundaries, runtime contracts, and
daemon-oriented workflow rather than end-user setup.

Current behavior:

Expand All @@ -17,23 +17,39 @@ Current behavior:
- copies the resulting text to the clipboard
- expects you to paste the text yourself with `Ctrl+V`

Current direction:
Current runtime shape:

- KDE-first
- local-only transcription
- CLI/service-first operation
- tray-shell work has started, but the daemon remains the product core
- `TranscriptionEngine` is the immutable runtime/provider boundary
- `TranscriptionSession` is the mutable per-session decode boundary
- `BackendCapabilities` reports engine-owned runtime metadata used for
diagnostics and orchestration
- `RuntimeError` and `RuntimeErrorCode` provide typed runtime failures
- `TranscriptionWorker` hosts transcription on a dedicated `QThread` and
creates live sessions lazily on that worker thread
- config parsing under `src/config.*` stays product-shaped and permissive, while
backend-specific support checks live in the runtime layer

Core API surface covered here:

- `HotkeyManager` registers the global push-to-talk shortcut through KDE.
- `AudioRecorder` captures microphone audio while the shortcut is held.
- `RecordingNormalizer` converts captured audio to Whisper-ready mono `float32`
samples at `16 kHz`.
- `TranscriptionEngine` and `TranscriptionSession` define the app-owned runtime
seam.
- `WhisperCppTranscriber` performs in-process transcription through vendored
`whisper.cpp`.
- `ClipboardWriter` copies the resulting text to the clipboard.
- `MutterkeyService` coordinates those pieces on the main thread plus a
dedicated transcription worker thread.

For build, runtime, and service setup use the repository `README.md`.
Current product direction:

- KDE-first
- local-only transcription
- CLI/service-first operation
- tray-shell work exists, but the daemon remains the product core
- `whisper.cpp` is still the only supported backend implementation

For build, runtime, release, and service setup use the repository `README.md`
and `RELEASE_CHECKLIST.md`.
186 changes: 186 additions & 0 deletions scripts/run-release-checklist.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env bash

set -euo pipefail

usage() {
cat <<'EOF'
Usage: bash scripts/run-release-checklist.sh [--build-dir DIR] [--skip-diagnose] [-- <cmake-configure-args...>]

Runs the automated portion of RELEASE_CHECKLIST.md up to, but not including,
install validation.

Options:
--build-dir DIR Use an existing or chosen build directory instead of a new tmpdir.
--skip-diagnose Skip the headless `mutterkey diagnose 1` step.
--help Show this help text.

Examples:
bash scripts/run-release-checklist.sh
bash scripts/run-release-checklist.sh --build-dir /tmp/mutterkey-build-abc123
bash scripts/run-release-checklist.sh -- -DMUTTERKEY_ENABLE_WHISPER_VULKAN=ON
EOF
}

die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}

note() {
printf '==> %s\n' "$*"
}

run_cmd() {
note "$*"
"$@"
}

run_build_target() {
shift

local output
if output="$("$@" 2>&1)"; then
printf '%s\n' "$output"
return 0
fi

printf '%s\n' "$output" >&2
if grep -Fq 'ccache: error: Read-only file system' <<<"$output"; then
note "Retrying with CCACHE_DISABLE=1 because ccache is read-only in this environment"
CCACHE_DISABLE=1 "$@"
return 0
fi

die "Build command failed"
}

assert_file_exists() {
local path="$1"
[[ -e "$path" ]] || die "Required file is missing: $path"
}

contains_vendored_rule() {
grep -Fqx 'third_party/whisper.cpp/** linguist-vendored' .gitattributes
}

scan_for_model_binaries() {
if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git ls-files | grep -E '(^|/)(ggml-.*\.bin|.*\.(bin|gguf))$' | grep -Ev '^third_party/whisper\.cpp/' || true
return
fi

find . \
-path './.git' -prune -o \
-path './third_party/whisper.cpp' -prune -o \
-path './build' -prune -o \
-path './build-*' -prune -o \
-path './cmake-build-*' -prune -o \
\( -name '*.bin' -o -name '*.gguf' \) -print
}

build_dir=""
skip_diagnose=0
declare -a extra_cmake_args=()

while [[ $# -gt 0 ]]; do
case "$1" in
--build-dir)
[[ $# -ge 2 ]] || die "--build-dir requires a value"
build_dir="$2"
shift 2
;;
--skip-diagnose)
skip_diagnose=1
shift
;;
--help)
usage
exit 0
;;
--)
shift
extra_cmake_args=("$@")
break
;;
*)
die "Unknown argument: $1"
;;
esac
done

if [[ -z "$build_dir" ]]; then
build_dir="$(mktemp -d /tmp/mutterkey-build-XXXXXX)"
fi

declare -a generator_args=()
if command -v ninja >/dev/null 2>&1 || command -v ninja-build >/dev/null 2>&1; then
generator_args=(-G Ninja)
fi

note "Release checklist build directory: $build_dir"

assert_file_exists LICENSE
assert_file_exists THIRD_PARTY_NOTICES.md
assert_file_exists third_party/whisper.cpp.UPSTREAM.md
assert_file_exists .gitattributes

contains_vendored_rule || die "Missing vendored linguist rule for third_party/whisper.cpp in .gitattributes"

tracked_binaries="$(scan_for_model_binaries)"
if [[ -n "$tracked_binaries" ]]; then
die "Unexpected model/binary artifacts found:\n$tracked_binaries"
fi

run_cmd bash scripts/check-release-hygiene.sh

run_cmd cmake -S . -B "$build_dir" "${generator_args[@]}" -DCMAKE_BUILD_TYPE=Debug -DGGML_CCACHE=OFF "${extra_cmake_args[@]}"
run_build_target "$build_dir" cmake --build "$build_dir" -j"$(nproc)"
run_cmd ctest --test-dir "$build_dir" --output-on-failure
run_cmd bash scripts/run-valgrind.sh "$build_dir"
if command -v clang-tidy >/dev/null 2>&1; then
run_build_target "$build_dir" cmake --build "$build_dir" --target clang-tidy
else
note "Skipping clang-tidy because clang-tidy is not installed"
fi

if command -v clazy-standalone >/dev/null 2>&1; then
run_build_target "$build_dir" cmake --build "$build_dir" --target clazy
else
note "Skipping clazy because clazy-standalone is not installed"
fi

if command -v doxygen >/dev/null 2>&1; then
run_build_target "$build_dir" cmake --build "$build_dir" --target docs
else
note "Skipping docs because doxygen is not installed"
fi
run_cmd env QT_QPA_PLATFORM=offscreen "$build_dir/mutterkey" --help

note "Running tray-shell smoke check"
set +e
timeout 2s env QT_QPA_PLATFORM=offscreen "$build_dir/mutterkey-tray"
tray_status=$?
set -e
if [[ $tray_status -ne 0 && $tray_status -ne 124 ]]; then
die "Headless tray-shell smoke check failed with exit code $tray_status"
fi

if [[ $skip_diagnose -eq 0 ]]; then
run_cmd env QT_QPA_PLATFORM=offscreen "$build_dir/mutterkey" diagnose 1
else
note "Skipping headless diagnose step by request"
fi

cat <<EOF

Automated pre-install release checks passed.

Build directory:
$build_dir

Manual review items still remaining from RELEASE_CHECKLIST.md:
- Review LICENSE, THIRD_PARTY_NOTICES.md, and third_party/whisper.cpp.UPSTREAM.md for release accuracy.
- Review README.md, docs/mainpage.md, contrib/mutterkey.service, and contrib/org.mutterkey.mutterkey.desktop for release consistency.
- Confirm runtime backend behavior on representative target hardware when shipping accelerated inference.
- Perform the Install Validation section next.
EOF
16 changes: 9 additions & 7 deletions src/app/applicationcommands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ void configureLogging(const QString &level)

int runDaemon(QGuiApplication &app, const AppConfig &config, const QString &configPath)
{
MutterkeyService service(config, QGuiApplication::clipboard());
const std::shared_ptr<const TranscriptionEngine> transcriptionEngine = createTranscriptionEngine(config.transcriber);
MutterkeyService service(config, transcriptionEngine, QGuiApplication::clipboard());
DaemonControlServer controlServer(configPath, config, &service);
QObject::connect(&app, &QCoreApplication::aboutToQuit, &service, &MutterkeyService::stop);
QObject::connect(&app, &QCoreApplication::aboutToQuit, &controlServer, &DaemonControlServer::stop);
Expand All @@ -59,14 +60,14 @@ int runDaemon(QGuiApplication &app, const AppConfig &config, const QString &conf
int runOnce(QGuiApplication &app, const AppConfig &config, double seconds)
{
AudioRecorder recorder(config.audio);
const std::unique_ptr<TranscriptionEngine> transcriptionEngine = createTranscriptionEngine(config.transcriber);
const std::shared_ptr<const TranscriptionEngine> transcriptionEngine = createTranscriptionEngine(config.transcriber);
std::unique_ptr<TranscriptionSession> transcriber = transcriptionEngine->createSession();
ClipboardWriter clipboardWriter(QGuiApplication::clipboard());

if (config.transcriber.warmupOnStart) {
QString warmupError;
if (!transcriber->warmup(&warmupError)) {
qCCritical(appLog) << "Failed to warm up transcriber:" << warmupError;
RuntimeError runtimeError;
if (!transcriber->warmup(&runtimeError)) {
qCCritical(appLog) << "Failed to warm up transcriber:" << runtimeError.message;
return 1;
}
}
Expand All @@ -90,7 +91,7 @@ int runOnce(QGuiApplication &app, const AppConfig &config, double seconds)

const TranscriptionResult result = transcriber->transcribe(recording);
if (!result.success) {
qCCritical(appLog) << "One-shot transcription failed:" << result.error;
qCCritical(appLog) << "One-shot transcription failed:" << result.error.message;
QGuiApplication::exit(1);
return;
}
Expand All @@ -113,7 +114,8 @@ int runOnce(QGuiApplication &app, const AppConfig &config, double seconds)

int runDiagnose(QGuiApplication &app, const AppConfig &config, double seconds, bool invokeShortcut)
{
MutterkeyService service(config, QGuiApplication::clipboard());
const std::shared_ptr<const TranscriptionEngine> transcriptionEngine = createTranscriptionEngine(config.transcriber);
MutterkeyService service(config, transcriptionEngine, QGuiApplication::clipboard());
QObject::connect(&app, &QCoreApplication::aboutToQuit, &service, &MutterkeyService::stop);

QString errorMessage;
Expand Down
Loading
Loading