swimmers 0.1.3

Axum server plus TUI for orchestrating Claude Code and Codex agents across tmux panes
Documentation
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

const SHELL_CAPTURE_START: &str = "__SWIMMERS_ENV_CAPTURE_START__";
const SHELL_CAPTURE_END: &str = "__SWIMMERS_ENV_CAPTURE_END__";
const PROVIDER_ENV_VARS: &[&str] = &["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"];

pub fn bootstrap_provider_env_from_shell() {
    let Some(shell) = detect_interactive_shell() else {
        return;
    };

    for key in PROVIDER_ENV_VARS {
        if env_value_present(key) {
            continue;
        }

        let Some(value) = read_env_var_from_interactive_shell(&shell, key) else {
            continue;
        };

        if !value.trim().is_empty() {
            env::set_var(key, value);
        }
    }
}

fn env_value_present(key: &str) -> bool {
    env::var(key)
        .map(|value| !value.trim().is_empty())
        .unwrap_or(false)
}

fn detect_interactive_shell() -> Option<PathBuf> {
    env::var_os("SHELL")
        .map(PathBuf::from)
        .filter(|path| path.is_file())
        .or_else(|| fallback_shell("/bin/zsh"))
        .or_else(|| fallback_shell("/bin/bash"))
}

fn fallback_shell(path: &str) -> Option<PathBuf> {
    let shell = PathBuf::from(path);
    shell.is_file().then_some(shell)
}

fn read_env_var_from_interactive_shell(shell: &Path, key: &str) -> Option<String> {
    let script = format!(
        "printf '%s\\n' '{SHELL_CAPTURE_START}'; printf '%s\\n' \"${{{key}:-}}\"; printf '%s\\n' '{SHELL_CAPTURE_END}'"
    );
    let output = Command::new(shell).arg("-ic").arg(script).output().ok()?;
    let stdout = String::from_utf8_lossy(&output.stdout);
    parse_marked_value(&stdout)
}

fn parse_marked_value(output: &str) -> Option<String> {
    let mut in_capture = false;
    for line in output.lines() {
        if line == SHELL_CAPTURE_START {
            in_capture = true;
            continue;
        }
        if line == SHELL_CAPTURE_END {
            return None;
        }
        if in_capture {
            return Some(line.to_string());
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_marked_value_returns_captured_line() {
        let output = format!("noise\n{SHELL_CAPTURE_START}\nsk-test\n{SHELL_CAPTURE_END}\n");
        assert_eq!(parse_marked_value(&output).as_deref(), Some("sk-test"));
    }

    #[test]
    fn parse_marked_value_returns_empty_string_when_capture_is_blank() {
        let output = format!("{SHELL_CAPTURE_START}\n\n{SHELL_CAPTURE_END}\n");
        assert_eq!(parse_marked_value(&output).as_deref(), Some(""));
    }

    #[test]
    fn parse_marked_value_ignores_missing_capture() {
        assert_eq!(parse_marked_value("no markers"), None);
    }
}