use std::{
sync::Arc,
time::Instant,
};
use crossterm::event::{
Event,
KeyCode,
KeyEvent,
KeyModifiers,
MouseEvent,
MouseEventKind,
};
use futures::{
channel::mpsc,
lock::Mutex,
prelude::*,
};
use textwrap::core::Fragment;
use tracing::{
debug,
instrument,
};
use tui::{
buffer::Buffer,
layout::Rect,
text::StyledGrapheme,
widgets::Widget,
};
use crate::text::{
Line,
Text,
};
#[derive(Debug, Default)]
pub struct Scroll {
pub y: Option<usize>,
pub x: Option<usize>,
}
pub struct ScrollbackBuffer {
pub lines: Vec<Line>,
pub current: Text,
pub scroll: Scroll,
pub wrap: bool,
pub last_height: usize,
pub render_tx: mpsc::Sender<Instant>,
}
impl ScrollbackBuffer {
pub fn new(render_tx: mpsc::Sender<Instant>) -> Self {
ScrollbackBuffer {
lines: vec![],
current: Text::new(),
scroll: Default::default(),
last_height: 20,
wrap: true,
render_tx,
}
}
pub fn append_lines(this: Arc<Mutex<Self>>, mut lines: mpsc::Receiver<(Vec<Line>, Text)>) {
tokio::spawn(async move {
while let Some(line) = lines.next().await {
let mut this = this.lock().await;
this.extend(line);
let _ = this.render_tx.send(Instant::now()).await;
}
});
}
#[instrument(level = "trace", skip(self))]
pub async fn handle_user_event(&mut self, event: &Event) -> bool {
match event {
Event::Key(KeyEvent {
code,
modifiers: KeyModifiers::NONE,
}) => match code {
KeyCode::PageDown => self.page_down(),
KeyCode::PageUp => self.page_up(),
_ => return false,
},
Event::Mouse(MouseEvent { kind, .. }) => match kind {
MouseEventKind::ScrollDown => self.scroll_down(1),
MouseEventKind::ScrollUp => self.scroll_up(1),
_ => return false,
},
_ => return false,
}
let _ = self.render_tx.send(Instant::now()).await;
true
}
pub fn scroll_up(&mut self, by: usize) {
let scroll_y = self.scroll.y.get_or_insert(self.lines.len() + 1);
*scroll_y = scroll_y.saturating_sub(by);
}
pub fn scroll_down(&mut self, by: usize) {
if let Some(mut y) = self.scroll.y.take() {
y += by;
if y <= self.lines.len() {
self.scroll.y = Some(y);
}
}
}
pub fn page_up(&mut self) {
self.scroll_up(self.last_height);
}
pub fn page_down(&mut self) {
self.scroll_down(self.last_height);
}
pub fn extend(&mut self, (lines, mut incomplete): (impl IntoIterator<Item = Line>, Text)) {
for line in lines {
let full_line = self.current.swap_complete(line);
self.lines.push(full_line);
}
self.current.extend(&mut incomplete)
}
pub fn widget(&self) -> ScrollbackBufferWidget {
let scroll = self.scroll.y;
ScrollbackBufferWidget {
lines: &self.lines[..scroll.unwrap_or(self.lines.len())],
last: Some(&self.current).filter(|_| scroll.is_none()),
x: if !self.wrap {
self.scroll.x.unwrap_or_default()
} else {
0
},
wrap: self.wrap,
}
}
}
pub struct ScrollbackBufferWidget<'a> {
pub lines: &'a [Line],
pub last: Option<&'a Text>,
pub x: usize,
pub wrap: bool,
}
impl<'a> Widget for ScrollbackBufferWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let max_buffer = &self.lines[self.lines.len().saturating_sub(area.height as usize)..];
let mut out_lines = vec![];
let lines = max_buffer
.iter()
.map(|l| l.spans())
.chain(self.last.iter().map(|t| t.spans()))
.map(|l| {
l.iter()
.flat_map(|s| s.styled_graphemes(Default::default()))
});
if self.wrap {
for line in lines {
let words = WordsIter::new(line).collect::<Vec<_>>();
let lines =
textwrap::wrap_algorithms::wrap_first_fit(&words, &[area.width as usize; 512]);
out_lines.extend(lines.into_iter().map(|words| {
words
.iter()
.flat_map(|word| word.graphemes.iter().cloned())
.collect::<Vec<StyledGrapheme>>()
}))
}
} else {
out_lines.extend(lines.map(Vec::from_iter));
}
let out_lines = &out_lines[out_lines.len().saturating_sub(area.height as usize)..];
let y_offset = area.height.saturating_sub(out_lines.len() as _);
for (mut y, line) in out_lines.iter().enumerate() {
y += y_offset as usize;
if y > area.height as usize {
break;
}
for (x, g) in line.iter().enumerate() {
if x >= area.width as usize {
break;
}
buf.get_mut(area.x + x as u16, area.y + y as u16)
.set_style(g.style.clone())
.set_symbol(g.symbol);
}
}
}
}
#[derive(Default, Debug, Clone)]
struct StyledWord<'a> {
graphemes: Vec<StyledGrapheme<'a>>,
word_len: usize,
trailing_ws_len: usize,
}
impl<'a> StyledWord<'a> {
fn is_complete_word(&self) -> bool {
self.graphemes.iter().any(|g| !is_whitespace(g))
&& self.graphemes.last().map(is_whitespace).unwrap_or(false)
}
fn push(&mut self, g: StyledGrapheme<'a>) {
let ws = is_whitespace(&g);
let existing_word = self.word_len > 0;
if !ws {
self.word_len += 1;
} else if existing_word {
self.trailing_ws_len += 1;
}
self.graphemes.push(g);
}
}
struct WordsIter<'a, I> {
graphemes: I,
current: Option<StyledWord<'a>>,
}
impl<'a, I> WordsIter<'a, I> {
fn new(graphemes: I) -> Self {
Self {
graphemes,
current: None,
}
}
}
impl<'a, I> Iterator for WordsIter<'a, I>
where
I: Iterator<Item = StyledGrapheme<'a>>,
{
type Item = StyledWord<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let g = if let Some(g) = self.graphemes.next() {
g
} else {
return self.current.take();
};
let whitespace = is_whitespace(&g);
let current = self.current.get_or_insert_with(Default::default);
if current.is_complete_word() {
if !whitespace {
let word = self.current.take();
let mut new = StyledWord::default();
new.push(g);
self.current = Some(new);
return word;
}
}
current.push(g);
}
}
}
fn is_whitespace(g: &StyledGrapheme) -> bool {
g.symbol
.chars()
.nth(0)
.map(|c| c.is_whitespace())
.unwrap_or(true)
}
impl<'a> Fragment for StyledWord<'a> {
fn whitespace_width(&self) -> usize {
self.trailing_ws_len
}
fn penalty_width(&self) -> usize {
0
}
fn width(&self) -> usize {
self.graphemes.len() - self.trailing_ws_len
}
}