use crate::widget::logformatter::LogFormatter;
use crate::widget::standard_formatter::LogStandardFormatter;
use parking_lot::Mutex;
use std::sync::Arc;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Style,
text::Line,
widgets::{Block, Widget},
};
use crate::widget::inner::LinePointer;
use crate::{CircularBuffer, ExtLogRecord, TuiLoggerLevelOutput, TuiWidgetState, TUI_LOGGER};
use super::inner::TuiWidgetInnerState;
pub struct TuiLoggerWidget<'b> {
block: Option<Block<'b>>,
logformatter: Option<Box<dyn LogFormatter>>,
style: Style,
style_error: Option<Style>,
style_warn: Option<Style>,
style_debug: Option<Style>,
style_trace: Option<Style>,
style_info: Option<Style>,
format_separator: char,
format_timestamp: Option<String>,
format_output_level: Option<TuiLoggerLevelOutput>,
format_output_target: bool,
format_output_file: bool,
format_output_line: bool,
state: Arc<Mutex<TuiWidgetInnerState>>,
}
impl<'b> Default for TuiLoggerWidget<'b> {
fn default() -> TuiLoggerWidget<'b> {
TuiLoggerWidget {
block: None,
logformatter: None,
style: Default::default(),
style_error: None,
style_warn: None,
style_debug: None,
style_trace: None,
style_info: None,
format_separator: ':',
format_timestamp: Some("%H:%M:%S".to_string()),
format_output_level: Some(TuiLoggerLevelOutput::Long),
format_output_target: true,
format_output_file: true,
format_output_line: true,
state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
}
}
}
impl<'b> TuiLoggerWidget<'b> {
pub fn block(mut self, block: Block<'b>) -> Self {
self.block = Some(block);
self
}
pub fn opt_formatter(mut self, formatter: Option<Box<dyn LogFormatter>>) -> Self {
self.logformatter = formatter;
self
}
pub fn formatter(mut self, formatter: Box<dyn LogFormatter>) -> Self {
self.logformatter = Some(formatter);
self
}
pub fn opt_style(mut self, style: Option<Style>) -> Self {
if let Some(s) = style {
self.style = s;
}
self
}
pub fn opt_style_error(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_error = style;
}
self
}
pub fn opt_style_warn(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_warn = style;
}
self
}
pub fn opt_style_info(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_info = style;
}
self
}
pub fn opt_style_trace(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_trace = style;
}
self
}
pub fn opt_style_debug(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_debug = style;
}
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn style_error(mut self, style: Style) -> Self {
self.style_error = Some(style);
self
}
pub fn style_warn(mut self, style: Style) -> Self {
self.style_warn = Some(style);
self
}
pub fn style_info(mut self, style: Style) -> Self {
self.style_info = Some(style);
self
}
pub fn style_trace(mut self, style: Style) -> Self {
self.style_trace = Some(style);
self
}
pub fn style_debug(mut self, style: Style) -> Self {
self.style_debug = Some(style);
self
}
pub fn opt_output_separator(mut self, opt_sep: Option<char>) -> Self {
if let Some(ch) = opt_sep {
self.format_separator = ch;
}
self
}
pub fn output_separator(mut self, sep: char) -> Self {
self.format_separator = sep;
self
}
pub fn opt_output_timestamp(mut self, opt_fmt: Option<Option<String>>) -> Self {
if let Some(fmt) = opt_fmt {
self.format_timestamp = fmt;
}
self
}
pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
self.format_timestamp = fmt;
self
}
pub fn opt_output_level(mut self, opt_fmt: Option<Option<TuiLoggerLevelOutput>>) -> Self {
if let Some(fmt) = opt_fmt {
self.format_output_level = fmt;
}
self
}
pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Self {
self.format_output_level = level;
self
}
pub fn opt_output_target(mut self, opt_enabled: Option<bool>) -> Self {
if let Some(enabled) = opt_enabled {
self.format_output_target = enabled;
}
self
}
pub fn output_target(mut self, enabled: bool) -> Self {
self.format_output_target = enabled;
self
}
pub fn opt_output_file(mut self, opt_enabled: Option<bool>) -> Self {
if let Some(enabled) = opt_enabled {
self.format_output_file = enabled;
}
self
}
pub fn output_file(mut self, enabled: bool) -> Self {
self.format_output_file = enabled;
self
}
pub fn opt_output_line(mut self, opt_enabled: Option<bool>) -> Self {
if let Some(enabled) = opt_enabled {
self.format_output_line = enabled;
}
self
}
pub fn output_line(mut self, enabled: bool) -> Self {
self.format_output_line = enabled;
self
}
pub fn inner_state(mut self, state: Arc<Mutex<TuiWidgetInnerState>>) -> Self {
self.state = state;
self
}
pub fn state(mut self, state: &TuiWidgetState) -> Self {
self.state = state.clone_state();
self
}
fn next_event<'a>(
&self,
events: &'a CircularBuffer<ExtLogRecord>,
mut index: usize,
ignore_current: bool,
increment: bool,
state: &TuiWidgetInnerState,
) -> Option<(Option<usize>, usize, &'a ExtLogRecord)> {
if ignore_current {
index = if increment {
index + 1
} else {
if index == 0 {
return None;
}
index - 1
};
}
while let Some(evt) = events.element_at_index(index) {
let mut skip = false;
if let Some(level) = state
.config
.get(evt.target())
.or(state.config.get_default_display_level())
{
if level < evt.level {
skip = true;
}
}
if !skip && state.focus_selected {
if let Some(target) = state.opt_selected_target.as_ref() {
if target != evt.target() {
skip = true;
}
}
}
if skip {
index = if increment {
index + 1
} else {
if index == 0 {
break;
}
index - 1
};
} else if increment {
return Some((Some(index + 1), index, evt));
} else {
if index == 0 {
return Some((None, index, evt));
}
return Some((Some(index - 1), index, evt));
}
}
None
}
}
impl<'b> Widget for TuiLoggerWidget<'b> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let render_debug = false;
let formatter = match self.logformatter.take() {
Some(fmt) => fmt,
None => {
let fmt = LogStandardFormatter {
style: self.style,
style_error: self.style_error,
style_warn: self.style_warn,
style_debug: self.style_debug,
style_trace: self.style_trace,
style_info: self.style_info,
format_separator: self.format_separator,
format_timestamp: self.format_timestamp.clone(),
format_output_level: self.format_output_level,
format_output_target: self.format_output_target,
format_output_file: self.format_output_file,
format_output_line: self.format_output_line,
};
Box::new(fmt)
}
};
buf.set_style(area, self.style);
let list_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if list_area.width < formatter.min_width() || list_area.height < 1 {
return;
}
let mut state = self.state.lock();
let la_height = list_area.height as usize;
let la_left = list_area.left();
let la_top = list_area.top();
let la_width = list_area.width as usize;
let mut rev_lines: Vec<(LinePointer, Line)> = vec![];
let mut can_scroll_up = true;
let mut can_scroll_down = state.opt_line_pointer_center.is_some();
{
enum Pos {
Top,
Bottom,
Center(usize),
}
let tui_lock = TUI_LOGGER.inner.lock();
let opt_pos_event_index = if let Some(lp) = state.opt_line_pointer_center {
tui_lock.events.first_index().map(|first_index| {
if first_index <= lp.event_index {
(Pos::Center(lp.subline), lp.event_index)
} else {
(Pos::Top, first_index)
}
})
} else {
tui_lock
.events
.last_index()
.map(|last_index| (Pos::Bottom, last_index))
};
if let Some((pos, event_index)) = opt_pos_event_index {
let mut lines: Vec<(usize, Vec<Line>, usize)> = Vec::new();
let mut from_line: isize = 0;
let mut to_line = 0;
match pos {
Pos::Center(subline) => {
if render_debug {
println!("CENTER {}", event_index);
}
if let Some((_, evt_index, evt)) =
self.next_event(&tui_lock.events, event_index, false, true, &state)
{
let evt_lines = formatter.format(la_width, evt);
from_line = (la_height / 2) as isize - subline as isize;
to_line = la_height / 2 + (evt_lines.len() - 1) - subline;
let n = evt_lines.len();
lines.push((evt_index, evt_lines, n));
if render_debug {
println!("Center is {}", evt_index);
}
}
}
Pos::Top => {
can_scroll_up = false;
if render_debug {
println!("TOP");
}
if let Some((_, evt_index, evt)) =
self.next_event(&tui_lock.events, event_index, false, true, &state)
{
let evt_lines = formatter.format(la_width, evt);
from_line = 0;
to_line = evt_lines.len() - 1;
let n = evt_lines.len();
lines.push((evt_index, evt_lines, n));
if render_debug {
println!("Top is {}", evt_index);
}
}
}
Pos::Bottom => {
if render_debug {
println!("TOP");
}
if let Some((_, evt_index, evt)) =
self.next_event(&tui_lock.events, event_index, false, false, &state)
{
let evt_lines = formatter.format(la_width, evt);
to_line = la_height - 1;
from_line = to_line as isize - (evt_lines.len() - 1) as isize;
let n = evt_lines.len();
lines.push((evt_index, evt_lines, n));
if render_debug {
println!("Bottom is {}", evt_index);
}
}
}
}
if !lines.is_empty() {
let mut cont = true;
let mut at_top = false;
let mut at_bottom = false;
while cont {
if render_debug {
println!("from_line {}, to_line {}", from_line, to_line);
}
cont = false;
if from_line > 0 {
if let Some((_, evt_index, evt)) = self.next_event(
&tui_lock.events,
lines.first().as_ref().unwrap().0,
true,
false,
&state,
) {
let evt_lines = formatter.format(la_width, evt);
from_line -= evt_lines.len() as isize;
let n = evt_lines.len();
lines.insert(0, (evt_index, evt_lines, n));
cont = true;
} else {
at_top = true;
if render_debug {
println!("no more events adjust start");
}
to_line -= from_line as usize;
from_line = 0;
if render_debug {
println!("=> from_line {}, to_line {}", from_line, to_line);
}
}
}
if to_line < la_height - 1 {
if let Some((_, evt_index, evt)) = self.next_event(
&tui_lock.events,
lines.last().as_ref().unwrap().0,
true,
true,
&state,
) {
let evt_lines = formatter.format(la_width, evt);
to_line += evt_lines.len();
let n = evt_lines.len();
lines.push((evt_index, evt_lines, n));
cont = true;
} else {
at_bottom = true;
can_scroll_down = false;
if render_debug {
println!("no more events at end");
}
if to_line != la_height - 1 {
cont = true;
} else if !cont {
break;
}
from_line += (la_height - 1 - to_line) as isize;
to_line = la_height - 1;
if render_debug {
println!("=> from_line {}, to_line {}", from_line, to_line);
}
}
}
if at_top && at_bottom {
break;
}
}
if at_top {
can_scroll_up = false;
}
if at_bottom {
can_scroll_down = false;
}
if render_debug {
println!("finished: from_line {}, to_line {}", from_line, to_line);
}
let mut curr: isize = to_line as isize;
while let Some((evt_index, evt_lines, mut n)) = lines.pop() {
for line in evt_lines.into_iter().rev() {
n -= 1;
if curr < 0 {
break;
}
if curr < la_height as isize {
let line_ptr = LinePointer {
event_index: evt_index,
subline: n,
};
rev_lines.push((line_ptr, line));
}
curr -= 1;
}
}
}
} else {
can_scroll_down = false;
can_scroll_up = false;
}
}
state.opt_line_pointer_next_page = if can_scroll_down {
rev_lines.first().map(|l| l.0)
} else {
None
};
state.opt_line_pointer_prev_page = if can_scroll_up {
rev_lines.last().map(|l| l.0)
} else {
None
};
if render_debug {
println!("Line pointers in buffer:");
for l in rev_lines.iter().rev() {
println!("event_index {}, subline {}", l.0.event_index, l.0.subline);
}
if state.opt_line_pointer_center.is_some() {
println!(
"Linepointer center: {:?}",
state.opt_line_pointer_center.unwrap()
);
}
if state.opt_line_pointer_next_page.is_some() {
println!(
"Linepointer next: {:?}",
state.opt_line_pointer_next_page.unwrap()
);
}
if state.opt_line_pointer_prev_page.is_some() {
println!(
"Linepointer prev: {:?}",
state.opt_line_pointer_prev_page.unwrap()
);
}
}
let offset: u16 = if state.opt_line_pointer_center.is_none() {
0
} else {
let lines_cnt = rev_lines.len();
std::cmp::max(0, la_height - lines_cnt) as u16
};
for (i, line) in rev_lines.into_iter().rev().take(la_height).enumerate() {
line.1.render(
Rect {
x: la_left,
y: la_top + i as u16 + offset,
width: list_area.width,
height: 1,
},
buf,
)
}
}
}