railmux 0.4.0.dev8__tar.gz → 0.4.0.dev10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {railmux-0.4.0.dev8/src/railmux.egg-info → railmux-0.4.0.dev10}/PKG-INFO +1 -1
- railmux-0.4.0.dev10/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/cli.py +26 -2
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/codex_index.py +2 -2
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/discovery.py +142 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/fast_display_server.py +1 -1
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/mouse_manager.py +1 -1
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/orphan_marker.py +2 -2
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/provider_paths.py +39 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/restart_state.py +8 -2
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/tmux_binding_manager.py +53 -14
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/tmux_ctl.py +145 -43
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/app.py +71 -2
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/windows_install_log.py +28 -8
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/windows_msys2.py +142 -4
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/windows_pacman.py +176 -0
- railmux-0.4.0.dev10/src/railmux/windows_tmux_lifecycle.py +259 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_discovery.py +113 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_fullscreen_binding.py +41 -0
- railmux-0.4.0.dev10/tests/test_provider_paths.py +88 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_restart_state.py +30 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_soft_quit.py +6 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_tmux_binding_manager.py +39 -5
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_tmux_ctl.py +36 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_tmux_integration.py +17 -2
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_windows_install_log.py +34 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_windows_msys2.py +68 -7
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_windows_pacman.py +96 -0
- railmux-0.4.0.dev10/tests/test_windows_tmux_lifecycle.py +140 -0
- railmux-0.4.0.dev8/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev8/tests/test_provider_paths.py +0 -29
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/LICENSE +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/README.md +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/pyproject.toml +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/setup.cfg +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev8 → railmux-0.4.0.dev10}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev10"
|
|
@@ -20,16 +20,20 @@ from railmux.runtime_config import (
|
|
|
20
20
|
)
|
|
21
21
|
from railmux import tmux_health
|
|
22
22
|
from railmux import tmux_server
|
|
23
|
+
from railmux import windows_tmux_lifecycle
|
|
23
24
|
from railmux.system_deps import ensure_tmux_available
|
|
24
25
|
from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def _show_startup_message(
|
|
28
|
+
def _show_startup_message(
|
|
29
|
+
detail: str = "Reconnecting sessions and panes…",
|
|
30
|
+
) -> None:
|
|
28
31
|
"""Paint immediate feedback before App performs its initial discovery."""
|
|
29
32
|
if not sys.stdout.isatty():
|
|
30
33
|
return
|
|
31
34
|
size = shutil.get_terminal_size((80, 24))
|
|
32
|
-
sys.stdout.write(render_startup_surface(
|
|
35
|
+
sys.stdout.write(render_startup_surface(
|
|
36
|
+
size.columns, size.lines, detail=detail))
|
|
33
37
|
sys.stdout.flush()
|
|
34
38
|
|
|
35
39
|
|
|
@@ -123,7 +127,16 @@ def _run_tmux_client_with_watchdog(
|
|
|
123
127
|
file=sys.stderr,
|
|
124
128
|
)
|
|
125
129
|
return 2
|
|
130
|
+
# tmux may leave the client TTY in raw mode even after its process
|
|
131
|
+
# exits. Restore it before a bounded recovery probe or user message.
|
|
132
|
+
_restore_terminal(attributes)
|
|
126
133
|
returncode = process.returncode or 0
|
|
134
|
+
if windows_tmux_lifecycle.recover_abandoned_socket():
|
|
135
|
+
print(
|
|
136
|
+
"info: cleaned an abandoned Windows tmux socket; Codex and "
|
|
137
|
+
"Claude session files were not changed",
|
|
138
|
+
file=sys.stderr,
|
|
139
|
+
)
|
|
127
140
|
if returncode and expected_target is not None:
|
|
128
141
|
try:
|
|
129
142
|
current_target = tmux_server.discover_target(timeout=1.0)
|
|
@@ -364,6 +377,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
364
377
|
# Validate before any command can address a tmux server. ``tmux -V``
|
|
365
378
|
# above is server-independent.
|
|
366
379
|
tmux_server.socket_label()
|
|
380
|
+
if windows_tmux_lifecycle.recover_abandoned_socket():
|
|
381
|
+
print(
|
|
382
|
+
"info: cleaned an abandoned Windows tmux socket; Codex and "
|
|
383
|
+
"Claude session files were not changed",
|
|
384
|
+
file=sys.stderr,
|
|
385
|
+
)
|
|
367
386
|
dedicated_target = tmux_server.discover_target()
|
|
368
387
|
on_dedicated_server = tmux_server.is_current_server(dedicated_target)
|
|
369
388
|
except tmux_server.TmuxServerError as exc:
|
|
@@ -441,6 +460,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
441
460
|
_show_startup_message()
|
|
442
461
|
# Lazy import so non-TUI invocations (--version etc) don't pull urwid.
|
|
443
462
|
from railmux.ui.app import App
|
|
463
|
+
from railmux.provider_paths import running_in_windows_wrapper
|
|
444
464
|
app = App(
|
|
445
465
|
claude_home=Path(args.claude_home),
|
|
446
466
|
config=config,
|
|
@@ -451,6 +471,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
451
471
|
is_ssh_session() if args.scroll_coalescing is None
|
|
452
472
|
else args.scroll_coalescing
|
|
453
473
|
),
|
|
474
|
+
startup_progress=(
|
|
475
|
+
_show_startup_message
|
|
476
|
+
if running_in_windows_wrapper() else None
|
|
477
|
+
),
|
|
454
478
|
)
|
|
455
479
|
app.run()
|
|
456
480
|
return 0
|
|
@@ -25,7 +25,7 @@ from railmux.models import (
|
|
|
25
25
|
Project,
|
|
26
26
|
SessionMeta,
|
|
27
27
|
)
|
|
28
|
-
from railmux.provider_paths import provider_path
|
|
28
|
+
from railmux.provider_paths import private_mode_is_safe, provider_path
|
|
29
29
|
from railmux.renames import Renames
|
|
30
30
|
|
|
31
31
|
# Codex has no reliable provider-neutral signal that distinguishes a long
|
|
@@ -461,7 +461,7 @@ class CodexIndex:
|
|
|
461
461
|
if (
|
|
462
462
|
not stat.S_ISREG(info.st_mode)
|
|
463
463
|
or info.st_uid != os.getuid()
|
|
464
|
-
or info.st_mode
|
|
464
|
+
or not private_mode_is_safe(info.st_mode)
|
|
465
465
|
or info.st_size > _PERSISTENT_CACHE_MAX_BYTES
|
|
466
466
|
):
|
|
467
467
|
return
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""Enumerate Claude project directories from ~/.claude/projects/."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import hashlib
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
7
|
+
import stat
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
|
|
8
10
|
from railmux.atomic_file import atomic_write_text
|
|
9
11
|
from railmux.models import Project
|
|
10
12
|
from railmux.path_codec import decode, is_encoded_project_name
|
|
13
|
+
from railmux.provider_paths import private_mode_is_safe
|
|
11
14
|
from railmux.session_index import _looks_like_uuid, _scan_session
|
|
12
15
|
|
|
13
16
|
|
|
@@ -19,6 +22,134 @@ _cache: dict[Path, tuple[int, list[Project]]] = {}
|
|
|
19
22
|
_session_validity: dict[
|
|
20
23
|
Path, dict[Path, tuple[tuple[int, int, int], bool]]
|
|
21
24
|
] = {}
|
|
25
|
+
_persistent_validity_loaded: set[Path] = set()
|
|
26
|
+
_persistent_validity_exact_only: set[Path] = set()
|
|
27
|
+
_persistent_validity_dirty: set[Path] = set()
|
|
28
|
+
_VALIDITY_CACHE_SCHEMA = 1
|
|
29
|
+
_VALIDITY_CACHE_LIMIT = 16 * 1024 * 1024
|
|
30
|
+
_VALIDITY_RECORD_LIMIT = 100_000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _windows_validity_cache_enabled() -> bool:
|
|
34
|
+
return os.environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _validity_cache_file(claude_home: Path) -> Path:
|
|
38
|
+
base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
39
|
+
identity = hashlib.sha256(
|
|
40
|
+
os.fsencode(os.path.abspath(claude_home))
|
|
41
|
+
).hexdigest()[:16]
|
|
42
|
+
return base / "railmux" / f"claude-validity-{identity}.json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_persistent_validity(claude_home: Path) -> None:
|
|
46
|
+
if (
|
|
47
|
+
not _windows_validity_cache_enabled()
|
|
48
|
+
or claude_home in _persistent_validity_loaded
|
|
49
|
+
):
|
|
50
|
+
return
|
|
51
|
+
_persistent_validity_loaded.add(claude_home)
|
|
52
|
+
path = _validity_cache_file(claude_home)
|
|
53
|
+
try:
|
|
54
|
+
info = path.lstat()
|
|
55
|
+
if (
|
|
56
|
+
not stat.S_ISREG(info.st_mode)
|
|
57
|
+
or info.st_uid != os.getuid()
|
|
58
|
+
or info.st_size > _VALIDITY_CACHE_LIMIT
|
|
59
|
+
or not private_mode_is_safe(info.st_mode)
|
|
60
|
+
):
|
|
61
|
+
return
|
|
62
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
63
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
64
|
+
return
|
|
65
|
+
if not isinstance(raw, dict) or raw.get("schema") != _VALIDITY_CACHE_SCHEMA:
|
|
66
|
+
return
|
|
67
|
+
records = raw.get("records")
|
|
68
|
+
if not isinstance(records, list) or len(records) > _VALIDITY_RECORD_LIMIT:
|
|
69
|
+
return
|
|
70
|
+
projects_dir = claude_home / "projects"
|
|
71
|
+
loaded: list[Path] = []
|
|
72
|
+
try:
|
|
73
|
+
for record in records:
|
|
74
|
+
if not isinstance(record, list) or len(record) != 6:
|
|
75
|
+
raise ValueError("invalid validity record")
|
|
76
|
+
project, filename, inode, mtime_ns, size, valid = record
|
|
77
|
+
if (
|
|
78
|
+
not isinstance(project, str)
|
|
79
|
+
or not is_encoded_project_name(project)
|
|
80
|
+
or not isinstance(filename, str)
|
|
81
|
+
or Path(filename).name != filename
|
|
82
|
+
or not filename.endswith(".jsonl")
|
|
83
|
+
or not _looks_like_uuid(Path(filename).stem)
|
|
84
|
+
or not all(isinstance(value, int) and value >= 0
|
|
85
|
+
for value in (inode, mtime_ns, size))
|
|
86
|
+
or not isinstance(valid, bool)
|
|
87
|
+
):
|
|
88
|
+
raise ValueError("invalid validity record")
|
|
89
|
+
project_dir = projects_dir / project
|
|
90
|
+
session_path = project_dir / filename
|
|
91
|
+
_session_validity.setdefault(project_dir, {})[session_path] = (
|
|
92
|
+
(inode, mtime_ns, size), valid,
|
|
93
|
+
)
|
|
94
|
+
loaded.append(session_path)
|
|
95
|
+
except ValueError:
|
|
96
|
+
for session_path in loaded:
|
|
97
|
+
cached = _session_validity.get(session_path.parent)
|
|
98
|
+
if cached is not None:
|
|
99
|
+
cached.pop(session_path, None)
|
|
100
|
+
if not cached:
|
|
101
|
+
_session_validity.pop(session_path.parent, None)
|
|
102
|
+
return
|
|
103
|
+
_persistent_validity_exact_only.update(loaded)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _mark_persistent_validity_dirty(claude_dir: Path) -> None:
|
|
107
|
+
if _windows_validity_cache_enabled():
|
|
108
|
+
_persistent_validity_dirty.add(claude_dir.parent.parent)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _save_persistent_validity(claude_home: Path) -> None:
|
|
112
|
+
if (
|
|
113
|
+
not _windows_validity_cache_enabled()
|
|
114
|
+
or claude_home not in _persistent_validity_dirty
|
|
115
|
+
):
|
|
116
|
+
return
|
|
117
|
+
projects_dir = claude_home / "projects"
|
|
118
|
+
records: list[list[object]] = []
|
|
119
|
+
for project_dir, values in _session_validity.items():
|
|
120
|
+
if (
|
|
121
|
+
project_dir.parent != projects_dir
|
|
122
|
+
or not is_encoded_project_name(project_dir.name)
|
|
123
|
+
):
|
|
124
|
+
continue
|
|
125
|
+
for session_path, (signature, valid) in values.items():
|
|
126
|
+
if session_path.parent != project_dir:
|
|
127
|
+
continue
|
|
128
|
+
inode, mtime_ns, size = signature
|
|
129
|
+
records.append([
|
|
130
|
+
project_dir.name,
|
|
131
|
+
session_path.name,
|
|
132
|
+
inode,
|
|
133
|
+
mtime_ns,
|
|
134
|
+
size,
|
|
135
|
+
valid,
|
|
136
|
+
])
|
|
137
|
+
records.sort(key=lambda item: int(item[3]), reverse=True)
|
|
138
|
+
records = records[:_VALIDITY_RECORD_LIMIT]
|
|
139
|
+
path = _validity_cache_file(claude_home)
|
|
140
|
+
try:
|
|
141
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
atomic_write_text(
|
|
143
|
+
path,
|
|
144
|
+
json.dumps(
|
|
145
|
+
{"schema": _VALIDITY_CACHE_SCHEMA, "records": records},
|
|
146
|
+
separators=(",", ":"),
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
os.chmod(path, 0o600)
|
|
150
|
+
except OSError:
|
|
151
|
+
return
|
|
152
|
+
_persistent_validity_dirty.discard(claude_home)
|
|
22
153
|
|
|
23
154
|
|
|
24
155
|
def invalidate_session(path: Path) -> None:
|
|
@@ -27,6 +158,8 @@ def invalidate_session(path: Path) -> None:
|
|
|
27
158
|
if cached is None:
|
|
28
159
|
return
|
|
29
160
|
cached.pop(path, None)
|
|
161
|
+
_persistent_validity_exact_only.discard(path)
|
|
162
|
+
_mark_persistent_validity_dirty(path.parent)
|
|
30
163
|
if not cached:
|
|
31
164
|
_session_validity.pop(path.parent, None)
|
|
32
165
|
|
|
@@ -64,6 +197,7 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
64
197
|
session counts and recency track files created, changed, or removed below
|
|
65
198
|
the cached top-level directory.
|
|
66
199
|
"""
|
|
200
|
+
_load_persistent_validity(claude_home)
|
|
67
201
|
projects_dir = claude_home / "projects"
|
|
68
202
|
if not projects_dir.is_dir():
|
|
69
203
|
return []
|
|
@@ -79,6 +213,7 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
79
213
|
# without re-decoding every project name.
|
|
80
214
|
refreshed = _refresh_activity(cached[1])
|
|
81
215
|
_cache[claude_home] = (parent_mtime, refreshed)
|
|
216
|
+
_save_persistent_validity(claude_home)
|
|
82
217
|
return refreshed
|
|
83
218
|
|
|
84
219
|
# Full scan needed.
|
|
@@ -153,6 +288,7 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
153
288
|
|
|
154
289
|
results.sort(key=lambda p: p.last_activity_ts, reverse=True)
|
|
155
290
|
_cache[claude_home] = (parent_mtime, results)
|
|
291
|
+
_save_persistent_validity(claude_home)
|
|
156
292
|
return results
|
|
157
293
|
|
|
158
294
|
|
|
@@ -198,6 +334,7 @@ def _count_and_latest_mtime(claude_dir: Path, real_path: Path) -> tuple[int, flo
|
|
|
198
334
|
if cached is not None and cached[0] == signature:
|
|
199
335
|
valid = cached[1]
|
|
200
336
|
elif (cached is not None and cached[1]
|
|
337
|
+
and path not in _persistent_validity_exact_only
|
|
201
338
|
and cached[0][0] == stat.st_ino
|
|
202
339
|
and stat.st_size > cached[0][2]):
|
|
203
340
|
# Claude session JSONLs are append-only. Once a file is valid,
|
|
@@ -208,9 +345,12 @@ def _count_and_latest_mtime(claude_dir: Path, real_path: Path) -> tuple[int, flo
|
|
|
208
345
|
# explicitly via ``invalidate_session``).
|
|
209
346
|
valid = True
|
|
210
347
|
validity[path] = (signature, True)
|
|
348
|
+
_mark_persistent_validity_dirty(claude_dir)
|
|
211
349
|
else:
|
|
212
350
|
valid = _scan_session(temp_project, path) is not None
|
|
213
351
|
validity[path] = (signature, valid)
|
|
352
|
+
_persistent_validity_exact_only.discard(path)
|
|
353
|
+
_mark_persistent_validity_dirty(claude_dir)
|
|
214
354
|
if not valid:
|
|
215
355
|
continue
|
|
216
356
|
count += 1
|
|
@@ -219,6 +359,8 @@ def _count_and_latest_mtime(claude_dir: Path, real_path: Path) -> tuple[int, flo
|
|
|
219
359
|
latest = m
|
|
220
360
|
for stale in set(validity) - current_paths:
|
|
221
361
|
del validity[stale]
|
|
362
|
+
_persistent_validity_exact_only.discard(stale)
|
|
363
|
+
_mark_persistent_validity_dirty(claude_dir)
|
|
222
364
|
if not validity:
|
|
223
365
|
_session_validity.pop(claude_dir, None)
|
|
224
366
|
return count, latest
|
|
@@ -1493,7 +1493,7 @@ def _acquire_display_lock(
|
|
|
1493
1493
|
if (
|
|
1494
1494
|
not stat.S_ISREG(info.st_mode)
|
|
1495
1495
|
or info.st_uid != os.getuid()
|
|
1496
|
-
or info.st_mode
|
|
1496
|
+
or not restart_state.private_mode_is_safe(info.st_mode)
|
|
1497
1497
|
):
|
|
1498
1498
|
raise OSError("unsafe display lock")
|
|
1499
1499
|
deadline = time.monotonic() + max(0.0, timeout)
|
|
@@ -60,7 +60,7 @@ class RootWheelForwardingManager:
|
|
|
60
60
|
info = self._state_path.lstat()
|
|
61
61
|
if (not stat.S_ISREG(info.st_mode)
|
|
62
62
|
or info.st_uid != os.getuid()
|
|
63
|
-
or info.st_mode
|
|
63
|
+
or not restart_state.private_mode_is_safe(info.st_mode)
|
|
64
64
|
or info.st_size > _MAX_STATE_BYTES):
|
|
65
65
|
return None
|
|
66
66
|
raw = json.loads(self._state_path.read_text(encoding="utf-8"))
|
|
@@ -231,7 +231,7 @@ def claim_owner(
|
|
|
231
231
|
parent = lock_path.parent.stat()
|
|
232
232
|
if (not stat.S_ISDIR(parent.st_mode)
|
|
233
233
|
or parent.st_uid != os.getuid()
|
|
234
|
-
or parent.st_mode
|
|
234
|
+
or not restart_state.private_mode_is_safe(parent.st_mode)):
|
|
235
235
|
return None
|
|
236
236
|
flags = os.O_CREAT | os.O_RDWR
|
|
237
237
|
if hasattr(os, "O_NOFOLLOW"):
|
|
@@ -243,7 +243,7 @@ def claim_owner(
|
|
|
243
243
|
info = os.fstat(fd)
|
|
244
244
|
if (not stat.S_ISREG(info.st_mode)
|
|
245
245
|
or info.st_uid != os.getuid()
|
|
246
|
-
or info.st_mode
|
|
246
|
+
or not restart_state.private_mode_is_safe(info.st_mode)):
|
|
247
247
|
return None
|
|
248
248
|
try:
|
|
249
249
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"""Translate provider-owned native Windows paths inside the MSYS2 preview."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
7
|
+
import stat
|
|
8
|
+
import sys
|
|
6
9
|
from collections.abc import Mapping
|
|
7
10
|
from pathlib import Path
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
_DRIVE_PATH = re.compile(r"^(?:\\\\\?\\)?([A-Za-z]):[\\/](.*)\Z")
|
|
14
|
+
_MANAGED_RUNTIME_MARKER = Path("/railmux-runtime.json")
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
def running_in_windows_wrapper(environ: Mapping[str, str] = os.environ) -> bool:
|
|
@@ -15,6 +19,41 @@ def running_in_windows_wrapper(environ: Mapping[str, str] = os.environ) -> bool:
|
|
|
15
19
|
return environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
|
|
16
20
|
|
|
17
21
|
|
|
22
|
+
def running_in_managed_windows_wrapper(
|
|
23
|
+
environ: Mapping[str, str] = os.environ,
|
|
24
|
+
) -> bool:
|
|
25
|
+
"""Verify the installed MSYS2 runtime, not merely its environment hint."""
|
|
26
|
+
if sys.platform != "cygwin" or not running_in_windows_wrapper(environ):
|
|
27
|
+
return False
|
|
28
|
+
marker = _MANAGED_RUNTIME_MARKER
|
|
29
|
+
try:
|
|
30
|
+
info = marker.lstat()
|
|
31
|
+
if (
|
|
32
|
+
not stat.S_ISREG(info.st_mode)
|
|
33
|
+
or info.st_uid != os.getuid()
|
|
34
|
+
or info.st_mode & 0o022
|
|
35
|
+
or info.st_size > 4096
|
|
36
|
+
):
|
|
37
|
+
return False
|
|
38
|
+
payload = json.loads(marker.read_text(encoding="utf-8"))
|
|
39
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
40
|
+
return False
|
|
41
|
+
from railmux import __version__
|
|
42
|
+
runtime_id = environ.get("RAILMUX_MSYS2_RUNTIME_ID")
|
|
43
|
+
return isinstance(runtime_id, str) and bool(runtime_id) and payload == {
|
|
44
|
+
"schema": 1,
|
|
45
|
+
"runtime": runtime_id,
|
|
46
|
+
"railmux": __version__,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def private_mode_is_safe(mode: int) -> bool:
|
|
51
|
+
"""Accept strict POSIX modes or the verified MSYS2 noacl projection."""
|
|
52
|
+
return not mode & 0o077 or (
|
|
53
|
+
running_in_managed_windows_wrapper() and not mode & 0o022
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
18
57
|
def provider_path(
|
|
19
58
|
raw: str,
|
|
20
59
|
*,
|
|
@@ -17,6 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
from railmux import tmux_ctl
|
|
18
18
|
from railmux.atomic_file import atomic_write_text
|
|
19
19
|
from railmux.config import default_config_path
|
|
20
|
+
from railmux.provider_paths import private_mode_is_safe
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
SCHEMA_VERSION = 1
|
|
@@ -164,10 +165,15 @@ def legacy_state_path() -> Path:
|
|
|
164
165
|
def _ensure_private_dir(path: Path) -> None:
|
|
165
166
|
"""Create and verify a Railmux-owned 0700 runtime directory."""
|
|
166
167
|
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
167
|
-
info = path.
|
|
168
|
+
info = path.lstat()
|
|
169
|
+
# MSYS2's default noacl projection reports user-owned NTFS directories as
|
|
170
|
+
# 0755 even after chmod(0700). The managed runtime itself lives below the
|
|
171
|
+
# Windows user's LocalAppData ACL. Accept that projection only on the real
|
|
172
|
+
# Cygwin/MSYS Python platform, and still reject symlinks, foreign owners,
|
|
173
|
+
# and any group/world-writable directory. POSIX platforms retain 0700.
|
|
168
174
|
if (not stat.S_ISDIR(info.st_mode)
|
|
169
175
|
or info.st_uid != os.getuid()
|
|
170
|
-
or info.st_mode
|
|
176
|
+
or not private_mode_is_safe(info.st_mode)):
|
|
171
177
|
raise OSError("Railmux runtime state directory is not private")
|
|
172
178
|
|
|
173
179
|
|
|
@@ -14,8 +14,14 @@ from railmux import restart_state, tmux_ctl
|
|
|
14
14
|
from railmux.atomic_file import atomic_write_text
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
_VERSION =
|
|
17
|
+
_VERSION = 9
|
|
18
18
|
_KEYS = ("F8", "F9")
|
|
19
|
+
_STATUS_CLICK_KEYS = (
|
|
20
|
+
"MouseDown1Status",
|
|
21
|
+
"MouseDown1Control0",
|
|
22
|
+
"MouseDown1Control1",
|
|
23
|
+
"MouseDown1Control2",
|
|
24
|
+
)
|
|
19
25
|
_MAX_STATE_BYTES = 64 * 1024
|
|
20
26
|
|
|
21
27
|
|
|
@@ -79,7 +85,7 @@ class SharedTmuxBindingManager:
|
|
|
79
85
|
info = self._state_path.lstat()
|
|
80
86
|
if (not stat.S_ISREG(info.st_mode)
|
|
81
87
|
or info.st_uid != os.getuid()
|
|
82
|
-
or info.st_mode
|
|
88
|
+
or not restart_state.private_mode_is_safe(info.st_mode)
|
|
83
89
|
or info.st_size > _MAX_STATE_BYTES):
|
|
84
90
|
return None
|
|
85
91
|
raw = json.loads(self._state_path.read_text(encoding="utf-8"))
|
|
@@ -114,12 +120,18 @@ class SharedTmuxBindingManager:
|
|
|
114
120
|
and all(value is None or isinstance(value, str)
|
|
115
121
|
for value in right_click_backup.values())
|
|
116
122
|
)
|
|
117
|
-
|
|
123
|
+
legacy_status_click_valid = (
|
|
118
124
|
isinstance(status_click_backup, dict)
|
|
119
125
|
and set(status_click_backup) == {"MouseDown1Status"}
|
|
120
126
|
and all(value is None or isinstance(value, str)
|
|
121
127
|
for value in status_click_backup.values())
|
|
122
128
|
)
|
|
129
|
+
status_click_valid = (
|
|
130
|
+
isinstance(status_click_backup, dict)
|
|
131
|
+
and set(status_click_backup) == set(_STATUS_CLICK_KEYS)
|
|
132
|
+
and all(value is None or isinstance(value, str)
|
|
133
|
+
for value in status_click_backup.values())
|
|
134
|
+
)
|
|
123
135
|
termux_tap_valid = (
|
|
124
136
|
isinstance(termux_tap_backup, dict)
|
|
125
137
|
and set(termux_tap_backup) == {"MouseDown1Pane"}
|
|
@@ -149,7 +161,13 @@ class SharedTmuxBindingManager:
|
|
|
149
161
|
or (not selection_hook_managed
|
|
150
162
|
and selection_hook_index is not None)))
|
|
151
163
|
or (raw["version"] >= 5
|
|
152
|
-
and (
|
|
164
|
+
and ((raw["version"] < 9
|
|
165
|
+
and not (
|
|
166
|
+
legacy_status_click_valid
|
|
167
|
+
or status_click_valid
|
|
168
|
+
))
|
|
169
|
+
or (raw["version"] >= 9
|
|
170
|
+
and not status_click_valid)
|
|
153
171
|
or not isinstance(status_click_managed, bool)))
|
|
154
172
|
or (raw["version"] >= 8
|
|
155
173
|
and (not termux_tap_valid
|
|
@@ -293,6 +311,29 @@ class SharedTmuxBindingManager:
|
|
|
293
311
|
or {"MouseDown1Pane": None}
|
|
294
312
|
)
|
|
295
313
|
upgraded = True
|
|
314
|
+
if state["version"] < 9:
|
|
315
|
+
if state["status_click_managed"]:
|
|
316
|
+
if set(state["status_click_backup"]) != set(
|
|
317
|
+
_STATUS_CLICK_KEYS
|
|
318
|
+
):
|
|
319
|
+
status_click_backup = (
|
|
320
|
+
tmux_ctl.upgrade_root_status_click_binding(
|
|
321
|
+
state["status_click_backup"],
|
|
322
|
+
state["token"],
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
if status_click_backup is None:
|
|
326
|
+
return False
|
|
327
|
+
state["status_click_backup"] = (
|
|
328
|
+
status_click_backup)
|
|
329
|
+
else:
|
|
330
|
+
state["status_click_backup"] = {
|
|
331
|
+
key: None for key in _STATUS_CLICK_KEYS
|
|
332
|
+
}
|
|
333
|
+
# tmux 3.7 routes control ranges through dedicated
|
|
334
|
+
# MouseDown1ControlN keys. Capture all originals before
|
|
335
|
+
# reinstalling the shared status-click lease.
|
|
336
|
+
upgraded = True
|
|
296
337
|
if upgraded:
|
|
297
338
|
state["version"] = _VERSION
|
|
298
339
|
state["phase"] = "installing"
|
|
@@ -341,17 +382,14 @@ class SharedTmuxBindingManager:
|
|
|
341
382
|
)
|
|
342
383
|
if state["status_click_managed"]:
|
|
343
384
|
current_status = (
|
|
344
|
-
tmux_ctl.read_root_status_click_binding()
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
and tmux_ctl.root_status_click_binding_is_original_or_owned(
|
|
350
|
-
current_status,
|
|
351
|
-
state["status_click_backup"].get(
|
|
352
|
-
"MouseDown1Status"),
|
|
385
|
+
tmux_ctl.read_root_status_click_binding())
|
|
386
|
+
safe = safe and all(
|
|
387
|
+
tmux_ctl.root_status_click_binding_is_original_or_owned(
|
|
388
|
+
current_status.get(key),
|
|
389
|
+
state["status_click_backup"].get(key),
|
|
353
390
|
token,
|
|
354
391
|
)
|
|
392
|
+
for key in _STATUS_CLICK_KEYS
|
|
355
393
|
)
|
|
356
394
|
if state["termux_tap_managed"]:
|
|
357
395
|
current_tap = (
|
|
@@ -543,7 +581,8 @@ class SharedTmuxBindingManager:
|
|
|
543
581
|
"selection_hook_index": selection_hook_index,
|
|
544
582
|
"status_click_managed": status_click_backup is not None,
|
|
545
583
|
"status_click_backup": (
|
|
546
|
-
status_click_backup
|
|
584
|
+
status_click_backup
|
|
585
|
+
or {key: None for key in _STATUS_CLICK_KEYS}),
|
|
547
586
|
"termux_tap_managed": termux_tap_backup is not None,
|
|
548
587
|
"termux_tap_backup": (
|
|
549
588
|
termux_tap_backup or {"MouseDown1Pane": None}),
|