Commit Graph
55 Commits
Author SHA1 Message Date
yusufipek eda1398a2b Call the OpenRouter subtitle model the audio file model
Timestamps are a file transcription option, so the model box is named
after the file rather than the format it ends up in.
2026-09-02 12:43:07 +03:00
yusufipek 6e307bd8d0 Let OpenRouter subtitles use a chosen model instead of whisper-1
A timestamped run on OpenRouter always asked openai/whisper-1 for the
segments, whatever model was picked for plain transcription. Not every
model there returns segment times, so the one to use is now its own
setting, openrouter_subtitle_model, shown in the speech-to-text box only
when OpenRouter is the provider. Empty keeps the old whisper-1 fallback.

Target carries the choice as subtitle_model and timestamp_model() reads
it; the other providers are unchanged.
2026-09-02 12:41:43 +03:00
yusufipek 310ef8d7cf Dikte 1.1.0 2026-08-27 16:38:59 +03:00
yusufipek c1554c092e Drop three explanatory texts from the settings window
The agent and meeting tabs opened with an intro paragraph, and the
cleanup provider box showed a long tooltip comparing the providers.
None of them earned the space: the intro texts were unclear and the
tooltip restated what picking a provider already shows. The orphaned
Turkish translations go with them.
2026-08-27 16:35:01 +03:00
yusufipek 22a73bd939 Merge master into the OpenCode Go branch
Master grew Google AI Studio and Antigravity as providers, a doctor that
names each provider's own key, and settings that fetch every hosted
model list as the window opens. OpenCode Go is folded into each: its
name joins SERVICES and the doctor's key table, its key row sits beside
Google's, its Fetch button follows the per-provider pattern Google's
uses, and _load_hosted_models fetches its catalog at open when a key is
on file, filling the cleanup and agent boxes alike.
2026-08-27 16:27:57 +03:00
yusufipek c503d12e62 Leave both halves of libxkbcommon to the system
Qt's xcb plugin links libxkbcommon and libxkbcommon-x11, and the x11 half
hands keymap objects to the core half to free, so the pair must come from
one build. The build machine has only the core half installed, so
PyInstaller bundled that one alone and the other kept loading from the
user's system; an Ubuntu 22.04 core freeing what a current x11 half
allocated is the segfault of issue #57, which a Turkish layout happened
to move to startup. Every desktop that can show a window carries both
libraries from one build, so the bundle now carries neither.

Fixes #57
2026-08-27 16:25:59 +03:00
yusufipekandClaude Fable 5 d331ccb877 Fetch every model list at open, without being asked
Codex's list already arrived on its own, because reading a cache on disk
costs nothing. The other three lists were behind a Fetch button, which
meant the built-in ones aged in front of anyone who never pressed it. Now
OpenRouter's and Google's lists are fetched when the settings window
opens, and Antigravity's comes off `agy models`, which prints one
id-tab-name line per model and answers over the network in a couple of
seconds; all three run off the interface thread.

Nobody asked, so nothing is reported: a failure changes nothing on
screen, the built-in lists stay, and the Fetch buttons remain both the
retry and the place an error is worth explaining. A provider whose key
has not been given is not called at all, so opening Settings is not by
itself a request to two vendors.

Also drops an import the merge had left in twice.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-27 16:12:18 +03:00
yusufipek fa3d72f5a7 Fetch OpenCode Go's catalog as the settings window opens
Codex already refreshes its boxes from the source at open, so the
built-in list is never the whole truth for longer than a window takes to
build. OpenCode Go now gets the same courtesy: one request to /models on
a background thread, both of its boxes refilled with the picked model
kept, skipped entirely when there is no key to send.
2026-08-27 16:04:30 +03:00
yusufipek 1ff18e46ab Call OpenRouter the quickest again
Nothing was measured that put OpenCode Go beside it, so the tooltip
claims only what is known: OpenRouter is the quickest, and OpenCode Go
merely needs nothing installed either. The translation follows word for
word.
2026-08-27 16:04:09 +03:00
yusufipek 664a6f53a4 Offer only models the OpenCode Go catalog actually lists
ox-alpha-free is not in the /models answer, and the comment claimed the
chat endpoint hides Grok, Luna, MiniMax and Qwen when its own catalog
lists them. The built-in list is a starting set; the Fetch button asks
the endpoint for the full catalog of the day.
2026-08-27 15:55:32 +03:00
yusufipek 0ecd4d251e Translate the cleanup provider tooltip again
The tooltip gained OpenCode Go but its translation was added without the
llama.cpp sentence the tooltip actually carries, so a Turkish window
showed the English text. The full entry now matches the tooltip word for
word, and the two shorter variants nothing looks up any more are gone.
2026-08-27 15:54:54 +03:00
yusufipek 65055eeb56 Give OpenCode Go a reachable Fetch model list button
The one button lived in the OpenRouter model row, which leaves the
screen whenever another provider is chosen, so the OpenCode fetch path
could never be clicked. The OpenCode row now carries its own button into
the same handler, the row as a whole is what hides, and the fetched list
lands in the box it belongs to without touching the meeting box.
2026-08-27 15:53:44 +03:00
yusufipek 23243c5171 Trim the README and polish the display selection
The settings-storage paragraph goes back to the one sentence it was. The screen list now shows native resolutions rather than the scaled ones, the Turkish tab is named Ekran, and the repositioning comment says what a named screen changes.
2026-08-27 15:52:02 +03:00
yusufipek ccc8ba596e Merge master into the OpenCode Go branch
Codex now asks itself for its model list, doctor grew ready flags and a
per-provider line, and the Groq key joined the masked ones; the OpenCode
additions are folded into each. The no-CLI cleanup test follows the
fake_run to fake_cli rename.
2026-08-27 15:51:49 +03:00
yusufipek 1df2d03056 Merge master into the Gemini and Antigravity branch
Both sides rewrote the doctor: master rebuilt it around ready flags so a
fully local setup stops reading as broken, while this branch taught it
that Google has a key of its own and that the local model has neither a
key nor a program. Kept master's structure and folded the branch in: the
two hosted providers answer for their keys, a CLI for its program, and
the JSON keeps both the provider-aware "key" and master's "ready".

SECRET_KEYS gained groq on master and gemini here; the resolution keeps
all four. _conclude was also changed by both: master stopped matching
stderr wording and asks _API_TROUBLE instead, this branch renamed its
last parameter to the provider's short name so a session is stored under
the name it is read back by; the rename now rides on master's body.
codex_models() and the settings loaders landed beside the new Gemini
ones, so both stay. The new cleanup tests called the CLI fake by its old
name, fake_run, which master had renamed to fake_cli.
2026-08-27 15:50:48 +03:00
yusufipek f6c6122253 Merge master into the transcript queue branch 2026-08-27 15:35:08 +03:00
yusufipek 17a55e1efc Merge master into the reliability branch
The paste block was rewritten by both sides: master taught press() to
put the remembered application back in front (focus), this branch moved
the history write ahead of the paste and stopped restoring the old
clipboard over a transcript the key press refused. Kept this branch's
order and error handling, and handed press() the focus it now takes.
2026-08-27 15:23:41 +03:00
yusufipek a3bb5ed29b Keep the rebuild away from a window a thread is still writing to
deleteLater destroyed a window whose model boxes a daemon thread was
still reporting into: a running download died with a RuntimeError, the
.part stayed on disk and the UI said nothing. Dropping the reference is
how the ordinary close path lets a window go, and the closures a thread
holds keep the object alive until it is done, so the rebuild now does
the same. While the transcriber or either model box is working the
rebuild is skipped altogether; _built_language keeps the old language,
so the next quiet save asks for it by itself.

The new window is also placed and turned to the old tab before it is
shown, so it no longer comes up at the default size and jumps. And
tests/test_ui.py now says that a save with a changed language emits
language_changed, and one without does not.
2026-08-27 15:13:54 +03:00
yusufipek e681accc90 Merge master into the live language switch branch 2026-08-27 15:10:57 +03:00
yusufipek be09cc3ac6 Let the next dictation start while the last one is still working
Pressing the shortcut while a transcript was being transcribed or cleaned up
did nothing, and the thought you had while waiting was lost. The microphone is
free the moment a recording stops, so the next dictation can now be spoken at
once; the pipeline queues it and each one is finished, pasted and reported in
the order it was spoken. The corner indicator stays with the recording under
way rather than being wiped by the previous run's progress, and a stop that
lands behind an unfinished run says it is waiting its turn.
2026-08-25 14:52:17 +03:00
yusufipek 0b06d6d16d Say that a model can be typed in, everywhere one can
Every model box has taken a typed name all along, but nothing said so, and a
closed-looking list reads as the whole choice. The Claude Code and Codex boxes
now carry a tooltip saying the list is a starting point, not a fence. Claude
gets no fetched list of its own: its CLI has no catalog command, and the
aliases it takes (sonnet, opus, haiku, fable) already follow the newest model
of each line.
2026-08-25 14:48:59 +03:00
yusufipek e9488a51a4 Ask Codex itself which models it offers
The Codex model boxes carried a hand-written list, which was already out of
date. The settings window now asks `codex debug models` for the catalog the
CLI's own picker reads, off the interface thread, and refills both boxes with
it; the built-in list is only what is on screen until Codex answers, or when
it is not installed at all.
2026-08-25 14:42:19 +03:00
yusufipek 084c48151d Say when a newer release is out, and stop there
A check on the releases page once a day: at start, on a timer while Dikte
runs, from the General tab on demand, and from `dikte update` at a terminal.
What it finds goes in the tray menu and in one notification per version, and
opens the release page.

Nothing is downloaded and nothing is installed. The four downloads are
installed four different ways and three of those belong to the platform: a
Mac bundle cannot rewrite itself while it is running, the Windows setup has
an uninstall entry of its own, an AppImage is a file kept wherever its owner
keeps it, and a checkout is updated with git. Being wrong about any one of
them means an installation somebody has to repair by hand.

Versions are compared by their numbers alone. A build off master carries the
released number with its commit after it, and that build is ahead of the
release it names rather than behind it; read as a version suffix, every
nightly would be told to go back to a release it had already passed.

The clock lives in its own file rather than in the settings, since a check
runs while the settings window may be open and a background write into
config.json is what would overwrite whatever it holds.
2026-08-22 10:34:59 +03:00
yusufipek f55e201785 Cut a long file into chunks a hosted request can outlive
An hour and a half of speech came back as "OpenRouter: HTTP 502: The
operation was aborted due to timeout", every time. The upload limit was
the only thing deciding where the file was cut, and mp3 at 48 kbps
reaches 24 MB after an hour, so a 90 minute file became two chunks and
the first one was 63 minutes of audio in a single request. Nothing
between here and the model stays on the line that long.

A chunk is capped at fifteen minutes now, whatever it weighs, and the
call is finally handed a timeout of its own: it was going out on the 300
second default sized for a dictation, which the same chunk would have
hit first anyway.

The other half is not throwing the run away when one request fails.
ApiError carries whether a second try can fix it, which is true of the
statuses a gateway raises itself and of a dropped connection, and false
of a rejected key. A chunk is asked for three times, waiting five then
ten seconds, with the Stop button still able to get through. If it does
fail in the end, what was already heard goes to the output box rather
than the bin, with the status line saying where it stops.
2026-08-20 12:27:42 +03:00
yusufipek d0c93e5a17 Let a stalled apt mirror fail the job rather than hold it
Both Linux test jobs on this branch sat an hour in apt-get, on runners
where the same step takes seconds, and a job with nothing to time it out
holds the pull request red-free and unfinished until the six hour limit.
Three retries and a twenty second timeout give apt another mirror, and
five minutes on the step gives the job an end.
2026-08-19 12:36:16 +03:00
yusufipek 29643f26b3 Keep the two Windows executables from being one file
Windows matches a filename without regard to its case, so Dikte.exe and
dikte.exe were one file in the installation directory: PyInstaller wrote
the console build second and that is the only one that landed. Every
launcher then opened a console window behind the tray, and closing it
killed the application.

The console one is dikte-cli.exe now, which the setup's shim names. The
build reads the PE subsystem of both afterwards, since a filesystem that
keeps the two names apart is exactly what the check would run on
otherwise.
2026-08-19 11:11:30 +03:00
yusufipek 23d56acfb5 Publish a Windows setup beside the AppImage and the disk image
The releases page had nothing for Windows, so the only way in was a
checkout, a Python and a pip install. What goes out now is one setup
program per release: PyInstaller's directory, the pinned ffmpeg the
disk image already uses, and Inno Setup around both. It installs for
the account alone, so no administrator is asked for.

Two executables over the one program there, because a windowed one on
Windows has no standard output at all: Dikte.exe for the Start Menu and
dikte.exe for the terminal, sharing everything they carry. The icon is
drawn by Dikte itself into an .ico, the way the Mac's .icns and Linux's
PNGs already are, so there is still no image file in the repository.

Starting at sign-in is a registry value rather than a Startup shortcut,
which is what lets the setup program, the uninstaller and
`dikte integrate` all mean the same thing: the wizard asks once, and
typing the command changes the answer later.

The three builds move into build.yml, which release.yml now calls
instead of holding its own copy, and which a pull request touching the
packaging runs on its own. A broken build is then a red pull request
rather than a failed release.
2026-08-18 16:08:47 +03:00
yusufipek aef33585c1 Say where the Windows install comes from now that there are downloads 2026-08-16 15:55:52 +03:00
yusufipek 180ddac37e Let the Windows job read the merged suite
The merge put a Windows runner under tests master wrote for two systems,
and 21 of them fell over on it. Two were the quoting: `command_for` goes
through shlex now, so a Windows path comes back in quotes, and the two
tests that read the command as a string were reading it as a Linux one.
They ask through the same join instead.

The other nineteen are `integrate.py`, which writes the menu entry and
the login item a downloaded build installs for itself. There are two
downloads, an AppImage and a disk image, so the module has a Linux half
and a macOS half and nothing a Windows host would run: its tests hand
the fake home over in $HOME, which Windows does not read, and compare
paths that start at the root. They carry `@posix_only`, which comes off
again the day there is a Windows build to integrate.
2026-08-16 15:54:44 +03:00
yusufipek 80ff4ecade Merge master: the downloadable builds
Three files disagreed. The test workflow gained a job on either side, so
both stay: the Mac now parses the release and packaging scripts as well,
and Windows keeps its own job below.

`restart` and `launch_gui` both start Dikte again, and master moved that
argv into `ipc.launcher()` because a packaged build has no `__main__.py`
to name. Windows still cannot use execv there, so the detached start it
needs now takes what the launcher hands it rather than spelling the
interpreter and the script itself.

The test counts in CONTRIBUTING are the suite as it stands after the
merge: 1104 of 1147 run anywhere, and the 43 left are the Linux ones.
2026-08-16 15:44:40 +03:00
yusufipek 4ca66b1e03 Carry the two paths the RHEL family uses as well
The list was four of the six that Go's crypto/x509 and curl both carry.
The missing two are the bundle under /etc/pki/ca-trust, which is where
RHEL 7 and CentOS keep it, and OpenELEC's. Neither is likely and both are
one line.

The rest was checked in containers rather than assumed: the first entry
answers Debian, Ubuntu, Arch, Gentoo, Fedora and Alpine, and openSUSE is
the only common distribution that needs a different one.
2026-08-16 15:29:29 +03:00
yusufipek 4cec05fa49 Look up the machine's certificates rather than the build machine's
A build carries the OpenSSL of the machine it was built on, and that
OpenSSL has one directory compiled into it as the only place it looks for
certificates. For an AppImage built on Ubuntu that is /usr/lib/ssl, which
Arch, Fedora and openSUSE do not have, so on any of them every HTTPS
request fails with CERTIFICATE_VERIFY_FAILED: transcription and cleanup
report it as a rejected key, and the model downloads fail too.

Ask the machine instead, from the list curl and Go use, and only when the
build's own answer turns out not to exist. The store on the machine
rather than a copy carried along, because a copy goes stale as roots are
rotated and would ignore a certificate somebody added themselves. Anybody
who has already set SSL_CERT_FILE is left alone.
2026-08-16 15:24:33 +03:00
yusufipek 01c91eb425 Resolve the test's home before comparing paths to it
A Mac keeps temporary directories under /var, which is a symlink to
/private/var, and target() resolves what it is handed because a bundle
reached through a symlink is still that bundle. So the login item named
/private/var/... while the test held /var/..., and the two spellings of
one path did not match.
2026-08-16 15:09:40 +03:00
yusufipek 7e510f8b35 Build an AppImage and a disk image, and publish them
Installing meant cloning the repository and running a shell script, which
is a fair ask of somebody who already has a terminal open and no ask at
all of anybody else. The releases page now carries an AppImage and a disk
image per Mac architecture: a push to master rebuilds a rolling "latest",
a v* tag publishes a version and leaves it there, and the Run button in
the Actions tab raises the number by running scripts/release.sh, which is
the same script and not a second copy of what it does.

Two things in the application had to give for that. A build has no
__main__.py on disk, and an AppImage is mounted somewhere new every run,
so the command a shortcut is registered with cannot go on being this
interpreter and this file; ipc.launcher() answers with the AppImage or
the bundle instead. And a build carries its own libstdc++, which every
process it starts inherits through LD_LIBRARY_PATH and none of them can
live with: ffmpeg, ydotool and wl-copy are the distribution's binaries
built against the distribution's libraries, and AppImageLauncher, which
is what starting the AppImage again goes through, refuses outright.
integrate.py puts that variable back before anything else runs.

Nothing installs itself over an installation that is already there.
install.sh's menu entry, install-mac.sh's login item and the desktop file
AppImageLauncher writes are each recognised and left alone, so trying a
download once does not quietly move the machine onto it. `dikte
integrate` is how you ask for it outright, and --remove takes it back.

The disk image carries an ffmpeg, pinned and checksummed, because macOS
records through one and ships nothing like it. It is signed ad-hoc and
not with an Apple certificate, so a first launch is refused until Open
Anyway and the permissions are asked for again after each update; both
READMEs and the release notes say so.
2026-08-16 15:03:44 +03:00
yusufipek bac988e0e6 Ask for the entry point by its parts, not by a slash
The assertion spelled the path with a forward slash, which is not the separator
Windows joins with. The Windows job this branch adds is the first to run it
there.
2026-08-16 14:13:12 +03:00
yusufipek b186f7fde2 Merge master: the modules moved into a package
Every file this branch touches moved into dikte/, so the merge is mostly the
rename following the edits. What needed a hand:

hotkey.py: master replaced the _macos()/_gnome() pair with one backend()
chooser, and this branch had added _windows() to the pair. Windows is a fifth
value of the chooser now, and everything that used to ask "macOS or Windows?"
asks backend() instead. The key is held by the running process there, so
installs_shortcuts() and shortcut_needs_restart() are both false for it, and
desktop_name() says Windows.

install.ps1 and the Windows README name dikte/__main__.py, the entry point the
Linux and macOS installers were pointed at in the same commit. The Start Menu
entry, the autostart entry and the dikte.cmd shim all come off one $entry
variable.

settings_ui.py: the shortcut tab now has a Windows sentence of its own, with
the Turkish for it. Falling through to the branch master wrote for a desktop
with no registry would have told a Windows user to check /dev/input. Nothing
covers that branch: there is no Windows Settings test class, the way there is
one for macOS.

CONTRIBUTING: the chooser it names is backend() now, and the test count is the
merged one, 1067 of 1110 running anywhere.
2026-08-16 14:10:28 +03:00
yusufipek 2f59029261 Put the modules in a package and the scripts in a folder
Twenty-two files at the top of the tree was the first thing anybody saw of
this repository. They are one package now, imported relatively, and the three
scripts that are not the front door moved under scripts/. install.sh stays
where the README has always said it is.

What starts the application is dikte/__main__.py: python3 -m dikte runs it,
and so does naming the file, which is what the launcher symlink, both .desktop
files, the macOS bundle and every registered shortcut do. Run by path there is
no package around it, so it puts the checkout on sys.path itself.

The installers now keep the keys you chose when they are given none, which is
what an update is: update.sh no longer has to read them out and pass them back.
An updater from before this commit cannot read them at all, so the one thing it
can say, the default key with an empty discard key, is read as "nothing was
asked for" rather than obeyed. That guard can go once nobody is updating across
this commit.
2026-08-16 14:01:01 +03:00
yusufipek 1f5ea70fa6 Stop telling every desktop that it is KDE
Only GNOME was recognised, and everything else was handed to KWin. On i3,
XFCE, Cinnamon, MATE, sway and the rest, Dikte wrote an entry into
kglobalshortcutsrc that nothing reads, called the session KDE, and
promised that the keys would work after the next login. They never did.

There is no backend to write for any of them. The /dev/input listener is
already desktop-agnostic, so those sessions are the case macOS has always
been: no registry, nothing to install, nothing to remove, and the
combination held by the running process. One backend() function decides
which of the four this session has, and the name shown, the status read
back, what Install writes, what Settings explains and what the installer
promises are all taken from it, so they cannot disagree.

A desktop now only counts when the program that writes its registry is
there too. A GNOME session without gsettings and a Plasma one without
kwriteconfig6 fall to the listener rather than to a file, which is also
how Plasma 5 stops erroring on a kwriteconfig6 it never had.

What Settings shows on those desktops is the truth: no Install button, no
KWin, no listener checkbox (it is the mechanism, not a choice), what
reading /dev/input costs, that the focused application sees the keys too,
and the command to bind if you would rather your desktop owned them. The
evdev listener records what it is listening for the way the Carbon one
does, so the status line has something to say there at all.

Closes #28
2026-08-16 13:41:27 +03:00
yusufipek e40191e3a7 Give the tray an icon a black bar cannot swallow
A session that names no desktop, which is what i3 and a bare X11 login are,
leaves Qt with hicolor as its only icon theme, and hicolor has none of the four
names the tray asks for. So fromTheme returns nothing and the shapes drawn in
trayicon.py are used, as they are on macOS. They were drawn in black, which
macOS recolours through the mask and X11 does not, and i3's bar is black: the
icon was there all along, painted onto a bar of its own colour. Outside macOS
they are now white over a dark copy of themselves spread a pixel out, which
stands out on a dark bar and stays readable on a light one, and the mask is set
only where something reads it.

The .desktop files had the same hole from the other side. They named
audio-input-microphone, which is in Breeze and in Adwaita but not in hicolor, so
the menu entry and the autostart entry were blank on the same systems. install.sh
now draws the application icon into ~/.local/share/icons/hicolor and both entries
name it; uninstall.sh takes it back. The windows carry it too on X11, where a
window has no .desktop file to be looked up in.

Closes #27
2026-08-16 13:28:57 +03:00
yusufipek 0e767454f3 Let a recording be held while something else happens
A phone call in the middle of a dictation left two choices: send what
was said so far off to be transcribed, or throw it away. Both end the
sentence you were in the middle of. Now the recording can be held: the
microphone stays ours, what was said before the pause stays in the
buffer, and what is said during it is dropped.

The capture program keeps running and keeps handing blocks over, which
are read and thrown away rather than left in the pipe. Stopping it
instead would mean asking the sound server for the device again on the
way back, and that is the one moment another application can take it: a
recording would be lost to the phone call it was paused for.

The clock stops with it. Paused time is time the recording does not
have, so the indicator and the length limit both go by what was
actually captured, and a five minute limit is not spent waiting.

The indicator says so as well, since a pulsing dot, moving bars and a
counting clock otherwise all say the words are still going in: the
ribbon freezes where the pause found it and turns amber behind two
bars. The tray menu holds and resumes it, `dikte pause` does, and so
does a global shortcut, which starts empty because holding a recording
is not something a keyboard has a habit for.

Dictation and a command to the agent both, whichever is recording. A
meeting is left out: it writes to a file as it goes and keeps two
streams aligned itself, and neither of those wants a hole in it.
2026-08-16 13:06:36 +03:00
yusufipek 5547f85848 Keep the wheel off the boxes and a floor under the window
Two things the scrolling tabs brought with them.

A combo box and a spin box read the wheel as a change of value, and Qt
hands them the focus before delivering it. Now that every tab scrolls,
rolling down the API tab with the pointer over the model box picks a
different model on the way past, and Save writes it down. The boxes take
the focus by click or by tab only, and a wheel that arrives at one
without the focus is refused rather than swallowed, so it carries on up
to the scroll area and the page moves instead.

A tab that scrolls also asks for no height of its own, which left
nothing to stop the window being dragged down to a tab bar and half a
button. It has a floor now, and the floor never asks for more room than
the screen was just found to have.
2026-08-16 12:09:21 +03:00
yusufipek 1135362afa Let the test server open its port without asking who 127.0.0.1 is
The stand-in whisper server in the ggml tests is an http.server, and
http.server looks up the reverse name of the address it bound in between
the bind and the listen. On Linux that answers at once. On a Mac nothing
answers and the lookup sits in a resolver timeout for thirty-five
seconds, with the port closed the whole time and _wait_ready watching it.

Seven tests start a server and one of them starts two, so the macOS job
spent 330 of its 334 seconds inside that lookup while the same suite took
fifteen seconds on Linux. Binding through socketserver and naming the
server after the address it already has skips the question.
2026-08-16 11:57:55 +03:00
yusufipek 5025f8293c Describe the restart race as what it is, not as what it is not
The comment claimed the new process would take itself for the second copy and
hand its command over. That is the command line's path, through cli.run and
ipc.send; run_app with --gui never asks whether anything is already running, it
calls listen() and prints if that fails. The change is the same one either way:
two processes asking for one name is either two servers answering on it or a
listen that fails into a console nobody reads, and closing first leaves neither.
2026-08-16 11:47:40 +03:00
yusufipek 93f3db889d Say out loud that Windows on ARM gets an emulated whisper
Checked against the release listings rather than guessed: whisper.cpp publishes
Win32 and x64 for Windows and nothing else, while llama.cpp does publish
bin-win-cpu-arm64.zip. So a Snapdragon machine gets a native cleanup model and
an emulated transcriber, which is slow enough that the cloud is the better
answer there, and neither the code nor the README said so.

The test pins it, so that a whisper.cpp release which does start publishing an
arm64 build turns the choice red rather than being quietly ignored.
2026-08-16 11:12:36 +03:00
yusufipek 0419c88ac2 Run the tests on Windows too, which is where they have never run
Sixty of them are about the Windows backends, and every one of them has only
ever run on Linux and on a Mac: user32 and kernel32 are faked at the one
function that loads them, which is the whole point and is also the whole limit.
The half that reads the real system has been taken on trust. The Mac has had a
job of its own for exactly this reason since it was ported, and the comment on
it says what it is for; this is the same job with the same reason.

What it should catch that nothing else does: whether %APPDATA% and
%LOCALAPPDATA% are the directories Windows actually hands out, whether a path
spelled with a backslash is still one the tests can read, whether the
config-permission test skips rather than fails where the mode bits mean
nothing, and whether install.ps1 parses. Running install.ps1 is not on the
table: it writes into the Start Menu and the user PATH.
2026-08-16 10:58:09 +03:00
yusufipek 89de7eadfe Merge master into the Windows port, again
Master was rewritten between the two merges: the macOS meeting commits came
back under new hashes with the same content, so the first merge points at a
commit the repository no longer has and audio.py collides with itself. Nothing
in the resolution is a choice between two versions of anything; both sides of
both hunks are this branch's own additions against nothing.
2026-08-16 10:34:36 +03:00
yusufipek ec4d7da09d Say there is a fourth system in the two documents that count them
README.tr.md still offered three, so a Turkish reader could not tell Windows was
supported at all. CONTRIBUTING opens the section a port is written from, and it
described three tables and three systems; there are four of each now, plus
paths.py, which master separated out while this branch was adding a Windows case
to the two copies it replaced.
2026-08-16 10:29:07 +03:00
yusufipek 476265465f Write the Windows installer in the language the rest of it is written in
install.sh and install-mac.sh are in English, and README.tr.md is where Turkish
lives. install.ps1 arrived in Turkish, and in a Turkish with the diacritics
stripped out of it, which is neither one language nor the other.

Two things while it was open: the `dikte` command ran whichever `python` the
PATH answered with rather than the one checked a few lines above it, and a
machine with no WindowsApps directory got no command and no word about why.
2026-08-16 10:29:07 +03:00
yusufipek 743dcc9b8a Ask doctor and devices about the programs this system actually uses
`doctor` had the Wayland pair spelled into it, so an X11 machine was never
asked about the two it really pastes with, a Mac was told ydotool and
kwriteconfig6 were missing, and Windows, which shells out for neither half of
the clipboard, got five red marks for programs it was never going to have. The
two come out of `paste.Desktop` now, and the Linux-only three are added on
Linux. A row saying a program is missing on a machine that would never have run
it is not a diagnosis, it is a mark to explain away.

`devices` had the same shape of answer: "pactl found nothing; is PipeWire
running?" on a Windows machine with no microphone. It names whatever this sound
system is missing instead, which is the string the table already carries for it.

The Windows README's troubleshooting sends people to both, so it says so.
2026-08-16 10:29:07 +03:00
yusufipek bbb9bccda4 Stop answering before the replacement starts, on a restart
execv leaves nothing behind to answer, so this never came up on Linux or a Mac.
A Windows restart is two processes for a moment: the new one looks for an
instance to hand its command to, and if it finds the old one still listening it
takes itself for the second copy and exits, leaving nothing running at all. The
server is closed before the replacement is started rather than after.

Not reproduced, because it is a race and it lost: found by reading the order of
the two calls, and worth a restart or twenty on a real machine.
2026-08-16 10:28:52 +03:00
yusufipek ca559e3961 Fill the clipboard buffer before emptying the clipboard
EmptyClipboard is the point of no return: after it, whatever was there is gone,
and the allocation that failed on the next line left the clipboard holding
nothing. That is the one path where restoring what a dictation borrowed could
lose it instead. The buffer is filled first, and the clipboard is opened only
once there is something to put in it.
2026-08-16 10:28:52 +03:00
yusufipek 8c62795b8b Give Windows devices an identifier, and ask ffmpeg for them once
Three things about the dshow backend, all of them found by reading rather
than by running, so all three want checking on a real Windows machine.

The device listing is parsed in both of the shapes ffmpeg has printed it in:
newer builds mark every device `(audio)` or `(video)`, older ones print a
heading and no marks, and only the first was read. Each pattern is anchored at
both ends now, so the error lines the command ends with, which quote the device
name it was told to look for, are no longer read as a device of that name.

What is stored for a device is the alternative name under it rather than the
friendly one. A laptop with a headset plugged in has two microphones called the
same thing, and `audio=Microphone` reaches the first of them whichever one was
picked; the alternative name is unique. The friendly name stays what is shown,
which is what the (id, description) pair in these lists has always been for.

An unset microphone meant "the first one listed", and the listing costs an
ffmpeg of its own, so every press of the key paid for a process before the
recording started. The last listing is remembered instead, and opening Settings
or running `dikte devices` takes a fresh one.

And a fourth thing, which is about what the interface says rather than what it
does: whether the far side of a meeting can be captured at all is now an entry
in `audio.Sound` instead of being read off an empty device list. The two are not
the same answer. An empty list on Linux means pactl is not installed, which a
user can go and fix; False on Windows means there is no such device and no
driver that would add one. The Meeting tab says so under the empty box, and
starting a meeting says it instead of sending somebody to Settings to pick from
a list that will never have anything in it.
2026-08-16 10:28:43 +03:00
yusufipekandbenfirad 322e06dd7a 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.

Co-authored-by: benfirad <[email protected]>
2026-08-16 10:20:23 +03:00
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
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