Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/core/ev_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,22 @@ pub fn handle_event(
) {
match ev {
Command::SetData(text) => {
if let Some(ref mut hs) = p.help_state {
hs.screen.orig_text = text;
hs.screen.line_count = hs.screen.orig_text.lines().count();
return;
}
p.screen.orig_text = text;
p.screen.line_count = p.screen.orig_text.lines().count();
p.reformat_display();
command_queue.push_back(Command::Io(IoCommand::RedrawDisplay));
}
Command::UserInput(InputEvent::Exit) => {
if p.help_state.is_some() {
p.exit_help();
command_queue.push_back(Command::Io(IoCommand::RedrawDisplay));
return;
}
p.run_hooks(Hook::PrePagerExit);
p.exit();
is_exited.store(true, std::sync::atomic::Ordering::SeqCst);
Expand Down Expand Up @@ -141,11 +151,24 @@ pub fn handle_event(
}
}
Command::UserInput(InputEvent::RestorePrompt) => {
if p.help_state.is_some() {
p.exit_help();
command_queue.push_back(Command::Io(IoCommand::RedrawDisplay));
return;
}
// Set the message to None and new messages to false as all messages have been shown
p.message = None;
p.format_prompt();
command_queue.push_back(Command::Io(IoCommand::RedrawPrompt));
}
Command::UserInput(InputEvent::ShowHelp) => {
if p.help_state.is_some() {
p.exit_help();
} else {
p.show_help();
}
command_queue.push_back(Command::Io(IoCommand::RedrawDisplay));
}
Command::UserInput(InputEvent::UpdateTermArea(c, r)) => {
p.rows = r;
p.cols = c;
Expand Down Expand Up @@ -283,6 +306,11 @@ pub fn handle_event(
}

Command::AppendData(text) => {
if let Some(ref mut hs) = p.help_state {
hs.screen.orig_text.push_str(&text);
hs.screen.line_count = hs.screen.orig_text.lines().count();
return;
}
let prev_unterminated = p.screen.unterminated;
let prev_fmt_lines_count = p.screen.formatted_lines_count();
let append_style = p.append_str(text.as_str());
Expand Down Expand Up @@ -548,6 +576,58 @@ mod tests {
assert_eq!(ps.message.unwrap(), TEST_STR.to_string());
}

#[test]
fn show_help() {
let mut ps = PagerState::new().unwrap();
ps.screen.orig_text = "original text\n".to_string();
ps.reformat_display();
ps.upper_mark = 0;

let ev = Command::UserInput(InputEvent::ShowHelp);
let mut command_queue = CommandQueue::new_zero();

// Showing help sets the screen to the formatted help table
handle_event(
ev,
&mut ps,
&mut command_queue,
&Arc::new(AtomicBool::new(false)),
);
assert!(ps.help_state.is_some());
assert!(ps.screen.orig_text.contains("COMMAND SUMMARY"));
assert!(ps.prompt.contains("HELP"));

// Pressing help again toggles it off and restores original text
let ev2 = Command::UserInput(InputEvent::ShowHelp);
handle_event(
ev2,
&mut ps,
&mut command_queue,
&Arc::new(AtomicBool::new(false)),
);
assert!(ps.help_state.is_none());
assert_eq!(ps.screen.orig_text, "original text\n");

// Showing help then exiting with Exit returns to pager
handle_event(
Command::UserInput(InputEvent::ShowHelp),
&mut ps,
&mut command_queue,
&Arc::new(AtomicBool::new(false)),
);
assert!(ps.help_state.is_some());
let is_exited = Arc::new(AtomicBool::new(false));
handle_event(
Command::UserInput(InputEvent::Exit),
&mut ps,
&mut command_queue,
&is_exited,
);
assert!(ps.help_state.is_none());
assert_eq!(is_exited.load(std::sync::atomic::Ordering::SeqCst), false);
assert_eq!(ps.screen.orig_text, "original text\n");
}

#[test]
#[cfg(feature = "static_output")]
fn set_run_no_overflow() {
Expand Down
152 changes: 152 additions & 0 deletions src/help.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! Help text and related definitions for the pager.

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

/// Format a [`KeyEvent`] into a human-readable representation (e.g. `"Ctrl-c"`, `"Alt-h"`).
pub fn format_key(ke: &KeyEvent) -> String {
let mut s = String::new();
if ke.modifiers.contains(KeyModifiers::CONTROL) {
s.push_str("Ctrl-");
}
if ke.modifiers.contains(KeyModifiers::ALT) {
s.push_str("Alt-");
}
if ke.modifiers.contains(KeyModifiers::SHIFT) {
if let KeyCode::Char(c) = ke.code {
if !c.is_ascii_uppercase() {
s.push_str("Shift-");
}
} else {
s.push_str("Shift-");
}
}

match ke.code {
KeyCode::Char(c) => s.push(c),
KeyCode::Enter => s.push_str("Enter"),
KeyCode::Tab => s.push_str("Tab"),
KeyCode::BackTab => s.push_str("BackTab"),
KeyCode::Backspace => s.push_str("Backspace"),
KeyCode::Esc => s.push_str("Esc"),
KeyCode::Up => s.push_str("Up"),
KeyCode::Down => s.push_str("Down"),
KeyCode::Left => s.push_str("Left"),
KeyCode::Right => s.push_str("Right"),
KeyCode::PageUp => s.push_str("PageUp"),
KeyCode::PageDown => s.push_str("PageDown"),
KeyCode::Home => s.push_str("Home"),
KeyCode::End => s.push_str("End"),
KeyCode::Delete => s.push_str("Delete"),
KeyCode::Insert => s.push_str("Insert"),
KeyCode::F(n) => s.push_str(&format!("F{n}")),
KeyCode::Null => s.push_str("Null"),
_ => s.push_str("Unknown"),
}
s
}

/// Format dynamic help table from key event entries and their descriptions.
///
/// Empty descriptions are omitted.
pub fn format_help_table_from_entries<'a, I>(entries: I) -> String
where
I: IntoIterator<Item = (&'a KeyEvent, &'a str)>,
{
let mut groups: Vec<(&'a str, Vec<String>)> = Vec::new();
for (key, desc) in entries {
let trimmed_desc = desc.trim();
if trimmed_desc.is_empty() {
continue;
}
let key_str = format_key(key);
if let Some((_, keys)) = groups.iter_mut().find(|(d, _)| *d == trimmed_desc) {
if !keys.contains(&key_str) {
keys.push(key_str);
}
} else {
groups.push((trimmed_desc, vec![key_str]));
}
}

if groups.is_empty() {
return String::new();
}

let mut out = String::new();
out.push_str(" COMMAND SUMMARY\n\n");
out.push_str(" Key(s) Action\n");
out.push_str(" ------ ------\n");

for (desc, keys) in groups {
let keys_str = keys.join(", ");
out.push_str(&format!(" {:<30} {}\n", keys_str, desc));
}

out.push_str("\n -- Press q, Enter, or Alt-h to return to pager --\n");
out
}

#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyEventState;

#[test]
fn test_format_key() {
let k1 = KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: KeyEventState::NONE,
};
assert_eq!(format_key(&k1), "q");

let k2 = KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
kind: crossterm::event::KeyEventKind::Press,
state: KeyEventState::NONE,
};
assert_eq!(format_key(&k2), "Ctrl-c");

let k3 = KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::ALT,
kind: crossterm::event::KeyEventKind::Press,
state: KeyEventState::NONE,
};
assert_eq!(format_key(&k3), "Alt-Up");
}

#[test]
fn test_format_help_table_from_entries() {
let k1 = KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: KeyEventState::NONE,
};
let k2 = KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
kind: crossterm::event::KeyEventKind::Press,
state: KeyEventState::NONE,
};
let k3 = KeyEvent {
code: KeyCode::Char('x'),
modifiers: KeyModifiers::NONE,
kind: crossterm::event::KeyEventKind::Press,
state: KeyEventState::NONE,
};

let entries = vec![(&k1, "quit"), (&k2, "quit"), (&k3, "")];
let table = format_help_table_from_entries(entries);
assert!(table.contains("COMMAND SUMMARY"));
assert!(table.contains("q, Ctrl-c"));
assert!(table.contains("quit"));
assert!(!table.contains(" x "));

let empty_table = format_help_table_from_entries(Vec::<(&KeyEvent, &str)>::new());
assert!(empty_table.is_empty());
}
}
11 changes: 10 additions & 1 deletion src/input/definitions/keydefs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl KeySeq {
}
}
Token::MultipleChar(c) => {
let c = c.to_ascii_lowercase().clone();
let c = c.to_ascii_lowercase();
SPECIAL_KEYS.get(c.as_str()).map_or_else(
|| panic!("'{}': Invalid key input sequence given", text),
|key| {
Expand Down Expand Up @@ -316,4 +316,13 @@ fn test_parse_key_event() {
state: KeyEventState::NONE,
}
);
assert_eq!(
parse_key_event("m-h"),
KeyEvent {
code: KeyCode::Char('h'),
modifiers: KeyModifiers::ALT,
kind: crossterm::event::KeyEventKind::Press,
state: KeyEventState::NONE,
}
);
}
2 changes: 1 addition & 1 deletion src/input/definitions/mousedefs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fn gen_mouse_event_from_tokenlist(token_list: &[Token], text: &str) -> MouseEven
);
}
Token::MultipleChar(c) => {
let c = c.to_ascii_lowercase().clone();
let c = c.to_ascii_lowercase();
MOUSE_ACTIONS.get(c.as_str()).map_or_else(
|| panic!("'{}': Invalid key input sequence given", text),
|k| {
Expand Down
Loading
Loading