mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
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]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
77b26e76be
commit
3436e6b426
@@ -13,6 +13,7 @@ is checked on a Mac and the macOS half on Linux, and a change to the chooser
|
||||
cannot quietly break the platform nobody is sitting at.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
@@ -56,6 +57,9 @@ class Chooser(DikteTest):
|
||||
def test_a_mac(self):
|
||||
self.assertIs(self.under("darwin"), paste.MACOS)
|
||||
|
||||
def test_windows(self):
|
||||
self.assertIs(self.under("win32"), paste.WINDOWS)
|
||||
|
||||
def test_a_mac_running_an_x_server_is_still_a_mac(self):
|
||||
"""XQuartz sets DISPLAY, and none of X's programs are what pastes here."""
|
||||
self.assertIs(self.under("darwin", DISPLAY=":0"), paste.MACOS)
|
||||
@@ -429,5 +433,128 @@ class MacClipboardSnapshot(DikteTest):
|
||||
self.assertFalse(os.path.exists(directory))
|
||||
|
||||
|
||||
class FakeWin32:
|
||||
"""user32 and kernel32, as much of both as paste.py calls.
|
||||
|
||||
The clipboard is a string held here. A read materialises it as this
|
||||
machine's own wide characters, which is what wstring_at reads wherever the
|
||||
test runs; a write arrives as the UTF-16 the real clipboard is handed, so
|
||||
what the code sent is exactly what is checked.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.text = None
|
||||
self.buffers = {}
|
||||
self.next_handle = 1
|
||||
self.pressed = [] # (virtual key, flags), in the order sent
|
||||
self.send_result = None # None: report every event as delivered
|
||||
|
||||
def _keep(self, buffer):
|
||||
handle = self.next_handle
|
||||
self.next_handle += 1
|
||||
self.buffers[handle] = buffer
|
||||
return handle
|
||||
|
||||
# --- user32
|
||||
def OpenClipboard(self, owner):
|
||||
return 1
|
||||
|
||||
def CloseClipboard(self):
|
||||
return 1
|
||||
|
||||
def EmptyClipboard(self):
|
||||
self.text = None
|
||||
return 1
|
||||
|
||||
def GetClipboardData(self, fmt):
|
||||
if self.text is None:
|
||||
return 0
|
||||
return self._keep(ctypes.create_unicode_buffer(self.text))
|
||||
|
||||
def SetClipboardData(self, fmt, handle):
|
||||
raw = self.buffers[handle].raw
|
||||
self.text = raw.decode("utf-16-le").split("\x00", 1)[0]
|
||||
return handle
|
||||
|
||||
def SendInput(self, count, inputs, size):
|
||||
self.pressed.extend((entry.union.ki.wVk, entry.union.ki.dwFlags)
|
||||
for entry in inputs)
|
||||
return count if self.send_result is None else self.send_result
|
||||
|
||||
# --- kernel32
|
||||
def GlobalAlloc(self, flags, size):
|
||||
return self._keep(ctypes.create_string_buffer(size))
|
||||
|
||||
def GlobalLock(self, handle):
|
||||
buffer = self.buffers.get(handle)
|
||||
return ctypes.addressof(buffer) if buffer else 0
|
||||
|
||||
def GlobalUnlock(self, handle):
|
||||
return 1
|
||||
|
||||
def GlobalFree(self, handle):
|
||||
self.buffers.pop(handle, None)
|
||||
return 1
|
||||
|
||||
|
||||
class Windows(Standing, DikteTest):
|
||||
"""Windows shells out to nothing: both halves are calls into the system."""
|
||||
|
||||
platform = "win32"
|
||||
here = paste.WINDOWS
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.api = FakeWin32()
|
||||
self.patch_attr(paste, "_win_api", lambda: (self.api, self.api))
|
||||
self.patch_attr(paste.time, "sleep", lambda seconds: None)
|
||||
|
||||
def test_what_is_copied_is_what_reads_back(self):
|
||||
paste.copy("ığüşöç İ")
|
||||
self.assertEqual(paste.read_clipboard(), "ığüşöç İ".encode("utf-8"))
|
||||
|
||||
def test_an_empty_clipboard_reads_as_empty_text(self):
|
||||
self.assertEqual(paste.read_clipboard(), b"")
|
||||
|
||||
def test_what_was_saved_goes_back_after_the_paste(self):
|
||||
paste.copy("mine")
|
||||
saved = paste.read_clipboard()
|
||||
paste.copy("the dictation")
|
||||
paste.copy_bytes(saved)
|
||||
self.assertEqual(self.api.text, "mine")
|
||||
|
||||
def test_readiness_asks_for_no_program_and_no_permission(self):
|
||||
with only_these_tools():
|
||||
self.assertTrue(paste.paste_ready())
|
||||
|
||||
def test_the_keys_go_down_in_order_and_up_in_reverse(self):
|
||||
paste.press("ctrl+v")
|
||||
keyup = 0x0002
|
||||
self.assertEqual(self.api.pressed,
|
||||
[(0x11, 0), (0x56, 0), (0x56, keyup), (0x11, keyup)])
|
||||
|
||||
def test_three_keys(self):
|
||||
paste.press("ctrl+shift+v")
|
||||
self.assertEqual([code for code, _ in self.api.pressed],
|
||||
[0x11, 0x10, 0x56, 0x56, 0x10, 0x11])
|
||||
|
||||
def test_the_other_spellings_land_on_the_same_keys(self):
|
||||
paste.press("super+enter")
|
||||
first, self.api.pressed = self.api.pressed, []
|
||||
paste.press("meta+return")
|
||||
self.assertEqual(self.api.pressed, first)
|
||||
|
||||
def test_a_key_nobody_mapped_is_refused_before_anything_is_sent(self):
|
||||
with self.assertRaises(paste.PasteError):
|
||||
paste.press("ctrl+f13")
|
||||
self.assertEqual(self.api.pressed, [])
|
||||
|
||||
def test_a_press_the_system_did_not_take_says_so(self):
|
||||
self.api.send_result = 0
|
||||
with self.assertRaises(paste.PasteError) as caught:
|
||||
paste.press("ctrl+v")
|
||||
self.assertIn("SendInput", str(caught.exception))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user