railmux 0.4.2.dev1__tar.gz → 0.4.2.dev2__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.2.dev1/src/railmux.egg-info → railmux-0.4.2.dev2}/PKG-INFO +1 -1
- railmux-0.4.2.dev2/src/railmux/__init__.py +1 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/tmux_binding_manager.py +1 -1
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/tmux_ctl.py +108 -24
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/app.py +63 -2
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_app_codex_placeholder.py +60 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_tmux_ctl.py +49 -6
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_tmux_integration.py +2 -0
- railmux-0.4.2.dev1/src/railmux/__init__.py +0 -1
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/LICENSE +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/MANIFEST.in +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/README.md +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/pyproject.toml +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/setup.cfg +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/__main__.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/background_index.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/cli.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/config.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/diagnostic_contract.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/discovery.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/favorites.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/image_paste.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/launcher.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/local_open.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/models.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/modes.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/preview_pager.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/project_favorites.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/release_version.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/renames.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/self_update.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/session_index.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/settings.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ssh_preflight.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/terminal_screen.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/tmux_capabilities.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/transcript.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/app_test_harness.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_app_quit.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_app_restart_restore.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_app_running_recovery.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_background_index.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_cli.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_code_map.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_codex_index.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_config.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_config_cli.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_discovery.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_display_transport.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_fast_display_bootstrap.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_fast_display_history.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_fast_display_protocol.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_fast_display_selection.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_fast_display_server.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_fast_display_terminal.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_help_agent.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_image_paste.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_keymap.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_launcher.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_local_open.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_modals.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_modes.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_new_project.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_options.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_path_codec.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_preview_pager.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_project_favorites.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_release_notes.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_release_version.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_remote_config.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_renames.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_restart_state.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_running_pane.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_running_sort.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_self_update.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_session_cache.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_session_index.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_session_lease.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_settings.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_sidebar.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_status.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_statusbar.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_system_deps.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_tmux_capabilities.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_transcript.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_widgets.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_synchronized_screen.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tools/release_notes.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.2.dev1 → railmux-0.4.2.dev2}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.2.dev2"
|
|
@@ -110,7 +110,7 @@ class SharedTmuxBindingManager:
|
|
|
110
110
|
selection_hook_index = raw.get("selection_hook_index")
|
|
111
111
|
prefix_valid = (
|
|
112
112
|
isinstance(prefix_backup, dict)
|
|
113
|
-
and set(prefix_backup)
|
|
113
|
+
and set(prefix_backup) in ({"Tab"}, {"Tab", "Tab.note"})
|
|
114
114
|
and all(value is None or isinstance(value, str)
|
|
115
115
|
for value in prefix_backup.values())
|
|
116
116
|
)
|
|
@@ -29,6 +29,7 @@ class ServerSnapshot:
|
|
|
29
29
|
sessions: frozenset[str]
|
|
30
30
|
panes: frozenset[str]
|
|
31
31
|
session_pids: tuple[tuple[str, int], ...] = ()
|
|
32
|
+
pane_titles: tuple[tuple[str, str, str], ...] = ()
|
|
32
33
|
|
|
33
34
|
def pane_pid_for(self, session_name: str) -> int | None:
|
|
34
35
|
"""Return the first pane PID captured for *session_name*."""
|
|
@@ -37,6 +38,22 @@ class ServerSnapshot:
|
|
|
37
38
|
return pid
|
|
38
39
|
return None
|
|
39
40
|
|
|
41
|
+
def pane_title_for(self, pane_id: str) -> str | None:
|
|
42
|
+
"""Return one live pane's terminal title from this same snapshot."""
|
|
43
|
+
titles = {
|
|
44
|
+
title for _session_name, candidate, title in self.pane_titles
|
|
45
|
+
if candidate == pane_id
|
|
46
|
+
}
|
|
47
|
+
return next(iter(titles)) if len(titles) == 1 else None
|
|
48
|
+
|
|
49
|
+
def pane_title_for_session(self, session_name: str) -> str | None:
|
|
50
|
+
"""Return the title of a single-live-pane provider session."""
|
|
51
|
+
matches = [
|
|
52
|
+
title for candidate, _pane_id, title in self.pane_titles
|
|
53
|
+
if candidate == session_name
|
|
54
|
+
]
|
|
55
|
+
return matches[0] if len(matches) == 1 else None
|
|
56
|
+
|
|
40
57
|
|
|
41
58
|
@dataclass(frozen=True)
|
|
42
59
|
class PaneIdentity:
|
|
@@ -277,7 +294,8 @@ def server_snapshot() -> ServerSnapshot | None:
|
|
|
277
294
|
out = subprocess.check_output(
|
|
278
295
|
[
|
|
279
296
|
"tmux", "list-panes", "-a", "-F",
|
|
280
|
-
"#{session_name}\t#{pane_id}\t#{pane_pid}\t#{pane_dead}"
|
|
297
|
+
"#{session_name}\t#{pane_id}\t#{pane_pid}\t#{pane_dead}"
|
|
298
|
+
"\t#{q:pane_title}",
|
|
281
299
|
],
|
|
282
300
|
stderr=subprocess.DEVNULL,
|
|
283
301
|
).decode()
|
|
@@ -287,26 +305,45 @@ def server_snapshot() -> ServerSnapshot | None:
|
|
|
287
305
|
sessions: set[str] = set()
|
|
288
306
|
panes: set[str] = set()
|
|
289
307
|
session_pids: dict[str, int] = {}
|
|
308
|
+
pane_titles: list[tuple[str, str, str]] = []
|
|
290
309
|
for line in out.splitlines():
|
|
291
|
-
fields = line.split("\t",
|
|
292
|
-
if len(fields) !=
|
|
310
|
+
fields = line.split("\t", 4)
|
|
311
|
+
if (len(fields) != 5 or not fields[0] or not fields[1]
|
|
312
|
+
or not fields[3]):
|
|
293
313
|
return None
|
|
294
|
-
session_name, pane_id, raw_pid, pane_dead = fields
|
|
314
|
+
session_name, pane_id, raw_pid, pane_dead, raw_title = fields
|
|
315
|
+
if raw_title:
|
|
316
|
+
try:
|
|
317
|
+
decoded = shlex.split(raw_title)
|
|
318
|
+
except ValueError:
|
|
319
|
+
return None
|
|
320
|
+
if len(decoded) != 1:
|
|
321
|
+
return None
|
|
322
|
+
pane_title = decoded[0]
|
|
323
|
+
else:
|
|
324
|
+
pane_title = ""
|
|
295
325
|
if pane_dead not in {"0", "1"}:
|
|
296
326
|
return None
|
|
327
|
+
if pane_dead == "1":
|
|
328
|
+
# tmux 3.8 renders pane_pid as empty for a dead remain-on-exit
|
|
329
|
+
# pane. It is excluded from this live snapshot, so no PID is
|
|
330
|
+
# required here.
|
|
331
|
+
continue
|
|
297
332
|
try:
|
|
298
333
|
pane_pid = int(raw_pid)
|
|
299
334
|
except ValueError:
|
|
300
335
|
return None
|
|
301
|
-
if
|
|
302
|
-
|
|
336
|
+
if pane_pid <= 0:
|
|
337
|
+
return None
|
|
303
338
|
sessions.add(session_name)
|
|
304
339
|
panes.add(pane_id)
|
|
305
340
|
session_pids.setdefault(session_name, pane_pid)
|
|
341
|
+
pane_titles.append((session_name, pane_id, pane_title))
|
|
306
342
|
return ServerSnapshot(
|
|
307
343
|
frozenset(sessions),
|
|
308
344
|
frozenset(panes),
|
|
309
345
|
tuple(sorted(session_pids.items())),
|
|
346
|
+
tuple(sorted(pane_titles)),
|
|
310
347
|
)
|
|
311
348
|
|
|
312
349
|
|
|
@@ -653,13 +690,20 @@ def pane_identity(pane_id: str) -> PaneIdentity | None:
|
|
|
653
690
|
fields = raw.split("\t")
|
|
654
691
|
if len(fields) != 8:
|
|
655
692
|
return None
|
|
693
|
+
dead = fields[5] == "1"
|
|
694
|
+
# tmux 3.8 leaves ``pane_pid`` empty after a pane dies. Keep zero as
|
|
695
|
+
# an internal dead-pane sentinel so callers can still identify and
|
|
696
|
+
# reap the exact remain-on-exit pane; live panes retain positive PIDs.
|
|
697
|
+
pane_pid = 0 if dead and not fields[1] else int(fields[1])
|
|
698
|
+
if pane_pid < 0 or (not dead and pane_pid == 0):
|
|
699
|
+
return None
|
|
656
700
|
return PaneIdentity(
|
|
657
701
|
pane_id=fields[0],
|
|
658
|
-
pane_pid=
|
|
702
|
+
pane_pid=pane_pid,
|
|
659
703
|
session_name=fields[2],
|
|
660
704
|
session_id=fields[3],
|
|
661
705
|
window_id=fields[4],
|
|
662
|
-
dead=
|
|
706
|
+
dead=dead,
|
|
663
707
|
width=int(fields[6]),
|
|
664
708
|
height=int(fields[7]),
|
|
665
709
|
)
|
|
@@ -735,17 +779,24 @@ def _tmux_state_snapshot(
|
|
|
735
779
|
):
|
|
736
780
|
return None
|
|
737
781
|
attached = int(attached_raw)
|
|
782
|
+
dead = dead_raw == "1"
|
|
783
|
+
pane_pid = (
|
|
784
|
+
0 if dead and not pane_pid_raw else int(pane_pid_raw)
|
|
785
|
+
)
|
|
738
786
|
pane = PaneIdentity(
|
|
739
787
|
pane_id=pane_id,
|
|
740
|
-
pane_pid=
|
|
788
|
+
pane_pid=pane_pid,
|
|
741
789
|
session_name=actual_name,
|
|
742
790
|
session_id=session_id,
|
|
743
791
|
window_id=window_id,
|
|
744
|
-
dead=
|
|
792
|
+
dead=dead,
|
|
745
793
|
width=int(width_raw),
|
|
746
794
|
height=int(height_raw),
|
|
747
795
|
)
|
|
748
|
-
if attached < 0
|
|
796
|
+
if (attached < 0
|
|
797
|
+
or (pane.dead and pane.pane_pid < 0)
|
|
798
|
+
or (not pane.dead and pane.pane_pid <= 0)
|
|
799
|
+
or pane.width <= 0 or pane.height <= 0):
|
|
749
800
|
return None
|
|
750
801
|
key = (actual_name, session_id)
|
|
751
802
|
group = grouped.setdefault(
|
|
@@ -3221,6 +3272,37 @@ def read_prefix_target_binding() -> PrefixTargetBindingBackup:
|
|
|
3221
3272
|
return {_PREFIX_TARGET_KEY: _read_key_binding("prefix", _PREFIX_TARGET_KEY)}
|
|
3222
3273
|
|
|
3223
3274
|
|
|
3275
|
+
def _prefix_target_note() -> str | None:
|
|
3276
|
+
"""Return Prefix-Tab's descriptive note without its rendered key prefix."""
|
|
3277
|
+
if tmux_version() < (3, 2):
|
|
3278
|
+
return None
|
|
3279
|
+
try:
|
|
3280
|
+
raw = subprocess.check_output(
|
|
3281
|
+
["tmux", "list-keys", "-N", "-T", "prefix", "-1",
|
|
3282
|
+
_PREFIX_TARGET_KEY],
|
|
3283
|
+
stderr=subprocess.DEVNULL,
|
|
3284
|
+
).decode().strip()
|
|
3285
|
+
except (OSError, subprocess.CalledProcessError, UnicodeError):
|
|
3286
|
+
return None
|
|
3287
|
+
fields = raw.split(None, 2)
|
|
3288
|
+
return fields[2] if len(fields) == 3 else None
|
|
3289
|
+
|
|
3290
|
+
|
|
3291
|
+
def _tmux_38_default_prefix_target(binding: str, note: str | None) -> bool:
|
|
3292
|
+
"""Whether *binding* is tmux 3.8's annotated stock Prefix-Tab action."""
|
|
3293
|
+
if tmux_version() < (3, 8) or note != "Switch to a window":
|
|
3294
|
+
return False
|
|
3295
|
+
try:
|
|
3296
|
+
command = tuple(shlex.split(_binding_command(binding)))
|
|
3297
|
+
except ValueError:
|
|
3298
|
+
return False
|
|
3299
|
+
return command == (
|
|
3300
|
+
"new-pane", "-E", "-X", "0", "-Y", "0", "-x", "75%", "-y",
|
|
3301
|
+
"30%", ";", "move-pane", "-P", "bottom-centre", ";",
|
|
3302
|
+
"switch-mode", "-kw",
|
|
3303
|
+
)
|
|
3304
|
+
|
|
3305
|
+
|
|
3224
3306
|
def prepare_prefix_target_binding() -> PrefixTargetBindingBackup | None:
|
|
3225
3307
|
if tmux_version() < (2, 7):
|
|
3226
3308
|
return None
|
|
@@ -3229,8 +3311,9 @@ def prepare_prefix_target_binding() -> PrefixTargetBindingBackup | None:
|
|
|
3229
3311
|
if binding and _PREFIX_TARGET_MARKER in binding:
|
|
3230
3312
|
return None
|
|
3231
3313
|
# A server-global wrapper cannot preserve repeat semantics only outside
|
|
3232
|
-
# Railmux.
|
|
3233
|
-
#
|
|
3314
|
+
# Railmux. Preserve tmux 3.8's known stock note explicitly; leave unknown
|
|
3315
|
+
# annotated user bindings untouched rather than risking lossy restoration.
|
|
3316
|
+
note: str | None = None
|
|
3234
3317
|
if binding is not None:
|
|
3235
3318
|
if re.match(r"^bind-key\s+-r\b", binding):
|
|
3236
3319
|
return None
|
|
@@ -3239,17 +3322,10 @@ def prepare_prefix_target_binding() -> PrefixTargetBindingBackup | None:
|
|
|
3239
3322
|
except ValueError:
|
|
3240
3323
|
return None
|
|
3241
3324
|
if tmux_version() >= (3, 2):
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
["tmux", "list-keys", "-N", "-T", "prefix", "-1",
|
|
3245
|
-
_PREFIX_TARGET_KEY],
|
|
3246
|
-
stderr=subprocess.DEVNULL,
|
|
3247
|
-
).decode().strip()
|
|
3248
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
3249
|
-
return None
|
|
3250
|
-
if note:
|
|
3325
|
+
note = _prefix_target_note()
|
|
3326
|
+
if note and not _tmux_38_default_prefix_target(binding, note):
|
|
3251
3327
|
return None
|
|
3252
|
-
return backup
|
|
3328
|
+
return {**backup, "Tab.note": note}
|
|
3253
3329
|
|
|
3254
3330
|
|
|
3255
3331
|
def set_prefix_target_binding(
|
|
@@ -3345,7 +3421,15 @@ def restore_prefix_target_binding(
|
|
|
3345
3421
|
prefix="railmux-prefix-target-", suffix=".conf")
|
|
3346
3422
|
try:
|
|
3347
3423
|
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
3348
|
-
|
|
3424
|
+
note = backup.get("Tab.note")
|
|
3425
|
+
restored = original
|
|
3426
|
+
if note:
|
|
3427
|
+
restored = original.replace(
|
|
3428
|
+
"bind-key",
|
|
3429
|
+
f"bind-key -N {shlex.quote(note)}",
|
|
3430
|
+
1,
|
|
3431
|
+
)
|
|
3432
|
+
fh.write(restored + "\n")
|
|
3349
3433
|
subprocess.check_call(
|
|
3350
3434
|
["tmux", "source-file", path],
|
|
3351
3435
|
stdout=subprocess.DEVNULL,
|
|
@@ -9193,7 +9193,7 @@ class App:
|
|
|
9193
9193
|
# clicking it spawns a duplicate session; and `force_projects` above
|
|
9194
9194
|
# stays stuck True, defeating the 3s project-scan cache. Codex
|
|
9195
9195
|
# resolution must run too or neither ever clears.
|
|
9196
|
-
self._resolve_placeholders(projects)
|
|
9196
|
+
self._resolve_placeholders(projects, server)
|
|
9197
9197
|
running_ids = self._running_session_ids()
|
|
9198
9198
|
if self._selected_project is not None:
|
|
9199
9199
|
matched = next((p for p in projects if p.encoded_name == self._selected_project.encoded_name), None)
|
|
@@ -10002,7 +10002,66 @@ class App:
|
|
|
10002
10002
|
]
|
|
10003
10003
|
self._running_pane.set_running(entries)
|
|
10004
10004
|
|
|
10005
|
-
|
|
10005
|
+
@staticmethod
|
|
10006
|
+
def _codex_terminal_session_title(
|
|
10007
|
+
pane_title: str | None,
|
|
10008
|
+
project: Project,
|
|
10009
|
+
) -> str | None:
|
|
10010
|
+
"""Extract a bounded Codex session title from its terminal title.
|
|
10011
|
+
|
|
10012
|
+
Current Codex releases publish ``<title> | <workspace>`` and prefix
|
|
10013
|
+
the title with one braille spinner cell while a turn is active. Treat
|
|
10014
|
+
every other shape as untrusted display text: a shell or tool running
|
|
10015
|
+
in the pane can also set the terminal title.
|
|
10016
|
+
"""
|
|
10017
|
+
if pane_title is None:
|
|
10018
|
+
return None
|
|
10019
|
+
value = pane_title.strip()
|
|
10020
|
+
if value and "\u2800" <= value[0] <= "\u28ff":
|
|
10021
|
+
value = value[1:].lstrip()
|
|
10022
|
+
suffix = f" | {project.display_name}"
|
|
10023
|
+
if not value.endswith(suffix):
|
|
10024
|
+
return None
|
|
10025
|
+
title = value[:-len(suffix)].strip()
|
|
10026
|
+
if not title or len(title) > 512 or not title.isprintable():
|
|
10027
|
+
return None
|
|
10028
|
+
return title
|
|
10029
|
+
|
|
10030
|
+
def _refresh_codex_placeholder_label(
|
|
10031
|
+
self,
|
|
10032
|
+
running: _Running,
|
|
10033
|
+
project: Project,
|
|
10034
|
+
server: tmux_ctl.ServerSnapshot | None,
|
|
10035
|
+
) -> None:
|
|
10036
|
+
"""Refresh only an unresolved placeholder's human-readable label.
|
|
10037
|
+
|
|
10038
|
+
New Codex app-server releases no longer keep their rollout JSONL open
|
|
10039
|
+
in the pane process tree, so exact UUID promotion can correctly remain
|
|
10040
|
+
unresolved. The terminal title still belongs to this exact pane and
|
|
10041
|
+
is safe as a *display* hint because it is read from the exact owned
|
|
10042
|
+
pane and accepted only in Codex's workspace-qualified title shape. It
|
|
10043
|
+
deliberately does not promote identity: Resume/Delete keep their
|
|
10044
|
+
existing fail-closed rules.
|
|
10045
|
+
"""
|
|
10046
|
+
if server is None or running.session_type != "codex":
|
|
10047
|
+
return
|
|
10048
|
+
real_pane = self._display_transport().displayed_real_pane(
|
|
10049
|
+
running.tmux_name)
|
|
10050
|
+
pane_title = (
|
|
10051
|
+
server.pane_title_for(real_pane)
|
|
10052
|
+
if isinstance(real_pane, str) and real_pane
|
|
10053
|
+
else server.pane_title_for_session(running.tmux_name)
|
|
10054
|
+
)
|
|
10055
|
+
title = self._codex_terminal_session_title(pane_title, project)
|
|
10056
|
+
if title is None:
|
|
10057
|
+
return
|
|
10058
|
+
running.label = f"{project.display_name}/{title}"
|
|
10059
|
+
|
|
10060
|
+
def _resolve_placeholders(
|
|
10061
|
+
self,
|
|
10062
|
+
projects: list[Project],
|
|
10063
|
+
server: tmux_ctl.ServerSnapshot | None = None,
|
|
10064
|
+
) -> None:
|
|
10006
10065
|
"""Re-key any `__new__-N` placeholder to its real session_id.
|
|
10007
10066
|
|
|
10008
10067
|
For each live placeholder, look at its project's sessions and pick the
|
|
@@ -10059,6 +10118,8 @@ class App:
|
|
|
10059
10118
|
and s.session_id not in r.pre_launch_ids # another process's (#12)
|
|
10060
10119
|
and s.last_mtime + 1.0 >= r.created_at
|
|
10061
10120
|
]
|
|
10121
|
+
self._refresh_codex_placeholder_label(
|
|
10122
|
+
r, project, server)
|
|
10062
10123
|
# #12: prefer EXACT child→rollout correlation over the heuristic. The
|
|
10063
10124
|
# codex process in this placeholder's pane holds its own rollout open;
|
|
10064
10125
|
# its filename UUID is the exact session_id. This defeats the
|
|
@@ -483,6 +483,66 @@ def test_resolve_placeholders_empty_correlation_waits_not_fallback(monkeypatch):
|
|
|
483
483
|
assert unrelated not in app._running # heuristic did NOT fire
|
|
484
484
|
|
|
485
485
|
|
|
486
|
+
@pytest.mark.parametrize(
|
|
487
|
+
"pane_title",
|
|
488
|
+
(
|
|
489
|
+
"Real session | test-proj",
|
|
490
|
+
"\u2827 Real session | test-proj",
|
|
491
|
+
),
|
|
492
|
+
)
|
|
493
|
+
def test_unresolved_codex_placeholder_uses_exact_pane_title_for_label(
|
|
494
|
+
pane_title,
|
|
495
|
+
):
|
|
496
|
+
"""A newer Codex may publish no rollout fd, but its exact pane can still
|
|
497
|
+
replace the stale ``(new)`` display label without weakening UUID binding."""
|
|
498
|
+
proj = _project()
|
|
499
|
+
running = _Running(
|
|
500
|
+
key="__new__-tok-1",
|
|
501
|
+
tmux_name="cx-new----tok-1",
|
|
502
|
+
label="test-proj/(new)",
|
|
503
|
+
project=proj,
|
|
504
|
+
placeholder_path=proj.real_path,
|
|
505
|
+
created_at=999.0,
|
|
506
|
+
session_type="codex",
|
|
507
|
+
allow_heuristic_resolution=False,
|
|
508
|
+
)
|
|
509
|
+
app = App.__new__(App)
|
|
510
|
+
app._running = {running.key: running}
|
|
511
|
+
app._codex_index = MagicMock()
|
|
512
|
+
# The pane title is available before the background index publishes the
|
|
513
|
+
# rollout, so display refresh must not depend on candidate discovery.
|
|
514
|
+
app._codex_index.sessions_for_cwd.return_value = []
|
|
515
|
+
app._correlate_codex_rollout = MagicMock(return_value=set())
|
|
516
|
+
app._display_transport_manager = MagicMock()
|
|
517
|
+
app._display_transport_manager.displayed_real_pane.return_value = None
|
|
518
|
+
server = tmux_ctl.ServerSnapshot(
|
|
519
|
+
sessions=frozenset({running.tmux_name}),
|
|
520
|
+
panes=frozenset({"%7"}),
|
|
521
|
+
pane_titles=((running.tmux_name, "%7", pane_title),),
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
app._resolve_placeholders([proj], server)
|
|
525
|
+
|
|
526
|
+
assert running.key in app._running
|
|
527
|
+
assert running.is_placeholder
|
|
528
|
+
assert running.label == "test-proj/Real session"
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@pytest.mark.parametrize(
|
|
532
|
+
"pane_title",
|
|
533
|
+
(
|
|
534
|
+
"bash",
|
|
535
|
+
"Real session | another-project",
|
|
536
|
+
"Real\n session | test-proj",
|
|
537
|
+
),
|
|
538
|
+
)
|
|
539
|
+
def test_unresolved_codex_placeholder_rejects_untrusted_pane_title(
|
|
540
|
+
pane_title,
|
|
541
|
+
):
|
|
542
|
+
proj = _project()
|
|
543
|
+
assert App._codex_terminal_session_title(pane_title, proj) is None
|
|
544
|
+
|
|
545
|
+
|
|
486
546
|
def test_correlate_codex_rollout_fails_closed_without_config(monkeypatch):
|
|
487
547
|
"""A helper failure on procfs must wait, never unlock the heuristic."""
|
|
488
548
|
monkeypatch.setattr(tmux_ctl, "proc_fs_available", lambda: True)
|
|
@@ -195,10 +195,10 @@ def test_copy_to_clipboard_rejects_unsafe_or_unbounded_payload():
|
|
|
195
195
|
|
|
196
196
|
def test_server_snapshot_collects_sessions_and_panes():
|
|
197
197
|
output = (
|
|
198
|
-
"cc-one\t%1\t101\t0\n"
|
|
199
|
-
"cc-one\t%2\t102\t1\n"
|
|
200
|
-
"cc-two\t%3\t201\t0\n"
|
|
201
|
-
"cc-dead\t%4\t301\t1\n"
|
|
198
|
+
"cc-one\t%1\t101\t0\tReview\\ code\\ \\|\\ repo\n"
|
|
199
|
+
"cc-one\t%2\t102\t1\told\n"
|
|
200
|
+
"cc-two\t%3\t201\t0\tSecond\n"
|
|
201
|
+
"cc-dead\t%4\t301\t1\tdead\n"
|
|
202
202
|
)
|
|
203
203
|
with patch("railmux.tmux_ctl.in_tmux", return_value=True), \
|
|
204
204
|
_mock_check_output(output) as call:
|
|
@@ -210,12 +210,30 @@ def test_server_snapshot_collects_sessions_and_panes():
|
|
|
210
210
|
assert snapshot.session_pids == (("cc-one", 101), ("cc-two", 201))
|
|
211
211
|
assert snapshot.pane_pid_for("cc-one") == 101
|
|
212
212
|
assert snapshot.pane_pid_for("missing") is None
|
|
213
|
+
assert snapshot.pane_title_for("%1") == "Review code | repo"
|
|
214
|
+
assert snapshot.pane_title_for_session("cc-one") == "Review code | repo"
|
|
215
|
+
assert snapshot.pane_title_for_session("missing") is None
|
|
213
216
|
assert call.call_args.args[0] == [
|
|
214
217
|
"tmux", "list-panes", "-a", "-F",
|
|
215
|
-
"#{session_name}\t#{pane_id}\t#{pane_pid}\t#{pane_dead}"
|
|
218
|
+
"#{session_name}\t#{pane_id}\t#{pane_pid}\t#{pane_dead}"
|
|
219
|
+
"\t#{q:pane_title}",
|
|
216
220
|
]
|
|
217
221
|
|
|
218
222
|
|
|
223
|
+
def test_server_snapshot_accepts_tmux_38_empty_dead_pane_pid():
|
|
224
|
+
output = (
|
|
225
|
+
"cc-live\t%1\t101\t0\tlive\n"
|
|
226
|
+
"cc-dead\t%2\t\t1\tdead\n"
|
|
227
|
+
)
|
|
228
|
+
with patch("railmux.tmux_ctl.in_tmux", return_value=True), \
|
|
229
|
+
_mock_check_output(output):
|
|
230
|
+
snapshot = server_snapshot()
|
|
231
|
+
|
|
232
|
+
assert snapshot is not None
|
|
233
|
+
assert snapshot.sessions == frozenset({"cc-live"})
|
|
234
|
+
assert snapshot.panes == frozenset({"%1"})
|
|
235
|
+
|
|
236
|
+
|
|
219
237
|
def test_server_snapshot_rejects_malformed_output():
|
|
220
238
|
with patch("railmux.tmux_ctl.in_tmux", return_value=True), \
|
|
221
239
|
_mock_check_output("cc-one %1\n"):
|
|
@@ -224,7 +242,7 @@ def test_server_snapshot_rejects_malformed_output():
|
|
|
224
242
|
|
|
225
243
|
def test_server_snapshot_rejects_unknown_dead_state():
|
|
226
244
|
with patch("railmux.tmux_ctl.in_tmux", return_value=True), \
|
|
227
|
-
_mock_check_output("cc-one\t%1\t101\tmaybe\n"):
|
|
245
|
+
_mock_check_output("cc-one\t%1\t101\tmaybe\ttitle\n"):
|
|
228
246
|
assert server_snapshot() is None
|
|
229
247
|
|
|
230
248
|
|
|
@@ -289,6 +307,15 @@ def test_pane_identity_parses_stable_location_outside_tmux():
|
|
|
289
307
|
"tmux", "display-message", "-p", "-t", "%7"]
|
|
290
308
|
|
|
291
309
|
|
|
310
|
+
def test_pane_identity_accepts_tmux_38_empty_dead_pane_pid():
|
|
311
|
+
output = "%7\t\tagent\t$2\t@3\t1\t91\t31\n"
|
|
312
|
+
with _mock_check_output(output):
|
|
313
|
+
pane = tmux_ctl.pane_identity("%7")
|
|
314
|
+
|
|
315
|
+
assert pane == tmux_ctl.PaneIdentity(
|
|
316
|
+
"%7", 0, "agent", "$2", "@3", True, 91, 31)
|
|
317
|
+
|
|
318
|
+
|
|
292
319
|
def test_session_topology_requires_exact_server_results(monkeypatch):
|
|
293
320
|
output = b"agent\t$2\t0\t@3\t%7\t4242\t0\t91\t31\n"
|
|
294
321
|
check = MagicMock(return_value=output)
|
|
@@ -1142,6 +1169,22 @@ def test_prefix_target_binding_rejects_unpreservable_user_binding():
|
|
|
1142
1169
|
{"Tab": "not replayable tmux config"}, "owner123")
|
|
1143
1170
|
|
|
1144
1171
|
|
|
1172
|
+
def test_prefix_target_binding_accepts_tmux_38_stock_note():
|
|
1173
|
+
stock = (
|
|
1174
|
+
'bind-key -T prefix Tab new-pane -E -X 0 -Y 0 -x "75%" '
|
|
1175
|
+
'-y "30%" \\; move-pane -P bottom-centre \\; switch-mode -kw'
|
|
1176
|
+
)
|
|
1177
|
+
with patch.object(tmux_ctl, "tmux_version", return_value=(3, 8)), \
|
|
1178
|
+
patch.object(tmux_ctl, "read_prefix_target_binding",
|
|
1179
|
+
return_value={"Tab": stock}), \
|
|
1180
|
+
patch.object(tmux_ctl, "_prefix_target_note",
|
|
1181
|
+
return_value="Switch to a window"):
|
|
1182
|
+
assert tmux_ctl.prepare_prefix_target_binding() == {
|
|
1183
|
+
"Tab": stock,
|
|
1184
|
+
"Tab.note": "Switch to a window",
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
|
|
1145
1188
|
def test_unset_window_user_option_requires_exact_owner_value():
|
|
1146
1189
|
with _mock_check_output("%1"), _mock_check_call() as call:
|
|
1147
1190
|
assert tmux_ctl.unset_window_user_option_if_value(
|
|
@@ -3039,6 +3039,7 @@ def test_real_tmux_binding_manager_round_trip_and_user_reload(
|
|
|
3039
3039
|
)
|
|
3040
3040
|
original = tmux_ctl.read_root_function_bindings()
|
|
3041
3041
|
original_prefix_tab = tmux_ctl.read_prefix_target_binding()
|
|
3042
|
+
original_prefix_note = tmux_ctl._prefix_target_note()
|
|
3042
3043
|
original_right_click = tmux_ctl.read_root_right_click_binding()
|
|
3043
3044
|
original_status_click = tmux_ctl.read_root_status_click_binding()
|
|
3044
3045
|
original_termux_tap = tmux_ctl.read_root_termux_tap_binding()
|
|
@@ -3229,6 +3230,7 @@ def test_real_tmux_binding_manager_round_trip_and_user_reload(
|
|
|
3229
3230
|
assert restored["F8"] == original["F8"]
|
|
3230
3231
|
assert restored["F9"] is not None and "new-user-f9" in restored["F9"]
|
|
3231
3232
|
assert tmux_ctl.read_prefix_target_binding() == original_prefix_tab
|
|
3233
|
+
assert tmux_ctl._prefix_target_note() == original_prefix_note
|
|
3232
3234
|
assert tmux_ctl.read_root_right_click_binding() == original_right_click
|
|
3233
3235
|
assert tmux_ctl.read_root_status_click_binding() == original_status_click
|
|
3234
3236
|
assert tmux_ctl.read_root_termux_tap_binding() == original_termux_tap
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.2.dev1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|