Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .codex/skills/dev-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ draft pull request. Never merge without explicit user permission after completin
4. Fetch and prune `origin`. Confirm the branch contains commits not in `origin/main` and inspect the
complete proposed diff.
5. Require an active Plasma graphical session and run `make screenshots`. Confirm that
`screenshots/` contains exactly the four expected numbered PNG files. Inspect the regenerated
`screenshots/` contains exactly the five expected numbered PNG files. Inspect the regenerated
images and repository diff for accidental or private content. If tracked screenshots changed,
stage only `screenshots/` and commit them with `Refresh application screenshots`; never create an
empty commit. Confirm the working tree is clean afterward. Stop if capture or inspection fails.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
base-devel cmake ninja git clang gcovr reuse appstream desktop-file-utils
extra-cmake-modules qt6-base qt6-declarative qt6-multimedia
kirigami kconfig kcoreaddons kdbusaddons kdeclarative kglobalaccel ki18n
knotifications kstatusnotifieritem
knotifications kstatusnotifieritem kwallet libsodium

- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -92,7 +92,7 @@ jobs:
extra-cmake-modules qt6-base qt6-declarative qt6-multimedia
qt6-multimedia-gstreamer
kirigami kconfig kcoreaddons kdbusaddons kdeclarative kglobalaccel ki18n
knotifications kstatusnotifieritem
knotifications kstatusnotifieritem kwallet libsodium

- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
- Keep the UI minimal. Prefer native KDE and Qt patterns, preserve behavior already supplied by the
platform, and follow the KDE Human Interface Guidelines instead of introducing custom UI
conventions.
- Reuse established Qt and Kirigami components and styling across equivalent UI. Use
`Kirigami.PromptDialog` for confirmation dialogs with an explicit action verb and Cancel; reserve
specialized dialogs for tasks such as file selection or content viewing.

## Git workflow

Expand Down
40 changes: 31 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ include(FetchContent)
include(GNUInstallDirs)
find_package(ECM 6.0 REQUIRED NO_MODULE)
list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
find_package(PkgConfig REQUIRED)
pkg_check_modules(Sodium REQUIRED IMPORTED_TARGET libsodium)
find_package(Qt6 6.6 REQUIRED COMPONENTS Concurrent Core DBus Gui Multimedia Network Quick QuickControls2 Widgets)
find_package(KF6 6.20 REQUIRED COMPONENTS Config CoreAddons DBusAddons Declarative GlobalAccel I18n Kirigami Notifications StatusNotifierItem)
find_package(KF6 6.20 REQUIRED COMPONENTS Config CoreAddons DBusAddons Declarative GlobalAccel I18n Kirigami Notifications StatusNotifierItem Wallet)

option(KASTWORD_FETCH_WHISPER "Download the pinned whisper.cpp dependency" ON)
option(KASTWORD_FETCH_DEFAULT_MODEL "Download the legacy default English Whisper model" OFF)
Expand Down Expand Up @@ -56,6 +58,8 @@ qt_add_executable(kastword
src/main.cpp
src/AppController.cpp
src/AppController.h
src/DictationHistory.cpp
src/DictationHistory.h
src/AudioConversion.cpp
src/AudioConversion.h
src/AudioCapture.cpp
Expand All @@ -79,19 +83,22 @@ qt_add_executable(kastword
)

set_source_files_properties(src/qml/Main.qml PROPERTIES QT_RESOURCE_ALIAS Main.qml)
set_source_files_properties(src/qml/HistoryEntryCard.qml PROPERTIES QT_RESOURCE_ALIAS HistoryEntryCard.qml)
set_source_files_properties(src/qml/FooterContainer.qml PROPERTIES QT_RESOURCE_ALIAS FooterContainer.qml)
qt_add_qml_module(kastword
URI io.github.shape_machine.Kastword
VERSION 1.0
RESOURCE_PREFIX /qt/qml
NO_IMPORT_SCAN
QML_FILES src/qml/Main.qml
QML_FILES src/qml/Main.qml src/qml/HistoryEntryCard.qml src/qml/FooterContainer.qml
)

target_link_libraries(kastword PRIVATE
Qt6::Concurrent Qt6::Core Qt6::DBus Qt6::Gui Qt6::Widgets Qt6::Quick Qt6::QuickControls2
Qt6::Multimedia Qt6::Network
KF6::ConfigCore KF6::CoreAddons KF6::DBusAddons KF6::GlobalAccel KF6::I18n
KF6::I18nQml KF6::Kirigami KF6::Notifications KF6::StatusNotifierItem whisper)
target_link_libraries(kastword PRIVATE KF6::Wallet PkgConfig::Sodium)

function(kastword_enable_coverage target)
if(KASTWORD_ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
Expand All @@ -118,7 +125,7 @@ target_link_libraries(kastword_screenshot_generator PRIVATE
KF6::I18n KF6::I18nQml KF6::Kirigami)
qt_add_resources(kastword_screenshot_generator "screenshot-qml-resources"
PREFIX "/"
FILES src/qml/Main.qml)
FILES src/qml/Main.qml src/qml/HistoryEntryCard.qml src/qml/FooterContainer.qml)
kastword_enable_sanitizers(kastword_screenshot_generator)

if(KASTWORD_FETCH_DEFAULT_MODEL)
Expand All @@ -133,13 +140,14 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/io.github.shape_machine.Kastword.deskt
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 DESTINATION share/doc/kastword COMPONENT Kastword)
install(FILES docs/DEVELOPMENT.md docs/AUTOMATIC_PASTE.md
install(FILES docs/DEVELOPMENT.md docs/AUTOMATIC_PASTE.md docs/DICTATION_HISTORY.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
screenshots/02-history.png
screenshots/03-speech-models.png
screenshots/04-audio-input.png
screenshots/05-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)
Expand Down Expand Up @@ -198,10 +206,23 @@ if(BUILD_TESTING)
kastword_enable_sanitizers(kastword_model_manager_test)
add_test(NAME model-manager COMMAND kastword_model_manager_test)

add_executable(kastword_history_test
tests/DictationHistoryTest.cpp
src/DictationHistory.cpp
src/DictationHistory.h)
target_include_directories(kastword_history_test PRIVATE src)
target_link_libraries(kastword_history_test PRIVATE
Qt6::Concurrent Qt6::Core Qt6::Test KF6::I18n KF6::Wallet PkgConfig::Sodium)
kastword_enable_coverage(kastword_history_test)
kastword_enable_sanitizers(kastword_history_test)
add_test(NAME history COMMAND kastword_history_test)

add_executable(kastword_controller_test
tests/AppControllerTest.cpp
src/AppController.cpp
src/AppController.h
src/DictationHistory.cpp
src/DictationHistory.h
src/AudioCapture.cpp
src/AudioCapture.h
src/AudioConversion.cpp
Expand All @@ -223,7 +244,8 @@ if(BUILD_TESTING)
target_include_directories(kastword_controller_test PRIVATE src)
target_link_libraries(kastword_controller_test PRIVATE
Qt6::Concurrent Qt6::Core Qt6::DBus Qt6::Gui Qt6::Multimedia Qt6::Network Qt6::Test Qt6::Widgets
KF6::ConfigCore KF6::GlobalAccel KF6::I18n KF6::Notifications whisper)
KF6::ConfigCore KF6::GlobalAccel KF6::I18n KF6::Notifications KF6::Wallet
PkgConfig::Sodium whisper)
kastword_enable_coverage(kastword_controller_test)
kastword_enable_sanitizers(kastword_controller_test)
add_test(NAME controller COMMAND kastword_controller_test)
Expand All @@ -236,7 +258,7 @@ if(BUILD_TESTING)
Qt6::Qml Qt6::Quick Qt6::QuickTest KF6::I18n KF6::I18nQml KF6::Kirigami)
qt_add_resources(kastword_qml_test "qml-test-resources"
PREFIX "/"
FILES src/qml/Main.qml)
FILES src/qml/Main.qml src/qml/HistoryEntryCard.qml src/qml/FooterContainer.qml)
kastword_enable_sanitizers(kastword_qml_test)
add_dependencies(kastword_qml_test kastword_screenshot_generator)
add_test(NAME qml COMMAND kastword_qml_test)
Expand Down
16 changes: 10 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@ install-smoke: ensure-configured
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/docs/DICTATION_HISTORY.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/screenshots/02-history.png"; \
test -f "$$smoke_prefix/share/doc/kastword/screenshots/03-speech-models.png"; \
test -f "$$smoke_prefix/share/doc/kastword/screenshots/04-audio-input.png"; \
test -f "$$smoke_prefix/share/doc/kastword/screenshots/05-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"; \
Expand All @@ -110,10 +112,12 @@ uninstall:
"$(PREFIX)/share/doc/kastword/README.md" \
"$(PREFIX)/share/doc/kastword/docs/DEVELOPMENT.md" \
"$(PREFIX)/share/doc/kastword/docs/AUTOMATIC_PASTE.md" \
"$(PREFIX)/share/doc/kastword/docs/DICTATION_HISTORY.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/screenshots/02-history.png" \
"$(PREFIX)/share/doc/kastword/screenshots/03-speech-models.png" \
"$(PREFIX)/share/doc/kastword/screenshots/04-audio-input.png" \
"$(PREFIX)/share/doc/kastword/screenshots/05-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 \
Expand Down
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ text are never sent to a cloud transcription service.
<table>
<tr>
<td><img src="screenshots/01-offline-dictation.png" alt="Offline dictation view"></td>
<td><img src="screenshots/02-speech-models.png" alt="Speech models view"></td>
<td><img src="screenshots/02-history.png" alt="Encrypted dictation history view"></td>
</tr>
<tr>
<td><img src="screenshots/03-audio-input.png" alt="Audio input view"></td>
<td><img src="screenshots/04-settings.png" alt="Settings view"></td>
<td><img src="screenshots/03-speech-models.png" alt="Speech models view"></td>
<td><img src="screenshots/04-audio-input.png" alt="Audio input view"></td>
</tr>
<tr>
<td><img src="screenshots/05-settings.png" alt="Settings view"></td>
<td></td>
</tr>
</table>

Expand All @@ -42,8 +46,9 @@ text are never sent to a cloud transcription service.
paste on Plasma Wayland or X11.
- **Configurable limits and output:** choose the transcription language, recording limit, paste
shortcuts, and global keyboard shortcut.
- **No history or telemetry:** Kastword does not save recordings, retain a transcription history,
or include usage tracking.
- **Private by default:** Kastword never saves recordings. Optional dictation history is off by
default and, when enabled, is encrypted locally with its key protected by KDE Wallet. Kastword
includes no usage tracking.

## How dictation works

Expand All @@ -67,7 +72,9 @@ another input instead of silently switching devices.

- Microphone samples remain in process memory only until transcription completes. Recordings stop
at the configured duration limit—five minutes by default—or a 256 MiB raw-audio ceiling.
- Raw audio and transcription history are not written to disk.
- Raw audio is never written to disk. Transcription history is not written until you explicitly
enable it; enabled history is authenticated and encrypted locally. See [Encrypted dictation
history](docs/DICTATION_HISTORY.md).
- Transcribed text is placed on the desktop clipboard and primary selection. Clipboard managers,
target applications, desktop services, crash dumps, and the operating system may retain it
independently of Kastword.
Expand All @@ -83,6 +90,7 @@ another input instead of silently switching devices.

The clear-transcription action removes Kastword's retained text and clears matching current
clipboard selections. It cannot erase entries already retained by clipboard-manager history.
Deleting a saved history entry affects Kastword's encrypted store only.

## Platform status

Expand Down Expand Up @@ -122,6 +130,7 @@ contributor instructions are in the [development guide](docs/DEVELOPMENT.md).
## Help and project information

- [Automatic paste setup and troubleshooting](docs/AUTOMATIC_PASTE.md)
- [Encrypted dictation history and its security boundaries](docs/DICTATION_HISTORY.md)
- [Source installation and development](docs/DEVELOPMENT.md)
- [Report a bug or request a feature](https://github.com/Shape-Machine/Kastword/issues)
- [License](LICENSES/GPL-3.0-or-later.txt)
Expand Down
4 changes: 2 additions & 2 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ sudo pacman -S --needed \
qt6-base qt6-declarative qt6-multimedia \
extra-cmake-modules kirigami kdeclarative \
kconfig kcoreaddons kdbusaddons kglobalaccel ki18n \
knotifications kstatusnotifieritem
knotifications kstatusnotifieritem kwallet libsodium
```

Automatic paste requires a separate session helper. See [Automatic paste setup and
Expand Down Expand Up @@ -111,7 +111,7 @@ reports under `build-coverage/coverage/`. The default gates require at least 68%
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
`make screenshots` renders all five top-level views in a 760×700 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.
Expand Down
50 changes: 50 additions & 0 deletions docs/DICTATION_HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!--
SPDX-FileCopyrightText: 2026 Sri Rang
SPDX-License-Identifier: GPL-3.0-or-later
-->

# Encrypted dictation history

Kastword can keep completed transcriptions available across restarts. History is disabled by
default: no history file or encryption key is created until you explicitly enable it from the
History view.

## Storage and encryption

Enabled history is stored at `~/.local/share/kastword/history.enc` by default. Kastword encrypts and
authenticates the complete history before writing it. A separate random encryption key is stored as
a binary secret in KDE Wallet; it is never written beside the history file. The history directory
and file are restricted to the current user, and updates use atomic replacement.

If KDE Wallet is unavailable, remains locked, or rejects access, Kastword does not load or save
history and never falls back to plaintext. A damaged file or incorrect key also fails closed. You
can restore wallet access and retry, or disable history and request deletion of the unreadable file
and wallet key.

Only successful, non-empty transcription text and its timestamp are retained. Recorded audio,
model data, microphone information, target applications, and usage analytics are never included.

## Retention and deletion

The History view lets you set both a maximum age and maximum entry count. The defaults are 30 days
and 100 entries. Both limits are applied after each insertion and whenever history is loaded;
age-based expiry also continues automatically while Kastword remains running.

You can delete one entry or clear all entries. When disabling history, choose whether to keep the
encrypted data for later or delete both the file and its KDE Wallet key. Deletion prevents Kastword
from reading those records, but cannot guarantee forensic erasure from solid-state storage,
filesystem snapshots, swap, crash dumps, or backups.

If Kastword closes while KDE Wallet key deletion is still pending, it records that intent in the
private history directory and retries the deletion the next time it starts.

## Security boundaries

Encryption at rest protects the history file when the wallet key is unavailable. It does not
protect text from software controlling your unlocked desktop session. Copying or automatically
pasting text can expose it to clipboard managers, target applications, desktop services, the
operating system, and their own history or backup mechanisms. Review those systems before using
dictation for sensitive material.

Return to the [project overview](../README.md) or read the [automatic paste security
guide](AUTOMATIC_PASTE.md).
Loading