railmux 0.4.0.dev1__tar.gz → 0.4.0.dev2__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.4.0.dev1/src/railmux.egg-info → railmux-0.4.0.dev2}/PKG-INFO +1 -1
- railmux-0.4.0.dev2/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/mux/__init__.py +4 -2
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/mux/backend.py +16 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/mux/tmux_backend.py +5 -1
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/app.py +19 -3
- railmux-0.4.0.dev2/src/railmux/ui/status_chrome.py +149 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/backend.py +212 -38
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/compositor.py +51 -18
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/SOURCES.txt +3 -0
- railmux-0.4.0.dev2/tests/test_status_chrome.py +87 -0
- railmux-0.4.0.dev2/tests/test_windows_parity_contract.py +79 -0
- railmux-0.4.0.dev2/tests/test_winlocal_backend.py +577 -0
- railmux-0.4.0.dev1/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev1/tests/test_winlocal_backend.py +0 -218
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/LICENSE +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/README.md +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/pyproject.toml +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/setup.cfg +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/__init__.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/config_paths.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/console.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/file_security.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/filelock.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/multiplex.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/process.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/runtime_paths.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/terminal_emulator.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/__init__.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/client.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/conpty.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/daemon.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/history_viewer.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/ipc.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/session_store.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/virtual_screen.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_mux_backend.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_platform.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_compositor.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_conpty.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_daemon.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_history_viewer.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_ipc.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_real_conpty.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_session_store.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_virtual_screen.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev2"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Typed multiplexer boundary shared by tmux and native Windows."""
|
|
2
|
-
from railmux.mux.backend import Capabilities, LaunchSpec, MuxBackend
|
|
2
|
+
from railmux.mux.backend import Capabilities, LaunchSpec, MuxBackend, StatusChrome
|
|
3
3
|
from railmux.mux.tmux_backend import TmuxBackend
|
|
4
4
|
|
|
5
|
-
__all__ = [
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Capabilities", "LaunchSpec", "MuxBackend", "StatusChrome", "TmuxBackend",
|
|
7
|
+
]
|
|
@@ -32,6 +32,20 @@ class LaunchSpec:
|
|
|
32
32
|
login_shell: bool = False
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class StatusChrome:
|
|
37
|
+
"""Platform-neutral state for Railmux's persistent bottom chrome.
|
|
38
|
+
|
|
39
|
+
POSIX projects this into tmux's status line; native Windows draws it in
|
|
40
|
+
the daemon compositor. The transient status message is intentionally a
|
|
41
|
+
separate value so it cannot replace the Mode/Layout controls.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
mode_label: str
|
|
45
|
+
layout_indicator: str | None
|
|
46
|
+
error: bool = False
|
|
47
|
+
|
|
48
|
+
|
|
35
49
|
class MuxBackend(Protocol):
|
|
36
50
|
"""Core topology/lifecycle surface consumed by the shared UI.
|
|
37
51
|
|
|
@@ -57,3 +71,5 @@ class MuxBackend(Protocol):
|
|
|
57
71
|
def detach_client(self) -> bool: ...
|
|
58
72
|
def kill_pane(self, pane_id: str) -> bool: ...
|
|
59
73
|
def kill_session(self, name: str) -> bool: ...
|
|
74
|
+
def set_status_text(self, text: str, level: str) -> None: ...
|
|
75
|
+
def set_status_chrome(self, chrome: StatusChrome) -> None: ...
|
|
@@ -6,7 +6,7 @@ from types import ModuleType
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from railmux import tmux_ctl
|
|
9
|
-
from railmux.mux.backend import Capabilities
|
|
9
|
+
from railmux.mux.backend import Capabilities, StatusChrome
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class TmuxBackend:
|
|
@@ -128,6 +128,10 @@ class TmuxBackend:
|
|
|
128
128
|
# tmux option formatting; native rendering uses this backend hook.
|
|
129
129
|
pass
|
|
130
130
|
|
|
131
|
+
def set_status_chrome(self, _chrome: StatusChrome) -> None:
|
|
132
|
+
# App retains the established tmux option formatting and leases.
|
|
133
|
+
pass
|
|
134
|
+
|
|
131
135
|
def prepare_launch(
|
|
132
136
|
self,
|
|
133
137
|
argv: list[str],
|
|
@@ -56,7 +56,7 @@ from railmux.modes import (
|
|
|
56
56
|
ProjectSource,
|
|
57
57
|
)
|
|
58
58
|
from railmux.models import AttentionState, Project, SessionMeta
|
|
59
|
-
from railmux.mux import MuxBackend, TmuxBackend
|
|
59
|
+
from railmux.mux import MuxBackend, StatusChrome, TmuxBackend
|
|
60
60
|
from railmux.tmux_binding_manager import SharedTmuxBindingManager
|
|
61
61
|
from railmux.mouse_manager import RootWheelForwardingManager
|
|
62
62
|
from railmux.renames import Renames
|
|
@@ -2028,8 +2028,9 @@ class App:
|
|
|
2028
2028
|
return False
|
|
2029
2029
|
slot.agent_tmux_name = None
|
|
2030
2030
|
slot.mode_key = self._current_mode_key()
|
|
2031
|
-
|
|
2032
|
-
|
|
2031
|
+
# A preview is the single-click/Space action. Keep keyboard focus
|
|
2032
|
+
# in the sidebar; only double-click/Enter is allowed to transfer
|
|
2033
|
+
# focus to the displayed surface (P06).
|
|
2033
2034
|
return True
|
|
2034
2035
|
|
|
2035
2036
|
import shlex
|
|
@@ -10033,6 +10034,21 @@ class App:
|
|
|
10033
10034
|
initial paint, from _render_status_to_tmux on the normal↔error
|
|
10034
10035
|
transition, and from mode cycling so the mode indicator repaints.
|
|
10035
10036
|
Best-effort — a tmux hiccup must not raise into the UI."""
|
|
10037
|
+
# Publish the semantic chrome even when tmux is not the renderer.
|
|
10038
|
+
# Native Windows consumes this hook; POSIX keeps its established tmux
|
|
10039
|
+
# formatting below. Test doubles and older third-party backends remain
|
|
10040
|
+
# best-effort compatible.
|
|
10041
|
+
mux = getattr(self, "mux", None)
|
|
10042
|
+
publish = getattr(mux, "set_status_chrome", None)
|
|
10043
|
+
if callable(publish):
|
|
10044
|
+
try:
|
|
10045
|
+
publish(StatusChrome(
|
|
10046
|
+
mode_label=self._active_mode().label,
|
|
10047
|
+
layout_indicator=self._status_layout_indicator(),
|
|
10048
|
+
error=error,
|
|
10049
|
+
))
|
|
10050
|
+
except (AttributeError, KeyError, TypeError, ValueError):
|
|
10051
|
+
pass
|
|
10036
10052
|
if not self._tmux_status_enabled or not self._tmux_status_session:
|
|
10037
10053
|
return
|
|
10038
10054
|
bar = _TMUX_BAR_STYLE_ERROR if error else _TMUX_BAR_STYLE_NORMAL
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Semantic bottom-chrome projection shared by non-tmux frontends."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from urwid.str_util import calc_width
|
|
7
|
+
|
|
8
|
+
from railmux.ui.workspace import WorkspacePage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ACTION_MODE = "mode"
|
|
12
|
+
ACTION_LAYOUT = "layout"
|
|
13
|
+
ACTION_COPY = "copy"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class StatusHit:
|
|
18
|
+
"""One clickable half-open cell range in the bottom status row."""
|
|
19
|
+
|
|
20
|
+
start: int
|
|
21
|
+
end: int
|
|
22
|
+
action: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class StatusProjection:
|
|
27
|
+
"""Plain terminal text plus semantic hit targets for one status row."""
|
|
28
|
+
|
|
29
|
+
text: str
|
|
30
|
+
hits: tuple[StatusHit, ...]
|
|
31
|
+
error: bool
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def project_status_chrome(
|
|
35
|
+
*,
|
|
36
|
+
width: int,
|
|
37
|
+
mode_label: str,
|
|
38
|
+
layout_indicator: str | None,
|
|
39
|
+
status_text: str,
|
|
40
|
+
status_level: str,
|
|
41
|
+
compact: bool,
|
|
42
|
+
active_page: WorkspacePage,
|
|
43
|
+
page_targets: tuple[str | None, str | None, str | None],
|
|
44
|
+
) -> StatusProjection:
|
|
45
|
+
"""Build the platform-neutral visible controls for a full-width bar.
|
|
46
|
+
|
|
47
|
+
Renderers remain free to choose colours, but labels and click authority
|
|
48
|
+
live here so a native frontend cannot accidentally expose only status-right.
|
|
49
|
+
"""
|
|
50
|
+
width = max(1, width)
|
|
51
|
+
parts: list[str] = []
|
|
52
|
+
hits: list[StatusHit] = []
|
|
53
|
+
cells = 0
|
|
54
|
+
|
|
55
|
+
def append(value: str, action: str | None = None) -> None:
|
|
56
|
+
nonlocal cells
|
|
57
|
+
value = _safe_text(value)
|
|
58
|
+
bounded = _fit_cells(value, max(0, width - cells))
|
|
59
|
+
if not bounded:
|
|
60
|
+
return
|
|
61
|
+
start = cells
|
|
62
|
+
parts.append(bounded)
|
|
63
|
+
cells += _cells(bounded)
|
|
64
|
+
if action is not None and cells > start:
|
|
65
|
+
hits.append(StatusHit(start, cells, action))
|
|
66
|
+
|
|
67
|
+
if compact:
|
|
68
|
+
if width < 52:
|
|
69
|
+
labels = ("R", "1", "2")
|
|
70
|
+
visible_mode = (
|
|
71
|
+
"Cx" if mode_label == "Codex" else
|
|
72
|
+
"CC" if mode_label == "Claude Code" else mode_label[:2]
|
|
73
|
+
)
|
|
74
|
+
elif width < 80:
|
|
75
|
+
labels = ("Railmux", "A1", "A2")
|
|
76
|
+
visible_mode = (
|
|
77
|
+
"Codex" if mode_label == "Codex" else
|
|
78
|
+
"CC" if mode_label == "Claude Code" else mode_label[:8]
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
labels = ("Railmux", "Agent 1", "Agent 2")
|
|
82
|
+
visible_mode = mode_label
|
|
83
|
+
pages = (
|
|
84
|
+
WorkspacePage.SIDEBAR,
|
|
85
|
+
WorkspacePage.PRIMARY,
|
|
86
|
+
WorkspacePage.SECONDARY,
|
|
87
|
+
)
|
|
88
|
+
for page, target, label in zip(pages, page_targets, labels):
|
|
89
|
+
# Native terminals do not all expose tmux's per-range style
|
|
90
|
+
# channel. Keep the selected page visible in plain text too.
|
|
91
|
+
marker = f"<{label}>" if page is active_page else f"[{label}]"
|
|
92
|
+
append(marker, f"page:{target}" if target is not None else None)
|
|
93
|
+
append(" ")
|
|
94
|
+
append(visible_mode, ACTION_MODE)
|
|
95
|
+
else:
|
|
96
|
+
append(" Railmux · ")
|
|
97
|
+
append(mode_label, ACTION_MODE)
|
|
98
|
+
|
|
99
|
+
if layout_indicator:
|
|
100
|
+
append(" · ")
|
|
101
|
+
append(layout_indicator, ACTION_LAYOUT)
|
|
102
|
+
append(" ")
|
|
103
|
+
|
|
104
|
+
safe_status = _safe_text(status_text)
|
|
105
|
+
available = max(0, width - cells)
|
|
106
|
+
if safe_status and available:
|
|
107
|
+
visible_status = _fit_cells(safe_status, available)
|
|
108
|
+
status_cells = _cells(visible_status)
|
|
109
|
+
padding = max(0, available - status_cells)
|
|
110
|
+
if padding:
|
|
111
|
+
parts.append(" " * padding)
|
|
112
|
+
cells += padding
|
|
113
|
+
start = cells
|
|
114
|
+
parts.append(visible_status)
|
|
115
|
+
cells += status_cells
|
|
116
|
+
if cells > start:
|
|
117
|
+
hits.append(StatusHit(start, cells, ACTION_COPY))
|
|
118
|
+
|
|
119
|
+
return StatusProjection(
|
|
120
|
+
text="".join(parts),
|
|
121
|
+
hits=tuple(hits),
|
|
122
|
+
error=status_level == "error",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _safe_text(value: str) -> str:
|
|
127
|
+
return "".join(
|
|
128
|
+
" " if ord(character) < 32 or 0x7F <= ord(character) <= 0x9F
|
|
129
|
+
else character
|
|
130
|
+
for character in value
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _cells(value: str) -> int:
|
|
135
|
+
return calc_width(value, 0, len(value))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _fit_cells(value: str, available: int) -> str:
|
|
139
|
+
if available <= 0:
|
|
140
|
+
return ""
|
|
141
|
+
result: list[str] = []
|
|
142
|
+
used = 0
|
|
143
|
+
for character in value:
|
|
144
|
+
width = _cells(character)
|
|
145
|
+
if used + width > available:
|
|
146
|
+
break
|
|
147
|
+
result.append(character)
|
|
148
|
+
used += width
|
|
149
|
+
return "".join(result)
|
|
@@ -14,10 +14,22 @@ from typing import Callable
|
|
|
14
14
|
|
|
15
15
|
from railmux import tmux_ctl
|
|
16
16
|
from railmux.fast_display_input import SgrMouseEvent, TerminalInputDecoder
|
|
17
|
-
from railmux.mux.backend import Capabilities, LaunchSpec
|
|
17
|
+
from railmux.mux.backend import Capabilities, LaunchSpec, StatusChrome
|
|
18
18
|
from railmux.platform.process import provider_argv
|
|
19
19
|
from railmux.restart_state import OuterTmuxIdentity
|
|
20
|
-
from railmux.ui.
|
|
20
|
+
from railmux.ui.status_chrome import (
|
|
21
|
+
ACTION_COPY,
|
|
22
|
+
ACTION_LAYOUT,
|
|
23
|
+
ACTION_MODE,
|
|
24
|
+
StatusHit,
|
|
25
|
+
project_status_chrome,
|
|
26
|
+
)
|
|
27
|
+
from railmux.ui.workspace import (
|
|
28
|
+
DisplayTransportKind,
|
|
29
|
+
WorkspaceLayout,
|
|
30
|
+
WorkspacePage,
|
|
31
|
+
WorkspacePresentation,
|
|
32
|
+
)
|
|
21
33
|
from railmux.winlocal.compositor import Compositor, Region, TerminalPane
|
|
22
34
|
from railmux.winlocal.conpty import ConPtyProcess, PyWinPtyProcess
|
|
23
35
|
from railmux.winlocal.history_viewer import HistoryViewer
|
|
@@ -78,9 +90,21 @@ class WinMuxBackend:
|
|
|
78
90
|
self._last_input_source: object | None = None
|
|
79
91
|
self._client_count: Callable[[], int] | None = None
|
|
80
92
|
self._workspace = None
|
|
81
|
-
self.
|
|
93
|
+
self._sidebar_width: int | None = None
|
|
94
|
+
# Divider positions are axis-specific. Keeping one value here makes
|
|
95
|
+
# a column width turn into a row height (and vice versa) when the user
|
|
96
|
+
# cycles between side-by-side and stacked layouts.
|
|
97
|
+
self._primary_width: int | None = None
|
|
98
|
+
self._primary_height: int | None = None
|
|
99
|
+
# The sidebar starts as the only surface and therefore owns the full
|
|
100
|
+
# viewport. Opening the first display pane narrows it through the same
|
|
101
|
+
# geometry synchronization path used by later layout changes.
|
|
102
|
+
self._screen = VirtualScreen(width, max(1, height - 1))
|
|
82
103
|
self._compositor = Compositor(width, height)
|
|
83
|
-
self.
|
|
104
|
+
self._status_text = ""
|
|
105
|
+
self._status_level = "tip"
|
|
106
|
+
self._status_chrome = StatusChrome("Claude Code", None)
|
|
107
|
+
self._status_hits: tuple[StatusHit, ...] = ()
|
|
84
108
|
self._lock = threading.RLock()
|
|
85
109
|
self._input_decoder = TerminalInputDecoder()
|
|
86
110
|
|
|
@@ -150,7 +174,13 @@ class WinMuxBackend:
|
|
|
150
174
|
|
|
151
175
|
def set_status_text(self, text: str, level: str) -> None:
|
|
152
176
|
prefix = {"error": "ERROR: ", "warn": "WARNING: "}.get(level, "")
|
|
153
|
-
self.
|
|
177
|
+
with self._lock:
|
|
178
|
+
self._status_text = prefix + text
|
|
179
|
+
self._status_level = level
|
|
180
|
+
|
|
181
|
+
def set_status_chrome(self, chrome: StatusChrome) -> None:
|
|
182
|
+
with self._lock:
|
|
183
|
+
self._status_chrome = chrome
|
|
154
184
|
|
|
155
185
|
def create_display_pane(self, key: str) -> str:
|
|
156
186
|
with self._lock:
|
|
@@ -329,7 +359,17 @@ class WinMuxBackend:
|
|
|
329
359
|
with self._lock:
|
|
330
360
|
if not self.pane_alive(pane_id):
|
|
331
361
|
return False
|
|
362
|
+
if pane_id == self._active_pane:
|
|
363
|
+
return True
|
|
332
364
|
self._last_pane, self._active_pane = self._active_pane, pane_id
|
|
365
|
+
# tmux status-page selection inside a zoomed window displays the
|
|
366
|
+
# newly selected pane. Preserve that semantic in the native
|
|
367
|
+
# compositor instead of leaving zoom ownership on an invisible
|
|
368
|
+
# previous page.
|
|
369
|
+
if self._zoomed_pane is not None:
|
|
370
|
+
self._zoomed_pane = pane_id
|
|
371
|
+
self._sync_display_sizes()
|
|
372
|
+
self.request_keyframe()
|
|
333
373
|
return True
|
|
334
374
|
|
|
335
375
|
def active_pane_id(self, _target: str | None = None) -> str:
|
|
@@ -340,7 +380,10 @@ class WinMuxBackend:
|
|
|
340
380
|
|
|
341
381
|
def pane_size(self, pane_id: str):
|
|
342
382
|
with self._lock:
|
|
343
|
-
|
|
383
|
+
# tmux preserves the logical sizes of panes hidden by zoom. App
|
|
384
|
+
# uses those sizes to retain/reflow layout profiles, so report the
|
|
385
|
+
# same unzoomed geometry rather than the full-window projection.
|
|
386
|
+
regions = self._layout_regions()
|
|
344
387
|
if pane_id == "%controller":
|
|
345
388
|
region = regions.get("sidebar")
|
|
346
389
|
return (region.width, region.height) if region else None
|
|
@@ -364,17 +407,43 @@ class WinMuxBackend:
|
|
|
364
407
|
def window_size(self, _pane_id: str):
|
|
365
408
|
return (self.width, self.height)
|
|
366
409
|
|
|
367
|
-
def resize_pane_width(self,
|
|
368
|
-
|
|
410
|
+
def resize_pane_width(self, pane_id: str, width: int) -> bool:
|
|
411
|
+
with self._lock:
|
|
412
|
+
workspace = self._workspace
|
|
413
|
+
if pane_id == "%controller":
|
|
414
|
+
self._sidebar_width = width
|
|
415
|
+
elif (
|
|
416
|
+
workspace is not None
|
|
417
|
+
and pane_id == workspace.primary.pane_id
|
|
418
|
+
and workspace.layout is WorkspaceLayout.SIDE_BY_SIDE
|
|
419
|
+
):
|
|
420
|
+
self._primary_width = width
|
|
421
|
+
else:
|
|
422
|
+
return False
|
|
423
|
+
self._sync_display_sizes()
|
|
424
|
+
self.request_keyframe()
|
|
425
|
+
return True
|
|
369
426
|
|
|
370
|
-
def resize_pane_height(self,
|
|
371
|
-
|
|
427
|
+
def resize_pane_height(self, pane_id: str, height: int) -> bool:
|
|
428
|
+
with self._lock:
|
|
429
|
+
workspace = self._workspace
|
|
430
|
+
if (
|
|
431
|
+
workspace is None
|
|
432
|
+
or pane_id != workspace.primary.pane_id
|
|
433
|
+
or workspace.layout is not WorkspaceLayout.STACKED
|
|
434
|
+
):
|
|
435
|
+
return False
|
|
436
|
+
self._primary_height = height
|
|
437
|
+
self._sync_display_sizes()
|
|
438
|
+
self.request_keyframe()
|
|
439
|
+
return True
|
|
372
440
|
|
|
373
441
|
def toggle_pane_zoom(self, _pane_id: str) -> bool:
|
|
374
442
|
with self._lock:
|
|
375
443
|
if not self.pane_alive(_pane_id):
|
|
376
444
|
return False
|
|
377
445
|
self._zoomed_pane = None if self._zoomed_pane == _pane_id else _pane_id
|
|
446
|
+
self._sync_display_sizes()
|
|
378
447
|
self.request_keyframe()
|
|
379
448
|
return True
|
|
380
449
|
|
|
@@ -562,6 +631,8 @@ class WinMuxBackend:
|
|
|
562
631
|
def screen_update(self):
|
|
563
632
|
with self._lock:
|
|
564
633
|
self._sync_display_sizes()
|
|
634
|
+
status = self._project_status()
|
|
635
|
+
self._status_hits = status.hits
|
|
565
636
|
workspace = self._workspace
|
|
566
637
|
primary = secondary = None
|
|
567
638
|
stacked = False
|
|
@@ -577,7 +648,9 @@ class WinMuxBackend:
|
|
|
577
648
|
)
|
|
578
649
|
if zoomed is not None:
|
|
579
650
|
return self._compositor.compose_full(
|
|
580
|
-
zoomed,
|
|
651
|
+
zoomed,
|
|
652
|
+
status=status.text.encode("utf-8", errors="replace"),
|
|
653
|
+
status_error=status.error,
|
|
581
654
|
)
|
|
582
655
|
focus = "sidebar"
|
|
583
656
|
if workspace is not None and self._active_pane == workspace.primary.pane_id:
|
|
@@ -586,7 +659,22 @@ class WinMuxBackend:
|
|
|
586
659
|
focus = "secondary"
|
|
587
660
|
return self._compositor.compose(
|
|
588
661
|
self._screen.pane, primary, secondary, stacked=stacked,
|
|
589
|
-
status=
|
|
662
|
+
status=status.text.encode("utf-8", errors="replace"),
|
|
663
|
+
status_error=status.error,
|
|
664
|
+
focus=focus,
|
|
665
|
+
show_primary=bool(
|
|
666
|
+
workspace is not None and workspace.primary.pane_id
|
|
667
|
+
),
|
|
668
|
+
show_secondary=bool(
|
|
669
|
+
workspace is not None
|
|
670
|
+
and workspace.secondary.pane_id
|
|
671
|
+
and workspace.layout is not WorkspaceLayout.SINGLE
|
|
672
|
+
),
|
|
673
|
+
layout=workspace.layout if workspace is not None else None,
|
|
674
|
+
sidebar_width=self._sidebar_width,
|
|
675
|
+
primary_size=(
|
|
676
|
+
self._primary_height if stacked else self._primary_width
|
|
677
|
+
),
|
|
590
678
|
)
|
|
591
679
|
|
|
592
680
|
def request_keyframe(self) -> None:
|
|
@@ -600,23 +688,7 @@ class WinMuxBackend:
|
|
|
600
688
|
with self._lock:
|
|
601
689
|
self.width, self.height = width, height
|
|
602
690
|
self._compositor.resize(width, height)
|
|
603
|
-
|
|
604
|
-
sidebar = regions["sidebar"]
|
|
605
|
-
self._screen.resize(sidebar.width, sidebar.height)
|
|
606
|
-
for name in ("primary", "secondary"):
|
|
607
|
-
region = regions.get(name)
|
|
608
|
-
pane_id = self._pane_for_region(name)
|
|
609
|
-
preview = self._previews.get(pane_id or "")
|
|
610
|
-
if region is not None and preview is not None:
|
|
611
|
-
preview.resize(region.width, region.height)
|
|
612
|
-
session = self._session_for_region(name)
|
|
613
|
-
if region is None or session is None:
|
|
614
|
-
continue
|
|
615
|
-
if session.terminal is not None:
|
|
616
|
-
session.terminal.resize(region.width, region.height)
|
|
617
|
-
if session.process is not None and session.process.is_alive():
|
|
618
|
-
session.process.resize(region.width, region.height)
|
|
619
|
-
session.process_size = (region.width, region.height)
|
|
691
|
+
self._sync_display_sizes()
|
|
620
692
|
|
|
621
693
|
def route_input(self, data: bytes, *, source: object | None = None) -> None:
|
|
622
694
|
"""Route keyboard and SGR mouse reports to the focused surface."""
|
|
@@ -634,10 +706,29 @@ class WinMuxBackend:
|
|
|
634
706
|
self._write_active(data)
|
|
635
707
|
|
|
636
708
|
def _sync_display_sizes(self) -> None:
|
|
637
|
-
"""Apply
|
|
709
|
+
"""Apply compositor geometry to Urwid, previews, and every ConPTY.
|
|
710
|
+
|
|
711
|
+
A topology change can narrow the sidebar without an outer terminal
|
|
712
|
+
resize. Urwid must receive that viewport change too; otherwise it
|
|
713
|
+
keeps drawing its old wide canvas into a narrow terminal emulator and
|
|
714
|
+
wrapped fragments appear as duplicated rows.
|
|
715
|
+
"""
|
|
638
716
|
regions = self._display_regions()
|
|
717
|
+
sidebar = regions.get("sidebar")
|
|
718
|
+
if (
|
|
719
|
+
sidebar is not None
|
|
720
|
+
and self._screen.get_cols_rows() != (sidebar.width, sidebar.height)
|
|
721
|
+
):
|
|
722
|
+
self._screen.resize(sidebar.width, sidebar.height)
|
|
639
723
|
for name in ("primary", "secondary"):
|
|
640
724
|
region = regions.get(name)
|
|
725
|
+
pane_id = self._pane_for_region(name)
|
|
726
|
+
preview = self._previews.get(pane_id or "")
|
|
727
|
+
if region is not None and preview is not None:
|
|
728
|
+
if (preview.width, preview.height) != (
|
|
729
|
+
region.width, region.height
|
|
730
|
+
):
|
|
731
|
+
preview.resize(region.width, region.height)
|
|
641
732
|
session = self._session_for_region(name)
|
|
642
733
|
if region is None or session is None:
|
|
643
734
|
continue
|
|
@@ -670,10 +761,24 @@ class WinMuxBackend:
|
|
|
670
761
|
session = self._session_for_pane(self._active_pane)
|
|
671
762
|
if session is not None and session.process is not None:
|
|
672
763
|
session.process.write(data)
|
|
764
|
+
return
|
|
765
|
+
# An empty display slot is not an input sink. This can occur
|
|
766
|
+
# briefly after resetting/reaping a slot while focus still points
|
|
767
|
+
# at its stable pane identity.
|
|
768
|
+
self.select_pane("%controller")
|
|
769
|
+
self._screen.inject(data)
|
|
673
770
|
|
|
674
771
|
def _route_mouse(self, event: SgrMouseEvent) -> None:
|
|
675
772
|
x, y = event.x - 1, event.y - 1
|
|
676
773
|
with self._lock:
|
|
774
|
+
if y == self.height - 1:
|
|
775
|
+
if (
|
|
776
|
+
event.pressed
|
|
777
|
+
and not event.button & (32 | 64)
|
|
778
|
+
and event.button & 3 == 0
|
|
779
|
+
):
|
|
780
|
+
self._route_status_click(x)
|
|
781
|
+
return
|
|
677
782
|
for name, region in self._display_regions().items():
|
|
678
783
|
if not (
|
|
679
784
|
region.x <= x < region.x + region.width
|
|
@@ -720,20 +825,79 @@ class WinMuxBackend:
|
|
|
720
825
|
return {
|
|
721
826
|
name: Region(0, 0, self.width, max(1, self.height - 1))
|
|
722
827
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
828
|
+
return self._layout_regions()
|
|
829
|
+
|
|
830
|
+
def _layout_regions(self):
|
|
831
|
+
"""Return persistent pane geometry without applying zoom projection."""
|
|
727
832
|
workspace = self._workspace
|
|
833
|
+
stacked = bool(
|
|
834
|
+
workspace is not None
|
|
835
|
+
and workspace.layout is WorkspaceLayout.STACKED
|
|
836
|
+
)
|
|
728
837
|
return self._compositor.regions(
|
|
729
|
-
has_primary=
|
|
730
|
-
|
|
731
|
-
|
|
838
|
+
has_primary=bool(
|
|
839
|
+
workspace is not None and workspace.primary.pane_id
|
|
840
|
+
),
|
|
841
|
+
has_secondary=bool(
|
|
732
842
|
workspace is not None
|
|
733
|
-
and workspace.
|
|
843
|
+
and workspace.secondary.pane_id
|
|
844
|
+
and workspace.layout is not WorkspaceLayout.SINGLE
|
|
845
|
+
),
|
|
846
|
+
stacked=stacked,
|
|
847
|
+
layout=workspace.layout if workspace is not None else None,
|
|
848
|
+
sidebar_width=self._sidebar_width,
|
|
849
|
+
primary_size=(
|
|
850
|
+
self._primary_height if stacked else self._primary_width
|
|
734
851
|
),
|
|
735
852
|
)
|
|
736
853
|
|
|
854
|
+
def _project_status(self):
|
|
855
|
+
workspace = self._workspace
|
|
856
|
+
compact = bool(
|
|
857
|
+
workspace is not None
|
|
858
|
+
and workspace.presentation is WorkspacePresentation.COMPACT
|
|
859
|
+
)
|
|
860
|
+
page = (
|
|
861
|
+
workspace.compact_page
|
|
862
|
+
if workspace is not None else WorkspacePage.SIDEBAR
|
|
863
|
+
)
|
|
864
|
+
targets = (
|
|
865
|
+
"%controller",
|
|
866
|
+
workspace.primary.pane_id if workspace is not None else None,
|
|
867
|
+
workspace.secondary.pane_id if workspace is not None else None,
|
|
868
|
+
)
|
|
869
|
+
return project_status_chrome(
|
|
870
|
+
width=self.width,
|
|
871
|
+
mode_label=self._status_chrome.mode_label,
|
|
872
|
+
layout_indicator=self._status_chrome.layout_indicator,
|
|
873
|
+
status_text=self._status_text,
|
|
874
|
+
status_level=(
|
|
875
|
+
"error" if self._status_chrome.error else self._status_level
|
|
876
|
+
),
|
|
877
|
+
compact=compact,
|
|
878
|
+
active_page=page,
|
|
879
|
+
page_targets=targets,
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
def _route_status_click(self, column: int) -> None:
|
|
883
|
+
hit = next(
|
|
884
|
+
(candidate for candidate in self._status_hits
|
|
885
|
+
if candidate.start <= column < candidate.end),
|
|
886
|
+
None,
|
|
887
|
+
)
|
|
888
|
+
if hit is None:
|
|
889
|
+
return
|
|
890
|
+
if hit.action == ACTION_MODE:
|
|
891
|
+
self._screen.inject(b"\x1b[15~") # F5
|
|
892
|
+
elif hit.action == ACTION_LAYOUT:
|
|
893
|
+
self._screen.inject(b"\x1b[18~") # F7
|
|
894
|
+
elif hit.action == ACTION_COPY:
|
|
895
|
+
self._screen.inject(b"\x1b[17~") # F6
|
|
896
|
+
elif hit.action.startswith("page:"):
|
|
897
|
+
pane_id = hit.action[5:]
|
|
898
|
+
if self.pane_alive(pane_id):
|
|
899
|
+
self.select_pane(pane_id)
|
|
900
|
+
|
|
737
901
|
def _pane_for_region(self, name: str) -> str | None:
|
|
738
902
|
workspace = self._workspace
|
|
739
903
|
if workspace is None:
|
|
@@ -826,12 +990,16 @@ class WinDisplayTransport:
|
|
|
826
990
|
def create_primary(self, **_kwargs) -> bool:
|
|
827
991
|
if self.workspace.primary.pane_id is None:
|
|
828
992
|
self.workspace.primary.pane_id = self.backend.create_display_pane("primary")
|
|
993
|
+
with self.backend._lock:
|
|
994
|
+
self.backend._sync_display_sizes()
|
|
829
995
|
return True
|
|
830
996
|
|
|
831
997
|
def create_secondary(self, _layout) -> bool:
|
|
832
998
|
self.create_primary()
|
|
833
999
|
if self.workspace.secondary.pane_id is None:
|
|
834
1000
|
self.workspace.secondary.pane_id = self.backend.create_display_pane("secondary")
|
|
1001
|
+
with self.backend._lock:
|
|
1002
|
+
self.backend._sync_display_sizes()
|
|
835
1003
|
return True
|
|
836
1004
|
|
|
837
1005
|
def create_dual(self, layout, **_kwargs) -> bool:
|
|
@@ -845,6 +1013,7 @@ class WinDisplayTransport:
|
|
|
845
1013
|
if slot.pane_id is None:
|
|
846
1014
|
slot.pane_id = self.backend.create_display_pane(slot.key)
|
|
847
1015
|
with self.backend._lock:
|
|
1016
|
+
self.backend._previews.pop(slot.pane_id, None)
|
|
848
1017
|
self.backend._displayed[slot.pane_id] = agent_tmux_name
|
|
849
1018
|
slot.agent_tmux_name = agent_tmux_name
|
|
850
1019
|
slot.transport_kind = DisplayTransportKind.SWAP
|
|
@@ -864,6 +1033,7 @@ class WinDisplayTransport:
|
|
|
864
1033
|
if slot.pane_id:
|
|
865
1034
|
self.backend._displayed.pop(slot.pane_id, None)
|
|
866
1035
|
self.backend._previews.pop(slot.pane_id, None)
|
|
1036
|
+
self.backend._sync_display_sizes()
|
|
867
1037
|
return True
|
|
868
1038
|
|
|
869
1039
|
def prepare_preview(self, slot) -> bool:
|
|
@@ -873,7 +1043,9 @@ class WinDisplayTransport:
|
|
|
873
1043
|
with self.backend._lock:
|
|
874
1044
|
if slot.pane_id:
|
|
875
1045
|
self.backend._displayed.pop(slot.pane_id, None)
|
|
1046
|
+
self.backend._previews.pop(slot.pane_id, None)
|
|
876
1047
|
slot.clear_content()
|
|
1048
|
+
self.backend._sync_display_sizes()
|
|
877
1049
|
return True
|
|
878
1050
|
|
|
879
1051
|
def prepare_kill(self, _name):
|
|
@@ -884,6 +1056,8 @@ class WinDisplayTransport:
|
|
|
884
1056
|
if slot.pane_id:
|
|
885
1057
|
self.backend.kill_pane(slot.pane_id)
|
|
886
1058
|
slot.clear_display()
|
|
1059
|
+
with self.backend._lock:
|
|
1060
|
+
self.backend._sync_display_sizes()
|
|
887
1061
|
return True
|
|
888
1062
|
|
|
889
1063
|
def close_all(self) -> bool:
|