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.
This commit is contained in:
2026-08-16 10:29:07 +03:00
parent bbb9bccda4
commit 743dcc9b8a
3 changed files with 67 additions and 7 deletions
+3 -2
View File
@@ -62,8 +62,9 @@ python dikte.py
## Troubleshooting
- **Recording does not start:** does `ffmpeg -version` run? Does
`dikte devices` list your microphone?
- **Recording does not start:** does `dikte doctor` find ffmpeg, and does
`dikte devices` list your microphone? `devices` also takes a fresh listing,
which is what to run after plugging one in.
- **Nothing is pasted:** a normal-privilege process cannot type into an
elevated (administrator) window; run Dikte elevated too, or paste by hand.
The text lands on the clipboard either way.
+16 -2
View File
@@ -655,7 +655,10 @@ def cmd_devices(opts):
"default": name == default}
for name, desc in audio.list_monitors()]
if not mics and not monitors:
return fail(opts, "pactl found nothing; is PipeWire running?")
# Which program was asked, and so which one to go and look at, is not
# the same on all four systems: naming pactl on Windows sends somebody
# after a program that was never going to be there.
return fail(opts, audio.sound().missing)
lines = ["Microphones:"]
lines += [f" {'*' if item['chosen'] else ' '} {item['name']}\n"
@@ -791,7 +794,18 @@ def cmd_status(opts):
def cmd_doctor(opts):
"""What the settings window checks behind its buttons, in one pass."""
conf = cfg.Config()
wanted = ["pw-record", "wl-copy", "ydotool", "ffmpeg", "pactl", "kwriteconfig6",
# The two the clipboard and the key press go through come out of the table
# rather than being spelled here, because they are not the same pair on all
# four systems: X11 pastes with xclip where Wayland pastes with wl-copy, a
# Mac shells out for one half and Windows for neither. A row saying ydotool
# is missing on a machine that would never have run it is not a diagnosis,
# it is a red mark to explain away.
here = paste.desktop()
wanted = [here.clipboard, here.keyboard]
if sys.platform.startswith("linux"):
# Recording, the device list, and KDE's shortcut registry.
wanted += ["pw-record", "pactl", "kwriteconfig6"]
wanted += ["ffmpeg",
assistant.executable(assistant.provider(conf)) or "claude",
cleanup.executable(cleanup.provider(conf))]
programs = {name: shutil.which(name) or "" for name in wanted if name}
+46 -1
View File
@@ -12,12 +12,14 @@ import json
import unittest
from unittest import mock
import audio
import cli
import config as cfg
import ggml
import hotkey
import ipc
from tests.support import DikteTest, fake_urlopen
import paste
from tests.support import DikteTest, fake_urlopen, only_these_tools
class Options:
@@ -435,6 +437,30 @@ class Doctor(DikteTest):
self.assertIn("OpenRouter key, cleaning up on some/model",
self.run_doctor(as_json=False, cleanup_model="some/model"))
def test_it_asks_after_the_programs_this_desktop_actually_uses(self):
"""A missing ydotool on a Mac is a red mark with nothing behind it."""
with mock.patch.object(cli.paste, "desktop", return_value=paste.MACOS):
mac = self.run_doctor()["programs"]
with mock.patch.object(cli.paste, "desktop", return_value=paste.WAYLAND):
wayland = self.run_doctor()["programs"]
self.assertIn("pbcopy", mac)
self.assertNotIn("ydotool", mac)
self.assertIn("ydotool", wayland)
self.assertIn("ffmpeg", mac) # the one every system records through
def test_a_system_that_shells_out_for_neither_half_is_asked_for_neither(self):
# shutil.which is faked as well as the platform: the real one reads
# sys.platform too, and reaches for a Windows API this machine has not
# got the moment it is told it is on Windows.
with mock.patch.object(cli.paste, "desktop", return_value=paste.WINDOWS), \
only_these_tools("ffmpeg"), \
mock.patch.object(cli.sys, "platform", "win32"):
programs = self.run_doctor()["programs"]
self.assertNotIn("", programs)
self.assertEqual([name for name in ("wl-copy", "ydotool", "pactl",
"pw-record", "kwriteconfig6")
if name in programs], [])
def test_cleanup_on_a_cli_is_a_question_about_the_program(self):
reply = self.run_doctor(cleanup_provider="codex",
cleanup_codex_model="gpt-5.4")
@@ -446,6 +472,25 @@ class Doctor(DikteTest):
cleanup_codex_model="gpt-5.4"))
class Devices(DikteTest):
def test_a_machine_with_nothing_names_its_own_missing_program(self):
"""The Windows README sends people here, and pactl is not on it."""
for here, expected in ((audio.DSHOW, "ffmpeg"),
(audio.PULSE, "pulseaudio-utils")):
with self.subTest(sound=expected):
with mock.patch.object(cli.audio, "sound", return_value=here), \
mock.patch.object(cli.audio, "list_sources",
return_value=[]), \
mock.patch.object(cli.audio, "list_monitors",
return_value=[]), \
mock.patch.object(cli.audio, "default_monitor",
return_value=""), \
captured() as (out, _err):
code = cli.cmd_devices(Options(json=True))
self.assertEqual(code, 1)
self.assertIn(expected, json.loads(out.getvalue())["error"])
class Finding(DikteTest):
def test_no_history_at_all(self):
self.assertIsNone(cli._find_history("last"))