railmux 0.3.6.dev4__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.dev4/src/railmux.egg-info → railmux-0.3.6.dev5}/PKG-INFO +4 -2
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/README.md +3 -1
- railmux-0.3.6.dev5/src/railmux/__init__.py +1 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/fast_display_client.py +114 -25
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/fast_display_history.py +6 -9
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/modals.py +47 -25
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5/src/railmux.egg-info}/PKG-INFO +4 -2
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_fast_full_window.py +87 -14
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_modals.py +23 -11
- railmux-0.3.6.dev4/src/railmux/__init__.py +0 -1
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/LICENSE +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/MANIFEST.in +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/pyproject.toml +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/setup.cfg +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/__main__.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/background_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/cli.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/discovery.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/favorites.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/launcher.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/local_open.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/models.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/modes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/renames.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/self_update.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/session_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/session_lease.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/settings.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/transcript.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/app.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_background_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_cli.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_codex_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_codex_lineage.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_config_cli.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_discovery.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_display_transport.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_focus_highlight.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_help_agent.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_keymap.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_launcher.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_local_open.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_modes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_new_project.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_options.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_path_codec.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_release_notes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_remote_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_renames.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_restart_state.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_running_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_running_sort.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_self_update.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_session_cache.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_session_index.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_session_lease.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_settings.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_sidebar.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_soft_quit.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_status.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_statusbar.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_system_deps.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_tmux_integration.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_transcript.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_widgets.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev4 → railmux-0.3.6.dev5}/tools/release_notes.py +0 -0
- {railmux-0.3.6.dev4 → 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:
|
|
@@ -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
|
|
@@ -95,7 +95,6 @@ from railmux.fast_display_history import (
|
|
|
95
95
|
HistoryAction,
|
|
96
96
|
LocalHistoryView,
|
|
97
97
|
PeriodicPrefetchGate,
|
|
98
|
-
claim_batched_wheel,
|
|
99
98
|
input_may_change_routes,
|
|
100
99
|
)
|
|
101
100
|
from railmux.fast_display_input import (
|
|
@@ -4197,7 +4196,7 @@ def test_history_content_cache_keeps_only_recent_pane_lifetimes():
|
|
|
4197
4196
|
assert tuple(view.content_cache) == tuple(pane_ids[-8:])
|
|
4198
4197
|
|
|
4199
4198
|
|
|
4200
|
-
def
|
|
4199
|
+
def test_local_history_wheel_batch_preserves_distance_with_one_paint():
|
|
4201
4200
|
view = LocalHistoryView()
|
|
4202
4201
|
prefetch = InputFrameDecoder().feed(view.begin_prefetch(1.0))[0]
|
|
4203
4202
|
request_id, _limit = decode_history_prefetch(prefetch.data)
|
|
@@ -4208,26 +4207,100 @@ def test_local_history_wheel_burst_is_bounded_before_it_moves_viewport():
|
|
|
4208
4207
|
0,
|
|
4209
4208
|
30,
|
|
4210
4209
|
3,
|
|
4211
|
-
tuple(f"line-{index}".encode() for index in range(
|
|
4210
|
+
tuple(f"line-{index}".encode() for index in range(20)),
|
|
4211
|
+
)
|
|
4212
|
+
view.accept_prefetch(HistoryBatch(request_id, (route,)))
|
|
4213
|
+
batch = fast_display_client._LocalInputBatch()
|
|
4214
|
+
events = TerminalInputDecoder().feed(b"\x1b[<64;5;2M" * 8)
|
|
4215
|
+
assert len(events) == 8
|
|
4216
|
+
for event in events:
|
|
4217
|
+
assert isinstance(event, SgrMouseEvent)
|
|
4218
|
+
action, deferred_paint = batch.prepare(event, view.pointer_event(event))
|
|
4219
|
+
assert action is not None
|
|
4220
|
+
assert deferred_paint is not None
|
|
4221
|
+
deferred_paint.defer(action)
|
|
4222
|
+
|
|
4223
|
+
assert view.viewports["%8"].offset == 8
|
|
4224
|
+
surface = MagicMock(spec=TerminalSurface)
|
|
4225
|
+
screen = MagicMock(spec=AppliedScreen)
|
|
4226
|
+
overlays = view.overlays()
|
|
4227
|
+
|
|
4228
|
+
batch.flush(surface, screen, overlays, ())
|
|
4229
|
+
|
|
4230
|
+
surface.paint_overlays.assert_called_once_with(screen, overlays, ())
|
|
4231
|
+
surface.paint.assert_not_called()
|
|
4232
|
+
|
|
4233
|
+
|
|
4234
|
+
def test_local_history_wheel_batch_restores_live_once_at_bottom():
|
|
4235
|
+
view = LocalHistoryView()
|
|
4236
|
+
prefetch = InputFrameDecoder().feed(view.begin_prefetch(1.0))[0]
|
|
4237
|
+
request_id, _limit = decode_history_prefetch(prefetch.data)
|
|
4238
|
+
route = HistorySnapshot(
|
|
4239
|
+
request_id,
|
|
4240
|
+
"%8",
|
|
4241
|
+
0,
|
|
4242
|
+
0,
|
|
4243
|
+
30,
|
|
4244
|
+
3,
|
|
4245
|
+
tuple(f"line-{index}".encode() for index in range(20)),
|
|
4212
4246
|
)
|
|
4213
4247
|
view.accept_prefetch(HistoryBatch(request_id, (route,)))
|
|
4214
4248
|
up = SgrMouseEvent(b"up", 64, 5, 2, True)
|
|
4249
|
+
for _ in range(3):
|
|
4250
|
+
view.pointer_event(up)
|
|
4251
|
+
assert view.viewports["%8"].offset == 3
|
|
4252
|
+
|
|
4253
|
+
batch = fast_display_client._LocalInputBatch()
|
|
4215
4254
|
down = SgrMouseEvent(b"down", 65, 5, 2, True)
|
|
4216
4255
|
for _ in range(5):
|
|
4217
|
-
view.
|
|
4218
|
-
|
|
4256
|
+
action, deferred_paint = batch.prepare(down, view.pointer_event(down))
|
|
4257
|
+
assert action is not None
|
|
4258
|
+
assert deferred_paint is not None
|
|
4259
|
+
deferred_paint.defer(action)
|
|
4260
|
+
assert not view.active
|
|
4261
|
+
|
|
4262
|
+
screen = ScreenModel().apply(
|
|
4263
|
+
ClientScreenUpdateDecoder().feed(
|
|
4264
|
+
encode_update(_keyframe(width=30, height=3))
|
|
4265
|
+
)[0],
|
|
4266
|
+
os.terminal_size((30, 3)),
|
|
4267
|
+
)
|
|
4268
|
+
assert screen is not None
|
|
4269
|
+
surface = MagicMock(spec=TerminalSurface)
|
|
4270
|
+
batch.flush(surface, screen, view.overlays(), ())
|
|
4271
|
+
|
|
4272
|
+
surface.paint.assert_called_once()
|
|
4273
|
+
surface.paint_overlays.assert_not_called()
|
|
4274
|
+
|
|
4275
|
+
|
|
4276
|
+
def test_forwarded_wheel_batch_remains_bounded_without_local_paint():
|
|
4277
|
+
batch = fast_display_client._LocalInputBatch()
|
|
4278
|
+
down = SgrMouseEvent(b"down", 65, 5, 2, True)
|
|
4279
|
+
up = SgrMouseEvent(b"up", 64, 5, 2, True)
|
|
4280
|
+
admitted = []
|
|
4219
4281
|
|
|
4220
|
-
handled: set[int] = set()
|
|
4221
4282
|
for _ in range(8):
|
|
4222
|
-
|
|
4223
|
-
|
|
4283
|
+
action, deferred_paint = batch.prepare(
|
|
4284
|
+
down,
|
|
4285
|
+
HistoryAction(forwarded_input=down.raw),
|
|
4286
|
+
)
|
|
4287
|
+
if action is not None:
|
|
4288
|
+
admitted.append(action.forwarded_input)
|
|
4289
|
+
assert deferred_paint is None
|
|
4290
|
+
action, deferred_paint = batch.prepare(
|
|
4291
|
+
up,
|
|
4292
|
+
HistoryAction(forwarded_input=up.raw),
|
|
4293
|
+
)
|
|
4294
|
+
assert action is not None
|
|
4295
|
+
admitted.append(action.forwarded_input)
|
|
4296
|
+
assert deferred_paint is None
|
|
4224
4297
|
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
assert
|
|
4229
|
-
|
|
4230
|
-
|
|
4298
|
+
surface = MagicMock(spec=TerminalSurface)
|
|
4299
|
+
batch.flush(surface, MagicMock(spec=AppliedScreen), (), ())
|
|
4300
|
+
|
|
4301
|
+
assert admitted == [b"down", b"up"]
|
|
4302
|
+
surface.paint.assert_not_called()
|
|
4303
|
+
surface.paint_overlays.assert_not_called()
|
|
4231
4304
|
|
|
4232
4305
|
|
|
4233
4306
|
def test_only_bounded_layout_and_modal_keys_invalidate_live_routes():
|
|
@@ -148,7 +148,9 @@ def test_modal_action_legends_use_high_contrast_attribute(tmp_path):
|
|
|
148
148
|
]
|
|
149
149
|
|
|
150
150
|
for modal in modals:
|
|
151
|
-
assert "modal_key"
|
|
151
|
+
assert {"modal_key", "btn_key"} & _rendered_attrs(modal), (
|
|
152
|
+
type(modal).__name__
|
|
153
|
+
)
|
|
152
154
|
|
|
153
155
|
|
|
154
156
|
def test_quit_confirm_wraps_all_choices_in_narrow_sidebar():
|
|
@@ -163,11 +165,27 @@ def test_quit_confirm_wraps_all_choices_in_narrow_sidebar():
|
|
|
163
165
|
text = " ".join(
|
|
164
166
|
_rendered_text(modal, size=(22, height)).replace("│", " ").split())
|
|
165
167
|
|
|
166
|
-
assert "quit
|
|
168
|
+
assert "hard quit (requires final confirmation)" in text
|
|
167
169
|
assert "soft quit (keep sessions alive)" in text
|
|
168
170
|
assert "n / Esc = cancel" in text
|
|
169
171
|
|
|
170
172
|
|
|
173
|
+
def test_quit_confirm_choices_have_filled_button_styling():
|
|
174
|
+
modal = QuitConfirmModal(
|
|
175
|
+
on_confirm=lambda: None,
|
|
176
|
+
on_soft_quit=lambda: None,
|
|
177
|
+
on_cancel=lambda: None,
|
|
178
|
+
running_count=2,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
attrs = _rendered_attrs(
|
|
182
|
+
modal, size=(48, modal.preferred_height(48))
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
assert "btn_key" in attrs
|
|
186
|
+
assert "btn_label" in attrs
|
|
187
|
+
|
|
188
|
+
|
|
171
189
|
def test_quit_confirm_warns_when_ui_is_shared_by_multiple_clients():
|
|
172
190
|
modal = QuitConfirmModal(
|
|
173
191
|
on_confirm=lambda: None,
|
|
@@ -185,7 +203,7 @@ def test_quit_confirm_warns_when_ui_is_shared_by_multiple_clients():
|
|
|
185
203
|
assert "quitting closes this UI in all of them" in text
|
|
186
204
|
|
|
187
205
|
|
|
188
|
-
def
|
|
206
|
+
def test_quit_confirm_mouse_clicks_hard_soft_quit_and_cancel():
|
|
189
207
|
called: list[str] = []
|
|
190
208
|
modal = QuitConfirmModal(
|
|
191
209
|
on_confirm=lambda: called.append("hard"),
|
|
@@ -206,17 +224,11 @@ def test_quit_confirm_mouse_clicks_soft_quit_and_cancel_but_not_hard_quit():
|
|
|
206
224
|
size, "mouse press", 1, size[0] // 2, row, True
|
|
207
225
|
)
|
|
208
226
|
|
|
227
|
+
click_row("hard quit")
|
|
209
228
|
click_row("soft quit")
|
|
210
229
|
click_row("cancel")
|
|
211
|
-
hard_canvas = modal.render(size, focus=True)
|
|
212
|
-
hard_row = next(
|
|
213
|
-
index
|
|
214
|
-
for index, line in enumerate(hard_canvas.text)
|
|
215
|
-
if b"quit and kill all sessions" in line
|
|
216
|
-
)
|
|
217
|
-
modal.mouse_event(size, "mouse press", 1, size[0] // 2, hard_row, True)
|
|
218
230
|
|
|
219
|
-
assert called == ["soft", "cancel"]
|
|
231
|
+
assert called == ["hard", "soft", "cancel"]
|
|
220
232
|
|
|
221
233
|
|
|
222
234
|
def test_hard_quit_confirm_preserves_enter_and_requires_a_second_action():
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.6.dev4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|