railmux 0.4.1.dev4__tar.gz → 0.4.1.dev6__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.1.dev4/src/railmux.egg-info → railmux-0.4.1.dev6}/PKG-INFO +1 -1
- railmux-0.4.1.dev6/src/railmux/__init__.py +1 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/fast_display_input.py +149 -28
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/fast_display_server.py +3 -2
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/terminal_screen.py +167 -1
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/tmux_ctl.py +33 -4
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/app.py +34 -8
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_fast_display_selection.py +60 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_fast_display_server.py +64 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_fullscreen_binding.py +22 -1
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_layout_profile.py +1 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_tmux_ctl.py +13 -1
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_tmux_integration.py +21 -9
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_tmux_status.py +28 -3
- railmux-0.4.1.dev4/src/railmux/__init__.py +0 -1
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/LICENSE +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/MANIFEST.in +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/README.md +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/pyproject.toml +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/setup.cfg +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/__main__.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/background_index.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/cli.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/config.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/diagnostic_contract.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/discovery.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/favorites.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/launcher.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/local_open.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/models.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/modes.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/preview_pager.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/project_favorites.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/release_version.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/renames.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/self_update.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/session_index.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/settings.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ssh_preflight.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/tmux_capabilities.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/transcript.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/app_test_harness.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_app_codex_placeholder.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_app_quit.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_app_restart_restore.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_app_running_recovery.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_background_index.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_cli.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_code_map.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_codex_index.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_config.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_config_cli.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_discovery.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_display_transport.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_fast_display_bootstrap.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_fast_display_history.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_fast_display_protocol.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_fast_display_terminal.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_help_agent.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_keymap.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_launcher.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_local_open.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_modals.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_modes.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_new_project.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_options.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_path_codec.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_preview_pager.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_project_favorites.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_release_notes.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_release_version.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_remote_config.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_renames.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_restart_state.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_running_pane.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_running_sort.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_self_update.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_session_cache.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_session_index.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_session_lease.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_settings.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_sidebar.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_status.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_statusbar.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_system_deps.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_tmux_capabilities.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_transcript.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_widgets.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_synchronized_screen.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tools/release_notes.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.1.dev4 → railmux-0.4.1.dev6}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.1.dev6"
|
|
@@ -22,6 +22,9 @@ from railmux.fast_display_protocol import (
|
|
|
22
22
|
|
|
23
23
|
_SGR_MOUSE_PREFIX = b"\x1b[<"
|
|
24
24
|
_SGR_STYLE_RE = re.compile(rb"\x1b\[[0-9;]*m")
|
|
25
|
+
_OSC8_LINK_RE = re.compile(
|
|
26
|
+
rb"\x1b\]8;[^;\x07\x1b]*;([^\x07\x1b]*)(?:\x07|\x1b\\)"
|
|
27
|
+
)
|
|
25
28
|
_PAGE_UP = b"\x1b[5~"
|
|
26
29
|
_PAGE_DOWN = b"\x1b[6~"
|
|
27
30
|
_BRACKETED_PASTE_BEGIN = b"\x1b[200~"
|
|
@@ -505,27 +508,62 @@ def _display_width(character: str) -> int:
|
|
|
505
508
|
return max(0, pyte.screens.wcwidth(character))
|
|
506
509
|
|
|
507
510
|
|
|
508
|
-
def
|
|
509
|
-
|
|
510
|
-
|
|
511
|
+
def _validated_osc8_url(value: bytes) -> str | None:
|
|
512
|
+
if not value or len(value) > 8192:
|
|
513
|
+
return None
|
|
514
|
+
try:
|
|
515
|
+
decoded = value.decode("utf-8")
|
|
516
|
+
parsed = urlsplit(decoded)
|
|
517
|
+
hostname = parsed.hostname
|
|
518
|
+
except (UnicodeDecodeError, ValueError):
|
|
519
|
+
return None
|
|
520
|
+
if (
|
|
521
|
+
parsed.scheme.lower() not in ("http", "https")
|
|
522
|
+
or hostname is None
|
|
523
|
+
or any(ord(character) < 0x20 or character == "\x7f" for character in decoded)
|
|
524
|
+
):
|
|
525
|
+
return None
|
|
526
|
+
return decoded
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _plain_display_row(
|
|
530
|
+
line: bytes,
|
|
531
|
+
width: int,
|
|
532
|
+
) -> tuple[tuple[str | None, ...], tuple[str | None, ...]]:
|
|
533
|
+
"""Decode one rendered row and retain bounded OSC 8 targets per cell."""
|
|
511
534
|
cells: list[str | None] = [" "] * width
|
|
535
|
+
hyperlinks: list[str | None] = [None] * width
|
|
512
536
|
column = 0
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
537
|
+
active_hyperlink: str | None = None
|
|
538
|
+
|
|
539
|
+
def append_visible(data: bytes) -> None:
|
|
540
|
+
nonlocal column
|
|
541
|
+
plain = _SGR_STYLE_RE.sub(b"", data).decode("utf-8", errors="replace")
|
|
542
|
+
for character in plain:
|
|
543
|
+
cell_width = _display_width(character)
|
|
544
|
+
if cell_width == 0:
|
|
545
|
+
previous = column - 1
|
|
546
|
+
while previous >= 0 and cells[previous] is None:
|
|
547
|
+
previous -= 1
|
|
548
|
+
if previous >= 0:
|
|
549
|
+
cells[previous] = (cells[previous] or "") + character
|
|
550
|
+
continue
|
|
551
|
+
if column >= width or column + cell_width > width:
|
|
552
|
+
return
|
|
553
|
+
cells[column] = character
|
|
554
|
+
hyperlinks[column] = active_hyperlink
|
|
555
|
+
for continuation in range(1, cell_width):
|
|
556
|
+
cells[column + continuation] = None
|
|
557
|
+
hyperlinks[column + continuation] = active_hyperlink
|
|
558
|
+
column += cell_width
|
|
559
|
+
|
|
560
|
+
offset = 0
|
|
561
|
+
for match in _OSC8_LINK_RE.finditer(line):
|
|
562
|
+
append_visible(line[offset : match.start()])
|
|
563
|
+
active_hyperlink = _validated_osc8_url(match.group(1))
|
|
564
|
+
offset = match.end()
|
|
565
|
+
append_visible(line[offset:])
|
|
566
|
+
return tuple(cells), tuple(hyperlinks)
|
|
529
567
|
|
|
530
568
|
|
|
531
569
|
_CLICK_TOKEN_RE = re.compile(r"""[^\s<>"'`]+""")
|
|
@@ -719,23 +757,97 @@ def click_target_at(
|
|
|
719
757
|
return None
|
|
720
758
|
|
|
721
759
|
|
|
722
|
-
def
|
|
760
|
+
def _pane_rows(
|
|
723
761
|
source: SelectionSource,
|
|
724
762
|
*,
|
|
725
763
|
start: int = 0,
|
|
726
764
|
end: int | None = None,
|
|
727
|
-
) -> tuple[
|
|
728
|
-
|
|
765
|
+
) -> tuple[
|
|
766
|
+
tuple[tuple[str | None, ...], ...],
|
|
767
|
+
tuple[tuple[str | None, ...], ...],
|
|
768
|
+
]:
|
|
769
|
+
"""Decode pane-local cells and OSC 8 targets in one bounded pass."""
|
|
729
770
|
route = source.route
|
|
730
771
|
if end is None:
|
|
731
772
|
end = route.height
|
|
732
773
|
decode_width = source.row_x_offset + route.width
|
|
733
|
-
|
|
774
|
+
decoded_cells: list[tuple[str | None, ...]] = []
|
|
775
|
+
decoded_hyperlinks: list[tuple[str | None, ...]] = []
|
|
734
776
|
for index in range(start, min(end, route.height)):
|
|
735
777
|
line = source.rows[index] if index < len(source.rows) else b""
|
|
736
|
-
cells =
|
|
737
|
-
|
|
738
|
-
|
|
778
|
+
cells, hyperlinks = _plain_display_row(line, decode_width)
|
|
779
|
+
span = slice(source.row_x_offset, source.row_x_offset + route.width)
|
|
780
|
+
decoded_cells.append(cells[span])
|
|
781
|
+
decoded_hyperlinks.append(hyperlinks[span])
|
|
782
|
+
return tuple(decoded_cells), tuple(decoded_hyperlinks)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _pane_cells(
|
|
786
|
+
source: SelectionSource,
|
|
787
|
+
*,
|
|
788
|
+
start: int = 0,
|
|
789
|
+
end: int | None = None,
|
|
790
|
+
) -> tuple[tuple[str | None, ...], ...]:
|
|
791
|
+
"""Decode pane-local cells from either full-screen or history rows."""
|
|
792
|
+
return _pane_rows(source, start=start, end=end)[0]
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _osc8_target(
|
|
796
|
+
rows: tuple[tuple[str | None, ...], ...],
|
|
797
|
+
hyperlinks: tuple[tuple[str | None, ...], ...],
|
|
798
|
+
row: int,
|
|
799
|
+
column: int,
|
|
800
|
+
*,
|
|
801
|
+
route: HistorySnapshot,
|
|
802
|
+
) -> ClickTarget | None:
|
|
803
|
+
"""Resolve one exact OSC 8 target without reconstructing its label."""
|
|
804
|
+
if not (0 <= row < len(rows) and 0 <= row < len(hyperlinks)):
|
|
805
|
+
return None
|
|
806
|
+
if not 0 <= column < len(hyperlinks[row]):
|
|
807
|
+
return None
|
|
808
|
+
value = hyperlinks[row][column]
|
|
809
|
+
if value is None:
|
|
810
|
+
return None
|
|
811
|
+
|
|
812
|
+
segments: list[SelectionSegment] = []
|
|
813
|
+
first = row
|
|
814
|
+
while first > 0 and value in hyperlinks[first - 1]:
|
|
815
|
+
first -= 1
|
|
816
|
+
last = row
|
|
817
|
+
while last + 1 < len(hyperlinks) and value in hyperlinks[last + 1]:
|
|
818
|
+
last += 1
|
|
819
|
+
for row_index in range(first, last + 1):
|
|
820
|
+
link_row = hyperlinks[row_index]
|
|
821
|
+
cell_row = rows[row_index]
|
|
822
|
+
run_start: int | None = None
|
|
823
|
+
for cell_index in range(len(link_row) + 1):
|
|
824
|
+
linked = cell_index < len(link_row) and link_row[cell_index] == value
|
|
825
|
+
if linked and run_start is None:
|
|
826
|
+
run_start = cell_index
|
|
827
|
+
if not linked and run_start is not None:
|
|
828
|
+
text = "".join(
|
|
829
|
+
cell or "" for cell in cell_row[run_start:cell_index]
|
|
830
|
+
).encode("utf-8")
|
|
831
|
+
if text:
|
|
832
|
+
segments.append(
|
|
833
|
+
(
|
|
834
|
+
route.y + row_index,
|
|
835
|
+
route.x + run_start,
|
|
836
|
+
text,
|
|
837
|
+
)
|
|
838
|
+
)
|
|
839
|
+
run_start = None
|
|
840
|
+
if not segments:
|
|
841
|
+
return None
|
|
842
|
+
return ClickTarget(
|
|
843
|
+
"url",
|
|
844
|
+
value,
|
|
845
|
+
route.pane_id or "",
|
|
846
|
+
highlight_row=segments[0][0],
|
|
847
|
+
highlight_column=segments[0][1],
|
|
848
|
+
highlight_text=segments[0][2],
|
|
849
|
+
highlight_segments=tuple(segments),
|
|
850
|
+
)
|
|
739
851
|
|
|
740
852
|
|
|
741
853
|
def _rows_are_wrapped(
|
|
@@ -1124,10 +1236,14 @@ def _target_in_rows(
|
|
|
1124
1236
|
column: int,
|
|
1125
1237
|
*,
|
|
1126
1238
|
route: HistorySnapshot,
|
|
1239
|
+
hyperlinks: tuple[tuple[str | None, ...], ...] = (),
|
|
1127
1240
|
) -> ClickTarget | None:
|
|
1128
1241
|
"""Resolve a semantic target across one bounded visual-wrap chain."""
|
|
1129
1242
|
if not 0 <= row < len(rows) or route.width <= 0:
|
|
1130
1243
|
return None
|
|
1244
|
+
exact = _osc8_target(rows, hyperlinks, row, column, route=route)
|
|
1245
|
+
if exact is not None:
|
|
1246
|
+
return exact
|
|
1131
1247
|
decorated = _decorated_tool_target(
|
|
1132
1248
|
rows,
|
|
1133
1249
|
row,
|
|
@@ -1200,6 +1316,7 @@ class LocalTextSelection:
|
|
|
1200
1316
|
self._press: SgrMouseEvent | None = None
|
|
1201
1317
|
self._route: HistorySnapshot | None = None
|
|
1202
1318
|
self._rows: tuple[tuple[str | None, ...], ...] = ()
|
|
1319
|
+
self._hyperlinks: tuple[tuple[str | None, ...], ...] = ()
|
|
1203
1320
|
self._anchor: tuple[int, int] | None = None
|
|
1204
1321
|
self._head: tuple[int, int] | None = None
|
|
1205
1322
|
self._active = False
|
|
@@ -1221,6 +1338,7 @@ class LocalTextSelection:
|
|
|
1221
1338
|
self._press = None
|
|
1222
1339
|
self._route = None
|
|
1223
1340
|
self._rows = ()
|
|
1341
|
+
self._hyperlinks = ()
|
|
1224
1342
|
self._anchor = None
|
|
1225
1343
|
self._head = None
|
|
1226
1344
|
self._active = False
|
|
@@ -1240,8 +1358,9 @@ class LocalTextSelection:
|
|
|
1240
1358
|
return None
|
|
1241
1359
|
start = max(0, row - 7)
|
|
1242
1360
|
end = min(source.route.height, row + 8)
|
|
1361
|
+
rows, hyperlinks = _pane_rows(source, start=start, end=end)
|
|
1243
1362
|
return _target_in_rows(
|
|
1244
|
-
|
|
1363
|
+
rows,
|
|
1245
1364
|
row - start,
|
|
1246
1365
|
column,
|
|
1247
1366
|
route=replace(
|
|
@@ -1249,6 +1368,7 @@ class LocalTextSelection:
|
|
|
1249
1368
|
y=source.route.y + start,
|
|
1250
1369
|
height=end - start,
|
|
1251
1370
|
),
|
|
1371
|
+
hyperlinks=hyperlinks,
|
|
1252
1372
|
)
|
|
1253
1373
|
|
|
1254
1374
|
def hover(
|
|
@@ -1352,7 +1472,7 @@ class LocalTextSelection:
|
|
|
1352
1472
|
route = source.route
|
|
1353
1473
|
self._press = event
|
|
1354
1474
|
self._route = route
|
|
1355
|
-
self._rows =
|
|
1475
|
+
self._rows, self._hyperlinks = _pane_rows(source)
|
|
1356
1476
|
self._anchor = self._point(event, route)
|
|
1357
1477
|
self._head = self._anchor
|
|
1358
1478
|
self._active = False
|
|
@@ -1385,6 +1505,7 @@ class LocalTextSelection:
|
|
|
1385
1505
|
self._anchor[1],
|
|
1386
1506
|
self._anchor[0],
|
|
1387
1507
|
route=self._route,
|
|
1508
|
+
hyperlinks=self._hyperlinks,
|
|
1388
1509
|
)
|
|
1389
1510
|
if (
|
|
1390
1511
|
self._semantic_open
|
|
@@ -80,6 +80,7 @@ from railmux.fast_display_protocol import (
|
|
|
80
80
|
)
|
|
81
81
|
from railmux.fast_display_history import HistoryCaptureJob, HistoryCaptureWorker
|
|
82
82
|
from railmux.terminal_screen import (
|
|
83
|
+
HyperlinkTrackingStream,
|
|
83
84
|
Osc52ClipboardDecoder as _Osc52ClipboardDecoder,
|
|
84
85
|
ScreenState as _ScreenState,
|
|
85
86
|
build_screen_update,
|
|
@@ -1226,7 +1227,7 @@ def _render_history_lines(
|
|
|
1226
1227
|
result independently paintable for the local history overlay.
|
|
1227
1228
|
"""
|
|
1228
1229
|
screen = pyte.Screen(width, 1)
|
|
1229
|
-
stream = pyte.ByteStream(screen)
|
|
1230
|
+
stream = HyperlinkTrackingStream(screen, pyte.ByteStream(screen))
|
|
1230
1231
|
rendered: list[bytes] = []
|
|
1231
1232
|
for line in lines:
|
|
1232
1233
|
screen.buffer[0].clear()
|
|
@@ -2206,7 +2207,7 @@ def _serve_attached(
|
|
|
2206
2207
|
master_fd: int,
|
|
2207
2208
|
) -> int:
|
|
2208
2209
|
screen = pyte.DiffScreen(width, height)
|
|
2209
|
-
stream = pyte.ByteStream(screen)
|
|
2210
|
+
stream = HyperlinkTrackingStream(screen, pyte.ByteStream(screen))
|
|
2210
2211
|
input_decoder = InputFrameDecoder()
|
|
2211
2212
|
stdin_fd = sys.stdin.buffer.fileno()
|
|
2212
2213
|
stdout_fd = sys.stdout.buffer.fileno()
|
|
@@ -14,6 +14,8 @@ import re
|
|
|
14
14
|
from dataclasses import dataclass
|
|
15
15
|
from functools import lru_cache
|
|
16
16
|
from types import SimpleNamespace
|
|
17
|
+
from typing import NamedTuple
|
|
18
|
+
from urllib.parse import urlsplit
|
|
17
19
|
|
|
18
20
|
from railmux.fast_display_protocol import (
|
|
19
21
|
MAX_CLIPBOARD_BYTES,
|
|
@@ -65,6 +67,121 @@ _ANSI_BG = {
|
|
|
65
67
|
}
|
|
66
68
|
_OSC52_PREFIX = b"\033]52;"
|
|
67
69
|
_OSC52_MAX_ENCODED = ((MAX_CLIPBOARD_BYTES + 2) // 3) * 4
|
|
70
|
+
_OSC8_PREFIX = b"\033]8;"
|
|
71
|
+
_OSC8_MAX_BYTES = 16 * 1024
|
|
72
|
+
_MAX_SCREEN_HYPERLINKS = 256
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _HyperlinkChar(NamedTuple):
|
|
76
|
+
"""A pyte-compatible cell with one bounded OSC 8 target."""
|
|
77
|
+
|
|
78
|
+
data: str
|
|
79
|
+
fg: str = "default"
|
|
80
|
+
bg: str = "default"
|
|
81
|
+
bold: bool = False
|
|
82
|
+
italics: bool = False
|
|
83
|
+
underscore: bool = False
|
|
84
|
+
strikethrough: bool = False
|
|
85
|
+
reverse: bool = False
|
|
86
|
+
blink: bool = False
|
|
87
|
+
hyperlink: str | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _hyperlink_char(char: object, hyperlink: str | None) -> _HyperlinkChar:
|
|
91
|
+
return _HyperlinkChar(
|
|
92
|
+
data=char.data,
|
|
93
|
+
fg=char.fg,
|
|
94
|
+
bg=char.bg,
|
|
95
|
+
bold=char.bold,
|
|
96
|
+
italics=char.italics,
|
|
97
|
+
underscore=char.underscore,
|
|
98
|
+
strikethrough=char.strikethrough,
|
|
99
|
+
reverse=char.reverse,
|
|
100
|
+
blink=char.blink,
|
|
101
|
+
hyperlink=hyperlink,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _safe_hyperlink(value: bytes) -> str | None:
|
|
106
|
+
if not value or len(value) > 8192:
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
decoded = value.decode("utf-8")
|
|
110
|
+
except UnicodeDecodeError:
|
|
111
|
+
return None
|
|
112
|
+
if any(ord(character) < 0x20 or character == "\x7f" for character in decoded):
|
|
113
|
+
return None
|
|
114
|
+
try:
|
|
115
|
+
parsed = urlsplit(decoded)
|
|
116
|
+
hostname = parsed.hostname
|
|
117
|
+
except ValueError:
|
|
118
|
+
return None
|
|
119
|
+
if parsed.scheme.lower() not in ("http", "https") or hostname is None:
|
|
120
|
+
return None
|
|
121
|
+
return decoded
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class HyperlinkTrackingStream:
|
|
125
|
+
"""Preserve bounded HTTP(S) OSC 8 targets in pyte screen cells.
|
|
126
|
+
|
|
127
|
+
pyte intentionally ignores OSC 8. Railmux needs the target independently
|
|
128
|
+
from its often-wrapped display label, so this wrapper consumes only OSC 8,
|
|
129
|
+
updates the extended screen's cell attribute, and passes every other byte
|
|
130
|
+
through unchanged. The parser is incremental and bounded because PTY
|
|
131
|
+
reads may split either terminator.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, screen: object, stream: object) -> None:
|
|
135
|
+
self.screen = screen
|
|
136
|
+
self.stream = stream
|
|
137
|
+
self._buffer = bytearray()
|
|
138
|
+
|
|
139
|
+
def _feed_visible(self, data: bytes) -> None:
|
|
140
|
+
if data:
|
|
141
|
+
self.stream.feed(data)
|
|
142
|
+
|
|
143
|
+
def feed(self, data: bytes) -> None:
|
|
144
|
+
if not data:
|
|
145
|
+
return
|
|
146
|
+
self._buffer.extend(data)
|
|
147
|
+
while self._buffer:
|
|
148
|
+
start = self._buffer.find(_OSC8_PREFIX)
|
|
149
|
+
if start < 0:
|
|
150
|
+
keep = 0
|
|
151
|
+
maximum = min(len(self._buffer), len(_OSC8_PREFIX) - 1)
|
|
152
|
+
for length in range(maximum, 0, -1):
|
|
153
|
+
if _OSC8_PREFIX.startswith(self._buffer[-length:]):
|
|
154
|
+
keep = length
|
|
155
|
+
break
|
|
156
|
+
visible_end = len(self._buffer) - keep
|
|
157
|
+
self._feed_visible(bytes(self._buffer[:visible_end]))
|
|
158
|
+
del self._buffer[:visible_end]
|
|
159
|
+
return
|
|
160
|
+
if start:
|
|
161
|
+
self._feed_visible(bytes(self._buffer[:start]))
|
|
162
|
+
del self._buffer[:start]
|
|
163
|
+
bel = self._buffer.find(b"\007", len(_OSC8_PREFIX))
|
|
164
|
+
st = self._buffer.find(b"\033\\", len(_OSC8_PREFIX))
|
|
165
|
+
endings = tuple(
|
|
166
|
+
(position, length)
|
|
167
|
+
for position, length in ((bel, 1), (st, 2))
|
|
168
|
+
if position >= 0
|
|
169
|
+
)
|
|
170
|
+
if not endings:
|
|
171
|
+
if len(self._buffer) <= _OSC8_MAX_BYTES:
|
|
172
|
+
return
|
|
173
|
+
# An unterminated lookalike is ordinary terminal output. Feed
|
|
174
|
+
# one byte and resume scanning without retaining an unbounded
|
|
175
|
+
# attacker-controlled prefix.
|
|
176
|
+
self._feed_visible(bytes(self._buffer[:1]))
|
|
177
|
+
del self._buffer[:1]
|
|
178
|
+
continue
|
|
179
|
+
end, terminator_length = min(endings)
|
|
180
|
+
body = bytes(self._buffer[len(_OSC8_PREFIX) : end])
|
|
181
|
+
del self._buffer[: end + terminator_length]
|
|
182
|
+
_params, separator, target = body.partition(b";")
|
|
183
|
+
hyperlink = _safe_hyperlink(target) if separator else None
|
|
184
|
+
self.screen.set_hyperlink(hyperlink)
|
|
68
185
|
|
|
69
186
|
|
|
70
187
|
class Osc52ClipboardDecoder:
|
|
@@ -123,9 +240,29 @@ class _ExtendedScreenMixin:
|
|
|
123
240
|
_last_graphic_character = ""
|
|
124
241
|
_character_width = staticmethod(lambda _character: 1)
|
|
125
242
|
|
|
243
|
+
@property
|
|
244
|
+
def default_char(self) -> _HyperlinkChar:
|
|
245
|
+
return _hyperlink_char(super().default_char, None)
|
|
246
|
+
|
|
126
247
|
def reset(self) -> None:
|
|
127
248
|
self._last_graphic_character = ""
|
|
249
|
+
self._active_hyperlink: str | None = None
|
|
250
|
+
self._known_hyperlinks: dict[str, str] = {}
|
|
128
251
|
super().reset()
|
|
252
|
+
self.cursor.attrs = _hyperlink_char(self.cursor.attrs, None)
|
|
253
|
+
|
|
254
|
+
def set_hyperlink(self, hyperlink: str | None) -> None:
|
|
255
|
+
if hyperlink is not None:
|
|
256
|
+
known = self._known_hyperlinks.get(hyperlink)
|
|
257
|
+
if known is None:
|
|
258
|
+
if len(self._known_hyperlinks) >= _MAX_SCREEN_HYPERLINKS:
|
|
259
|
+
hyperlink = None
|
|
260
|
+
else:
|
|
261
|
+
self._known_hyperlinks[hyperlink] = hyperlink
|
|
262
|
+
else:
|
|
263
|
+
hyperlink = known
|
|
264
|
+
self._active_hyperlink = hyperlink
|
|
265
|
+
self.cursor.attrs = _hyperlink_char(self.cursor.attrs, hyperlink)
|
|
129
266
|
|
|
130
267
|
def draw(self, data: str) -> None:
|
|
131
268
|
super().draw(data)
|
|
@@ -174,6 +311,10 @@ class _ExtendedScreenMixin:
|
|
|
174
311
|
def select_graphic_rendition(self, *attrs: int) -> None:
|
|
175
312
|
"""Retain 256-colour indices instead of baking in pyte's palette."""
|
|
176
313
|
super().select_graphic_rendition(*attrs)
|
|
314
|
+
self.cursor.attrs = _hyperlink_char(
|
|
315
|
+
self.cursor.attrs,
|
|
316
|
+
self._active_hyperlink,
|
|
317
|
+
)
|
|
177
318
|
values = list(attrs)
|
|
178
319
|
indexed: dict[str, int] = {}
|
|
179
320
|
index = 0
|
|
@@ -209,6 +350,14 @@ class _ExtendedScreenMixin:
|
|
|
209
350
|
**{key: f"ansi256:{colour}" for key, colour in indexed.items()}
|
|
210
351
|
)
|
|
211
352
|
|
|
353
|
+
def restore_cursor(self) -> None:
|
|
354
|
+
super().restore_cursor()
|
|
355
|
+
# OSC 8 state is not part of DECSC/DECRC cursor attributes.
|
|
356
|
+
self.cursor.attrs = _hyperlink_char(
|
|
357
|
+
self.cursor.attrs,
|
|
358
|
+
self._active_hyperlink,
|
|
359
|
+
)
|
|
360
|
+
|
|
212
361
|
|
|
213
362
|
@lru_cache(maxsize=4)
|
|
214
363
|
def _build_extended_pyte(pyte: object) -> object:
|
|
@@ -333,6 +482,7 @@ def render_rows(screen: object) -> tuple[bytes, ...]:
|
|
|
333
482
|
for row_index in range(screen.lines):
|
|
334
483
|
rendered = [b"\033[0m"]
|
|
335
484
|
previous_style: tuple[object, ...] | None = None
|
|
485
|
+
previous_hyperlink: str | None = None
|
|
336
486
|
row = screen.buffer[row_index]
|
|
337
487
|
rendered_columns = _last_rendered_column(row, screen.columns)
|
|
338
488
|
continuation_cells = 0
|
|
@@ -346,6 +496,17 @@ def render_rows(screen: object) -> tuple[bytes, ...]:
|
|
|
346
496
|
continuation_cells = 0
|
|
347
497
|
continuation_data = ""
|
|
348
498
|
char = row[column]
|
|
499
|
+
hyperlink = getattr(char, "hyperlink", None)
|
|
500
|
+
if hyperlink != previous_hyperlink:
|
|
501
|
+
if previous_hyperlink is not None:
|
|
502
|
+
rendered.append(b"\033]8;;\033\\")
|
|
503
|
+
if hyperlink is not None:
|
|
504
|
+
rendered.append(
|
|
505
|
+
b"\033]8;;"
|
|
506
|
+
+ hyperlink.encode("utf-8", errors="strict")
|
|
507
|
+
+ b"\033\\"
|
|
508
|
+
)
|
|
509
|
+
previous_hyperlink = hyperlink
|
|
349
510
|
style = _style_key(char)
|
|
350
511
|
if style != previous_style:
|
|
351
512
|
rendered.append(_style(char))
|
|
@@ -366,6 +527,8 @@ def render_rows(screen: object) -> tuple[bytes, ...]:
|
|
|
366
527
|
)
|
|
367
528
|
continuation_cells = max(0, cell_width - 1)
|
|
368
529
|
continuation_data = char.data
|
|
530
|
+
if previous_hyperlink is not None:
|
|
531
|
+
rendered.append(b"\033]8;;\033\\")
|
|
369
532
|
rendered.append(b"\033[0m")
|
|
370
533
|
rendered_rows.append(b"".join(rendered))
|
|
371
534
|
return tuple(rendered_rows)
|
|
@@ -465,7 +628,10 @@ class ScreenProducer:
|
|
|
465
628
|
self.width = width
|
|
466
629
|
self.height = height
|
|
467
630
|
self.screen = self.pyte.DiffScreen(width, height)
|
|
468
|
-
self.stream =
|
|
631
|
+
self.stream = HyperlinkTrackingStream(
|
|
632
|
+
self.screen,
|
|
633
|
+
self.pyte.ByteStream(self.screen),
|
|
634
|
+
)
|
|
469
635
|
self._clipboard = Osc52ClipboardDecoder()
|
|
470
636
|
self._clipboard_ready: list[bytes] = []
|
|
471
637
|
self.delivered: ScreenState | None = None
|
|
@@ -2046,6 +2046,9 @@ _ROOT_TERMUX_TAP_MARKER = "railmux-termux-tap-v1"
|
|
|
2046
2046
|
STATUS_ACTION_MODE = "railmux-mode"
|
|
2047
2047
|
STATUS_ACTION_LAYOUT = "railmux-layout"
|
|
2048
2048
|
STATUS_ACTION_COPY = "railmux-copy"
|
|
2049
|
+
STATUS_ACTION_PAGE_SIDEBAR = "railmux-page-r"
|
|
2050
|
+
STATUS_ACTION_PAGE_PRIMARY = "railmux-page-1"
|
|
2051
|
+
STATUS_ACTION_PAGE_SECONDARY = "railmux-page-2"
|
|
2049
2052
|
_ROOT_STATUS_ACTION_KEYS = {
|
|
2050
2053
|
STATUS_ACTION_MODE: "MouseDown1Control0",
|
|
2051
2054
|
STATUS_ACTION_LAYOUT: "MouseDown1Control1",
|
|
@@ -2059,6 +2062,9 @@ _STATUS_ACTIONS = frozenset({
|
|
|
2059
2062
|
STATUS_ACTION_MODE,
|
|
2060
2063
|
STATUS_ACTION_LAYOUT,
|
|
2061
2064
|
STATUS_ACTION_COPY,
|
|
2065
|
+
STATUS_ACTION_PAGE_SIDEBAR,
|
|
2066
|
+
STATUS_ACTION_PAGE_PRIMARY,
|
|
2067
|
+
STATUS_ACTION_PAGE_SECONDARY,
|
|
2062
2068
|
})
|
|
2063
2069
|
_PREFIX_TARGET_KEY = "Tab"
|
|
2064
2070
|
_PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
|
|
@@ -2085,6 +2091,15 @@ RAILMUX_HISTORY_RESTORE_KEYS = {
|
|
|
2085
2091
|
"primary": ("F23", "\x1b[42~"),
|
|
2086
2092
|
"secondary": ("F24", "\x1b[43~"),
|
|
2087
2093
|
}
|
|
2094
|
+
# Compact status clicks must reach the controller before pane focus changes so
|
|
2095
|
+
# it can park the visible provider, switch the zoomed page, and resume the new
|
|
2096
|
+
# provider as one transaction. tmux names keys only through F12, therefore
|
|
2097
|
+
# these private routes use canonical xterm sequences in literal mode.
|
|
2098
|
+
RAILMUX_COMPACT_PAGE_KEYS = {
|
|
2099
|
+
STATUS_ACTION_PAGE_SIDEBAR: ("F13", "\x1b[25~"),
|
|
2100
|
+
STATUS_ACTION_PAGE_PRIMARY: ("F14", "\x1b[26~"),
|
|
2101
|
+
STATUS_ACTION_PAGE_SECONDARY: ("F15", "\x1b[28~"),
|
|
2102
|
+
}
|
|
2088
2103
|
RAILMUX_SELECTION_KEY_OPTION = "@railmux_selection_key"
|
|
2089
2104
|
RAILMUX_SELECTION_PEER_OPTION = "@railmux_selection_peer"
|
|
2090
2105
|
RAILMUX_SELECTION_FROZEN_OPTION = "@railmux_selection_frozen_by"
|
|
@@ -2124,7 +2139,9 @@ def status_pane_range(pane_id: str, content: str) -> str:
|
|
|
2124
2139
|
# A pane range sets ``mouse_status_range`` to only the literal ``pane``;
|
|
2125
2140
|
# ``=`` still resolves to the pane geometrically below the status cell,
|
|
2126
2141
|
# not to X. A user range preserves the strict pane ID as its argument, so
|
|
2127
|
-
# the managed binding can select
|
|
2142
|
+
# the managed binding can select a pane-targeted status control explicitly.
|
|
2143
|
+
# Compact page navigation uses controller actions instead so parking,
|
|
2144
|
+
# resuming, and zooming remain one ordered transaction.
|
|
2128
2145
|
#
|
|
2129
2146
|
# Status strings also pass through tmux's strftime expansion. Escape the
|
|
2130
2147
|
# pane-ID percent sign so IDs such as ``%76`` are not parsed as padding
|
|
@@ -2149,8 +2166,8 @@ def status_action_range(action: str, content: str) -> str:
|
|
|
2149
2166
|
# keys avoids routing fixed actions through the generic user-range
|
|
2150
2167
|
# dispatcher and provides deterministic bindings on the 3.7b version
|
|
2151
2168
|
# bundled by the managed Windows runtime.
|
|
2152
|
-
|
|
2153
|
-
|
|
2169
|
+
key = _ROOT_STATUS_ACTION_KEYS.get(action)
|
|
2170
|
+
if tmux_version() >= (3, 7) and key is not None:
|
|
2154
2171
|
index = key.removeprefix("MouseDown1Control")
|
|
2155
2172
|
return f"#[range=control|{index}]{content}#[norange]"
|
|
2156
2173
|
return f"#[range=user|{action}]{content}#[norange]"
|
|
@@ -3079,7 +3096,7 @@ def set_root_status_click_forwarding(
|
|
|
3079
3096
|
marker = f"{_ROOT_STATUS_CLICK_MARKER}-{token}"
|
|
3080
3097
|
condition = (
|
|
3081
3098
|
"#{&&:"
|
|
3082
|
-
"#{m/r:^(%[0-9]+|railmux-(mode|layout|copy))$,"
|
|
3099
|
+
"#{m/r:^(%[0-9]+|railmux-(mode|layout|copy|page-[r12]))$,"
|
|
3083
3100
|
"#{mouse_status_range}},"
|
|
3084
3101
|
"#{!=:#{@railmux_controller_pane},},"
|
|
3085
3102
|
f"#{{==:{marker},{marker}}}}}"
|
|
@@ -3097,6 +3114,18 @@ def set_root_status_click_forwarding(
|
|
|
3097
3114
|
f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' F7 ;; '
|
|
3098
3115
|
f'{STATUS_ACTION_COPY}) {_tmux_shell_executable()} send-keys -t '
|
|
3099
3116
|
f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' F6 ;; '
|
|
3117
|
+
f'{STATUS_ACTION_PAGE_SIDEBAR}) '
|
|
3118
|
+
f'{_tmux_shell_executable()} send-keys -l -t '
|
|
3119
|
+
f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' -- '
|
|
3120
|
+
f'{shlex.quote(RAILMUX_COMPACT_PAGE_KEYS[STATUS_ACTION_PAGE_SIDEBAR][1])} ;; '
|
|
3121
|
+
f'{STATUS_ACTION_PAGE_PRIMARY}) '
|
|
3122
|
+
f'{_tmux_shell_executable()} send-keys -l -t '
|
|
3123
|
+
f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' -- '
|
|
3124
|
+
f'{shlex.quote(RAILMUX_COMPACT_PAGE_KEYS[STATUS_ACTION_PAGE_PRIMARY][1])} ;; '
|
|
3125
|
+
f'{STATUS_ACTION_PAGE_SECONDARY}) '
|
|
3126
|
+
f'{_tmux_shell_executable()} send-keys -l -t '
|
|
3127
|
+
f'\'#{{{RAILMUX_CONTROLLER_OPTION}}}\' -- '
|
|
3128
|
+
f'{shlex.quote(RAILMUX_COMPACT_PAGE_KEYS[STATUS_ACTION_PAGE_SECONDARY][1])} ;; '
|
|
3100
3129
|
'esac"'
|
|
3101
3130
|
)
|
|
3102
3131
|
subprocess.check_call(
|