diff --git a/src/arena/release.rs b/src/arena/release.rs index 7e4ebcf216bd..464bbbd7f095 100644 --- a/src/arena/release.rs +++ b/src/arena/release.rs @@ -11,6 +11,7 @@ use std::ptr::{self, NonNull}; use std::{mem, slice}; use crate::helpers::*; +use crate::sys::Syscall; use crate::{apperr, sys}; const ALLOC_CHUNK_SIZE: usize = 64 * KIBI; @@ -66,7 +67,7 @@ impl Arena { pub fn new(capacity: usize) -> apperr::Result { let capacity = (capacity.max(1) + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1); - let base = unsafe { sys::virtual_reserve(capacity)? }; + let base = unsafe { sys::syscall::virtual_reserve(capacity)? }; Ok(Self { base, @@ -139,7 +140,8 @@ impl Arena { if commit_new > self.capacity || unsafe { - sys::virtual_commit(self.base.add(commit_old), commit_new - commit_old).is_err() + sys::syscall::virtual_commit(self.base.add(commit_old), commit_new - commit_old) + .is_err() } { return Err(AllocError); @@ -176,7 +178,7 @@ impl Arena { impl Drop for Arena { fn drop(&mut self) { if !self.is_empty() { - unsafe { sys::virtual_release(self.base, self.capacity) }; + unsafe { sys::syscall::virtual_release(self.base, self.capacity) }; } } } diff --git a/src/bin/edit/documents.rs b/src/bin/edit/documents.rs index 33fc8cf5a76d..a6f946955a75 100644 --- a/src/bin/edit/documents.rs +++ b/src/bin/edit/documents.rs @@ -8,6 +8,7 @@ use std::path::{Path, PathBuf}; use edit::buffer::{RcTextBuffer, TextBuffer}; use edit::helpers::{CoordType, Point}; +use edit::sys::Syscall; use edit::{apperr, path, sys}; use crate::state::DisplayablePathBuf; @@ -31,7 +32,7 @@ impl Document { tb.write_file(&mut file)?; } - if let Ok(id) = sys::file_id(None, path) { + if let Ok(id) = sys::syscall::file_id(None, path) { self.file_id = Some(id); } @@ -51,7 +52,7 @@ impl Document { tb.read_file(&mut file, encoding)?; } - if let Ok(id) = sys::file_id(None, path) { + if let Ok(id) = sys::syscall::file_id(None, path) { self.file_id = Some(id); } @@ -145,11 +146,12 @@ impl DocumentManager { let mut file = match Self::open_for_reading(&path) { Ok(file) => Some(file), - Err(err) if sys::apperr_is_not_found(err) => None, + Err(err) if sys::syscall::apperr_is_not_found(err) => None, Err(err) => return Err(err), }; - let file_id = if file.is_some() { Some(sys::file_id(file.as_ref(), &path)?) } else { None }; + let file_id = + if file.is_some() { Some(sys::syscall::file_id(file.as_ref(), &path)?) } else { None }; // Check if the file is already open. if file_id.is_some() && self.update_active(|doc| doc.file_id == file_id) { diff --git a/src/bin/edit/localization.rs b/src/bin/edit/localization.rs index aeecec7aa56d..b8564ca8de80 100644 --- a/src/bin/edit/localization.rs +++ b/src/bin/edit/localization.rs @@ -3,7 +3,7 @@ use edit::arena::scratch_arena; use edit::helpers::AsciiStringHelpers; -use edit::sys; +use edit::sys::{self, Syscall}; include!(concat!(env!("OUT_DIR"), "/i18n_edit.rs")); @@ -11,7 +11,7 @@ static mut S_LANG: LangId = LangId::en; pub fn init() { let scratch = scratch_arena(None); - let langs = sys::preferred_languages(&scratch); + let langs = sys::syscall::preferred_languages(&scratch); let mut lang = LangId::en; 'outer: for l in langs { diff --git a/src/bin/edit/main.rs b/src/bin/edit/main.rs index f21ae84ebc0a..b8db7834564d 100644 --- a/src/bin/edit/main.rs +++ b/src/bin/edit/main.rs @@ -27,6 +27,7 @@ use edit::framebuffer::{self, IndexedColor}; use edit::helpers::{CoordType, KIBI, MEBI, MetricFormatter, Rect, Size}; use edit::input::{self, kbmod, vk}; use edit::oklab::StraightRgba; +use edit::sys::Syscall; use edit::tui::*; use edit::vt::{self, Token}; use edit::{apperr, arena_format, base64, path, sys, unicode}; @@ -51,7 +52,7 @@ fn main() -> process::ExitCode { match run() { Ok(()) => process::ExitCode::SUCCESS, Err(err) => { - sys::write_stdout(&format!("{}\n", FormatApperr::from(err))); + sys::syscall::write_stdout(&format!("{}\n", FormatApperr::from(err))); process::ExitCode::FAILURE } } @@ -59,7 +60,7 @@ fn main() -> process::ExitCode { fn run() -> apperr::Result<()> { // Init `sys` first, as everything else may depend on its functionality (IO, function pointers, etc.). - let _sys_deinit = sys::init(); + let _sys_deinit = sys::syscall::init(); // Next init `arena`, so that `scratch_arena` works. `loc` depends on it. arena::init(SCRATCH_ARENA_CAPACITY)?; // Init the `loc` module, so that error messages are localized. @@ -75,7 +76,7 @@ fn run() -> apperr::Result<()> { // `handle_args` may want to print a help message (must not fail), // and reads files (may hang; should be cancelable with Ctrl+C). // As such, we call this after `handle_args`. - sys::switch_modes()?; + sys::syscall::switch_modes()?; let mut vt_parser = vt::Parser::new(); let mut input_parser = input::Parser::new(); @@ -103,7 +104,7 @@ fn run() -> apperr::Result<()> { tui.set_modal_default_bg(floater_bg); tui.set_modal_default_fg(floater_fg); - sys::inject_window_size_into_stdin(); + sys::syscall::inject_window_size_into_stdin(); #[cfg(feature = "debug-latency")] let mut last_latency_width = 0; @@ -118,7 +119,7 @@ fn run() -> apperr::Result<()> { { let scratch = scratch_arena(None); let read_timeout = vt_parser.read_timeout().min(tui.read_timeout()); - let Some(input) = sys::read_stdin(&scratch, read_timeout) else { + let Some(input) = sys::syscall::read_stdin(&scratch, read_timeout) else { break; }; @@ -214,7 +215,7 @@ fn run() -> apperr::Result<()> { last_latency_width = cols; } - sys::write_stdout(&output); + sys::syscall::write_stdout(&output); } } @@ -263,7 +264,7 @@ fn handle_args(state: &mut State) -> apperr::Result { cwd = parent.to_path_buf(); } - if let Some(mut file) = sys::open_stdin_if_redirected() { + if let Some(mut file) = sys::syscall::open_stdin_if_redirected() { let doc = state.documents.add_untitled()?; let mut tb = doc.buffer.borrow_mut(); tb.read_file(&mut file, None)?; @@ -278,7 +279,7 @@ fn handle_args(state: &mut State) -> apperr::Result { } fn print_help() { - sys::write_stdout(concat!( + sys::syscall::write_stdout(concat!( "Usage: edit [OPTIONS] [FILE[:LINE[:COLUMN]]]\n", "Options:\n", " -h, --help Print this help message\n", @@ -290,7 +291,7 @@ fn print_help() { } fn print_version() { - sys::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\n")); + sys::syscall::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\n")); } fn draw(ctx: &mut Context, state: &mut State) { @@ -527,12 +528,12 @@ impl Drop for RestoreModes { // Same as in the beginning but in the reverse order. // It also includes DECSCUSR 0 to reset the cursor style and DECTCEM to show the cursor. // We specifically don't reset mode 1036, because most applications expect it to be set nowadays. - sys::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1002;1006;2004l\x1b[?1049l"); + sys::syscall::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1002;1006;2004l\x1b[?1049l"); } } fn setup_terminal(tui: &mut Tui, state: &mut State, vt_parser: &mut vt::Parser) -> RestoreModes { - sys::write_stdout(concat!( + sys::syscall::write_stdout(concat!( // 1049: Alternative Screen Buffer // I put the ASB switch in the beginning, just in case the terminal performs // some additional state tracking beyond the modes we enable/disable. @@ -570,7 +571,7 @@ fn setup_terminal(tui: &mut Tui, state: &mut State, vt_parser: &mut vt::Parser) // We explicitly set a high read timeout, because we're not // waiting for user keyboard input. If we encounter a lone ESC, // it's unlikely to be from a ESC keypress, but rather from a VT sequence. - let Some(input) = sys::read_stdin(&scratch, Duration::from_secs(3)) else { + let Some(input) = sys::syscall::read_stdin(&scratch, Duration::from_secs(3)) else { break; }; diff --git a/src/bin/edit/state.rs b/src/bin/edit/state.rs index 451060bf6db4..222762d4f51e 100644 --- a/src/bin/edit/state.rs +++ b/src/bin/edit/state.rs @@ -9,6 +9,7 @@ use std::path::{Path, PathBuf}; use edit::framebuffer::IndexedColor; use edit::helpers::*; use edit::oklab::StraightRgba; +use edit::sys::Syscall; use edit::tui::*; use edit::{apperr, buffer, icu, sys}; @@ -30,7 +31,7 @@ impl std::fmt::Display for FormatApperr { apperr::APP_ICU_MISSING => f.write_str(loc(LocId::ErrorIcuMissing)), apperr::Error::App(code) => write!(f, "Unknown app error code: {code}"), apperr::Error::Icu(code) => icu::apperr_format(f, code), - apperr::Error::Sys(code) => sys::apperr_format(f, code), + apperr::Error::Sys(code) => sys::syscall::apperr_format(f, code), } } } diff --git a/src/buffer/gap_buffer.rs b/src/buffer/gap_buffer.rs index df1e38d3ace7..7fc7e27c2c7a 100644 --- a/src/buffer/gap_buffer.rs +++ b/src/buffer/gap_buffer.rs @@ -7,6 +7,7 @@ use std::slice; use crate::document::{ReadableDocument, WriteableDocument}; use crate::helpers::*; +use crate::sys::Syscall; use crate::{apperr, sys}; #[cfg(target_pointer_width = "32")] @@ -31,7 +32,7 @@ impl Drop for BackingBuffer { fn drop(&mut self) { unsafe { if let Self::VirtualMemory(ptr, reserve) = *self { - sys::virtual_release(ptr, reserve); + sys::syscall::virtual_release(ptr, reserve); } } } @@ -73,7 +74,7 @@ impl GapBuffer { buffer = BackingBuffer::Vec(Vec::new()); } else { reserve = LARGE_CAPACITY; - text = unsafe { sys::virtual_reserve(reserve)? }; + text = unsafe { sys::syscall::virtual_reserve(reserve)? }; buffer = BackingBuffer::VirtualMemory(text, reserve); } @@ -195,7 +196,9 @@ impl GapBuffer { match &mut self.buffer { BackingBuffer::VirtualMemory(ptr, _) => unsafe { - if sys::virtual_commit(ptr.add(bytes_old), bytes_new - bytes_old).is_err() { + if sys::syscall::virtual_commit(ptr.add(bytes_old), bytes_new - bytes_old) + .is_err() + { return; } }, diff --git a/src/icu.rs b/src/icu.rs index 9687794a9aaf..959617bd57e1 100644 --- a/src/icu.rs +++ b/src/icu.rs @@ -12,6 +12,7 @@ use std::ptr::{null, null_mut}; use crate::arena::{Arena, ArenaString, scratch_arena}; use crate::buffer::TextBuffer; +use crate::sys::Syscall; use crate::unicode::Utf8Chars; use crate::{apperr, arena_format, sys}; @@ -978,7 +979,7 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> { unsafe { LIBRARY_FUNCTIONS = LibraryFunctionsState::Failed; - let Ok(icu) = sys::load_icu() else { + let Ok(icu) = sys::syscall::load_icu() else { return; }; @@ -1017,7 +1018,7 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> { #[cfg(edit_icu_renaming_auto_detect)] let name = sys::icu_add_renaming_suffix(&scratch, name, &suffix); - let Ok(func) = sys::get_proc_address(handle, name) else { + let Ok(func) = sys::syscall::get_proc_address(handle, name) else { debug_assert!( false, "Failed to load ICU function: {:?}", diff --git a/src/simd/memchr2.rs b/src/simd/memchr2.rs index 3e1708d0cc7a..8819c6fcd744 100644 --- a/src/simd/memchr2.rs +++ b/src/simd/memchr2.rs @@ -226,7 +226,7 @@ mod tests { use std::slice; use super::*; - use crate::sys; + use crate::sys::{self, Syscall}; #[test] fn test_empty() { @@ -265,8 +265,8 @@ mod tests { const PAGE_SIZE: usize = 64 * 1024; // 64 KiB to cover many architectures. // 3 pages: uncommitted, committed, uncommitted - let ptr = sys::virtual_reserve(PAGE_SIZE * 3).unwrap(); - sys::virtual_commit(ptr.add(PAGE_SIZE), PAGE_SIZE).unwrap(); + let ptr = sys::syscall::virtual_reserve(PAGE_SIZE * 3).unwrap(); + sys::syscall::virtual_commit(ptr.add(PAGE_SIZE), PAGE_SIZE).unwrap(); slice::from_raw_parts_mut(ptr.add(PAGE_SIZE).as_ptr(), PAGE_SIZE) }; diff --git a/src/sys/mod.rs b/src/sys/mod.rs index 91d598812484..b6d459476158 100644 --- a/src/sys/mod.rs +++ b/src/sys/mod.rs @@ -3,15 +3,131 @@ //! Platform abstractions. +#![allow(non_camel_case_types)] + #[cfg(unix)] mod unix; #[cfg(windows)] mod windows; +use std::ffi::{c_char, c_void}; +use std::fs::File; #[cfg(not(windows))] pub use std::fs::canonicalize; +use std::path::Path; +use std::ptr::NonNull; +use std::{fmt, time}; #[cfg(unix)] pub use unix::*; #[cfg(windows)] pub use windows::*; + +use crate::apperr; +use crate::arena::{Arena, ArenaString}; + +#[cfg(unix)] +pub type syscall = UnixSys; +#[cfg(windows)] +pub type syscall = WindowsSys; + +pub trait Syscall { + /// Initializes the platform-specific state. + fn init() -> Deinit; + + /// Switches the terminal into raw mode, etc. + fn switch_modes() -> apperr::Result<()>; + + /// During startup we need to get the window size from the terminal. + /// Because I didn't want to type a bunch of code, this function tells + /// [`read_stdin`] to inject a fake sequence, which gets picked up by + /// the input parser and provided to the TUI code. + fn inject_window_size_into_stdin(); + + /// Reads from stdin. + /// + /// # Returns + /// + /// * `None` if there was an error reading from stdin. + /// * `Some("")` if the given timeout was reached. + /// * Otherwise, it returns the read, non-empty string. + fn read_stdin(arena: &Arena, timeout: time::Duration) -> Option>; + + /// Writes a string to stdout. + /// + /// Use this instead of `print!` or `println!` to avoid + /// the overhead of Rust's stdio handling. Don't need that. + fn write_stdout(text: &str); + + /// Check if the stdin handle is redirected to a file, etc. + /// + /// # Returns + /// + /// * `Some(file)` if stdin is redirected. + /// * Otherwise, `None`. + fn open_stdin_if_redirected() -> Option; + + /// Returns a unique identifier for the given file by handle or path. + fn file_id(file: Option<&File>, path: &Path) -> apperr::Result; + + /// Reserves a virtual memory region of the given size. + /// To commit the memory, use `virtual_commit`. + /// To release the memory, use `virtual_release`. + /// + /// # Safety + /// + /// This function is unsafe because it uses raw pointers. + /// Don't forget to release the memory when you're done with it or you'll leak it. + unsafe fn virtual_reserve(size: usize) -> apperr::Result>; + + /// Commits a virtual memory region of the given size. + /// + /// # Safety + /// + /// This function is unsafe because it uses raw pointers. + /// Make sure to only pass pointers acquired from `virtual_reserve` + /// and to pass a size less than or equal to the size passed to `virtual_reserve`. + unsafe fn virtual_commit(base: NonNull, size: usize) -> apperr::Result<()>; + + /// Releases a virtual memory region of the given size. + /// + /// # Safety + /// + /// This function is unsafe because it uses raw pointers. + /// Make sure to only pass pointers acquired from `virtual_reserve`. + unsafe fn virtual_release(base: NonNull, size: usize); + + /// Associated type for `load_library` because the library name type differs between platforms. + /// on `Unix` it is `*const c_char`, on `Windows` it is `*const u16`. + type LibraryName; + + /// Loads a dynamic library. + /// + /// # Safety + /// + /// This function is unsafe because it uses raw pointers. + unsafe fn load_library(name: Self::LibraryName) -> apperr::Result>; + + /// Loads a function from a dynamic library. + /// + /// # Safety + /// + /// This function is highly unsafe as it requires you to know the exact type + /// of the function you're loading. No type checks whatsoever are performed. + // + // It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. + unsafe fn get_proc_address( + handle: NonNull, + name: *const c_char, + ) -> apperr::Result; + + fn load_icu() -> apperr::Result; + + /// Returns a list of preferred languages for the current user. + fn preferred_languages(arena: &Arena) -> Vec, &Arena>; + + fn apperr_format(f: &mut fmt::Formatter<'_>, code: u32) -> fmt::Result; + + /// Checks if the given error is a "file not found" error. + fn apperr_is_not_found(err: apperr::Error) -> bool; +} diff --git a/src/sys/unix.rs b/src/sys/unix.rs index f3b067bd0366..6d2b2ea2d6ec 100644 --- a/src/sys/unix.rs +++ b/src/sys/unix.rs @@ -4,7 +4,6 @@ //! Unix-specific platform code. //! //! Read the `windows` module for reference. -//! TODO: This reminds me that the sys API should probably be a trait. use std::ffi::{CStr, c_char, c_int, c_void}; use std::fs::File; @@ -12,8 +11,9 @@ use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::os::fd::{AsRawFd as _, FromRawFd as _}; use std::path::Path; use std::ptr::{self, NonNull, null_mut}; -use std::{thread, time}; +use std::{fmt, thread, time}; +use super::Syscall; use crate::arena::{Arena, ArenaString, scratch_arena}; use crate::helpers::*; use crate::{apperr, arena_format}; @@ -60,34 +60,37 @@ extern "C" fn sigwinch_handler(_: libc::c_int) { } } -pub fn init() -> Deinit { - Deinit -} +pub struct UnixSys; -pub fn switch_modes() -> apperr::Result<()> { - unsafe { - // Reopen stdin if it's redirected (= piped input). - if libc::isatty(STATE.stdin) == 0 { - STATE.stdin = check_int_return(libc::open(c"/dev/tty".as_ptr(), libc::O_RDONLY))?; - } +impl Syscall for UnixSys { + fn init() -> Deinit { + Deinit + } - // Store the stdin flags so we can more easily toggle `O_NONBLOCK` later on. - STATE.stdin_flags = check_int_return(libc::fcntl(STATE.stdin, libc::F_GETFL))?; + fn switch_modes() -> apperr::Result<()> { + unsafe { + // Reopen stdin if it's redirected (= piped input). + if libc::isatty(STATE.stdin) == 0 { + STATE.stdin = check_int_return(libc::open(c"/dev/tty".as_ptr(), libc::O_RDONLY))?; + } - // Set STATE.inject_resize to true whenever we get a SIGWINCH. - let mut sigwinch_action: libc::sigaction = mem::zeroed(); - sigwinch_action.sa_sigaction = sigwinch_handler as libc::sighandler_t; - check_int_return(libc::sigaction(libc::SIGWINCH, &sigwinch_action, null_mut()))?; + // Store the stdin flags so we can more easily toggle `O_NONBLOCK` later on. + STATE.stdin_flags = check_int_return(libc::fcntl(STATE.stdin, libc::F_GETFL))?; - // Get the original terminal modes so we can disable raw mode on exit. - let mut termios = MaybeUninit::::uninit(); - check_int_return(libc::tcgetattr(STATE.stdout, termios.as_mut_ptr()))?; - let mut termios = termios.assume_init(); - STATE.stdout_initial_termios = Some(termios); + // Set STATE.inject_resize to true whenever we get a SIGWINCH. + let mut sigwinch_action: libc::sigaction = mem::zeroed(); + sigwinch_action.sa_sigaction = sigwinch_handler as libc::sighandler_t; + check_int_return(libc::sigaction(libc::SIGWINCH, &sigwinch_action, null_mut()))?; - termios.c_iflag &= !( - // When neither IGNBRK... - libc::IGNBRK + // Get the original terminal modes so we can disable raw mode on exit. + let mut termios = MaybeUninit::::uninit(); + check_int_return(libc::tcgetattr(STATE.stdout, termios.as_mut_ptr()))?; + let mut termios = termios.assume_init(); + STATE.stdout_initial_termios = Some(termios); + + termios.c_iflag &= !( + // When neither IGNBRK... + libc::IGNBRK // ...nor BRKINT are set, a BREAK reads as a null byte ('\0'), ... | libc::BRKINT // ...except when PARMRK is set, in which case it reads as the sequence \377 \0 \0. @@ -104,20 +107,20 @@ pub fn switch_modes() -> apperr::Result<()> { | libc::ICRNL // Disable software flow control. | libc::IXON - ); - // Disable output processing. - termios.c_oflag &= !libc::OPOST; - termios.c_cflag &= !( - // Reset character size mask. - libc::CSIZE + ); + // Disable output processing. + termios.c_oflag &= !libc::OPOST; + termios.c_cflag &= !( + // Reset character size mask. + libc::CSIZE // Disable parity generation. | libc::PARENB - ); - // Set character size back to 8 bits. - termios.c_cflag |= libc::CS8; - termios.c_lflag &= !( - // Disable signal generation (SIGINT, SIGTSTP, SIGQUIT). - libc::ISIG + ); + // Set character size back to 8 bits. + termios.c_cflag |= libc::CS8; + termios.c_lflag &= !( + // Disable signal generation (SIGINT, SIGTSTP, SIGQUIT). + libc::ISIG // Disable canonical mode (line buffering). | libc::ICANON // Disable echoing of input characters. @@ -126,211 +129,355 @@ pub fn switch_modes() -> apperr::Result<()> { | libc::ECHONL // Disable extended input processing (e.g. Ctrl-V). | libc::IEXTEN - ); + ); - // Set the terminal to raw mode. - termios.c_lflag &= !(libc::ICANON | libc::ECHO); - check_int_return(libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios))?; + // Set the terminal to raw mode. + termios.c_lflag &= !(libc::ICANON | libc::ECHO); + check_int_return(libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios))?; - Ok(()) + Ok(()) + } } -} -pub struct Deinit; + fn inject_window_size_into_stdin() { + unsafe { + STATE.inject_resize = true; + } + } -impl Drop for Deinit { - fn drop(&mut self) { + fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option> { unsafe { - #[allow(static_mut_refs)] - if let Some(termios) = STATE.stdout_initial_termios.take() { - // Restore the original terminal modes. - libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios); + if STATE.inject_resize { + timeout = time::Duration::ZERO; + } + + let read_poll = timeout != time::Duration::MAX; + let mut buf = Vec::new_in(arena); + + // We don't know if the input is valid UTF8, so we first use a Vec and then + // later turn it into UTF8 using `from_utf8_lossy_owned`. + // It is important that we allocate the buffer with an explicit capacity, + // because we later use `spare_capacity_mut` to access it. + buf.reserve(4 * KIBI); + + // We got some leftover broken UTF8 from a previous read? Prepend it. + if STATE.utf8_len != 0 { + buf.extend_from_slice(&STATE.utf8_buf[..STATE.utf8_len]); + STATE.utf8_len = 0; } + + loop { + if timeout != time::Duration::MAX { + let beg = time::Instant::now(); + + let mut pollfd = + libc::pollfd { fd: STATE.stdin, events: libc::POLLIN, revents: 0 }; + let ret; + #[cfg(target_os = "linux")] + { + let ts = libc::timespec { + tv_sec: timeout.as_secs() as libc::time_t, + tv_nsec: timeout.subsec_nanos() as libc::c_long, + }; + ret = libc::ppoll(&mut pollfd, 1, &ts, ptr::null()); + } + #[cfg(not(target_os = "linux"))] + { + ret = libc::poll(&mut pollfd, 1, timeout.as_millis() as libc::c_int); + } + if ret < 0 { + return None; // Error? Let's assume it's an EOF. + } + if ret == 0 { + break; // Timeout? We can stop reading. + } + + timeout = timeout.saturating_sub(beg.elapsed()); + }; + + // If we're asked for a non-blocking read we need + // to manipulate `O_NONBLOCK` and vice versa. + set_tty_nonblocking(read_poll); + + // Read from stdin. + let spare = buf.spare_capacity_mut(); + let ret = libc::read(STATE.stdin, spare.as_mut_ptr() as *mut _, spare.len()); + if ret > 0 { + buf.set_len(buf.len() + ret as usize); + break; + } + if ret == 0 { + return None; // EOF + } + if ret < 0 { + match errno() { + libc::EINTR if STATE.inject_resize => break, + libc::EAGAIN if timeout == time::Duration::ZERO => break, + libc::EINTR | libc::EAGAIN => {} + _ => return None, + } + } + } + + if !buf.is_empty() { + // We only need to check the last 3 bytes for UTF-8 continuation bytes, + // because we should be able to assume that any 4 byte sequence is complete. + let lim = buf.len().saturating_sub(3); + let mut off = buf.len() - 1; + + // Find the start of the last potentially incomplete UTF-8 sequence. + while off > lim && buf[off] & 0b1100_0000 == 0b1000_0000 { + off -= 1; + } + + let seq_len = match buf[off] { + b if b & 0b1000_0000 == 0 => 1, + b if b & 0b1110_0000 == 0b1100_0000 => 2, + b if b & 0b1111_0000 == 0b1110_0000 => 3, + b if b & 0b1111_1000 == 0b1111_0000 => 4, + // If the lead byte we found isn't actually one, we don't cache it. + // `from_utf8_lossy_owned` will replace it with U+FFFD. + _ => 0, + }; + + // Cache incomplete sequence if any. + if off + seq_len > buf.len() { + STATE.utf8_len = buf.len() - off; + STATE.utf8_buf[..STATE.utf8_len].copy_from_slice(&buf[off..]); + buf.truncate(off); + } + } + + let mut result = ArenaString::from_utf8_lossy_owned(buf); + + // We received a SIGWINCH? Add a fake window size sequence for our input parser. + // I prepend it so that on startup, the TUI system gets first initialized with a size. + if STATE.inject_resize { + STATE.inject_resize = false; + let (w, h) = get_window_size(); + if w > 0 && h > 0 { + let scratch = scratch_arena(Some(arena)); + let seq = arena_format!(&scratch, "\x1b[8;{h};{w}t"); + result.replace_range(0..0, &seq); + } + } + + result.shrink_to_fit(); + Some(result) } } -} -pub fn inject_window_size_into_stdin() { - unsafe { - STATE.inject_resize = true; - } -} + fn write_stdout(text: &str) { + if text.is_empty() { + return; + } -fn get_window_size() -> (u16, u16) { - let mut winsz: libc::winsize = unsafe { mem::zeroed() }; + // If we don't set the TTY to blocking mode, + // the write will potentially fail with EAGAIN. + set_tty_nonblocking(false); - for attempt in 1.. { - let ret = unsafe { libc::ioctl(STATE.stdout, libc::TIOCGWINSZ, &raw mut winsz) }; - if ret == -1 || (winsz.ws_col != 0 && winsz.ws_row != 0) { - break; + let buf = text.as_bytes(); + let mut written = 0; + + while written < buf.len() { + let w = &buf[written..]; + let w = &buf[..w.len().min(GIBI)]; + let n = unsafe { libc::write(STATE.stdout, w.as_ptr() as *const _, w.len()) }; + + if n >= 0 { + written += n as usize; + continue; + } + + let err = errno(); + if err != libc::EINTR { + return; + } } + } - if attempt == 10 { - winsz.ws_col = 80; - winsz.ws_row = 24; - break; + fn open_stdin_if_redirected() -> Option { + unsafe { + // Did we reopen stdin during `init()`? + if STATE.stdin != libc::STDIN_FILENO { + Some(File::from_raw_fd(libc::STDIN_FILENO)) + } else { + None + } } + } - // Some terminals are bad emulators and don't report TIOCGWINSZ immediately. - thread::sleep(time::Duration::from_millis(10 * attempt)); + fn file_id(file: Option<&File>, path: &Path) -> apperr::Result { + let file = match file { + Some(f) => f, + None => &File::open(path)?, + }; + + unsafe { + let mut stat = MaybeUninit::::uninit(); + check_int_return(libc::fstat(file.as_raw_fd(), stat.as_mut_ptr()))?; + let stat = stat.assume_init(); + Ok(FileId { st_dev: stat.st_dev, st_ino: stat.st_ino }) + } } - (winsz.ws_col, winsz.ws_row) -} + unsafe fn virtual_reserve(size: usize) -> apperr::Result> { + unsafe { + let ptr = libc::mmap( + null_mut(), + size, + desired_mprotect(libc::PROT_READ | libc::PROT_WRITE), + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ); + if ptr.is_null() || ptr::eq(ptr, libc::MAP_FAILED) { + Err(errno_to_apperr(libc::ENOMEM)) + } else { + Ok(NonNull::new_unchecked(ptr as *mut u8)) + } + } + } -/// Reads from stdin. -/// -/// Returns `None` if there was an error reading from stdin. -/// Returns `Some("")` if the given timeout was reached. -/// Otherwise, it returns the read, non-empty string. -pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option> { - unsafe { - if STATE.inject_resize { - timeout = time::Duration::ZERO; + unsafe fn virtual_commit(base: NonNull, size: usize) -> apperr::Result<()> { + unsafe { + let status = + libc::mprotect(base.cast().as_ptr(), size, libc::PROT_READ | libc::PROT_WRITE); + if status != 0 { Err(errno_to_apperr(libc::ENOMEM)) } else { Ok(()) } } + } - let read_poll = timeout != time::Duration::MAX; - let mut buf = Vec::new_in(arena); + unsafe fn virtual_release(base: NonNull, size: usize) { + unsafe { + libc::munmap(base.cast().as_ptr(), size); + } + } - // We don't know if the input is valid UTF8, so we first use a Vec and then - // later turn it into UTF8 using `from_utf8_lossy_owned`. - // It is important that we allocate the buffer with an explicit capacity, - // because we later use `spare_capacity_mut` to access it. - buf.reserve(4 * KIBI); + type LibraryName = *const c_char; + unsafe fn load_library(name: Self::LibraryName) -> apperr::Result> { + unsafe { + NonNull::new(libc::dlopen(name, libc::RTLD_LAZY)) + .ok_or_else(|| errno_to_apperr(libc::ENOENT)) + } + } - // We got some leftover broken UTF8 from a previous read? Prepend it. - if STATE.utf8_len != 0 { - buf.extend_from_slice(&STATE.utf8_buf[..STATE.utf8_len]); - STATE.utf8_len = 0; + unsafe fn get_proc_address( + handle: NonNull, + name: *const c_char, + ) -> apperr::Result { + unsafe { + let sym = libc::dlsym(handle.as_ptr(), name); + if sym.is_null() { + Err(errno_to_apperr(libc::ENOENT)) + } else { + Ok(mem::transmute_copy(&sym)) + } } + } - loop { - if timeout != time::Duration::MAX { - let beg = time::Instant::now(); - - let mut pollfd = libc::pollfd { fd: STATE.stdin, events: libc::POLLIN, revents: 0 }; - let ret; - #[cfg(target_os = "linux")] - { - let ts = libc::timespec { - tv_sec: timeout.as_secs() as libc::time_t, - tv_nsec: timeout.subsec_nanos() as libc::c_long, - }; - ret = libc::ppoll(&mut pollfd, 1, &ts, ptr::null()); - } - #[cfg(not(target_os = "linux"))] - { - ret = libc::poll(&mut pollfd, 1, timeout.as_millis() as libc::c_int); - } - if ret < 0 { - return None; // Error? Let's assume it's an EOF. + fn load_icu() -> apperr::Result { + const fn const_str_eq(a: &str, b: &str) -> bool { + let a = a.as_bytes(); + let b = b.as_bytes(); + let mut i = 0; + + loop { + if i >= a.len() || i >= b.len() { + return a.len() == b.len(); } - if ret == 0 { - break; // Timeout? We can stop reading. + if a[i] != b[i] { + return false; } + i += 1; + } + } - timeout = timeout.saturating_sub(beg.elapsed()); - }; + const LIBICUUC: &str = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0"); + const LIBICUI18N: &str = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0"); - // If we're asked for a non-blocking read we need - // to manipulate `O_NONBLOCK` and vice versa. - set_tty_nonblocking(read_poll); + if const { const_str_eq(LIBICUUC, LIBICUI18N) } { + let icu = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; + Ok(LibIcu { libicuuc: icu, libicui18n: icu }) + } else { + let libicuuc = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; + let libicui18n = unsafe { load_library(LIBICUI18N.as_ptr() as *const _)? }; + Ok(LibIcu { libicuuc, libicui18n }) + } + } - // Read from stdin. - let spare = buf.spare_capacity_mut(); - let ret = libc::read(STATE.stdin, spare.as_mut_ptr() as *mut _, spare.len()); - if ret > 0 { - buf.set_len(buf.len() + ret as usize); + fn preferred_languages(arena: &Arena) -> Vec, &Arena> { + let mut locales = Vec::new_in(arena); + + for key in ["LANGUAGE", "LC_ALL", "LANG"] { + if let Ok(val) = std::env::var(key) + && !val.is_empty() + { + locales.extend(val.split(':').filter(|s| !s.is_empty()).map(|s| { + // Replace all underscores with dashes, + // because the localization code expects pt-br, not pt_BR. + let mut res = Vec::new_in(arena); + res.extend(s.as_bytes().iter().map(|&b| if b == b'_' { b'-' } else { b })); + unsafe { ArenaString::from_utf8_unchecked(res) } + })); break; } - if ret == 0 { - return None; // EOF - } - if ret < 0 { - match errno() { - libc::EINTR if STATE.inject_resize => break, - libc::EAGAIN if timeout == time::Duration::ZERO => break, - libc::EINTR | libc::EAGAIN => {} - _ => return None, - } - } } - if !buf.is_empty() { - // We only need to check the last 3 bytes for UTF-8 continuation bytes, - // because we should be able to assume that any 4 byte sequence is complete. - let lim = buf.len().saturating_sub(3); - let mut off = buf.len() - 1; + locales + } - // Find the start of the last potentially incomplete UTF-8 sequence. - while off > lim && buf[off] & 0b1100_0000 == 0b1000_0000 { - off -= 1; - } + fn apperr_format(f: &mut fmt::Formatter<'_>, code: u32) -> fmt::Result { + write!(f, "Error {code}")?; - let seq_len = match buf[off] { - b if b & 0b1000_0000 == 0 => 1, - b if b & 0b1110_0000 == 0b1100_0000 => 2, - b if b & 0b1111_0000 == 0b1110_0000 => 3, - b if b & 0b1111_1000 == 0b1111_0000 => 4, - // If the lead byte we found isn't actually one, we don't cache it. - // `from_utf8_lossy_owned` will replace it with U+FFFD. - _ => 0, - }; - - // Cache incomplete sequence if any. - if off + seq_len > buf.len() { - STATE.utf8_len = buf.len() - off; - STATE.utf8_buf[..STATE.utf8_len].copy_from_slice(&buf[off..]); - buf.truncate(off); + unsafe { + let ptr = libc::strerror(code as i32); + if !ptr.is_null() { + let msg = CStr::from_ptr(ptr).to_string_lossy(); + write!(f, ": {msg}")?; } } - let mut result = ArenaString::from_utf8_lossy_owned(buf); - - // We received a SIGWINCH? Add a fake window size sequence for our input parser. - // I prepend it so that on startup, the TUI system gets first initialized with a size. - if STATE.inject_resize { - STATE.inject_resize = false; - let (w, h) = get_window_size(); - if w > 0 && h > 0 { - let scratch = scratch_arena(Some(arena)); - let seq = arena_format!(&scratch, "\x1b[8;{h};{w}t"); - result.replace_range(0..0, &seq); - } - } + Ok(()) + } - result.shrink_to_fit(); - Some(result) + fn apperr_is_not_found(err: apperr::Error) -> bool { + err == errno_to_apperr(libc::ENOENT) } } -pub fn write_stdout(text: &str) { - if text.is_empty() { - return; - } +pub struct Deinit; - // If we don't set the TTY to blocking mode, - // the write will potentially fail with EAGAIN. - set_tty_nonblocking(false); +impl Drop for Deinit { + fn drop(&mut self) { + unsafe { + #[allow(static_mut_refs)] + if let Some(termios) = STATE.stdout_initial_termios.take() { + // Restore the original terminal modes. + libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios); + } + } + } +} - let buf = text.as_bytes(); - let mut written = 0; +fn check_int_return(ret: libc::c_int) -> apperr::Result { + if ret < 0 { Err(errno_to_apperr(errno())) } else { Ok(ret) } +} - while written < buf.len() { - let w = &buf[written..]; - let w = &buf[..w.len().min(GIBI)]; - let n = unsafe { libc::write(STATE.stdout, w.as_ptr() as *const _, w.len()) }; +const fn errno_to_apperr(no: c_int) -> apperr::Error { + apperr::Error::new_sys(if no < 0 { 0 } else { no as u32 }) +} - if n >= 0 { - written += n as usize; - continue; - } +#[inline] +fn errno() -> i32 { + // Under `-O -Copt-level=s` the 1.87 compiler fails to fully inline and + // remove the raw_os_error() call. This leaves us with the drop() call. + // ManuallyDrop fixes that and results in a direct `std::sys::os::errno` call. + ManuallyDrop::new(std::io::Error::last_os_error()).raw_os_error().unwrap_or(0) +} - let err = errno(); - if err != libc::EINTR { - return; - } - } +#[inline] +pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error { + errno_to_apperr(err.raw_os_error().unwrap_or(0)) } /// Sets/Resets `O_NONBLOCK` on the TTY handle. @@ -347,15 +494,26 @@ fn set_tty_nonblocking(nonblock: bool) { } } -pub fn open_stdin_if_redirected() -> Option { - unsafe { - // Did we reopen stdin during `init()`? - if STATE.stdin != libc::STDIN_FILENO { - Some(File::from_raw_fd(libc::STDIN_FILENO)) - } else { - None +fn get_window_size() -> (u16, u16) { + let mut winsz: libc::winsize = unsafe { mem::zeroed() }; + + for attempt in 1.. { + let ret = unsafe { libc::ioctl(STATE.stdout, libc::TIOCGWINSZ, &raw mut winsz) }; + if ret == -1 || (winsz.ws_col != 0 && winsz.ws_row != 0) { + break; } + + if attempt == 10 { + winsz.ws_col = 80; + winsz.ws_row = 24; + break; + } + + // Some terminals are bad emulators and don't report TIOCGWINSZ immediately. + thread::sleep(time::Duration::from_millis(10 * attempt)); } + + (winsz.ws_col, winsz.ws_row) } #[derive(Clone, PartialEq, Eq)] @@ -364,71 +522,9 @@ pub struct FileId { st_ino: libc::ino_t, } -/// Returns a unique identifier for the given file by handle or path. -pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result { - let file = match file { - Some(f) => f, - None => &File::open(path)?, - }; - - unsafe { - let mut stat = MaybeUninit::::uninit(); - check_int_return(libc::fstat(file.as_raw_fd(), stat.as_mut_ptr()))?; - let stat = stat.assume_init(); - Ok(FileId { st_dev: stat.st_dev, st_ino: stat.st_ino }) - } -} - -/// Reserves a virtual memory region of the given size. -/// To commit the memory, use `virtual_commit`. -/// To release the memory, use `virtual_release`. -/// -/// # Safety -/// -/// This function is unsafe because it uses raw pointers. -/// Don't forget to release the memory when you're done with it or you'll leak it. -pub unsafe fn virtual_reserve(size: usize) -> apperr::Result> { - unsafe { - let ptr = libc::mmap( - null_mut(), - size, - desired_mprotect(libc::PROT_READ | libc::PROT_WRITE), - libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, - -1, - 0, - ); - if ptr.is_null() || ptr::eq(ptr, libc::MAP_FAILED) { - Err(errno_to_apperr(libc::ENOMEM)) - } else { - Ok(NonNull::new_unchecked(ptr as *mut u8)) - } - } -} - -/// Releases a virtual memory region of the given size. -/// -/// # Safety -/// -/// This function is unsafe because it uses raw pointers. -/// Make sure to only pass pointers acquired from `virtual_reserve`. -pub unsafe fn virtual_release(base: NonNull, size: usize) { - unsafe { - libc::munmap(base.cast().as_ptr(), size); - } -} - -/// Commits a virtual memory region of the given size. -/// -/// # Safety -/// -/// This function is unsafe because it uses raw pointers. -/// Make sure to only pass pointers acquired from `virtual_reserve` -/// and to pass a size less than or equal to the size passed to `virtual_reserve`. -pub unsafe fn virtual_commit(base: NonNull, size: usize) -> apperr::Result<()> { - unsafe { - let status = libc::mprotect(base.cast().as_ptr(), size, libc::PROT_READ | libc::PROT_WRITE); - if status != 0 { Err(errno_to_apperr(libc::ENOMEM)) } else { Ok(()) } - } +pub struct LibIcu { + pub libicuuc: NonNull, + pub libicui18n: NonNull, } unsafe fn load_library(name: *const c_char) -> apperr::Result> { @@ -438,62 +534,6 @@ unsafe fn load_library(name: *const c_char) -> apperr::Result> { } } -/// Loads a function from a dynamic library. -/// -/// # Safety -/// -/// This function is highly unsafe as it requires you to know the exact type -/// of the function you're loading. No type checks whatsoever are performed. -// -// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. -pub unsafe fn get_proc_address( - handle: NonNull, - name: *const c_char, -) -> apperr::Result { - unsafe { - let sym = libc::dlsym(handle.as_ptr(), name); - if sym.is_null() { - Err(errno_to_apperr(libc::ENOENT)) - } else { - Ok(mem::transmute_copy(&sym)) - } - } -} - -pub struct LibIcu { - pub libicuuc: NonNull, - pub libicui18n: NonNull, -} - -pub fn load_icu() -> apperr::Result { - const fn const_str_eq(a: &str, b: &str) -> bool { - let a = a.as_bytes(); - let b = b.as_bytes(); - let mut i = 0; - - loop { - if i >= a.len() || i >= b.len() { - return a.len() == b.len(); - } - if a[i] != b[i] { - return false; - } - i += 1; - } - } - - const LIBICUUC: &str = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0"); - const LIBICUI18N: &str = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0"); - - if const { const_str_eq(LIBICUUC, LIBICUI18N) } { - let icu = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; - Ok(LibIcu { libicuuc: icu, libicui18n: icu }) - } else { - let libicuuc = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? }; - let libicui18n = unsafe { load_library(LIBICUI18N.as_ptr() as *const _)? }; - Ok(LibIcu { libicuuc, libicui18n }) - } -} /// ICU, by default, adds the major version as a suffix to each exported symbol. /// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`), /// but I found that many (most?) Linux distributions don't do this for some reason. @@ -582,63 +622,3 @@ where res.as_ptr() as *const c_char } } - -pub fn preferred_languages(arena: &Arena) -> Vec, &Arena> { - let mut locales = Vec::new_in(arena); - - for key in ["LANGUAGE", "LC_ALL", "LANG"] { - if let Ok(val) = std::env::var(key) - && !val.is_empty() - { - locales.extend(val.split(':').filter(|s| !s.is_empty()).map(|s| { - // Replace all underscores with dashes, - // because the localization code expects pt-br, not pt_BR. - let mut res = Vec::new_in(arena); - res.extend(s.as_bytes().iter().map(|&b| if b == b'_' { b'-' } else { b })); - unsafe { ArenaString::from_utf8_unchecked(res) } - })); - break; - } - } - - locales -} - -#[inline] -fn errno() -> i32 { - // Under `-O -Copt-level=s` the 1.87 compiler fails to fully inline and - // remove the raw_os_error() call. This leaves us with the drop() call. - // ManuallyDrop fixes that and results in a direct `std::sys::os::errno` call. - ManuallyDrop::new(std::io::Error::last_os_error()).raw_os_error().unwrap_or(0) -} - -#[inline] -pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error { - errno_to_apperr(err.raw_os_error().unwrap_or(0)) -} - -pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result { - write!(f, "Error {code}")?; - - unsafe { - let ptr = libc::strerror(code as i32); - if !ptr.is_null() { - let msg = CStr::from_ptr(ptr).to_string_lossy(); - write!(f, ": {msg}")?; - } - } - - Ok(()) -} - -pub fn apperr_is_not_found(err: apperr::Error) -> bool { - err == errno_to_apperr(libc::ENOENT) -} - -const fn errno_to_apperr(no: c_int) -> apperr::Error { - apperr::Error::new_sys(if no < 0 { 0 } else { no as u32 }) -} - -fn check_int_return(ret: libc::c_int) -> apperr::Result { - if ret < 0 { Err(errno_to_apperr(errno())) } else { Ok(ret) } -} diff --git a/src/sys/windows.rs b/src/sys/windows.rs index 48b44942d2c9..6fd46f22d8d8 100644 --- a/src/sys/windows.rs +++ b/src/sys/windows.rs @@ -8,7 +8,7 @@ use std::mem::MaybeUninit; use std::os::windows::io::{AsRawHandle as _, FromRawHandle}; use std::path::{Path, PathBuf}; use std::ptr::{self, NonNull, null, null_mut}; -use std::{mem, time}; +use std::{fmt, mem, time}; use windows_sys::Win32::Storage::FileSystem; use windows_sys::Win32::System::Diagnostics::Debug; @@ -16,6 +16,7 @@ use windows_sys::Win32::System::{Console, IO, LibraryLoader, Memory, Threading}; use windows_sys::Win32::{Foundation, Globalization}; use windows_sys::w; +use super::Syscall; use crate::apperr; use crate::arena::{Arena, ArenaString, scratch_arena}; use crate::helpers::*; @@ -105,322 +106,496 @@ extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL { 1 } -/// Initializes the platform-specific state. -pub fn init() -> Deinit { - unsafe { - // Get the stdin and stdout handles first, so that if this function fails, - // we at least got something to use for `write_stdout`. - STATE.stdin = Console::GetStdHandle(Console::STD_INPUT_HANDLE); - STATE.stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE); +pub struct WindowsSys; + +impl Syscall for WindowsSys { + fn init() -> Deinit { + unsafe { + // Get the stdin and stdout handles first, so that if this function fails, + // we at least got something to use for `write_stdout`. + STATE.stdin = Console::GetStdHandle(Console::STD_INPUT_HANDLE); + STATE.stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE); - Deinit + Deinit + } } -} -/// Switches the terminal into raw mode, etc. -pub fn switch_modes() -> apperr::Result<()> { - unsafe { - // `kernel32.dll` doesn't exist on OneCore variants of Windows. - // NOTE: `kernelbase.dll` is NOT a stable API to rely on. In our case it's the best option though. - // - // This is written as two nested `match` statements so that we can return the error from the first - // `load_read_func` call if it fails. The kernel32.dll lookup may contain some valid information, - // while the kernelbase.dll lookup may not, since it's not a stable API. - unsafe fn load_read_func(module: *const u16) -> apperr::Result { - unsafe { + fn switch_modes() -> apperr::Result<()> { + unsafe { + // `kernel32.dll` doesn't exist on OneCore variants of Windows. + // NOTE: `kernelbase.dll` is NOT a stable API to rely on. In our case it's the best option though. + // + // This is written as two nested `match` statements so that we can return the error from the first + // `load_read_func` call if it fails. The kernel32.dll lookup may contain some valid information, + // while the kernelbase.dll lookup may not, since it's not a stable API. + let load_read_func = |module: *const u16| -> apperr::Result { get_module(module) - .and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr())) - } - } - STATE.read_console_input_ex = match load_read_func(w!("kernel32.dll")) { - Ok(func) => func, - Err(err) => match load_read_func(w!("kernelbase.dll")) { + .and_then(|m| Self::get_proc_address(m, c"ReadConsoleInputExW".as_ptr())) + }; + + STATE.read_console_input_ex = match load_read_func(w!("kernel32.dll")) { Ok(func) => func, - Err(_) => return Err(err), - }, - }; + Err(err) => match load_read_func(w!("kernelbase.dll")) { + Ok(func) => func, + Err(_) => return Err(err), + }, + }; + + // Reopen stdin if it's redirected (= piped input). + if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE) + || !matches!(FileSystem::GetFileType(STATE.stdin), FileSystem::FILE_TYPE_CHAR) + { + STATE.stdin = FileSystem::CreateFileW( + w!("CONIN$"), + Foundation::GENERIC_READ | Foundation::GENERIC_WRITE, + FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_WRITE, + null_mut(), + FileSystem::OPEN_EXISTING, + 0, + null_mut(), + ); + } + if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE) + || ptr::eq(STATE.stdout, Foundation::INVALID_HANDLE_VALUE) + { + return Err(get_last_error()); + } - // Reopen stdin if it's redirected (= piped input). - if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE) - || !matches!(FileSystem::GetFileType(STATE.stdin), FileSystem::FILE_TYPE_CHAR) - { - STATE.stdin = FileSystem::CreateFileW( - w!("CONIN$"), - Foundation::GENERIC_READ | Foundation::GENERIC_WRITE, - FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_WRITE, - null_mut(), - FileSystem::OPEN_EXISTING, - 0, - null_mut(), - ); + check_bool_return(Console::SetConsoleCtrlHandler(Some(console_ctrl_handler), 1))?; + + STATE.stdin_cp_old = Console::GetConsoleCP(); + STATE.stdout_cp_old = Console::GetConsoleOutputCP(); + check_bool_return(Console::GetConsoleMode(STATE.stdin, &raw mut STATE.stdin_mode_old))?; + check_bool_return(Console::GetConsoleMode( + STATE.stdout, + &raw mut STATE.stdout_mode_old, + ))?; + + check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?; + check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?; + check_bool_return(Console::SetConsoleMode( + STATE.stdin, + Console::ENABLE_WINDOW_INPUT + | Console::ENABLE_EXTENDED_FLAGS + | Console::ENABLE_VIRTUAL_TERMINAL_INPUT, + ))?; + check_bool_return(Console::SetConsoleMode( + STATE.stdout, + Console::ENABLE_PROCESSED_OUTPUT + | Console::ENABLE_WRAP_AT_EOL_OUTPUT + | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING + | Console::DISABLE_NEWLINE_AUTO_RETURN, + ))?; + + Ok(()) } - if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE) - || ptr::eq(STATE.stdout, Foundation::INVALID_HANDLE_VALUE) - { - return Err(get_last_error()); + } + + fn inject_window_size_into_stdin() { + unsafe { + STATE.inject_resize = true; } + } - check_bool_return(Console::SetConsoleCtrlHandler(Some(console_ctrl_handler), 1))?; + fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option> { + let scratch = scratch_arena(Some(arena)); - STATE.stdin_cp_old = Console::GetConsoleCP(); - STATE.stdout_cp_old = Console::GetConsoleOutputCP(); - check_bool_return(Console::GetConsoleMode(STATE.stdin, &raw mut STATE.stdin_mode_old))?; - check_bool_return(Console::GetConsoleMode(STATE.stdout, &raw mut STATE.stdout_mode_old))?; + // On startup we're asked to inject a window size so that the UI system can layout the elements. + // --> Inject a fake sequence for our input parser. + let mut resize_event = None; + if unsafe { STATE.inject_resize } { + unsafe { STATE.inject_resize = false }; + timeout = time::Duration::ZERO; + resize_event = get_console_size(); + } - check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?; - check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?; - check_bool_return(Console::SetConsoleMode( - STATE.stdin, - Console::ENABLE_WINDOW_INPUT - | Console::ENABLE_EXTENDED_FLAGS - | Console::ENABLE_VIRTUAL_TERMINAL_INPUT, - ))?; - check_bool_return(Console::SetConsoleMode( - STATE.stdout, - Console::ENABLE_PROCESSED_OUTPUT - | Console::ENABLE_WRAP_AT_EOL_OUTPUT - | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING - | Console::DISABLE_NEWLINE_AUTO_RETURN, - ))?; + let read_poll = timeout != time::Duration::MAX; // there is a timeout -> don't block in read() + let input_buf = scratch.alloc_uninit_slice(4 * KIBI); + let mut input_buf_cap = input_buf.len(); + let utf16_buf = scratch.alloc_uninit_slice(4 * KIBI); + let mut utf16_buf_len = 0; + + // If there was a leftover leading surrogate from the last read, we prepend it to the buffer. + if unsafe { STATE.leading_surrogate } != 0 { + utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate }); + utf16_buf_len = 1; + input_buf_cap -= 1; + unsafe { STATE.leading_surrogate = 0 }; + } - Ok(()) - } -} + // Read until there's either a timeout or we have something to process. + loop { + if timeout != time::Duration::MAX { + let beg = time::Instant::now(); + + match unsafe { + Threading::WaitForSingleObject(STATE.stdin, timeout.as_millis() as u32) + } { + // Ready to read? Continue with reading below. + Foundation::WAIT_OBJECT_0 => {} + // Timeout? Skip reading entirely. + Foundation::WAIT_TIMEOUT => break, + // Error? Tell the caller stdin is broken. + _ => return None, + } -pub struct Deinit; + timeout = timeout.saturating_sub(beg.elapsed()); + } -impl Drop for Deinit { - fn drop(&mut self) { - unsafe { - if STATE.stdin_cp_old != 0 { - Console::SetConsoleCP(STATE.stdin_cp_old); - STATE.stdin_cp_old = 0; + // Read from stdin. + let input = unsafe { + // If we had a `inject_resize`, we don't want to block indefinitely for other pending input on startup, + // but are still interested in any other pending input that may be waiting for us. + let flags = if read_poll { CONSOLE_READ_NOWAIT } else { 0 }; + let mut read = 0; + let ok = (STATE.read_console_input_ex)( + STATE.stdin, + input_buf[0].as_mut_ptr(), + input_buf_cap as u32, + &mut read, + flags, + ); + if ok == 0 || STATE.wants_exit { + return None; + } + input_buf[..read as usize].assume_init_ref() + }; + + // Convert Win32 input records into UTF16. + for inp in input { + match inp.EventType as u32 { + Console::KEY_EVENT => { + let event = unsafe { &inp.Event.KeyEvent }; + let ch = unsafe { event.uChar.UnicodeChar }; + if event.bKeyDown != 0 && ch != 0 { + utf16_buf[utf16_buf_len] = MaybeUninit::new(ch); + utf16_buf_len += 1; + } + } + Console::WINDOW_BUFFER_SIZE_EVENT => { + let event = unsafe { &inp.Event.WindowBufferSizeEvent }; + let w = event.dwSize.X as CoordType; + let h = event.dwSize.Y as CoordType; + // Windows is prone to sending broken/useless `WINDOW_BUFFER_SIZE_EVENT`s. + // E.g. starting conhost will emit 3 in a row. Skip rendering in that case. + if w > 0 && h > 0 { + resize_event = Some(Size { width: w, height: h }); + } + } + _ => {} + } } - if STATE.stdout_cp_old != 0 { - Console::SetConsoleOutputCP(STATE.stdout_cp_old); - STATE.stdout_cp_old = 0; + + if resize_event.is_some() || utf16_buf_len != 0 { + break; } - if STATE.stdin_mode_old != INVALID_CONSOLE_MODE { - Console::SetConsoleMode(STATE.stdin, STATE.stdin_mode_old); - STATE.stdin_mode_old = INVALID_CONSOLE_MODE; + } + + const RESIZE_EVENT_FMT_MAX_LEN: usize = 16; // "\x1b[8;65535;65535t" + let resize_event_len = if resize_event.is_some() { RESIZE_EVENT_FMT_MAX_LEN } else { 0 }; + // +1 to account for a potential `STATE.leading_surrogate`. + let utf8_max_len = (utf16_buf_len + 1) * 3; + let mut text = ArenaString::new_in(arena); + text.reserve(utf8_max_len + resize_event_len); + + // Now prepend our previously extracted resize event. + if let Some(resize_event) = resize_event { + // If I read xterm's documentation correctly, CSI 18 t reports the window size in characters. + // CSI 8 ; height ; width t is the response. Of course, we didn't send the request, + // but we can use this fake response to trigger the editor to resize itself. + _ = write!(text, "\x1b[8;{};{}t", resize_event.height, resize_event.width); + } + + // If the input ends with a lone lead surrogate, we need to remember it for the next read. + if utf16_buf_len > 0 { + unsafe { + let last_char = utf16_buf[utf16_buf_len - 1].assume_init(); + if (0xD800..0xDC00).contains(&last_char) { + STATE.leading_surrogate = last_char; + utf16_buf_len -= 1; + } } - if STATE.stdout_mode_old != INVALID_CONSOLE_MODE { - Console::SetConsoleMode(STATE.stdout, STATE.stdout_mode_old); - STATE.stdout_mode_old = INVALID_CONSOLE_MODE; + } + + // Convert the remaining input to UTF8, the sane encoding. + if utf16_buf_len > 0 { + unsafe { + let vec = text.as_mut_vec(); + let spare = vec.spare_capacity_mut(); + + let len = Globalization::WideCharToMultiByte( + Globalization::CP_UTF8, + 0, + utf16_buf[0].as_ptr(), + utf16_buf_len as i32, + spare.as_mut_ptr() as *mut _, + spare.len() as i32, + null(), + null_mut(), + ); + + if len > 0 { + vec.set_len(vec.len() + len as usize); + } } } - } -} -/// During startup we need to get the window size from the terminal. -/// Because I didn't want to type a bunch of code, this function tells -/// [`read_stdin`] to inject a fake sequence, which gets picked up by -/// the input parser and provided to the TUI code. -pub fn inject_window_size_into_stdin() { - unsafe { - STATE.inject_resize = true; + text.shrink_to_fit(); + Some(text) } -} -fn get_console_size() -> Option { - unsafe { - let mut info: Console::CONSOLE_SCREEN_BUFFER_INFOEX = mem::zeroed(); - info.cbSize = mem::size_of::() as u32; - if Console::GetConsoleScreenBufferInfoEx(STATE.stdout, &mut info) == 0 { - return None; + fn write_stdout(text: &str) { + unsafe { + let mut offset = 0; + + while offset < text.len() { + let ptr = text.as_ptr().add(offset); + let write = (text.len() - offset).min(GIBI) as u32; + let mut written = 0; + let ok = FileSystem::WriteFile(STATE.stdout, ptr, write, &mut written, null_mut()); + offset += written as usize; + if ok == 0 || written == 0 { + break; + } + } } - - let w = (info.srWindow.Right - info.srWindow.Left + 1).max(1) as CoordType; - let h = (info.srWindow.Bottom - info.srWindow.Top + 1).max(1) as CoordType; - Some(Size { width: w, height: h }) } -} -/// Reads from stdin. -/// -/// # Returns -/// -/// * `None` if there was an error reading from stdin. -/// * `Some("")` if the given timeout was reached. -/// * Otherwise, it returns the read, non-empty string. -pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option> { - let scratch = scratch_arena(Some(arena)); - - // On startup we're asked to inject a window size so that the UI system can layout the elements. - // --> Inject a fake sequence for our input parser. - let mut resize_event = None; - if unsafe { STATE.inject_resize } { - unsafe { STATE.inject_resize = false }; - timeout = time::Duration::ZERO; - resize_event = get_console_size(); + fn open_stdin_if_redirected() -> Option { + unsafe { + let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE); + // Did we reopen stdin during `init()`? + if !ptr::eq(STATE.stdin, handle) { Some(File::from_raw_handle(handle)) } else { None } + } } - let read_poll = timeout != time::Duration::MAX; // there is a timeout -> don't block in read() - let input_buf = scratch.alloc_uninit_slice(4 * KIBI); - let mut input_buf_cap = input_buf.len(); - let utf16_buf = scratch.alloc_uninit_slice(4 * KIBI); - let mut utf16_buf_len = 0; - - // If there was a leftover leading surrogate from the last read, we prepend it to the buffer. - if unsafe { STATE.leading_surrogate } != 0 { - utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate }); - utf16_buf_len = 1; - input_buf_cap -= 1; - unsafe { STATE.leading_surrogate = 0 }; + fn file_id(file: Option<&File>, path: &Path) -> apperr::Result { + let file = match file { + Some(f) => f, + None => &File::open(path)?, + }; + + file_id_from_handle(file).or_else(|_| Ok(FileId::Path(fs::canonicalize(path)?))) } - // Read until there's either a timeout or we have something to process. - loop { - if timeout != time::Duration::MAX { - let beg = time::Instant::now(); + unsafe fn virtual_reserve(size: usize) -> apperr::Result> { + unsafe { + #[allow(unused_assignments, unused_mut)] + let mut base = null_mut(); - match unsafe { Threading::WaitForSingleObject(STATE.stdin, timeout.as_millis() as u32) } + // In debug builds, we use fixed addresses to aid in debugging. + // Makes it possible to immediately tell which address space a pointer belongs to. + #[cfg(all(debug_assertions, not(target_pointer_width = "32")))] { - // Ready to read? Continue with reading below. - Foundation::WAIT_OBJECT_0 => {} - // Timeout? Skip reading entirely. - Foundation::WAIT_TIMEOUT => break, - // Error? Tell the caller stdin is broken. - _ => return None, + static mut S_BASE_GEN: usize = 0x0000100000000000; // 16 TiB + S_BASE_GEN += 0x0000001000000000; // 64 GiB + base = S_BASE_GEN as *mut _; } - timeout = timeout.saturating_sub(beg.elapsed()); + check_ptr_return(Memory::VirtualAlloc( + base, + size, + Memory::MEM_RESERVE, + Memory::PAGE_READWRITE, + ) as *mut u8) } + } - // Read from stdin. - let input = unsafe { - // If we had a `inject_resize`, we don't want to block indefinitely for other pending input on startup, - // but are still interested in any other pending input that may be waiting for us. - let flags = if read_poll { CONSOLE_READ_NOWAIT } else { 0 }; - let mut read = 0; - let ok = (STATE.read_console_input_ex)( - STATE.stdin, - input_buf[0].as_mut_ptr(), - input_buf_cap as u32, - &mut read, - flags, - ); - if ok == 0 || STATE.wants_exit { - return None; - } - input_buf[..read as usize].assume_init_ref() - }; + unsafe fn virtual_commit(base: NonNull, size: usize) -> apperr::Result<()> { + unsafe { + check_ptr_return(Memory::VirtualAlloc( + base.as_ptr() as *mut _, + size, + Memory::MEM_COMMIT, + Memory::PAGE_READWRITE, + )) + .map(|_| ()) + } + } + + unsafe fn virtual_release(base: NonNull, _size: usize) { + unsafe { + // NOTE: `VirtualFree` fails if the pointer isn't + // a valid base address or if the size isn't zero. + Memory::VirtualFree(base.as_ptr() as *mut _, 0, Memory::MEM_RELEASE); + } + } - // Convert Win32 input records into UTF16. - for inp in input { - match inp.EventType as u32 { - Console::KEY_EVENT => { - let event = unsafe { &inp.Event.KeyEvent }; - let ch = unsafe { event.uChar.UnicodeChar }; - if event.bKeyDown != 0 && ch != 0 { - utf16_buf[utf16_buf_len] = MaybeUninit::new(ch); - utf16_buf_len += 1; + type LibraryName = *const u16; + unsafe fn load_library(name: Self::LibraryName) -> apperr::Result> { + unsafe { + check_ptr_return(LibraryLoader::LoadLibraryExW( + name, + null_mut(), + LibraryLoader::LOAD_LIBRARY_SEARCH_SYSTEM32, + )) + } + } + + unsafe fn get_proc_address( + handle: NonNull, + name: *const c_char, + ) -> apperr::Result { + unsafe { + let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8); + if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) } + } + } + + fn load_icu() -> apperr::Result { + const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool { + unsafe { + let mut a = a; + let mut b = b; + loop { + if *a != *b { + return false; } - } - Console::WINDOW_BUFFER_SIZE_EVENT => { - let event = unsafe { &inp.Event.WindowBufferSizeEvent }; - let w = event.dwSize.X as CoordType; - let h = event.dwSize.Y as CoordType; - // Windows is prone to sending broken/useless `WINDOW_BUFFER_SIZE_EVENT`s. - // E.g. starting conhost will emit 3 in a row. Skip rendering in that case. - if w > 0 && h > 0 { - resize_event = Some(Size { width: w, height: h }); + if *a == 0 { + return true; } + a = a.add(1); + b = b.add(1); } - _ => {} } } - if resize_event.is_some() || utf16_buf_len != 0 { - break; + const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME"); + const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME"); + + if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } { + let icu = unsafe { Self::load_library(LIBICUUC)? }; + Ok(LibIcu { libicuuc: icu, libicui18n: icu }) + } else { + let libicuuc = unsafe { Self::load_library(LIBICUUC)? }; + let libicui18n = unsafe { Self::load_library(LIBICUI18N)? }; + Ok(LibIcu { libicuuc, libicui18n }) } } - const RESIZE_EVENT_FMT_MAX_LEN: usize = 16; // "\x1b[8;65535;65535t" - let resize_event_len = if resize_event.is_some() { RESIZE_EVENT_FMT_MAX_LEN } else { 0 }; - // +1 to account for a potential `STATE.leading_surrogate`. - let utf8_max_len = (utf16_buf_len + 1) * 3; - let mut text = ArenaString::new_in(arena); - text.reserve(utf8_max_len + resize_event_len); - - // Now prepend our previously extracted resize event. - if let Some(resize_event) = resize_event { - // If I read xterm's documentation correctly, CSI 18 t reports the window size in characters. - // CSI 8 ; height ; width t is the response. Of course, we didn't send the request, - // but we can use this fake response to trigger the editor to resize itself. - _ = write!(text, "\x1b[8;{};{}t", resize_event.height, resize_event.width); - } + fn preferred_languages(arena: &Arena) -> Vec, &Arena> { + // If the GetUserPreferredUILanguages() don't fit into 512 characters, + // honestly, just give up. How many languages do you realistically need? + const LEN: usize = 512; + + let scratch = scratch_arena(Some(arena)); + let mut res = Vec::new_in(arena); + + // Get the list of preferred languages via `GetUserPreferredUILanguages`. + let langs = unsafe { + let buf = scratch.alloc_uninit_slice(LEN); + let mut len = buf.len() as u32; + let mut num = 0; + + let ok = Globalization::GetUserPreferredUILanguages( + Globalization::MUI_LANGUAGE_NAME, + &mut num, + buf[0].as_mut_ptr(), + &mut len, + ); - // If the input ends with a lone lead surrogate, we need to remember it for the next read. - if utf16_buf_len > 0 { - unsafe { - let last_char = utf16_buf[utf16_buf_len - 1].assume_init(); - if (0xD800..0xDC00).contains(&last_char) { - STATE.leading_surrogate = last_char; - utf16_buf_len -= 1; + if ok == 0 || num == 0 { + len = 0; } - } + + // Drop the terminating double-null character. + len = len.saturating_sub(1); + + buf[..len as usize].assume_init_ref() + }; + + // Convert UTF16 to UTF8. + let langs = wide_to_utf8(&scratch, langs); + + // Split the null-delimited string into individual chunks + // and copy them into the given arena. + res.extend( + langs + .split_terminator('\0') + .filter(|s| !s.is_empty()) + .map(|s| ArenaString::from_str(arena, s)), + ); + res } - // Convert the remaining input to UTF8, the sane encoding. - if utf16_buf_len > 0 { + fn apperr_format(f: &mut fmt::Formatter<'_>, code: u32) -> fmt::Result { unsafe { - let vec = text.as_mut_vec(); - let spare = vec.spare_capacity_mut(); - - let len = Globalization::WideCharToMultiByte( - Globalization::CP_UTF8, - 0, - utf16_buf[0].as_ptr(), - utf16_buf_len as i32, - spare.as_mut_ptr() as *mut _, - spare.len() as i32, + let mut ptr: *mut u8 = null_mut(); + let len = Debug::FormatMessageA( + Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER + | Debug::FORMAT_MESSAGE_FROM_SYSTEM + | Debug::FORMAT_MESSAGE_IGNORE_INSERTS, null(), + code, + 0, + &mut ptr as *mut *mut _ as *mut _, + 0, null_mut(), ); + write!(f, "Error {code:#08x}")?; + if len > 0 { - vec.set_len(vec.len() + len as usize); + let msg = str_from_raw_parts(ptr, len as usize); + let msg = msg.trim_ascii(); + let msg = msg.replace(['\r', '\n'], " "); + write!(f, ": {msg}")?; + Foundation::LocalFree(ptr as *mut _); } + + Ok(()) } } - text.shrink_to_fit(); - Some(text) + fn apperr_is_not_found(err: apperr::Error) -> bool { + err == gle_to_apperr(Foundation::ERROR_FILE_NOT_FOUND) + } } -/// Writes a string to stdout. -/// -/// Use this instead of `print!` or `println!` to avoid -/// the overhead of Rust's stdio handling. Don't need that. -pub fn write_stdout(text: &str) { - unsafe { - let mut offset = 0; - - while offset < text.len() { - let ptr = text.as_ptr().add(offset); - let write = (text.len() - offset).min(GIBI) as u32; - let mut written = 0; - let ok = FileSystem::WriteFile(STATE.stdout, ptr, write, &mut written, null_mut()); - offset += written as usize; - if ok == 0 || written == 0 { - break; +pub struct Deinit; + +impl Drop for Deinit { + fn drop(&mut self) { + unsafe { + if STATE.stdin_cp_old != 0 { + Console::SetConsoleCP(STATE.stdin_cp_old); + STATE.stdin_cp_old = 0; + } + if STATE.stdout_cp_old != 0 { + Console::SetConsoleOutputCP(STATE.stdout_cp_old); + STATE.stdout_cp_old = 0; + } + if STATE.stdin_mode_old != INVALID_CONSOLE_MODE { + Console::SetConsoleMode(STATE.stdin, STATE.stdin_mode_old); + STATE.stdin_mode_old = INVALID_CONSOLE_MODE; + } + if STATE.stdout_mode_old != INVALID_CONSOLE_MODE { + Console::SetConsoleMode(STATE.stdout, STATE.stdout_mode_old); + STATE.stdout_mode_old = INVALID_CONSOLE_MODE; } } } } -/// Check if the stdin handle is redirected to a file, etc. -/// -/// # Returns -/// -/// * `Some(file)` if stdin is redirected. -/// * Otherwise, `None`. -pub fn open_stdin_if_redirected() -> Option { +fn check_bool_return(ret: Foundation::BOOL) -> apperr::Result<()> { + if ret == 0 { Err(get_last_error()) } else { Ok(()) } +} + +unsafe fn get_module(name: *const u16) -> apperr::Result> { + unsafe { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) } +} + +fn get_console_size() -> Option { unsafe { - let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE); - // Did we reopen stdin during `init()`? - if !std::ptr::eq(STATE.stdin, handle) { Some(File::from_raw_handle(handle)) } else { None } + let mut info: Console::CONSOLE_SCREEN_BUFFER_INFOEX = mem::zeroed(); + info.cbSize = mem::size_of::() as u32; + if Console::GetConsoleScreenBufferInfoEx(STATE.stdout, &mut info) == 0 { + return None; + } + + let w = (info.srWindow.Right - info.srWindow.Left + 1).max(1) as CoordType; + let h = (info.srWindow.Bottom - info.srWindow.Top + 1).max(1) as CoordType; + Some(Size { width: w, height: h }) } } @@ -463,16 +638,6 @@ impl PartialEq for FileId { impl Eq for FileId {} -/// Returns a unique identifier for the given file by handle or path. -pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result { - let file = match file { - Some(f) => f, - None => &File::open(path)?, - }; - - file_id_from_handle(file).or_else(|_| Ok(FileId::Path(std::fs::canonicalize(path)?))) -} - fn file_id_from_handle(file: &File) -> apperr::Result { unsafe { let mut info = MaybeUninit::::uninit(); @@ -505,100 +670,23 @@ pub fn canonicalize(path: &Path) -> std::io::Result { Ok(path) } -/// Reserves a virtual memory region of the given size. -/// To commit the memory, use [`virtual_commit`]. -/// To release the memory, use [`virtual_release`]. -/// -/// # Safety -/// -/// This function is unsafe because it uses raw pointers. -/// Don't forget to release the memory when you're done with it or you'll leak it. -pub unsafe fn virtual_reserve(size: usize) -> apperr::Result> { - unsafe { - #[allow(unused_assignments, unused_mut)] - let mut base = null_mut(); - - // In debug builds, we use fixed addresses to aid in debugging. - // Makes it possible to immediately tell which address space a pointer belongs to. - #[cfg(all(debug_assertions, not(target_pointer_width = "32")))] - { - static mut S_BASE_GEN: usize = 0x0000100000000000; // 16 TiB - S_BASE_GEN += 0x0000001000000000; // 64 GiB - base = S_BASE_GEN as *mut _; - } - - check_ptr_return(Memory::VirtualAlloc( - base, - size, - Memory::MEM_RESERVE, - Memory::PAGE_READWRITE, - ) as *mut u8) - } -} - -/// Releases a virtual memory region of the given size. -/// -/// # Safety -/// -/// This function is unsafe because it uses raw pointers. -/// Make sure to only pass pointers acquired from [`virtual_reserve`]. -pub unsafe fn virtual_release(base: NonNull, _size: usize) { - unsafe { - // NOTE: `VirtualFree` fails if the pointer isn't - // a valid base address or if the size isn't zero. - Memory::VirtualFree(base.as_ptr() as *mut _, 0, Memory::MEM_RELEASE); - } -} - -/// Commits a virtual memory region of the given size. -/// -/// # Safety -/// -/// This function is unsafe because it uses raw pointers. -/// Make sure to only pass pointers acquired from [`virtual_reserve`] -/// and to pass a size less than or equal to the size passed to [`virtual_reserve`]. -pub unsafe fn virtual_commit(base: NonNull, size: usize) -> apperr::Result<()> { - unsafe { - check_ptr_return(Memory::VirtualAlloc( - base.as_ptr() as *mut _, - size, - Memory::MEM_COMMIT, - Memory::PAGE_READWRITE, - )) - .map(|_| ()) - } +fn check_ptr_return(ret: *mut T) -> apperr::Result> { + NonNull::new(ret).ok_or_else(get_last_error) } -unsafe fn get_module(name: *const u16) -> apperr::Result> { - unsafe { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) } +#[cold] +fn get_last_error() -> apperr::Error { + unsafe { gle_to_apperr(Foundation::GetLastError()) } } -unsafe fn load_library(name: *const u16) -> apperr::Result> { - unsafe { - check_ptr_return(LibraryLoader::LoadLibraryExW( - name, - null_mut(), - LibraryLoader::LOAD_LIBRARY_SEARCH_SYSTEM32, - )) - } +#[inline] +const fn gle_to_apperr(gle: u32) -> apperr::Error { + apperr::Error::new_sys(if gle == 0 { 0x8000FFFF } else { 0x80070000 | gle }) } -/// Loads a function from a dynamic library. -/// -/// # Safety -/// -/// This function is highly unsafe as it requires you to know the exact type -/// of the function you're loading. No type checks whatsoever are performed. -// -// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable. -pub unsafe fn get_proc_address( - handle: NonNull, - name: *const c_char, -) -> apperr::Result { - unsafe { - let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8); - if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) } - } +#[inline] +pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error { + gle_to_apperr(err.raw_os_error().unwrap_or(0) as u32) } pub struct LibIcu { @@ -606,83 +694,6 @@ pub struct LibIcu { pub libicui18n: NonNull, } -pub fn load_icu() -> apperr::Result { - const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool { - unsafe { - let mut a = a; - let mut b = b; - loop { - if *a != *b { - return false; - } - if *a == 0 { - return true; - } - a = a.add(1); - b = b.add(1); - } - } - } - - const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME"); - const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME"); - - if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } { - let icu = unsafe { load_library(LIBICUUC)? }; - Ok(LibIcu { libicuuc: icu, libicui18n: icu }) - } else { - let libicuuc = unsafe { load_library(LIBICUUC)? }; - let libicui18n = unsafe { load_library(LIBICUI18N)? }; - Ok(LibIcu { libicuuc, libicui18n }) - } -} - -/// Returns a list of preferred languages for the current user. -pub fn preferred_languages(arena: &Arena) -> Vec, &Arena> { - // If the GetUserPreferredUILanguages() don't fit into 512 characters, - // honestly, just give up. How many languages do you realistically need? - const LEN: usize = 512; - - let scratch = scratch_arena(Some(arena)); - let mut res = Vec::new_in(arena); - - // Get the list of preferred languages via `GetUserPreferredUILanguages`. - let langs = unsafe { - let buf = scratch.alloc_uninit_slice(LEN); - let mut len = buf.len() as u32; - let mut num = 0; - - let ok = Globalization::GetUserPreferredUILanguages( - Globalization::MUI_LANGUAGE_NAME, - &mut num, - buf[0].as_mut_ptr(), - &mut len, - ); - - if ok == 0 || num == 0 { - len = 0; - } - - // Drop the terminating double-null character. - len = len.saturating_sub(1); - - buf[..len as usize].assume_init_ref() - }; - - // Convert UTF16 to UTF8. - let langs = wide_to_utf8(&scratch, langs); - - // Split the null-delimited string into individual chunks - // and copy them into the given arena. - res.extend( - langs - .split_terminator('\0') - .filter(|s| !s.is_empty()) - .map(|s| ArenaString::from_str(arena, s)), - ); - res -} - fn wide_to_utf8<'a>(arena: &'a Arena, wide: &[u16]) -> ArenaString<'a> { let mut res = ArenaString::new_in(arena); res.reserve(wide.len() * 3); @@ -706,61 +717,3 @@ fn wide_to_utf8<'a>(arena: &'a Arena, wide: &[u16]) -> ArenaString<'a> { res.shrink_to_fit(); res } - -#[cold] -fn get_last_error() -> apperr::Error { - unsafe { gle_to_apperr(Foundation::GetLastError()) } -} - -#[inline] -const fn gle_to_apperr(gle: u32) -> apperr::Error { - apperr::Error::new_sys(if gle == 0 { 0x8000FFFF } else { 0x80070000 | gle }) -} - -#[inline] -pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error { - gle_to_apperr(err.raw_os_error().unwrap_or(0) as u32) -} - -/// Formats a platform error code into a human-readable string. -pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result { - unsafe { - let mut ptr: *mut u8 = null_mut(); - let len = Debug::FormatMessageA( - Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER - | Debug::FORMAT_MESSAGE_FROM_SYSTEM - | Debug::FORMAT_MESSAGE_IGNORE_INSERTS, - null(), - code, - 0, - &mut ptr as *mut *mut _ as *mut _, - 0, - null_mut(), - ); - - write!(f, "Error {code:#08x}")?; - - if len > 0 { - let msg = str_from_raw_parts(ptr, len as usize); - let msg = msg.trim_ascii(); - let msg = msg.replace(['\r', '\n'], " "); - write!(f, ": {msg}")?; - Foundation::LocalFree(ptr as *mut _); - } - - Ok(()) - } -} - -/// Checks if the given error is a "file not found" error. -pub fn apperr_is_not_found(err: apperr::Error) -> bool { - err == gle_to_apperr(Foundation::ERROR_FILE_NOT_FOUND) -} - -fn check_bool_return(ret: Foundation::BOOL) -> apperr::Result<()> { - if ret == 0 { Err(get_last_error()) } else { Ok(()) } -} - -fn check_ptr_return(ret: *mut T) -> apperr::Result> { - NonNull::new(ret).ok_or_else(get_last_error) -}