1use actix::prelude::*;
2use ansi_to_tui::IntoText;
3use chrono::prelude::*;
4use crossterm::event::KeyEvent;
5use ratatui::backend::Backend;
6use ratatui::layout::Rect;
7use ratatui::text::Line;
8use ratatui::Frame;
9use std::rc::Rc;
10use std::str;
11use std::{cmp::min, collections::HashMap, io};
12use subprocess::ExitStatus;
13
14use ratatui::{
15 backend::CrosstermBackend,
16 layout::{Constraint, Direction, Layout},
17 style::{Color, Modifier, Style},
18 text::Span,
19 widgets::{Block, Borders, Paragraph, Tabs, Wrap},
20 Terminal,
21};
22
23use crossterm::{
24 cursor,
25 event::{self, Event, KeyCode, KeyModifiers, MouseEventKind},
26 execute,
27 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
28};
29
30use super::command::{CommandActor, PoisonPill, Reload};
31
32pub struct Panel {
33 logs: Vec<(String, Style)>,
34 lines: u16,
35 shift: u16,
36 command: Addr<CommandActor>,
37 status: Option<ExitStatus>,
38}
39
40impl Panel {
41 pub fn new(command: Addr<CommandActor>) -> Self {
42 Self {
43 logs: Vec::default(),
44 lines: 0,
45 shift: 0,
46 command,
47 status: None,
48 }
49 }
50}
51
52pub struct ConsoleActor {
53 terminal: Terminal<CrosstermBackend<io::Stdout>>,
54 index: String,
55 order: Vec<String>,
56 arbiter: Arbiter,
57 panels: HashMap<String, Panel>,
58 timestamp: bool,
59}
60
61pub fn chunks<T: Backend>(f: &Frame<T>) -> Rc<[Rect]> {
62 Layout::default()
63 .direction(Direction::Vertical)
64 .constraints([Constraint::Min(0), Constraint::Length(3)].as_ref())
65 .split(f.size())
66}
67
68impl ConsoleActor {
69 pub fn new(order: Vec<String>, timestamp: bool) -> Self {
70 let stdout = io::stdout();
71 let backend = CrosstermBackend::new(stdout);
72 let terminal = Terminal::new(backend).unwrap();
73 Self {
74 terminal,
75 index: order[0].clone(),
76 order,
77 arbiter: Arbiter::new(),
78 panels: HashMap::default(),
79 timestamp,
80 }
81 }
82
83 pub fn up(&mut self, shift: u16) {
84 let log_height = self.get_log_height();
85 if let Some(focused_panel) = self.panels.get_mut(&self.index) {
86 let maximum_scroll = focused_panel.lines - min(focused_panel.lines, log_height);
89
90 focused_panel.shift = min(focused_panel.shift + shift, maximum_scroll);
92 }
93 }
94
95 pub fn down(&mut self, shift: u16) {
96 if let Some(focused_panel) = self.panels.get_mut(&self.index) {
97 if focused_panel.shift >= shift {
98 focused_panel.shift -= shift;
99 } else {
100 focused_panel.shift = 0;
101 }
102 }
103 }
104
105 pub fn get_log_height(&mut self) -> u16 {
106 let frame = self.terminal.get_frame();
107 chunks(&frame)[0].height
108 }
109
110 pub fn go_to(&mut self, panel_index: usize) {
111 if panel_index < self.order.len() {
112 self.index = self.order[panel_index].clone();
113 }
114 }
115
116 pub fn idx(&self) -> usize {
117 self.order
118 .iter()
119 .position(|e| e == &self.index)
120 .unwrap_or(0)
121 }
122
123 pub fn next(&mut self) {
124 self.index = self.order[(self.idx() + 1) % self.order.len()].clone();
125 }
126
127 pub fn previous(&mut self) {
128 self.index = self.order[(self.idx() + self.order.len() - 1) % self.order.len()].clone();
129 }
130
131 fn clean(&mut self) {
132 self.terminal
133 .draw(|f| {
134 let clean =
135 Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
136 f.render_widget(clean, f.size());
137 })
138 .unwrap();
139 }
140
141 fn draw(&mut self) {
142 let idx = self.idx();
143 if let Some(focused_panel) = &self.panels.get(&self.index) {
144 self.terminal
145 .draw(|f| {
146 let chunks = chunks(f);
147 let logs = &focused_panel.logs;
148
149 let log_height = chunks[0].height;
150 let maximum_scroll = focused_panel.lines - min(focused_panel.lines, log_height);
151
152 let lines: Vec<Line> = logs
153 .iter()
154 .flat_map(|l| {
155 let mut t = l.0.into_text().unwrap();
156 t.patch_style(l.1);
157 t.lines
158 })
159 .collect();
160
161 let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
162
163 let paragraph = paragraph
165 .scroll((maximum_scroll - min(maximum_scroll, focused_panel.shift), 0));
166 f.render_widget(paragraph, chunks[0]);
167
168 let titles: Vec<Line> = self
169 .order
170 .iter()
171 .map(|panel| {
172 let span = self.panels.get(panel).map(|p| match p.status {
173 Some(ExitStatus::Exited(0)) => Span::styled(format!("{}.", panel), Style::default().fg(Color::Green)),
174 Some(_) => Span::styled(format!("{}!", panel), Style::default().fg(Color::Red)),
175 None => Span::styled(format!("{}*", panel), Style::default()),
176 }).unwrap_or_else(|| Span::styled(panel, Style::default()));
177 Line::from(span)
178 })
179 .collect();
180 let tabs = Tabs::new(titles)
191 .block(Block::default().borders(Borders::ALL))
192 .select(idx)
193 .highlight_style(
194 Style::default()
195 .add_modifier(Modifier::BOLD)
196 .bg(Color::DarkGray),
197 );
198 f.render_widget(tabs, chunks[1]);
199 })
200 .unwrap();
201 }
202 }
203}
204
205impl Actor for ConsoleActor {
206 type Context = Context<Self>;
207
208 fn started(&mut self, ctx: &mut Context<Self>) {
209 enable_raw_mode().unwrap();
210 execute!(
211 self.terminal.backend_mut(),
212 cursor::Hide,
213 EnterAlternateScreen,
214 )
215 .unwrap();
216
217 let addr = ctx.address();
218 self.arbiter.spawn(async move {
219 loop {
220 addr.do_send(TermEvent(event::read().unwrap()));
221 }
222 });
223
224 self.clean();
225 self.draw();
226 }
227
228 fn stopped(&mut self, _: &mut Self::Context) {
229 self.arbiter.stop();
230 self.clean();
231
232 execute!(
233 self.terminal.backend_mut(),
234 LeaveAlternateScreen,
235 cursor::Show,
236 )
237 .unwrap();
238 disable_raw_mode().unwrap();
239 }
240}
241
242#[derive(Message, Debug)]
243#[rtype(result = "()")]
244pub struct TermEvent(Event);
245
246impl TermEvent {
247 pub fn quit() -> Self {
248 Self(Event::Key(KeyEvent::new(
249 KeyCode::Char('q'),
250 KeyModifiers::NONE,
251 )))
252 }
253}
254
255impl Handler<TermEvent> for ConsoleActor {
256 type Result = ();
257
258 fn handle(&mut self, msg: TermEvent, _: &mut Context<Self>) -> Self::Result {
259 match msg.0 {
260 Event::Key(e) => match (e.modifiers, e.code) {
261 (KeyModifiers::CONTROL, KeyCode::Char('c'))
262 | (KeyModifiers::NONE, KeyCode::Char('q')) => {
263 self.panels
264 .values()
265 .for_each(|e| e.command.do_send(PoisonPill));
266 System::current().stop();
267 }
268 (KeyModifiers::NONE, KeyCode::Up | KeyCode::Char('k'))
269 | (KeyModifiers::CONTROL, KeyCode::Char('p')) => {
270 self.up(1);
271 }
272 (KeyModifiers::NONE, KeyCode::Down | KeyCode::Char('j'))
273 | (KeyModifiers::CONTROL, KeyCode::Char('n')) => {
274 self.down(1);
275 }
276 (KeyModifiers::CONTROL, key_code) => match key_code {
277 KeyCode::Char('f') => {
278 let log_height = self.get_log_height();
279 self.down(log_height);
280 }
281 KeyCode::Char('u') => {
282 let log_height = self.get_log_height();
283 self.up(log_height / 2);
284 }
285 KeyCode::Char('d') => {
286 let log_height = self.get_log_height();
287 self.down(log_height / 2);
288 }
289 KeyCode::Char('b') => {
290 let log_height = self.get_log_height();
291 self.up(log_height);
292 }
293 _ => {}
294 },
295 (KeyModifiers::NONE, key_code) => match key_code {
296 KeyCode::Char('r') => {
297 if let Some(focused_panel) = self.panels.get(&self.index) {
298 focused_panel.command.do_send(Reload::Manual);
299 }
300 }
301 KeyCode::Right | KeyCode::Char('l') => {
302 self.next();
303 }
304 KeyCode::Left | KeyCode::Char('h') => {
305 self.previous();
306 }
307 KeyCode::Char(ch) => {
308 if ch.is_ascii_digit() {
309 let mut panel_index = ch.to_digit(10).unwrap() as usize;
310 if panel_index == 0 {
313 panel_index = self.order.len() - 1;
314 } else {
315 panel_index -= 1;
316 }
317 self.go_to(panel_index);
318 }
319 }
320 _ => {}
321 },
322 _ => {}
323 },
324 Event::Resize(width, _) => {
325 for panel in self.panels.values_mut() {
326 panel.shift = 0;
327 let new_lines = panel
328 .logs
329 .iter()
330 .fold(0, |agg, l| agg + wrapped_lines(&l.0, width));
331 panel.lines = new_lines;
332 }
333 }
334 Event::Mouse(e) => match e.kind {
335 MouseEventKind::ScrollUp => {
336 self.up(1);
337 }
338 MouseEventKind::ScrollDown => {
339 self.down(1);
340 }
341 _ => {}
342 },
343 _ => {}
344 }
345 self.draw();
346 }
347}
348
349#[derive(Message)]
350#[rtype(result = "()")]
351pub struct Output {
352 panel_name: String,
353 pub message: String,
354 service: bool,
355 timestamp: DateTime<Local>,
356}
357
358impl Output {
359 pub fn now(panel_name: String, message: String, service: bool) -> Self {
360 Self {
361 panel_name,
362 message,
363 service,
364 timestamp: Local::now(),
365 }
366 }
367}
368
369fn wrapped_lines(message: &String, width: u16) -> u16 {
370 let clean = strip_ansi_escapes::strip(message);
371 textwrap::wrap(str::from_utf8(&clean).unwrap(), width as usize).len() as u16
372}
373
374fn format_message(message: &str, timestamp: &DateTime<Local>) -> String {
376 format!("{} {}", timestamp.format("%H:%M:%S%.3f"), message)
377}
378
379impl Handler<Output> for ConsoleActor {
380 type Result = ();
381
382 fn handle(&mut self, msg: Output, _: &mut Context<Self>) -> Self::Result {
383 let panel = self.panels.get_mut(&msg.panel_name).unwrap();
384 let style = match msg.service {
385 true => Style::default().bg(Color::DarkGray),
386 false => Style::default(),
387 };
388
389 let message = match self.timestamp {
390 true => format_message(&msg.message, &msg.timestamp),
391 false => msg.message,
392 };
393 let width = self.terminal.get_frame().size().width;
394 panel.lines += wrapped_lines(&message, width);
395 panel.logs.push((message, style));
396 self.draw();
397 }
398}
399
400#[derive(Message)]
401#[rtype(result = "()")]
402pub struct RegisterPanel {
403 pub name: String,
404 pub addr: Addr<CommandActor>,
405}
406
407impl Handler<RegisterPanel> for ConsoleActor {
408 type Result = ();
409
410 fn handle(&mut self, msg: RegisterPanel, _: &mut Context<Self>) -> Self::Result {
411 if !self.panels.contains_key(&msg.name) {
412 self.panels.insert(msg.name.clone(), Panel::new(msg.addr));
413 }
414 if !self.order.contains(&msg.name) {
415 self.order.push(msg.name);
416 }
417 self.draw();
418 }
419}
420
421#[derive(Message)]
422#[rtype(result = "()")]
423pub struct PanelStatus {
424 pub panel_name: String,
425 pub status: Option<ExitStatus>,
426}
427
428impl Handler<PanelStatus> for ConsoleActor {
429 type Result = ();
430
431 fn handle(&mut self, msg: PanelStatus, ctx: &mut Context<Self>) -> Self::Result {
432 let focused_panel = self.panels.get_mut(&msg.panel_name).unwrap();
433 focused_panel.status = msg.status;
434
435 if let Some(message) = msg.status.map(|c| format!("Status: {:?}", c)) {
436 ctx.address()
437 .do_send(Output::now(msg.panel_name, message, true));
438 }
439
440 self.draw();
441 }
442}