diff --git a/veilid-cli/src/cached_text_view.rs b/veilid-cli/src/cached_text_view.rs index f944e05a..7f9b6d6c 100644 --- a/veilid-cli/src/cached_text_view.rs +++ b/veilid-cli/src/cached_text_view.rs @@ -1,3 +1,4 @@ +use std::collections::VecDeque; use std::ops::Deref; use std::sync::Arc; use std::sync::{Mutex, MutexGuard}; @@ -12,7 +13,32 @@ use owning_ref::{ArcRef, OwningHandle}; use unicode_width::UnicodeWidthStr; // Content type used internally for caching and storage -type InnerContentType = Arc; +type ContentType = VecDeque; +type InnerContentType = Arc; +type CacheType = StyledString; +type InnerCacheType = Arc; + +/// A reference to the text content. +/// +/// This can be deref'ed into a [`StyledString`]. +/// +/// [`StyledString`]: ../utils/markup/type.StyledString.html +/// +/// This keeps the content locked. Do not store this! +pub struct TextContentRef { + _handle: OwningHandle>, MutexGuard<'static, TextContentInner>>, + // We also need to keep a copy of Arc so `deref` can return + // a reference to the `StyledString` + data: Arc>, +} + +impl Deref for TextContentRef { + type Target = VecDeque; + + fn deref(&self) -> &VecDeque { + self.data.as_ref() + } +} /// Provides access to the content of a [`TextView`]. /// @@ -36,69 +62,75 @@ pub struct TextContent { content: Arc>, } +#[allow(dead_code)] + impl TextContent { /// Creates a new text content around the given value. /// /// Parses the given value. pub fn new(content: S) -> Self where - S: Into, + S: Into, { let content = Arc::new(content.into()); TextContent { content: Arc::new(Mutex::new(TextContentInner { content_value: content, - content_cache: Arc::new(StyledString::default()), + content_cache: Arc::new(CacheType::default()), size_cache: None, })), } } -} -/// A reference to the text content. -/// -/// This can be deref'ed into a [`StyledString`]. -/// -/// [`StyledString`]: ../utils/markup/type.StyledString.html -/// -/// This keeps the content locked. Do not store this! -pub struct TextContentRef { - _handle: OwningHandle>, MutexGuard<'static, TextContentInner>>, - // We also need to keep a copy of Arc so `deref` can return - // a reference to the `StyledString` - data: Arc, -} - -impl Deref for TextContentRef { - type Target = StyledString; - - fn deref(&self) -> &StyledString { - self.data.as_ref() - } -} - -#[allow(dead_code)] -impl TextContent { /// Replaces the content with the given value. pub fn set_content(&self, content: S) where - S: Into, + S: Into, { self.with_content(|c| { *c = content.into(); }); } - /// Append `content` to the end of a `TextView`. - pub fn append(&self, content: S) + /// Append `line` to the end of a `TextView`. + pub fn append_line(&self, line: S) where S: Into, { self.with_content(|c| { - // This will only clone content if content_cached and content_value - // are sharing the same underlying Rc. - c.append(content); + c.push_back(line.into()); + }) + } + + /// Append `lines` to the end of a `TextView`. + pub fn append_lines(&self, lines: S) + where + S: Iterator, + I: Into, + { + self.with_content(|c| { + for line in lines { + c.push_back(line.into()); + } + }) + } + + /// Remove lines from the beginning until we have no more than 'count' from the end + pub fn resize_back(&self, count: usize) { + self.with_content(|c| { + while c.len() > count { + c.remove(0); + } + }) + } + + /// Remove lines from the end until we have no more than 'count' from the beginning + pub fn resize_front(&self, count: usize) { + self.with_content(|c| { + while c.len() > count { + c.remove(c.len() - 1); + } }) } @@ -113,7 +145,7 @@ impl TextContent { /// Apply the given closure to the inner content, and bust the cache afterward. pub fn with_content(&self, f: F) -> O where - F: FnOnce(&mut StyledString) -> O, + F: FnOnce(&mut ContentType) -> O, { self.with_content_inner(|c| f(Arc::make_mut(&mut c.content_value))) } @@ -141,7 +173,7 @@ impl TextContent { struct TextContentInner { // content: String, content_value: InnerContentType, - content_cache: InnerContentType, + content_cache: InnerCacheType, // We keep the cache here so it can be busted when we change the content. size_cache: Option>, @@ -167,7 +199,7 @@ impl TextContentInner { } } - fn get_cache(&self) -> &InnerContentType { + fn get_cache(&self) -> &InnerCacheType { &self.content_cache } } @@ -194,6 +226,9 @@ pub struct CachedTextView { // True if we can wrap long lines. wrap: bool, + // Maximum number of lines to keep while appending + max_lines: Option, + // ScrollBase make many scrolling-related things easier width: Option, } @@ -201,11 +236,11 @@ pub struct CachedTextView { #[allow(dead_code)] impl CachedTextView { /// Creates a new TextView with the given content. - pub fn new(content: S, cache_size: usize) -> Self + pub fn new(content: S, cache_size: usize, max_lines: Option) -> Self where - S: Into, + S: Into, { - Self::new_with_content(TextContent::new(content), cache_size) + Self::new_with_content(TextContent::new(content), cache_size, max_lines) } /// Creates a new TextView using the given `TextContent`. @@ -224,7 +259,11 @@ impl CachedTextView { /// content.set_content("new content"); /// assert!(view.get_content().source().contains("new")); /// ``` - pub fn new_with_content(content: TextContent, cache_size: usize) -> Self { + pub fn new_with_content( + content: TextContent, + cache_size: usize, + max_lines: Option, + ) -> Self { CachedTextView { cache: TinyCache::new(cache_size), content, @@ -232,12 +271,13 @@ impl CachedTextView { wrap: true, align: Align::top_left(), width: None, + max_lines, } } /// Creates a new empty `TextView`. - pub fn empty() -> Self { - CachedTextView::new("", 5) + pub fn empty(cache_size: usize, max_lines: Option) -> Self { + CachedTextView::new(ContentType::default(), cache_size, max_lines) } /// Sets the style for the content. @@ -307,7 +347,7 @@ impl CachedTextView { #[must_use] pub fn content(self, content: S) -> Self where - S: Into, + S: Into, { self.with(|s| s.set_content(content)) } @@ -315,19 +355,35 @@ impl CachedTextView { /// Replace the text in this view. pub fn set_content(&mut self, content: S) where - S: Into, + S: Into, { self.cache.clear(); self.content.set_content(content); } /// Append `content` to the end of a `TextView`. - pub fn append(&mut self, content: S) + pub fn append_line(&mut self, content: S) where S: Into, { self.cache.clear(); - self.content.append(content); + self.content.append_line(content); + if let Some(max_lines) = self.max_lines { + self.content.resize_back(max_lines); + } + } + + /// Append `content` lines to the end of a `TextView`. + pub fn append_lines(&mut self, content: I) + where + I: Iterator, + S: Into, + { + self.cache.clear(); + self.content.append_lines(content); + if let Some(max_lines) = self.max_lines { + self.content.resize_back(max_lines); + } } /// Returns the current text in this view. @@ -357,7 +413,13 @@ impl CachedTextView { // Completely bust the cache // Just in case we fail, we don't want to leave a bad cache. content.size_cache = None; - content.content_cache = Arc::clone(&content.content_value); + content.content_cache = Arc::new(StyledString::from_iter( + content.content_value.iter().map(|s| { + let mut s = s.clone(); + s.append_plain("\n"); + s + }), + )); let rows = self.cache.compute(size.x, || { LinesIterator::new(content.get_cache().as_ref(), size.x).collect() @@ -522,13 +584,14 @@ mod tests { #[test] fn sanity() { - let text_view = CachedTextView::new("", 5); - assert_eq!(text_view.get_content().data.spans().len(), 1); + let text_view = CachedTextView::new(ContentType::default(), 5, None); + assert_eq!(text_view.get_content().data.len(), 0); } #[test] fn test_cache() { - let mut text_view = CachedTextView::new("sample", 3); + let mut text_view = + CachedTextView::new(VecDeque::from([StyledString::from("sample")]), 3, None); assert!(text_view.cache.is_empty()); text_view.compute_rows(Vec2::new(0, 0)); @@ -547,11 +610,11 @@ mod tests { assert_eq!(text_view.cache.keys(), [(&0, 1), (&2, 0), (&3, 0)]); - text_view.set_content(""); + text_view.set_content(VecDeque::new()); assert_eq!(text_view.cache.len(), 0); text_view.compute_rows(Vec2::new(0, 0)); - text_view.append("sample"); + text_view.append_line("sample"); assert_eq!(text_view.cache.len(), 0); text_view.compute_rows(Vec2::new(0, 0)); diff --git a/veilid-cli/src/settings.rs b/veilid-cli/src/settings.rs index 73ecb29d..2014ab37 100644 --- a/veilid-cli/src/settings.rs +++ b/veilid-cli/src/settings.rs @@ -20,7 +20,7 @@ logging: append: true interface: node_log: - scrollback: 2048 + scrollback: 10000 command_line: history_size: 2048 theme: diff --git a/veilid-cli/src/ui.rs b/veilid-cli/src/ui.rs index 95d1b60a..10685c97 100644 --- a/veilid-cli/src/ui.rs +++ b/veilid-cli/src/ui.rs @@ -223,7 +223,7 @@ impl UI { }); } fn clear_handler(siv: &mut Cursive) { - Self::node_events_view(siv).set_content(""); + Self::node_events_view(siv).set_content([]); UI::update_cb(siv); } @@ -262,15 +262,22 @@ impl UI { } //////////////////////////////////////////////////////////////////////////////////////////////// - pub fn push_styled(s: &mut Cursive, styled_string: StyledString) { + fn push_styled_line(s: &mut Cursive, styled_string: StyledString) { let mut ctv = UI::node_events_view(s); - ctv.append(styled_string) + ctv.append_line(styled_string) } - pub fn push_ansi_lines(s: &mut Cursive, starting_style: Style, lines: String) { + fn push_ansi_lines(s: &mut Cursive, mut starting_style: Style, lines: String) { let mut ctv = UI::node_events_view(s); - let (spanned_string, _end_style) = ansi::parse_with_starting_style(starting_style, lines); - ctv.append(spanned_string); + + let mut sslines: Vec = vec![]; + for line in lines.lines() { + let (spanned_string, end_style) = ansi::parse_with_starting_style(starting_style, line); + sslines.push(spanned_string); + starting_style = end_style; + } + + ctv.append_lines(sslines.into_iter()); } //////////////////////////////////////////////////////////////////////////////////////////////// @@ -883,7 +890,7 @@ impl UI { node_log_scrollback: usize, ) -> ResizedView> { Panel::new( - CachedTextView::new("", node_log_scrollback) + CachedTextView::new([], node_log_scrollback, Some(node_log_scrollback)) .with_name("node-events-view") .scrollable() .scroll_strategy(cursive::view::ScrollStrategy::StickToBottom) @@ -1167,7 +1174,7 @@ impl UISender { pub fn push_styled(&self, styled_string: StyledString) -> std::io::Result<()> { let res = self.cb_sink.send(Box::new(move |s| { - UI::push_styled(s, styled_string); + UI::push_styled_line(s, styled_string); })); if res.is_err() { return Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)); @@ -1206,6 +1213,7 @@ impl LogWriter for UISender { for argline in args.lines() { line.append_styled(argline, color); + line.append_plain("\n"); self.push_styled(line)?; line = StyledString::new(); line.append_plain(" ".repeat(indent));