feat(tui): support custom terminal colors

This commit is contained in:
Orhun Parmaksız 2022-02-15 03:19:14 +03:00
parent f40b76738e
commit 95a8f2e5ef
No known key found for this signature in database
GPG key ID: F83424824B3E4B90
8 changed files with 242 additions and 65 deletions

7
Cargo.lock generated
View file

@ -100,6 +100,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "colorsys"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dfdf9179d546b55ff3f88c9d93ecfaa3e9760163da5a1080af5243230dbbb70"
[[package]]
name = "copypasta"
version = "0.7.1"
@ -734,6 +740,7 @@ dependencies = [
name = "systeroid-tui"
version = "0.1.0"
dependencies = [
"colorsys",
"copypasta-ext",
"getopts",
"systeroid-core",

View file

@ -16,6 +16,7 @@ unicode-width = "0.1.9"
thiserror = "1.0.30"
getopts = "0.2.21"
copypasta-ext = { version = "0.3.7", optional = true }
colorsys = "0.6.5"
[dependencies.systeroid-core]
version = "0.1.0"

View file

@ -1,3 +1,4 @@
use crate::color::Colors;
use getopts::Options;
use std::path::PathBuf;
use systeroid_core::sysctl::section::Section;
@ -23,6 +24,8 @@ pub struct Args {
pub section: Option<Section>,
/// Query to search on startup.
pub search_query: Option<String>,
/// Background/foreground colors.
pub colors: Colors,
/// Do not parse/show Linux kernel documentation.
pub no_docs: bool,
}
@ -45,6 +48,18 @@ impl Args {
);
opts.optopt("s", "section", "set the section to filter", "<section>");
opts.optopt("q", "query", "set the query to search", "<query>");
opts.optopt(
"",
"bg-color",
"set the background color [default: black]",
"<color>",
);
opts.optopt(
"",
"fg-color",
"set the foreground color [default: white]",
"<color>",
);
opts.optflag("n", "no-docs", "do not show the kernel documentation");
opts.optflag("h", "help", "display this help and exit");
opts.optflag("V", "version", "output version information and exit");
@ -79,6 +94,12 @@ impl Args {
kernel_docs: matches.opt_str("D").map(PathBuf::from),
section: matches.opt_str("s").map(Section::from),
search_query: matches.opt_str("q"),
colors: Colors::new(
matches.opt_str("bg-color").as_deref().unwrap_or("black"),
matches.opt_str("fg-color").as_deref().unwrap_or("white"),
)
.map_err(|e| eprintln!("error: `{}`", e))
.ok()?,
no_docs: matches.opt_present("n"),
})
}

108
systeroid-tui/src/color.rs Normal file
View file

@ -0,0 +1,108 @@
use crate::error::Result;
use colorsys::{ParseError, Rgb};
use std::result::Result as StdResult;
use std::str::FromStr;
use tui::style::Color as TuiColor;
/// Color configuration.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Colors {
/// Background color.
pub bg: Color,
/// Foreground color.
pub fg: Color,
}
impl Colors {
/// Constructs a new instance.
pub fn new(background: &str, foreground: &str) -> Result<Self> {
Ok(Self {
bg: Color::from_str(background)?,
fg: Color::from_str(foreground)?,
})
}
}
/// Wrapper for widget colors.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Color {
/// Inner type.
inner: TuiColor,
}
impl Default for Color {
fn default() -> Self {
Self {
inner: TuiColor::Reset,
}
}
}
impl Color {
/// Returns the underlying [`Color`] type.
///
/// [`Color`]: tui::style::Color
pub fn get(self) -> TuiColor {
self.inner
}
}
impl FromStr for Color {
type Err = ParseError;
fn from_str(s: &str) -> StdResult<Self, Self::Err> {
Ok(Self {
inner: match s.to_lowercase().as_ref() {
"reset" => TuiColor::Reset,
"black" => TuiColor::Black,
"red" => TuiColor::Red,
"green" => TuiColor::Green,
"yellow" => TuiColor::Yellow,
"blue" => TuiColor::Blue,
"magenta" => TuiColor::Magenta,
"cyan" => TuiColor::Cyan,
"gray" => TuiColor::Gray,
"darkgray" => TuiColor::DarkGray,
"lightred" => TuiColor::LightRed,
"lightgreen" => TuiColor::LightGreen,
"lightyellow" => TuiColor::LightYellow,
"lightblue" => TuiColor::LightBlue,
"lightmagenta" => TuiColor::LightMagenta,
"lightcyan" => TuiColor::LightCyan,
"white" => TuiColor::White,
_ => {
let rgb = Rgb::from_hex_str(&format!("#{}", s))?;
TuiColor::Rgb(rgb.red() as u8, rgb.green() as u8, rgb.blue() as u8)
}
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color() -> Result<()> {
assert_eq!(TuiColor::Reset, Color::default().get());
assert_eq!(TuiColor::Gray, Color::from_str("gray")?.get());
assert_eq!(TuiColor::Black, Color::from_str("black")?.get());
assert_eq!(TuiColor::Green, Color::from_str("green")?.get());
assert_eq!(
TuiColor::Rgb(152, 157, 69),
Color::from_str("989D45")?.get()
);
assert_eq!(TuiColor::Rgb(18, 49, 47), Color::from_str("12312F")?.get());
assert_eq!(
TuiColor::Rgb(255, 242, 255),
Color::from_str("FFF2FF")?.get()
);
assert_eq!(
Colors {
bg: Color::from_str("red")?,
fg: Color::from_str("blue")?,
},
Colors::new("red", "blue")?
);
Ok(())
}
}

View file

@ -12,6 +12,9 @@ pub enum Error {
/// Error that may occur while getting/setting the contents of the clipboard.
#[error("Clipboard error: `{0}`")]
ClipboardError(String),
/// Error that may occur while parsing a color.
#[error(transparent)]
ColorParseError(#[from] colorsys::ParseError),
/// Error that may occur in the core library.
#[error("{0}")]
SysctlError(#[from] systeroid_core::error::Error),

View file

@ -6,6 +6,8 @@
pub mod app;
/// Command-line argument parser.
pub mod args;
/// Color helper.
pub mod color;
/// Application commands.
pub mod command;
/// Error implementation.
@ -57,7 +59,7 @@ pub fn run<B: Backend>(args: Args, backend: B) -> Result<()> {
app.input = None;
}
while app.running {
terminal.draw(|frame| ui::render(frame, &mut app))?;
terminal.draw(|frame| ui::render(frame, &mut app, &args.colors))?;
match event_handler.next()? {
Event::KeyPress(key) => {
let command = Command::parse(key, app.is_input_mode());

View file

@ -1,15 +1,16 @@
use crate::app::{App, KeyBinding, HELP_TEXT};
use crate::color::Colors;
use crate::widgets::SelectableList;
use tui::backend::Backend;
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use tui::style::{Color, Style};
use tui::style::Style;
use tui::text::{Span, Text};
use tui::widgets::{Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table, Wrap};
use tui::Frame;
use unicode_width::UnicodeWidthStr;
/// Renders the user interface.
pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App) {
pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App, colors: &Colors) {
let documentation = app
.parameter_list
.selected()
@ -32,9 +33,9 @@ pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App) {
[Constraint::Percentage(100), Constraint::Min(0)]
})
.split(chunks[0]);
render_parameter_list(frame, chunks[0], app);
render_parameter_list(frame, chunks[0], app, colors);
if app.input.is_some() {
render_input_prompt(frame, chunks[1], rect.height - 2, app);
render_input_prompt(frame, chunks[1], rect.height - 2, app, colors);
}
}
if let Some(documentation) = documentation {
@ -43,15 +44,21 @@ pub fn render<B: Backend>(frame: &mut Frame<'_, B>, app: &mut App) {
chunks[1],
documentation,
&mut app.docs_scroll_amount,
colors,
);
}
if app.show_help {
render_help_text(frame, rect, &mut app.key_bindings);
render_help_text(frame, rect, &mut app.key_bindings, colors);
}
}
/// Renders the list that contains the sysctl parameters.
fn render_parameter_list<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, app: &mut App) {
fn render_parameter_list<B: Backend>(
frame: &mut Frame<'_, B>,
rect: Rect,
app: &mut App,
colors: &Colors,
) {
let max_width = app
.parameter_list
.items
@ -65,17 +72,17 @@ fn render_parameter_list<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, app:
Row::new(if minimize_rows {
vec![Cell::from(Span::styled(
format!("{} = {}", item.name, item.value),
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
))]
} else {
vec![
Cell::from(Span::styled(
item.name.clone(),
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
)),
Cell::from(Span::styled(
item.value.clone(),
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
)),
]
})
@ -88,15 +95,15 @@ fn render_parameter_list<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, app:
Block::default()
.title(Span::styled(
"Parameters",
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
))
.title_alignment(Alignment::Left)
.borders(Borders::all())
.border_style(Style::default().fg(Color::White))
.border_style(Style::default().fg(colors.fg.get()))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Black)),
.style(Style::default().bg(colors.bg.get())),
)
.highlight_style(Style::default().bg(Color::White).fg(Color::Black))
.highlight_style(Style::default().bg(colors.fg.get()).fg(colors.bg.get()))
.widths(&if minimize_rows {
[Constraint::Percentage(100), Constraint::Min(0)]
} else {
@ -117,17 +124,23 @@ fn render_parameter_list<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, app:
.unwrap_or(0),
app.parameter_list.items.len()
),
colors,
);
if let Some(section) = app.section_list.selected() {
render_section_text(frame, rect, section);
render_section_text(frame, rect, section, colors);
}
if let Some(options) = app.options.as_mut() {
render_options_menu(frame, rect, options);
render_options_menu(frame, rect, options, colors);
}
}
/// Renders the text for displaying the selected index.
fn render_selection_text<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, selection_text: String) {
fn render_selection_text<B: Backend>(
frame: &mut Frame<'_, B>,
rect: Rect,
selection_text: String,
colors: &Colors,
) {
let selection_text_width = u16::try_from(selection_text.width()).unwrap_or_default();
if let Some(horizontal_area_width) = rect.width.checked_sub(selection_text_width + 2) {
let vertical_area = Layout::default()
@ -158,7 +171,7 @@ fn render_selection_text<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, selec
Paragraph::new(selection_text).block(
Block::default()
.borders(Borders::NONE)
.style(Style::default().bg(Color::White).fg(Color::Black)),
.style(Style::default().bg(colors.fg.get()).fg(colors.bg.get())),
),
horizontal_area[1],
);
@ -167,7 +180,7 @@ fn render_selection_text<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, selec
Paragraph::new(Text::default()).block(
Block::default()
.borders(Borders::NONE)
.style(Style::default().bg(Color::Black)),
.style(Style::default().bg(colors.bg.get())),
),
horizontal_area[2],
);
@ -175,7 +188,12 @@ fn render_selection_text<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, selec
}
/// Renders the text for displaying the parameter section.
fn render_section_text<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, section: &str) {
fn render_section_text<B: Backend>(
frame: &mut Frame<'_, B>,
rect: Rect,
section: &str,
colors: &Colors,
) {
let section = format!("|{}|", section);
let text_width: u16 = section.width().try_into().unwrap_or(1);
let vertical_area = Layout::default()
@ -205,10 +223,10 @@ fn render_section_text<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, section
.split(vertical_area[0]);
frame.render_widget(Clear, area[1]);
frame.render_widget(
Paragraph::new(Span::styled(section, Style::default().fg(Color::White))).block(
Paragraph::new(Span::styled(section, Style::default().fg(colors.fg.get()))).block(
Block::default()
.borders(Borders::NONE)
.style(Style::default().bg(Color::Black)),
.style(Style::default().bg(colors.bg.get())),
),
area[1],
);
@ -219,6 +237,7 @@ fn render_help_text<B: Backend>(
frame: &mut Frame<'_, B>,
rect: Rect,
key_bindings: &mut SelectableList<&KeyBinding>,
colors: &Colors,
) {
let (percent_x, percent_y) = (50, 50);
let popup_layout = Layout::default()
@ -264,26 +283,32 @@ fn render_help_text<B: Backend>(
.split(rect);
frame.render_widget(Clear, area[0]);
frame.render_widget(
Paragraph::new(Text::styled(HELP_TEXT, Style::default().fg(Color::White)))
.block(
Block::default()
.title(Span::styled("About", Style::default().fg(Color::White)))
.title_alignment(Alignment::Center)
.borders(Borders::all())
.border_style(Style::default().fg(Color::White))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Black)),
)
.alignment(Alignment::Center)
.wrap(Wrap { trim: false }),
Paragraph::new(Text::styled(
HELP_TEXT,
Style::default().fg(colors.fg.get()),
))
.block(
Block::default()
.title(Span::styled("About", Style::default().fg(colors.fg.get())))
.title_alignment(Alignment::Center)
.borders(Borders::all())
.border_style(Style::default().fg(colors.fg.get()))
.border_type(BorderType::Rounded)
.style(Style::default().bg(colors.bg.get())),
)
.alignment(Alignment::Center)
.wrap(Wrap { trim: false }),
area[0],
);
frame.render_widget(Clear, area[1]);
frame.render_stateful_widget(
Table::new(key_bindings.items.iter().map(|item| {
Row::new(vec![
Cell::from(Span::styled(item.key, Style::default().fg(Color::White))),
Cell::from(Span::styled(item.action, Style::default().fg(Color::White))),
Cell::from(Span::styled(item.key, Style::default().fg(colors.fg.get()))),
Cell::from(Span::styled(
item.action,
Style::default().fg(colors.fg.get()),
)),
])
.height(1)
.bottom_margin(0)
@ -292,15 +317,15 @@ fn render_help_text<B: Backend>(
Block::default()
.title(Span::styled(
"Key Bindings",
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
))
.title_alignment(Alignment::Center)
.borders(Borders::all())
.border_style(Style::default().fg(Color::White))
.border_style(Style::default().fg(colors.fg.get()))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Black)),
.style(Style::default().bg(colors.bg.get())),
)
.highlight_style(Style::default().bg(Color::White).fg(Color::Black))
.highlight_style(Style::default().bg(colors.fg.get()).fg(colors.bg.get()))
.widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]),
area[1],
&mut key_bindings.state,
@ -312,6 +337,7 @@ fn render_options_menu<B: Backend>(
frame: &mut Frame<'_, B>,
rect: Rect,
options: &mut SelectableList<&str>,
colors: &Colors,
) {
let (length_x, length_y) = (
25,
@ -348,7 +374,7 @@ fn render_options_menu<B: Backend>(
Table::new(options.items.iter().map(|item| {
Row::new(vec![Cell::from(Span::styled(
item.to_string(),
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
))])
.height(1)
.bottom_margin(0)
@ -357,15 +383,15 @@ fn render_options_menu<B: Backend>(
Block::default()
.title(Span::styled(
"Copy to clipboard",
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
))
.title_alignment(Alignment::Center)
.borders(Borders::all())
.border_style(Style::default().fg(Color::White))
.border_style(Style::default().fg(colors.fg.get()))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Black)),
.style(Style::default().bg(colors.bg.get())),
)
.highlight_style(Style::default().bg(Color::White).fg(Color::Black))
.highlight_style(Style::default().bg(colors.fg.get()).fg(colors.bg.get()))
.widths(&[Constraint::Percentage(100)]),
rect,
&mut options.state,
@ -378,6 +404,7 @@ fn render_parameter_documentation<B: Backend>(
rect: Rect,
documentation: String,
scroll_amount: &mut u16,
colors: &Colors,
) {
match (documentation.lines().count() * 2).checked_sub(rect.height.into()) {
Some(scroll_overflow) => {
@ -392,19 +419,19 @@ fn render_parameter_documentation<B: Backend>(
frame.render_widget(
Paragraph::new(Text::styled(
documentation,
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
))
.block(
Block::default()
.title(Span::styled(
"Documentation",
Style::default().fg(Color::White),
Style::default().fg(colors.fg.get()),
))
.title_alignment(Alignment::Center)
.borders(Borders::all())
.border_style(Style::default().fg(Color::White))
.border_style(Style::default().fg(colors.fg.get()))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Black)),
.style(Style::default().bg(colors.bg.get())),
)
.scroll((*scroll_amount, 0))
.wrap(Wrap { trim: false }),
@ -413,7 +440,13 @@ fn render_parameter_documentation<B: Backend>(
}
/// Renders the input prompt for running commands.
fn render_input_prompt<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, cursor_y: u16, app: &App) {
fn render_input_prompt<B: Backend>(
frame: &mut Frame<'_, B>,
rect: Rect,
cursor_y: u16,
app: &App,
colors: &Colors,
) {
let text = match app.input.clone() {
Some(mut input) => {
if app.input_time.is_some() {
@ -447,12 +480,12 @@ fn render_input_prompt<B: Backend>(frame: &mut Frame<'_, B>, rect: Rect, cursor_
None => String::new(),
};
frame.render_widget(
Paragraph::new(Span::styled(text, Style::default().fg(Color::White))).block(
Paragraph::new(Span::styled(text, Style::default().fg(colors.fg.get()))).block(
Block::default()
.borders(Borders::all())
.border_style(Style::default().fg(Color::White))
.border_style(Style::default().fg(colors.fg.get()))
.border_type(BorderType::Rounded)
.style(Style::default().bg(Color::Black)),
.style(Style::default().bg(colors.bg.get())),
),
rect,
);

View file

@ -6,6 +6,7 @@ use systeroid_core::sysctl::controller::Sysctl;
use systeroid_core::sysctl::parameter::Parameter;
use systeroid_core::sysctl::section::Section;
use systeroid_tui::app::App;
use systeroid_tui::color::Colors;
use systeroid_tui::command::Command;
use systeroid_tui::error::Result;
use systeroid_tui::options::{Direction, ScrollArea};
@ -61,9 +62,10 @@ fn test_render_tui() -> Result<()> {
config: Config::default(),
};
let mut app = App::new(&mut sysctl);
let colors = Colors::default();
let backend = TestBackend::new(40, 10);
let mut terminal = Terminal::new(backend)?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──────────────────────|all|─╮",
@ -83,7 +85,7 @@ fn test_render_tui() -> Result<()> {
app.run_command(Command::Help)?;
app.run_command(Command::Scroll(ScrollArea::List, Direction::Down, 1))?;
app.run_command(Command::Scroll(ScrollArea::List, Direction::Up, 1))?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──────────────────────|all|─╮",
@ -102,7 +104,7 @@ fn test_render_tui() -> Result<()> {
app.run_command(Command::Select)?;
app.run_command(Command::Select)?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──────────────────────|all|─╮",
@ -128,7 +130,7 @@ fn test_render_tui() -> Result<()> {
.chars()
.try_for_each(|c| app.run_command(Command::UpdateInput(c)))?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──────────────────────|all|─╮",
@ -146,7 +148,7 @@ fn test_render_tui() -> Result<()> {
)?;
app.run_command(Command::ProcessInput)?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──────────────────────|all|─╮",
@ -165,7 +167,7 @@ fn test_render_tui() -> Result<()> {
thread::sleep(Duration::from_millis(2000));
app.tick();
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──────────────────────|all|─╮",
@ -185,7 +187,7 @@ fn test_render_tui() -> Result<()> {
app.run_command(Command::Search)?;
app.run_command(Command::Cancel)?;
app.run_command(Command::Copy)?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──────────────────────|all|─╮",
@ -205,7 +207,7 @@ fn test_render_tui() -> Result<()> {
app.run_command(Command::Scroll(ScrollArea::List, Direction::Down, 1))?;
app.run_command(Command::Scroll(ScrollArea::List, Direction::Up, 1))?;
app.run_command(Command::Select)?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──────────────────────|all|─╮",
@ -226,7 +228,7 @@ fn test_render_tui() -> Result<()> {
app.tick();
app.run_command(Command::Scroll(ScrollArea::Section, Direction::Left, 1))?;
app.run_command(Command::Scroll(ScrollArea::Section, Direction::Left, 1))?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters─────────────────────|user|─╮",
@ -248,7 +250,7 @@ fn test_render_tui() -> Result<()> {
app.input = Some(String::new());
app.run_command(Command::Search)?;
app.run_command(Command::UpdateInput('_'))?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──|all|─╮╭──Documentation───╮",
@ -273,7 +275,7 @@ fn test_render_tui() -> Result<()> {
5,
))?;
app.run_command(Command::Scroll(ScrollArea::Documentation, Direction::Up, 1))?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──|all|─╮╭──Documentation───╮",
@ -297,7 +299,7 @@ fn test_render_tui() -> Result<()> {
app.run_command(Command::Scroll(ScrollArea::List, Direction::Down, 1))?;
app.run_command(Command::Scroll(ScrollArea::List, Direction::Down, 2))?;
app.run_command(Command::Refresh)?;
terminal.draw(|frame| render(frame, &mut app))?;
terminal.draw(|frame| render(frame, &mut app, &colors))?;
assert_buffer(
Buffer::with_lines(vec![
"╭Parameters──|all|─╮╭──Documentation───╮",