railmux 0.4.0.dev1__tar.gz → 0.4.0.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {railmux-0.4.0.dev1/src/railmux.egg-info → railmux-0.4.0.dev3}/PKG-INFO +1 -5
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/pyproject.toml +1 -5
- railmux-0.4.0.dev3/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/__main__.py +1 -1
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/cli.py +2 -40
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/codex_index.py +15 -6
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/config.py +1 -3
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/discovery.py +7 -66
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/display_transport.py +18 -0
- railmux-0.4.0.dev3/src/railmux/entrypoint.py +26 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/fast_display_client.py +24 -11
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/fast_display_server.py +35 -8
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/favorites.py +1 -2
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/help_workspace.py +7 -2
- railmux-0.4.0.dev3/src/railmux/local_clipboard.py +47 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/local_open.py +10 -37
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/modes.py +0 -8
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/mouse_manager.py +3 -7
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/orphan_marker.py +14 -9
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/renames.py +1 -2
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/restart_state.py +12 -7
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/runtime_config.py +1 -4
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/scroll_manager.py +5 -3
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ssh_display_diagnostics.py +5 -3
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/tmux_binding_manager.py +3 -7
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/app.py +273 -320
- railmux-0.4.0.dev3/src/railmux/windows_bootstrap.py +253 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3/src/railmux.egg-info}/PKG-INFO +1 -5
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux.egg-info/SOURCES.txt +5 -33
- railmux-0.4.0.dev3/src/railmux.egg-info/entry_points.txt +2 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux.egg-info/requires.txt +0 -4
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_cli.py +0 -15
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_codex_delete.py +3 -7
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_config.py +0 -1
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_discovery.py +0 -24
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_display_transport.py +21 -1
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_local_open.py +0 -26
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_modes.py +0 -2
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_orphan_recovery.py +2 -2
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_release_workflow.py +22 -17
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_runtime_config.py +0 -12
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_soft_quit.py +41 -2
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_tmux_integration.py +9 -2
- railmux-0.4.0.dev3/tests/test_utf8_state.py +111 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_wheel_smoke.py +1 -6
- railmux-0.4.0.dev3/tests/test_windows_bootstrap.py +236 -0
- railmux-0.4.0.dev3/tests/test_windows_wrapper_contract.py +68 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tools/wheel_smoke.py +6 -12
- railmux-0.4.0.dev1/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev1/src/railmux/local_clipboard.py +0 -101
- railmux-0.4.0.dev1/src/railmux/mux/__init__.py +0 -5
- railmux-0.4.0.dev1/src/railmux/mux/backend.py +0 -59
- railmux-0.4.0.dev1/src/railmux/mux/tmux_backend.py +0 -161
- railmux-0.4.0.dev1/src/railmux/platform/__init__.py +0 -44
- railmux-0.4.0.dev1/src/railmux/platform/config_paths.py +0 -30
- railmux-0.4.0.dev1/src/railmux/platform/console.py +0 -165
- railmux-0.4.0.dev1/src/railmux/platform/file_security.py +0 -34
- railmux-0.4.0.dev1/src/railmux/platform/filelock.py +0 -42
- railmux-0.4.0.dev1/src/railmux/platform/multiplex.py +0 -132
- railmux-0.4.0.dev1/src/railmux/platform/process.py +0 -21
- railmux-0.4.0.dev1/src/railmux/platform/runtime_paths.py +0 -75
- railmux-0.4.0.dev1/src/railmux/terminal_emulator.py +0 -180
- railmux-0.4.0.dev1/src/railmux/winlocal/__init__.py +0 -2
- railmux-0.4.0.dev1/src/railmux/winlocal/backend.py +0 -914
- railmux-0.4.0.dev1/src/railmux/winlocal/client.py +0 -299
- railmux-0.4.0.dev1/src/railmux/winlocal/compositor.py +0 -254
- railmux-0.4.0.dev1/src/railmux/winlocal/conpty.py +0 -200
- railmux-0.4.0.dev1/src/railmux/winlocal/daemon.py +0 -398
- railmux-0.4.0.dev1/src/railmux/winlocal/history_viewer.py +0 -93
- railmux-0.4.0.dev1/src/railmux/winlocal/ipc.py +0 -88
- railmux-0.4.0.dev1/src/railmux/winlocal/session_store.py +0 -96
- railmux-0.4.0.dev1/src/railmux/winlocal/virtual_screen.py +0 -105
- railmux-0.4.0.dev1/src/railmux.egg-info/entry_points.txt +0 -2
- railmux-0.4.0.dev1/tests/test_mux_backend.py +0 -60
- railmux-0.4.0.dev1/tests/test_platform.py +0 -248
- railmux-0.4.0.dev1/tests/test_winlocal_backend.py +0 -218
- railmux-0.4.0.dev1/tests/test_winlocal_compositor.py +0 -67
- railmux-0.4.0.dev1/tests/test_winlocal_conpty.py +0 -60
- railmux-0.4.0.dev1/tests/test_winlocal_daemon.py +0 -203
- railmux-0.4.0.dev1/tests/test_winlocal_history_viewer.py +0 -43
- railmux-0.4.0.dev1/tests/test_winlocal_ipc.py +0 -39
- railmux-0.4.0.dev1/tests/test_winlocal_real_conpty.py +0 -94
- railmux-0.4.0.dev1/tests/test_winlocal_session_store.py +0 -42
- railmux-0.4.0.dev1/tests/test_winlocal_virtual_screen.py +0 -62
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/LICENSE +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/README.md +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/setup.cfg +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev1 → railmux-0.4.0.dev3}/tools/release_notes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.4.0.
|
|
3
|
+
Version: 0.4.0.dev3
|
|
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,14 +14,12 @@ 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
|
|
18
17
|
Classifier: Programming Language :: Python :: 3
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.9
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
25
23
|
Classifier: Topic :: Software Development
|
|
26
24
|
Classifier: Topic :: Terminals
|
|
27
25
|
Classifier: Topic :: Utilities
|
|
@@ -32,8 +30,6 @@ Requires-Dist: packaging>=23.0
|
|
|
32
30
|
Requires-Dist: tomlkit<1,>=0.13.2
|
|
33
31
|
Requires-Dist: urwid>=2.6.16
|
|
34
32
|
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"
|
|
37
33
|
Provides-Extra: ssh
|
|
38
34
|
Requires-Dist: pyte<0.9,>=0.8.2; extra == "ssh"
|
|
39
35
|
Provides-Extra: dev
|
|
@@ -20,14 +20,12 @@ classifiers = [
|
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
21
|
"Operating System :: POSIX :: Linux",
|
|
22
22
|
"Operating System :: MacOS",
|
|
23
|
-
"Operating System :: Microsoft :: Windows",
|
|
24
23
|
"Programming Language :: Python :: 3",
|
|
25
24
|
"Programming Language :: Python :: 3.9",
|
|
26
25
|
"Programming Language :: Python :: 3.10",
|
|
27
26
|
"Programming Language :: Python :: 3.11",
|
|
28
27
|
"Programming Language :: Python :: 3.12",
|
|
29
28
|
"Programming Language :: Python :: 3.13",
|
|
30
|
-
"Programming Language :: Python :: 3.14",
|
|
31
29
|
"Topic :: Software Development",
|
|
32
30
|
"Topic :: Terminals",
|
|
33
31
|
"Topic :: Utilities",
|
|
@@ -37,8 +35,6 @@ dependencies = [
|
|
|
37
35
|
"tomlkit>=0.13.2,<1",
|
|
38
36
|
"urwid>=2.6.16",
|
|
39
37
|
"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'",
|
|
42
38
|
]
|
|
43
39
|
|
|
44
40
|
[project.optional-dependencies]
|
|
@@ -54,7 +50,7 @@ dev = [
|
|
|
54
50
|
]
|
|
55
51
|
|
|
56
52
|
[project.scripts]
|
|
57
|
-
railmux = "railmux.
|
|
53
|
+
railmux = "railmux.entrypoint:main"
|
|
58
54
|
|
|
59
55
|
[project.urls]
|
|
60
56
|
Homepage = "https://rightglow.github.io/Railmux/"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev3"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import os
|
|
5
4
|
import shutil
|
|
6
5
|
import subprocess
|
|
7
6
|
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
|
|
14
15
|
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,29 +37,9 @@ _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
|
-
|
|
58
40
|
def _restore_terminal(attributes: list | None) -> None:
|
|
59
41
|
if attributes is None:
|
|
60
42
|
return
|
|
61
|
-
import termios
|
|
62
|
-
|
|
63
43
|
try:
|
|
64
44
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, attributes)
|
|
65
45
|
except (OSError, termios.error):
|
|
@@ -87,8 +67,6 @@ def _run_tmux_client_with_watchdog(
|
|
|
87
67
|
"""Keep one monitor outside tmux so a frozen server cannot trap the TTY."""
|
|
88
68
|
attributes = None
|
|
89
69
|
if sys.stdin.isatty():
|
|
90
|
-
import termios
|
|
91
|
-
|
|
92
70
|
try:
|
|
93
71
|
attributes = termios.tcgetattr(sys.stdin.fileno())
|
|
94
72
|
except (OSError, termios.error):
|
|
@@ -174,10 +152,6 @@ def _run_tmux_client_with_watchdog(
|
|
|
174
152
|
|
|
175
153
|
|
|
176
154
|
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
|
|
181
155
|
raw_args = list(sys.argv[1:] if argv is None else argv)
|
|
182
156
|
if raw_args and raw_args[0] == "config":
|
|
183
157
|
from railmux.config_cli import main as config_main
|
|
@@ -364,18 +338,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
364
338
|
file=sys.stderr,
|
|
365
339
|
)
|
|
366
340
|
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)
|
|
379
341
|
if config.tmux_binary != "tmux":
|
|
380
342
|
tmux_check = check_executable("tmux", config.tmux_binary)
|
|
381
343
|
if not tmux_check.valid:
|
|
@@ -13,15 +13,12 @@ import hashlib
|
|
|
13
13
|
import json
|
|
14
14
|
import math
|
|
15
15
|
import os
|
|
16
|
+
import stat
|
|
16
17
|
import time
|
|
17
18
|
from dataclasses import dataclass, replace
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
from railmux.atomic_file import atomic_write_text
|
|
21
|
-
from railmux.platform.file_security import (
|
|
22
|
-
prepare_private_directory,
|
|
23
|
-
private_regular_file,
|
|
24
|
-
)
|
|
25
22
|
from railmux.models import (
|
|
26
23
|
AttentionCategory,
|
|
27
24
|
AttentionState,
|
|
@@ -460,7 +457,9 @@ class CodexIndex:
|
|
|
460
457
|
try:
|
|
461
458
|
info = path.lstat()
|
|
462
459
|
if (
|
|
463
|
-
not
|
|
460
|
+
not stat.S_ISREG(info.st_mode)
|
|
461
|
+
or info.st_uid != os.getuid()
|
|
462
|
+
or info.st_mode & 0o077
|
|
464
463
|
or info.st_size > _PERSISTENT_CACHE_MAX_BYTES
|
|
465
464
|
):
|
|
466
465
|
return
|
|
@@ -515,7 +514,17 @@ class CodexIndex:
|
|
|
515
514
|
path = self._cache_path
|
|
516
515
|
if path is None:
|
|
517
516
|
return False
|
|
518
|
-
|
|
517
|
+
parent = path.parent
|
|
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
|
|
519
528
|
|
|
520
529
|
def _save_persistent_cache(self) -> None:
|
|
521
530
|
path = self._cache_path
|
|
@@ -6,8 +6,6 @@ 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
|
-
|
|
11
9
|
try:
|
|
12
10
|
import tomllib
|
|
13
11
|
except ModuleNotFoundError: # Python 3.9-3.10
|
|
@@ -57,7 +55,7 @@ class Config:
|
|
|
57
55
|
|
|
58
56
|
|
|
59
57
|
def default_config_path() -> Path:
|
|
60
|
-
return
|
|
58
|
+
return Path.home() / ".config" / "railmux" / "config.toml"
|
|
61
59
|
|
|
62
60
|
|
|
63
61
|
def _table(data: dict[str, Any], name: str) -> dict[str, Any]:
|
|
@@ -7,8 +7,7 @@ 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
|
|
11
|
-
from railmux.platform.config_paths import config_dir
|
|
10
|
+
from railmux.path_codec import decode
|
|
12
11
|
from railmux.session_index import _looks_like_uuid, _scan_session
|
|
13
12
|
|
|
14
13
|
|
|
@@ -38,7 +37,7 @@ def _path_cache_file() -> Path:
|
|
|
38
37
|
Decoding an encoded project name is expensive on an NFS home (see
|
|
39
38
|
path_codec), and the mapping is stable, so we persist it across launches.
|
|
40
39
|
"""
|
|
41
|
-
return
|
|
40
|
+
return Path.home() / ".config" / "railmux" / "path-cache.json"
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
def _load_path_cache() -> dict[str, str]:
|
|
@@ -95,7 +94,7 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
95
94
|
for entry in scan:
|
|
96
95
|
if not entry.is_dir(follow_symlinks=False):
|
|
97
96
|
continue
|
|
98
|
-
if
|
|
97
|
+
if not entry.name.startswith("-"):
|
|
99
98
|
continue
|
|
100
99
|
seen.add(entry.name)
|
|
101
100
|
|
|
@@ -107,16 +106,10 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
107
106
|
if cached is not None and Path(cached).is_dir():
|
|
108
107
|
real_path = Path(cached)
|
|
109
108
|
else:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
else:
|
|
116
|
-
try:
|
|
117
|
-
decoded = decode(entry.name)
|
|
118
|
-
except ValueError:
|
|
119
|
-
pass
|
|
109
|
+
try:
|
|
110
|
+
decoded = decode(entry.name)
|
|
111
|
+
except ValueError:
|
|
112
|
+
decoded = None
|
|
120
113
|
# Skip projects whose original directory no longer exists on
|
|
121
114
|
# this machine (deleted or moved). They can't be launched —
|
|
122
115
|
# `cd` into a missing dir fails — so don't list them, and drop
|
|
@@ -163,58 +156,6 @@ def list_projects(claude_home: Path) -> list[Project]:
|
|
|
163
156
|
return results
|
|
164
157
|
|
|
165
158
|
|
|
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
|
-
|
|
218
159
|
def _count_and_latest_mtime(claude_dir: Path, real_path: Path) -> tuple[int, float]:
|
|
219
160
|
"""Count sessions that pass every ``_scan_session`` filter and return the max
|
|
220
161
|
JSONL mtime. Uses ``_scan_session`` so the count exactly matches the
|
|
@@ -738,6 +738,22 @@ class AgentDisplayTransport:
|
|
|
738
738
|
def _with_phase(state: SwapState, phase: str) -> SwapState:
|
|
739
739
|
return SwapState(**{**asdict(state), "phase": phase})
|
|
740
740
|
|
|
741
|
+
@staticmethod
|
|
742
|
+
def _fit_home_to_display(state: SwapState) -> None:
|
|
743
|
+
"""Best-effort preserve the provider's visible geometry at home.
|
|
744
|
+
|
|
745
|
+
While a real pane is displayed, its named session contains only the
|
|
746
|
+
inert placeholder. Resize that detached home window to the real
|
|
747
|
+
pane's current size before swapping back. Otherwise background output
|
|
748
|
+
is formatted at the placeholder's older (often much narrower) launch
|
|
749
|
+
geometry and later appears as a narrow discontinuity in scrollback.
|
|
750
|
+
|
|
751
|
+
Geometry is presentation-only: failure must never weaken or abort the
|
|
752
|
+
identity-pinned return transaction.
|
|
753
|
+
"""
|
|
754
|
+
tmux_ctl.fit_session_to_pane(
|
|
755
|
+
state.agent_tmux_name, state.agent_pane_id)
|
|
756
|
+
|
|
741
757
|
def park(self, slot: AgentSlot) -> bool:
|
|
742
758
|
"""Move one displayed swap agent home without forgetting its slot.
|
|
743
759
|
|
|
@@ -764,6 +780,7 @@ class AgentDisplayTransport:
|
|
|
764
780
|
parking = self._with_phase(state, "parking")
|
|
765
781
|
if not _write_marker_pair(parking, slot.key):
|
|
766
782
|
return False
|
|
783
|
+
self._fit_home_to_display(state)
|
|
767
784
|
if not tmux_ctl.swap_panes(
|
|
768
785
|
state.agent_pane_id, state.placeholder_pane_id):
|
|
769
786
|
_write_marker_pair(state, slot.key)
|
|
@@ -857,6 +874,7 @@ class AgentDisplayTransport:
|
|
|
857
874
|
return False
|
|
858
875
|
returning = SwapState(**{**asdict(state), "phase": "returning"})
|
|
859
876
|
_write_marker_pair(returning, slot.key)
|
|
877
|
+
self._fit_home_to_display(state)
|
|
860
878
|
if not tmux_ctl.swap_panes(
|
|
861
879
|
state.agent_pane_id, state.placeholder_pane_id):
|
|
862
880
|
return False
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Platform-safe public entry point.
|
|
2
|
+
|
|
3
|
+
Keep this module free of POSIX-only imports. The Windows preview must choose
|
|
4
|
+
its delegated runtime before importing the ordinary Railmux CLI, which owns
|
|
5
|
+
``termios`` and the tmux-backed application.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(
|
|
14
|
+
argv: Sequence[str] | None = None,
|
|
15
|
+
*,
|
|
16
|
+
platform_name: str | None = None,
|
|
17
|
+
) -> int:
|
|
18
|
+
current_platform = os.name if platform_name is None else platform_name
|
|
19
|
+
if current_platform == "nt":
|
|
20
|
+
from railmux.windows_bootstrap import main as windows_main
|
|
21
|
+
|
|
22
|
+
return windows_main(argv)
|
|
23
|
+
|
|
24
|
+
from railmux.cli import main as posix_main
|
|
25
|
+
|
|
26
|
+
return posix_main(None if argv is None else list(argv))
|
|
@@ -14,12 +14,15 @@ import base64
|
|
|
14
14
|
import json
|
|
15
15
|
import os
|
|
16
16
|
import re
|
|
17
|
+
import select
|
|
17
18
|
import selectors
|
|
18
19
|
import shlex
|
|
19
20
|
import shutil
|
|
20
21
|
import subprocess
|
|
21
22
|
import sys
|
|
23
|
+
import termios
|
|
22
24
|
import time
|
|
25
|
+
import tty
|
|
23
26
|
from collections.abc import Callable
|
|
24
27
|
from dataclasses import dataclass
|
|
25
28
|
from enum import Enum
|
|
@@ -78,8 +81,6 @@ from railmux.fast_display_protocol import (
|
|
|
78
81
|
encode_path_open_request,
|
|
79
82
|
encode_resize,
|
|
80
83
|
)
|
|
81
|
-
from railmux.platform.console import RawConsole
|
|
82
|
-
from railmux.platform.multiplex import PortableSelector, wait_readable
|
|
83
84
|
from railmux.pane_surface import render_startup_surface
|
|
84
85
|
from railmux.ssh_compat import CompatibilityFacts, decide as decide_compatibility
|
|
85
86
|
from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
|
|
@@ -319,7 +320,7 @@ def await_remote_attach_status(
|
|
|
319
320
|
readers = [process.stdout.fileno()]
|
|
320
321
|
if cancel_fd is not None:
|
|
321
322
|
readers.append(cancel_fd)
|
|
322
|
-
readable =
|
|
323
|
+
readable, _writable, _exceptional = select.select(readers, [], [], remaining)
|
|
323
324
|
if not readable:
|
|
324
325
|
return RemoteAttachKind.TIMEOUT
|
|
325
326
|
if cancel_fd is not None and cancel_fd in readable:
|
|
@@ -415,7 +416,7 @@ def await_remote_startup(
|
|
|
415
416
|
readers = [process.stdout.fileno()]
|
|
416
417
|
if cancel_fd is not None:
|
|
417
418
|
readers.append(cancel_fd)
|
|
418
|
-
readable =
|
|
419
|
+
readable, _writable, _exceptional = select.select(readers, [], [], remaining)
|
|
419
420
|
if not readable:
|
|
420
421
|
return RemoteStartup(RemoteStartKind.TIMEOUT)
|
|
421
422
|
if cancel_fd is not None and cancel_fd in readable:
|
|
@@ -602,8 +603,20 @@ def _is_soft_keyboard_projection(
|
|
|
602
603
|
)
|
|
603
604
|
|
|
604
605
|
|
|
605
|
-
class RawTerminal
|
|
606
|
-
|
|
606
|
+
class RawTerminal:
|
|
607
|
+
def __init__(self, fd: int) -> None:
|
|
608
|
+
self.fd = fd
|
|
609
|
+
self.saved: Optional[list[object]] = None
|
|
610
|
+
|
|
611
|
+
def __enter__(self) -> "RawTerminal":
|
|
612
|
+
self.saved = termios.tcgetattr(self.fd)
|
|
613
|
+
tty.setraw(self.fd)
|
|
614
|
+
return self
|
|
615
|
+
|
|
616
|
+
def __exit__(self, *_exc: object) -> None:
|
|
617
|
+
if self.saved is not None:
|
|
618
|
+
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.saved)
|
|
619
|
+
self.saved = None
|
|
607
620
|
|
|
608
621
|
|
|
609
622
|
class TerminalSurface:
|
|
@@ -1877,7 +1890,9 @@ def _wait_reconnect_delay(delay: float, cancel_fd: int) -> None:
|
|
|
1877
1890
|
remaining = deadline - time.monotonic()
|
|
1878
1891
|
if remaining <= 0:
|
|
1879
1892
|
return
|
|
1880
|
-
readable =
|
|
1893
|
+
readable, _writable, _exceptional = select.select(
|
|
1894
|
+
[cancel_fd], [], [], remaining
|
|
1895
|
+
)
|
|
1881
1896
|
if readable:
|
|
1882
1897
|
_consume_reconnect_input(cancel_fd)
|
|
1883
1898
|
|
|
@@ -2398,7 +2413,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2398
2413
|
touch_keyboard = TermuxTouchKeyboard(
|
|
2399
2414
|
enabled=not args.no_mouse and termux_touch
|
|
2400
2415
|
)
|
|
2401
|
-
selector =
|
|
2416
|
+
selector = selectors.DefaultSelector()
|
|
2402
2417
|
selector.register(process.stdout.fileno(), selectors.EVENT_READ, "remote")
|
|
2403
2418
|
selector.register(sys.stdin.fileno(), selectors.EVENT_READ, "local")
|
|
2404
2419
|
started = time.monotonic()
|
|
@@ -2943,9 +2958,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2943
2958
|
)
|
|
2944
2959
|
history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
|
|
2945
2960
|
try:
|
|
2946
|
-
with RawTerminal(
|
|
2947
|
-
sys.stdin.fileno(), output_fd=sys.stdout.fileno()
|
|
2948
|
-
):
|
|
2961
|
+
with RawTerminal(sys.stdin.fileno()):
|
|
2949
2962
|
while True:
|
|
2950
2963
|
observed_size = os.get_terminal_size(sys.stdout.fileno())
|
|
2951
2964
|
if observed_size != local_size:
|
|
@@ -83,11 +83,6 @@ 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
|
-
)
|
|
91
86
|
from railmux.ui.workspace import (
|
|
92
87
|
COMPACT_ENTER_HEIGHT,
|
|
93
88
|
COMPACT_ENTER_WIDTH,
|
|
@@ -376,7 +371,9 @@ def _build_extended_pyte(pyte: object) -> object:
|
|
|
376
371
|
|
|
377
372
|
def _extended_pyte(pyte: object) -> object:
|
|
378
373
|
"""Idempotently adapt one imported pyte module for tmux's VT stream."""
|
|
379
|
-
|
|
374
|
+
if getattr(pyte, "_railmux_extended", False):
|
|
375
|
+
return pyte
|
|
376
|
+
return _build_extended_pyte(pyte)
|
|
380
377
|
|
|
381
378
|
|
|
382
379
|
@dataclass(frozen=True)
|
|
@@ -1890,12 +1887,42 @@ def _style_key(char: object) -> tuple[object, ...]:
|
|
|
1890
1887
|
|
|
1891
1888
|
def render_rows(screen: object) -> tuple[bytes, ...]:
|
|
1892
1889
|
"""Render independently paintable rows with allowlisted SGR controls."""
|
|
1893
|
-
|
|
1890
|
+
rendered_rows: list[bytes] = []
|
|
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)
|
|
1894
1916
|
|
|
1895
1917
|
|
|
1896
1918
|
def terminal_modes_for_screen(screen: object) -> TerminalMode:
|
|
1897
1919
|
"""Project pyte's private-mode set onto the bounded v14 wire allowlist."""
|
|
1898
|
-
|
|
1920
|
+
terminal_modes = TerminalMode.NONE
|
|
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
|
|
1899
1926
|
|
|
1900
1927
|
|
|
1901
1928
|
def _child_exited(pid: int) -> bool:
|
|
@@ -10,11 +10,10 @@ 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
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def _favorites_path() -> Path:
|
|
17
|
-
return
|
|
16
|
+
return Path.home() / ".config" / "railmux" / "favorites.json"
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
class Favorites:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Materialise the private, read-only context used by Ask Railmux."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import os
|
|
4
5
|
from importlib import metadata
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from railmux import __version__
|
|
8
9
|
from railmux.atomic_file import atomic_write_text
|
|
9
|
-
from railmux.platform.config_paths import data_dir
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
_FALLBACK_GUIDE = """# Railmux
|
|
@@ -35,7 +35,12 @@ permissions.
|
|
|
35
35
|
|
|
36
36
|
def help_workspace_path() -> Path:
|
|
37
37
|
"""Return Railmux's per-user help workspace, following the XDG spec."""
|
|
38
|
-
|
|
38
|
+
raw = os.environ.get("XDG_DATA_HOME")
|
|
39
|
+
if raw:
|
|
40
|
+
candidate = Path(raw).expanduser()
|
|
41
|
+
if candidate.is_absolute():
|
|
42
|
+
return candidate / "railmux" / "help"
|
|
43
|
+
return Path.home() / ".local" / "share" / "railmux" / "help"
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
def _source_readme() -> str | None:
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Best-effort native clipboard writers for explicit user copy actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def command() -> tuple[str, ...] | None:
|
|
12
|
+
"""Return a trusted platform clipboard command, if one is available."""
|
|
13
|
+
if sys.platform == "darwin":
|
|
14
|
+
if executable := shutil.which("pbcopy"):
|
|
15
|
+
return (executable,)
|
|
16
|
+
return None
|
|
17
|
+
if os.environ.get("WAYLAND_DISPLAY"):
|
|
18
|
+
if executable := shutil.which("wl-copy"):
|
|
19
|
+
return (executable,)
|
|
20
|
+
if os.environ.get("DISPLAY"):
|
|
21
|
+
if executable := shutil.which("xclip"):
|
|
22
|
+
return (executable, "-selection", "clipboard")
|
|
23
|
+
if executable := shutil.which("xsel"):
|
|
24
|
+
return (executable, "--clipboard", "--input")
|
|
25
|
+
if os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP"):
|
|
26
|
+
if executable := shutil.which("clip.exe"):
|
|
27
|
+
return (executable,)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def copy(data: bytes) -> bool:
|
|
32
|
+
"""Write bytes to the local OS clipboard without invoking a shell."""
|
|
33
|
+
writer = command()
|
|
34
|
+
if writer is None:
|
|
35
|
+
return False
|
|
36
|
+
try:
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
writer,
|
|
39
|
+
input=data,
|
|
40
|
+
stdout=subprocess.DEVNULL,
|
|
41
|
+
stderr=subprocess.DEVNULL,
|
|
42
|
+
timeout=2.0,
|
|
43
|
+
check=False,
|
|
44
|
+
)
|
|
45
|
+
except (OSError, subprocess.SubprocessError):
|
|
46
|
+
return False
|
|
47
|
+
return result.returncode == 0
|