• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lpenz / ogle / 21597759092

02 Feb 2026 04:10PM UTC coverage: 58.629% (-0.5%) from 59.173%
21597759092

push

github

lpenz
Use raw mode in terminal to get keys immediately and avoid echo

Raw mode is enabled when the user stream is constructed, and disabled
when it's dropped.

4 of 9 new or added lines in 2 files covered. (44.44%)

10 existing lines in 2 files now uncovered.

462 of 788 relevant lines covered (58.63%)

1.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

26.47
/src/user_wrapper.rs
1
// Copyright (C) 2025 Leandro Lisboa Penz <lpenz@lpenz.org>
2
// This file is subject to the terms and conditions defined in
3
// file 'LICENSE', which is part of this source code package.
4

5
//! Wrapper for user interaction.
6
//!
7
//! For now we just check if the user has typed ENTER, which makes
8
//! ogle exit after the current run is over.
9

10
use crossterm::tty::IsTty;
11
use crossterm::{
12
    event::{self, Event},
13
    terminal::{disable_raw_mode, enable_raw_mode},
14
};
15
use std::pin::Pin;
16
use std::task::{Context, Poll};
17
use tokio::io;
18
use tokio::sync::mpsc;
19
use tokio::time::{Duration, sleep};
20
use tokio_stream::Stream;
21
use tracing::instrument;
22

23
/// A wrapper for `crossterm` that polls the keyboard and provides the
24
/// keypress in a tokio stream.
25
///
26
/// Also provides a virtual implementation for use in tests.
27
#[derive(Debug, Default)]
28
pub enum UserStream {
29
    /// A real implementation that reads a line from stdin.
30
    Real(mpsc::UnboundedReceiver<String>),
31
    /// A virtual implementation that doesn't do anything.
32
    #[default]
33
    Virtual,
34
}
35

36
impl UserStream {
UNCOV
37
    pub fn new_real() -> Option<UserStream> {
×
UNCOV
38
        let stdin = io::stdin();
×
UNCOV
39
        if stdin.is_tty() {
×
40
            let (tx, rx) = mpsc::unbounded_channel::<String>();
×
NEW
41
            let _ = enable_raw_mode();
×
42
            tokio::spawn(async move {
×
43
                loop {
44
                    let key_event = matches!(event::poll(Duration::from_secs(0)), Ok(true))
×
45
                        .then(|| match event::read() {
×
UNCOV
46
                            Ok(Event::Key(key_event)) => Some(key_event),
×
47
                            Ok(_) => None,
×
48
                            Err(e) => {
×
49
                                panic!("could not read key after poll returned true: {}", e)
×
50
                            }
51
                        })
×
52
                        .flatten();
×
53
                    if let Some(key_event) = key_event {
×
54
                        // If sending the key fails, it's probably because we
×
UNCOV
55
                        // are in the process of being dropped, so we can
×
56
                        // ignore it:
×
UNCOV
57
                        let _ = tx.send(format!("{}", key_event.code));
×
UNCOV
58
                    } else {
×
59
                        // We tokio-sleep here to provide a cancellation point:
60
                        sleep(Duration::from_millis(127)).await;
×
61
                    }
62
                }
63
            });
64
            Some(UserStream::Real(rx))
×
65
        } else {
UNCOV
66
            None
×
67
        }
UNCOV
68
    }
×
69

70
    pub fn new_virtual() -> UserStream {
2✔
71
        UserStream::Virtual
2✔
72
    }
2✔
73
}
74

75
impl Drop for UserStream {
76
    fn drop(&mut self) {
2✔
77
        if matches!(self, UserStream::Real(_)) {
2✔
NEW
78
            let _ = disable_raw_mode();
×
79
        }
2✔
80
    }
2✔
81
}
82

83
impl Stream for UserStream {
84
    type Item = String;
85

86
    #[instrument(level = "debug", ret, skip(cx))]
87
    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
2✔
88
        let this = self.get_mut();
89
        match this {
90
            UserStream::Real(rx) => {
91
                let next = Pin::new(rx).poll_recv(cx);
92
                match next {
93
                    Poll::Ready(Some(s)) => Poll::Ready(Some(s)),
94
                    Poll::Ready(None) => Poll::Ready(None),
95
                    Poll::Pending => Poll::Pending,
96
                }
97
            }
98
            UserStream::Virtual => Poll::Ready(None),
99
        }
100
    }
2✔
101
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc