railmux 0.3.2.dev5__tar.gz → 0.4.0.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {railmux-0.3.2.dev5/src/railmux.egg-info → railmux-0.4.0.dev1}/PKG-INFO +1 -1
- railmux-0.4.0.dev1/src/railmux/__init__.py +1 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tmux_ctl.py +3 -1
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/backend.py +24 -7
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/client.py +25 -1
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/daemon.py +42 -3
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_release_workflow.py +14 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_ctl.py +9 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_integration.py +9 -9
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_backend.py +42 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_compositor.py +19 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_daemon.py +55 -2
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_real_conpty.py +33 -0
- railmux-0.3.2.dev5/src/railmux/__init__.py +0 -1
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/LICENSE +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/MANIFEST.in +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/README.md +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/pyproject.toml +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/setup.cfg +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/__main__.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/background_index.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/cli.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/config.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/discovery.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/favorites.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/launcher.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/local_open.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/models.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/modes.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/mux/__init__.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/mux/backend.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/mux/tmux_backend.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/__init__.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/config_paths.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/console.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/file_security.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/filelock.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/multiplex.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/process.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/runtime_paths.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/renames.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/self_update.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/session_index.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/settings.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/terminal_emulator.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/transcript.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/app.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/__init__.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/compositor.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/conpty.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/history_viewer.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/ipc.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/session_store.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/virtual_screen.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_background_index.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_cli.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_codex_index.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_config.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_config_cli.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_discovery.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_display_transport.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_fast_full_window.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_help_agent.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_keymap.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_launcher.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_local_open.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_modals.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_modes.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_mux_backend.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_new_project.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_options.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_path_codec.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_platform.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_release_notes.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_remote_config.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_renames.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_restart_state.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_running_pane.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_running_sort.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_self_update.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_session_cache.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_session_index.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_settings.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_sidebar.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_soft_quit.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_status.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_statusbar.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_system_deps.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_transcript.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_widgets.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_conpty.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_history_viewer.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_ipc.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_session_store.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_virtual_screen.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tools/release_notes.py +0 -0
- {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev1"
|
|
@@ -2125,7 +2125,9 @@ def _root_wheel_bindings_are_defaults(
|
|
|
2125
2125
|
return False
|
|
2126
2126
|
return (
|
|
2127
2127
|
"mouse_any_flag" in body
|
|
2128
|
-
|
|
2128
|
+
# tmux 3.2 serializes its stock command with the accepted ``send``
|
|
2129
|
+
# alias, while newer releases print the canonical ``send-keys`` name.
|
|
2130
|
+
and re.search(r"\bsend(?:-keys)?\s+-M\b", body) is not None
|
|
2129
2131
|
and "copy-mode -e" in body
|
|
2130
2132
|
and _ROOT_WHEEL_MARKER not in body
|
|
2131
2133
|
)
|
|
@@ -304,12 +304,26 @@ class WinMuxBackend:
|
|
|
304
304
|
)
|
|
305
305
|
|
|
306
306
|
def server_snapshot(self):
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
307
|
+
with self._lock:
|
|
308
|
+
live = [
|
|
309
|
+
session
|
|
310
|
+
for session in self._sessions.values()
|
|
311
|
+
if self.session_exists(session.name)
|
|
312
|
+
]
|
|
313
|
+
panes = frozenset(
|
|
314
|
+
pane_id
|
|
315
|
+
for pane_id in self._panes
|
|
316
|
+
if self.pane_alive(pane_id)
|
|
317
|
+
)
|
|
318
|
+
return tmux_ctl.ServerSnapshot(
|
|
319
|
+
frozenset(session.name for session in live),
|
|
320
|
+
panes,
|
|
321
|
+
tuple(
|
|
322
|
+
(session.name, session.process.pid)
|
|
323
|
+
for session in live
|
|
324
|
+
if session.process
|
|
325
|
+
),
|
|
326
|
+
)
|
|
313
327
|
|
|
314
328
|
def select_pane(self, pane_id: str) -> bool:
|
|
315
329
|
with self._lock:
|
|
@@ -891,7 +905,10 @@ class WinDisplayTransport:
|
|
|
891
905
|
return None
|
|
892
906
|
|
|
893
907
|
def fallback_for_external_client(self, *_args, **_kwargs):
|
|
894
|
-
|
|
908
|
+
# Native Windows has no external tmux client or nested-display
|
|
909
|
+
# fallback. ``App`` treats ``None`` as not applicable and otherwise
|
|
910
|
+
# requires an AttachOutcome-compatible object.
|
|
911
|
+
return None
|
|
895
912
|
|
|
896
913
|
def outer_session_lost(self) -> bool:
|
|
897
914
|
return False
|
|
@@ -28,7 +28,13 @@ from railmux.fast_display_protocol import (
|
|
|
28
28
|
encode_resize,
|
|
29
29
|
)
|
|
30
30
|
from railmux.platform.console import RawConsole
|
|
31
|
-
from railmux.winlocal.daemon import
|
|
31
|
+
from railmux.winlocal.daemon import (
|
|
32
|
+
AUTH_OK,
|
|
33
|
+
AUTH_PREFIX,
|
|
34
|
+
NATIVE_UI_FAILED,
|
|
35
|
+
endpoint_path,
|
|
36
|
+
ui_error_path,
|
|
37
|
+
)
|
|
32
38
|
from railmux.winlocal.ipc import Endpoint, read_endpoint, receive_message, send_message
|
|
33
39
|
|
|
34
40
|
|
|
@@ -38,6 +44,10 @@ _MAX_COLUMNS = 1000
|
|
|
38
44
|
_MAX_LINES = 500
|
|
39
45
|
|
|
40
46
|
|
|
47
|
+
class NativeUIError(RuntimeError):
|
|
48
|
+
"""The daemon's shared UI failed while provider ConPTYs stayed alive."""
|
|
49
|
+
|
|
50
|
+
|
|
41
51
|
def connect_endpoint(endpoint: Endpoint, *, timeout: float = 2.0) -> socket.socket:
|
|
42
52
|
sock = socket.create_connection(("127.0.0.1", endpoint.port), timeout=timeout)
|
|
43
53
|
try:
|
|
@@ -160,6 +170,8 @@ class NativeClient:
|
|
|
160
170
|
kind, payload = self._events.get(timeout=0.1)
|
|
161
171
|
except queue.Empty:
|
|
162
172
|
kind, payload = "tick", None
|
|
173
|
+
if kind == "ui_error":
|
|
174
|
+
raise NativeUIError
|
|
163
175
|
if kind == "closed":
|
|
164
176
|
return 2
|
|
165
177
|
if kind == "input":
|
|
@@ -203,6 +215,15 @@ class NativeClient:
|
|
|
203
215
|
except ValueError as exc:
|
|
204
216
|
print(f"error: {exc}", file=sys.stderr)
|
|
205
217
|
return 2
|
|
218
|
+
except NativeUIError:
|
|
219
|
+
# Leave the alternate screen before printing actionable feedback.
|
|
220
|
+
surface.close()
|
|
221
|
+
print(
|
|
222
|
+
"error: native Railmux UI stopped unexpectedly; provider "
|
|
223
|
+
f"sessions are still running. Retry railmux; details: {ui_error_path()}",
|
|
224
|
+
file=sys.stderr,
|
|
225
|
+
)
|
|
226
|
+
return 2
|
|
206
227
|
except (ConnectionError, OSError):
|
|
207
228
|
return 2
|
|
208
229
|
finally:
|
|
@@ -219,6 +240,9 @@ class NativeClient:
|
|
|
219
240
|
packet = receive_message(self.sock)
|
|
220
241
|
if packet is None:
|
|
221
242
|
break
|
|
243
|
+
if packet == NATIVE_UI_FAILED:
|
|
244
|
+
self._events.put(("ui_error", None))
|
|
245
|
+
return
|
|
222
246
|
self._events.put(("server", packet))
|
|
223
247
|
except (ConnectionError, OSError, ValueError):
|
|
224
248
|
pass
|
|
@@ -7,10 +7,12 @@ import socket
|
|
|
7
7
|
import struct
|
|
8
8
|
import threading
|
|
9
9
|
import time
|
|
10
|
+
import traceback
|
|
10
11
|
import uuid
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Callable
|
|
13
14
|
|
|
15
|
+
from railmux.atomic_file import atomic_write_text
|
|
14
16
|
from railmux.config import Config, ConfigError, load_config
|
|
15
17
|
from railmux.fast_display_protocol import (
|
|
16
18
|
InputFrameDecoder,
|
|
@@ -33,6 +35,8 @@ from railmux.winlocal.session_store import SessionStore
|
|
|
33
35
|
AUTH_PREFIX = b"AUTH "
|
|
34
36
|
AUTH_OK = b"OK"
|
|
35
37
|
AUTH_FAILED = b"ERROR authentication failed"
|
|
38
|
+
NATIVE_UI_FAILED = b"RAILMUX-NATIVE/1 UI_FAILED"
|
|
39
|
+
_UI_ERROR_LIMIT = 64 * 1024
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
def native_runtime_dir() -> Path:
|
|
@@ -47,6 +51,10 @@ def session_store_path() -> Path:
|
|
|
47
51
|
return native_runtime_dir() / "sessions.json"
|
|
48
52
|
|
|
49
53
|
|
|
54
|
+
def ui_error_path() -> Path:
|
|
55
|
+
return native_runtime_dir() / "native-ui-error.log"
|
|
56
|
+
|
|
57
|
+
|
|
50
58
|
class DaemonServer:
|
|
51
59
|
"""Authenticated loopback server around one daemon-owned backend."""
|
|
52
60
|
|
|
@@ -60,6 +68,7 @@ class DaemonServer:
|
|
|
60
68
|
fps: float = 20.0,
|
|
61
69
|
idle_timeout: float = 300.0,
|
|
62
70
|
app_factory: Callable[[], object] | None = None,
|
|
71
|
+
ui_error_file: Path | None = None,
|
|
63
72
|
) -> None:
|
|
64
73
|
self.backend = backend
|
|
65
74
|
self.endpoint_file = endpoint_file
|
|
@@ -69,6 +78,7 @@ class DaemonServer:
|
|
|
69
78
|
self.idle_timeout = max(1.0, idle_timeout)
|
|
70
79
|
self._last_nonidle = time.monotonic()
|
|
71
80
|
self.app_factory = app_factory
|
|
81
|
+
self.ui_error_file = ui_error_file or endpoint_file.parent / ui_error_path().name
|
|
72
82
|
self._listener: socket.socket | None = None
|
|
73
83
|
self._clients: set[socket.socket] = set()
|
|
74
84
|
self._clients_lock = threading.RLock()
|
|
@@ -194,14 +204,17 @@ class DaemonServer:
|
|
|
194
204
|
self._app_thread.start()
|
|
195
205
|
|
|
196
206
|
def _run_app(self) -> None:
|
|
207
|
+
failed = False
|
|
197
208
|
try:
|
|
198
209
|
assert self.app_factory is not None
|
|
199
210
|
app = self.app_factory()
|
|
200
211
|
run = getattr(app, "run")
|
|
201
212
|
run()
|
|
202
|
-
except Exception:
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
failed = True
|
|
203
215
|
# Keep the daemon/ConPTY authority alive so a later attachment can
|
|
204
216
|
# retry the UI without converting a display fault into session loss.
|
|
217
|
+
self._record_ui_failure(exc)
|
|
205
218
|
self.backend.set_status_text(
|
|
206
219
|
"Native UI stopped unexpectedly; reconnect to retry",
|
|
207
220
|
"error",
|
|
@@ -210,7 +223,28 @@ class DaemonServer:
|
|
|
210
223
|
# Urwid returning means hard quit, soft quit, or a failed UI. The
|
|
211
224
|
# daemon and any surviving ConPTYs remain authoritative, but every
|
|
212
225
|
# frontend must leave instead of displaying a frozen last frame.
|
|
213
|
-
self._detach_all_clients(
|
|
226
|
+
self._detach_all_clients(
|
|
227
|
+
final_message=NATIVE_UI_FAILED if failed else None
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _record_ui_failure(self, exc: BaseException) -> None:
|
|
231
|
+
"""Persist one bounded private traceback for the local user."""
|
|
232
|
+
detail = "".join(
|
|
233
|
+
traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
234
|
+
)
|
|
235
|
+
if len(detail) > _UI_ERROR_LIMIT:
|
|
236
|
+
detail = "[traceback truncated]\n" + detail[-_UI_ERROR_LIMIT:]
|
|
237
|
+
payload = (
|
|
238
|
+
"Railmux native UI failure\n"
|
|
239
|
+
f"recorded_at={int(time.time())}\n"
|
|
240
|
+
f"daemon_id={self.daemon_id}\n"
|
|
241
|
+
f"{detail}"
|
|
242
|
+
)
|
|
243
|
+
try:
|
|
244
|
+
ensure_private_dir(self.ui_error_file.parent)
|
|
245
|
+
atomic_write_text(self.ui_error_file, payload)
|
|
246
|
+
except OSError:
|
|
247
|
+
pass
|
|
214
248
|
|
|
215
249
|
def _render_loop(self) -> None:
|
|
216
250
|
interval = 1.0 / self.fps
|
|
@@ -249,11 +283,16 @@ class DaemonServer:
|
|
|
249
283
|
with self._clients_lock:
|
|
250
284
|
return len(self._clients)
|
|
251
285
|
|
|
252
|
-
def _detach_all_clients(self) -> None:
|
|
286
|
+
def _detach_all_clients(self, *, final_message: bytes | None = None) -> None:
|
|
253
287
|
with self._clients_lock:
|
|
254
288
|
clients = tuple(self._clients)
|
|
255
289
|
self._clients.clear()
|
|
256
290
|
for client in clients:
|
|
291
|
+
if final_message is not None:
|
|
292
|
+
try:
|
|
293
|
+
send_message(client, final_message)
|
|
294
|
+
except (ConnectionError, OSError, ValueError):
|
|
295
|
+
pass
|
|
257
296
|
try:
|
|
258
297
|
client.shutdown(socket.SHUT_RDWR)
|
|
259
298
|
except OSError:
|
|
@@ -50,3 +50,17 @@ def test_development_tag_creates_a_github_prerelease():
|
|
|
50
50
|
assert 'if [[ "$GITHUB_REF_NAME" == *.dev* ]]' in release
|
|
51
51
|
assert "prerelease+=(--prerelease)" in release
|
|
52
52
|
assert '"${prerelease[@]}"' in release
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_release_tags_are_fenced_to_their_product_branches():
|
|
56
|
+
release = (ROOT / ".github/workflows/release.yml").read_text()
|
|
57
|
+
|
|
58
|
+
assert "fetch-depth: 0" in release
|
|
59
|
+
assert "origin/main" in release
|
|
60
|
+
assert "origin/windows-preview" in release
|
|
61
|
+
assert "POSIX/WSL development release from main" in release
|
|
62
|
+
assert "native-Windows development release from windows-preview" in release
|
|
63
|
+
assert "main development releases must use the 0.3.x.devN series" in release
|
|
64
|
+
assert "native-Windows releases must use the 0.4.0.devN series" in release
|
|
65
|
+
assert "development releases must come from main or windows-preview" in release
|
|
66
|
+
assert "final releases must come from main" in release
|
|
@@ -514,6 +514,15 @@ def test_root_wheel_forwarding_requires_stock_bindings_and_tmux_27():
|
|
|
514
514
|
return_value=backup):
|
|
515
515
|
assert tmux_ctl.prepare_root_wheel_bindings() == backup
|
|
516
516
|
|
|
517
|
+
tmux_32_backup = dict(
|
|
518
|
+
backup,
|
|
519
|
+
WheelUpPane=backup["WheelUpPane"].replace("send-keys -M", "send -M"),
|
|
520
|
+
)
|
|
521
|
+
with patch("railmux.tmux_ctl.tmux_version", return_value=(3, 2)), \
|
|
522
|
+
patch("railmux.tmux_ctl.read_root_wheel_bindings",
|
|
523
|
+
return_value=tmux_32_backup):
|
|
524
|
+
assert tmux_ctl.prepare_root_wheel_bindings() == tmux_32_backup
|
|
525
|
+
|
|
517
526
|
custom = dict(backup, WheelDownPane=(
|
|
518
527
|
"bind-key -T root WheelDownPane display-message custom"))
|
|
519
528
|
with patch("railmux.tmux_ctl.tmux_version", return_value=(3, 4)), \
|
|
@@ -1522,6 +1522,12 @@ def test_real_soft_quit_disconnects_shared_clients_and_agents_survive(
|
|
|
1522
1522
|
for directory in (socket_root, home, runtime, claude_home):
|
|
1523
1523
|
directory.mkdir(parents=True, exist_ok=True)
|
|
1524
1524
|
directory.chmod(0o700)
|
|
1525
|
+
config_dir = home / ".config" / "railmux"
|
|
1526
|
+
config_dir.mkdir(parents=True)
|
|
1527
|
+
(config_dir / "config.toml").write_text(
|
|
1528
|
+
'[updates]\nauto_update = "never"\n',
|
|
1529
|
+
encoding="utf-8",
|
|
1530
|
+
)
|
|
1525
1531
|
|
|
1526
1532
|
label = f"rx-soft-quit-{os.getpid()}"
|
|
1527
1533
|
monkeypatch.setenv("TMUX_TMPDIR", str(socket_root))
|
|
@@ -2617,15 +2623,9 @@ def test_real_tmux_binding_manager_round_trip_and_user_reload(
|
|
|
2617
2623
|
client_name = ""
|
|
2618
2624
|
|
|
2619
2625
|
def send_client_key(key: str) -> None:
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
check=True,
|
|
2624
|
-
)
|
|
2625
|
-
return
|
|
2626
|
-
# tmux 2.7 predates send-keys -K. Feed the corresponding xterm bytes
|
|
2627
|
-
# to the same attached client so the binding still executes through
|
|
2628
|
-
# the real terminal input path.
|
|
2626
|
+
# Feed xterm bytes to the attached client on every supported version.
|
|
2627
|
+
# ``send-keys -K`` is absent from tmux 3.2 even though later releases
|
|
2628
|
+
# provide it; the raw path also exercises the real terminal decoder.
|
|
2629
2629
|
sequence = {"F8": b"\x1b[19~", "C-b": b"\x02", "Tab": b"\t"}[key]
|
|
2630
2630
|
assert client_process.stdin is not None
|
|
2631
2631
|
client_process.stdin.write(sequence)
|
|
@@ -59,6 +59,48 @@ def test_windows_backend_starts_typed_provider_and_composes_display():
|
|
|
59
59
|
assert update.width == 120 and update.height == 30
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def test_windows_snapshot_keeps_attached_display_pane_alive():
|
|
63
|
+
backend = WinMuxBackend(process_factory=_factory)
|
|
64
|
+
launch = backend.prepare_launch(["codex", "resume", "abc"], Path("C:/repo"))
|
|
65
|
+
assert backend.new_detached_session("codex-abc", launch) == (True, None)
|
|
66
|
+
workspace = AgentWorkspace()
|
|
67
|
+
transport = backend.create_display_transport(workspace, "swap")
|
|
68
|
+
|
|
69
|
+
assert transport.attach(workspace.primary, "codex-abc").ok
|
|
70
|
+
assert workspace.primary.pane_id is not None
|
|
71
|
+
snapshot = backend.server_snapshot()
|
|
72
|
+
|
|
73
|
+
assert workspace.primary.pane_id in snapshot.panes
|
|
74
|
+
assert backend._sessions["codex-abc"].pane_id in snapshot.panes
|
|
75
|
+
assert transport.fallback_for_external_client(workspace.primary) is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_shared_reconciliation_preserves_native_attached_agent(tmp_path):
|
|
79
|
+
backend = WinMuxBackend(process_factory=_factory)
|
|
80
|
+
app = App(tmp_path, Config(), mux_backend=backend)
|
|
81
|
+
launch = backend.prepare_launch(["codex", "resume", "abc"], tmp_path)
|
|
82
|
+
assert backend.new_detached_session("codex-abc", launch) == (True, None)
|
|
83
|
+
session = backend._sessions["codex-abc"]
|
|
84
|
+
assert session.terminal is not None
|
|
85
|
+
session.terminal.feed(b"AGENT_PANEL_READY")
|
|
86
|
+
slot = app._agent_workspace().primary
|
|
87
|
+
assert app._display_transport().attach(slot, "codex-abc").ok
|
|
88
|
+
|
|
89
|
+
snapshot = backend.server_snapshot()
|
|
90
|
+
app._reconcile_display_slots(
|
|
91
|
+
lambda name: name in snapshot.sessions,
|
|
92
|
+
lambda pane_id: pane_id in snapshot.panes,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert slot.pane_id is not None
|
|
96
|
+
assert slot.agent_tmux_name == "codex-abc"
|
|
97
|
+
assert "primary" in backend._display_regions()
|
|
98
|
+
assert any(
|
|
99
|
+
b"AGENT_PANEL_READY" in row
|
|
100
|
+
for _index, row in backend.screen_update().rows
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
62
104
|
def test_windows_backend_preserves_unsuperseded_resume_offers(tmp_path):
|
|
63
105
|
store = SessionStore(tmp_path / "sessions.json", "new-daemon")
|
|
64
106
|
offer = SessionRecord(
|
|
@@ -46,3 +46,22 @@ def test_sidebar_is_full_width_without_agent_and_status_cannot_scroll_frame():
|
|
|
46
46
|
"sidebar"
|
|
47
47
|
].width == 40
|
|
48
48
|
assert b"Projects" in rows[0]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_compositor_preserves_cjk_agent_output_and_bottom_status():
|
|
52
|
+
sidebar = TerminalPane(20, 6)
|
|
53
|
+
primary = TerminalPane(20, 6)
|
|
54
|
+
sidebar.feed("会话列表".encode("utf-8"))
|
|
55
|
+
primary.feed("代理已恢复".encode("utf-8"))
|
|
56
|
+
compositor = Compositor(60, 12)
|
|
57
|
+
|
|
58
|
+
update = compositor.compose(
|
|
59
|
+
sidebar,
|
|
60
|
+
primary,
|
|
61
|
+
status="状态栏可见".encode("utf-8"),
|
|
62
|
+
)
|
|
63
|
+
rows = dict(update.rows)
|
|
64
|
+
|
|
65
|
+
assert "会话列表".encode("utf-8") in rows[0]
|
|
66
|
+
assert "代理已恢复".encode("utf-8") in rows[0]
|
|
67
|
+
assert "状态栏可见".encode("utf-8") in rows[11]
|
|
@@ -10,8 +10,8 @@ from railmux.fast_display_protocol import (
|
|
|
10
10
|
encode_resize,
|
|
11
11
|
)
|
|
12
12
|
from railmux.winlocal.backend import WinMuxBackend
|
|
13
|
-
from railmux.winlocal.client import connect_endpoint, ensure_daemon
|
|
14
|
-
from railmux.winlocal.daemon import AUTH_PREFIX, DaemonServer
|
|
13
|
+
from railmux.winlocal.client import NativeClient, connect_endpoint, ensure_daemon
|
|
14
|
+
from railmux.winlocal.daemon import AUTH_PREFIX, NATIVE_UI_FAILED, DaemonServer
|
|
15
15
|
from railmux.winlocal.ipc import read_endpoint, receive_message, send_message
|
|
16
16
|
|
|
17
17
|
|
|
@@ -132,6 +132,59 @@ def test_app_exit_disconnects_frontend_but_keeps_daemon_available(tmp_path):
|
|
|
132
132
|
thread.join(timeout=2)
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
def test_app_crash_reports_failure_and_writes_bounded_traceback(tmp_path):
|
|
136
|
+
class App:
|
|
137
|
+
def run(self):
|
|
138
|
+
raise RuntimeError("refresh contract failed")
|
|
139
|
+
|
|
140
|
+
error_file = tmp_path / "native-ui-error.log"
|
|
141
|
+
server = DaemonServer(
|
|
142
|
+
_backend(),
|
|
143
|
+
endpoint_file=tmp_path / "endpoint.json",
|
|
144
|
+
app_factory=App,
|
|
145
|
+
ui_error_file=error_file,
|
|
146
|
+
)
|
|
147
|
+
endpoint = server.start()
|
|
148
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
149
|
+
thread.start()
|
|
150
|
+
client = connect_endpoint(endpoint)
|
|
151
|
+
client.settimeout(2)
|
|
152
|
+
try:
|
|
153
|
+
messages = []
|
|
154
|
+
deadline = time.monotonic() + 2
|
|
155
|
+
while time.monotonic() < deadline:
|
|
156
|
+
message = receive_message(client)
|
|
157
|
+
if message is None:
|
|
158
|
+
break
|
|
159
|
+
messages.append(message)
|
|
160
|
+
if message == NATIVE_UI_FAILED:
|
|
161
|
+
break
|
|
162
|
+
assert NATIVE_UI_FAILED in messages
|
|
163
|
+
detail = error_file.read_text(encoding="utf-8")
|
|
164
|
+
assert "RuntimeError: refresh contract failed" in detail
|
|
165
|
+
assert f"daemon_id={server.daemon_id}" in detail
|
|
166
|
+
assert len(detail) <= 65 * 1024
|
|
167
|
+
finally:
|
|
168
|
+
client.close()
|
|
169
|
+
server.close()
|
|
170
|
+
thread.join(timeout=2)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_native_client_classifies_ui_failure_before_socket_close():
|
|
174
|
+
daemon, frontend = socket.socketpair()
|
|
175
|
+
client = NativeClient(frontend)
|
|
176
|
+
thread = threading.Thread(target=client._read_socket, daemon=True)
|
|
177
|
+
thread.start()
|
|
178
|
+
try:
|
|
179
|
+
send_message(daemon, NATIVE_UI_FAILED)
|
|
180
|
+
assert client._events.get(timeout=2) == ("ui_error", None)
|
|
181
|
+
finally:
|
|
182
|
+
client._stop.set()
|
|
183
|
+
daemon.close()
|
|
184
|
+
frontend.close()
|
|
185
|
+
thread.join(timeout=2)
|
|
186
|
+
|
|
187
|
+
|
|
135
188
|
def test_idle_daemon_without_clients_or_sessions_exits(tmp_path):
|
|
136
189
|
endpoint_file = tmp_path / "endpoint.json"
|
|
137
190
|
server = DaemonServer(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
import time
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -59,3 +60,35 @@ def test_real_conpty_launches_cmd_shim_from_path_with_spaces(tmp_path):
|
|
|
59
60
|
finally:
|
|
60
61
|
if process.is_alive():
|
|
61
62
|
process.terminate(force=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.skipif(os.name != "nt", reason="requires Windows ConPTY")
|
|
66
|
+
def test_real_conpty_normalizes_cp936_child_output_to_utf8(tmp_path):
|
|
67
|
+
shim = tmp_path / "cp936-provider.cmd"
|
|
68
|
+
shim.write_text(
|
|
69
|
+
"@echo off\r\n"
|
|
70
|
+
"chcp 936 >nul\r\n"
|
|
71
|
+
f'"{sys.executable}" -c "print(\'\\u4e2d\\u6587\')"\r\n',
|
|
72
|
+
encoding="ascii",
|
|
73
|
+
)
|
|
74
|
+
process = PyWinPtyProcess.spawn(
|
|
75
|
+
provider_argv(str(shim), (), windows=True),
|
|
76
|
+
cwd=tmp_path,
|
|
77
|
+
env=dict(os.environ),
|
|
78
|
+
columns=80,
|
|
79
|
+
rows=24,
|
|
80
|
+
)
|
|
81
|
+
output = bytearray()
|
|
82
|
+
expected = "中文".encode("utf-8")
|
|
83
|
+
deadline = time.monotonic() + 10
|
|
84
|
+
try:
|
|
85
|
+
while time.monotonic() < deadline and expected not in output:
|
|
86
|
+
try:
|
|
87
|
+
output.extend(process.read(65536))
|
|
88
|
+
except EOFError:
|
|
89
|
+
break
|
|
90
|
+
assert expected in output
|
|
91
|
+
output.decode("utf-8")
|
|
92
|
+
finally:
|
|
93
|
+
if process.is_alive():
|
|
94
|
+
process.terminate(force=True)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.2.dev5"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|