Skip to content

Commit

Permalink
Migrate to Ratatui (#42)
Browse files Browse the repository at this point in the history
* Migrate to Ratatui

Ratatui is a replacement for tui-rs that is actively maintained.
See https://ratatui.rs/ for more info.

* fix: only respond to key press events

Crossterm 0.26+ reports key release events on windows, which results in
the application seeing multiple events for a single key press.

See ratatui/ratatui#347 for more info.

Fixes #30
  • Loading branch information
joshka committed Dec 31, 2023
1 parent 15c3e63 commit fe09917
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 133 deletions.
233 changes: 178 additions & 55 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ edition = "2021"

[dependencies]
csv = "1.2"
tui = "0.19"
crossterm = { version = "0.26.1", features = ["use-dev-tty"] }
ratatui = "0.25.0"
crossterm = { version = "0.27.0", features = ["use-dev-tty"] }
anyhow = "1.0"
clap = { version = "4.2", features = ["derive"] }
tempfile = "3.5"
regex = "1.8"
csv-sniffer = "0.3.1"

[target.'cfg(windows)'.dependencies]
crossterm = "0.25"
crossterm = "0.27.0"

# The profile that 'cargo dist' will build with
[profile.dist]
Expand Down
14 changes: 7 additions & 7 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use crate::ui::{CsvTable, CsvTableState, FilterColumnsState, FinderState};
use crate::view;

use anyhow::ensure;
use tui::backend::Backend;
use tui::{Frame, Terminal};
use ratatui::backend::Backend;
use ratatui::{Frame, Terminal};

use anyhow::{Context, Result};
use regex::Regex;
Expand Down Expand Up @@ -532,7 +532,7 @@ impl App {
}
}

fn render_frame<B: Backend>(&mut self, f: &mut Frame<B>) {
fn render_frame(&mut self, f: &mut Frame) {
let size = f.size();

// Render help; if so exit early.
Expand Down Expand Up @@ -573,16 +573,16 @@ mod tests {
use std::thread;

use super::*;
use tui::backend::TestBackend;
use tui::buffer::Buffer;
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;

fn to_lines(buf: &Buffer) -> Vec<String> {
let mut symbols: String = "".to_owned();
let area = buf.area();
for y in 0..area.bottom() {
for x in 0..area.right() {
let symbol = buf.get(x, y).symbol.clone();
symbols.push_str(&symbol);
let symbol = buf.get(x, y).symbol();
symbols.push_str(symbol);
}
if y != area.bottom() - 1 {
symbols.push('\n');
Expand Down
8 changes: 4 additions & 4 deletions src/help.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use tui::{
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Span, Spans},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, StatefulWidget, Widget, Wrap},
};

Expand Down Expand Up @@ -118,9 +118,9 @@ impl StatefulWidget for HelpPage {
}
}

let text: Vec<Spans> = HELP_CONTENT
let text: Vec<Line> = HELP_CONTENT
.split('\n')
.map(|s| Spans::from(line_to_span(s)))
.map(|s| Line::from(line_to_span(s)))
.collect();

// Minus 2 to account for borders.
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::panic;
use std::thread::panicking;
use tempfile::NamedTempFile;
use tui::backend::CrosstermBackend;
use tui::Terminal;

struct SeekableFile {
filename: Option<String>,
Expand Down
37 changes: 18 additions & 19 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ use crate::input::InputMode;
use crate::view;
use crate::view::Header;
use crate::wrap;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::symbols::line;
use ratatui::text::{Line, Span};
use ratatui::widgets::Widget;
use ratatui::widgets::{Block, Borders, StatefulWidget};
use regex::Regex;
use tui::buffer::Buffer;
use tui::layout::Rect;
use tui::style::{Color, Modifier, Style};
use tui::symbols::line;
use tui::text::{Span, Spans};
use tui::widgets::Widget;
use tui::widgets::{Block, Borders, StatefulWidget};

use std::cmp::{max, min};
use std::collections::HashMap;
Expand Down Expand Up @@ -457,32 +457,31 @@ impl<'a> CsvTable<'a> {
NUM_SPACES_BETWEEN_COLUMNS
} as usize;

let mut spans_wrapper = wrap::SpansWrapper::new(spans, effective_width as usize);
let mut line_wrapper = wrap::LineWrapper::new(spans, effective_width as usize);

for offset in 0..height {
if let Some(mut spans) = spans_wrapper.next() {
if let Some(mut line) = line_wrapper.next() {
// There is some content to render. Truncate with ... if there is no more vertical
// space available.
if offset == height - 1 && !spans_wrapper.finished() {
if let Some(last_span) = spans.0.pop() {
if offset == height - 1 && !line_wrapper.finished() {
if let Some(last_span) = line.spans.pop() {
let truncate_length = last_span.width().saturating_sub(SUFFIX_LEN as usize);
let truncated_content: String =
last_span.content.chars().take(truncate_length).collect();
let truncated_span = Span::styled(truncated_content, last_span.style);
spans.0.push(truncated_span);
spans.0.push(Span::styled(SUFFIX, last_span.style));
line.spans.push(truncated_span);
line.spans.push(Span::styled(SUFFIX, last_span.style));
}
}
let padding_width = min(
(effective_width as usize).saturating_sub(spans.width()) + buffer_space,
(effective_width as usize).saturating_sub(line.width()) + buffer_space,
width as usize,
);
if padding_width > 0 {
spans
.0
line.spans
.push(Span::styled(" ".repeat(padding_width), filler_style.style));
}
buf.set_spans(x, y + offset, &spans, width);
buf.set_line(x, y + offset, &line, width);
} else {
// There are extra vertical spaces that are just empty lines. Fill them with the
// correct style.
Expand All @@ -491,15 +490,15 @@ impl<'a> CsvTable<'a> {

// It's possible that no spans are yielded due to insufficient remaining width.
// Render ... in this case.
if !spans_wrapper.finished() {
if !line_wrapper.finished() {
let truncated_content: String = content
.chars()
.take(content.len().saturating_sub(1))
.collect();
content = format!("{SUFFIX}{}", truncated_content.as_str());
}
let span = Span::styled(content, filler_style.style);
buf.set_spans(x, y + offset, &Spans::from(vec![span]), width);
buf.set_line(x, y + offset, &Line::from(vec![span]), width);
}
}
}
Expand Down
16 changes: 7 additions & 9 deletions src/util/events.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
use std::time::{Duration, Instant};

use crossterm::{
event::{poll, read, Event, KeyCode, KeyEvent},
ErrorKind,
};
use crossterm::event::{poll, read, Event, KeyCode, KeyEvent, KeyEventKind};

pub enum CsvlensEvent<I> {
Input(I),
Expand Down Expand Up @@ -42,19 +39,20 @@ impl CsvlensEvents {
}
}

pub fn next(&self) -> Result<CsvlensEvent<KeyEvent>, ErrorKind> {
pub fn next(&self) -> std::io::Result<CsvlensEvent<KeyEvent>> {
let now = Instant::now();
match poll(self.tick_rate) {
Ok(true) => {
if let Event::Key(event) = read()? {
Ok(true) => match read()? {
Event::Key(event) if event.kind == KeyEventKind::Press => {
Ok(CsvlensEvent::Input(event))
} else {
}
_ => {
let time_spent = now.elapsed();
let rest = self.tick_rate - time_spent;

Self { tick_rate: rest }.next()
}
}
},
Ok(false) => Ok(CsvlensEvent::Tick),
Err(_) => todo!(),
}
Expand Down
68 changes: 34 additions & 34 deletions src/wrap.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
use tui::text::{Span, Spans};
use ratatui::text::{Line, Span};

pub struct SpansWrapper<'a> {
pub struct LineWrapper<'a> {
spans: &'a [Span<'a>],
max_width: usize,
index: usize,
pending: Option<Span<'a>>,
}

impl<'a> SpansWrapper<'a> {
impl<'a> LineWrapper<'a> {
pub fn new(spans: &'a [Span<'a>], max_width: usize) -> Self {
SpansWrapper {
LineWrapper {
spans,
max_width,
index: 0,
pending: None,
}
}

pub fn next(&mut self) -> Option<Spans<'a>> {
pub fn next(&mut self) -> Option<Line<'a>> {
let mut out_spans = vec![];
let mut remaining_width = self.max_width;
loop {
Expand Down Expand Up @@ -70,7 +70,7 @@ impl<'a> SpansWrapper<'a> {
if out_spans.is_empty() {
return None;
}
Some(Spans::from(out_spans))
Some(Line::from(out_spans))
}

pub fn finished(&self) -> bool {
Expand All @@ -82,47 +82,47 @@ impl<'a> SpansWrapper<'a> {
mod tests {

use super::*;
use tui::style::{Color, Style};
use ratatui::style::{Color, Style};

#[test]
fn test_no_wrapping() {
let s = Span::raw("hello");
let spans = vec![s.clone()];
let mut wrapper = SpansWrapper::new(&spans, 10);
assert_eq!(wrapper.next(), Some(Spans::from(vec![s.clone()])));
let mut wrapper = LineWrapper::new(&spans, 10);
assert_eq!(wrapper.next(), Some(Line::from(vec![s.clone()])));
assert_eq!(wrapper.next(), None);
}

#[test]
fn test_with_wrapping() {
let s = Span::raw("hello");
let spans = vec![s.clone()];
let mut wrapper = SpansWrapper::new(&spans, 2);
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("he")])));
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("ll")])));
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("o")])));
let mut wrapper = LineWrapper::new(&spans, 2);
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("he")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("ll")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("o")])));
assert_eq!(wrapper.next(), None);
}

#[test]
fn test_new_lines_before_max_width() {
let s = Span::raw("hello\nworld");
let spans = vec![s.clone()];
let mut wrapper = SpansWrapper::new(&spans, 10);
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("hello")])));
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("world")])));
let mut wrapper = LineWrapper::new(&spans, 10);
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("hello")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("world")])));
assert_eq!(wrapper.next(), None);
}

#[test]
fn test_new_lines_after_max_width() {
let s = Span::raw("hello\nworld");
let spans = vec![s.clone()];
let mut wrapper = SpansWrapper::new(&spans, 3);
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("hel")])));
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("lo")])));
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("wor")])));
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("ld")])));
let mut wrapper = LineWrapper::new(&spans, 3);
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("hel")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("lo")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("wor")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("ld")])));
assert_eq!(wrapper.next(), None);
}

Expand All @@ -134,16 +134,16 @@ mod tests {
Span::styled("my", style),
Span::raw("world"),
];
let mut wrapper = SpansWrapper::new(&spans, 5);
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("hello")])));
let mut wrapper = LineWrapper::new(&spans, 5);
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("hello")])));
assert_eq!(
wrapper.next(),
Some(Spans::from(vec![
Some(Line::from(vec![
Span::styled("my", style),
Span::raw("wor")
]))
);
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("ld")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("ld")])));
assert_eq!(wrapper.next(), None);
}

Expand All @@ -155,31 +155,31 @@ mod tests {
Span::styled("m\ny", style),
Span::raw("world"),
];
let mut wrapper = SpansWrapper::new(&spans, 5);
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("hello")])));
let mut wrapper = LineWrapper::new(&spans, 5);
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("hello")])));
assert_eq!(
wrapper.next(),
Some(Spans::from(vec![Span::styled("m", style)]))
Some(Line::from(vec![Span::styled("m", style)]))
);
assert_eq!(
wrapper.next(),
Some(Spans::from(vec![
Some(Line::from(vec![
Span::styled("y", style),
Span::raw("worl")
]))
);
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("d")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("d")])));
assert_eq!(wrapper.next(), None);
}

#[test]
fn test_unicode() {
let s = Span::raw("héllo");
let spans = vec![s.clone()];
let mut wrapper = SpansWrapper::new(&spans, 2);
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("hé")])));
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("ll")])));
assert_eq!(wrapper.next(), Some(Spans::from(vec![Span::raw("o")])));
let mut wrapper = LineWrapper::new(&spans, 2);
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("hé")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("ll")])));
assert_eq!(wrapper.next(), Some(Line::from(vec![Span::raw("o")])));
assert_eq!(wrapper.next(), None);
}
}

0 comments on commit fe09917

Please sign in to comment.