Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions crates/edit/src/bin/edit/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand All @@ -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;

Expand All @@ -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;
};

Expand All @@ -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")]
{
Expand All @@ -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")]
{
Expand Down Expand Up @@ -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<input::Input>, state: &mut State) {
let ctx = &mut tui.create_context(input);

draw_menubar(ctx, state);
draw_editor(ctx, state);
draw_statusbar(ctx, state);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}

Expand Down
11 changes: 11 additions & 0 deletions crates/edit/src/framebuffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 }
}

Expand Down Expand Up @@ -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 }
}

Expand Down Expand Up @@ -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 }
}

Expand Down
4 changes: 4 additions & 0 deletions crates/edit/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
6 changes: 0 additions & 6 deletions crates/edit/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
}
_ => {}
}
}
Expand Down
93 changes: 37 additions & 56 deletions crates/edit/src/sys/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -25,7 +24,7 @@ struct State {
stdin_flags: libc::c_int,
stdout: libc::c_int,
stdout_initial_termios: Option<libc::termios>,
inject_resize: bool,
resize_pending: bool,
// Buffer for incomplete UTF-8 sequences (max 4 bytes needed)
utf8_buf: [u8; 4],
utf8_len: usize,
Expand All @@ -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<Deinit> {
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).
Expand All @@ -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::<libc::termios>::uninit();
check_int_return(libc::tcgetattr(STATE.stdout, termios.as_mut_ptr()))?;
Expand Down Expand Up @@ -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<Size> {
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<BString<'_>> {
/// 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<Size>, BString<'_>)> {
unsafe {
if STATE.inject_resize {
if STATE.resize_pending {
timeout = time::Duration::ZERO;
}

Expand Down Expand Up @@ -242,7 +230,7 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<BString<
}
if ret < 0 {
match errno() {
libc::EINTR if STATE.inject_resize => break,
libc::EINTR if STATE.resize_pending => break,
libc::EAGAIN if timeout == time::Duration::ZERO => break,
libc::EINTR | libc::EAGAIN => {}
_ => return None,
Expand Down Expand Up @@ -279,21 +267,14 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<BString<
}
}

let mut result = BString::from_utf8_lossy(arena, 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(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)))
}
}

Expand Down
Loading
Loading