railmux 0.3.6.dev3__tar.gz → 0.3.6.dev5__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.6.dev3/src/railmux.egg-info → railmux-0.3.6.dev5}/PKG-INFO +4 -2
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/README.md +3 -1
- railmux-0.3.6.dev5/src/railmux/__init__.py +1 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/fast_display_client.py +114 -25
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/fast_display_history.py +6 -9
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/fast_display_server.py +33 -16
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/modals.py +47 -25
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5/src/railmux.egg-info}/PKG-INFO +4 -2
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_fast_full_window.py +135 -32
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_modals.py +23 -11
- railmux-0.3.6.dev3/src/railmux/__init__.py +0 -1
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/LICENSE +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/MANIFEST.in +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/pyproject.toml +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/setup.cfg +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/__main__.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/background_index.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/cli.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/config.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/discovery.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/favorites.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/launcher.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/local_open.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/models.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/modes.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/renames.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/self_update.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/session_index.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/session_lease.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/settings.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/transcript.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/app.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_background_index.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_cli.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_codex_index.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_config.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_config_cli.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_discovery.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_display_transport.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_help_agent.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_keymap.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_launcher.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_local_open.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_modes.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_new_project.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_options.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_path_codec.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_release_notes.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_remote_config.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_renames.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_restart_state.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_running_pane.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_running_sort.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_self_update.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_session_cache.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_session_index.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_session_lease.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_settings.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_sidebar.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_soft_quit.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_status.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_statusbar.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_system_deps.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_tmux_integration.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_transcript.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_widgets.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tools/release_notes.py +0 -0
- {railmux-0.3.6.dev3 → railmux-0.3.6.dev5}/tools/wheel_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.3.6.
|
|
3
|
+
Version: 0.3.6.dev5
|
|
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
|
|
@@ -445,7 +445,9 @@ not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
|
445
445
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
446
446
|
restores the usable workspace when those sessions are still available. A normal
|
|
447
447
|
quit ends all running sessions instead and therefore requires a second
|
|
448
|
-
confirmation
|
|
448
|
+
confirmation. The first chooser has visible mouse buttons, but the final
|
|
449
|
+
destructive confirmation remains keyboard-only; `y` and Enter keep their
|
|
450
|
+
existing confirm meaning in both steps.
|
|
449
451
|
|
|
450
452
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
451
453
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -403,7 +403,9 @@ not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
|
403
403
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
404
404
|
restores the usable workspace when those sessions are still available. A normal
|
|
405
405
|
quit ends all running sessions instead and therefore requires a second
|
|
406
|
-
confirmation
|
|
406
|
+
confirmation. The first chooser has visible mouse buttons, but the final
|
|
407
|
+
destructive confirmation remains keyboard-only; `y` and Enter keep their
|
|
408
|
+
existing confirm meaning in both steps.
|
|
407
409
|
|
|
408
410
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
409
411
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.6.dev5"
|
|
@@ -39,7 +39,7 @@ from railmux.fast_display_history import (
|
|
|
39
39
|
HistoryAction,
|
|
40
40
|
LocalHistoryView,
|
|
41
41
|
PeriodicPrefetchGate,
|
|
42
|
-
|
|
42
|
+
claim_batched_forwarded_wheel,
|
|
43
43
|
input_may_change_routes,
|
|
44
44
|
)
|
|
45
45
|
from railmux.fast_display_input import (
|
|
@@ -1423,6 +1423,73 @@ class TerminalSurface:
|
|
|
1423
1423
|
self.interaction_active = False
|
|
1424
1424
|
|
|
1425
1425
|
|
|
1426
|
+
@dataclass
|
|
1427
|
+
class _DeferredHistoryPaint:
|
|
1428
|
+
"""Collapse one local-input batch into its final history viewport paint."""
|
|
1429
|
+
|
|
1430
|
+
render_history: bool = False
|
|
1431
|
+
restore_live: bool = False
|
|
1432
|
+
|
|
1433
|
+
def defer(self, action: HistoryAction) -> None:
|
|
1434
|
+
self.render_history = self.render_history or action.render_history
|
|
1435
|
+
self.restore_live = self.restore_live or action.restore_live
|
|
1436
|
+
|
|
1437
|
+
def flush(
|
|
1438
|
+
self,
|
|
1439
|
+
surface: TerminalSurface,
|
|
1440
|
+
screen: AppliedScreen | None,
|
|
1441
|
+
overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
|
|
1442
|
+
selection: tuple[SelectionSegment, ...],
|
|
1443
|
+
) -> None:
|
|
1444
|
+
if screen is not None:
|
|
1445
|
+
if self.restore_live:
|
|
1446
|
+
# A viewport may have reached the live bottom and another may
|
|
1447
|
+
# still be frozen. Restore the authoritative frame and compose
|
|
1448
|
+
# only the final overlays in one terminal write.
|
|
1449
|
+
surface.paint(full_repaint(screen), overlays, selection)
|
|
1450
|
+
elif self.render_history:
|
|
1451
|
+
surface.paint_overlays(screen, overlays, selection)
|
|
1452
|
+
self.render_history = False
|
|
1453
|
+
self.restore_live = False
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
class _LocalInputBatch:
|
|
1457
|
+
"""Preserve local wheel distance while bounding forwarded wheel bursts."""
|
|
1458
|
+
|
|
1459
|
+
def __init__(self) -> None:
|
|
1460
|
+
self._forwarded_wheel_directions: set[int] = set()
|
|
1461
|
+
self._history_paint = _DeferredHistoryPaint()
|
|
1462
|
+
|
|
1463
|
+
def prepare(
|
|
1464
|
+
self,
|
|
1465
|
+
event: SgrMouseEvent,
|
|
1466
|
+
action: HistoryAction,
|
|
1467
|
+
) -> tuple[HistoryAction | None, _DeferredHistoryPaint | None]:
|
|
1468
|
+
if event.wheel_direction == 0:
|
|
1469
|
+
return action, None
|
|
1470
|
+
if action.forwarded_input:
|
|
1471
|
+
if not claim_batched_forwarded_wheel(
|
|
1472
|
+
event,
|
|
1473
|
+
self._forwarded_wheel_directions,
|
|
1474
|
+
):
|
|
1475
|
+
return None, None
|
|
1476
|
+
return action, None
|
|
1477
|
+
# Every locally owned history tick has already advanced the viewport.
|
|
1478
|
+
# Defer only its terminal paint so a Windows/RDP read containing
|
|
1479
|
+
# several SGR packets preserves the complete distance without issuing
|
|
1480
|
+
# one synchronous pane repaint per packet.
|
|
1481
|
+
return action, self._history_paint
|
|
1482
|
+
|
|
1483
|
+
def flush(
|
|
1484
|
+
self,
|
|
1485
|
+
surface: TerminalSurface,
|
|
1486
|
+
screen: AppliedScreen | None,
|
|
1487
|
+
overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
|
|
1488
|
+
selection: tuple[SelectionSegment, ...],
|
|
1489
|
+
) -> None:
|
|
1490
|
+
self._history_paint.flush(surface, screen, overlays, selection)
|
|
1491
|
+
|
|
1492
|
+
|
|
1426
1493
|
def _remote_server_args(
|
|
1427
1494
|
*,
|
|
1428
1495
|
session: str,
|
|
@@ -2605,22 +2672,28 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2605
2672
|
if frame is not None:
|
|
2606
2673
|
send_protocol_frame(frame)
|
|
2607
2674
|
|
|
2608
|
-
def apply_history_action(
|
|
2675
|
+
def apply_history_action(
|
|
2676
|
+
action: HistoryAction,
|
|
2677
|
+
deferred_paint: _DeferredHistoryPaint | None = None,
|
|
2678
|
+
) -> None:
|
|
2609
2679
|
nonlocal route_refresh_needed
|
|
2610
2680
|
nonlocal history_info_until, claude_history_prompt_input
|
|
2611
2681
|
overlays = history.overlays()
|
|
2612
|
-
if
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2682
|
+
if deferred_paint is not None:
|
|
2683
|
+
deferred_paint.defer(action)
|
|
2684
|
+
else:
|
|
2685
|
+
if action.restore_live and latest_screen is not None:
|
|
2686
|
+
surface.paint(
|
|
2687
|
+
full_repaint(latest_screen),
|
|
2688
|
+
overlays,
|
|
2689
|
+
selection.segments(),
|
|
2690
|
+
)
|
|
2691
|
+
elif action.render_history and latest_screen is not None:
|
|
2692
|
+
surface.paint_overlays(
|
|
2693
|
+
latest_screen,
|
|
2694
|
+
overlays,
|
|
2695
|
+
selection.segments(),
|
|
2696
|
+
)
|
|
2624
2697
|
if action.protocol_frame:
|
|
2625
2698
|
send_protocol_frame(action.protocol_frame)
|
|
2626
2699
|
if action.forwarded_input:
|
|
@@ -2637,7 +2710,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2637
2710
|
|
|
2638
2711
|
def handle_terminal_part(
|
|
2639
2712
|
part: bytes | SgrMouseEvent,
|
|
2640
|
-
|
|
2713
|
+
input_batch: _LocalInputBatch,
|
|
2641
2714
|
) -> None:
|
|
2642
2715
|
nonlocal route_refresh_needed
|
|
2643
2716
|
nonlocal claude_history_prompt_input, claude_history_pending_choice
|
|
@@ -2900,15 +2973,18 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2900
2973
|
selection_changed_rows(selection_before, selection_after),
|
|
2901
2974
|
)
|
|
2902
2975
|
for replay_event in selection_action.replay_events:
|
|
2903
|
-
if not claim_batched_wheel(replay_event, handled_wheels):
|
|
2904
|
-
continue
|
|
2905
2976
|
replay_action = history.pointer_event(
|
|
2906
2977
|
replay_event,
|
|
2907
2978
|
focused_pane_id,
|
|
2908
2979
|
status_row=status_row,
|
|
2909
2980
|
now=time.monotonic(),
|
|
2910
2981
|
)
|
|
2911
|
-
|
|
2982
|
+
replay_action, deferred_paint = input_batch.prepare(
|
|
2983
|
+
replay_event,
|
|
2984
|
+
replay_action,
|
|
2985
|
+
)
|
|
2986
|
+
if replay_action is not None:
|
|
2987
|
+
apply_history_action(replay_action, deferred_paint)
|
|
2912
2988
|
if selection_action.copy_data is not None:
|
|
2913
2989
|
surface.copy_to_clipboard(selection_action.copy_data)
|
|
2914
2990
|
character_count = len(
|
|
@@ -2944,15 +3020,15 @@ def run(args: argparse.Namespace) -> int:
|
|
|
2944
3020
|
begin_path_open(target)
|
|
2945
3021
|
if selection_action.handled:
|
|
2946
3022
|
return
|
|
2947
|
-
if not claim_batched_wheel(part, handled_wheels):
|
|
2948
|
-
return
|
|
2949
3023
|
action = history.pointer_event(
|
|
2950
3024
|
part,
|
|
2951
3025
|
focused_pane_id,
|
|
2952
3026
|
status_row=status_row,
|
|
2953
3027
|
now=time.monotonic(),
|
|
2954
3028
|
)
|
|
2955
|
-
|
|
3029
|
+
action, deferred_paint = input_batch.prepare(part, action)
|
|
3030
|
+
if action is not None:
|
|
3031
|
+
apply_history_action(action, deferred_paint)
|
|
2956
3032
|
return
|
|
2957
3033
|
if not part:
|
|
2958
3034
|
return
|
|
@@ -3327,19 +3403,32 @@ def run(args: argparse.Namespace) -> int:
|
|
|
3327
3403
|
data, emergency_exit = split_local_escape(data)
|
|
3328
3404
|
if emergency_exit:
|
|
3329
3405
|
local_exit = True
|
|
3330
|
-
|
|
3406
|
+
input_batch = _LocalInputBatch()
|
|
3331
3407
|
for part in terminal_input.feed(data):
|
|
3332
3408
|
if isinstance(part, bytes):
|
|
3333
3409
|
for key_part in split_page_key_input(part):
|
|
3334
|
-
handle_terminal_part(key_part,
|
|
3410
|
+
handle_terminal_part(key_part, input_batch)
|
|
3335
3411
|
else:
|
|
3336
|
-
handle_terminal_part(part,
|
|
3412
|
+
handle_terminal_part(part, input_batch)
|
|
3413
|
+
input_batch.flush(
|
|
3414
|
+
surface,
|
|
3415
|
+
latest_screen,
|
|
3416
|
+
history.overlays(),
|
|
3417
|
+
selection.segments(),
|
|
3418
|
+
)
|
|
3337
3419
|
if local_exit:
|
|
3338
3420
|
break
|
|
3339
3421
|
if not local_exit:
|
|
3340
3422
|
for part in terminal_input.flush_pending():
|
|
3423
|
+
input_batch = _LocalInputBatch()
|
|
3341
3424
|
for key_part in split_page_key_input(part):
|
|
3342
|
-
handle_terminal_part(key_part,
|
|
3425
|
+
handle_terminal_part(key_part, input_batch)
|
|
3426
|
+
input_batch.flush(
|
|
3427
|
+
surface,
|
|
3428
|
+
latest_screen,
|
|
3429
|
+
history.overlays(),
|
|
3430
|
+
selection.segments(),
|
|
3431
|
+
)
|
|
3343
3432
|
now = time.monotonic()
|
|
3344
3433
|
keyboard_projected = touch_keyboard.keyboard_projected
|
|
3345
3434
|
if touch_keyboard.expire(now):
|
|
@@ -1021,18 +1021,15 @@ class LocalHistoryView:
|
|
|
1021
1021
|
return was_active
|
|
1022
1022
|
|
|
1023
1023
|
|
|
1024
|
-
def
|
|
1024
|
+
def claim_batched_forwarded_wheel(
|
|
1025
1025
|
event: SgrMouseEvent,
|
|
1026
1026
|
handled_directions: set[int],
|
|
1027
1027
|
) -> bool:
|
|
1028
|
-
"""Admit at most one wheel tick per direction
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
lines at once. Bounding each read preserves the established one-line
|
|
1034
|
-
local scroll feel without using a timing heuristic; a later read remains
|
|
1035
|
-
immediately eligible.
|
|
1028
|
+
"""Admit at most one forwarded wheel tick per direction in one read.
|
|
1029
|
+
|
|
1030
|
+
Remote sidebar/modal owners need one bounded event rather than a stale
|
|
1031
|
+
backlog. Locally owned agent history must not use this helper: it retains
|
|
1032
|
+
the complete reported distance and coalesces only its terminal paint.
|
|
1036
1033
|
"""
|
|
1037
1034
|
direction = event.wheel_direction
|
|
1038
1035
|
if direction == 0:
|
|
@@ -1171,12 +1171,35 @@ def apply_path_open_request(
|
|
|
1171
1171
|
)
|
|
1172
1172
|
|
|
1173
1173
|
|
|
1174
|
-
def
|
|
1175
|
-
|
|
1174
|
+
def _render_history_lines(
|
|
1175
|
+
pyte: object,
|
|
1176
|
+
lines: Sequence[bytes],
|
|
1177
|
+
width: int,
|
|
1178
|
+
) -> tuple[bytes, ...]:
|
|
1179
|
+
"""Render physical tmux rows while retaining cross-row terminal style.
|
|
1180
|
+
|
|
1181
|
+
``tmux capture-pane -e`` is a terminal stream, not a collection of
|
|
1182
|
+
independently styled strings. In particular, tmux omits an SGR prefix
|
|
1183
|
+
when a row inherits the previous row's foreground, background, or text
|
|
1184
|
+
attributes. Keep one decoder alive for the ordered capture, but clear its
|
|
1185
|
+
one-row cell buffer between rows. :func:`render_rows` then makes every
|
|
1186
|
+
result independently paintable for the local history overlay.
|
|
1187
|
+
"""
|
|
1176
1188
|
screen = pyte.Screen(width, 1)
|
|
1177
1189
|
stream = pyte.ByteStream(screen)
|
|
1178
|
-
|
|
1179
|
-
|
|
1190
|
+
rendered: list[bytes] = []
|
|
1191
|
+
for line in lines:
|
|
1192
|
+
screen.buffer[0].clear()
|
|
1193
|
+
screen.cursor.x = 0
|
|
1194
|
+
screen.cursor.y = 0
|
|
1195
|
+
stream.feed(line)
|
|
1196
|
+
rendered.append(render_rows(screen)[0])
|
|
1197
|
+
return tuple(rendered)
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def _render_history_line(pyte: object, line: bytes, width: int) -> bytes:
|
|
1201
|
+
"""Render one self-contained physical row with allowlisted SGR styling."""
|
|
1202
|
+
return _render_history_lines(pyte, (line,), width)[0]
|
|
1180
1203
|
|
|
1181
1204
|
|
|
1182
1205
|
def _read_transcript_tail(fd: int) -> tuple[str, bool]:
|
|
@@ -1352,7 +1375,8 @@ def _capture_pane_history(
|
|
|
1352
1375
|
# Retain the newest suffix when the styled representation reaches its byte
|
|
1353
1376
|
# budget. Iterate backwards so an oversized response never sacrifices the
|
|
1354
1377
|
# pane's current viewport merely to retain older scrollback.
|
|
1355
|
-
|
|
1378
|
+
rendered_raw = _render_history_lines(pyte, raw_lines, pane.width)
|
|
1379
|
+
current_raw = rendered_raw[-pane.height :]
|
|
1356
1380
|
transcript_entry = None
|
|
1357
1381
|
# ``None`` is retained only for old/test geometry that predates the
|
|
1358
1382
|
# provider field; every validated live marker carries an explicit value.
|
|
@@ -1383,21 +1407,14 @@ def _capture_pane_history(
|
|
|
1383
1407
|
transcript_lines = transcript_entry.rows
|
|
1384
1408
|
history_count = max(0, max_lines - pane.height)
|
|
1385
1409
|
source_lines = (
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
else ()
|
|
1389
|
-
) + tuple((line, False) for line in current_raw)
|
|
1410
|
+
transcript_lines[-history_count:] if history_count else ()
|
|
1411
|
+
) + current_raw
|
|
1390
1412
|
else:
|
|
1391
|
-
source_lines =
|
|
1413
|
+
source_lines = rendered_raw
|
|
1392
1414
|
newest_first: list[bytes] = []
|
|
1393
1415
|
packed_size = 2 # history line-count prefix
|
|
1394
1416
|
budget_truncated = False
|
|
1395
|
-
for
|
|
1396
|
-
rendered = (
|
|
1397
|
-
raw_line
|
|
1398
|
-
if rendered_already
|
|
1399
|
-
else _render_history_line(pyte, raw_line, pane.width)
|
|
1400
|
-
)
|
|
1417
|
+
for rendered in reversed(source_lines[-max_lines:]):
|
|
1401
1418
|
line_size = 4 + len(rendered)
|
|
1402
1419
|
if packed_size + line_size > _HISTORY_SNAPSHOT_RAW_BUDGET:
|
|
1403
1420
|
budget_truncated = True
|
|
@@ -44,6 +44,29 @@ def _action_legend(
|
|
|
44
44
|
return urwid.Text(markup, align=align, wrap=wrap)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _action_button(
|
|
48
|
+
keys: str,
|
|
49
|
+
description: str,
|
|
50
|
+
*,
|
|
51
|
+
on_click: Callable[[], None],
|
|
52
|
+
click_key: str,
|
|
53
|
+
) -> ClickableRow:
|
|
54
|
+
"""Render a terminal-safe filled action button with a mouse target."""
|
|
55
|
+
label = urwid.Text(
|
|
56
|
+
[
|
|
57
|
+
("btn_key", keys),
|
|
58
|
+
("btn_label", f" = {description}"),
|
|
59
|
+
],
|
|
60
|
+
align="center",
|
|
61
|
+
wrap="space",
|
|
62
|
+
)
|
|
63
|
+
return ClickableRow(
|
|
64
|
+
urwid.AttrMap(label, "btn_label"),
|
|
65
|
+
on_click=on_click,
|
|
66
|
+
click_key=click_key,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
47
70
|
def _attention_lines(attention: AttentionState | None) -> list:
|
|
48
71
|
"""Compact, generic rendering for known and future attention categories."""
|
|
49
72
|
if attention is None:
|
|
@@ -141,38 +164,37 @@ class QuitConfirmModal(urwid.WidgetWrap):
|
|
|
141
164
|
summary = "No running sessions."
|
|
142
165
|
|
|
143
166
|
action_rows: list[urwid.Widget] = [
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
_action_button(
|
|
168
|
+
"y / ↵",
|
|
169
|
+
"hard quit (requires final confirmation)",
|
|
170
|
+
on_click=on_confirm,
|
|
171
|
+
click_key="quit:hard",
|
|
148
172
|
),
|
|
149
173
|
]
|
|
150
174
|
if on_soft_quit is not None:
|
|
151
|
-
action_rows.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
wrap="space",
|
|
157
|
-
),
|
|
175
|
+
action_rows.extend([
|
|
176
|
+
urwid.Divider(),
|
|
177
|
+
_action_button(
|
|
178
|
+
"s",
|
|
179
|
+
"soft quit (keep sessions alive)",
|
|
158
180
|
on_click=on_soft_quit,
|
|
159
181
|
click_key="quit:soft",
|
|
160
|
-
)
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
action_rows.append(
|
|
168
|
-
ClickableRow(
|
|
169
|
-
cancel_row,
|
|
182
|
+
),
|
|
183
|
+
])
|
|
184
|
+
action_rows.append(urwid.Divider())
|
|
185
|
+
if on_cancel is not None:
|
|
186
|
+
action_rows.append(_action_button(
|
|
187
|
+
"n / Esc",
|
|
188
|
+
"cancel",
|
|
170
189
|
on_click=on_cancel,
|
|
171
190
|
click_key="quit:cancel",
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
))
|
|
192
|
+
else:
|
|
193
|
+
action_rows.append(_action_legend(
|
|
194
|
+
[("n / Esc", "cancel")],
|
|
195
|
+
align="center",
|
|
196
|
+
wrap="space",
|
|
197
|
+
))
|
|
176
198
|
self._title = urwid.Text("Quit railmux?", align="center")
|
|
177
199
|
self._summary = urwid.Text(("live", summary), align="center")
|
|
178
200
|
self._shared_notice = (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.3.6.
|
|
3
|
+
Version: 0.3.6.dev5
|
|
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
|
|
@@ -445,7 +445,9 @@ not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
|
445
445
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
446
446
|
restores the usable workspace when those sessions are still available. A normal
|
|
447
447
|
quit ends all running sessions instead and therefore requires a second
|
|
448
|
-
confirmation
|
|
448
|
+
confirmation. The first chooser has visible mouse buttons, but the final
|
|
449
|
+
destructive confirmation remains keyboard-only; `y` and Enter keep their
|
|
450
|
+
existing confirm meaning in both steps.
|
|
449
451
|
|
|
450
452
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
451
453
|
Soft Quit therefore closes that shared UI for every attached view, although it
|