Skip to content

Commit 1848ff8

Browse files
joshkatobz
authored andcommitted
Use ratatui instead of tui-rs for the terminal UI (#505)
Signed-off-by: Toby Lawrence <[email protected]>
1 parent 9974d82 commit 1848ff8

File tree

7 files changed

+61
-58
lines changed

7 files changed

+61
-58
lines changed

.github/workflows/ci.yml

+8-2
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,22 @@ jobs:
4444
name: Test ${{ matrix.rust_version }}
4545
runs-on: ubuntu-latest
4646
strategy:
47+
# 1.70 is the MSRV for the project, which currently does not match the version specified in
48+
# the rust-toolchain.toml file as metrics-observer requires 1.74 to build. See
49+
# https://github.com/metrics-rs/metrics/pull/505#discussion_r1724092556 for more information.
4750
matrix:
48-
rust_version: ['stable', 'nightly']
51+
rust_version: ['stable', 'nightly', '1.70']
52+
include:
53+
- rust_version: '1.70'
54+
exclude-packages: '--exclude metrics-observer'
4955
steps:
5056
- uses: actions/checkout@v3
5157
- name: Install Protobuf Compiler
5258
run: sudo apt-get install protobuf-compiler
5359
- name: Install Rust ${{ matrix.rust_version }}
5460
run: rustup install ${{ matrix.rust_version }}
5561
- name: Run Tests
56-
run: cargo +${{ matrix.rust_version }} test --all-features --workspace
62+
run: cargo +${{ matrix.rust_version }} test --all-features --workspace ${{ matrix.exclude-packages }}
5763
docs:
5864
runs-on: ubuntu-latest
5965
env:

metrics-observer/Cargo.toml

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "metrics-observer"
33
version = "0.4.0"
44
authors = ["Toby Lawrence <[email protected]>"]
55
edition = "2018"
6-
rust-version = "1.70.0"
6+
rust-version = "1.74.0"
77

88
license = "MIT"
99

@@ -23,8 +23,7 @@ bytes = { version = "1", default-features = false }
2323
crossbeam-channel = { version = "0.5", default-features = false, features = ["std"] }
2424
prost = { version = "0.12", default-features = false }
2525
prost-types = { version = "0.12", default-features = false }
26-
tui = { version = "0.19", default-features = false, features = ["termion"] }
27-
termion = { version = "2", default-features = false }
26+
ratatui = { version = "0.28.0", default-features = false, features = ["crossterm"] }
2827
chrono = { version = "0.4", default-features = false, features = ["clock"] }
2928

3029
[build-dependencies]

metrics-observer/src/input.rs

+8-27
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,18 @@
11
use std::io;
2-
use std::thread;
32
use std::time::Duration;
43

5-
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, TrySendError};
6-
use termion::event::Key;
7-
use termion::input::TermRead;
4+
use ratatui::crossterm::event::{self, Event, KeyEvent, KeyEventKind};
85

9-
pub struct InputEvents {
10-
rx: Receiver<Key>,
11-
}
6+
pub struct InputEvents;
127

138
impl InputEvents {
14-
pub fn new() -> InputEvents {
15-
let (tx, rx) = bounded(1);
16-
thread::spawn(move || {
17-
let stdin = io::stdin();
18-
for key in stdin.keys().flatten() {
19-
// If our queue is full, we don't care. The user can just press the key again.
20-
if let Err(TrySendError::Disconnected(_)) = tx.try_send(key) {
21-
eprintln!("input event channel disconnected");
22-
return;
23-
}
9+
pub fn next() -> io::Result<Option<KeyEvent>> {
10+
if event::poll(Duration::from_secs(1))? {
11+
match event::read()? {
12+
Event::Key(key) if key.kind == KeyEventKind::Press => return Ok(Some(key)),
13+
_ => {}
2414
}
25-
});
26-
27-
InputEvents { rx }
28-
}
29-
30-
pub fn next(&mut self) -> Result<Option<Key>, RecvTimeoutError> {
31-
match self.rx.recv_timeout(Duration::from_secs(1)) {
32-
Ok(key) => Ok(Some(key)),
33-
Err(RecvTimeoutError::Timeout) => Ok(None),
34-
Err(e) => Err(e),
3515
}
16+
Ok(None)
3617
}
3718
}

metrics-observer/src/main.rs

+37-23
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
use std::fmt;
21
use std::num::FpCategory;
32
use std::time::Duration;
43
use std::{error::Error, io};
4+
use std::{fmt, io::Stdout};
55

66
use chrono::Local;
77
use metrics::Unit;
8-
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::IntoAlternateScreen};
9-
use tui::{
10-
backend::TermionBackend,
8+
use ratatui::{
9+
backend::CrosstermBackend,
10+
crossterm::{
11+
event::KeyCode,
12+
execute,
13+
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
14+
},
1115
layout::{Constraint, Direction, Layout},
1216
style::{Color, Modifier, Style},
13-
text::{Span, Spans},
17+
text::{Line, Span},
1418
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
1519
Terminal,
1620
};
@@ -27,23 +31,23 @@ mod selector;
2731
use self::selector::Selector;
2832

2933
fn main() -> Result<(), Box<dyn Error>> {
30-
let stdout = io::stdout().into_raw_mode()?;
31-
let stdout = MouseTerminal::from(stdout).into_alternate_screen()?;
32-
let backend = TermionBackend::new(stdout);
33-
let mut terminal = Terminal::new(backend)?;
34+
let terminal = init_terminal()?;
35+
let result = run(terminal);
36+
restore_terminal()?;
37+
result
38+
}
3439

35-
let mut events = InputEvents::new();
40+
fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> {
3641
let address = std::env::args().nth(1).unwrap_or_else(|| "127.0.0.1:5000".to_owned());
3742
let client = metrics_inner::Client::new(address);
3843
let mut selector = Selector::new();
39-
4044
loop {
4145
terminal.draw(|f| {
4246
let chunks = Layout::default()
4347
.direction(Direction::Vertical)
4448
.margin(1)
4549
.constraints([Constraint::Length(4), Constraint::Percentage(90)].as_ref())
46-
.split(f.size());
50+
.split(f.area());
4751

4852
let current_dt = Local::now().format(" (%Y/%m/%d %I:%M:%S %p)").to_string();
4953
let client_state = match client.state() {
@@ -58,9 +62,9 @@ fn main() -> Result<(), Box<dyn Error>> {
5862
spans.push(Span::raw(s));
5963
}
6064

61-
Spans::from(spans)
65+
Line::from(spans)
6266
}
63-
ClientState::Connected => Spans::from(vec![
67+
ClientState::Connected => Line::from(vec![
6468
Span::raw("state: "),
6569
Span::styled("connected", Style::default().fg(Color::Green)),
6670
]),
@@ -75,7 +79,7 @@ fn main() -> Result<(), Box<dyn Error>> {
7579

7680
let text = vec![
7781
client_state,
78-
Spans::from(vec![
82+
Line::from(vec![
7983
Span::styled("controls: ", Style::default().add_modifier(Modifier::BOLD)),
8084
Span::raw("up/down = scroll, q = quit"),
8185
]),
@@ -149,21 +153,31 @@ fn main() -> Result<(), Box<dyn Error>> {
149153

150154
// Poll the event queue for input events. `next` will only block for 1 second,
151155
// so our screen is never stale by more than 1 second.
152-
if let Some(input) = events.next()? {
153-
match input {
154-
Key::Char('q') => break,
155-
Key::Up => selector.previous(),
156-
Key::Down => selector.next(),
157-
Key::PageUp => selector.top(),
158-
Key::PageDown => selector.bottom(),
156+
if let Some(input) = InputEvents::next()? {
157+
match input.code {
158+
KeyCode::Char('q') => break,
159+
KeyCode::Up => selector.previous(),
160+
KeyCode::Down => selector.next(),
161+
KeyCode::PageUp => selector.top(),
162+
KeyCode::PageDown => selector.bottom(),
159163
_ => {}
160164
}
161165
}
162166
}
163-
164167
Ok(())
165168
}
166169

170+
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
171+
enable_raw_mode()?;
172+
execute!(io::stdout(), EnterAlternateScreen)?;
173+
Terminal::new(CrosstermBackend::new(io::stdout()))
174+
}
175+
176+
fn restore_terminal() -> io::Result<()> {
177+
disable_raw_mode()?;
178+
execute!(io::stdout(), LeaveAlternateScreen)
179+
}
180+
167181
fn u64_to_displayable(value: u64, unit: Option<Unit>) -> String {
168182
let unit = match unit {
169183
None => return value.to_string(),

metrics-observer/src/selector.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use tui::widgets::ListState;
1+
use ratatui::widgets::ListState;
22

33
pub struct Selector(usize, ListState);
44

metrics/src/common.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ macro_rules! into_f64 {
276276
};
277277
}
278278

279-
pub(self) use into_f64;
279+
use into_f64;
280280

281281
#[cfg(test)]
282282
mod tests {

rust-toolchain.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[toolchain]
2-
channel = "1.70.0"
2+
# Note that this is greater than the MSRV of the workspace (1.70) due to metrics-observer needing
3+
# 1.74, while all the other crates only require 1.70. See
4+
# https://github.com/metrics-rs/metrics/pull/505#discussion_r1724092556 for more information.
5+
channel = "1.74.0"

0 commit comments

Comments
 (0)