wsl-clip-core 0.5.1

Core library for wsl-clip clipboard bridge
Documentation
// <FILE>wsl_clip_core/src/bridge/fnc_status.rs</FILE> - <DESC>Bridge status helper</DESC>
// <VERS>VERSION: 0.1.0 - 2025-12-08T00:00:00Z</VERS>
// <WCTX>Status reporting for bridge stubs using SessionStore summary.</WCTX>
// <CLOG>Added BridgeStatus struct and builder.</CLOG>

use crate::bridge::fnc_session_store::SessionStoreSummary;
use crate::config::{ResolvedBridgeCommand, parse_duration};
use serde::Serialize;

#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct BridgeStatus {
    pub mode: String,
    pub bind: Option<String>,
    pub socket: Option<String>,
    pub max_bytes: Option<u64>,
    pub ttl_ms: Option<u64>,
    pub sessions_active: usize,
    pub last_activity_ms_ago: Option<u64>,
    pub clipboard_enabled: bool,
    pub clipboard_applied: u64,
    pub token_required: bool,
    pub last_abort_reason: Option<String>,
    pub size_violations: u64,
    pub ttl_violations: u64,
    pub uptime_ms: Option<u64>,
    pub ttl_remaining_ms: Option<u64>,
}

pub fn build_status(resolved: &ResolvedBridgeCommand, summary: Option<SessionStoreSummary>) -> BridgeStatus {
    let (mode, bind, socket, max_bytes, ttl_str) = match resolved {
        ResolvedBridgeCommand::Status(s) => (
            s.mode.to_string(),
            Some(s.bind.clone()),
            Some(s.socket.to_string_lossy().to_string()),
            None,
            None,
        ),
        ResolvedBridgeCommand::Listen(l) => (
            l.mode.to_string(),
            Some(l.bind.clone()),
            None,
            Some(l.max_bytes),
            l.ttl.clone(),
        ),
        ResolvedBridgeCommand::Connect(c) => (
            c.mode.to_string(),
            None,
            Some(c.socket.to_string_lossy().to_string()),
            Some(c.max_bytes),
            None,
        ),
    };

    let ttl_ms = ttl_str
        .and_then(|s| parse_duration(s.as_str()))
        .map(|d| d.as_millis() as u64);

    let (sessions_active, last_activity_ms_ago, clipboard_applied, size_violations, ttl_violations, last_abort_reason, uptime_ms) = match summary {
        Some(sum) => {
            let now = std::time::Instant::now();
            let ago = sum.last_activity.map(|ts| now.duration_since(ts)).map(|d| d.as_millis() as u64);
            let up = now.duration_since(sum.start_instant).as_millis() as u64;
            (
                sum.active,
                ago,
                sum.clipboard_applied,
                sum.size_violations,
                sum.ttl_violations,
                sum.last_abort_reason.clone(),
                Some(up),
            )
        }
        None => (0, None, 0, 0, 0, None, None),
    };

    let token_required = match resolved {
        ResolvedBridgeCommand::Listen(l) => l.token.is_some() || l.token_file.is_some(),
        ResolvedBridgeCommand::Connect(c) => c.token.is_some() || c.token_file.is_some(),
        ResolvedBridgeCommand::Status(_) => false,
    };

    let ttl_remaining_ms = ttl_ms;

    BridgeStatus {
        mode,
        bind,
        socket,
        max_bytes,
        ttl_ms,
        sessions_active,
        last_activity_ms_ago,
        clipboard_enabled: true,
        clipboard_applied,
        token_required,
        last_abort_reason,
        size_violations,
        ttl_violations,
        uptime_ms,
        ttl_remaining_ms,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{Duration, Instant};

    #[test]
    fn status_reports_clipboard_counts() {
        let resolved = ResolvedBridgeCommand::Listen(crate::config::ResolvedBridgeListen {
            bind: "127.0.0.1:0".into(),
            mode: crate::config::BridgeMode::Tcp,
            idle_exit: None,
            ttl: Some("1000".into()),
            max_bytes: 1024,
            token: None,
            token_file: None,
            daemon: false,
            log_file: None,
        });

        let summary = SessionStoreSummary {
            active: 2,
            last_activity: Some(Instant::now() - Duration::from_millis(500)),
            clipboard_applied: 3,
            size_violations: 0,
            ttl_violations: 0,
            last_abort_reason: None,
            start_instant: Instant::now() - Duration::from_secs(10),
        };

        let status = build_status(&resolved, Some(summary));
        assert_eq!(status.sessions_active, 2);
        assert_eq!(status.clipboard_applied, 3);
        assert!(status.clipboard_enabled);
        assert_eq!(status.ttl_ms, Some(1000));
        assert_eq!(status.token_required, false);
        assert_eq!(status.size_violations, 0);
        assert_eq!(status.ttl_violations, 0);
        assert_eq!(status.last_abort_reason, None);
    }

    #[test]
    fn status_exposes_violation_counts_and_abort_reason() {
        use crate::bridge::fnc_session_store::{SessionStore, SessionStoreConfig};
        let cfg = SessionStoreConfig {
            default_max_bytes: 1024,
            default_ttl: Duration::from_secs(60),
            default_idle_exit: Duration::from_secs(60),
            spill_threshold: 512,
        };
        let store = SessionStore::new(cfg);
        {
            let mut s = store.lock().unwrap();
            s.record_size_violation("too big");
            s.record_ttl_violation("expired");
            s.record_abort_reason("token mismatch");
        }
        let summary = store.lock().unwrap().summary();

        let resolved = ResolvedBridgeCommand::Listen(crate::config::ResolvedBridgeListen {
            bind: "127.0.0.1:0".into(),
            mode: crate::config::BridgeMode::Tcp,
            idle_exit: None,
            ttl: None,
            max_bytes: 1024,
            token: None,
            token_file: None,
            daemon: false,
            log_file: None,
        });

        let status = build_status(&resolved, Some(summary));
        assert_eq!(status.size_violations, 1);
        assert_eq!(status.ttl_violations, 1);
        assert_eq!(status.last_abort_reason.as_deref(), Some("token mismatch"));
    }
}

// <FILE>wsl_clip_core/src/bridge/fnc_status.rs</FILE> - <DESC>Bridge status helper</DESC>
// <VERS>END OF VERSION: 0.1.0 - 2025-12-08T00:00:00Z</VERS>