rmux 0.10.0

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use serde_json::{json, Value};

use super::super::ExitFailure;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct PaneExitStatus {
    stale: bool,
    exit_status: Option<i32>,
    exit_signal: Option<i32>,
}

impl PaneExitStatus {
    pub(super) const fn stale() -> Self {
        Self {
            stale: true,
            exit_status: None,
            exit_signal: None,
        }
    }

    pub(super) const fn known(exit_status: Option<i32>, exit_signal: Option<i32>) -> Self {
        Self {
            stale: false,
            exit_status,
            exit_signal,
        }
    }

    pub(super) fn json_value(self) -> Value {
        json!({
            "stale": self.stale,
            "exit_status": self.exit_status,
            "exit_signal": self.exit_signal,
        })
    }

    pub(super) fn send_keys_exit_code(observation: Option<Self>) -> Result<i32, ExitFailure> {
        let observation = observation.ok_or_else(|| {
            ExitFailure::new(
                1,
                "send-keys observed pane exit without process exit metadata",
            )
        })?;
        if let Some(exit_status) = observation.exit_status {
            return Ok(exit_status);
        }
        if let Some(exit_signal) = observation.exit_signal.filter(|signal| *signal > 0) {
            return 128_i32.checked_add(exit_signal).ok_or_else(|| {
                ExitFailure::new(
                    1,
                    format!("send-keys observed invalid pane exit signal {exit_signal}"),
                )
            });
        }

        let detail = if observation.stale {
            " because the pane target became stale"
        } else {
            ""
        };
        Err(ExitFailure::new(
            1,
            format!("send-keys could not determine the pane process exit status{detail}"),
        ))
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::PaneExitStatus;

    #[test]
    fn normal_exit_codes_are_preserved_without_cli_truncation() {
        for exit_status in [0, 7, 513] {
            assert_eq!(
                PaneExitStatus::send_keys_exit_code(Some(PaneExitStatus::known(
                    Some(exit_status),
                    None,
                )))
                .expect("normal exit status must be available"),
                exit_status
            );
        }
    }

    #[test]
    fn unix_signal_uses_conventional_shell_exit_code() {
        assert_eq!(
            PaneExitStatus::send_keys_exit_code(Some(PaneExitStatus::known(None, Some(15))))
                .expect("valid signal must map to a shell exit code"),
            143
        );
    }

    #[test]
    fn missing_or_unknown_exit_metadata_fails_closed() {
        for observation in [
            None,
            Some(PaneExitStatus::stale()),
            Some(PaneExitStatus::known(None, None)),
            Some(PaneExitStatus::known(None, Some(0))),
        ] {
            let error = PaneExitStatus::send_keys_exit_code(observation)
                .expect_err("unknown exit outcome must not become success");
            assert_eq!(error.exit_code(), 1);
        }
    }

    #[test]
    fn serialized_exit_metadata_keeps_the_existing_json_shape() {
        let value = PaneExitStatus::known(Some(7), None).json_value();

        assert_eq!(
            value,
            json!({
                "stale": false,
                "exit_status": 7,
                "exit_signal": null,
            })
        );
    }
}