railmux 0.3.2.dev4__tar.gz → 0.3.3.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {railmux-0.3.2.dev4/src/railmux.egg-info → railmux-0.3.3.dev1}/PKG-INFO +1 -5
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/pyproject.toml +0 -4
- railmux-0.3.3.dev1/src/railmux/__init__.py +1 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/cli.py +2 -40
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/codex_index.py +15 -6
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/config.py +1 -3
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/discovery.py +7 -66
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/fast_display_client.py +24 -11
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/fast_display_server.py +35 -8
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/favorites.py +1 -2
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/help_workspace.py +7 -2
- railmux-0.3.3.dev1/src/railmux/local_clipboard.py +47 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/local_open.py +10 -37
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/modes.py +0 -8
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/mouse_manager.py +3 -7
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/orphan_marker.py +14 -9
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/renames.py +1 -2
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/restart_state.py +12 -7
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/runtime_config.py +1 -4
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/scroll_manager.py +5 -3
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ssh_display_diagnostics.py +5 -3
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/tmux_binding_manager.py +109 -10
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/tmux_ctl.py +396 -1
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/app.py +393 -319
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1/src/railmux.egg-info}/PKG-INFO +1 -5
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux.egg-info/SOURCES.txt +2 -33
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux.egg-info/requires.txt +0 -4
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_cli.py +0 -15
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_codex_delete.py +3 -7
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_config.py +0 -1
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_discovery.py +0 -24
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_local_open.py +0 -26
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_modes.py +0 -2
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_orphan_recovery.py +2 -2
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_release_workflow.py +14 -9
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_runtime_config.py +0 -12
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_soft_quit.py +2 -2
- railmux-0.3.3.dev1/tests/test_termux_local_touch.py +120 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_tmux_binding_manager.py +81 -4
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_tmux_ctl.py +130 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_tmux_integration.py +159 -9
- railmux-0.3.3.dev1/tests/test_utf8_state.py +111 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_wheel_smoke.py +1 -6
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tools/wheel_smoke.py +6 -12
- railmux-0.3.2.dev4/src/railmux/__init__.py +0 -1
- railmux-0.3.2.dev4/src/railmux/local_clipboard.py +0 -101
- railmux-0.3.2.dev4/src/railmux/mux/__init__.py +0 -5
- railmux-0.3.2.dev4/src/railmux/mux/backend.py +0 -59
- railmux-0.3.2.dev4/src/railmux/mux/tmux_backend.py +0 -161
- railmux-0.3.2.dev4/src/railmux/platform/__init__.py +0 -44
- railmux-0.3.2.dev4/src/railmux/platform/config_paths.py +0 -30
- railmux-0.3.2.dev4/src/railmux/platform/console.py +0 -165
- railmux-0.3.2.dev4/src/railmux/platform/file_security.py +0 -34
- railmux-0.3.2.dev4/src/railmux/platform/filelock.py +0 -42
- railmux-0.3.2.dev4/src/railmux/platform/multiplex.py +0 -132
- railmux-0.3.2.dev4/src/railmux/platform/process.py +0 -21
- railmux-0.3.2.dev4/src/railmux/platform/runtime_paths.py +0 -75
- railmux-0.3.2.dev4/src/railmux/terminal_emulator.py +0 -180
- railmux-0.3.2.dev4/src/railmux/winlocal/__init__.py +0 -2
- railmux-0.3.2.dev4/src/railmux/winlocal/backend.py +0 -897
- railmux-0.3.2.dev4/src/railmux/winlocal/client.py +0 -275
- railmux-0.3.2.dev4/src/railmux/winlocal/compositor.py +0 -254
- railmux-0.3.2.dev4/src/railmux/winlocal/conpty.py +0 -200
- railmux-0.3.2.dev4/src/railmux/winlocal/daemon.py +0 -359
- railmux-0.3.2.dev4/src/railmux/winlocal/history_viewer.py +0 -93
- railmux-0.3.2.dev4/src/railmux/winlocal/ipc.py +0 -88
- railmux-0.3.2.dev4/src/railmux/winlocal/session_store.py +0 -96
- railmux-0.3.2.dev4/src/railmux/winlocal/virtual_screen.py +0 -105
- railmux-0.3.2.dev4/tests/test_mux_backend.py +0 -60
- railmux-0.3.2.dev4/tests/test_platform.py +0 -248
- railmux-0.3.2.dev4/tests/test_winlocal_backend.py +0 -176
- railmux-0.3.2.dev4/tests/test_winlocal_compositor.py +0 -48
- railmux-0.3.2.dev4/tests/test_winlocal_conpty.py +0 -60
- railmux-0.3.2.dev4/tests/test_winlocal_daemon.py +0 -150
- railmux-0.3.2.dev4/tests/test_winlocal_history_viewer.py +0 -43
- railmux-0.3.2.dev4/tests/test_winlocal_ipc.py +0 -39
- railmux-0.3.2.dev4/tests/test_winlocal_real_conpty.py +0 -61
- railmux-0.3.2.dev4/tests/test_winlocal_session_store.py +0 -42
- railmux-0.3.2.dev4/tests/test_winlocal_virtual_screen.py +0 -62
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/LICENSE +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/MANIFEST.in +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/README.md +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/setup.cfg +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/__main__.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/background_index.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/launcher.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/models.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/self_update.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/session_index.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/settings.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/transcript.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_background_index.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_codex_index.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_config_cli.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_display_transport.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_fast_full_window.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_help_agent.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_keymap.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_launcher.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_modals.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_new_project.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_options.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_path_codec.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_release_notes.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_remote_config.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_renames.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_restart_state.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_running_pane.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_running_sort.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_self_update.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_session_cache.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_session_index.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_settings.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_sidebar.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_status.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_statusbar.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_system_deps.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_transcript.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_widgets.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.2.dev4 → railmux-0.3.3.dev1}/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.3.dev1
|
|
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]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.3.dev1"
|
|
@@ -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
|
|
@@ -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
|
|
@@ -87,17 +87,14 @@ class OpenResult:
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def _detached_popen(argv: Sequence[str]) -> None:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
else:
|
|
99
|
-
kwargs["start_new_session"] = True
|
|
100
|
-
subprocess.Popen(list(argv), **kwargs)
|
|
90
|
+
subprocess.Popen(
|
|
91
|
+
list(argv),
|
|
92
|
+
stdin=subprocess.DEVNULL,
|
|
93
|
+
stdout=subprocess.DEVNULL,
|
|
94
|
+
stderr=subprocess.DEVNULL,
|
|
95
|
+
start_new_session=True,
|
|
96
|
+
close_fds=True,
|
|
97
|
+
)
|
|
101
98
|
|
|
102
99
|
|
|
103
100
|
def _url_opener() -> tuple[str, ...] | None:
|
|
@@ -120,17 +117,6 @@ def _url_opener() -> tuple[str, ...] | None:
|
|
|
120
117
|
|
|
121
118
|
def open_url(url: str) -> OpenResult:
|
|
122
119
|
"""Open one already validated HTTP(S) URL without invoking a shell."""
|
|
123
|
-
if os.name == "nt":
|
|
124
|
-
try:
|
|
125
|
-
os.startfile(url)
|
|
126
|
-
except OSError:
|
|
127
|
-
return OpenResult(
|
|
128
|
-
False,
|
|
129
|
-
"Could not open local browser · URL copied",
|
|
130
|
-
"warning",
|
|
131
|
-
url.encode("utf-8"),
|
|
132
|
-
)
|
|
133
|
-
return OpenResult(True, "Opened URL in local browser", "success")
|
|
134
120
|
opener = _url_opener()
|
|
135
121
|
if opener is None:
|
|
136
122
|
return OpenResult(
|
|
@@ -274,13 +260,6 @@ def _wsl_terminal_argv(ssh_argv: Sequence[str]) -> tuple[str, ...] | None:
|
|
|
274
260
|
)
|
|
275
261
|
|
|
276
262
|
|
|
277
|
-
def _windows_terminal_argv(ssh_argv: Sequence[str]) -> tuple[str, ...] | None:
|
|
278
|
-
terminal = shutil.which("wt.exe") or shutil.which("wt")
|
|
279
|
-
if terminal is None:
|
|
280
|
-
return None
|
|
281
|
-
return (terminal, "new-tab", "--", *ssh_argv)
|
|
282
|
-
|
|
283
|
-
|
|
284
263
|
def open_remote_path(
|
|
285
264
|
destination: str,
|
|
286
265
|
*,
|
|
@@ -301,14 +280,8 @@ def open_remote_path(
|
|
|
301
280
|
line=line,
|
|
302
281
|
column=column,
|
|
303
282
|
)
|
|
304
|
-
command = (
|
|
305
|
-
|
|
306
|
-
if os.name == "nt"
|
|
307
|
-
else shlex.join(ssh_argv)
|
|
308
|
-
)
|
|
309
|
-
if os.name == "nt":
|
|
310
|
-
terminal_argv = _windows_terminal_argv(ssh_argv)
|
|
311
|
-
elif sys.platform == "darwin":
|
|
283
|
+
command = shlex.join(ssh_argv)
|
|
284
|
+
if sys.platform == "darwin":
|
|
312
285
|
terminal_argv = _mac_terminal_argv(command)
|
|
313
286
|
elif os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP"):
|
|
314
287
|
terminal_argv = _wsl_terminal_argv(ssh_argv)
|
|
@@ -80,14 +80,6 @@ class ModeRegistry:
|
|
|
80
80
|
None,
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
def for_session_type(self, session_type: str) -> AgentMode | None:
|
|
84
|
-
"""Resolve launch ownership without consulting mutable UI mode state."""
|
|
85
|
-
return next(
|
|
86
|
-
(mode for mode in self._ordered
|
|
87
|
-
if mode.session_type == session_type),
|
|
88
|
-
None,
|
|
89
|
-
)
|
|
90
|
-
|
|
91
83
|
|
|
92
84
|
CLAUDE_MODE = AgentMode(
|
|
93
85
|
key="claude",
|