mod args;
mod input_bridge;
mod tty_frontend;
use std::collections::HashMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use neomacs_display_runtime::render_thread::{
RenderEventLoopProxy, RenderUserEvent, SharedImageDimensions, SharedMonitorInfo,
build_render_event_loop, run_render_loop_current_thread,
};
use neomacs_display_runtime::thread_comm::{
EmacsComms, InputEvent as DisplayInputEvent, RenderCommand, ThreadComms,
};
use neomacs_layout_engine::font_metrics::FontMetricsService;
use neomacs_layout_engine::fontconfig::face_height_to_pixels;
use neomacs_layout_engine::gui_chrome::{collect_gui_menu_bar_items, collect_gui_tool_bar_items};
use neovm_core::buffer::BufferId;
use neovm_core::emacs_core::Value;
use neovm_core::emacs_core::builtins::set_neomacs_monitor_info;
use neovm_core::emacs_core::display::gui_window_system_symbol;
use neovm_core::emacs_core::eval::{
FontResolveRequest, FontSpecResolveRequest, GuiFrameHostSize, ImageResolveRequest,
ImageResolveSource, ResolvedFontMatch, ResolvedFontSpecMatch, ResolvedFrameFont, ResolvedImage,
};
use neovm_core::emacs_core::load::LoadupDumpMode;
use neovm_core::emacs_core::load::LoadupStartupSurface;
use neovm_core::emacs_core::load::RuntimeImageRole;
#[cfg(test)]
use neovm_core::emacs_core::print_value_with_eval;
use neovm_core::emacs_core::terminal::pure::{
TerminalHost, TerminalRuntimeConfig, configure_terminal_runtime, reset_terminal_host,
reset_terminal_runtime, set_terminal_host,
};
use neovm_core::emacs_core::{Context, DisplayHost, GuiFrameHostRequest};
use neovm_core::face::{FaceHeight, FontSlant, FontWeight, FontWidth};
use neovm_core::heap_types::LispString;
use neovm_core::window::{FrameId, Window};
#[derive(Debug, Clone, PartialEq, Eq)]
enum EarlyCliAction {
PrintHelp { program: String },
PrintVersion,
PrintFingerprint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FrontendKind {
Gui,
Tty,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeMode {
Raw,
BootstrapUse,
FinalRun,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DumpImageKind {
Bootstrap,
Final,
}
impl RuntimeMode {
pub const fn binary_name(self) -> &'static str {
match self {
Self::Raw => "neomacs-temacs",
Self::BootstrapUse => "bootstrap-neomacs",
Self::FinalRun => "neomacs",
}
}
pub const fn dump_image_kind(self) -> Option<DumpImageKind> {
match self {
Self::Raw => None,
Self::BootstrapUse => Some(DumpImageKind::Bootstrap),
Self::FinalRun => Some(DumpImageKind::Final),
}
}
}
fn runtime_mode_from_program_name(program: &str) -> RuntimeMode {
let file_name = Path::new(program)
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new(program))
.to_string_lossy();
let file_name = file_name.strip_suffix(".exe").unwrap_or(&file_name);
match file_name {
"neomacs-temacs" => RuntimeMode::Raw,
"bootstrap-neomacs" => RuntimeMode::BootstrapUse,
_ => RuntimeMode::FinalRun,
}
}
fn runtime_mode_from_argv<I, S>(args: I) -> RuntimeMode
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
args.into_iter()
.next()
.map(|arg| runtime_mode_from_program_name(arg.as_ref()))
.unwrap_or(RuntimeMode::FinalRun)
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct StartupOptions {
frontend: FrontendKind,
forwarded_args: Vec<String>,
terminal_device: Option<String>,
noninteractive: bool,
temacs_mode: Option<LoadupDumpMode>,
dump_file_override: Option<PathBuf>,
no_site_lisp: bool,
no_loadup: bool,
no_build_details: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct BootstrapDisplayConfig {
frontend: FrontendKind,
color_cells: i64,
background_mode: &'static str,
}
const EARLY_HELP_BODY: &str = concat!(
"Run Neomacs, the extensible, customizable, self-documenting real-time\n",
"display editor. The recommended way to start Neomacs for normal editing\n",
"is with no options at all.\n",
"\n",
"Run M-x info RET m emacs RET m emacs invocation RET inside Emacs to\n",
"read the main documentation for these command-line arguments.\n",
"\n",
"Initialization options:\n",
"\n",
"--batch do not do interactive display; implies -q\n",
"--chdir DIR change to directory DIR\n",
"--daemon, --bg-daemon[=NAME] start a (named) server in the background\n",
"--fg-daemon[=NAME] start a (named) server in the foreground\n",
"--debug-init enable Emacs Lisp debugger for init file\n",
"--display, -d DISPLAY use X server DISPLAY\n",
"--no-build-details do not add build details such as time stamps\n",
"--no-desktop do not load a saved desktop\n",
"--no-init-file, -q load neither ~/.emacs nor default.el\n",
"--no-loadup, -nl do not load loadup.el into bare Emacs\n",
"--no-site-file do not load site-start.el\n",
"--no-x-resources do not load X resources\n",
"--no-site-lisp, -nsl do not add site-lisp directories to load-path\n",
"--no-splash do not display a splash screen on startup\n",
"--no-window-system, -nw do not communicate with X, ignoring $DISPLAY\n",
"--init-directory=DIR use DIR when looking for the Emacs init files.\n",
"--quick, -Q equivalent to:\n",
" -q --no-site-file --no-site-lisp --no-splash\n",
" --no-x-resources\n",
"--script FILE run FILE as an Emacs Lisp script\n",
"-x to be used in #!/usr/bin/emacs -x\n",
" and has approximately the same meaning\n",
" as -Q --script\n",
"--terminal, -t DEVICE use DEVICE for terminal I/O\n",
"--user, -u USER load ~USER/.emacs instead of your own\n",
"\n",
"Action options:\n",
"\n",
"FILE visit FILE\n",
"+LINE go to line LINE in next FILE\n",
"+LINE:COLUMN go to line LINE, column COLUMN, in next FILE\n",
"--directory, -L DIR prepend DIR to load-path (with :DIR, append DIR)\n",
"--eval EXPR evaluate Emacs Lisp expression EXPR\n",
"--execute EXPR evaluate Emacs Lisp expression EXPR\n",
"--file FILE visit FILE\n",
"--find-file FILE visit FILE\n",
"--funcall, -f FUNC call Emacs Lisp function FUNC with no arguments\n",
"--insert FILE insert contents of FILE into current buffer\n",
"--kill exit without asking for confirmation\n",
"--load, -l FILE load Emacs Lisp FILE using the load function\n",
"--visit FILE visit FILE\n",
"\n",
"Display options:\n",
"\n",
"--background-color, -bg COLOR window background color\n",
"--basic-display, -D disable many display features;\n",
" used for debugging Emacs\n",
"--border-color, -bd COLOR main border color\n",
"--border-width, -bw WIDTH width of main border\n",
"--cursor-color, -cr COLOR color of the Emacs cursor indicating point\n",
"--font, -fn FONT default font; must be fixed-width\n",
"--foreground-color, -fg COLOR window foreground color\n",
"--fullheight, -fh make the first frame high as the screen\n",
"--fullscreen, -fs make the first frame fullscreen\n",
"--fullwidth, -fw make the first frame wide as the screen\n",
"--maximized, -mm make the first frame maximized\n",
"--geometry, -g GEOMETRY window geometry\n",
"--iconic start Neomacs in iconified state\n",
"--internal-border, -ib WIDTH width between text and main border\n",
"--line-spacing, -lsp PIXELS additional space to put between lines\n",
"--mouse-color, -ms COLOR mouse cursor color in Neomacs window\n",
"--name NAME title for initial Neomacs frame\n",
"--no-blinking-cursor, -nbc disable blinking cursor\n",
"--reverse-video, -r, -rv switch foreground and background\n",
"--title, -T TITLE title for initial Neomacs frame\n",
"--vertical-scroll-bars, -vb enable vertical scroll bars\n",
"--xrm XRESOURCES set additional X resources\n",
"--parent-id XID set parent window\n",
"--help display this help and exit\n",
"--fingerprint output fingerprint and exit\n",
"--version output version information and exit\n",
"\n",
"You can generally also specify long option names with a single -; for\n",
"example, -batch as well as --batch. You can use any unambiguous\n",
"abbreviation for a --option.\n",
"\n",
"Various environment variables and window system resources also affect\n",
"the operation of Neomacs. See the main documentation.\n",
"\n",
"Report bugs to https://github.com/eval-exec/neomacs-windows/issues.\n",
);
const BOOTSTRAP_CORE_FEATURES: &[&str] = &["neomacs"];
fn classify_early_cli_action(args: impl IntoIterator<Item = String>) -> Option<EarlyCliAction> {
let mut args = args.into_iter();
let program = args.next().unwrap_or_else(|| "neomacs".to_string());
for arg in args {
if arg == "--" {
break;
}
match arg.as_str() {
"--help" | "-help" => {
return Some(EarlyCliAction::PrintHelp { program });
}
"--version" | "-version" => {
return Some(EarlyCliAction::PrintVersion);
}
"--fingerprint" | "-fingerprint" => {
return Some(EarlyCliAction::PrintFingerprint);
}
_ => {}
}
}
None
}
fn render_help_text(program: &str) -> String {
let mut out = String::new();
let _ = write!(&mut out, "Usage: {program} [OPTION-OR-FILENAME]...\n\n");
out.push_str(EARLY_HELP_BODY);
out
}
fn render_version_text() -> String {
format!(
"Neomacs {}\nStandalone Rust binary for Neomacs (no C dependency)\n",
neomacs_display_runtime::VERSION
)
}
fn render_fingerprint_text() -> String {
format!("{}\n", neovm_core::emacs_core::pdump::fingerprint_hex())
}
fn render_startup_image_error(err: &neovm_core::emacs_core::error::EvalError) -> String {
match err {
neovm_core::emacs_core::error::EvalError::Signal {
raw_data: Some(payload),
..
} => payload
.as_symbol_name()
.map(str::to_owned)
.or_else(|| payload.as_utf8_str().map(str::to_owned))
.unwrap_or_else(|| format!("{err:?}")),
_ => format!("{err:?}"),
}
}
fn parse_startup_options(args: impl IntoIterator<Item = String>) -> Result<StartupOptions, String> {
use args::{ArgMatch, argmatch, sort_args};
let mut parsed: Vec<String> = args.into_iter().collect();
sort_args(&mut parsed)?;
let program = parsed
.first()
.cloned()
.unwrap_or_else(|| "neomacs".to_string());
let mut forwarded_args = vec![program];
let mut frontend = FrontendKind::Gui;
let mut terminal_device = None;
let mut noninteractive = false;
let mut temacs_mode = None;
let mut dump_file_override = None;
let mut no_site_lisp = false;
let mut no_loadup = false;
let mut no_build_details = false;
let mut idx = 0usize;
while idx + 1 < parsed.len() {
let next = parsed[idx + 1].as_str();
if next == "--" {
forwarded_args.extend(parsed[idx + 1..].iter().cloned());
break;
}
match argmatch(&parsed, &mut idx, "-chdir", Some("--chdir"), 4, true) {
ArgMatch::Value(dir) => {
if let Err(e) = std::env::set_current_dir(&dir) {
return Err(format!("neomacs: Can't chdir to {dir}: {e}"));
}
continue;
}
ArgMatch::MissingValue => {
return Err("neomacs: option `-chdir' requires an argument".to_string());
}
ArgMatch::NoMatch => {}
ArgMatch::Bare => unreachable!(),
}
match argmatch(
&parsed,
&mut idx,
"-nw",
Some("--no-window-system"),
6,
false,
) {
ArgMatch::Bare => {
frontend = FrontendKind::Tty;
continue;
}
ArgMatch::NoMatch => {}
ArgMatch::Value(_) | ArgMatch::MissingValue => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-nw", Some("--no-windows"), 6, false) {
ArgMatch::Bare => {
frontend = FrontendKind::Tty;
continue;
}
ArgMatch::NoMatch => {}
ArgMatch::Value(_) | ArgMatch::MissingValue => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-batch", Some("--batch"), 5, false) {
ArgMatch::Bare => {
noninteractive = true;
frontend = FrontendKind::Tty;
continue;
}
ArgMatch::NoMatch => {}
ArgMatch::Value(_) | ArgMatch::MissingValue => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-script", Some("--script"), 3, true) {
ArgMatch::Value(script_file) => {
noninteractive = true;
frontend = FrontendKind::Tty;
forwarded_args.push("-scriptload".to_string());
forwarded_args.push(script_file);
continue;
}
ArgMatch::MissingValue => {
return Err("neomacs: option `-script' requires an argument".to_string());
}
ArgMatch::NoMatch => {}
ArgMatch::Bare => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-x", None, 1, false) {
ArgMatch::Bare => {
noninteractive = true;
frontend = FrontendKind::Tty;
no_site_lisp = true;
forwarded_args.push("-scripteval".to_string());
continue;
}
ArgMatch::NoMatch => {}
ArgMatch::Value(_) | ArgMatch::MissingValue => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-nl", Some("--no-loadup"), 6, false) {
ArgMatch::Bare => {
no_loadup = true;
continue;
}
ArgMatch::NoMatch => {}
ArgMatch::Value(_) | ArgMatch::MissingValue => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-nsl", Some("--no-site-lisp"), 11, false) {
ArgMatch::Bare => {
no_site_lisp = true;
continue;
}
ArgMatch::NoMatch => {}
ArgMatch::Value(_) | ArgMatch::MissingValue => unreachable!(),
}
match argmatch(
&parsed,
&mut idx,
"-no-build-details",
Some("--no-build-details"),
7,
false,
) {
ArgMatch::Bare => {
no_build_details = true;
continue;
}
ArgMatch::NoMatch => {}
ArgMatch::Value(_) | ArgMatch::MissingValue => unreachable!(),
}
let pre_idx = idx;
match argmatch(&parsed, &mut idx, "-temacs", Some("--temacs"), 8, true) {
ArgMatch::Value(value) => {
temacs_mode = Some(parse_temacs_mode(&value)?);
for slot in &parsed[pre_idx + 1..=idx] {
forwarded_args.push(slot.clone());
}
continue;
}
ArgMatch::MissingValue => {
return Err("neomacs: option `-temacs' requires an argument".to_string());
}
ArgMatch::NoMatch => {}
ArgMatch::Bare => unreachable!(),
}
let pre_idx = idx;
match argmatch(
&parsed,
&mut idx,
"-dump-file",
Some("--dump-file"),
6,
true,
) {
ArgMatch::Value(value) => {
dump_file_override = Some(PathBuf::from(&value));
for slot in &parsed[pre_idx + 1..=idx] {
forwarded_args.push(slot.clone());
}
continue;
}
ArgMatch::MissingValue => {
return Err("neomacs: option `-dump-file' requires an argument".to_string());
}
ArgMatch::NoMatch => {}
ArgMatch::Bare => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-t", Some("--terminal"), 4, true) {
ArgMatch::Value(device) => {
frontend = FrontendKind::Tty;
terminal_device = Some(device);
continue;
}
ArgMatch::MissingValue => {
return Err("neomacs: option `-t' requires an argument".to_string());
}
ArgMatch::NoMatch => {}
ArgMatch::Bare => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-d", Some("--display"), 3, true) {
ArgMatch::Value(value) => {
forwarded_args.push("-d".to_string());
forwarded_args.push(value);
continue;
}
ArgMatch::MissingValue => {
return Err("neomacs: option `-d' requires an argument".to_string());
}
ArgMatch::NoMatch => {}
ArgMatch::Bare => unreachable!(),
}
match argmatch(&parsed, &mut idx, "-display", None, 0, true) {
ArgMatch::Value(value) => {
forwarded_args.push("-display".to_string());
forwarded_args.push(value);
continue;
}
ArgMatch::MissingValue => {
return Err("neomacs: option `-display' requires an argument".to_string());
}
ArgMatch::NoMatch => {}
ArgMatch::Bare => unreachable!(),
}
forwarded_args.push(parsed[idx + 1].clone());
idx += 1;
}
if !no_site_lisp
&& forwarded_args
.iter()
.skip(1)
.any(|a| a == "-Q" || a == "--quick" || a == "-quick")
{
no_site_lisp = true;
}
Ok(StartupOptions {
frontend,
forwarded_args,
terminal_device,
noninteractive,
temacs_mode,
dump_file_override,
no_site_lisp,
no_loadup,
no_build_details,
})
}
fn parse_temacs_mode(value: &str) -> Result<LoadupDumpMode, String> {
match value {
"pbootstrap" => Ok(LoadupDumpMode::Pbootstrap),
"pdump" => Ok(LoadupDumpMode::Pdump),
other => Err(format!("neomacs: invalid --temacs mode `{other}`")),
}
}
fn bootstrap_display_config(frontend: FrontendKind) -> BootstrapDisplayConfig {
match frontend {
FrontendKind::Gui => BootstrapDisplayConfig {
frontend,
color_cells: 16777216,
background_mode: "light",
},
FrontendKind::Tty => BootstrapDisplayConfig {
frontend,
color_cells: detect_tty_color_cells(),
background_mode: detect_tty_background_mode(),
},
}
}
impl BootstrapDisplayConfig {
fn window_system_symbol(self) -> Option<&'static str> {
match self.frontend {
FrontendKind::Gui => Some(gui_window_system_symbol()),
FrontendKind::Tty => None,
}
}
fn display_type_symbol(self) -> &'static str {
if self.color_cells > 0 {
"color"
} else {
"mono"
}
}
}
fn detect_tty_type() -> Option<String> {
std::env::var("TERM").ok().filter(|value| !value.is_empty())
}
fn default_controlling_tty_name() -> &'static str {
#[cfg(windows)]
{
"CONOUT$"
}
#[cfg(not(windows))]
{
"/dev/tty"
}
}
fn detect_tty_name(_startup: &StartupOptions) -> String {
default_controlling_tty_name().to_string()
}
fn detect_tty_runtime(startup: &StartupOptions) -> TerminalRuntimeConfig {
TerminalRuntimeConfig::interactive(detect_tty_type(), detect_tty_color_cells())
.with_name(detect_tty_name(startup))
}
fn detect_tty_color_cells() -> i64 {
let colorterm = std::env::var("COLORTERM")
.unwrap_or_default()
.to_ascii_lowercase();
if colorterm.contains("truecolor") || colorterm.contains("24bit") {
return 16777216;
}
let term = std::env::var("TERM")
.unwrap_or_default()
.to_ascii_lowercase();
if term.is_empty() || term == "dumb" {
return 0;
}
if term.contains("256color") {
return 256;
}
8
}
fn detect_tty_background_mode() -> &'static str {
let Some(colorfgbg) = std::env::var("COLORFGBG").ok() else {
return "dark";
};
let Some(background) = colorfgbg
.split(';')
.next_back()
.and_then(|value| value.parse::<i32>().ok())
else {
return "dark";
};
if (7..=15).contains(&background) {
"light"
} else {
"dark"
}
}
fn startup_dimensions(frontend: FrontendKind, frame_metrics: BootstrapFrameMetrics) -> (u32, u32) {
match frontend {
FrontendKind::Gui => {
let cols = 80u32;
let lines = 36u32;
let width = (cols as f32 * frame_metrics.char_width).round() as u32;
let height = (lines as f32 * frame_metrics.char_height).round() as u32;
(width.max(200), height.max(100))
}
FrontendKind::Tty => {
let (cols, rows) = query_terminal_size_cells().unwrap_or((80, 25));
(cols as u32, rows as u32)
}
}
}
#[cfg(unix)]
fn query_terminal_size_cells() -> Option<(u16, u16)> {
use std::mem::MaybeUninit;
unsafe {
let mut winsize = MaybeUninit::<libc::winsize>::uninit();
if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, winsize.as_mut_ptr()) == 0 {
let winsize = winsize.assume_init();
if winsize.ws_col > 0 && winsize.ws_row > 0 {
return Some((winsize.ws_col, winsize.ws_row));
}
}
}
None
}
#[cfg(not(unix))]
fn query_terminal_size_cells() -> Option<(u16, u16)> {
None
}
enum FrontendHandle {
TtyRifInput(tty_frontend::TtyInputReader),
Batch,
}
impl FrontendHandle {
fn join(self) {
match self {
Self::TtyRifInput(handle) => handle.join(),
Self::Batch => {}
}
}
}
#[derive(Clone)]
struct GuiEventLoopWaker {
proxy: RenderEventLoopProxy,
}
impl GuiEventLoopWaker {
fn new(proxy: RenderEventLoopProxy) -> Self {
Self { proxy }
}
fn wake(&self) {
if let Err(err) = self.proxy.send_event(RenderUserEvent::Wake) {
tracing::debug!("GUI event loop wake dropped after loop closed: {err}");
}
}
}
#[derive(Debug, Clone, Copy)]
struct EvaluatorExit {
exit_code: i32,
restart: bool,
}
impl EvaluatorExit {
const OK: Self = Self {
exit_code: 0,
restart: false,
};
}
const GUI_EVALUATOR_THREAD_STACK_SIZE: usize = 64 * 1024 * 1024;
struct PrimaryWindowDisplayHost {
cmd_tx: crossbeam_channel::Sender<RenderCommand>,
render_waker: Option<GuiEventLoopWaker>,
primary_window_adopted: bool,
primary_frame_id: Option<neovm_core::window::FrameId>,
last_window_titles: Mutex<HashMap<neovm_core::window::FrameId, LispString>>,
font_metrics: Option<FontMetricsService>,
primary_window_size: SharedPrimaryWindowSize,
image_dimensions: SharedImageDimensions,
resolved_images: Mutex<HashMap<ImageResolveRequest, ResolvedImage>>,
}
struct TtyTerminalHost {
cmd_tx: crossbeam_channel::Sender<RenderCommand>,
}
impl TerminalHost for TtyTerminalHost {
fn suspend_tty(&mut self) -> Result<(), String> {
self.cmd_tx
.send(RenderCommand::SuspendTty)
.map_err(|err| format!("failed to suspend tty frontend: {err}"))
}
fn resume_tty(&mut self) -> Result<(), String> {
self.cmd_tx
.send(RenderCommand::ResumeTty)
.map_err(|err| format!("failed to resume tty frontend: {err}"))
}
fn delete_terminal(&mut self) -> Result<(), String> {
self.cmd_tx
.send(RenderCommand::Shutdown)
.map_err(|err| format!("failed to delete tty terminal frontend: {err}"))
}
}
fn should_enable_live_tty_io(startup: &StartupOptions) -> bool {
startup.frontend == FrontendKind::Tty && !startup.noninteractive
}
fn maybe_install_tty_redisplay_callback(evaluator: &mut Context, startup: &StartupOptions) {
if !should_enable_live_tty_io(startup) {
return;
}
provide_lisp_feature(evaluator, "tty-child-frames");
let (cols, rows) = query_terminal_size_cells().unwrap_or((80, 25));
let mut tty_rif = neomacs_display_protocol::tty_rif::TtyRif::new(cols as usize, rows as usize);
LAYOUT_ENGINE.with(|engine| {
engine.borrow_mut().disable_cosmic_metrics();
});
evaluator.redisplay_fn = Some(Box::new(move |eval: &mut Context| {
eval.setup_thread_locals();
if let Some((cols, rows)) = query_terminal_size_cells() {
let cols = usize::from(cols);
let rows = usize::from(rows);
if tty_rif.width() != cols || tty_rif.height() != rows {
tty_rif.resize(cols, rows);
}
}
if let Some((root, children)) = run_tty_layout_tree(eval) {
run_tty_rif_redisplay(&mut tty_rif, &root, &children);
}
}));
}
fn provide_lisp_feature(evaluator: &mut Context, feature: &str) {
let features = evaluator
.obarray()
.symbol_value("features")
.copied()
.unwrap_or(Value::NIL);
let feature_value = Value::symbol(feature);
let already_present = neovm_core::emacs_core::value::list_to_vec(&features)
.is_some_and(|items| items.into_iter().any(|item| item == feature_value));
if !already_present {
evaluator.set_variable("features", Value::cons(feature_value, features));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct PrimaryWindowSize {
width: u32,
height: u32,
}
type SharedPrimaryWindowSize = Arc<Mutex<PrimaryWindowSize>>;
const HOST_IMAGE_ID_START: u32 = 0x4000_0000;
static HOST_IMAGE_ID_ALLOCATOR: AtomicU32 = AtomicU32::new(HOST_IMAGE_ID_START);
fn next_host_image_id() -> u32 {
HOST_IMAGE_ID_ALLOCATOR.fetch_add(1, Ordering::Relaxed)
}
fn wait_for_image_dimensions(
shared: &SharedImageDimensions,
id: u32,
timeout: Duration,
) -> Option<(u32, u32)> {
let (lock, cvar) = &**shared;
let deadline = Instant::now() + timeout;
let mut dims = match lock.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
loop {
if let Some(size) = dims.get(&id).copied() {
return Some(size);
}
let remaining = deadline.checked_duration_since(Instant::now())?;
match cvar.wait_timeout(dims, remaining) {
Ok((guard, result)) => {
dims = guard;
if result.timed_out() {
return dims.get(&id).copied();
}
}
Err(poisoned) => {
let (guard, _) = poisoned.into_inner();
dims = guard;
}
}
}
}
fn read_primary_window_size(shared: &SharedPrimaryWindowSize) -> PrimaryWindowSize {
match shared.lock() {
Ok(state) => *state,
Err(poisoned) => *poisoned.into_inner(),
}
}
fn prime_initial_monitor_snapshot(shared: &SharedMonitorInfo) {
let (lock, cvar) = &**shared;
let monitors = match lock.lock() {
Ok(guard) => {
if guard.is_empty() {
match cvar.wait_timeout(guard, Duration::from_secs(2)) {
Ok((guard, _)) => guard.clone(),
Err(poisoned) => {
let (guard, _) = poisoned.into_inner();
guard.clone()
}
}
} else {
guard.clone()
}
}
Err(poisoned) => poisoned.into_inner().clone(),
};
if !monitors.is_empty() {
set_neomacs_monitor_info(input_bridge::convert_monitor_infos(&monitors));
}
}
fn record_primary_window_resize(shared: &SharedPrimaryWindowSize, event: &DisplayInputEvent) {
let DisplayInputEvent::WindowResize {
width,
height,
emacs_frame_id,
} = event
else {
return;
};
if *emacs_frame_id != 0 || *width == 0 || *height == 0 {
return;
}
match shared.lock() {
Ok(mut state) => {
state.width = *width;
state.height = *height;
}
Err(poisoned) => {
let mut state = poisoned.into_inner();
state.width = *width;
state.height = *height;
}
}
}
impl PrimaryWindowDisplayHost {
fn send_render_command(
&self,
command: RenderCommand,
error_context: &str,
) -> Result<(), String> {
self.cmd_tx
.send(command)
.map_err(|err| format!("{error_context}: {err}"))?;
if let Some(waker) = &self.render_waker {
waker.wake();
}
Ok(())
}
}
impl DisplayHost for PrimaryWindowDisplayHost {
fn realize_gui_frame(&mut self, request: GuiFrameHostRequest) -> Result<(), String> {
let title_string = request.title.as_utf8_str().unwrap_or("Neomacs").to_owned();
tracing::debug!(
"PrimaryWindowDisplayHost::realize_gui_frame fid=0x{:x} adopted={} size={}x{} title={}",
request.frame_id.0,
self.primary_window_adopted,
request.width,
request.height,
title_string
);
if !self.primary_window_adopted {
self.send_render_command(
RenderCommand::SetWindowTitle {
title: title_string.clone(),
},
"failed to update primary window title",
)?;
self.send_render_command(
RenderCommand::SetFrameGeometryHints {
emacs_frame_id: 0,
geometry_hints: request.geometry_hints,
},
"failed to update primary window geometry hints",
)?;
self.primary_window_adopted = true;
self.primary_frame_id = Some(request.frame_id);
} else {
self.send_render_command(
RenderCommand::CreateWindow {
emacs_frame_id: request.frame_id.0,
width: request.width,
height: request.height,
title: title_string,
geometry_hints: request.geometry_hints,
},
"failed to create additional GUI window",
)?;
}
self.last_window_titles
.lock()
.map_err(|err| format!("failed to cache GUI frame title: {err}"))?
.insert(request.frame_id, request.title);
Ok(())
}
fn opening_gui_frame_pending(&self) -> bool {
!self.primary_window_adopted
}
fn resize_gui_frame(&mut self, request: GuiFrameHostRequest) -> Result<(), String> {
let emacs_frame_id = if self.primary_frame_id == Some(request.frame_id) {
0
} else {
request.frame_id.0
};
tracing::debug!(
"PrimaryWindowDisplayHost::resize_gui_frame fid=0x{:x} route=0x{:x} size={}x{}",
request.frame_id.0,
emacs_frame_id,
request.width,
request.height
);
self.send_render_command(
RenderCommand::ResizeWindow {
emacs_frame_id,
width: request.width,
height: request.height,
geometry_hints: request.geometry_hints,
},
"failed to resize GUI frame",
)?;
Ok(())
}
fn set_gui_frame_geometry_hints(
&mut self,
frame_id: neovm_core::window::FrameId,
geometry_hints: neovm_core::window::GuiFrameGeometryHints,
) -> Result<(), String> {
let emacs_frame_id =
if !self.primary_window_adopted || self.primary_frame_id == Some(frame_id) {
0
} else {
frame_id.0
};
self.send_render_command(
RenderCommand::SetFrameGeometryHints {
emacs_frame_id,
geometry_hints,
},
"failed to update GUI frame geometry hints",
)?;
Ok(())
}
fn set_gui_frame_title(
&mut self,
frame_id: neovm_core::window::FrameId,
title: LispString,
) -> Result<(), String> {
let mut cached_titles = self
.last_window_titles
.lock()
.map_err(|err| format!("failed to cache GUI frame title: {err}"))?;
if cached_titles
.get(&frame_id)
.is_some_and(|cached| cached == &title)
{
return Ok(());
}
cached_titles.insert(frame_id, title.clone());
drop(cached_titles);
let title_string = title.as_utf8_str().unwrap_or("Neomacs").to_owned();
let emacs_frame_id = if self.primary_frame_id == Some(frame_id) {
0
} else {
frame_id.0
};
self.send_render_command(
RenderCommand::SetFrameWindowTitle {
emacs_frame_id,
title: title_string,
},
"failed to update GUI frame title",
)?;
Ok(())
}
fn current_primary_window_size(&self) -> Option<GuiFrameHostSize> {
if self.primary_window_adopted {
return None;
}
let state = read_primary_window_size(&self.primary_window_size);
Some(GuiFrameHostSize {
width: state.width,
height: state.height,
})
}
fn set_cursor_blink(&mut self, enabled: bool, interval_ms: u32) -> Result<(), String> {
self.send_render_command(
RenderCommand::SetCursorBlink {
enabled,
interval_ms,
},
"failed to set cursor blink",
)
}
fn resolve_font_for_char(
&mut self,
request: FontResolveRequest,
) -> Result<Option<ResolvedFontMatch>, String> {
let requested_family_storage = request.face.family_runtime_string_owned();
let requested_family = requested_family_storage.as_deref().unwrap_or("Monospace");
let requested_weight = request.face.weight.unwrap_or(FontWeight::NORMAL).0;
let requested_italic = request
.face
.slant
.map(|slant| slant.is_italic())
.unwrap_or(false);
let font_size = font_size_px_for_face(&request.face);
let selected = self
.font_metrics
.get_or_insert_with(FontMetricsService::new)
.select_font_for_char(
request.character,
requested_family,
requested_weight,
requested_italic,
font_size,
);
tracing::debug!(
target: "neomacs::font_at",
character = %request.character,
requested_family,
requested_weight,
requested_italic,
font_size,
request_face = ?request.face,
selected = ?selected,
"display host resolved font-at request"
);
Ok(selected.map(|font| ResolvedFontMatch {
family: LispString::from_utf8(&font.family),
foundry: None,
weight: font.weight,
slant: font.slant,
width: font.width,
postscript_name: font.postscript_name.map(|s| LispString::from_utf8(&s)),
}))
}
fn resolve_frame_font(
&mut self,
_frame_id: FrameId,
face: neovm_core::face::Face,
) -> Result<Option<ResolvedFrameFont>, String> {
let requested_family_storage = face.family_runtime_string_owned();
let requested_family = requested_family_storage.as_deref().unwrap_or("Monospace");
let requested_weight = face.weight.unwrap_or(FontWeight::NORMAL).0;
let requested_italic = face.slant.map(|slant| slant.is_italic()).unwrap_or(false);
let font_size = font_size_px_for_face(&face);
let selected = self
.font_metrics
.get_or_insert_with(FontMetricsService::new)
.select_font_for_char(
'M',
requested_family,
requested_weight,
requested_italic,
font_size,
);
let Some(font) = selected else {
return Ok(None);
};
let metrics = self
.font_metrics
.get_or_insert_with(FontMetricsService::new)
.font_metrics(
&font.family,
font.weight.0,
font.slant.is_italic(),
font_size,
);
Ok(Some(ResolvedFrameFont {
family: LispString::from_utf8(&font.family),
foundry: None,
weight: font.weight,
slant: font.slant,
width: font.width,
postscript_name: font.postscript_name.map(|s| LispString::from_utf8(&s)),
font_size_px: font_size,
char_width: metrics.char_width.max(1.0),
line_height: metrics.line_height.max(1.0),
}))
}
fn resolve_font_for_spec(
&mut self,
request: FontSpecResolveRequest,
) -> Result<Option<ResolvedFontSpecMatch>, String> {
let matched = neomacs_layout_engine::fontconfig::find_font_for_spec(
request.family.as_ref().and_then(|ls| ls.as_utf8_str()),
request.registry.as_ref().and_then(|ls| ls.as_utf8_str()),
request.lang.as_ref().and_then(|ls| ls.as_utf8_str()),
request.weight.map(|weight| weight.0),
request.slant,
);
Ok(matched.map(|font| ResolvedFontSpecMatch {
family: LispString::from_utf8(&font.family),
registry: Some(LispString::from_utf8("iso10646-1")),
weight: font.weight.map(FontWeight),
slant: Some(font.slant),
width: font.width,
spacing: font.spacing,
postscript_name: font.postscript_name.map(|s| LispString::from_utf8(&s)),
}))
}
fn resolve_image(&self, request: ImageResolveRequest) -> Result<Option<ResolvedImage>, String> {
let cache = match self.resolved_images.lock() {
Ok(cache) => cache,
Err(poisoned) => poisoned.into_inner(),
};
if let Some(image) = cache.get(&request) {
return Ok(Some(image.clone()));
}
drop(cache);
let image_id = next_host_image_id();
match &request.source {
ImageResolveSource::File(path) => {
self.send_render_command(
RenderCommand::ImageLoadFile {
id: image_id,
path: path.as_utf8_str().unwrap_or_default().to_owned(),
max_width: request.max_width,
max_height: request.max_height,
fg_color: request.fg_color,
bg_color: request.bg_color,
},
"failed to queue image load",
)?;
}
ImageResolveSource::Data(data) => {
self.send_render_command(
RenderCommand::ImageLoadData {
id: image_id,
data: data.clone(),
max_width: request.max_width,
max_height: request.max_height,
fg_color: request.fg_color,
bg_color: request.bg_color,
},
"failed to queue image data load",
)?;
}
}
let Some((width, height)) =
wait_for_image_dimensions(&self.image_dimensions, image_id, Duration::from_secs(1))
else {
return Ok(None);
};
let resolved = ResolvedImage {
image_id,
width,
height,
};
match self.resolved_images.lock() {
Ok(mut cache) => {
cache.insert(request, resolved.clone());
}
Err(poisoned) => {
let mut cache = poisoned.into_inner();
cache.insert(request, resolved.clone());
}
}
Ok(Some(resolved))
}
}
fn frame_host_title(eval: &mut Context, frame_id: FrameId) -> LispString {
let Some((selected_window_id, buffer_id, fallback_title, target_cols)) =
eval.frame_manager().get(frame_id).map(|frame| {
let fallback_title = frame.host_title_lisp_string();
let buffer_id = match frame.selected_window() {
Some(Window::Leaf { buffer_id, .. }) => Some(*buffer_id),
_ => None,
};
let target_cols = if frame.char_width > 0.0 {
((frame.width as f32) / frame.char_width.max(1.0))
.floor()
.max(1.0) as usize
} else {
frame.width.max(1) as usize
};
(
frame.selected_window,
buffer_id,
fallback_title,
target_cols.max(1),
)
})
else {
return LispString::from_utf8("Neomacs");
};
let format = eval
.obarray()
.symbol_value("frame-title-format")
.copied()
.unwrap_or(Value::NIL);
if format.is_nil() {
return fallback_title;
}
let rendered = neovm_core::emacs_core::xdisp::format_mode_line_for_display(
eval,
format,
Value::make_window(selected_window_id.0),
buffer_id
.map(|buffer_id| Value::make_buffer(buffer_id))
.unwrap_or(Value::NIL),
target_cols,
);
rendered.as_lisp_string().cloned().unwrap_or(fallback_title)
}
fn adopt_existing_primary_gui_frame(eval: &mut Context) -> Result<(), String> {
if eval
.display_host
.as_ref()
.is_none_or(|host| !host.opening_gui_frame_pending())
{
return Ok(());
}
let Some((frame_id, width, height)) = eval
.frame_manager()
.selected_frame()
.map(|frame| (frame.id, frame.width, frame.height))
else {
return Ok(());
};
let title = frame_host_title(eval, frame_id);
let geometry_hints = eval
.frame_manager()
.get(frame_id)
.map(|frame| frame.gui_geometry_hints())
.ok_or_else(|| "selected GUI frame disappeared before adoption".to_string())?;
let Some(host) = eval.display_host.as_mut() else {
return Ok(());
};
host.realize_gui_frame(GuiFrameHostRequest {
frame_id,
width,
height,
title,
geometry_hints,
})
}
fn sync_live_gui_frame_titles(eval: &mut Context) {
let frame_ids = eval.frame_manager().frame_list();
for frame_id in frame_ids {
let is_gui_frame = eval
.frame_manager()
.get(frame_id)
.is_some_and(|frame| frame.effective_window_system().is_some());
if !is_gui_frame {
continue;
}
let title = frame_host_title(eval, frame_id);
if let Some(host) = eval.display_host.as_mut() {
let _ = host.set_gui_frame_title(frame_id, title);
}
}
}
fn seed_gnu_default_gui_chrome_modes(eval: &mut Context) {
eval.set_variable("menu-bar-mode", Value::T);
eval.set_variable("tool-bar-mode", Value::T);
}
fn ensure_gnu_tool_bar_setup(eval: &mut Context) {
let needs_setup = match eval.eval_str(
"(and (fboundp 'tool-bar-setup) tool-bar-mode (= 1 (length (default-value 'tool-bar-map))))",
) {
Ok(value) => value.is_truthy(),
Err(err) => {
tracing::warn!("failed probing tool-bar setup state: {err}");
false
}
};
if !needs_setup {
return;
}
if let Err(err) = eval.eval_str("(tool-bar-setup)") {
tracing::warn!("failed running GNU tool-bar setup: {err}");
}
}
fn sync_selected_gui_chrome_state(eval: &mut Context) {
let menu_enabled = !eval
.obarray()
.symbol_value("menu-bar-mode")
.copied()
.unwrap_or(Value::NIL)
.is_nil();
let tool_enabled = !eval
.obarray()
.symbol_value("tool-bar-mode")
.copied()
.unwrap_or(Value::NIL)
.is_nil();
if tool_enabled {
ensure_gnu_tool_bar_setup(eval);
}
let menu_items = if menu_enabled {
collect_gui_menu_bar_items(eval)
} else {
Vec::new()
};
let tool_items = if tool_enabled {
collect_gui_tool_bar_items(eval)
} else {
Vec::new()
};
let mut geometry_hints = None;
if let Some(frame) = eval.frame_manager_mut().selected_frame_mut() {
if frame.effective_window_system().is_none() {
return;
}
frame.set_parameter(
Value::symbol("menu-bar-lines"),
Value::fixnum(if menu_items.is_empty() { 0 } else { 1 }),
);
frame.set_parameter(
Value::symbol("tool-bar-lines"),
Value::fixnum(if tool_items.is_empty() { 0 } else { 1 }),
);
frame.sync_menu_bar_height_from_parameters();
frame.sync_tool_bar_height_from_parameters();
geometry_hints = Some((frame.id, frame.gui_geometry_hints()));
}
if let Some((frame_id, hints)) = geometry_hints
&& let Some(host) = eval.display_host.as_mut()
{
let _ = host.set_gui_frame_geometry_hints(frame_id, hints);
}
}
fn font_size_px_for_face(face: &neovm_core::face::Face) -> f32 {
let default_font_size = face_height_to_pixels(100);
match &face.height {
Some(FaceHeight::Absolute(tenths)) => face_height_to_pixels(*tenths),
Some(FaceHeight::Relative(scale)) => default_font_size * (*scale as f32),
None => default_font_size,
}
}
fn create_startup_evaluator_for_mode(mode: RuntimeMode, startup: &StartupOptions) -> Context {
match mode {
RuntimeMode::Raw => {
let startup_surface = raw_loadup_startup_surface(startup, None);
neovm_core::emacs_core::load::create_bootstrap_evaluator_with_startup_surface(
BOOTSTRAP_CORE_FEATURES,
None,
Some(&startup_surface),
)
.expect("raw bootstrap should succeed")
}
RuntimeMode::BootstrapUse => {
neovm_core::emacs_core::load::load_runtime_image_with_features(
RuntimeImageRole::Bootstrap,
BOOTSTRAP_CORE_FEATURES,
startup.dump_file_override.as_deref(),
)
.unwrap_or_else(|err| {
panic!(
"bootstrap image should load: {}",
render_startup_image_error(&err)
)
})
}
RuntimeMode::FinalRun => neovm_core::emacs_core::load::load_runtime_image_with_features(
RuntimeImageRole::Final,
BOOTSTRAP_CORE_FEATURES,
startup.dump_file_override.as_deref(),
)
.unwrap_or_else(|err| {
panic!(
"final image should load: {}",
render_startup_image_error(&err)
)
}),
}
}
fn raw_loadup_command_line(
startup: &StartupOptions,
dump_mode: Option<LoadupDumpMode>,
) -> Vec<String> {
let mut args = startup.forwarded_args.clone();
if args.is_empty() {
args.push(RuntimeMode::Raw.binary_name().to_string());
}
let has_internal_loadup_marker =
matches!(args.get(1).map(String::as_str), Some("-l" | "--load"))
&& args.get(2).map(String::as_str) == Some("loadup");
if !startup.no_loadup && !has_internal_loadup_marker {
args.splice(1..1, ["-l".to_string(), "loadup".to_string()]);
}
if let Some(dump_mode) = dump_mode {
let has_temacs_mode = args
.iter()
.any(|arg| arg == "-temacs" || arg == "--temacs" || arg.starts_with("--temacs="));
if !has_temacs_mode {
args.push(format!("--temacs={}", dump_mode.as_gnu_string()));
}
}
args
}
fn raw_loadup_startup_surface(
startup: &StartupOptions,
dump_mode: Option<LoadupDumpMode>,
) -> LoadupStartupSurface {
LoadupStartupSurface {
command_line_args: raw_loadup_command_line(startup, dump_mode),
noninteractive: startup.noninteractive || dump_mode.is_some(),
}
}
fn run_gui_main_thread(
mode: RuntimeMode,
startup: StartupOptions,
width: u32,
height: u32,
bootstrap_display: BootstrapDisplayConfig,
) {
let event_loop = build_render_event_loop().unwrap_or_else(|err| {
eprintln!("neomacs: failed to build GUI event loop: {err}");
std::process::exit(1);
});
let render_waker = GuiEventLoopWaker::new(event_loop.create_proxy());
let comms = ThreadComms::new().expect("Failed to create thread comms");
let (emacs_comms, render_comms) = comms.split();
let primary_window_size: SharedPrimaryWindowSize =
Arc::new(Mutex::new(PrimaryWindowSize { width, height }));
let gui_image_dimensions: SharedImageDimensions =
Arc::new((Mutex::new(HashMap::new()), Condvar::new()));
let shared_monitors: SharedMonitorInfo = Arc::new((Mutex::new(Vec::new()), Condvar::new()));
let evaluator_handle = spawn_gui_evaluator_worker(
mode,
startup,
width,
height,
bootstrap_display,
emacs_comms,
Arc::clone(&primary_window_size),
Arc::clone(&gui_image_dimensions),
Arc::clone(&shared_monitors),
render_waker.clone(),
);
tracing::info!(
"GUI event loop entering on OS main thread ({}x{})",
width,
height
);
let render_result = run_render_loop_current_thread(
event_loop,
render_comms,
width,
height,
"Neomacs".to_string(),
Arc::clone(&gui_image_dimensions),
Arc::clone(&shared_monitors),
);
if let Err(err) = &render_result {
tracing::error!("GUI event loop exited with error: {err}");
}
let evaluator_exit = match evaluator_handle.join() {
Ok(exit) => exit,
Err(payload) => {
std::panic::resume_unwind(payload);
}
};
if evaluator_exit.restart {
tracing::warn!("restart requested via kill-emacs, but restart is not implemented yet");
}
if evaluator_exit.exit_code != 0 {
std::process::exit(evaluator_exit.exit_code);
}
if render_result.is_err() {
std::process::exit(1);
}
}
fn spawn_gui_evaluator_worker(
mode: RuntimeMode,
startup: StartupOptions,
width: u32,
height: u32,
bootstrap_display: BootstrapDisplayConfig,
emacs_comms: EmacsComms,
primary_window_size: SharedPrimaryWindowSize,
gui_image_dimensions: SharedImageDimensions,
shared_monitors: SharedMonitorInfo,
render_waker: GuiEventLoopWaker,
) -> std::thread::JoinHandle<EvaluatorExit> {
let cmd_tx_for_panic = emacs_comms.cmd_tx.clone();
let render_waker_for_panic = render_waker.clone();
std::thread::Builder::new()
.name("neomacs-evaluator".to_string())
.stack_size(GUI_EVALUATOR_THREAD_STACK_SIZE)
.spawn(move || {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
run_gui_evaluator_worker(
mode,
startup,
width,
height,
bootstrap_display,
emacs_comms,
primary_window_size,
gui_image_dimensions,
shared_monitors,
render_waker,
)
}));
match outcome {
Ok(exit) => exit,
Err(payload) => {
let _ = cmd_tx_for_panic.try_send(RenderCommand::Shutdown);
render_waker_for_panic.wake();
std::panic::resume_unwind(payload);
}
}
})
.expect("Failed to spawn GUI evaluator worker")
}
fn run_gui_evaluator_worker(
mode: RuntimeMode,
startup: StartupOptions,
width: u32,
height: u32,
bootstrap_display: BootstrapDisplayConfig,
emacs_comms: EmacsComms,
primary_window_size: SharedPrimaryWindowSize,
gui_image_dimensions: SharedImageDimensions,
shared_monitors: SharedMonitorInfo,
render_waker: GuiEventLoopWaker,
) -> EvaluatorExit {
let mut evaluator = create_startup_evaluator_for_mode(mode, &startup);
evaluator.setup_thread_locals();
evaluator.set_max_depth(1600);
reset_terminal_host();
reset_terminal_runtime();
evaluator.set_variable("dump-mode", Value::NIL);
tracing::info!("GUI evaluator context initialized");
let _bootstrap = bootstrap_buffers(&mut evaluator, width, height, bootstrap_display);
let frame_id = evaluator
.frame_manager()
.selected_frame()
.expect("No selected frame after bootstrap")
.id;
configure_gnu_startup_state(&mut evaluator, frame_id, &startup);
maybe_install_startup_phase_trace(&mut evaluator);
evaluator.set_display_host(Box::new(PrimaryWindowDisplayHost {
cmd_tx: emacs_comms.cmd_tx.clone(),
render_waker: Some(render_waker.clone()),
primary_window_adopted: false,
primary_frame_id: None,
last_window_titles: Mutex::new(HashMap::new()),
font_metrics: None,
primary_window_size: Arc::clone(&primary_window_size),
image_dimensions: Arc::clone(&gui_image_dimensions),
resolved_images: Mutex::new(HashMap::new()),
}));
adopt_existing_primary_gui_frame(&mut evaluator)
.expect("bootstrap GUI frame adoption should succeed");
prime_initial_monitor_snapshot(&shared_monitors);
let (input_tx, input_rx) = crossbeam_channel::unbounded();
let display_input_rx = emacs_comms.input_rx;
let primary_window_size_for_input = Arc::clone(&primary_window_size);
let quit_requested = Arc::clone(&evaluator.quit_requested);
std::thread::Builder::new()
.name("input-bridge".to_string())
.spawn(move || {
while let Ok(event) = display_input_rx.recv() {
tracing::info!("input-bridge: received event");
record_primary_window_resize(&primary_window_size_for_input, &event);
if let Some(kb_event) = input_bridge::convert_display_event(event) {
tracing::info!("input-bridge: converted to kb event");
if let neovm_core::keyboard::InputEvent::KeyPress { key, .. } = &kb_event
&& key.is_default_quit_char()
{
quit_requested.store(true, std::sync::atomic::Ordering::Relaxed);
}
if input_tx.send(kb_event).is_err() {
break;
}
}
}
})
.expect("Failed to spawn input bridge thread");
evaluator.init_input_system(input_rx, emacs_comms.wakeup_read_fd);
LAYOUT_ENGINE.with(|engine| {
engine.borrow_mut().enable_cosmic_metrics();
});
let frame_tx = emacs_comms.frame_tx;
let initial_frame_tx = frame_tx.clone();
let redisplay_waker = render_waker.clone();
evaluator.redisplay_fn = Some(Box::new(move |eval: &mut Context| {
publish_gui_frame(eval, &frame_tx, Some(&redisplay_waker));
}));
publish_gui_frame(&mut evaluator, &initial_frame_tx, Some(&render_waker));
if let Some(buf) = evaluator.buffer_manager_mut().current_buffer_mut() {
let mut ul = buf.get_undo_list();
neovm_core::buffer::undo_list_boundary(&mut ul);
buf.set_undo_list(ul);
}
neovm_core::emacs_core::load::maybe_run_after_pdump_load_hook(&mut evaluator);
tracing::info!("Entering GNU command loop on GUI evaluator worker...");
let exit_status = evaluator.recursive_edit();
if exit_status.is_ok() {
tracing::info!("Command loop exited normally");
} else {
tracing::warn!("Command loop exited with error");
}
tracing::info!("GUI evaluator shutting down render loop...");
let _ = emacs_comms.cmd_tx.try_send(RenderCommand::Shutdown);
render_waker.wake();
if let Some(request) = evaluator.shutdown_request() {
return EvaluatorExit {
exit_code: request.exit_code,
restart: request.restart,
};
}
EvaluatorExit::OK
}
pub fn run(mode: RuntimeMode) {
if std::env::var("RUST_BACKTRACE").is_err() {
unsafe {
std::env::set_var("RUST_BACKTRACE", "1");
}
}
increase_stack_limit();
if let Some(action) = classify_early_cli_action(std::env::args()) {
match action {
EarlyCliAction::PrintHelp { program } => {
print!("{}", render_help_text(&program));
}
EarlyCliAction::PrintVersion => {
print!("{}", render_version_text());
}
EarlyCliAction::PrintFingerprint => {
print!("{}", render_fingerprint_text());
}
}
return;
}
let startup = parse_startup_options(std::env::args()).unwrap_or_else(|message| {
eprintln!("{message}");
std::process::exit(1);
});
let log_target = match mode {
RuntimeMode::Raw | RuntimeMode::BootstrapUse => neovm_core::logging::LogTarget::Stdout,
RuntimeMode::FinalRun => match startup.frontend {
FrontendKind::Gui => neovm_core::logging::LogTarget::Stdout,
FrontendKind::Tty => neovm_core::logging::LogTarget::File,
},
};
let _logging_guard = neovm_core::logging::init(log_target);
if mode == RuntimeMode::Raw
&& let Some(temacs_mode) = startup.temacs_mode
{
run_temacs_dump_mode(temacs_mode, &startup);
return;
}
tracing::info!(
"{} {} starting (pure Rust, backend={}, pid={}, mode={:?}, image={:?})",
mode.binary_name(),
neomacs_display_runtime::VERSION,
neomacs_display_runtime::CORE_BACKEND,
std::process::id(),
mode,
mode.dump_image_kind()
);
tracing::info!("Startup frontend: {:?}", startup.frontend);
if let Some(device) = startup.terminal_device.as_deref() {
tracing::warn!(
"terminal device {:?} requested; using current tty until explicit device handoff lands",
device
);
}
let bootstrap_display = bootstrap_display_config(startup.frontend);
let frame_metrics = bootstrap_frame_metrics_for_frontend(startup.frontend);
let (width, height) = startup_dimensions(startup.frontend, frame_metrics);
if startup.frontend == FrontendKind::Gui {
run_gui_main_thread(mode, startup, width, height, bootstrap_display);
return;
}
let mut evaluator = create_startup_evaluator_for_mode(mode, &startup);
evaluator.setup_thread_locals();
evaluator.set_max_depth(1600);
if should_enable_live_tty_io(&startup) {
reset_terminal_host();
configure_terminal_runtime(detect_tty_runtime(&startup));
} else {
reset_terminal_host();
reset_terminal_runtime();
}
evaluator.set_variable("dump-mode", Value::NIL);
tracing::info!("Context initialized");
let _bootstrap = bootstrap_buffers(&mut evaluator, width, height, bootstrap_display);
let frame_id = evaluator
.frame_manager()
.selected_frame()
.expect("No selected frame after bootstrap")
.id;
configure_gnu_startup_state(&mut evaluator, frame_id, &startup);
maybe_install_startup_phase_trace(&mut evaluator);
let comms = ThreadComms::new().expect("Failed to create thread comms");
let (emacs_comms, render_comms) = comms.split();
let primary_window_size: SharedPrimaryWindowSize =
Arc::new(Mutex::new(PrimaryWindowSize { width, height }));
if should_enable_live_tty_io(&startup) {
set_terminal_host(Box::new(TtyTerminalHost {
cmd_tx: emacs_comms.cmd_tx.clone(),
}));
}
let frontend = if startup.noninteractive {
tracing::info!("TTY batch mode — skipping terminal init");
FrontendHandle::Batch
} else {
tty_init_terminal();
let input_reader = tty_frontend::TtyInputReader::spawn(render_comms);
tracing::info!("TTY frontend spawned (TtyRif single-thread redisplay)");
FrontendHandle::TtyRifInput(input_reader)
};
if !startup.noninteractive {
let (input_tx, input_rx) = crossbeam_channel::unbounded();
let display_input_rx = emacs_comms.input_rx;
let primary_window_size_for_input = Arc::clone(&primary_window_size);
let quit_requested = Arc::clone(&evaluator.quit_requested);
std::thread::Builder::new()
.name("input-bridge".to_string())
.spawn(move || {
while let Ok(event) = display_input_rx.recv() {
tracing::info!("input-bridge: received event");
record_primary_window_resize(&primary_window_size_for_input, &event);
if let Some(kb_event) = input_bridge::convert_display_event(event) {
tracing::info!("input-bridge: converted to kb event");
if let neovm_core::keyboard::InputEvent::KeyPress { key, .. } = &kb_event {
if key.is_default_quit_char() {
quit_requested.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
if input_tx.send(kb_event).is_err() {
break; }
}
}
})
.expect("Failed to spawn input bridge thread");
let wakeup_fd = emacs_comms.wakeup_read_fd;
evaluator.init_input_system(input_rx, wakeup_fd);
}
maybe_install_tty_redisplay_callback(&mut evaluator, &startup);
if let Some(buf) = evaluator.buffer_manager_mut().current_buffer_mut() {
let mut ul = buf.get_undo_list();
neovm_core::buffer::undo_list_boundary(&mut ul);
buf.set_undo_list(ul);
}
neovm_core::emacs_core::load::maybe_run_after_pdump_load_hook(&mut evaluator);
tracing::info!("Entering GNU command loop (recursive-edit)...");
let exit_status = evaluator.recursive_edit();
if exit_status.is_ok() {
tracing::info!("Command loop exited normally");
} else {
tracing::warn!("Command loop exited with error");
}
tracing::info!("Shutting down...");
let _ = emacs_comms
.cmd_tx
.try_send(neomacs_display_runtime::thread_comm::RenderCommand::Shutdown);
frontend.join();
if should_enable_live_tty_io(&startup) {
tty_shutdown_terminal();
}
tracing::info!("Neomacs exited cleanly");
if let Some(request) = evaluator.shutdown_request() {
if request.restart {
tracing::warn!("restart requested via kill-emacs, but restart is not implemented yet");
}
if request.exit_code != 0 {
std::process::exit(request.exit_code);
}
}
}
#[cfg(unix)]
static TTY_SAVED_TERMIOS: std::sync::Mutex<Option<libc::termios>> = std::sync::Mutex::new(None);
#[cfg(unix)]
fn tty_init_terminal() {
use std::io::Write;
use std::mem::MaybeUninit;
unsafe {
let mut original = MaybeUninit::<libc::termios>::uninit();
if libc::tcgetattr(libc::STDIN_FILENO, original.as_mut_ptr()) != 0 {
tracing::error!("tty_init_terminal: tcgetattr failed");
return;
}
let original = original.assume_init();
if let Ok(mut guard) = TTY_SAVED_TERMIOS.lock() {
*guard = Some(original);
}
let mut raw = original;
raw.c_iflag &= !(libc::BRKINT | libc::ICRNL | libc::INPCK | libc::ISTRIP | libc::IXON);
raw.c_oflag &= !libc::OPOST;
raw.c_cflag |= libc::CS8;
raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::ISIG | libc::IEXTEN);
raw.c_cc[libc::VMIN] = 0;
raw.c_cc[libc::VTIME] = 0;
if libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, &raw) != 0 {
tracing::error!("tty_init_terminal: tcsetattr failed");
return;
}
}
let mut stdout = std::io::stdout();
let _ = stdout.write_all(b"\x1b[?1049h\x1b[?25l\x1b[2J");
let _ = stdout.flush();
tracing::info!("TTY terminal initialized (raw mode + alt screen)");
}
#[cfg(not(unix))]
fn tty_init_terminal() {
tracing::warn!("tty_init_terminal: not implemented on this platform");
}
#[cfg(unix)]
fn tty_shutdown_terminal() {
use std::io::Write;
let mut stdout = std::io::stdout();
let _ = stdout.write_all(b"\x1b[0m\x1b[?25h\x1b[?1049l");
let _ = stdout.flush();
if let Ok(guard) = TTY_SAVED_TERMIOS.lock() {
if let Some(ref original) = *guard {
unsafe {
let _ = libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, original);
}
}
}
tracing::info!("TTY terminal restored");
}
#[cfg(not(unix))]
fn tty_shutdown_terminal() {
tracing::warn!("tty_shutdown_terminal: not implemented on this platform");
}
fn run_temacs_dump_mode(dump_mode: LoadupDumpMode, startup: &StartupOptions) {
tracing::info!(
"{} {} starting raw loadup dump (dump-mode={}, pid={})",
RuntimeMode::Raw.binary_name(),
neomacs_display_runtime::VERSION,
dump_mode.as_gnu_string(),
std::process::id()
);
let startup_surface = raw_loadup_startup_surface(startup, Some(dump_mode));
let eval = neovm_core::emacs_core::load::create_bootstrap_evaluator_with_startup_surface(
BOOTSTRAP_CORE_FEATURES,
Some(dump_mode),
Some(&startup_surface),
)
.expect("temacs bootstrap dump should succeed");
if let Some(request) = eval.shutdown_request()
&& request.exit_code != 0
{
std::process::exit(request.exit_code);
}
}
#[allow(dead_code)]
fn main() {
run(runtime_mode_from_argv(std::env::args()));
}
struct BootstrapResult {
#[allow(dead_code)]
scratch_id: BufferId,
#[allow(dead_code)]
minibuf_id: BufferId,
}
#[derive(Clone, Copy, Debug)]
struct BootstrapFrameMetrics {
char_width: f32,
char_height: f32,
font_pixel_size: f32,
}
fn font_weight_symbol(weight: FontWeight) -> &'static str {
match weight.0 {
0..=150 => "thin",
151..=250 => "extra-light",
251..=350 => "light",
351..=450 => "normal",
451..=550 => "medium",
551..=650 => "semi-bold",
651..=750 => "bold",
751..=850 => "extra-bold",
_ => "black",
}
}
fn startup_font_weight_symbol(weight: FontWeight) -> &'static str {
match weight.0 {
351..=450 => "regular",
_ => font_weight_symbol(weight),
}
}
fn font_slant_symbol(slant: FontSlant) -> &'static str {
match slant {
FontSlant::Normal => "normal",
FontSlant::Italic => "italic",
FontSlant::Oblique => "oblique",
FontSlant::ReverseItalic => "reverse-italic",
FontSlant::ReverseOblique => "reverse-oblique",
}
}
fn font_width_symbol(width: FontWidth) -> &'static str {
match width {
FontWidth::UltraCondensed => "ultra-condensed",
FontWidth::ExtraCondensed => "extra-condensed",
FontWidth::Condensed => "condensed",
FontWidth::SemiCondensed => "semi-condensed",
FontWidth::Normal => "normal",
FontWidth::SemiExpanded => "semi-expanded",
FontWidth::Expanded => "expanded",
FontWidth::ExtraExpanded => "extra-expanded",
FontWidth::UltraExpanded => "ultra-expanded",
}
}
fn bootstrap_default_font_parameter(font_pixel_size: f32) -> Value {
let mut metrics_svc = FontMetricsService::new();
let selected = metrics_svc.select_font_for_char('M', "Monospace", 400, false, font_pixel_size);
let rounded_pixel_size = font_pixel_size.max(1.0).round() as i64;
let family = selected
.as_ref()
.map(|font| font.family.as_str())
.unwrap_or("Monospace");
let weight = selected
.as_ref()
.map(|font| startup_font_weight_symbol(font.weight))
.unwrap_or("regular");
let slant = selected
.as_ref()
.map(|font| font_slant_symbol(font.slant))
.unwrap_or("normal");
let width = selected
.as_ref()
.map(|font| font_width_symbol(font.width))
.unwrap_or("normal");
Value::vector(vec![
Value::keyword("font-object"),
Value::keyword("family"),
Value::string(family),
Value::keyword("weight"),
Value::symbol(weight),
Value::keyword("slant"),
Value::symbol(slant),
Value::keyword("width"),
Value::symbol(width),
Value::keyword("size"),
Value::fixnum(rounded_pixel_size),
Value::keyword("height"),
Value::fixnum(100),
])
}
fn bootstrap_default_font_name(font_pixel_size: f32) -> Value {
let mut metrics_svc = FontMetricsService::new();
let selected = metrics_svc.select_font_for_char('M', "Monospace", 400, false, font_pixel_size);
let rounded_pixel_size = font_pixel_size.max(1.0).round() as i64;
let family = selected
.as_ref()
.map(|font| font.family.as_str())
.unwrap_or("Monospace");
let weight = selected
.as_ref()
.map(|font| startup_font_weight_symbol(font.weight))
.unwrap_or("regular");
let slant = selected
.as_ref()
.map(|font| font_slant_symbol(font.slant))
.unwrap_or("normal");
Value::string(format!(
"-*-{family}-{weight}-{slant}-*-*-{rounded_pixel_size}-*-*-*-*-*-*-*"
))
}
fn bootstrap_frame_metrics() -> BootstrapFrameMetrics {
let font_pixel_size = face_height_to_pixels(100);
let mut metrics_svc = FontMetricsService::new();
let metrics = metrics_svc.font_metrics("Monospace", 400, false, font_pixel_size);
BootstrapFrameMetrics {
char_width: metrics.char_width.max(1.0),
char_height: metrics.line_height.max(1.0),
font_pixel_size,
}
}
fn bootstrap_frame_metrics_for_frontend(frontend: FrontendKind) -> BootstrapFrameMetrics {
if frontend == FrontendKind::Tty {
BootstrapFrameMetrics {
char_width: 1.0,
char_height: 1.0,
font_pixel_size: 16.0,
}
} else {
bootstrap_frame_metrics()
}
}
fn bootstrap_buffers(
eval: &mut Context,
width: u32,
height: u32,
display: BootstrapDisplayConfig,
) -> BootstrapResult {
let frame_metrics = bootstrap_frame_metrics_for_frontend(display.frontend);
let find_or_create_buffer = |eval: &mut Context, name: &str| {
eval.buffer_manager()
.find_buffer_by_name(name)
.unwrap_or_else(|| eval.buffer_manager_mut().create_buffer(name))
};
let scratch_id = find_or_create_buffer(eval, "*scratch*");
let _ = eval
.buffer_manager_mut()
.clear_buffer_labeled_restrictions(scratch_id);
if let Some(buf) = eval.buffer_manager_mut().get_mut(scratch_id) {
buf.widen();
buf.goto_byte(buf.point_max());
}
eval.buffer_manager_mut().set_current(scratch_id);
let msg_id = find_or_create_buffer(eval, "*Messages*");
let _ = eval
.buffer_manager_mut()
.clear_buffer_labeled_restrictions(msg_id);
if let Some(buf) = eval.buffer_manager_mut().get_mut(msg_id) {
buf.widen();
let len = buf.total_bytes();
if len > 0 {
buf.delete_region(0, len);
}
buf.goto_byte(0);
}
let mini_id = find_or_create_buffer(eval, " *Minibuf-0*");
let _ = eval
.buffer_manager_mut()
.clear_buffer_labeled_restrictions(mini_id);
if let Some(buf) = eval.buffer_manager_mut().get_mut(mini_id) {
buf.widen();
buf.goto_byte(0);
}
let frame_id = {
let frame_manager = eval.frame_manager();
let selected = frame_manager.selected_frame().map(|frame| frame.id);
let should_reuse_existing = selected.is_some() && frame_manager.frame_list().len() == 1;
(selected, should_reuse_existing)
};
let frame_id = if frame_id.1 {
let frame_id = frame_id.0.expect("selected startup frame");
tracing::info!(
"Reusing existing startup frame {:?} as bootstrap frame ({}x{})",
frame_id,
width,
height
);
frame_id
} else {
let frame_id = eval
.frame_manager_mut()
.create_frame("F1", width, height, scratch_id);
tracing::info!(
"Created frame {:?} ({}x{}) with *scratch*={:?}",
frame_id,
width,
height,
scratch_id
);
frame_id
};
let _ = eval.frame_manager_mut().select_frame(frame_id);
let initial_tty_frame = display.frontend == FrontendKind::Tty
&& eval
.obarray()
.symbol_value("noninteractive")
.is_some_and(|value| value.is_truthy());
if let Some(frame) = eval.frame_manager_mut().get_mut(frame_id) {
let (default_font, default_font_name) = if display.frontend == FrontendKind::Tty {
(Value::NIL, Value::string("fixed"))
} else {
(
bootstrap_default_font_parameter(frame_metrics.font_pixel_size),
bootstrap_default_font_name(frame_metrics.font_pixel_size),
)
};
frame.set_generated_name_value(frame.generated_name_value());
frame.clear_title();
frame.icon_name = Value::NIL;
frame.initial = initial_tty_frame;
frame.width = width;
frame.height = height;
frame.visible = true;
if let Some(window_system) = display.window_system_symbol() {
frame.set_window_system(Some(Value::symbol(window_system)));
frame.set_parameter(Value::symbol("foreground-color"), Value::string("black"));
frame.set_parameter(Value::symbol("background-color"), Value::string("white"));
} else {
frame.set_window_system(None);
}
frame.set_parameter(
Value::symbol("display-type"),
Value::symbol(display.display_type_symbol()),
);
frame.set_parameter(
Value::symbol("background-mode"),
Value::symbol(display.background_mode),
);
frame.set_parameter(Value::symbol("font"), default_font_name);
frame.set_parameter(Value::symbol("font-parameter"), default_font);
frame.font_pixel_size = frame_metrics.font_pixel_size;
if display.frontend == FrontendKind::Tty {
frame.char_width = 1.0;
frame.char_height = 1.0;
if let Some(mini) = frame.minibuffer_leaf.as_mut() {
let b = *mini.bounds();
mini.set_bounds(neovm_core::window::Rect::new(b.x, b.y, b.width, 1.0));
}
} else {
frame.char_width = frame_metrics.char_width;
frame.char_height = frame_metrics.char_height;
}
frame.sync_tab_bar_height_from_parameters();
if display.frontend == FrontendKind::Tty {
frame.set_parameter(
neovm_core::emacs_core::Value::symbol("menu-bar-lines"),
neovm_core::emacs_core::Value::fixnum(1),
);
}
frame.sync_menu_bar_height_from_parameters();
frame.sync_tool_bar_height_from_parameters();
if let Window::Leaf {
buffer_id,
window_start,
point,
..
} = &mut frame.root_window
{
*buffer_id = scratch_id;
*window_start = 0;
*point = 0;
}
}
if display.frontend == FrontendKind::Gui {
seed_gnu_default_gui_chrome_modes(eval);
sync_selected_gui_chrome_state(eval);
} else {
eval.set_face_attribute(
"default",
":foreground",
neovm_core::face::FaceAttrValue::Unspecified,
);
eval.set_face_attribute(
"default",
":background",
neovm_core::face::FaceAttrValue::Unspecified,
);
}
if display.window_system_symbol().is_some() {
neovm_core::emacs_core::font::seed_live_frame_default_face_from_font_parameter(
eval, frame_id,
);
}
if let Some(frame) = eval.frame_manager_mut().get_mut(frame_id) {
let mini_h = frame.char_height.max(1.0);
let mini_y = height as f32 - mini_h;
if let Window::Leaf { bounds, .. } = &mut frame.root_window {
bounds.height = mini_y;
}
if let Some(mini_leaf) = &mut frame.minibuffer_leaf {
if let Window::Leaf {
buffer_id,
window_start,
point,
bounds,
..
} = mini_leaf
{
*buffer_id = mini_id;
*window_start = 0;
*point = 0;
bounds.y = mini_y;
bounds.height = mini_h;
bounds.width = width as f32;
}
}
}
BootstrapResult {
scratch_id,
minibuf_id: mini_id,
}
}
fn configure_gnu_startup_state(eval: &mut Context, frame_id: FrameId, startup: &StartupOptions) {
let argv_strings = startup.forwarded_args.iter().cloned().collect::<Vec<_>>();
let argv = argv_strings
.iter()
.cloned()
.map(Value::string)
.collect::<Vec<_>>();
let argv_left = argv_strings
.iter()
.skip(1)
.cloned()
.map(Value::string)
.collect::<Vec<_>>();
let invocation_directory = std::env::current_exe()
.ok()
.and_then(|path| path.parent().map(|parent| parent.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("/"));
let invocation_name = std::env::current_exe()
.ok()
.and_then(|path| {
path.file_name()
.map(|name| name.to_string_lossy().to_string())
})
.unwrap_or_else(|| "neomacs".to_string());
let invocation_directory = ensure_dir_string(&invocation_directory);
eval.set_variable("command-line-args", Value::list(argv));
eval.set_variable("command-line-args-left", Value::list(argv_left));
eval.set_variable("command-line-processed", Value::NIL);
eval.set_variable(
"noninteractive",
if startup.noninteractive {
Value::T
} else {
Value::NIL
},
);
eval.set_variable(
"no-site-lisp",
if startup.no_site_lisp {
Value::T
} else {
Value::NIL
},
);
eval.set_variable(
"build-details",
if startup.no_build_details {
Value::NIL
} else {
Value::T
},
);
let (terminal_frame, frame_initial_frame, default_minibuffer_frame) = match startup.frontend {
FrontendKind::Gui => {
let terminal_frame_id = ensure_gnu_startup_terminal_frame(eval, frame_id);
let window_system = Value::symbol(gui_window_system_symbol());
eval.set_variable("window-system", window_system);
eval.set_variable("initial-window-system", window_system);
eval.set_variable(
"frame-initial-frame-alist",
opening_frame_initial_alist(eval, window_system),
);
(
Value::make_frame(terminal_frame_id.0),
Value::make_frame(frame_id.0),
Value::make_frame(frame_id.0),
)
}
FrontendKind::Tty => {
eval.set_variable("window-system", Value::NIL);
eval.set_variable("initial-window-system", Value::NIL);
if should_enable_live_tty_io(startup) {
seed_live_tty_frame_parameters(eval, frame_id, startup);
}
(Value::make_frame(frame_id.0), Value::NIL, Value::NIL)
}
};
eval.set_variable("invocation-name", Value::string(invocation_name));
eval.set_variable(
"invocation-directory",
Value::unibyte_string(invocation_directory),
);
let cwd = std::env::current_dir()
.map(|p| ensure_dir_string(&p))
.unwrap_or_else(|_| "/".to_string());
eval.set_variable("default-directory", Value::unibyte_string(cwd));
eval.set_variable("terminal-frame", terminal_frame);
eval.set_variable("frame-initial-frame", frame_initial_frame);
eval.set_variable("default-minibuffer-frame", default_minibuffer_frame);
eval.set_variable("inhibit-startup-screen", Value::T);
}
fn seed_live_tty_frame_parameters(eval: &mut Context, frame_id: FrameId, startup: &StartupOptions) {
let tty_name = detect_tty_name(startup);
let tty_type = detect_tty_type();
if let Some(frame) = eval.frame_manager_mut().get_mut(frame_id) {
frame.set_parameter(Value::symbol("tty"), Value::string(tty_name));
if let Some(tty_type) = tty_type {
frame.set_parameter(Value::symbol("tty-type"), Value::string(tty_type));
} else {
frame.remove_parameter(Value::symbol("tty-type"));
}
}
}
fn ensure_gnu_startup_terminal_frame(eval: &mut Context, opening_frame_id: FrameId) -> FrameId {
if let Some(existing) = eval
.frame_manager()
.frame_list()
.into_iter()
.find(|candidate| {
*candidate != opening_frame_id
&& eval.frame_manager().get(*candidate).is_some_and(|frame| {
!frame.visible && frame.effective_window_system().is_none()
})
})
{
return existing;
}
let seed_buffer_id = if let Some(id) = eval.buffer_manager().current_buffer_id() {
id
} else if let Some(id) = eval.buffer_manager().find_buffer_by_name("*scratch*") {
id
} else {
eval.buffer_manager_mut().create_buffer("*scratch*")
};
let (width, height, environment) = eval
.frame_manager()
.get(opening_frame_id)
.map(|frame| {
(
frame.width.max(1),
frame.height.max(1),
frame.parameter("environment"),
)
})
.unwrap_or((80, 25, None));
let terminal_frame_id =
eval.frame_manager_mut()
.create_frame("Fstartup-tty", width, height, seed_buffer_id);
if let Some(frame) = eval.frame_manager_mut().get_mut(terminal_frame_id) {
frame.visible = false;
frame.set_window_system(None);
frame.remove_parameter(Value::symbol("display-type"));
frame.remove_parameter(Value::symbol("background-mode"));
if let Some(environment) = environment {
frame.set_parameter(Value::symbol("environment"), environment);
}
}
terminal_frame_id
}
fn opening_frame_initial_alist(eval: &Context, window_system: Value) -> Value {
let mut params = vec![Value::cons(Value::symbol("window-system"), window_system)];
for symbol_name in ["initial-frame-alist", "default-frame-alist"] {
if let Some(value) = eval.obarray().symbol_value(symbol_name)
&& let Some(items) = neovm_core::emacs_core::value::list_to_vec(value)
{
params.extend(items);
}
}
Value::list(params)
}
#[cfg(test)]
fn run_gnu_startup(eval: &mut Context) {
increase_stack_limit();
stacker::grow(64 * 1024 * 1024, || run_gnu_startup_inner(eval));
}
#[cfg(test)]
fn run_gnu_startup_inner(eval: &mut Context) {
eval.setup_thread_locals();
let _ = std::fs::write("/tmp/neomacs-startup-phases.trace", "");
maybe_install_startup_phase_trace(eval);
eval.eval_str(
r#"
(progn
(defun neomacs--test-exit-startup-recursive-edit ()
(remove-hook 'window-setup-hook
#'neomacs--test-exit-startup-recursive-edit)
(exit-recursive-edit))
(add-hook 'window-setup-hook
#'neomacs--test-exit-startup-recursive-edit))
"#,
)
.expect("startup exit helper should install");
let top_level = eval.obarray().symbol_value("top-level").cloned();
tracing::info!("top-level variable before startup: {:?}", top_level);
let (_tx, rx) = crossbeam_channel::unbounded();
let mut wake_pipe = [0; 2];
let pipe_result = unsafe { libc::pipe(wake_pipe.as_mut_ptr()) };
assert_eq!(pipe_result, 0, "pipe should initialize");
eval.init_input_system(rx, wake_pipe[0]);
let result = eval.recursive_edit();
unsafe {
libc::close(wake_pipe[0]);
libc::close(wake_pipe[1]);
}
if let Err(other) = result {
let last_phase = eval
.obarray()
.symbol_value("neomacs--startup-last-phase")
.cloned()
.map(|value| print_value_with_eval(eval, &value));
let last_call = eval
.obarray()
.symbol_value("neomacs--startup-last-call")
.cloned()
.map(|value| print_value_with_eval(eval, &value));
panic!(
"GNU startup via recursive_edit failed: {other} last-phase={last_phase:?} last-call={last_call:?}"
);
}
}
fn maybe_install_startup_phase_trace(eval: &mut Context) {
if !cfg!(test) && std::env::var("NEOMACS_TRACE_STARTUP_PHASES").unwrap_or_default() != "1" {
return;
}
let source = r#"
(progn
(defvar neomacs--startup-last-phase nil)
(defvar neomacs--startup-last-call nil)
(defvar neomacs--startup-trace-active nil)
(with-temp-buffer
(write-region (point-min) (point-max)
"/tmp/neomacs-startup-phases.trace" nil 'silent))
(defun neomacs--startup-trace-around (name orig &rest args)
(if neomacs--startup-trace-active
(apply orig args)
(let ((neomacs--startup-trace-active t))
(setq neomacs--startup-last-phase name)
(setq neomacs--startup-last-call (cons name args))
(with-temp-buffer
(insert (format "enter %S %S\n" name args))
(append-to-file (point-min) (point-max)
"/tmp/neomacs-startup-phases.trace"))
(prog1
(apply orig args)
(with-temp-buffer
(insert (format "leave %S\n" name))
(append-to-file (point-min) (point-max)
"/tmp/neomacs-startup-phases.trace"))))))
(dolist (fn '(set-locale-environment
handle-args-function
x-handle-args
x-open-connection
create-default-fontset
create-fontset-from-fontset-spec
create-fontset-from-x-resource
neomacs--setup-cursor-blink
neomacs--setup-animations
pixel-scroll-precision-mode
frame-initialize
startup--setup-quote-display
normal-erase-is-backspace-setup-frame
tty-register-default-colors
startup--load-user-init-file
custom-reevaluate-setting
tty-run-terminal-initialization
display-startup-echo-area-message
command-line-1
display-startup-screen
frame-notice-user-settings))
(when (fboundp fn)
(advice-add fn :around
(eval `(lambda (orig &rest args)
(apply #'neomacs--startup-trace-around
',fn orig args)))))))
"#;
if let Err(err) = eval.eval_str(source) {
tracing::warn!("startup trace helper install failed: {err:?}");
}
}
fn ensure_dir_string(path: &Path) -> String {
let mut dir = path.to_string_lossy().to_string();
if !dir.ends_with('/') {
dir.push('/');
}
dir
}
fn current_layout_frame_id(evaluator: &Context) -> Option<FrameId> {
evaluator
.frame_manager()
.selected_frame()
.map(|frame| frame.id)
}
fn publish_gui_frame(
evaluator: &mut Context,
frame_tx: &crossbeam_channel::Sender<neomacs_display_protocol::glyph_matrix::FrameDisplayState>,
render_waker: Option<&GuiEventLoopWaker>,
) {
evaluator.setup_thread_locals();
sync_selected_gui_chrome_state(evaluator);
run_layout(evaluator);
sync_live_gui_frame_titles(evaluator);
let display_state =
LAYOUT_ENGINE.with(|engine| engine.borrow_mut().last_frame_display_state.take());
let Some(display_state) = display_state else {
return;
};
if frame_tx.try_send(display_state).is_ok() {
if let Some(waker) = render_waker {
waker.wake();
}
}
}
thread_local! {
static LAYOUT_ENGINE: std::cell::RefCell<neomacs_display_runtime::layout::LayoutEngine> =
std::cell::RefCell::new(neomacs_display_runtime::layout::LayoutEngine::new_without_font_metrics());
}
fn run_layout(evaluator: &mut Context) {
let Some(frame_id) = current_layout_frame_id(evaluator) else {
tracing::warn!("run_layout: no selected live frame");
return;
};
LAYOUT_ENGINE.with(|engine| {
engine.borrow_mut().layout_frame_rust(evaluator, frame_id);
});
}
fn layout_frame_display_state(
evaluator: &mut Context,
frame_id: FrameId,
) -> Option<neomacs_display_protocol::glyph_matrix::FrameDisplayState> {
LAYOUT_ENGINE.with(|engine| {
let mut engine = engine.borrow_mut();
engine.layout_frame_rust(evaluator, frame_id);
engine.last_frame_display_state.take()
})
}
fn frame_origin_in_root(evaluator: &Context, frame_id: FrameId) -> (f32, f32) {
let mut x = 0_i64;
let mut y = 0_i64;
let mut current = Some(frame_id);
let mut seen = std::collections::HashSet::new();
while let Some(fid) = current {
if !seen.insert(fid) {
break;
}
let Some(frame) = evaluator.frame_manager().get(fid) else {
break;
};
x += frame.left_pos;
y += frame.top_pos;
current = evaluator.frame_manager().frame_parent_id(fid);
}
(x as f32, y as f32)
}
fn run_tty_layout_tree(
evaluator: &mut Context,
) -> Option<(
neomacs_display_protocol::glyph_matrix::FrameDisplayState,
Vec<neomacs_display_protocol::glyph_matrix::FrameDisplayState>,
)> {
let selected = current_layout_frame_id(evaluator)?;
let root_id = evaluator
.frame_manager()
.root_frame_id(selected)
.unwrap_or(selected);
let frame_order = evaluator
.frame_manager()
.frames_in_reverse_z_order(root_id, true);
let mut root_state = layout_frame_display_state(evaluator, root_id)?;
root_state.parent_id = 0;
root_state.parent_x = 0.0;
root_state.parent_y = 0.0;
let mut child_states = Vec::new();
for frame_id in frame_order {
if frame_id == root_id {
continue;
}
let Some(mut state) = layout_frame_display_state(evaluator, frame_id) else {
continue;
};
let (x, y) = frame_origin_in_root(evaluator, frame_id);
state.parent_id = root_state.frame_id;
state.parent_x = x;
state.parent_y = y;
child_states.push(state);
}
Some((root_state, child_states))
}
fn run_tty_rif_redisplay(
tty_rif: &mut neomacs_display_protocol::tty_rif::TtyRif,
root: &neomacs_display_protocol::glyph_matrix::FrameDisplayState,
children: &[neomacs_display_protocol::glyph_matrix::FrameDisplayState],
) {
tty_rif.rasterize_frame_tree(root, children);
tty_rif.diff_and_render();
let output = tty_rif.take_output();
tracing::debug!("tty_rif: output {} bytes", output.len());
if !output.is_empty() {
use std::io::Write;
let _ = std::io::stdout().write_all(&output);
let _ = std::io::stdout().flush();
}
}
#[cfg(unix)]
fn increase_stack_limit() {
const TARGET_STACK_MB: u64 = 128;
let target = TARGET_STACK_MB * 1024 * 1024;
unsafe {
let mut rlim = std::mem::MaybeUninit::<libc::rlimit>::uninit();
if libc::getrlimit(libc::RLIMIT_STACK, rlim.as_mut_ptr()) == 0 {
let mut rlim = rlim.assume_init();
if rlim.rlim_cur < target as libc::rlim_t {
rlim.rlim_cur = std::cmp::min(target as libc::rlim_t, rlim.rlim_max);
let _ = libc::setrlimit(libc::RLIMIT_STACK, &rlim);
}
}
}
}
#[cfg(not(unix))]
fn increase_stack_limit() {}
#[cfg(test)]
#[path = "main_test.rs"]
mod tests;