rmux-server 0.10.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use std::time::Duration;

use crate::status_jobs::StatusJobRuntime;
use crate::terminal::TerminalProfile;

pub(super) fn render_template_with_status_jobs<C, T>(
    template: &str,
    profile: Option<&TerminalProfile>,
    cache_ttl: Duration,
    status_jobs: Option<&StatusJobRuntime>,
    mut render_command: C,
    mut render_template: T,
) -> String
where
    C: FnMut(&str) -> String,
    T: FnMut(&str) -> String,
{
    if !template.contains("#(") {
        return render_template(template);
    }

    let bytes = template.as_bytes();
    let mut prepared = String::with_capacity(template.len());
    let mut replacements = Vec::new();
    let mut index = 0;
    let mut segment_start = 0;
    let mut job_index = 0usize;
    while index < bytes.len() {
        if bytes[index] != b'#' || bytes.get(index + 1) != Some(&b'(') {
            index += 1;
            continue;
        }

        if segment_start < index {
            prepared.push_str(&template[segment_start..index]);
        }
        let command_start = index + 2;
        let Some(command_end) = find_job_end(bytes, command_start) else {
            return render_template(&prepared);
        };
        let command = render_command(&template[command_start..command_end]);
        let placeholder = status_job_placeholder(job_index);
        let output = status_jobs
            .map(|runtime| runtime.cached_output(&command, profile, cache_ttl))
            .unwrap_or_default();
        replacements.push((placeholder.clone(), output));
        prepared.push_str(&placeholder);
        index = command_end + 1;
        segment_start = index;
        job_index += 1;
    }
    if segment_start < template.len() {
        prepared.push_str(&template[segment_start..]);
    }
    let mut rendered = render_template(&prepared);
    for (placeholder, output) in replacements {
        rendered = rendered.replace(&placeholder, &output);
    }
    rendered
}

fn status_job_placeholder(index: usize) -> String {
    format!("\u{E000}rmux-status-job-{index}\u{E001}")
}

fn find_job_end(bytes: &[u8], mut index: usize) -> Option<usize> {
    let mut depth = 1usize;
    while index < bytes.len() {
        match bytes[index] {
            b'(' => depth = depth.saturating_add(1),
            b')' => {
                depth = depth.saturating_sub(1);
                if depth == 0 {
                    return Some(index);
                }
            }
            _ => {}
        }
        index += 1;
    }
    None
}

#[cfg(test)]
mod tests {
    use super::{find_job_end, render_template_with_status_jobs};
    use crate::status_jobs::StatusJobRuntime;
    use std::time::{Duration, Instant};

    #[test]
    fn status_jobs_replace_stdout_and_trim_trailing_newlines_from_cache() {
        let runtime = StatusJobRuntime::new();
        let command = format!("echo job-ok-{}", std::process::id());
        let template = format!("a#({command})b");

        assert_eq!(render(&runtime, &template), "ab");
        let deadline = Instant::now() + Duration::from_secs(2);
        loop {
            let rendered = render(&runtime, &template);
            if rendered.contains("job-ok-") {
                assert_eq!(rendered, format!("ajob-ok-{}b", std::process::id()));
                break;
            }
            assert!(
                Instant::now() < deadline,
                "status job cache was not populated; last render was {rendered:?}"
            );
            std::thread::sleep(Duration::from_millis(10));
        }
        runtime.shutdown_and_join();
    }

    #[test]
    fn status_jobs_scan_nested_parentheses() {
        assert_eq!(find_job_end(b"#(echo (ok))", 2), Some(11));
    }

    #[test]
    fn status_jobs_drop_unclosed_job_and_stop_expansion() {
        let runtime = StatusJobRuntime::new();
        assert_eq!(render(&runtime, "before#(echo no close"), "before");
    }

    #[test]
    fn status_jobs_render_commands_but_not_job_output() {
        let runtime = StatusJobRuntime::new();
        let command = format!("cached-job-#{{session_name}}-{}", std::process::id());
        runtime.seed_completed_output(
            &format!("cached-job-alpha-{}", std::process::id()),
            &format!("#{{session_name}}-{}", std::process::id()),
        );
        let template = format!("plain #{{session_name}} #({command})");

        assert_eq!(
            render_template_with_status_jobs(
                &template,
                None,
                Duration::from_secs(1),
                Some(&runtime),
                render_alpha,
                render_alpha,
            ),
            format!("plain alpha #{{session_name}}-{}", std::process::id())
        );
    }

    fn render(runtime: &StatusJobRuntime, template: &str) -> String {
        render_template_with_status_jobs(
            template,
            None,
            Duration::from_secs(1),
            Some(runtime),
            str::to_owned,
            str::to_owned,
        )
    }

    fn render_alpha(segment: &str) -> String {
        segment.replace("#{session_name}", "alpha")
    }
}