railmux 0.4.0.dev29__tar.gz → 0.4.0.dev30__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 (183) hide show
  1. {railmux-0.4.0.dev29/src/railmux.egg-info → railmux-0.4.0.dev30}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev30/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/windows_install_log.py +6 -5
  4. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/windows_msys2.py +38 -9
  5. railmux-0.4.0.dev30/src/railmux/windows_paths.py +73 -0
  6. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30/src/railmux.egg-info}/PKG-INFO +1 -1
  7. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux.egg-info/SOURCES.txt +2 -0
  8. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_windows_install_log.py +19 -0
  9. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_windows_msys2.py +67 -0
  10. railmux-0.4.0.dev30/tests/test_windows_paths.py +83 -0
  11. railmux-0.4.0.dev29/src/railmux/__init__.py +0 -1
  12. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/LICENSE +0 -0
  13. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/MANIFEST.in +0 -0
  14. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/README.md +0 -0
  15. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/pyproject.toml +0 -0
  16. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/setup.cfg +0 -0
  17. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/__main__.py +0 -0
  18. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/atomic_file.py +0 -0
  19. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/background_index.py +0 -0
  20. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/cli.py +0 -0
  21. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/codex_index.py +0 -0
  22. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/config.py +0 -0
  23. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/config_cli.py +0 -0
  24. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/diagnostics.py +0 -0
  25. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/discovery.py +0 -0
  26. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/display_transport.py +0 -0
  27. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/entrypoint.py +0 -0
  28. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/fast_display_client.py +0 -0
  29. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/fast_display_history.py +0 -0
  30. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/fast_display_input.py +0 -0
  31. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/fast_display_protocol.py +0 -0
  32. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/fast_display_server.py +0 -0
  33. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/favorites.py +0 -0
  34. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/fuzzy.py +0 -0
  35. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/help_workspace.py +0 -0
  36. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/launcher.py +0 -0
  37. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/legacy_sessions.py +0 -0
  38. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/local_clipboard.py +0 -0
  39. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/local_open.py +0 -0
  40. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/models.py +0 -0
  41. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/modes.py +0 -0
  42. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/mouse_manager.py +0 -0
  43. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/orphan_marker.py +0 -0
  44. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/pane_surface.py +0 -0
  45. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/path_codec.py +0 -0
  46. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/provider_paths.py +0 -0
  47. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/remote_config.py +0 -0
  48. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/renames.py +0 -0
  49. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/restart_state.py +0 -0
  50. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/runtime_config.py +0 -0
  51. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/scroll_agent.py +0 -0
  52. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/scroll_manager.py +0 -0
  53. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/selection_isolation.py +0 -0
  54. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/self_update.py +0 -0
  55. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/session_cache.py +0 -0
  56. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/session_index.py +0 -0
  57. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/session_lease.py +0 -0
  58. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/setting_contracts.py +0 -0
  59. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/settings.py +0 -0
  60. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ssh_args.py +0 -0
  61. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ssh_compat.py +0 -0
  62. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ssh_display_diagnostics.py +0 -0
  63. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ssh_doctor.py +0 -0
  64. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/system_deps.py +0 -0
  65. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/terminal_status.py +0 -0
  66. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/tmux_binding_manager.py +0 -0
  67. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/tmux_ctl.py +0 -0
  68. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/tmux_health.py +0 -0
  69. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/tmux_server.py +0 -0
  70. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/tool_panes.py +0 -0
  71. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/transcript.py +0 -0
  72. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/__init__.py +0 -0
  73. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/_widgets.py +0 -0
  74. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/app.py +0 -0
  75. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/keymap.py +0 -0
  76. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/modals.py +0 -0
  77. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/projects_pane.py +0 -0
  78. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/running_pane.py +0 -0
  79. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/sessions_pane.py +0 -0
  80. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/sidebar.py +0 -0
  81. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/statusbar.py +0 -0
  82. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/ui/workspace.py +0 -0
  83. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/windows_attach_relay.py +0 -0
  84. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/windows_bootstrap.py +0 -0
  85. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/windows_pacman.py +0 -0
  86. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/windows_tmux_lifecycle.py +0 -0
  87. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux/windows_ui_transition.py +0 -0
  88. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux.egg-info/dependency_links.txt +0 -0
  89. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux.egg-info/entry_points.txt +0 -0
  90. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux.egg-info/requires.txt +0 -0
  91. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/src/railmux.egg-info/top_level.txt +0 -0
  92. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_agent_workspace.py +0 -0
  93. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_atomic_file.py +0 -0
  94. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_background_index.py +0 -0
  95. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_cli.py +0 -0
  96. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_codex_delete.py +0 -0
  97. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_codex_index.py +0 -0
  98. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_codex_lineage.py +0 -0
  99. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_codex_transcript.py +0 -0
  100. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_config.py +0 -0
  101. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_config_cli.py +0 -0
  102. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_denested_transport_benchmark.py +0 -0
  103. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_diagnostics.py +0 -0
  104. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_discovery.py +0 -0
  105. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_display_transport.py +0 -0
  106. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_fast_full_window.py +0 -0
  107. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_focus_highlight.py +0 -0
  108. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_fullscreen_binding.py +0 -0
  109. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_fuzzy.py +0 -0
  110. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_help_agent.py +0 -0
  111. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_help_workspace.py +0 -0
  112. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_history_cleanup.py +0 -0
  113. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_keymap.py +0 -0
  114. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_launcher.py +0 -0
  115. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_layout_profile.py +0 -0
  116. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_legacy_sessions.py +0 -0
  117. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_local_clipboard.py +0 -0
  118. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_local_open.py +0 -0
  119. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_modals.py +0 -0
  120. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_mode_selection.py +0 -0
  121. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_modes.py +0 -0
  122. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_mouse_manager.py +0 -0
  123. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_new_project.py +0 -0
  124. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_options.py +0 -0
  125. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_orphan_recovery.py +0 -0
  126. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_pane_surface.py +0 -0
  127. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_path_codec.py +0 -0
  128. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_projects_pane.py +0 -0
  129. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_provider_paths.py +0 -0
  130. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_release_notes.py +0 -0
  131. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_release_workflow.py +0 -0
  132. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_remote_config.py +0 -0
  133. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_renames.py +0 -0
  134. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_restart_state.py +0 -0
  135. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_running_pane.py +0 -0
  136. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_running_sort.py +0 -0
  137. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_runtime_config.py +0 -0
  138. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_scroll_agent.py +0 -0
  139. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_scroll_manager.py +0 -0
  140. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_selection_isolation.py +0 -0
  141. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_self_update.py +0 -0
  142. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_session_cache.py +0 -0
  143. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_session_index.py +0 -0
  144. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_session_lease.py +0 -0
  145. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_sessions_pane.py +0 -0
  146. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_setting_contracts.py +0 -0
  147. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_settings.py +0 -0
  148. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_sidebar.py +0 -0
  149. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_soft_quit.py +0 -0
  150. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_ssh_args.py +0 -0
  151. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_ssh_compat.py +0 -0
  152. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_ssh_display_benchmark.py +0 -0
  153. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_ssh_display_diagnostics.py +0 -0
  154. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_ssh_doctor.py +0 -0
  155. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_status.py +0 -0
  156. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_statusbar.py +0 -0
  157. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_system_deps.py +0 -0
  158. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_terminal_size.py +0 -0
  159. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_terminal_status.py +0 -0
  160. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_termux_local_touch.py +0 -0
  161. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_tmux_binding_manager.py +0 -0
  162. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_tmux_ctl.py +0 -0
  163. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_tmux_health.py +0 -0
  164. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_tmux_integration.py +0 -0
  165. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_tmux_server.py +0 -0
  166. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_tmux_status.py +0 -0
  167. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_tool_panes.py +0 -0
  168. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_transcript.py +0 -0
  169. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_utf8_state.py +0 -0
  170. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_web_demo_recorder.py +0 -0
  171. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_wheel_smoke.py +0 -0
  172. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_widgets.py +0 -0
  173. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_windows_attach_relay.py +0 -0
  174. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_windows_bootstrap.py +0 -0
  175. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_windows_pacman.py +0 -0
  176. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_windows_tmux_lifecycle.py +0 -0
  177. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_windows_ui_transition.py +0 -0
  178. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_windows_wrapper_contract.py +0 -0
  179. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tests/test_yolo_prompt.py +0 -0
  180. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tools/denested_transport_benchmark.py +0 -0
  181. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tools/release_notes.py +0 -0
  182. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/tools/wheel_smoke.py +0 -0
  183. {railmux-0.4.0.dev29 → railmux-0.4.0.dev30}/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.0.dev29
3
+ Version: 0.4.0.dev30
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.0.dev30"
@@ -22,6 +22,7 @@ from railmux.terminal_status import (
22
22
  STYLE_WARNING,
23
23
  styled,
24
24
  )
25
+ from railmux.windows_paths import managed_windows_data_root
25
26
 
26
27
 
27
28
  _LOG_KEEP_COUNT = 5
@@ -79,14 +80,14 @@ def install_log_path(
79
80
  *,
80
81
  version: str,
81
82
  clock: Callable[[], float] = time.time,
83
+ data_root: Path | None = None,
82
84
  ) -> Path:
83
- local_app_data = environ.get("LOCALAPPDATA", "").strip()
84
- if not local_app_data:
85
- raise OSError("LOCALAPPDATA is unavailable")
85
+ root = managed_windows_data_root(environ) if data_root is None else data_root
86
+ if root is None:
87
+ raise OSError("a non-virtualized Windows data directory is unavailable")
86
88
  stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(clock()))
87
89
  return (
88
- Path(local_app_data)
89
- / "Railmux"
90
+ root
90
91
  / "logs"
91
92
  / f"install-{version}-{stamp}-{os.getpid()}.log"
92
93
  )
@@ -42,6 +42,10 @@ from railmux.windows_pacman import (
42
42
  validate_transaction_package_mirrors,
43
43
  write_msys_only_pacman_config,
44
44
  )
45
+ from railmux.windows_paths import (
46
+ legacy_local_app_data_root,
47
+ managed_windows_data_root,
48
+ )
45
49
  from railmux.terminal_status import STYLE_ACCENT, STYLE_MUTED, styled
46
50
 
47
51
 
@@ -554,17 +558,17 @@ class Msys2Runtime:
554
558
 
555
559
 
556
560
  def _managed_base(environ: Mapping[str, str]) -> Path | None:
557
- local_app_data = environ.get("LOCALAPPDATA", "").strip()
558
- if not local_app_data:
561
+ data_root = managed_windows_data_root(environ)
562
+ if data_root is None:
559
563
  return None
560
- return Path(local_app_data) / "Railmux" / "runtimes"
564
+ return data_root / "runtimes"
561
565
 
562
566
 
563
567
  def _managed_cache(environ: Mapping[str, str]) -> Path | None:
564
- local_app_data = environ.get("LOCALAPPDATA", "").strip()
565
- if not local_app_data:
568
+ data_root = managed_windows_data_root(environ)
569
+ if data_root is None:
566
570
  return None
567
- return Path(local_app_data) / "Railmux" / "cache"
571
+ return data_root / "cache"
568
572
 
569
573
 
570
574
  def _path_is_link_or_reparse(path: Path) -> bool:
@@ -595,6 +599,7 @@ def _prepare_cached_archive(
595
599
  cache: Path,
596
600
  *,
597
601
  downloader: Downloader | None,
602
+ prior_caches: Sequence[Path] = (),
598
603
  ) -> tuple[Path, str]:
599
604
  cache.mkdir(parents=True, exist_ok=True)
600
605
  archive = cache / MSYS2_ARCHIVE_NAME
@@ -604,6 +609,18 @@ def _prepare_cached_archive(
604
609
  archive.unlink(missing_ok=True)
605
610
  except OSError as exc:
606
611
  raise RuntimeInstallError("could not replace the private archive cache") from exc
612
+ for prior_cache in prior_caches:
613
+ prior_archive = prior_cache / MSYS2_ARCHIVE_NAME
614
+ if prior_archive == archive or not _archive_cache_is_valid(prior_archive):
615
+ continue
616
+ try:
617
+ shutil.copyfile(prior_archive, archive)
618
+ except OSError:
619
+ archive.unlink(missing_ok=True)
620
+ continue
621
+ if _archive_cache_is_valid(archive):
622
+ return archive, "verified prior local cache"
623
+ archive.unlink(missing_ok=True)
607
624
  source = download_from_sources(
608
625
  archive,
609
626
  MSYS2_ARCHIVE_SHA256,
@@ -1178,7 +1195,9 @@ def apply_managed_runtime_prune(
1178
1195
  """Re-plan under the install lock, then remove only unchanged candidates."""
1179
1196
  base = _managed_base(environ)
1180
1197
  if base is None:
1181
- raise RuntimeInstallError("LOCALAPPDATA is unavailable")
1198
+ raise RuntimeInstallError(
1199
+ "a non-virtualized Windows user data directory is unavailable"
1200
+ )
1182
1201
  effective_lock = install_lock if lock_factory is None else lock_factory
1183
1202
  with effective_lock(base):
1184
1203
  current = plan_managed_runtime_prune(
@@ -2629,11 +2648,16 @@ def install_managed_runtime(
2629
2648
  base = _managed_base(environ)
2630
2649
  cache_base = _managed_cache(environ)
2631
2650
  if base is None or cache_base is None:
2632
- raise RuntimeInstallError("LOCALAPPDATA is unavailable")
2651
+ raise RuntimeInstallError(
2652
+ "a non-virtualized Windows user data directory is unavailable"
2653
+ )
2654
+ data_root = base.parent
2633
2655
  try:
2634
2656
  base.mkdir(parents=True, exist_ok=True)
2635
2657
  cache_base.mkdir(parents=True, exist_ok=True)
2636
- log_path = install_log_path(environ, version=version)
2658
+ log_path = install_log_path(
2659
+ environ, version=version, data_root=data_root
2660
+ )
2637
2661
  except OSError as exc:
2638
2662
  raise RuntimeInstallError("could not create the private runtime log") from exc
2639
2663
  final_root = base / "shared" / MSYS2_RUNTIME_ID
@@ -2749,6 +2773,11 @@ def install_managed_runtime(
2749
2773
  archive, source = _prepare_cached_archive(
2750
2774
  cache_base,
2751
2775
  downloader=downloader,
2776
+ prior_caches=tuple(
2777
+ legacy / "cache"
2778
+ for legacy in (legacy_local_app_data_root(environ),)
2779
+ if legacy is not None and legacy != data_root
2780
+ ),
2752
2781
  )
2753
2782
  reporter.done(source)
2754
2783
 
@@ -0,0 +1,73 @@
1
+ """Stable native-Windows storage paths for the managed runtime.
2
+
3
+ Microsoft Store and other MSIX-packaged Python interpreters can virtualize new
4
+ writes beneath AppData. Railmux's runtime contains executables that must be
5
+ opened by child processes outside that virtualized view, so packaged Python
6
+ uses a user-profile directory instead.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ctypes
12
+ import os
13
+ import sys
14
+ from collections.abc import Mapping
15
+ from pathlib import Path
16
+
17
+
18
+ _ERROR_INSUFFICIENT_BUFFER = 122
19
+ _APPMODEL_ERROR_NO_PACKAGE = 15700
20
+ _PACKAGED_PROFILE_DIRECTORY = ".railmux"
21
+ _PACKAGED_WINDOWS_DIRECTORY = "windows"
22
+
23
+
24
+ def _has_windows_package_identity() -> bool:
25
+ """Return whether this process has an MSIX/AppX package identity."""
26
+ if os.name != "nt":
27
+ return False
28
+ try:
29
+ get_family = ctypes.windll.kernel32.GetCurrentPackageFamilyName
30
+ length = ctypes.c_uint32(0)
31
+ result = int(get_family(ctypes.byref(length), None))
32
+ except (AttributeError, OSError, TypeError, ValueError):
33
+ return _looks_like_packaged_executable(sys.executable)
34
+ if result == _APPMODEL_ERROR_NO_PACKAGE:
35
+ return False
36
+ if result == _ERROR_INSUFFICIENT_BUFFER and length.value > 0:
37
+ return True
38
+ # A Store Python launcher and interpreter both have characteristic physical
39
+ # paths. Keep this fallback fail-safe if the package API is unavailable or
40
+ # returns an unexpected platform error.
41
+ return _looks_like_packaged_executable(sys.executable)
42
+
43
+
44
+ def _looks_like_packaged_executable(executable: str) -> bool:
45
+ normalized = executable.replace("/", "\\").casefold()
46
+ return "\\windowsapps\\" in normalized or (
47
+ "\\appdata\\local\\packages\\" in normalized and "\\localcache\\" in normalized
48
+ )
49
+
50
+
51
+ def managed_windows_data_root(environ: Mapping[str, str]) -> Path | None:
52
+ """Select one data root visible to both Python and MSYS2 child processes."""
53
+ if _has_windows_package_identity():
54
+ user_profile = environ.get("USERPROFILE", "").strip()
55
+ if not user_profile:
56
+ return None
57
+ return (
58
+ Path(user_profile)
59
+ / _PACKAGED_PROFILE_DIRECTORY
60
+ / _PACKAGED_WINDOWS_DIRECTORY
61
+ )
62
+ local_app_data = environ.get("LOCALAPPDATA", "").strip()
63
+ if not local_app_data:
64
+ return None
65
+ return Path(local_app_data) / "Railmux"
66
+
67
+
68
+ def legacy_local_app_data_root(environ: Mapping[str, str]) -> Path | None:
69
+ """Return the pre-packaged-Python-fix location for read-only cache reuse."""
70
+ local_app_data = environ.get("LOCALAPPDATA", "").strip()
71
+ if not local_app_data:
72
+ return None
73
+ return Path(local_app_data) / "Railmux"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev29
3
+ Version: 0.4.0.dev30
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
@@ -63,6 +63,7 @@ src/railmux/windows_bootstrap.py
63
63
  src/railmux/windows_install_log.py
64
64
  src/railmux/windows_msys2.py
65
65
  src/railmux/windows_pacman.py
66
+ src/railmux/windows_paths.py
66
67
  src/railmux/windows_tmux_lifecycle.py
67
68
  src/railmux/windows_ui_transition.py
68
69
  src/railmux.egg-info/PKG-INFO
@@ -168,6 +169,7 @@ tests/test_windows_bootstrap.py
168
169
  tests/test_windows_install_log.py
169
170
  tests/test_windows_msys2.py
170
171
  tests/test_windows_pacman.py
172
+ tests/test_windows_paths.py
171
173
  tests/test_windows_tmux_lifecycle.py
172
174
  tests/test_windows_ui_transition.py
173
175
  tests/test_windows_wrapper_contract.py
@@ -223,6 +223,25 @@ def test_log_path_is_private_versioned_and_utc(tmp_path, monkeypatch):
223
223
  )
224
224
 
225
225
 
226
+ def test_log_path_uses_packaged_python_non_virtualized_root(tmp_path, monkeypatch):
227
+ monkeypatch.setattr(
228
+ windows_install_log,
229
+ "managed_windows_data_root",
230
+ lambda _environ: tmp_path / ".railmux" / "windows",
231
+ )
232
+
233
+ path = install_log_path(
234
+ {
235
+ "LOCALAPPDATA": str(tmp_path / "AppData" / "Local"),
236
+ "USERPROFILE": str(tmp_path),
237
+ },
238
+ version="0.4.0.dev30",
239
+ clock=lambda: 0,
240
+ )
241
+
242
+ assert path.parent == tmp_path / ".railmux" / "windows" / "logs"
243
+
244
+
226
245
  def test_default_command_runner_uses_no_stdin_and_captures_utf8(tmp_path):
227
246
  stream = io.StringIO()
228
247
  path = tmp_path / "install-0.4.0.dev7-20260803T000000Z-4.log"
@@ -628,6 +628,22 @@ def test_preview_versions_share_one_base_but_use_separate_app_layers(tmp_path):
628
628
  )
629
629
 
630
630
 
631
+ def test_packaged_python_runtime_root_avoids_local_app_data(tmp_path, monkeypatch):
632
+ safe = tmp_path / ".railmux" / "windows"
633
+ monkeypatch.setattr(
634
+ windows_msys2, "managed_windows_data_root", lambda _environ: safe
635
+ )
636
+ environ = {
637
+ "LOCALAPPDATA": str(tmp_path / "AppData" / "Local"),
638
+ "USERPROFILE": str(tmp_path),
639
+ }
640
+
641
+ assert managed_root(environ) == (
642
+ safe / "runtimes" / "shared" / windows_msys2.MSYS2_RUNTIME_ID
643
+ )
644
+ assert windows_msys2._managed_cache(environ) == safe / "cache"
645
+
646
+
631
647
  def test_explicit_user_runtime_is_probed_but_never_requires_managed_marker(tmp_path):
632
648
  root = tmp_path / "用户-owned-msys"
633
649
  runtime = make_runtime(root, managed=False)
@@ -1268,6 +1284,57 @@ def test_verified_base_archive_cache_avoids_a_second_download(tmp_path, monkeypa
1268
1284
  assert source == "verified local cache"
1269
1285
 
1270
1286
 
1287
+ def test_packaged_python_reuses_verified_pre_fix_archive_cache(tmp_path, monkeypatch):
1288
+ payload = b"verified prior cache archive"
1289
+ monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_SIZE", len(payload))
1290
+ monkeypatch.setattr(
1291
+ windows_msys2,
1292
+ "MSYS2_ARCHIVE_SHA256",
1293
+ hashlib.sha256(payload).hexdigest(),
1294
+ )
1295
+ prior = tmp_path / "virtualized-cache"
1296
+ prior.mkdir()
1297
+ (prior / MSYS2_ARCHIVE_NAME).write_bytes(payload)
1298
+ current = tmp_path / "non-virtualized-cache"
1299
+
1300
+ selected, source = windows_msys2._prepare_cached_archive(
1301
+ current,
1302
+ downloader=lambda *_args: pytest.fail("prior cache should avoid downloading"),
1303
+ prior_caches=(prior,),
1304
+ )
1305
+
1306
+ assert selected.read_bytes() == payload
1307
+ assert source == "verified prior local cache"
1308
+
1309
+
1310
+ def test_packaged_python_never_reuses_unverified_pre_fix_archive(
1311
+ tmp_path, monkeypatch
1312
+ ):
1313
+ payload = b"expected archive"
1314
+ monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_SIZE", len(payload))
1315
+ monkeypatch.setattr(
1316
+ windows_msys2,
1317
+ "MSYS2_ARCHIVE_SHA256",
1318
+ hashlib.sha256(payload).hexdigest(),
1319
+ )
1320
+ prior = tmp_path / "virtualized-cache"
1321
+ prior.mkdir()
1322
+ (prior / MSYS2_ARCHIVE_NAME).write_bytes(b"tampered archive")
1323
+ current = tmp_path / "non-virtualized-cache"
1324
+
1325
+ def downloader(_url, destination, _sha256):
1326
+ destination.write_bytes(payload)
1327
+
1328
+ selected, source = windows_msys2._prepare_cached_archive(
1329
+ current,
1330
+ downloader=downloader,
1331
+ prior_caches=(prior,),
1332
+ )
1333
+
1334
+ assert selected.read_bytes() == payload
1335
+ assert source in {label for label, _url in MSYS2_ARCHIVE_SOURCES}
1336
+
1337
+
1271
1338
  def _write_test_tar(path: Path, entries: list[tuple[tarfile.TarInfo, bytes]]) -> None:
1272
1339
  with tarfile.open(path, mode="w:xz") as archive:
1273
1340
  for member, payload in entries:
@@ -0,0 +1,83 @@
1
+ from types import SimpleNamespace
2
+ import ctypes
3
+
4
+ from railmux import windows_paths
5
+
6
+
7
+ def test_unpacked_python_keeps_local_app_data_contract(tmp_path, monkeypatch):
8
+ monkeypatch.setattr(windows_paths, "_has_windows_package_identity", lambda: False)
9
+ local = tmp_path / "AppData" / "Local"
10
+
11
+ root = windows_paths.managed_windows_data_root(
12
+ {"LOCALAPPDATA": str(local), "USERPROFILE": str(tmp_path)}
13
+ )
14
+
15
+ assert root == local / "Railmux"
16
+
17
+
18
+ def test_packaged_python_avoids_virtualized_app_data(tmp_path, monkeypatch):
19
+ monkeypatch.setattr(windows_paths, "_has_windows_package_identity", lambda: True)
20
+ local = tmp_path / "AppData" / "Local"
21
+
22
+ root = windows_paths.managed_windows_data_root(
23
+ {"LOCALAPPDATA": str(local), "USERPROFILE": str(tmp_path)}
24
+ )
25
+
26
+ assert root == tmp_path / ".railmux" / "windows"
27
+ assert local not in root.parents
28
+
29
+
30
+ def test_packaged_python_never_falls_back_to_virtualized_app_data(
31
+ tmp_path, monkeypatch
32
+ ):
33
+ monkeypatch.setattr(windows_paths, "_has_windows_package_identity", lambda: True)
34
+
35
+ assert (
36
+ windows_paths.managed_windows_data_root({"LOCALAPPDATA": str(tmp_path)}) is None
37
+ )
38
+
39
+
40
+ def test_packaged_executable_path_fallback_is_narrow():
41
+ assert windows_paths._looks_like_packaged_executable(
42
+ r"C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12\python.exe"
43
+ )
44
+ assert windows_paths._looks_like_packaged_executable(
45
+ r"C:\Users\u\AppData\Local\Packages\Python.Package\LocalCache\python.exe"
46
+ )
47
+ assert not windows_paths._looks_like_packaged_executable(
48
+ r"C:\Users\u\AppData\Local\Programs\Python\Python312\python.exe"
49
+ )
50
+
51
+
52
+ def test_windows_package_identity_api_distinguishes_packaged_process(monkeypatch):
53
+ def get_family(length, _buffer):
54
+ ctypes.cast(length, ctypes.POINTER(ctypes.c_uint32)).contents.value = 64
55
+ return 122
56
+
57
+ monkeypatch.setattr(windows_paths.os, "name", "nt")
58
+ monkeypatch.setattr(
59
+ windows_paths.ctypes,
60
+ "windll",
61
+ SimpleNamespace(
62
+ kernel32=SimpleNamespace(GetCurrentPackageFamilyName=get_family)
63
+ ),
64
+ raising=False,
65
+ )
66
+
67
+ assert windows_paths._has_windows_package_identity()
68
+
69
+
70
+ def test_windows_package_identity_api_preserves_unpacked_python(monkeypatch):
71
+ monkeypatch.setattr(windows_paths.os, "name", "nt")
72
+ monkeypatch.setattr(
73
+ windows_paths.ctypes,
74
+ "windll",
75
+ SimpleNamespace(
76
+ kernel32=SimpleNamespace(
77
+ GetCurrentPackageFamilyName=lambda _length, _buffer: 15700
78
+ )
79
+ ),
80
+ raising=False,
81
+ )
82
+
83
+ assert not windows_paths._has_windows_package_identity()
@@ -1 +0,0 @@
1
- __version__ = "0.4.0.dev29"
File without changes
File without changes
File without changes
File without changes