fix: make tray windows reliably appear on Windows#19
Merged
Conversation
…y 0.5 s On Linux, _out_tick called _read_clipboard_image() on every 0.5 s poll tick. That spawned xclip which sent an X11 SelectionRequest to the clipboard owner (typically a browser). Browsers service these on their main thread, so 2 requests/second caused the browser to freeze the moment the user pressed Ctrl+V. Two-layer fix: 1. TARGETS pre-check (_linux_clipboard_has_image): before requesting image bytes, run xclip/wl-paste with TARGETS/--list-types. The clipboard owner responds immediately with a list of formats; if image/png is absent we return None without ever sending an image/png SelectionRequest. This eliminates the expensive request for the common case (text on clipboard). 2. Rate-limit image checks in _out_tick to _LINUX_IMAGE_CHECK_INTERVAL (2 s) on Linux. Even the cheap TARGETS request spawns a subprocess and touches the X server, so throttling to 2 s reduces X11 traffic 4x vs the previous 0.5 s rate. Windows and macOS are unaffected (ImageGrab.grabclipboard() is in-process). Image sync latency on Linux increases to ~2 s in the worst case, which is acceptable. Text sync latency is unchanged. 16 regression tests added in tests/test_linux_paste_freeze.py covering the TARGETS gate, rate-limiter logic, and end-to-end image/text sync correctness.
…inate paste freezes Root cause: the in-process xlib clipboard owner used X.XA_ATOM and X.XA_STRING which do not exist in python-xlib. Every SelectionRequest from a pasting app silently hit AttributeError, replied with property=X.NONE, and caused the browser to retry through its fallback atom list until it timed out -- manifesting as a visible paste freeze. Fixed by using Xatom.ATOM (=4) and Xatom.STRING (=31) from `from Xlib import Xatom`. Also adds: - XFixes-based clipboard owner watcher so the OUT loop wakes only on actual copies instead of polling with xclip every 0.5 s (no more SelectionRequests sent to the browser between copies) - 300 ms debounce after XFixes events to avoid competing with an immediate paste - Event-driven OUT loop with polling fallback for Wayland / no XFixes - CLIPSYNC_NO_XFIXES and CLIPSYNC_NO_XLIB escape hatches for debugging - CLIPSYNC_LOG_LEVEL env var support in configure_logging (previously hardcoded to INFO, silently ignoring --log-level DEBUG) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t-path overhead - _STOP_SENTINEL = object() instead of None -- prevents accidental None on queue silently killing the OUT loop - Pass _opcode/_first_event from the probe into _watch() so query_extension is called once instead of twice (one fewer Display connection + round-trip) - _SelectionNotify._code set at class definition time (was re-set inside _watch) - Remove _stopped bool from _XlibClipboardOwner -- the \xff pipe signal already wakes and exits the event loop immediately; _stopped was redundant and implied a 10-s tail latency on shutdown that never actually applied - Remove self._d.flush() after intern_atom calls -- intern_atom is synchronous (waits for reply), so nothing is buffered to flush - Remove redundant `import os` inside set/close/_event_loop (module-level import) - Remove pre-debounce queue drain in _out_loop -- it ran before the 300 ms wait, so nothing useful had accumulated yet; the post-debounce drain is the one that matters - Drop time.monotonic() timing from _write_clipboard hot path -- two syscalls per write whose result was discarded at default INFO log level - configure_logging: remove unused `level` parameter; hard-code INFO default and apply CLIPSYNC_LOG_LEVEL override directly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Files are copied into <sync_folder>/files/<hostname>/ where Syncthing picks them up automatically. Incoming files from peer hosts are saved to ~/Downloads with collision-safe naming and a tray notification. The file picker opens a native OS dialog in a lightweight subprocess. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Four changes to fix the long-standing issue where clicking tray menu items on Windows produced no visible window: 1. Route child subprocess stderr to the ClipSync log file instead of DEVNULL. Crashes were silently swallowed; they now appear in %APPDATA%\ClipSync\clipsync.log so the root cause is diagnosable. 2. Call AllowSetForegroundWindow(pid) on the child after spawning. Windows' foreground-activation lock silently ignores focus_force() and lift() from any process that wasn't directly activated by user input. Tray callbacks never qualify, so windows were being created but rendered behind everything with no way to raise them. 3. Add CREATE_NO_WINDOW to the Popen creationflags on Windows so the child subprocess does not spawn a transient console window, which can confuse the Win32 subsystem when the parent has no console. 4. Deactivate CTk's Windows titlebar manipulation on CTk (root) in addition to CTkToplevel. CTk.__init__ was triggering a withdraw/update/deiconify dance that raced with our root.withdraw() call and could leave internal window-exists state inconsistent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tests/test_linux_paste_freeze.py still imported and exercised _LINUX_IMAGE_CHECK_INTERVAL and _linux_clipboard_has_image, both removed in d27eb0a when polling was replaced by an XFixes-based clipboard-owner watcher. The whole module has been failing to collect since that commit, so this suite hasn't actually run. - Drop the dead imports; fold the TARGETS-gate scenarios that referenced _linux_clipboard_has_image into TestReadImageGate, which now exercises _read_image_from_system_clipboard directly (the TARGETS check is inlined there post-refactor). - Replace TestOutTickRateLimiting (tested a deleted interval-based throttle) with TestOutLoopEventDriven, which drives _out_loop with a fake XFixes queue and asserts the real invariants: one tick at startup, a tick after an event once the 300 ms debounce elapses, rapid events collapsing to a single follow-up tick, and a prompt exit on the stop sentinel. - Rename the two_sided_linux end-to-end tests/fixture away from "throttle" terminology now that there's no interval to throttle on the polling fallback path; drop the frequency-bound assertion that no longer applies.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AllowSetForegroundWindowso the child can raise itself past Windows' foreground-activation lock, addCREATE_NO_WINDOWto suppress a transient console window, and fix a CTk root titlebar-manipulation race.tests/test_linux_paste_freeze.py, which had been failing to collect since the earlier XFixes refactor (d27eb0a) deleted_LINUX_IMAGE_CHECK_INTERVAL/_linux_clipboard_has_imagewithout updating the tests that referenced them. Rewrote the affected tests against the current event-driven debounce design.Test plan
pytestsuite passes (pre-existingtest_settings_reload.py::test_second_instance_sees_writes_from_firstis flaky due to mtime-resolution timing, unrelated to this change)