1use crate::widget::logformatter::LogFormatter;
2use crate::widget::standard_formatter::LogStandardFormatter;
3use parking_lot::Mutex;
4use std::sync::Arc;
5
6use ratatui::{
7 buffer::Buffer,
8 layout::Rect,
9 style::Style,
10 text::Line,
11 widgets::{Block, Widget},
12};
13
14use crate::widget::inner::LinePointer;
15use crate::{CircularBuffer, ExtLogRecord, TuiLoggerLevelOutput, TuiWidgetState, TUI_LOGGER};
16
17use super::inner::TuiWidgetInnerState;
18
19pub struct TuiLoggerWidget<'b> {
20 block: Option<Block<'b>>,
21 logformatter: Option<Box<dyn LogFormatter>>,
22 style: Style,
24 style_error: Option<Style>,
26 style_warn: Option<Style>,
27 style_debug: Option<Style>,
28 style_trace: Option<Style>,
29 style_info: Option<Style>,
30 format_separator: char,
31 format_timestamp: Option<String>,
32 format_output_level: Option<TuiLoggerLevelOutput>,
33 format_output_target: bool,
34 format_output_file: bool,
35 format_output_line: bool,
36 state: Arc<Mutex<TuiWidgetInnerState>>,
37}
38impl<'b> Default for TuiLoggerWidget<'b> {
39 fn default() -> TuiLoggerWidget<'b> {
40 TuiLoggerWidget {
41 block: None,
42 logformatter: None,
43 style: Default::default(),
44 style_error: None,
45 style_warn: None,
46 style_debug: None,
47 style_trace: None,
48 style_info: None,
49 format_separator: ':',
50 format_timestamp: Some("%H:%M:%S".to_string()),
51 format_output_level: Some(TuiLoggerLevelOutput::Long),
52 format_output_target: true,
53 format_output_file: true,
54 format_output_line: true,
55 state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
56 }
57 }
58}
59impl<'b> TuiLoggerWidget<'b> {
60 pub fn block(mut self, block: Block<'b>) -> Self {
61 self.block = Some(block);
62 self
63 }
64 pub fn opt_formatter(mut self, formatter: Option<Box<dyn LogFormatter>>) -> Self {
65 self.logformatter = formatter;
66 self
67 }
68 pub fn formatter(mut self, formatter: Box<dyn LogFormatter>) -> Self {
69 self.logformatter = Some(formatter);
70 self
71 }
72 pub fn opt_style(mut self, style: Option<Style>) -> Self {
73 if let Some(s) = style {
74 self.style = s;
75 }
76 self
77 }
78 pub fn opt_style_error(mut self, style: Option<Style>) -> Self {
79 if style.is_some() {
80 self.style_error = style;
81 }
82 self
83 }
84 pub fn opt_style_warn(mut self, style: Option<Style>) -> Self {
85 if style.is_some() {
86 self.style_warn = style;
87 }
88 self
89 }
90 pub fn opt_style_info(mut self, style: Option<Style>) -> Self {
91 if style.is_some() {
92 self.style_info = style;
93 }
94 self
95 }
96 pub fn opt_style_trace(mut self, style: Option<Style>) -> Self {
97 if style.is_some() {
98 self.style_trace = style;
99 }
100 self
101 }
102 pub fn opt_style_debug(mut self, style: Option<Style>) -> Self {
103 if style.is_some() {
104 self.style_debug = style;
105 }
106 self
107 }
108 pub fn style(mut self, style: Style) -> Self {
109 self.style = style;
110 self
111 }
112 pub fn style_error(mut self, style: Style) -> Self {
113 self.style_error = Some(style);
114 self
115 }
116 pub fn style_warn(mut self, style: Style) -> Self {
117 self.style_warn = Some(style);
118 self
119 }
120 pub fn style_info(mut self, style: Style) -> Self {
121 self.style_info = Some(style);
122 self
123 }
124 pub fn style_trace(mut self, style: Style) -> Self {
125 self.style_trace = Some(style);
126 self
127 }
128 pub fn style_debug(mut self, style: Style) -> Self {
129 self.style_debug = Some(style);
130 self
131 }
132 pub fn opt_output_separator(mut self, opt_sep: Option<char>) -> Self {
133 if let Some(ch) = opt_sep {
134 self.format_separator = ch;
135 }
136 self
137 }
138 pub fn output_separator(mut self, sep: char) -> Self {
141 self.format_separator = sep;
142 self
143 }
144 pub fn opt_output_timestamp(mut self, opt_fmt: Option<Option<String>>) -> Self {
145 if let Some(fmt) = opt_fmt {
146 self.format_timestamp = fmt;
147 }
148 self
149 }
150 pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
157 self.format_timestamp = fmt;
158 self
159 }
160 pub fn opt_output_level(mut self, opt_fmt: Option<Option<TuiLoggerLevelOutput>>) -> Self {
161 if let Some(fmt) = opt_fmt {
162 self.format_output_level = fmt;
163 }
164 self
165 }
166 pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Self {
174 self.format_output_level = level;
175 self
176 }
177 pub fn opt_output_target(mut self, opt_enabled: Option<bool>) -> Self {
178 if let Some(enabled) = opt_enabled {
179 self.format_output_target = enabled;
180 }
181 self
182 }
183 pub fn output_target(mut self, enabled: bool) -> Self {
187 self.format_output_target = enabled;
188 self
189 }
190 pub fn opt_output_file(mut self, opt_enabled: Option<bool>) -> Self {
191 if let Some(enabled) = opt_enabled {
192 self.format_output_file = enabled;
193 }
194 self
195 }
196 pub fn output_file(mut self, enabled: bool) -> Self {
200 self.format_output_file = enabled;
201 self
202 }
203 pub fn opt_output_line(mut self, opt_enabled: Option<bool>) -> Self {
204 if let Some(enabled) = opt_enabled {
205 self.format_output_line = enabled;
206 }
207 self
208 }
209 pub fn output_line(mut self, enabled: bool) -> Self {
213 self.format_output_line = enabled;
214 self
215 }
216 pub fn inner_state(mut self, state: Arc<Mutex<TuiWidgetInnerState>>) -> Self {
217 self.state = state;
218 self
219 }
220 pub fn state(mut self, state: &TuiWidgetState) -> Self {
221 self.state = state.clone_state();
222 self
223 }
224 fn next_event<'a>(
225 &self,
226 events: &'a CircularBuffer<ExtLogRecord>,
227 mut index: usize,
228 ignore_current: bool,
229 increment: bool,
230 state: &TuiWidgetInnerState,
231 ) -> Option<(Option<usize>, usize, &'a ExtLogRecord)> {
232 if ignore_current {
234 index = if increment {
235 index + 1
236 } else {
237 if index == 0 {
238 return None;
239 }
240 index - 1
241 };
242 }
243 while let Some(evt) = events.element_at_index(index) {
244 let mut skip = false;
245 if let Some(level) = state
246 .config
247 .get(&evt.target())
248 .or(state.config.get_default_display_level())
249 {
250 if level < evt.level {
251 skip = true;
252 }
253 }
254 if !skip && state.focus_selected {
255 if let Some(target) = state.opt_selected_target.as_ref() {
256 if target != &evt.target() {
257 skip = true;
258 }
259 }
260 }
261 if skip {
262 index = if increment {
263 index + 1
264 } else {
265 if index == 0 {
266 break;
267 }
268 index - 1
269 };
270 } else {
271 if increment {
272 return Some((Some(index + 1), index, evt));
273 } else {
274 if index == 0 {
275 return Some((None, index, evt));
276 }
277 return Some((Some(index - 1), index, evt));
278 };
279 }
280 }
281 None
282 }
283}
284impl<'b> Widget for TuiLoggerWidget<'b> {
285 fn render(mut self, area: Rect, buf: &mut Buffer) {
286 let render_debug = false;
287
288 let formatter = match self.logformatter.take() {
289 Some(fmt) => fmt,
290 None => {
291 let fmt = LogStandardFormatter {
292 style: self.style,
293 style_error: self.style_error,
294 style_warn: self.style_warn,
295 style_debug: self.style_debug,
296 style_trace: self.style_trace,
297 style_info: self.style_info,
298 format_separator: self.format_separator,
299 format_timestamp: self.format_timestamp.clone(),
300 format_output_level: self.format_output_level,
301 format_output_target: self.format_output_target,
302 format_output_file: self.format_output_file,
303 format_output_line: self.format_output_line,
304 };
305 Box::new(fmt)
306 }
307 };
308
309 buf.set_style(area, self.style);
310 let list_area = match self.block.take() {
311 Some(b) => {
312 let inner_area = b.inner(area);
313 b.render(area, buf);
314 inner_area
315 }
316 None => area,
317 };
318 if list_area.width < formatter.min_width() || list_area.height < 1 {
319 return;
320 }
321
322 let mut state = self.state.lock();
323 let la_height = list_area.height as usize;
324 let la_left = list_area.left();
325 let la_top = list_area.top();
326 let la_width = list_area.width as usize;
327 let mut rev_lines: Vec<(LinePointer, Line)> = vec![];
328 let mut can_scroll_up = true;
329 let mut can_scroll_down = state.opt_line_pointer_center.is_some();
330 {
331 enum Pos {
332 Top,
333 Bottom,
334 Center(usize),
335 }
336 let tui_lock = TUI_LOGGER.inner.lock();
337 let opt_pos_event_index = if let Some(lp) = state.opt_line_pointer_center {
340 tui_lock.events.first_index().map(|first_index| {
341 if first_index <= lp.event_index {
342 (Pos::Center(lp.subline), lp.event_index)
343 } else {
344 (Pos::Top, first_index)
345 }
346 })
347 } else {
348 tui_lock
349 .events
350 .last_index()
351 .map(|last_index| (Pos::Bottom, last_index))
352 };
353 if let Some((pos, event_index)) = opt_pos_event_index {
354 let mut lines: Vec<(usize, Vec<Line>, usize)> = Vec::new();
356 let mut from_line: isize = 0;
357 let mut to_line = 0;
358 match pos {
359 Pos::Center(subline) => {
360 if render_debug {
361 println!("CENTER {}", event_index);
362 }
363 if let Some((_, evt_index, evt)) =
364 self.next_event(&tui_lock.events, event_index, false, true, &state)
365 {
366 let evt_lines = formatter.format(la_width, evt);
367 from_line = (la_height / 2) as isize - subline as isize;
368 to_line = la_height / 2 + (evt_lines.len() - 1) - subline;
369 let n = evt_lines.len();
370 lines.push((evt_index, evt_lines, n));
371 if render_debug {
372 println!("Center is {}", evt_index);
373 }
374 }
375 }
376 Pos::Top => {
377 can_scroll_up = false;
378 if render_debug {
379 println!("TOP");
380 }
381 if let Some((_, evt_index, evt)) =
382 self.next_event(&tui_lock.events, event_index, false, true, &state)
383 {
384 let evt_lines = formatter.format(la_width, evt);
385 from_line = 0;
386 to_line = evt_lines.len() - 1;
387 let n = evt_lines.len();
388 lines.push((evt_index, evt_lines, n));
389 if render_debug {
390 println!("Top is {}", evt_index);
391 }
392 }
393 }
394 Pos::Bottom => {
395 if render_debug {
396 println!("TOP");
397 }
398 if let Some((_, evt_index, evt)) =
399 self.next_event(&tui_lock.events, event_index, false, false, &state)
400 {
401 let evt_lines = formatter.format(la_width, evt);
402 to_line = la_height - 1;
403 from_line = to_line as isize - (evt_lines.len() - 1) as isize;
404 let n = evt_lines.len();
405 lines.push((evt_index, evt_lines, n));
406 if render_debug {
407 println!("Bottom is {}", evt_index);
408 }
409 }
410 }
411 }
412 if !lines.is_empty() {
413 let mut cont = true;
414 let mut at_top = false;
415 let mut at_bottom = false;
416 while cont {
417 if render_debug {
418 println!("from_line {}, to_line {}", from_line, to_line);
419 }
420 cont = false;
421 if from_line > 0 {
422 if let Some((_, evt_index, evt)) = self.next_event(
423 &tui_lock.events,
424 lines.first().as_ref().unwrap().0,
425 true,
426 false,
427 &state,
428 ) {
429 let evt_lines = formatter.format(la_width, evt);
430 from_line -= evt_lines.len() as isize;
431 let n = evt_lines.len();
432 lines.insert(0, (evt_index, evt_lines, n));
433 cont = true;
434 } else {
435 at_top = true;
437 if render_debug {
438 println!("no more events adjust start");
439 }
440 to_line = to_line - from_line as usize;
441 from_line = 0;
442 if render_debug {
443 println!("=> from_line {}, to_line {}", from_line, to_line);
444 }
445 }
446 }
447 if to_line < la_height - 1 {
448 if let Some((_, evt_index, evt)) = self.next_event(
449 &tui_lock.events,
450 lines.last().as_ref().unwrap().0,
451 true,
452 true,
453 &state,
454 ) {
455 let evt_lines = formatter.format(la_width, evt);
456 to_line += evt_lines.len();
457 let n = evt_lines.len();
458 lines.push((evt_index, evt_lines, n));
459 cont = true;
460 } else {
461 at_bottom = true;
462 can_scroll_down = false;
463 if render_debug {
464 println!("no more events at end");
465 }
466 if to_line != la_height - 1 {
468 cont = true;
469 } else if !cont {
470 break;
471 }
472 from_line = from_line + (la_height - 1 - to_line) as isize;
474 to_line = la_height - 1;
475 if render_debug {
476 println!("=> from_line {}, to_line {}", from_line, to_line);
477 }
478 }
479 }
480 if at_top && at_bottom {
481 break;
482 }
483 }
484 if at_top {
485 can_scroll_up = false;
486 }
487 if at_bottom {
488 can_scroll_down = false;
489 }
490 if render_debug {
491 println!("finished: from_line {}, to_line {}", from_line, to_line);
492 }
493 let mut curr: isize = to_line as isize;
494 while let Some((evt_index, evt_lines, mut n)) = lines.pop() {
495 for line in evt_lines.into_iter().rev() {
496 n -= 1;
497 if curr < 0 {
498 break;
499 }
500 if curr < la_height as isize {
501 let line_ptr = LinePointer {
502 event_index: evt_index,
503 subline: n,
504 };
505 rev_lines.push((line_ptr, line));
506 }
507 curr -= 1;
508 }
509 }
510 }
511 } else {
512 can_scroll_down = false;
513 can_scroll_up = false;
514 }
515 }
516
517 state.opt_line_pointer_next_page = if can_scroll_down {
518 rev_lines.first().map(|l| l.0)
519 } else {
520 None
521 };
522 state.opt_line_pointer_prev_page = if can_scroll_up {
523 rev_lines.last().map(|l| l.0)
524 } else {
525 None
526 };
527
528 if render_debug {
529 println!("Line pointers in buffer:");
530 for l in rev_lines.iter().rev() {
531 println!("event_index {}, subline {}", l.0.event_index, l.0.subline);
532 }
533 if state.opt_line_pointer_center.is_some() {
534 println!(
535 "Linepointer center: {:?}",
536 state.opt_line_pointer_center.unwrap()
537 );
538 }
539 if state.opt_line_pointer_next_page.is_some() {
540 println!(
541 "Linepointer next: {:?}",
542 state.opt_line_pointer_next_page.unwrap()
543 );
544 }
545 if state.opt_line_pointer_prev_page.is_some() {
546 println!(
547 "Linepointer prev: {:?}",
548 state.opt_line_pointer_prev_page.unwrap()
549 );
550 }
551 }
552
553 let offset: u16 = if state.opt_line_pointer_center.is_none() {
555 0
556 } else {
557 let lines_cnt = rev_lines.len();
558 std::cmp::max(0, la_height - lines_cnt) as u16
559 };
560
561 for (i, line) in rev_lines.into_iter().rev().take(la_height).enumerate() {
562 line.1.render(
563 Rect {
564 x: la_left,
565 y: la_top + i as u16 + offset,
566 width: list_area.width,
567 height: 1,
568 },
569 buf,
570 )
571 }
572 }
573}