rmux 0.9.0

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;

use rmux_client::{
    AutoStartConfig, Connection, EnsuredServerConnection, ServerConnectionProvenance,
};
use rmux_server::{DaemonConfig, ServerDaemon};

use crate::cli_args::{Cli, Command, ConfigFileSelection, StartServerArgs, TopLevelCommandScan};
use crate::server_runtime::build_daemon_runtime;

use super::ExitFailure;

#[derive(Debug)]
pub(in crate::cli) struct PrestartedServerConnection {
    pub(in crate::cli) connection: Connection,
    pub(in crate::cli) provenance: ServerConnectionProvenance,
}

/// A single prestarted connection shared by cloned per-command startup options.
///
/// Runtime alias resolution borrows this connection first. The first attach
/// command then consumes the same connection, so an idle-only shutdown does
/// not mistake a separate keepalive connection for concurrent activity.
#[derive(Debug, Clone)]
pub(in crate::cli) struct PrestartedConnection {
    inner: Rc<RefCell<Option<PrestartedServerConnection>>>,
}

impl PrestartedConnection {
    pub(in crate::cli) fn new(outcome: EnsuredServerConnection) -> Self {
        let provenance = outcome.provenance();
        let connection = outcome.into_connection();
        Self {
            inner: Rc::new(RefCell::new(Some(PrestartedServerConnection {
                connection,
                provenance,
            }))),
        }
    }

    pub(in crate::cli) fn with_connection_mut<T>(
        &self,
        use_connection: impl FnOnce(&mut Connection) -> T,
    ) -> T {
        let mut inner = self.inner.borrow_mut();
        let prestarted = inner
            .as_mut()
            .expect("prestarted connection must exist during alias resolution");
        use_connection(&mut prestarted.connection)
    }

    pub(in crate::cli) fn take(&self) -> Option<PrestartedServerConnection> {
        self.inner.borrow_mut().take()
    }
}

#[derive(Debug, Clone)]
pub(in crate::cli) struct StartupOptions {
    pub(in crate::cli) no_start_server: bool,
    pub(in crate::cli) config: AutoStartConfig,
    pub(in crate::cli) prestarted_connection: Option<PrestartedConnection>,
}

impl StartupOptions {
    pub(in crate::cli) fn new(no_start_server: bool, config: AutoStartConfig) -> Self {
        Self {
            no_start_server,
            config,
            prestarted_connection: None,
        }
    }

    pub(in crate::cli) fn with_prestarted_connection(
        mut self,
        connection: Option<PrestartedConnection>,
    ) -> Self {
        self.prestarted_connection = connection;
        self
    }

    pub(in crate::cli) fn for_command(
        &self,
        command_has_start_server_flag: bool,
        command_requires_web: bool,
        start_server_args: Option<&StartServerArgs>,
    ) -> Self {
        let mut config = if command_requires_web {
            self.config.clone().with_web_required()
        } else {
            self.config.clone()
        };
        if let Some(args) = start_server_args {
            config = apply_web_auto_start_config(config, args);
        }
        Self {
            no_start_server: self.no_start_server || !command_has_start_server_flag,
            config,
            prestarted_connection: self.prestarted_connection.clone(),
        }
    }
}

#[derive(Debug, Clone)]
pub(super) struct StartupConfig {
    pub(super) server: ServerStartupConfig,
    pub(super) auto_start: AutoStartConfig,
    pub(super) web_frontend: Option<String>,
    pub(super) web_port: Option<u16>,
}

#[derive(Debug, Clone)]
pub(super) enum ServerStartupConfig {
    Default {
        quiet: bool,
        cwd: Option<std::path::PathBuf>,
    },
    Files {
        files: Vec<std::path::PathBuf>,
        quiet: bool,
        cwd: Option<std::path::PathBuf>,
    },
}

pub(super) fn startup_config_from_cli(cli: &Cli) -> StartupConfig {
    startup_config_from_selection(cli.config_file_selection(), cli.command.as_ref())
}

pub(super) fn startup_config_from_top_level_scan(
    scan: &TopLevelCommandScan,
    first_command: &Command,
) -> StartupConfig {
    let selection = match scan.config_files.as_slice() {
        [] => ConfigFileSelection::Default,
        files => ConfigFileSelection::Custom(files),
    };
    startup_config_from_selection(selection, Some(first_command))
}

fn startup_config_from_selection(
    selection: ConfigFileSelection<'_>,
    command: Option<&Command>,
) -> StartupConfig {
    let cwd = std::env::current_dir().ok();
    let web = start_server_web_args(command);
    let mut config = match selection {
        ConfigFileSelection::Default => {
            let quiet = true;
            StartupConfig {
                server: ServerStartupConfig::Default {
                    quiet,
                    cwd: cwd.clone(),
                },
                auto_start: AutoStartConfig::default_files(quiet, cwd),
                web_frontend: web.web_frontend.clone(),
                web_port: web.web_port,
            }
        }
        ConfigFileSelection::Custom(files) => {
            let quiet = false;
            let files = files.to_vec();
            StartupConfig {
                server: ServerStartupConfig::Files {
                    files: files.clone(),
                    quiet,
                    cwd: cwd.clone(),
                },
                auto_start: AutoStartConfig::custom_files(files, quiet, cwd),
                web_frontend: web.web_frontend.clone(),
                web_port: web.web_port,
            }
        }
    };
    config.auto_start = apply_web_auto_start_config(config.auto_start, &web);
    config
}

fn start_server_web_args(command: Option<&Command>) -> StartServerArgs {
    match command {
        Some(Command::StartServer(args)) => args.clone(),
        _ => StartServerArgs::default(),
    }
}

fn apply_web_auto_start_config(
    mut config: AutoStartConfig,
    args: &StartServerArgs,
) -> AutoStartConfig {
    if let Some(port) = args.web_port {
        config = config.with_web_port(port);
    }
    if let Some(frontend) = &args.web_frontend {
        config = config.with_web_frontend(frontend.clone());
    }
    config
}

fn apply_server_startup_config(
    config: DaemonConfig,
    startup: &ServerStartupConfig,
) -> DaemonConfig {
    match startup {
        ServerStartupConfig::Default { quiet, cwd } => {
            config.with_default_config_load(*quiet, cwd.clone())
        }
        ServerStartupConfig::Files { files, quiet, cwd } => {
            config.with_config_files(files.clone(), *quiet, cwd.clone())
        }
    }
}

fn apply_web_daemon_config(config: DaemonConfig, startup: &StartupConfig) -> DaemonConfig {
    let config = match startup.web_port {
        Some(port) => config.with_web_port(port),
        None => config,
    };
    match &startup.web_frontend {
        Some(frontend) => config.with_web_frontend(frontend.clone()),
        None => config,
    }
}

pub(super) fn run_foreground_server(
    socket_path: &Path,
    startup_config: &StartupConfig,
) -> Result<i32, ExitFailure> {
    let config = apply_web_daemon_config(
        apply_server_startup_config(
            DaemonConfig::new(socket_path.to_path_buf()),
            &startup_config.server,
        ),
        startup_config,
    );
    let runtime = build_daemon_runtime().map_err(|error| ExitFailure::new(1, error.to_string()))?;

    runtime
        .block_on(async move {
            let server = ServerDaemon::new(config).bind().await?;
            server.wait().await
        })
        .map(|()| 0)
        .map_err(|error| ExitFailure::new(1, error.to_string()))
}