diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index 27ae7fab6c0..e9728c52fdd 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -64,7 +64,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::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. @@ -83,16 +83,17 @@ fn run() -> apperr::Result<()> { handle_stdin(&mut state)?; + let mut vt_parser = vt::Parser::new(); + let mut input_parser = input::Parser::new(); + let mut tui = Tui::new()?; + tui.set_size(sys::get_window_size()?); + // Switch the terminal to raw mode which prevents the user from pressing Ctrl+C. // `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()?; - let mut vt_parser = vt::Parser::new(); - let mut input_parser = input::Parser::new(); - let mut tui = Tui::new()?; - let _restore = setup_terminal(&mut tui, &mut state, &mut vt_parser); state.menubar_color_bg = tui.indexed(IndexedColor::Background).oklab_blend(tui.indexed_alpha( @@ -115,8 +116,6 @@ fn run() -> apperr::Result<()> { tui.set_modal_default_bg(floater_bg); tui.set_modal_default_fg(floater_fg); - sys::inject_window_size_into_stdin(); - #[cfg(feature = "debug-latency")] let mut last_latency_width = 0; @@ -130,7 +129,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((resize, input)) = sys::read_stdin(&scratch, read_timeout) else { break; }; @@ -140,15 +139,21 @@ fn run() -> apperr::Result<()> { passes = 0usize; } + if let Some(size) = resize { + draw(&mut tui, Some(input::Input::Resize(size)), &mut state); + #[cfg(feature = "debug-latency")] + { + passes += 1; + } + } + let vt_iter = vt_parser.parse(&input); let mut input_iter = input_parser.parse(vt_iter); while { let input = input_iter.next(); let more = input.is_some(); - let mut ctx = tui.create_context(input); - - draw(&mut ctx, &mut state); + draw(&mut tui, input, &mut state); #[cfg(feature = "debug-latency")] { @@ -162,9 +167,7 @@ fn run() -> apperr::Result<()> { // Continue rendering until the layout has settled. // This can take >1 frame, if the input focus is tossed between different controls. while tui.needs_settling() { - let mut ctx = tui.create_context(None); - - draw(&mut ctx, &mut state); + draw(&mut tui, None, &mut state); #[cfg(feature = "debug-latency")] { @@ -336,7 +339,9 @@ fn print_version() { sys::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\n")); } -fn draw(ctx: &mut Context, state: &mut State) { +fn draw(tui: &mut Tui, input: Option, state: &mut State) { + let ctx = &mut tui.create_context(input); + draw_menubar(ctx, state); draw_editor(ctx, state); draw_statusbar(ctx, state); @@ -623,10 +628,14 @@ 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((resize, input)) = sys::read_stdin(&scratch, Duration::from_secs(3)) else { break; }; + if let Some(size) = resize { + tui.set_size(size); + } + let mut vt_stream = vt_parser.parse(&input); while let Some(token) = vt_stream.next() { match token { @@ -694,6 +703,8 @@ fn setup_terminal(tui: &mut Tui, state: &mut State, vt_parser: &mut vt::Parser) if ambiguous_width == 2 { unicode::setup_ambiguous_width(2); + // The text buffer cursor caches the visual column, which + // may change if ambiguous width characters are now wide. state.documents.reflow_all(); } diff --git a/crates/edit/src/framebuffer.rs b/crates/edit/src/framebuffer.rs index 74562af98d0..8372b659a4d 100644 --- a/crates/edit/src/framebuffer.rs +++ b/crates/edit/src/framebuffer.rs @@ -167,6 +167,10 @@ impl Framebuffer { /// Begins a new frame with the given `size`. pub fn flip(&mut self, size: Size) { + if size.is_empty() { + return; + } + if size != self.buffers[0].bg_bitmap.size { for buffer in &mut self.buffers { buffer.text = LineBuffer::new(size); @@ -479,6 +483,10 @@ impl Framebuffer { (back, front) }; + if front.text.size.is_empty() { + return BString::empty(); + } + let mut front_lines = front.text.lines.iter(); // hahaha let mut front_bgs = front.bg_bitmap.iter(); let mut front_fgs = front.fg_bitmap.iter(); @@ -674,6 +682,7 @@ struct LineBuffer { impl LineBuffer { fn new(size: Size) -> Self { + debug_assert!(!size.is_empty()); Self { lines: vec![String::new(); size.height as usize], size } } @@ -821,6 +830,7 @@ struct Bitmap { impl Bitmap { fn new(size: Size) -> Self { + debug_assert!(!size.is_empty()); Self { data: vec![StraightRgba::zero(); (size.width * size.height) as usize], size } } @@ -931,6 +941,7 @@ struct AttributeBuffer { impl AttributeBuffer { fn new(size: Size) -> Self { + debug_assert!(!size.is_empty()); Self { data: vec![Default::default(); (size.width * size.height) as usize], size } } diff --git a/crates/edit/src/helpers.rs b/crates/edit/src/helpers.rs index e85b62f7dc6..200815226c8 100644 --- a/crates/edit/src/helpers.rs +++ b/crates/edit/src/helpers.rs @@ -84,6 +84,10 @@ pub struct Size { } impl Size { + pub fn is_empty(&self) -> bool { + self.width <= 0 || self.height <= 0 + } + pub fn as_rect(&self) -> Rect { Rect { left: 0, top: 0, right: self.width, bottom: self.height } } diff --git a/crates/edit/src/input.rs b/crates/edit/src/input.rs index 09144fb7b64..bbc1a7b6456 100644 --- a/crates/edit/src/input.rs +++ b/crates/edit/src/input.rs @@ -448,12 +448,6 @@ impl<'input> Iterator for Stream<'_, '_, 'input> { 'M' if csi.param_count == 0 => { self.parser.x10_mouse_want = true; } - 't' if csi.params[0] == 8 => { - // Window Size - let width = (csi.params[2] as CoordType).clamp(1, 32767); - let height = (csi.params[1] as CoordType).clamp(1, 32767); - return Some(Input::Resize(Size { width, height })); - } _ => {} } } diff --git a/crates/edit/src/sys/unix.rs b/crates/edit/src/sys/unix.rs index 1d51fa6f3d0..552de33a2f7 100644 --- a/crates/edit/src/sys/unix.rs +++ b/crates/edit/src/sys/unix.rs @@ -12,10 +12,9 @@ use std::mem::{self, ManuallyDrop, MaybeUninit}; use std::os::fd::{AsRawFd as _, FromRawFd as _}; use std::path::Path; use std::ptr::{NonNull, null_mut}; -use std::{io, thread, time}; +use std::{io, time}; -use stdext::arena::{Arena, scratch_arena}; -use stdext::arena_format; +use stdext::arena::Arena; use stdext::collections::{BString, BVec}; use crate::helpers::*; @@ -25,7 +24,7 @@ struct State { stdin_flags: libc::c_int, stdout: libc::c_int, stdout_initial_termios: Option, - inject_resize: bool, + resize_pending: bool, // Buffer for incomplete UTF-8 sequences (max 4 bytes needed) utf8_buf: [u8; 4], utf8_len: usize, @@ -36,19 +35,26 @@ static mut STATE: State = State { stdin_flags: 0, stdout: libc::STDOUT_FILENO, stdout_initial_termios: None, - inject_resize: false, + resize_pending: false, utf8_buf: [0; 4], utf8_len: 0, }; extern "C" fn sigwinch_handler(_: libc::c_int) { unsafe { - STATE.inject_resize = true; + STATE.resize_pending = true; } } -pub fn init() -> Deinit { - Deinit +pub fn init() -> io::Result { + unsafe { + // Set STATE.resize_pending to true whenever we get a SIGWINCH. + let mut sigwinch_action: libc::sigaction = mem::zeroed(); + sigwinch_action.sa_sigaction = sigwinch_handler as *const () as libc::sighandler_t; + check_int_return(libc::sigaction(libc::SIGWINCH, &sigwinch_action, null_mut()))?; + } + + Ok(Deinit) } /// Reopen stdin if it's redirected (= piped input). @@ -68,11 +74,6 @@ pub fn switch_modes() -> io::Result<()> { // 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))?; - // 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 *const () as libc::sighandler_t; - check_int_return(libc::sigaction(libc::SIGWINCH, &sigwinch_action, null_mut()))?; - // 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()))?; @@ -144,42 +145,29 @@ impl Drop for Deinit { } } -pub fn inject_window_size_into_stdin() { - unsafe { - STATE.inject_resize = true; - } -} - -fn get_window_size() -> (u16, u16) { +pub fn get_window_size() -> io::Result { 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)); + let ret = unsafe { libc::ioctl(STATE.stdout, libc::TIOCGWINSZ, &raw mut winsz) }; + if ret != 0 { + Err(last_os_error()) + } else if winsz.ws_row == 0 || winsz.ws_col == 0 { + Err(io::Error::other("invalid terminal size")) + } else { + Ok(Size { width: winsz.ws_col as CoordType, height: winsz.ws_row as CoordType }) } - - (winsz.ws_col, winsz.ws_row) } /// 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> { +/// Returns `Some((_, ""))` if the given timeout was reached. +/// Otherwise, it returns a pending resize and the read string. +pub fn read_stdin( + arena: &Arena, + mut timeout: time::Duration, +) -> Option<(Option, BString<'_>)> { unsafe { - if STATE.inject_resize { + if STATE.resize_pending { timeout = time::Duration::ZERO; } @@ -242,7 +230,7 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option break, + libc::EINTR if STATE.resize_pending => break, libc::EAGAIN if timeout == time::Duration::ZERO => break, libc::EINTR | libc::EAGAIN => {} _ => return None, @@ -279,21 +267,14 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option 0 && h > 0 { - let scratch = scratch_arena(Some(arena)); - let seq = arena_format!(&*scratch, "\x1b[8;{h};{w}t"); - result.replace_range(arena, 0..0, &seq); - } - } + let resize = if STATE.resize_pending { + STATE.resize_pending = false; + Some(get_window_size().ok()?) + } else { + None + }; - Some(result) + Some((resize, BString::from_utf8_lossy(arena, buf))) } } diff --git a/crates/edit/src/sys/windows.rs b/crates/edit/src/sys/windows.rs index 98a100835d6..f231faec390 100644 --- a/crates/edit/src/sys/windows.rs +++ b/crates/edit/src/sys/windows.rs @@ -10,7 +10,6 @@ use std::ptr::{self, NonNull, null, null_mut}; use std::{io, mem, time}; use stdext::arena::{Arena, scratch_arena}; -use stdext::arena_write_fmt; use stdext::collections::{BString, BVec}; use windows_sys::Win32::Storage::FileSystem; use windows_sys::Win32::System::{Console, IO, LibraryLoader, Threading}; @@ -78,7 +77,6 @@ struct State { stdin_mode_old: u32, stdout_mode_old: u32, leading_surrogate: u16, - inject_resize: bool, wants_exit: bool, } @@ -91,7 +89,6 @@ static mut STATE: State = State { stdin_mode_old: INVALID_CONSOLE_MODE, stdout_mode_old: INVALID_CONSOLE_MODE, leading_surrogate: 0, - inject_resize: false, wants_exit: false, }; @@ -104,14 +101,14 @@ extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> BOOL { } /// Initializes the platform-specific state. -pub fn init() -> Deinit { +pub fn init() -> io::Result { 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 + Ok(Deinit) } } @@ -235,27 +232,21 @@ impl Drop for Deinit { } } -/// 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; - } -} - -fn get_console_size() -> Option { +pub fn get_window_size() -> io::Result { 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; + return Err(last_os_error()); } - 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 }) + let width = info.srWindow.Right as CoordType - info.srWindow.Left as CoordType + 1; + let height = info.srWindow.Bottom as CoordType - info.srWindow.Top as CoordType + 1; + if width <= 0 || height <= 0 { + Err(io::Error::other("invalid terminal size")) + } else { + Ok(Size { width, height }) + } } } @@ -264,19 +255,14 @@ fn get_console_size() -> Option { /// # 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> { +/// * `Some((_, ""))` if the given timeout was reached. +/// * Otherwise, it returns a pending resize and the read string. +pub fn read_stdin( + arena: &Arena, + mut timeout: time::Duration, +) -> Option<(Option, BString<'_>)> { 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(); - } 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); @@ -312,7 +298,7 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option Option 0 { @@ -409,7 +385,7 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option StraightRgba {