mirage 0.0.1

LSP proxying between build servers and local machines
mod lsp;
mod sync;
use clap::{Parser, Subcommand};
use derive_more::{Display, From, Into};
use lsp::LspServerCommand;
use std::{path::PathBuf, str::FromStr};
use tracing::{error, info};
use tracing_subscriber::{EnvFilter, fmt, prelude::*};

#[derive(Clone, Debug, Display, From, Into)]
struct Host(String);

#[derive(Clone, Copy, Debug, Display, From, Into)]
struct Port(u16);

impl Default for Port {
    fn default() -> Self {
        Self(22)
    }
}

#[derive(Clone, Debug, From, Into)]
struct Workspace(PathBuf);

impl Workspace {
    fn namespace(&self) -> eyre::Result<Namespace> {
        let username = std::env::var("USER")
            .or_else(|_| std::env::var("USERNAME"))
            .or_else(|_| std::env::var("LOGNAME"))
            .unwrap_or("unknown".to_string());

        let hostname = std::env::var("HOST")
            .or_else(|_| std::env::var("HOSTNAME"))
            .unwrap_or("unknown".to_string());

        let folder = self
            .0
            .display()
            .to_string()
            .replacen("/", "", 1)
            .replace("/", "-");

        let path = PathBuf::from(format!("{username}@{hostname}/{folder}"));

        Ok(Namespace::from(path))
    }

    fn display(&self) -> std::path::Display<'_> {
        self.0.display()
    }
}

impl FromStr for Workspace {
    type Err = eyre::Error;

    fn from_str(s: &str) -> eyre::Result<Self> {
        let path = PathBuf::from(s).canonicalize()?;

        Ok(Self::from(path))
    }
}

#[derive(Clone, Debug, From, Into)]
struct Namespace(PathBuf);

impl Namespace {
    fn display(&self) -> std::path::Display<'_> {
        self.0.display()
    }
}

#[derive(Clone, Debug, Display, From, Into)]
#[display("{host}:{port}")]
struct Remote {
    host: Host,
    port: Port,
}

impl Remote {
    fn host(&self) -> &Host {
        &self.host
    }

    fn port(&self) -> Port {
        self.port
    }
}

impl FromStr for Remote {
    type Err = eyre::Error;

    fn from_str(s: &str) -> eyre::Result<Self> {
        let remote = match s.split_once(':') {
            Some((h, p)) => Remote {
                host: h.to_string().into(),
                port: u16::from_str_radix(p, 10)?.into(),
            },
            None => Remote {
                host: s.to_string().into(),
                port: Port::default(),
            },
        };

        Ok(remote)
    }
}

#[derive(Parser)]
#[command(name = "mirage")]
#[command(about = "Remote LSP proxy for Neovim", long_about = None)]
struct Args {
    /// Remote host to connect to (format: user@host:port or just host)
    #[arg(short = 'r', long = "remote", value_name = "root@127.0.0.1:22", value_parser = Remote::from_str)]
    remote: Remote,

    /// Local workspace path to sync (defaults to current directory)
    #[arg(short = 'w', long = "workspace", env = "PWD", value_name = "/some/directory", value_parser = Workspace::from_str)]
    workspace: Workspace,

    /// Directories or patterns to exclude from sync (can be specified multiple times)
    #[arg(short = 'e', long = "exclude", default_values = ["target", ".git"])]
    exclude: Vec<String>,

    /// Sets the level of verbosity
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Start an LSP server on the remote machine
    Lsp {
        /// LSP server to run
        #[command(subcommand)]
        server: LspServerCommand,
    },
}

#[tokio::main]
async fn main() {
    let cli = Args::parse();

    // For LSP mode, only show logs if verbose is enabled
    let level = match &cli.command {
        Commands::Lsp { .. } => {
            if cli.verbose == 0 {
                "off"
            } else {
                match cli.verbose {
                    1 => "info",
                    2 => "debug",
                    _ => "trace",
                }
            }
        }
    };

    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)))
        .init();

    let remote = cli.remote;
    let workspace = cli.workspace;
    let namespace = workspace
        .namespace()
        .expect("something went wrong while determening namespace");

    let session = sync::Session::start(&remote, &workspace, &namespace)
        .expect("failed to start mutagen session");

    match &cli.command {
        Commands::Lsp { server } => {
            info!("Starting mirage LSP proxy");

            let lsp_server = lsp::LspServer::from(server);

            if let Err(e) = lsp::start(
                remote.host().clone(),
                remote.port(),
                workspace,
                &cli.exclude,
                &lsp_server,
            )
            .await
            {
                error!("Failed to start remote LSP: {}", e);
            }
        }
    }

    if let Err(e) = session.terminate() {
        error!("Failed to terminate sync session: {}", e);
        std::process::exit(1);
    }
}