Repository navigation
port(linux): 2.0 UI starts and connects to the Linux engine (Phase B1) - #83
Conversation
2.0's bridge only knows the kernel32 mapping and the UTF-16 layout, so initialize() raised on Linux and the UI could never attach to the Linux engine. Restore the Linux side from the linux branch: the c_char layout with the 1024/2048-byte UTF-8 string buffers, the /dev/shm mmap with the size-mismatch guard, UTF-8 path encoding truncated on a character boundary, and the BACKEND_FAILED detail read from engine_string. The Windows c_wchar struct is deliberately not reused on Linux: c_wchar is four bytes there, so the offsets would line up by accident and the engine would receive UTF-32 instead of UTF-8. Manual recording stays refused on Linux at the bridge because the Linux engine has no continuous recorder yet. 2.0's mapping RLock and singleton-replacement guard are kept and now cover the Linux mapping too. The Linux bridge tests come back; the two kernel32-specific tests are skipped off Windows.
2.0's HotkeyManager calls ctypes.WinDLL at import time, so main.py could not even be imported on Linux, and it only knows the Windows Raw Input listener. Keep that listener unchanged for win32 and put the Linux backends from the linux branch back behind the same public API: - the XDG GlobalShortcuts portal (core/portal_shortcuts.py) with the desktop-entry app id from #80, adopting the key the desktop really bound and reporting kept keys; - the private hotkey socket plus generated Hyprland binds (conf and Lua), which Hyprland keeps using unless FTHR_FORCE_PORTAL_SHORTCUTS=1; - the manual-setup warning when neither path works. 2.0 additions are carried over to Linux: configurable custom clip actions are announced to the portal, written as Hyprland binds and accepted on the socket, and dispatch through custom_clip_triggered. Clearing a shortcut must stick as it does on Windows. The portal has no unbind call and the desktop keeps answering with its old key, so a cleared action is remembered (~/.fthr/hotkeys-portal-cleared.json): its activations are ignored and the desktop's key is not adopted back until the user assigns a key in FTHR again. register_all() runs again after every shortcut capture; the socket server now starts once instead of failing on its own live socket. On Linux a failed registration no longer refuses the change, because there is no authoritative collision result there (as on the linux branch). Windows: all additions sit behind sys.platform checks and the Linux modules are imported only off win32.
The 2.0 snapshot dropped the sys.platform checks the shared modules had
at the common base, so on Linux they reach ctypes.windll, winreg or
subprocess.CREATE_NO_WINDOW, none of which exist there. Most of these
were caught as AttributeError and turned into wrong diagnostics
('query_failed' instead of 'not_windows'), and some forced a native
window through winId() first.
Put the base-era guards back, unchanged on win32:
- field_diagnostics: /proc/cpuinfo CPU model, sysconf memory, rusage
working set and no registry probe off Windows;
- windows_monitor: monitor enumeration returns empty off Windows;
- third_party_keyboard: the keyboard-visualizer source is Windows-only;
- capture_settings_widget: no Win32 window enumeration on Linux (the
Linux engine captures the whole output; the Linux window list for
game detection returns with the game-detection port);
- windows_dpi / frameless_window_windows: report the 96-DPI baseline and
skip WS_THICKFRAME without touching winId() off Windows;
- encoder_capabilities, export_profiles, camera_recorder: no
CREATE_NO_WINDOW reference off Windows.
These modules had working Linux branches at the common base that the 2.0 snapshot deleted in lines the linux branch never touched, so the merge kept the Windows-only version: - single_instance: nonblocking flock on ~/.fthr/fthr.lock, so a second Linux UI cannot share the single-writer capture IPC channel; - focus_monitor: active window title through hyprctl on Hyprland and xdotool elsewhere instead of the Win32 foreground detector; - capture_card_client: run the notification card under XWayland on Wayland sessions as before; - ffmpeg_playback: load libFTHRPlaybackMixer.so, including the FTHRcapture_linux/build development copy, and only call add_dll_directory on Windows.
resume_recording() writes START_RECORDING without touching ui_string, which still holds the path of the last SAVE_CLIP. The Linux engine reads START_RECORDING with a path as a manual-recording request and answers MANUAL_RECORDING_ERROR, so a focus resume after a save would be refused. Clear the string on Linux before the command. The engine clears it on its side too (PR #82); doing both keeps either side safe alone. Windows is unchanged.
2.0 replaced the Linux start-up hooks with 'pass' or dropped them: - crash logging from #79: Qt message handler plus faulthandler writing ~/.fthr/logs/fthr-crash.log; - restore_library_search_path_for_children() so the engine, ffmpeg and host tools never inherit PyInstaller's _internal library path; - linux_tools.report() so missing helpers are visible in the log; - the desktop entry before QApplication on portal desktops (#80) and setDesktopFileName('fthr-clips') for portal identity and Wayland icon grouping, also for the second-instance activation path; - QT_MEDIA_BACKEND=windows only on Windows: Linux has no Media Foundation backend and must keep Qt's FFmpeg plugin; - the Media Foundation pre-warm only on Windows, as before. Qt style setup is 2.0's configure_qt_style(), the renamed configure_qt_for_linux_ui(). Every addition is behind sys.platform, and the Linux-only modules are imported only off Windows.
The 2.0 engine path only knows FTHRClips.exe and the Windows monitor model, so the UI never found or started the Linux engine: - engine lookup: _MEIPASS/FTHRclips when frozen, else $FTHR_ENGINE and FTHRcapture_linux/build/FTHRclips (newest wins, as on Windows); - monitor argument: 2.0's resolve_monitor_selection() and validate_engine_monitor_argument() accept only Windows device paths and fail closed with NO_DISPLAYS on Linux. Linux sends the output name instead (QScreen name on Wayland, xrandr resolution on native X11 via core/x11_monitor.py); Windows paths, desktop identities and sentinels from a copied settings file fall back to the default output. The source popup lists QScreen outputs on Linux again; - the engine gets linux_runtime.child_process_environment(); - the 'not found' hint names the Linux build; - the clip path length check is the Windows UTF-16 buffer limit; the Linux buffer is 1024 bytes of UTF-8 and the bridge truncates safely. The argv is 2.0's 26-entry list unchanged; the Linux engine ignores what it does not parse. The connect-worker race guard from the linux branch comes back: the status poll must not shut the singleton bridge down while the connect worker is inside initialize(). It sits next to 2.0's handshake generation check, which covers the same window on Windows, so the Windows reconnect decisions are unchanged. The race test stub learns the 2.0 status-poll collaborators.
2.0 replaced the Linux early return in _start_manual_recording() with 'pass', so the UI would ask the Linux engine for a recording it cannot write. Restore the skip, keep the record button disabled with the explanation it had, and hide the start/stop recording shortcut rows on Linux (2.0's own popup test already expects them hidden off Windows). The bridge refuses the command on Linux as a second line.
- showEvent installs the compositor-driven resize filter (ui/frameless_window_linux.py) on Linux; WS_THICKFRAME stays Windows-only; - the hotkey popup explains who owns the keys: on portal desktops a note plus 'EDIT IN DESKTOP SETTINGS' and live refresh from desktop_bindings_changed, elsewhere the socket bind instructions; - the limited-feature warning on compositors without xdotool; - open clips/recordings folder through xdg-open instead of os.startfile, which does not exist on Linux; - the Windows autostart checkbox is hidden on Linux as before.
The 2.0 snapshot deleted the hyprctl/xdotool window enumeration from core/game_detector.py, leaving tests/test_game_detector_compat.py unable to import, and capture_settings_widget's window list used it on Linux at the common base. Put the helpers back unchanged (wiring the Linux game detector into the 2.0 popup is the game-detection PR) and let the window list use them on Linux again. pyproject.toml loses the linux_only marker registration in the merge, so --strict-markers rejected the Wayland screenshot portal tests at collection.
With the Linux backends back in HotkeyManager, any test that reaches set_hotkey() or register_all() on a Hyprland session wrote binds to the developer's ~/.config/hypr (paths fixed at import time, so a patched Path.home() did not help), ran hyprctl reload on the live compositor and bound the real hotkey socket. An autouse fixture now redirects the Hyprland paths and the socket into tmp_path and drops HYPRLAND_INSTANCE_SIGNATURE so no reload runs. _write_hyprland_lua_config() bound its default path at definition time, so redirecting the module path did not reach _apply_compositor_config(); it now resolves the default at call time.
Patching linux_runtime.hotkey_socket_path() replaced the function under test in test_linux_runtime.py. Point XDG_RUNTIME_DIR at tmp_path instead, which keeps a running FTHR's socket untouched just the same.
- The title-bar shutdown button is Windows-only: 2.0 dropped the platform check in _on_close_to_tray_changed(), so toggling close-to-tray showed it on Linux (the initial visibility kept the check). - _connect_worker_active() tolerates hosts without a real thread and is called through the class, so 2.0's SimpleNamespace engine tests (and their Windows runs) keep working with the race guard in place.
Redirecting XDG_RUNTIME_DIR also hid the Wayland compositor socket, so the engine backend-detection tests could no longer connect. Give the hotkey module a delegating stand-in for linux_runtime whose hotkey_socket_path() binds into tmp_path; path queries for displayed commands still use the real resolver that test_linux_tools checks.
2.0 records the microphone natively in the Windows engine, so its _apply_active_audio_state() only stops the Python MicRecorder and _start_mic_recorder() is an empty stub. The Linux engine does not capture the microphone, so Linux clips lost their mic track: the save route still picked the mic mux, but MicRecorder had nothing recorded. Restore the linux branch behaviour off Windows: start the Python recorder when audio is enabled (at start-up, on capture config changes and through the Gary/visualiser sync), resolve the saved device by unique name, mux the recorded segment into the saved clip, and restart the recorder when the device or gain changes in Settings. Windows keeps 2.0's bodies unchanged. tests/test_linux_microphone_selection.py passes again; new tests cover start/stop with the capture config and the mux route.
Removing HYPRLAND_INSTANCE_SIGNATURE was not enough. With it gone, detect_compositor() no longer saw Hyprland, so HotkeyManager went for the GlobalShortcuts portal and opened a real D-Bus session on the developer's bus. HotkeyManager also read and could rewrite the real ~/.fthr/hotkeys.json and the cleared-shortcut list. Several modules also build ~/.fthr paths at import time (log dir, single-instance lock, diagnostics, playback cache, thumbnails), and main.py writes its log as soon as it is imported during collection. Off Windows, conftest now replaces HOME and the XDG config, data, cache and state directories with a session temp dir at import time, before anything is collected. It also drops HYPRLAND_INSTANCE_SIGNATURE. Every test then gets its own home (Path.home(), HOME and the XDG variables under tmp_path). The portal reports itself absent unless a test stubs it, and the cached portal and compositor answers are reset for both module import names. XDG_RUNTIME_DIR stays, because Wayland clients need it. test_test_isolation.py checks the isolation itself: a private home, HotkeyManager state and binds under tmp_path, import-time paths outside the real home, and no portal or cached compositor.
Every silent handler in hotkey_manager.py now says why it is silent. The exception gate goes from 85 to 81 undocumented handlers (linux-2.0 has 83; the baseline stays 92). _start_socket_server() now closes the socket when bind, listen or settimeout fails instead of leaking the descriptor. Stray blank lines are removed.
Several tests make tmp_path/'home' themselves or assert on exactly what tmp_path contains (library scans, SFTP and screenshot staging). Others set HOME or patch Path.home() and expect their value to win. The home now comes from tmp_path_factory, and Path.home() is left to follow HOME as POSIX does.
|
Review follow-up pushed:
Tests. Each file ran in its own process under Launch. Run with a throwaway HOME: the engine reaches HEALTHY on h264_vaapi with |
Phase B1 of the Linux catch-up (docs/linux-2.0-catch-up.md): the 2.0 UI imports, starts and connects to the Linux capture engine on Linux. Windows code paths are unchanged; every addition sits behind
sys.platform, and the Linux-only modules are imported only off Windows.Restored
Imports on Linux
core/hotkey_manager.py:ctypes.WinDLL(user32, wtsapi32, xinput) is created only on win32.field_diagnostics,windows_monitor,third_party_keyboard,capture_settings_widget,windows_dpi,frameless_window_windows,encoder_capabilities,export_profilesandcamera_recorderget back the base-era platform guards. This covers/proc/cpuinfo, sysconf memory, no registry, nowinId(), and noCREATE_NO_WINDOWoff Windows.Capture bridge
c_charlayout (ui_string[1024]/engine_string[2048]),/dev/shmmmap with the size-mismatch guard, UTF-8 paths truncated on a character boundary, and theBACKEND_FAILEDdetail.c_wcharstruct is not reused on Linux:c_wcharis 4 bytes there, so the text would be UTF-32. 2.0's RLock and its singleton-replacement guard are kept.resume_recording()clearsui_stringon Linux, so a resume never sends the last clip path as a manual-recording request (matches Linux engine: 2.0 engine contract (Phase C) #82).Hotkeys (same public API as 2.0's manager; Windows keeps the Raw Input listener)
core/portal_shortcuts.py, with the desktop-entry app id from fix(linux): register the fthr-clips app id with the desktop portal #80. The desktop's real key is adopted and kept keys are reported.~/.fthr/hotkeys-portal-cleared.json. The desktop's old key is ignored and not adopted back until a key is assigned in FTHR again.register_all()runs after every capture, and before this fix it reported its own live socket as "in use".Start-up (
main.py)fthr-crash.log.restore_library_search_path_for_children()andlinux_tools.report().QApplicationon portal desktops (fix(linux): register the fthr-clips app id with the desktop portal #80), andsetDesktopFileName('fthr-clips'), also on the second-instance path.QT_MEDIA_BACKEND=windowsis set only on Windows. The Media Foundation pre-warm is Windows-only.flockincore/single_instance.py.Engine launch
_MEIPASS/FTHRclips, then$FTHR_ENGINE, thenFTHRcapture_linux/build/FTHRclips.resolve_monitor_selection/validate_engine_monitor_argumentare skipped on Linux.child_process_environment().Manual recording
_start_manual_recording()is back.Other base-era Linux bits
focus_monitor(hyprctl/xdotool), the card process under XWayland, andlibFTHRPlaybackMixer.solookup.game_detector; it is not yet wired into the 2.0 game popup.xdg-openfor folders, the compositor warning and the hidden autostart checkbox.linux_onlypytest marker.Tests
~/.config/hypr, the livehyprctl reloadand the real hotkey socket. Without it, a test reachingset_hotkey()on a Hyprland box rewrote the user's binds.Deferred to later PRs
test_wayland_screenshot_portal).test_game_detection_popup,test_alpha_ui_truthcrop case).test_linux_camera_refresh,test_linux_microphone_selection,test_camera_provider_failures).test_encoder_capabilitiescapture-card row).FTHRclips --list-playback-devices.tools/qualify_alpha_stability.pypassingcreationflagson Linux.test_capture_settings_popupstill run on Linux and fail there.Verification (Arch, Hyprland/Wayland, AMD VA-API, Python 3.14, pinned deps)
Each test file was run in its own process with a throwaway
HOME:linux-2.0(9b49995)Every failure that is new against the base comes from a file that could not be collected on the base. Those failures are out of scope (camera, microphone, screenshots, game popup) or environment-only:
test_capture_backend_detection: the engine in tree has no RPATH to the pinned FFmpeg; all 4 pass withLD_LIBRARY_PATHset.test_simple_dropdowns: popup grab under offscreen.test_ui_efficiency: camera overlay repaint.test_design_overhaul_repairs: 2.0's master-volume row versus its own test. This is not a Linux issue.test_editor_sessions(the QtQuick editor) aborted the interpreter in 2 of 4 runs under offscreen. That belongs to Phase D editor qualification.tools/verify_exception_handling.py(92/92),verify_shared_memory_contract.pyandruff check .all pass.Real launch from source (engine built from this tree,
FTHR_ENGINEset, real HOME, stopped with SIGTERM):The window renders, and Record is disabled. The engine exits with the UI.
Known start-up noise on Hyprland: Qt logs
Could not register app ID … 'fthr-clips'because the desktop entry is installed only on portal desktops. The linux branch behaves the same.