railmux 0.3.2.dev4__tar.gz → 0.3.3__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 (196) hide show
  1. {railmux-0.3.2.dev4/src/railmux.egg-info → railmux-0.3.3}/PKG-INFO +28 -16
  2. {railmux-0.3.2.dev4 → railmux-0.3.3}/README.md +27 -11
  3. {railmux-0.3.2.dev4 → railmux-0.3.3}/pyproject.toml +0 -4
  4. railmux-0.3.3/src/railmux/__init__.py +1 -0
  5. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/cli.py +2 -40
  6. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/codex_index.py +15 -6
  7. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/config.py +1 -3
  8. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/discovery.py +7 -66
  9. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/display_transport.py +18 -0
  10. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/fast_display_client.py +26 -13
  11. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/fast_display_history.py +12 -3
  12. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/fast_display_protocol.py +20 -7
  13. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/fast_display_server.py +90 -19
  14. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/favorites.py +1 -2
  15. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/help_workspace.py +7 -2
  16. railmux-0.3.3/src/railmux/local_clipboard.py +47 -0
  17. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/local_open.py +10 -37
  18. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/modes.py +0 -8
  19. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/mouse_manager.py +11 -7
  20. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/orphan_marker.py +14 -9
  21. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/renames.py +1 -2
  22. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/restart_state.py +12 -7
  23. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/runtime_config.py +1 -4
  24. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/scroll_manager.py +5 -3
  25. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ssh_display_diagnostics.py +5 -3
  26. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/tmux_binding_manager.py +109 -10
  27. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/tmux_ctl.py +525 -8
  28. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/tmux_server.py +22 -4
  29. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/transcript.py +83 -28
  30. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/app.py +974 -452
  31. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/modals.py +1 -1
  32. {railmux-0.3.2.dev4 → railmux-0.3.3/src/railmux.egg-info}/PKG-INFO +28 -16
  33. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux.egg-info/SOURCES.txt +2 -33
  34. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux.egg-info/requires.txt +0 -4
  35. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_cli.py +0 -15
  36. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_codex_delete.py +137 -46
  37. railmux-0.3.3/tests/test_codex_lineage.py +205 -0
  38. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_config.py +0 -1
  39. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_discovery.py +0 -24
  40. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_display_transport.py +21 -1
  41. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_fast_full_window.py +198 -12
  42. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_focus_highlight.py +155 -8
  43. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_local_open.py +0 -26
  44. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_modes.py +0 -2
  45. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_mouse_manager.py +17 -0
  46. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_orphan_recovery.py +2 -2
  47. railmux-0.3.3/tests/test_release_workflow.py +74 -0
  48. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_runtime_config.py +0 -12
  49. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_soft_quit.py +83 -2
  50. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_statusbar.py +25 -0
  51. railmux-0.3.3/tests/test_termux_local_touch.py +120 -0
  52. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_tmux_binding_manager.py +81 -4
  53. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_tmux_ctl.py +192 -0
  54. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_tmux_integration.py +218 -11
  55. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_tmux_server.py +22 -0
  56. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_transcript.py +84 -0
  57. railmux-0.3.3/tests/test_utf8_state.py +111 -0
  58. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_wheel_smoke.py +1 -6
  59. {railmux-0.3.2.dev4 → railmux-0.3.3}/tools/wheel_smoke.py +6 -12
  60. railmux-0.3.2.dev4/src/railmux/__init__.py +0 -1
  61. railmux-0.3.2.dev4/src/railmux/local_clipboard.py +0 -101
  62. railmux-0.3.2.dev4/src/railmux/mux/__init__.py +0 -5
  63. railmux-0.3.2.dev4/src/railmux/mux/backend.py +0 -59
  64. railmux-0.3.2.dev4/src/railmux/mux/tmux_backend.py +0 -161
  65. railmux-0.3.2.dev4/src/railmux/platform/__init__.py +0 -44
  66. railmux-0.3.2.dev4/src/railmux/platform/config_paths.py +0 -30
  67. railmux-0.3.2.dev4/src/railmux/platform/console.py +0 -165
  68. railmux-0.3.2.dev4/src/railmux/platform/file_security.py +0 -34
  69. railmux-0.3.2.dev4/src/railmux/platform/filelock.py +0 -42
  70. railmux-0.3.2.dev4/src/railmux/platform/multiplex.py +0 -132
  71. railmux-0.3.2.dev4/src/railmux/platform/process.py +0 -21
  72. railmux-0.3.2.dev4/src/railmux/platform/runtime_paths.py +0 -75
  73. railmux-0.3.2.dev4/src/railmux/terminal_emulator.py +0 -180
  74. railmux-0.3.2.dev4/src/railmux/winlocal/__init__.py +0 -2
  75. railmux-0.3.2.dev4/src/railmux/winlocal/backend.py +0 -897
  76. railmux-0.3.2.dev4/src/railmux/winlocal/client.py +0 -275
  77. railmux-0.3.2.dev4/src/railmux/winlocal/compositor.py +0 -254
  78. railmux-0.3.2.dev4/src/railmux/winlocal/conpty.py +0 -200
  79. railmux-0.3.2.dev4/src/railmux/winlocal/daemon.py +0 -359
  80. railmux-0.3.2.dev4/src/railmux/winlocal/history_viewer.py +0 -93
  81. railmux-0.3.2.dev4/src/railmux/winlocal/ipc.py +0 -88
  82. railmux-0.3.2.dev4/src/railmux/winlocal/session_store.py +0 -96
  83. railmux-0.3.2.dev4/src/railmux/winlocal/virtual_screen.py +0 -105
  84. railmux-0.3.2.dev4/tests/test_codex_lineage.py +0 -64
  85. railmux-0.3.2.dev4/tests/test_mux_backend.py +0 -60
  86. railmux-0.3.2.dev4/tests/test_platform.py +0 -248
  87. railmux-0.3.2.dev4/tests/test_release_workflow.py +0 -52
  88. railmux-0.3.2.dev4/tests/test_winlocal_backend.py +0 -176
  89. railmux-0.3.2.dev4/tests/test_winlocal_compositor.py +0 -48
  90. railmux-0.3.2.dev4/tests/test_winlocal_conpty.py +0 -60
  91. railmux-0.3.2.dev4/tests/test_winlocal_daemon.py +0 -150
  92. railmux-0.3.2.dev4/tests/test_winlocal_history_viewer.py +0 -43
  93. railmux-0.3.2.dev4/tests/test_winlocal_ipc.py +0 -39
  94. railmux-0.3.2.dev4/tests/test_winlocal_real_conpty.py +0 -61
  95. railmux-0.3.2.dev4/tests/test_winlocal_session_store.py +0 -42
  96. railmux-0.3.2.dev4/tests/test_winlocal_virtual_screen.py +0 -62
  97. {railmux-0.3.2.dev4 → railmux-0.3.3}/LICENSE +0 -0
  98. {railmux-0.3.2.dev4 → railmux-0.3.3}/MANIFEST.in +0 -0
  99. {railmux-0.3.2.dev4 → railmux-0.3.3}/setup.cfg +0 -0
  100. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/__main__.py +0 -0
  101. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/atomic_file.py +0 -0
  102. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/background_index.py +0 -0
  103. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/config_cli.py +0 -0
  104. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/diagnostics.py +0 -0
  105. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/fast_display_input.py +0 -0
  106. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/fuzzy.py +0 -0
  107. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/launcher.py +0 -0
  108. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/legacy_sessions.py +0 -0
  109. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/models.py +0 -0
  110. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/pane_surface.py +0 -0
  111. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/path_codec.py +0 -0
  112. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/remote_config.py +0 -0
  113. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/scroll_agent.py +0 -0
  114. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/selection_isolation.py +0 -0
  115. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/self_update.py +0 -0
  116. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/session_cache.py +0 -0
  117. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/session_index.py +0 -0
  118. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/setting_contracts.py +0 -0
  119. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/settings.py +0 -0
  120. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ssh_args.py +0 -0
  121. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ssh_compat.py +0 -0
  122. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ssh_doctor.py +0 -0
  123. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/system_deps.py +0 -0
  124. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/terminal_status.py +0 -0
  125. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/tmux_health.py +0 -0
  126. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/tool_panes.py +0 -0
  127. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/__init__.py +0 -0
  128. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/_widgets.py +0 -0
  129. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/keymap.py +0 -0
  130. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/projects_pane.py +0 -0
  131. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/running_pane.py +0 -0
  132. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/sessions_pane.py +0 -0
  133. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/sidebar.py +0 -0
  134. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/statusbar.py +0 -0
  135. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux/ui/workspace.py +0 -0
  136. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux.egg-info/dependency_links.txt +0 -0
  137. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux.egg-info/entry_points.txt +0 -0
  138. {railmux-0.3.2.dev4 → railmux-0.3.3}/src/railmux.egg-info/top_level.txt +0 -0
  139. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_agent_workspace.py +0 -0
  140. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_atomic_file.py +0 -0
  141. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_background_index.py +0 -0
  142. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_codex_index.py +0 -0
  143. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_codex_transcript.py +0 -0
  144. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_config_cli.py +0 -0
  145. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_denested_transport_benchmark.py +0 -0
  146. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_diagnostics.py +0 -0
  147. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_fullscreen_binding.py +0 -0
  148. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_fuzzy.py +0 -0
  149. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_help_agent.py +0 -0
  150. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_help_workspace.py +0 -0
  151. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_history_cleanup.py +0 -0
  152. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_keymap.py +0 -0
  153. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_launcher.py +0 -0
  154. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_layout_profile.py +0 -0
  155. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_legacy_sessions.py +0 -0
  156. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_local_clipboard.py +0 -0
  157. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_modals.py +0 -0
  158. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_mode_selection.py +0 -0
  159. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_new_project.py +0 -0
  160. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_options.py +0 -0
  161. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_pane_surface.py +0 -0
  162. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_path_codec.py +0 -0
  163. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_projects_pane.py +0 -0
  164. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_release_notes.py +0 -0
  165. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_remote_config.py +0 -0
  166. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_renames.py +0 -0
  167. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_restart_state.py +0 -0
  168. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_running_pane.py +0 -0
  169. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_running_sort.py +0 -0
  170. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_scroll_agent.py +0 -0
  171. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_scroll_manager.py +0 -0
  172. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_selection_isolation.py +0 -0
  173. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_self_update.py +0 -0
  174. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_session_cache.py +0 -0
  175. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_session_index.py +0 -0
  176. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_sessions_pane.py +0 -0
  177. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_setting_contracts.py +0 -0
  178. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_settings.py +0 -0
  179. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_sidebar.py +0 -0
  180. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_ssh_args.py +0 -0
  181. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_ssh_compat.py +0 -0
  182. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_ssh_display_benchmark.py +0 -0
  183. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_ssh_display_diagnostics.py +0 -0
  184. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_ssh_doctor.py +0 -0
  185. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_status.py +0 -0
  186. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_system_deps.py +0 -0
  187. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_terminal_size.py +0 -0
  188. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_terminal_status.py +0 -0
  189. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_tmux_health.py +0 -0
  190. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_tmux_status.py +0 -0
  191. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_tool_panes.py +0 -0
  192. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_web_demo_recorder.py +0 -0
  193. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_widgets.py +0 -0
  194. {railmux-0.3.2.dev4 → railmux-0.3.3}/tests/test_yolo_prompt.py +0 -0
  195. {railmux-0.3.2.dev4 → railmux-0.3.3}/tools/denested_transport_benchmark.py +0 -0
  196. {railmux-0.3.2.dev4 → railmux-0.3.3}/tools/release_notes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.2.dev4
3
+ Version: 0.3.3
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
@@ -14,14 +14,12 @@ Classifier: Environment :: Console :: Curses
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Operating System :: POSIX :: Linux
16
16
  Classifier: Operating System :: MacOS
17
- Classifier: Operating System :: Microsoft :: Windows
18
17
  Classifier: Programming Language :: Python :: 3
19
18
  Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
22
  Classifier: Programming Language :: Python :: 3.13
24
- Classifier: Programming Language :: Python :: 3.14
25
23
  Classifier: Topic :: Software Development
26
24
  Classifier: Topic :: Terminals
27
25
  Classifier: Topic :: Utilities
@@ -32,8 +30,6 @@ Requires-Dist: packaging>=23.0
32
30
  Requires-Dist: tomlkit<1,>=0.13.2
33
31
  Requires-Dist: urwid>=2.6.16
34
32
  Requires-Dist: tomli>=2.0.1; python_version < "3.11"
35
- Requires-Dist: pyte<0.9,>=0.8.2; sys_platform == "win32"
36
- Requires-Dist: pywinpty<4,>=3.0.5; sys_platform == "win32"
37
33
  Provides-Extra: ssh
38
34
  Requires-Dist: pyte<0.9,>=0.8.2; extra == "ssh"
39
35
  Provides-Extra: dev
@@ -173,12 +169,14 @@ for focus and layout limits.
173
169
  |--------|--------|
174
170
  | Left-click (non-running) | Preview session history in the Target pane |
175
171
  | Left-click (running) | Switch the Target pane to that session |
172
+ | Wheel up over a live agent | Open its canonical current-branch history |
176
173
  | Double-click | Open/attach in the Target pane and move focus there |
177
174
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
178
175
 
179
176
  Unavailable context-menu actions are hidden for the selected session state.
180
- Single-click and `␣` act in the Target pane without moving keyboard focus;
181
- double-click and `Enter` also move focus there. The terminal must report mouse
177
+ Single-click and `␣` act in the Target pane without moving keyboard focus, but
178
+ `␣` always means history while a click on a running row means live attach.
179
+ Double-click and `Enter` also move focus there. The terminal must report mouse
182
180
  buttons to applications for these actions to reach Railmux. Right-click
183
181
  reporting is sometimes a separate setting from ordinary mouse reporting; see
184
182
  [FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
@@ -206,12 +204,12 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
206
204
  | `r` | Rename the focused session |
207
205
  | `s` | Toggle star — starred sessions pinned to top with ⭐ |
208
206
  | `k` | Kill the running agent process (keeps session file) |
209
- | `d` | Delete the focused session (prompts for confirmation) |
207
+ | `d` | Delete the focused session (confirms, then finishes in background) |
210
208
  | `t` | Open or return to the managed terminal below the Target agent |
211
209
  | `T` | Return to that agent's managed Vim viewer, when open |
212
210
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
213
211
  | `o` | Open persistent Railmux options |
214
- | `␣` | Preview stopped or switch running target (like single-click) |
212
+ | `␣` | Preview canonical history for the selected stopped or running session |
215
213
  | `F8` | Cycle agent layout: single → side-by-side → stacked |
216
214
  | `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
217
215
  | `?` | Full help popup with all keybindings |
@@ -244,6 +242,11 @@ The rename popup starts with the current title pre-filled. Press
244
242
  `Ctrl-U` to clear the entire input, `Enter` to save a non-empty title, or `Esc`
245
243
  to cancel.
246
244
 
245
+ After a delete is confirmed, its popup closes immediately. The bottom-right
246
+ status shows **Deleting “session name”…** while Railmux stops the exact
247
+ provider session and removes its history in the background, then reports the
248
+ final result.
249
+
247
250
  Press `/` in Projects, Sessions, or Running to filter that section. Active
248
251
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
249
252
  shows the current value. Press `Ctrl-U` in the filter editor to clear it, then
@@ -303,7 +306,8 @@ to single leaves Pane 2's agent running in the background.
303
306
 
304
307
  In a split, focus an agent pane to make it the Target pane. After focus returns
305
308
  to the sidebar, single-click or `␣` acts in that pane without moving keyboard
306
- focus; double-click or `Enter` opens there and transfers focus. The status bar
309
+ focus; a running-row click attaches live while `␣` previews history.
310
+ Double-click or `Enter` opens there and transfers focus. The status bar
307
311
  at the bottom-left shows the current layout and Target pane:
308
312
 
309
313
  | Symbol | Meaning |
@@ -367,15 +371,23 @@ sessions move ahead of the other results.
367
371
  ## History preview
368
372
 
369
373
  For a stopped session, left-click or press `␣` to view conversation history in
370
- the Target pane without starting or resuming the agent. Preview is read-only: it
371
- cannot send a message or change the session. User and assistant messages, tool
372
- calls, and abbreviated tool output are colour-coded, while internal context and
373
- encrypted reasoning are hidden.
374
+ the Target pane without starting or resuming the agent. For a running session,
375
+ press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
376
+ read-only: it cannot send a message or change the session. User and assistant
377
+ messages, tool calls, and abbreviated tool output are colour-coded, while
378
+ internal context and encrypted reasoning are hidden. Codex rewind lineages and
379
+ Claude Code `parentUuid` branches are projected onto the provider's current
380
+ conversation: the retained prefix and replacement suffix remain visible, while
381
+ the abandoned suffix is hidden. When Railmux observes a running Codex session
382
+ advance after rewind or steering, it resets only the current terminal view
383
+ before Codex redraws. Retained tmux scrollback is not erased, and managed
384
+ history continues from the canonical rollout without its abandoned suffix.
374
385
 
375
386
  Preview opens at the latest activity in `less`; large sessions are limited to
376
387
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
377
- matches, and `q` to exit and restore the pane. Double-click to skip preview and
378
- open the session directly.
388
+ matches, and `q` to exit. If preview began from a live pane, `q` restores that
389
+ exact session; otherwise it restores the prior Target pane. Double-click to
390
+ skip preview and open the session directly.
379
391
 
380
392
  ## Status indicators
381
393
 
@@ -127,12 +127,14 @@ for focus and layout limits.
127
127
  |--------|--------|
128
128
  | Left-click (non-running) | Preview session history in the Target pane |
129
129
  | Left-click (running) | Switch the Target pane to that session |
130
+ | Wheel up over a live agent | Open its canonical current-branch history |
130
131
  | Double-click | Open/attach in the Target pane and move focus there |
131
132
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
132
133
 
133
134
  Unavailable context-menu actions are hidden for the selected session state.
134
- Single-click and `␣` act in the Target pane without moving keyboard focus;
135
- double-click and `Enter` also move focus there. The terminal must report mouse
135
+ Single-click and `␣` act in the Target pane without moving keyboard focus, but
136
+ `␣` always means history while a click on a running row means live attach.
137
+ Double-click and `Enter` also move focus there. The terminal must report mouse
136
138
  buttons to applications for these actions to reach Railmux. Right-click
137
139
  reporting is sometimes a separate setting from ordinary mouse reporting; see
138
140
  [FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
@@ -160,12 +162,12 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
160
162
  | `r` | Rename the focused session |
161
163
  | `s` | Toggle star — starred sessions pinned to top with ⭐ |
162
164
  | `k` | Kill the running agent process (keeps session file) |
163
- | `d` | Delete the focused session (prompts for confirmation) |
165
+ | `d` | Delete the focused session (confirms, then finishes in background) |
164
166
  | `t` | Open or return to the managed terminal below the Target agent |
165
167
  | `T` | Return to that agent's managed Vim viewer, when open |
166
168
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
167
169
  | `o` | Open persistent Railmux options |
168
- | `␣` | Preview stopped or switch running target (like single-click) |
170
+ | `␣` | Preview canonical history for the selected stopped or running session |
169
171
  | `F8` | Cycle agent layout: single → side-by-side → stacked |
170
172
  | `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
171
173
  | `?` | Full help popup with all keybindings |
@@ -198,6 +200,11 @@ The rename popup starts with the current title pre-filled. Press
198
200
  `Ctrl-U` to clear the entire input, `Enter` to save a non-empty title, or `Esc`
199
201
  to cancel.
200
202
 
203
+ After a delete is confirmed, its popup closes immediately. The bottom-right
204
+ status shows **Deleting “session name”…** while Railmux stops the exact
205
+ provider session and removes its history in the background, then reports the
206
+ final result.
207
+
201
208
  Press `/` in Projects, Sessions, or Running to filter that section. Active
202
209
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
203
210
  shows the current value. Press `Ctrl-U` in the filter editor to clear it, then
@@ -257,7 +264,8 @@ to single leaves Pane 2's agent running in the background.
257
264
 
258
265
  In a split, focus an agent pane to make it the Target pane. After focus returns
259
266
  to the sidebar, single-click or `␣` acts in that pane without moving keyboard
260
- focus; double-click or `Enter` opens there and transfers focus. The status bar
267
+ focus; a running-row click attaches live while `␣` previews history.
268
+ Double-click or `Enter` opens there and transfers focus. The status bar
261
269
  at the bottom-left shows the current layout and Target pane:
262
270
 
263
271
  | Symbol | Meaning |
@@ -321,15 +329,23 @@ sessions move ahead of the other results.
321
329
  ## History preview
322
330
 
323
331
  For a stopped session, left-click or press `␣` to view conversation history in
324
- the Target pane without starting or resuming the agent. Preview is read-only: it
325
- cannot send a message or change the session. User and assistant messages, tool
326
- calls, and abbreviated tool output are colour-coded, while internal context and
327
- encrypted reasoning are hidden.
332
+ the Target pane without starting or resuming the agent. For a running session,
333
+ press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
334
+ read-only: it cannot send a message or change the session. User and assistant
335
+ messages, tool calls, and abbreviated tool output are colour-coded, while
336
+ internal context and encrypted reasoning are hidden. Codex rewind lineages and
337
+ Claude Code `parentUuid` branches are projected onto the provider's current
338
+ conversation: the retained prefix and replacement suffix remain visible, while
339
+ the abandoned suffix is hidden. When Railmux observes a running Codex session
340
+ advance after rewind or steering, it resets only the current terminal view
341
+ before Codex redraws. Retained tmux scrollback is not erased, and managed
342
+ history continues from the canonical rollout without its abandoned suffix.
328
343
 
329
344
  Preview opens at the latest activity in `less`; large sessions are limited to
330
345
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
331
- matches, and `q` to exit and restore the pane. Double-click to skip preview and
332
- open the session directly.
346
+ matches, and `q` to exit. If preview began from a live pane, `q` restores that
347
+ exact session; otherwise it restores the prior Target pane. Double-click to
348
+ skip preview and open the session directly.
333
349
 
334
350
  ## Status indicators
335
351
 
@@ -20,14 +20,12 @@ classifiers = [
20
20
  "Intended Audience :: Developers",
21
21
  "Operating System :: POSIX :: Linux",
22
22
  "Operating System :: MacOS",
23
- "Operating System :: Microsoft :: Windows",
24
23
  "Programming Language :: Python :: 3",
25
24
  "Programming Language :: Python :: 3.9",
26
25
  "Programming Language :: Python :: 3.10",
27
26
  "Programming Language :: Python :: 3.11",
28
27
  "Programming Language :: Python :: 3.12",
29
28
  "Programming Language :: Python :: 3.13",
30
- "Programming Language :: Python :: 3.14",
31
29
  "Topic :: Software Development",
32
30
  "Topic :: Terminals",
33
31
  "Topic :: Utilities",
@@ -37,8 +35,6 @@ dependencies = [
37
35
  "tomlkit>=0.13.2,<1",
38
36
  "urwid>=2.6.16",
39
37
  "tomli>=2.0.1; python_version < '3.11'",
40
- "pyte>=0.8.2,<0.9; sys_platform == 'win32'",
41
- "pywinpty>=3.0.5,<4; sys_platform == 'win32'",
42
38
  ]
43
39
 
44
40
  [project.optional-dependencies]
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3"
@@ -1,18 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
- import os
5
4
  import shutil
6
5
  import subprocess
7
6
  import sys
7
+ import termios
8
8
  import time
9
9
  from dataclasses import replace
10
10
  from pathlib import Path
11
11
 
12
12
  from railmux import __version__
13
13
  from railmux.config import ConfigError, default_config_path, load_config
14
+ from railmux.diagnostics import is_ssh_session, run_doctor
14
15
  from railmux.pane_surface import render_startup_surface
15
- from railmux.platform import python_support_error
16
16
  from railmux.runtime_config import (
17
17
  activate_runtime_environment,
18
18
  check_executable,
@@ -37,29 +37,9 @@ _LOCAL_WATCHDOG_INTERVAL = 5.0
37
37
  _LOCAL_WATCHDOG_FAILURES = 3
38
38
 
39
39
 
40
- def _native_windows() -> bool:
41
- return os.name == "nt"
42
-
43
-
44
- def is_ssh_session(environment: dict[str, str] | None = None) -> bool:
45
- """Lazy compatibility wrapper that keeps POSIX diagnostics import-clean."""
46
- from railmux.diagnostics import is_ssh_session as detect
47
-
48
- return detect(environment)
49
-
50
-
51
- def run_doctor(*args: object, **kwargs: object) -> int:
52
- """Lazy compatibility wrapper for the established CLI test/repair hook."""
53
- from railmux.diagnostics import run_doctor as doctor
54
-
55
- return doctor(*args, **kwargs)
56
-
57
-
58
40
  def _restore_terminal(attributes: list | None) -> None:
59
41
  if attributes is None:
60
42
  return
61
- import termios
62
-
63
43
  try:
64
44
  termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
65
45
  except (OSError, termios.error):
@@ -87,8 +67,6 @@ def _run_tmux_client_with_watchdog(
87
67
  """Keep one monitor outside tmux so a frozen server cannot trap the TTY."""
88
68
  attributes = None
89
69
  if sys.stdin.isatty():
90
- import termios
91
-
92
70
  try:
93
71
  attributes = termios.tcgetattr(sys.stdin.fileno())
94
72
  except (OSError, termios.error):
@@ -174,10 +152,6 @@ def _run_tmux_client_with_watchdog(
174
152
 
175
153
 
176
154
  def main(argv: list[str] | None = None) -> int:
177
- support_error = python_support_error()
178
- if support_error is not None:
179
- print(f"error: {support_error}", file=sys.stderr)
180
- return 2
181
155
  raw_args = list(sys.argv[1:] if argv is None else argv)
182
156
  if raw_args and raw_args[0] == "config":
183
157
  from railmux.config_cli import main as config_main
@@ -364,18 +338,6 @@ def main(argv: list[str] | None = None) -> int:
364
338
  file=sys.stderr,
365
339
  )
366
340
  return 2
367
- if _native_windows():
368
- # Native Windows owns no local tmux server. The detached daemon keeps
369
- # ConPTY handles alive while this terminal remains a disposable client.
370
- activate_runtime_environment(replace(config, tmux_binary="tmux"))
371
- if not args.inside_tmux:
372
- from railmux.self_update import maybe_upgrade_before_launch
373
- from railmux.settings import Settings
374
-
375
- maybe_upgrade_before_launch(raw_args, Settings())
376
- from railmux.winlocal.client import main as native_main
377
-
378
- return native_main(raw_args, config=config)
379
341
  if config.tmux_binary != "tmux":
380
342
  tmux_check = check_executable("tmux", config.tmux_binary)
381
343
  if not tmux_check.valid:
@@ -13,15 +13,12 @@ import hashlib
13
13
  import json
14
14
  import math
15
15
  import os
16
+ import stat
16
17
  import time
17
18
  from dataclasses import dataclass, replace
18
19
  from pathlib import Path
19
20
 
20
21
  from railmux.atomic_file import atomic_write_text
21
- from railmux.platform.file_security import (
22
- prepare_private_directory,
23
- private_regular_file,
24
- )
25
22
  from railmux.models import (
26
23
  AttentionCategory,
27
24
  AttentionState,
@@ -460,7 +457,9 @@ class CodexIndex:
460
457
  try:
461
458
  info = path.lstat()
462
459
  if (
463
- not private_regular_file(info)
460
+ not stat.S_ISREG(info.st_mode)
461
+ or info.st_uid != os.getuid()
462
+ or info.st_mode & 0o077
464
463
  or info.st_size > _PERSISTENT_CACHE_MAX_BYTES
465
464
  ):
466
465
  return
@@ -515,7 +514,17 @@ class CodexIndex:
515
514
  path = self._cache_path
516
515
  if path is None:
517
516
  return False
518
- return prepare_private_directory(path.parent)
517
+ parent = path.parent
518
+ try:
519
+ parent.mkdir(mode=0o700, parents=True, exist_ok=True)
520
+ info = parent.lstat()
521
+ if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid():
522
+ return False
523
+ if info.st_mode & 0o077:
524
+ os.chmod(parent, stat.S_IMODE(info.st_mode) & ~0o077)
525
+ return True
526
+ except OSError:
527
+ return False
519
528
 
520
529
  def _save_persistent_cache(self) -> None:
521
530
  path = self._cache_path
@@ -6,8 +6,6 @@ from dataclasses import dataclass
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
- from railmux.platform.config_paths import default_config_path as _platform_config_path
10
-
11
9
  try:
12
10
  import tomllib
13
11
  except ModuleNotFoundError: # Python 3.9-3.10
@@ -57,7 +55,7 @@ class Config:
57
55
 
58
56
 
59
57
  def default_config_path() -> Path:
60
- return _platform_config_path()
58
+ return Path.home() / ".config" / "railmux" / "config.toml"
61
59
 
62
60
 
63
61
  def _table(data: dict[str, Any], name: str) -> dict[str, Any]:
@@ -7,8 +7,7 @@ from pathlib import Path
7
7
 
8
8
  from railmux.atomic_file import atomic_write_text
9
9
  from railmux.models import Project
10
- from railmux.path_codec import _claude_encode_path, decode
11
- from railmux.platform.config_paths import config_dir
10
+ from railmux.path_codec import decode
12
11
  from railmux.session_index import _looks_like_uuid, _scan_session
13
12
 
14
13
 
@@ -38,7 +37,7 @@ def _path_cache_file() -> Path:
38
37
  Decoding an encoded project name is expensive on an NFS home (see
39
38
  path_codec), and the mapping is stable, so we persist it across launches.
40
39
  """
41
- return config_dir() / "path-cache.json"
40
+ return Path.home() / ".config" / "railmux" / "path-cache.json"
42
41
 
43
42
 
44
43
  def _load_path_cache() -> dict[str, str]:
@@ -95,7 +94,7 @@ def list_projects(claude_home: Path) -> list[Project]:
95
94
  for entry in scan:
96
95
  if not entry.is_dir(follow_symlinks=False):
97
96
  continue
98
- if os.name != "nt" and not entry.name.startswith("-"):
97
+ if not entry.name.startswith("-"):
99
98
  continue
100
99
  seen.add(entry.name)
101
100
 
@@ -107,16 +106,10 @@ def list_projects(claude_home: Path) -> list[Project]:
107
106
  if cached is not None and Path(cached).is_dir():
108
107
  real_path = Path(cached)
109
108
  else:
110
- decoded = None
111
- if os.name == "nt":
112
- decoded = _windows_project_path_from_transcript(
113
- Path(entry.path), entry.name
114
- )
115
- else:
116
- try:
117
- decoded = decode(entry.name)
118
- except ValueError:
119
- pass
109
+ try:
110
+ decoded = decode(entry.name)
111
+ except ValueError:
112
+ decoded = None
120
113
  # Skip projects whose original directory no longer exists on
121
114
  # this machine (deleted or moved). They can't be launched —
122
115
  # `cd` into a missing dir fails — so don't list them, and drop
@@ -163,58 +156,6 @@ def list_projects(claude_home: Path) -> list[Project]:
163
156
  return results
164
157
 
165
158
 
166
- def _windows_project_path_from_transcript(
167
- claude_dir: Path,
168
- encoded_name: str,
169
- ) -> Path | None:
170
- """Recover a Windows project cwd from provider-owned transcript metadata.
171
-
172
- Drive letters and UNC roots do not fit the POSIX path backtracker. Claude
173
- records the exact cwd in its JSONL, so validate that bounded value against
174
- the enclosing encoded directory instead of guessing a Windows path.
175
- """
176
- try:
177
- candidates = sorted(
178
- claude_dir.glob("*.jsonl"),
179
- key=lambda path: path.stat().st_mtime_ns,
180
- reverse=True,
181
- )[:8]
182
- except OSError:
183
- return None
184
- for path in candidates:
185
- try:
186
- with path.open("r", encoding="utf-8", errors="replace") as stream:
187
- for _line_number in range(64):
188
- line = stream.readline(64 * 1024 + 1)
189
- if not line:
190
- break
191
- if len(line) > 64 * 1024:
192
- continue
193
- try:
194
- record = json.loads(line)
195
- except (json.JSONDecodeError, ValueError):
196
- continue
197
- raw_cwd = record.get("cwd") if isinstance(record, dict) else None
198
- if (
199
- not isinstance(raw_cwd, str)
200
- or not raw_cwd
201
- or len(raw_cwd) > 32768
202
- or "\0" in raw_cwd
203
- ):
204
- continue
205
- candidate = Path(raw_cwd)
206
- if (
207
- candidate.is_absolute()
208
- and candidate.is_dir()
209
- and _claude_encode_path(str(candidate)).casefold()
210
- == encoded_name.casefold()
211
- ):
212
- return candidate
213
- except OSError:
214
- continue
215
- return None
216
-
217
-
218
159
  def _count_and_latest_mtime(claude_dir: Path, real_path: Path) -> tuple[int, float]:
219
160
  """Count sessions that pass every ``_scan_session`` filter and return the max
220
161
  JSONL mtime. Uses ``_scan_session`` so the count exactly matches the
@@ -738,6 +738,22 @@ class AgentDisplayTransport:
738
738
  def _with_phase(state: SwapState, phase: str) -> SwapState:
739
739
  return SwapState(**{**asdict(state), "phase": phase})
740
740
 
741
+ @staticmethod
742
+ def _fit_home_to_display(state: SwapState) -> None:
743
+ """Best-effort preserve the provider's visible geometry at home.
744
+
745
+ While a real pane is displayed, its named session contains only the
746
+ inert placeholder. Resize that detached home window to the real
747
+ pane's current size before swapping back. Otherwise background output
748
+ is formatted at the placeholder's older (often much narrower) launch
749
+ geometry and later appears as a narrow discontinuity in scrollback.
750
+
751
+ Geometry is presentation-only: failure must never weaken or abort the
752
+ identity-pinned return transaction.
753
+ """
754
+ tmux_ctl.fit_session_to_pane(
755
+ state.agent_tmux_name, state.agent_pane_id)
756
+
741
757
  def park(self, slot: AgentSlot) -> bool:
742
758
  """Move one displayed swap agent home without forgetting its slot.
743
759
 
@@ -764,6 +780,7 @@ class AgentDisplayTransport:
764
780
  parking = self._with_phase(state, "parking")
765
781
  if not _write_marker_pair(parking, slot.key):
766
782
  return False
783
+ self._fit_home_to_display(state)
767
784
  if not tmux_ctl.swap_panes(
768
785
  state.agent_pane_id, state.placeholder_pane_id):
769
786
  _write_marker_pair(state, slot.key)
@@ -857,6 +874,7 @@ class AgentDisplayTransport:
857
874
  return False
858
875
  returning = SwapState(**{**asdict(state), "phase": "returning"})
859
876
  _write_marker_pair(returning, slot.key)
877
+ self._fit_home_to_display(state)
860
878
  if not tmux_ctl.swap_panes(
861
879
  state.agent_pane_id, state.placeholder_pane_id):
862
880
  return False
@@ -14,12 +14,15 @@ import base64
14
14
  import json
15
15
  import os
16
16
  import re
17
+ import select
17
18
  import selectors
18
19
  import shlex
19
20
  import shutil
20
21
  import subprocess
21
22
  import sys
23
+ import termios
22
24
  import time
25
+ import tty
23
26
  from collections.abc import Callable
24
27
  from dataclasses import dataclass
25
28
  from enum import Enum
@@ -78,8 +81,6 @@ from railmux.fast_display_protocol import (
78
81
  encode_path_open_request,
79
82
  encode_resize,
80
83
  )
81
- from railmux.platform.console import RawConsole
82
- from railmux.platform.multiplex import PortableSelector, wait_readable
83
84
  from railmux.pane_surface import render_startup_surface
84
85
  from railmux.ssh_compat import CompatibilityFacts, decide as decide_compatibility
85
86
  from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
@@ -319,7 +320,7 @@ def await_remote_attach_status(
319
320
  readers = [process.stdout.fileno()]
320
321
  if cancel_fd is not None:
321
322
  readers.append(cancel_fd)
322
- readable = wait_readable(readers, remaining)
323
+ readable, _writable, _exceptional = select.select(readers, [], [], remaining)
323
324
  if not readable:
324
325
  return RemoteAttachKind.TIMEOUT
325
326
  if cancel_fd is not None and cancel_fd in readable:
@@ -415,7 +416,7 @@ def await_remote_startup(
415
416
  readers = [process.stdout.fileno()]
416
417
  if cancel_fd is not None:
417
418
  readers.append(cancel_fd)
418
- readable = wait_readable(readers, remaining)
419
+ readable, _writable, _exceptional = select.select(readers, [], [], remaining)
419
420
  if not readable:
420
421
  return RemoteStartup(RemoteStartKind.TIMEOUT)
421
422
  if cancel_fd is not None and cancel_fd in readable:
@@ -602,8 +603,20 @@ def _is_soft_keyboard_projection(
602
603
  )
603
604
 
604
605
 
605
- class RawTerminal(RawConsole):
606
- """Backward-compatible name for the platform raw-console port."""
606
+ class RawTerminal:
607
+ def __init__(self, fd: int) -> None:
608
+ self.fd = fd
609
+ self.saved: Optional[list[object]] = None
610
+
611
+ def __enter__(self) -> "RawTerminal":
612
+ self.saved = termios.tcgetattr(self.fd)
613
+ tty.setraw(self.fd)
614
+ return self
615
+
616
+ def __exit__(self, *_exc: object) -> None:
617
+ if self.saved is not None:
618
+ termios.tcsetattr(self.fd, termios.TCSADRAIN, self.saved)
619
+ self.saved = None
607
620
 
608
621
 
609
622
  class TerminalSurface:
@@ -1061,7 +1074,7 @@ class TerminalSurface:
1061
1074
  self,
1062
1075
  requested: TerminalMode,
1063
1076
  ) -> TerminalMode:
1064
- """Mirror only input-affecting modes explicitly carried by protocol v14."""
1077
+ """Mirror only input-affecting modes explicitly carried by protocol v15."""
1065
1078
  disabled = self.terminal_modes & ~requested
1066
1079
  enabled = requested & ~self.terminal_modes
1067
1080
  controls: list[bytes] = []
@@ -1877,7 +1890,9 @@ def _wait_reconnect_delay(delay: float, cancel_fd: int) -> None:
1877
1890
  remaining = deadline - time.monotonic()
1878
1891
  if remaining <= 0:
1879
1892
  return
1880
- readable = wait_readable([cancel_fd], remaining)
1893
+ readable, _writable, _exceptional = select.select(
1894
+ [cancel_fd], [], [], remaining
1895
+ )
1881
1896
  if readable:
1882
1897
  _consume_reconnect_input(cancel_fd)
1883
1898
 
@@ -1998,7 +2013,7 @@ def _finish_remote_attach(
1998
2013
  raise ProbeError("remote display helper failed before attaching")
1999
2014
 
2000
2015
  _stop_unstarted_remote(process)
2001
- # A current v14 helper holds the mutex only while registering its exact tmux
2016
+ # A current v15 helper holds the mutex only while registering its exact tmux
2002
2017
  # child. Give that ordinary race one fresh SSH process before presenting
2003
2018
  # the explicit legacy-lock takeover choice.
2004
2019
  time.sleep(_REMOTE_ATTACH_RETRY_DELAY)
@@ -2398,7 +2413,7 @@ def run(args: argparse.Namespace) -> int:
2398
2413
  touch_keyboard = TermuxTouchKeyboard(
2399
2414
  enabled=not args.no_mouse and termux_touch
2400
2415
  )
2401
- selector = PortableSelector()
2416
+ selector = selectors.DefaultSelector()
2402
2417
  selector.register(process.stdout.fileno(), selectors.EVENT_READ, "remote")
2403
2418
  selector.register(sys.stdin.fileno(), selectors.EVENT_READ, "local")
2404
2419
  started = time.monotonic()
@@ -2943,9 +2958,7 @@ def run(args: argparse.Namespace) -> int:
2943
2958
  )
2944
2959
  history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
2945
2960
  try:
2946
- with RawTerminal(
2947
- sys.stdin.fileno(), output_fd=sys.stdout.fileno()
2948
- ):
2961
+ with RawTerminal(sys.stdin.fileno()):
2949
2962
  while True:
2950
2963
  observed_size = os.get_terminal_size(sys.stdout.fileno())
2951
2964
  if observed_size != local_size: