diff --git a/Cargo.lock b/Cargo.lock index 010140e..5d4ae5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1577,6 +1577,7 @@ dependencies = [ "euclid", "flexi_logger", "font-kit", + "futures 0.3.12", "image", "lazy_static", "log", @@ -1585,6 +1586,7 @@ dependencies = [ "neovide-derive", "nvim-rs", "parking_lot 0.10.2", + "pin-project", "rand", "rmpv", "rust-embed", diff --git a/Cargo.toml b/Cargo.toml index b148b11..b02fa92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ rmpv = "0.4.4" rust-embed = { version = "5.2.0", features = ["debug-embed"] } image = "0.22.3" nvim-rs = { git = "https://github.com/kethku/nvim-rs", features = [ "use_tokio" ] } -tokio = { version = "0.2.9", features = [ "blocking", "process", "time" ] } +tokio = { version = "0.2.9", features = [ "blocking", "process", "time", "tcp" ] } async-trait = "0.1.18" crossfire = "0.1" lazy_static = "1.4.0" @@ -43,6 +43,8 @@ which = "4" dirs = "2" rand = "0.7" skia-safe = "0.32.1" +pin-project = "0.4.27" +futures = "0.3.12" [dev-dependencies] mockall = "0.7.0" diff --git a/README.md b/README.md index dea74f1..ebdf076 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,28 @@ Font fallback supports rendering of emoji not contained in the configured font. Neovide supports displaying a full gui window from inside wsl via the `--wsl` command argument. Communication is passed via standard io into the wsl copy of neovim providing identical experience similar to visual studio code's remote editing https://code.visualstudio.com/docs/remote/remote-overview. +### Remote TCP Support + +Neovide supports connecting to a remote instance of Neovim over a TCP socket via the `--remote-tcp` command argument. This would allow you to run Neovim on a remote machine and use the GUI on your local machine, connecting over the network. + +Launch Neovim as a TCP server (on port 6666) by running: + +```sh +nvim --headless --listen localhost:6666 +``` + +And then connect to it using: + +```sh +/path/to/neovide --remote-tcp=localhost:6666 +``` + +By specifying to listen on localhost, you only allow connections from your local computer. If you are actually doing this over a network you will want to use SSH port forwarding for security, and then connect as before. + +```sh +ssh -L 6666:localhost:6666 ip.of.other.machine nvim --headless --listen localhost:6666 +``` + ### Some Nonsense ;) ``` diff --git a/src/bridge/create.rs b/src/bridge/create.rs new file mode 100644 index 0000000..ca2d2a5 --- /dev/null +++ b/src/bridge/create.rs @@ -0,0 +1,65 @@ +//! This module contains adaptations of the functions found in +//! https://github.com/KillTheMule/nvim-rs/blob/master/src/create/tokio.rs + +use std::{ + io::{self, Error, ErrorKind}, + process::Stdio, +}; + +use tokio::{ + io::split, + net::{TcpStream, ToSocketAddrs}, + process::Command, + spawn, + task::JoinHandle, +}; + +use nvim_rs::compat::tokio::TokioAsyncReadCompatExt; +use nvim_rs::{error::LoopError, neovim::Neovim, Handler}; + +use crate::bridge::{TxWrapper, WrapTx}; + +/// Connect to a neovim instance via tcp +pub async fn new_tcp( + addr: A, + handler: H, +) -> io::Result<(Neovim, JoinHandle>>)> +where + A: ToSocketAddrs, + H: Handler, +{ + let stream = TcpStream::connect(addr).await?; + let (reader, writer) = split(stream); + let (neovim, io) = Neovim::::new(reader.compat_read(), writer.wrap_tx(), handler); + let io_handle = spawn(io); + + Ok((neovim, io_handle)) +} + +/// Connect to a neovim instance by spawning a new one +/// +/// stdin/stdout will be rewritten to `Stdio::piped()` +pub async fn new_child_cmd( + cmd: &mut Command, + handler: H, +) -> io::Result<(Neovim, JoinHandle>>)> +where + H: Handler, +{ + let mut child = cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?; + let stdout = child + .stdout + .take() + .ok_or_else(|| Error::new(ErrorKind::Other, "Can't open stdout"))? + .compat_read(); + let stdin = child + .stdin + .take() + .ok_or_else(|| Error::new(ErrorKind::Other, "Can't open stdin"))? + .wrap_tx(); + + let (neovim, io) = Neovim::::new(stdout, stdin, handler); + let io_handle = spawn(io); + + Ok((neovim, io_handle)) +} diff --git a/src/bridge/handler.rs b/src/bridge/handler.rs index a513947..3a75544 100644 --- a/src/bridge/handler.rs +++ b/src/bridge/handler.rs @@ -3,14 +3,14 @@ use std::sync::Arc; use async_trait::async_trait; use crossfire::mpsc::TxUnbounded; use log::trace; -use nvim_rs::{compat::tokio::Compat, Handler, Neovim}; +use nvim_rs::{Handler, Neovim}; use parking_lot::Mutex; use rmpv::Value; -use tokio::process::ChildStdin; use tokio::task; use super::events::{parse_redraw_event, RedrawEvent}; use super::ui_commands::UiCommand; +use crate::bridge::TxWrapper; use crate::error_handling::ResultPanicExplanation; use crate::settings::SETTINGS; @@ -34,13 +34,13 @@ impl NeovimHandler { #[async_trait] impl Handler for NeovimHandler { - type Writer = Compat; + type Writer = TxWrapper; async fn handle_notify( &self, event_name: String, arguments: Vec, - _neovim: Neovim>, + _neovim: Neovim, ) { trace!("Neovim notification: {:?}", &event_name); diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs index daf1010..79a5cb6 100644 --- a/src/bridge/mod.rs +++ b/src/bridge/mod.rs @@ -1,5 +1,7 @@ +pub mod create; mod events; mod handler; +mod tx_wrapper; mod ui_commands; use std::env; @@ -10,7 +12,7 @@ use std::sync::Arc; use crossfire::mpsc::{RxUnbounded, TxUnbounded}; use log::{error, info, warn}; -use nvim_rs::{create::tokio as create, UiAttachOptions}; +use nvim_rs::UiAttachOptions; use rmpv::Value; use tokio::process::Command; use tokio::runtime::Runtime; @@ -20,6 +22,7 @@ use crate::settings::*; use crate::window::window_geometry_or_default; pub use events::*; use handler::NeovimHandler; +pub use tx_wrapper::{TxWrapper, WrapTx}; pub use ui_commands::UiCommand; #[cfg(windows)] @@ -129,6 +132,22 @@ pub fn create_nvim_command() -> Command { cmd } +enum ConnectionMode { + Child, + RemoteTcp(String), +} + +fn connection_mode() -> ConnectionMode { + let tcp_prefix = "--remote-tcp="; + + if let Some(arg) = std::env::args().find(|arg| arg.starts_with(tcp_prefix)) { + let input = &arg[tcp_prefix.len()..]; + ConnectionMode::RemoteTcp(input.to_owned()) + } else { + ConnectionMode::Child + } +} + async fn start_neovim_runtime( ui_command_sender: TxUnbounded, ui_command_receiver: RxUnbounded, @@ -137,9 +156,11 @@ async fn start_neovim_runtime( ) { let (width, height) = window_geometry_or_default(); let handler = NeovimHandler::new(ui_command_sender.clone(), redraw_event_sender.clone()); - let (mut nvim, io_handler, _) = create::new_child_cmd(&mut create_nvim_command(), handler) - .await - .unwrap_or_explained_panic("Could not locate or start neovim process"); + let (mut nvim, io_handler) = match connection_mode() { + ConnectionMode::Child => create::new_child_cmd(&mut create_nvim_command(), handler).await, + ConnectionMode::RemoteTcp(address) => create::new_tcp(address, handler).await, + } + .unwrap_or_explained_panic("Could not locate or start neovim process"); if nvim.get_api_info().await.is_err() { error!("Cannot get neovim api info, either neovide is launched with an unknown command line option or neovim version not supported!"); diff --git a/src/bridge/tx_wrapper.rs b/src/bridge/tx_wrapper.rs new file mode 100644 index 0000000..1ffdba6 --- /dev/null +++ b/src/bridge/tx_wrapper.rs @@ -0,0 +1,58 @@ +use pin_project::pin_project; +use std::io; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::{ + io::{AsyncWrite, WriteHalf}, + net::TcpStream, + process::ChildStdin, +}; + +#[pin_project(project = TxProj)] +pub enum TxWrapper { + Child(#[pin] ChildStdin), + Tcp(#[pin] WriteHalf), +} + +impl futures::io::AsyncWrite for TxWrapper { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.project() { + TxProj::Child(inner) => inner.poll_write(cx, buf), + TxProj::Tcp(inner) => inner.poll_write(cx, buf), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project() { + TxProj::Child(inner) => inner.poll_flush(cx), + TxProj::Tcp(inner) => inner.poll_flush(cx), + } + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project() { + TxProj::Child(inner) => inner.poll_shutdown(cx), + TxProj::Tcp(inner) => inner.poll_shutdown(cx), + } + } +} + +pub trait WrapTx { + fn wrap_tx(self) -> TxWrapper; +} + +impl WrapTx for ChildStdin { + fn wrap_tx(self) -> TxWrapper { + TxWrapper::Child(self) + } +} + +impl WrapTx for WriteHalf { + fn wrap_tx(self) -> TxWrapper { + TxWrapper::Tcp(self) + } +} diff --git a/src/bridge/ui_commands.rs b/src/bridge/ui_commands.rs index 5a8413d..c3fdc4a 100644 --- a/src/bridge/ui_commands.rs +++ b/src/bridge/ui_commands.rs @@ -3,9 +3,9 @@ use log::trace; #[cfg(windows)] use log::error; -use nvim_rs::compat::tokio::Compat; use nvim_rs::Neovim; -use tokio::process::ChildStdin; + +use crate::bridge::TxWrapper; #[cfg(windows)] use crate::windows_utils::{ @@ -43,7 +43,7 @@ pub enum UiCommand { } impl UiCommand { - pub async fn execute(self, nvim: &Neovim>) { + pub async fn execute(self, nvim: &Neovim) { match self { UiCommand::Resize { width, height } => nvim .ui_try_resize(width.max(10) as i64, height.max(3) as i64) diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 2f0a57f..e809afa 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -7,12 +7,11 @@ use flexi_logger::{Cleanup, Criterion, Duplicate, Logger, Naming}; mod from_value; pub use from_value::FromValue; use log::warn; -use nvim_rs::compat::tokio::Compat; use nvim_rs::Neovim; use parking_lot::RwLock; pub use rmpv::Value; -use tokio::process::ChildStdin; +use crate::bridge::TxWrapper; use crate::error_handling::ResultPanicExplanation; lazy_static! { @@ -51,6 +50,7 @@ impl Settings { false } else { !(arg.starts_with("--geometry=") + || arg.starts_with("--remote-tcp=") || arg == "--version" || arg == "-v" || arg == "--help" @@ -129,7 +129,7 @@ impl Settings { (*value).clone() } - pub async fn read_initial_values(&self, nvim: &Neovim>) { + pub async fn read_initial_values(&self, nvim: &Neovim) { let keys: Vec = self.listeners.read().keys().cloned().collect(); for name in keys { @@ -147,7 +147,7 @@ impl Settings { } } - pub async fn setup_changed_listeners(&self, nvim: &Neovim>) { + pub async fn setup_changed_listeners(&self, nvim: &Neovim) { let keys: Vec = self.listeners.read().keys().cloned().collect(); for name in keys { @@ -184,25 +184,24 @@ impl Settings { #[cfg(test)] mod tests { use async_trait::async_trait; - use nvim_rs::create::tokio as create; - use nvim_rs::{compat::tokio::Compat, Handler, Neovim}; + use nvim_rs::{Handler, Neovim}; use tokio; use super::*; - use crate::bridge::create_nvim_command; + use crate::bridge::{create, create_nvim_command}; #[derive(Clone)] pub struct NeovimHandler(); #[async_trait] impl Handler for NeovimHandler { - type Writer = Compat; + type Writer = TxWrapper; async fn handle_notify( &self, _event_name: String, _arguments: Vec, - _neovim: Neovim>, + _neovim: Neovim, ) { } } @@ -288,7 +287,7 @@ mod tests { let v4: String = format!("neovide_{}", v1); let v5: String = format!("neovide_{}", v2); - let (nvim, _, _) = create::new_child_cmd(&mut create_nvim_command(), NeovimHandler()) + let (nvim, _) = create::new_child_cmd(&mut create_nvim_command(), NeovimHandler()) .await .unwrap_or_explained_panic("Could not locate or start the neovim process"); nvim.set_var(&v4, Value::from(v2.clone())).await.ok();