1use std::io::{IsTerminal, Write};
2use std::{io, thread};
3
4use crate::element::Size;
5use crate::{Constraint, Element, Line, Paint};
6
7use crossbeam_channel as chan;
8use radicle_signals as signals;
9use termion::event::{Event, Key, MouseButton, MouseEvent};
10use termion::{input::TermRead, raw::IntoRawMode, screen::IntoAlternateScreen};
11
12const MOUSE_SCROLL_LINES: usize = 3;
14
15#[derive(Debug, thiserror::Error)]
17pub enum Error {
18 #[error(transparent)]
19 Io(#[from] io::Error),
20 #[error(transparent)]
21 Channel(#[from] chan::RecvError),
22}
23
24pub fn page<E: Element + Send + 'static>(element: E) -> Result<(), Error> {
34 let (events_tx, events_rx) = chan::unbounded();
35 let (signals_tx, signals_rx) = chan::unbounded();
36
37 signals::install(signals_tx)?;
38
39 thread::spawn(move || {
40 for e in io::stdin().events() {
41 events_tx.send(e).ok();
42 }
43 });
44 let result = thread::spawn(move || main(element, signals_rx, events_rx))
45 .join()
46 .unwrap();
47
48 signals::uninstall()?;
49
50 result
51}
52
53fn main<E: Element>(
54 element: E,
55 signals_rx: chan::Receiver<signals::Signal>,
56 events_rx: chan::Receiver<Result<Event, io::Error>>,
57) -> Result<(), Error> {
58 let stdout = io::stdout();
59 if !stdout.is_terminal() {
60 element.print();
61 return Ok(());
62 }
63 let raw = stdout.into_raw_mode()?;
64 let mut stdout = termion::input::MouseTerminal::from(raw).into_alternate_screen()?;
65 let (mut width, mut height) = termion::terminal_size()?;
66 let mut lines = element.render(Constraint::max(Size::new(width as usize, height as usize)));
67 let mut line = 0;
68
69 render(&mut stdout, lines.as_slice(), line, (width, height))?;
70
71 loop {
72 chan::select! {
73 recv(signals_rx) -> signal => {
74 match signal? {
75 signals::Signal::WindowChanged => {
76 let (w, h) = termion::terminal_size()?;
77
78 lines = element.render(Constraint::max(Size::new(w as usize, h as usize)));
79 width = w;
80 height = h;
81 }
82 signals::Signal::Interrupt | signals::Signal::Terminate => {
83 break;
84 }
85 _ => continue,
86 }
87 }
88 recv(events_rx) -> event => {
89 let event = event??;
90 let page = height as usize - 1; let end = if page > lines.len() { 0 } else { lines.len() - page };
92 let prev = line;
93
94 match event {
95 Event::Key(key) => match key {
96 Key::Up | Key::Char('k') => {
97 line = line.saturating_sub(1);
98 }
99 Key::Home => {
100 line = 0;
101 }
102 Key::End | Key::Char('G') => {
103 line = end;
104 }
105 Key::PageUp | Key::Char('b') => {
106 line = line.saturating_sub(page);
107 }
108 Key::PageDown | Key::Char(' ') => {
109 line = (line + page).min(end);
110 }
111 Key::Down | Key::Char('j') => {
112 if line < end {
113 line += 1;
114 }
115 }
116 Key::Char('q') => break,
117
118 _ => continue,
119 }
120 Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, _, _)) => {
121 if line < end {
122 line += MOUSE_SCROLL_LINES;
123 }
124 }
125 Event::Mouse(MouseEvent::Press(MouseButton::WheelUp, _, _)) => {
126 line = line.saturating_sub(MOUSE_SCROLL_LINES);
127 }
128 _ => continue,
129 }
130 if line == prev {
132 continue;
133 }
134 }
135 }
136 render(&mut stdout, &lines, line, (width, height))?;
137 }
138 Ok(())
139}
140
141fn render<W: Write>(
142 out: &mut W,
143 lines: &[Line],
144 start_line: usize,
145 (width, height): (u16, u16),
146) -> io::Result<()> {
147 write!(
148 out,
149 "{}{}",
150 termion::clear::All,
151 termion::cursor::Goto(1, 1)
152 )?;
153
154 let content_length = lines.len();
155 let window_size = height as usize - 1;
156 let end_line = if start_line + window_size > content_length {
157 content_length
158 } else {
159 start_line + window_size
160 };
161 for (ix, line) in lines[start_line..end_line].iter().enumerate() {
163 write!(out, "{}{}", termion::cursor::Goto(1, ix as u16 + 1), line)?;
164 }
165 write!(
167 out,
168 "{}{}",
169 termion::cursor::Goto(width - 3, height),
170 Paint::new(format!(
171 "{:.0}%",
172 end_line as f64 / lines.len() as f64 * 100.
173 ))
174 .dim()
175 )?;
176 write!(
178 out,
179 "{}{}",
180 termion::cursor::Goto(1, height),
181 Paint::new(":").dim()
182 )?;
183 out.flush()?;
184
185 Ok(())
186}