navy-nvim-rs 0.0.21

A library for writing neovim rpc clients
use std::{
    env,
    error::Error,
    ffi::OsString,
    io::{self, Write},
    process::Stdio,
    time::Duration,
};

use navy_nvim_rs::{Handler, Neovim, UiAttachOptions, rpc::payload::NotificationPayload};
use rmpv::ValueRef;
use tokio::{
    io::{AsyncBufReadExt, BufReader},
    process::{Child, ChildStdin, Command},
    time,
};

type AnyError = Box<dyn Error + Send + Sync>;

#[derive(Clone)]
struct DumpRedrawHandler;

impl Handler for DumpRedrawHandler {
    fn handle_notify(&self, payload: NotificationPayload) -> io::Result<()> {
        if payload.method()? != "redraw" {
            return Ok(());
        }

        let mut batches = payload.params();
        println!("redraw notification: {} batch(es)", batches.remaining());
        while !batches.is_empty() {
            batches.read_array(|batch| {
                println!("  {}", batch.read::<&str>()?);

                let mut i = 0usize;
                while !batch.is_empty() {
                    let value = batch.read::<ValueRef<'_>>()?;
                    println!("    [{i}] {value:?}");
                    i += 1;
                }

                Ok(())
            })?;
        }
        Ok(())
    }
}

fn default_nvim_path() -> OsString {
    if cfg!(windows) {
        "nvim.exe".into()
    } else {
        "nvim".into()
    }
}

fn nvim_path() -> OsString {
    env::args_os()
        .nth(1)
        .or_else(|| env::var_os("NVIMRS_TEST_BIN"))
        .unwrap_or_else(default_nvim_path)
}

fn nvim_command() -> Command {
    let mut cmd = Command::new(nvim_path());
    cmd.args(["--embed", "--headless", "--clean", "-i", "NONE"])
        .stderr(Stdio::inherit());
    cmd
}

fn print_prompt() -> io::Result<()> {
    eprint!("nvim> ");
    io::stderr().flush()
}

async fn input_repl(nvim: Neovim<ChildStdin>) -> Result<(), AnyError> {
    let stdin = BufReader::new(tokio::io::stdin());
    let mut lines = stdin.lines();

    loop {
        print_prompt()?;

        let Some(line) = lines.next_line().await? else {
            break;
        };

        if line.is_empty() {
            continue;
        }

        let input = format!("{line}<Enter>");
        nvim.notify_input(&input).await?;
    }

    Ok(())
}

fn handle_io_loop_result(result: Result<io::Result<()>, tokio::task::JoinError>) {
    match result {
        Ok(Ok(())) => {}
        Ok(Err(err)) if err.kind() == io::ErrorKind::UnexpectedEof => {}
        Ok(Err(err)) => eprintln!("RPC IO loop failed: {err}"),
        Err(err) => eprintln!("RPC IO task failed: {err}"),
    }
}

async fn wait_or_kill_child(mut child: Child) -> Result<(), AnyError> {
    if child.try_wait()?.is_some() {
        return Ok(());
    }

    if time::timeout(Duration::from_secs(2), child.wait())
        .await
        .is_err()
    {
        child.kill().await?;
    }

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), AnyError> {
    let (nvim, mut io_handle, child) = Neovim::spawn(nvim_command(), DumpRedrawHandler)?;

    let mut opts = UiAttachOptions::default();
    opts.set_rgb(true);
    opts.set_linegrid_external(true);
    opts.set_hlstate_external(true);
    nvim.ui_attach(80, 24, opts).await?;

    eprintln!("Type Neovim input line by line. Example: :quit");
    let mut input_task = tokio::spawn(input_repl(nvim.clone()));

    tokio::select! {
        result = &mut io_handle => {
            input_task.abort();
            handle_io_loop_result(result);
        }
        result = &mut input_task => {
            match result {
                Ok(Ok(())) => {
                    let _ = nvim.notify_input("<Cmd>qall!<CR>").await;
                }
                Ok(Err(err)) => return Err(err),
                Err(err) => return Err(err.into()),
            }
            io_handle.abort();
        }
    }

    wait_or_kill_child(child).await
}