Repository: fujiapple852/trippy (language: rust)

Issue:
add `quit-preserve-screen` key binding
Trippy already supports `--tui-preserve-screen` to preserve the screen when the application exits but there is no way to enable this once the application has started.

Add a new Tui command and key binding (default to `shift+q`) to quit the application and preserve the screen. 


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 205efa541..7ccf76547 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 ### Added
 
 - Highlight lost probes in sample history ([#1247](https://github.com/fujiapple852/trippy/issues/1247))
+- Added `quit-preserve-screen` (default: `shift+q`) key binding to quit Tui without clearing the
+  screen ([#1382](https://github.com/fujiapple852/trippy/issues/1382))
 
 ### Changed
 
@@ -26,6 +28,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 - Reverse dns enqueued multiple times when dns-ttl expires ([#1290](https://github.com/fujiapple852/trippy/issues/1290))
 - Fixed panic for icmp extensions with malformed length ([#1287](https://github.com/fujiapple852/trippy/issues/1287))
+- Cursor not moved to the bottom on exit when using
+  `--tui-preserve-screen` ([#1375](https://github.com/fujiapple852/trippy/issues/1375))
 
 ## [0.11.0] - 2024-08-11
 
diff --git a/README.md b/README.md
index e7f601ffb..3b3f837d2 100644
--- a/README.md
+++ b/README.md
@@ -886,6 +886,7 @@ command line option or in the `bindings` section of the configuration file.
 | `toggle-as-info`           | Toggle AS info display                          | `z`      |
 | `toggle-hop-details`       | Toggle hop details                              | `d`      |
 | `quit`                     | Quit the application                            | `q`      |
+| `quit-preserve-screen`     | Quit the application and preserve the screen    | `shift+q`|
 
 The supported modifiers are: `shift`, `ctrl`, `alt`, `super`, `hyper` & `meta`. Multiple modifiers may be specified, for
 example `ctrl+shift+b`.
diff --git a/crates/trippy-tui/src/config/binding.rs b/crates/trippy-tui/src/config/binding.rs
index dde1da652..371975fc9 100644
--- a/crates/trippy-tui/src/config/binding.rs
+++ b/crates/trippy-tui/src/config/binding.rs
@@ -47,6 +47,7 @@ pub struct TuiBindings {
     pub toggle_as_info: TuiKeyBinding,
     pub toggle_hop_details: TuiKeyBinding,
     pub quit: TuiKeyBinding,
+    pub quit_preserve_screen: TuiKeyBinding,
 }
 
 impl Default for TuiBindings {
@@ -98,6 +99,10 @@ impl Default for TuiBindings {
             toggle_as_info: TuiKeyBinding::new(KeyCode::Char('z')),
             toggle_hop_details: TuiKeyBinding::new(KeyCode::Char('d')),
             quit: TuiKeyBinding::new(KeyCode::Char('q')),
+            quit_preserve_screen: TuiKeyBinding::new_with_modifier(
+                KeyCode::Char('q'),
+                KeyModifiers::SHIFT,
+            ),
         }
     }
 }
@@ -151,6 +156,10 @@ impl TuiBindings {
             (self.toggle_as_info, TuiCommandItem::ToggleASInfo),
             (self.toggle_hop_details, TuiCommandItem::ToggleHopDetails),
             (self.quit, TuiCommandItem::Quit),
+            (
+                self.quit_preserve_screen,
+                TuiCommandItem::QuitPreserveScreen,
+            ),
         ]
         .iter()
         .fold(
@@ -326,6 +335,10 @@ impl From<(HashMap<TuiCommandItem, TuiKeyBinding>, ConfigBindings)> for TuiBindi
                 .get(&TuiCommandItem::Quit)
                 .or(cfg.quit.as_ref())
                 .unwrap_or(&Self::default().quit),
+            quit_preserve_screen: *cmd_items
+                .get(&TuiCommandItem::QuitPreserveScreen)
+                .or(cfg.quit_preserve_screen.as_ref())
+                .unwrap_or(&Self::default().quit_preserve_screen),
         }
     }
 }
@@ -628,4 +641,6 @@ pub enum TuiCommandItem {
     ToggleHopDetails,
     /// Quit the application.
     Quit,
+    /// Quit the application and preserve the screen.
+    QuitPreserveScreen,
 }
diff --git a/crates/trippy-tui/src/config/file.rs b/crates/trippy-tui/src/config/file.rs
index 25b367a73..acada47e5 100644
--- a/crates/trippy-tui/src/config/file.rs
+++ b/crates/trippy-tui/src/config/file.rs
@@ -398,6 +398,7 @@ pub struct ConfigBindings {
     pub toggle_as_info: Option<TuiKeyBinding>,
     pub toggle_hop_details: Option<TuiKeyBinding>,
     pub quit: Option<TuiKeyBinding>,
+    pub quit_preserve_screen: Option<TuiKeyBinding>,
 }
 
 impl Default for ConfigBindings {
@@ -442,6 +443,7 @@ impl Default for ConfigBindings {
             toggle_as_info: Some(bindings.toggle_as_info),
             toggle_hop_details: Some(bindings.toggle_hop_details),
             quit: Some(bindings.quit),
+            quit_preserve_screen: Some(bindings.quit_preserve_screen),
         }
     }
 }
diff --git a/crates/trippy-tui/src/frontend.rs b/crates/trippy-tui/src/frontend.rs
index 3f1011611..4fd0fda68 100644
--- a/crates/trippy-tui/src/frontend.rs
+++ b/crates/trippy-tui/src/frontend.rs
@@ -9,6 +9,7 @@ use crossterm::{
     execute,
     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
 };
+use ratatui::layout::Position;
 use ratatui::{
     backend::{Backend, CrosstermBackend},
     Terminal,
@@ -43,9 +44,13 @@ pub fn run_frontend(
     let backend = CrosstermBackend::new(stdout);
     let mut terminal = Terminal::new(backend)?;
     let preserve_screen = tui_config.preserve_screen;
-    let res = run_app(&mut terminal, traces, tui_config, resolver, geoip_lookup);
+    let mut app = TuiApp::new(tui_config, resolver, geoip_lookup, traces);
+    let res = run_app(&mut terminal, &mut app);
     disable_raw_mode()?;
-    if !preserve_screen {
+    if preserve_screen || matches!(res, Ok(ExitAction::PreserveScreen)) {
+        terminal.set_cursor_position(Position::new(0, terminal.size()?.height))?;
+        terminal.backend_mut().append_lines(1)?;
+    } else {
         execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
     }
     terminal.show_cursor()?;
@@ -55,22 +60,23 @@ pub fn run_frontend(
     Ok(())
 }
 
+/// The exit action to take when the frontend exits.
+enum ExitAction {
+    /// Exit the frontend normally.
+    Normal,
+    /// Preserve the screen on exit.
+    PreserveScreen,
+}
+
 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
-fn run_app<B: Backend>(
-    terminal: &mut Terminal<B>,
-    trace_info: Vec<TraceInfo>,
-    tui_config: TuiConfig,
-    resolver: DnsResolver,
-    geoip_lookup: GeoIpLookup,
-) -> io::Result<()> {
-    let mut app = TuiApp::new(tui_config, resolver, geoip_lookup, trace_info);
+fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut TuiApp) -> io::Result<ExitAction> {
     loop {
         if app.frozen_start.is_none() {
             app.snapshot_trace_data();
             app.clamp_selected_hop();
             app.update_order_flow_counts();
         };
-        terminal.draw(|f| render::app::render(f, &mut app))?;
+        terminal.draw(|f| render::app::render(f, app))?;
         if event::poll(app.tui_config.refresh_rate)? {
             if let Event::Key(key) = event::read()? {
                 if key.kind == KeyEventKind::Press {
@@ -223,7 +229,9 @@ fn run_app<B: Backend>(
                     } else if bindings.toggle_hop_details.check(key) {
                         app.toggle_hop_details();
                     } else if bindings.quit.check(key) || CTRL_C.check(key) {
-                        return Ok(());
+                        return Ok(ExitAction::Normal);
+                    } else if bindings.quit_preserve_screen.check(key) {
+                        return Ok(ExitAction::PreserveScreen);
                     }
                 }
             }
diff --git a/crates/trippy-tui/src/frontend/binding.rs b/crates/trippy-tui/src/frontend/binding.rs
index 8e0189b98..f74aea421 100644
--- a/crates/trippy-tui/src/frontend/binding.rs
+++ b/crates/trippy-tui/src/frontend/binding.rs
@@ -43,6 +43,7 @@ pub struct Bindings {
     pub toggle_as_info: KeyBinding,
     pub toggle_hop_details: KeyBinding,
     pub quit: KeyBinding,
+    pub quit_preserve_screen: KeyBinding,
 }
 
 impl From<TuiBindings> for Bindings {
@@ -85,6 +86,7 @@ impl From<TuiBindings> for Bindings {
             toggle_as_info: KeyBinding::from(value.toggle_as_info),
             toggle_hop_details: KeyBinding::from(value.toggle_hop_details),
             quit: KeyBinding::from(value.quit),
+            quit_preserve_screen: KeyBinding::from(value.quit_preserve_screen),
         }
     }
 }
diff --git a/crates/trippy-tui/src/frontend/render/settings.rs b/crates/trippy-tui/src/frontend/render/settings.rs
index d62a4dce2..49fa5e7ca 100644
--- a/crates/trippy-tui/src/frontend/render/settings.rs
+++ b/crates/trippy-tui/src/frontend/render/settings.rs
@@ -393,6 +393,10 @@ fn format_binding_settings(app: &TuiApp) -> Vec<SettingsItem> {
             format!("{}", binds.toggle_hop_details),
         ),
         SettingsItem::new("quit", format!("{}", binds.quit)),
+        SettingsItem::new(
+            "quit-preserve-screen",
+            format!("{}", binds.quit_preserve_screen),
+        ),
     ]
 }
 
diff --git a/trippy-config-sample.toml b/trippy-config-sample.toml
index 010d88ff7..64305c9d5 100644
--- a/trippy-config-sample.toml
+++ b/trippy-config-sample.toml
@@ -424,3 +424,4 @@ clear-selection = "esc"
 toggle-as-info = "z"
 toggle-hop-details = "d"
 quit = "q"
+quit-preserve-screen = "shift+q"


Test patch:
diff --git a/crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@tui_binding_commands_match.snap b/crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@tui_binding_commands_match.snap
index 21d0a648a..01e183af1 100644
--- a/crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@tui_binding_commands_match.snap
+++ b/crates/trippy-tui/tests/resources/snapshots/trippy_tui__print__tests__output@tui_binding_commands_match.snap
@@ -1,4 +1,4 @@
 ---
 source: crates/trippy-tui/src/print.rs
 ---
-TUIbindingcommands:toggle-help,toggle-help-alt,toggle-settings,toggle-settings-tui,toggle-settings-trace,toggle-settings-dns,toggle-settings-geoip,toggle-settings-bindings,toggle-settings-theme,toggle-settings-columns,next-hop,previous-hop,next-trace,previous-trace,next-hop-address,previous-hop-address,address-mode-ip,address-mode-host,address-mode-both,toggle-freeze,toggle-chart,toggle-map,toggle-flows,toggle-privacy,expand-privacy,contract-privacy,expand-hosts,expand-hosts-max,contract-hosts,contract-hosts-min,chart-zoom-in,chart-zoom-out,clear-trace-data,clear-dns-cache,clear-selection,toggle-as-info,toggle-hop-details,quit
+TUIbindingcommands:toggle-help,toggle-help-alt,toggle-settings,toggle-settings-tui,toggle-settings-trace,toggle-settings-dns,toggle-settings-geoip,toggle-settings-bindings,toggle-settings-theme,toggle-settings-columns,next-hop,previous-hop,next-trace,previous-trace,next-hop-address,previous-hop-address,address-mode-ip,address-mode-host,address-mode-both,toggle-freeze,toggle-chart,toggle-map,toggle-flows,toggle-privacy,expand-privacy,contract-privacy,expand-hosts,expand-hosts-max,contract-hosts,contract-hosts-min,chart-zoom-in,chart-zoom-out,clear-trace-data,clear-dns-cache,clear-selection,toggle-as-info,toggle-hop-details,quit,quit-preserve-screen