dotune 0.1.1

A cross-platform dotfile manager configured with Lua
use std::path::{Component, Path as StdPath};
use std::sync::Arc;

use itertools::Itertools;
use mlua::prelude::*;

use super::utils::bad_argument;
use crate::model::{AbsolutePath, AbsolutePathBuf, SystemInfo};

#[derive(LuaUserData, FromLua, Clone)]
pub struct Path(#[lua(skip)] pub Arc<AbsolutePath>);

#[derive(LuaUserData)]
pub struct PathFactory {
    #[lua(skip)]
    pub script_path: Arc<AbsolutePath>,
    #[lua(skip)]
    pub system_info: Arc<SystemInfo>,
}

#[mlua::userdata_impl]
impl PathFactory {
    #[lua(getter, infallible)]
    fn script(&self) -> Path {
        Path(self.script_path.clone())
    }

    #[lua(getter, infallible)]
    fn script_dir(&self) -> Path {
        self.script_path
            .parent()
            .map(|parent| Path(parent.into()))
            .expect("script path always lives in some directory")
    }

    #[lua(getter, infallible)]
    fn here(&self) -> Path {
        self.script_dir()
    }

    #[lua(getter, infallible)]
    fn home(&self) -> Path {
        Path(self.system_info.home.clone())
    }

    #[lua(getter, infallible)]
    fn root(&self) -> Path {
        Path(self.system_info.root.clone())
    }

    #[lua]
    fn which(path: &str) -> LuaResult<Option<Path>> {
        match which::which(path) {
            Err(which::Error::CannotFindBinaryPath) => Ok(None),
            Err(error) => Err(LuaError::external(error)),
            Ok(bin_path) => Ok(Some(Path(
                AbsolutePathBuf::new(bin_path)
                    .expect("which should return an absolute path")
                    .into(),
            ))),
        }
    }

    #[lua(meta, infallible)]
    fn __tostring(&self) -> String {
        "<Path>".to_string()
    }
}

#[mlua::userdata_impl]
impl Path {
    #[lua(getter, infallible)]
    fn parent(&self) -> Option<Self> {
        self.0.parent().map(|parent| Self(parent.into()))
    }

    #[lua]
    fn join(&self, relative_path: &str) -> LuaResult<Self> {
        Ok(Self(self.0.join(relative_path).into()))
    }

    #[lua]
    fn to(&self, dir: &Path) -> LuaResult<(Self, Self)> {
        let file_name = self.0.file_name().ok_or_else(|| {
            bad_argument("Path.to", 1, Some("self"), "path with a file name expected")
        })?;

        Ok((self.clone(), Self(dir.0.join(file_name).into())))
    }

    #[lua]
    fn with_tail(&self, tail: &str) -> LuaResult<Self> {
        let Ok(Component::Normal(normal_tail)) = StdPath::new(tail).components().exactly_one()
        else {
            return Err(bad_argument(
                "Path.with_tail",
                1,
                None,
                "normal path component expected",
            ));
        };

        Ok(Self(self.0.with_file_name(normal_tail).into()))
    }

    #[lua]
    fn with_extension(&self, extension: &str) -> LuaResult<Self> {
        let Ok(Component::Normal(normal_extension)) =
            StdPath::new(extension).components().exactly_one()
        else {
            return Err(bad_argument(
                "Path.with_extension",
                1,
                None,
                "normal path component expected",
            ));
        };

        Ok(Self(self.0.with_extension(normal_extension).into()))
    }

    #[lua]
    fn exists(&self) -> LuaResult<bool> {
        self.0.try_exists().map_err(LuaError::external)
    }

    #[lua(infallible)]
    fn resolve(&self) -> Option<Self> {
        AbsolutePathBuf::canonicalize(self.0.as_ref())
            .map(|path| Self(path.into()))
            .ok()
    }

    #[lua(meta, infallible)]
    fn __tostring(&self) -> String {
        self.0.to_string_lossy().to_string()
    }

    #[lua(meta)]
    fn __div(&self, part: &str) -> LuaResult<Self> {
        self.join(part)
    }

    #[lua(meta)]
    fn __mod(&self, tail: &str) -> LuaResult<Self> {
        self.with_tail(tail)
    }

    #[lua(meta)]
    fn __mul(&self, extension: &str) -> LuaResult<Self> {
        self.with_extension(extension)
    }
}