Skip to content

fix: make tray windows reliably appear on Windows#19

Merged
offbyonebit merged 6 commits into
mainfrom
fix/windows-tray-no-window
Jun 17, 2026
Merged

fix: make tray windows reliably appear on Windows#19
offbyonebit merged 6 commits into
mainfrom
fix/windows-tray-no-window

Conversation

@offbyonebit

Copy link
Copy Markdown
Owner

Summary

  • Fixes a long-standing bug where clicking tray menu items on Windows produced no visible window: route child stderr to the log file, call AllowSetForegroundWindow so the child can raise itself past Windows' foreground-activation lock, add CREATE_NO_WINDOW to suppress a transient console window, and fix a CTk root titlebar-manipulation race.
  • Tested manually: confirmed tray windows (Settings, History) now open and come to the foreground on Windows.
  • Also updates 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_image without updating the tests that referenced them. Rewrote the affected tests against the current event-driven debounce design.

Test plan

  • Manually verified tray menu windows open and focus correctly on Windows
  • pytest suite passes (pre-existing test_settings_reload.py::test_second_instance_sees_writes_from_first is flaky due to mtime-resolution timing, unrelated to this change)

claude and others added 6 commits June 10, 2026 18:03
…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.
@offbyonebit offbyonebit merged commit 05fbf3a into main Jun 17, 2026
1 check failed
@offbyonebit offbyonebit deleted the fix/windows-tray-no-window branch June 17, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants