diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3aafd30..1eeb1f2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,14 @@ 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 @@ -53,7 +59,7 @@ jobs: - 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: diff --git a/metrics-observer/Cargo.toml b/metrics-observer/Cargo.toml index 57df5494..06415f5b 100644 --- a/metrics-observer/Cargo.toml +++ b/metrics-observer/Cargo.toml @@ -3,7 +3,7 @@ name = "metrics-observer" version = "0.4.0" authors = ["Toby Lawrence "] edition = "2018" -rust-version = "1.70.0" +rust-version = "1.74.0" license = "MIT" @@ -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] diff --git a/metrics-observer/src/input.rs b/metrics-observer/src/input.rs index 65fd27a0..a150c1d6 100644 --- a/metrics-observer/src/input.rs +++ b/metrics-observer/src/input.rs @@ -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, -} +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> { + 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, 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) } } diff --git a/metrics-observer/src/main.rs b/metrics-observer/src/main.rs index 86a71a55..9f079529 100644 --- a/metrics-observer/src/main.rs +++ b/metrics-observer/src/main.rs @@ -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, }; @@ -27,23 +31,23 @@ mod selector; use self::selector::Selector; fn main() -> Result<(), Box> { - 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>) -> Result<(), Box> { 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() { @@ -58,9 +62,9 @@ fn main() -> Result<(), Box> { 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)), ]), @@ -75,7 +79,7 @@ fn main() -> Result<(), Box> { 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"), ]), @@ -149,21 +153,31 @@ fn main() -> Result<(), Box> { // 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>> { + 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) -> String { let unit = match unit { None => return value.to_string(), diff --git a/metrics-observer/src/selector.rs b/metrics-observer/src/selector.rs index 6b7a13a6..8c1a75df 100644 --- a/metrics-observer/src/selector.rs +++ b/metrics-observer/src/selector.rs @@ -1,4 +1,4 @@ -use tui::widgets::ListState; +use ratatui::widgets::ListState; pub struct Selector(usize, ListState); diff --git a/metrics/src/common.rs b/metrics/src/common.rs index 7f3e5c9f..e3bdecd0 100644 --- a/metrics/src/common.rs +++ b/metrics/src/common.rs @@ -276,7 +276,7 @@ macro_rules! into_f64 { }; } -pub(self) use into_f64; +use into_f64; #[cfg(test)] mod tests { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 22048ac5..ee9a0f0f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -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"