railmux 0.3.2.dev2__tar.gz → 0.3.2.dev4__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.dev2/src/railmux.egg-info → railmux-0.3.2.dev4}/PKG-INFO +1 -1
- railmux-0.3.2.dev4/src/railmux/__init__.py +1 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/diagnostics.py +4 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/discovery.py +2 -2
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_client.py +32 -22
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_input.py +5 -2
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/favorites.py +2 -2
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/legacy_sessions.py +2 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/mux/tmux_backend.py +2 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/renames.py +2 -2
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/runtime_config.py +4 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/self_update.py +2 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/session_index.py +1 -1
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/settings.py +1 -1
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tmux_ctl.py +14 -4
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tool_panes.py +2 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/app.py +6 -4
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_fast_full_window.py +50 -17
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_platform.py +106 -0
- railmux-0.3.2.dev2/src/railmux/__init__.py +0 -1
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/LICENSE +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/MANIFEST.in +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/README.md +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/pyproject.toml +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/setup.cfg +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/__main__.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/background_index.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/cli.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/config.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/launcher.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/local_open.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/models.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/modes.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/mux/__init__.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/mux/backend.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/__init__.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/config_paths.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/console.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/file_security.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/filelock.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/multiplex.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/process.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/platform/runtime_paths.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/terminal_emulator.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/transcript.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/__init__.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/backend.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/client.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/compositor.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/conpty.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/daemon.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/history_viewer.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/ipc.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/session_store.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux/winlocal/virtual_screen.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_background_index.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_cli.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_codex_index.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_config.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_config_cli.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_discovery.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_display_transport.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_help_agent.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_keymap.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_launcher.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_local_open.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_modals.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_modes.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_mux_backend.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_new_project.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_options.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_path_codec.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_release_notes.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_remote_config.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_renames.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_restart_state.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_running_pane.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_running_sort.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_self_update.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_session_cache.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_session_index.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_settings.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_sidebar.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_soft_quit.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_status.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_statusbar.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_system_deps.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_integration.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_transcript.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_widgets.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_backend.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_compositor.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_conpty.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_daemon.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_history_viewer.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_ipc.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_real_conpty.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_session_store.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_winlocal_virtual_screen.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tools/release_notes.py +0 -0
- {railmux-0.3.2.dev2 → railmux-0.3.2.dev4}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.2.dev4"
|
|
@@ -151,6 +151,8 @@ def _tool_diagnostic(
|
|
|
151
151
|
env=environ,
|
|
152
152
|
capture_output=True,
|
|
153
153
|
text=True,
|
|
154
|
+
encoding="utf-8",
|
|
155
|
+
errors="replace",
|
|
154
156
|
timeout=3,
|
|
155
157
|
check=False,
|
|
156
158
|
)
|
|
@@ -300,6 +302,8 @@ def collect_doctor_snapshot(
|
|
|
300
302
|
env=effective_env,
|
|
301
303
|
capture_output=True,
|
|
302
304
|
text=True,
|
|
305
|
+
encoding="utf-8",
|
|
306
|
+
errors="replace",
|
|
303
307
|
timeout=2,
|
|
304
308
|
check=False,
|
|
305
309
|
)
|
|
@@ -43,9 +43,9 @@ def _path_cache_file() -> Path:
|
|
|
43
43
|
|
|
44
44
|
def _load_path_cache() -> dict[str, str]:
|
|
45
45
|
try:
|
|
46
|
-
data = json.loads(_path_cache_file().read_text())
|
|
46
|
+
data = json.loads(_path_cache_file().read_text(encoding="utf-8"))
|
|
47
47
|
return data if isinstance(data, dict) else {}
|
|
48
|
-
except (OSError, json.JSONDecodeError):
|
|
48
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
49
49
|
return {}
|
|
50
50
|
|
|
51
51
|
|
|
@@ -45,6 +45,7 @@ from railmux.fast_display_input import (
|
|
|
45
45
|
SelectionSegment,
|
|
46
46
|
SgrMouseEvent,
|
|
47
47
|
TermuxTouchKeyboard,
|
|
48
|
+
TouchKeyboardAction,
|
|
48
49
|
TerminalInputDecoder,
|
|
49
50
|
is_termux_environment,
|
|
50
51
|
page_key_direction,
|
|
@@ -223,6 +224,33 @@ def compact_status_row(screen: AppliedScreen) -> int | None:
|
|
|
223
224
|
return None
|
|
224
225
|
|
|
225
226
|
|
|
227
|
+
def termux_prompt_touch_action(
|
|
228
|
+
touch_keyboard: TermuxTouchKeyboard,
|
|
229
|
+
event: SgrMouseEvent,
|
|
230
|
+
history: LocalHistoryView,
|
|
231
|
+
screen: AppliedScreen | None,
|
|
232
|
+
*,
|
|
233
|
+
now: float | None = None,
|
|
234
|
+
) -> TouchKeyboardAction:
|
|
235
|
+
"""Classify one translated Termux tap against live screen route authority."""
|
|
236
|
+
if screen is None:
|
|
237
|
+
return TouchKeyboardAction()
|
|
238
|
+
clicked_pane_id = history.pane_id_at_position(event.x - 1, event.y - 1)
|
|
239
|
+
cursor_pane_id = history.pane_id_at_position(
|
|
240
|
+
screen.cursor_x,
|
|
241
|
+
screen.cursor_y,
|
|
242
|
+
)
|
|
243
|
+
return touch_keyboard.pointer_event(
|
|
244
|
+
event,
|
|
245
|
+
clicked_pane_id=clicked_pane_id,
|
|
246
|
+
cursor_pane_id=cursor_pane_id,
|
|
247
|
+
cursor_y=screen.cursor_y,
|
|
248
|
+
pane_frozen=history.pane_is_frozen(clicked_pane_id),
|
|
249
|
+
navigation_row=compact_status_row(screen),
|
|
250
|
+
now=now,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
226
254
|
class ProbeError(RuntimeError):
|
|
227
255
|
"""A bounded, user-facing SSH display failure."""
|
|
228
256
|
|
|
@@ -2723,34 +2751,16 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2723
2751
|
latest_screen.cursor_x, latest_screen.cursor_y
|
|
2724
2752
|
)
|
|
2725
2753
|
)
|
|
2726
|
-
clicked_pane_id = (
|
|
2727
|
-
None
|
|
2728
|
-
if latest_screen is None
|
|
2729
|
-
else history.pane_id_at_position(part.x - 1, part.y - 1)
|
|
2730
|
-
)
|
|
2731
|
-
cursor_pane_id = (
|
|
2732
|
-
None
|
|
2733
|
-
if latest_screen is None
|
|
2734
|
-
else history.pane_id_at_position(
|
|
2735
|
-
latest_screen.cursor_x,
|
|
2736
|
-
latest_screen.cursor_y,
|
|
2737
|
-
)
|
|
2738
|
-
)
|
|
2739
2754
|
status_row = (
|
|
2740
2755
|
compact_status_row(latest_screen)
|
|
2741
2756
|
if latest_screen is not None
|
|
2742
2757
|
else None
|
|
2743
2758
|
)
|
|
2744
|
-
touch_action =
|
|
2759
|
+
touch_action = termux_prompt_touch_action(
|
|
2760
|
+
touch_keyboard,
|
|
2745
2761
|
part,
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
cursor_y=0 if latest_screen is None else latest_screen.cursor_y,
|
|
2749
|
-
cursor_visible=(
|
|
2750
|
-
False if latest_screen is None else latest_screen.cursor_visible
|
|
2751
|
-
),
|
|
2752
|
-
pane_frozen=history.pane_is_frozen(clicked_pane_id),
|
|
2753
|
-
navigation_row=status_row,
|
|
2762
|
+
history,
|
|
2763
|
+
latest_screen,
|
|
2754
2764
|
now=time.monotonic(),
|
|
2755
2765
|
)
|
|
2756
2766
|
if touch_action.suspend_mouse:
|
|
@@ -129,7 +129,6 @@ class TermuxTouchKeyboard:
|
|
|
129
129
|
clicked_pane_id: str | None,
|
|
130
130
|
cursor_pane_id: str | None,
|
|
131
131
|
cursor_y: int,
|
|
132
|
-
cursor_visible: bool,
|
|
133
132
|
pane_frozen: bool,
|
|
134
133
|
navigation_row: int | None = None,
|
|
135
134
|
now: float | None = None,
|
|
@@ -150,7 +149,6 @@ class TermuxTouchKeyboard:
|
|
|
150
149
|
or self._active
|
|
151
150
|
or self._close_reassert_pending
|
|
152
151
|
or pane_frozen
|
|
153
|
-
or not cursor_visible
|
|
154
152
|
or clicked_pane_id is None
|
|
155
153
|
or clicked_pane_id != cursor_pane_id
|
|
156
154
|
or (navigation_row is not None and event.y == navigation_row)
|
|
@@ -158,6 +156,11 @@ class TermuxTouchKeyboard:
|
|
|
158
156
|
or not self._is_plain_left_press(event)
|
|
159
157
|
):
|
|
160
158
|
return TouchKeyboardAction()
|
|
159
|
+
# DEC cursor visibility is presentation state, not prompt authority.
|
|
160
|
+
# Providers may hide the hardware cursor while retaining its exact
|
|
161
|
+
# input coordinates. The verified live-agent route, matching cursor
|
|
162
|
+
# route, unfrozen viewport, and bounded row distance remain the
|
|
163
|
+
# fail-closed authority for yielding one Termux tap.
|
|
161
164
|
requested_at = time.monotonic() if now is None else now
|
|
162
165
|
# A rapid reopen supersedes the delayed ownership reassertion queued by
|
|
163
166
|
# the previous close; Termux must receive this new native prompt tap.
|
|
@@ -29,10 +29,10 @@ class Favorites:
|
|
|
29
29
|
if not self._path.is_file():
|
|
30
30
|
return
|
|
31
31
|
try:
|
|
32
|
-
data = json.loads(self._path.read_text())
|
|
32
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
33
33
|
if isinstance(data, list):
|
|
34
34
|
self._ids = set(data)
|
|
35
|
-
except (json.JSONDecodeError, OSError):
|
|
35
|
+
except (json.JSONDecodeError, OSError, UnicodeError):
|
|
36
36
|
self._ids = set()
|
|
37
37
|
|
|
38
38
|
def _save(self) -> None:
|
|
@@ -32,8 +32,8 @@ class Renames:
|
|
|
32
32
|
if not self._path.is_file():
|
|
33
33
|
return
|
|
34
34
|
try:
|
|
35
|
-
data = json.loads(self._path.read_text())
|
|
36
|
-
except (json.JSONDecodeError, OSError):
|
|
35
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
36
|
+
except (json.JSONDecodeError, OSError, UnicodeError):
|
|
37
37
|
return
|
|
38
38
|
if isinstance(data, dict):
|
|
39
39
|
self._titles = {
|
|
@@ -125,6 +125,8 @@ def check_executable(
|
|
|
125
125
|
env=None if environ is None else dict(environ),
|
|
126
126
|
capture_output=True,
|
|
127
127
|
text=True,
|
|
128
|
+
encoding="utf-8",
|
|
129
|
+
errors="replace",
|
|
128
130
|
timeout=timeout,
|
|
129
131
|
check=False,
|
|
130
132
|
)
|
|
@@ -168,6 +170,8 @@ def check_utf8_locale(
|
|
|
168
170
|
env=env,
|
|
169
171
|
capture_output=True,
|
|
170
172
|
text=True,
|
|
173
|
+
encoding="utf-8",
|
|
174
|
+
errors="replace",
|
|
171
175
|
timeout=timeout,
|
|
172
176
|
check=False,
|
|
173
177
|
)
|
|
@@ -92,7 +92,7 @@ def _scan_session(project: Project, jsonl_path: Path) -> SessionMeta | None:
|
|
|
92
92
|
last_stop_reason: str = "" # only set for assistant records
|
|
93
93
|
|
|
94
94
|
try:
|
|
95
|
-
with jsonl_path.open("r") as f:
|
|
95
|
+
with jsonl_path.open("r", encoding="utf-8", errors="replace") as f:
|
|
96
96
|
for line in f:
|
|
97
97
|
line = line.strip()
|
|
98
98
|
if not line:
|
|
@@ -102,6 +102,8 @@ def _tmux_version_for_authority(
|
|
|
102
102
|
argv,
|
|
103
103
|
stderr=subprocess.DEVNULL,
|
|
104
104
|
text=True,
|
|
105
|
+
encoding="utf-8",
|
|
106
|
+
errors="replace",
|
|
105
107
|
timeout=2.0,
|
|
106
108
|
)
|
|
107
109
|
except (
|
|
@@ -1016,6 +1018,8 @@ def create_dual_pane_layout(
|
|
|
1016
1018
|
stdout=subprocess.PIPE,
|
|
1017
1019
|
stderr=subprocess.DEVNULL,
|
|
1018
1020
|
text=True,
|
|
1021
|
+
encoding="utf-8",
|
|
1022
|
+
errors="replace",
|
|
1019
1023
|
check=False,
|
|
1020
1024
|
)
|
|
1021
1025
|
except OSError:
|
|
@@ -1284,6 +1288,8 @@ def pane_process_alive(pane_id: str) -> bool:
|
|
|
1284
1288
|
["tmux", "display-message", "-p", "-t", pane_id, "#{pane_dead}"],
|
|
1285
1289
|
capture_output=True,
|
|
1286
1290
|
text=True,
|
|
1291
|
+
encoding="utf-8",
|
|
1292
|
+
errors="replace",
|
|
1287
1293
|
timeout=1,
|
|
1288
1294
|
)
|
|
1289
1295
|
return result.returncode == 0 and result.stdout.strip() == "0"
|
|
@@ -2772,7 +2778,8 @@ def new_detached_session(name: str, cmd: str,
|
|
|
2772
2778
|
try:
|
|
2773
2779
|
dead = subprocess.run(
|
|
2774
2780
|
["tmux", "list-panes", "-t", name, "-F", "#{pane_dead}"],
|
|
2775
|
-
capture_output=True, text=True,
|
|
2781
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
2782
|
+
errors="replace", timeout=2,
|
|
2776
2783
|
)
|
|
2777
2784
|
if dead.returncode != 0:
|
|
2778
2785
|
# With tmux's default ``remain-on-exit off``, an immediately
|
|
@@ -2789,7 +2796,8 @@ def new_detached_session(name: str, cmd: str,
|
|
|
2789
2796
|
if dead.stdout.strip() == "1":
|
|
2790
2797
|
capture = subprocess.run(
|
|
2791
2798
|
["tmux", "capture-pane", "-t", name, "-p", "-S", "-20"],
|
|
2792
|
-
capture_output=True, text=True,
|
|
2799
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
2800
|
+
errors="replace", timeout=2,
|
|
2793
2801
|
)
|
|
2794
2802
|
pane_content = capture.stdout.strip() if capture.returncode == 0 else ""
|
|
2795
2803
|
# Clean up the dead session — it can never recover.
|
|
@@ -2913,13 +2921,15 @@ def start_detached_holder(
|
|
|
2913
2921
|
dead = subprocess.run(
|
|
2914
2922
|
["tmux", "display-message", "-p", "-t", identity.pane_id,
|
|
2915
2923
|
"#{pane_dead}"],
|
|
2916
|
-
capture_output=True, text=True,
|
|
2924
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
2925
|
+
errors="replace", timeout=2,
|
|
2917
2926
|
)
|
|
2918
2927
|
if dead.returncode == 0 and dead.stdout.strip() == "1":
|
|
2919
2928
|
capture = subprocess.run(
|
|
2920
2929
|
["tmux", "capture-pane", "-t", identity.pane_id,
|
|
2921
2930
|
"-p", "-S", "-20"],
|
|
2922
|
-
capture_output=True, text=True,
|
|
2931
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
2932
|
+
errors="replace", timeout=2,
|
|
2923
2933
|
)
|
|
2924
2934
|
lines = [line.strip() for line in capture.stdout.splitlines()
|
|
2925
2935
|
if line.strip()] if capture.returncode == 0 else []
|
|
@@ -1981,7 +1981,8 @@ class App:
|
|
|
1981
1981
|
import subprocess as _sp
|
|
1982
1982
|
try:
|
|
1983
1983
|
out = _sp.check_output(
|
|
1984
|
-
["less", "--version"], stderr=_sp.STDOUT, text=True,
|
|
1984
|
+
["less", "--version"], stderr=_sp.STDOUT, text=True,
|
|
1985
|
+
encoding="utf-8", errors="replace", timeout=3)
|
|
1985
1986
|
# "less 668 (GNU regular expressions)" → major version
|
|
1986
1987
|
ver = int(out.strip().split()[1].split(".")[0])
|
|
1987
1988
|
if ver >= 590:
|
|
@@ -6722,6 +6723,7 @@ class App:
|
|
|
6722
6723
|
f"\t#{{{orphan_marker.OPTION_NAME}}}"
|
|
6723
6724
|
f"\t#{{{_SESSION_BINDING_OPTION}}}"],
|
|
6724
6725
|
stderr=_sp.DEVNULL, text=True,
|
|
6726
|
+
encoding="utf-8", errors="replace",
|
|
6725
6727
|
)
|
|
6726
6728
|
except (OSError, _sp.CalledProcessError):
|
|
6727
6729
|
return False
|
|
@@ -9000,8 +9002,8 @@ class App:
|
|
|
9000
9002
|
return True
|
|
9001
9003
|
try:
|
|
9002
9004
|
source_stat = history_path.stat()
|
|
9003
|
-
lines = history_path.read_text().splitlines()
|
|
9004
|
-
except OSError:
|
|
9005
|
+
lines = history_path.read_text(encoding="utf-8").splitlines()
|
|
9006
|
+
except (OSError, UnicodeError):
|
|
9005
9007
|
return False
|
|
9006
9008
|
kept = []
|
|
9007
9009
|
changed = False
|
|
@@ -9081,7 +9083,7 @@ class App:
|
|
|
9081
9083
|
import json
|
|
9082
9084
|
record = json.dumps({"type": "ai-title", "aiTitle": new_title})
|
|
9083
9085
|
try:
|
|
9084
|
-
with session.jsonl_path.open("a") as f:
|
|
9086
|
+
with session.jsonl_path.open("a", encoding="utf-8") as f:
|
|
9085
9087
|
f.write(record + "\n")
|
|
9086
9088
|
except OSError:
|
|
9087
9089
|
synced = False
|
|
@@ -88,6 +88,7 @@ from railmux.fast_display_client import (
|
|
|
88
88
|
remote_install_help,
|
|
89
89
|
screen_input_may_change_routes,
|
|
90
90
|
split_local_escape,
|
|
91
|
+
termux_prompt_touch_action,
|
|
91
92
|
)
|
|
92
93
|
from railmux import fast_display_history
|
|
93
94
|
from railmux.fast_display_history import (
|
|
@@ -1395,7 +1396,6 @@ def test_termux_prompt_tap_yields_mouse_until_keyboard_input():
|
|
|
1395
1396
|
clicked_pane_id="%8",
|
|
1396
1397
|
cursor_pane_id="%8",
|
|
1397
1398
|
cursor_y=21,
|
|
1398
|
-
cursor_visible=True,
|
|
1399
1399
|
pane_frozen=False,
|
|
1400
1400
|
now=5.0,
|
|
1401
1401
|
)
|
|
@@ -1410,7 +1410,6 @@ def test_termux_prompt_tap_yields_mouse_until_keyboard_input():
|
|
|
1410
1410
|
clicked_pane_id="%8",
|
|
1411
1411
|
cursor_pane_id="%8",
|
|
1412
1412
|
cursor_y=21,
|
|
1413
|
-
cursor_visible=True,
|
|
1414
1413
|
pane_frozen=False,
|
|
1415
1414
|
now=5.0,
|
|
1416
1415
|
).handled
|
|
@@ -1420,6 +1419,51 @@ def test_termux_prompt_tap_yields_mouse_until_keyboard_input():
|
|
|
1420
1419
|
assert not touch.consumes_focus_out(b"\033[O")
|
|
1421
1420
|
|
|
1422
1421
|
|
|
1422
|
+
def test_termux_live_route_tap_yields_mouse_when_provider_hides_dec_cursor():
|
|
1423
|
+
history = LocalHistoryView()
|
|
1424
|
+
assert history.begin_prefetch(1.0) is not None
|
|
1425
|
+
request_id = history.prefetch_pending_id
|
|
1426
|
+
assert request_id is not None
|
|
1427
|
+
route = HistorySnapshot(
|
|
1428
|
+
request_id,
|
|
1429
|
+
"%8",
|
|
1430
|
+
30,
|
|
1431
|
+
0,
|
|
1432
|
+
50,
|
|
1433
|
+
24,
|
|
1434
|
+
(b"",) * 24,
|
|
1435
|
+
)
|
|
1436
|
+
history.accept_prefetch(HistoryBatch(request_id, (route,)))
|
|
1437
|
+
screen = AppliedScreen(
|
|
1438
|
+
width=80,
|
|
1439
|
+
height=24,
|
|
1440
|
+
cursor_x=40,
|
|
1441
|
+
cursor_y=21,
|
|
1442
|
+
cursor_visible=False,
|
|
1443
|
+
terminal_modes=TerminalMode.FOCUS_EVENTS,
|
|
1444
|
+
rows=(b"",) * 24,
|
|
1445
|
+
changed_rows=(),
|
|
1446
|
+
clear=False,
|
|
1447
|
+
)
|
|
1448
|
+
touch = TermuxTouchKeyboard(enabled=True)
|
|
1449
|
+
|
|
1450
|
+
action = termux_prompt_touch_action(
|
|
1451
|
+
touch,
|
|
1452
|
+
SgrMouseEvent(b"press", 0, 41, 22, True),
|
|
1453
|
+
history,
|
|
1454
|
+
screen,
|
|
1455
|
+
now=5.0,
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
stream = io.BytesIO()
|
|
1459
|
+
surface = TerminalSurface(stream, mouse_hover=False)
|
|
1460
|
+
surface.start()
|
|
1461
|
+
if action.suspend_mouse:
|
|
1462
|
+
surface.suspend_mouse()
|
|
1463
|
+
assert action.handled and action.show_hint
|
|
1464
|
+
assert stream.getvalue().endswith(b"\033[?1002l\033[?1006l")
|
|
1465
|
+
|
|
1466
|
+
|
|
1423
1467
|
def test_termux_focus_reassertion_requires_remote_focus_events():
|
|
1424
1468
|
screen = AppliedScreen(
|
|
1425
1469
|
width=20,
|
|
@@ -1452,24 +1496,21 @@ def test_termux_prompt_tap_restores_mouse_as_keyboard_projection_opens():
|
|
|
1452
1496
|
clicked_pane_id="%8",
|
|
1453
1497
|
cursor_pane_id="%8",
|
|
1454
1498
|
cursor_y=21,
|
|
1455
|
-
cursor_visible=True,
|
|
1456
1499
|
pane_frozen=False,
|
|
1457
1500
|
now=5.0,
|
|
1458
1501
|
).handled
|
|
1459
1502
|
|
|
1460
1503
|
touch = TermuxTouchKeyboard(enabled=True, timeout=10.0)
|
|
1461
|
-
for clicked, cursor,
|
|
1462
|
-
("%9", "%8",
|
|
1463
|
-
("%8", "%8",
|
|
1464
|
-
("%8", "%8",
|
|
1465
|
-
("%8", "%8", True, False, 18),
|
|
1504
|
+
for clicked, cursor, frozen, y in (
|
|
1505
|
+
("%9", "%8", False, 21),
|
|
1506
|
+
("%8", "%8", True, 21),
|
|
1507
|
+
("%8", "%8", False, 18),
|
|
1466
1508
|
):
|
|
1467
1509
|
assert not touch.pointer_event(
|
|
1468
1510
|
press,
|
|
1469
1511
|
clicked_pane_id=clicked,
|
|
1470
1512
|
cursor_pane_id=cursor,
|
|
1471
1513
|
cursor_y=y,
|
|
1472
|
-
cursor_visible=visible,
|
|
1473
1514
|
pane_frozen=frozen,
|
|
1474
1515
|
now=5.0,
|
|
1475
1516
|
).handled
|
|
@@ -1479,7 +1520,6 @@ def test_termux_prompt_tap_restores_mouse_as_keyboard_projection_opens():
|
|
|
1479
1520
|
clicked_pane_id="%8",
|
|
1480
1521
|
cursor_pane_id="%8",
|
|
1481
1522
|
cursor_y=21,
|
|
1482
|
-
cursor_visible=True,
|
|
1483
1523
|
pane_frozen=False,
|
|
1484
1524
|
now=5.0,
|
|
1485
1525
|
)
|
|
@@ -1495,7 +1535,6 @@ def test_termux_prompt_tap_restores_mouse_as_keyboard_projection_opens():
|
|
|
1495
1535
|
clicked_pane_id="%8",
|
|
1496
1536
|
cursor_pane_id="%8",
|
|
1497
1537
|
cursor_y=21,
|
|
1498
|
-
cursor_visible=True,
|
|
1499
1538
|
pane_frozen=False,
|
|
1500
1539
|
now=15.2,
|
|
1501
1540
|
).handled
|
|
@@ -1504,7 +1543,6 @@ def test_termux_prompt_tap_restores_mouse_as_keyboard_projection_opens():
|
|
|
1504
1543
|
clicked_pane_id="%8",
|
|
1505
1544
|
cursor_pane_id="%8",
|
|
1506
1545
|
cursor_y=21,
|
|
1507
|
-
cursor_visible=True,
|
|
1508
1546
|
pane_frozen=False,
|
|
1509
1547
|
now=15.2,
|
|
1510
1548
|
).handled
|
|
@@ -1525,7 +1563,6 @@ def test_termux_compact_navigation_never_yields_touch_to_soft_keyboard():
|
|
|
1525
1563
|
clicked_pane_id="%8",
|
|
1526
1564
|
cursor_pane_id="%8",
|
|
1527
1565
|
cursor_y=21,
|
|
1528
|
-
cursor_visible=True,
|
|
1529
1566
|
pane_frozen=False,
|
|
1530
1567
|
navigation_row=22,
|
|
1531
1568
|
now=5.0,
|
|
@@ -1543,7 +1580,6 @@ def test_termux_keyboard_projection_state_has_a_bounded_fallback():
|
|
|
1543
1580
|
clicked_pane_id="%8",
|
|
1544
1581
|
cursor_pane_id="%8",
|
|
1545
1582
|
cursor_y=21,
|
|
1546
|
-
cursor_visible=True,
|
|
1547
1583
|
pane_frozen=False,
|
|
1548
1584
|
now=5.0,
|
|
1549
1585
|
)
|
|
@@ -1571,7 +1607,6 @@ def test_termux_keyboard_close_queues_one_delayed_mouse_reassert():
|
|
|
1571
1607
|
clicked_pane_id="%8",
|
|
1572
1608
|
cursor_pane_id="%8",
|
|
1573
1609
|
cursor_y=21,
|
|
1574
|
-
cursor_visible=True,
|
|
1575
1610
|
pane_frozen=False,
|
|
1576
1611
|
now=5.0,
|
|
1577
1612
|
)
|
|
@@ -1593,7 +1628,6 @@ def test_termux_rapid_keyboard_reopen_cancels_delayed_reassert():
|
|
|
1593
1628
|
"clicked_pane_id": "%8",
|
|
1594
1629
|
"cursor_pane_id": "%8",
|
|
1595
1630
|
"cursor_y": 21,
|
|
1596
|
-
"cursor_visible": True,
|
|
1597
1631
|
"pane_frozen": False,
|
|
1598
1632
|
}
|
|
1599
1633
|
touch.pointer_event(press, now=5.0, **fields)
|
|
@@ -1611,7 +1645,6 @@ def test_termux_prompt_tap_times_out_when_keyboard_does_not_open():
|
|
|
1611
1645
|
clicked_pane_id="%8",
|
|
1612
1646
|
cursor_pane_id="%8",
|
|
1613
1647
|
cursor_y=21,
|
|
1614
|
-
cursor_visible=True,
|
|
1615
1648
|
pane_frozen=False,
|
|
1616
1649
|
now=5.0,
|
|
1617
1650
|
)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
@@ -92,6 +93,111 @@ def test_windows_npm_provider_shim_uses_one_bounded_cmd_command():
|
|
|
92
93
|
assert '"session with spaces"' in argv[4]
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
def test_utf8_user_state_survives_a_non_utf8_process_locale(tmp_path):
|
|
97
|
+
"""UTF-8 persistence must not inherit CP936/ANSI locale decoding."""
|
|
98
|
+
config_roots = {
|
|
99
|
+
tmp_path / ".config" / "railmux",
|
|
100
|
+
tmp_path / ".config" / "Railmux",
|
|
101
|
+
}
|
|
102
|
+
for config_root in config_roots:
|
|
103
|
+
config_root.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
(config_root / "config.toml").write_bytes(
|
|
105
|
+
'# 中文注释\n[updates]\nauto_update = "never"\n'.encode("utf-8")
|
|
106
|
+
)
|
|
107
|
+
(config_root / "favorites.json").write_bytes(
|
|
108
|
+
json.dumps(["会话"], ensure_ascii=False).encode("utf-8")
|
|
109
|
+
)
|
|
110
|
+
(config_root / "renames.json").write_bytes(
|
|
111
|
+
json.dumps({"会话": "中文标题"}, ensure_ascii=False).encode("utf-8")
|
|
112
|
+
)
|
|
113
|
+
(config_root / "path-cache.json").write_bytes(
|
|
114
|
+
json.dumps(
|
|
115
|
+
{"编码目录": str(tmp_path / "项目")}, ensure_ascii=False
|
|
116
|
+
).encode("utf-8")
|
|
117
|
+
)
|
|
118
|
+
project_dir = tmp_path / ".claude" / "projects" / "encoded"
|
|
119
|
+
project_dir.mkdir(parents=True)
|
|
120
|
+
session_id = "11111111-1111-1111-1111-111111111111"
|
|
121
|
+
session_path = project_dir / f"{session_id}.jsonl"
|
|
122
|
+
records = (
|
|
123
|
+
{"type": "ai-title", "aiTitle": "会话标题"},
|
|
124
|
+
{"type": "user", "message": {"content": "中文问题"}},
|
|
125
|
+
{
|
|
126
|
+
"type": "assistant",
|
|
127
|
+
"message": {
|
|
128
|
+
"content": "中文回答",
|
|
129
|
+
"stop_reason": "end_turn",
|
|
130
|
+
"usage": {"input_tokens": 1, "output_tokens": 1},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
session_path.write_bytes(
|
|
135
|
+
("\n".join(json.dumps(row, ensure_ascii=False) for row in records) + "\n").encode(
|
|
136
|
+
"utf-8"
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
history = tmp_path / ".claude" / "history.jsonl"
|
|
140
|
+
history.write_bytes(
|
|
141
|
+
(
|
|
142
|
+
json.dumps(
|
|
143
|
+
{"sessionId": "remove", "display": "删除我"}, ensure_ascii=False
|
|
144
|
+
)
|
|
145
|
+
+ "\n"
|
|
146
|
+
+ json.dumps(
|
|
147
|
+
{"sessionId": "keep", "display": "保留我"}, ensure_ascii=False
|
|
148
|
+
)
|
|
149
|
+
+ "\n"
|
|
150
|
+
).encode("utf-8")
|
|
151
|
+
)
|
|
152
|
+
script = r"""
|
|
153
|
+
import json
|
|
154
|
+
import locale
|
|
155
|
+
import sys
|
|
156
|
+
from pathlib import Path
|
|
157
|
+
from railmux.discovery import _load_path_cache
|
|
158
|
+
from railmux.favorites import Favorites
|
|
159
|
+
from railmux.models import Project
|
|
160
|
+
from railmux.renames import Renames
|
|
161
|
+
from railmux.session_index import _scan_session
|
|
162
|
+
from railmux.settings import Settings
|
|
163
|
+
from railmux.ui.app import App
|
|
164
|
+
|
|
165
|
+
home = Path(sys.argv[1])
|
|
166
|
+
assert locale.getpreferredencoding(False).upper() not in {'UTF-8', 'UTF8'}
|
|
167
|
+
assert Settings().update_policy == 'never'
|
|
168
|
+
assert Favorites().is_favorite('\u4f1a\u8bdd')
|
|
169
|
+
assert Renames().get('\u4f1a\u8bdd') == '\u4e2d\u6587\u6807\u9898'
|
|
170
|
+
assert _load_path_cache()['\u7f16\u7801\u76ee\u5f55'].endswith('\u9879\u76ee')
|
|
171
|
+
project = Project(home / '\u9879\u76ee', 'encoded', home / '.claude' / 'projects' / 'encoded', 1, 0)
|
|
172
|
+
session = _scan_session(project, project.claude_dir / '11111111-1111-1111-1111-111111111111.jsonl')
|
|
173
|
+
assert session is not None and session.title == '\u4f1a\u8bdd\u6807\u9898'
|
|
174
|
+
assert App._remove_from_history('remove', claude_home=home / '.claude')
|
|
175
|
+
rows = [json.loads(line) for line in (home / '.claude' / 'history.jsonl').read_bytes().decode('utf-8').splitlines()]
|
|
176
|
+
assert rows == [{'sessionId': 'keep', 'display': '\u4fdd\u7559\u6211'}]
|
|
177
|
+
print('ok')
|
|
178
|
+
"""
|
|
179
|
+
result = subprocess.run(
|
|
180
|
+
[sys.executable, "-c", script, str(tmp_path)],
|
|
181
|
+
env={
|
|
182
|
+
**os.environ,
|
|
183
|
+
"APPDATA": str(tmp_path / ".config"),
|
|
184
|
+
"HOME": str(tmp_path),
|
|
185
|
+
"LC_ALL": "C",
|
|
186
|
+
"PYTHONCOERCECLOCALE": "0",
|
|
187
|
+
"PYTHONUTF8": "0",
|
|
188
|
+
"PYTHONPATH": str(Path(__file__).parents[1] / "src"),
|
|
189
|
+
},
|
|
190
|
+
capture_output=True,
|
|
191
|
+
text=True,
|
|
192
|
+
encoding="utf-8",
|
|
193
|
+
errors="replace",
|
|
194
|
+
check=False,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert result.returncode == 0, result.stderr
|
|
198
|
+
assert result.stdout.strip() == "ok"
|
|
199
|
+
|
|
200
|
+
|
|
95
201
|
def test_posix_provider_launch_remains_direct_argv():
|
|
96
202
|
assert provider_argv("/opt/bin/codex", ("resume", "abc"), windows=False) == (
|
|
97
203
|
"/opt/bin/codex",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.2.dev2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|