Skip to content

Commit

Permalink
Use ratatui instead of tui-rs for the terminal UI (#505)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshka authored Aug 25, 2024
1 parent 4116832 commit c0d2b75
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 58 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,22 @@ jobs:
name: Test ${{ matrix.rust_version }}
runs-on: ubuntu-latest
strategy:
# 1.70 is the MSRV for the project, which currently does not match the version specified in
# the rust-toolchain.toml file as metrics-observer requires 1.74 to build. See
# https://github.com/metrics-rs/metrics/pull/505#discussion_r1724092556 for more information.
matrix:
rust_version: ['stable', 'nightly']
rust_version: ['stable', 'nightly', '1.70']
include:
- rust_version: '1.70'
exclude-packages: '--exclude metrics-observer'
steps:
- uses: actions/checkout@v3
- name: Install Protobuf Compiler
run: sudo apt-get install protobuf-compiler
- name: Install Rust ${{ matrix.rust_version }}
run: rustup install ${{ matrix.rust_version }}
- name: Run Tests
run: cargo +${{ matrix.rust_version }} test --all-features --workspace
run: cargo +${{ matrix.rust_version }} test --all-features --workspace ${{ matrix.exclude-packages }}
docs:
runs-on: ubuntu-latest
env:
Expand Down
5 changes: 2 additions & 3 deletions metrics-observer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "metrics-observer"
version = "0.4.0"
authors = ["Toby Lawrence <[email protected]>"]
edition = "2018"
rust-version = "1.70.0"
rust-version = "1.74.0"

license = "MIT"

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

[build-dependencies]
Expand Down
35 changes: 8 additions & 27 deletions metrics-observer/src/input.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
use std::io;
use std::thread;
use std::time::Duration;

use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, TrySendError};
use termion::event::Key;
use termion::input::TermRead;
use ratatui::crossterm::event::{self, Event, KeyEvent, KeyEventKind};

pub struct InputEvents {
rx: Receiver<Key>,
}
pub struct InputEvents;

impl InputEvents {
pub fn new() -> InputEvents {
let (tx, rx) = bounded(1);
thread::spawn(move || {
let stdin = io::stdin();
for key in stdin.keys().flatten() {
// If our queue is full, we don't care. The user can just press the key again.
if let Err(TrySendError::Disconnected(_)) = tx.try_send(key) {
eprintln!("input event channel disconnected");
return;
}
pub fn next() -> io::Result<Option<KeyEvent>> {
if event::poll(Duration::from_secs(1))? {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => return Ok(Some(key)),
_ => {}
}
});

InputEvents { rx }
}

pub fn next(&mut self) -> Result<Option<Key>, RecvTimeoutError> {
match self.rx.recv_timeout(Duration::from_secs(1)) {
Ok(key) => Ok(Some(key)),
Err(RecvTimeoutError::Timeout) => Ok(None),
Err(e) => Err(e),
}
Ok(None)
}
}
60 changes: 37 additions & 23 deletions metrics-observer/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
use std::fmt;
use std::num::FpCategory;
use std::time::Duration;
use std::{error::Error, io};
use std::{fmt, io::Stdout};

use chrono::Local;
use metrics::Unit;
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::IntoAlternateScreen};
use tui::{
backend::TermionBackend,
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::KeyCode,
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Span, Spans},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
Terminal,
};
Expand All @@ -27,23 +31,23 @@ mod selector;
use self::selector::Selector;

fn main() -> Result<(), Box<dyn Error>> {
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout).into_alternate_screen()?;
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let terminal = init_terminal()?;
let result = run(terminal);
restore_terminal()?;
result
}

let mut events = InputEvents::new();
fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> {
let address = std::env::args().nth(1).unwrap_or_else(|| "127.0.0.1:5000".to_owned());
let client = metrics_inner::Client::new(address);
let mut selector = Selector::new();

loop {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Length(4), Constraint::Percentage(90)].as_ref())
.split(f.size());
.split(f.area());

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

Spans::from(spans)
Line::from(spans)
}
ClientState::Connected => Spans::from(vec![
ClientState::Connected => Line::from(vec![
Span::raw("state: "),
Span::styled("connected", Style::default().fg(Color::Green)),
]),
Expand All @@ -75,7 +79,7 @@ fn main() -> Result<(), Box<dyn Error>> {

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

// Poll the event queue for input events. `next` will only block for 1 second,
// so our screen is never stale by more than 1 second.
if let Some(input) = events.next()? {
match input {
Key::Char('q') => break,
Key::Up => selector.previous(),
Key::Down => selector.next(),
Key::PageUp => selector.top(),
Key::PageDown => selector.bottom(),
if let Some(input) = InputEvents::next()? {
match input.code {
KeyCode::Char('q') => break,
KeyCode::Up => selector.previous(),
KeyCode::Down => selector.next(),
KeyCode::PageUp => selector.top(),
KeyCode::PageDown => selector.bottom(),
_ => {}
}
}
}

Ok(())
}

fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(io::stdout()))
}

fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
execute!(io::stdout(), LeaveAlternateScreen)
}

fn u64_to_displayable(value: u64, unit: Option<Unit>) -> String {
let unit = match unit {
None => return value.to_string(),
Expand Down
2 changes: 1 addition & 1 deletion metrics-observer/src/selector.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use tui::widgets::ListState;
use ratatui::widgets::ListState;

pub struct Selector(usize, ListState);

Expand Down
2 changes: 1 addition & 1 deletion metrics/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ macro_rules! into_f64 {
};
}

pub(self) use into_f64;
use into_f64;

#[cfg(test)]
mod tests {
Expand Down
5 changes: 4 additions & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[toolchain]
channel = "1.70.0"
# Note that this is greater than the MSRV of the workspace (1.70) due to metrics-observer needing
# 1.74, while all the other crates only require 1.70. See
# https://github.com/metrics-rs/metrics/pull/505#discussion_r1724092556 for more information.
channel = "1.74.0"

0 comments on commit c0d2b75

Please sign in to comment.