railmux 0.3.3.dev2__tar.gz → 0.4.0.dev2__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.3.dev2/src/railmux.egg-info → railmux-0.4.0.dev2}/PKG-INFO +5 -1
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/pyproject.toml +4 -0
- railmux-0.4.0.dev2/src/railmux/__init__.py +1 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/cli.py +40 -2
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/codex_index.py +6 -15
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/config.py +3 -1
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/discovery.py +66 -7
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/display_transport.py +0 -18
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/fast_display_client.py +11 -24
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/fast_display_server.py +8 -35
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/favorites.py +2 -1
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/help_workspace.py +2 -7
- railmux-0.4.0.dev2/src/railmux/local_clipboard.py +101 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/local_open.py +37 -10
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/modes.py +8 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/mouse_manager.py +7 -3
- railmux-0.4.0.dev2/src/railmux/mux/__init__.py +7 -0
- railmux-0.4.0.dev2/src/railmux/mux/backend.py +75 -0
- railmux-0.4.0.dev2/src/railmux/mux/tmux_backend.py +165 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/orphan_marker.py +9 -14
- railmux-0.4.0.dev2/src/railmux/platform/__init__.py +44 -0
- railmux-0.4.0.dev2/src/railmux/platform/config_paths.py +30 -0
- railmux-0.4.0.dev2/src/railmux/platform/console.py +165 -0
- railmux-0.4.0.dev2/src/railmux/platform/file_security.py +34 -0
- railmux-0.4.0.dev2/src/railmux/platform/filelock.py +42 -0
- railmux-0.4.0.dev2/src/railmux/platform/multiplex.py +132 -0
- railmux-0.4.0.dev2/src/railmux/platform/process.py +21 -0
- railmux-0.4.0.dev2/src/railmux/platform/runtime_paths.py +75 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/renames.py +2 -1
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/restart_state.py +7 -12
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/runtime_config.py +4 -1
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/scroll_manager.py +3 -5
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ssh_display_diagnostics.py +3 -5
- railmux-0.4.0.dev2/src/railmux/terminal_emulator.py +180 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/tmux_binding_manager.py +7 -3
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/app.py +336 -273
- railmux-0.4.0.dev2/src/railmux/ui/status_chrome.py +149 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/__init__.py +2 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/backend.py +1088 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/client.py +299 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/compositor.py +287 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/conpty.py +200 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/daemon.py +398 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/history_viewer.py +93 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/ipc.py +88 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/session_store.py +96 -0
- railmux-0.4.0.dev2/src/railmux/winlocal/virtual_screen.py +105 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2/src/railmux.egg-info}/PKG-INFO +5 -1
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux.egg-info/SOURCES.txt +36 -1
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux.egg-info/requires.txt +4 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_cli.py +15 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_codex_delete.py +7 -3
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_config.py +1 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_discovery.py +24 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_display_transport.py +1 -21
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_local_open.py +26 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_modes.py +2 -0
- railmux-0.4.0.dev2/tests/test_mux_backend.py +60 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_orphan_recovery.py +2 -2
- railmux-0.4.0.dev2/tests/test_platform.py +248 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_release_workflow.py +9 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_runtime_config.py +12 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_soft_quit.py +2 -41
- railmux-0.4.0.dev2/tests/test_status_chrome.py +87 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_tmux_integration.py +2 -9
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_wheel_smoke.py +6 -1
- railmux-0.4.0.dev2/tests/test_windows_parity_contract.py +79 -0
- railmux-0.4.0.dev2/tests/test_winlocal_backend.py +577 -0
- railmux-0.4.0.dev2/tests/test_winlocal_compositor.py +67 -0
- railmux-0.4.0.dev2/tests/test_winlocal_conpty.py +60 -0
- railmux-0.4.0.dev2/tests/test_winlocal_daemon.py +203 -0
- railmux-0.4.0.dev2/tests/test_winlocal_history_viewer.py +43 -0
- railmux-0.4.0.dev2/tests/test_winlocal_ipc.py +39 -0
- railmux-0.4.0.dev2/tests/test_winlocal_real_conpty.py +94 -0
- railmux-0.4.0.dev2/tests/test_winlocal_session_store.py +42 -0
- railmux-0.4.0.dev2/tests/test_winlocal_virtual_screen.py +62 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tools/wheel_smoke.py +12 -6
- railmux-0.3.3.dev2/src/railmux/__init__.py +0 -1
- railmux-0.3.3.dev2/src/railmux/local_clipboard.py +0 -47
- railmux-0.3.3.dev2/tests/test_utf8_state.py +0 -111
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/LICENSE +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/MANIFEST.in +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/README.md +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/setup.cfg +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/__main__.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/background_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/launcher.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/models.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/self_update.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/session_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/settings.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/transcript.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_background_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_codex_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_config_cli.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_fast_full_window.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_help_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_keymap.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_launcher.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_modals.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_new_project.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_options.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_path_codec.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_release_notes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_remote_config.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_renames.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_restart_state.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_running_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_running_sort.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_self_update.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_session_cache.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_session_index.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_settings.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_sidebar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_statusbar.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_system_deps.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_transcript.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_widgets.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev2 → railmux-0.4.0.dev2}/tools/release_notes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0.dev2
|
|
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
|
|
@@ -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.4.0.dev2"
|
|
@@ -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]:
|
|
@@ -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,7 +38,7 @@ 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]:
|
|
@@ -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
|
|
@@ -738,22 +738,6 @@ 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
|
-
|
|
757
741
|
def park(self, slot: AgentSlot) -> bool:
|
|
758
742
|
"""Move one displayed swap agent home without forgetting its slot.
|
|
759
743
|
|
|
@@ -780,7 +764,6 @@ class AgentDisplayTransport:
|
|
|
780
764
|
parking = self._with_phase(state, "parking")
|
|
781
765
|
if not _write_marker_pair(parking, slot.key):
|
|
782
766
|
return False
|
|
783
|
-
self._fit_home_to_display(state)
|
|
784
767
|
if not tmux_ctl.swap_panes(
|
|
785
768
|
state.agent_pane_id, state.placeholder_pane_id):
|
|
786
769
|
_write_marker_pair(state, slot.key)
|
|
@@ -874,7 +857,6 @@ class AgentDisplayTransport:
|
|
|
874
857
|
return False
|
|
875
858
|
returning = SwapState(**{**asdict(state), "phase": "returning"})
|
|
876
859
|
_write_marker_pair(returning, slot.key)
|
|
877
|
-
self._fit_home_to_display(state)
|
|
878
860
|
if not tmux_ctl.swap_panes(
|
|
879
861
|
state.agent_pane_id, state.placeholder_pane_id):
|
|
880
862
|
return False
|
|
@@ -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
|
|
@@ -81,6 +78,8 @@ from railmux.fast_display_protocol import (
|
|
|
81
78
|
encode_path_open_request,
|
|
82
79
|
encode_resize,
|
|
83
80
|
)
|
|
81
|
+
from railmux.platform.console import RawConsole
|
|
82
|
+
from railmux.platform.multiplex import PortableSelector, wait_readable
|
|
84
83
|
from railmux.pane_surface import render_startup_surface
|
|
85
84
|
from railmux.ssh_compat import CompatibilityFacts, decide as decide_compatibility
|
|
86
85
|
from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
|
|
@@ -320,7 +319,7 @@ def await_remote_attach_status(
|
|
|
320
319
|
readers = [process.stdout.fileno()]
|
|
321
320
|
if cancel_fd is not None:
|
|
322
321
|
readers.append(cancel_fd)
|
|
323
|
-
readable
|
|
322
|
+
readable = wait_readable(readers, remaining)
|
|
324
323
|
if not readable:
|
|
325
324
|
return RemoteAttachKind.TIMEOUT
|
|
326
325
|
if cancel_fd is not None and cancel_fd in readable:
|
|
@@ -416,7 +415,7 @@ def await_remote_startup(
|
|
|
416
415
|
readers = [process.stdout.fileno()]
|
|
417
416
|
if cancel_fd is not None:
|
|
418
417
|
readers.append(cancel_fd)
|
|
419
|
-
readable
|
|
418
|
+
readable = wait_readable(readers, remaining)
|
|
420
419
|
if not readable:
|
|
421
420
|
return RemoteStartup(RemoteStartKind.TIMEOUT)
|
|
422
421
|
if cancel_fd is not None and cancel_fd in readable:
|
|
@@ -603,20 +602,8 @@ def _is_soft_keyboard_projection(
|
|
|
603
602
|
)
|
|
604
603
|
|
|
605
604
|
|
|
606
|
-
class RawTerminal:
|
|
607
|
-
|
|
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
|
|
605
|
+
class RawTerminal(RawConsole):
|
|
606
|
+
"""Backward-compatible name for the platform raw-console port."""
|
|
620
607
|
|
|
621
608
|
|
|
622
609
|
class TerminalSurface:
|
|
@@ -1890,9 +1877,7 @@ def _wait_reconnect_delay(delay: float, cancel_fd: int) -> None:
|
|
|
1890
1877
|
remaining = deadline - time.monotonic()
|
|
1891
1878
|
if remaining <= 0:
|
|
1892
1879
|
return
|
|
1893
|
-
readable
|
|
1894
|
-
[cancel_fd], [], [], remaining
|
|
1895
|
-
)
|
|
1880
|
+
readable = wait_readable([cancel_fd], remaining)
|
|
1896
1881
|
if readable:
|
|
1897
1882
|
_consume_reconnect_input(cancel_fd)
|
|
1898
1883
|
|
|
@@ -2413,7 +2398,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2413
2398
|
touch_keyboard = TermuxTouchKeyboard(
|
|
2414
2399
|
enabled=not args.no_mouse and termux_touch
|
|
2415
2400
|
)
|
|
2416
|
-
selector =
|
|
2401
|
+
selector = PortableSelector()
|
|
2417
2402
|
selector.register(process.stdout.fileno(), selectors.EVENT_READ, "remote")
|
|
2418
2403
|
selector.register(sys.stdin.fileno(), selectors.EVENT_READ, "local")
|
|
2419
2404
|
started = time.monotonic()
|
|
@@ -2958,7 +2943,9 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2958
2943
|
)
|
|
2959
2944
|
history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
|
|
2960
2945
|
try:
|
|
2961
|
-
with RawTerminal(
|
|
2946
|
+
with RawTerminal(
|
|
2947
|
+
sys.stdin.fileno(), output_fd=sys.stdout.fileno()
|
|
2948
|
+
):
|
|
2962
2949
|
while True:
|
|
2963
2950
|
observed_size = os.get_terminal_size(sys.stdout.fileno())
|
|
2964
2951
|
if observed_size != local_size:
|
|
@@ -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:
|
|
@@ -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
|
|
5
4
|
from importlib import metadata
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
from railmux import __version__
|
|
9
8
|
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,12 +35,7 @@ permissions.
|
|
|
35
35
|
|
|
36
36
|
def help_workspace_path() -> Path:
|
|
37
37
|
"""Return Railmux's per-user help workspace, following the XDG spec."""
|
|
38
|
-
|
|
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"
|
|
38
|
+
return data_dir() / "help"
|
|
44
39
|
|
|
45
40
|
|
|
46
41
|
def _source_readme() -> str | None:
|
|
@@ -0,0 +1,101 @@
|
|
|
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 os.name == "nt":
|
|
14
|
+
return None
|
|
15
|
+
if sys.platform == "darwin":
|
|
16
|
+
if executable := shutil.which("pbcopy"):
|
|
17
|
+
return (executable,)
|
|
18
|
+
return None
|
|
19
|
+
if os.environ.get("WAYLAND_DISPLAY"):
|
|
20
|
+
if executable := shutil.which("wl-copy"):
|
|
21
|
+
return (executable,)
|
|
22
|
+
if os.environ.get("DISPLAY"):
|
|
23
|
+
if executable := shutil.which("xclip"):
|
|
24
|
+
return (executable, "-selection", "clipboard")
|
|
25
|
+
if executable := shutil.which("xsel"):
|
|
26
|
+
return (executable, "--clipboard", "--input")
|
|
27
|
+
if os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP"):
|
|
28
|
+
if executable := shutil.which("clip.exe"):
|
|
29
|
+
return (executable,)
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def copy(data: bytes) -> bool:
|
|
34
|
+
"""Write bytes to the local OS clipboard without invoking a shell."""
|
|
35
|
+
if os.name == "nt":
|
|
36
|
+
return _copy_windows(data)
|
|
37
|
+
writer = command()
|
|
38
|
+
if writer is None:
|
|
39
|
+
return False
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
writer,
|
|
43
|
+
input=data,
|
|
44
|
+
stdout=subprocess.DEVNULL,
|
|
45
|
+
stderr=subprocess.DEVNULL,
|
|
46
|
+
timeout=2.0,
|
|
47
|
+
check=False,
|
|
48
|
+
)
|
|
49
|
+
except (OSError, subprocess.SubprocessError):
|
|
50
|
+
return False
|
|
51
|
+
return result.returncode == 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _copy_windows(data: bytes) -> bool:
|
|
55
|
+
"""Write bounded UTF-8 data as CF_UNICODETEXT using the Win32 API."""
|
|
56
|
+
import ctypes
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
text = data.decode("utf-8")
|
|
60
|
+
except UnicodeDecodeError:
|
|
61
|
+
return False
|
|
62
|
+
if "\x00" in text:
|
|
63
|
+
return False
|
|
64
|
+
encoded = (text + "\x00").encode("utf-16-le")
|
|
65
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
66
|
+
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
|
67
|
+
kernel32.GlobalAlloc.argtypes = (ctypes.c_uint, ctypes.c_size_t)
|
|
68
|
+
kernel32.GlobalAlloc.restype = ctypes.c_void_p
|
|
69
|
+
kernel32.GlobalLock.argtypes = (ctypes.c_void_p,)
|
|
70
|
+
kernel32.GlobalLock.restype = ctypes.c_void_p
|
|
71
|
+
kernel32.GlobalUnlock.argtypes = (ctypes.c_void_p,)
|
|
72
|
+
kernel32.GlobalFree.argtypes = (ctypes.c_void_p,)
|
|
73
|
+
user32.SetClipboardData.argtypes = (ctypes.c_uint, ctypes.c_void_p)
|
|
74
|
+
user32.SetClipboardData.restype = ctypes.c_void_p
|
|
75
|
+
movable = 0x0002
|
|
76
|
+
handle = kernel32.GlobalAlloc(movable, len(encoded))
|
|
77
|
+
if not handle:
|
|
78
|
+
return False
|
|
79
|
+
owned_by_clipboard = False
|
|
80
|
+
try:
|
|
81
|
+
pointer = kernel32.GlobalLock(handle)
|
|
82
|
+
if not pointer:
|
|
83
|
+
return False
|
|
84
|
+
try:
|
|
85
|
+
ctypes.memmove(pointer, encoded, len(encoded))
|
|
86
|
+
finally:
|
|
87
|
+
kernel32.GlobalUnlock(handle)
|
|
88
|
+
if not user32.OpenClipboard(None):
|
|
89
|
+
return False
|
|
90
|
+
try:
|
|
91
|
+
if not user32.EmptyClipboard():
|
|
92
|
+
return False
|
|
93
|
+
if not user32.SetClipboardData(13, handle): # CF_UNICODETEXT
|
|
94
|
+
return False
|
|
95
|
+
owned_by_clipboard = True
|
|
96
|
+
return True
|
|
97
|
+
finally:
|
|
98
|
+
user32.CloseClipboard()
|
|
99
|
+
finally:
|
|
100
|
+
if not owned_by_clipboard:
|
|
101
|
+
kernel32.GlobalFree(handle)
|