mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 07:04:17 +00:00
feat: add pancurses backend
This commit is contained in:
parent
cadb41c9e3
commit
d75198a8ee
6 changed files with 785 additions and 1 deletions
|
@ -18,6 +18,7 @@ appveyor = { repository = "fdehau/tui-rs" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["termion"]
|
default = ["termion"]
|
||||||
|
curses = ["easycurses", "pancurses"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = "1.0"
|
bitflags = "1.0"
|
||||||
|
@ -30,6 +31,8 @@ unicode-width = "0.1"
|
||||||
termion = { version = "1.5", optional = true }
|
termion = { version = "1.5", optional = true }
|
||||||
rustbox = { version = "0.11", optional = true }
|
rustbox = { version = "0.11", optional = true }
|
||||||
crossterm = { version = "0.6", optional = true }
|
crossterm = { version = "0.6", optional = true }
|
||||||
|
easycurses = { version = "0.12.2", optional = true }
|
||||||
|
pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
stderrlog = "0.4"
|
stderrlog = "0.4"
|
||||||
|
@ -51,3 +54,8 @@ required-features = ["rustbox"]
|
||||||
name = "crossterm_demo"
|
name = "crossterm_demo"
|
||||||
path = "examples/crossterm_demo.rs"
|
path = "examples/crossterm_demo.rs"
|
||||||
required-features = ["crossterm"]
|
required-features = ["crossterm"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "curses"
|
||||||
|
path = "examples/curses.rs"
|
||||||
|
required-features = ["curses"]
|
||||||
|
|
|
@ -12,12 +12,13 @@ user interfaces and dashboards. It is heavily inspired by the `Javascript`
|
||||||
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
||||||
`Go` library [termui](https://github.com/gizak/termui).
|
`Go` library [termui](https://github.com/gizak/termui).
|
||||||
|
|
||||||
The library itself supports three different backends to draw to the terminal. You
|
The library itself supports four different backends to draw to the terminal. You
|
||||||
can either choose from:
|
can either choose from:
|
||||||
|
|
||||||
- [termion](https://github.com/ticki/termion)
|
- [termion](https://github.com/ticki/termion)
|
||||||
- [rustbox](https://github.com/gchp/rustbox)
|
- [rustbox](https://github.com/gchp/rustbox)
|
||||||
- [crossterm](https://github.com/TimonPost/crossterm)
|
- [crossterm](https://github.com/TimonPost/crossterm)
|
||||||
|
- [pancurses](https://github.com/ihalila/pancurses)
|
||||||
|
|
||||||
However, some features may only be available in one of the three.
|
However, some features may only be available in one of the three.
|
||||||
|
|
||||||
|
|
42
examples/curses.rs
Normal file
42
examples/curses.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use tui::backend::CursesBackend;
|
||||||
|
use tui::style::{Color, Modifier, Style};
|
||||||
|
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
|
||||||
|
use tui::Terminal;
|
||||||
|
|
||||||
|
fn main() -> Result<(), failure::Error> {
|
||||||
|
let mut terminal = Terminal::new(CursesBackend::new().unwrap()).unwrap();
|
||||||
|
terminal.clear().unwrap();
|
||||||
|
terminal.hide_cursor().unwrap();
|
||||||
|
loop {
|
||||||
|
draw(&mut terminal)?;
|
||||||
|
match terminal.backend_mut().get_curses_window_mut().get_input() {
|
||||||
|
Some(easycurses::Input::Character(char)) => {
|
||||||
|
if char == 'q' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(t: &mut Terminal<CursesBackend>) -> Result<(), std::io::Error> {
|
||||||
|
let text = [
|
||||||
|
Text::raw("It "),
|
||||||
|
Text::styled("works", Style::default().fg(Color::Yellow)),
|
||||||
|
];
|
||||||
|
t.draw(|mut f| {
|
||||||
|
let size = f.size();
|
||||||
|
Paragraph::new(text.iter())
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title("Curses backend")
|
||||||
|
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Magenta)),
|
||||||
|
)
|
||||||
|
.render(&mut f, size)
|
||||||
|
})
|
||||||
|
}
|
497
examples/curses_demo.rs
Normal file
497
examples/curses_demo.rs
Normal file
|
@ -0,0 +1,497 @@
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use easycurses;
|
||||||
|
use tui::backend::{Backend, CursesBackend};
|
||||||
|
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
|
use tui::style::{Color, Modifier, Style};
|
||||||
|
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle};
|
||||||
|
use tui::widgets::{
|
||||||
|
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row,
|
||||||
|
SelectableList, Sparkline, Table, Tabs, Text, Widget,
|
||||||
|
};
|
||||||
|
use tui::{Frame, Terminal};
|
||||||
|
|
||||||
|
use crate::util::{RandomSignal, SinSignal, TabsState};
|
||||||
|
|
||||||
|
struct Server<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
location: &'a str,
|
||||||
|
coords: (f64, f64),
|
||||||
|
status: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App<'a> {
|
||||||
|
items: Vec<&'a str>,
|
||||||
|
events: Vec<(&'a str, &'a str)>,
|
||||||
|
selected: usize,
|
||||||
|
tabs: TabsState<'a>,
|
||||||
|
show_chart: bool,
|
||||||
|
progress: u16,
|
||||||
|
data: Vec<u64>,
|
||||||
|
data2: Vec<(f64, f64)>,
|
||||||
|
data3: Vec<(f64, f64)>,
|
||||||
|
data4: Vec<(&'a str, u64)>,
|
||||||
|
window: [f64; 2],
|
||||||
|
colors: [Color; 2],
|
||||||
|
color_index: usize,
|
||||||
|
servers: Vec<Server<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), failure::Error> {
|
||||||
|
stderrlog::new()
|
||||||
|
.module(module_path!())
|
||||||
|
.verbosity(4)
|
||||||
|
.init()?;
|
||||||
|
|
||||||
|
let mut terminal = Terminal::new(CursesBackend::new().unwrap()).unwrap();
|
||||||
|
terminal.clear().unwrap();
|
||||||
|
terminal.hide_cursor().unwrap();
|
||||||
|
terminal
|
||||||
|
.backend_mut()
|
||||||
|
.get_curses_window_mut()
|
||||||
|
.set_input_timeout(easycurses::TimeoutMode::WaitUpTo(50));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut counter = 1;
|
||||||
|
|
||||||
|
let mut app = 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",
|
||||||
|
],
|
||||||
|
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"),
|
||||||
|
],
|
||||||
|
selected: 0,
|
||||||
|
tabs: TabsState::new(vec!["Tab0", "Tab1"]),
|
||||||
|
show_chart: true,
|
||||||
|
progress: 0,
|
||||||
|
data: rand_signal.by_ref().take(300).collect(),
|
||||||
|
data2: sin_signal.by_ref().take(100).collect(),
|
||||||
|
data3: sin_signal2.by_ref().take(200).collect(),
|
||||||
|
data4: 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),
|
||||||
|
],
|
||||||
|
window: [0.0, 20.0],
|
||||||
|
colors: [Color::Magenta, Color::Red],
|
||||||
|
color_index: 0,
|
||||||
|
servers: vec![
|
||||||
|
Server {
|
||||||
|
name: "NorthAmerica-1",
|
||||||
|
location: "New York City",
|
||||||
|
coords: (40.71, -74.00),
|
||||||
|
status: "Up",
|
||||||
|
},
|
||||||
|
Server {
|
||||||
|
name: "Europe-1",
|
||||||
|
location: "Paris",
|
||||||
|
coords: (48.85, 2.35),
|
||||||
|
status: "Failure",
|
||||||
|
},
|
||||||
|
Server {
|
||||||
|
name: "SouthAmerica-1",
|
||||||
|
location: "São Paulo",
|
||||||
|
coords: (-23.54, -46.62),
|
||||||
|
status: "Up",
|
||||||
|
},
|
||||||
|
Server {
|
||||||
|
name: "Asia-1",
|
||||||
|
location: "Singapore",
|
||||||
|
coords: (1.35, 103.86),
|
||||||
|
status: "Up",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Draw UI
|
||||||
|
terminal.draw(|mut f| {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||||
|
.split(f.size());
|
||||||
|
Tabs::default()
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||||
|
.titles(&app.tabs.titles)
|
||||||
|
.style(Style::default().fg(Color::Green))
|
||||||
|
.highlight_style(Style::default().fg(Color::Yellow))
|
||||||
|
.select(app.tabs.index)
|
||||||
|
.render(&mut f, chunks[0]);
|
||||||
|
match app.tabs.index {
|
||||||
|
0 => draw_first_tab(&mut f, &app, chunks[1]),
|
||||||
|
1 => draw_second_tab(&mut f, &app, chunks[1]),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Check for user input
|
||||||
|
match terminal.backend_mut().get_curses_window_mut().get_input() {
|
||||||
|
Some(input) => {
|
||||||
|
match input {
|
||||||
|
easycurses::Input::Character('q') => break,
|
||||||
|
easycurses::Input::KeyUp => {
|
||||||
|
if app.selected > 0 {
|
||||||
|
app.selected -= 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
easycurses::Input::KeyDown => {
|
||||||
|
if app.selected < app.items.len() - 1 {
|
||||||
|
app.selected += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
easycurses::Input::KeyLeft => {
|
||||||
|
app.tabs.previous();
|
||||||
|
}
|
||||||
|
easycurses::Input::KeyRight => {
|
||||||
|
app.tabs.next();
|
||||||
|
}
|
||||||
|
easycurses::Input::Character('t') => {
|
||||||
|
app.show_chart = !app.show_chart;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
terminal.backend_mut().get_curses_window_mut().flush_input();
|
||||||
|
|
||||||
|
if start.elapsed() > Duration::from_millis(250) * counter {
|
||||||
|
app.progress += 5;
|
||||||
|
if app.progress > 100 {
|
||||||
|
app.progress = 0;
|
||||||
|
}
|
||||||
|
app.data.insert(0, rand_signal.next().unwrap());
|
||||||
|
app.data.pop();
|
||||||
|
for _ in 0..5 {
|
||||||
|
app.data2.remove(0);
|
||||||
|
app.data2.push(sin_signal.next().unwrap());
|
||||||
|
}
|
||||||
|
for _ in 0..10 {
|
||||||
|
app.data3.remove(0);
|
||||||
|
app.data3.push(sin_signal2.next().unwrap());
|
||||||
|
}
|
||||||
|
let i = app.data4.pop().unwrap();
|
||||||
|
app.data4.insert(0, i);
|
||||||
|
app.window[0] += 1.0;
|
||||||
|
app.window[1] += 1.0;
|
||||||
|
let i = app.events.pop().unwrap();
|
||||||
|
app.events.insert(0, i);
|
||||||
|
app.color_index += 1;
|
||||||
|
if app.color_index >= app.colors.len() {
|
||||||
|
app.color_index = 0;
|
||||||
|
}
|
||||||
|
counter += 1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_first_tab<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(7),
|
||||||
|
Constraint::Min(7),
|
||||||
|
Constraint::Length(7),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(area);
|
||||||
|
draw_gauges(f, app, chunks[0]);
|
||||||
|
draw_charts(f, app, chunks[1]);
|
||||||
|
draw_text(f, chunks[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_gauges<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
|
||||||
|
.margin(1)
|
||||||
|
.split(area);
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title("Graphs")
|
||||||
|
.render(f, area);
|
||||||
|
Gauge::default()
|
||||||
|
.block(Block::default().title("Gauge:"))
|
||||||
|
.style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Magenta)
|
||||||
|
.bg(Color::Black)
|
||||||
|
.modifier(Modifier::Italic),
|
||||||
|
)
|
||||||
|
.label(&format!("{} / 100", app.progress))
|
||||||
|
.percent(app.progress)
|
||||||
|
.render(f, chunks[0]);
|
||||||
|
Sparkline::default()
|
||||||
|
.block(Block::default().title("Sparkline:"))
|
||||||
|
.style(Style::default().fg(Color::Green))
|
||||||
|
.data(&app.data)
|
||||||
|
.render(f, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_charts<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
let constraints = if app.show_chart {
|
||||||
|
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||||
|
} else {
|
||||||
|
vec![Constraint::Percentage(100)]
|
||||||
|
};
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints(constraints)
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.split(area);
|
||||||
|
{
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
|
.split(chunks[0]);
|
||||||
|
{
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.split(chunks[0]);
|
||||||
|
SelectableList::default()
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||||
|
.items(&app.items)
|
||||||
|
.select(Some(app.selected))
|
||||||
|
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
|
||||||
|
.highlight_symbol(">")
|
||||||
|
.render(f, chunks[0]);
|
||||||
|
let info_style = Style::default().fg(Color::White);
|
||||||
|
let warning_style = Style::default().fg(Color::Yellow);
|
||||||
|
let error_style = Style::default().fg(Color::Magenta);
|
||||||
|
let critical_style = Style::default().fg(Color::Red);
|
||||||
|
let events = app.events.iter().map(|&(evt, level)| {
|
||||||
|
Text::styled(
|
||||||
|
format!("{}: {}", level, evt),
|
||||||
|
match level {
|
||||||
|
"ERROR" => error_style,
|
||||||
|
"CRITICAL" => critical_style,
|
||||||
|
"WARNING" => warning_style,
|
||||||
|
_ => info_style,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
List::new(events)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||||
|
.render(f, chunks[1]);
|
||||||
|
}
|
||||||
|
BarChart::default()
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||||
|
.data(&app.data4)
|
||||||
|
.bar_width(3)
|
||||||
|
.bar_gap(2)
|
||||||
|
.value_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Black)
|
||||||
|
.bg(Color::Green)
|
||||||
|
.modifier(Modifier::Italic),
|
||||||
|
)
|
||||||
|
.label_style(Style::default().fg(Color::Yellow))
|
||||||
|
.style(Style::default().fg(Color::Green))
|
||||||
|
.render(f, chunks[1]);
|
||||||
|
}
|
||||||
|
if app.show_chart {
|
||||||
|
Chart::default()
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title("Chart")
|
||||||
|
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::Bold))
|
||||||
|
.borders(Borders::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.data2),
|
||||||
|
Dataset::default()
|
||||||
|
.name("data3")
|
||||||
|
.marker(Marker::Braille)
|
||||||
|
.style(Style::default().fg(Color::Yellow))
|
||||||
|
.data(&app.data3),
|
||||||
|
])
|
||||||
|
.render(f, chunks[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_text<B>(f: &mut Frame<B>, area: Rect)
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
let text = [
|
||||||
|
Text::raw("This is a paragraph with several lines. You can change style your text the way you want.\n\nFox example: "),
|
||||||
|
Text::styled("under", Style::default().fg(Color::Red)),
|
||||||
|
Text::raw(" "),
|
||||||
|
Text::styled("the", Style::default().fg(Color::Green)),
|
||||||
|
Text::raw(" "),
|
||||||
|
Text::styled("rainbow", Style::default().fg(Color::Blue)),
|
||||||
|
Text::raw(".\nOh and if you didn't "),
|
||||||
|
Text::styled("notice", Style::default().modifier(Modifier::Italic)),
|
||||||
|
Text::raw(" you can "),
|
||||||
|
Text::styled("automatically", Style::default().modifier(Modifier::Bold)),
|
||||||
|
Text::raw(" "),
|
||||||
|
Text::styled("wrap", Style::default().modifier(Modifier::Invert)),
|
||||||
|
Text::raw(" your "),
|
||||||
|
Text::styled("text", Style::default().modifier(Modifier::Underline)),
|
||||||
|
Text::raw(".\nOne more thing is that it should display unicode characters: 10€ (but only on Windows, use the termion backend if you want to see them on Unix.)")
|
||||||
|
];
|
||||||
|
Paragraph::new(text.iter())
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title("Footer")
|
||||||
|
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::Bold)),
|
||||||
|
)
|
||||||
|
.wrap(true)
|
||||||
|
.render(f, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_second_tab<B>(f: &mut Frame<B>, app: &App, area: Rect)
|
||||||
|
where
|
||||||
|
B: Backend,
|
||||||
|
{
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.split(area);
|
||||||
|
let up_style = Style::default().fg(Color::Green);
|
||||||
|
let failure_style = Style::default().fg(Color::Red);
|
||||||
|
let header = ["Server", "Location", "Status"];
|
||||||
|
let rows = app.servers.iter().map(|s| {
|
||||||
|
let style = if s.status == "Up" {
|
||||||
|
up_style
|
||||||
|
} else {
|
||||||
|
failure_style
|
||||||
|
};
|
||||||
|
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||||
|
});
|
||||||
|
Table::new(header.into_iter(), rows)
|
||||||
|
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||||
|
.header_style(Style::default().fg(Color::Yellow))
|
||||||
|
.widths(&[15, 15, 10])
|
||||||
|
.render(f, chunks[0]);
|
||||||
|
|
||||||
|
Canvas::default()
|
||||||
|
.block(Block::default().title("World").borders(Borders::ALL))
|
||||||
|
.paint(|ctx| {
|
||||||
|
ctx.draw(&Map {
|
||||||
|
color: Color::White,
|
||||||
|
resolution: MapResolution::High,
|
||||||
|
});
|
||||||
|
ctx.layer();
|
||||||
|
ctx.draw(&Rectangle {
|
||||||
|
rect: Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 30,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
},
|
||||||
|
color: Color::Yellow,
|
||||||
|
});
|
||||||
|
for (i, s1) in app.servers.iter().enumerate() {
|
||||||
|
for s2 in &app.servers[i + 1..] {
|
||||||
|
ctx.draw(&Line {
|
||||||
|
x1: s1.coords.1,
|
||||||
|
y1: s1.coords.0,
|
||||||
|
y2: s2.coords.0,
|
||||||
|
x2: s2.coords.1,
|
||||||
|
color: Color::Yellow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for server in &app.servers {
|
||||||
|
let color = if server.status == "Up" {
|
||||||
|
Color::Green
|
||||||
|
} else {
|
||||||
|
Color::Red
|
||||||
|
};
|
||||||
|
ctx.print(server.coords.1, server.coords.0, "X", color);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.x_bounds([-180.0, 180.0])
|
||||||
|
.y_bounds([-90.0, 90.0])
|
||||||
|
.render(f, chunks[1]);
|
||||||
|
}
|
231
src/backend/curses.rs
Normal file
231
src/backend/curses.rs
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crate::backend::Backend;
|
||||||
|
use crate::buffer::Cell;
|
||||||
|
use crate::layout::Rect;
|
||||||
|
use crate::style::{Color, Modifier, Style};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use crate::symbols::{bar, block, line, DOT};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use pancurses::ToChtype;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
pub struct CursesBackend {
|
||||||
|
curses: easycurses::EasyCurses,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursesBackend {
|
||||||
|
pub fn new() -> Result<CursesBackend, String> {
|
||||||
|
match easycurses::EasyCurses::initialize_system() {
|
||||||
|
Some(mut curses) => {
|
||||||
|
curses.set_echo(false);
|
||||||
|
curses.set_input_timeout(easycurses::TimeoutMode::Never);
|
||||||
|
curses.set_input_mode(easycurses::InputMode::RawCharacter);
|
||||||
|
curses.set_keypad_enabled(true);
|
||||||
|
Ok(CursesBackend { curses })
|
||||||
|
}
|
||||||
|
None => Err(String::from(
|
||||||
|
"Can't initialize curses, make sure it is not running already.",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_curses_window(&self) -> &easycurses::EasyCurses {
|
||||||
|
&self.curses
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_curses_window_mut(&mut self) -> &mut easycurses::EasyCurses {
|
||||||
|
&mut self.curses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend for CursesBackend {
|
||||||
|
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||||
|
{
|
||||||
|
let mut last_col = 0;
|
||||||
|
let mut last_row = 0;
|
||||||
|
let mut style = Style {
|
||||||
|
fg: Color::Reset,
|
||||||
|
bg: Color::Reset,
|
||||||
|
modifier: Modifier::Reset,
|
||||||
|
};
|
||||||
|
let mut curses_style = CursesStyle {
|
||||||
|
fg: easycurses::Color::White,
|
||||||
|
bg: easycurses::Color::Black,
|
||||||
|
attribute: pancurses::Attribute::Normal,
|
||||||
|
};
|
||||||
|
let mut update_color = false;
|
||||||
|
for (col, row, cell) in content {
|
||||||
|
// eprintln!("{:?}", cell);
|
||||||
|
if row != last_row || col != last_col + 1 {
|
||||||
|
self.curses.move_rc(row as i32, col as i32);
|
||||||
|
}
|
||||||
|
last_col = col;
|
||||||
|
last_row = row;
|
||||||
|
if cell.style.modifier != style.modifier {
|
||||||
|
if curses_style.attribute != pancurses::Attribute::Normal {
|
||||||
|
self.curses.win.attroff(curses_style.attribute);
|
||||||
|
}
|
||||||
|
let attribute: pancurses::Attribute = cell.style.modifier.into();
|
||||||
|
self.curses.win.attron(attribute);
|
||||||
|
curses_style.attribute = attribute;
|
||||||
|
style.modifier = cell.style.modifier;
|
||||||
|
};
|
||||||
|
if cell.style.fg != style.fg {
|
||||||
|
update_color = true;
|
||||||
|
if let Some(ccolor) = cell.style.fg.into() {
|
||||||
|
style.fg = cell.style.fg;
|
||||||
|
curses_style.fg = ccolor;
|
||||||
|
} else {
|
||||||
|
style.fg = Color::White;
|
||||||
|
curses_style.fg = easycurses::Color::White;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if cell.style.bg != style.bg {
|
||||||
|
update_color = true;
|
||||||
|
if let Some(ccolor) = cell.style.bg.into() {
|
||||||
|
style.bg = cell.style.bg;
|
||||||
|
curses_style.bg = ccolor;
|
||||||
|
} else {
|
||||||
|
style.bg = Color::Black;
|
||||||
|
curses_style.bg = easycurses::Color::Black;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if update_color {
|
||||||
|
self.curses
|
||||||
|
.set_color_pair(easycurses::ColorPair::new(curses_style.fg, curses_style.bg));
|
||||||
|
};
|
||||||
|
update_color = false;
|
||||||
|
draw(&mut self.curses, cell.symbol.as_str());
|
||||||
|
}
|
||||||
|
self.curses.win.attrset(pancurses::Attribute::Normal);
|
||||||
|
self.curses.set_color_pair(easycurses::ColorPair::new(
|
||||||
|
easycurses::Color::White,
|
||||||
|
easycurses::Color::Black,
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||||
|
self.curses
|
||||||
|
.set_cursor_visibility(easycurses::CursorVisibility::Invisible);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||||
|
self.curses
|
||||||
|
.set_cursor_visibility(easycurses::CursorVisibility::Visible);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn clear(&mut self) -> Result<(), io::Error> {
|
||||||
|
self.curses.clear();
|
||||||
|
// self.curses.refresh();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn size(&self) -> Result<Rect, io::Error> {
|
||||||
|
let (nrows, ncols) = self.curses.get_row_col_count();
|
||||||
|
Ok(Rect::new(0, 0, ncols as u16, nrows as u16))
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> Result<(), io::Error> {
|
||||||
|
self.curses.refresh();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CursesStyle {
|
||||||
|
fg: easycurses::Color,
|
||||||
|
bg: easycurses::Color,
|
||||||
|
attribute: pancurses::Attribute,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
/// Deals with lack of unicode support for ncurses on unix
|
||||||
|
fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
|
||||||
|
for grapheme in symbol.graphemes(true) {
|
||||||
|
let ch = convert_to_curses_char(grapheme);
|
||||||
|
curses.win.addch(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) {
|
||||||
|
curses.print(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
/// Unicode to ASCII / ncurses extended characters
|
||||||
|
fn convert_to_curses_char(unicode: &str) -> pancurses::chtype {
|
||||||
|
match unicode {
|
||||||
|
line::TOP_RIGHT => pancurses::ACS_URCORNER(),
|
||||||
|
line::VERTICAL => pancurses::ACS_VLINE(),
|
||||||
|
line::HORIZONTAL => pancurses::ACS_HLINE(),
|
||||||
|
line::TOP_LEFT => pancurses::ACS_ULCORNER(),
|
||||||
|
line::BOTTOM_RIGHT => pancurses::ACS_LRCORNER(),
|
||||||
|
line::BOTTOM_LEFT => pancurses::ACS_LLCORNER(),
|
||||||
|
line::VERTICAL_LEFT => pancurses::ACS_RTEE(),
|
||||||
|
line::VERTICAL_RIGHT => pancurses::ACS_LTEE(),
|
||||||
|
line::HORIZONTAL_DOWN => pancurses::ACS_TTEE(),
|
||||||
|
line::HORIZONTAL_UP => pancurses::ACS_BTEE(),
|
||||||
|
block::FULL => pancurses::ACS_BLOCK(),
|
||||||
|
block::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||||
|
block::THREE_QUATERS => pancurses::ACS_BLOCK(),
|
||||||
|
block::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||||
|
block::HALF => pancurses::ACS_BLOCK(),
|
||||||
|
block::THREE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||||
|
block::ONE_QUATER => pancurses::ACS_BLOCK(),
|
||||||
|
block::ONE_EIGHTH => pancurses::ACS_BLOCK(),
|
||||||
|
bar::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||||
|
bar::THREE_QUATERS => pancurses::ACS_BLOCK(),
|
||||||
|
bar::FIVE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||||
|
bar::HALF => pancurses::ACS_BLOCK(),
|
||||||
|
bar::THREE_EIGHTHS => pancurses::ACS_BLOCK(),
|
||||||
|
bar::ONE_QUATER => pancurses::ACS_BLOCK(),
|
||||||
|
bar::ONE_EIGHTH => pancurses::ACS_BLOCK(),
|
||||||
|
DOT => pancurses::ACS_BULLET(),
|
||||||
|
unicode_char => {
|
||||||
|
if unicode_char.is_ascii() {
|
||||||
|
let mut chars = unicode_char.chars();
|
||||||
|
if let Some(ch) = chars.next() {
|
||||||
|
ch.to_chtype()
|
||||||
|
} else {
|
||||||
|
pancurses::ACS_BLOCK()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pancurses::ACS_BLOCK()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Color> for Option<easycurses::Color> {
|
||||||
|
fn from(color: Color) -> Option<easycurses::Color> {
|
||||||
|
match color {
|
||||||
|
Color::Reset => None,
|
||||||
|
Color::Black => Some(easycurses::Color::Black),
|
||||||
|
Color::Red | Color::LightRed => Some(easycurses::Color::Red),
|
||||||
|
Color::Green | Color::LightGreen => Some(easycurses::Color::Green),
|
||||||
|
Color::Yellow | Color::LightYellow => Some(easycurses::Color::Yellow),
|
||||||
|
Color::Magenta | Color::LightMagenta => Some(easycurses::Color::Magenta),
|
||||||
|
Color::Cyan | Color::LightCyan => Some(easycurses::Color::Cyan),
|
||||||
|
Color::White | Color::Gray | Color::DarkGray => Some(easycurses::Color::White),
|
||||||
|
Color::Blue | Color::LightBlue => Some(easycurses::Color::Blue),
|
||||||
|
Color::Rgb(_, _, _) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Modifier> for pancurses::Attribute {
|
||||||
|
fn from(modifier: Modifier) -> pancurses::Attribute {
|
||||||
|
match modifier {
|
||||||
|
Modifier::Blink => pancurses::Attribute::Blink,
|
||||||
|
Modifier::Bold => pancurses::Attribute::Bold,
|
||||||
|
Modifier::CrossedOut => pancurses::Attribute::Strikeout,
|
||||||
|
Modifier::Faint => pancurses::Attribute::Dim,
|
||||||
|
Modifier::Invert => pancurses::Attribute::Reverse,
|
||||||
|
Modifier::Italic => pancurses::Attribute::Italic,
|
||||||
|
Modifier::Underline => pancurses::Attribute::Underline,
|
||||||
|
_ => pancurses::Attribute::Normal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,11 @@ mod crossterm;
|
||||||
#[cfg(feature = "crossterm")]
|
#[cfg(feature = "crossterm")]
|
||||||
pub use self::crossterm::CrosstermBackend;
|
pub use self::crossterm::CrosstermBackend;
|
||||||
|
|
||||||
|
#[cfg(feature = "curses")]
|
||||||
|
mod curses;
|
||||||
|
#[cfg(feature = "curses")]
|
||||||
|
pub use self::curses::CursesBackend;
|
||||||
|
|
||||||
mod test;
|
mod test;
|
||||||
pub use self::test::TestBackend;
|
pub use self::test::TestBackend;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue