summaryrefslogtreecommitdiffstats
path: root/src/popups/help.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/popups/help.rs')
-rw-r--r--src/popups/help.rs249
1 files changed, 249 insertions, 0 deletions
diff --git a/src/popups/help.rs b/src/popups/help.rs
new file mode 100644
index 00000000..716216f0
--- /dev/null
+++ b/src/popups/help.rs
@@ -0,0 +1,249 @@
+use crate::components::{
+ visibility_blocking, CommandBlocking, CommandInfo, Component,
+ DrawableComponent, EventState,
+};
+use crate::{
+ app::Environment,
+ keys::{key_match, SharedKeyConfig},
+ strings, ui,
+ version::Version,
+};
+use anyhow::Result;
+use asyncgit::hash;
+use crossterm::event::Event;
+use itertools::Itertools;
+use ratatui::{
+ backend::Backend,
+ layout::{Alignment, Constraint, Direction, Layout, Rect},
+ style::{Modifier, Style},
+ text::{Line, Span},
+ widgets::{Block, BorderType, Borders, Clear, Paragraph},
+ Frame,
+};
+use std::{borrow::Cow, cmp};
+use ui::style::SharedTheme;
+
+///
+pub struct HelpPopup {
+ cmds: Vec<CommandInfo>,
+ visible: bool,
+ selection: u16,
+ theme: SharedTheme,
+ key_config: SharedKeyConfig,
+}
+
+impl DrawableComponent for HelpPopup {
+ fn draw<B: Backend>(
+ &self,
+ f: &mut Frame<B>,
+ _rect: Rect,
+ ) -> Result<()> {
+ if self.visible {
+ const SIZE: (u16, u16) = (65, 24);
+ let scroll_threshold = SIZE.1 / 3;
+ let scroll =
+ self.selection.saturating_sub(scroll_threshold);
+
+ let area =
+ ui::centered_rect_absolute(SIZE.0, SIZE.1, f.size());
+
+ f.render_widget(Clear, area);
+ f.render_widget(
+ Block::default()
+ .title(strings::help_title(&self.key_config))
+ .borders(Borders::ALL)
+ .border_type(BorderType::Thick),
+ area,
+ );
+
+ let chunks = Layout::default()
+ .vertical_margin(1)
+ .horizontal_margin(1)
+ .direction(Direction::Vertical)
+ .constraints(
+ [Constraint::Min(1), Constraint::Length(1)]
+ .as_ref(),
+ )
+ .split(area);
+
+ f.render_widget(
+ Paragraph::new(self.get_text())
+ .scroll((scroll, 0))
+ .alignment(Alignment::Left),
+ chunks[0],
+ );
+
+ f.render_widget(
+ Paragraph::new(Line::from(vec![Span::styled(
+ Cow::from(format!("gitui {}", Version::new(),)),
+ Style::default(),
+ )]))
+ .alignment(Alignment::Right),
+ chunks[1],
+ );
+ }
+
+ Ok(())
+ }
+}
+
+impl Component for HelpPopup {
+ fn commands(
+ &self,
+ out: &mut Vec<CommandInfo>,
+ force_all: bool,
+ ) -> CommandBlocking {
+ // only if help is open we have no other commands available
+ if self.visible && !force_all {
+ out.clear();
+ }
+
+ if self.visible {
+ out.push(CommandInfo::new(
+ strings::commands::scroll(&self.key_config),
+ true,
+ true,
+ ));
+
+ out.push(CommandInfo::new(
+ strings::commands::close_popup(&self.key_config),
+ true,
+ true,
+ ));
+ }
+
+ if !self.visible || force_all {
+ out.push(
+ CommandInfo::new(
+ strings::commands::help_open(&self.key_config),
+ true,
+ true,
+ )
+ .order(99),
+ );
+ }
+
+ visibility_blocking(self)
+ }
+
+ fn event(&mut self, ev: &Event) -> Result<EventState> {
+ if self.visible {
+ if let Event::Key(e) = ev {
+ if key_match(e, self.key_config.keys.exit_popup) {
+ self.hide();
+ } else if key_match(e, self.key_config.keys.move_down)
+ {
+ self.move_selection(true);
+ } else if key_match(e, self.key_config.keys.move_up) {
+ self.move_selection(false);
+ }
+ }
+
+ Ok(EventState::Consumed)
+ } else if let Event::Key(k) = ev {
+ if key_match(k, self.key_config.keys.open_help) {
+ self.show()?;
+ Ok(EventState::Consumed)
+ } else {
+ Ok(EventState::NotConsumed)
+ }
+ } else {
+ Ok(EventState::NotConsumed)
+ }
+ }
+
+ fn is_visible(&self) -> bool {
+ self.visible
+ }
+
+ fn hide(&mut self) {
+ self.visible = false;
+ }
+
+ fn show(&mut self) -> Result<()> {
+ self.visible = true;
+
+ Ok(())
+ }
+}
+
+impl HelpPopup {
+ pub fn new(env: &Environment) -> Self {
+ Self {
+ cmds: vec![],
+ visible: false,
+ selection: 0,
+ theme: env.theme.clone(),
+ key_config: env.key_config.clone(),
+ }
+ }
+ ///
+ pub fn set_cmds(&mut self, cmds: Vec<CommandInfo>) {
+ self.cmds = cmds
+ .into_iter()
+ .filter(|e| !e.text.hide_help)
+ .collect::<Vec<_>>();
+ self.cmds.sort_by_key(|e| e.text.clone());
+ self.cmds.dedup_by_key(|e| e.text.clone());
+ self.cmds.sort_by_key(|e| hash(&e.text.group));
+ }
+
+ fn move_selection(&mut self, inc: bool) {
+ let mut new_selection = self.selection;
+
+ new_selection = if inc {
+ new_selection.saturating_add(1)
+ } else {
+ new_selection.saturating_sub(1)
+ };
+ new_selection = cmp::max(new_selection, 0);
+
+ if let Ok(max) =
+ u16::try_from(self.cmds.len().saturating_sub(1))
+ {
+ self.selection = cmp::min(new_selection, max);
+ }
+ }
+
+ fn get_text(&self) -> Vec<Line> {
+ let mut txt: Vec<Line> = Vec::new();
+
+ let mut processed = 0_u16;
+
+ for (key, group) in
+ &self.cmds.iter().group_by(|e| e.text.group)
+ {
+ txt.push(Line::from(Span::styled(
+ Cow::from(key.to_string()),
+ Style::default().add_modifier(Modifier::REVERSED),
+ )));
+
+ for command_info in group {
+ let is_selected = self.selection == processed;
+
+ processed += 1;
+
+ txt.push(Line::from(Span::styled(
+ Cow::from(if is_selected {
+ format!(">{}", command_info.text.name)
+ } else {
+ format!(" {}", command_info.text.name)
+ }),
+ self.theme.text(true, is_selected),
+ )));
+
+ if is_selected {
+ txt.push(Line::from(Span::styled(
+ Cow::from(format!(
+ " {}\n",
+ command_info.text.desc
+ )),
+ self.theme.text(true, is_selected),
+ )));
+ }
+ }
+ }
+
+ txt
+ }
+}