railmux 0.4.1.dev5__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.dev5/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.dev5 → railmux-0.4.1.dev6}/src/railmux/fast_display_input.py +149 -28
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/fast_display_server.py +3 -2
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/terminal_screen.py +167 -1
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_fast_display_selection.py +60 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_fast_display_server.py +64 -0
- railmux-0.4.1.dev5/src/railmux/__init__.py +0 -1
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/LICENSE +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/MANIFEST.in +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/README.md +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/pyproject.toml +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/setup.cfg +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/__main__.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/background_index.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/cli.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/config.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/diagnostic_contract.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/discovery.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/favorites.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/launcher.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/local_open.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/models.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/modes.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/preview_pager.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/project_favorites.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/release_version.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/renames.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/self_update.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/session_index.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/settings.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ssh_preflight.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/tmux_capabilities.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/transcript.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/app_test_harness.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_app_codex_placeholder.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_app_quit.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_app_restart_restore.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_app_running_recovery.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_background_index.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_cli.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_code_map.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_codex_index.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_config.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_config_cli.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_discovery.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_display_transport.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_fast_display_bootstrap.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_fast_display_history.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_fast_display_protocol.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_fast_display_terminal.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_help_agent.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_keymap.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_launcher.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_local_open.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_modals.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_modes.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_new_project.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_options.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_path_codec.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_preview_pager.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_project_favorites.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_release_notes.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_release_version.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_remote_config.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_renames.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_restart_state.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_running_pane.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_running_sort.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_self_update.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_session_cache.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_session_index.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_session_lease.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_settings.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_sidebar.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_status.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_statusbar.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_system_deps.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_tmux_capabilities.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_transcript.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_widgets.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_synchronized_screen.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tools/release_notes.py +0 -0
- {railmux-0.4.1.dev5 → railmux-0.4.1.dev6}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.1.dev5 → 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
|
|
@@ -717,6 +717,66 @@ def test_local_text_selection_joins_url_across_codex_ran_decoration():
|
|
|
717
717
|
assert action.open_target.value == expected
|
|
718
718
|
|
|
719
719
|
|
|
720
|
+
def test_local_text_selection_uses_exact_osc8_target_across_table_rows():
|
|
721
|
+
target = "https://jirasw.nvidia.com/browse/TRT-28431"
|
|
722
|
+
|
|
723
|
+
def linked(label: str) -> bytes:
|
|
724
|
+
return (
|
|
725
|
+
b"\033]8;;"
|
|
726
|
+
+ target.encode()
|
|
727
|
+
+ b"\033\\"
|
|
728
|
+
+ label.encode()
|
|
729
|
+
+ b"\033]8;;\033\\"
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
visible = ("TRT-28431", "https://", "jirasw.nvidia.co", "m/browse/TRT-28431")
|
|
733
|
+
width = max(len(value) for value in visible)
|
|
734
|
+
route = HistorySnapshot(1, "%8", 7, 3, width, len(visible))
|
|
735
|
+
source = SelectionSource(
|
|
736
|
+
route,
|
|
737
|
+
tuple(linked(value) for value in visible),
|
|
738
|
+
0,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
for row, label in enumerate(visible):
|
|
742
|
+
selection = LocalTextSelection()
|
|
743
|
+
event_row = route.y + row + 1
|
|
744
|
+
selection.pointer_event(
|
|
745
|
+
SgrMouseEvent(b"down", 0, route.x + 2, event_row, True),
|
|
746
|
+
source,
|
|
747
|
+
)
|
|
748
|
+
action = selection.pointer_event(
|
|
749
|
+
SgrMouseEvent(b"up", 0, route.x + 2, event_row, False),
|
|
750
|
+
source,
|
|
751
|
+
)
|
|
752
|
+
assert action.open_target is not None, label
|
|
753
|
+
assert action.open_target.kind == "url"
|
|
754
|
+
assert action.open_target.value == target
|
|
755
|
+
assert action.open_target.highlight_segments == tuple(
|
|
756
|
+
(route.y + index, route.x, value.encode())
|
|
757
|
+
for index, value in enumerate(visible)
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def test_local_text_selection_does_not_trust_non_http_osc8_target():
|
|
762
|
+
route = HistorySnapshot(1, "%8", 0, 0, 24, 1)
|
|
763
|
+
source = SelectionSource(
|
|
764
|
+
route,
|
|
765
|
+
(b"\033]8;;file:///tmp/report\033\\open report\033]8;;\033\\",),
|
|
766
|
+
0,
|
|
767
|
+
)
|
|
768
|
+
selection = LocalTextSelection()
|
|
769
|
+
|
|
770
|
+
selection.pointer_event(SgrMouseEvent(b"down", 0, 2, 1, True), source)
|
|
771
|
+
action = selection.pointer_event(
|
|
772
|
+
SgrMouseEvent(b"up", 0, 2, 1, False),
|
|
773
|
+
source,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
assert action.open_target is None
|
|
777
|
+
assert action.replay_events
|
|
778
|
+
|
|
779
|
+
|
|
720
780
|
def test_local_text_selection_joins_path_across_codex_ran_decoration():
|
|
721
781
|
first = "• Ran /tmp/railmux-published-0.4.1.dev1-"
|
|
722
782
|
second = " │ 20260816/bin/pip --isolated"
|
|
@@ -1900,6 +1900,70 @@ def test_server_renderer_keeps_visible_styled_trailing_blanks():
|
|
|
1900
1900
|
assert b";41m " in rendered
|
|
1901
1901
|
|
|
1902
1902
|
|
|
1903
|
+
def test_terminal_model_preserves_fragmented_http_osc8_targets_in_rows():
|
|
1904
|
+
pyte = pytest.importorskip("pyte")
|
|
1905
|
+
terminal = fast_display_server._extended_pyte(pyte)
|
|
1906
|
+
screen = terminal.Screen(24, 2)
|
|
1907
|
+
stream = fast_display_server.HyperlinkTrackingStream(
|
|
1908
|
+
screen,
|
|
1909
|
+
terminal.ByteStream(screen),
|
|
1910
|
+
)
|
|
1911
|
+
target = b"https://jirasw.nvidia.com/browse/TRT-28431"
|
|
1912
|
+
|
|
1913
|
+
for chunk in (
|
|
1914
|
+
b"\033]8;i",
|
|
1915
|
+
b"d=issue;" + target[:20],
|
|
1916
|
+
target[20:] + b"\033",
|
|
1917
|
+
b"\\TRT-28431\033]8;;\033\\",
|
|
1918
|
+
):
|
|
1919
|
+
stream.feed(chunk)
|
|
1920
|
+
|
|
1921
|
+
rendered = render_rows(screen)[0]
|
|
1922
|
+
assert b"TRT-28431" in rendered
|
|
1923
|
+
assert b"\033]8;;" + target + b"\033\\" in rendered
|
|
1924
|
+
assert screen.buffer[0][0].hyperlink == target.decode()
|
|
1925
|
+
assert screen.buffer[0][8].hyperlink == target.decode()
|
|
1926
|
+
assert screen.buffer[0][9].hyperlink is None
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
def test_terminal_model_drops_unsafe_or_stale_osc8_targets():
|
|
1930
|
+
pyte = pytest.importorskip("pyte")
|
|
1931
|
+
terminal = fast_display_server._extended_pyte(pyte)
|
|
1932
|
+
screen = terminal.Screen(8, 1)
|
|
1933
|
+
stream = fast_display_server.HyperlinkTrackingStream(
|
|
1934
|
+
screen,
|
|
1935
|
+
terminal.ByteStream(screen),
|
|
1936
|
+
)
|
|
1937
|
+
|
|
1938
|
+
stream.feed(b"\033]8;;https://safe.test\007ok\033]8;;\007")
|
|
1939
|
+
stream.feed(b"\033[1Gplain")
|
|
1940
|
+
stream.feed(b"\033]8;;file:///tmp/nope\007x\033]8;;\007")
|
|
1941
|
+
|
|
1942
|
+
rendered = render_rows(screen)[0]
|
|
1943
|
+
assert b"https://safe.test" not in rendered
|
|
1944
|
+
assert b"file:///tmp/nope" not in rendered
|
|
1945
|
+
assert all(screen.buffer[0][column].hyperlink is None for column in range(6))
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
def test_terminal_model_bounds_distinct_live_osc8_targets():
|
|
1949
|
+
pyte = pytest.importorskip("pyte")
|
|
1950
|
+
terminal = fast_display_server._extended_pyte(pyte)
|
|
1951
|
+
screen = terminal.Screen(300, 1)
|
|
1952
|
+
stream = fast_display_server.HyperlinkTrackingStream(
|
|
1953
|
+
screen,
|
|
1954
|
+
terminal.ByteStream(screen),
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
for index in range(257):
|
|
1958
|
+
stream.feed(
|
|
1959
|
+
f"\033]8;;https://example.test/{index}\007x\033]8;;\007".encode()
|
|
1960
|
+
)
|
|
1961
|
+
|
|
1962
|
+
assert len(screen._known_hyperlinks) == 256
|
|
1963
|
+
assert screen.buffer[0][255].hyperlink == "https://example.test/255"
|
|
1964
|
+
assert screen.buffer[0][256].hyperlink is None
|
|
1965
|
+
|
|
1966
|
+
|
|
1903
1967
|
def test_server_renderer_collapses_reported_repeated_cjk_physical_cells():
|
|
1904
1968
|
text = "基本通了,但发现一个真 bug:"
|
|
1905
1969
|
cells = [
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.1.dev5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|