1use std::{
17 io::{self},
18 time::{Duration, Instant},
19};
20
21use color_eyre::Result;
22use ratatui::{
23 buffer::Buffer,
24 crossterm::event::{self, Event, KeyCode, KeyEventKind},
25 layout::{Constraint, Layout, Rect},
26 style::{Color, Stylize},
27 text::{Line, Masked, Span},
28 widgets::{Block, Paragraph, Widget, Wrap},
29 DefaultTerminal,
30};
31
32fn main() -> Result<()> {
33 color_eyre::install()?;
34 let terminal = ratatui::init();
35 let app_result = App::new().run(terminal);
36 ratatui::restore();
37 app_result
38}
39
40#[derive(Debug)]
41struct App {
42 should_exit: bool,
43 scroll: u16,
44 last_tick: Instant,
45}
46
47impl App {
48 const TICK_RATE: Duration = Duration::from_millis(250);
50
51 fn new() -> Self {
53 Self {
54 should_exit: false,
55 scroll: 0,
56 last_tick: Instant::now(),
57 }
58 }
59
60 fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
62 while !self.should_exit {
63 terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
64 self.handle_events()?;
65 if self.last_tick.elapsed() >= Self::TICK_RATE {
66 self.on_tick();
67 self.last_tick = Instant::now();
68 }
69 }
70 Ok(())
71 }
72
73 fn handle_events(&mut self) -> io::Result<()> {
75 let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
76 while event::poll(timeout)? {
77 if let Event::Key(key) = event::read()? {
78 if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
79 self.should_exit = true;
80 }
81 }
82 }
83 Ok(())
84 }
85
86 fn on_tick(&mut self) {
88 self.scroll = (self.scroll + 1) % 10;
89 }
90}
91
92impl Widget for &App {
93 fn render(self, area: Rect, buf: &mut Buffer) {
94 let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
95 Paragraph::new(create_lines(area))
96 .block(title_block("Default alignment (Left), no wrap"))
97 .gray()
98 .render(areas[0], buf);
99 Paragraph::new(create_lines(area))
100 .block(title_block("Default alignment (Left), with wrap"))
101 .gray()
102 .wrap(Wrap { trim: true })
103 .render(areas[1], buf);
104 Paragraph::new(create_lines(area))
105 .block(title_block("Right alignment, with wrap"))
106 .gray()
107 .right_aligned()
108 .wrap(Wrap { trim: true })
109 .render(areas[2], buf);
110 Paragraph::new(create_lines(area))
111 .block(title_block("Center alignment, with wrap, with scroll"))
112 .gray()
113 .centered()
114 .wrap(Wrap { trim: true })
115 .scroll((self.scroll, 0))
116 .render(areas[3], buf);
117 }
118}
119
120fn title_block(title: &str) -> Block {
122 Block::bordered()
123 .gray()
124 .title(title.bold().into_centered_line())
125}
126
127fn create_lines(area: Rect) -> Vec<Line<'static>> {
129 let short_line = "A long line to demonstrate line wrapping. ";
130 let long_line = short_line.repeat(usize::from(area.width) / short_line.len() + 4);
131 let mut styled_spans = vec![];
132 for span in [
133 "Styled".blue(),
134 "Spans".red().on_white(),
135 "Bold".bold(),
136 "Italic".italic(),
137 "Underlined".underlined(),
138 "Strikethrough".crossed_out(),
139 ] {
140 styled_spans.push(span);
141 styled_spans.push(" ".into());
142 }
143 vec![
144 Line::raw("Unstyled Line"),
145 Line::raw("Styled Line").black().on_red().bold().italic(),
146 Line::from(styled_spans),
147 Line::from(long_line.green().italic()),
148 Line::from_iter([
149 "Masked text: ".into(),
150 Span::styled(Masked::new("my secret password", '*'), Color::Red),
151 ]),
152 ]
153}