mirror of
https://github.com/neovide/neovide.git
synced 2026-09-10 07:16:25 -04:00
435 lines
15 KiB
Rust
435 lines
15 KiB
Rust
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
// Test naming occasionally uses camelCase with underscores to separate sections of
|
|
// the test name.
|
|
#![cfg_attr(test, allow(non_snake_case))]
|
|
#![allow(unknown_lints)]
|
|
#[macro_use]
|
|
extern crate neovide_derive;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
#[cfg(test)]
|
|
#[macro_use]
|
|
extern crate approx;
|
|
|
|
#[macro_use]
|
|
extern crate clap;
|
|
|
|
mod bridge;
|
|
mod channel_utils;
|
|
mod clipboard;
|
|
mod cmd_line;
|
|
mod dimensions;
|
|
mod editor;
|
|
mod error_handling;
|
|
mod frame;
|
|
mod platform;
|
|
mod profiling;
|
|
mod renderer;
|
|
mod running_tracker;
|
|
mod settings;
|
|
mod units;
|
|
mod utils;
|
|
mod window;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
mod windows_utils;
|
|
|
|
#[macro_use]
|
|
extern crate derive_new;
|
|
|
|
use std::{
|
|
env::{self, args},
|
|
fs::{create_dir_all, File, OpenOptions},
|
|
io::Write,
|
|
panic::set_hook,
|
|
process::ExitCode,
|
|
sync::Arc,
|
|
time::{Duration, SystemTime},
|
|
};
|
|
|
|
use anyhow::Result;
|
|
use log::trace;
|
|
use std::env::var;
|
|
use std::panic::PanicHookInfo;
|
|
use std::path::PathBuf;
|
|
use time::macros::format_description;
|
|
use time::OffsetDateTime;
|
|
use winit::{error::EventLoopError, event_loop::EventLoopProxy};
|
|
|
|
#[cfg(not(test))]
|
|
use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming};
|
|
|
|
use backtrace::Backtrace;
|
|
use bridge::NeovimRuntime;
|
|
use cmd_line::CmdLineSettings;
|
|
use error_handling::handle_startup_errors;
|
|
use renderer::{
|
|
cursor_renderer::CursorSettings, progress_bar::ProgressBarSettings, RendererSettings,
|
|
};
|
|
use running_tracker::RunningTracker;
|
|
use window::{
|
|
create_event_loop, determine_window_size, UpdateLoop, UserEvent, WindowSettings, WindowSize,
|
|
};
|
|
|
|
pub use channel_utils::*;
|
|
#[cfg(target_os = "windows")]
|
|
pub use windows_utils::*;
|
|
|
|
use crate::settings::{load_last_window_settings, Config, PersistentWindowSettings, Settings};
|
|
|
|
pub use profiling::startup_profiler;
|
|
|
|
#[cfg(target_os = "macos")]
|
|
use crate::frame::Frame;
|
|
|
|
const DEFAULT_BACKTRACES_FILE: &str = "neovide_backtraces.log";
|
|
const BACKTRACES_FILE_ENV_VAR: &str = "NEOVIDE_BACKTRACES";
|
|
const REQUEST_MESSAGE: &str = "This is a bug and we would love for it to be reported to https://github.com/neovide/neovide/issues";
|
|
|
|
fn main() -> ExitCode {
|
|
set_hook(Box::new(|panic_info| {
|
|
let backtrace = Backtrace::new();
|
|
|
|
let stderr_msg = generate_stderr_log_message(panic_info, &backtrace);
|
|
eprintln!("{stderr_msg}");
|
|
|
|
log_panic_to_file(panic_info, &backtrace, &None);
|
|
}));
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
windows_fix_dpi();
|
|
}
|
|
|
|
// This variable is set by the AppImage runtime and causes problems for child processes
|
|
#[cfg(target_os = "linux")]
|
|
env::remove_var("ARGV0");
|
|
|
|
let event_loop = create_event_loop();
|
|
clipboard::init(&event_loop);
|
|
|
|
let colorscheme_stream = mundy::Preferences::stream(mundy::Interest::ColorScheme);
|
|
|
|
let running_tracker = RunningTracker::new();
|
|
let settings = Arc::new(Settings::new());
|
|
|
|
match setup(
|
|
event_loop.create_proxy(),
|
|
running_tracker.clone(),
|
|
settings.clone(),
|
|
colorscheme_stream,
|
|
) {
|
|
Err(err) => handle_startup_errors(err, event_loop, settings.clone()),
|
|
Ok((window_size, initial_config, runtime)) => {
|
|
let mut update_loop = UpdateLoop::new(
|
|
window_size,
|
|
initial_config,
|
|
event_loop.create_proxy(),
|
|
settings.clone(),
|
|
);
|
|
|
|
let result = event_loop.run_app(&mut update_loop);
|
|
|
|
// Wait a little bit more and force Nevoim to exit after that.
|
|
// This should not be required, but Neovim through libuv spawns childprocesses that inherits all the handles
|
|
// This means that the stdio and stderr handles are not properly closed, so the nvim-rs
|
|
// read will hang forever, waiting for more data to read.
|
|
// See https://github.com/neovide/neovide/issues/2182 (which includes links to libuv issues)
|
|
runtime.runtime.shutdown_timeout(Duration::from_millis(500));
|
|
|
|
match result {
|
|
Ok(_) => running_tracker.exit_code(),
|
|
Err(EventLoopError::ExitFailure(code)) => ExitCode::from(code as u8),
|
|
_ => ExitCode::FAILURE,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn setup(
|
|
proxy: EventLoopProxy<UserEvent>,
|
|
running_tracker: RunningTracker,
|
|
settings: Arc<Settings>,
|
|
colorscheme_stream: mundy::PreferencesStream,
|
|
) -> Result<(WindowSize, Config, NeovimRuntime)> {
|
|
// --------------
|
|
// | Architecture |
|
|
// --------------
|
|
//
|
|
// BRIDGE:
|
|
// The bridge is responsible for the connection to the neovim process itself. It is in charge
|
|
// of starting and communicating to and from the process. The bridge is async and has a
|
|
// couple of sub components:
|
|
//
|
|
// NEOVIM HANDLER:
|
|
// This component handles events from neovim sent specifically to the gui. This includes
|
|
// redraw events responsible for updating the gui state, and custom neovide specific
|
|
// events which are registered on startup and handle syncing of settings or features from
|
|
// the neovim process.
|
|
//
|
|
// UI COMMAND HANDLER:
|
|
// This component handles communication from other components to the neovim process. The
|
|
// commands are split into Serial and Parallel commands. Serial commands must be
|
|
// processed in order while parallel commands can be processed in any order and in
|
|
// parallel. `send_ui` is used to send those commands from the window code.
|
|
//
|
|
// EDITOR:
|
|
// The editor is responsible for processing and transforming redraw events into something
|
|
// more readily renderable. Ligature support and multi window management requires some
|
|
// significant preprocessing of the redraw events in order to capture what exactly should get
|
|
// drawn where. Further this step takes a bit of processing power to accomplish, so it is done
|
|
// on it's own thread. Ideally heavily computationally expensive tasks should be done in the
|
|
// editor.
|
|
//
|
|
// RENDERER:
|
|
// The renderer is responsible for drawing the editor's output to the screen. It uses skia
|
|
// for drawing and is responsible for maintaining the various draw surfaces which are stored
|
|
// to prevent unnecessary redraws.
|
|
//
|
|
// WINDOW:
|
|
// The window is responsible for rendering and gathering input events from the user. This
|
|
// inncludes taking the draw commands from the editor and turning them into pixels on the
|
|
// screen. The ui commands are then forwarded back to the BRIDGE to convert them into
|
|
// commands for neovim to handle properly.
|
|
//
|
|
// ------------------
|
|
// | Other Components |
|
|
// ------------------
|
|
//
|
|
// Neovide also includes some other systems which are globally available via lazy static
|
|
// instantiations or passed between components.
|
|
//
|
|
// Settings:
|
|
// The settings system is live updated from global variables in neovim with the prefix
|
|
// "neovide". They allow us to configure and manage the functionality of neovide from neovim
|
|
// init scripts and variables.
|
|
//
|
|
// RunningTracker:
|
|
// The running tracker responds to quit requests, allowing other systems to trigger a process
|
|
// exit.
|
|
//
|
|
// ------------------
|
|
// | Communication flow |
|
|
// ------------------
|
|
//
|
|
// The bridge reads from Neovim, and sends `RedrawEvent` to the editor. Some events are also
|
|
// sent directly to the window event loop using `WindowCommand`. Finally changed settings are
|
|
// parsed, which are sent as a window event through `SettingChanged`.
|
|
//
|
|
// The editor reads `RedrawEvent` and sends `DrawCommand` to the Window.
|
|
//
|
|
// The Window event loop sends UICommand to the bridge, which forwards them to Neovim. It also
|
|
// reads `DrawCommand`, `SettingChanged`, and `WindowCommand` from the other components.
|
|
|
|
settings.register::<WindowSettings>();
|
|
settings.register::<RendererSettings>();
|
|
settings.register::<CursorSettings>();
|
|
settings.register::<ProgressBarSettings>();
|
|
|
|
let config = Config::init();
|
|
Config::watch_config_file(config.clone(), proxy.clone());
|
|
|
|
set_hook(Box::new({
|
|
let path = config.backtraces_path.clone();
|
|
move |panic_info: &PanicHookInfo<'_>| {
|
|
let backtrace = Backtrace::new();
|
|
|
|
let stderr_msg = generate_stderr_log_message(panic_info, &backtrace);
|
|
eprintln!("{stderr_msg}");
|
|
|
|
log_panic_to_file(panic_info, &backtrace, &path);
|
|
}
|
|
}));
|
|
|
|
//Will exit if -h or -v
|
|
cmd_line::handle_command_line_arguments(args().collect(), settings.as_ref())?;
|
|
{
|
|
let cmdline_settings = settings.get::<CmdLineSettings>();
|
|
if let Some(status) = cmd_line::maybe_passthrough_to_neovim(&cmdline_settings)? {
|
|
std::process::exit(cmd_line::exit_status_code(status));
|
|
}
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
maybe_disown(&settings);
|
|
|
|
startup_profiler();
|
|
|
|
#[cfg(not(test))]
|
|
init_logger(&settings);
|
|
|
|
trace!("Neovide version: {}", crate_version!());
|
|
|
|
// Set BgColor by default when using a transparent frame, so the titlebar text gets correct
|
|
// color.
|
|
#[cfg(target_os = "macos")]
|
|
if settings.get::<CmdLineSettings>().frame == Frame::Transparent {
|
|
let mut window_settings = settings.get::<WindowSettings>();
|
|
window_settings.theme = window::ThemeSettings::BgColor;
|
|
settings.set(&window_settings);
|
|
}
|
|
|
|
let window_settings = load_last_window_settings().ok();
|
|
let window_size = determine_window_size(window_settings.as_ref(), &settings);
|
|
let grid_size = match window_size {
|
|
WindowSize::Grid(grid_size) => Some(grid_size),
|
|
// Clippy wrongly suggests to use unwrap or default here
|
|
#[allow(clippy::manual_unwrap_or_default)]
|
|
_ => match window_settings {
|
|
Some(PersistentWindowSettings::Maximized { grid_size, .. }) => grid_size,
|
|
Some(PersistentWindowSettings::Windowed { grid_size, .. }) => grid_size,
|
|
_ => None,
|
|
},
|
|
};
|
|
|
|
let mut runtime = NeovimRuntime::new()?;
|
|
runtime.launch(
|
|
proxy,
|
|
grid_size,
|
|
running_tracker,
|
|
settings,
|
|
colorscheme_stream,
|
|
)?;
|
|
Ok((window_size, config, runtime))
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
pub fn init_logger(settings: &Settings) {
|
|
let cmdline_settings = settings.get::<CmdLineSettings>();
|
|
|
|
let logger = if cmdline_settings.log_to_file {
|
|
Logger::try_with_env_or_str("neovide")
|
|
.expect("Could not init logger")
|
|
.log_to_file(FileSpec::default())
|
|
.rotate(
|
|
Criterion::Size(10_000_000),
|
|
Naming::Timestamps,
|
|
Cleanup::KeepLogFiles(1),
|
|
)
|
|
.duplicate_to_stderr(Duplicate::Error)
|
|
} else {
|
|
Logger::try_with_env_or_str("neovide = error").expect("Could not init logger")
|
|
};
|
|
|
|
logger.start().expect("Could not start logger");
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn maybe_disown(settings: &Settings) {
|
|
use std::process;
|
|
|
|
let cmdline_settings = settings.get::<CmdLineSettings>();
|
|
|
|
// Never fork unless a tty is attached
|
|
if !cmdline_settings.fork || !utils::is_tty() {
|
|
return;
|
|
}
|
|
|
|
if let Ok(current_exe) = env::current_exe() {
|
|
assert!(process::Command::new(current_exe)
|
|
.stdin(process::Stdio::null())
|
|
.stdout(process::Stdio::null())
|
|
.stderr(process::Stdio::null())
|
|
.args(env::args().skip(1))
|
|
.spawn()
|
|
.is_ok());
|
|
process::exit(0);
|
|
} else {
|
|
eprintln!("error in disowning process, cannot obtain the path for the current executable, continuing without disowning...");
|
|
}
|
|
}
|
|
|
|
fn generate_stderr_log_message(panic_info: &PanicHookInfo, backtrace: &Backtrace) -> String {
|
|
if cfg!(debug_assertions) {
|
|
let print_backtrace = match env::var("RUST_BACKTRACE") {
|
|
Ok(x) => x == "full" || x == "1",
|
|
Err(_) => false,
|
|
};
|
|
|
|
let backtrace_msg = match print_backtrace {
|
|
true => format!("{backtrace:?}"),
|
|
false => {
|
|
"note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace"
|
|
.to_owned()
|
|
}
|
|
};
|
|
|
|
let panic_msg = generate_panic_message(panic_info);
|
|
|
|
format!("{panic_msg}\n{REQUEST_MESSAGE}\n{backtrace_msg}")
|
|
} else {
|
|
let panic_msg = generate_panic_message(panic_info);
|
|
format!("{panic_msg}\n{REQUEST_MESSAGE}")
|
|
}
|
|
}
|
|
|
|
fn log_panic_to_file(panic_info: &PanicHookInfo, backtrace: &Backtrace, path: &Option<PathBuf>) {
|
|
let log_msg = generate_panic_log_message(panic_info, backtrace);
|
|
|
|
let file_path = match path {
|
|
Some(v) => v,
|
|
None => &match var(BACKTRACES_FILE_ENV_VAR) {
|
|
Ok(v) => PathBuf::from(v),
|
|
Err(_) => settings::neovide_std_datapath().join(DEFAULT_BACKTRACES_FILE),
|
|
},
|
|
};
|
|
|
|
if let Some(parent) = file_path.parent() {
|
|
create_dir_all(parent).ok();
|
|
}
|
|
|
|
let mut file = match OpenOptions::new()
|
|
.append(true)
|
|
.open(file_path)
|
|
.or_else(|_| File::create(file_path))
|
|
{
|
|
Ok(x) => x,
|
|
Err(e) => {
|
|
eprintln!("Could not create backtraces file. ({e})");
|
|
return;
|
|
}
|
|
};
|
|
|
|
match file.write_all(log_msg.as_bytes()) {
|
|
Ok(()) => eprintln!("\nBacktrace saved to {file_path:?}!"),
|
|
Err(e) => eprintln!("Failed writing panic to {file_path:?}: {e}"),
|
|
}
|
|
}
|
|
|
|
fn generate_panic_log_message(panic_info: &PanicHookInfo, backtrace: &Backtrace) -> String {
|
|
let system_time: OffsetDateTime = SystemTime::now().into();
|
|
|
|
let timestamp = system_time
|
|
.format(format_description!(
|
|
"[year]-[month]-[day] [hour]:[minute]:[second]"
|
|
))
|
|
.expect("Failed to parse current time");
|
|
|
|
let partial_panic_msg = generate_panic_message(panic_info);
|
|
let full_panic_msg = format!("{timestamp} - {partial_panic_msg}");
|
|
|
|
format!("{full_panic_msg}\n{backtrace:?}\n")
|
|
}
|
|
|
|
fn generate_panic_message(panic_info: &PanicHookInfo) -> String {
|
|
// As per the documentation for `.location()`(https://doc.rust-lang.org/std/panic/struct.PanicInfo.html#method.location)
|
|
// the call to location cannot currently return `None`, so we unwrap.
|
|
let location_info = panic_info.location().unwrap();
|
|
let file = location_info.file();
|
|
let line = location_info.line();
|
|
let column = location_info.column();
|
|
|
|
let raw_payload = panic_info.payload();
|
|
|
|
let payload = match raw_payload
|
|
.downcast_ref::<&str>()
|
|
.map(ToOwned::to_owned)
|
|
// Some panic messages are &str, some are String, try both to see which it is
|
|
.or_else(|| raw_payload.downcast_ref().map(String::as_str))
|
|
{
|
|
Some(msg) => msg.to_owned(),
|
|
None => return "Could not parse panic payload to a string. This is a bug.".to_owned(),
|
|
};
|
|
|
|
format!("Neovide panicked with the message '{payload}'. (File: {file}; Line: {line}, Column: {column})")
|
|
}
|