diff --git a/README.md b/README.md index dab758b..c0dba8b 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,123 @@ can either choose from: - [termion](https://github.com/ticki/termion) - [rustbox](https://github.com/gchp/rustbox) +However, some features may only be available in one of the two. + +The library is based on the principle of immediate rendering with intermediate +buffers. This means that at each new frame you are meant to issue a call for +each widget that is part of the UI. While providing a great flexibility for rich +and interactive UI, this may introduce overhead for highly dynamic content. So, the +implementation try to minimize the number of ansi escapes sequences outputed to +draw the updated UI. In practice, given the speed of rust the overhead rather +comes from the terminal emulator than the library itself. + +Moreover, the library does not provide any input handling nor any event system and +you may rely on the previously cited libraries to achieve such features. ## Cargo.toml ```toml [dependencies] -tui: "0.1" +tui = "0.1" +``` + +## Get Started + +### Create the terminal interface + +The first thing to do is to choose from one of the two backends: + +For Termion: + +```rust +use tui::{Terminal, TermionBackend}; + +fn main() { + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend); +} +``` + +For Rustbox: + +```rust +use tui::{Terminal, RustboxBackend}; + +fn main() { + let backend = RustboxBackend::new().unwrap(); + let mut terminal = Terminal::new(backend); +} ``` +### Layout + +The library comes with a basic yet useful layout management object called +`Group`. As you may see below and in the examples, the library makes heavy use +of the builder pattern to provide full customization. And the `Group` object is +no exception: + +```rust +use tui::widgets::{Block, border}; +use tui::layout::{Group, Rect, Direction}; + +fn draw(t: &mut Terminal) { + + let size = t.size().unwrap(); + + Group::default() + /// You first choose a main direction for the group + .direction(Direction::Vertical) + /// An optional margin + .margin(1) + /// The preferred sizes (heights in this case) + .sizes(&[Size::Fixed(10), Size::Max(20), Size::Min(10)]) + /// The computed (or cached) layout is then available as the second argument + /// of the closure + .render(t, &size, |t, chunks| { + /// Continue to describe your UI there. + /// Examples: + Block::default() + .title("Block") + .borders(border::ALL) + .render(t, &chunks[0]); + }) +``` + +This let you describe responsive terminal UI by nesting groups. You should note +that by default the computed layout tries to fill the available space +completely. So if for any reason you might need a blank space somewhere, try to +pass an additional size to the group and don't use the corresponding area inside +the render method. + +Once you have finished to describe the UI, you just need to call: + +```rust +t.draw().unwrap() +``` + +to actually draw to the terminal. + +### Widgets + +The library comes with the following list of widgets: + + * [Block](examples/block.rs) + * [Gauge](examples/gauge.rs) + * [Sparkline](examples/sparkline.rs) + * [Chart](examples/chart.rs) + * [BarChart](examples/bar_chart.rs) + * [List](examples/list.rs) + * [Table](examples/table.rs) + * [Paragraph](examples/paragraph.rs) + * [Canvas (with line, point cloud, map)](examples/canvas.rs) + * [Tabs](examples/tabs.rs) + +Click on each item to get an example. + +### Demo + +The [source code](examples/demo.rs) of the source gif. + ## License [MIT](LICENSE) diff --git a/docs/demo.gif b/docs/demo.gif index 290f8f8..7289ebf 100644 Binary files a/docs/demo.gif and b/docs/demo.gif differ diff --git a/examples/barchart.rs b/examples/barchart.rs new file mode 100644 index 0000000..3e667cc --- /dev/null +++ b/examples/barchart.rs @@ -0,0 +1,159 @@ +extern crate tui; +extern crate termion; + +use std::io; +use std::thread; +use std::time; +use std::sync::mpsc; + +use termion::event; +use termion::input::TermRead; + +use tui::{Terminal, TermionBackend}; +use tui::widgets::{Widget, Block, border, BarChart}; +use tui::layout::{Group, Direction, Size}; +use tui::style::{Style, Color, Modifier}; + +struct App<'a> { + data: Vec<(&'a str, u64)>, +} + +impl<'a> App<'a> { + fn new() -> App<'a> { + App { + data: vec![("B1", 9), + ("B2", 12), + ("B3", 5), + ("B4", 8), + ("B5", 2), + ("B6", 4), + ("B7", 5), + ("B8", 9), + ("B9", 14), + ("B10", 15), + ("B11", 1), + ("B12", 0), + ("B13", 4), + ("B14", 6), + ("B15", 4), + ("B16", 6), + ("B17", 4), + ("B18", 7), + ("B19", 13), + ("B20", 8), + ("B21", 11), + ("B22", 9), + ("B23", 3), + ("B24", 5)], + } + } + + fn advance(&mut self) { + let value = self.data.pop().unwrap(); + self.data.insert(0, value); + } +} + +enum Event { + Input(event::Key), + Tick, +} + +fn main() { + // Terminal initialization + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend).unwrap(); + + // Channels + let (tx, rx) = mpsc::channel(); + let input_tx = tx.clone(); + let clock_tx = tx.clone(); + + // Input + thread::spawn(move || { + let stdin = io::stdin(); + for c in stdin.keys() { + let evt = c.unwrap(); + input_tx.send(Event::Input(evt)).unwrap(); + if evt == event::Key::Char('q') { + break; + } + } + }); + + // Tick + thread::spawn(move || { + loop { + clock_tx.send(Event::Tick).unwrap(); + thread::sleep(time::Duration::from_millis(500)); + } + }); + + // App + let mut app = App::new(); + + // First draw call + terminal.clear().unwrap(); + terminal.hide_cursor().unwrap(); + draw(&mut terminal, &app); + + loop { + let evt = rx.recv().unwrap(); + match evt { + Event::Input(input) => { + if input == event::Key::Char('q') { + break; + } + } + Event::Tick => { + app.advance(); + } + } + draw(&mut terminal, &app); + } + + terminal.show_cursor().unwrap(); +} + +fn draw(t: &mut Terminal, app: &App) { + + let size = t.size().unwrap(); + + Group::default() + .direction(Direction::Vertical) + .margin(2) + .sizes(&[Size::Percent(50), Size::Percent(50)]) + .render(t, &size, |t, chunks| { + BarChart::default() + .block(Block::default().title("Data1").borders(border::ALL)) + .data(&app.data) + .bar_width(9) + .style(Style::default().fg(Color::Yellow)) + .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)) + .render(t, &chunks[0]); + Group::default() + .direction(Direction::Horizontal) + .sizes(&[Size::Percent(50), Size::Percent(50)]) + .render(t, &chunks[1], |t, chunks| { + BarChart::default() + .block(Block::default().title("Data2").borders(border::ALL)) + .data(&app.data) + .bar_width(5) + .bar_gap(3) + .style(Style::default().fg(Color::Green)) + .value_style(Style::default().bg(Color::Green).modifier(Modifier::Bold)) + .render(t, &chunks[0]); + BarChart::default() + .block(Block::default().title("Data3").borders(border::ALL)) + .data(&app.data) + .style(Style::default().fg(Color::Red)) + .bar_width(7) + .bar_gap(0) + .value_style(Style::default().bg(Color::Red)) + .label_style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic)) + .render(t, &chunks[1]); + }) + }); + + t.draw().unwrap(); +} diff --git a/examples/block.rs b/examples/block.rs index 6817a07..7e7d19f 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -8,7 +8,7 @@ use termion::input::TermRead; use tui::{Terminal, TermionBackend}; use tui::widgets::{Widget, Block, border}; use tui::layout::{Group, Direction, Size}; -use tui::style::{Style, Color}; +use tui::style::{Style, Color, Modifier}; fn main() { let mut terminal = Terminal::new(TermionBackend::new().unwrap()).unwrap(); @@ -30,41 +30,48 @@ fn draw(t: &mut Terminal) { let size = t.size().unwrap(); + // Wrapping block for a group + // Just draw the block and the group on the same area and build the group + // with at least a margin of 1 + Block::default() + .borders(border::ALL) + .render(t, &size); Group::default() .direction(Direction::Vertical) - .sizes(&[Size::Fixed(7), Size::Min(0), Size::Fixed(7)]) + .margin(4) + .sizes(&[Size::Percent(50), Size::Percent(50)]) .render(t, &size, |t, chunks| { - Block::default() - .title("Top") - .title_style(Style::default().fg(Color::Magenta)) - .border_style(Style::default().fg(Color::Magenta)) - .borders(border::BOTTOM) - .render(t, &chunks[0]); Group::default() .direction(Direction::Horizontal) - .sizes(&[Size::Fixed(7), Size::Min(0), Size::Fixed(7)]) - .render(t, &chunks[1], |t, chunks| { + .sizes(&[Size::Percent(50), Size::Percent(50)]) + .render(t, &chunks[0], |t, chunks| { Block::default() - .title("Left") + .title("With background") .title_style(Style::default().fg(Color::Yellow)) + .style(Style::default().bg(Color::Green)) + .render(t, &chunks[0]); + Block::default() + .title("Styled title") + .title_style(Style::default() + .fg(Color::White) + .bg(Color::Red) + .modifier(Modifier::Bold)) + .render(t, &chunks[1]); + }); + Group::default() + .direction(Direction::Horizontal) + .sizes(&[Size::Percent(50), Size::Percent(50)]) + .render(t, &chunks[1], |t, chunks| { + Block::default() + .title("With borders") + .borders(border::ALL) .render(t, &chunks[0]); Block::default() - .title("Middle") - .title_style(Style::default().fg(Color::Cyan)) + .title("With styled borders") .border_style(Style::default().fg(Color::Cyan)) .borders(border::LEFT | border::RIGHT) .render(t, &chunks[1]); - Block::default() - .title("Right") - .title_style(Style::default().fg(Color::Green)) - .render(t, &chunks[2]); }); - Block::default() - .title("Bottom") - .title_style(Style::default().fg(Color::Green)) - .border_style(Style::default().fg(Color::Green)) - .borders(border::TOP) - .render(t, &chunks[2]); }); t.draw().unwrap(); diff --git a/examples/canvas.rs b/examples/canvas.rs new file mode 100644 index 0000000..b644569 --- /dev/null +++ b/examples/canvas.rs @@ -0,0 +1,200 @@ +extern crate tui; +extern crate termion; + +use std::io; +use std::thread; +use std::time; +use std::sync::mpsc; + +use termion::event; +use termion::input::TermRead; + +use tui::{Terminal, TermionBackend}; +use tui::widgets::{Widget, Block, border}; +use tui::widgets::canvas::{Canvas, Map, MapResolution, Line}; +use tui::layout::{Group, Rect, Direction, Size}; +use tui::style::Color; + +struct App { + size: Rect, + x: f64, + y: f64, + ball: Rect, + playground: Rect, + vx: u16, + vy: u16, +} + + +impl App { + fn new() -> App { + App { + size: Default::default(), + x: 0.0, + y: 0.0, + ball: Rect::new(20, 20, 10, 10), + playground: Rect::new(10, 10, 100, 100), + vx: 1, + vy: 1, + } + } + + fn advance(&mut self) { + if self.ball.left() < self.playground.left() || + self.ball.right() > self.playground.right() { + self.vx = !self.vx; + } else if self.ball.top() < self.playground.top() || + self.ball.bottom() > self.playground.bottom() { + self.vy = !self.vy; + } + self.ball.x += self.vx; + self.ball.y += self.vy; + } +} + +enum Event { + Input(event::Key), + Tick, +} + +fn main() { + // Terminal initialization + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend).unwrap(); + + // Channels + let (tx, rx) = mpsc::channel(); + let input_tx = tx.clone(); + let clock_tx = tx.clone(); + + // Input + thread::spawn(move || { + let stdin = io::stdin(); + for c in stdin.keys() { + let evt = c.unwrap(); + input_tx.send(Event::Input(evt)).unwrap(); + if evt == event::Key::Char('q') { + break; + } + } + }); + + // Tick + thread::spawn(move || { + loop { + clock_tx.send(Event::Tick).unwrap(); + thread::sleep(time::Duration::from_millis(500)); + } + }); + + // App + let mut app = App::new(); + + // First draw call + terminal.clear().unwrap(); + terminal.hide_cursor().unwrap(); + let size = terminal.size().unwrap(); + app.size = size; + draw(&mut terminal, &app); + + loop { + let evt = rx.recv().unwrap(); + match evt { + Event::Input(input) => { + match input { + event::Key::Char('q') => { + break; + } + event::Key::Down => { + app.y += 1.0; + } + event::Key::Up => { + app.y -= 1.0; + } + event::Key::Right => { + app.x += 1.0; + } + event::Key::Left => { + app.x -= 1.0; + } + + _ => {} + } + } + Event::Tick => { + app.advance(); + } + } + let size = terminal.size().unwrap(); + if size != app.size { + app.size = size; + terminal.resize(size).unwrap(); + } + draw(&mut terminal, &app); + } + + terminal.show_cursor().unwrap(); +} + +fn draw(t: &mut Terminal, app: &App) { + + + Group::default() + .direction(Direction::Horizontal) + .sizes(&[Size::Percent(50), Size::Percent(50)]) + .render(t, &app.size, |t, chunks| { + Canvas::default() + .block(Block::default() + .borders(border::ALL) + .title("World")) + .paint(|ctx| { + ctx.draw(&Map { + color: Color::White, + resolution: MapResolution::High, + }); + ctx.print(app.x, -app.y, "You are here", Color::Yellow); + }) + .x_bounds([-180.0, 180.0]) + .y_bounds([-90.0, 90.0]) + .render(t, &chunks[0]); + Canvas::default() + .block(Block::default() + .borders(border::ALL) + .title("List")) + .paint(|ctx| { + ctx.draw(&Line { + x1: app.ball.left() as f64, + y1: app.ball.top() as f64, + x2: app.ball.right() as f64, + y2: app.ball.top() as f64, + color: Color::Yellow, + }); + ctx.draw(&Line { + x1: app.ball.right() as f64, + y1: app.ball.top() as f64, + x2: app.ball.right() as f64, + y2: app.ball.bottom() as f64, + color: Color::Yellow, + }); + ctx.draw(&Line { + x1: app.ball.right() as f64, + y1: app.ball.bottom() as f64, + x2: app.ball.left() as f64, + y2: app.ball.bottom() as f64, + color: Color::Yellow, + }); + ctx.draw(&Line { + x1: app.ball.left() as f64, + y1: app.ball.bottom() as f64, + x2: app.ball.left() as f64, + y2: app.ball.top() as f64, + color: Color::Yellow, + }); + }) + .x_bounds([10.0, 110.0]) + .y_bounds([10.0, 110.0]) + .render(t, &chunks[1]); + }); + + t.draw().unwrap(); +} diff --git a/examples/chart.rs b/examples/chart.rs new file mode 100644 index 0000000..970f0a2 --- /dev/null +++ b/examples/chart.rs @@ -0,0 +1,155 @@ +extern crate tui; +extern crate termion; + +mod util; +use util::*; + +use std::io; +use std::thread; +use std::time; +use std::sync::mpsc; + +use termion::event; +use termion::input::TermRead; + +use tui::{Terminal, TermionBackend}; +use tui::widgets::{Widget, Block, border, Chart, Axis, Marker, Dataset}; +use tui::style::{Style, Color, Modifier}; + +struct App { + signal1: SinSignal, + data1: Vec<(f64, f64)>, + signal2: SinSignal, + data2: Vec<(f64, f64)>, + window: [f64; 2], +} + +impl App { + fn new() -> App { + let mut signal1 = SinSignal::new(0.2, 3.0, 18.0); + let mut signal2 = SinSignal::new(0.1, 2.0, 10.0); + let data1 = signal1.by_ref().take(200).collect::>(); + let data2 = signal2.by_ref().take(200).collect::>(); + App { + signal1: signal1, + data1: data1, + signal2: signal2, + data2: data2, + window: [0.0, 20.0], + } + } + + fn advance(&mut self) { + for _ in 0..5 { + self.data1.remove(0); + } + self.data1.extend(self.signal1.by_ref().take(5)); + for _ in 0..10 { + self.data2.remove(0); + } + self.data2.extend(self.signal2.by_ref().take(10)); + self.window[0] += 1.0; + self.window[1] += 1.0; + } +} + +enum Event { + Input(event::Key), + Tick, +} + +fn main() { + // Terminal initialization + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend).unwrap(); + + // Channels + let (tx, rx) = mpsc::channel(); + let input_tx = tx.clone(); + let clock_tx = tx.clone(); + + // Input + thread::spawn(move || { + let stdin = io::stdin(); + for c in stdin.keys() { + let evt = c.unwrap(); + input_tx.send(Event::Input(evt)).unwrap(); + if evt == event::Key::Char('q') { + break; + } + } + }); + + // Tick + thread::spawn(move || { + loop { + clock_tx.send(Event::Tick).unwrap(); + thread::sleep(time::Duration::from_millis(500)); + } + }); + + // App + let mut app = App::new(); + + // First draw call + terminal.clear().unwrap(); + terminal.hide_cursor().unwrap(); + draw(&mut terminal, &app); + + loop { + let evt = rx.recv().unwrap(); + match evt { + Event::Input(input) => { + if input == event::Key::Char('q') { + break; + } + } + Event::Tick => { + app.advance(); + } + } + draw(&mut terminal, &app); + } + + terminal.show_cursor().unwrap(); +} + +fn draw(t: &mut Terminal, app: &App) { + + let size = t.size().unwrap(); + + Chart::default() + .block(Block::default() + .title("Chart") + .title_style(Style::default() + .fg(Color::Cyan) + .modifier(Modifier::Bold)) + .borders(border::ALL)) + .x_axis(Axis::default() + .title("X Axis") + .style(Style::default().fg(Color::Gray)) + .labels_style(Style::default().modifier(Modifier::Italic)) + .bounds(app.window) + .labels(&[&format!("{}", app.window[0]), + &format!("{}", (app.window[0] + app.window[1]) / 2.0), + &format!("{}", app.window[1])])) + .y_axis(Axis::default() + .title("Y Axis") + .style(Style::default().fg(Color::Gray)) + .labels_style(Style::default().modifier(Modifier::Italic)) + .bounds([-20.0, 20.0]) + .labels(&["-20", "0", "20"])) + .datasets(&[Dataset::default() + .name("data2") + .marker(Marker::Dot) + .style(Style::default().fg(Color::Cyan)) + .data(&app.data1), + Dataset::default() + .name("data3") + .marker(Marker::Braille) + .style(Style::default().fg(Color::Yellow)) + .data(&app.data2)]) + .render(t, &size); + + t.draw().unwrap(); +} diff --git a/examples/demo.rs b/examples/demo.rs index 6bcad41..8954c88 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -1,9 +1,11 @@ -extern crate tui; #[macro_use] extern crate log; -extern crate log4rs; + +extern crate tui; +#[macro_use] extern crate termion; -extern crate rand; + +mod util; use std::io; use std::thread; @@ -11,16 +13,9 @@ use std::env; use std::time; use std::sync::mpsc; -use rand::distributions::{IndependentSample, Range}; - use termion::event; use termion::input::TermRead; -use log::LogLevelFilter; -use log4rs::append::file::FileAppender; -use log4rs::encode::pattern::PatternEncoder; -use log4rs::config::{Appender, Config, Root}; - use tui::{Terminal, TermionBackend}; use tui::widgets::{Widget, Block, SelectableList, List, Gauge, Sparkline, Paragraph, border, Chart, Axis, Dataset, BarChart, Marker, Tabs, Table}; @@ -28,55 +23,7 @@ use tui::widgets::canvas::{Canvas, Map, MapResolution, Line}; use tui::layout::{Group, Direction, Size, Rect}; use tui::style::{Style, Color, Modifier}; -#[derive(Clone)] -struct RandomSignal { - range: Range, - rng: rand::ThreadRng, -} - -impl RandomSignal { - fn new(r: Range) -> RandomSignal { - RandomSignal { - range: r, - rng: rand::thread_rng(), - } - } -} - -impl Iterator for RandomSignal { - type Item = u64; - fn next(&mut self) -> Option { - Some(self.range.ind_sample(&mut self.rng)) - } -} - -#[derive(Clone)] -struct SinSignal { - x: f64, - interval: f64, - period: f64, - scale: f64, -} - -impl SinSignal { - fn new(interval: f64, period: f64, scale: f64) -> SinSignal { - SinSignal { - x: 0.0, - interval: interval, - period: period, - scale: 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) - } -} +use util::*; struct Server<'a> { name: &'a str, @@ -85,29 +32,13 @@ struct Server<'a> { status: &'a str, } -struct MyTabs { - titles: [&'static str; 2], - selection: usize, -} - -impl MyTabs { - fn next(&mut self) { - self.selection = (self.selection + 1) % self.titles.len(); - } - - fn previous(&mut self) { - if self.selection > 0 { - self.selection -= 1; - } - } -} struct App<'a> { size: Rect, items: Vec<&'a str>, events: Vec<(&'a str, &'a str)>, selected: usize, - tabs: MyTabs, + tabs: MyTabs<'a>, show_chart: bool, progress: u16, data: Vec, @@ -130,23 +61,13 @@ fn main() { for argument in env::args() { if argument == "--log" { - let log = FileAppender::builder() - .encoder(Box::new(PatternEncoder::new("{l} / {d(%H:%M:%S)} / \ - {M}:{L}{n}{m}{n}{n}"))) - .build("demo.log") - .unwrap(); - - let config = Config::builder() - .appender(Appender::builder().build("log", Box::new(log))) - .build(Root::builder().appender("log").build(LogLevelFilter::Debug)) - .unwrap(); - log4rs::init_config(config).unwrap(); + setup_log("demo.log"); } } info!("Start"); - let mut rand_signal = RandomSignal::new(Range::new(0, 100)); + let mut rand_signal = RandomSignal::new(0, 100); let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0); let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0); @@ -183,7 +104,7 @@ fn main() { ("Event26", "INFO")], selected: 0, tabs: MyTabs { - titles: ["Tab0", "Tab1"], + titles: vec!["Tab0", "Tab1"], selection: 0, }, show_chart: true, @@ -272,7 +193,8 @@ fn main() { } }); - let mut terminal = Terminal::new(TermionBackend::new().unwrap()).unwrap(); + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend).unwrap(); terminal.clear().unwrap(); terminal.hide_cursor().unwrap(); diff --git a/examples/gauge.rs b/examples/gauge.rs new file mode 100644 index 0000000..5bd72d8 --- /dev/null +++ b/examples/gauge.rs @@ -0,0 +1,149 @@ +extern crate tui; +extern crate termion; + +use std::io; +use std::thread; +use std::time; +use std::sync::mpsc; + +use termion::event; +use termion::input::TermRead; + +use tui::{Terminal, TermionBackend}; +use tui::widgets::{Widget, Block, border, Gauge}; +use tui::layout::{Group, Direction, Size}; +use tui::style::{Style, Color, Modifier}; + +struct App { + progress1: u16, + progress2: u16, + progress3: u16, + progress4: u16, +} + +impl App { + fn new() -> App { + App { + progress1: 0, + progress2: 0, + progress3: 0, + progress4: 0, + } + } + + fn advance(&mut self) { + self.progress1 += 5; + if self.progress1 > 100 { + self.progress1 = 0; + } + self.progress2 += 10; + if self.progress2 > 100 { + self.progress2 = 0; + } + self.progress3 += 1; + if self.progress3 > 100 { + self.progress3 = 0; + } + self.progress4 += 3; + if self.progress4 > 100 { + self.progress4 = 0; + } + } +} + +enum Event { + Input(event::Key), + Tick, +} + +fn main() { + // Terminal initialization + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend).unwrap(); + + // Channels + let (tx, rx) = mpsc::channel(); + let input_tx = tx.clone(); + let clock_tx = tx.clone(); + + // Input + thread::spawn(move || { + let stdin = io::stdin(); + for c in stdin.keys() { + let evt = c.unwrap(); + input_tx.send(Event::Input(evt)).unwrap(); + if evt == event::Key::Char('q') { + break; + } + } + }); + + // Tick + thread::spawn(move || { + loop { + clock_tx.send(Event::Tick).unwrap(); + thread::sleep(time::Duration::from_millis(500)); + } + }); + + // App + let mut app = App::new(); + + // First draw call + terminal.clear().unwrap(); + terminal.hide_cursor().unwrap(); + draw(&mut terminal, &app); + + loop { + let evt = rx.recv().unwrap(); + match evt { + Event::Input(input) => { + if input == event::Key::Char('q') { + break; + } + } + Event::Tick => { + app.advance(); + } + } + draw(&mut terminal, &app); + } + + terminal.show_cursor().unwrap(); +} + +fn draw(t: &mut Terminal, app: &App) { + + let size = t.size().unwrap(); + + Group::default() + .direction(Direction::Vertical) + .margin(2) + .sizes(&[Size::Percent(25), Size::Percent(25), Size::Percent(25), Size::Percent(25)]) + .render(t, &size, |t, chunks| { + Gauge::default() + .block(Block::default().title("Gauge1").borders(border::ALL)) + .style(Style::default().fg(Color::Yellow)) + .percent(app.progress1) + .render(t, &chunks[0]); + Gauge::default() + .block(Block::default().title("Gauge2").borders(border::ALL)) + .style(Style::default().fg(Color::Magenta).bg(Color::Green)) + .percent(app.progress2) + .label(&format!("{}/100", app.progress2)) + .render(t, &chunks[1]); + Gauge::default() + .block(Block::default().title("Gauge2").borders(border::ALL)) + .style(Style::default().fg(Color::Yellow)) + .percent(app.progress3) + .render(t, &chunks[2]); + Gauge::default() + .block(Block::default().title("Gauge3").borders(border::ALL)) + .style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic)) + .percent(app.progress4) + .label(&format!("{}/100", app.progress2)) + .render(t, &chunks[3]); + }); + + t.draw().unwrap(); +} diff --git a/examples/list.rs b/examples/list.rs new file mode 100644 index 0000000..5864776 --- /dev/null +++ b/examples/list.rs @@ -0,0 +1,188 @@ +extern crate tui; +extern crate termion; + +use std::io; +use std::thread; +use std::time; +use std::sync::mpsc; + +use termion::event; +use termion::input::TermRead; + +use tui::{Terminal, TermionBackend}; +use tui::widgets::{Widget, Block, border, SelectableList, List}; +use tui::layout::{Group, Direction, Size}; +use tui::style::{Style, Color, Modifier}; + +struct App<'a> { + items: Vec<&'a str>, + selected: usize, + events: Vec<(&'a str, &'a str)>, + info_style: Style, + warning_style: Style, + error_style: Style, + critical_style: Style, +} + +impl<'a> App<'a> { + fn new() -> App<'a> { + App { + items: vec!["Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", + "Item9", "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", + "Item16", "Item17", "Item18", "Item19", "Item20", "Item21", "Item22", + "Item23", "Item24"], + selected: 0, + events: vec![("Event1", "INFO"), + ("Event2", "INFO"), + ("Event3", "CRITICAL"), + ("Event4", "ERROR"), + ("Event5", "INFO"), + ("Event6", "INFO"), + ("Event7", "WARNING"), + ("Event8", "INFO"), + ("Event9", "INFO"), + ("Event10", "INFO"), + ("Event11", "CRITICAL"), + ("Event12", "INFO"), + ("Event13", "INFO"), + ("Event14", "INFO"), + ("Event15", "INFO"), + ("Event16", "INFO"), + ("Event17", "ERROR"), + ("Event18", "ERROR"), + ("Event19", "INFO"), + ("Event20", "INFO"), + ("Event21", "WARNING"), + ("Event22", "INFO"), + ("Event23", "INFO"), + ("Event24", "WARNING"), + ("Event25", "INFO"), + ("Event26", "INFO")], + info_style: Style::default().fg(Color::White), + warning_style: Style::default().fg(Color::Yellow), + error_style: Style::default().fg(Color::Magenta), + critical_style: Style::default().fg(Color::Red), + } + } + + fn advance(&mut self) { + let event = self.events.pop().unwrap(); + self.events.insert(0, event); + } +} + +enum Event { + Input(event::Key), + Tick, +} + +fn main() { + // Terminal initialization + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend).unwrap(); + + // Channels + let (tx, rx) = mpsc::channel(); + let input_tx = tx.clone(); + let clock_tx = tx.clone(); + + // Input + thread::spawn(move || { + let stdin = io::stdin(); + for c in stdin.keys() { + let evt = c.unwrap(); + input_tx.send(Event::Input(evt)).unwrap(); + if evt == event::Key::Char('q') { + break; + } + } + }); + + // Tick + thread::spawn(move || { + loop { + clock_tx.send(Event::Tick).unwrap(); + thread::sleep(time::Duration::from_millis(500)); + } + }); + + // App + let mut app = App::new(); + + // First draw call + terminal.clear().unwrap(); + terminal.hide_cursor().unwrap(); + draw(&mut terminal, &app); + + loop { + let evt = rx.recv().unwrap(); + match evt { + Event::Input(input) => { + match input { + event::Key::Char('q') => { + break; + } + event::Key::Down => { + app.selected += 1; + if app.selected > app.items.len() - 1 { + app.selected = 0; + } + } + event::Key::Up => { + if app.selected > 0 { + app.selected -= 1; + } else { + app.selected = app.items.len() - 1; + } + } + _ => {} + } + } + Event::Tick => { + app.advance(); + } + } + draw(&mut terminal, &app); + } + + terminal.show_cursor().unwrap(); +} + +fn draw(t: &mut Terminal, app: &App) { + + let size = t.size().unwrap(); + + Group::default() + .direction(Direction::Horizontal) + .sizes(&[Size::Percent(50), Size::Percent(50)]) + .render(t, &size, |t, chunks| { + SelectableList::default() + .block(Block::default() + .borders(border::ALL) + .title("List")) + .items(&app.items) + .select(app.selected) + .highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold)) + .highlight_symbol(">") + .render(t, &chunks[0]); + List::default() + .block(Block::default() + .borders(border::ALL) + .title("List")) + .items(&app.events + .iter() + .map(|&(evt, level)| { + (format!("{}: {}", level, evt), + match level { + "ERROR" => &app.error_style, + "CRITICAL" => &app.critical_style, + "WARNING" => &app.warning_style, + _ => &app.info_style, + }) + }) + .collect::>()) + .render(t, &chunks[1]); + }); + + t.draw().unwrap(); +} diff --git a/examples/paragraph.rs b/examples/paragraph.rs new file mode 100644 index 0000000..16ff76d --- /dev/null +++ b/examples/paragraph.rs @@ -0,0 +1,57 @@ +extern crate tui; +extern crate termion; + +use std::io; +use termion::event; +use termion::input::TermRead; + +use tui::{Terminal, TermionBackend}; +use tui::widgets::{Widget, Block, Paragraph}; +use tui::layout::{Group, Direction, Size}; +use tui::style::{Style, Color}; + +fn main() { + let mut terminal = Terminal::new(TermionBackend::new().unwrap()).unwrap(); + let stdin = io::stdin(); + terminal.clear().unwrap(); + terminal.hide_cursor().unwrap(); + draw(&mut terminal); + for c in stdin.keys() { + draw(&mut terminal); + let evt = c.unwrap(); + if evt == event::Key::Char('q') { + break; + } + } + terminal.show_cursor().unwrap(); +} + +fn draw(t: &mut Terminal) { + + let size = t.size().unwrap(); + + Block::default() + .style(Style::default().bg(Color::White)) + .render(t, &size); + + Group::default() + .direction(Direction::Vertical) + .margin(5) + .sizes(&[Size::Percent(100)]) + .render(t, &size, |t, chunks| { + Group::default() + .direction(Direction::Horizontal) + .sizes(&[Size::Percent(100)]) + .render(t, &chunks[0], |t, chunks| { + Paragraph::default() + .text("This is a line\n{fg=red This is a line}\n{bg=red This is a \ + line}\n{mod=italic This is a line}\n{mod=bold This is a \ + line}\n{mod=crossed_out This is a line}\n{mod=invert This is a \ + line}\n{mod=underline This is a \ + line}\n{bg=green;fg=yellow;mod=italic This is a line}\n") + .render(t, &chunks[0]); + }); + }); + + t.draw().unwrap(); +} diff --git a/examples/rustbox.rs b/examples/rustbox.rs index b12817d..82333e0 100644 --- a/examples/rustbox.rs +++ b/examples/rustbox.rs @@ -17,11 +17,8 @@ fn main() { loop { match terminal.backend().rustbox().poll_event(false) { Ok(rustbox::Event::KeyEvent(key)) => { - match key { - Key::Char('q') => { - break; - } - _ => {} + if key == Key::Char('q') { + break; } } Err(e) => panic!("{}", e.description()), diff --git a/examples/sparkline.rs b/examples/sparkline.rs new file mode 100644 index 0000000..e1bdac8 --- /dev/null +++ b/examples/sparkline.rs @@ -0,0 +1,143 @@ +extern crate tui; +extern crate termion; + +mod util; +use util::*; + +use std::io; +use std::thread; +use std::time; +use std::sync::mpsc; + +use termion::event; +use termion::input::TermRead; + +use tui::{Terminal, TermionBackend}; +use tui::widgets::{Widget, Block, border, Sparkline}; +use tui::layout::{Group, Direction, Size}; +use tui::style::{Style, Color}; + +struct App { + signal: RandomSignal, + data1: Vec, + data2: Vec, + data3: Vec, +} + +impl App { + fn new() -> App { + let mut signal = RandomSignal::new(0, 100); + let data1 = signal.by_ref().take(200).collect::>(); + let data2 = signal.by_ref().take(200).collect::>(); + let data3 = signal.by_ref().take(200).collect::>(); + App { + signal: signal, + data1: data1, + data2: data2, + data3: data3, + } + } + + fn advance(&mut self) { + let value = self.signal.next().unwrap(); + self.data1.pop(); + self.data1.insert(0, value); + let value = self.signal.next().unwrap(); + self.data2.pop(); + self.data2.insert(0, value); + let value = self.signal.next().unwrap(); + self.data3.pop(); + self.data3.insert(0, value); + } +} + +enum Event { + Input(event::Key), + Tick, +} + +fn main() { + // Terminal initialization + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend).unwrap(); + + // Channels + let (tx, rx) = mpsc::channel(); + let input_tx = tx.clone(); + let clock_tx = tx.clone(); + + // Input + thread::spawn(move || { + let stdin = io::stdin(); + for c in stdin.keys() { + let evt = c.unwrap(); + input_tx.send(Event::Input(evt)).unwrap(); + if evt == event::Key::Char('q') { + break; + } + } + }); + + // Tick + thread::spawn(move || { + loop { + clock_tx.send(Event::Tick).unwrap(); + thread::sleep(time::Duration::from_millis(500)); + } + }); + + // App + let mut app = App::new(); + + // First draw call + terminal.clear().unwrap(); + terminal.hide_cursor().unwrap(); + draw(&mut terminal, &app); + + loop { + let evt = rx.recv().unwrap(); + match evt { + Event::Input(input) => { + if input == event::Key::Char('q') { + break; + } + } + Event::Tick => { + app.advance(); + } + } + draw(&mut terminal, &app); + } + + terminal.show_cursor().unwrap(); +} + +fn draw(t: &mut Terminal, app: &App) { + + let size = t.size().unwrap(); + + Group::default() + .direction(Direction::Vertical) + .margin(2) + .sizes(&[Size::Fixed(3), Size::Fixed(3), Size::Fixed(7), Size::Min(0)]) + .render(t, &size, |t, chunks| { + Sparkline::default() + .block(Block::default().title("Data1").borders(border::LEFT | border::RIGHT)) + .data(&app.data1) + .style(Style::default().fg(Color::Yellow)) + .render(t, &chunks[0]); + Sparkline::default() + .block(Block::default().title("Data2").borders(border::LEFT | border::RIGHT)) + .data(&app.data2) + .style(Style::default().bg(Color::Green)) + .render(t, &chunks[1]); + // Multiline + Sparkline::default() + .block(Block::default().title("Data3").borders(border::LEFT | border::RIGHT)) + .data(&app.data3) + .style(Style::default().fg(Color::Red)) + .render(t, &chunks[2]); + }); + + t.draw().unwrap(); +} diff --git a/examples/tabs.rs b/examples/tabs.rs new file mode 100644 index 0000000..ee61ad0 --- /dev/null +++ b/examples/tabs.rs @@ -0,0 +1,94 @@ +extern crate tui; +extern crate termion; + +mod util; +use util::*; + +use std::io; +use termion::event; +use termion::input::TermRead; + +use tui::{Terminal, TermionBackend}; +use tui::widgets::{Widget, Block, border, Tabs}; +use tui::layout::{Group, Direction, Size}; +use tui::style::{Style, Color}; + +struct App<'a> { + tabs: MyTabs<'a>, +} + +fn main() { + // Terminal initialization + let backend = TermionBackend::new().unwrap(); + let mut terminal = Terminal::new(backend).unwrap(); + + // App + let mut app = App { + tabs: MyTabs { + titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"], + selection: 0, + }, + }; + + // First draw call + terminal.clear().unwrap(); + terminal.hide_cursor().unwrap(); + draw(&mut terminal, &mut app); + + // Main loop + let stdin = io::stdin(); + for c in stdin.keys() { + let evt = c.unwrap(); + match evt { + event::Key::Char('q') => { + break; + } + event::Key::Right => app.tabs.next(), + event::Key::Left => app.tabs.previous(), + _ => {} + } + draw(&mut terminal, &mut app); + } + + terminal.show_cursor().unwrap(); +} + +fn draw(t: &mut Terminal, app: &mut App) { + + let size = t.size().unwrap(); + + Block::default() + .style(Style::default().bg(Color::White)) + .render(t, &size); + + Group::default() + .direction(Direction::Vertical) + .margin(5) + .sizes(&[Size::Fixed(3), Size::Min(0)]) + .render(t, &size, |t, chunks| { + Tabs::default() + .block(Block::default().borders(border::ALL).title("Tabs")) + .titles(&app.tabs.titles) + .select(app.tabs.selection) + .style(Style::default().fg(Color::Cyan)) + .highlight_style(Style::default().fg(Color::Yellow)) + .render(t, &chunks[0]); + match app.tabs.selection { + 0 => { + Block::default().title("Inner 0").borders(border::ALL).render(t, &chunks[1]); + } + 1 => { + Block::default().title("Inner 1").borders(border::ALL).render(t, &chunks[1]); + } + 2 => { + Block::default().title("Inner 2").borders(border::ALL).render(t, &chunks[1]); + } + 3 => { + Block::default().title("Inner 3").borders(border::ALL).render(t, &chunks[1]); + } + _ => {} + } + }); + + t.draw().unwrap(); +} diff --git a/examples/util/mod.rs b/examples/util/mod.rs new file mode 100644 index 0000000..4526dcb --- /dev/null +++ b/examples/util/mod.rs @@ -0,0 +1,95 @@ +#![allow(dead_code)] + +extern crate rand; +extern crate log4rs; +extern crate log; + +use self::rand::distributions::{IndependentSample, Range}; + +use self::log::LogLevelFilter; +use self::log4rs::append::file::FileAppender; +use self::log4rs::encode::pattern::PatternEncoder; +use self::log4rs::config::{Appender, Config, Root}; + +pub fn setup_log(file_name: &str) { + let log = FileAppender::builder() + .encoder(Box::new(PatternEncoder::new("{l} / {d(%H:%M:%S)} / \ + {M}:{L}{n}{m}{n}{n}"))) + .build(file_name) + .unwrap(); + + let config = Config::builder() + .appender(Appender::builder().build("log", Box::new(log))) + .build(Root::builder().appender("log").build(LogLevelFilter::Debug)) + .unwrap(); + log4rs::init_config(config).unwrap(); +} + +#[derive(Clone)] +pub struct RandomSignal { + range: Range, + rng: rand::ThreadRng, +} + +impl RandomSignal { + pub fn new(lower: u64, upper: u64) -> RandomSignal { + RandomSignal { + range: Range::new(lower, upper), + rng: rand::thread_rng(), + } + } +} + +impl Iterator for RandomSignal { + type Item = u64; + fn next(&mut self) -> Option { + Some(self.range.ind_sample(&mut self.rng)) + } +} + +#[derive(Clone)] +pub struct SinSignal { + 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: interval, + period: period, + scale: 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) + } +} + +pub struct MyTabs<'a> { + pub titles: Vec<&'a str>, + pub selection: usize, +} + +impl<'a> MyTabs<'a> { + pub fn next(&mut self) { + self.selection = (self.selection + 1) % self.titles.len(); + } + + pub fn previous(&mut self) { + if self.selection > 0 { + self.selection -= 1; + } else { + self.selection = self.titles.len() - 1; + } + } +}