fux 0.8.0

Minimal persistent terminal multiplexer built on bevy_ecs
Documentation
//! Private runtime/state locations for one user.

use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DaemonPaths {
    pub runtime_dir: PathBuf,
    pub state_dir: PathBuf,
    pub manager_socket: PathBuf,
    pub descriptors_dir: PathBuf,
}

impl DaemonPaths {
    pub fn discover() -> Result<Self, PathError> {
        Self::from_env(
            env::var_os("XDG_RUNTIME_DIR"),
            env::var_os("XDG_STATE_HOME"),
            env::var_os("HOME"),
        )
    }

    pub fn from_env(
        runtime: Option<OsString>,
        state: Option<OsString>,
        home: Option<OsString>,
    ) -> Result<Self, PathError> {
        let runtime = absolute(runtime)
            .or_else(|| macos_runtime_fallback(home.as_ref()))
            .ok_or(PathError::MissingRuntime)?;
        let state = absolute(state)
            .or_else(|| absolute(home).map(|path| path.join(".local/state")))
            .ok_or(PathError::MissingState)?;
        let runtime_dir = runtime.join("fux");
        let state_dir = state.join("fux");
        Ok(Self {
            manager_socket: runtime_dir.join("manager.sock"),
            descriptors_dir: runtime_dir.join("workspaces"),
            runtime_dir,
            state_dir,
        })
    }

    pub fn prepare(&self) -> Result<(), PathError> {
        private_dir(&self.runtime_dir)?;
        private_dir(&self.state_dir)?;
        private_dir(&self.descriptors_dir)
    }

    pub fn descriptor(&self, name: &str) -> Result<PathBuf, PathError> {
        crate::ids::validate_workspace_name(name).map_err(|_| PathError::UnsafeName)?;
        Ok(self.descriptors_dir.join(format!("{name}.json")))
    }

    pub fn attach_socket(&self, name: &str) -> Result<PathBuf, PathError> {
        crate::ids::validate_workspace_name(name).map_err(|_| PathError::UnsafeName)?;
        Ok(self.runtime_dir.join(format!("{name}.attach.sock")))
    }

    pub fn control_socket(&self, name: &str) -> Result<PathBuf, PathError> {
        crate::ids::validate_workspace_name(name).map_err(|_| PathError::UnsafeName)?;
        Ok(self.runtime_dir.join(format!("{name}.sock")))
    }
}

#[cfg(target_os = "macos")]
fn macos_runtime_fallback(home: Option<&OsString>) -> Option<PathBuf> {
    absolute(home.cloned()).map(|path| path.join("Library/Caches/fux-runtime"))
}

#[cfg(not(target_os = "macos"))]
fn macos_runtime_fallback(_: Option<&OsString>) -> Option<PathBuf> {
    None
}

#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum PathError {
    #[error("XDG_RUNTIME_DIR (or HOME on macOS) must be set to an absolute path")]
    MissingRuntime,
    #[error("XDG_STATE_HOME or HOME must be set to an absolute path")]
    MissingState,
    #[error("unsafe workspace name")]
    UnsafeName,
    #[error("{} must be a private directory owned by this user", .0.display())]
    UnsafeDirectory(PathBuf),
    #[error("{0}")]
    Io(String),
}

fn absolute(value: Option<OsString>) -> Option<PathBuf> {
    value
        .filter(|value| !value.is_empty())
        .map(PathBuf::from)
        .filter(|path| path.is_absolute())
}

fn private_dir(path: &Path) -> Result<(), PathError> {
    crate::proto::socket::ensure_private_directory(path).map_err(|error| {
        if error.kind() == std::io::ErrorKind::PermissionDenied {
            PathError::UnsafeDirectory(path.to_owned())
        } else {
            PathError::Io(error.to_string())
        }
    })
}

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

    #[test]
    fn paths_derive_from_runtime_and_state_homes() {
        let paths = DaemonPaths::from_env(
            Some("/run/user/1".into()),
            Some("/home/u/.local/state".into()),
            Some("/home/u".into()),
        )
        .unwrap_or_else(|_| DaemonPaths {
            runtime_dir: PathBuf::new(),
            state_dir: PathBuf::new(),
            manager_socket: PathBuf::new(),
            descriptors_dir: PathBuf::new(),
        });
        assert_eq!(
            paths.manager_socket,
            PathBuf::from("/run/user/1/fux/manager.sock")
        );
        assert_eq!(
            paths.attach_socket("default").ok(),
            Some(PathBuf::from("/run/user/1/fux/default.attach.sock"))
        );
        assert_eq!(
            paths.control_socket("default").ok(),
            Some(PathBuf::from("/run/user/1/fux/default.sock"))
        );
        assert!(paths.descriptor("../x").is_err());
        assert!(DaemonPaths::from_env(Some("relative".into()), None, None).is_err());
    }
}