sessionizer 0.6.4

Tmux session manager
Documentation
use std::{
    cmp::Reverse,
    process::{Command, Output},
};

use crate::{Entry, Result, TmuxSession, debug, info, warn};

pub fn fetch_tmux_sessions(handle: impl FnMut(Entry) -> Result<()>) {
    let mut cmd = Command::new("tmux");

    let cmd = cmd.args([
        "list-sessions",
        "-F",
        concat!(
            "#{session_attached},#{session_last_attached},#{session_name},",
            "#{session_path},created #{t/f/%Y-%m-%d %H#:%M:session_created}"
        ),
    ]);

    match cmd.output() {
        Ok(out) => {
            if let Err(err) = parse_tmux_output(out, handle) {
                warn!("failed to parse tmux sessions: {}", err);
            }
        }
        Err(err) => {
            warn!("failed to get tmux output: {}", err);
        }
    }
}

fn parse_tmux_output(out: Output, mut handle: impl FnMut(Entry) -> Result<()>) -> Result<()> {
    if !out.status.success() {
        let err = String::from_utf8_lossy(&out.stderr);
        debug!(status =? out.status, exit_code =? out.status.code(), "tmux error: {}", err);
        if out.status.code() == Some(1) {
            info!("Tmux doesn't seem to be running");
        }
    }

    let out = String::from_utf8(out.stdout)?;
    out.lines()
        .map(parse_session)
        .filter_map(Result::transpose)
        .try_for_each(move |session| handle(Entry::Session(session?)))
}

fn parse_session(tmux_ls_output: &str) -> Result<Option<TmuxSession>> {
    let line = tmux_ls_output.trim();
    if line.is_empty() {
        return Ok(None);
    }
    let parts: [&str; 5] = line
        .splitn(5, ',')
        .collect::<Vec<_>>()
        .try_into()
        .expect("tmux format");

    let attached = parts[0].parse::<u64>()? > 0;
    let last_attached = Reverse(
        Some(parts[1])
            .filter(|s| !s.is_empty())
            .and_then(|s| s.parse::<u64>().ok())
            .unwrap_or(0),
    );
    let name = parts[2].into();
    let root = parts[3].into();
    let info = parts[4].into();
    let session = TmuxSession {
        attached,
        last_attached,
        name,
        root,
        info,
    };

    Ok(Some(session))
}
#[cfg(test)]
mod tests {
    use std::path::Path;

    use super::*;

    #[test]
    fn ignore_blank_line() {
        assert!(matches!(parse_session(""), Ok(None)));
        assert!(matches!(parse_session("\n"), Ok(None)));
        assert!(matches!(parse_session("     \n"), Ok(None)));
    }

    #[test]
    fn parse_result_line() {
        let line = "1,1711654501,sessionizer,/dev/sessionizer,created 2024-03-28 20:35";
        let result = parse_session(line).unwrap().unwrap();

        assert!(result.attached);
        assert_eq!(result.last_attached.0, 1_711_654_501);
        assert_eq!(result.name, "sessionizer");
        assert_eq!(result.root, Path::new("/dev/sessionizer"));
        assert_eq!(result.info, "created 2024-03-28 20:35");
    }

    #[test]
    fn perse_result_list() {
        let result = r"
0,1711100755,neo4rs,/dev/neo4rs,created 2024-03-22 10:45
1,1711654501,sessionizer,/dev/sessionizer,created 2024-03-28 20:35
        ";

        let output = Output {
            status: std::process::ExitStatus::default(),
            stdout: result.as_bytes().to_vec(),
            stderr: Vec::new(),
        };
        let mut sessions = Vec::new();
        let handle = |entry| {
            sessions.push(entry);
            Ok(())
        };

        parse_tmux_output(output, handle).unwrap();

        assert_eq!(
            sessions,
            [
                Entry::Session(TmuxSession {
                    attached: false,
                    last_attached: Reverse(1_711_100_755),
                    name: "neo4rs".into(),
                    root: Path::new("/dev/neo4rs").into(),
                    info: "created 2024-03-22 10:45".into(),
                }),
                Entry::Session(TmuxSession {
                    attached: true,
                    last_attached: Reverse(1_711_654_501),
                    name: "sessionizer".into(),
                    root: Path::new("/dev/sessionizer").into(),
                    info: "created 2024-03-28 20:35".into(),
                }),
            ]
        );
    }
}