//! Git Interactive Rebase Tool - Display Module //! //! # Description //! This module is used to handle working with the terminal display. //! //! ## Test Utilities //! To facilitate testing the usages of this crate, a set of testing utilities are provided. Since //! these utilities are not tested, and often are optimized for developer experience than //! performance should only be used in test code. mod color_mode; mod crossterm; mod display_color; mod error; mod size; mod tui; mod utils; use ::crossterm::style::{Color, Colors}; use self::utils::register_selectable_color_pairs; pub(crate) use self::{ color_mode::ColorMode, crossterm::CrossTerm, display_color::DisplayColor, error::DisplayError, size::Size, tui::Tui, }; use crate::config::Theme; /// A high level interface to the terminal display. #[derive(Debug)] pub(crate) struct Display { action_break: (Colors, Colors), action_drop: (Colors, Colors), action_edit: (Colors, Colors), action_exec: (Colors, Colors), action_fixup: (Colors, Colors), action_label: (Colors, Colors), action_merge: (Colors, Colors), action_pick: (Colors, Colors), action_reset: (Colors, Colors), action_reword: (Colors, Colors), action_squash: (Colors, Colors), action_update_ref: (Colors, Colors), tui: T, diff_add: (Colors, Colors), diff_change: (Colors, Colors), diff_context: (Colors, Colors), diff_remove: (Colors, Colors), diff_whitespace: (Colors, Colors), indicator: (Colors, Colors), normal: (Colors, Colors), } impl Display { /// Create a new display instance. pub(crate) fn new(tui: T, theme: &Theme) -> Self { let color_mode = tui.get_color_mode(); let normal = register_selectable_color_pairs( color_mode, theme.color_foreground, theme.color_background, theme.color_selected_background, ); let indicator = register_selectable_color_pairs( color_mode, theme.color_indicator, theme.color_background, theme.color_selected_background, ); let action_break = register_selectable_color_pairs( color_mode, theme.color_action_break, theme.color_background, theme.color_selected_background, ); let action_drop = register_selectable_color_pairs( color_mode, theme.color_action_drop, theme.color_background, theme.color_selected_background, ); let action_edit = register_selectable_color_pairs( color_mode, theme.color_action_edit, theme.color_background, theme.color_selected_background, ); let action_exec = register_selectable_color_pairs( color_mode, theme.color_action_exec, theme.color_background, theme.color_selected_background, ); let action_fixup = register_selectable_color_pairs( color_mode, theme.color_action_fixup, theme.color_background, theme.color_selected_background, ); let action_pick = register_selectable_color_pairs( color_mode, theme.color_action_pick, theme.color_background, theme.color_selected_background, ); let action_reword = register_selectable_color_pairs( color_mode, theme.color_action_reword, theme.color_background, theme.color_selected_background, ); let action_squash = register_selectable_color_pairs( color_mode, theme.color_action_squash, theme.color_background, theme.color_selected_background, ); let action_label = register_selectable_color_pairs( color_mode, theme.color_action_label, theme.color_background, theme.color_selected_background, ); let action_reset = register_selectable_color_pairs( color_mode, theme.color_action_reset, theme.color_background, theme.color_selected_background, ); let action_merge = register_selectable_color_pairs( color_mode, theme.color_action_merge, theme.color_background, theme.color_selected_background, ); let action_update_ref = register_selectable_color_pairs( color_mode, theme.color_action_update_ref, theme.color_background, theme.color_selected_background, ); let diff_add = register_selectable_color_pairs( color_mode, theme.color_diff_add, theme.color_background, theme.color_selected_background, ); let diff_change = register_selectable_color_pairs( color_mode, theme.color_diff_change, theme.color_background, theme.color_selected_background, ); let diff_remove = register_selectable_color_pairs( color_mode, theme.color_diff_remove, theme.color_background, theme.color_selected_background, ); let diff_context = register_selectable_color_pairs( color_mode, theme.color_diff_context, theme.color_background, theme.color_selected_background, ); let diff_whitespace = register_selectable_color_pairs( color_mode, theme.color_diff_whitespace, theme.color_background, theme.color_selected_background, ); Self { action_break, action_drop, action_edit, action_exec, action_fixup, action_label, action_merge, action_pick, action_reset, action_reword, action_squash, action_update_ref, tui, diff_add, diff_change, diff_context, diff_remove, diff_whitespace, indicator, normal, } } /// Draws a string of text to the terminal interface. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn draw_str(&mut self, s: &str) -> Result<(), DisplayError> { self.tui.print(s) } /// Clear the terminal interface and reset any style and color attributes. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn clear(&mut self) -> Result<(), DisplayError> { self.color(DisplayColor::Normal, false)?; self.set_style(false, false, false)?; self.tui.reset() } /// Force a refresh of the terminal interface. This normally should be called after after all /// text has been drawn to the terminal interface. This is considered a slow operation, so /// should be called only as needed. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn refresh(&mut self) -> Result<(), DisplayError> { self.tui.flush() } /// Set the color of text drawn to the terminal interface. This will only change text drawn to /// the terminal after this function call. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn color(&mut self, color: DisplayColor, selected: bool) -> Result<(), DisplayError> { self.tui.set_color( if selected { match color { DisplayColor::ActionBreak => self.action_break.1, DisplayColor::ActionDrop => self.action_drop.1, DisplayColor::ActionEdit => self.action_edit.1, DisplayColor::ActionExec => self.action_exec.1, DisplayColor::ActionFixup => self.action_fixup.1, DisplayColor::ActionPick => self.action_pick.1, DisplayColor::ActionReword => self.action_reword.1, DisplayColor::ActionSquash => self.action_squash.1, DisplayColor::ActionLabel => self.action_label.1, DisplayColor::ActionReset => self.action_reset.1, DisplayColor::ActionMerge => self.action_merge.1, DisplayColor::ActionUpdateRef => self.action_update_ref.1, DisplayColor::Normal => self.normal.1, DisplayColor::IndicatorColor => self.indicator.1, DisplayColor::DiffAddColor => self.diff_add.1, DisplayColor::DiffRemoveColor => self.diff_remove.1, DisplayColor::DiffChangeColor => self.diff_change.1, DisplayColor::DiffContextColor => self.diff_context.1, DisplayColor::DiffWhitespaceColor => self.diff_whitespace.1, } } else { match color { DisplayColor::ActionBreak => self.action_break.0, DisplayColor::ActionDrop => self.action_drop.0, DisplayColor::ActionEdit => self.action_edit.0, DisplayColor::ActionExec => self.action_exec.0, DisplayColor::ActionFixup => self.action_fixup.0, DisplayColor::ActionPick => self.action_pick.0, DisplayColor::ActionReword => self.action_reword.0, DisplayColor::ActionSquash => self.action_squash.0, DisplayColor::ActionLabel => self.action_label.0, DisplayColor::ActionReset => self.action_reset.0, DisplayColor::ActionMerge => self.action_merge.0, DisplayColor::ActionUpdateRef => self.action_update_ref.0, DisplayColor::Normal => self.normal.0, DisplayColor::IndicatorColor => self.indicator.0, DisplayColor::DiffAddColor => self.diff_add.0, DisplayColor::DiffRemoveColor => self.diff_remove.0, DisplayColor::DiffChangeColor => self.diff_change.0, DisplayColor::DiffContextColor => self.diff_context.0, DisplayColor::DiffWhitespaceColor => self.diff_whitespace.0, } }, ) } /// Set the style attributes of text drawn to the terminal interface. This will only change text /// drawn to the terminal after this function call. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn set_style(&mut self, dim: bool, underline: bool, reverse: bool) -> Result<(), DisplayError> { self.set_dim(dim)?; self.set_underline(underline)?; self.set_reverse(reverse) } /// Get the width and height of the terminal interface. This can be a slow operation, so should /// not be called unless absolutely needed. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn get_window_size(&self) -> Size { self.tui.get_size() } /// Reset the cursor position to the start of the line. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn ensure_at_line_start(&mut self) -> Result<(), DisplayError> { self.tui.move_to_column(0) } /// Move the cursor position `right` characters from the end of the line. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn move_from_end_of_line(&mut self, right: u16) -> Result<(), DisplayError> { let width = self.get_window_size().width().try_into().unwrap_or(u16::MAX); self.tui.move_to_column(width - right) } /// Move the cursor to the next line. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn next_line(&mut self) -> Result<(), DisplayError> { self.tui.move_next_line() } /// Start the terminal interface interactions. This should be called before any terminal /// interactions are performed. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn start(&mut self) -> Result<(), DisplayError> { self.tui.start()?; self.tui.flush() } /// End the terminal interface interactions. This should be called after all terminal /// interactions are complete. This resets the terminal interface to the default state, and /// should be called on program exit. /// /// # Errors /// Will error if the underlying terminal interface is in an error state. pub(crate) fn end(&mut self) -> Result<(), DisplayError> { self.tui.end()?; self.tui.flush() } fn set_dim(&mut self, on: bool) -> Result<(), DisplayError> { self.tui.set_dim(on) } fn set_underline(&mut self, on: bool) -> Result<(), DisplayError> { self.tui.set_underline(on) } fn set_reverse(&mut self, on: bool) -> Result<(), DisplayError> { self.tui.set_reverse(on) } } #[cfg(test)] mod tests { use ::crossterm::style::Color as CrosstermColor; use rstest::rstest; use super::*; use crate::test_helpers::mocks; fn create_theme() -> Theme { Theme::new_with_config(None).unwrap() } #[test] fn draw_str() { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.draw_str("Test String").unwrap(); assert_eq!(display.tui.get_output(), &["Test String"]); } #[test] fn clear() { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.draw_str("Test String").unwrap(); display.set_dim(true).unwrap(); display.set_reverse(true).unwrap(); display.set_underline(true).unwrap(); display.clear().unwrap(); assert!(display.tui.get_output().is_empty()); assert!(!display.tui.is_dimmed()); assert!(!display.tui.is_reverse()); assert!(!display.tui.is_underline()); } #[test] fn refresh() { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.refresh().unwrap(); assert!(!display.tui.is_dirty()); } #[rstest] #[case::action_break(DisplayColor::ActionBreak, false, CrosstermColor::White, CrosstermColor::Reset)] #[case::action_break_selected( DisplayColor::ActionBreak, true, CrosstermColor::White, CrosstermColor::AnsiValue(237) )] #[case::action_drop(DisplayColor::ActionDrop, false, CrosstermColor::Red, CrosstermColor::Reset)] #[case::action_drop_selected(DisplayColor::ActionDrop, true, CrosstermColor::Red, CrosstermColor::AnsiValue(237))] #[case::action_edit(DisplayColor::ActionEdit, false, CrosstermColor::Blue, CrosstermColor::Reset)] #[case::action_edit_selected(DisplayColor::ActionEdit, true, CrosstermColor::Blue, CrosstermColor::AnsiValue(237))] #[case::action_exec(DisplayColor::ActionExec, false, CrosstermColor::White, CrosstermColor::Reset)] #[case::action_exec_selected( DisplayColor::ActionExec, true, CrosstermColor::White, CrosstermColor::AnsiValue(237) )] #[case::action_fixup(DisplayColor::ActionFixup, false, CrosstermColor::Magenta, CrosstermColor::Reset)] #[case::action_fixup_selected( DisplayColor::ActionFixup, true, CrosstermColor::Magenta, CrosstermColor::AnsiValue(237) )] #[case::action_pick(DisplayColor::ActionPick, false, CrosstermColor::Green, CrosstermColor::Reset)] #[case::action_pick_selected( DisplayColor::ActionPick, true, CrosstermColor::Green, CrosstermColor::AnsiValue(237) )] #[case::action_reword(DisplayColor::ActionReword, false, CrosstermColor::Yellow, CrosstermColor::Reset)] #[case::action_reword_selected( DisplayColor::ActionReword, true, CrosstermColor::Yellow, CrosstermColor::AnsiValue(237) )] #[case::action_squash(DisplayColor::ActionSquash, false, CrosstermColor::Cyan, CrosstermColor::Reset)] #[case::action_squash_selected( DisplayColor::ActionSquash, true, CrosstermColor::Cyan, CrosstermColor::AnsiValue(237) )] #[case::action_label(DisplayColor::ActionLabel, false, CrosstermColor::DarkYellow, CrosstermColor::Reset)] #[case::action_label_selected( DisplayColor::ActionLabel, true, CrosstermColor::DarkYellow, CrosstermColor::AnsiValue(237) )] #[case::action_reset(DisplayColor::ActionReset, false, CrosstermColor::DarkYellow, CrosstermColor::Reset)] #[case::action_reset_selected( DisplayColor::ActionReset, true, CrosstermColor::DarkYellow, CrosstermColor::AnsiValue(237) )] #[case::action_merge(DisplayColor::ActionMerge, false, CrosstermColor::DarkYellow, CrosstermColor::Reset)] #[case::action_merge_selected( DisplayColor::ActionMerge, true, CrosstermColor::DarkYellow, CrosstermColor::AnsiValue(237) )] #[case::action_update_ref( DisplayColor::ActionUpdateRef, false, CrosstermColor::DarkMagenta, CrosstermColor::Reset )] #[case::action_update_ref_selected( DisplayColor::ActionUpdateRef, true, CrosstermColor::DarkMagenta, CrosstermColor::AnsiValue(237) )] #[case::normal(DisplayColor::Normal, false, CrosstermColor::Reset, CrosstermColor::Reset)] #[case::normal_selected(DisplayColor::Normal, true, CrosstermColor::Reset, CrosstermColor::AnsiValue(237))] #[case::indicator(DisplayColor::IndicatorColor, false, CrosstermColor::Cyan, CrosstermColor::Reset)] #[case::indicator_selected( DisplayColor::IndicatorColor, true, CrosstermColor::Cyan, CrosstermColor::AnsiValue(237) )] #[case::diff_add(DisplayColor::DiffAddColor, false, CrosstermColor::Green, CrosstermColor::Reset)] #[case::diff_add_selected( DisplayColor::DiffAddColor, true, CrosstermColor::Green, CrosstermColor::AnsiValue(237) )] #[case::diff_remove(DisplayColor::DiffRemoveColor, false, CrosstermColor::Red, CrosstermColor::Reset)] #[case::diff_remove_selected( DisplayColor::DiffRemoveColor, true, CrosstermColor::Red, CrosstermColor::AnsiValue(237) )] #[case::diff_change(DisplayColor::DiffChangeColor, false, CrosstermColor::Yellow, CrosstermColor::Reset)] #[case::diff_change_selected( DisplayColor::DiffChangeColor, true, CrosstermColor::Yellow, CrosstermColor::AnsiValue(237) )] #[case::diff_context(DisplayColor::DiffContextColor, false, CrosstermColor::White, CrosstermColor::Reset)] #[case::diff_context_selected( DisplayColor::DiffContextColor, true, CrosstermColor::White, CrosstermColor::AnsiValue(237) )] #[case::diff_whitespace( DisplayColor::DiffWhitespaceColor, false, CrosstermColor::DarkGrey, CrosstermColor::Reset )] #[case::diff_whitespace_selected( DisplayColor::DiffWhitespaceColor, true, CrosstermColor::DarkGrey, CrosstermColor::AnsiValue(237) )] fn color( #[case] display_color: DisplayColor, #[case] selected: bool, #[case] expected_foreground: CrosstermColor, #[case] expected_background: CrosstermColor, ) { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.color(display_color, selected).unwrap(); assert!( display .tui .is_colors_enabled(Colors::new(expected_foreground, expected_background)) ); } #[rstest] #[case::all_off(false, false, false)] #[case::reverse(false, false, true)] #[case::underline(false, true, false)] #[case::underline_reverse(false, true, true)] #[case::dim(true, false, false)] #[case::dim_reverse(true, false, true)] #[case::dim_underline(true, true, false)] #[case::all_on(true, true, true)] fn style(#[case] dim: bool, #[case] underline: bool, #[case] reverse: bool) { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.set_style(dim, underline, reverse).unwrap(); assert_eq!(display.tui.is_dimmed(), dim); assert_eq!(display.tui.is_underline(), underline); assert_eq!(display.tui.is_reverse(), reverse); } #[test] fn get_window_size() { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.tui.set_size(Size::new(12, 10)); assert_eq!(display.get_window_size(), Size::new(12, 10)); } #[test] fn ensure_at_line_start() { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.ensure_at_line_start().unwrap(); assert_eq!(display.tui.get_position(), (0, 0)); } #[test] fn move_from_end_of_line() { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.tui.set_size(Size::new(20, 10)); display.move_from_end_of_line(5).unwrap(); // character after the 15th character (0-indexed) assert_eq!(display.tui.get_position(), (15, 0)); } #[test] fn start() { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.start().unwrap(); assert_eq!(display.tui.get_state(), mocks::CrosstermMockState::Normal); } #[test] fn end() { let mut display = Display::new(mocks::CrossTerm::new(), &create_theme()); display.end().unwrap(); assert_eq!(display.tui.get_state(), mocks::CrosstermMockState::Ended); } }