Skip to main content

par_term/session/
capture.rs

1//! Capture current session state from live windows
2
3use super::{SessionPaneNode, SessionState, SessionTab, SessionWindow};
4use crate::app::window_state::WindowState;
5use crate::pane::PaneNode;
6use par_term_config::snapshot_types::TabSnapshot;
7use std::collections::HashMap;
8use winit::window::WindowId;
9
10/// Capture the current session state from all open windows
11pub fn capture_session(windows: &HashMap<WindowId, WindowState>) -> SessionState {
12    let mut session_windows = Vec::new();
13
14    for window_state in windows.values() {
15        let Some(window) = &window_state.window else {
16            continue;
17        };
18
19        // Get window position and size in logical pixels.
20        // Dividing physical values by scale_factor gives scale-factor-
21        // independent logical pixels that winit correctly places via
22        // LogicalPosition on restore (important for mixed-DPI setups).
23        // Use inner_size (content area) not outer_size (includes decorations).
24        let scale = window.scale_factor();
25        let window_pos = window.outer_position().unwrap_or_default();
26        let inner_size = window.inner_size();
27
28        // Capture visible tabs only — hidden tabs (e.g. tmux gateway) are
29        // transient control-mode connections that should not be persisted.
30        let visible_tabs = window_state.tab_manager.visible_tabs();
31        let tabs: Vec<SessionTab> = visible_tabs
32            .iter()
33            .map(|tab| {
34                // Only capture pane_layout for multi-pane (Split) layouts.
35                // Single-pane tabs use pane_layout=None so that session restore
36                // uses the tab-level CWD (snapshot.cwd) without calling
37                // restore_pane_layout(). Capturing a Leaf here would cause
38                // restore_pane_layout() to spawn a second shell unnecessarily —
39                // and its Pane::Drop would kill the first shell via the shared Arc,
40                // leading to a window that closes on the first redraw after restore.
41                let pane_layout = tab.pane_manager.as_ref().and_then(|pm| pm.root()).and_then(
42                    |root| match root {
43                        PaneNode::Leaf(_) => None,
44                        PaneNode::Split { .. } => Some(capture_pane_node(root)),
45                    },
46                );
47
48                SessionTab {
49                    snapshot: TabSnapshot {
50                        cwd: tab.get_cwd(),
51                        title: tab.title.clone(),
52                        custom_color: tab.custom_color,
53                        user_title: if tab.user_named {
54                            Some(tab.title.clone())
55                        } else {
56                            None
57                        },
58                        custom_icon: tab.custom_icon.clone(),
59                    },
60                    pane_layout,
61                }
62            })
63            .collect();
64
65        // active_tab_index must be relative to the visible-only list
66        let active_tab_id = window_state.tab_manager.active_tab_id();
67        let active_tab_index = active_tab_id
68            .and_then(|id| visible_tabs.iter().position(|t| t.id == id))
69            .unwrap_or(0);
70
71        session_windows.push(SessionWindow {
72            position: (
73                (window_pos.x as f64 / scale) as i32,
74                (window_pos.y as f64 / scale) as i32,
75            ),
76            size: (
77                (inner_size.width as f64 / scale) as u32,
78                (inner_size.height as f64 / scale) as u32,
79            ),
80            tabs,
81            active_tab_index,
82            tmux_session_name: window_state.tmux_state.tmux_session_name.clone(),
83        });
84    }
85
86    SessionState {
87        saved_at: chrono::Utc::now().to_rfc3339(),
88        windows: session_windows,
89    }
90}
91
92/// Recursively capture a pane tree node into a session-serializable form
93pub fn capture_pane_node(node: &PaneNode) -> SessionPaneNode {
94    match node {
95        PaneNode::Leaf(pane) => SessionPaneNode::Leaf {
96            cwd: pane.get_cwd(),
97        },
98        PaneNode::Split {
99            direction,
100            ratio,
101            first,
102            second,
103        } => SessionPaneNode::Split {
104            direction: *direction,
105            ratio: *ratio,
106            first: Box::new(capture_pane_node(first)),
107            second: Box::new(capture_pane_node(second)),
108        },
109    }
110}