-
Notifications
You must be signed in to change notification settings - Fork 284
Upgrade from 0.13 to 0.14
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.
Let's discuss what's changed, and the technical implementations of this.
Requested improvements ontop of the previous implementation:
- API compatible with the async environment
- Support for Teminal Resize Events
- Advanced modifiers support.
- FFI portability
- Less use of system resources.
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 anEvent
immediately (if available) or blocks until anEvent
is available. - The
poll
function allows you to check if there is or isn't anEvent
available within the given period of time. This can be used together withread
to prevent a blocking call.
Take a look at the examples (event-*
) for implementation details. Or here for technical details.
You can enable the event-stream
feature flag for event::EventStream, which implements future::Stream
, and produces Result<Event>
.
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.
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.
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.
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.
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.
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.
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.
In this chapter, you can find notes on how to migrate to crossterm 0.14.
- Removed
cursor
,style
,terminal
,screen
andinput
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.
- Provides
- 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
bringsfutures
crate dependency. - The number of crates you have to compile is roughly doubled with this feature and the compile-time is 4x longer.
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.
screen::LeaveAlternateScreen
=> terminal::LeaveAlternateScreen
screen::EnterAlternateScreen
=> terminal::EnterAlternateScreen
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.