tokio_terminal_resize/
lib.rs

1//! Implements a stream of terminal resize events.
2//!
3//! # Overview
4//!
5//! Whenever the user resizes their terminal, a notification is sent to the
6//! application running in it. This crate provides those notifications in the
7//! form of a stream.
8//!
9//! # Synopsis
10//!
11//! ```
12//! # use futures::future::Future as _;
13//! # use futures::stream::Stream as _;
14//! let stream = tokio_terminal_resize::resizes().flatten_stream();
15//! let prog = stream
16//!     .for_each(|(rows, cols)| {
17//!         println!("terminal is now {}x{}", cols, rows);
18//!         Ok(())
19//!     })
20//!     .map_err(|e| eprintln!("error: {}", e));
21//! tokio::run(prog);
22//! ```
23
24// XXX this is broken with ale
25// #![warn(clippy::cargo)]
26#![warn(clippy::pedantic)]
27#![warn(clippy::nursery)]
28#![allow(clippy::multiple_crate_versions)]
29
30use futures::future::Future as _;
31use futures::stream::Stream as _;
32use snafu::futures01::FutureExt as _;
33use snafu::futures01::StreamExt as _;
34use snafu::ResultExt as _;
35use std::convert::TryInto as _;
36
37/// Errors returned by this crate.
38#[derive(Debug, snafu::Snafu)]
39pub enum Error {
40    /// failed to get terminal size
41    #[snafu(display("failed to get terminal size"))]
42    GetTerminalSize,
43
44    /// invalid terminal size found
45    #[snafu(display("invalid terminal size found: {}", source))]
46    InvalidTerminalSize { source: std::num::TryFromIntError },
47
48    /// SIGWINCH handler failed
49    #[snafu(display("SIGWINCH handler failed: {}", source))]
50    SigWinchHandler { source: std::io::Error },
51}
52
53/// Creates a stream which receives the new terminal size every time the
54/// user's terminal is resized.
55pub fn resizes() -> ResizeFuture {
56    ResizeFuture::default()
57}
58
59/// Future which sets up the terminal size stream
60pub struct ResizeFuture {
61    stream_fut: Box<
62        dyn futures::future::Future<Item = ResizeStream, Error = Error>
63            + Send,
64    >,
65}
66
67impl Default for ResizeFuture {
68    fn default() -> Self {
69        let stream_fut = tokio_signal::unix::Signal::new(
70            tokio_signal::unix::libc::SIGWINCH,
71        )
72        .context(SigWinchHandler)
73        .and_then(|stream| {
74            futures::future::ok(ResizeStream {
75                winches: Box::new(
76                    stream.map(|_| ()).context(SigWinchHandler),
77                ),
78                sent_initial_size: false,
79            })
80        });
81        Self {
82            stream_fut: Box::new(stream_fut),
83        }
84    }
85}
86
87#[must_use = "streams do nothing unless polled"]
88impl futures::future::Future for ResizeFuture {
89    type Item = ResizeStream;
90    type Error = Error;
91
92    fn poll(&mut self) -> futures::Poll<Self::Item, Self::Error> {
93        self.stream_fut.poll()
94    }
95}
96
97/// Stream which returns the new terminal size every time it changes
98pub struct ResizeStream {
99    winches:
100        Box<dyn futures::stream::Stream<Item = (), Error = Error> + Send>,
101    sent_initial_size: bool,
102}
103
104#[must_use = "streams do nothing unless polled"]
105impl futures::stream::Stream for ResizeStream {
106    type Item = (u16, u16);
107    type Error = Error;
108
109    fn poll(&mut self) -> futures::Poll<Option<Self::Item>, Self::Error> {
110        if !self.sent_initial_size {
111            self.sent_initial_size = true;
112            return Ok(futures::Async::Ready(Some(term_size()?)));
113        }
114        futures::try_ready!(self.winches.poll());
115        Ok(futures::Async::Ready(Some(term_size()?)))
116    }
117}
118
119fn term_size() -> Result<(u16, u16), Error> {
120    if let Some((cols, rows)) = term_size::dimensions() {
121        Ok((
122            rows.try_into().context(InvalidTerminalSize)?,
123            cols.try_into().context(InvalidTerminalSize)?,
124        ))
125    } else {
126        Err(Error::GetTerminalSize)
127    }
128}