lawn 0.1.1

utility to interact with trusted remote development systems
#![allow(dead_code)]
use crate::error::{Error, ErrorKind};
use crate::template::{Template, TemplateContext};
use bytes::{Bytes, BytesMut};
use lawn_constants::logger::Logger as LoggerTrait;
use lawn_constants::logger::{LogFormat, LogLevel};
use lawn_protocol::protocol::{ClipboardChannelOperation, ClipboardChannelTarget};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use std::collections::{BTreeMap, HashSet};
use std::ffi::OsStr;
use std::ffi::OsString;
use std::fs;
use std::io;
use std::io::{Read, Write};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::os::unix::fs::FileTypeExt;
use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::{Arc, Mutex, RwLock};

pub const VERSION: &str = concat!("Lawn/", env!("CARGO_PKG_VERSION"));

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ClipboardBackend {
    XClip,
    XSel,
    MacOS,
}

impl ClipboardBackend {
    pub fn supports_target(&self, target: ClipboardChannelTarget) -> bool {
        matches!(
            (self, target),
            (Self::XClip, _) | (Self::XSel, _) | (Self::MacOS, ClipboardChannelTarget::Clipboard)
        )
    }

    pub fn command(
        &self,
        target: ClipboardChannelTarget,
        op: ClipboardChannelOperation,
    ) -> Vec<Bytes> {
        let mut v: Vec<&'static [u8]> = Vec::new();
        match self {
            Self::XClip => {
                v.push(b"xclip");
                v.push(b"-selection");
                match target {
                    ClipboardChannelTarget::Primary => v.push(b"primary"),
                    ClipboardChannelTarget::Clipboard => v.push(b"clipboard"),
                }
                match op {
                    ClipboardChannelOperation::Copy => v.push(b"-i"),
                    ClipboardChannelOperation::Paste => v.push(b"-o"),
                }
            }
            Self::XSel => {
                v.push(b"xsel");
                match target {
                    ClipboardChannelTarget::Primary => v.push(b"-p"),
                    ClipboardChannelTarget::Clipboard => v.push(b"-b"),
                }
                match op {
                    ClipboardChannelOperation::Copy => v.push(b"-i"),
                    ClipboardChannelOperation::Paste => v.push(b"-o"),
                }
            }
            Self::MacOS => match op {
                ClipboardChannelOperation::Copy => v.push(b"pbcopy"),
                ClipboardChannelOperation::Paste => v.push(b"pbpaste"),
            },
        }
        v.iter().map(|&x| x.into()).collect()
    }
}

struct ConfigData {
    detach: bool,
    runtime_dir: PathBuf,
    format: LogFormat,
    config_file: ConfigFile,
    root: Option<bool>,
    clipboard_backend: Option<ClipboardBackend>,
    clipboard_enabled: Option<bool>,
}

pub struct Config {
    logger: Arc<Logger>,
    data: RwLock<ConfigData>,
    env_vars: BTreeMap<Bytes, Bytes>,
}

impl Config {
    pub fn new<
        E: FnMut(&str) -> Option<OsString>,
        I: Iterator<Item = (OsString, OsString)>,
        V: FnMut() -> I,
    >(
        env: E,
        env_iter: V,
        create: bool,
        verbosity: i32,
        stdout: Box<dyn Write + Sync + Send>,
        stderr: Box<dyn Write + Sync + Send>,
        config_file: Option<&PathBuf>,
    ) -> Result<Config, Error> {
        let mut env_iter = env_iter;
        let logger = Logger::new(verbosity, stdout, stderr);
        let runtime_dir = Self::create_runtime_dir(&logger, env, create).ok_or_else(|| {
            Error::new_with_message(
                ErrorKind::RuntimeDirectoryFailure,
                "cannot detect runtime directory",
            )
        })?;
        let config_file = {
            match config_file {
                Some(name) => match fs::File::open(name) {
                    Ok(f) => match serde_yaml::from_reader(f) {
                        Ok(config) => config,
                        Err(e) => {
                            return Err(Error::new_with_cause(ErrorKind::InvalidConfigFile, e))
                        }
                    },
                    Err(e) => return Err(Error::new_with_cause(ErrorKind::MissingConfigFile, e)),
                },
                None => ConfigFile::new(),
            }
        };
        Ok(Self {
            logger: Arc::new(logger),
            data: RwLock::new(ConfigData {
                detach: true,
                runtime_dir,
                format: LogFormat::Text,
                config_file,
                root: None,
                clipboard_backend: None,
                clipboard_enabled: None,
            }),
            env_vars: env_iter()
                .map(|(k, v)| (k.as_bytes().to_vec().into(), v.as_bytes().to_vec().into()))
                .collect(),
        })
    }

    pub fn template_context<'a>(
        &self,
        cenv: Option<&'a BTreeMap<Bytes, Bytes>>,
        args: Option<&'a [Bytes]>,
    ) -> TemplateContext<'_, 'a> {
        TemplateContext {
            senv: Some(&self.env_vars),
            cenv,
            args,
        }
    }

    pub fn format(&self) -> LogFormat {
        let g = self.data.read().unwrap();
        g.format
    }

    pub fn set_format(&self, format: LogFormat) {
        {
            let mut g = self.data.write().unwrap();
            g.format = format;
        }
        self.logger.set_format(format);
    }

    pub fn set_detach(&self, detach: bool) {
        let mut g = self.data.write().unwrap();
        g.detach = detach;
    }

    pub fn logger(&self) -> Arc<Logger> {
        self.logger.clone()
    }

    pub fn detach(&self) -> bool {
        let g = self.data.read().unwrap();
        g.detach
    }

    pub fn runtime_dir(&self) -> PathBuf {
        let g = self.data.read().unwrap();
        g.runtime_dir.clone()
    }

    #[allow(clippy::mutable_key_type)]
    pub fn env_vars(&self) -> &BTreeMap<Bytes, Bytes> {
        &self.env_vars
    }

    pub fn autoprune_sockets(&self) -> bool {
        let g = self.data.read().unwrap();
        g.config_file
            .v0
            .socket
            .as_ref()
            .and_then(|m| m.autoprune)
            .unwrap_or(false)
    }

    pub fn is_root(&self) -> Result<bool, Error> {
        let val = {
            let g = self.data.read().unwrap();
            if let Some(val) = g.root {
                return Ok(val);
            }
            // This is the default algorithm for determining whether we're running in a GUI.  It is
            // subject to change at any time.
            g.config_file.v0.root.clone().unwrap_or_else(|| Value::String("![ -z \"$SSH_TTY\" ] && { [ -n \"$WAYLAND_DISPLAY\" ] || [ -n \"$DISPLAY\" ] || [ \"$(uname -s)\" = Darwin ]; }".to_string()))
        };
        let ctx = self.template_context(None, None);
        let result = ConfigValue::new(val, &ctx)?.into_bool()?;
        let mut g = self.data.write().unwrap();
        g.root = Some(result);
        Ok(result)
    }

    pub fn p9p_enabled(&self, name: &str) -> Result<bool, Error> {
        let val = {
            let g = self.data.read().unwrap();
            if let Some(val) = g.clipboard_enabled {
                return Ok(val);
            }
            match g
                .config_file
                .v0
                .p9p
                .as_ref()
                .and_then(|p| p.get(name))
                .map(|x| x.if_value.clone())
            {
                Some(v) => v,
                None => Value::Bool(false),
            }
        };
        let ctx = self.template_context(None, None);
        ConfigValue::new(val, &ctx)?.into_bool()
    }

    pub fn p9p_location(&self, name: &str) -> Result<Option<String>, Error> {
        let val = {
            let g = self.data.read().unwrap();
            match g
                .config_file
                .v0
                .p9p
                .as_ref()
                .and_then(|p| p.get(name))
                .and_then(|x| x.location.clone())
            {
                Some(Value::String(ref s)) => s.clone(),
                None => return Ok(None),
                _ => return Err(Error::new(ErrorKind::InvalidConfigurationValue)),
            }
        };
        let ctx = self.template_context(None, None);
        let val = Value::String(val);
        Ok(Some(ConfigValue::new(val, &ctx)?.into_string()?))
    }

    fn clipboard_command_from_str(s: &str) -> Option<ClipboardBackend> {
        match s {
            "xclip" => Some(ClipboardBackend::XClip),
            "xsel" => Some(ClipboardBackend::XSel),
            "macos" => Some(ClipboardBackend::MacOS),
            _ => None,
        }
    }

    pub fn clipboard_enabled(&self) -> Result<bool, Error> {
        let val = {
            let g = self.data.read().unwrap();
            if let Some(val) = g.clipboard_enabled {
                return Ok(val);
            }
            match g
                .config_file
                .v0
                .clipboard
                .as_ref()
                .map(|x| x.if_value.clone())
            {
                Some(v) => v,
                None => Value::Bool(false),
            }
        };
        let ctx = self.template_context(None, None);
        let result = ConfigValue::new(val, &ctx)?.into_bool()?;
        let mut g = self.data.write().unwrap();
        g.clipboard_enabled = Some(result);
        Ok(result)
    }

    pub fn clipboard_backend(&self) -> Result<Option<ClipboardBackend>, Error> {
        let val = {
            let g = self.data.read().unwrap();
            if let Some(val) = g.clipboard_backend {
                return Ok(Some(val));
            }
            match g
                .config_file
                .v0
                .clipboard
                .as_ref()
                .and_then(|x| x.backend.clone())
            {
                Some(Value::String(ref s)) => {
                    if let Some(backend) = Self::clipboard_command_from_str(s) {
                        return Ok(Some(backend));
                    } else if s.starts_with('!') {
                        s.to_string()
                    } else if s == "default" {
                        "!f() { if command -v pbcopy >/dev/null 2>&1 && command -v pbpaste >/dev/null 2>&1; then echo macos; elif command -v xclip >/dev/null 2>&1; then echo xclip; elif command -v xsel >/dev/null 2>&1; then echo xsel; fi; };f".to_string()
                    } else {
                        return Err(Error::new(ErrorKind::InvalidConfigurationValue));
                    }
                }
                None => return Ok(None),
                _ => return Err(Error::new(ErrorKind::InvalidConfigurationValue)),
            }
        };
        let ctx = self.template_context(None, None);
        let val = Value::String(val);
        let result = ConfigValue::new(val, &ctx)?.into_string()?;
        let backend = Self::clipboard_command_from_str(&result);
        let mut g = self.data.write().unwrap();
        g.clipboard_backend = backend;
        Ok(backend)
    }

    pub fn config_command(&self, name: &[u8]) -> Option<ConfigCommand> {
        let name = match String::from_utf8(name.to_vec()) {
            Ok(name) => name,
            Err(_) => return None,
        };
        let g = self.data.read().unwrap();
        let commands = g.config_file.v0.commands.as_ref()?;
        Some(commands.get(&name)?.clone())
    }

    pub fn sockets(&self) -> Vec<PathBuf> {
        let logger = self.logger.clone();
        Self::find_sockets(&logger, |s| {
            self.env_vars
                .get(&Bytes::copy_from_slice(s.as_bytes()))
                .map(|x| OsStr::from_bytes(x).into())
        })
    }

    fn find_runtime_dirs<E: FnMut(&str) -> Option<OsString>>(
        logger: &Logger,
        mut env: E,
    ) -> Vec<PathBuf> {
        let mut v = vec![];
        logger.trace("runtime_dir: looking for XDG_RUNTIME_DIR");
        if let Some(dir) = env("XDG_RUNTIME_DIR") {
            let mut buf: PathBuf = dir.into();
            buf.push("lawn");
            logger.trace(&format!("runtime_dir: found, using {:?}", buf));
            v.push(buf);
        }
        logger.trace("runtime_dir: looking for HOME");
        if let Some(dir) = env("HOME") {
            let mut buf: PathBuf = dir.into();
            buf.push(".local");
            buf.push("run");
            buf.push("lawn");
            logger.trace(&format!("runtime_dir: found, using {:?}", buf));
            v.push(buf);
        }
        let mut m = HashSet::new();
        v.iter()
            .filter(|&x| {
                if m.contains(x) {
                    false
                } else {
                    m.insert(x.clone());
                    true
                }
            })
            .cloned()
            .collect()
    }

    fn find_sockets<E: FnMut(&str) -> Option<OsString>>(logger: &Logger, env: E) -> Vec<PathBuf> {
        let dirs = Self::find_runtime_dirs(logger, env);
        let mut v = vec![];
        for dir in dirs {
            if let Ok(iter) = std::fs::read_dir(&dir) {
                v.extend(iter.filter_map(|f| {
                    let f = match f {
                        Ok(f) => f,
                        Err(_) => return None,
                    };
                    match f.file_type() {
                        Ok(m) if m.is_socket() => Some(f.path()),
                        _ => None,
                    }
                }));
            }
        }
        v
    }

    fn find_runtime_dir<E: FnMut(&str) -> Option<OsString>>(
        logger: &Logger,
        env: E,
    ) -> Option<PathBuf> {
        match Self::find_runtime_dirs(logger, env).get(0) {
            Some(s) => Some(s.clone()),
            None => {
                trace!(logger, "runtime_dir: unable to find runtime directory");
                None
            }
        }
    }

    fn create_runtime_dir<E: FnMut(&str) -> Option<OsString>>(
        logger: &Logger,
        env: E,
        create: bool,
    ) -> Option<PathBuf> {
        let dir = Self::find_runtime_dir(logger, env)?;
        if create {
            logger.trace("runtime_dir: attempting to create");
            match std::fs::create_dir_all(&dir) {
                Ok(()) => {
                    logger.trace("runtime_dir: successfully created");
                    return Some(dir);
                }
                Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
                    logger.trace("runtime_dir: already exists (success)");
                    return Some(dir);
                }
                Err(e) => {
                    logger.trace(&format!("runtime_dir: failed: {}", e));
                    return None;
                }
            }
        }
        Some(dir)
    }
}

pub struct Logger {
    output: Mutex<Box<dyn Write + Sync + Send>>,
    error: Mutex<Box<dyn Write + Sync + Send>>,
    verbosity: i32,
    level: LogLevel,
    format: RwLock<LogFormat>,
}

impl Logger {
    pub fn new(
        verbosity: i32,
        output: Box<dyn Write + Sync + Send>,
        error: Box<dyn Write + Sync + Send>,
    ) -> Self {
        let level = match verbosity {
            x if x < -1 => LogLevel::Fatal,
            -1 => LogLevel::Error,
            0 => LogLevel::Normal,
            1 => LogLevel::Info,
            2 => LogLevel::Debug,
            3 => LogLevel::Trace,
            4 => LogLevel::Dump,
            _ => LogLevel::Dump,
        };
        Self {
            output: Mutex::new(output),
            error: Mutex::new(error),
            verbosity,
            level,
            format: RwLock::new(LogFormat::Text),
        }
    }

    fn write(&self, desired: i32, io: &Mutex<Box<dyn Write + Sync + Send>>, msg: &str) {
        if self.verbosity >= desired {
            let mut m = io.lock().unwrap();
            let _ = m.write_all(msg.as_bytes());
        }
    }

    fn set_format(&self, format: LogFormat) {
        let mut g = self.format.write().unwrap();
        *g = format;
    }
}

impl lawn_protocol::config::Logger for Logger {
    fn level(&self) -> LogLevel {
        self.level
    }

    fn format(&self) -> LogFormat {
        let g = self.format.read().unwrap();
        *g
    }

    fn fatal(&self, msg: &str) {
        self.write(-1, &self.error, &format!("fatal: {}\n", msg));
    }

    fn error(&self, msg: &str) {
        self.write(-1, &self.error, &format!("error: {}\n", msg));
    }

    fn message(&self, msg: &str) {
        let format = {
            let g = self.format.read().unwrap();
            *g
        };
        if format == LogFormat::Text {
            self.write(0, &self.output, &format!("{}\n", msg));
        }
    }

    fn info(&self, msg: &str) {
        self.write(1, &self.error, &format!("info: {}\n", msg));
    }

    fn debug(&self, msg: &str) {
        self.write(2, &self.error, &format!("debug: {}\n", msg));
    }

    fn trace(&self, msg: &str) {
        self.write(3, &self.error, &format!("trace: {}\n", msg));
    }
}

pub struct ConfigValue<'a, 'b, 'c> {
    value: Value,
    context: &'c TemplateContext<'a, 'b>,
}

impl<'a, 'b, 'c> ConfigValue<'a, 'b, 'c> {
    pub fn new(
        value: Value,
        context: &'c TemplateContext<'a, 'b>,
    ) -> Result<ConfigValue<'a, 'b, 'c>, Error> {
        Ok(ConfigValue { value, context })
    }

    fn templatize(s: &str, context: &'c TemplateContext<'a, 'b>) -> Result<Bytes, Error> {
        if s.is_empty() || !s.starts_with('!') {
            return Err(Error::new_with_message(
                ErrorKind::UnknownCommandType,
                format!("command {} must start with a !", s),
            ));
        }
        let t = Template::new(s[1..].as_bytes());
        t.expand(context).map_err(|e| {
            Error::new_full(
                ErrorKind::TemplateError,
                e,
                format!("invalid template string '{}'", s),
            )
        })
    }

    fn into_bool(self) -> Result<bool, Error> {
        let command = match self.value {
            Value::Bool(b) => return Ok(b),
            Value::String(ref s) => s,
            _ => return Err(Error::new(ErrorKind::InvalidConfigurationValue)),
        };
        let mut cmd = self.create_command(&Self::templatize(command, self.context)?);
        cmd.stdin(Stdio::null());
        cmd.stdout(Stdio::null());
        cmd.stderr(Stdio::null());
        let mut child = match cmd.spawn() {
            Ok(c) => c,
            Err(_) => return Ok(false),
        };
        match child.wait() {
            Ok(es) => Ok(es.success()),
            Err(_) => Ok(false),
        }
    }

    fn into_string(self) -> Result<String, Error> {
        let s = match self.value {
            Value::String(ref s) => s,
            _ => return Err(Error::new(ErrorKind::InvalidConfigurationValue)),
        };
        if !s.starts_with('!') {
            return Ok(s.into());
        }
        let mut cmd = self.create_command(&Self::templatize(s, self.context)?);
        cmd.stdin(Stdio::null());
        cmd.stdout(Stdio::piped());
        cmd.stderr(Stdio::null());
        let mut child = match cmd.spawn() {
            Ok(c) => c,
            Err(_) => return Ok(String::new()),
        };
        let mut s = String::new();
        if child
            .stdout
            .as_mut()
            .unwrap()
            .read_to_string(&mut s)
            .is_err()
        {
            return Err(Error::new(ErrorKind::ConfigurationSpawnError));
        }
        let _ = child.wait();
        Ok(s.trim_end_matches(|c| c == '\n').into())
    }

    fn create_command(&self, shell: &Bytes) -> std::process::Command {
        let mut shell: BytesMut = shell.as_ref().into();
        if self.context.args.is_some() {
            shell.extend_from_slice(b" \"$@\"");
        }
        let mut cmd = std::process::Command::new("sh");
        cmd.arg("-c");
        cmd.arg(OsStr::from_bytes(&shell));
        if let Some(args) = self.context.args {
            for arg in args {
                cmd.arg(OsString::from_vec(arg.to_vec()));
            }
        }
        if let Some(senv) = self.context.senv {
            cmd.env_clear();
            cmd.envs(senv.iter().map(|(k, v)| {
                (
                    OsString::from_vec(k.to_vec()),
                    OsString::from_vec(v.to_vec()),
                )
            }));
        }
        cmd.current_dir("/");
        cmd
    }
}

pub fn command_from_shell(
    shell: &Bytes,
    context: &TemplateContext<'_, '_>,
) -> tokio::process::Command {
    let mut shell: BytesMut = shell.as_ref().into();
    shell.extend_from_slice(b" \"$@\"");
    command_from_args(
        &[
            (b"sh" as &'static [u8]).into(),
            (b"-c" as &'static [u8]).into(),
            shell.into(),
        ],
        context,
    )
}

pub fn std_command_from_shell(
    shell: &Bytes,
    context: &TemplateContext<'_, '_>,
) -> std::process::Command {
    let mut shell: BytesMut = shell.as_ref().into();
    shell.extend_from_slice(b" \"$@\"");
    std_command_from_args(
        &[
            (b"sh" as &'static [u8]).into(),
            (b"-c" as &'static [u8]).into(),
            shell.into(),
        ],
        context,
    )
}

pub fn command_from_args(
    args: &[Bytes],
    context: &TemplateContext<'_, '_>,
) -> tokio::process::Command {
    let args: Vec<OsString> = args
        .iter()
        .map(|x| OsString::from_vec(x.to_vec()))
        .collect();
    let mut cmd = tokio::process::Command::new(&args[0]);
    if args.len() > 1 {
        cmd.args(&args[1..]);
    }
    if let Some(args) = context.args {
        for arg in args {
            cmd.arg(OsString::from_vec(arg.to_vec()));
        }
    }
    if let Some(senv) = context.senv {
        cmd.env_clear();
        cmd.envs(senv.iter().map(|(k, v)| {
            (
                OsString::from_vec(k.to_vec()),
                OsString::from_vec(v.to_vec()),
            )
        }));
    }
    cmd.current_dir("/");
    cmd
}

pub fn std_command_from_args(
    args: &[Bytes],
    context: &TemplateContext<'_, '_>,
) -> std::process::Command {
    let args: Vec<OsString> = args
        .iter()
        .map(|x| OsString::from_vec(x.to_vec()))
        .collect();
    let mut cmd = std::process::Command::new(&args[0]);
    if args.len() > 1 {
        cmd.args(&args[1..]);
    }
    if let Some(args) = context.args {
        for arg in args {
            cmd.arg(OsString::from_vec(arg.to_vec()));
        }
    }
    if let Some(senv) = context.senv {
        cmd.env_clear();
        cmd.envs(senv.iter().map(|(k, v)| {
            (
                OsString::from_vec(k.to_vec()),
                OsString::from_vec(v.to_vec()),
            )
        }));
    }
    cmd.current_dir("/");
    cmd
}

pub struct Command<'a, 'b, 'c> {
    condition: Option<Value>,
    pre: Vec<Bytes>,
    post: Vec<Bytes>,
    command: Bytes,
    context: &'c TemplateContext<'a, 'b>,
}

impl<'a, 'b, 'c> Command<'a, 'b, 'c> {
    pub fn new(
        config: &ConfigCommand,
        context: &'c TemplateContext<'a, 'b>,
    ) -> Result<Command<'a, 'b, 'c>, Error> {
        let pre = match &config.pre {
            Some(cmds) => cmds
                .iter()
                .map(|s| Self::templatize(s, context))
                .collect::<Result<Vec<_>, _>>()?,
            None => Vec::new(),
        };
        let post = match &config.post {
            Some(cmds) => cmds
                .iter()
                .map(|s| Self::templatize(s, context))
                .collect::<Result<Vec<_>, _>>()?,
            None => Vec::new(),
        };
        Ok(Command {
            condition: Some(config.if_value.clone()),
            pre,
            post,
            command: Self::templatize(&config.command, context)?,
            context,
        })
    }

    fn templatize(s: &str, context: &'c TemplateContext<'a, 'b>) -> Result<Bytes, Error> {
        if s.is_empty() || !s.starts_with('!') {
            return Err(Error::new_with_message(
                ErrorKind::UnknownCommandType,
                format!("command {} must start with a !", s),
            ));
        }
        let t = Template::new(s[1..].as_bytes());
        t.expand(context).map_err(|e| {
            Error::new_full(
                ErrorKind::TemplateError,
                e,
                format!("invalid template string '{}'", s),
            )
        })
    }

    async fn run_one(&self, shell: &Bytes) -> Result<i32, Error> {
        if shell == b"true" as &[u8] {
            return Ok(0);
        } else if shell == b"false" as &[u8] {
            return Ok(1);
        }
        let mut cmd = command_from_shell(shell, self.context);
        cmd.stdin(Stdio::null());
        cmd.stdout(Stdio::inherit());
        cmd.stderr(Stdio::inherit());
        match cmd.spawn() {
            Ok(mut child) => match child.wait().await {
                Ok(status) => Ok(status
                    .code()
                    .or_else(|| status.signal().map(|x| x + 128))
                    .unwrap_or(-1)),
                // It's unclear in what situation this can happen.  The command ran, just not
                // successfully in our opinion.
                Err(_) => Ok(-1),
            },
            Err(e) => Err(Error::new_with_cause(ErrorKind::CommandFailure, e)),
        }
    }

    pub fn run_command(&self) -> tokio::process::Command {
        command_from_shell(&self.command, self.context)
    }

    pub fn run_std_command(&self) -> std::process::Command {
        std_command_from_shell(&self.command, self.context)
    }

    pub async fn check_condition(&self) -> Result<bool, Error> {
        match &self.condition {
            Some(condition) => ConfigValue::new(condition.clone(), self.context)?.into_bool(),
            None => Ok(false),
        }
    }

    pub async fn run_pre_hooks(&self) -> Result<bool, Error> {
        let mut state = true;
        for hook in &self.pre {
            state = state && self.run_one(hook).await? == 0;
        }
        Ok(state)
    }

    pub async fn run_post_hooks(&self) -> Result<(), Error> {
        for hook in &self.post {
            self.run_one(hook).await?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
struct ConfigFile {
    v0: ConfigFileV0,
}

impl ConfigFile {
    fn new() -> Self {
        ConfigFile {
            v0: ConfigFileV0 {
                root: None,
                clipboard: None,
                socket: None,
                commands: None,
                p9p: None,
            },
        }
    }
}

#[derive(Serialize, Deserialize)]
struct ConfigFileV0 {
    root: Option<Value>,
    clipboard: Option<ConfigClipboard>,
    socket: Option<ConfigSockets>,
    commands: Option<BTreeMap<String, ConfigCommand>>,
    #[serde(rename = "9p")]
    p9p: Option<BTreeMap<String, Config9P>>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ConfigClipboard {
    #[serde(rename = "if")]
    if_value: Value,
    backend: Option<Value>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ConfigSockets {
    autoprune: Option<bool>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ConfigCommand {
    #[serde(rename = "if")]
    if_value: Value,
    command: String,
    #[serde(rename = "pre")]
    pre: Option<Vec<String>>,
    #[serde(rename = "post")]
    post: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct Config9P {
    #[serde(rename = "if")]
    if_value: Value,
    location: Option<Value>,
}

#[cfg(test)]
mod tests {
    use super::{Config, ConfigFile};
    use lawn_constants::logger::{LogFormat, LogLevel, Logger};
    use serde_yaml::Value;
    use std::collections::BTreeMap;
    use std::ffi::OsString;

    fn config_with_values<F: FnOnce(&mut ConfigFile)>(
        env: BTreeMap<OsString, OsString>,
        f: F,
    ) -> Config {
        let stdout = std::io::Cursor::new(Vec::new());
        let stderr = std::io::Cursor::new(Vec::new());
        let mut env = env;
        env.insert("PATH".into(), std::env::var_os("PATH").unwrap());
        env.insert("XDG_RUNTIME_DIR".into(), "/tmp".into());
        let env2 = env.clone();
        let cfg = Config::new(
            |var| env2.get(&OsString::from(var)).map(|x| x.clone()),
            || env.iter().map(|(a, b)| (a.clone(), b.clone())),
            false,
            3,
            Box::new(stdout),
            Box::new(stderr),
            None,
        )
        .unwrap();
        {
            let mut g = cfg.data.write().unwrap();
            f(&mut g.config_file);
        }
        cfg
    }

    #[test]
    fn default_is_root() {
        let mut env = BTreeMap::new();
        env.insert("SSH_TTY".into(), "/nonexistent/pts/0".into());
        env.insert("DISPLAY".into(), ":none".into());
        let cfg = config_with_values(env, |_| ());
        assert_eq!(cfg.is_root().unwrap(), false);

        let mut env = BTreeMap::new();
        env.insert("DISPLAY".into(), ":none".into());
        let cfg = config_with_values(env, |_| ());
        assert_eq!(cfg.is_root().unwrap(), true);

        let mut env = BTreeMap::new();
        env.insert("WAYLAND_DISPLAY".into(), ":none".into());
        let cfg = config_with_values(env, |_| ());
        assert_eq!(cfg.is_root().unwrap(), true);
    }

    #[test]
    fn is_root_with_values() {
        let cfg = config_with_values(BTreeMap::new(), |c| {
            c.v0.root = Some(Value::String("!command true".into()))
        });
        assert_eq!(cfg.is_root().unwrap(), true);

        let cfg = config_with_values(BTreeMap::new(), |c| c.v0.root = Some(Value::Bool(true)));
        assert_eq!(cfg.is_root().unwrap(), true);

        let cfg = config_with_values(BTreeMap::new(), |c| {
            c.v0.root = Some(Value::String("!command false".into()))
        });
        assert_eq!(cfg.is_root().unwrap(), false);

        let cfg = config_with_values(BTreeMap::new(), |c| c.v0.root = Some(Value::Bool(false)));
        assert_eq!(cfg.is_root().unwrap(), false);

        let cfg = config_with_values(BTreeMap::new(), |c| {
            c.v0.root = Some("!cat /dev/null".into())
        });
        assert_eq!(cfg.is_root().unwrap(), true);
    }

    struct PanicLogger {
        level: LogLevel,
    }

    impl Logger for PanicLogger {
        fn level(&self) -> LogLevel {
            self.level
        }

        fn format(&self) -> LogFormat {
            LogFormat::Text
        }

        fn fatal(&self, _msg: &str) {
            panic!("fatal");
        }

        fn error(&self, _msg: &str) {
            panic!("error");
        }

        fn message(&self, _msg: &str) {
            panic!("message");
        }

        fn info(&self, _msg: &str) {
            panic!("info");
        }

        fn debug(&self, _msg: &str) {
            panic!("debug");
        }

        fn trace(&self, _msg: &str) {
            panic!("trace");
        }
    }

    #[test]
    fn skips_calls() {
        let logger = PanicLogger {
            level: LogLevel::Debug,
        };
        trace!(logger, "this should never be invoked");
    }
}