diff --git a/Cargo.toml b/Cargo.toml index 82bc241..78067bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,7 @@ edition = "2018" [badges] [features] -default = ["termion"] -curses = ["easycurses", "pancurses"] +default = ["crossterm"] [dependencies] bitflags = "1.3" @@ -26,102 +25,14 @@ cassowary = "0.3" unicode-segmentation = "1.2" unicode-width = "0.1" termion = { version = "1.5", optional = true } -rustbox = { version = "0.11", optional = true } -crossterm = { version = "0.20", optional = true } -easycurses = { version = "0.12.2", optional = true } -pancurses = { version = "0.16.1", optional = true, features = ["win32a"] } +crossterm = { version = "0.22", optional = true } serde = { version = "1", "optional" = true, features = ["derive"]} [dev-dependencies] rand = "0.8" argh = "0.1" -[[example]] -name = "canvas" -path = "examples/canvas.rs" -required-features = ["termion"] - -[[example]] -name = "user_input" -path = "examples/user_input.rs" -required-features = ["termion"] - -[[example]] -name = "gauge" -path = "examples/gauge.rs" -required-features = ["termion"] - -[[example]] -name = "barchart" -path = "examples/barchart.rs" -required-features = ["termion"] - -[[example]] -name = "chart" -path = "examples/chart.rs" -required-features = ["termion"] - -[[example]] -name = "paragraph" -path = "examples/paragraph.rs" -required-features = ["termion"] - -[[example]] -name = "list" -path = "examples/list.rs" -required-features = ["termion"] - -[[example]] -name = "table" -path = "examples/table.rs" -required-features = ["termion"] - -[[example]] -name = "tabs" -path = "examples/tabs.rs" -required-features = ["termion"] - -[[example]] -name = "custom_widget" -path = "examples/custom_widget.rs" -required-features = ["termion"] - -[[example]] -name = "layout" -path = "examples/layout.rs" -required-features = ["termion"] - -[[example]] -name = "popup" -path = "examples/popup.rs" -required-features = ["termion"] - -[[example]] -name = "block" -path = "examples/block.rs" -required-features = ["termion"] - -[[example]] -name = "sparkline" -path = "examples/sparkline.rs" -required-features = ["termion"] - [[example]] name = "termion_demo" path = "examples/termion_demo.rs" required-features = ["termion"] - -[[example]] -name = "rustbox_demo" -path = "examples/rustbox_demo.rs" -required-features = ["rustbox"] - -[[example]] -name = "crossterm_demo" -path = "examples/crossterm_demo.rs" -required-features = ["crossterm"] - -[[example]] -name = "curses_demo" -path = "examples/curses_demo.rs" -required-features = ["curses"] diff --git a/Makefile.toml b/Makefile.toml index c858f22..9a0771a 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -6,8 +6,8 @@ source = "${CARGO_MAKE_RUST_TARGET_OS}" default_value = "unknown" [env.TUI_FEATURES.mapping] -linux = "serde,crossterm,termion,rustbox,curses" -macos = "serde,crossterm,termion,rustbox,curses" +linux = "serde,crossterm,termion" +macos = "serde,crossterm,termion" windows = "serde,crossterm" [tasks.default] @@ -66,11 +66,6 @@ args = [ ] [tasks.test] -linux_alias = "test-unix" -mac_alias = "test-unix" -windows_alias = "test-windows" - -[tasks.test-unix] command = "cargo" args = [ "test", @@ -79,19 +74,6 @@ args = [ "${TUI_FEATURES}" ] -# Documentation tests cannot be run on Windows for now -[tasks.test-windows] -command = "cargo" -args = [ - "test", - "--no-default-features", - "--features", - "${TUI_FEATURES}", - "--lib", - "--tests", - "--examples", -] - [tasks.run-example] private = true condition = { env_set = ["TUI_EXAMPLE_NAME", "TUI_FEATURES"] } diff --git a/examples/barchart.rs b/examples/barchart.rs index 8179cb7..9109589 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -1,15 +1,19 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Event, Events}; -use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, +}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, widgets::{BarChart, Block, Borders}, - Terminal, + Frame, Terminal, }; struct App<'a> { @@ -48,85 +52,111 @@ impl<'a> App<'a> { } } - fn update(&mut self) { + fn on_tick(&mut self) { let value = self.data.pop().unwrap(); self.data.insert(0, value); } } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Setup event handlers - let events = Events::new(); - - // App - let mut app = App::new(); + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); - loop { - terminal.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(f.size()); - let barchart = BarChart::default() - .block(Block::default().title("Data1").borders(Borders::ALL)) - .data(&app.data) - .bar_width(9) - .bar_style(Style::default().fg(Color::Yellow)) - .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)); - f.render_widget(barchart, chunks[0]); + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[1]); + if let Err(err) = res { + println!("{:?}", err) + } - let barchart = BarChart::default() - .block(Block::default().title("Data2").borders(Borders::ALL)) - .data(&app.data) - .bar_width(5) - .bar_gap(3) - .bar_style(Style::default().fg(Color::Green)) - .value_style( - Style::default() - .bg(Color::Green) - .add_modifier(Modifier::BOLD), - ); - f.render_widget(barchart, chunks[0]); + Ok(()) +} - let barchart = BarChart::default() - .block(Block::default().title("Data3").borders(Borders::ALL)) - .data(&app.data) - .bar_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) - .add_modifier(Modifier::ITALIC), - ); - f.render_widget(barchart, chunks[1]); - })?; +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); + loop { + terminal.draw(|f| ui(f, &mut app))?; - match events.next()? { - Event::Input(input) => { - if input == Key::Char('q') { - break; + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} } } - Event::Tick => { - app.update(); - } + } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); } } +} - Ok(()) +fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(f.size()); + let barchart = BarChart::default() + .block(Block::default().title("Data1").borders(Borders::ALL)) + .data(&app.data) + .bar_width(9) + .bar_style(Style::default().fg(Color::Yellow)) + .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)); + f.render_widget(barchart, chunks[0]); + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(chunks[1]); + + let barchart = BarChart::default() + .block(Block::default().title("Data2").borders(Borders::ALL)) + .data(&app.data) + .bar_width(5) + .bar_gap(3) + .bar_style(Style::default().fg(Color::Green)) + .value_style( + Style::default() + .bg(Color::Green) + .add_modifier(Modifier::BOLD), + ); + f.render_widget(barchart, chunks[0]); + + let barchart = BarChart::default() + .block(Block::default().title("Data3").borders(Borders::ALL)) + .data(&app.data) + .bar_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) + .add_modifier(Modifier::ITALIC), + ); + f.render_widget(barchart, chunks[1]); } diff --git a/examples/block.rs b/examples/block.rs index d7c84f1..60c82d4 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -1,101 +1,120 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Event, Events}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::Span, widgets::{Block, BorderType, Borders}, - Terminal, + Frame, Terminal, }; fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Setup event handlers - let events = Events::new(); + // create app and run it + let res = run_app(&mut terminal); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} +fn run_app(terminal: &mut Terminal) -> io::Result<()> { loop { - terminal.draw(|f| { - // 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 - let size = f.size(); - - // Surrounding block - let block = Block::default() - .borders(Borders::ALL) - .title("Main block with round corners") - .title_alignment(Alignment::Center) - .border_type(BorderType::Rounded); - f.render_widget(block, size); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(4) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(f.size()); - - // Top two inner blocks - let top_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[0]); - - // Top left inner block with green background - let block = Block::default() - .title(vec![ - Span::styled("With", Style::default().fg(Color::Yellow)), - Span::from(" background"), - ]) - .style(Style::default().bg(Color::Green)); - f.render_widget(block, top_chunks[0]); - - // Top right inner block with styled title aligned to the right - let block = Block::default() - .title(Span::styled( - "Styled title", - Style::default() - .fg(Color::White) - .bg(Color::Red) - .add_modifier(Modifier::BOLD), - )) - .title_alignment(Alignment::Right); - f.render_widget(block, top_chunks[1]); - - // Bottom two inner blocks - let bottom_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[1]); - - // Bottom left block with all default borders - let block = Block::default().title("With borders").borders(Borders::ALL); - f.render_widget(block, bottom_chunks[0]); - - // Bottom right block with styled left and right border - let block = Block::default() - .title("With styled borders and doubled borders") - .border_style(Style::default().fg(Color::Cyan)) - .borders(Borders::LEFT | Borders::RIGHT) - .border_type(BorderType::Double); - f.render_widget(block, bottom_chunks[1]); - })?; - - if let Event::Input(key) = events.next()? { - if key == Key::Char('q') { - break; + terminal.draw(ui)?; + + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} } } } - Ok(()) +} + +fn ui(f: &mut Frame) { + // 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 + let size = f.size(); + + // Surrounding block + let block = Block::default() + .borders(Borders::ALL) + .title("Main block with round corners") + .title_alignment(Alignment::Center) + .border_type(BorderType::Rounded); + f.render_widget(block, size); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(4) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(f.size()); + + // Top two inner blocks + let top_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(chunks[0]); + + // Top left inner block with green background + let block = Block::default() + .title(vec![ + Span::styled("With", Style::default().fg(Color::Yellow)), + Span::from(" background"), + ]) + .style(Style::default().bg(Color::Green)); + f.render_widget(block, top_chunks[0]); + + // Top right inner block with styled title aligned to the right + let block = Block::default() + .title(Span::styled( + "Styled title", + Style::default() + .fg(Color::White) + .bg(Color::Red) + .add_modifier(Modifier::BOLD), + )) + .title_alignment(Alignment::Right); + f.render_widget(block, top_chunks[1]); + + // Bottom two inner blocks + let bottom_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(chunks[1]); + + // Bottom left block with all default borders + let block = Block::default().title("With borders").borders(Borders::ALL); + f.render_widget(block, bottom_chunks[0]); + + // Bottom right block with styled left and right border + let block = Block::default() + .title("With styled borders and doubled borders") + .border_style(Style::default().fg(Color::Cyan)) + .borders(Borders::LEFT | Borders::RIGHT) + .border_type(BorderType::Double); + f.render_widget(block, bottom_chunks[1]); } diff --git a/examples/canvas.rs b/examples/canvas.rs index f3cb380..1e08fb0 100644 --- a/examples/canvas.rs +++ b/examples/canvas.rs @@ -1,11 +1,15 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Config, Event, Events}; -use std::{error::Error, io, time::Duration}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, +}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, text::Span, @@ -13,7 +17,7 @@ use tui::{ canvas::{Canvas, Map, MapResolution, Rectangle}, Block, Borders, }, - Terminal, + Frame, Terminal, }; struct App { @@ -47,7 +51,7 @@ impl App { } } - fn update(&mut self) { + fn on_tick(&mut self) { if self.ball.x < self.playground.left() as f64 || self.ball.x + self.ball.width > self.playground.right() as f64 { @@ -74,80 +78,103 @@ impl App { } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Setup event handlers - let config = Config { - tick_rate: Duration::from_millis(250), - ..Default::default() - }; - let events = Events::with_config(config); + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); - // App - let mut app = App::new(); + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); loop { - terminal.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(f.size()); - let canvas = Canvas::default() - .block(Block::default().borders(Borders::ALL).title("World")) - .paint(|ctx| { - ctx.draw(&Map { - color: Color::White, - resolution: MapResolution::High, - }); - ctx.print( - app.x, - -app.y, - Span::styled("You are here", Style::default().fg(Color::Yellow)), - ); - }) - .x_bounds([-180.0, 180.0]) - .y_bounds([-90.0, 90.0]); - f.render_widget(canvas, chunks[0]); - let canvas = Canvas::default() - .block(Block::default().borders(Borders::ALL).title("Pong")) - .paint(|ctx| { - ctx.draw(&app.ball); - }) - .x_bounds([10.0, 110.0]) - .y_bounds([10.0, 110.0]); - f.render_widget(canvas, chunks[1]); - })?; + terminal.draw(|f| ui(f, &app))?; - match events.next()? { - Event::Input(input) => match input { - Key::Char('q') => { - break; - } - Key::Down => { - app.y += 1.0; + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => { + return Ok(()); + } + KeyCode::Down => { + app.y += 1.0; + } + KeyCode::Up => { + app.y -= 1.0; + } + KeyCode::Right => { + app.x += 1.0; + } + KeyCode::Left => { + app.x -= 1.0; + } + _ => {} } - Key::Up => { - app.y -= 1.0; - } - Key::Right => { - app.x += 1.0; - } - Key::Left => { - app.x -= 1.0; - } - - _ => {} - }, - Event::Tick => { - app.update(); } } + + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); + } } +} - Ok(()) +fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(f.size()); + let canvas = Canvas::default() + .block(Block::default().borders(Borders::ALL).title("World")) + .paint(|ctx| { + ctx.draw(&Map { + color: Color::White, + resolution: MapResolution::High, + }); + ctx.print( + app.x, + -app.y, + Span::styled("You are here", Style::default().fg(Color::Yellow)), + ); + }) + .x_bounds([-180.0, 180.0]) + .y_bounds([-90.0, 90.0]); + f.render_widget(canvas, chunks[0]); + let canvas = Canvas::default() + .block(Block::default().borders(Borders::ALL).title("Pong")) + .paint(|ctx| { + ctx.draw(&app.ball); + }) + .x_bounds([10.0, 110.0]) + .y_bounds([10.0, 110.0]); + f.render_widget(canvas, chunks[1]); } diff --git a/examples/chart.rs b/examples/chart.rs index 58d7ee4..71a9dba 100644 --- a/examples/chart.rs +++ b/examples/chart.rs @@ -1,20 +1,25 @@ #[allow(dead_code)] mod util; -use crate::util::{ - event::{Event, Events}, - SinSignal, +use crate::util::SinSignal; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, }; -use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, symbols, text::Span, widgets::{Axis, Block, Borders, Chart, Dataset, GraphType}, - Terminal, + Frame, Terminal, }; const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)]; @@ -51,7 +56,7 @@ impl App { } } - fn update(&mut self) { + fn on_tick(&mut self) { for _ in 0..5 { self.data1.remove(0); } @@ -66,181 +71,208 @@ impl App { } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let events = Events::new(); + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - // App - let mut app = App::new(); + if let Err(err) = res { + println!("{:?}", err) + } + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); loop { - terminal.draw(|f| { - let size = f.size(); - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - ] - .as_ref(), - ) - .split(size); - let x_labels = vec![ - Span::styled( - format!("{}", app.window[0]), - Style::default().add_modifier(Modifier::BOLD), - ), - Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)), - Span::styled( - format!("{}", app.window[1]), - Style::default().add_modifier(Modifier::BOLD), - ), - ]; - let datasets = vec![ - Dataset::default() - .name("data2") - .marker(symbols::Marker::Dot) - .style(Style::default().fg(Color::Cyan)) - .data(&app.data1), - Dataset::default() - .name("data3") - .marker(symbols::Marker::Braille) - .style(Style::default().fg(Color::Yellow)) - .data(&app.data2), - ]; - - let chart = Chart::new(datasets) - .block( - Block::default() - .title(Span::styled( - "Chart 1", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL), - ) - .x_axis( - Axis::default() - .title("X Axis") - .style(Style::default().fg(Color::Gray)) - .labels(x_labels) - .bounds(app.window), - ) - .y_axis( - Axis::default() - .title("Y Axis") - .style(Style::default().fg(Color::Gray)) - .labels(vec![ - Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("0"), - Span::styled("20", Style::default().add_modifier(Modifier::BOLD)), - ]) - .bounds([-20.0, 20.0]), - ); - f.render_widget(chart, chunks[0]); - - let datasets = vec![Dataset::default() - .name("data") - .marker(symbols::Marker::Braille) - .style(Style::default().fg(Color::Yellow)) - .graph_type(GraphType::Line) - .data(&DATA)]; - let chart = Chart::new(datasets) - .block( - Block::default() - .title(Span::styled( - "Chart 2", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL), - ) - .x_axis( - Axis::default() - .title("X Axis") - .style(Style::default().fg(Color::Gray)) - .bounds([0.0, 5.0]) - .labels(vec![ - Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("2.5"), - Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)), - ]), - ) - .y_axis( - Axis::default() - .title("Y Axis") - .style(Style::default().fg(Color::Gray)) - .bounds([0.0, 5.0]) - .labels(vec![ - Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("2.5"), - Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)), - ]), - ); - f.render_widget(chart, chunks[1]); - - let datasets = vec![Dataset::default() - .name("data") - .marker(symbols::Marker::Braille) - .style(Style::default().fg(Color::Yellow)) - .graph_type(GraphType::Line) - .data(&DATA2)]; - let chart = Chart::new(datasets) - .block( - Block::default() - .title(Span::styled( - "Chart 3", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL), - ) - .x_axis( - Axis::default() - .title("X Axis") - .style(Style::default().fg(Color::Gray)) - .bounds([0.0, 50.0]) - .labels(vec![ - Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("25"), - Span::styled("50", Style::default().add_modifier(Modifier::BOLD)), - ]), - ) - .y_axis( - Axis::default() - .title("Y Axis") - .style(Style::default().fg(Color::Gray)) - .bounds([0.0, 5.0]) - .labels(vec![ - Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), - Span::raw("2.5"), - Span::styled("5", Style::default().add_modifier(Modifier::BOLD)), - ]), - ); - f.render_widget(chart, chunks[2]); - })?; - - match events.next()? { - Event::Input(input) => { - if input == Key::Char('q') { - break; + terminal.draw(|f| ui(f, &mut app))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} } } - Event::Tick => { - app.update(); - } + } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); } } +} - Ok(()) +fn ui(f: &mut Frame, app: &App) { + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ] + .as_ref(), + ) + .split(size); + let x_labels = vec![ + Span::styled( + format!("{}", app.window[0]), + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)), + Span::styled( + format!("{}", app.window[1]), + Style::default().add_modifier(Modifier::BOLD), + ), + ]; + let datasets = vec![ + Dataset::default() + .name("data2") + .marker(symbols::Marker::Dot) + .style(Style::default().fg(Color::Cyan)) + .data(&app.data1), + Dataset::default() + .name("data3") + .marker(symbols::Marker::Braille) + .style(Style::default().fg(Color::Yellow)) + .data(&app.data2), + ]; + + let chart = Chart::new(datasets) + .block( + Block::default() + .title(Span::styled( + "Chart 1", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .x_axis( + Axis::default() + .title("X Axis") + .style(Style::default().fg(Color::Gray)) + .labels(x_labels) + .bounds(app.window), + ) + .y_axis( + Axis::default() + .title("Y Axis") + .style(Style::default().fg(Color::Gray)) + .labels(vec![ + Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("0"), + Span::styled("20", Style::default().add_modifier(Modifier::BOLD)), + ]) + .bounds([-20.0, 20.0]), + ); + f.render_widget(chart, chunks[0]); + + let datasets = vec![Dataset::default() + .name("data") + .marker(symbols::Marker::Braille) + .style(Style::default().fg(Color::Yellow)) + .graph_type(GraphType::Line) + .data(&DATA)]; + let chart = Chart::new(datasets) + .block( + Block::default() + .title(Span::styled( + "Chart 2", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .x_axis( + Axis::default() + .title("X Axis") + .style(Style::default().fg(Color::Gray)) + .bounds([0.0, 5.0]) + .labels(vec![ + Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("2.5"), + Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)), + ]), + ) + .y_axis( + Axis::default() + .title("Y Axis") + .style(Style::default().fg(Color::Gray)) + .bounds([0.0, 5.0]) + .labels(vec![ + Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("2.5"), + Span::styled("5.0", Style::default().add_modifier(Modifier::BOLD)), + ]), + ); + f.render_widget(chart, chunks[1]); + + let datasets = vec![Dataset::default() + .name("data") + .marker(symbols::Marker::Braille) + .style(Style::default().fg(Color::Yellow)) + .graph_type(GraphType::Line) + .data(&DATA2)]; + let chart = Chart::new(datasets) + .block( + Block::default() + .title(Span::styled( + "Chart 3", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL), + ) + .x_axis( + Axis::default() + .title("X Axis") + .style(Style::default().fg(Color::Gray)) + .bounds([0.0, 50.0]) + .labels(vec![ + Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("25"), + Span::styled("50", Style::default().add_modifier(Modifier::BOLD)), + ]), + ) + .y_axis( + Axis::default() + .title("Y Axis") + .style(Style::default().fg(Color::Gray)) + .bounds([0.0, 5.0]) + .labels(vec![ + Span::styled("0", Style::default().add_modifier(Modifier::BOLD)), + Span::raw("2.5"), + Span::styled("5", Style::default().add_modifier(Modifier::BOLD)), + ]), + ); + f.render_widget(chart, chunks[2]); } diff --git a/examples/crossterm_demo.rs b/examples/crossterm_demo.rs index 5dc8475..7c5adec 100644 --- a/examples/crossterm_demo.rs +++ b/examples/crossterm_demo.rs @@ -6,23 +6,19 @@ mod util; use crate::demo::{ui, App}; use argh::FromArgs; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use std::{ error::Error, - io::stdout, - sync::mpsc, - thread, + io, time::{Duration, Instant}, }; -use tui::{backend::CrosstermBackend, Terminal}; - -enum Event { - Input(I), - Tick, -} +use tui::{ + backend::{Backend, CrosstermBackend}, + Terminal, +}; /// Crossterm demo #[derive(Debug, FromArgs)] @@ -38,71 +34,64 @@ struct Cli { fn main() -> Result<(), Box> { let cli: Cli = argh::from_env(); + // setup terminal enable_raw_mode()?; - - let mut stdout = stdout(); + let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - // Setup input handling - let (tx, rx) = mpsc::channel(); - + // create app and run it let tick_rate = Duration::from_millis(cli.tick_rate); - thread::spawn(move || { - let mut last_tick = Instant::now(); - loop { - // poll for tick rate duration, if no events, sent tick event. - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - if event::poll(timeout).unwrap() { - if let CEvent::Key(key) = event::read().unwrap() { - tx.send(Event::Input(key)).unwrap(); - } - } - if last_tick.elapsed() >= tick_rate { - tx.send(Event::Tick).unwrap(); - last_tick = Instant::now(); - } - } - }); + let app = App::new("Crossterm Demo", cli.enhanced_graphics); + let res = run_app(&mut terminal, app, tick_rate); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - let mut app = App::new("Crossterm Demo", cli.enhanced_graphics); + if let Err(err) = res { + println!("{:?}", err) + } - terminal.clear()?; + Ok(()) +} +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); loop { terminal.draw(|f| ui::draw(f, &mut app))?; - match rx.recv()? { - Event::Input(event) => match event.code { - KeyCode::Char('q') => { - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - break; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char(c) => app.on_key(c), + KeyCode::Left => app.on_left(), + KeyCode::Up => app.on_up(), + KeyCode::Right => app.on_right(), + KeyCode::Down => app.on_down(), + _ => {} } - KeyCode::Char(c) => app.on_key(c), - KeyCode::Left => app.on_left(), - KeyCode::Up => app.on_up(), - KeyCode::Right => app.on_right(), - KeyCode::Down => app.on_down(), - _ => {} - }, - Event::Tick => { - app.on_tick(); } } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); + } if app.should_quit { - break; + return Ok(()); } } - - Ok(()) } diff --git a/examples/curses_demo.rs b/examples/curses_demo.rs deleted file mode 100644 index b8c3a6a..0000000 --- a/examples/curses_demo.rs +++ /dev/null @@ -1,74 +0,0 @@ -mod demo; -#[allow(dead_code)] -mod util; - -use crate::demo::{ui, App}; -use argh::FromArgs; -use std::{ - error::Error, - io, - time::{Duration, Instant}, -}; -use tui::{backend::CursesBackend, Terminal}; - -/// Curses demo -#[derive(Debug, FromArgs)] -struct Cli { - /// time in ms between two ticks. - #[argh(option, default = "250")] - tick_rate: u64, - /// whether unicode symbols are used to improve the overall look of the app - #[argh(option, default = "true")] - enhanced_graphics: bool, -} - -fn main() -> Result<(), Box> { - let cli: Cli = argh::from_env(); - - let mut backend = - CursesBackend::new().ok_or_else(|| io::Error::new(io::ErrorKind::Other, ""))?; - let curses = backend.get_curses_mut(); - curses.set_echo(false); - curses.set_input_timeout(easycurses::TimeoutMode::WaitUpTo(50)); - curses.set_input_mode(easycurses::InputMode::RawCharacter); - curses.set_keypad_enabled(true); - let mut terminal = Terminal::new(backend)?; - terminal.hide_cursor()?; - - let mut app = App::new("Curses demo", cli.enhanced_graphics); - - let mut last_tick = Instant::now(); - let tick_rate = Duration::from_millis(cli.tick_rate); - loop { - terminal.draw(|f| ui::draw(f, &mut app))?; - if let Some(input) = terminal.backend_mut().get_curses_mut().get_input() { - match input { - easycurses::Input::Character(c) => { - app.on_key(c); - } - easycurses::Input::KeyUp => { - app.on_up(); - } - easycurses::Input::KeyDown => { - app.on_down(); - } - easycurses::Input::KeyLeft => { - app.on_left(); - } - easycurses::Input::KeyRight => { - app.on_right(); - } - _ => {} - }; - }; - terminal.backend_mut().get_curses_mut().flush_input(); - if last_tick.elapsed() > tick_rate { - app.on_tick(); - last_tick = Instant::now(); - } - if app.should_quit { - break; - } - } - Ok(()) -} diff --git a/examples/custom_widget.rs b/examples/custom_widget.rs index d01ef3b..8e7b48c 100644 --- a/examples/custom_widget.rs +++ b/examples/custom_widget.rs @@ -1,11 +1,16 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Event, Events}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, buffer::Buffer, layout::Rect, style::Style, widgets::Widget, Terminal, + backend::{Backend, CrosstermBackend}, + buffer::Buffer, + layout::Rect, + style::Style, + widgets::Widget, + Frame, Terminal, }; struct Label<'a> { @@ -32,28 +37,47 @@ impl<'a> Label<'a> { } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let events = Events::new(); + // create app and run it + let res = run_app(&mut terminal); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} + +fn run_app(terminal: &mut Terminal) -> io::Result<()> { loop { - terminal.draw(|f| { - let size = f.size(); - let label = Label::default().text("Test"); - f.render_widget(label, size); - })?; - - if let Event::Input(key) = events.next()? { - if key == Key::Char('q') { - break; + terminal.draw(ui)?; + + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} } } } +} - Ok(()) +fn ui(f: &mut Frame) { + let size = f.size(); + let label = Label::default().text("Test"); + f.render_widget(label, size); } diff --git a/examples/gauge.rs b/examples/gauge.rs index 9cb097b..de0d969 100644 --- a/examples/gauge.rs +++ b/examples/gauge.rs @@ -1,16 +1,20 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Config, Event, Events}; -use std::{error::Error, io, time::Duration}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, +}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::Span, widgets::{Block, Borders, Gauge}, - Terminal, + Frame, Terminal, }; struct App { @@ -30,7 +34,7 @@ impl App { } } - fn update(&mut self) { + fn on_tick(&mut self) { self.progress1 += 1; if self.progress1 > 100 { self.progress1 = 0; @@ -51,87 +55,113 @@ impl App { } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let events = Events::with_config(Config { - tick_rate: Duration::from_millis(100), - }); + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); - let mut app = App::new(); - - loop { - terminal.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints( - [ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - ] - .as_ref(), - ) - .split(f.size()); + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - let gauge = Gauge::default() - .block(Block::default().title("Gauge1").borders(Borders::ALL)) - .gauge_style(Style::default().fg(Color::Yellow)) - .percent(app.progress1); - f.render_widget(gauge, chunks[0]); - - let label = format!("{}/100", app.progress2); - let gauge = Gauge::default() - .block(Block::default().title("Gauge2").borders(Borders::ALL)) - .gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green)) - .percent(app.progress2) - .label(label); - f.render_widget(gauge, chunks[1]); + if let Err(err) = res { + println!("{:?}", err) + } - let label = Span::styled( - format!("{:.2}%", app.progress3 * 100.0), - Style::default() - .fg(Color::Red) - .add_modifier(Modifier::ITALIC | Modifier::BOLD), - ); - let gauge = Gauge::default() - .block(Block::default().title("Gauge3").borders(Borders::ALL)) - .gauge_style(Style::default().fg(Color::Yellow)) - .ratio(app.progress3) - .label(label) - .use_unicode(true); - f.render_widget(gauge, chunks[2]); + Ok(()) +} - let label = format!("{}/100", app.progress2); - let gauge = Gauge::default() - .block(Block::default().title("Gauge4")) - .gauge_style( - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::ITALIC), - ) - .percent(app.progress4) - .label(label); - f.render_widget(gauge, chunks[3]); - })?; +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); + loop { + terminal.draw(|f| ui(f, &mut app))?; - match events.next()? { - Event::Input(input) => { - if input == Key::Char('q') { - break; + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} } } - Event::Tick => { - app.update(); - } + } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); } } +} - Ok(()) +fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .split(f.size()); + + let gauge = Gauge::default() + .block(Block::default().title("Gauge1").borders(Borders::ALL)) + .gauge_style(Style::default().fg(Color::Yellow)) + .percent(app.progress1); + f.render_widget(gauge, chunks[0]); + + let label = format!("{}/100", app.progress2); + let gauge = Gauge::default() + .block(Block::default().title("Gauge2").borders(Borders::ALL)) + .gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green)) + .percent(app.progress2) + .label(label); + f.render_widget(gauge, chunks[1]); + + let label = Span::styled( + format!("{:.2}%", app.progress3 * 100.0), + Style::default() + .fg(Color::Red) + .add_modifier(Modifier::ITALIC | Modifier::BOLD), + ); + let gauge = Gauge::default() + .block(Block::default().title("Gauge3").borders(Borders::ALL)) + .gauge_style(Style::default().fg(Color::Yellow)) + .ratio(app.progress3) + .label(label) + .use_unicode(true); + f.render_widget(gauge, chunks[2]); + + let label = format!("{}/100", app.progress2); + let gauge = Gauge::default() + .block(Block::default().title("Gauge4")) + .gauge_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::ITALIC), + ) + .percent(app.progress4) + .label(label); + f.render_widget(gauge, chunks[3]); } diff --git a/examples/layout.rs b/examples/layout.rs index b740186..baf3626 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -1,52 +1,71 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Event, Events}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, widgets::{Block, Borders}, - Terminal, + Frame, Terminal, }; fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let events = Events::new(); + // create app and run it + let res = run_app(&mut terminal); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} +fn run_app(terminal: &mut Terminal) -> io::Result<()> { loop { - terminal.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(10), - Constraint::Percentage(80), - Constraint::Percentage(10), - ] - .as_ref(), - ) - .split(f.size()); - - let block = Block::default().title("Block").borders(Borders::ALL); - f.render_widget(block, chunks[0]); - let block = Block::default().title("Block 2").borders(Borders::ALL); - f.render_widget(block, chunks[2]); - })?; - - if let Event::Input(input) = events.next()? { - if let Key::Char('q') = input { - break; + terminal.draw(|f| ui(f))?; + + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} } } } +} - Ok(()) +fn ui(f: &mut Frame) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(10), + Constraint::Percentage(80), + Constraint::Percentage(10), + ] + .as_ref(), + ) + .split(f.size()); + + let block = Block::default().title("Block").borders(Borders::ALL); + f.render_widget(block, chunks[0]); + let block = Block::default().title("Block 2").borders(Borders::ALL); + f.render_widget(block, chunks[2]); } diff --git a/examples/list.rs b/examples/list.rs index 086b1ba..0cb14bc 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -1,19 +1,24 @@ #[allow(dead_code)] mod util; -use crate::util::{ - event::{Event, Events}, - StatefulList, +use crate::util::StatefulList; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, }; -use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Corner, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans}, widgets::{Block, Borders, List, ListItem}, - Terminal, + Frame, Terminal, }; /// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper @@ -90,134 +95,150 @@ impl<'a> App<'a> { /// Rotate through the event list. /// This only exists to simulate some kind of "progress" - fn advance(&mut self) { + fn on_tick(&mut self) { let event = self.events.remove(0); self.events.push(event); } } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let events = Events::new(); + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - // Create a new app with some example state - let mut app = App::new(); + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); loop { - terminal.draw(|f| { - // Create two chunks with equal horizontal screen space - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(f.size()); - - // Iterate through all elements in the `items` app and append some debug text to it. - let items: Vec = app - .items - .items - .iter() - .map(|i| { - let mut lines = vec![Spans::from(i.0)]; - for _ in 0..i.1 { - lines.push(Spans::from(Span::styled( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - Style::default().add_modifier(Modifier::ITALIC), - ))); - } - ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White)) - }) - .collect(); - - // Create a List from all list items and highlight the currently selected one - let items = List::new(items) - .block(Block::default().borders(Borders::ALL).title("List")) - .highlight_style( - Style::default() - .bg(Color::LightGreen) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">> "); - - // We can now render the item list - f.render_stateful_widget(items, chunks[0], &mut app.items.state); - - // Let's do the same for the events. - // The event list doesn't have any state and only displays the current state of the list. - let events: Vec = app - .events - .iter() - .rev() - .map(|&(event, level)| { - // Colorcode the level depending on its type - let s = match level { - "CRITICAL" => Style::default().fg(Color::Red), - "ERROR" => Style::default().fg(Color::Magenta), - "WARNING" => Style::default().fg(Color::Yellow), - "INFO" => Style::default().fg(Color::Blue), - _ => Style::default(), - }; - // Add a example datetime and apply proper spacing between them - let header = Spans::from(vec![ - Span::styled(format!("{:<9}", level), s), - Span::raw(" "), - Span::styled( - "2020-01-01 10:00:00", - Style::default().add_modifier(Modifier::ITALIC), - ), - ]); - // The event gets its own line - let log = Spans::from(vec![Span::raw(event)]); - - // Here several things happen: - // 1. Add a `---` spacing line above the final list entry - // 2. Add the Level + datetime - // 3. Add a spacer line - // 4. Add the actual event - ListItem::new(vec![ - Spans::from("-".repeat(chunks[1].width as usize)), - header, - Spans::from(""), - log, - ]) - }) - .collect(); - let events_list = List::new(events) - .block(Block::default().borders(Borders::ALL).title("List")) - .start_corner(Corner::BottomLeft); - f.render_widget(events_list, chunks[1]); - })?; - - // This is a simple example on how to handle events - // 1. This breaks the loop and exits the program on `q` button press. - // 2. The `up`/`down` keys change the currently selected item in the App's `items` list. - // 3. `left` unselects the current item. - match events.next()? { - Event::Input(input) => match input { - Key::Char('q') => { - break; - } - Key::Left => { - app.items.unselect(); - } - Key::Down => { - app.items.next(); - } - Key::Up => { - app.items.previous(); + terminal.draw(|f| ui(f, &mut app))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Left => app.items.unselect(), + KeyCode::Down => app.items.next(), + KeyCode::Up => app.items.previous(), + _ => {} } - _ => {} - }, - Event::Tick => { - app.advance(); } } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); + } } +} - Ok(()) +fn ui(f: &mut Frame, app: &mut App) { + // Create two chunks with equal horizontal screen space + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(f.size()); + + // Iterate through all elements in the `items` app and append some debug text to it. + let items: Vec = app + .items + .items + .iter() + .map(|i| { + let mut lines = vec![Spans::from(i.0)]; + for _ in 0..i.1 { + lines.push(Spans::from(Span::styled( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + Style::default().add_modifier(Modifier::ITALIC), + ))); + } + ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White)) + }) + .collect(); + + // Create a List from all list items and highlight the currently selected one + let items = List::new(items) + .block(Block::default().borders(Borders::ALL).title("List")) + .highlight_style( + Style::default() + .bg(Color::LightGreen) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + // We can now render the item list + f.render_stateful_widget(items, chunks[0], &mut app.items.state); + + // Let's do the same for the events. + // The event list doesn't have any state and only displays the current state of the list. + let events: Vec = app + .events + .iter() + .rev() + .map(|&(event, level)| { + // Colorcode the level depending on its type + let s = match level { + "CRITICAL" => Style::default().fg(Color::Red), + "ERROR" => Style::default().fg(Color::Magenta), + "WARNING" => Style::default().fg(Color::Yellow), + "INFO" => Style::default().fg(Color::Blue), + _ => Style::default(), + }; + // Add a example datetime and apply proper spacing between them + let header = Spans::from(vec![ + Span::styled(format!("{:<9}", level), s), + Span::raw(" "), + Span::styled( + "2020-01-01 10:00:00", + Style::default().add_modifier(Modifier::ITALIC), + ), + ]); + // The event gets its own line + let log = Spans::from(vec![Span::raw(event)]); + + // Here several things happen: + // 1. Add a `---` spacing line above the final list entry + // 2. Add the Level + datetime + // 3. Add a spacer line + // 4. Add the actual event + ListItem::new(vec![ + Spans::from("-".repeat(chunks[1].width as usize)), + header, + Spans::from(""), + log, + ]) + }) + .collect(); + let events_list = List::new(events) + .block(Block::default().borders(Borders::ALL).title("List")) + .start_corner(Corner::BottomLeft); + f.render_widget(events_list, chunks[1]); } diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 587046c..407d176 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -1,111 +1,172 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Event, Events}; -use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, +}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans}, widgets::{Block, Borders, Paragraph, Wrap}, - Terminal, + Frame, Terminal, }; +struct App { + scroll: u16, +} + +impl App { + fn new() -> App { + App { scroll: 0 } + } + + fn on_tick(&mut self) { + self.scroll += 1; + self.scroll %= 10; + } +} + fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let events = Events::new(); + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} - let mut scroll: u16 = 0; +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); loop { - terminal.draw(|f| { - let size = f.size(); - - // Words made "loooong" to demonstrate line breaking. - let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. "; - let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4); - long_line.push('\n'); - - let block = Block::default() - .style(Style::default().bg(Color::White).fg(Color::Black)); - f.render_widget(block, size); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(5) - .constraints( - [ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - ] - .as_ref(), - ) - .split(size); - - let text = vec![ - Spans::from("This is a line "), - Spans::from(Span::styled("This is a line ", Style::default().fg(Color::Red))), - Spans::from(Span::styled("This is a line", Style::default().bg(Color::Blue))), - Spans::from(Span::styled( - "This is a longer line", - Style::default().add_modifier(Modifier::CROSSED_OUT), - )), - Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))), - Spans::from(Span::styled( - "This is a line", - Style::default().fg(Color::Green).add_modifier(Modifier::ITALIC), - )), - ]; - - let create_block = |title| { - Block::default() - .borders(Borders::ALL) - .style(Style::default().bg(Color::White).fg(Color::Black)) - .title(Span::styled(title, Style::default().add_modifier(Modifier::BOLD))) - }; - let paragraph = Paragraph::new(text.clone()) - .style(Style::default().bg(Color::White).fg(Color::Black)) - .block(create_block("Left, no wrap")) - .alignment(Alignment::Left); - f.render_widget(paragraph, chunks[0]); - let paragraph = Paragraph::new(text.clone()) - .style(Style::default().bg(Color::White).fg(Color::Black)) - .block(create_block("Left, wrap")) - .alignment(Alignment::Left) - .wrap(Wrap { trim: true }); - f.render_widget(paragraph, chunks[1]); - let paragraph = Paragraph::new(text.clone()) - .style(Style::default().bg(Color::White).fg(Color::Black)) - .block(create_block("Center, wrap")) - .alignment(Alignment::Center) - .wrap(Wrap { trim: true }) - .scroll((scroll, 0)); - f.render_widget(paragraph, chunks[2]); - let paragraph = Paragraph::new(text) - .style(Style::default().bg(Color::White).fg(Color::Black)) - .block(create_block("Right, wrap")) - .alignment(Alignment::Right) - .wrap(Wrap { trim: true }); - f.render_widget(paragraph, chunks[3]); - })?; - - scroll += 1; - scroll %= 10; - - if let Event::Input(key) = events.next()? { - if key == Key::Char('q') { - break; + terminal.draw(|f| ui(f, &mut app))?; + + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} + } } } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); + } } - Ok(()) +} + +fn ui(f: &mut Frame, app: &App) { + let size = f.size(); + + // Words made "loooong" to demonstrate line breaking. + let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. "; + let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4); + long_line.push('\n'); + + let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black)); + f.render_widget(block, size); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(5) + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .split(size); + + let text = vec![ + Spans::from("This is a line "), + Spans::from(Span::styled( + "This is a line ", + Style::default().fg(Color::Red), + )), + Spans::from(Span::styled( + "This is a line", + Style::default().bg(Color::Blue), + )), + Spans::from(Span::styled( + "This is a longer line", + Style::default().add_modifier(Modifier::CROSSED_OUT), + )), + Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))), + Spans::from(Span::styled( + "This is a line", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::ITALIC), + )), + ]; + + let create_block = |title| { + Block::default() + .borders(Borders::ALL) + .style(Style::default().bg(Color::White).fg(Color::Black)) + .title(Span::styled( + title, + Style::default().add_modifier(Modifier::BOLD), + )) + }; + let paragraph = Paragraph::new(text.clone()) + .style(Style::default().bg(Color::White).fg(Color::Black)) + .block(create_block("Left, no wrap")) + .alignment(Alignment::Left); + f.render_widget(paragraph, chunks[0]); + let paragraph = Paragraph::new(text.clone()) + .style(Style::default().bg(Color::White).fg(Color::Black)) + .block(create_block("Left, wrap")) + .alignment(Alignment::Left) + .wrap(Wrap { trim: true }); + f.render_widget(paragraph, chunks[1]); + let paragraph = Paragraph::new(text.clone()) + .style(Style::default().bg(Color::White).fg(Color::Black)) + .block(create_block("Center, wrap")) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }) + .scroll((app.scroll, 0)); + f.render_widget(paragraph, chunks[2]); + let paragraph = Paragraph::new(text) + .style(Style::default().bg(Color::White).fg(Color::Black)) + .block(create_block("Right, wrap")) + .alignment(Alignment::Right) + .wrap(Wrap { trim: true }); + f.render_widget(paragraph, chunks[3]); } diff --git a/examples/popup.rs b/examples/popup.rs index ca58bc5..0ec9d95 100644 --- a/examples/popup.rs +++ b/examples/popup.rs @@ -1,20 +1,106 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Event, Events}; use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, - text::{Span, Spans}, + text::Span, widgets::{Block, Borders, Clear, Paragraph, Wrap}, - Terminal, + Frame, Terminal, +}; + +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -/// helper function to create a centered rect using up -/// certain percentage of the available rect `r` +struct App { + show_popup: bool, +} + +impl App { + fn new() -> App { + App { show_popup: false } + } +} + +fn main() -> Result<(), Box> { + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // create app and run it + let app = App::new(); + let res = run_app(&mut terminal, app); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} + +fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { + loop { + terminal.draw(|f| ui(f, &mut app))?; + + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Char('p') => app.show_popup = !app.show_popup, + _ => {} + } + } + } +} + +fn ui(f: &mut Frame, app: &App) { + let size = f.size(); + + let chunks = Layout::default() + .constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref()) + .split(size); + + let text = if app.show_popup { + "Press p to close the popup" + } else { + "Press p to show the popup" + }; + let paragraph = Paragraph::new(Span::styled( + text, + Style::default().add_modifier(Modifier::SLOW_BLINK), + )) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + f.render_widget(paragraph, chunks[0]); + + let block = Block::default() + .title("Content") + .borders(Borders::ALL) + .style(Style::default().bg(Color::Blue)); + f.render_widget(block, chunks[1]); + + if app.show_popup { + let block = Block::default().title("Popup").borders(Borders::ALL); + let area = centered_rect(60, 20, size); + f.render_widget(Clear, area); //this clears out the background + f.render_widget(block, area); + } +} + +/// helper function to create a centered rect using up certain percentage of the available rect `r` fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) @@ -40,67 +126,3 @@ fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { ) .split(popup_layout[1])[1] } - -fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let events = Events::new(); - - loop { - terminal.draw(|f| { - let size = f.size(); - - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(size); - - let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. "; - let mut long_line = s.repeat(usize::from(size.width)*usize::from(size.height)/300); - long_line.push('\n'); - - let text = vec![ - Spans::from("This is a line "), - Spans::from(Span::styled("This is a line ", Style::default().fg(Color::Red))), - Spans::from(Span::styled("This is a line", Style::default().bg(Color::Blue))), - Spans::from(Span::styled( - "This is a longer line\n", - Style::default().add_modifier(Modifier::CROSSED_OUT), - )), - Spans::from(Span::styled(&long_line, Style::default().bg(Color::Green))), - Spans::from(Span::styled( - "This is a line\n", - Style::default().fg(Color::Green).add_modifier(Modifier::ITALIC), - )), - ]; - - let paragraph = Paragraph::new(text.clone()) - .block(Block::default().title("Left Block").borders(Borders::ALL)) - .alignment(Alignment::Left).wrap(Wrap { trim: true }); - f.render_widget(paragraph, chunks[0]); - - let paragraph = Paragraph::new(text) - .block(Block::default().title("Right Block").borders(Borders::ALL)) - .alignment(Alignment::Left).wrap(Wrap { trim: true }); - f.render_widget(paragraph, chunks[1]); - - let block = Block::default().title("Popup").borders(Borders::ALL); - let area = centered_rect(60, 20, size); - f.render_widget(Clear, area); //this clears out the background - f.render_widget(block, area); - })?; - - if let Event::Input(input) = events.next()? { - if let Key::Char('q') = input { - break; - } - } - } - - Ok(()) -} diff --git a/examples/rustbox_demo.rs b/examples/rustbox_demo.rs deleted file mode 100644 index 3a5e82f..0000000 --- a/examples/rustbox_demo.rs +++ /dev/null @@ -1,68 +0,0 @@ -mod demo; -#[allow(dead_code)] -mod util; - -use crate::demo::{ui, App}; -use argh::FromArgs; -use rustbox::keyboard::Key; -use std::{ - error::Error, - time::{Duration, Instant}, -}; -use tui::{backend::RustboxBackend, Terminal}; - -/// Rustbox demo -#[derive(Debug, FromArgs)] -struct Cli { - /// time in ms between two ticks. - #[argh(option, default = "250")] - tick_rate: u64, - /// whether unicode symbols are used to improve the overall look of the app - #[argh(option, default = "true")] - enhanced_graphics: bool, -} - -fn main() -> Result<(), Box> { - let cli: Cli = argh::from_env(); - - let backend = RustboxBackend::new()?; - let mut terminal = Terminal::new(backend)?; - - let mut app = App::new("Rustbox demo", cli.enhanced_graphics); - - let mut last_tick = Instant::now(); - let tick_rate = Duration::from_millis(cli.tick_rate); - loop { - terminal.draw(|f| ui::draw(f, &mut app))?; - if let Ok(rustbox::Event::KeyEvent(key)) = - terminal.backend().rustbox().peek_event(tick_rate, false) - { - match key { - Key::Char(c) => { - app.on_key(c); - } - Key::Up => { - app.on_up(); - } - Key::Down => { - app.on_down(); - } - Key::Left => { - app.on_left(); - } - Key::Right => { - app.on_right(); - } - _ => {} - } - } - if last_tick.elapsed() > tick_rate { - app.on_tick(); - last_tick = Instant::now(); - } - if app.should_quit { - break; - } - } - Ok(()) -} diff --git a/examples/sparkline.rs b/examples/sparkline.rs index eb0d3f4..76e7830 100644 --- a/examples/sparkline.rs +++ b/examples/sparkline.rs @@ -1,18 +1,23 @@ #[allow(dead_code)] mod util; -use crate::util::{ - event::{Event, Events}, - RandomSignal, +use crate::util::RandomSignal; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use std::{ + error::Error, + io, + time::{Duration, Instant}, }; -use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Style}, widgets::{Block, Borders, Sparkline}, - Terminal, + Frame, Terminal, }; struct App { @@ -36,7 +41,7 @@ impl App { } } - fn update(&mut self) { + fn on_tick(&mut self) { let value = self.signal.next().unwrap(); self.data1.pop(); self.data1.insert(0, value); @@ -50,75 +55,101 @@ impl App { } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Setup event handlers - let events = Events::new(); + // create app and run it + let tick_rate = Duration::from_millis(250); + let app = App::new(); + let res = run_app(&mut terminal, app, tick_rate); + + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - // Create default app state - let mut app = App::new(); + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> io::Result<()> { + let mut last_tick = Instant::now(); loop { - terminal.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints( - [ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(7), - Constraint::Min(0), - ] - .as_ref(), - ) - .split(f.size()); - let sparkline = Sparkline::default() - .block( - Block::default() - .title("Data1") - .borders(Borders::LEFT | Borders::RIGHT), - ) - .data(&app.data1) - .style(Style::default().fg(Color::Yellow)); - f.render_widget(sparkline, chunks[0]); - let sparkline = Sparkline::default() - .block( - Block::default() - .title("Data2") - .borders(Borders::LEFT | Borders::RIGHT), - ) - .data(&app.data2) - .style(Style::default().bg(Color::Green)); - f.render_widget(sparkline, chunks[1]); - // Multiline - let sparkline = Sparkline::default() - .block( - Block::default() - .title("Data3") - .borders(Borders::LEFT | Borders::RIGHT), - ) - .data(&app.data3) - .style(Style::default().fg(Color::Red)); - f.render_widget(sparkline, chunks[2]); - })?; + terminal.draw(|f| ui(f, &mut app))?; - match events.next()? { - Event::Input(input) => { - if input == Key::Char('q') { - break; + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} } } - Event::Tick => { - app.update(); - } + } + if last_tick.elapsed() >= tick_rate { + app.on_tick(); + last_tick = Instant::now(); } } +} - Ok(()) +fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(7), + Constraint::Min(0), + ] + .as_ref(), + ) + .split(f.size()); + let sparkline = Sparkline::default() + .block( + Block::default() + .title("Data1") + .borders(Borders::LEFT | Borders::RIGHT), + ) + .data(&app.data1) + .style(Style::default().fg(Color::Yellow)); + f.render_widget(sparkline, chunks[0]); + let sparkline = Sparkline::default() + .block( + Block::default() + .title("Data2") + .borders(Borders::LEFT | Borders::RIGHT), + ) + .data(&app.data2) + .style(Style::default().bg(Color::Green)); + f.render_widget(sparkline, chunks[1]); + // Multiline + let sparkline = Sparkline::default() + .block( + Block::default() + .title("Data3") + .borders(Borders::LEFT | Borders::RIGHT), + ) + .data(&app.data3) + .style(Style::default().fg(Color::Red)); + f.render_widget(sparkline, chunks[2]); } diff --git a/examples/table.rs b/examples/table.rs index 846a55c..354bb98 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -1,25 +1,25 @@ -#[allow(dead_code)] -mod util; - -use crate::util::event::{Event, Events}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Layout}, style::{Color, Modifier, Style}, widgets::{Block, Borders, Cell, Row, Table, TableState}, - Terminal, + Frame, Terminal, }; -pub struct StatefulTable<'a> { +struct App<'a> { state: TableState, items: Vec>, } -impl<'a> StatefulTable<'a> { - fn new() -> StatefulTable<'a> { - StatefulTable { +impl<'a> App<'a> { + fn new() -> App<'a> { + App { state: TableState::default(), items: vec![ vec!["Row11", "Row12", "Row13"], @@ -74,72 +74,82 @@ impl<'a> StatefulTable<'a> { } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let events = Events::new(); + // create app and run it + let app = App::new(); + let res = run_app(&mut terminal, app); - let mut table = StatefulTable::new(); + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - // Input - loop { - terminal.draw(|f| { - let rects = Layout::default() - .constraints([Constraint::Percentage(100)].as_ref()) - .margin(5) - .split(f.size()); + if let Err(err) = res { + println!("{:?}", err) + } - let selected_style = Style::default().add_modifier(Modifier::REVERSED); - let normal_style = Style::default().bg(Color::Blue); - let header_cells = ["Header1", "Header2", "Header3"] - .iter() - .map(|h| Cell::from(*h).style(Style::default().fg(Color::Red))); - let header = Row::new(header_cells) - .style(normal_style) - .height(1) - .bottom_margin(1); - let rows = table.items.iter().map(|item| { - let height = item - .iter() - .map(|content| content.chars().filter(|c| *c == '\n').count()) - .max() - .unwrap_or(0) - + 1; - let cells = item.iter().map(|c| Cell::from(*c)); - Row::new(cells).height(height as u16).bottom_margin(1) - }); - let t = Table::new(rows) - .header(header) - .block(Block::default().borders(Borders::ALL).title("Table")) - .highlight_style(selected_style) - .highlight_symbol(">> ") - .widths(&[ - Constraint::Percentage(50), - Constraint::Length(30), - Constraint::Max(10), - ]); - f.render_stateful_widget(t, rects[0], &mut table.state); - })?; + Ok(()) +} - if let Event::Input(key) = events.next()? { - match key { - Key::Char('q') => { - break; - } - Key::Down => { - table.next(); - } - Key::Up => { - table.previous(); - } +fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { + loop { + terminal.draw(|f| ui(f, &mut app))?; + + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Down => app.next(), + KeyCode::Up => app.previous(), _ => {} } - }; + } } +} - Ok(()) +fn ui(f: &mut Frame, app: &mut App) { + let rects = Layout::default() + .constraints([Constraint::Percentage(100)].as_ref()) + .margin(5) + .split(f.size()); + + let selected_style = Style::default().add_modifier(Modifier::REVERSED); + let normal_style = Style::default().bg(Color::Blue); + let header_cells = ["Header1", "Header2", "Header3"] + .iter() + .map(|h| Cell::from(*h).style(Style::default().fg(Color::Red))); + let header = Row::new(header_cells) + .style(normal_style) + .height(1) + .bottom_margin(1); + let rows = app.items.iter().map(|item| { + let height = item + .iter() + .map(|content| content.chars().filter(|c| *c == '\n').count()) + .max() + .unwrap_or(0) + + 1; + let cells = item.iter().map(|c| Cell::from(*c)); + Row::new(cells).height(height as u16).bottom_margin(1) + }); + let t = Table::new(rows) + .header(header) + .block(Block::default().borders(Borders::ALL).title("Table")) + .highlight_style(selected_style) + .highlight_symbol(">> ") + .widths(&[ + Constraint::Percentage(50), + Constraint::Length(30), + Constraint::Max(10), + ]); + f.render_stateful_widget(t, rects[0], &mut app.state); } diff --git a/examples/tabs.rs b/examples/tabs.rs index b7a9e95..2cec057 100644 --- a/examples/tabs.rs +++ b/examples/tabs.rs @@ -1,94 +1,115 @@ #[allow(dead_code)] mod util; -use crate::util::{ - event::{Event, Events}, - TabsState, +use crate::util::TabsState; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans}, widgets::{Block, Borders, Tabs}, - Terminal, + Frame, Terminal, }; struct App<'a> { tabs: TabsState<'a>, } +impl<'a> App<'a> { + fn new() -> App<'a> { + App { + tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2", "Tab3"]), + } + } +} + fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let events = Events::new(); + // create app and run it + let app = App::new(); + let res = run_app(&mut terminal, app); - // App - let mut app = App { - tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2", "Tab3"]), - }; + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - // Main loop - loop { - terminal.draw(|f| { - let size = f.size(); - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(5) - .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) - .split(size); + if let Err(err) = res { + println!("{:?}", err) + } + + Ok(()) +} - let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black)); - f.render_widget(block, size); - let titles = app - .tabs - .titles - .iter() - .map(|t| { - let (first, rest) = t.split_at(1); - Spans::from(vec![ - Span::styled(first, Style::default().fg(Color::Yellow)), - Span::styled(rest, Style::default().fg(Color::Green)), - ]) - }) - .collect(); - let tabs = Tabs::new(titles) - .block(Block::default().borders(Borders::ALL).title("Tabs")) - .select(app.tabs.index) - .style(Style::default().fg(Color::Cyan)) - .highlight_style( - Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Black), - ); - f.render_widget(tabs, chunks[0]); - let inner = match app.tabs.index { - 0 => Block::default().title("Inner 0").borders(Borders::ALL), - 1 => Block::default().title("Inner 1").borders(Borders::ALL), - 2 => Block::default().title("Inner 2").borders(Borders::ALL), - 3 => Block::default().title("Inner 3").borders(Borders::ALL), - _ => unreachable!(), - }; - f.render_widget(inner, chunks[1]); - })?; +fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { + loop { + terminal.draw(|f| ui(f, &mut app))?; - if let Event::Input(input) = events.next()? { - match input { - Key::Char('q') => { - break; - } - Key::Right => app.tabs.next(), - Key::Left => app.tabs.previous(), + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Right => app.tabs.next(), + KeyCode::Left => app.tabs.previous(), _ => {} } } } - Ok(()) +} + +fn ui(f: &mut Frame, app: &App) { + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(5) + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(size); + + let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black)); + f.render_widget(block, size); + let titles = app + .tabs + .titles + .iter() + .map(|t| { + let (first, rest) = t.split_at(1); + Spans::from(vec![ + Span::styled(first, Style::default().fg(Color::Yellow)), + Span::styled(rest, Style::default().fg(Color::Green)), + ]) + }) + .collect(); + let tabs = Tabs::new(titles) + .block(Block::default().borders(Borders::ALL).title("Tabs")) + .select(app.tabs.index) + .style(Style::default().fg(Color::Cyan)) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::Black), + ); + f.render_widget(tabs, chunks[0]); + let inner = match app.tabs.index { + 0 => Block::default().title("Inner 0").borders(Borders::ALL), + 1 => Block::default().title("Inner 1").borders(Borders::ALL), + 2 => Block::default().title("Inner 2").borders(Borders::ALL), + 3 => Block::default().title("Inner 3").borders(Borders::ALL), + _ => unreachable!(), + }; + f.render_widget(inner, chunks[1]); } diff --git a/examples/termion_demo.rs b/examples/termion_demo.rs index 27e5030..9e2c97b 100644 --- a/examples/termion_demo.rs +++ b/examples/termion_demo.rs @@ -2,14 +2,19 @@ mod demo; #[allow(dead_code)] mod util; -use crate::{ - demo::{ui, App}, - util::event::{Config, Event, Events}, -}; +use crate::demo::{ui, App}; use argh::FromArgs; -use std::{error::Error, io, time::Duration}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; -use tui::{backend::TermionBackend, Terminal}; +use std::{error::Error, io, sync::mpsc, thread, time::Duration}; +use termion::{ + event::Key, + input::{MouseTerminal, TermRead}, + raw::IntoRawMode, + screen::AlternateScreen, +}; +use tui::{ + backend::{Backend, TermionBackend}, + Terminal, +}; /// Termion demo #[derive(Debug, FromArgs)] @@ -25,48 +30,72 @@ struct Cli { fn main() -> Result<(), Box> { let cli: Cli = argh::from_env(); - let events = Events::with_config(Config { - tick_rate: Duration::from_millis(cli.tick_rate), - ..Config::default() - }); - + // setup terminal let stdout = io::stdout().into_raw_mode()?; let stdout = MouseTerminal::from(stdout); let stdout = AlternateScreen::from(stdout); let backend = TermionBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let mut app = App::new("Termion demo", cli.enhanced_graphics); + // create app and run it + let tick_rate = Duration::from_millis(cli.tick_rate); + let app = App::new("Termion demo", cli.enhanced_graphics); + run_app(&mut terminal, app, tick_rate)?; + + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, + tick_rate: Duration, +) -> Result<(), Box> { + let events = events(tick_rate); loop { terminal.draw(|f| ui::draw(f, &mut app))?; - match events.next()? { + match events.recv()? { Event::Input(key) => match key { - Key::Char(c) => { - app.on_key(c); - } - Key::Up => { - app.on_up(); - } - Key::Down => { - app.on_down(); - } - Key::Left => { - app.on_left(); - } - Key::Right => { - app.on_right(); - } + Key::Char(c) => app.on_key(c), + Key::Up => app.on_up(), + Key::Down => app.on_down(), + Key::Left => app.on_left(), + Key::Right => app.on_right(), _ => {} }, - Event::Tick => { - app.on_tick(); - } + Event::Tick => app.on_tick(), } if app.should_quit { - break; + return Ok(()); } } +} - Ok(()) +enum Event { + Input(Key), + Tick, +} + +fn events(tick_rate: Duration) -> mpsc::Receiver { + let (tx, rx) = mpsc::channel(); + let keys_tx = tx.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for evt in stdin.keys() { + if let Ok(key) = evt { + if let Err(err) = keys_tx.send(Event::Input(key)) { + eprintln!("{}", err); + return; + } + } + } + }); + thread::spawn(move || loop { + if let Err(err) = tx.send(Event::Tick) { + eprintln!("{}", err); + break; + } + thread::sleep(tick_rate); + }); + rx } diff --git a/examples/user_input.rs b/examples/user_input.rs index dddea7f..786d0d2 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -9,20 +9,19 @@ /// * Pressing Backspace erases a character /// * Pressing Enter pushes the current input in the history of previous /// messages - -#[allow(dead_code)] -mod util; - -use crate::util::event::{Event, Events}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use std::{error::Error, io}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans, Text}, widgets::{Block, Borders, List, ListItem, Paragraph}, - Terminal, + Frame, Terminal, }; use unicode_width::UnicodeWidthStr; @@ -52,122 +51,59 @@ impl Default for App { } fn main() -> Result<(), Box> { - // Terminal initialization - let stdout = io::stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Setup event handlers - let events = Events::new(); - - // Create default app state - let mut app = App::default(); - - loop { - // Draw UI - terminal.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints( - [ - Constraint::Length(1), - Constraint::Length(3), - Constraint::Min(1), - ] - .as_ref(), - ) - .split(f.size()); + // create app and run it + let app = App::default(); + let res = run_app(&mut terminal, app); - let (msg, style) = match app.input_mode { - InputMode::Normal => ( - vec![ - Span::raw("Press "), - Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to exit, "), - Span::styled("e", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to start editing."), - ], - Style::default().add_modifier(Modifier::RAPID_BLINK), - ), - InputMode::Editing => ( - vec![ - Span::raw("Press "), - Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to stop editing, "), - Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to record the message"), - ], - Style::default(), - ), - }; - let mut text = Text::from(Spans::from(msg)); - text.patch_style(style); - let help_message = Paragraph::new(text); - f.render_widget(help_message, chunks[0]); + // restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - let input = Paragraph::new(app.input.as_ref()) - .style(match app.input_mode { - InputMode::Normal => Style::default(), - InputMode::Editing => Style::default().fg(Color::Yellow), - }) - .block(Block::default().borders(Borders::ALL).title("Input")); - f.render_widget(input, chunks[1]); - match app.input_mode { - InputMode::Normal => - // Hide the cursor. `Frame` does this by default, so we don't need to do anything here - {} + if let Err(err) = res { + println!("{:?}", err) + } - InputMode::Editing => { - // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering - f.set_cursor( - // Put cursor past the end of the input text - chunks[1].x + app.input.width() as u16 + 1, - // Move one line down, from the border to the input line - chunks[1].y + 1, - ) - } - } + Ok(()) +} - let messages: Vec = app - .messages - .iter() - .enumerate() - .map(|(i, m)| { - let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))]; - ListItem::new(content) - }) - .collect(); - let messages = - List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages")); - f.render_widget(messages, chunks[2]); - })?; +fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<()> { + loop { + terminal.draw(|f| ui(f, &mut app))?; - // Handle input - if let Event::Input(input) = events.next()? { + if let Event::Key(key) = event::read()? { match app.input_mode { - InputMode::Normal => match input { - Key::Char('e') => { + InputMode::Normal => match key.code { + KeyCode::Char('e') => { app.input_mode = InputMode::Editing; } - Key::Char('q') => { - break; + KeyCode::Char('q') => { + return Ok(()); } _ => {} }, - InputMode::Editing => match input { - Key::Char('\n') => { + InputMode::Editing => match key.code { + KeyCode::Enter => { app.messages.push(app.input.drain(..).collect()); } - Key::Char(c) => { + KeyCode::Char(c) => { app.input.push(c); } - Key::Backspace => { + KeyCode::Backspace => { app.input.pop(); } - Key::Esc => { + KeyCode::Esc => { app.input_mode = InputMode::Normal; } _ => {} @@ -175,5 +111,82 @@ fn main() -> Result<(), Box> { } } } - Ok(()) +} + +fn ui(f: &mut Frame, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(f.size()); + + let (msg, style) = match app.input_mode { + InputMode::Normal => ( + vec![ + Span::raw("Press "), + Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit, "), + Span::styled("e", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to start editing."), + ], + Style::default().add_modifier(Modifier::RAPID_BLINK), + ), + InputMode::Editing => ( + vec![ + Span::raw("Press "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to stop editing, "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to record the message"), + ], + Style::default(), + ), + }; + let mut text = Text::from(Spans::from(msg)); + text.patch_style(style); + let help_message = Paragraph::new(text); + f.render_widget(help_message, chunks[0]); + + let input = Paragraph::new(app.input.as_ref()) + .style(match app.input_mode { + InputMode::Normal => Style::default(), + InputMode::Editing => Style::default().fg(Color::Yellow), + }) + .block(Block::default().borders(Borders::ALL).title("Input")); + f.render_widget(input, chunks[1]); + match app.input_mode { + InputMode::Normal => + // Hide the cursor. `Frame` does this by default, so we don't need to do anything here + {} + + InputMode::Editing => { + // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering + f.set_cursor( + // Put cursor past the end of the input text + chunks[1].x + app.input.width() as u16 + 1, + // Move one line down, from the border to the input line + chunks[1].y + 1, + ) + } + } + + let messages: Vec = app + .messages + .iter() + .enumerate() + .map(|(i, m)| { + let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))]; + ListItem::new(content) + }) + .collect(); + let messages = + List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages")); + f.render_widget(messages, chunks[2]); } diff --git a/examples/util/event.rs b/examples/util/event.rs deleted file mode 100644 index 33ee9ec..0000000 --- a/examples/util/event.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::io; -use std::sync::mpsc; -use std::thread; -use std::time::Duration; - -use termion::event::Key; -use termion::input::TermRead; - -pub enum Event { - Input(I), - Tick, -} - -/// A small event handler that wrap termion input and tick events. Each event -/// type is handled in its own thread and returned to a common `Receiver` -pub struct Events { - rx: mpsc::Receiver>, - input_handle: thread::JoinHandle<()>, - tick_handle: thread::JoinHandle<()>, -} - -#[derive(Debug, Clone, Copy)] -pub struct Config { - pub tick_rate: Duration, -} - -impl Default for Config { - fn default() -> Config { - Config { - tick_rate: Duration::from_millis(250), - } - } -} - -impl Events { - pub fn new() -> Events { - Events::with_config(Config::default()) - } - - pub fn with_config(config: Config) -> Events { - let (tx, rx) = mpsc::channel(); - let input_handle = { - let tx = tx.clone(); - thread::spawn(move || { - let stdin = io::stdin(); - for evt in stdin.keys() { - if let Ok(key) = evt { - if let Err(err) = tx.send(Event::Input(key)) { - eprintln!("{}", err); - return; - } - } - } - }) - }; - let tick_handle = { - thread::spawn(move || loop { - if let Err(err) = tx.send(Event::Tick) { - eprintln!("{}", err); - break; - } - thread::sleep(config.tick_rate); - }) - }; - Events { - rx, - input_handle, - tick_handle, - } - } - - pub fn next(&self) -> Result, mpsc::RecvError> { - self.rx.recv() - } -} diff --git a/examples/util/mod.rs b/examples/util/mod.rs index c926b3a..b7573cc 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "termion")] -pub mod event; - use rand::distributions::{Distribution, Uniform}; use rand::rngs::ThreadRng; use tui::widgets::ListState; diff --git a/src/backend/curses.rs b/src/backend/curses.rs deleted file mode 100644 index 8a2d4d2..0000000 --- a/src/backend/curses.rs +++ /dev/null @@ -1,280 +0,0 @@ -use std::io; - -use crate::backend::Backend; -use crate::buffer::Cell; -use crate::layout::Rect; -use crate::style::{Color, Modifier}; -use crate::symbols::{bar, block}; -#[cfg(unix)] -use crate::symbols::{line, DOT}; -#[cfg(unix)] -use pancurses::{chtype, ToChtype}; -use unicode_segmentation::UnicodeSegmentation; - -pub struct CursesBackend { - curses: easycurses::EasyCurses, -} - -impl CursesBackend { - pub fn new() -> Option { - let curses = easycurses::EasyCurses::initialize_system()?; - Some(CursesBackend { curses }) - } - - pub fn with_curses(curses: easycurses::EasyCurses) -> CursesBackend { - CursesBackend { curses } - } - - pub fn get_curses(&self) -> &easycurses::EasyCurses { - &self.curses - } - - pub fn get_curses_mut(&mut self) -> &mut easycurses::EasyCurses { - &mut self.curses - } -} - -impl Backend for CursesBackend { - fn draw<'a, I>(&mut self, content: I) -> io::Result<()> - where - I: Iterator, - { - let mut last_col = 0; - let mut last_row = 0; - let mut fg = Color::Reset; - let mut bg = Color::Reset; - let mut modifier = Modifier::empty(); - let mut curses_style = CursesStyle { - fg: easycurses::Color::White, - bg: easycurses::Color::Black, - }; - let mut update_color = false; - for (col, row, cell) in content { - if row != last_row || col != last_col + 1 { - self.curses.move_rc(i32::from(row), i32::from(col)); - } - last_col = col; - last_row = row; - if cell.modifier != modifier { - apply_modifier_diff(&mut self.curses.win, modifier, cell.modifier); - modifier = cell.modifier; - }; - if cell.fg != fg { - update_color = true; - if let Some(ccolor) = cell.fg.into() { - fg = cell.fg; - curses_style.fg = ccolor; - } else { - fg = Color::White; - curses_style.fg = easycurses::Color::White; - } - }; - if cell.bg != bg { - update_color = true; - if let Some(ccolor) = cell.bg.into() { - bg = cell.bg; - curses_style.bg = ccolor; - } else { - 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) -> io::Result<()> { - self.curses - .set_cursor_visibility(easycurses::CursorVisibility::Invisible); - Ok(()) - } - fn show_cursor(&mut self) -> io::Result<()> { - self.curses - .set_cursor_visibility(easycurses::CursorVisibility::Visible); - Ok(()) - } - fn get_cursor(&mut self) -> io::Result<(u16, u16)> { - let (y, x) = self.curses.get_cursor_rc(); - Ok((x as u16, y as u16)) - } - fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { - self.curses.move_rc(i32::from(y), i32::from(x)); - Ok(()) - } - fn clear(&mut self) -> io::Result<()> { - 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) -> io::Result<()> { - self.curses.refresh(); - Ok(()) - } -} - -struct CursesStyle { - fg: easycurses::Color, - bg: easycurses::Color, -} - -#[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 = match grapheme { - 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_QUARTERS => pancurses::ACS_BLOCK(), - block::FIVE_EIGHTHS => pancurses::ACS_BLOCK(), - block::HALF => pancurses::ACS_BLOCK(), - block::THREE_EIGHTHS => ' ' as chtype, - block::ONE_QUARTER => ' ' as chtype, - block::ONE_EIGHTH => ' ' as chtype, - bar::SEVEN_EIGHTHS => pancurses::ACS_BLOCK(), - bar::THREE_QUARTERS => pancurses::ACS_BLOCK(), - bar::FIVE_EIGHTHS => pancurses::ACS_BLOCK(), - bar::HALF => pancurses::ACS_BLOCK(), - bar::THREE_EIGHTHS => pancurses::ACS_S9(), - bar::ONE_QUARTER => pancurses::ACS_S9(), - bar::ONE_EIGHTH => pancurses::ACS_S9(), - 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() - } - } - }; - curses.win.addch(ch); - } -} - -#[cfg(windows)] -fn draw(curses: &mut easycurses::EasyCurses, symbol: &str) { - for grapheme in symbol.graphemes(true) { - let ch = match grapheme { - block::SEVEN_EIGHTHS => block::FULL, - block::THREE_QUARTERS => block::FULL, - block::FIVE_EIGHTHS => block::HALF, - block::THREE_EIGHTHS => block::HALF, - block::ONE_QUARTER => block::HALF, - block::ONE_EIGHTH => " ", - bar::SEVEN_EIGHTHS => bar::FULL, - bar::THREE_QUARTERS => bar::FULL, - bar::FIVE_EIGHTHS => bar::HALF, - bar::THREE_EIGHTHS => bar::HALF, - bar::ONE_QUARTER => bar::HALF, - bar::ONE_EIGHTH => " ", - ch => ch, - }; - // curses.win.addch(ch); - curses.print(ch); - } -} - -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::Indexed(_) => None, - Color::Rgb(_, _, _) => None, - } - } -} - -fn apply_modifier_diff(win: &mut pancurses::Window, from: Modifier, to: Modifier) { - remove_modifier(win, from - to); - add_modifier(win, to - from); -} - -fn remove_modifier(win: &mut pancurses::Window, remove: Modifier) { - if remove.contains(Modifier::BOLD) { - win.attroff(pancurses::Attribute::Bold); - } - if remove.contains(Modifier::DIM) { - win.attroff(pancurses::Attribute::Dim); - } - if remove.contains(Modifier::ITALIC) { - win.attroff(pancurses::Attribute::Italic); - } - if remove.contains(Modifier::UNDERLINED) { - win.attroff(pancurses::Attribute::Underline); - } - if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) { - win.attroff(pancurses::Attribute::Blink); - } - if remove.contains(Modifier::REVERSED) { - win.attroff(pancurses::Attribute::Reverse); - } - if remove.contains(Modifier::HIDDEN) { - win.attroff(pancurses::Attribute::Invisible); - } - if remove.contains(Modifier::CROSSED_OUT) { - win.attroff(pancurses::Attribute::Strikeout); - } -} - -fn add_modifier(win: &mut pancurses::Window, add: Modifier) { - if add.contains(Modifier::BOLD) { - win.attron(pancurses::Attribute::Bold); - } - if add.contains(Modifier::DIM) { - win.attron(pancurses::Attribute::Dim); - } - if add.contains(Modifier::ITALIC) { - win.attron(pancurses::Attribute::Italic); - } - if add.contains(Modifier::UNDERLINED) { - win.attron(pancurses::Attribute::Underline); - } - if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) { - win.attron(pancurses::Attribute::Blink); - } - if add.contains(Modifier::REVERSED) { - win.attron(pancurses::Attribute::Reverse); - } - if add.contains(Modifier::HIDDEN) { - win.attron(pancurses::Attribute::Invisible); - } - if add.contains(Modifier::CROSSED_OUT) { - win.attron(pancurses::Attribute::Strikeout); - } -} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index e21be38..42b8001 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -3,11 +3,6 @@ use std::io; use crate::buffer::Cell; use crate::layout::Rect; -#[cfg(feature = "rustbox")] -mod rustbox; -#[cfg(feature = "rustbox")] -pub use self::rustbox::RustboxBackend; - #[cfg(feature = "termion")] mod termion; #[cfg(feature = "termion")] @@ -18,11 +13,6 @@ 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; diff --git a/src/backend/rustbox.rs b/src/backend/rustbox.rs deleted file mode 100644 index 3854ed1..0000000 --- a/src/backend/rustbox.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::{ - backend::Backend, - buffer::Cell, - layout::Rect, - style::{Color, Modifier}, -}; -use std::io; - -pub struct RustboxBackend { - rustbox: rustbox::RustBox, -} - -impl RustboxBackend { - pub fn new() -> Result { - let rustbox = rustbox::RustBox::init(Default::default())?; - Ok(RustboxBackend { rustbox }) - } - - pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend { - RustboxBackend { rustbox: instance } - } - - pub fn rustbox(&self) -> &rustbox::RustBox { - &self.rustbox - } -} - -impl Backend for RustboxBackend { - fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error> - where - I: Iterator, - { - for (x, y, cell) in content { - self.rustbox.print( - x as usize, - y as usize, - cell.modifier.into(), - cell.fg.into(), - cell.bg.into(), - &cell.symbol, - ); - } - Ok(()) - } - fn hide_cursor(&mut self) -> Result<(), io::Error> { - Ok(()) - } - fn show_cursor(&mut self) -> Result<(), io::Error> { - Ok(()) - } - fn get_cursor(&mut self) -> io::Result<(u16, u16)> { - Err(io::Error::from(io::ErrorKind::Other)) - } - fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> { - self.rustbox.set_cursor(x as isize, y as isize); - Ok(()) - } - fn clear(&mut self) -> Result<(), io::Error> { - self.rustbox.clear(); - Ok(()) - } - fn size(&self) -> Result { - let term_width = self.rustbox.width(); - let term_height = self.rustbox.height(); - let max = u16::max_value(); - Ok(Rect::new( - 0, - 0, - if term_width > usize::from(max) { - max - } else { - term_width as u16 - }, - if term_height > usize::from(max) { - max - } else { - term_height as u16 - }, - )) - } - fn flush(&mut self) -> Result<(), io::Error> { - self.rustbox.present(); - Ok(()) - } -} - -fn rgb_to_byte(r: u8, g: u8, b: u8) -> u16 { - u16::from((r & 0xC0) + ((g & 0xE0) >> 2) + ((b & 0xE0) >> 5)) -} - -impl From for rustbox::Color { - fn from(color: Color) -> rustbox::Color { - match color { - Color::Reset => rustbox::Color::Default, - Color::Black | Color::Gray | Color::DarkGray => rustbox::Color::Black, - Color::Red | Color::LightRed => rustbox::Color::Red, - Color::Green | Color::LightGreen => rustbox::Color::Green, - Color::Yellow | Color::LightYellow => rustbox::Color::Yellow, - Color::Magenta | Color::LightMagenta => rustbox::Color::Magenta, - Color::Cyan | Color::LightCyan => rustbox::Color::Cyan, - Color::White => rustbox::Color::White, - Color::Blue | Color::LightBlue => rustbox::Color::Blue, - Color::Indexed(i) => rustbox::Color::Byte(u16::from(i)), - Color::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)), - } - } -} - -impl From for rustbox::Style { - fn from(modifier: Modifier) -> rustbox::Style { - let mut result = rustbox::Style::empty(); - if modifier.contains(Modifier::BOLD) { - result.insert(rustbox::RB_BOLD); - } - if modifier.contains(Modifier::UNDERLINED) { - result.insert(rustbox::RB_UNDERLINE); - } - if modifier.contains(Modifier::REVERSED) { - result.insert(rustbox::RB_REVERSE); - } - result - } -} diff --git a/src/lib.rs b/src/lib.rs index 0c228ce..1439350 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,17 +10,17 @@ //! ```toml //! [dependencies] //! tui = "0.16" -//! termion = "1.5" +//! crossterm = "0.22" //! ``` //! -//! The crate is using the `termion` backend by default but if for example you want your -//! application to work on Windows, you might want to use the `crossterm` backend instead. This can -//! be done by changing your dependencies specification to the following: +//! The crate is using the `crossterm` backend by default that works on most platforms. But if for +//! example you want to use the `termion` backend instead. This can be done by changing your +//! dependencies specification to the following: //! //! ```toml //! [dependencies] -//! crossterm = "0.20" -//! tui = { version = "0.16", default-features = false, features = ['crossterm'] } +//! termion = "1.5" +//! tui = { version = "0.16", default-features = false, features = ['termion'] } //! ``` //! //! The same logic applies for all other available backends. @@ -33,29 +33,27 @@ //! //! ```rust,no_run //! use std::io; -//! use tui::Terminal; -//! use tui::backend::TermionBackend; -//! use termion::raw::IntoRawMode; +//! use tui::{backend::CrosstermBackend, Terminal}; //! //! fn main() -> Result<(), io::Error> { -//! let stdout = io::stdout().into_raw_mode()?; -//! let backend = TermionBackend::new(stdout); +//! let stdout = io::stdout(); +//! let backend = CrosstermBackend::new(stdout); //! let mut terminal = Terminal::new(backend)?; //! Ok(()) //! } //! ``` //! -//! If you had previously chosen `crossterm` as a backend, the terminal can be created in a similar +//! If you had previously chosen `termion` as a backend, the terminal can be created in a similar //! way: //! //! ```rust,ignore //! use std::io; -//! use tui::Terminal; -//! use tui::backend::CrosstermBackend; +//! use tui::{backend::TermionBackend, Terminal}; +//! use termion::raw::IntoRawMode; //! //! fn main() -> Result<(), io::Error> { -//! let stdout = io::stdout(); -//! let backend = CrosstermBackend::new(stdout); +//! let stdout = io::stdout().into_raw_mode()?; +//! let backend = TermionBackend::new(stdout); //! let mut terminal = Terminal::new(backend)?; //! Ok(()) //! } @@ -77,17 +75,27 @@ //! The following example renders a block of the size of the terminal: //! //! ```rust,no_run -//! use std::io; -//! use termion::raw::IntoRawMode; -//! use tui::Terminal; -//! use tui::backend::TermionBackend; -//! use tui::widgets::{Widget, Block, Borders}; -//! use tui::layout::{Layout, Constraint, Direction}; +//! use std::{io, thread, time::Duration}; +//! use tui::{ +//! backend::CrosstermBackend, +//! widgets::{Widget, Block, Borders}, +//! layout::{Layout, Constraint, Direction}, +//! Terminal +//! }; +//! use crossterm::{ +//! event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, +//! execute, +//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +//! }; //! //! fn main() -> Result<(), io::Error> { -//! let stdout = io::stdout().into_raw_mode()?; -//! let backend = TermionBackend::new(stdout); +//! // setup terminal +//! enable_raw_mode()?; +//! let mut stdout = io::stdout(); +//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; +//! let backend = CrosstermBackend::new(stdout); //! let mut terminal = Terminal::new(backend)?; +//! //! terminal.draw(|f| { //! let size = f.size(); //! let block = Block::default() @@ -95,6 +103,18 @@ //! .borders(Borders::ALL); //! f.render_widget(block, size); //! })?; +//! +//! thread::sleep(Duration::from_millis(5000)); +//! +//! // restore terminal +//! disable_raw_mode()?; +//! execute!( +//! terminal.backend_mut(), +//! LeaveAlternateScreen, +//! DisableMouseCapture +//! )?; +//! terminal.show_cursor()?; +//! //! Ok(()) //! } //! ``` @@ -106,39 +126,32 @@ //! full customization. And `Layout` is no exception: //! //! ```rust,no_run -//! use std::io; -//! use termion::raw::IntoRawMode; -//! use tui::Terminal; -//! use tui::backend::TermionBackend; -//! use tui::widgets::{Widget, Block, Borders}; -//! use tui::layout::{Layout, Constraint, Direction}; -//! -//! fn main() -> Result<(), io::Error> { -//! let stdout = io::stdout().into_raw_mode()?; -//! let backend = TermionBackend::new(stdout); -//! let mut terminal = Terminal::new(backend)?; -//! terminal.draw(|f| { -//! let chunks = Layout::default() -//! .direction(Direction::Vertical) -//! .margin(1) -//! .constraints( -//! [ -//! Constraint::Percentage(10), -//! Constraint::Percentage(80), -//! Constraint::Percentage(10) -//! ].as_ref() -//! ) -//! .split(f.size()); -//! let block = Block::default() -//! .title("Block") -//! .borders(Borders::ALL); -//! f.render_widget(block, chunks[0]); -//! let block = Block::default() -//! .title("Block 2") -//! .borders(Borders::ALL); -//! f.render_widget(block, chunks[1]); -//! })?; -//! Ok(()) +//! use tui::{ +//! backend::Backend, +//! layout::{Constraint, Direction, Layout}, +//! widgets::{Block, Borders}, +//! Frame, +//! }; +//! fn ui(f: &mut Frame) { +//! let chunks = Layout::default() +//! .direction(Direction::Vertical) +//! .margin(1) +//! .constraints( +//! [ +//! Constraint::Percentage(10), +//! Constraint::Percentage(80), +//! Constraint::Percentage(10) +//! ].as_ref() +//! ) +//! .split(f.size()); +//! let block = Block::default() +//! .title("Block") +//! .borders(Borders::ALL); +//! f.render_widget(block, chunks[0]); +//! let block = Block::default() +//! .title("Block 2") +//! .borders(Borders::ALL); +//! f.render_widget(block, chunks[1]); //! } //! ``` //! diff --git a/src/terminal.rs b/src/terminal.rs index 48d43be..3a1d37f 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -82,14 +82,12 @@ where /// /// # Examples /// - /// ```rust,no_run - /// # use std::io; + /// ```rust /// # use tui::Terminal; - /// # use tui::backend::TermionBackend; + /// # use tui::backend::TestBackend; /// # use tui::layout::Rect; /// # use tui::widgets::Block; - /// # let stdout = io::stdout(); - /// # let backend = TermionBackend::new(stdout); + /// # let backend = TestBackend::new(5, 5); /// # let mut terminal = Terminal::new(backend).unwrap(); /// let block = Block::default(); /// let area = Rect::new(0, 0, 5, 5); @@ -110,14 +108,12 @@ where /// /// # Examples /// - /// ```rust,no_run - /// # use std::io; + /// ```rust /// # use tui::Terminal; - /// # use tui::backend::TermionBackend; + /// # use tui::backend::TestBackend; /// # use tui::layout::Rect; /// # use tui::widgets::{List, ListItem, ListState}; - /// # let stdout = io::stdout(); - /// # let backend = TermionBackend::new(stdout); + /// # let backend = TestBackend::new(5, 5); /// # let mut terminal = Terminal::new(backend).unwrap(); /// let mut state = ListState::default(); /// state.select(Some(1)); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 4b8476d..8b005ec 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -87,7 +87,7 @@ pub trait Widget { /// ```rust,no_run /// # use std::io; /// # use tui::Terminal; -/// # use tui::backend::{Backend, TermionBackend}; +/// # use tui::backend::{Backend, TestBackend}; /// # use tui::widgets::{Widget, List, ListItem, ListState}; /// /// // Let's say we have some events to display. @@ -154,9 +154,8 @@ pub trait Widget { /// } /// } /// -/// let stdout = io::stdout(); -/// let backend = TermionBackend::new(stdout); -/// let mut terminal = Terminal::new(backend).unwrap(); +/// # let backend = TestBackend::new(5, 5); +/// # let mut terminal = Terminal::new(backend).unwrap(); /// /// let mut events = Events::new(vec![ /// String::from("Item 1"),