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
17 changes: 15 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,17 @@ install(TARGETS kastword RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT K
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.github.shape_machine.Kastword.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications COMPONENT Kastword)
install(FILES data/io.github.shape_machine.Kastword.metainfo.xml DESTINATION share/metainfo COMPONENT Kastword)
install(FILES README.md LICENSES/GPL-3.0-or-later.txt
DESTINATION share/doc/kastword COMPONENT Kastword)
install(FILES README.md DESTINATION share/doc/kastword COMPONENT Kastword)
install(FILES docs/DEVELOPMENT.md docs/AUTOMATIC_PASTE.md
DESTINATION share/doc/kastword/docs COMPONENT Kastword)
install(FILES
screenshots/01-offline-dictation.png
screenshots/02-speech-models.png
screenshots/03-audio-input.png
screenshots/04-settings.png
DESTINATION share/doc/kastword/screenshots COMPONENT Kastword)
install(FILES LICENSES/GPL-3.0-or-later.txt
DESTINATION share/doc/kastword/LICENSES COMPONENT Kastword)
if(KASTWORD_FETCH_DEFAULT_MODEL)
install(FILES "${KASTWORD_DEFAULT_MODEL}" DESTINATION share/kastword/models COMPONENT Kastword)
endif()
Expand Down Expand Up @@ -248,6 +257,10 @@ if(BUILD_TESTING)
kastword_enable_sanitizers(kastword_screenshot_platform_test)
add_test(NAME screenshot-platform COMMAND kastword_screenshot_platform_test)

add_test(NAME documentation-links
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/check-documentation-links.sh
${CMAKE_CURRENT_SOURCE_DIR})

add_test(NAME translation-catalog
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tests/check-translations.sh)
set_tests_properties(translation-catalog PROPERTIES
Expand Down
20 changes: 19 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ install-smoke: ensure-configured
"$$smoke_prefix/share/metainfo/io.github.shape_machine.Kastword.metainfo.xml"; \
test -f "$$smoke_prefix/share/locale/x-test/LC_MESSAGES/kastword.mo"; \
test ! -e "$$smoke_prefix/share/kastword/models/ggml-base.en.bin"; \
test -f "$$smoke_prefix/share/doc/kastword/README.md"; \
test -f "$$smoke_prefix/share/doc/kastword/docs/DEVELOPMENT.md"; \
test -f "$$smoke_prefix/share/doc/kastword/docs/AUTOMATIC_PASTE.md"; \
test -f "$$smoke_prefix/share/doc/kastword/screenshots/01-offline-dictation.png"; \
test -f "$$smoke_prefix/share/doc/kastword/screenshots/02-speech-models.png"; \
test -f "$$smoke_prefix/share/doc/kastword/screenshots/03-audio-input.png"; \
test -f "$$smoke_prefix/share/doc/kastword/screenshots/04-settings.png"; \
test -f "$$smoke_prefix/share/doc/kastword/LICENSES/GPL-3.0-or-later.txt"; \
./tools/check-documentation-links.sh "$$smoke_prefix/share/doc/kastword"; \
legacy_license="$$smoke_prefix/share/doc/kastword/GPL-3.0-or-later.txt"; \
cmake -E touch "$$legacy_license"; \
find "$$smoke_prefix" -type f -print > "$$installed_files"; \
user_model="$$smoke_prefix/share/kastword/models/ggml-base.en.bin"; \
cmake -E make_directory "$$(dirname "$$user_model")"; \
Expand All @@ -97,7 +108,14 @@ uninstall:
"$(PREFIX)/share/metainfo/io.github.shape_machine.Kastword.metainfo.xml" \
"$(PREFIX)/share/locale/x-test/LC_MESSAGES/kastword.mo" \
"$(PREFIX)/share/doc/kastword/README.md" \
"$(PREFIX)/share/doc/kastword/GPL-3.0-or-later.txt"
"$(PREFIX)/share/doc/kastword/docs/DEVELOPMENT.md" \
"$(PREFIX)/share/doc/kastword/docs/AUTOMATIC_PASTE.md" \
"$(PREFIX)/share/doc/kastword/screenshots/01-offline-dictation.png" \
"$(PREFIX)/share/doc/kastword/screenshots/02-speech-models.png" \
"$(PREFIX)/share/doc/kastword/screenshots/03-audio-input.png" \
"$(PREFIX)/share/doc/kastword/screenshots/04-settings.png" \
"$(PREFIX)/share/doc/kastword/GPL-3.0-or-later.txt" \
"$(PREFIX)/share/doc/kastword/LICENSES/GPL-3.0-or-later.txt"
@if command -v update-desktop-database >/dev/null 2>&1; then \
update-desktop-database "$(PREFIX)/share/applications"; \
fi
Expand Down
330 changes: 90 additions & 240 deletions README.md

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions docs/AUTOMATIC_PASTE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!--
SPDX-FileCopyrightText: 2026 Sri Rang
SPDX-License-Identifier: GPL-3.0-or-later
-->

# Automatic paste setup and troubleshooting

Kastword always places completed transcription on the clipboard. Automatic paste is optional,
disabled by default, and uses a separately installed helper to send your configured paste shortcut.
If no helper is available, copy and transcription still work for manual pasting.

## Install the session helper

On CachyOS or Arch Linux:

```sh
# Plasma Wayland
sudo pacman -S --needed ydotool

# Plasma X11
sudo pacman -S --needed xdotool
```

X11 paste uses `xdotool`. Kastword checks that the focused window is unchanged immediately before
sending keys, although focus can still change afterward.

Wayland does not expose an equivalent global focus check. Kastword uses `ydotool`, whose
`ydotoold` service creates a virtual keyboard through `/dev/uinput`. Focus can change during the
short delay before keys are sent.

## Enable automatic paste on Plasma Wayland

Enable and start the packaged user service:

```sh
systemctl --user enable --now ydotool.service
systemctl --user is-active ydotool.service
```

The second command must print `active`. Verify that the input device and daemon socket exist and
that the client can connect:

```sh
ls -l /dev/uinput "$XDG_RUNTIME_DIR/.ydotool_socket"
ydotool debug
```

Test actual input delivery by running this command, immediately focusing an editable field, and
waiting one second. `Kastword ydotool test` should appear:

```sh
sleep 1 && ydotool type 'Kastword ydotool test'
```

## Troubleshoot `ydotoold`

If the service is inactive or the socket is missing, inspect its status and current-boot log:

```sh
systemctl --user status ydotool.service
journalctl --user -u ydotool.service -b
```

Errors mentioning `/dev/uinput`, the daemon socket, or permission denied mean the helper is not
usable by the logged-in user. Check that the distribution package is current, restart the user
service, and log out and back in after changing device or group permissions.

Kastword never requests elevated permissions and does not start or configure `ydotoold`. Do not run
Kastword or `ydotool` with `sudo`.

## Security and privacy considerations

- Paste helpers are resolved from the inherited `PATH`; ensure every directory in `PATH` is trusted.
- The target application receives the transcription and may retain it.
- Clipboard managers may save clipboard history independently of Kastword.
- The clear-transcription action clears matching current clipboard selections, not entries already
retained by a clipboard manager.
- Automatic paste sends keystrokes to whichever field has focus at delivery time. Confirm focus
before dictating sensitive text, especially on Wayland.

Return to the [Kastword overview](../README.md) or see the [source installation and development
guide](DEVELOPMENT.md).
143 changes: 143 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<!--
SPDX-FileCopyrightText: 2026 Sri Rang
SPDX-License-Identifier: GPL-3.0-or-later
-->

# Source installation and development

Kastword is early alpha software and currently has no supported binary releases. This guide covers
source installation, packaging, architecture, and contributor tooling. See the [project
overview](../README.md) for product capabilities, privacy, and usage.

## Dependencies

On CachyOS or Arch Linux:

```sh
sudo pacman -S --needed \
base-devel cmake ninja git \
qt6-base qt6-declarative qt6-multimedia \
extra-cmake-modules kirigami kdeclarative \
kconfig kcoreaddons kdbusaddons kglobalaccel ki18n \
knotifications kstatusnotifieritem
```

Automatic paste requires a separate session helper. See [Automatic paste setup and
troubleshooting](AUTOMATIC_PASTE.md).

## Build and run

The first default build downloads one immutable, pinned dependency:

- `whisper.cpp` source at commit `a91dd3be72f70dd1b3cb6e252f35fa17b93f596c`

```sh
make
make run
```

`make run` shows the application window immediately for local development. A normal installed
launch starts Kastword in the system tray. No speech model is downloaded or packaged during a
normal build. The build needs network access only when the pinned Whisper.cpp source is not already
available.

## Install and uninstall

Install for the current user:

```sh
make install
```

This installs below `~/.local` and refreshes Plasma's application database. Set `PREFIX` explicitly
to install elsewhere. Launch Kastword from the application menu after installation.

Uninstall application-owned files:

```sh
make uninstall
```

Uninstalling preserves downloaded models and user settings.

The application ID is `io.github.shape_machine.Kastword`; the underscore follows D-Bus guidance
for the hyphen in the `Shape-Machine` organization name.

## Distribution builds

Packagers can provide Whisper.cpp without a build-time download:

```sh
cmake -S . -B build -G Ninja \
-DKASTWORD_FETCH_WHISPER=OFF
```

This requires a compatible system `whisper` CMake package. Kastword packages must not include a
speech model; users choose models after installation. The legacy
`KASTWORD_FETCH_DEFAULT_MODEL=ON` option remains available only for development compatibility.

## Architecture

```text
Global shortcut / tray
AppController state machine
├── AudioCapture ─── Qt Multimedia
├── WhisperEngine ── worker thread / whisper.cpp
├── ModelManager ─── explicit verified downloads + per-user storage
└── TextOutput ───── clipboard + optional paste helper
```

Inference runs away from the UI thread. Recording and transcription status is reported through the
application window, system tray, and desktop notifications without opening the main window during
a normal dictation.

## Development and validation

```sh
make test
make coverage BUILD_DIR=build-coverage CMAKE_ARGS=-DKASTWORD_FETCH_DEFAULT_MODEL=OFF
make screenshots
make lint
make install-smoke
make format
make validate
```

`make test` builds and runs the deterministic test suites. `make coverage` enables instrumentation,
enforces the repository's line and branch thresholds, and writes HTML, text, and Cobertura XML
reports under `build-coverage/coverage/`. The default gates require at least 68% line and 55% branch
coverage and can be raised with `COVERAGE_MIN_LINE` and `COVERAGE_MIN_BRANCH`. Coverage uses an
installed `gcovr`, or runs it through `uvx` when available.

`make screenshots` renders all four top-level views in a 760×520 logical window with deterministic
fake data and safely replaces the permanent PNG assets in `screenshots/`. It requires an active
graphical session so output inherits the current Plasma theme, icons, fonts, and display scale. It
does not access the microphone or network and preserves the previous complete set on failure.

`make lint` checks C++ formatting without changing files; `make format` applies it.
`make install-smoke` installs into a temporary prefix, launches through the installed desktop entry,
validates metadata, and verifies complete uninstall behavior. `make validate` runs the build, tests,
formatting check, REUSE validation, desktop metadata validation, and QML linting. Coverage and
license checks use installed `gcovr` and `reuse`, or temporary tools through `uvx`. Remaining checks
require `appstreamcli`, `desktop-file-validate`, and `clang-format`.

CI uses the same Make targets without downloading a model, installs packages from a dated Arch
Linux Archive snapshot, and runs tests under AddressSanitizer and UndefinedBehaviorSanitizer. Each
run publishes its coverage summary and uploads the HTML, text, and XML reports as the
`coverage-report` artifact.

## Kate

Open the repository directory and enable the Project, Build, and LSP Client plugins.
`.kateproject` provides Build, Run, Test, and Clean targets, while CMake generates
`build/compile_commands.json` for clangd.

## Roadmap

- broader Plasma Wayland/X11 and application compatibility testing
- broader audio-device and hot-plug compatibility testing
- reproducible distribution packages and signed binary releases

Roadmap items are intentions, not promised dates.
Binary file modified screenshots/01-offline-dictation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/02-speech-models.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/03-audio-input.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/04-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/ModelManagerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ void ModelManagerTest::invalidatesChangedManagedModel() {

QTRY_VERIFY(!manager.modelReady());
QTRY_VERIFY(!manager.busy());
QVERIFY(!QFileInfo::exists(path));
QTRY_VERIFY(!QFileInfo::exists(path));
}

void ModelManagerTest::invalidatesChangedLocalModel() {
Expand Down
74 changes: 74 additions & 0 deletions tools/check-documentation-links.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2026 Sri Rang
# SPDX-License-Identifier: GPL-3.0-or-later

set -eu

if [ "$#" -ne 1 ] || [ ! -d "$1" ]; then
echo "Usage: $0 DOCUMENTATION_ROOT" >&2
exit 2
fi

documentation_root=$(CDPATH= cd -- "$1" && pwd -P)
documents=$(mktemp)
targets=$(mktemp)
trap 'cmake -E rm -f "$documents" "$targets"' EXIT HUP INT TERM

if [ -f "$documentation_root/README.md" ]; then
printf '%s\n' "$documentation_root/README.md" > "$documents"
fi
if [ -d "$documentation_root/docs" ]; then
find "$documentation_root/docs" -type f -name '*.md' -print >> "$documents"
fi

while IFS= read -r document; do
awk -v document="$document" '
{ text = text " " $0 }
END {
markdown = text
html = text
while (match(markdown, /\]\([^)]*\)/)) {
print document "\t" substr(markdown, RSTART + 2, RLENGTH - 3)
markdown = substr(markdown, RSTART + RLENGTH)
}
while (match(html, /src="[^"]+"/)) {
print document "\t" substr(html, RSTART + 5, RLENGTH - 6)
html = substr(html, RSTART + RLENGTH)
}
}
' "$document"
done < "$documents" > "$targets"

while IFS=" " read -r document destination; do
case "$destination" in
''|'#'*|http://*|https://*|mailto:*) continue ;;
esac

relative_path=${destination%%#*}
fragment=
if [ "$relative_path" != "$destination" ]; then
fragment=${destination#*#}
fi
resolved_directory=$(CDPATH= cd -- "$(dirname -- "$document")" && pwd -P)
resolved_path="$resolved_directory/$relative_path"
if [ ! -f "$resolved_path" ]; then
echo "$document: broken relative link: $destination" >&2
exit 1
fi

if [ -n "$fragment" ] && ! awk -v wanted="$fragment" '
/^#{1,6}[[:space:]]/ {
heading = $0
sub(/^#+[[:space:]]+/, "", heading)
heading = tolower(heading)
gsub(/[^a-z0-9 _-]/, "", heading)
gsub(/[ _]+/, "-", heading)
if (heading == wanted)
found = 1
}
END { exit !found }
' "$resolved_path"; then
echo "$document: missing heading for link: $destination" >&2
exit 1
fi
done < "$targets"