diff --git a/src/bin/logtail-crossterm.rs b/src/bin/logtail-crossterm.rs index 43c7e78..6920445 100644 --- a/src/bin/logtail-crossterm.rs +++ b/src/bin/logtail-crossterm.rs @@ -3,208 +3,212 @@ //! It is based on logtail-dash, which is a basic logfile dashboard //! and also a framework for similar apps with customised dahsboard //! displays. -//! +//! //! Custom apps based on logtail can be created by creating a //! fork of logtail-dash and modifying the files in src/custom -//! +//! //! See README for more information. -#![recursion_limit="256"] // Prevent select! macro blowing up +#![recursion_limit = "256"] // Prevent select! macro blowing up use linemux::MuxedLines; -use tokio::stream::StreamExt; use std::collections::HashMap; +use tokio::stream::StreamExt; ///! forks of logterm customise the files in src/custom #[path = "../custom/mod.rs"] pub mod custom; -use self::custom::app::{DashState, LogMonitor, DashViewMain}; -use self::custom::opt::{Opt}; -use self::custom::ui::{draw_dashboard}; +use self::custom::app::{DashState, DashViewMain, LogMonitor}; +use self::custom::opt::Opt; +use self::custom::ui::draw_dashboard; ///! logtail and its forks share code in src/ #[path = "../mod.rs"] pub mod shared; -use crate::shared::util::{StatefulList}; - +use crate::shared::util::StatefulList; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use std::{ - error::Error, - io::{stdout, Write}, - sync::mpsc, - thread, - time::{Duration, Instant}, + error::Error, + io::{stdout, Write}, + sync::mpsc, + thread, + time::{Duration, Instant}, }; use tui::{ - backend::CrosstermBackend, - layout::{Constraint, Corner, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Span, Spans, Text}, - widgets::{Widget, Block, BorderType, Borders, List, ListItem}, - Terminal, Frame, + backend::CrosstermBackend, + layout::{Constraint, Corner, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, List, ListItem, Widget}, + Frame, Terminal, }; use futures::{ - future::FutureExt, // for `.fuse()` - pin_mut, - select, + future::FutureExt, // for `.fuse()` + pin_mut, + select, }; enum Event { - Input(I), - Tick, + Input(I), + Tick, } use structopt::StructOpt; // RUSTFLAGS="-A unused" cargo run --bin logtail-crossterm --features="crossterm" /var/log/auth.log /var/log/dmesg #[tokio::main] - pub async fn main() -> Result<(), Box> { - // pub async fn main() -> std::io::Result<()> { - let opt = Opt::from_args(); - - if opt.files.is_empty() { - println!("{}: no logfile(s) specified.", Opt::clap().get_name()); - println!("Try '{} --help' for more information.", Opt::clap().get_name()); - return Ok(()); - } - - let mut dash_state = DashState::new(); - let mut monitors: HashMap = HashMap::new(); - let mut logfiles = MuxedLines::new()?; - - println!("Loading..."); - for f in opt.files { - let mut monitor = LogMonitor::new(f.to_string(), opt.lines_max); - println!("{}", monitor.logfile); - if opt.ignore_existing { - monitors.insert(f.to_string(), monitor); - } else { - match monitor.load_logfile() { - Ok(()) => {monitors.insert(f.to_string(), monitor);}, - Err(e) => { - println!("...failed: {}", e); - return Ok(()); - }, - } - } - match logfiles.add_file(&f).await { - Ok(_) => {}, - Err(e) => { - println!("ERROR: {}", e); - println!("Note: it is ok for the file not to exist, but the file's parent directory must exist."); - return Ok(()); - } - } - } - - // Terminal initialization - enable_raw_mode()?; - let mut stdout = stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - let rx = initialise_events(opt.tick_rate); - terminal.clear()?; - - // Use futures of async functions to handle events - // concurrently with logfile changes. - loop { - terminal.draw(|f| draw_dashboard(f, &dash_state, &mut monitors))?; - let logfiles_future = logfiles.next().fuse(); - let events_future = next_event(&rx).fuse(); - pin_mut!(logfiles_future, events_future); - - select! { - (e) = events_future => { - match e { - Ok(Event::Input(event)) => match event.code { - KeyCode::Char('q')| - KeyCode::Char('Q') => { - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - break Ok(()); - }, - KeyCode::Char('h')| - KeyCode::Char('H') => dash_state.main_view = DashViewMain::DashHorizontal, - KeyCode::Char('v')| - KeyCode::Char('V') => dash_state.main_view = DashViewMain::DashVertical, - _ => {}, - } - - Ok(Event::Tick) => { - // draw_dashboard(&mut f, &dash_state, &mut monitors).unwrap(); - // draw_dashboard(f, &dash_state, &mut monitors)?; - } - - Err(error) => { - println!("{}", error); - } - } - }, - - (line) = logfiles_future => { - match line { - Some(Ok(line)) => { - let source_str = line.source().to_str().unwrap(); - let source = String::from(source_str); - - match monitors.get_mut(&source) { - None => (), - Some(monitor) => monitor.append_to_content(line.line()) - } - }, - Some(Err(e)) => panic!("{}", e), - None => (), - } - }, - } - } +pub async fn main() -> Result<(), Box> { + // pub async fn main() -> std::io::Result<()> { + let opt = Opt::from_args(); + + if opt.files.is_empty() { + println!("{}: no logfile(s) specified.", Opt::clap().get_name()); + println!( + "Try '{} --help' for more information.", + Opt::clap().get_name() + ); + return Ok(()); + } + + let mut dash_state = DashState::new(); + let mut monitors: HashMap = HashMap::new(); + let mut logfiles = MuxedLines::new()?; + + println!("Loading..."); + for f in opt.files { + let mut monitor = LogMonitor::new(f.to_string(), opt.lines_max); + println!("{}", monitor.logfile); + if opt.ignore_existing { + monitors.insert(f.to_string(), monitor); + } else { + match monitor.load_logfile() { + Ok(()) => { + monitors.insert(f.to_string(), monitor); + } + Err(e) => { + println!("...failed: {}", e); + return Ok(()); + } + } + } + match logfiles.add_file(&f).await { + Ok(_) => {} + Err(e) => { + println!("ERROR: {}", e); + println!("Note: it is ok for the file not to exist, but the file's parent directory must exist."); + return Ok(()); + } + } + } + + // Terminal initialization + enable_raw_mode()?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + let rx = initialise_events(opt.tick_rate); + terminal.clear()?; + + // Use futures of async functions to handle events + // concurrently with logfile changes. + loop { + terminal.draw(|f| draw_dashboard(f, &dash_state, &mut monitors))?; + let logfiles_future = logfiles.next().fuse(); + let events_future = next_event(&rx).fuse(); + pin_mut!(logfiles_future, events_future); + + select! { + (e) = events_future => { + match e { + Ok(Event::Input(event)) => match event.code { + KeyCode::Char('q')| + KeyCode::Char('Q') => { + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + break Ok(()); + }, + KeyCode::Char('h')| + KeyCode::Char('H') => dash_state.main_view = DashViewMain::DashHorizontal, + KeyCode::Char('v')| + KeyCode::Char('V') => dash_state.main_view = DashViewMain::DashVertical, + _ => {}, + } + + Ok(Event::Tick) => { + // draw_dashboard(&mut f, &dash_state, &mut monitors).unwrap(); + // draw_dashboard(f, &dash_state, &mut monitors)?; + } + + Err(error) => { + println!("{}", error); + } + } + }, + + (line) = logfiles_future => { + match line { + Some(Ok(line)) => { + let source_str = line.source().to_str().unwrap(); + let source = String::from(source_str); + + match monitors.get_mut(&source) { + None => (), + Some(monitor) => monitor.append_to_content(line.line()) + } + }, + Some(Err(e)) => panic!("{}", e), + None => (), + } + }, + } + } } // type Tx = std::sync::mpsc::Sender>; type Rx = std::sync::mpsc::Receiver>; fn initialise_events(tick_rate: u64) -> Rx { - let tick_rate = Duration::from_millis(tick_rate); - let (tx, rx) = mpsc::channel(); // Setup input handling - - thread::spawn(move || { - let mut last_tick = Instant::now(); - loop { - // poll for tick rate duration, if no events, sent tick event. - if event::poll(tick_rate - last_tick.elapsed()).unwrap() { - if let CEvent::Key(key) = event::read().unwrap() { - tx.send(Event::Input(key)).unwrap(); - } - } - if last_tick.elapsed() >= tick_rate { - tx.send(Event::Tick).unwrap(); // <-- PANICS HERE - last_tick = Instant::now(); - } - - if last_tick.elapsed() >= tick_rate { - match tx.send(Event::Tick) { - Ok(()) => last_tick = Instant::now(), - Err(e) => println!("send error: {}", e) - } - } - } - }); - rx + let tick_rate = Duration::from_millis(tick_rate); + let (tx, rx) = mpsc::channel(); // Setup input handling + + thread::spawn(move || { + let mut last_tick = Instant::now(); + loop { + // poll for tick rate duration, if no events, sent tick event. + if event::poll(tick_rate - last_tick.elapsed()).unwrap() { + if let CEvent::Key(key) = event::read().unwrap() { + tx.send(Event::Input(key)).unwrap(); + } + } + if last_tick.elapsed() >= tick_rate { + tx.send(Event::Tick).unwrap(); // <-- PANICS HERE + last_tick = Instant::now(); + } + + if last_tick.elapsed() >= tick_rate { + match tx.send(Event::Tick) { + Ok(()) => last_tick = Instant::now(), + Err(e) => println!("send error: {}", e), + } + } + } + }); + rx } async fn next_event(rx: &Rx) -> Result, mpsc::RecvError> { - rx.recv() + rx.recv() } diff --git a/src/bin/logtail-termion.rs b/src/bin/logtail-termion.rs index ec94d35..be667fc 100644 --- a/src/bin/logtail-termion.rs +++ b/src/bin/logtail-termion.rs @@ -3,16 +3,16 @@ //! It is based on logtail-dash, which is a basic logfile dashboard //! and also a framework for similar apps with customised dahsboard //! displays. -//! +//! //! Custom apps based on logtail can be created by creating a //! fork of logtail-dash and modifying the files in src/custom -//! +//! //! See README for more information. -#![recursion_limit="256"] // Prevent select! macro blowing up +#![recursion_limit = "256"] // Prevent select! macro blowing up -use std::io; use std::collections::HashMap; +use std::io; use linemux::MuxedLines; use tokio::stream::StreamExt; @@ -20,140 +20,149 @@ use tokio::stream::StreamExt; ///! forks of logterm customise the files in src/custom #[path = "../custom/mod.rs"] pub mod custom; -use self::custom::app::{DashState, LogMonitor, DashViewMain}; -use self::custom::opt::{Opt}; -use self::custom::ui::{draw_dashboard}; +use self::custom::app::{DashState, DashViewMain, LogMonitor}; +use self::custom::opt::Opt; +use self::custom::ui::draw_dashboard; ///! logtail and its forks share code in src/ #[path = "../mod.rs"] pub mod shared; +use crate::shared::util::StatefulList; use shared::event::{Event, Events}; -use crate::shared::util::{StatefulList}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::Backend, - backend::TermionBackend, - layout::{Constraint, Corner, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Span, Spans, Text}, - widgets::{Widget, Block, BorderType, Borders, List, ListItem}, - Terminal, Frame, + backend::Backend, + backend::TermionBackend, + layout::{Constraint, Corner, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, List, ListItem, Widget}, + Frame, Terminal, }; -type TuiTerminal = tui::terminal::Terminal>>>>; +type TuiTerminal = tui::terminal::Terminal< + TermionBackend< + termion::screen::AlternateScreen< + termion::input::MouseTerminal>, + >, + >, +>; use std::fs::File; use std::io::{BufRead, BufReader}; use structopt::StructOpt; use futures::{ - future::FutureExt, // for `.fuse()` - pin_mut, - select, + future::FutureExt, // for `.fuse()` + pin_mut, + select, }; #[tokio::main] pub async fn main() -> std::io::Result<()> { - let opt = Opt::from_args(); - - if opt.files.is_empty() { - println!("{}: no logfile(s) specified.", Opt::clap().get_name()); - println!("Try '{} --help' for more information.", Opt::clap().get_name()); - return Ok(()); - } - - let mut dash_state = DashState::new(); - let events = Events::new(); - let mut monitors: HashMap = HashMap::new(); - let mut logfiles = MuxedLines::new()?; - - println!("Loading..."); - for f in opt.files { - let mut monitor = LogMonitor::new(f.to_string(), opt.lines_max); - println!("{}", monitor.logfile); - if opt.ignore_existing { - monitors.insert(f.to_string(), monitor); - } else { - match monitor.load_logfile() { - Ok(()) => {monitors.insert(f.to_string(), monitor);}, - Err(e) => { - println!("...failed: {}", e); - return Ok(()); - }, - } - } - match logfiles.add_file(&f).await { - Ok(_) => println!("{} done.", &f), - Err(e) => { - println!("ERROR: {}", e); - println!("Note: it is ok for the file not to exist, but the file's parent directory must exist."); - return Ok(()); - } - } - } - - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - - // Use futures of async functions to handle events - // concurrently with logfile changes. - loop { - let events_future = next_event(&events).fuse(); - let logfiles_future = logfiles.next().fuse(); - pin_mut!(events_future, logfiles_future); - - select! { - (e) = events_future => { - match e { - Ok(Event::Input(input)) => { - match input { - Key::Char('q')| - Key::Char('Q') => return Ok(()), - Key::Char('h')| - Key::Char('H') => dash_state.main_view = DashViewMain::DashHorizontal, - Key::Char('v')| - Key::Char('V') => dash_state.main_view = DashViewMain::DashVertical, - _ => {}, - } - } - - Ok(Event::Tick) => { - terminal.draw(|f| draw_dashboard(f, &dash_state, &mut monitors))?; - } - - Err(error) => { - println!("{}", error); - } - } - }, - (line) = logfiles_future => { - match line { - Some(Ok(line)) => { - let source_str = line.source().to_str().unwrap(); - let source = String::from(source_str); - - match monitors.get_mut(&source) { - None => (), - Some(monitor) => monitor.append_to_content(line.line()) - } - }, - Some(Err(e)) => panic!("{}", e), - None => (), - } - }, - } - } + let opt = Opt::from_args(); + + if opt.files.is_empty() { + println!("{}: no logfile(s) specified.", Opt::clap().get_name()); + println!( + "Try '{} --help' for more information.", + Opt::clap().get_name() + ); + return Ok(()); + } + + let mut dash_state = DashState::new(); + let events = Events::new(); + let mut monitors: HashMap = HashMap::new(); + let mut logfiles = MuxedLines::new()?; + + println!("Loading..."); + for f in opt.files { + let mut monitor = LogMonitor::new(f.to_string(), opt.lines_max); + println!("{}", monitor.logfile); + if opt.ignore_existing { + monitors.insert(f.to_string(), monitor); + } else { + match monitor.load_logfile() { + Ok(()) => { + monitors.insert(f.to_string(), monitor); + } + Err(e) => { + println!("...failed: {}", e); + return Ok(()); + } + } + } + match logfiles.add_file(&f).await { + Ok(_) => println!("{} done.", &f), + Err(e) => { + println!("ERROR: {}", e); + println!("Note: it is ok for the file not to exist, but the file's parent directory must exist."); + return Ok(()); + } + } + } + + // Terminal initialization + let stdout = io::stdout().into_raw_mode()?; + let stdout = MouseTerminal::from(stdout); + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Use futures of async functions to handle events + // concurrently with logfile changes. + loop { + let events_future = next_event(&events).fuse(); + let logfiles_future = logfiles.next().fuse(); + pin_mut!(events_future, logfiles_future); + + select! { + (e) = events_future => { + match e { + Ok(Event::Input(input)) => { + match input { + Key::Char('q')| + Key::Char('Q') => return Ok(()), + Key::Char('h')| + Key::Char('H') => dash_state.main_view = DashViewMain::DashHorizontal, + Key::Char('v')| + Key::Char('V') => dash_state.main_view = DashViewMain::DashVertical, + _ => {}, + } + } + + Ok(Event::Tick) => { + terminal.draw(|f| draw_dashboard(f, &dash_state, &mut monitors))?; + } + + Err(error) => { + println!("{}", error); + } + } + }, + (line) = logfiles_future => { + match line { + Some(Ok(line)) => { + let source_str = line.source().to_str().unwrap(); + let source = String::from(source_str); + + match monitors.get_mut(&source) { + None => (), + Some(monitor) => monitor.append_to_content(line.line()) + } + }, + Some(Err(e)) => panic!("{}", e), + None => (), + } + }, + } + } } use std::sync::mpsc; async fn next_event(events: &Events) -> Result, mpsc::RecvError> { - events.next() + events.next() } - diff --git a/src/custom/app.rs b/src/custom/app.rs index 1ecc0ec..da78f45 100644 --- a/src/custom/app.rs +++ b/src/custom/app.rs @@ -1,91 +1,96 @@ ///! Application logic ///! ///! Edit src/custom/app.rs to create a customised fork of logtail-dash - use std::fs::File; -use crate::shared::util::{StatefulList}; +use crate::shared::util::StatefulList; pub struct LogMonitor { - pub index: usize, - pub content: StatefulList, - pub logfile: String, + pub index: usize, + pub content: StatefulList, + pub logfile: String, - max_content: usize, // Limit number of lines in content + max_content: usize, // Limit number of lines in content } use std::sync::atomic::{AtomicUsize, Ordering}; static NEXT_MONITOR: AtomicUsize = AtomicUsize::new(0); impl LogMonitor { - pub fn new(f: String, max_lines: usize) -> LogMonitor { - let index = NEXT_MONITOR.fetch_add(1, Ordering::Relaxed); - LogMonitor { - index, - logfile: f, - max_content: max_lines, - content: StatefulList::with_items(vec![]), - } - } - - pub fn load_logfile(&mut self) -> std::io::Result<()> { - use std::io::{BufRead, BufReader}; - - let f = File::open(self.logfile.to_string()); - let f = match f { - Ok(file) => file, - Err(_e) => return Ok(()), // It's ok for a logfile not to exist yet - }; - - let f = BufReader::new(f); - - for line in f.lines() { - let line = line.expect("Unable to read line"); - self.process_line(&line); - } - - Ok(()) - } - - pub fn process_line(&mut self, text: &str) { - // TODO parse and update metrics - self.append_to_content(text); - } - - pub fn append_to_content(&mut self, text: &str) { - self.content.items.push(text.to_string()); - if self.content.items.len() > self.max_content { - self.content.items = self.content.items.split_off(self.content.items.len() - self.max_content); - } - } - - fn _reset_metrics(&mut self) {} + pub fn new(f: String, max_lines: usize) -> LogMonitor { + let index = NEXT_MONITOR.fetch_add(1, Ordering::Relaxed); + LogMonitor { + index, + logfile: f, + max_content: max_lines, + content: StatefulList::with_items(vec![]), + } + } + + pub fn load_logfile(&mut self) -> std::io::Result<()> { + use std::io::{BufRead, BufReader}; + + let f = File::open(self.logfile.to_string()); + let f = match f { + Ok(file) => file, + Err(_e) => return Ok(()), // It's ok for a logfile not to exist yet + }; + + let f = BufReader::new(f); + + for line in f.lines() { + let line = line.expect("Unable to read line"); + self.process_line(&line); + } + + Ok(()) + } + + pub fn process_line(&mut self, text: &str) { + // TODO parse and update metrics + self.append_to_content(text); + } + + pub fn append_to_content(&mut self, text: &str) { + self.content.items.push(text.to_string()); + if self.content.items.len() > self.max_content { + self.content.items = self + .content + .items + .split_off(self.content.items.len() - self.max_content); + } + } + + fn _reset_metrics(&mut self) {} } -pub enum DashViewMain {DashHorizontal, DashVertical} +pub enum DashViewMain { + DashHorizontal, + DashVertical, +} pub struct DashState { - pub main_view: DashViewMain, + pub main_view: DashViewMain, - // For DashViewMain::DashVertical - dash_vertical: DashVertical, + // For DashViewMain::DashVertical + dash_vertical: DashVertical, } impl DashState { - pub fn new() -> DashState { - DashState { - main_view: DashViewMain::DashHorizontal, - dash_vertical: DashVertical::new(), - } - } + pub fn new() -> DashState { + DashState { + main_view: DashViewMain::DashHorizontal, + dash_vertical: DashVertical::new(), + } + } } pub struct DashVertical { - active_view: usize, + active_view: usize, } impl DashVertical { - pub fn new() -> Self { - DashVertical { active_view: 0, } - } + pub fn new() -> Self { + DashVertical { active_view: 0 } + } } diff --git a/src/custom/opt.rs b/src/custom/opt.rs index 084331f..6701baf 100644 --- a/src/custom/opt.rs +++ b/src/custom/opt.rs @@ -9,20 +9,19 @@ pub use structopt::StructOpt; #[derive(StructOpt, Debug)] #[structopt(about = "Monitor multiple logfiles in the terminal.")] pub struct Opt { - /// Maximum number of lines to keep for each logfile - #[structopt(short = "l", long, default_value = "100")] - pub lines_max: usize, + /// Maximum number of lines to keep for each logfile + #[structopt(short = "l", long, default_value = "100")] + pub lines_max: usize, - /// Time between ticks in milliseconds - #[structopt(short, long, default_value = "200")] - pub tick_rate: u64, + /// Time between ticks in milliseconds + #[structopt(short, long, default_value = "200")] + pub tick_rate: u64, - /// Ignore any existing logfile content - #[structopt(short, long)] - pub ignore_existing: bool, + /// Ignore any existing logfile content + #[structopt(short, long)] + pub ignore_existing: bool, - /// One or more logfiles to monitor - #[structopt(name = "LOGFILE")] - pub files: Vec, + /// One or more logfiles to monitor + #[structopt(name = "LOGFILE")] + pub files: Vec, } - diff --git a/src/custom/ui.rs b/src/custom/ui.rs index e3abc9d..1d74474 100644 --- a/src/custom/ui.rs +++ b/src/custom/ui.rs @@ -1,105 +1,141 @@ ///! Terminal based interface and dashboard ///! ///! Edit src/custom/ui.rs to create a customised fork of logtail-dash - -use super::app::{DashState, LogMonitor, DashViewMain}; +use super::app::{DashState, DashViewMain, LogMonitor}; use std::collections::HashMap; use tui::{ - backend::Backend, - layout::{Constraint, Corner, Direction, Layout}, - style::{Color, Modifier, Style}, - text::{Span, Spans, Text}, - widgets::{Widget, Block, BorderType, Borders, List, ListItem}, - Terminal, Frame, + backend::Backend, + layout::{Constraint, Corner, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, BorderType, Borders, List, ListItem, Widget}, + Frame, Terminal, }; - -pub fn draw_dashboard(f: &mut Frame, - dash_state: &DashState, - monitors: &mut HashMap) { - match dash_state.main_view { - DashViewMain::DashHorizontal => draw_dash_horizontal(f, dash_state, monitors), - DashViewMain::DashVertical => draw_dash_vertical(f, dash_state, monitors), - } +pub fn draw_dashboard( + f: &mut Frame, + dash_state: &DashState, + monitors: &mut HashMap, +) { + match dash_state.main_view { + DashViewMain::DashHorizontal => draw_dash_horizontal(f, dash_state, monitors), + DashViewMain::DashVertical => draw_dash_vertical(f, dash_state, monitors), + } } -fn draw_dash_horizontal(f: &mut Frame, - dash_state: &DashState, - monitors: &mut HashMap) { - - let constraints = make_percentage_constraints(monitors.len()); - - let size = f.size(); - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints(constraints.as_ref()) - .split(size); - - for (logfile, monitor) in monitors.iter_mut() { - let len = monitor.content.items.len(); - if len > 0 {monitor.content.state.select(Some(monitor.content.items.len()-1));} - - let items: Vec = monitor.content.items.iter().map(|s| { - ListItem::new(vec![Spans::from(s.clone())]).style(Style::default().fg(Color::Black).bg(Color::White)) - }) - .collect(); - - let monitor_widget = List::new(items) - .block(Block::default().borders(Borders::ALL).title(logfile.clone())) - .highlight_style( - Style::default() - .bg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ); - f.render_stateful_widget(monitor_widget,chunks[monitor.index], &mut monitor.content.state); - } +fn draw_dash_horizontal( + f: &mut Frame, + dash_state: &DashState, + monitors: &mut HashMap, +) { + let constraints = make_percentage_constraints(monitors.len()); + + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(constraints.as_ref()) + .split(size); + + for (logfile, monitor) in monitors.iter_mut() { + let len = monitor.content.items.len(); + if len > 0 { + monitor + .content + .state + .select(Some(monitor.content.items.len() - 1)); + } + + let items: Vec = monitor + .content + .items + .iter() + .map(|s| { + ListItem::new(vec![Spans::from(s.clone())]) + .style(Style::default().fg(Color::Black).bg(Color::White)) + }) + .collect(); + + let monitor_widget = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title(logfile.clone()), + ) + .highlight_style( + Style::default() + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ); + f.render_stateful_widget( + monitor_widget, + chunks[monitor.index], + &mut monitor.content.state, + ); + } } -fn draw_dash_vertical(f: &mut Frame, - dash_state: &DashState, - monitors: &mut HashMap) { - - let constraints = make_percentage_constraints(monitors.len()); - let size = f.size(); - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints(constraints.as_ref()) - .split(size); - - for (logfile, monitor) in monitors.iter_mut() { - monitor.content.state.select(Some(monitor.content.items.len()-1)); - let items: Vec = monitor.content.items.iter().map(|s| { - ListItem::new(vec![Spans::from(s.clone())]).style(Style::default().fg(Color::Black).bg(Color::White)) - }) - .collect(); - - let monitor_widget = List::new(items) - .block(Block::default().borders(Borders::ALL).title(logfile.clone())) - .highlight_style( - Style::default() - .bg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ); - f.render_stateful_widget(monitor_widget,chunks[monitor.index], &mut monitor.content.state); - } +fn draw_dash_vertical( + f: &mut Frame, + dash_state: &DashState, + monitors: &mut HashMap, +) { + let constraints = make_percentage_constraints(monitors.len()); + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints.as_ref()) + .split(size); + + for (logfile, monitor) in monitors.iter_mut() { + monitor + .content + .state + .select(Some(monitor.content.items.len() - 1)); + let items: Vec = monitor + .content + .items + .iter() + .map(|s| { + ListItem::new(vec![Spans::from(s.clone())]) + .style(Style::default().fg(Color::Black).bg(Color::White)) + }) + .collect(); + + let monitor_widget = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title(logfile.clone()), + ) + .highlight_style( + Style::default() + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ); + f.render_stateful_widget( + monitor_widget, + chunks[monitor.index], + &mut monitor.content.state, + ); + } } fn make_percentage_constraints(count: usize) -> Vec { - let percent = if count > 0 { 100 / count as u16 } else { 0 }; - let mut constraints = Vec::new(); - let mut total_percent = 0; - - for i in 1..count+1 { - total_percent += percent; - - let next_percent = if i == count && total_percent < 100 - { 100 - total_percent } else { percent }; - - constraints.push(Constraint::Percentage(next_percent)); - } - constraints + let percent = if count > 0 { 100 / count as u16 } else { 0 }; + let mut constraints = Vec::new(); + let mut total_percent = 0; + + for i in 1..count + 1 { + total_percent += percent; + + let next_percent = if i == count && total_percent < 100 { + 100 - total_percent + } else { + percent + }; + + constraints.push(Constraint::Percentage(next_percent)); + } + constraints } diff --git a/src/event.rs b/src/event.rs index 333096a..41188d7 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,8 +1,8 @@ use std::io; use std::sync::mpsc; use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, + atomic::{AtomicBool, Ordering}, + Arc, }; use std::thread; use std::time::Duration; @@ -11,85 +11,85 @@ use termion::event::Key; use termion::input::TermRead; pub enum Event { - Input(I), - Tick, + Input(I), + Tick, } /// A small event handler that wrap termion input and tick events. Each event /// type is handled in its own thread and returned to a common `Receiver` pub struct Events { - rx: mpsc::Receiver>, - input_handle: thread::JoinHandle<()>, - ignore_exit_key: Arc, - tick_handle: thread::JoinHandle<()>, + rx: mpsc::Receiver>, + input_handle: thread::JoinHandle<()>, + ignore_exit_key: Arc, + tick_handle: thread::JoinHandle<()>, } #[derive(Debug, Clone, Copy)] pub struct Config { - pub exit_key: Key, - pub tick_rate: Duration, + pub exit_key: Key, + pub tick_rate: Duration, } impl Default for Config { - fn default() -> Config { - Config { - exit_key: Key::Char('q'), - tick_rate: Duration::from_millis(250), - } - } + fn default() -> Config { + Config { + exit_key: Key::Char('q'), + tick_rate: Duration::from_millis(250), + } + } } impl Events { - pub fn new() -> Events { - Events::with_config(Config::default()) - } + pub fn new() -> Events { + Events::with_config(Config::default()) + } - pub fn with_config(config: Config) -> Events { - let (tx, rx) = mpsc::channel(); - let ignore_exit_key = Arc::new(AtomicBool::new(false)); - let input_handle = { - let tx = tx.clone(); - let ignore_exit_key = ignore_exit_key.clone(); - thread::spawn(move || { - let stdin = io::stdin(); - for evt in stdin.keys() { - if let Ok(key) = evt { - if let Err(err) = tx.send(Event::Input(key)) { - eprintln!("{}", err); - return; - } - if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { - return; - } - } - } - }) - }; - let tick_handle = { - thread::spawn(move || loop { - if tx.send(Event::Tick).is_err() { - break; - } - thread::sleep(config.tick_rate); - }) - }; - Events { - rx, - ignore_exit_key, - input_handle, - tick_handle, - } - } + pub fn with_config(config: Config) -> Events { + let (tx, rx) = mpsc::channel(); + let ignore_exit_key = Arc::new(AtomicBool::new(false)); + let input_handle = { + let tx = tx.clone(); + let ignore_exit_key = ignore_exit_key.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for evt in stdin.keys() { + if let Ok(key) = evt { + if let Err(err) = tx.send(Event::Input(key)) { + eprintln!("{}", err); + return; + } + if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { + return; + } + } + } + }) + }; + let tick_handle = { + thread::spawn(move || loop { + if tx.send(Event::Tick).is_err() { + break; + } + thread::sleep(config.tick_rate); + }) + }; + Events { + rx, + ignore_exit_key, + input_handle, + tick_handle, + } + } - pub fn next(&self) -> Result, mpsc::RecvError> { - self.rx.recv() - } + pub fn next(&self) -> Result, mpsc::RecvError> { + self.rx.recv() + } - pub fn disable_exit_key(&mut self) { - self.ignore_exit_key.store(true, Ordering::Relaxed); - } + pub fn disable_exit_key(&mut self) { + self.ignore_exit_key.store(true, Ordering::Relaxed); + } - pub fn enable_exit_key(&mut self) { - self.ignore_exit_key.store(false, Ordering::Relaxed); - } + pub fn enable_exit_key(&mut self) { + self.ignore_exit_key.store(false, Ordering::Relaxed); + } } diff --git a/src/util.rs b/src/util.rs index b7573cc..4a5f184 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,125 +4,125 @@ use tui::widgets::ListState; #[derive(Clone)] pub struct RandomSignal { - distribution: Uniform, - rng: ThreadRng, + distribution: Uniform, + rng: ThreadRng, } impl RandomSignal { - pub fn new(lower: u64, upper: u64) -> RandomSignal { - RandomSignal { - distribution: Uniform::new(lower, upper), - rng: rand::thread_rng(), - } - } + pub fn new(lower: u64, upper: u64) -> RandomSignal { + RandomSignal { + distribution: Uniform::new(lower, upper), + rng: rand::thread_rng(), + } + } } impl Iterator for RandomSignal { - type Item = u64; - fn next(&mut self) -> Option { - Some(self.distribution.sample(&mut self.rng)) - } + type Item = u64; + fn next(&mut self) -> Option { + Some(self.distribution.sample(&mut self.rng)) + } } #[derive(Clone)] pub struct SinSignal { - x: f64, - interval: f64, - period: f64, - scale: f64, + x: f64, + interval: f64, + period: f64, + scale: f64, } impl SinSignal { - pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal { - SinSignal { - x: 0.0, - interval, - period, - scale, - } - } + pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal { + SinSignal { + x: 0.0, + interval, + period, + scale, + } + } } impl Iterator for SinSignal { - type Item = (f64, f64); - fn next(&mut self) -> Option { - let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale); - self.x += self.interval; - Some(point) - } + type Item = (f64, f64); + fn next(&mut self) -> Option { + let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale); + self.x += self.interval; + Some(point) + } } pub struct TabsState<'a> { - pub titles: Vec<&'a str>, - pub index: usize, + pub titles: Vec<&'a str>, + pub index: usize, } impl<'a> TabsState<'a> { - pub fn new(titles: Vec<&'a str>) -> TabsState { - TabsState { titles, index: 0 } - } - pub fn next(&mut self) { - self.index = (self.index + 1) % self.titles.len(); - } + pub fn new(titles: Vec<&'a str>) -> TabsState { + TabsState { titles, index: 0 } + } + pub fn next(&mut self) { + self.index = (self.index + 1) % self.titles.len(); + } - pub fn previous(&mut self) { - if self.index > 0 { - self.index -= 1; - } else { - self.index = self.titles.len() - 1; - } - } + pub fn previous(&mut self) { + if self.index > 0 { + self.index -= 1; + } else { + self.index = self.titles.len() - 1; + } + } } pub struct StatefulList { - pub state: ListState, - pub items: Vec, + pub state: ListState, + pub items: Vec, } impl StatefulList { - pub fn new() -> StatefulList { - StatefulList { - state: ListState::default(), - items: Vec::new(), - } - } + pub fn new() -> StatefulList { + StatefulList { + state: ListState::default(), + items: Vec::new(), + } + } - pub fn with_items(items: Vec) -> StatefulList { - StatefulList { - state: ListState::default(), - items, - } - } + pub fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items, + } + } - pub fn next(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i >= self.items.len() - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } - pub fn previous(&mut self) { - let i = match self.state.selected() { - Some(i) => { - if i == 0 { - self.items.len() - 1 - } else { - i - 1 - } - } - None => 0, - }; - self.state.select(Some(i)); - } + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } - pub fn unselect(&mut self) { - self.state.select(None); - } + pub fn unselect(&mut self) { + self.state.select(None); + } }