railmux 0.3.1.dev202608010__tar.gz → 0.3.2.dev4__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 (192) hide show
  1. {railmux-0.3.1.dev202608010/src/railmux.egg-info → railmux-0.3.2.dev4}/PKG-INFO +11 -1
  2. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/README.md +6 -0
  3. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/pyproject.toml +4 -0
  4. railmux-0.3.2.dev4/src/railmux/__init__.py +1 -0
  5. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/cli.py +40 -2
  6. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/codex_index.py +6 -15
  7. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/config.py +3 -1
  8. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/diagnostics.py +4 -0
  9. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/discovery.py +68 -9
  10. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_client.py +43 -46
  11. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_input.py +5 -2
  12. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_server.py +8 -35
  13. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/favorites.py +4 -3
  14. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/help_workspace.py +2 -7
  15. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/legacy_sessions.py +2 -0
  16. railmux-0.3.2.dev4/src/railmux/local_clipboard.py +101 -0
  17. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/local_open.py +37 -10
  18. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/modes.py +8 -0
  19. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/mouse_manager.py +7 -3
  20. railmux-0.3.2.dev4/src/railmux/mux/__init__.py +5 -0
  21. railmux-0.3.2.dev4/src/railmux/mux/backend.py +59 -0
  22. railmux-0.3.2.dev4/src/railmux/mux/tmux_backend.py +161 -0
  23. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/orphan_marker.py +9 -14
  24. railmux-0.3.2.dev4/src/railmux/platform/__init__.py +44 -0
  25. railmux-0.3.2.dev4/src/railmux/platform/config_paths.py +30 -0
  26. railmux-0.3.2.dev4/src/railmux/platform/console.py +165 -0
  27. railmux-0.3.2.dev4/src/railmux/platform/file_security.py +34 -0
  28. railmux-0.3.2.dev4/src/railmux/platform/filelock.py +42 -0
  29. railmux-0.3.2.dev4/src/railmux/platform/multiplex.py +132 -0
  30. railmux-0.3.2.dev4/src/railmux/platform/process.py +21 -0
  31. railmux-0.3.2.dev4/src/railmux/platform/runtime_paths.py +75 -0
  32. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/renames.py +4 -3
  33. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/restart_state.py +7 -12
  34. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/runtime_config.py +8 -1
  35. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/scroll_manager.py +3 -5
  36. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/self_update.py +2 -0
  37. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/session_index.py +1 -1
  38. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/settings.py +1 -1
  39. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ssh_display_diagnostics.py +3 -5
  40. railmux-0.3.2.dev4/src/railmux/terminal_emulator.py +180 -0
  41. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tmux_binding_manager.py +7 -3
  42. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tmux_ctl.py +14 -4
  43. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tool_panes.py +2 -0
  44. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/app.py +324 -246
  45. railmux-0.3.2.dev4/src/railmux/winlocal/__init__.py +2 -0
  46. railmux-0.3.2.dev4/src/railmux/winlocal/backend.py +897 -0
  47. railmux-0.3.2.dev4/src/railmux/winlocal/client.py +275 -0
  48. railmux-0.3.2.dev4/src/railmux/winlocal/compositor.py +254 -0
  49. railmux-0.3.2.dev4/src/railmux/winlocal/conpty.py +200 -0
  50. railmux-0.3.2.dev4/src/railmux/winlocal/daemon.py +359 -0
  51. railmux-0.3.2.dev4/src/railmux/winlocal/history_viewer.py +93 -0
  52. railmux-0.3.2.dev4/src/railmux/winlocal/ipc.py +88 -0
  53. railmux-0.3.2.dev4/src/railmux/winlocal/session_store.py +96 -0
  54. railmux-0.3.2.dev4/src/railmux/winlocal/virtual_screen.py +105 -0
  55. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4/src/railmux.egg-info}/PKG-INFO +11 -1
  56. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/SOURCES.txt +33 -0
  57. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/requires.txt +4 -0
  58. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_cli.py +15 -0
  59. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_codex_delete.py +7 -3
  60. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_config.py +1 -0
  61. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_discovery.py +24 -0
  62. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_fast_full_window.py +50 -17
  63. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_local_open.py +26 -0
  64. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_modes.py +2 -0
  65. railmux-0.3.2.dev4/tests/test_mux_backend.py +60 -0
  66. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_orphan_recovery.py +2 -2
  67. railmux-0.3.2.dev4/tests/test_platform.py +248 -0
  68. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_release_workflow.py +9 -0
  69. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_runtime_config.py +12 -0
  70. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_soft_quit.py +2 -2
  71. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_wheel_smoke.py +6 -1
  72. railmux-0.3.2.dev4/tests/test_winlocal_backend.py +176 -0
  73. railmux-0.3.2.dev4/tests/test_winlocal_compositor.py +48 -0
  74. railmux-0.3.2.dev4/tests/test_winlocal_conpty.py +60 -0
  75. railmux-0.3.2.dev4/tests/test_winlocal_daemon.py +150 -0
  76. railmux-0.3.2.dev4/tests/test_winlocal_history_viewer.py +43 -0
  77. railmux-0.3.2.dev4/tests/test_winlocal_ipc.py +39 -0
  78. railmux-0.3.2.dev4/tests/test_winlocal_real_conpty.py +61 -0
  79. railmux-0.3.2.dev4/tests/test_winlocal_session_store.py +42 -0
  80. railmux-0.3.2.dev4/tests/test_winlocal_virtual_screen.py +62 -0
  81. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tools/wheel_smoke.py +12 -6
  82. railmux-0.3.1.dev202608010/src/railmux/__init__.py +0 -1
  83. railmux-0.3.1.dev202608010/src/railmux/local_clipboard.py +0 -47
  84. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/LICENSE +0 -0
  85. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/MANIFEST.in +0 -0
  86. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/setup.cfg +0 -0
  87. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/__main__.py +0 -0
  88. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/atomic_file.py +0 -0
  89. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/background_index.py +0 -0
  90. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/config_cli.py +0 -0
  91. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/display_transport.py +0 -0
  92. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_history.py +0 -0
  93. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_protocol.py +0 -0
  94. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fuzzy.py +0 -0
  95. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/launcher.py +0 -0
  96. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/models.py +0 -0
  97. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/pane_surface.py +0 -0
  98. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/path_codec.py +0 -0
  99. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/remote_config.py +0 -0
  100. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/scroll_agent.py +0 -0
  101. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/selection_isolation.py +0 -0
  102. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/session_cache.py +0 -0
  103. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/setting_contracts.py +0 -0
  104. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ssh_args.py +0 -0
  105. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ssh_compat.py +0 -0
  106. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ssh_doctor.py +0 -0
  107. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/system_deps.py +0 -0
  108. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/terminal_status.py +0 -0
  109. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tmux_health.py +0 -0
  110. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tmux_server.py +0 -0
  111. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/transcript.py +0 -0
  112. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/__init__.py +0 -0
  113. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/_widgets.py +0 -0
  114. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/keymap.py +0 -0
  115. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/modals.py +0 -0
  116. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/projects_pane.py +0 -0
  117. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/running_pane.py +0 -0
  118. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/sessions_pane.py +0 -0
  119. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/sidebar.py +0 -0
  120. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/statusbar.py +0 -0
  121. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/workspace.py +0 -0
  122. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
  123. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
  124. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/top_level.txt +0 -0
  125. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_agent_workspace.py +0 -0
  126. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_atomic_file.py +0 -0
  127. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_background_index.py +0 -0
  128. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_codex_index.py +0 -0
  129. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_codex_lineage.py +0 -0
  130. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_codex_transcript.py +0 -0
  131. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_config_cli.py +0 -0
  132. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_denested_transport_benchmark.py +0 -0
  133. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_diagnostics.py +0 -0
  134. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_display_transport.py +0 -0
  135. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_focus_highlight.py +0 -0
  136. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_fullscreen_binding.py +0 -0
  137. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_fuzzy.py +0 -0
  138. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_help_agent.py +0 -0
  139. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_help_workspace.py +0 -0
  140. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_history_cleanup.py +0 -0
  141. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_keymap.py +0 -0
  142. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_launcher.py +0 -0
  143. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_layout_profile.py +0 -0
  144. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_legacy_sessions.py +0 -0
  145. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_local_clipboard.py +0 -0
  146. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_modals.py +0 -0
  147. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_mode_selection.py +0 -0
  148. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_mouse_manager.py +0 -0
  149. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_new_project.py +0 -0
  150. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_options.py +0 -0
  151. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_pane_surface.py +0 -0
  152. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_path_codec.py +0 -0
  153. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_projects_pane.py +0 -0
  154. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_release_notes.py +0 -0
  155. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_remote_config.py +0 -0
  156. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_renames.py +0 -0
  157. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_restart_state.py +0 -0
  158. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_running_pane.py +0 -0
  159. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_running_sort.py +0 -0
  160. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_scroll_agent.py +0 -0
  161. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_scroll_manager.py +0 -0
  162. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_selection_isolation.py +0 -0
  163. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_self_update.py +0 -0
  164. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_session_cache.py +0 -0
  165. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_session_index.py +0 -0
  166. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_sessions_pane.py +0 -0
  167. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_setting_contracts.py +0 -0
  168. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_settings.py +0 -0
  169. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_sidebar.py +0 -0
  170. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_args.py +0 -0
  171. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_compat.py +0 -0
  172. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_display_benchmark.py +0 -0
  173. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
  174. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_doctor.py +0 -0
  175. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_status.py +0 -0
  176. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_statusbar.py +0 -0
  177. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_system_deps.py +0 -0
  178. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_terminal_size.py +0 -0
  179. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_terminal_status.py +0 -0
  180. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_binding_manager.py +0 -0
  181. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_ctl.py +0 -0
  182. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_health.py +0 -0
  183. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_integration.py +0 -0
  184. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_server.py +0 -0
  185. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_status.py +0 -0
  186. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tool_panes.py +0 -0
  187. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_transcript.py +0 -0
  188. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_web_demo_recorder.py +0 -0
  189. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_widgets.py +0 -0
  190. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_yolo_prompt.py +0 -0
  191. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tools/denested_transport_benchmark.py +0 -0
  192. {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tools/release_notes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.1.dev202608010
3
+ Version: 0.3.2.dev4
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,12 +14,14 @@ 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
17
18
  Classifier: Programming Language :: Python :: 3
18
19
  Classifier: Programming Language :: Python :: 3.9
19
20
  Classifier: Programming Language :: Python :: 3.10
20
21
  Classifier: Programming Language :: Python :: 3.11
21
22
  Classifier: Programming Language :: Python :: 3.12
22
23
  Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
23
25
  Classifier: Topic :: Software Development
24
26
  Classifier: Topic :: Terminals
25
27
  Classifier: Topic :: Utilities
@@ -30,6 +32,8 @@ Requires-Dist: packaging>=23.0
30
32
  Requires-Dist: tomlkit<1,>=0.13.2
31
33
  Requires-Dist: urwid>=2.6.16
32
34
  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"
33
37
  Provides-Extra: ssh
34
38
  Requires-Dist: pyte<0.9,>=0.8.2; extra == "ssh"
35
39
  Provides-Extra: dev
@@ -127,6 +131,12 @@ not permanent traps: press `o`, or choose **More → Options**, to change them
127
131
  later. One-time safety confirmations such as dependency installation and
128
132
  session deletion are intentionally asked at the action itself.
129
133
 
134
+ For setup outside the full-screen UI, run `railmux config`; it edits the same
135
+ settings as Options and also covers executable paths, SSH history capacity,
136
+ and the runtime locale. Use `railmux config --remote your-server` to configure
137
+ that host without opening or resizing its tmux workspace. See
138
+ [Configuration](#configuration) for the complete interface.
139
+
130
140
  If ordinary SSH cannot keep up with full-screen redraws, install Railmux on
131
141
  your local macOS, Linux, or Windows WSL environment and use its responsive,
132
142
  locally cached SSH display:
@@ -85,6 +85,12 @@ not permanent traps: press `o`, or choose **More → Options**, to change them
85
85
  later. One-time safety confirmations such as dependency installation and
86
86
  session deletion are intentionally asked at the action itself.
87
87
 
88
+ For setup outside the full-screen UI, run `railmux config`; it edits the same
89
+ settings as Options and also covers executable paths, SSH history capacity,
90
+ and the runtime locale. Use `railmux config --remote your-server` to configure
91
+ that host without opening or resizing its tmux workspace. See
92
+ [Configuration](#configuration) for the complete interface.
93
+
88
94
  If ordinary SSH cannot keep up with full-screen redraws, install Railmux on
89
95
  your local macOS, Linux, or Windows WSL environment and use its responsive,
90
96
  locally cached SSH display:
@@ -20,12 +20,14 @@ classifiers = [
20
20
  "Intended Audience :: Developers",
21
21
  "Operating System :: POSIX :: Linux",
22
22
  "Operating System :: MacOS",
23
+ "Operating System :: Microsoft :: Windows",
23
24
  "Programming Language :: Python :: 3",
24
25
  "Programming Language :: Python :: 3.9",
25
26
  "Programming Language :: Python :: 3.10",
26
27
  "Programming Language :: Python :: 3.11",
27
28
  "Programming Language :: Python :: 3.12",
28
29
  "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
29
31
  "Topic :: Software Development",
30
32
  "Topic :: Terminals",
31
33
  "Topic :: Utilities",
@@ -35,6 +37,8 @@ dependencies = [
35
37
  "tomlkit>=0.13.2,<1",
36
38
  "urwid>=2.6.16",
37
39
  "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'",
38
42
  ]
39
43
 
40
44
  [project.optional-dependencies]
@@ -0,0 +1 @@
1
+ __version__ = "0.3.2.dev4"
@@ -1,18 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import os
4
5
  import shutil
5
6
  import subprocess
6
7
  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
15
14
  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,9 +37,29 @@ _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
+
40
58
  def _restore_terminal(attributes: list | None) -> None:
41
59
  if attributes is None:
42
60
  return
61
+ import termios
62
+
43
63
  try:
44
64
  termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
45
65
  except (OSError, termios.error):
@@ -67,6 +87,8 @@ def _run_tmux_client_with_watchdog(
67
87
  """Keep one monitor outside tmux so a frozen server cannot trap the TTY."""
68
88
  attributes = None
69
89
  if sys.stdin.isatty():
90
+ import termios
91
+
70
92
  try:
71
93
  attributes = termios.tcgetattr(sys.stdin.fileno())
72
94
  except (OSError, termios.error):
@@ -152,6 +174,10 @@ def _run_tmux_client_with_watchdog(
152
174
 
153
175
 
154
176
  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
155
181
  raw_args = list(sys.argv[1:] if argv is None else argv)
156
182
  if raw_args and raw_args[0] == "config":
157
183
  from railmux.config_cli import main as config_main
@@ -338,6 +364,18 @@ def main(argv: list[str] | None = None) -> int:
338
364
  file=sys.stderr,
339
365
  )
340
366
  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)
341
379
  if config.tmux_binary != "tmux":
342
380
  tmux_check = check_executable("tmux", config.tmux_binary)
343
381
  if not tmux_check.valid:
@@ -13,12 +13,15 @@ import hashlib
13
13
  import json
14
14
  import math
15
15
  import os
16
- import stat
17
16
  import time
18
17
  from dataclasses import dataclass, replace
19
18
  from pathlib import Path
20
19
 
21
20
  from railmux.atomic_file import atomic_write_text
21
+ from railmux.platform.file_security import (
22
+ prepare_private_directory,
23
+ private_regular_file,
24
+ )
22
25
  from railmux.models import (
23
26
  AttentionCategory,
24
27
  AttentionState,
@@ -457,9 +460,7 @@ class CodexIndex:
457
460
  try:
458
461
  info = path.lstat()
459
462
  if (
460
- not stat.S_ISREG(info.st_mode)
461
- or info.st_uid != os.getuid()
462
- or info.st_mode & 0o077
463
+ not private_regular_file(info)
463
464
  or info.st_size > _PERSISTENT_CACHE_MAX_BYTES
464
465
  ):
465
466
  return
@@ -514,17 +515,7 @@ class CodexIndex:
514
515
  path = self._cache_path
515
516
  if path is None:
516
517
  return False
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
518
+ return prepare_private_directory(path.parent)
528
519
 
529
520
  def _save_persistent_cache(self) -> None:
530
521
  path = self._cache_path
@@ -6,6 +6,8 @@ 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
+
9
11
  try:
10
12
  import tomllib
11
13
  except ModuleNotFoundError: # Python 3.9-3.10
@@ -55,7 +57,7 @@ class Config:
55
57
 
56
58
 
57
59
  def default_config_path() -> Path:
58
- return Path.home() / ".config" / "railmux" / "config.toml"
60
+ return _platform_config_path()
59
61
 
60
62
 
61
63
  def _table(data: dict[str, Any], name: str) -> dict[str, Any]:
@@ -151,6 +151,8 @@ def _tool_diagnostic(
151
151
  env=environ,
152
152
  capture_output=True,
153
153
  text=True,
154
+ encoding="utf-8",
155
+ errors="replace",
154
156
  timeout=3,
155
157
  check=False,
156
158
  )
@@ -300,6 +302,8 @@ def collect_doctor_snapshot(
300
302
  env=effective_env,
301
303
  capture_output=True,
302
304
  text=True,
305
+ encoding="utf-8",
306
+ errors="replace",
303
307
  timeout=2,
304
308
  check=False,
305
309
  )
@@ -7,7 +7,8 @@ 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 decode
10
+ from railmux.path_codec import _claude_encode_path, decode
11
+ from railmux.platform.config_paths import config_dir
11
12
  from railmux.session_index import _looks_like_uuid, _scan_session
12
13
 
13
14
 
@@ -37,14 +38,14 @@ def _path_cache_file() -> Path:
37
38
  Decoding an encoded project name is expensive on an NFS home (see
38
39
  path_codec), and the mapping is stable, so we persist it across launches.
39
40
  """
40
- return Path.home() / ".config" / "railmux" / "path-cache.json"
41
+ return config_dir() / "path-cache.json"
41
42
 
42
43
 
43
44
  def _load_path_cache() -> dict[str, str]:
44
45
  try:
45
- data = json.loads(_path_cache_file().read_text())
46
+ data = json.loads(_path_cache_file().read_text(encoding="utf-8"))
46
47
  return data if isinstance(data, dict) else {}
47
- except (OSError, json.JSONDecodeError):
48
+ except (OSError, UnicodeError, json.JSONDecodeError):
48
49
  return {}
49
50
 
50
51
 
@@ -94,7 +95,7 @@ def list_projects(claude_home: Path) -> list[Project]:
94
95
  for entry in scan:
95
96
  if not entry.is_dir(follow_symlinks=False):
96
97
  continue
97
- if not entry.name.startswith("-"):
98
+ if os.name != "nt" and not entry.name.startswith("-"):
98
99
  continue
99
100
  seen.add(entry.name)
100
101
 
@@ -106,10 +107,16 @@ def list_projects(claude_home: Path) -> list[Project]:
106
107
  if cached is not None and Path(cached).is_dir():
107
108
  real_path = Path(cached)
108
109
  else:
109
- try:
110
- decoded = decode(entry.name)
111
- except ValueError:
112
- decoded = None
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
113
120
  # Skip projects whose original directory no longer exists on
114
121
  # this machine (deleted or moved). They can't be launched —
115
122
  # `cd` into a missing dir fails — so don't list them, and drop
@@ -156,6 +163,58 @@ def list_projects(claude_home: Path) -> list[Project]:
156
163
  return results
157
164
 
158
165
 
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
+
159
218
  def _count_and_latest_mtime(claude_dir: Path, real_path: Path) -> tuple[int, float]:
160
219
  """Count sessions that pass every ``_scan_session`` filter and return the max
161
220
  JSONL mtime. Uses ``_scan_session`` so the count exactly matches the
@@ -14,15 +14,12 @@ import base64
14
14
  import json
15
15
  import os
16
16
  import re
17
- import select
18
17
  import selectors
19
18
  import shlex
20
19
  import shutil
21
20
  import subprocess
22
21
  import sys
23
- import termios
24
22
  import time
25
- import tty
26
23
  from collections.abc import Callable
27
24
  from dataclasses import dataclass
28
25
  from enum import Enum
@@ -48,6 +45,7 @@ from railmux.fast_display_input import (
48
45
  SelectionSegment,
49
46
  SgrMouseEvent,
50
47
  TermuxTouchKeyboard,
48
+ TouchKeyboardAction,
51
49
  TerminalInputDecoder,
52
50
  is_termux_environment,
53
51
  page_key_direction,
@@ -80,6 +78,8 @@ from railmux.fast_display_protocol import (
80
78
  encode_path_open_request,
81
79
  encode_resize,
82
80
  )
81
+ from railmux.platform.console import RawConsole
82
+ from railmux.platform.multiplex import PortableSelector, wait_readable
83
83
  from railmux.pane_surface import render_startup_surface
84
84
  from railmux.ssh_compat import CompatibilityFacts, decide as decide_compatibility
85
85
  from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
@@ -224,6 +224,33 @@ def compact_status_row(screen: AppliedScreen) -> int | None:
224
224
  return None
225
225
 
226
226
 
227
+ def termux_prompt_touch_action(
228
+ touch_keyboard: TermuxTouchKeyboard,
229
+ event: SgrMouseEvent,
230
+ history: LocalHistoryView,
231
+ screen: AppliedScreen | None,
232
+ *,
233
+ now: float | None = None,
234
+ ) -> TouchKeyboardAction:
235
+ """Classify one translated Termux tap against live screen route authority."""
236
+ if screen is None:
237
+ return TouchKeyboardAction()
238
+ clicked_pane_id = history.pane_id_at_position(event.x - 1, event.y - 1)
239
+ cursor_pane_id = history.pane_id_at_position(
240
+ screen.cursor_x,
241
+ screen.cursor_y,
242
+ )
243
+ return touch_keyboard.pointer_event(
244
+ event,
245
+ clicked_pane_id=clicked_pane_id,
246
+ cursor_pane_id=cursor_pane_id,
247
+ cursor_y=screen.cursor_y,
248
+ pane_frozen=history.pane_is_frozen(clicked_pane_id),
249
+ navigation_row=compact_status_row(screen),
250
+ now=now,
251
+ )
252
+
253
+
227
254
  class ProbeError(RuntimeError):
228
255
  """A bounded, user-facing SSH display failure."""
229
256
 
@@ -292,7 +319,7 @@ def await_remote_attach_status(
292
319
  readers = [process.stdout.fileno()]
293
320
  if cancel_fd is not None:
294
321
  readers.append(cancel_fd)
295
- readable, _writable, _exceptional = select.select(readers, [], [], remaining)
322
+ readable = wait_readable(readers, remaining)
296
323
  if not readable:
297
324
  return RemoteAttachKind.TIMEOUT
298
325
  if cancel_fd is not None and cancel_fd in readable:
@@ -388,7 +415,7 @@ def await_remote_startup(
388
415
  readers = [process.stdout.fileno()]
389
416
  if cancel_fd is not None:
390
417
  readers.append(cancel_fd)
391
- readable, _writable, _exceptional = select.select(readers, [], [], remaining)
418
+ readable = wait_readable(readers, remaining)
392
419
  if not readable:
393
420
  return RemoteStartup(RemoteStartKind.TIMEOUT)
394
421
  if cancel_fd is not None and cancel_fd in readable:
@@ -575,20 +602,8 @@ def _is_soft_keyboard_projection(
575
602
  )
576
603
 
577
604
 
578
- class RawTerminal:
579
- def __init__(self, fd: int) -> None:
580
- self.fd = fd
581
- self.saved: Optional[list[object]] = None
582
-
583
- def __enter__(self) -> "RawTerminal":
584
- self.saved = termios.tcgetattr(self.fd)
585
- tty.setraw(self.fd)
586
- return self
587
-
588
- def __exit__(self, *_exc: object) -> None:
589
- if self.saved is not None:
590
- termios.tcsetattr(self.fd, termios.TCSADRAIN, self.saved)
591
- self.saved = None
605
+ class RawTerminal(RawConsole):
606
+ """Backward-compatible name for the platform raw-console port."""
592
607
 
593
608
 
594
609
  class TerminalSurface:
@@ -1862,9 +1877,7 @@ def _wait_reconnect_delay(delay: float, cancel_fd: int) -> None:
1862
1877
  remaining = deadline - time.monotonic()
1863
1878
  if remaining <= 0:
1864
1879
  return
1865
- readable, _writable, _exceptional = select.select(
1866
- [cancel_fd], [], [], remaining
1867
- )
1880
+ readable = wait_readable([cancel_fd], remaining)
1868
1881
  if readable:
1869
1882
  _consume_reconnect_input(cancel_fd)
1870
1883
 
@@ -2385,7 +2398,7 @@ def run(args: argparse.Namespace) -> int:
2385
2398
  touch_keyboard = TermuxTouchKeyboard(
2386
2399
  enabled=not args.no_mouse and termux_touch
2387
2400
  )
2388
- selector = selectors.DefaultSelector()
2401
+ selector = PortableSelector()
2389
2402
  selector.register(process.stdout.fileno(), selectors.EVENT_READ, "remote")
2390
2403
  selector.register(sys.stdin.fileno(), selectors.EVENT_READ, "local")
2391
2404
  started = time.monotonic()
@@ -2738,34 +2751,16 @@ def run(args: argparse.Namespace) -> int:
2738
2751
  latest_screen.cursor_x, latest_screen.cursor_y
2739
2752
  )
2740
2753
  )
2741
- clicked_pane_id = (
2742
- None
2743
- if latest_screen is None
2744
- else history.pane_id_at_position(part.x - 1, part.y - 1)
2745
- )
2746
- cursor_pane_id = (
2747
- None
2748
- if latest_screen is None
2749
- else history.pane_id_at_position(
2750
- latest_screen.cursor_x,
2751
- latest_screen.cursor_y,
2752
- )
2753
- )
2754
2754
  status_row = (
2755
2755
  compact_status_row(latest_screen)
2756
2756
  if latest_screen is not None
2757
2757
  else None
2758
2758
  )
2759
- touch_action = touch_keyboard.pointer_event(
2759
+ touch_action = termux_prompt_touch_action(
2760
+ touch_keyboard,
2760
2761
  part,
2761
- clicked_pane_id=clicked_pane_id,
2762
- cursor_pane_id=cursor_pane_id,
2763
- cursor_y=0 if latest_screen is None else latest_screen.cursor_y,
2764
- cursor_visible=(
2765
- False if latest_screen is None else latest_screen.cursor_visible
2766
- ),
2767
- pane_frozen=history.pane_is_frozen(clicked_pane_id),
2768
- navigation_row=status_row,
2762
+ history,
2763
+ latest_screen,
2769
2764
  now=time.monotonic(),
2770
2765
  )
2771
2766
  if touch_action.suspend_mouse:
@@ -2948,7 +2943,9 @@ def run(args: argparse.Namespace) -> int:
2948
2943
  )
2949
2944
  history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
2950
2945
  try:
2951
- with RawTerminal(sys.stdin.fileno()):
2946
+ with RawTerminal(
2947
+ sys.stdin.fileno(), output_fd=sys.stdout.fileno()
2948
+ ):
2952
2949
  while True:
2953
2950
  observed_size = os.get_terminal_size(sys.stdout.fileno())
2954
2951
  if observed_size != local_size:
@@ -129,7 +129,6 @@ class TermuxTouchKeyboard:
129
129
  clicked_pane_id: str | None,
130
130
  cursor_pane_id: str | None,
131
131
  cursor_y: int,
132
- cursor_visible: bool,
133
132
  pane_frozen: bool,
134
133
  navigation_row: int | None = None,
135
134
  now: float | None = None,
@@ -150,7 +149,6 @@ class TermuxTouchKeyboard:
150
149
  or self._active
151
150
  or self._close_reassert_pending
152
151
  or pane_frozen
153
- or not cursor_visible
154
152
  or clicked_pane_id is None
155
153
  or clicked_pane_id != cursor_pane_id
156
154
  or (navigation_row is not None and event.y == navigation_row)
@@ -158,6 +156,11 @@ class TermuxTouchKeyboard:
158
156
  or not self._is_plain_left_press(event)
159
157
  ):
160
158
  return TouchKeyboardAction()
159
+ # DEC cursor visibility is presentation state, not prompt authority.
160
+ # Providers may hide the hardware cursor while retaining its exact
161
+ # input coordinates. The verified live-agent route, matching cursor
162
+ # route, unfrozen viewport, and bounded row distance remain the
163
+ # fail-closed authority for yielding one Termux tap.
161
164
  requested_at = time.monotonic() if now is None else now
162
165
  # A rapid reopen supersedes the delayed ownership reassertion queued by
163
166
  # the previous close; Termux must receive this new native prompt tap.
@@ -83,6 +83,11 @@ from railmux.fast_display_protocol import (
83
83
  encode_path_open_result,
84
84
  encode_update,
85
85
  )
86
+ from railmux.terminal_emulator import (
87
+ extended_pyte as _shared_extended_pyte,
88
+ render_rows as _shared_render_rows,
89
+ terminal_modes_for_screen as _shared_terminal_modes_for_screen,
90
+ )
86
91
  from railmux.ui.workspace import (
87
92
  COMPACT_ENTER_HEIGHT,
88
93
  COMPACT_ENTER_WIDTH,
@@ -371,9 +376,7 @@ def _build_extended_pyte(pyte: object) -> object:
371
376
 
372
377
  def _extended_pyte(pyte: object) -> object:
373
378
  """Idempotently adapt one imported pyte module for tmux's VT stream."""
374
- if getattr(pyte, "_railmux_extended", False):
375
- return pyte
376
- return _build_extended_pyte(pyte)
379
+ return _shared_extended_pyte(pyte)
377
380
 
378
381
 
379
382
  @dataclass(frozen=True)
@@ -1887,42 +1890,12 @@ def _style_key(char: object) -> tuple[object, ...]:
1887
1890
 
1888
1891
  def render_rows(screen: object) -> tuple[bytes, ...]:
1889
1892
  """Render independently paintable rows with allowlisted SGR controls."""
1890
- rendered_rows: list[bytes] = []
1891
- for row_index in range(screen.lines):
1892
- rendered = [b"\033[0m"]
1893
- previous_style: tuple[object, ...] | None = None
1894
- row = screen.buffer[row_index]
1895
- for column in range(screen.columns):
1896
- char = row[column]
1897
- style = _style_key(char)
1898
- if style != previous_style:
1899
- rendered.append(_style(char))
1900
- previous_style = style
1901
- # pyte represents the second cell of a wide glyph as empty data;
1902
- # the real terminal already advances two columns for the glyph.
1903
- if char.data:
1904
- safe_data = "".join(
1905
- value
1906
- if value >= " "
1907
- and value != "\x7f"
1908
- and not "\x80" <= value <= "\x9f"
1909
- else "�"
1910
- for value in char.data
1911
- )
1912
- rendered.append(safe_data.encode("utf-8", errors="replace"))
1913
- rendered.append(b"\033[0m")
1914
- rendered_rows.append(b"".join(rendered))
1915
- return tuple(rendered_rows)
1893
+ return _shared_render_rows(screen)
1916
1894
 
1917
1895
 
1918
1896
  def terminal_modes_for_screen(screen: object) -> TerminalMode:
1919
1897
  """Project pyte's private-mode set onto the bounded v14 wire allowlist."""
1920
- terminal_modes = TerminalMode.NONE
1921
- if 2004 << 5 in screen.mode:
1922
- terminal_modes |= TerminalMode.BRACKETED_PASTE
1923
- if 1004 << 5 in screen.mode:
1924
- terminal_modes |= TerminalMode.FOCUS_EVENTS
1925
- return terminal_modes
1898
+ return _shared_terminal_modes_for_screen(screen)
1926
1899
 
1927
1900
 
1928
1901
  def _child_exited(pid: int) -> bool:
@@ -10,10 +10,11 @@ import json
10
10
  from pathlib import Path
11
11
 
12
12
  from railmux.atomic_file import atomic_write_text
13
+ from railmux.platform.config_paths import config_dir
13
14
 
14
15
 
15
16
  def _favorites_path() -> Path:
16
- return Path.home() / ".config" / "railmux" / "favorites.json"
17
+ return config_dir() / "favorites.json"
17
18
 
18
19
 
19
20
  class Favorites:
@@ -28,10 +29,10 @@ class Favorites:
28
29
  if not self._path.is_file():
29
30
  return
30
31
  try:
31
- data = json.loads(self._path.read_text())
32
+ data = json.loads(self._path.read_text(encoding="utf-8"))
32
33
  if isinstance(data, list):
33
34
  self._ids = set(data)
34
- except (json.JSONDecodeError, OSError):
35
+ except (json.JSONDecodeError, OSError, UnicodeError):
35
36
  self._ids = set()
36
37
 
37
38
  def _save(self) -> None: