1pub use style::Style;
2
3mod style;
4
5use std::panic;
6
7use crate::{
8 core::{
9 App,
10 event::{Event, Key, KeyEvent},
11 input,
12 screen::FlushError,
13 },
14 text,
15};
16use thedes_async_util::progress::{self, Progress};
17use thedes_tui_core::{geometry::Coord, mutation::Set, screen};
18use thiserror::Error;
19use tokio::task;
20
21pub fn default_key_bindings() -> KeyBindingMap {
22 let map = KeyBindingMap::new()
23 .with(Key::Esc, Command::Cancel)
24 .with(Key::Char('q'), Command::Cancel)
25 .with(Key::Char('Q'), Command::Cancel);
26 map
27}
28
29#[derive(Debug, Error)]
30pub enum Error {
31 #[error("Generator task was cancelled")]
32 Join(
33 #[source]
34 #[from]
35 task::JoinError,
36 ),
37 #[error("Failed to flush commands to screen")]
38 Flush(
39 #[from]
40 #[source]
41 FlushError,
42 ),
43 #[error("Failed to render text")]
44 Error(
45 #[from]
46 #[source]
47 text::Error,
48 ),
49 #[error("TUI cancelled")]
50 Cancelled,
51 #[error("Input driver was cancelled")]
52 InputCancelled(
53 #[source]
54 #[from]
55 input::ReadError,
56 ),
57}
58
59#[derive(Debug, Clone)]
60pub enum Command {
61 Cancel,
62}
63
64pub type KeyBindingMap = crate::key_bindings::KeyBindingMap<Command>;
65
66#[derive(Debug, Clone)]
67pub struct Component {
68 key_bindings: KeyBindingMap,
69 title: String,
70 style: Style,
71}
72
73impl Component {
74 pub fn new(title: impl AsRef<str>) -> Self {
75 Self {
76 title: title.as_ref().to_owned(),
77 style: Style::default(),
78 key_bindings: default_key_bindings(),
79 }
80 }
81
82 pub async fn run<A>(
83 &self,
84 app: &mut App,
85 monitor: progress::Monitor,
86 task: A,
87 ) -> Result<Option<A::Output>, Error>
88 where
89 A: Future + Send + 'static,
90 A::Output: Send + 'static,
91 {
92 let cancel_token = app.cancel_token.child_token();
93
94 let task = task::spawn({
95 let cancel_token = cancel_token.clone();
96 async move {
97 tokio::select! {
98 output = task => Some(output),
99 _ = cancel_token.cancelled() => None,
100 }
101 }
102 });
103
104 let should_continue = loop {
105 if !self.handle_input(app)? {
106 cancel_token.cancel();
107 break false;
108 }
109 let progress = monitor.read();
110 if progress.current() >= monitor.goal() {
111 break true;
112 }
113 self.render(app, &progress, monitor.goal())?;
114 app.canvas.flush()?;
115 tokio::select! {
116 _ = app.tick_session.tick() => (),
117 _ = app.cancel_token.cancelled() => Err(Error::Cancelled)?,
118 }
119 };
120
121 let item = match task.await {
122 Ok(item) => item,
123 Err(join_error) => match join_error.try_into_panic() {
124 Ok(payload) => panic::resume_unwind(payload),
125 Err(join_error) => Err(join_error)?,
126 },
127 };
128
129 Ok(item.filter(|_| should_continue))
130 }
131
132 fn handle_input(&self, app: &mut App) -> Result<bool, Error> {
133 let events: Vec<_> = app.events.read_until_now()?.collect();
134
135 for event in events {
136 match event {
137 Event::Key(key) => {
138 if !self.handle_key(key)? {
139 return Ok(false);
140 }
141 },
142 Event::Paste(_) => (),
143 }
144 }
145
146 Ok(true)
147 }
148
149 fn handle_key(&self, key: KeyEvent) -> Result<bool, Error> {
150 if let Some(command) = self.key_bindings.command_for(key) {
151 match command {
152 Command::Cancel => return Ok(false),
153 }
154 }
155
156 Ok(true)
157 }
158
159 fn render(
160 &self,
161 app: &mut App,
162 progress: &Progress,
163 goal: usize,
164 ) -> Result<(), Error> {
165 app.canvas.queue([screen::Command::new_clear_screen(
166 self.style.background(),
167 )]);
168 self.render_title(app)?;
169 self.render_bar(app, progress, goal)?;
170 self.render_perc(app, progress, goal)?;
171 self.render_absolute(app, progress, goal)?;
172 self.render_status(app, progress)?;
173 Ok(())
174 }
175
176 fn render_title(&self, app: &mut App) -> Result<(), Error> {
177 let style = text::Style::default()
178 .with_align(1, 2)
179 .with_colors(Set(self.style.title_colors()))
180 .with_top_margin(self.style.title_y());
181 text::styled(app, &self.title, &style)?;
182 Ok(())
183 }
184
185 fn render_bar(
186 &self,
187 app: &mut App,
188 progress: &Progress,
189 goal: usize,
190 ) -> Result<(), Error> {
191 let style = text::Style::default()
192 .with_align(1, 2)
193 .with_colors(Set(self.style.bar_colors()))
194 .with_top_margin(self.y_of_bar());
195 let mut text = String::new();
196 let bar_size = usize::from(self.style.bar_size());
197 let normalized_progress = progress.current() * bar_size / goal;
198 let normalized_progress = normalized_progress as Coord;
199 for _ in 0 .. normalized_progress {
200 text.push_str("█");
201 }
202 for _ in normalized_progress .. self.style.bar_size() {
203 text.push_str(" ");
204 }
205 text::styled(app, &text, &style)?;
206 Ok(())
207 }
208
209 fn render_perc(
210 &self,
211 app: &mut App,
212 progress: &Progress,
213 goal: usize,
214 ) -> Result<(), Error> {
215 let style = text::Style::default()
216 .with_align(1, 2)
217 .with_colors(Set(self.style.perc_colors()))
218 .with_top_margin(self.y_of_perc());
219 let perc = progress.current() * 100 / goal;
220 let text = format!("{perc}%");
221 text::styled(app, &text, &style)?;
222 Ok(())
223 }
224
225 fn render_absolute(
226 &self,
227 app: &mut App,
228 progress: &Progress,
229 goal: usize,
230 ) -> Result<(), Error> {
231 let style = text::Style::default()
232 .with_align(1, 2)
233 .with_colors(Set(self.style.absolute_colors()))
234 .with_top_margin(self.y_of_absolute());
235 let text = format!("{current}/{goal}", current = progress.current());
236 text::styled(app, &text, &style)?;
237 Ok(())
238 }
239
240 fn render_status(
241 &self,
242 app: &mut App,
243 progress: &Progress,
244 ) -> Result<(), Error> {
245 let style = text::Style::default()
246 .with_align(1, 2)
247 .with_colors(Set(self.style.status_colors()))
248 .with_top_margin(self.y_of_status());
249 text::styled(app, progress.status(), &style)?;
250 Ok(())
251 }
252
253 fn y_of_bar(&self) -> Coord {
254 self.style.pad_after_title() + 1 + self.style.title_y()
255 }
256
257 fn y_of_perc(&self) -> Coord {
258 self.y_of_bar() + 1 + self.style.pad_after_bar()
259 }
260
261 fn y_of_absolute(&self) -> Coord {
262 self.y_of_perc() + 1 + self.style.pad_after_perc()
263 }
264
265 fn y_of_status(&self) -> Coord {
266 self.y_of_absolute() + 1 + self.style.pad_after_abs()
267 }
268}