railmux 0.3.4.dev1__tar.gz → 0.3.5.dev1__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 (160) hide show
  1. {railmux-0.3.4.dev1/src/railmux.egg-info → railmux-0.3.5.dev1}/PKG-INFO +1 -1
  2. railmux-0.3.5.dev1/src/railmux/__init__.py +1 -0
  3. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_client.py +142 -40
  4. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tmux_server.py +15 -1
  5. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/app.py +19 -23
  6. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1/src/railmux.egg-info}/PKG-INFO +1 -1
  7. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_cli.py +3 -1
  8. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_fast_full_window.py +56 -2
  9. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_soft_quit.py +10 -5
  10. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_server.py +3 -1
  11. railmux-0.3.4.dev1/src/railmux/__init__.py +0 -1
  12. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/LICENSE +0 -0
  13. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/MANIFEST.in +0 -0
  14. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/README.md +0 -0
  15. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/pyproject.toml +0 -0
  16. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/setup.cfg +0 -0
  17. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/__main__.py +0 -0
  18. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/atomic_file.py +0 -0
  19. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/background_index.py +0 -0
  20. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/cli.py +0 -0
  21. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/codex_index.py +0 -0
  22. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/config.py +0 -0
  23. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/config_cli.py +0 -0
  24. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/diagnostics.py +0 -0
  25. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/discovery.py +0 -0
  26. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/display_transport.py +0 -0
  27. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_history.py +0 -0
  28. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_input.py +0 -0
  29. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_protocol.py +0 -0
  30. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_server.py +0 -0
  31. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/favorites.py +0 -0
  32. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fuzzy.py +0 -0
  33. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/help_workspace.py +0 -0
  34. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/launcher.py +0 -0
  35. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/legacy_sessions.py +0 -0
  36. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/local_clipboard.py +0 -0
  37. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/local_open.py +0 -0
  38. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/models.py +0 -0
  39. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/modes.py +0 -0
  40. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/mouse_manager.py +0 -0
  41. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/orphan_marker.py +0 -0
  42. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/pane_surface.py +0 -0
  43. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/path_codec.py +0 -0
  44. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/remote_config.py +0 -0
  45. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/renames.py +0 -0
  46. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/restart_state.py +0 -0
  47. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/runtime_config.py +0 -0
  48. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/scroll_agent.py +0 -0
  49. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/scroll_manager.py +0 -0
  50. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/selection_isolation.py +0 -0
  51. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/self_update.py +0 -0
  52. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/session_cache.py +0 -0
  53. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/session_index.py +0 -0
  54. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/setting_contracts.py +0 -0
  55. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/settings.py +0 -0
  56. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ssh_args.py +0 -0
  57. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ssh_compat.py +0 -0
  58. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
  59. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ssh_doctor.py +0 -0
  60. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/system_deps.py +0 -0
  61. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/terminal_status.py +0 -0
  62. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tmux_binding_manager.py +0 -0
  63. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tmux_ctl.py +0 -0
  64. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tmux_health.py +0 -0
  65. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tool_panes.py +0 -0
  66. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/transcript.py +0 -0
  67. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/__init__.py +0 -0
  68. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/_widgets.py +0 -0
  69. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/keymap.py +0 -0
  70. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/modals.py +0 -0
  71. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/projects_pane.py +0 -0
  72. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/running_pane.py +0 -0
  73. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/sessions_pane.py +0 -0
  74. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/sidebar.py +0 -0
  75. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/statusbar.py +0 -0
  76. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/workspace.py +0 -0
  77. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/SOURCES.txt +0 -0
  78. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
  79. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
  80. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/requires.txt +0 -0
  81. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/top_level.txt +0 -0
  82. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_agent_workspace.py +0 -0
  83. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_atomic_file.py +0 -0
  84. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_background_index.py +0 -0
  85. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_codex_delete.py +0 -0
  86. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_codex_index.py +0 -0
  87. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_codex_lineage.py +0 -0
  88. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_codex_transcript.py +0 -0
  89. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_config.py +0 -0
  90. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_config_cli.py +0 -0
  91. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_denested_transport_benchmark.py +0 -0
  92. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_diagnostics.py +0 -0
  93. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_discovery.py +0 -0
  94. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_display_transport.py +0 -0
  95. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_focus_highlight.py +0 -0
  96. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_fullscreen_binding.py +0 -0
  97. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_fuzzy.py +0 -0
  98. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_help_agent.py +0 -0
  99. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_help_workspace.py +0 -0
  100. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_history_cleanup.py +0 -0
  101. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_keymap.py +0 -0
  102. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_launcher.py +0 -0
  103. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_layout_profile.py +0 -0
  104. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_legacy_sessions.py +0 -0
  105. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_local_clipboard.py +0 -0
  106. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_local_open.py +0 -0
  107. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_modals.py +0 -0
  108. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_mode_selection.py +0 -0
  109. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_modes.py +0 -0
  110. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_mouse_manager.py +0 -0
  111. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_new_project.py +0 -0
  112. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_options.py +0 -0
  113. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_orphan_recovery.py +0 -0
  114. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_pane_surface.py +0 -0
  115. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_path_codec.py +0 -0
  116. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_projects_pane.py +0 -0
  117. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_release_notes.py +0 -0
  118. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_release_workflow.py +0 -0
  119. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_remote_config.py +0 -0
  120. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_renames.py +0 -0
  121. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_restart_state.py +0 -0
  122. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_running_pane.py +0 -0
  123. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_running_sort.py +0 -0
  124. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_runtime_config.py +0 -0
  125. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_scroll_agent.py +0 -0
  126. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_scroll_manager.py +0 -0
  127. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_selection_isolation.py +0 -0
  128. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_self_update.py +0 -0
  129. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_session_cache.py +0 -0
  130. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_session_index.py +0 -0
  131. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_sessions_pane.py +0 -0
  132. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_setting_contracts.py +0 -0
  133. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_settings.py +0 -0
  134. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_sidebar.py +0 -0
  135. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_args.py +0 -0
  136. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_compat.py +0 -0
  137. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_display_benchmark.py +0 -0
  138. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
  139. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_doctor.py +0 -0
  140. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_status.py +0 -0
  141. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_statusbar.py +0 -0
  142. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_system_deps.py +0 -0
  143. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_terminal_size.py +0 -0
  144. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_terminal_status.py +0 -0
  145. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_termux_local_touch.py +0 -0
  146. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_binding_manager.py +0 -0
  147. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_ctl.py +0 -0
  148. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_health.py +0 -0
  149. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_integration.py +0 -0
  150. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_status.py +0 -0
  151. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tool_panes.py +0 -0
  152. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_transcript.py +0 -0
  153. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_utf8_state.py +0 -0
  154. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_web_demo_recorder.py +0 -0
  155. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_wheel_smoke.py +0 -0
  156. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_widgets.py +0 -0
  157. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_yolo_prompt.py +0 -0
  158. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tools/denested_transport_benchmark.py +0 -0
  159. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tools/release_notes.py +0 -0
  160. {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.4.dev1
3
+ Version: 0.3.5.dev1
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.3.5.dev1"
@@ -202,6 +202,14 @@ def full_repaint(screen: AppliedScreen) -> AppliedScreen:
202
202
  )
203
203
 
204
204
 
205
+ def selection_changed_rows(
206
+ before: tuple[SelectionSegment, ...],
207
+ after: tuple[SelectionSegment, ...],
208
+ ) -> set[int]:
209
+ """Rows that must be restored when one local highlight changes."""
210
+ return {row for row, _column, _text in (*before, *after)}
211
+
212
+
205
213
  def focus_in_frame_for_screen(screen: AppliedScreen | None) -> bytes | None:
206
214
  """Reassert focus only while the remote application requested events."""
207
215
  if screen is None or not screen.terminal_modes & TerminalMode.FOCUS_EVENTS:
@@ -637,6 +645,10 @@ class TerminalSurface:
637
645
  self.mouse_active = False
638
646
  self.mouse_suspended = False
639
647
  self.cursor_hidden = False
648
+ # Track the physical emulator cursor separately from the remote model.
649
+ # Re-sending DECSET/DECRST 25 on every 20 fps patch restarts the cursor
650
+ # animation in Windows Terminal and presents as a rapid flash.
651
+ self._painted_cursor_visible: bool | None = None
640
652
  self.terminal_modes = TerminalMode.NONE
641
653
  self.physical_size: os.terminal_size | None = None
642
654
  self._last_screen: AppliedScreen | None = None
@@ -694,6 +706,7 @@ class TerminalSurface:
694
706
  if interactive and not self.cursor_hidden:
695
707
  controls.append(b"\033[?25l")
696
708
  self.cursor_hidden = True
709
+ self._painted_cursor_visible = False
697
710
  if (
698
711
  interactive
699
712
  and self.mouse
@@ -793,6 +806,7 @@ class TerminalSurface:
793
806
  self.mouse_active = False
794
807
  self.mouse_suspended = False
795
808
  self.cursor_hidden = False
809
+ self._painted_cursor_visible = True
796
810
  self.interaction_active = True
797
811
 
798
812
  def show_local_status(
@@ -813,7 +827,8 @@ class TerminalSurface:
813
827
  self._local_status_level = level
814
828
  self._local_status_interruptible = interruptible
815
829
  self._local_status_expires_at = expires_at
816
- rendered = [self._render_local_status()]
830
+ restore_cursor = self._last_screen is not None
831
+ rendered = [self._render_local_status(hide_cursor=not restore_cursor)]
817
832
  if self._last_screen is not None:
818
833
  projection_top, visible_height = self._projection(
819
834
  self._last_screen.height
@@ -828,7 +843,7 @@ class TerminalSurface:
828
843
  self.stream.write(b"".join(rendered))
829
844
  self.stream.flush()
830
845
 
831
- def _render_local_status(self) -> bytes:
846
+ def _render_local_status(self, *, hide_cursor: bool = True) -> bytes:
832
847
  """Render and locate the current local status-right replacement."""
833
848
  if self._local_status_text is None:
834
849
  self._local_status_bounds = None
@@ -841,10 +856,14 @@ class TerminalSurface:
841
856
  if self.physical_size is not None:
842
857
  safe = safe[: self.physical_size.columns]
843
858
  self._local_status_bounds = (max(1, height), 1, max(1, len(safe)))
844
- return (
859
+ rendered = (
845
860
  f"\033[?7l\033[0m\033[{max(1, height)};1H\033[2K{safe}"
846
- "\033[0m\033[?7h\033[?25l"
861
+ "\033[0m\033[?7h"
847
862
  ).encode("utf-8")
863
+ if hide_cursor:
864
+ rendered += b"\033[?25l"
865
+ self._painted_cursor_visible = False
866
+ return rendered
848
867
  width = screen.width
849
868
  if self.physical_size is not None:
850
869
  width = min(width, self.physical_size.columns)
@@ -862,8 +881,8 @@ class TerminalSurface:
862
881
  "warning": b"\033[1;38;5;220m",
863
882
  "error": b"\033[1;38;5;196m",
864
883
  }.get(level, b"\033[38;5;231m")
865
- return b"".join(
866
- (
884
+ rendered = b"".join(
885
+ [
867
886
  b"\033[?7l\033[0m",
868
887
  f"\033[{max(1, height)};{reserved + 1}H".encode(),
869
888
  background,
@@ -871,9 +890,13 @@ class TerminalSurface:
871
890
  b"\033[K",
872
891
  f"\033[{max(1, height)};{column}H".encode(),
873
892
  safe.encode("utf-8"),
874
- b"\033[0m\033[?25l",
875
- )
893
+ b"\033[0m",
894
+ ]
876
895
  )
896
+ if hide_cursor:
897
+ rendered += b"\033[?25l"
898
+ self._painted_cursor_visible = False
899
+ return rendered
877
900
 
878
901
  def clear_local_status(self) -> None:
879
902
  """Forget local feedback before restoring the authoritative screen."""
@@ -991,6 +1014,7 @@ class TerminalSurface:
991
1014
  )
992
1015
  self.stream.write(b"".join(rendered))
993
1016
  self.stream.flush()
1017
+ self._painted_cursor_visible = False
994
1018
 
995
1019
  def claude_history_prompt_choice(
996
1020
  self,
@@ -1052,6 +1076,7 @@ class TerminalSurface:
1052
1076
  )
1053
1077
  self.stream.write(b"".join(rendered))
1054
1078
  self.stream.flush()
1079
+ self._painted_cursor_visible = False
1055
1080
 
1056
1081
  def path_open_prompt_choice(
1057
1082
  self,
@@ -1108,6 +1133,7 @@ class TerminalSurface:
1108
1133
  # scroll the alternate screen.
1109
1134
  self.stream.write(b"\033[0m\033[?7h\033[?25l")
1110
1135
  self.stream.flush()
1136
+ self._painted_cursor_visible = False
1111
1137
 
1112
1138
  @staticmethod
1113
1139
  def _cursor_is_covered(
@@ -1171,38 +1197,37 @@ class TerminalSurface:
1171
1197
  )
1172
1198
  )
1173
1199
 
1174
- @classmethod
1175
1200
  def _append_cursor(
1176
- cls,
1201
+ self,
1177
1202
  rendered: list[bytes],
1178
1203
  screen: AppliedScreen,
1179
1204
  overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
1180
1205
  *,
1181
1206
  projection_top: int = 0,
1182
1207
  visible_height: int | None = None,
1208
+ force_visibility: bool = False,
1183
1209
  ) -> None:
1184
1210
  cursor_in_projection = (
1185
1211
  visible_height is None
1186
1212
  or projection_top <= screen.cursor_y < projection_top + visible_height
1187
1213
  )
1188
- rendered.extend(
1189
- (
1190
- b"\033[0m\033[?7h",
1191
- (
1192
- f"\033[{screen.cursor_y - projection_top + 1};"
1193
- f"{screen.cursor_x + 1}H"
1194
- ).encode()
1195
- if cursor_in_projection
1196
- else b"\033[1;1H",
1197
- (
1198
- b"\033[?25h"
1199
- if screen.cursor_visible
1200
- and cursor_in_projection
1201
- and not cls._cursor_is_covered(screen, overlays)
1202
- else b"\033[?25l"
1203
- ),
1204
- )
1214
+ visible = bool(
1215
+ screen.cursor_visible
1216
+ and cursor_in_projection
1217
+ and not self._cursor_is_covered(screen, overlays)
1205
1218
  )
1219
+ rendered.extend((
1220
+ b"\033[0m\033[?7h",
1221
+ (
1222
+ f"\033[{screen.cursor_y - projection_top + 1};"
1223
+ f"{screen.cursor_x + 1}H"
1224
+ ).encode()
1225
+ if cursor_in_projection
1226
+ else b"\033[1;1H",
1227
+ ))
1228
+ if force_visibility or self._painted_cursor_visible is not visible:
1229
+ rendered.append(b"\033[?25h" if visible else b"\033[?25l")
1230
+ self._painted_cursor_visible = visible
1206
1231
 
1207
1232
  def paint(
1208
1233
  self,
@@ -1250,7 +1275,7 @@ class TerminalSurface:
1250
1275
  visible_height=visible_height,
1251
1276
  )
1252
1277
  if self._local_status_text is not None:
1253
- rendered.append(self._render_local_status())
1278
+ rendered.append(self._render_local_status(hide_cursor=False))
1254
1279
  # Status and selection painting move and may temporarily hide the
1255
1280
  # terminal cursor. Restoring the authoritative cursor must therefore
1256
1281
  # be the final operation in every complete paint.
@@ -1289,7 +1314,61 @@ class TerminalSurface:
1289
1314
  visible_height=visible_height,
1290
1315
  )
1291
1316
  if self._local_status_text is not None:
1292
- rendered.append(self._render_local_status())
1317
+ rendered.append(self._render_local_status(hide_cursor=False))
1318
+ self._append_cursor(
1319
+ rendered,
1320
+ screen,
1321
+ overlays,
1322
+ projection_top=projection_top,
1323
+ visible_height=visible_height,
1324
+ )
1325
+ self.stream.write(b"".join(rendered))
1326
+ self.stream.flush()
1327
+
1328
+ def paint_changed_rows(
1329
+ self,
1330
+ screen: AppliedScreen,
1331
+ overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
1332
+ selection: tuple[SelectionSegment, ...],
1333
+ changed_rows: set[int] | frozenset[int],
1334
+ ) -> None:
1335
+ """Restore and repaint only rows touched by a local highlight.
1336
+
1337
+ Hover, drag selection, and click flash are local overlays. Clearing the
1338
+ whole alternate screen for them is both unnecessary and visibly flashes
1339
+ in Windows Terminal. Restore each affected row from the authoritative
1340
+ live frame, then layer frozen history and the new highlight back on it.
1341
+ """
1342
+ self.start()
1343
+ self._last_screen = screen
1344
+ self._last_overlays = overlays
1345
+ projection_top, visible_height = self._projection(screen.height)
1346
+ rows = frozenset(
1347
+ row for row in changed_rows
1348
+ if projection_top <= row < projection_top + visible_height
1349
+ )
1350
+ rendered: list[bytes] = [b"\033[?7l"]
1351
+ for row in sorted(rows):
1352
+ rendered.extend((
1353
+ f"\033[{row - projection_top + 1};1H".encode(),
1354
+ b"\033[2K",
1355
+ screen.rows[row],
1356
+ ))
1357
+ self._append_overlay_rows(
1358
+ rendered,
1359
+ overlays,
1360
+ projection_top=projection_top,
1361
+ visible_height=visible_height,
1362
+ changed_rows=rows,
1363
+ )
1364
+ self._append_selection_segments(
1365
+ rendered,
1366
+ selection,
1367
+ projection_top=projection_top,
1368
+ visible_height=visible_height,
1369
+ )
1370
+ if self._local_status_text is not None and screen.height - 1 in rows:
1371
+ rendered.append(self._render_local_status(hide_cursor=False))
1293
1372
  self._append_cursor(
1294
1373
  rendered,
1295
1374
  screen,
@@ -1312,6 +1391,7 @@ class TerminalSurface:
1312
1391
  self._last_overlays,
1313
1392
  projection_top=projection_top,
1314
1393
  visible_height=visible_height,
1394
+ force_visibility=True,
1315
1395
  )
1316
1396
  self.stream.write(b"".join(rendered))
1317
1397
  self.stream.flush()
@@ -1333,6 +1413,7 @@ class TerminalSurface:
1333
1413
  self.mouse_active = False
1334
1414
  self.mouse_suspended = False
1335
1415
  self.cursor_hidden = False
1416
+ self._painted_cursor_visible = None
1336
1417
  self.clear_local_status()
1337
1418
  self._last_screen = None
1338
1419
  self._reconnect_status_screen = None
@@ -2627,11 +2708,14 @@ def run(args: argparse.Namespace) -> int:
2627
2708
  focused_pane_id=focused_pane_id,
2628
2709
  )
2629
2710
  )
2711
+ before = selection.segments()
2630
2712
  if selection.hover(hover_event, source) and latest_screen is not None:
2631
- surface.paint(
2632
- full_repaint(latest_screen),
2713
+ after = selection.segments()
2714
+ surface.paint_changed_rows(
2715
+ latest_screen,
2633
2716
  history.overlays(),
2634
- selection.segments(),
2717
+ after,
2718
+ selection_changed_rows(before, after),
2635
2719
  )
2636
2720
  return
2637
2721
  if path_open_prompt is not None:
@@ -2751,6 +2835,7 @@ def run(args: argparse.Namespace) -> int:
2751
2835
  # one persistent choice is still being confirmed.
2752
2836
  return
2753
2837
  if isinstance(part, SgrMouseEvent):
2838
+ selection_before = selection.segments()
2754
2839
  displayed_height = (
2755
2840
  latest_screen.height
2756
2841
  if latest_screen is not None
@@ -2807,10 +2892,12 @@ def run(args: argparse.Namespace) -> int:
2807
2892
  if selection.capturing or not selection.active:
2808
2893
  selection_clear_at = None
2809
2894
  if selection_action.repaint and latest_screen is not None:
2810
- surface.paint(
2811
- full_repaint(latest_screen),
2895
+ selection_after = selection.segments()
2896
+ surface.paint_changed_rows(
2897
+ latest_screen,
2812
2898
  history.overlays(),
2813
- selection.segments(),
2899
+ selection_after,
2900
+ selection_changed_rows(selection_before, selection_after),
2814
2901
  )
2815
2902
  for replay_event in selection_action.replay_events:
2816
2903
  if not claim_batched_wheel(replay_event, handled_wheels):
@@ -2835,6 +2922,7 @@ def run(args: argparse.Namespace) -> int:
2835
2922
  selection_clear_at = time.monotonic() + _SELECTION_HIGHLIGHT_SECONDS
2836
2923
  if selection_action.open_target is not None:
2837
2924
  target = selection_action.open_target
2925
+ flash_before = selection.segments()
2838
2926
  if (
2839
2927
  selection.flash(
2840
2928
  target,
@@ -2843,10 +2931,12 @@ def run(args: argparse.Namespace) -> int:
2843
2931
  )
2844
2932
  and latest_screen is not None
2845
2933
  ):
2846
- surface.paint(
2847
- full_repaint(latest_screen),
2934
+ flash_after = selection.segments()
2935
+ surface.paint_changed_rows(
2936
+ latest_screen,
2848
2937
  history.overlays(),
2849
- selection.segments(),
2938
+ flash_after,
2939
+ selection_changed_rows(flash_before, flash_after),
2850
2940
  )
2851
2941
  if target.kind == "url":
2852
2942
  show_open_result(local_open.open_url(target.value))
@@ -3301,6 +3391,7 @@ def run(args: argparse.Namespace) -> int:
3301
3391
  restore_local_status = surface.expire_local_status(now) or (
3302
3392
  history_info_until is not None and now >= history_info_until
3303
3393
  )
3394
+ timed_selection_before = selection.segments()
3304
3395
  clear_selection = (
3305
3396
  selection_clear_at is not None and now >= selection_clear_at
3306
3397
  )
@@ -3310,13 +3401,24 @@ def run(args: argparse.Namespace) -> int:
3310
3401
  selection.cancel()
3311
3402
  if restore_local_status:
3312
3403
  surface.clear_local_status()
3313
- if latest_screen is not None:
3404
+ timed_selection_after = selection.segments()
3405
+ if latest_screen is not None and restore_local_status:
3314
3406
  surface.paint(
3315
3407
  full_repaint(latest_screen),
3316
3408
  history.overlays(),
3317
- selection.segments(),
3409
+ timed_selection_after,
3318
3410
  )
3319
3411
  redraw_local_prompt()
3412
+ elif latest_screen is not None:
3413
+ surface.paint_changed_rows(
3414
+ latest_screen,
3415
+ history.overlays(),
3416
+ timed_selection_after,
3417
+ selection_changed_rows(
3418
+ timed_selection_before,
3419
+ timed_selection_after,
3420
+ ),
3421
+ )
3320
3422
  if restore_local_status:
3321
3423
  history_info_until = None
3322
3424
  if clear_selection:
@@ -604,8 +604,22 @@ def launcher_argv(
604
604
  launch_prefix: Sequence[str],
605
605
  forwarded_args: Sequence[str],
606
606
  ) -> list[str]:
607
- """Build the only supported entry into the dedicated Railmux workspace."""
607
+ """Build the only supported entry into the dedicated Railmux workspace.
608
+
609
+ The dedicated server's inherited status line stays off. ``App.run`` turns
610
+ it on at session scope only after the managed UI is ready, so tmux cannot
611
+ expose its stock status line underneath the startup/restoration surface.
612
+ Keeping the baseline global also makes early failures and soft-quit shells
613
+ clean without requiring a child process to repair partially-started UI.
614
+ """
608
615
  return tmux_argv(
616
+ "start-server",
617
+ ";",
618
+ "set-option",
619
+ "-g",
620
+ "status",
621
+ "off",
622
+ ";",
609
623
  "new-session",
610
624
  "-A",
611
625
  "-s",
@@ -10598,10 +10598,9 @@ class App:
10598
10598
  # - set-clipboard on: text selection in either pane is copied to the
10599
10599
  # system clipboard.
10600
10600
  import subprocess as _sp
10601
- # Wrap the whole setup (not just loop.run) so `finally` reverts our tmux
10602
- # status-bar overrides even if Screen()/MainLoop() construction raises
10603
- # after we've mutated the outer session — otherwise the user's bar would
10604
- # keep railmux's `status on`, style, brand and blanked window-list.
10601
+ # Wrap the whole setup (not just loop.run) so `finally` reverts any tmux
10602
+ # state acquired before the UI loop exits or initialization fails.
10603
+ sess: str | None = None
10605
10604
  try:
10606
10605
  if tmux_ctl.in_tmux():
10607
10606
  sess = tmux_ctl.current_session_name() or "railmux"
@@ -10628,25 +10627,6 @@ class App:
10628
10627
  "custom root wheel bindings.",
10629
10628
  "warn",
10630
10629
  )
10631
- # Shared binding ownership below scopes right-click forwarding
10632
- # to Railmux windows and preserves the user's original command
10633
- # everywhere else. Left-click keeps tmux's stock
10634
- # select-pane-and-forward behavior.
10635
- # The outer tmux status bar is now railmux's only status surface (the
10636
- # in-pane StatusBar was removed). Apply the static options (window
10637
- # list blanked, bar forced on, length cap) here; the bar background
10638
- # + brand are set by _apply_tmux_bar (green now, dark red on error).
10639
- # All session-scoped + reverted on teardown, so the user's global
10640
- # tmux config is untouched.
10641
- for opt, val in self._TMUX_BAR_OPTIONS:
10642
- _sp.run(
10643
- ["tmux", "set-option", "-t", sess, opt, val],
10644
- stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
10645
- )
10646
- self._tmux_status_session = sess
10647
- self._tmux_status_enabled = True
10648
- self._apply_tmux_bar(error=False) # initial green bar
10649
-
10650
10630
  self._railmux_pane_id = tmux_ctl.current_pane_id()
10651
10631
  if (self._railmux_pane_id is not None
10652
10632
  and tmux_ctl.current_session_name() == "railmux"
@@ -10688,6 +10668,22 @@ class App:
10688
10668
  self._loop.screen.set_terminal_properties(colors=_TERMINAL_COLORS)
10689
10669
  except Exception:
10690
10670
  pass
10671
+ if sess is not None:
10672
+ # The dedicated server inherits ``status off`` while the
10673
+ # terminal-native Restoring surface is visible. Claim and
10674
+ # reveal Railmux's sole status surface only after Screen and
10675
+ # MainLoop are ready; an earlier initialization failure stays
10676
+ # visually clean. These overrides remain session-scoped and
10677
+ # teardown reverts them to the dedicated server's hidden
10678
+ # baseline.
10679
+ for opt, val in self._TMUX_BAR_OPTIONS:
10680
+ _sp.run(
10681
+ ["tmux", "set-option", "-t", sess, opt, val],
10682
+ stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
10683
+ )
10684
+ self._tmux_status_session = sess
10685
+ self._tmux_status_enabled = True
10686
+ self._apply_tmux_bar(error=False) # initial green bar
10691
10687
  self._check_terminal_size()
10692
10688
  # If a saved agent workspace is valid, establish its final empty
10693
10689
  # tmux boundaries before Urwid emits the first sidebar frame. The
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.4.dev1
3
+ Version: 0.3.5.dev1
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
@@ -395,7 +395,9 @@ def test_foreign_tmux_launches_dedicated_server_with_clean_environment(
395
395
  "expected_session_id": None,
396
396
  }
397
397
  assert argv == [
398
- "tmux", "-L", "railmux", "new-session", "-A", "-s", "railmux",
398
+ "tmux", "-L", "railmux",
399
+ "start-server", ";", "set-option", "-g", "status", "off", ";",
400
+ "new-session", "-A", "-s", "railmux",
399
401
  "/bin/railmux", "--inside-tmux", "--project", "/work",
400
402
  ]
401
403
  assert "TMUX" not in env
@@ -1718,7 +1718,9 @@ def test_terminal_surface_hides_cursor_covered_by_a_frozen_pane():
1718
1718
 
1719
1719
  TerminalSurface(stream).paint(screen, ((covering, (b"a", b"b")),))
1720
1720
 
1721
- assert stream.getvalue().endswith(b"\033[?25l")
1721
+ rendered = stream.getvalue()
1722
+ assert rendered.endswith(b"\033[1;2H")
1723
+ assert rendered.count(b"\033[?25l") == 1
1722
1724
 
1723
1725
 
1724
1726
  def test_terminal_surface_reasserts_cached_cursor_without_repainting():
@@ -1739,6 +1741,55 @@ def test_terminal_surface_reasserts_cached_cursor_without_repainting():
1739
1741
  surface.reassert_cursor()
1740
1742
 
1741
1743
  assert stream.getvalue() == b"\033[0m\033[?7h\033[2;5H\033[?25h"
1744
+
1745
+
1746
+ def test_terminal_surface_local_highlight_repaints_only_changed_rows():
1747
+ screen = ScreenModel().apply(
1748
+ ClientScreenUpdateDecoder().feed(
1749
+ encode_update(_keyframe(width=20, height=4))
1750
+ )[0],
1751
+ os.terminal_size((20, 4)),
1752
+ )
1753
+ assert screen is not None
1754
+ stream = io.BytesIO()
1755
+ surface = TerminalSurface(stream, mouse_hover=False)
1756
+ surface.paint(screen)
1757
+ stream.seek(0)
1758
+ stream.truncate()
1759
+
1760
+ surface.paint_changed_rows(
1761
+ screen,
1762
+ (),
1763
+ ((1, 4, b"https://example"),),
1764
+ {1},
1765
+ )
1766
+
1767
+ rendered = stream.getvalue()
1768
+ assert b"\033[2J" not in rendered
1769
+ assert b"\033[2;1H\033[2Krow-1" in rendered
1770
+ assert b"\033[2;5H\033[0;7mhttps://example" in rendered
1771
+
1772
+
1773
+ def test_terminal_surface_stable_cursor_visibility_is_not_reasserted_per_patch():
1774
+ screen = ScreenModel().apply(
1775
+ ClientScreenUpdateDecoder().feed(
1776
+ encode_update(_keyframe(width=12, height=3))
1777
+ )[0],
1778
+ os.terminal_size((12, 3)),
1779
+ )
1780
+ assert screen is not None
1781
+ stream = io.BytesIO()
1782
+ surface = TerminalSurface(stream, mouse_hover=False)
1783
+ surface.paint(screen)
1784
+ stream.seek(0)
1785
+ stream.truncate()
1786
+
1787
+ surface.paint(replace(screen, changed_rows=(1,), clear=False))
1788
+
1789
+ rendered = stream.getvalue()
1790
+ assert b"\033[?25h" not in rendered
1791
+ assert b"\033[?25l" not in rendered
1792
+ assert rendered.endswith(b"\033[1;2H")
1742
1793
  assert b"\033[2J" not in stream.getvalue()
1743
1794
 
1744
1795
 
@@ -1798,7 +1849,8 @@ def test_terminal_surface_clips_projected_patches_overlays_and_cursor():
1798
1849
  assert b"\033[4;1H\033[2Krow-14" in rendered
1799
1850
  assert b"hidden" not in rendered
1800
1851
  assert b"\033[1;4H\033[6Xone" in rendered
1801
- assert rendered.endswith(b"\033[1;1H\033[?25l")
1852
+ assert rendered.endswith(b"\033[1;1H")
1853
+ assert rendered.count(b"\033[?25l") == 1
1802
1854
 
1803
1855
 
1804
1856
  def test_terminal_surface_maps_projected_mouse_rows_to_logical_screen():
@@ -4869,6 +4921,8 @@ def test_local_status_preserves_painted_status_left_and_background():
4869
4921
  assert b"\033[4;21H" in painted
4870
4922
  assert b"\033[48;2;95;175;0m\033[1;38;5;17m\033[K" in painted
4871
4923
  assert b"Copied 12 chars." in painted
4924
+ assert b"\033[?25h" not in painted
4925
+ assert b"\033[?25l" not in painted
4872
4926
 
4873
4927
 
4874
4928
  def test_reconnect_status_uses_retained_status_right_without_stale_cursor():
@@ -2661,7 +2661,7 @@ def test_teardown_reverts_every_bar_option(monkeypatch):
2661
2661
 
2662
2662
  def test_run_defers_saved_agent_focus_and_reverts_bar_if_setup_raises(
2663
2663
  monkeypatch):
2664
- """Startup hides a saved agent focus before drawing and always tears down."""
2664
+ """A pre-loop failure leaves the startup status hidden and tears down."""
2665
2665
  import railmux.ui.app as app_mod
2666
2666
 
2667
2667
  app = _minimal_app()
@@ -2681,8 +2681,9 @@ def test_run_defers_saved_agent_focus_and_reverts_bar_if_setup_raises(
2681
2681
  monkeypatch.setattr(app_mod.tmux_ctl, "current_pane_id", lambda: "%0")
2682
2682
  monkeypatch.setattr(
2683
2683
  app_mod.tmux_ctl, "use_smallest_window_size", lambda _pane: True)
2684
- monkeypatch.setattr("subprocess.run", MagicMock())
2685
- # Screen construction blows up AFTER the status bar has been set up.
2684
+ run = MagicMock()
2685
+ monkeypatch.setattr("subprocess.run", run)
2686
+ # Screen construction blows up before the managed status bar is revealed.
2686
2687
  monkeypatch.setattr("urwid.raw_display.Screen",
2687
2688
  MagicMock(side_effect=RuntimeError("boom")))
2688
2689
 
@@ -2690,8 +2691,12 @@ def test_run_defers_saved_agent_focus_and_reverts_bar_if_setup_raises(
2690
2691
  app.run()
2691
2692
 
2692
2693
  app._frame.set_window_active.assert_called_once_with(False)
2693
- assert app._tmux_status_enabled is True # setup ran (bar was mutated)...
2694
- teardown.assert_called_once_with() # ...and teardown reverted it
2694
+ assert app._tmux_status_enabled is False
2695
+ assert not any(
2696
+ call.args[0][-2:] == ["status", "on"]
2697
+ for call in run.call_args_list
2698
+ )
2699
+ teardown.assert_called_once_with()
2695
2700
 
2696
2701
 
2697
2702
  # ── QuitConfirmModal ─────────────────────────────────────────────────────
@@ -29,7 +29,9 @@ def test_launcher_argv_preserves_multi_argument_python_module_prefix():
29
29
  ["/usr/bin/python3", "-m", "railmux"],
30
30
  ["--mode", "codex"],
31
31
  ) == [
32
- "tmux", "-L", "railmux", "new-session", "-A", "-s", "railmux",
32
+ "tmux", "-L", "railmux",
33
+ "start-server", ";", "set-option", "-g", "status", "off", ";",
34
+ "new-session", "-A", "-s", "railmux",
33
35
  "/usr/bin/python3", "-m", "railmux", "--inside-tmux",
34
36
  "--mode", "codex",
35
37
  ]
@@ -1 +0,0 @@
1
- __version__ = "0.3.4.dev1"
File without changes
File without changes
File without changes
File without changes