Commit Graph
89 Commits
Author SHA1 Message Date
yusufipek 191eef8f8d Merge master into the Windows port
Three of the four collisions were the same one: master moved the directory
rule into paths.py while this branch was adding a Windows case to the copy in
config.py and the second copy in ggml.py. The case moves to paths.py with the
rest of it, and the directories test moves to tests/test_paths.py where master
put its neighbours.

The fourth is MeetingRecorder, which now starts a process per capture device.
Windows keeps its two lines there: no console window for either process, and
a stop that terminates rather than sending a signal the platform does not have.
2026-08-16 10:14:37 +03:00
Yusuf İpek ef016d326f Merge pull request #26 from benfirad/codex/macos-audio-device-safety
Harden macOS meeting audio capture
2026-08-16 10:00:29 +03:00
yusufipek a6fab18a22 Keep the meeting a quiet microphone gave us, and read both captures at once
A recording is never deleted for being disappointing. A microphone that
handed over nothing still leaves the right channel, which is everyone
else, and an hour of them is worth more than the empty channel costs; the
one thing the user cannot get back is the half that was there. So the
exact-zero check stays and stops throwing the file away: it says what the
microphone did, in a tray warning next to the recording being written up,
and the minutes are produced from what there is.

Reading the two capture pipes in turn from one thread put the failure it
was meant to fix back in a worse place. A microphone that stops delivering
leaves that read waiting forever, and the far side is not read either
until its pipe fills and its ffmpeg stops writing into it: the meeting
freezes, the levels sit still, and nothing is said for as long as nobody
looks. Each stream now has a reader of its own and a queue, so neither can
hold the other up, and a side that has said nothing for STALL_SECONDS ends
the recording the way a dead ffmpeg already did, out loud and keeping what
was captured.

Which system needs how many processes belongs in the table with everything
else that differs, so meeting() returns the list of commands it takes:
one on PulseAudio, one per device on a Mac. meeting_commands() is the
chooser again rather than a function with a Mac inside it, and the empty
entry in COREAUDIO is gone. The two AVFoundation targets are resolved
against a single device listing, which costs one ffmpeg run instead of two
and cannot see the indexes renumber between the microphone and the far
side.
2026-08-16 09:51:02 +03:00
Can Soykan Yılmaz 44cfb76652 Give a Mac an installer, an app bundle and a menu bar icon it can see
The macOS backends were already here: CoreAudio capture through ffmpeg,
pbcopy and CoreGraphics, Carbon hotkeys, the paths under ~/Library. What
was missing was everything that installs them, so install.sh hands over to
install-mac.sh on Darwin rather than growing a branch per line: the XDG
directories, the .desktop files and the shortcut registry mean nothing
there, and an application is a bundle rather than a path. The bundle
carries a copy of the interpreter, because macOS files the microphone and
Accessibility permissions against the process that asks, and a launcher
running Homebrew's python3 would have asked as python3 and shared the
grant with everything else on that interpreter. It is signed ad-hoc so a
reinstall is the same application rather than two more dialogs, and it
says so plainly when a brew upgrade has moved the tree it needs.
uninstall.sh and update.sh follow it.

The tray icon was invisible: QIcon.fromTheme wants a freedesktop icon
theme and hands back a null icon without one, which in a menu bar is the
whole interface gone. trayicon.py draws the three shapes as template
images, so they follow the menu bar into dark mode, and the bundle's icon
comes off the same glyph rather than a binary in the repository. Linux
keeps its own icons; these are used only where the theme has nothing.

paths.py is the fix that was never about a Mac. config.py imports ggml.py,
so ggml.py could not ask it where the data goes; each worked it out for
itself and only one of them knew about macOS. Settings went to ~/Library
while several gigabytes of models went to ~/.local/share, which is not a
place a Mac user looks and not a place uninstall.sh --purge would have
deleted from.

Ctrl+Space is the input-source switch there and Cmd+Space is Spotlight, so
the default is Ctrl+Option+Space, and hotkey.default_combo is the one
place that difference lives. The first paste asks for Accessibility with
kAXTrustedCheckOptionPrompt, which is what creates the row to switch on;
asking the other way opens a pane Dikte is not listed in. `dikte shortcut
status` asks the running instance, since the combination is held by that
process and by nothing else.

Local speech to text is the one piece a Mac builds by hand. whisper.cpp
publishes no macOS binary and Homebrew's is configured with
WHISPER_BUILD_SERVER=OFF, so it installs whisper-cli and not the server
Dikte talks to; program_path already takes a whisper-server off the PATH
or out of Settings, so the answer is the one a Linux distribution gets,
and the README carries the cmake line. CI grows a macOS job on 3.11 and
3.13, the only place the Carbon and CoreGraphics libraries have to be
there to be opened.

Written and tested on macOS 27.0 arm64. Two things are still unverified on
a Mac: the paste end to end, which waits on the Accessibility toggle, and
a meeting recording, which needs a loopback driver.
2026-08-15 15:56:27 +03:00
Seyit Gokce f7fa37b4b7 Keep the settings window inside the screen
Only two tabs scrolled, so the rest handed their full content height to
the window as a minimum. The API tab alone asked for 668px, which put the
window's minimum at 756px: taller than a laptop screen has room for once
the menu bar and dock have taken theirs, and unshrinkable, so Save sat off
the bottom edge. Every tab scrolls now, and the opening size is clamped to
what the screen actually offers.

The program path in the local model box was clipped to one line for a
related reason: it shares a form row with a button, and the row is
measured before its width is known. WrappedLabel re-measures against the
width it ends up with and claims that height back.
2026-08-14 17:21:37 -04:00
huseyin-emre-tigciandClaude Fable 5 6b86696648 Spell the Windows paths with forward slashes in the directories test
A backslash only separates on Windows, and this test also runs on the
Linux that checks the Windows half.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 18:09:24 +03:00
huseyin-emre-tigciandClaude Fable 5 3436e6b426 Add Windows support
Windows joins the three systems as its own entry in each table: DirectShow
through ffmpeg for capture, the Win32 clipboard and SendInput for the paste,
RegisterHotKey for the global shortcut, and the whisper.cpp and llama.cpp
Windows zips (the OpenBLAS whisper build, which transcribes about twice as
fast on a plain CPU). Settings go to APPDATA, data to LOCALAPPDATA, and
install.ps1 adds the Start Menu entry, the dikte command and an optional
autostart. Meetings are not supported yet: Windows offers nothing to record
the far side from.

Porting surfaced three fixes that were not Windows specific:

- A stopped or overlong download tried to delete its .part file while still
  holding it open, which Windows refuses. The unlinks now wait for the handle.
- The CLI transcribed files without handing the local servers their settings
  first, so a local provider failed with "no model downloaded" wherever the
  GUI had not run in the same process.
- The audio content types are pinned instead of asked of the registry, which
  answers differently machine to machine.

One fix is Windows specific but sits in shared code: shutdown() does not end
a blocked recv there, so stopping a request also closes the socket handle.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 16:53:21 +03:00
yusufipk 77b26e76be Stop the indicator from swallowing clicks in its corner
WA_TransparentForMouseEvents does nothing for a top-level window: Qt
takes the click and then drops it, so it still never reaches whatever is
underneath. Since the window stays mapped while idle, that turned its
corner of the screen into a dead zone for good. Qt::WindowTransparentForInput
is the one that leaves the window without an input region at all.

The dismissable one has to keep taking clicks, and the flag is read once
when the window is created, so it shrinks to a point while concealed
instead. Resizing keeps the surface alive, which is the whole reason
concealing does not simply hide it.
2026-08-10 15:59:35 +03:00
firat 2cd561da77 Harden macOS meeting audio capture 2026-08-09 00:33:06 +02:00
yusufipk f6754ebf43 Name the PipeWire the check is for, and keep it out of the tests
--raw arrived in 1.4, the same release that stopped reading a bare "-" as
raw on its own. The split is there rather than at 1.0: Ubuntu 24.10 and
anything else on 1.2 refuses the option too, and 1.4 onwards writes a
container around the stream without it.

Asking the installed binary meant the command tests ran pw-record --help
four times for real, on a machine whose answer decides what they see, which
the module docstring promises they never do. They pin the answer in setUp,
and the reading itself gets its own class, the empty help nobody covered
included.
2026-08-05 18:41:23 +03:00
yusufipk 49b940bcb9 Merge master into the pw-record raw check
recording_command became a table of sound systems while this branch was
open, so the pw-record command it patched now lives in _pulse_record. The
check moves there with it, and nothing else about it changes.
2026-08-05 18:40:54 +03:00
yusufipk 9a3284c504 Merge master: the audio goes up as mp3 2026-08-05 15:03:42 +03:00
Yusuf İpek 0d8411502b Merge pull request #20 from benfirad/codex/macos-model-builds
Select native macOS local-model builds
2026-08-05 15:03:20 +03:00
firat fa14ec90f4 Select native macOS model builds 2026-08-03 18:57:55 +02:00
firat ca6444c011 Preserve native macOS clipboard contents 2026-08-03 18:57:43 +02:00
Hakan Ekin 4e19d7279f Handle pw-record versions without raw support 2026-08-02 20:47:44 +03:00
yusufipk 395b52d685 Send the audio as mp3, and stop cutting a file that fits in one request
Whisper hears in thirty second windows and decides for itself where one cue
ends and the next begins. A chunk that starts in the middle of a sentence can
answer with one cue per window, twenty seconds of text at a time, for the whole
rest of the chunk: a twenty five minute recording was fine until 20:00, which
was where the second cut fell, and ran on in paragraphs from there. Sending the
same audio in one request instead of three gives cues of two and a half seconds
throughout.

The cuts were only ever there for the upload limit, and we were the ones
walking into it: ffmpeg opened a 24 MB m4a into 48 MB of uncompressed WAV, over
the 25 MB the APIs take, so the file had to be cut every ten minutes. As mp3 it
is 9 MB, and an hour of speech goes in one request. A server on this machine is
still handed the WAV, where nothing is uploaded and the encoder would only cost
quality. How long a chunk may be is now measured from the encoded file rather
than assumed from a bitrate.

Where a file still has to be cut, the chunks overlap by a whisper window and
stitch() drops the telling that was cut short, keeping the one that heard the
sentence whole. Meetings, which upload the WAV itself and so still cut every
ten minutes, get the same stitching.
2026-08-02 12:20:26 +03:00
yusufipk 822cf3b2a3 Merge master: the shortcut table, local models, and a Mac still in them
Four files disagreed, and all four the same way: master had turned things the
Mac branch wrote out by hand into one list to read from.

Shortcuts are the whole of it. master gave every binding a row in
hotkey.SHORTCUTS, so the Mac's DESKTOP_IDS is gone and CarbonHotkey reads the
desktop id off that row, which also gives the new cancel key a status line on a
Mac. Settings builds its four rows through master's _shortcut_row, and that one
now asks _install_buttons for Install and Remove, so macOS gets a combination
box and nothing to press, and everywhere else the button says the desktop's own
name. dikte.py starts the listener from the same table, on macOS whatever the
setting says: there is nothing installed for it to be a fallback to.

The rest is two imports and a paste list that lives in paste.Desktop now.
2026-08-02 09:36:22 +03:00
yusufipk 7a507c52eb Make the audio file tab remember, and its Stop stop
The two switches were written to disk by the Save button at the far end of
the window, so a file transcribed with timestamps and cleanup was
transcribed without either the next time. They belong to the run rather than
to the form: they go to disk as they are ticked now, and the folder the last
file came from goes with them.

Stop only set a flag that was looked at between chunks, and a file under ten
minutes is one chunk, so for most files it was looked at after the work it
was meant to stop had already finished. Nothing that blocks is reached by a
flag. The request is inside urlopen, ffmpeg is inside communicate, and a
whisper on this machine is a process of ours that would grind on to the end
of the chunk with nobody left to hand the answer to. So the socket is shut
down under the read, ffmpeg is killed, and a local server is stopped and
left for the next run to start again.

Shutting the socket down rather than closing it is the point: close() alone
leaves a thread already inside recv() waiting for bytes that are never
coming now. The connection is registered before it has a socket, so a stop
landing in the few lines between making a connection and blocking on it
refuses the connection rather than missing it and letting urllib quietly
open another.
2026-08-02 09:29:12 +03:00
yusufipk 97396d4734 Refuse a download that was published without a checksum
The hash was checked when there was one and skipped when there was not,
which is the wrong way round: everything fetched here is either run or
parsed by something written in C++, and the case with no hash is the one
where that matters most and says least.

Not hypothetical. GitHub publishes a digest for whisper.cpp v1.9.1 and for
llama.cpp today, and publishes none for whisper.cpp v1.8.0 and older, so
anything reaching for an older tag would have installed an executable
nobody checked.
2026-08-01 20:42:47 +03:00
yusufipk 785ceba02a Merge master: Groq, and cleanup on a subscription
Three of master's changes land on the same lines as this branch, so most of
this is picking the newer shape and putting the local half back into it.

cleanup.py arrived while this was being written and is the right place for
a fourth provider, so the Target refactor of api.cleanup goes away: llama.cpp
becomes a name in cleanup.PROVIDERS next to OpenRouter, Claude Code and
Codex, and worker.py, meeting.py and filetranscribe.py go back to master's.

The settings window keeps master's one row per provider, hidden with
setRowVisible, rather than the two wrapper widgets this branch had. So does
speech to text, which was doing the same thing its own way.

The transcriber table has no room for a provider with no key and no base
URL, so the local one is answered before the lookup rather than added to
it, and an unknown name now falls back to openai by name: the shipped
default is no longer a key of that table.

The minutes stay on OpenRouter, which master already decided by routing
only the transcript through cleanup.run, so meeting_provider goes.
2026-08-01 20:34:24 +03:00
yusufipk 593d307ecc Show a model name whole in the list it is chosen from
A combo box hands its own width to the list under it and elides whatever
does not fit, in the middle of the word: "ggml-org/Qwen....7B-Base-GGUF"
is not something anybody can choose between. The list is now as wide as its
longest row while the box stays the width the form gave it.
2026-08-01 20:16:38 +03:00
yusufipk f3573908d0 Count a model bigger than two gigabytes upwards
Qt's int is C++'s 32-bit one, so the progress of a 2.3 GB download arrived
at the other end of the signal as -1805209440, which is the real figure
less 2^32. The file was landing correctly; only the line under it was
wrong, reading "1.0 MB of -1805209440 B (-1%)".
2026-08-01 20:14:50 +03:00
yusufipk 634b46fcb0 Take the process with a start that goes wrong
_wait_ready can raise rather than return, and the process it was waiting on
is ours with nothing else holding a reference to it. Leaving it running
leaks a loaded model with nobody left to ask it anything, which is the
whole failure this class is careful about everywhere else. Found by two
stand-in servers still running after a test run.
2026-08-01 20:09:00 +03:00
yusufipk 2cfbbb2d99 Transcribe and clean up on this machine, without installing anything first
whisper-server is started on --inference-path /v1/audio/transcriptions,
which is exactly the path api.py already builds for the hosted providers,
and llama-server answers /chat/completions the way OpenRouter does. So the
local half is one more base URL rather than a second code path: worker.py,
filetranscribe.py and meeting.py are untouched, and dictation, subtitles
and meetings all work here on the first try.

Three findings worth naming, none of them in the new code:

whisper.cpp cuts segments on tokens, which in Turkish lands inside a word
about as often as between two. Pasted raw that gives "akraba değ\niller.";
in a subtitle it gives a cue reading "değ". Whisper marks the start of a
word with a leading space, so a piece that does not begin with one
continues the word above it.

A small model will repeat the transcript until the context is full, and
every one of those tokens is a second of somebody waiting: measured at 206
seconds, and 25 with a ceiling on the reply. Hosted models are left alone,
where the same runaway is rare and a ceiling would cut the minutes short.

A server outlives SIGTERM and SIGKILL holding its model in memory. Signals
are now turned into an event Qt delivers, since Qt blocks in C where a
Python handler never runs, and a pid file lets the next start sweep up
what a SIGKILL left behind.

The minutes keep their own provider rather than following cleanup's. The
two jobs are not the same size: a 4B model here will strip the filler words
out of a dictation and will not write up an hour long meeting.

The suite runs offline now: a test that reaches the network says so instead
of quietly going there.
2026-08-01 20:00:35 +03:00
yusufipk c0b892f53c Fetch whisper.cpp and llama.cpp, and keep a server alive on each
Two programs, one treatment: ask GitHub what was released, take the build
this machine can run, check it against the sha256 the index published, then
fetch a model from Hugging Face the same way and keep one server up on a
port of its own.

Neither list is written into the source. A model catalogue in here means a
release of Dikte for every model somebody else publishes, and a pinned
version means one for every whisper.cpp release; both are somebody else's
news. The lists are read at the moment the settings window is opened and
cached for a few hours, and a cache that has gone stale still answers when
the network does not.

Nothing that lands on disk is trusted for having arrived, which matters
more here than usual because half of what is fetched is a program Dikte
then runs.
2026-08-01 20:00:21 +03:00
yusufipk 034c2bec7e Clean the transcript up on the subscription you already have
Cleanup was the one step with only one place to run. Speech to text has three
providers behind a setting and the agent has three behind another, but the
model that drops the "eee"s out of a sentence was always a request to
OpenRouter, which meant a second key on a machine that already pays for a model
and already hands whole dictations to it as commands. Claude Code and Codex can
rewrite a sentence as easily as they can put something in your calendar, and
now they may.

cleanup.py is where that choice lives, so worker, the file transcriber and the
meeting all ask the same question rather than each building the same OpenRouter
request. What comes out of a CLI that failed is a CleanupError, which is an
ApiError, because to the chain a cleanup that failed is a cleanup that failed
however it was run: the raw transcript is still pasted and the reason still
shows in the corner, unchanged.

Neither CLI is given anything it does not need for the job. No tools, no MCP
servers, no session to resume, and the home directory rather than wherever the
agent is pointed, since a project's instructions have opinions about how text
should be written and none of them are about this transcript. The transcript
goes in fenced the same way the OpenRouter call fences it, because it is
material rather than an instruction however much of it reads like one. Claude
takes the cleanup rules as its whole system prompt; Codex has no system prompt
of its own, so they ride in front of the text, and its answer is read from the
file it writes on the way out rather than from a stdout that also carries a
header, its thinking and a token count.

The cost is seconds. OpenRouter answers in about one, a CLI in six or seven,
because each one opens a whole session to do it. That is the trade the box
says out loud, and the default has not moved: OpenRouter cleans up until you
say otherwise.

Codex's two lowest thinking levels now ask for "low". "minimal" was its bottom
rung until the newer models replaced it with "none", and each of them answers
the other's word with a 400, which the agent has been quietly hitting too.

In the settings window the model box belongs to whoever is chosen rather than
meaning three different things in turn, since an OpenRouter id and a Claude
alias do not belong in the same field, and under it is the same "found it or
not" line the agent tab has. dikte doctor asks about the program instead of the
key when a CLI does the cleaning, and the history records which model actually
did it.
2026-08-01 19:30:55 +03:00
yusufipk 3486892ec6 Give throwing a recording away a key of its own
Stopping a recording is the step there is no taking back. It is what sends the
audio off, and a moment later the sentence you did not mean to dictate is in
the clipboard and pasted into whatever window you were typing in. The tray menu
was the only way out, and by the time it is open the recording has already
gone. Discarding needed to be as quick as starting, which means a key.

Ctrl+Alt+Space rather than Escape: the combination the recording started with,
one modifier along, so the two are one gesture with a modifier between them.
Escape is the obvious choice and the wrong one, because it belongs to whichever
window has focus, and while you are talking something else usually has it. It
works on a dictation and on a command for the agent alike, since the one you
want to take back is the one that is running.

That made four global shortcuts, and four is the number at which three copies
of the same forty lines stop being a coincidence. The command line already had
a table of them; hotkey.py now holds it, and the settings window reads it too,
so a shortcut is a row rather than a combination box, two buttons, a status
label and three methods written out again. The window no longer takes the
commands to run as arguments either, because ipc.command_for already knows them
from the verb. Adding the fourth key is what this buys: one line in the table
and one call per row.

Both places a key can live are told about it. The listener catching the press
and the KDE shortcut arriving behind it go through the same echo guard the
toggle already had, so the two are one discard rather than two, and the tray
calls the inner method as it already did for the toggle. Ctrl+Space and
Ctrl+Alt+Space land on the same evdev key code, so there is a test for the
modifier matching that keeps them apart.

install.sh takes the second key as a second argument and refuses to register
two of the same. It hands both to `dikte shortcut install` rather than writing
kglobalshortcutsrc itself, which is what makes it work on GNOME, and what
finally puts the chosen key in the settings as well: the built-in listener
reads it from there, so a key written to only one of the two places was a key
that half worked.

Left empty in the settings window the discard key stays empty, unlike the
dictation shortcut which falls back to Ctrl+Space: a recording can always be
thrown away from the tray menu, so there is nothing to guarantee here.
2026-08-01 18:57:50 +03:00
yusufipkandMuzaffer Emre d04efe235a Put the three providers in a table, and test the request rather than the list
Reworked on top of master. The Groq request itself needed nothing beyond a key,
a base URL and a model id, but two providers fit in an if and an else where
three do not, and each one was written out four times: in transcribe_target(),
in the key rows of the settings window, in save and in load.

config.TRANSCRIBERS holds them now, one row each: the name the user sees and
the three settings that keep its key, its endpoint and its model. The variable
an empty key falls back to is the name of its setting, shouted. The provider
box, save, load, `dikte models --provider` and `dikte test-key` all read that
table, so a fourth provider is a row and a default rather than a branch in five
files. The key field, its Test button and its answer line are built once and
the three signals became one that carries which key was asked about.

The tests moved into the files of the modules they cover and check what a
provider actually changes: that the request goes to api.groq.com with the right
model and fields, that the glossary now reaches everything except OpenRouter,
that a Groq error says Groq, and that the settings window carries the key and
the model there and back. GROQ_API_KEY is cleared for the test run like the
other two, so a developer who has one does not send a test to the network.

Co-authored-by: Muzaffer Emre <[email protected]>
2026-08-01 22:09:41 +07:00
yusufipk 22edd2f247 Merge master into feature/groq-transcription 2026-08-01 22:09:01 +07:00
yusufipkandfirat 3bd8c1ad27 Add a Mac as the third system, beside Wayland and X11
Dikte already chose its clipboard programs once instead of in every
function; macOS joins that table rather than adding a branch to each one.
A Mac copies through pbcopy and presses Cmd+V straight into CoreGraphics,
records through AVFoundation, and asks Carbon for its global shortcuts.

The three tables are paste.Desktop, audio.Sound, and the pair of
predicates in hotkey.py. Each reads sys.platform inside the chooser, so a
test can stand somewhere else: 697 of the 737 tests now run on any
machine, the Wayland and X11 halves included, and the suite passes whole
whichever system it is run on.

Two things a Mac does not have needed saying rather than pretending:
there is no shortcut registry to install into, so Settings offers no
Install button and the listener is the mechanism instead of a fallback;
and nothing is offered as the sound the speakers are playing, so a
meeting needs BlackHole or Loopback and says so. The KDE-only labels
around them were already wrong on GNOME, and now name whichever desktop
is there.

Co-authored-by: firat <[email protected]>
2026-08-01 21:28:58 +07:00
yusufipk 5c429ab077 Ask parec for sound as it arrives, not two seconds of it at a time
Left alone, parec buffers about two seconds and then hands the lot over at
once. Measured against the chunk the level meter reads:

    parec, as it was          longest gap  2005 ms
    parec --latency-msec=64   longest gap    86 ms
    pw-record                 longest gap   129 ms

On a machine that has parec, which is every PulseAudio one and every PipeWire
one with the compatibility service, the waveform stood still and then jumped,
looking like a microphone that was not picking anything up. The recording
itself was fine, but the tail of one can be lost that way too: stop() ends the
process, and whatever is still held back goes with it.

The number asked for is the meter's own chunk, which is the unit the rest of
this file is measured in.
2026-08-01 20:52:43 +07:00
yusufipk 45a064e545 Tell a recorder that died from one that was asked to stop
The pump now says when the capture ended with nothing captured, which is worth
saying: parec refusing the device looks like silence otherwise. But stop() ends
it the same way, so a recording shorter than 0.3 s raised that alarm first and
"Recording too short" second, sending the user after a sound server that is
fine. It follows the flag MeetingRecorder already carries for this.

test_desktop_compat.py moves into the files for the modules it covers, so a
test is where the next person looking at that module will find it.
2026-08-01 20:34:15 +07:00
yusufipk 1544cca15c Install the GNOME shortcut this already knew how to install
install_shortcut(), remove_shortcut(), shortcut_status() and desktop_name()
were added and nothing called them: the settings window and the command line
both still went straight to the KDE ones. On GNOME, pressing Install ran
kwriteconfig6, which is not there, and the three interface strings written for
this never appeared. README said the settings window installs a GNOME or KDE
shortcut, and it installed neither.

Both callers go through the chooser now, and the status line names whichever
desktop answered.
2026-08-01 20:32:07 +07:00
yusufipk 0e6ec3aea9 Choose the clipboard's programs once, instead of in every function
Which pair of tools a session uses was being asked inside copy(), read(),
copy_bytes() and press(), four times, in a conditional each. A third desktop
means a third branch in each of them, and macOS is already open as a pull
request doing exactly that in a different style.

Wayland and X11 are two entries at the top of the file now, each naming its
programs and how to build a key press from them. The functions below take
whichever entry the session gets. Adding a desktop is an entry and a line in
desktop(), and it touches nothing else.

Two things fall out of it. The key press is checked against the same table on
both: xdotool was being handed the shortcut as typed, so ctrl+f13 was refused
on Wayland and passed through on X11. And a missing tool names the package to
install again, which the shared message had dropped.

The tests say the promises once and run them against both desktops, so a third
one inherits the list rather than needing a copy of it.
2026-08-01 20:32:07 +07:00
yusufipk 3d4c29c853 Merge branch 'test-suite' into feature/ubuntu-x11-gnome 2026-08-01 20:15:22 +07:00
yusufipk 80a4832818 Test what the application does, and run it on every pull request
570 tests over the standard library's unittest, so there is nothing to
install beyond the PyQt6 the application already needs. They reach neither
the network, the microphone, nor the real ~/.config/dikte: urllib is faked at
one function, the tools are faked at shutil.which, and every test is handed
its own config and data directory.

What they hold onto is what a change is most likely to move without meaning
to. The request each provider is sent, field by field. A settings window that
loads a value into a widget and writes it back, which is where a setting
added to one half and not the other is silently reset. The dictation chain
end to end: what is transcribed, what is pasted, what lands in the history,
and what happens to the audio afterwards. A config file written by an older
version. A meeting whose two channels heard the same sentence.

59 of them carry @linux_only, because they cover what Dikte is on this
desktop rather than what it does: PipeWire, wl-clipboard, ydotool, KDE's
shortcut file. The other 511 pass on any platform, and that line is worth
holding as the ports arrive.

CONTRIBUTING.md says how to run them, what support.py offers, and the three
things about this codebase that trip up a new test.
2026-08-01 20:06:47 +07:00
Muzaffer Emre dd73735617 Add Groq transcription support 2026-07-31 18:48:21 +03:00
Muzaffer Emre d64ed8968a Add GNOME X11 and PulseAudio support 2026-07-31 18:47:49 +03:00