mod api_info;
mod clipboard;
mod command;
mod events;
mod handler;
mod restart;
pub mod session;
mod setup;
mod ui_commands;
use std::{
io::Error,
ops::Add,
path::Path,
sync::{Arc, Mutex},
time::Duration,
};
use crate::{
clipboard::ClipboardHandle,
cmd_line::CmdLineSettings,
editor::start_editor_handler,
running_tracker::RunningTracker,
settings::*,
units::GridSize,
window::{EventPayload, RouteId, UserEvent, WindowSettings},
};
use anyhow::{Context, Result, bail};
use futures::StreamExt;
pub use handler::NeovimHandler;
use itertools::Itertools;
use log::info;
use mundy::{Interest, Preferences};
use nvim_rs::{Neovim, UiAttachOptions, Value, error::CallError};
use rmpv::Utf8String;
use session::{NeovimInstance, NeovimSession};
use setup::{get_api_information, setup_neovide_specific_state};
use tokio::{
runtime::{Builder, Runtime},
select,
time::timeout,
};
use winit::event_loop::EventLoopProxy;
#[cfg(test)]
pub use command::create_tokio_nvim_command;
pub use command::{OpenArgs, OpenMode, create_blocking_nvim_command};
pub use events::*;
pub use restart::RestartDetails;
pub use session::NeovimWriter;
#[cfg(target_os = "macos")]
pub use ui_commands::send_or_queue_file_drop;
pub use ui_commands::{
ParallelCommand, SerialCommand, require_active_handler, send_ui, set_active_route_handler,
start_ui_command_handler, unregister_route_handler,
};
const NEOVIM_REQUIRED_VERSION: (u64, u64, u64) = (0, 10, 0);
macro_rules! nvim_dict {
( $( $key:expr => $value:expr ),* $(,)? ) => {
vec![
$( (Value::from($key), Value::from($value)) ),*
]
};
}
pub(crate) use nvim_dict;
async fn nvim_exec_output(
nvim: &Neovim<NeovimWriter>,
func: &str,
) -> Result<String, Box<CallError>> {
let result = nvim
.exec2(
func,
nvim_dict! {
"output" => true,
},
)
.await?;
Ok(result
.iter()
.find(|(k, _)| k.as_str() == Some("output"))
.and_then(|(_, v)| v.as_str())
.unwrap_or("")
.to_string())
}
pub struct NeovimRuntime {
pub runtime: Option<Runtime>,
clipboard: ClipboardHandle,
background_preference: Arc<Mutex<String>>,
}
async fn neovim_instance(
settings: &Settings,
restart: Option<&RestartDetails>,
cwd: Option<&Path>,
mode: OpenMode,
) -> Result<NeovimInstance> {
if let Some(info) = restart {
return Ok(NeovimInstance::Server { address: info.listen_addr.clone() });
}
if let Some(address) = settings.get::<CmdLineSettings>().server {
return Ok(NeovimInstance::Server { address });
}
let cmdline_settings = settings.get::<CmdLineSettings>();
Ok(NeovimInstance::Embedded(command::create_tokio_nvim_command(
&cmdline_settings,
true,
cwd,
mode,
)))
}
pub async fn show_error_message(
nvim: &Neovim<NeovimWriter>,
lines: &[String],
) -> Result<(), Box<CallError>> {
let error_msg_highlight: Utf8String = "ErrorMsg".into();
let mut prepared_lines = lines
.iter()
.map(|l| {
Value::Array(vec![
Value::String(l.clone().add("\n").into()),
Value::String(error_msg_highlight.clone()),
])
})
.collect_vec();
prepared_lines.insert(
0,
Value::Array(vec![
Value::String("Error: ".into()),
Value::String(error_msg_highlight.clone()),
]),
);
nvim.echo(prepared_lines, true, nvim_dict! {}).await
}
#[allow(clippy::too_many_arguments)]
async fn create_neovim_session(
route_id: RouteId,
handler: NeovimHandler,
grid_size: Option<GridSize<u32>>,
settings: Arc<Settings>,
background: &str,
restart_details: Option<&RestartDetails>,
cwd: Option<&Path>,
mode: OpenMode,
) -> Result<NeovimSession> {
let neovim_instance = neovim_instance(settings.as_ref(), restart_details, cwd, mode).await?;
#[allow(unused_mut)]
let mut session = NeovimSession::new(neovim_instance, handler.clone())
.await
.context("Could not locate or start neovim process")?;
let api_information = get_api_information(&session.neovim).await?;
info!("Neovide registered to nvim with channel id {}", api_information.channel);
let (major, minor, patch) = NEOVIM_REQUIRED_VERSION;
if !api_information.version.has_version(major, minor, patch, None) {
let found = api_information.version.string;
bail!(
"Neovide requires nvim version {major}.{minor}.{patch} or higher, but {found} was detected. Download the latest version here https://github.com/neovim/neovim/wiki/Installing-Neovim"
);
}
let cmdline_settings = settings.get::<CmdLineSettings>();
let remote = cmdline_settings.wsl || cmdline_settings.server.is_some();
setup_neovide_specific_state(&session.neovim, remote, &api_information, &settings).await?;
if api_information.version.has_version(0, 12, 0, Some(1264)) {
let mut window_settings = settings.get::<WindowSettings>();
window_settings.has_mouse_grid_detection = true;
settings.set::<WindowSettings>(&window_settings);
}
let can_support_ime_api = api_information.version.has_version(0, 12, 0, Some(1724));
start_ui_command_handler(
route_id,
handler.clone(),
session.neovim.clone(),
settings.clone(),
can_support_ime_api,
);
settings.read_initial_values(&session.neovim).await?;
set_background_if_allowed(background, &session.neovim).await;
let mut options = UiAttachOptions::new();
options.set_linegrid_external(true);
options.set_multigrid_external(!cmdline_settings.no_multi_grid);
options.set_rgb(true);
#[cfg(target_os = "macos")]
options.set_hlstate_external(true);
#[cfg(not(target_os = "windows"))]
if let Some(fd) = session.stdin_fd.take() {
use rustix::fd::AsRawFd;
if let Ok(fd) = fd.as_raw_fd().try_into() {
options.set_stdin_fd(fd);
}
}
let grid_size = grid_size.map_or(DEFAULT_GRID_SIZE, |v| clamped_grid_size(&v));
session
.neovim
.ui_attach(grid_size.width as i64, grid_size.height as i64, &options)
.await
.context("Could not attach ui to neovim process")?;
#[cfg(target_os = "macos")]
ui_commands::mark_file_drop_handler_ready(&handler);
info!("Neovim process attached");
Ok(session)
}
async fn run(route_id: RouteId, session: NeovimSession, proxy: EventLoopProxy<EventPayload>) {
let mut session = session;
if let Some(process) = session.neovim_process.as_mut() {
select! {
_ = &mut session.io_handle => {}
_ = process.wait() => {
log::info!("The Neovim process quit before the IO stream, waiting for a half second");
if timeout(Duration::from_millis(500), &mut session.io_handle)
.await
.is_err()
{
log::info!("The IO stream was never closed, forcing Neovide to exit");
}
}
};
} else {
session.io_handle.await.ok();
}
if let Some(stderr_task) = &mut session.stderr_task {
timeout(Duration::from_millis(500), stderr_task).await.ok();
};
proxy.send_event(EventPayload::for_route(UserEvent::NeovimExited, route_id)).ok();
}
pub async fn set_background_if_allowed(background: &str, neovim: &Neovim<NeovimWriter>) {
if let Ok(can_set) = neovim
.exec_lua("return neovide.private.can_set_background()", vec![background.into()])
.await
{
if can_set.as_bool().unwrap() {
let _ = neovim.set_option("background", background.into()).await;
}
}
}
fn background_from_preferences(preferences: &mundy::Preferences) -> Option<&'static str> {
match preferences.color_scheme {
mundy::ColorScheme::Dark => Some("dark"),
mundy::ColorScheme::Light => Some("light"),
mundy::ColorScheme::NoPreference => None,
}
}
async fn initial_background_from_stream(stream: &mut mundy::PreferencesStream) -> String {
match timeout(Duration::from_millis(200), stream.next()).await {
Ok(Some(preferences)) => {
background_from_preferences(&preferences).unwrap_or("dark").to_string()
}
Ok(None) => "dark".to_string(),
Err(_) => "dark".to_string(),
}
}
async fn update_colorscheme(
mut stream: mundy::PreferencesStream,
background_preference: Arc<Mutex<String>>,
handler: NeovimHandler,
) {
while let Some(preferences) = stream.next().await {
if let Some(background) = background_from_preferences(&preferences) {
{
if let Ok(mut guard) = background_preference.lock() {
guard.clear();
guard.push_str(background);
}
}
send_ui(
ParallelCommand::SetBackground { background: background.to_string() },
&handler,
);
}
}
}
impl NeovimRuntime {
pub fn new(clipboard: ClipboardHandle) -> Result<Self, Error> {
let runtime = Builder::new_multi_thread().enable_all().build()?;
Ok(Self {
runtime: Some(runtime),
clipboard,
background_preference: Arc::new(Mutex::new("dark".to_string())),
})
}
#[allow(clippy::too_many_arguments)]
pub fn launch(
&mut self,
route_id: RouteId,
event_loop_proxy: EventLoopProxy<EventPayload>,
grid_size: Option<GridSize<u32>>,
running_tracker: RunningTracker,
settings: Arc<Settings>,
config: &Config,
cwd: Option<&Path>,
mode: OpenMode,
) -> Result<NeovimHandler> {
let mut colorscheme_stream = self.colorscheme_stream();
let editor_handler = start_editor_handler(
route_id,
event_loop_proxy.clone(),
running_tracker,
settings.clone(),
self.clipboard.clone(),
);
let initial_background =
self.runtime().block_on(initial_background_from_stream(&mut colorscheme_stream));
self.set_background_preference(&initial_background);
let mut font_config_state = settings.get::<FontConfigState>();
font_config_state.has_font = config.font.is_some();
settings.set(&font_config_state);
let session = match self.runtime().block_on(create_neovim_session(
route_id,
editor_handler.clone(),
grid_size,
settings,
&initial_background,
None,
cwd,
mode,
)) {
Ok(session) => session,
Err(err) => {
self.runtime().block_on(async move {
drop(colorscheme_stream);
});
return Err(err);
}
};
self.runtime().spawn(update_colorscheme(
colorscheme_stream,
self.background_preference.clone(),
editor_handler.clone(),
));
self.runtime().spawn(run(route_id, session, event_loop_proxy));
Ok(editor_handler)
}
pub fn shutdown(mut self, timeout: Duration) {
self.shutdown_timeout(timeout);
}
#[allow(clippy::too_many_arguments)]
pub fn restart(
&mut self,
route_id: RouteId,
event_loop_proxy: EventLoopProxy<EventPayload>,
handler: NeovimHandler,
grid_size: GridSize<u32>,
settings: Arc<Settings>,
restart_details: RestartDetails,
cwd: Option<&Path>,
) -> Result<()> {
let background = self.current_background();
let session = self.runtime().block_on(create_neovim_session(
route_id,
handler,
Some(grid_size),
settings,
&background,
Some(&restart_details),
cwd,
OpenMode::None,
))?;
self.runtime().spawn(run(route_id, session, event_loop_proxy));
Ok(())
}
fn runtime(&self) -> &Runtime {
self.runtime.as_ref().expect("runtime must be available while NeovimRuntime is alive")
}
pub fn shutdown_timeout(&mut self, timeout: Duration) {
if let Some(runtime) = self.runtime.take() {
runtime.shutdown_timeout(timeout);
}
}
pub fn colorscheme_stream(&self) -> mundy::PreferencesStream {
let _guard = self.runtime().enter();
Preferences::stream(Interest::ColorScheme)
}
fn set_background_preference(&self, background: &str) {
if let Ok(mut guard) = self.background_preference.lock() {
guard.clear();
guard.push_str(background);
}
}
fn current_background(&self) -> String {
self.background_preference
.lock()
.map(|guard| guard.clone())
.unwrap_or_else(|_| "dark".to_string())
}
}
impl Drop for NeovimRuntime {
fn drop(&mut self) {
self.shutdown_timeout(Duration::from_millis(500));
}
}