Skip to content

Commit

Permalink
Added support for ANSI color in watched command output lines
Browse files Browse the repository at this point in the history
  • Loading branch information
fritzrehde committed Nov 6, 2023
1 parent 4037368 commit 1e65308
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 35 deletions.
84 changes: 84 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ ranges = "0.3.3"
# TODO: maybe we don't need all tokio and futures features, try to reduce
tokio = { version = "1.33.0", features = ["full"] }
futures = "0.3.29"
ansi-to-tui = "3.1.0"
strip-ansi-escapes = "0.2.0"

# Config for 'cargo dist'
[workspace.metadata.dist]
Expand Down
7 changes: 3 additions & 4 deletions examples/fields.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ watched-command = """
printf \
"id,col1,col2,col3
123,v1,v2,v3
456,v4,v5,v6"
456,v4,v5,v6
789,v7,v8,v9"
"""
interval = 5.0
header-lines = 1
header-lines = 2
field-separator = ","
fields = "1,3-4"

[keybindings]
8 changes: 5 additions & 3 deletions src/config/fields/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ impl Fields {
}
}

/// Format a string as a table that has its fields separated by an elastic
/// tabstop, and only displays the fields that should be selected.
/// Only applies any formatting if a separator or selection is present.
pub trait TableFormatter {
/// Format a string as a table that has its fields separated by an elastic
/// tabstop, and only displays the fields that should be selected.
/// Only applies any formatting if a field separator or a field selection
/// are present.
/// Returns `None` if no formatting was applied.
fn format_as_table(&self, fields: &Fields) -> Result<Option<String>>;
}

Expand Down
13 changes: 10 additions & 3 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,20 @@ enum BlockingState {
BlockedExecutingTUISubcommand,
}

/// Draws the UI. Prevents code duplication, because making this a method would
/// require borrowing self completely, which causes borrow-checker problems.
/// Clean wrapper around draw() which prevents borrow-checking problems caused
/// by mutably borrowing self.
macro_rules! draw {
($self:expr) => {
$self.tui.terminal.draw(|frame| $self.state.draw(frame))
draw(&mut $self.tui, &mut $self.state)
};
}

/// Draw state to a TUI.
fn draw(tui: &mut Tui, state: &mut State) -> Result<()> {
tui.draw(|frame| state.draw(frame))?;
Ok(())
}

/// Save all remaining operations, if there are any. Used as macro to prevent
/// borrow-checking problems.
macro_rules! save_remaining_operations {
Expand Down Expand Up @@ -222,6 +228,7 @@ impl UI {
BlockingState::BlockedExecutingTUISubcommand => {}
_ => {
draw!(self)?;
self.tui.terminal.draw(|frame| self.state.draw(frame))?;
}
};

Expand Down
48 changes: 36 additions & 12 deletions src/ui/state/lines/line.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
use ansi_to_tui::IntoText;
use anyhow::Result;
use ratatui::{style::Style, widgets::Cell};
use strip_ansi_escapes::strip_str;

pub struct Line {
/// Unformatted string that has any ANSI escape codes stripped out.
/// This string will be made available to the user's command's subshell
/// through an environment variable.
unformatted: String,
formatted: Option<String>,
style: Style,
/// Cell containing the text string that will be displayed in the TUI.
/// The text string is formatted according to the user's field separator.
/// The text string has ANSI color codes converted to ratatui colors.
displayed: Cell<'static>,
}

impl Line {
pub fn new(unformatted: String, formatted: Option<String>, style: Style) -> Self {
Self {
unformatted,
formatted,
style,
}
/// Create a new Line with a given style.
/// The formatted string was formatted according to the user's field
/// separator.
/// The unformatted and formatted strings can both contain ANSI escape
/// codes.
pub fn new(unformatted: String, formatted: Option<String>, style: Style) -> Result<Self> {
// Add one space to create separation to left frame edge.
let formatted = format!(" {}", formatted.as_ref().unwrap_or(&unformatted));

// TODO: duplicated effort: should receive stripped_ansi directly from into_text conversion

// ANSI escape codes are converted to ratatui Style's.
let formatted_colored_text = formatted.into_text()?;

// Remove ANSI escape codes from unformatted.
let unformatted_ansi_stripped = strip_str(unformatted);

Ok(Self {
unformatted: unformatted_ansi_stripped,
displayed: Cell::from(formatted_colored_text).style(style),
})
}

pub fn draw(&self) -> Cell {
let line = self.formatted.as_ref().unwrap_or(&self.unformatted);
Cell::from(" ".to_owned() + line).style(self.style)
self.displayed.clone()
}

pub fn update_style(&mut self, style: Style) {
self.style = style;
pub fn update_style(&mut self, new_style: Style) {
// TODO: ask ratatui to add a method to directly replace the style without having to clone
let displayed = self.displayed.clone();
self.displayed = displayed.style(new_style);
}

pub fn unformatted(&self) -> &String {
Expand Down
19 changes: 10 additions & 9 deletions src/ui/state/lines/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ use ratatui::{
use std::cmp::max;

pub struct Lines {
pub lines: Vec<Line>,
pub selected: Vec<bool>,
pub styles: Styles,
pub fields: Fields,
pub index_after_header_lines: usize,
pub cursor_index: Option<usize>,
lines: Vec<Line>,
selected: Vec<bool>,
styles: Styles,
fields: Fields,
index_after_header_lines: usize,
cursor_index: Option<usize>,
// TODO: deprecate in future
pub table_state: TableState,
table_state: TableState,
}

impl Lines {
Expand Down Expand Up @@ -63,7 +63,9 @@ impl Lines {
// TODO: might be better suited as a new() method or similar
pub fn update_lines(&mut self, lines: String) -> Result<()> {
let formatted: Vec<Option<String>> = match lines.as_str().format_as_table(&self.fields)? {
// All lines have formatting.
Some(formatted) => formatted.lines().map(str::to_owned).map(Some).collect(),
// No lines have formatting.
None => vec![None; lines.lines().count()],
};

Expand All @@ -75,10 +77,9 @@ impl Lines {
} else {
self.styles.line
};

Line::new(unformatted.to_owned(), formatted, style)
})
.collect();
.collect::<Result<_>>()?;

self.selected.resize(self.lines.len(), false);
self.calibrate_cursor();
Expand Down
17 changes: 13 additions & 4 deletions src/ui/terminal_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use anyhow::Result;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::{backend::CrosstermBackend, Frame};
use std::io::{stdout, Stdout};

type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Backend = CrosstermBackend<Stdout>;
type Terminal = ratatui::Terminal<Backend>;

/// A Terminal User Interface (TUI).
pub struct Tui {
Expand All @@ -29,6 +30,15 @@ impl Tui {
Ok(Terminal::new(CrosstermBackend::new(stdout()))?)
}

/// Draw provided frame to TUI.
pub fn draw<F>(&mut self, f: F) -> Result<()>
where
F: FnOnce(&mut Frame<Backend>),
{
self.terminal.draw(f)?;
Ok(())
}

/// Show the TUI.
fn show(&mut self) -> Result<()> {
enable_raw_mode()?;
Expand Down Expand Up @@ -78,8 +88,7 @@ impl Tui {
impl Drop for Tui {
fn drop(&mut self) {
if let Err(e) = self.exit() {
// TODO: one shouldn't panic in a drop impl, since a second panic would cause instant termination
panic!("Tearing down the TUI failed with: {}", e);
log::error!("Tearing down the TUI failed with: {}", e);
}
}
}

0 comments on commit 1e65308

Please sign in to comment.