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.
- {railmux-0.3.4.dev1/src/railmux.egg-info → railmux-0.3.5.dev1}/PKG-INFO +1 -1
- railmux-0.3.5.dev1/src/railmux/__init__.py +1 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_client.py +142 -40
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tmux_server.py +15 -1
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/app.py +19 -23
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_cli.py +3 -1
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_fast_full_window.py +56 -2
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_soft_quit.py +10 -5
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_server.py +3 -1
- railmux-0.3.4.dev1/src/railmux/__init__.py +0 -1
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/LICENSE +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/MANIFEST.in +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/README.md +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/pyproject.toml +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/setup.cfg +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/__main__.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/background_index.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/cli.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/config.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/discovery.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/favorites.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/launcher.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/local_open.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/models.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/modes.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/renames.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/self_update.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/session_index.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/settings.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/transcript.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_background_index.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_codex_index.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_config.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_config_cli.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_discovery.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_display_transport.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_help_agent.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_keymap.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_launcher.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_local_open.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_modals.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_modes.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_new_project.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_options.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_path_codec.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_release_notes.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_remote_config.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_renames.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_restart_state.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_running_pane.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_running_sort.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_self_update.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_session_cache.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_session_index.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_settings.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_sidebar.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_status.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_statusbar.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_system_deps.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_integration.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_transcript.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_widgets.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tools/release_notes.py +0 -0
- {railmux-0.3.4.dev1 → railmux-0.3.5.dev1}/tools/wheel_smoke.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
859
|
+
rendered = (
|
|
845
860
|
f"\033[?7l\033[0m\033[{max(1, height)};1H\033[2K{safe}"
|
|
846
|
-
"\033[0m\033[?7h
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
2632
|
-
|
|
2713
|
+
after = selection.segments()
|
|
2714
|
+
surface.paint_changed_rows(
|
|
2715
|
+
latest_screen,
|
|
2633
2716
|
history.overlays(),
|
|
2634
|
-
|
|
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
|
-
|
|
2811
|
-
|
|
2895
|
+
selection_after = selection.segments()
|
|
2896
|
+
surface.paint_changed_rows(
|
|
2897
|
+
latest_screen,
|
|
2812
2898
|
history.overlays(),
|
|
2813
|
-
|
|
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
|
-
|
|
2847
|
-
|
|
2934
|
+
flash_after = selection.segments()
|
|
2935
|
+
surface.paint_changed_rows(
|
|
2936
|
+
latest_screen,
|
|
2848
2937
|
history.overlays(),
|
|
2849
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10602
|
-
#
|
|
10603
|
-
|
|
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
|
|
@@ -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",
|
|
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
|
-
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
2685
|
-
|
|
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
|
|
2694
|
-
|
|
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",
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|