railmux 0.4.1.dev3__tar.gz → 0.4.1.dev5__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.
Files changed (205) hide show
  1. {railmux-0.4.1.dev3/src/railmux.egg-info → railmux-0.4.1.dev5}/PKG-INFO +1 -1
  2. railmux-0.4.1.dev5/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/tmux_ctl.py +33 -4
  4. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/app.py +34 -8
  5. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/windows_attach_relay.py +26 -5
  6. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5/src/railmux.egg-info}/PKG-INFO +1 -1
  7. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_fullscreen_binding.py +22 -1
  8. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_layout_profile.py +1 -0
  9. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_tmux_ctl.py +13 -1
  10. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_tmux_integration.py +21 -9
  11. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_tmux_status.py +28 -3
  12. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_attach_relay.py +114 -0
  13. railmux-0.4.1.dev3/src/railmux/__init__.py +0 -1
  14. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/LICENSE +0 -0
  15. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/MANIFEST.in +0 -0
  16. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/README.md +0 -0
  17. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/pyproject.toml +0 -0
  18. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/setup.cfg +0 -0
  19. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/__main__.py +0 -0
  20. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/atomic_file.py +0 -0
  21. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/background_index.py +0 -0
  22. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/cli.py +0 -0
  23. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/codex_index.py +0 -0
  24. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/config.py +0 -0
  25. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/config_cli.py +0 -0
  26. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/diagnostic_contract.py +0 -0
  27. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/diagnostics.py +0 -0
  28. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/discovery.py +0 -0
  29. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/display_transport.py +0 -0
  30. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/entrypoint.py +0 -0
  31. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/fast_display_client.py +0 -0
  32. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/fast_display_history.py +0 -0
  33. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/fast_display_input.py +0 -0
  34. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/fast_display_protocol.py +0 -0
  35. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/fast_display_server.py +0 -0
  36. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/favorites.py +0 -0
  37. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/fuzzy.py +0 -0
  38. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/help_workspace.py +0 -0
  39. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/launcher.py +0 -0
  40. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/legacy_sessions.py +0 -0
  41. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/local_clipboard.py +0 -0
  42. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/local_open.py +0 -0
  43. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/models.py +0 -0
  44. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/modes.py +0 -0
  45. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/mouse_manager.py +0 -0
  46. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/orphan_marker.py +0 -0
  47. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/pane_surface.py +0 -0
  48. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/path_codec.py +0 -0
  49. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/preview_pager.py +0 -0
  50. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/project_favorites.py +0 -0
  51. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/provider_paths.py +0 -0
  52. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/release_version.py +0 -0
  53. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/remote_config.py +0 -0
  54. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/renames.py +0 -0
  55. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/restart_state.py +0 -0
  56. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/runtime_config.py +0 -0
  57. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/scroll_agent.py +0 -0
  58. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/scroll_manager.py +0 -0
  59. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/selection_isolation.py +0 -0
  60. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/self_update.py +0 -0
  61. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/session_cache.py +0 -0
  62. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/session_index.py +0 -0
  63. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/session_lease.py +0 -0
  64. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/setting_contracts.py +0 -0
  65. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/settings.py +0 -0
  66. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ssh_args.py +0 -0
  67. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ssh_compat.py +0 -0
  68. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ssh_display_diagnostics.py +0 -0
  69. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ssh_doctor.py +0 -0
  70. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ssh_preflight.py +0 -0
  71. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/system_deps.py +0 -0
  72. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/terminal_screen.py +0 -0
  73. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/terminal_status.py +0 -0
  74. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/tmux_binding_manager.py +0 -0
  75. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/tmux_capabilities.py +0 -0
  76. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/tmux_health.py +0 -0
  77. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/tmux_server.py +0 -0
  78. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/tool_panes.py +0 -0
  79. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/transcript.py +0 -0
  80. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/__init__.py +0 -0
  81. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/_widgets.py +0 -0
  82. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/keymap.py +0 -0
  83. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/modals.py +0 -0
  84. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/projects_pane.py +0 -0
  85. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/running_pane.py +0 -0
  86. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/sessions_pane.py +0 -0
  87. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/sidebar.py +0 -0
  88. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/statusbar.py +0 -0
  89. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/ui/workspace.py +0 -0
  90. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/windows_bootstrap.py +0 -0
  91. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/windows_install_log.py +0 -0
  92. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/windows_msys2.py +0 -0
  93. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/windows_pacman.py +0 -0
  94. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/windows_paths.py +0 -0
  95. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/windows_tmux_lifecycle.py +0 -0
  96. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux/windows_ui_transition.py +0 -0
  97. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux.egg-info/SOURCES.txt +0 -0
  98. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux.egg-info/dependency_links.txt +0 -0
  99. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux.egg-info/entry_points.txt +0 -0
  100. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux.egg-info/requires.txt +0 -0
  101. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/src/railmux.egg-info/top_level.txt +0 -0
  102. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/app_test_harness.py +0 -0
  103. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_agent_workspace.py +0 -0
  104. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_app_codex_placeholder.py +0 -0
  105. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_app_quit.py +0 -0
  106. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_app_restart_restore.py +0 -0
  107. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_app_running_recovery.py +0 -0
  108. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_atomic_file.py +0 -0
  109. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_background_index.py +0 -0
  110. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_cli.py +0 -0
  111. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_code_map.py +0 -0
  112. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_codex_delete.py +0 -0
  113. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_codex_index.py +0 -0
  114. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_codex_lineage.py +0 -0
  115. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_codex_transcript.py +0 -0
  116. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_config.py +0 -0
  117. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_config_cli.py +0 -0
  118. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_denested_transport_benchmark.py +0 -0
  119. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_diagnostics.py +0 -0
  120. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_discovery.py +0 -0
  121. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_display_transport.py +0 -0
  122. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_fast_display_bootstrap.py +0 -0
  123. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_fast_display_history.py +0 -0
  124. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_fast_display_protocol.py +0 -0
  125. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_fast_display_selection.py +0 -0
  126. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_fast_display_server.py +0 -0
  127. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_fast_display_terminal.py +0 -0
  128. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_focus_highlight.py +0 -0
  129. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_fuzzy.py +0 -0
  130. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_help_agent.py +0 -0
  131. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_help_workspace.py +0 -0
  132. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_history_cleanup.py +0 -0
  133. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_keymap.py +0 -0
  134. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_launcher.py +0 -0
  135. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_legacy_sessions.py +0 -0
  136. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_local_clipboard.py +0 -0
  137. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_local_open.py +0 -0
  138. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_modals.py +0 -0
  139. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_mode_selection.py +0 -0
  140. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_modes.py +0 -0
  141. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_mouse_manager.py +0 -0
  142. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_new_project.py +0 -0
  143. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_options.py +0 -0
  144. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_orphan_recovery.py +0 -0
  145. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_pane_surface.py +0 -0
  146. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_path_codec.py +0 -0
  147. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_preview_pager.py +0 -0
  148. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_project_favorites.py +0 -0
  149. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_projects_pane.py +0 -0
  150. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_provider_paths.py +0 -0
  151. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_release_notes.py +0 -0
  152. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_release_version.py +0 -0
  153. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_release_workflow.py +0 -0
  154. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_remote_config.py +0 -0
  155. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_renames.py +0 -0
  156. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_restart_state.py +0 -0
  157. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_running_pane.py +0 -0
  158. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_running_sort.py +0 -0
  159. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_runtime_config.py +0 -0
  160. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_scroll_agent.py +0 -0
  161. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_scroll_manager.py +0 -0
  162. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_selection_isolation.py +0 -0
  163. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_self_update.py +0 -0
  164. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_session_cache.py +0 -0
  165. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_session_index.py +0 -0
  166. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_session_lease.py +0 -0
  167. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_sessions_pane.py +0 -0
  168. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_setting_contracts.py +0 -0
  169. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_settings.py +0 -0
  170. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_sidebar.py +0 -0
  171. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_ssh_args.py +0 -0
  172. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_ssh_compat.py +0 -0
  173. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_ssh_display_benchmark.py +0 -0
  174. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_ssh_display_diagnostics.py +0 -0
  175. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_ssh_doctor.py +0 -0
  176. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_status.py +0 -0
  177. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_statusbar.py +0 -0
  178. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_system_deps.py +0 -0
  179. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_terminal_size.py +0 -0
  180. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_terminal_status.py +0 -0
  181. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_termux_local_touch.py +0 -0
  182. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_tmux_binding_manager.py +0 -0
  183. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_tmux_capabilities.py +0 -0
  184. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_tmux_health.py +0 -0
  185. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_tmux_server.py +0 -0
  186. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_tool_panes.py +0 -0
  187. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_transcript.py +0 -0
  188. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_utf8_state.py +0 -0
  189. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_web_demo_recorder.py +0 -0
  190. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_wheel_smoke.py +0 -0
  191. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_widgets.py +0 -0
  192. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_bootstrap.py +0 -0
  193. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_install_log.py +0 -0
  194. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_msys2.py +0 -0
  195. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_pacman.py +0 -0
  196. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_paths.py +0 -0
  197. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_synchronized_screen.py +0 -0
  198. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_tmux_lifecycle.py +0 -0
  199. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_ui_transition.py +0 -0
  200. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_windows_wrapper_contract.py +0 -0
  201. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tests/test_yolo_prompt.py +0 -0
  202. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tools/denested_transport_benchmark.py +0 -0
  203. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tools/release_notes.py +0 -0
  204. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tools/wheel_smoke.py +0 -0
  205. {railmux-0.4.1.dev3 → railmux-0.4.1.dev5}/tools/windows_archive_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.1.dev3
3
+ Version: 0.4.1.dev5
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.4.1.dev5"
@@ -2046,6 +2046,9 @@ _ROOT_TERMUX_TAP_MARKER = "railmux-termux-tap-v1"
2046
2046
  STATUS_ACTION_MODE = "railmux-mode"
2047
2047
  STATUS_ACTION_LAYOUT = "railmux-layout"
2048
2048
  STATUS_ACTION_COPY = "railmux-copy"
2049
+ STATUS_ACTION_PAGE_SIDEBAR = "railmux-page-r"
2050
+ STATUS_ACTION_PAGE_PRIMARY = "railmux-page-1"
2051
+ STATUS_ACTION_PAGE_SECONDARY = "railmux-page-2"
2049
2052
  _ROOT_STATUS_ACTION_KEYS = {
2050
2053
  STATUS_ACTION_MODE: "MouseDown1Control0",
2051
2054
  STATUS_ACTION_LAYOUT: "MouseDown1Control1",
@@ -2059,6 +2062,9 @@ _STATUS_ACTIONS = frozenset({
2059
2062
  STATUS_ACTION_MODE,
2060
2063
  STATUS_ACTION_LAYOUT,
2061
2064
  STATUS_ACTION_COPY,
2065
+ STATUS_ACTION_PAGE_SIDEBAR,
2066
+ STATUS_ACTION_PAGE_PRIMARY,
2067
+ STATUS_ACTION_PAGE_SECONDARY,
2062
2068
  })
2063
2069
  _PREFIX_TARGET_KEY = "Tab"
2064
2070
  _PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
@@ -2085,6 +2091,15 @@ RAILMUX_HISTORY_RESTORE_KEYS = {
2085
2091
  "primary": ("F23", "\x1b[42~"),
2086
2092
  "secondary": ("F24", "\x1b[43~"),
2087
2093
  }
2094
+ # Compact status clicks must reach the controller before pane focus changes so
2095
+ # it can park the visible provider, switch the zoomed page, and resume the new
2096
+ # provider as one transaction. tmux names keys only through F12, therefore
2097
+ # these private routes use canonical xterm sequences in literal mode.
2098
+ RAILMUX_COMPACT_PAGE_KEYS = {
2099
+ STATUS_ACTION_PAGE_SIDEBAR: ("F13", "\x1b[25~"),
2100
+ STATUS_ACTION_PAGE_PRIMARY: ("F14", "\x1b[26~"),
2101
+ STATUS_ACTION_PAGE_SECONDARY: ("F15", "\x1b[28~"),
2102
+ }
2088
2103
  RAILMUX_SELECTION_KEY_OPTION = "@railmux_selection_key"
2089
2104
  RAILMUX_SELECTION_PEER_OPTION = "@railmux_selection_peer"
2090
2105
  RAILMUX_SELECTION_FROZEN_OPTION = "@railmux_selection_frozen_by"
@@ -2124,7 +2139,9 @@ def status_pane_range(pane_id: str, content: str) -> str:
2124
2139
  # A pane range sets ``mouse_status_range`` to only the literal ``pane``;
2125
2140
  # ``=`` still resolves to the pane geometrically below the status cell,
2126
2141
  # not to X. A user range preserves the strict pane ID as its argument, so
2127
- # the managed binding can select the intended compact page explicitly.
2142
+ # the managed binding can select a pane-targeted status control explicitly.
2143
+ # Compact page navigation uses controller actions instead so parking,
2144
+ # resuming, and zooming remain one ordered transaction.
2128
2145
  #
2129
2146
  # Status strings also pass through tmux's strftime expansion. Escape the
2130
2147
  # pane-ID percent sign so IDs such as ``%76`` are not parsed as padding
@@ -2149,8 +2166,8 @@ def status_action_range(action: str, content: str) -> str:
2149
2166
  # keys avoids routing fixed actions through the generic user-range
2150
2167
  # dispatcher and provides deterministic bindings on the 3.7b version
2151
2168
  # bundled by the managed Windows runtime.
2152
- if tmux_version() >= (3, 7):
2153
- key = _ROOT_STATUS_ACTION_KEYS[action]
2169
+ key = _ROOT_STATUS_ACTION_KEYS.get(action)
2170
+ if tmux_version() >= (3, 7) and key is not None:
2154
2171
  index = key.removeprefix("MouseDown1Control")
2155
2172
  return f"#[range=control|{index}]{content}#[norange]"
2156
2173
  return f"#[range=user|{action}]{content}#[norange]"
@@ -3079,7 +3096,7 @@ def set_root_status_click_forwarding(
3079
3096
  marker = f"{_ROOT_STATUS_CLICK_MARKER}-{token}"
3080
3097
  condition = (
3081
3098
  "#{&&:"
3082
- "#{m/r:^(%[0-9]+|railmux-(mode|layout|copy))$,"
3099
+ "#{m/r:^(%[0-9]+|railmux-(mode|layout|copy|page-[r12]))$,"
3083
3100
  "#{mouse_status_range}},"
3084
3101
  "#{!=:#{@railmux_controller_pane},},"
3085
3102
  f"#{{==:{marker},{marker}}}}}"
@@ -3097,6 +3114,18 @@ def set_root_status_click_forwarding(
3097
3114
  f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' F7 ;; '
3098
3115
  f'{STATUS_ACTION_COPY}) {_tmux_shell_executable()} send-keys -t '
3099
3116
  f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' F6 ;; '
3117
+ f'{STATUS_ACTION_PAGE_SIDEBAR}) '
3118
+ f'{_tmux_shell_executable()} send-keys -l -t '
3119
+ f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' -- '
3120
+ f'{shlex.quote(RAILMUX_COMPACT_PAGE_KEYS[STATUS_ACTION_PAGE_SIDEBAR][1])} ;; '
3121
+ f'{STATUS_ACTION_PAGE_PRIMARY}) '
3122
+ f'{_tmux_shell_executable()} send-keys -l -t '
3123
+ f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' -- '
3124
+ f'{shlex.quote(RAILMUX_COMPACT_PAGE_KEYS[STATUS_ACTION_PAGE_PRIMARY][1])} ;; '
3125
+ f'{STATUS_ACTION_PAGE_SECONDARY}) '
3126
+ f'{_tmux_shell_executable()} send-keys -l -t '
3127
+ f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' -- '
3128
+ f'{shlex.quote(RAILMUX_COMPACT_PAGE_KEYS[STATUS_ACTION_PAGE_SECONDARY][1])} ;; '
3100
3129
  'esac"'
3101
3130
  )
3102
3131
  subprocess.check_call(
@@ -305,7 +305,7 @@ def _compact_tmux_status_left(
305
305
  page: WorkspacePage,
306
306
  panes: tuple[str | None, str | None, str | None],
307
307
  width: int,
308
- range_wrapper: Callable[[str, str], str] | None = None,
308
+ page_range_wrapper: Callable[[str, str], str] | None = None,
309
309
  action_range_wrapper: Callable[[str, str], str] | None = None,
310
310
  layout_indicator: str | None = None,
311
311
  ) -> tuple[str, int]:
@@ -332,19 +332,26 @@ def _compact_tmux_status_left(
332
332
  WorkspacePage.PRIMARY,
333
333
  WorkspacePage.SECONDARY,
334
334
  )
335
+ page_actions = (
336
+ tmux_ctl.STATUS_ACTION_PAGE_SIDEBAR,
337
+ tmux_ctl.STATUS_ACTION_PAGE_PRIMARY,
338
+ tmux_ctl.STATUS_ACTION_PAGE_SECONDARY,
339
+ )
335
340
  fg_inactive = "colour231" if error else "colour0"
336
341
  fg_active = "colour220" if error else "colour231"
337
342
  rendered: list[str] = []
338
343
  visible = 0
339
- for candidate, pane_id, label in zip(pages, panes, labels):
344
+ for candidate, action, pane_id, label in zip(
345
+ pages, page_actions, panes, labels,
346
+ ):
340
347
  # Keep all three positions stable even before F8 creates Pane 2. A
341
348
  # missing pane has no tmux range, so it is visible but not clickable.
342
349
  content = (
343
350
  f"#[fg={fg_active if candidate is page else fg_inactive}]"
344
351
  f"[{label}]"
345
352
  )
346
- if range_wrapper is not None and pane_id is not None:
347
- content = range_wrapper(pane_id, content)
353
+ if page_range_wrapper is not None and pane_id is not None:
354
+ content = page_range_wrapper(action, content)
348
355
  rendered.append(content)
349
356
  visible += len(label) + 2
350
357
  mode_content = mode
@@ -3418,6 +3425,7 @@ class App:
3418
3425
  for changed in reversed(parked):
3419
3426
  self._resume_compact_slot(changed)
3420
3427
  return False
3428
+ resume_required = bool(desired is not None and desired.display_parked)
3421
3429
  if desired is not None and not self._resume_compact_slot(desired):
3422
3430
  fallback_page = workspace.compact_page
3423
3431
  fallback_pane = self._pane_for_workspace_page(fallback_page)
@@ -3435,6 +3443,14 @@ class App:
3435
3443
  pane_id = self._pane_for_workspace_page(page)
3436
3444
  if pane_id is None or not tmux_ctl.pane_alive(pane_id):
3437
3445
  return False
3446
+ # swap-pane unzooms a window when the active compact placeholder moves
3447
+ # back to the provider's home window. Re-establish zoom against the
3448
+ # real pane that is now visible before committing the logical page.
3449
+ if resume_required and not self._zoom_pane(pane_id):
3450
+ if announce:
3451
+ self._set_status(
3452
+ "That compact page could not fill the screen.", "warn")
3453
+ return False
3438
3454
 
3439
3455
  workspace.compact_page = page
3440
3456
  if page is WorkspacePage.PRIMARY:
@@ -8332,6 +8348,17 @@ class App:
8332
8348
  if key == COMPACT_RESIZE_KEY.lower():
8333
8349
  self._handle_remote_compact_prepare()
8334
8350
  return
8351
+ compact_pages = {
8352
+ "f13": WorkspacePage.SIDEBAR,
8353
+ "f14": WorkspacePage.PRIMARY,
8354
+ "f15": WorkspacePage.SECONDARY,
8355
+ }
8356
+ if key in compact_pages:
8357
+ if (self._agent_workspace().presentation
8358
+ is WorkspacePresentation.COMPACT):
8359
+ self._select_workspace_page(
8360
+ compact_pages[key], announce=True)
8361
+ return
8335
8362
  for slot_key, (restore_key, _sequence) in (
8336
8363
  tmux_ctl.RAILMUX_HISTORY_RESTORE_KEYS.items()):
8337
8364
  if key == restore_key.lower():
@@ -11785,16 +11812,15 @@ class App:
11785
11812
  is WorkspacePresentation.COMPACT):
11786
11813
  width = (getattr(self, "_last_workspace_size", None) or (80, 24))[0]
11787
11814
  manager = getattr(self, "_tmux_binding_manager", None)
11788
- pane_range_helper = getattr(tmux_ctl, "status_pane_range", None)
11789
11815
  action_range_helper = getattr(
11790
11816
  tmux_ctl, "status_action_range", None)
11791
11817
  wrap_pane = None
11792
11818
  wrap_action = None
11793
11819
  if (getattr(manager, "status_navigation_available", False)
11794
- is True and callable(pane_range_helper)):
11795
- def wrap_pane(pane_id: str, content: str) -> str:
11820
+ is True and callable(action_range_helper)):
11821
+ def wrap_pane(action: str, content: str) -> str:
11796
11822
  try:
11797
- return pane_range_helper(pane_id, content)
11823
+ return action_range_helper(action, content)
11798
11824
  except (TypeError, ValueError):
11799
11825
  return content
11800
11826
  if (getattr(manager, "status_navigation_available", False)
@@ -320,6 +320,22 @@ class _SemanticTerminalRenderer:
320
320
  and self._presentation_serial >= self._presentation_required_serial
321
321
  )
322
322
 
323
+ @property
324
+ def pointer_routing_stable(self) -> bool:
325
+ """Whether the last physical screen still has trusted pointer geometry.
326
+
327
+ An ordinary provider repaint can keep one terminal write in flight for
328
+ long enough to overlap several Windows wheel reports. That does not
329
+ invalidate the already visible pane layout. Explicit topology
330
+ transitions call :meth:`require_fresh_presentation` and remain
331
+ fail-closed until the matching newer frame is physically visible.
332
+ """
333
+ self._promote_presented_screen()
334
+ return bool(
335
+ self._screen is not None
336
+ and self._presentation_serial >= self._presentation_required_serial
337
+ )
338
+
323
339
  def require_fresh_presentation(self) -> None:
324
340
  """Fail pointer input closed until one newer frame is displayed."""
325
341
  required = self._presentation_serial + 1
@@ -1815,14 +1831,19 @@ class LocalPtyClient:
1815
1831
  part,
1816
1832
  logical_height=logical_height,
1817
1833
  )
1818
- if not self._renderer.presentation_stable:
1834
+ routing_stable = self._renderer.presentation_stable or (
1835
+ event.wheel_direction != 0
1836
+ and self._renderer.pointer_routing_stable
1837
+ )
1838
+ if not routing_stable:
1819
1839
  if not self._selection.capturing and self._selection.cancel():
1820
1840
  self._renderer.set_selection(self._selection.segments())
1821
1841
  # A queued frame can already represent a new tmux pane topology
1822
- # while the user still sees the prior one. Dropping only pointer
1823
- # input during this bounded transition is safer than forwarding it
1824
- # to Quit or stock tmux copy-mode. A capture already owns an
1825
- # immutable visible snapshot and therefore keeps the whole gesture.
1842
+ # while the user still sees the prior one. Explicitly untrusted
1843
+ # geometry stays fail-closed; an ordinary content-only write may
1844
+ # still route wheel input against the last physical screen. A
1845
+ # capture already owns an immutable visible snapshot and therefore
1846
+ # keeps the whole gesture.
1826
1847
  if not self._selection.capturing:
1827
1848
  return
1828
1849
  if self._selection.capturing:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.1.dev3
3
+ Version: 0.4.1.dev5
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  """Tests for F8/F9 dispatch and managed binding lifecycle."""
2
2
  import threading
3
- from unittest.mock import MagicMock
3
+ from unittest.mock import MagicMock, call
4
4
 
5
5
  from railmux import tmux_server
6
6
  from railmux.config import Config
@@ -847,6 +847,27 @@ def test_compact_page_switch_retargets_and_rezooms_without_losing_layout(
847
847
  False, force_border=True)
848
848
 
849
849
 
850
+ def test_private_compact_page_keys_delegate_only_in_compact_presentation():
851
+ app = _bare_app()
852
+ app._select_workspace_page = MagicMock(return_value=True)
853
+
854
+ app._agent_workspace().presentation = WorkspacePresentation.COMPACT
855
+ app._on_input("f14")
856
+ app._on_input("f15")
857
+ app._on_input("f13")
858
+
859
+ assert app._select_workspace_page.call_args_list == [
860
+ call(WorkspacePage.PRIMARY, announce=True),
861
+ call(WorkspacePage.SECONDARY, announce=True),
862
+ call(WorkspacePage.SIDEBAR, announce=True),
863
+ ]
864
+
865
+ app._select_workspace_page.reset_mock()
866
+ app._agent_workspace().presentation = WorkspacePresentation.WIDE
867
+ app._on_input("f14")
868
+ app._select_workspace_page.assert_not_called()
869
+
870
+
850
871
  def test_full_sidebar_modal_restores_preexisting_agent_zoom(monkeypatch):
851
872
  app = _bare_app(_railmux_pane_id="%1")
852
873
  app._show_overlay = MagicMock()
@@ -219,6 +219,7 @@ def test_compact_page_switch_parks_old_agent_before_resuming_target(
219
219
 
220
220
  transport.park.assert_called_once_with(workspace.primary)
221
221
  transport.resume.assert_called_once_with(workspace.secondary)
222
+ assert app._zoom_pane.call_args_list == [call("%30"), call("%3")]
222
223
  assert workspace.compact_page.value == "secondary"
223
224
 
224
225
 
@@ -970,6 +970,9 @@ def test_status_action_range_requires_tmux_34_and_known_action():
970
970
  tmux_ctl.STATUS_ACTION_MODE,
971
971
  tmux_ctl.STATUS_ACTION_LAYOUT,
972
972
  tmux_ctl.STATUS_ACTION_COPY,
973
+ tmux_ctl.STATUS_ACTION_PAGE_SIDEBAR,
974
+ tmux_ctl.STATUS_ACTION_PAGE_PRIMARY,
975
+ tmux_ctl.STATUS_ACTION_PAGE_SECONDARY,
973
976
  )
974
977
  )
975
978
  with patch.object(tmux_ctl, "tmux_version", return_value=(3, 3)):
@@ -985,6 +988,10 @@ def test_status_action_range_requires_tmux_34_and_known_action():
985
988
  tmux_ctl.STATUS_ACTION_LAYOUT, "◨") == (
986
989
  "#[range=control|1]◨#[norange]"
987
990
  )
991
+ assert tmux_ctl.status_action_range(
992
+ tmux_ctl.STATUS_ACTION_PAGE_PRIMARY, "[1]") == (
993
+ "#[range=user|railmux-page-1][1]#[norange]"
994
+ )
988
995
  try:
989
996
  tmux_ctl.status_action_range("railmux-arbitrary", "unsafe")
990
997
  except ValueError:
@@ -1013,7 +1020,7 @@ def test_root_status_click_scopes_ranges_and_dispatches_actions():
1013
1020
  assert "-t" not in argv[5:8]
1014
1021
  assert "mouse_status_range" in argv[7]
1015
1022
  assert "%[0-9]+" in argv[7]
1016
- assert "railmux-(mode|layout|copy)" in argv[7]
1023
+ assert "railmux-(mode|layout|copy|page-[r12])" in argv[7]
1017
1024
  assert tmux_ctl.RAILMUX_CONTROLLER_OPTION in argv[7]
1018
1025
  assert "railmux-status-pane-v1-owner123" in argv[7]
1019
1026
  assert "tmux select-pane -Z -t '#{mouse_status_range}'" in argv[8]
@@ -1023,6 +1030,11 @@ def test_root_status_click_scopes_ranges_and_dispatches_actions():
1023
1030
  assert "' F7 ;;" in argv[8]
1024
1031
  assert "railmux-copy) tmux send-keys" in argv[8]
1025
1032
  assert "' F6 ;;" in argv[8]
1033
+ assert "railmux-page-r) tmux send-keys -l" in argv[8]
1034
+ assert "railmux-page-1) tmux send-keys -l" in argv[8]
1035
+ assert "railmux-page-2) tmux send-keys -l" in argv[8]
1036
+ for _key, sequence in tmux_ctl.RAILMUX_COMPACT_PAGE_KEYS.values():
1037
+ assert sequence in argv[8]
1026
1038
  assert argv[-1] == "select-window -t ="
1027
1039
 
1028
1040
 
@@ -3517,18 +3517,20 @@ def test_real_tmux_status_pane_range_selects_and_keeps_zoom(
3517
3517
  # status range beneath the pointer.
3518
3518
  os.write(master_fd, release)
3519
3519
 
3520
- # Exercise a real action range too. A raw-mode reader in the controller
3521
- # pane records the F5 generated by the Mode branch; this closes the
3522
- # second tmux parsing pass that argv-string assertions cannot cover.
3520
+ # Exercise the compact page action used by the product UI. A raw-mode
3521
+ # reader in the controller records its private sequence; the click
3522
+ # must not select/unzoom a pane before the controller performs the
3523
+ # complete park/zoom/resume transaction.
3523
3524
  ready_path = tmp_path / "status-action-ready"
3524
3525
  key_path = tmp_path / "status-action-key"
3525
3526
  reader = (
3526
- "import os,termios,pathlib;"
3527
+ "import os,termios,pathlib,time;"
3527
3528
  "a=termios.tcgetattr(0);"
3528
3529
  "a[3]&=~(termios.ICANON|termios.ECHO);"
3529
3530
  "termios.tcsetattr(0,termios.TCSANOW,a);"
3530
3531
  f"pathlib.Path({str(ready_path)!r}).touch();"
3531
- f"pathlib.Path({str(key_path)!r}).write_bytes(os.read(0,32))"
3532
+ f"pathlib.Path({str(key_path)!r}).write_bytes(os.read(0,32));"
3533
+ "time.sleep(10)"
3532
3534
  )
3533
3535
  subprocess.run(
3534
3536
  [
@@ -3542,7 +3544,7 @@ def test_real_tmux_status_pane_range_selects_and_keeps_zoom(
3542
3544
  [
3543
3545
  "tmux", "set-option", "-t", display_session, "status-left",
3544
3546
  tmux_ctl.status_action_range(
3545
- tmux_ctl.STATUS_ACTION_MODE, "[M]"),
3547
+ tmux_ctl.STATUS_ACTION_PAGE_PRIMARY, "[1]"),
3546
3548
  ],
3547
3549
  check=True,
3548
3550
  )
@@ -3553,7 +3555,7 @@ def test_real_tmux_status_pane_range_selects_and_keeps_zoom(
3553
3555
  action_painted = bytearray()
3554
3556
  action_deadline = time.monotonic() + 3.0
3555
3557
  while (time.monotonic() < action_deadline
3556
- and b"[M]" not in action_painted):
3558
+ and b"[1]" not in action_painted):
3557
3559
  readable, _, _ = select.select([master_fd], [], [], 0.1)
3558
3560
  if not readable:
3559
3561
  continue
@@ -3561,13 +3563,23 @@ def test_real_tmux_status_pane_range_selects_and_keeps_zoom(
3561
3563
  action_painted.extend(os.read(master_fd, 65536))
3562
3564
  except BlockingIOError:
3563
3565
  pass
3564
- assert b"[M]" in action_painted
3566
+ assert b"[1]" in action_painted
3565
3567
  # Keep this a second ordinary click rather than tmux coalescing the two
3566
3568
  # synthetic presses into DoubleClick1Status (a different key binding).
3567
3569
  time.sleep(0.6)
3568
3570
  os.write(master_fd, click)
3569
3571
  assert _wait_until(key_path.exists)
3570
- assert key_path.read_bytes()
3572
+ assert key_path.read_bytes() == (
3573
+ tmux_ctl.RAILMUX_COMPACT_PAGE_KEYS[
3574
+ tmux_ctl.STATUS_ACTION_PAGE_PRIMARY
3575
+ ][1].encode()
3576
+ )
3577
+ assert tmux_ctl.active_pane_id(owner_pane) == other_pane
3578
+ assert subprocess.check_output(
3579
+ ["tmux", "display-message", "-p", "-t", other_pane,
3580
+ "#{window_zoomed_flag}"],
3581
+ text=True,
3582
+ ).strip() == "1"
3571
3583
  finally:
3572
3584
  if client.poll() is None:
3573
3585
  client.kill()
@@ -367,6 +367,31 @@ def test_compact_status_left_wraps_mode_and_layout_as_actions():
367
367
  assert visible == len("[R][1][2] CC · ◨ ")
368
368
 
369
369
 
370
+ def test_compact_status_left_routes_available_pages_through_controller_actions():
371
+ pages = []
372
+
373
+ def wrap_page(action, content):
374
+ pages.append((action, content))
375
+ return f"<{action}>{content}</{action}>"
376
+
377
+ value, _visible = _compact_tmux_status_left(
378
+ False,
379
+ "Codex",
380
+ WorkspacePage.PRIMARY,
381
+ ("%1", "%2", None),
382
+ 40,
383
+ page_range_wrapper=wrap_page,
384
+ )
385
+
386
+ assert pages == [
387
+ ("railmux-page-r", "#[fg=colour0][R]"),
388
+ ("railmux-page-1", "#[fg=colour231][1]"),
389
+ ]
390
+ assert "<railmux-page-r>" in value
391
+ assert "<railmux-page-1>" in value
392
+ assert "<railmux-page-2>" not in value
393
+
394
+
370
395
  def test_apply_bar_uses_dynamic_compact_left_length(monkeypatch):
371
396
  run = MagicMock()
372
397
  monkeypatch.setattr("subprocess.run", run)
@@ -413,9 +438,9 @@ def test_apply_compact_bar_makes_pages_mode_and_layout_clickable(monkeypatch):
413
438
  app._apply_tmux_bar(error=False)
414
439
 
415
440
  left = _style_calls(run, "status-left")[-1]
416
- assert "#[range=user|%%1]" in left
417
- assert "#[range=user|%%2]" in left
418
- assert "#[range=user|%%3]" in left
441
+ assert "#[range=user|railmux-page-r]" in left
442
+ assert "#[range=user|railmux-page-1]" in left
443
+ assert "#[range=user|railmux-page-2]" in left
419
444
  assert "#[range=user|railmux-mode]CC#[norange]" in left
420
445
  assert "#[range=user|railmux-layout]◨#[norange]" in left
421
446
  visible = len("[R][1][2] CC · ◨ ")
@@ -430,6 +430,114 @@ def test_local_windows_wheel_uses_shared_managed_history_without_tmux_copy_mode(
430
430
  child_socket.close()
431
431
 
432
432
 
433
+ def test_local_windows_real_sgr_wheel_survives_busy_content_paint():
434
+ proxy_socket, child_socket = socket.socketpair()
435
+ client = windows_attach_relay.LocalPtyClient.__new__(
436
+ windows_attach_relay.LocalPtyClient
437
+ )
438
+ client.master_fd = proxy_socket.detach()
439
+ client._session_id = "$4"
440
+ client._selection = LocalTextSelection()
441
+ client._routes = ()
442
+ client._routes_checked_at = time.monotonic()
443
+ client._path_prompt = None
444
+ client._path_prompt_mouse_button = None
445
+ _set_local_history_state(client)
446
+ _install_local_history_snapshot(
447
+ client,
448
+ HistorySnapshot(
449
+ 0,
450
+ "%8",
451
+ 0,
452
+ 0,
453
+ 20,
454
+ 2,
455
+ lines=tuple(f"row-{index}".encode() for index in range(8)),
456
+ ),
457
+ )
458
+ renderer = MagicMock()
459
+ renderer.presentation_stable = False
460
+ renderer.pointer_routing_stable = True
461
+ renderer.screen = SimpleNamespace(
462
+ width=20,
463
+ height=3,
464
+ cursor_x=1,
465
+ cursor_y=0,
466
+ rows=(b"live-0", b"live-1", b"status"),
467
+ )
468
+ renderer.surface.translate_mouse_event.side_effect = lambda event, **_kwargs: event
469
+ client._renderer = renderer
470
+ decoder = windows_attach_relay.TerminalInputDecoder()
471
+ child_socket.setblocking(False)
472
+ try:
473
+ parts = decoder.feed(b"\033[<64;2;1M")
474
+ assert len(parts) == 1
475
+ client._forward_input_part(parts[0])
476
+
477
+ assert client._history.active
478
+ renderer.set_history_overlays.assert_called_with(
479
+ client._history.overlays()
480
+ )
481
+ with pytest.raises(BlockingIOError):
482
+ child_socket.recv(64)
483
+ finally:
484
+ os.close(client.master_fd)
485
+ child_socket.close()
486
+
487
+
488
+ def test_local_windows_wheel_stays_fail_closed_during_topology_transition():
489
+ proxy_socket, child_socket = socket.socketpair()
490
+ client = windows_attach_relay.LocalPtyClient.__new__(
491
+ windows_attach_relay.LocalPtyClient
492
+ )
493
+ client.master_fd = proxy_socket.detach()
494
+ client._session_id = "$4"
495
+ client._selection = LocalTextSelection()
496
+ client._routes = ()
497
+ client._routes_checked_at = time.monotonic()
498
+ client._path_prompt = None
499
+ client._path_prompt_mouse_button = None
500
+ _set_local_history_state(client)
501
+ _install_local_history_snapshot(
502
+ client,
503
+ HistorySnapshot(
504
+ 0,
505
+ "%8",
506
+ 0,
507
+ 0,
508
+ 20,
509
+ 2,
510
+ lines=tuple(f"row-{index}".encode() for index in range(8)),
511
+ ),
512
+ )
513
+ renderer = MagicMock()
514
+ renderer.presentation_stable = False
515
+ renderer.pointer_routing_stable = False
516
+ renderer.screen = SimpleNamespace(
517
+ width=20,
518
+ height=3,
519
+ cursor_x=1,
520
+ cursor_y=0,
521
+ rows=(b"live-0", b"live-1", b"status"),
522
+ )
523
+ renderer.surface.translate_mouse_event.side_effect = lambda event, **_kwargs: event
524
+ client._renderer = renderer
525
+ decoder = windows_attach_relay.TerminalInputDecoder()
526
+ child_socket.setblocking(False)
527
+ try:
528
+ parts = decoder.feed(b"\033[<64;2;1M")
529
+ assert len(parts) == 1
530
+ client._forward_input_part(parts[0])
531
+
532
+ assert not client._history.active
533
+ renderer.set_history_overlays.assert_not_called()
534
+ with pytest.raises(BlockingIOError):
535
+ child_socket.recv(64)
536
+ finally:
537
+ os.close(client.master_fd)
538
+ child_socket.close()
539
+
540
+
433
541
  def test_local_windows_sidebar_wheel_stays_tmux_owned():
434
542
  proxy_socket, child_socket = socket.socketpair()
435
543
  client = windows_attach_relay.LocalPtyClient.__new__(
@@ -1199,18 +1307,24 @@ def test_semantic_renderer_hit_testing_waits_for_physical_frame(monkeypatch):
1199
1307
  assert renderer.screen is old_screen
1200
1308
  assert renderer.presentation_serial == old_serial
1201
1309
  assert renderer.presentation_stable is False
1310
+ # Ordinary content backpressure does not invalidate the pane geometry
1311
+ # of the frame that is still physically visible.
1312
+ assert renderer.pointer_routing_stable is True
1202
1313
 
1203
1314
  # A topology mutation requested while this already-queued frame is
1204
1315
  # writing must wait for one still-newer authoritative frame.
1205
1316
  renderer.require_fresh_presentation()
1317
+ assert renderer.pointer_routing_stable is False
1206
1318
  release.set()
1207
1319
  assert renderer.writer.wait_idle(1.0)
1208
1320
  assert renderer.presentation_stable is False
1321
+ assert renderer.pointer_routing_stable is False
1209
1322
  assert renderer.presentation_serial == old_serial + 1
1210
1323
  renderer.feed(b"\033[H\033[2KNEW-TOPOLOGY-VISIBLE")
1211
1324
  renderer.paint_due(3.0, force=True)
1212
1325
  assert renderer.writer.wait_idle(1.0)
1213
1326
  assert renderer.presentation_stable is True
1327
+ assert renderer.pointer_routing_stable is True
1214
1328
  assert b"NEW-TOPOLOGY-VISIBLE" in renderer.screen.rows[0]
1215
1329
  finally:
1216
1330
  release.set()
@@ -1 +0,0 @@
1
- __version__ = "0.4.1.dev3"
File without changes
File without changes
File without changes
File without changes