From d75198a8ee0fc0c09f3462a98219770d9afba91b Mon Sep 17 00:00:00 2001 From: defiori <46789010+defiori@users.noreply.github.com> Date: Thu, 24 Jan 2019 18:45:59 +0000 Subject: [PATCH] feat: add pancurses backend --- Cargo.toml | 8 + README.md | 3 +- examples/curses.rs | 42 ++++ examples/curses_demo.rs | 497 ++++++++++++++++++++++++++++++++++++++++ src/backend/curses.rs | 231 +++++++++++++++++++ src/backend/mod.rs | 5 + 6 files changed, 785 insertions(+), 1 deletion(-) create mode 100644 examples/curses.rs create mode 100644 examples/curses_demo.rs create mode 100644 src/backend/curses.rs diff --git a/Cargo.toml b/Cargo.toml index 4950f5e..187f4c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ appveyor = { repository = "fdehau/tui-rs" } [features] default = ["termion"] +curses = ["easycurses", "pancurses"] [dependencies] bitflags = "1.0" @@ -30,6 +31,8 @@ unicode-width = "0.1" termion = { version = "1.5", optional = true } rustbox = { version = "0.11", 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] stderrlog = "0.4" @@ -51,3 +54,8 @@ required-features = ["rustbox"] name = "crossterm_demo" path = "examples/crossterm_demo.rs" required-features = ["crossterm"] + +[[example]] +name = "curses" +path = "examples/curses.rs" +required-features = ["curses"] diff --git a/README.md b/README.md index cf80562..675d273 100644 --- a/README.md +++ b/README.md @@ -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 `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: - [termion](https://github.com/ticki/termion) - [rustbox](https://github.com/gchp/rustbox) - [crossterm](https://github.com/TimonPost/crossterm) + - [pancurses](https://github.com/ihalila/pancurses) However, some features may only be available in one of the three. diff --git a/examples/curses.rs b/examples/curses.rs new file mode 100644 index 0000000..0428dda --- /dev/null +++ b/examples/curses.rs @@ -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) -> 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) + }) +} diff --git a/examples/curses_demo.rs b/examples/curses_demo.rs new file mode 100644 index 0000000..498578a --- /dev/null +++ b/examples/curses_demo.rs @@ -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, + data2: Vec<(f64, f64)>, + data3: Vec<(f64, f64)>, + data4: Vec<(&'a str, u64)>, + window: [f64; 2], + colors: [Color; 2], + color_index: usize, + servers: Vec>, +} + +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(f: &mut Frame, 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(f: &mut Frame, 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(f: &mut Frame, 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(f: &mut Frame, 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(f: &mut Frame, 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]); +} diff --git a/src/backend/curses.rs b/src/backend/curses.rs new file mode 100644 index 0000000..d982072 --- /dev/null +++ b/src/backend/curses.rs @@ -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 { + 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, + { + 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 { + 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 for Option { + fn from(color: Color) -> Option { + 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 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, + } + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 5abe0cb..e21be38 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -18,6 +18,11 @@ mod crossterm; #[cfg(feature = "crossterm")] pub use self::crossterm::CrosstermBackend; +#[cfg(feature = "curses")] +mod curses; +#[cfg(feature = "curses")] +pub use self::curses::CursesBackend; + mod test; pub use self::test::TestBackend;