1 use super::{ 2 visibility_blocking, CommandBlocking, CommandInfo, Component, 3 DrawableComponent, 4 }; 5 use crate::{keys::SharedKeyConfig, strings, ui, version::Version}; 6 use asyncgit::hash; 7 use crossterm::event::Event; 8 use itertools::Itertools; 9 use std::{borrow::Cow, cmp, convert::TryFrom}; 10 use tui::{ 11 backend::Backend, 12 layout::{Alignment, Constraint, Direction, Layout, Rect}, 13 style::{Modifier, Style}, 14 widgets::{Block, BorderType, Borders, Clear, Paragraph, Text}, 15 Frame, 16 }; 17 18 use anyhow::Result; 19 use ui::style::SharedTheme; 20 21 /// 22 pub struct HelpComponent { 23 cmds: Vec<CommandInfo>, 24 visible: bool, 25 selection: u16, 26 theme: SharedTheme, 27 key_config: SharedKeyConfig, 28 } 29 30 impl DrawableComponent for HelpComponent { draw<B: Backend>( &self, f: &mut Frame<B>, _rect: Rect, ) -> Result<()>31 fn draw<B: Backend>( 32 &self, 33 f: &mut Frame<B>, 34 _rect: Rect, 35 ) -> Result<()> { 36 if self.visible { 37 const SIZE: (u16, u16) = (65, 24); 38 let scroll_threshold = SIZE.1 / 3; 39 let scroll = 40 self.selection.saturating_sub(scroll_threshold); 41 42 let area = 43 ui::centered_rect_absolute(SIZE.0, SIZE.1, f.size()); 44 45 f.render_widget(Clear, area); 46 f.render_widget( 47 Block::default() 48 .title(&strings::help_title(&self.key_config)) 49 .borders(Borders::ALL) 50 .border_type(BorderType::Thick), 51 area, 52 ); 53 54 let chunks = Layout::default() 55 .vertical_margin(1) 56 .horizontal_margin(1) 57 .direction(Direction::Vertical) 58 .constraints( 59 [Constraint::Min(1), Constraint::Length(1)] 60 .as_ref(), 61 ) 62 .split(area); 63 64 f.render_widget( 65 Paragraph::new(self.get_text().iter()) 66 .scroll(scroll) 67 .alignment(Alignment::Left), 68 chunks[0], 69 ); 70 71 f.render_widget( 72 Paragraph::new( 73 vec![Text::Styled( 74 Cow::from(format!( 75 "gitui {}", 76 Version::new(), 77 )), 78 Style::default(), 79 )] 80 .iter(), 81 ) 82 .alignment(Alignment::Right), 83 chunks[1], 84 ); 85 } 86 87 Ok(()) 88 } 89 } 90 91 impl Component for HelpComponent { commands( &self, out: &mut Vec<CommandInfo>, force_all: bool, ) -> CommandBlocking92 fn commands( 93 &self, 94 out: &mut Vec<CommandInfo>, 95 force_all: bool, 96 ) -> CommandBlocking { 97 // only if help is open we have no other commands available 98 if self.visible && !force_all { 99 out.clear(); 100 } 101 102 if self.visible { 103 out.push(CommandInfo::new( 104 strings::commands::scroll(&self.key_config), 105 true, 106 true, 107 )); 108 109 out.push(CommandInfo::new( 110 strings::commands::close_popup(&self.key_config), 111 true, 112 true, 113 )); 114 } 115 116 if !self.visible || force_all { 117 out.push( 118 CommandInfo::new( 119 strings::commands::help_open(&self.key_config), 120 true, 121 true, 122 ) 123 .order(99), 124 ); 125 } 126 127 visibility_blocking(self) 128 } 129 event(&mut self, ev: Event) -> Result<bool>130 fn event(&mut self, ev: Event) -> Result<bool> { 131 if self.visible { 132 if let Event::Key(e) = ev { 133 if e == self.key_config.exit_popup { 134 self.hide() 135 } else if e == self.key_config.move_down { 136 self.move_selection(true) 137 } else if e == self.key_config.move_up { 138 self.move_selection(false) 139 } else { 140 } 141 } 142 143 Ok(true) 144 } else if let Event::Key(k) = ev { 145 if k == self.key_config.open_help { 146 self.show()?; 147 Ok(true) 148 } else { 149 Ok(false) 150 } 151 } else { 152 Ok(false) 153 } 154 } 155 is_visible(&self) -> bool156 fn is_visible(&self) -> bool { 157 self.visible 158 } 159 hide(&mut self)160 fn hide(&mut self) { 161 self.visible = false 162 } 163 show(&mut self) -> Result<()>164 fn show(&mut self) -> Result<()> { 165 self.visible = true; 166 167 Ok(()) 168 } 169 } 170 171 impl HelpComponent { new( theme: SharedTheme, key_config: SharedKeyConfig, ) -> Self172 pub const fn new( 173 theme: SharedTheme, 174 key_config: SharedKeyConfig, 175 ) -> Self { 176 Self { 177 cmds: vec![], 178 visible: false, 179 selection: 0, 180 theme, 181 key_config, 182 } 183 } 184 /// set_cmds(&mut self, cmds: Vec<CommandInfo>)185 pub fn set_cmds(&mut self, cmds: Vec<CommandInfo>) { 186 self.cmds = cmds 187 .into_iter() 188 .filter(|e| !e.text.hide_help) 189 .collect::<Vec<_>>(); 190 self.cmds.sort_by_key(|e| e.text.clone()); 191 self.cmds.dedup_by_key(|e| e.text.clone()); 192 self.cmds.sort_by_key(|e| hash(&e.text.group)); 193 } 194 move_selection(&mut self, inc: bool)195 fn move_selection(&mut self, inc: bool) { 196 let mut new_selection = self.selection; 197 198 new_selection = if inc { 199 new_selection.saturating_add(1) 200 } else { 201 new_selection.saturating_sub(1) 202 }; 203 new_selection = cmp::max(new_selection, 0); 204 205 if let Ok(max) = 206 u16::try_from(self.cmds.len().saturating_sub(1)) 207 { 208 self.selection = cmp::min(new_selection, max); 209 } 210 } 211 get_text(&self) -> Vec<Text>212 fn get_text(&self) -> Vec<Text> { 213 let mut txt = Vec::new(); 214 215 let mut processed = 0_u16; 216 217 for (key, group) in 218 &self.cmds.iter().group_by(|e| e.text.group) 219 { 220 txt.push(Text::Styled( 221 Cow::from(format!("{}\n", key)), 222 Style::default().modifier(Modifier::REVERSED), 223 )); 224 225 txt.extend( 226 group 227 .sorted_by_key(|e| e.order) 228 .map(|e| { 229 let is_selected = self.selection == processed; 230 231 processed += 1; 232 233 let mut out = String::from(if is_selected { 234 ">" 235 } else { 236 " " 237 }); 238 239 e.print(&mut out); 240 out.push('\n'); 241 242 if is_selected { 243 out.push_str( 244 format!(" {}\n", e.text.desc) 245 .as_str(), 246 ); 247 } 248 249 Text::Styled( 250 Cow::from(out), 251 self.theme.text(true, is_selected), 252 ) 253 }) 254 .collect::<Vec<_>>(), 255 ); 256 } 257 258 txt 259 } 260 } 261