mirror of
https://github.com/ratatui/ratatui.git
synced 2025-10-02 15:25:54 +00:00
Add sparkline widget and fix warnings
This commit is contained in:
parent
5b5d37ee69
commit
d11dedd864
@ -8,4 +8,7 @@ termion = "1.1.1"
|
|||||||
bitflags = "0.7"
|
bitflags = "0.7"
|
||||||
cassowary = "0.2.0"
|
cassowary = "0.2.0"
|
||||||
log = "0.3"
|
log = "0.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
log4rs = "*"
|
log4rs = "*"
|
||||||
|
rand = "0.3"
|
||||||
|
@ -3,23 +3,23 @@ extern crate tui;
|
|||||||
extern crate log;
|
extern crate log;
|
||||||
extern crate log4rs;
|
extern crate log4rs;
|
||||||
extern crate termion;
|
extern crate termion;
|
||||||
|
extern crate rand;
|
||||||
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time;
|
use std::time;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::io::{Write, stdin};
|
use std::io::stdin;
|
||||||
|
|
||||||
use termion::event;
|
use termion::event;
|
||||||
use termion::input::TermRead;
|
use termion::input::TermRead;
|
||||||
|
|
||||||
use log::LogLevelFilter;
|
use log::LogLevelFilter;
|
||||||
use log4rs::append::console::ConsoleAppender;
|
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
use log4rs::encode::pattern::PatternEncoder;
|
use log4rs::encode::pattern::PatternEncoder;
|
||||||
use log4rs::config::{Appender, Config, Logger, Root};
|
use log4rs::config::{Appender, Config, Logger, Root};
|
||||||
|
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
use tui::widgets::{Widget, Block, List, Gauge, border};
|
use tui::widgets::{Widget, Block, List, Gauge, Sparkline, border};
|
||||||
use tui::layout::{Group, Direction, Alignment, Size};
|
use tui::layout::{Group, Direction, Alignment, Size};
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
@ -29,6 +29,7 @@ struct App {
|
|||||||
selected: usize,
|
selected: usize,
|
||||||
show_episodes: bool,
|
show_episodes: bool,
|
||||||
progress: u16,
|
progress: u16,
|
||||||
|
data: Vec<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Event {
|
enum Event {
|
||||||
@ -58,6 +59,7 @@ fn main() {
|
|||||||
selected: 0,
|
selected: 0,
|
||||||
show_episodes: false,
|
show_episodes: false,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
|
data: (0..100).map(|_| rand::random::<u8>() as u64).collect(),
|
||||||
};
|
};
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let input_tx = tx.clone();
|
let input_tx = tx.clone();
|
||||||
@ -115,6 +117,8 @@ fn main() {
|
|||||||
if app.progress > 100 {
|
if app.progress > 100 {
|
||||||
app.progress = 0;
|
app.progress = 0;
|
||||||
}
|
}
|
||||||
|
app.data.insert(0, rand::random::<u8>() as u64);
|
||||||
|
app.data.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,6 +132,7 @@ fn draw(terminal: &mut Terminal, app: &App) {
|
|||||||
.alignment(Alignment::Left)
|
.alignment(Alignment::Left)
|
||||||
.chunks(&[Size::Fixed(5), Size::Percent(80), Size::Fixed(10)])
|
.chunks(&[Size::Fixed(5), Size::Percent(80), Size::Fixed(10)])
|
||||||
.render(&terminal.area(), |chunks, tree| {
|
.render(&terminal.area(), |chunks, tree| {
|
||||||
|
info!("{:?}", terminal.area());
|
||||||
tree.add(Block::default().borders(border::ALL).title("Gauges").render(&chunks[0]));
|
tree.add(Block::default().borders(border::ALL).title("Gauges").render(&chunks[0]));
|
||||||
tree.add(Group::default()
|
tree.add(Group::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
@ -138,14 +143,14 @@ fn draw(terminal: &mut Terminal, app: &App) {
|
|||||||
tree.add(Gauge::new()
|
tree.add(Gauge::new()
|
||||||
.percent(app.progress)
|
.percent(app.progress)
|
||||||
.render(&chunks[0]));
|
.render(&chunks[0]));
|
||||||
tree.add(Gauge::new()
|
tree.add(Sparkline::new()
|
||||||
.percent(app.progress)
|
.data(&app.data)
|
||||||
.render(&chunks[2]));
|
.render(&chunks[2]));
|
||||||
}));
|
}));
|
||||||
let sizes = if app.show_episodes {
|
let sizes = if app.show_episodes {
|
||||||
vec![Size::Percent(50), Size::Percent(50)]
|
vec![Size::Percent(50), Size::Percent(50)]
|
||||||
} else {
|
} else {
|
||||||
vec![Size::Percent(50), Size::Percent(50)]
|
vec![Size::Percent(100)]
|
||||||
};
|
};
|
||||||
tree.add(Group::default()
|
tree.add(Group::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
@ -153,9 +158,7 @@ fn draw(terminal: &mut Terminal, app: &App) {
|
|||||||
.chunks(&sizes)
|
.chunks(&sizes)
|
||||||
.render(&chunks[1], |chunks, tree| {
|
.render(&chunks[1], |chunks, tree| {
|
||||||
tree.add(List::default()
|
tree.add(List::default()
|
||||||
.block(|b| {
|
.block(*Block::default().borders(border::ALL).title("Podcasts"))
|
||||||
b.borders(border::ALL).title("Podcasts");
|
|
||||||
})
|
|
||||||
.items(&app.items)
|
.items(&app.items)
|
||||||
.select(app.selected)
|
.select(app.selected)
|
||||||
.formatter(|i, s| {
|
.formatter(|i, s| {
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
extern crate termion;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate bitflags;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate log;
|
|
||||||
extern crate cassowary;
|
|
||||||
|
|
||||||
mod buffer;
|
|
||||||
mod util;
|
|
||||||
pub mod symbols;
|
|
||||||
pub mod terminal;
|
|
||||||
pub mod widgets;
|
|
||||||
pub mod style;
|
|
||||||
pub mod layout;
|
|
||||||
|
|
||||||
pub use self::terminal::Terminal;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
#[test]
|
|
||||||
fn it_works() {}
|
|
||||||
}
|
|
@ -121,6 +121,7 @@ pub enum Size {
|
|||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
#[allow(unused_variables)]
|
||||||
pub fn split(area: &Rect,
|
pub fn split(area: &Rect,
|
||||||
dir: &Direction,
|
dir: &Direction,
|
||||||
align: &Alignment,
|
align: &Alignment,
|
||||||
|
@ -8,3 +8,14 @@ pub mod block {
|
|||||||
pub const ONE_QUATER: char = '▎';
|
pub const ONE_QUATER: char = '▎';
|
||||||
pub const ONE_EIGHTH: char = '▏';
|
pub const ONE_EIGHTH: char = '▏';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod bar {
|
||||||
|
pub const FULL: char = '█';
|
||||||
|
pub const SEVEN_EIGHTHS: char = '▇';
|
||||||
|
pub const THREE_QUATERS: char = '▆';
|
||||||
|
pub const FIVE_EIGHTHS: char = '▅';
|
||||||
|
pub const HALF: char = '▄';
|
||||||
|
pub const THREE_EIGHTHS: char = '▃';
|
||||||
|
pub const ONE_QUATER: char = '▂';
|
||||||
|
pub const ONE_EIGHTH: char = '▁';
|
||||||
|
}
|
||||||
|
@ -59,7 +59,7 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
previous.insert((node.widget_type, area), node.hash);
|
previous.insert((node.widget_type, area), node.hash);
|
||||||
}
|
}
|
||||||
for (&(t, a), h) in &self.previous {
|
for (&(t, a), _h) in &self.previous {
|
||||||
buffers.insert(0, Buffer::empty(a));
|
buffers.insert(0, Buffer::empty(a));
|
||||||
debug!("Erased {:?} at {:?}", t, a);
|
debug!("Erased {:?} at {:?}", t, a);
|
||||||
}
|
}
|
||||||
|
12
src/util.rs
12
src/util.rs
@ -1,10 +1,12 @@
|
|||||||
use std::hash::{Hash, SipHasher, Hasher};
|
use std::collections::hash_map::RandomState;
|
||||||
|
use std::hash::{Hash, Hasher, BuildHasher};
|
||||||
|
|
||||||
use layout::Rect;
|
use layout::Rect;
|
||||||
|
|
||||||
pub fn hash<T: Hash>(t: &T, area: &Rect) -> u64 {
|
pub fn hash<T: Hash>(t: &T, area: &Rect) -> u64 {
|
||||||
let mut s = SipHasher::new();
|
let state = RandomState::new();
|
||||||
t.hash(&mut s);
|
let mut hasher = state.build_hasher();
|
||||||
area.hash(&mut s);
|
t.hash(&mut hasher);
|
||||||
s.finish()
|
area.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ impl<'a> Block<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for Block<'a> {
|
impl<'a> Widget for Block<'a> {
|
||||||
fn _buffer(&self, area: &Rect) -> Buffer {
|
fn buffer(&self, area: &Rect) -> Buffer {
|
||||||
|
|
||||||
let mut buf = Buffer::empty(*area);
|
let mut buf = Buffer::empty(*area);
|
||||||
|
|
||||||
|
@ -47,9 +47,9 @@ impl<'a> Gauge<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget for Gauge<'a> {
|
impl<'a> Widget for Gauge<'a> {
|
||||||
fn _buffer(&self, area: &Rect) -> Buffer {
|
fn buffer(&self, area: &Rect) -> Buffer {
|
||||||
let (mut buf, gauge_area) = match self.block {
|
let (mut buf, gauge_area) = match self.block {
|
||||||
Some(ref b) => (b._buffer(area), area.inner(1)),
|
Some(ref b) => (b.buffer(area), area.inner(1)),
|
||||||
None => (Buffer::empty(*area), *area),
|
None => (Buffer::empty(*area), *area),
|
||||||
};
|
};
|
||||||
if gauge_area.height < 1 {
|
if gauge_area.height < 1 {
|
||||||
|
@ -7,7 +7,7 @@ use widgets::{Widget, WidgetType, Block};
|
|||||||
use layout::Rect;
|
use layout::Rect;
|
||||||
|
|
||||||
pub struct List<'a, T> {
|
pub struct List<'a, T> {
|
||||||
block: Block<'a>,
|
block: Option<Block<'a>>,
|
||||||
selected: usize,
|
selected: usize,
|
||||||
formatter: Box<Fn(&T, bool) -> String>,
|
formatter: Box<Fn(&T, bool) -> String>,
|
||||||
items: Vec<T>,
|
items: Vec<T>,
|
||||||
@ -26,7 +26,7 @@ impl<'a, T> Hash for List<'a, T>
|
|||||||
impl<'a, T> Default for List<'a, T> {
|
impl<'a, T> Default for List<'a, T> {
|
||||||
fn default() -> List<'a, T> {
|
fn default() -> List<'a, T> {
|
||||||
List {
|
List {
|
||||||
block: Block::default(),
|
block: None,
|
||||||
selected: 0,
|
selected: 0,
|
||||||
formatter: Box::new(|_, _| String::from("")),
|
formatter: Box::new(|_, _| String::from("")),
|
||||||
items: Vec::new(),
|
items: Vec::new(),
|
||||||
@ -37,10 +37,8 @@ impl<'a, T> Default for List<'a, T> {
|
|||||||
impl<'a, T> List<'a, T>
|
impl<'a, T> List<'a, T>
|
||||||
where T: Clone
|
where T: Clone
|
||||||
{
|
{
|
||||||
pub fn block<F>(&'a mut self, f: F) -> &mut List<'a, T>
|
pub fn block(&'a mut self, block: Block<'a>) -> &mut List<'a, T> {
|
||||||
where F: Fn(&mut Block)
|
self.block = Some(block);
|
||||||
{
|
|
||||||
f(&mut self.block);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,14 +63,14 @@ impl<'a, T> List<'a, T>
|
|||||||
impl<'a, T> Widget for List<'a, T>
|
impl<'a, T> Widget for List<'a, T>
|
||||||
where T: Display + Hash
|
where T: Display + Hash
|
||||||
{
|
{
|
||||||
fn _buffer(&self, area: &Rect) -> Buffer {
|
fn buffer(&self, area: &Rect) -> Buffer {
|
||||||
let mut buf = self.block.buffer(area);
|
|
||||||
if area.area() == 0 {
|
let (mut buf, list_area) = match self.block {
|
||||||
return buf;
|
Some(ref b) => (b.buffer(area), area.inner(1)),
|
||||||
}
|
None => (Buffer::empty(*area), *area),
|
||||||
|
};
|
||||||
|
|
||||||
let list_length = self.items.len();
|
let list_length = self.items.len();
|
||||||
let list_area = area.inner(1);
|
|
||||||
let list_height = list_area.height as usize;
|
let list_height = list_area.height as usize;
|
||||||
let bound = min(list_height, list_length);
|
let bound = min(list_height, list_length);
|
||||||
let offset = if self.selected > list_height {
|
let offset = if self.selected > list_height {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
mod block;
|
mod block;
|
||||||
mod list;
|
mod list;
|
||||||
mod gauge;
|
mod gauge;
|
||||||
|
mod sparkline;
|
||||||
|
|
||||||
pub use self::block::Block;
|
pub use self::block::Block;
|
||||||
pub use self::list::List;
|
pub use self::list::List;
|
||||||
pub use self::gauge::Gauge;
|
pub use self::gauge::Gauge;
|
||||||
|
pub use self::sparkline::Sparkline;
|
||||||
|
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
|
||||||
@ -13,6 +15,7 @@ use buffer::{Buffer, Cell};
|
|||||||
use layout::{Rect, Tree, Leaf};
|
use layout::{Rect, Tree, Leaf};
|
||||||
use style::Color;
|
use style::Color;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
enum Line {
|
enum Line {
|
||||||
Horizontal,
|
Horizontal,
|
||||||
Vertical,
|
Vertical,
|
||||||
@ -88,16 +91,11 @@ pub enum WidgetType {
|
|||||||
Block,
|
Block,
|
||||||
List,
|
List,
|
||||||
Gauge,
|
Gauge,
|
||||||
|
Sparkline,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Widget: Hash {
|
pub trait Widget: Hash {
|
||||||
fn _buffer(&self, area: &Rect) -> Buffer;
|
fn buffer(&self, area: &Rect) -> Buffer;
|
||||||
fn buffer(&self, area: &Rect) -> Buffer {
|
|
||||||
match area.area() {
|
|
||||||
0 => Buffer::empty(*area),
|
|
||||||
_ => self._buffer(area),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn widget_type(&self) -> WidgetType;
|
fn widget_type(&self) -> WidgetType;
|
||||||
fn render(&self, area: &Rect) -> Tree {
|
fn render(&self, area: &Rect) -> Tree {
|
||||||
let widget_type = self.widget_type();
|
let widget_type = self.widget_type();
|
||||||
|
84
src/widgets/sparkline.rs
Normal file
84
src/widgets/sparkline.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
use std::cmp::min;
|
||||||
|
|
||||||
|
use layout::Rect;
|
||||||
|
use buffer::Buffer;
|
||||||
|
use widgets::{Widget, WidgetType, Block};
|
||||||
|
use style::Color;
|
||||||
|
use symbols::bar;
|
||||||
|
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub struct Sparkline<'a> {
|
||||||
|
block: Option<Block<'a>>,
|
||||||
|
color: Color,
|
||||||
|
data: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Sparkline<'a> {
|
||||||
|
pub fn new() -> Sparkline<'a> {
|
||||||
|
Sparkline {
|
||||||
|
block: None,
|
||||||
|
color: Color::White,
|
||||||
|
data: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block(&mut self, block: Block<'a>) -> &mut Sparkline<'a> {
|
||||||
|
self.block = Some(block);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color(&mut self, color: Color) -> &mut Sparkline<'a> {
|
||||||
|
self.color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data(&mut self, data: &[u64]) -> &mut Sparkline<'a> {
|
||||||
|
self.data = data.to_vec();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Sparkline<'a> {
|
||||||
|
fn buffer(&self, area: &Rect) -> Buffer {
|
||||||
|
let (mut buf, spark_area) = match self.block {
|
||||||
|
Some(ref b) => (b.buffer(area), area.inner(1)),
|
||||||
|
None => (Buffer::empty(*area), *area),
|
||||||
|
};
|
||||||
|
if spark_area.height < 1 {
|
||||||
|
return buf;
|
||||||
|
} else {
|
||||||
|
let margin = spark_area.x - area.x;
|
||||||
|
match self.data.iter().max() {
|
||||||
|
Some(max_value) => {
|
||||||
|
let max_index = min(spark_area.width as usize, self.data.len());
|
||||||
|
let line = self.data
|
||||||
|
.iter()
|
||||||
|
.take(max_index)
|
||||||
|
.filter_map(|e| {
|
||||||
|
let value = e * 8 / max_value;
|
||||||
|
match value {
|
||||||
|
0 => Some(' '),
|
||||||
|
1 => Some(bar::ONE_EIGHTH),
|
||||||
|
2 => Some(bar::ONE_QUATER),
|
||||||
|
3 => Some(bar::THREE_EIGHTHS),
|
||||||
|
4 => Some(bar::HALF),
|
||||||
|
5 => Some(bar::FIVE_EIGHTHS),
|
||||||
|
6 => Some(bar::THREE_EIGHTHS),
|
||||||
|
7 => Some(bar::THREE_QUATERS),
|
||||||
|
8 => Some(bar::FULL),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<String>();
|
||||||
|
buf.set_string(margin, margin, &line);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn widget_type(&self) -> WidgetType {
|
||||||
|
WidgetType::Sparkline
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user