railmux 0.3.1.dev202608010__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.1.dev202608010/src/railmux.egg-info → railmux-0.3.2.dev4}/PKG-INFO +11 -1
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/README.md +6 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/pyproject.toml +4 -0
- railmux-0.3.2.dev4/src/railmux/__init__.py +1 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/cli.py +40 -2
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/codex_index.py +6 -15
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/config.py +3 -1
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/diagnostics.py +4 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/discovery.py +68 -9
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_client.py +43 -46
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_input.py +5 -2
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_server.py +8 -35
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/favorites.py +4 -3
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/help_workspace.py +2 -7
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/legacy_sessions.py +2 -0
- railmux-0.3.2.dev4/src/railmux/local_clipboard.py +101 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/local_open.py +37 -10
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/modes.py +8 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/mouse_manager.py +7 -3
- railmux-0.3.2.dev4/src/railmux/mux/__init__.py +5 -0
- railmux-0.3.2.dev4/src/railmux/mux/backend.py +59 -0
- railmux-0.3.2.dev4/src/railmux/mux/tmux_backend.py +161 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/orphan_marker.py +9 -14
- railmux-0.3.2.dev4/src/railmux/platform/__init__.py +44 -0
- railmux-0.3.2.dev4/src/railmux/platform/config_paths.py +30 -0
- railmux-0.3.2.dev4/src/railmux/platform/console.py +165 -0
- railmux-0.3.2.dev4/src/railmux/platform/file_security.py +34 -0
- railmux-0.3.2.dev4/src/railmux/platform/filelock.py +42 -0
- railmux-0.3.2.dev4/src/railmux/platform/multiplex.py +132 -0
- railmux-0.3.2.dev4/src/railmux/platform/process.py +21 -0
- railmux-0.3.2.dev4/src/railmux/platform/runtime_paths.py +75 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/renames.py +4 -3
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/restart_state.py +7 -12
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/runtime_config.py +8 -1
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/scroll_manager.py +3 -5
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/self_update.py +2 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/session_index.py +1 -1
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/settings.py +1 -1
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ssh_display_diagnostics.py +3 -5
- railmux-0.3.2.dev4/src/railmux/terminal_emulator.py +180 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tmux_binding_manager.py +7 -3
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tmux_ctl.py +14 -4
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tool_panes.py +2 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/app.py +324 -246
- railmux-0.3.2.dev4/src/railmux/winlocal/__init__.py +2 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/backend.py +897 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/client.py +275 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/compositor.py +254 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/conpty.py +200 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/daemon.py +359 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/history_viewer.py +93 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/ipc.py +88 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/session_store.py +96 -0
- railmux-0.3.2.dev4/src/railmux/winlocal/virtual_screen.py +105 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4/src/railmux.egg-info}/PKG-INFO +11 -1
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/SOURCES.txt +33 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/requires.txt +4 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_cli.py +15 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_codex_delete.py +7 -3
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_config.py +1 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_discovery.py +24 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_fast_full_window.py +50 -17
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_local_open.py +26 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_modes.py +2 -0
- railmux-0.3.2.dev4/tests/test_mux_backend.py +60 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_orphan_recovery.py +2 -2
- railmux-0.3.2.dev4/tests/test_platform.py +248 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_release_workflow.py +9 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_runtime_config.py +12 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_soft_quit.py +2 -2
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_wheel_smoke.py +6 -1
- railmux-0.3.2.dev4/tests/test_winlocal_backend.py +176 -0
- railmux-0.3.2.dev4/tests/test_winlocal_compositor.py +48 -0
- railmux-0.3.2.dev4/tests/test_winlocal_conpty.py +60 -0
- railmux-0.3.2.dev4/tests/test_winlocal_daemon.py +150 -0
- railmux-0.3.2.dev4/tests/test_winlocal_history_viewer.py +43 -0
- railmux-0.3.2.dev4/tests/test_winlocal_ipc.py +39 -0
- railmux-0.3.2.dev4/tests/test_winlocal_real_conpty.py +61 -0
- railmux-0.3.2.dev4/tests/test_winlocal_session_store.py +42 -0
- railmux-0.3.2.dev4/tests/test_winlocal_virtual_screen.py +62 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tools/wheel_smoke.py +12 -6
- railmux-0.3.1.dev202608010/src/railmux/__init__.py +0 -1
- railmux-0.3.1.dev202608010/src/railmux/local_clipboard.py +0 -47
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/LICENSE +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/MANIFEST.in +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/setup.cfg +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/__main__.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/background_index.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/launcher.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/models.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/transcript.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_background_index.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_codex_index.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_config_cli.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_display_transport.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_help_agent.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_keymap.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_launcher.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_modals.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_new_project.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_options.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_path_codec.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_release_notes.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_remote_config.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_renames.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_restart_state.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_running_pane.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_running_sort.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_self_update.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_session_cache.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_session_index.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_settings.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_sidebar.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_status.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_statusbar.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_system_deps.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_integration.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_transcript.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_widgets.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.1.dev202608010 → railmux-0.3.2.dev4}/tools/release_notes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2.dev4
|
|
4
4
|
Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
|
|
5
5
|
Author-email: Rightglow <zhang.taian@foxmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,12 +14,14 @@ Classifier: Environment :: Console :: Curses
|
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Operating System :: POSIX :: Linux
|
|
16
16
|
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
18
|
Classifier: Programming Language :: Python :: 3
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
25
|
Classifier: Topic :: Software Development
|
|
24
26
|
Classifier: Topic :: Terminals
|
|
25
27
|
Classifier: Topic :: Utilities
|
|
@@ -30,6 +32,8 @@ Requires-Dist: packaging>=23.0
|
|
|
30
32
|
Requires-Dist: tomlkit<1,>=0.13.2
|
|
31
33
|
Requires-Dist: urwid>=2.6.16
|
|
32
34
|
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
35
|
+
Requires-Dist: pyte<0.9,>=0.8.2; sys_platform == "win32"
|
|
36
|
+
Requires-Dist: pywinpty<4,>=3.0.5; sys_platform == "win32"
|
|
33
37
|
Provides-Extra: ssh
|
|
34
38
|
Requires-Dist: pyte<0.9,>=0.8.2; extra == "ssh"
|
|
35
39
|
Provides-Extra: dev
|
|
@@ -127,6 +131,12 @@ not permanent traps: press `o`, or choose **More → Options**, to change them
|
|
|
127
131
|
later. One-time safety confirmations such as dependency installation and
|
|
128
132
|
session deletion are intentionally asked at the action itself.
|
|
129
133
|
|
|
134
|
+
For setup outside the full-screen UI, run `railmux config`; it edits the same
|
|
135
|
+
settings as Options and also covers executable paths, SSH history capacity,
|
|
136
|
+
and the runtime locale. Use `railmux config --remote your-server` to configure
|
|
137
|
+
that host without opening or resizing its tmux workspace. See
|
|
138
|
+
[Configuration](#configuration) for the complete interface.
|
|
139
|
+
|
|
130
140
|
If ordinary SSH cannot keep up with full-screen redraws, install Railmux on
|
|
131
141
|
your local macOS, Linux, or Windows WSL environment and use its responsive,
|
|
132
142
|
locally cached SSH display:
|
|
@@ -85,6 +85,12 @@ not permanent traps: press `o`, or choose **More → Options**, to change them
|
|
|
85
85
|
later. One-time safety confirmations such as dependency installation and
|
|
86
86
|
session deletion are intentionally asked at the action itself.
|
|
87
87
|
|
|
88
|
+
For setup outside the full-screen UI, run `railmux config`; it edits the same
|
|
89
|
+
settings as Options and also covers executable paths, SSH history capacity,
|
|
90
|
+
and the runtime locale. Use `railmux config --remote your-server` to configure
|
|
91
|
+
that host without opening or resizing its tmux workspace. See
|
|
92
|
+
[Configuration](#configuration) for the complete interface.
|
|
93
|
+
|
|
88
94
|
If ordinary SSH cannot keep up with full-screen redraws, install Railmux on
|
|
89
95
|
your local macOS, Linux, or Windows WSL environment and use its responsive,
|
|
90
96
|
locally cached SSH display:
|
|
@@ -20,12 +20,14 @@ classifiers = [
|
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
21
|
"Operating System :: POSIX :: Linux",
|
|
22
22
|
"Operating System :: MacOS",
|
|
23
|
+
"Operating System :: Microsoft :: Windows",
|
|
23
24
|
"Programming Language :: Python :: 3",
|
|
24
25
|
"Programming Language :: Python :: 3.9",
|
|
25
26
|
"Programming Language :: Python :: 3.10",
|
|
26
27
|
"Programming Language :: Python :: 3.11",
|
|
27
28
|
"Programming Language :: Python :: 3.12",
|
|
28
29
|
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
29
31
|
"Topic :: Software Development",
|
|
30
32
|
"Topic :: Terminals",
|
|
31
33
|
"Topic :: Utilities",
|
|
@@ -35,6 +37,8 @@ dependencies = [
|
|
|
35
37
|
"tomlkit>=0.13.2,<1",
|
|
36
38
|
"urwid>=2.6.16",
|
|
37
39
|
"tomli>=2.0.1; python_version < '3.11'",
|
|
40
|
+
"pyte>=0.8.2,<0.9; sys_platform == 'win32'",
|
|
41
|
+
"pywinpty>=3.0.5,<4; sys_platform == 'win32'",
|
|
38
42
|
]
|
|
39
43
|
|
|
40
44
|
[project.optional-dependencies]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.2.dev4"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import os
|
|
4
5
|
import shutil
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
|
-
import termios
|
|
8
8
|
import time
|
|
9
9
|
from dataclasses import replace
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
from railmux import __version__
|
|
13
13
|
from railmux.config import ConfigError, default_config_path, load_config
|
|
14
|
-
from railmux.diagnostics import is_ssh_session, run_doctor
|
|
15
14
|
from railmux.pane_surface import render_startup_surface
|
|
15
|
+
from railmux.platform import python_support_error
|
|
16
16
|
from railmux.runtime_config import (
|
|
17
17
|
activate_runtime_environment,
|
|
18
18
|
check_executable,
|
|
@@ -37,9 +37,29 @@ _LOCAL_WATCHDOG_INTERVAL = 5.0
|
|
|
37
37
|
_LOCAL_WATCHDOG_FAILURES = 3
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def _native_windows() -> bool:
|
|
41
|
+
return os.name == "nt"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_ssh_session(environment: dict[str, str] | None = None) -> bool:
|
|
45
|
+
"""Lazy compatibility wrapper that keeps POSIX diagnostics import-clean."""
|
|
46
|
+
from railmux.diagnostics import is_ssh_session as detect
|
|
47
|
+
|
|
48
|
+
return detect(environment)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run_doctor(*args: object, **kwargs: object) -> int:
|
|
52
|
+
"""Lazy compatibility wrapper for the established CLI test/repair hook."""
|
|
53
|
+
from railmux.diagnostics import run_doctor as doctor
|
|
54
|
+
|
|
55
|
+
return doctor(*args, **kwargs)
|
|
56
|
+
|
|
57
|
+
|
|
40
58
|
def _restore_terminal(attributes: list | None) -> None:
|
|
41
59
|
if attributes is None:
|
|
42
60
|
return
|
|
61
|
+
import termios
|
|
62
|
+
|
|
43
63
|
try:
|
|
44
64
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
45
65
|
except (OSError, termios.error):
|
|
@@ -67,6 +87,8 @@ def _run_tmux_client_with_watchdog(
|
|
|
67
87
|
"""Keep one monitor outside tmux so a frozen server cannot trap the TTY."""
|
|
68
88
|
attributes = None
|
|
69
89
|
if sys.stdin.isatty():
|
|
90
|
+
import termios
|
|
91
|
+
|
|
70
92
|
try:
|
|
71
93
|
attributes = termios.tcgetattr(sys.stdin.fileno())
|
|
72
94
|
except (OSError, termios.error):
|
|
@@ -152,6 +174,10 @@ def _run_tmux_client_with_watchdog(
|
|
|
152
174
|
|
|
153
175
|
|
|
154
176
|
def main(argv: list[str] | None = None) -> int:
|
|
177
|
+
support_error = python_support_error()
|
|
178
|
+
if support_error is not None:
|
|
179
|
+
print(f"error: {support_error}", file=sys.stderr)
|
|
180
|
+
return 2
|
|
155
181
|
raw_args = list(sys.argv[1:] if argv is None else argv)
|
|
156
182
|
if raw_args and raw_args[0] == "config":
|
|
157
183
|
from railmux.config_cli import main as config_main
|
|
@@ -338,6 +364,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
338
364
|
file=sys.stderr,
|
|
339
365
|
)
|
|
340
366
|
return 2
|
|
367
|
+
if _native_windows():
|
|
368
|
+
# Native Windows owns no local tmux server. The detached daemon keeps
|
|
369
|
+
# ConPTY handles alive while this terminal remains a disposable client.
|
|
370
|
+
activate_runtime_environment(replace(config, tmux_binary="tmux"))
|
|
371
|
+
if not args.inside_tmux:
|
|
372
|
+
from railmux.self_update import maybe_upgrade_before_launch
|
|
373
|
+
from railmux.settings import Settings
|
|
374
|
+
|
|
375
|
+
maybe_upgrade_before_launch(raw_args, Settings())
|
|
376
|
+
from railmux.winlocal.client import main as native_main
|
|
377
|
+
|
|
378
|
+
return native_main(raw_args, config=config)
|
|
341
379
|
if config.tmux_binary != "tmux":
|
|
342
380
|
tmux_check = check_executable("tmux", config.tmux_binary)
|
|
343
381
|
if not tmux_check.valid:
|
|
@@ -13,12 +13,15 @@ import hashlib
|
|
|
13
13
|
import json
|
|
14
14
|
import math
|
|
15
15
|
import os
|
|
16
|
-
import stat
|
|
17
16
|
import time
|
|
18
17
|
from dataclasses import dataclass, replace
|
|
19
18
|
from pathlib import Path
|
|
20
19
|
|
|
21
20
|
from railmux.atomic_file import atomic_write_text
|
|
21
|
+
from railmux.platform.file_security import (
|
|
22
|
+
prepare_private_directory,
|
|
23
|
+
private_regular_file,
|
|
24
|
+
)
|
|
22
25
|
from railmux.models import (
|
|
23
26
|
AttentionCategory,
|
|
24
27
|
AttentionState,
|
|
@@ -457,9 +460,7 @@ class CodexIndex:
|
|
|
457
460
|
try:
|
|
458
461
|
info = path.lstat()
|
|
459
462
|
if (
|
|
460
|
-
not
|
|
461
|
-
or info.st_uid != os.getuid()
|
|
462
|
-
or info.st_mode & 0o077
|
|
463
|
+
not private_regular_file(info)
|
|
463
464
|
or info.st_size > _PERSISTENT_CACHE_MAX_BYTES
|
|
464
465
|
):
|
|
465
466
|
return
|
|
@@ -514,17 +515,7 @@ class CodexIndex:
|
|
|
514
515
|
path = self._cache_path
|
|
515
516
|
if path is None:
|
|
516
517
|
return False
|
|
517
|
-
|
|
518
|
-
try:
|
|
519
|
-
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
520
|
-
info = parent.lstat()
|
|
521
|
-
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid():
|
|
522
|
-
return False
|
|
523
|
-
if info.st_mode & 0o077:
|
|
524
|
-
os.chmod(parent, stat.S_IMODE(info.st_mode) & ~0o077)
|
|
525
|
-
return True
|
|
526
|
-
except OSError:
|
|
527
|
-
return False
|
|
518
|
+
return prepare_private_directory(path.parent)
|
|
528
519
|
|
|
529
520
|
def _save_persistent_cache(self) -> None:
|
|
530
521
|
path = self._cache_path
|
|
@@ -6,6 +6,8 @@ from dataclasses import dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from railmux.platform.config_paths import default_config_path as _platform_config_path
|
|
10
|
+
|
|
9
11
|
try:
|
|
10
12
|
import tomllib
|
|
11
13
|
except ModuleNotFoundError: # Python 3.9-3.10
|
|
@@ -55,7 +57,7 @@ class Config:
|
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
def default_config_path() -> Path:
|
|
58
|
-
return
|
|
60
|
+
return _platform_config_path()
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
def _table(data: dict[str, Any], name: str) -> dict[str, Any]:
|
|
@@ -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
|
)
|
|
@@ -7,7 +7,8 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from railmux.atomic_file import atomic_write_text
|
|
9
9
|
from railmux.models import Project
|
|
10
|
-
from railmux.path_codec import decode
|
|
10
|
+
from railmux.path_codec import _claude_encode_path, decode
|
|
11
|
+
from railmux.platform.config_paths import config_dir
|
|
11
12
|
from railmux.session_index import _looks_like_uuid, _scan_session
|
|
12
13
|
|
|
13
14
|
|
|
@@ -37,14 +38,14 @@ def _path_cache_file() -> Path:
|
|
|
37
38
|
Decoding an encoded project name is expensive on an NFS home (see
|
|
38
39
|
path_codec), and the mapping is stable, so we persist it across launches.
|
|
39
40
|
"""
|
|
40
|
-
return
|
|
41
|
+
return config_dir() / "path-cache.json"
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def _load_path_cache() -> dict[str, str]:
|
|
44
45
|
try:
|
|
45
|
-
data = json.loads(_path_cache_file().read_text())
|
|
46
|
+
data = json.loads(_path_cache_file().read_text(encoding="utf-8"))
|
|
46
47
|
return data if isinstance(data, dict) else {}
|
|
47
|
-
except (OSError, json.JSONDecodeError):
|
|
48
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
48
49
|
return {}
|
|
49
50
|
|
|
50
51
|
|
|
@@ -94,7 +95,7 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
94
95
|
for entry in scan:
|
|
95
96
|
if not entry.is_dir(follow_symlinks=False):
|
|
96
97
|
continue
|
|
97
|
-
if not entry.name.startswith("-"):
|
|
98
|
+
if os.name != "nt" and not entry.name.startswith("-"):
|
|
98
99
|
continue
|
|
99
100
|
seen.add(entry.name)
|
|
100
101
|
|
|
@@ -106,10 +107,16 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
106
107
|
if cached is not None and Path(cached).is_dir():
|
|
107
108
|
real_path = Path(cached)
|
|
108
109
|
else:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
decoded = None
|
|
111
|
+
if os.name == "nt":
|
|
112
|
+
decoded = _windows_project_path_from_transcript(
|
|
113
|
+
Path(entry.path), entry.name
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
try:
|
|
117
|
+
decoded = decode(entry.name)
|
|
118
|
+
except ValueError:
|
|
119
|
+
pass
|
|
113
120
|
# Skip projects whose original directory no longer exists on
|
|
114
121
|
# this machine (deleted or moved). They can't be launched —
|
|
115
122
|
# `cd` into a missing dir fails — so don't list them, and drop
|
|
@@ -156,6 +163,58 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
156
163
|
return results
|
|
157
164
|
|
|
158
165
|
|
|
166
|
+
def _windows_project_path_from_transcript(
|
|
167
|
+
claude_dir: Path,
|
|
168
|
+
encoded_name: str,
|
|
169
|
+
) -> Path | None:
|
|
170
|
+
"""Recover a Windows project cwd from provider-owned transcript metadata.
|
|
171
|
+
|
|
172
|
+
Drive letters and UNC roots do not fit the POSIX path backtracker. Claude
|
|
173
|
+
records the exact cwd in its JSONL, so validate that bounded value against
|
|
174
|
+
the enclosing encoded directory instead of guessing a Windows path.
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
candidates = sorted(
|
|
178
|
+
claude_dir.glob("*.jsonl"),
|
|
179
|
+
key=lambda path: path.stat().st_mtime_ns,
|
|
180
|
+
reverse=True,
|
|
181
|
+
)[:8]
|
|
182
|
+
except OSError:
|
|
183
|
+
return None
|
|
184
|
+
for path in candidates:
|
|
185
|
+
try:
|
|
186
|
+
with path.open("r", encoding="utf-8", errors="replace") as stream:
|
|
187
|
+
for _line_number in range(64):
|
|
188
|
+
line = stream.readline(64 * 1024 + 1)
|
|
189
|
+
if not line:
|
|
190
|
+
break
|
|
191
|
+
if len(line) > 64 * 1024:
|
|
192
|
+
continue
|
|
193
|
+
try:
|
|
194
|
+
record = json.loads(line)
|
|
195
|
+
except (json.JSONDecodeError, ValueError):
|
|
196
|
+
continue
|
|
197
|
+
raw_cwd = record.get("cwd") if isinstance(record, dict) else None
|
|
198
|
+
if (
|
|
199
|
+
not isinstance(raw_cwd, str)
|
|
200
|
+
or not raw_cwd
|
|
201
|
+
or len(raw_cwd) > 32768
|
|
202
|
+
or "\0" in raw_cwd
|
|
203
|
+
):
|
|
204
|
+
continue
|
|
205
|
+
candidate = Path(raw_cwd)
|
|
206
|
+
if (
|
|
207
|
+
candidate.is_absolute()
|
|
208
|
+
and candidate.is_dir()
|
|
209
|
+
and _claude_encode_path(str(candidate)).casefold()
|
|
210
|
+
== encoded_name.casefold()
|
|
211
|
+
):
|
|
212
|
+
return candidate
|
|
213
|
+
except OSError:
|
|
214
|
+
continue
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
159
218
|
def _count_and_latest_mtime(claude_dir: Path, real_path: Path) -> tuple[int, float]:
|
|
160
219
|
"""Count sessions that pass every ``_scan_session`` filter and return the max
|
|
161
220
|
JSONL mtime. Uses ``_scan_session`` so the count exactly matches the
|
|
@@ -14,15 +14,12 @@ import base64
|
|
|
14
14
|
import json
|
|
15
15
|
import os
|
|
16
16
|
import re
|
|
17
|
-
import select
|
|
18
17
|
import selectors
|
|
19
18
|
import shlex
|
|
20
19
|
import shutil
|
|
21
20
|
import subprocess
|
|
22
21
|
import sys
|
|
23
|
-
import termios
|
|
24
22
|
import time
|
|
25
|
-
import tty
|
|
26
23
|
from collections.abc import Callable
|
|
27
24
|
from dataclasses import dataclass
|
|
28
25
|
from enum import Enum
|
|
@@ -48,6 +45,7 @@ from railmux.fast_display_input import (
|
|
|
48
45
|
SelectionSegment,
|
|
49
46
|
SgrMouseEvent,
|
|
50
47
|
TermuxTouchKeyboard,
|
|
48
|
+
TouchKeyboardAction,
|
|
51
49
|
TerminalInputDecoder,
|
|
52
50
|
is_termux_environment,
|
|
53
51
|
page_key_direction,
|
|
@@ -80,6 +78,8 @@ from railmux.fast_display_protocol import (
|
|
|
80
78
|
encode_path_open_request,
|
|
81
79
|
encode_resize,
|
|
82
80
|
)
|
|
81
|
+
from railmux.platform.console import RawConsole
|
|
82
|
+
from railmux.platform.multiplex import PortableSelector, wait_readable
|
|
83
83
|
from railmux.pane_surface import render_startup_surface
|
|
84
84
|
from railmux.ssh_compat import CompatibilityFacts, decide as decide_compatibility
|
|
85
85
|
from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
|
|
@@ -224,6 +224,33 @@ def compact_status_row(screen: AppliedScreen) -> int | None:
|
|
|
224
224
|
return None
|
|
225
225
|
|
|
226
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
|
+
|
|
227
254
|
class ProbeError(RuntimeError):
|
|
228
255
|
"""A bounded, user-facing SSH display failure."""
|
|
229
256
|
|
|
@@ -292,7 +319,7 @@ def await_remote_attach_status(
|
|
|
292
319
|
readers = [process.stdout.fileno()]
|
|
293
320
|
if cancel_fd is not None:
|
|
294
321
|
readers.append(cancel_fd)
|
|
295
|
-
readable
|
|
322
|
+
readable = wait_readable(readers, remaining)
|
|
296
323
|
if not readable:
|
|
297
324
|
return RemoteAttachKind.TIMEOUT
|
|
298
325
|
if cancel_fd is not None and cancel_fd in readable:
|
|
@@ -388,7 +415,7 @@ def await_remote_startup(
|
|
|
388
415
|
readers = [process.stdout.fileno()]
|
|
389
416
|
if cancel_fd is not None:
|
|
390
417
|
readers.append(cancel_fd)
|
|
391
|
-
readable
|
|
418
|
+
readable = wait_readable(readers, remaining)
|
|
392
419
|
if not readable:
|
|
393
420
|
return RemoteStartup(RemoteStartKind.TIMEOUT)
|
|
394
421
|
if cancel_fd is not None and cancel_fd in readable:
|
|
@@ -575,20 +602,8 @@ def _is_soft_keyboard_projection(
|
|
|
575
602
|
)
|
|
576
603
|
|
|
577
604
|
|
|
578
|
-
class RawTerminal:
|
|
579
|
-
|
|
580
|
-
self.fd = fd
|
|
581
|
-
self.saved: Optional[list[object]] = None
|
|
582
|
-
|
|
583
|
-
def __enter__(self) -> "RawTerminal":
|
|
584
|
-
self.saved = termios.tcgetattr(self.fd)
|
|
585
|
-
tty.setraw(self.fd)
|
|
586
|
-
return self
|
|
587
|
-
|
|
588
|
-
def __exit__(self, *_exc: object) -> None:
|
|
589
|
-
if self.saved is not None:
|
|
590
|
-
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.saved)
|
|
591
|
-
self.saved = None
|
|
605
|
+
class RawTerminal(RawConsole):
|
|
606
|
+
"""Backward-compatible name for the platform raw-console port."""
|
|
592
607
|
|
|
593
608
|
|
|
594
609
|
class TerminalSurface:
|
|
@@ -1862,9 +1877,7 @@ def _wait_reconnect_delay(delay: float, cancel_fd: int) -> None:
|
|
|
1862
1877
|
remaining = deadline - time.monotonic()
|
|
1863
1878
|
if remaining <= 0:
|
|
1864
1879
|
return
|
|
1865
|
-
readable
|
|
1866
|
-
[cancel_fd], [], [], remaining
|
|
1867
|
-
)
|
|
1880
|
+
readable = wait_readable([cancel_fd], remaining)
|
|
1868
1881
|
if readable:
|
|
1869
1882
|
_consume_reconnect_input(cancel_fd)
|
|
1870
1883
|
|
|
@@ -2385,7 +2398,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2385
2398
|
touch_keyboard = TermuxTouchKeyboard(
|
|
2386
2399
|
enabled=not args.no_mouse and termux_touch
|
|
2387
2400
|
)
|
|
2388
|
-
selector =
|
|
2401
|
+
selector = PortableSelector()
|
|
2389
2402
|
selector.register(process.stdout.fileno(), selectors.EVENT_READ, "remote")
|
|
2390
2403
|
selector.register(sys.stdin.fileno(), selectors.EVENT_READ, "local")
|
|
2391
2404
|
started = time.monotonic()
|
|
@@ -2738,34 +2751,16 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2738
2751
|
latest_screen.cursor_x, latest_screen.cursor_y
|
|
2739
2752
|
)
|
|
2740
2753
|
)
|
|
2741
|
-
clicked_pane_id = (
|
|
2742
|
-
None
|
|
2743
|
-
if latest_screen is None
|
|
2744
|
-
else history.pane_id_at_position(part.x - 1, part.y - 1)
|
|
2745
|
-
)
|
|
2746
|
-
cursor_pane_id = (
|
|
2747
|
-
None
|
|
2748
|
-
if latest_screen is None
|
|
2749
|
-
else history.pane_id_at_position(
|
|
2750
|
-
latest_screen.cursor_x,
|
|
2751
|
-
latest_screen.cursor_y,
|
|
2752
|
-
)
|
|
2753
|
-
)
|
|
2754
2754
|
status_row = (
|
|
2755
2755
|
compact_status_row(latest_screen)
|
|
2756
2756
|
if latest_screen is not None
|
|
2757
2757
|
else None
|
|
2758
2758
|
)
|
|
2759
|
-
touch_action =
|
|
2759
|
+
touch_action = termux_prompt_touch_action(
|
|
2760
|
+
touch_keyboard,
|
|
2760
2761
|
part,
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
cursor_y=0 if latest_screen is None else latest_screen.cursor_y,
|
|
2764
|
-
cursor_visible=(
|
|
2765
|
-
False if latest_screen is None else latest_screen.cursor_visible
|
|
2766
|
-
),
|
|
2767
|
-
pane_frozen=history.pane_is_frozen(clicked_pane_id),
|
|
2768
|
-
navigation_row=status_row,
|
|
2762
|
+
history,
|
|
2763
|
+
latest_screen,
|
|
2769
2764
|
now=time.monotonic(),
|
|
2770
2765
|
)
|
|
2771
2766
|
if touch_action.suspend_mouse:
|
|
@@ -2948,7 +2943,9 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2948
2943
|
)
|
|
2949
2944
|
history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
|
|
2950
2945
|
try:
|
|
2951
|
-
with RawTerminal(
|
|
2946
|
+
with RawTerminal(
|
|
2947
|
+
sys.stdin.fileno(), output_fd=sys.stdout.fileno()
|
|
2948
|
+
):
|
|
2952
2949
|
while True:
|
|
2953
2950
|
observed_size = os.get_terminal_size(sys.stdout.fileno())
|
|
2954
2951
|
if observed_size != local_size:
|
|
@@ -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.
|
|
@@ -83,6 +83,11 @@ from railmux.fast_display_protocol import (
|
|
|
83
83
|
encode_path_open_result,
|
|
84
84
|
encode_update,
|
|
85
85
|
)
|
|
86
|
+
from railmux.terminal_emulator import (
|
|
87
|
+
extended_pyte as _shared_extended_pyte,
|
|
88
|
+
render_rows as _shared_render_rows,
|
|
89
|
+
terminal_modes_for_screen as _shared_terminal_modes_for_screen,
|
|
90
|
+
)
|
|
86
91
|
from railmux.ui.workspace import (
|
|
87
92
|
COMPACT_ENTER_HEIGHT,
|
|
88
93
|
COMPACT_ENTER_WIDTH,
|
|
@@ -371,9 +376,7 @@ def _build_extended_pyte(pyte: object) -> object:
|
|
|
371
376
|
|
|
372
377
|
def _extended_pyte(pyte: object) -> object:
|
|
373
378
|
"""Idempotently adapt one imported pyte module for tmux's VT stream."""
|
|
374
|
-
|
|
375
|
-
return pyte
|
|
376
|
-
return _build_extended_pyte(pyte)
|
|
379
|
+
return _shared_extended_pyte(pyte)
|
|
377
380
|
|
|
378
381
|
|
|
379
382
|
@dataclass(frozen=True)
|
|
@@ -1887,42 +1890,12 @@ def _style_key(char: object) -> tuple[object, ...]:
|
|
|
1887
1890
|
|
|
1888
1891
|
def render_rows(screen: object) -> tuple[bytes, ...]:
|
|
1889
1892
|
"""Render independently paintable rows with allowlisted SGR controls."""
|
|
1890
|
-
|
|
1891
|
-
for row_index in range(screen.lines):
|
|
1892
|
-
rendered = [b"\033[0m"]
|
|
1893
|
-
previous_style: tuple[object, ...] | None = None
|
|
1894
|
-
row = screen.buffer[row_index]
|
|
1895
|
-
for column in range(screen.columns):
|
|
1896
|
-
char = row[column]
|
|
1897
|
-
style = _style_key(char)
|
|
1898
|
-
if style != previous_style:
|
|
1899
|
-
rendered.append(_style(char))
|
|
1900
|
-
previous_style = style
|
|
1901
|
-
# pyte represents the second cell of a wide glyph as empty data;
|
|
1902
|
-
# the real terminal already advances two columns for the glyph.
|
|
1903
|
-
if char.data:
|
|
1904
|
-
safe_data = "".join(
|
|
1905
|
-
value
|
|
1906
|
-
if value >= " "
|
|
1907
|
-
and value != "\x7f"
|
|
1908
|
-
and not "\x80" <= value <= "\x9f"
|
|
1909
|
-
else "�"
|
|
1910
|
-
for value in char.data
|
|
1911
|
-
)
|
|
1912
|
-
rendered.append(safe_data.encode("utf-8", errors="replace"))
|
|
1913
|
-
rendered.append(b"\033[0m")
|
|
1914
|
-
rendered_rows.append(b"".join(rendered))
|
|
1915
|
-
return tuple(rendered_rows)
|
|
1893
|
+
return _shared_render_rows(screen)
|
|
1916
1894
|
|
|
1917
1895
|
|
|
1918
1896
|
def terminal_modes_for_screen(screen: object) -> TerminalMode:
|
|
1919
1897
|
"""Project pyte's private-mode set onto the bounded v14 wire allowlist."""
|
|
1920
|
-
|
|
1921
|
-
if 2004 << 5 in screen.mode:
|
|
1922
|
-
terminal_modes |= TerminalMode.BRACKETED_PASTE
|
|
1923
|
-
if 1004 << 5 in screen.mode:
|
|
1924
|
-
terminal_modes |= TerminalMode.FOCUS_EVENTS
|
|
1925
|
-
return terminal_modes
|
|
1898
|
+
return _shared_terminal_modes_for_screen(screen)
|
|
1926
1899
|
|
|
1927
1900
|
|
|
1928
1901
|
def _child_exited(pid: int) -> bool:
|
|
@@ -10,10 +10,11 @@ import json
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
from railmux.atomic_file import atomic_write_text
|
|
13
|
+
from railmux.platform.config_paths import config_dir
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def _favorites_path() -> Path:
|
|
16
|
-
return
|
|
17
|
+
return config_dir() / "favorites.json"
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class Favorites:
|
|
@@ -28,10 +29,10 @@ class Favorites:
|
|
|
28
29
|
if not self._path.is_file():
|
|
29
30
|
return
|
|
30
31
|
try:
|
|
31
|
-
data = json.loads(self._path.read_text())
|
|
32
|
+
data = json.loads(self._path.read_text(encoding="utf-8"))
|
|
32
33
|
if isinstance(data, list):
|
|
33
34
|
self._ids = set(data)
|
|
34
|
-
except (json.JSONDecodeError, OSError):
|
|
35
|
+
except (json.JSONDecodeError, OSError, UnicodeError):
|
|
35
36
|
self._ids = set()
|
|
36
37
|
|
|
37
38
|
def _save(self) -> None:
|