Skip to main content

tui_lipan/widgets/log_view/
mod.rs

1//! Log stream widgets and helpers.
2
3mod buffer;
4pub(crate) mod component;
5pub(crate) mod matching;
6
7pub use buffer::{LogBuffer, LogEntry, LogLevel};
8
9use std::sync::Arc;
10
11use crate::callback::Callback;
12use crate::core::element::Element;
13use crate::style::{BorderStyle, Length, Padding, ScrollbarConfig, Style, StyleSlot};
14
15/// Log row event emitted from `LogView`.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct LogViewEvent {
18    /// Row index within currently visible (filtered) rows.
19    pub visible_index: usize,
20    /// Index in the original source entries passed to `LogView`.
21    pub source_index: usize,
22    /// Selected entry value.
23    pub entry: LogEntry,
24}
25
26pub use crate::utils::nucleo::MatchMode as LogFilterMode;
27
28#[derive(Clone, PartialEq)]
29pub struct LogViewProps {
30    pub entries: Arc<[LogEntry]>,
31    pub filter: Option<Arc<str>>,
32    pub filter_mode: LogFilterMode,
33    pub case_sensitive: bool,
34    pub show_level: bool,
35    pub auto_follow: bool,
36    pub paused: bool,
37    pub selected: usize,
38    pub style: Style,
39    pub hover_style: StyleSlot,
40    pub item_hover_style: StyleSlot,
41    pub selection_style: StyleSlot,
42    pub unfocused_selection_style: StyleSlot,
43    pub border: bool,
44    /// Border style.
45    /// Default: `BorderStyle::Plain`.
46    pub border_style: BorderStyle,
47    /// Inner padding.
48    /// Default: `Padding::default()`.
49    pub padding: Padding,
50    pub scrollbar: bool,
51    pub scrollbar_config: ScrollbarConfig,
52    pub show_scroll_indicators: bool,
53    pub scroll_indicator_style: Style,
54    /// Requested width.
55    /// Default: `Length::Flex(1)`.
56    pub width: Length,
57    /// Requested height.
58    /// Default: `Length::Flex(1)`.
59    pub height: Length,
60    pub empty_text: Option<Arc<str>>,
61    pub empty_text_style: Style,
62    pub trace_style: Style,
63    pub debug_style: Style,
64    pub info_style: Style,
65    pub warn_style: Style,
66    pub error_style: Style,
67    pub on_select: Option<Callback<LogViewEvent>>,
68    pub on_activate: Option<Callback<LogViewEvent>>,
69    /// Whether a single click activates a row (firing `on_activate`).
70    ///
71    /// When `false`, `on_activate` only fires on `Enter` or a double-click,
72    /// while a single click still selects via `on_select`.
73    /// Default: `true`.
74    pub activate_on_click: bool,
75}
76
77impl LogViewProps {
78    pub fn level_style(&self, level: LogLevel) -> Style {
79        match level {
80            LogLevel::Trace => self.trace_style,
81            LogLevel::Debug => self.debug_style,
82            LogLevel::Info => self.info_style,
83            LogLevel::Warn => self.warn_style,
84            LogLevel::Error => self.error_style,
85        }
86    }
87}
88
89/// High-throughput log list with nucleo-powered filtering and level highlighting.
90#[derive(Clone)]
91pub struct LogView {
92    props: LogViewProps,
93}
94
95impl Default for LogView {
96    fn default() -> Self {
97        Self {
98            props: LogViewProps {
99                entries: Arc::new([]),
100                filter: None,
101                filter_mode: LogFilterMode::Fuzzy,
102                case_sensitive: true,
103                show_level: true,
104                auto_follow: true,
105                paused: false,
106                selected: 0,
107                style: Style::default(),
108                hover_style: StyleSlot::Inherit,
109                item_hover_style: StyleSlot::Inherit,
110                selection_style: StyleSlot::Inherit,
111                unfocused_selection_style: StyleSlot::Inherit,
112                border: false,
113                border_style: BorderStyle::default(),
114                padding: Padding::default(),
115                scrollbar: true,
116                scrollbar_config: ScrollbarConfig::default(),
117                show_scroll_indicators: false,
118                scroll_indicator_style: Style::default(),
119                width: Length::Flex(1),
120                height: Length::Flex(1),
121                empty_text: Some("No log lines".into()),
122                empty_text_style: Style::default(),
123                trace_style: Style::default(),
124                debug_style: Style::default(),
125                info_style: Style::default(),
126                warn_style: Style::default(),
127                error_style: Style::default(),
128                on_select: None,
129                on_activate: None,
130                activate_on_click: true,
131            },
132        }
133    }
134}
135
136impl LogView {
137    /// Create an empty log view.
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// Replace source entries.
143    pub fn entries<I>(mut self, entries: I) -> Self
144    where
145        I: IntoIterator<Item = LogEntry>,
146    {
147        self.props.entries = entries.into_iter().collect();
148        self
149    }
150
151    /// Set source entries from a shared slice.
152    pub fn entries_arc(mut self, entries: Arc<[LogEntry]>) -> Self {
153        self.props.entries = entries;
154        self
155    }
156
157    /// Add one entry.
158    ///
159    /// For large updates prefer `entries()` or `entries_arc()`.
160    pub fn entry(mut self, entry: LogEntry) -> Self {
161        let mut entries = self.props.entries.to_vec();
162        entries.push(entry);
163        self.props.entries = entries.into();
164        self
165    }
166
167    /// Set row filter text.
168    pub fn filter(mut self, filter: impl Into<Arc<str>>) -> Self {
169        self.props.filter = Some(filter.into());
170        self
171    }
172
173    /// Clear row filter text.
174    pub fn clear_filter(mut self) -> Self {
175        self.props.filter = None;
176        self
177    }
178
179    /// Set filtering mode used by nucleo.
180    pub fn filter_mode(mut self, mode: LogFilterMode) -> Self {
181        self.props.filter_mode = mode;
182        self
183    }
184
185    /// Use fuzzy matching for filtering (nucleo).
186    pub fn fuzzy(mut self) -> Self {
187        self.props.filter_mode = LogFilterMode::Fuzzy;
188        self
189    }
190
191    /// Use substring matching for filtering (nucleo).
192    pub fn substring(mut self) -> Self {
193        self.props.filter_mode = LogFilterMode::Substring;
194        self
195    }
196
197    /// Use exact matching for filtering (nucleo).
198    pub fn exact(mut self) -> Self {
199        self.props.filter_mode = LogFilterMode::Exact;
200        self
201    }
202
203    /// Toggle case-sensitive matching for non-regex filtering.
204    pub fn case_sensitive(mut self, enabled: bool) -> Self {
205        self.props.case_sensitive = enabled;
206        self
207    }
208
209    /// Toggle level prefix (`[INFO]`) rendering.
210    pub fn show_level(mut self, show_level: bool) -> Self {
211        self.props.show_level = show_level;
212        self
213    }
214
215    /// Toggle auto-follow to the newest visible row.
216    pub fn auto_follow(mut self, auto_follow: bool) -> Self {
217        self.props.auto_follow = auto_follow;
218        self
219    }
220
221    /// Toggle paused mode.
222    ///
223    /// When paused, `auto_follow` is ignored and explicit selection is used.
224    pub fn paused(mut self, paused: bool) -> Self {
225        self.props.paused = paused;
226        self
227    }
228
229    /// Set selected visible row index.
230    pub fn selected(mut self, selected: usize) -> Self {
231        self.props.selected = selected;
232        self
233    }
234
235    /// Set base list style.
236    pub fn style(mut self, style: Style) -> Self {
237        self.props.style = style;
238        self
239    }
240
241    /// Set hovered list style.
242    pub fn hover_style(mut self, style: Style) -> Self {
243        self.props.hover_style = StyleSlot::Replace(style);
244        self
245    }
246
247    /// Extend the themed hovered list style.
248    pub fn extend_hover_style(mut self, style: Style) -> Self {
249        self.props.hover_style = StyleSlot::Extend(style);
250        self
251    }
252
253    /// Inherit the themed hovered list style.
254    pub fn inherit_hover_style(mut self) -> Self {
255        self.props.hover_style = StyleSlot::Inherit;
256        self
257    }
258
259    /// Set hovered list style slot directly for composite forwarding.
260    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
261        self.props.hover_style = slot;
262        self
263    }
264
265    /// Set hovered row style.
266    pub fn item_hover_style(mut self, style: Style) -> Self {
267        self.props.item_hover_style = StyleSlot::Replace(style);
268        self
269    }
270
271    /// Extend the themed hovered row style.
272    pub fn extend_item_hover_style(mut self, style: Style) -> Self {
273        self.props.item_hover_style = StyleSlot::Extend(style);
274        self
275    }
276
277    /// Inherit the themed hovered row style.
278    pub fn inherit_item_hover_style(mut self) -> Self {
279        self.props.item_hover_style = StyleSlot::Inherit;
280        self
281    }
282
283    /// Set hovered row style slot directly for composite forwarding.
284    pub fn item_hover_style_slot(mut self, slot: StyleSlot) -> Self {
285        self.props.item_hover_style = slot;
286        self
287    }
288
289    /// Set selected row style.
290    pub fn selection_style(mut self, style: Style) -> Self {
291        self.props.selection_style = StyleSlot::Replace(style);
292        self
293    }
294
295    /// Extend the themed selected row style.
296    pub fn extend_selection_style(mut self, style: Style) -> Self {
297        self.props.selection_style = StyleSlot::Extend(style);
298        self
299    }
300
301    /// Inherit the themed selected row style.
302    pub fn inherit_selection_style(mut self) -> Self {
303        self.props.selection_style = StyleSlot::Inherit;
304        self
305    }
306
307    /// Set selected row style slot directly for composite forwarding.
308    pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
309        self.props.selection_style = slot;
310        self
311    }
312
313    /// Set selected row style while the log view is not focused.
314    pub fn unfocused_selection_style(mut self, style: Style) -> Self {
315        self.props.unfocused_selection_style = StyleSlot::Replace(style);
316        self
317    }
318
319    /// Extend the themed selected row style while the log view is not focused.
320    pub fn extend_unfocused_selection_style(mut self, style: Style) -> Self {
321        self.props.unfocused_selection_style = StyleSlot::Extend(style);
322        self
323    }
324
325    /// Inherit the themed selected row style while the log view is not focused.
326    pub fn inherit_unfocused_selection_style(mut self) -> Self {
327        self.props.unfocused_selection_style = StyleSlot::Inherit;
328        self
329    }
330
331    /// Set unfocused selected row style slot directly for composite forwarding.
332    pub fn unfocused_selection_style_slot(mut self, slot: StyleSlot) -> Self {
333        self.props.unfocused_selection_style = slot;
334        self
335    }
336
337    /// Toggle border.
338    pub fn border(mut self, border: bool) -> Self {
339        self.props.border = border;
340        self
341    }
342
343    /// Set border style.
344    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
345        self.props.border_style = border_style;
346        self
347    }
348
349    /// Set inner padding.
350    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
351        self.props.padding = padding.into();
352        self
353    }
354
355    /// Toggle vertical scrollbar.
356    pub fn scrollbar(mut self, scrollbar: bool) -> Self {
357        self.props.scrollbar = scrollbar;
358        self
359    }
360
361    /// Set scrollbar configuration.
362    pub fn scrollbar_config(mut self, config: ScrollbarConfig) -> Self {
363        self.props.scrollbar_config = config;
364        self
365    }
366
367    /// Toggle hidden-row indicators (`N more`).
368    pub fn show_scroll_indicators(mut self, show: bool) -> Self {
369        self.props.show_scroll_indicators = show;
370        self
371    }
372
373    /// Set hidden-row indicator style.
374    pub fn scroll_indicator_style(mut self, style: Style) -> Self {
375        self.props.scroll_indicator_style = style;
376        self
377    }
378
379    /// Set width.
380    pub fn width(mut self, width: Length) -> Self {
381        self.props.width = width;
382        self
383    }
384
385    /// Set height.
386    pub fn height(mut self, height: Length) -> Self {
387        self.props.height = height;
388        self
389    }
390
391    /// Set empty-state text.
392    pub fn empty_text(mut self, text: impl Into<Arc<str>>) -> Self {
393        self.props.empty_text = Some(text.into());
394        self
395    }
396
397    /// Set empty-state style.
398    pub fn empty_text_style(mut self, style: Style) -> Self {
399        self.props.empty_text_style = style;
400        self
401    }
402
403    /// Set style for `TRACE` prefix.
404    pub fn trace_style(mut self, style: Style) -> Self {
405        self.props.trace_style = style;
406        self
407    }
408
409    /// Set style for `DEBUG` prefix.
410    pub fn debug_style(mut self, style: Style) -> Self {
411        self.props.debug_style = style;
412        self
413    }
414
415    /// Set style for `INFO` prefix.
416    pub fn info_style(mut self, style: Style) -> Self {
417        self.props.info_style = style;
418        self
419    }
420
421    /// Set style for `WARN` prefix.
422    pub fn warn_style(mut self, style: Style) -> Self {
423        self.props.warn_style = style;
424        self
425    }
426
427    /// Set style for `ERROR` prefix.
428    pub fn error_style(mut self, style: Style) -> Self {
429        self.props.error_style = style;
430        self
431    }
432
433    /// Set selection callback.
434    pub fn on_select(mut self, cb: Callback<LogViewEvent>) -> Self {
435        self.props.on_select = Some(cb);
436        self
437    }
438
439    /// Set activation callback (Enter).
440    pub fn on_activate(mut self, cb: Callback<LogViewEvent>) -> Self {
441        self.props.on_activate = Some(cb);
442        self
443    }
444
445    /// Control whether a single click activates a row (fires `on_activate`).
446    ///
447    /// When `false`, only `Enter` or a double-click activates; a single click
448    /// still selects. Default: `true`.
449    pub fn activate_on_click(mut self, activate: bool) -> Self {
450        self.props.activate_on_click = activate;
451        self
452    }
453}
454
455impl From<LogView> for Element {
456    fn from(view: LogView) -> Self {
457        crate::child(component::LogViewComponent::new, view.props)
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn log_buffer_keeps_newest_entries() {
467        let mut buffer = LogBuffer::new(2);
468        buffer.push(LogEntry::info("a"));
469        buffer.push(LogEntry::info("b"));
470        buffer.push(LogEntry::info("c"));
471
472        let snapshot = buffer.snapshot();
473        assert_eq!(snapshot.len(), 2);
474        assert_eq!(snapshot[0].message.as_ref(), "b");
475        assert_eq!(snapshot[1].message.as_ref(), "c");
476    }
477
478    #[test]
479    fn paused_snapshot_stays_frozen() {
480        let mut buffer = LogBuffer::new(8);
481        buffer.push(LogEntry::info("one"));
482        buffer.push(LogEntry::info("two"));
483        buffer.set_paused(true);
484        buffer.push(LogEntry::info("three"));
485
486        let snapshot = buffer.snapshot();
487        assert_eq!(snapshot.len(), 2);
488        assert_eq!(snapshot[1].message.as_ref(), "two");
489    }
490}