par-term 0.48.0

Cross-platform GPU-accelerated terminal emulator with inline graphics support (Sixel, iTerm2, Kitty)
//! Session state types for save/restore on startup.
//!
//! This module provides **automatic session persistence**: save the current window
//! layout, tabs, and pane splits on clean exit, then restore them on next launch.
//!
//! # Relationship to `crate::arrangements`
//!
//! par-term has two overlapping session persistence mechanisms:
//!
//! | Feature | Module | Trigger | Scope |
//! |---------|--------|---------|-------|
//! | Auto session restore | `crate::session` (this module) | Automatic on clean exit / next launch | Last-session state only (single slot) |
//! | Named arrangements | `crate::arrangements` | User-initiated save/restore via UI | Multiple named snapshots, monitor-aware |
//!
//! Both capture window positions, sizes, tab CWDs, and tab titles using similar
//! serialization patterns (serde JSON via `storage` submodule). The key distinction
//! is lifecycle: session state is ephemeral (overwritten on each clean exit), while
//! arrangements are user-named and persist indefinitely.
//!
//! # Crash recovery
//!
//! Neither of those survives a panic: both are written on the way out, and a
//! panicking process never gets there. [`crash_guard`] adds a third file,
//! `crash_session.yaml`, written from a panic hook out of a snapshot the event
//! loop publishes periodically. It is deliberately a separate file — a process
//! that has just panicked must not overwrite the good session — and
//! `WindowManager::restore_session` prefers it when present, then deletes it.
//! Read that module's header before changing anything here: it is explicit about
//! which panic classes it covers and which it cannot.
//!
//! # Shared types
//!
//! The per-tab fields (`cwd`, `title`, `custom_color`, `user_title`,
//! `custom_icon`, `pane_layout`) are defined once in
//! [`par_term_config::snapshot_types::TabSnapshot`] and wrapped by
//! [`SessionTab`] via `#[serde(transparent)]`.  The arrangements
//! module re-exports the same type directly, eliminating the previous duplication.
//! Existing YAML session files are fully backward-compatible — all fields remain at
//! the same nesting level.

pub mod capture;
pub mod crash_guard;
pub mod restore;
pub mod storage;

// Re-export the shared snapshot types so session consumers can use
// `crate::session::TabSnapshot` / `crate::session::SessionPaneNode`.
pub use par_term_config::snapshot_types::{SessionPaneNode, TabSnapshot};
use serde::{Deserialize, Serialize};

/// Top-level session state: all windows at the time of save
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionState {
    /// Timestamp when the session was saved (ISO 8601)
    pub saved_at: String,
    /// All windows in the session
    pub windows: Vec<SessionWindow>,
}

/// A single window in the saved session
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionWindow {
    /// Window position (x, y) in physical pixels
    pub position: (i32, i32),
    /// Window size (width, height) in physical pixels
    pub size: (u32, u32),
    /// Tabs in this window
    pub tabs: Vec<SessionTab>,
    /// Index of the active tab
    pub active_tab_index: usize,
    /// tmux session name to auto-reconnect on restore (if any)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tmux_session_name: Option<String>,
    /// par-mux session name to reattach on restore (if any). Distinct from
    /// `tmux_session_name`: restoring a mux name through the tmux gateway
    /// spawns a real `tmux -CC` session of the same name instead of
    /// reattaching to the daemon.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mux_session_name: Option<String>,
}

/// A single tab in a saved session.
///
/// Serialized exactly as its [`TabSnapshot`] (`transparent`), so the YAML
/// layout is unchanged: `pane_layout` stays a key of each tab. Not
/// `flatten`: a flattened struct buffers its fields, and the YAML-tagged
/// `SessionPaneNode` cannot be read back out of that buffer.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct SessionTab {
    /// Tab snapshot fields shared with the arrangements module
    pub snapshot: TabSnapshot,
}

#[cfg(test)]
mod pane_title_persistence_tests {
    use super::SessionPaneNode;

    /// Card 01a0d95dd318 criterion 4: a leaf's user title round-trips, and
    /// a pre-rename session file (no `user_title` key) still loads.
    #[test]
    fn leaf_user_title_round_trips_and_old_files_load() {
        let leaf = SessionPaneNode::Leaf {
            cwd: Some("/tmp".to_string()),
            user_title: Some("build box".to_string()),
        };
        let yaml = serde_yaml_ng::to_string(&leaf).expect("serialize");
        let back: SessionPaneNode = serde_yaml_ng::from_str(&yaml).expect("deserialize");
        let SessionPaneNode::Leaf { cwd, user_title } = back else {
            panic!("round-trip must stay a leaf")
        };
        assert_eq!(cwd.as_deref(), Some("/tmp"));
        assert_eq!(
            user_title.as_deref(),
            Some("build box"),
            "the user title must survive the YAML round-trip"
        );
        assert!(
            !yaml.contains("user_title: null"),
            "absent titles are omitted"
        );

        // The pre-field form (saved by older par-term: same serializer,
        // user_title absent via skip_serializing_if) loads with
        // `user_title: None` via serde default.
        let untitled = SessionPaneNode::Leaf {
            cwd: Some("/tmp".to_string()),
            user_title: None,
        };
        let old_yaml = serde_yaml_ng::to_string(&untitled).expect("serialize untitled");
        assert!(
            !old_yaml.contains("user_title"),
            "an absent title is not written: {old_yaml}"
        );
        let parsed: SessionPaneNode =
            serde_yaml_ng::from_str(&old_yaml).expect("old-format leaf loads");
        let SessionPaneNode::Leaf { cwd, user_title } = parsed else {
            panic!("old-format parse must stay a leaf")
        };
        assert_eq!(cwd.as_deref(), Some("/tmp"));
        assert_eq!(user_title, None);
    }
}