Skip to content

Upgrade from 0.13 to 0.14

Timon edited this page Dec 13, 2019 · 19 revisions

This release is centered toward an advanced event reading layer. During the rewrite, strict attention has been paid to adaptability, performance, and ease of use. The design has been tuned with several projects (cursive, broot, termimad) in order to obtain a well-designed API.

Chapter 1 outlines the changes and technical implementations. Chapter 2 defines ways to perform a migration to this new version.

Table of Contents

1. Release Notes

Let's discuss what's changed, and the technical implementations of this.

1.1 Events

Requested improvements ontop of the previous implementation:

1.1.1 New Event API

A new event reading API has been developed that is flexible, fast and easy to use. This API can be used in both asynchronous and synchronous environments. It consists of two functions (read/poll) and one stream that can be used in async environments.

Two functions:

  • The read function returns an Event immediately (if available) or blocks until an Event is available.
  • The poll function allows you to check if there is or isn't an Event available within the given period of time. This can be used together with read to prevent a blocking call.

Take a look at the examples (event-*) for implementation details. Or here for technical details.

Async API

You can enable the event-stream feature flag for event::EventStream, which implements future::Stream, and produces Result<Event>.

1.1.2 Terminal Resize Events

Terminal resize events can be recorded when the size of the terminal is changed. The resize event is sent as an event like Event::Resize(x, y). For UNIX systems, Signal Hook with MIO Extention is used. Windows provides them in the INPUT_RECORD and thus no external crate is required.

1.1.3 Advanced Modifier Support

A modifier is a key like ALT, SHIFT, CTRL, META which can be pressed alongside another key. This update introduces advanced modifier support for both mouse and key events.

Crossterm used to stored modifier information like:

InputEvent::Key(KeyEvent::Ctrl(char))
InputEvent::Key(KeyEvent::Alt(char))
InputEvent::Key(KeyEvent::ShiftLeft)

However, it would be a maintenance hell, if we had to keep track of all the keys with all the possible modifiers. And therefore also limited to the number of combinations we implement.

The modifier information is moved out of the KeyEvent and into its own type, KeyModifiers. This type is sent along with the key and mouse events, and contains three fields: ALT, CTRL, SHIFT. It uses bitflags and therefore can it contain multiple modifiers at the same time. Although one has to note that it is not guaranteed all combinations are supported. Some OS's do not provide certain combinations.

1.1.4 Less Resource-Consuming

First, crossterm fired a thread to catch events or signals. Or use a spinning loop with some delay to check for event readiness. Fortunately, a poll/read API is introduced with proper system event readiness signaling and cancelation (See the Event Polling section).

In addition, the way crossterm dealt with windows HANDLE's has been improved significantly. Crossterm first made a lot of instances of these HANDLE's and did not close them in the right way. This led to high resource consumption levels. Now it is much more carefully with those and performs fewer system calls.

1.1.5 Event Polling

Let's talk about how crossterm polls for events.

UNIX

The MIO crate is used to make polling more efficient. When a byte is ready to be read, it is written to an internal input buffer. A user can then read an event when crossterm is able to turn these bytes into an Event.

Windows

For windows, crossterm is using WaitForMultipleObjects, with this call we can wait for the input HANDLE or a semaphore HANDLE to signal.

When a signal from the HANDLE input is received, we check with GetNumberOfConsoleInputEvents to validate if there is actually something to read. If this is the case, ReadConsoleInputW is used to read the ready event.

1.1.5.1 Cancelation of infinite poll(None)

In the EventStream we set up a thread that uses poll(None) to wait indefinitely for an event. If there is an event-ready, futures::task::Waker::wake is called to wake up the task. There is a problem around the corner. If the EventStream is dropped, poll(None)operation must be stopped in order to close and dispose of the thread.

For this scenario, we created a Waker who can interrupt the poll operation. With UNIX we register the Waker into Mio and for windows, we trigger the semaphore object.

1.2 Removed screen module

The 'screen' module is no longer with us. The functionality remains and has been moved to the 'terminal' module. We did this because the 'screen' module does not add much value, and exists because of historical reasons.

1.3 New Cursor Commands

New cursor commands have been added. These commands make it easier to work with the terminal cursor. These are: MoveToColumn, MoveToNextLine and MoveToPreviousLine. I think the names speak for themselves otherwise you can check the documentation for more information.

2. Migrations

In this chapter, you can find notes on how to migrate to crossterm 0.14.

2.1 Feature Flags

  • Removed cursor, style, terminal, screen and input feature flags.
    • All the functionality is included by default and there's no way to disable it.
  • New feature flag event-stream.
    • Provides EventStream (futures::Stream) that can be used for asynchronous event reading.

Motivation

  • The previous features flags were getting unmanageable (cyclic dependencies, ...).
  • Crossterm has shrunk a few thousand lines of code since the feature flags were introduced.
  • Easier development on our side, easier usage on your side.

Why did we introduce event-stream feature flag?

  • The event-stream brings futures crate dependency.
  • The number of crates you have to compile is roughly doubled with this feature and the compile-time is 4x longer.

2.2 Removed TerminalInput

TerminalInput and all of it's functions are removed:

  • read_line(), read_char(), enable_mouse_modes(), disable_mouse_modes(), read_sync, read_async. We decided to keep the API minamilistic and useable for lot's of usecases, and therefore had to remove some of the functions.

Those removed functions can be replaced with the following:

replacement of TerminalInput::read_char()

pub fn read_char() -> Result<char> {
    loop {
        if let Event::Key(KeyEvent {
            code: KeyCode::Char(c),
            ..
        }) = event::read()?
        {
            return Ok(c);
        }
    }
}

Alternatively you could use the standard library:

pub fn read_char() -> Result<char> {  
  let char = io::stdin().bytes().next()?.map(|b| b as char)?; 
  	/* or something simmilar */
}

replacement of TerminalInput::read_line()

pub fn read_line() -> Result<String> {
    let mut line = String::new();
    while let Event::Key(KeyEvent { code: code, .. }) = event::read()? {
        match code {
            KeyCode::Enter => {
                break;
            },
            KeyCode::Char(c) => {
                line.push(c);
            },
            _ => {}
        }
    }

    return Ok(line);
}

Alternatively you could use the standard library:

fn read_line(&self) -> Result<String> {
	let mut rv = String::new();
	io::stdin().read_line(&mut rv)?;
	let len = rv.trim_end_matches(&['\r', '\n'][..]).len();
	rv.truncate(len);
	Ok(rv)
}

replacement of TerminalInput::enable_mouse_modes()

use crossterm::event;

execute!(io::stdout(), event::EnableMouseCapture)?;

replacement of TerminalInput::disable_mouse_modes()

use crossterm::event;

execute!(io::stdout(), event::DisableMouseCapture)?;

replacement of TerminalInput::read_sync()

use crossterm::event;

let event = event::read()?;

replacement of read_async() (actualy non-blocking)

use crossterm::event;

if poll(Duration::from_millis(0))? {
    // Guaranteed that `read()` wont block if `poll` returns `Ok(true)`
    let event = event::read()?;
}

Check this chapter for real async event reading.

Replacement of modifiers (Ctrl, Shift, ALT) entry KeyEvent enum

match event {
    Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code }) => { }
    Event::Key(KeyEvent { modifiers: KeyModifiers::Shift, code}) => { }
    Event::Key(KeyEvent { modifiers: KeyModifiers::ALT, code }) => { }
}

For more examples please have a look over at the examples directory.

2.3 Moved 'screen' module into 'terminal'

screen::LeaveAlternateScreen => terminal::LeaveAlternateScreen screen::EnterAlternateScreen => terminal::EnterAlternateScreen

2.3.1 Removed RawScreen

When you used RawScreen::into_raw_mode() you got a RawScreen instance back, if it went out of the scope, the raw screen would be disabled again. This type didn't serve any purpose other than to bother the user because one had to keep it around like this:

let _ = RawScreen::into_raw_mode();

New way This is inconvenient and created a lack of clarity for some users. And because we want to make the API simpler, we decided to stick with two functions.

terminal::enable_raw_mode()
terminal::disable_raw_mode()

No state is kept, and the user is responsible for disabling raw modes. So make sure that your migration will call disable_raw_mode() were usually RawScreen would go out of scope.