Skip to content
Merged

Hooks #162

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
9 changes: 8 additions & 1 deletion src/core/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::fmt::Debug;

use crate::{
ExitStrategy, LineNumbers,
hooks::{Hook, HookCallback},
input::{InputClassifier, InputEvent},
minus_core::utils::display::AppendStyle,
};
Expand Down Expand Up @@ -53,6 +54,8 @@ pub enum Command {
SetExitStrategy(ExitStrategy),
SetInputClassifier(Box<dyn InputClassifier + Send + Sync + 'static>),
AddExitCallback(Box<dyn FnMut() + Send + Sync + 'static>),
AddHook(Hook, u64, HookCallback),
RemoveHook(Hook, u64),
#[cfg(feature = "static_output")]
SetRunNoOverflow(bool),
#[cfg(feature = "search")]
Expand All @@ -75,7 +78,9 @@ impl PartialEq for Command {
#[cfg(feature = "static_output")]
(Self::SetRunNoOverflow(d1), Self::SetRunNoOverflow(d2)) => d1 == d2,
(Self::SetInputClassifier(_), Self::SetInputClassifier(_))
| (Self::AddExitCallback(_), Self::AddExitCallback(_)) => true,
| (Self::AddExitCallback(_), Self::AddExitCallback(_))
| (Self::AddHook(..), Self::AddHook(..)) => true,
(Self::RemoveHook(h1, id1), Self::RemoveHook(h2, id2)) => h1 == h2 && id1 == id2,
#[cfg(feature = "search")]
(Self::IncrementalSearchCondition(_), Self::IncrementalSearchCondition(_)) => true,
(Self::Io(a), Self::Io(b)) => a == b,
Expand All @@ -99,6 +104,8 @@ impl Debug for Command {
#[cfg(feature = "search")]
Self::IncrementalSearchCondition(_) => write!(f, "IncrementalSearchCondition"),
Self::AddExitCallback(_) => write!(f, "AddExitCallback"),
Self::AddHook(h, id, _) => write!(f, "AddHook({h:?}, {id})"),
Self::RemoveHook(h, id) => write!(f, "RemoveHook({h:?}, {id})"),
#[cfg(feature = "static_output")]
Self::SetRunNoOverflow(val) => write!(f, "SetRunNoOverflow({val:?})"),
Self::UserInput(input) => write!(f, "UserInput({input:?})"),
Expand Down
45 changes: 27 additions & 18 deletions src/core/ev_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ use parking_lot::{Condvar, Mutex};
use super::CommandQueue;
use super::commands::{Command, IoCommand};
use super::utils::display::{self, AppendStyle};
use crate::ExitStrategy;
#[cfg(feature = "search")]
use crate::search;
use crate::{PagerState, error::MinusError, input::InputEvent};
use crate::{PagerState, error::MinusError, hooks::Hook, input::InputEvent};

/// Respond based on the type of command
///
Expand All @@ -36,6 +37,7 @@ pub fn handle_event(
command_queue.push_back(Command::Io(IoCommand::RedrawDisplay));
}
Command::UserInput(InputEvent::Exit) => {
p.run_hooks(Hook::PrePagerExit);
p.exit();
is_exited.store(true, std::sync::atomic::Ordering::SeqCst);
}
Expand Down Expand Up @@ -228,7 +230,21 @@ pub fn handle_event(
p.format_lines();
command_queue.push_back(Command::Io(IoCommand::RedrawDisplay));
}
Command::SetExitStrategy(es) => p.exit_strategy = es,
Command::SetExitStrategy(es) => {
p.hooks.remove_callback(Hook::PostPagerExit, 1);
if es == ExitStrategy::ProcessQuit {
p.hooks.add_callback(
Hook::PostPagerExit,
1,
Box::new(|_| {
std::process::exit(1);
}),
);
} else {
p.hooks
.add_callback(Hook::PostPagerExit, 1, Box::new(|_| {}));
}
}
Command::LineWrapping(lw) => {
p.screen.line_wrapping = lw;
p.format_lines();
Expand All @@ -239,6 +255,10 @@ pub fn handle_event(
Command::IncrementalSearchCondition(cb) => p.search_state.incremental_search_condition = cb,
Command::SetInputClassifier(clf) => p.input_classifier = clf,
Command::AddExitCallback(cb) => p.exit_callbacks.push(cb),
Command::AddHook(hook, id, cb) => p.hooks.add_callback(hook, id, cb),
Command::RemoveHook(hook, id) => {
p.hooks.remove_callback(hook, id);
}
Command::ShowPrompt(show) => p.show_prompt = show,
Command::FollowOutput(follow_output)
| Command::UserInput(InputEvent::FollowOutput(follow_output)) => {
Expand Down Expand Up @@ -278,6 +298,10 @@ pub fn handle_io_command(
}
IoCommand::SetUpperMark(mut um) => {
display::draw_for_change(out, p, &mut um)?;
let line_count = p.screen.formatted_lines_count();
if um >= line_count.saturating_sub(p.rows.saturating_sub(1)) && line_count > p.rows {
p.run_hooks(Hook::EofReached);
}
p.upper_mark = um;
}
IoCommand::DrawAppendedText(prev_unterminated, prev_fmt_lines_count, append_style) => {
Expand Down Expand Up @@ -346,7 +370,7 @@ pub fn handle_io_command(
mod tests {
use super::super::commands::Command;
use super::handle_event;
use crate::{ExitStrategy, PagerState, minus_core::CommandQueue};
use crate::{PagerState, minus_core::CommandQueue};
use std::sync::{Arc, atomic::AtomicBool};

const TEST_STR: &str = "This is some sample text";
Expand Down Expand Up @@ -442,21 +466,6 @@ mod tests {
assert!(!ps.run_no_overflow);
}

#[test]
fn set_exit_strategy() {
let mut ps = PagerState::new().unwrap();
let ev = Command::SetExitStrategy(ExitStrategy::PagerQuit);
let mut command_queue = CommandQueue::new_zero();

handle_event(
ev,
&mut ps,
&mut command_queue,
&Arc::new(AtomicBool::new(false)),
);
assert_eq!(ps.exit_strategy, ExitStrategy::PagerQuit);
}

#[test]
fn add_exit_callback() {
let mut ps = PagerState::new().unwrap();
Expand Down
17 changes: 9 additions & 8 deletions src/core/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use crate::{
Pager, PagerState,
error::MinusError,
hooks::Hook,
input::InputEvent,
minus_core::{
RunMode,
Expand Down Expand Up @@ -93,6 +94,7 @@ pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusErr
#[allow(unused_mut)]
let mut ps = crate::state::PagerState::generate_initial_state(&pager.rx)?;
*super::RUNMODE.lock() = rm;
ps.run_hooks(Hook::PrePagerStart);

// Static mode checks
#[cfg(all(feature = "static_output", not(test)))]
Expand Down Expand Up @@ -135,11 +137,7 @@ pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusErr

// While silently ignoring error is considered a bad practice, we are forced to do it here
// as we cannot use the ? and panicking here will (probably?) cause an immediate abort
drop(term::cleanup(
&mut out2,
&crate::ExitStrategy::PagerQuit,
true,
));
drop(term::cleanup(&mut out2, true));
panic_hook(pinfo);
}));
}
Expand Down Expand Up @@ -198,7 +196,7 @@ pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusErr

if r1.is_err() || r2.is_err() {
*RUNMODE.lock() = RunMode::Uninitialized;
term::cleanup(&mut out, &crate::ExitStrategy::PagerQuit, true)?;
term::cleanup(&mut out, true)?;
}

r1?;
Expand Down Expand Up @@ -233,6 +231,7 @@ fn start_reactor(
let mut p = ps.lock();

draw_full(&mut out_lock, &mut p)?;
p.run_hooks(Hook::PostPagerStart);

if p.follow_output {
draw_for_change(&mut out_lock, &mut p, &mut (usize::MAX - 1))?;
Expand All @@ -244,7 +243,8 @@ fn start_reactor(
#[cfg(feature = "dynamic_output")]
RunMode::Dynamic => loop {
if is_exited.load(Ordering::SeqCst) {
term::cleanup(&mut out_lock, &ps.lock().exit_strategy, true)?;
term::cleanup(&mut out_lock, true)?;
ps.lock().run_hooks(Hook::PostPagerExit);
let mut rm = RUNMODE.lock();
*rm = RunMode::Uninitialized;
drop(rm);
Expand Down Expand Up @@ -280,7 +280,8 @@ fn start_reactor(
// Cleanup the screen
//
// This is not needed in dynamic paging because this is already handled by handle_event
term::cleanup(&mut out_lock, &ps.lock().exit_strategy, true)?;
term::cleanup(&mut out_lock, true)?;
ps.lock().run_hooks(Hook::PostPagerExit);

let mut rm = RUNMODE.lock();
*rm = RunMode::Uninitialized;
Expand Down
8 changes: 1 addition & 7 deletions src/core/utils/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ pub fn setup(out: &mut io::Stdout) -> std::result::Result<(), SetupError> {
/// [raw mode]: ../../../crossterm/terminal/index.html#raw-mode
pub fn cleanup(
out: &mut impl io::Write,
es: &crate::ExitStrategy,
cleanup_screen: bool,
) -> std::result::Result<(), CleanupError> {
if cleanup_screen {
Expand All @@ -68,12 +67,7 @@ pub fn cleanup(
execute!(out, terminal::LeaveAlternateScreen)
.map_err(|e| CleanupError::LeaveAlternateScreen(e.into()))?;
}

if *es == crate::ExitStrategy::ProcessQuit {
std::process::exit(0);
} else {
Ok(())
}
Ok(())
}

/// Moves the terminal cursor to given x, y coordinates
Expand Down
80 changes: 80 additions & 0 deletions src/hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! Manages and runs callbacks for events happening in minus.
//!
//! ## Note on Thread Blacking
//!
//! Callbacks registered for hooks are run on the same thread as the pager.
//! This means that if you add a long-running task in a callback, it will block the pager
//! from rendering, scrolling and responding to events. Hence you should avoid adding
//! long-running tasks in callbacks. If you have a long running task, you should run it on a
//! separate thread.

use std::collections::HashMap;

use crate::PagerState;

/// Events that can have callbacks registered
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
pub enum Hook {
/// Fired just before the terminal UI is drawn (before switching to the alternate screen).
PrePagerStart,
/// Fired after the terminal UI is drawn with text.
PostPagerStart,
/// Fired when the user hits the end of the page
EofReached,
/// Fired just before the pager exits due to [`InputEvent::Exit`](crate::input::InputEvent::Exit).
PrePagerExit,
/// Fired after the terminal UI is cleared up and main screen is restored.
///
/// For this hook, start your IDs from 2 because 1 is occupied for the
/// [ExitStrategy](crate::ExitStrategy).
PostPagerExit,
}

/// A callback that can be executed on a hook
pub type HookCallback = Box<dyn FnMut(&PagerState) + Send + Sync + 'static>;

/// Stores callbacks for all hooks
#[derive(Default)]
pub(crate) struct Hooks {
hooks: HashMap<Hook, Vec<(u64, HookCallback)>>,
next_id: u64,
}

impl Hooks {
#[must_use]
pub(crate) fn new() -> Self {
Self::default()
}

pub(crate) fn add_callback(&mut self, hook: Hook, mut id: u64, cb: HookCallback) {
if id == 0 {
id = self.next_id;
self.next_id += 1;
}

let callbacks = self.hooks.entry(hook).or_default();
assert!(
!callbacks.iter().any(|(cb_id, _)| *cb_id == id),
"Callback ID {id} already exists for hook {hook:?}"
);
callbacks.push((id, cb));
}

pub(crate) fn remove_callback(&mut self, hook: Hook, id: u64) -> bool {
if let Some(cbs) = self.hooks.get_mut(&hook)
&& let Some(pos) = cbs.iter().position(|(cb_id, _)| *cb_id == id)
{
_ = cbs.remove(pos);
return true;
}
false
}

pub(crate) fn run_hooks(&mut self, hook: Hook, pager_state: &PagerState) {
if let Some(cbs) = self.hooks.get_mut(&hook) {
for (_, cb) in cbs {
cb(pager_state);
}
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
#[cfg(feature = "dynamic_output")]
mod dynamic_pager;
pub mod error;
pub mod hooks;
pub mod input;
#[path = "core/mod.rs"]
mod minus_core;
Expand Down
35 changes: 34 additions & 1 deletion src/pager.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
//! Proivdes the [Pager] type

use crate::{ExitStrategy, LineNumbers, error::MinusError, input, minus_core::commands::Command};
use crate::{
ExitStrategy, LineNumbers,
error::MinusError,
hooks::{Hook, HookCallback},
input,
minus_core::commands::Command,
};
use crossbeam_channel::{Receiver, Sender};
use std::fmt;

Expand Down Expand Up @@ -306,6 +312,33 @@ impl Pager {
Ok(self.tx.send(Command::AddExitCallback(cb))?)
}

/// Add a function to be called when a specific [`Hook`] is triggered
///
/// The `id` parameter is a unique identifier for the callback. If you don't care about the
/// `id`, pass `0` and minus will automatically assign a unique ID.
///
/// # Panics
/// This function will panic if a callback with the same `id` is already registered for the
/// given `hook`.
///
/// # Errors
/// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
/// could not be sent to the receiver
pub fn add_hook(&self, hook: Hook, id: u64, cb: HookCallback) -> Result<(), MinusError> {
Ok(self.tx.send(Command::AddHook(hook, id, cb))?)
}

/// Remove a callback
///
/// This function will return `false` if the callback is not found.
///
/// # Errors
/// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
/// could not be sent to the receiver
pub fn remove_hook(&self, hook: Hook, id: u64) -> Result<(), MinusError> {
Ok(self.tx.send(Command::RemoveHook(hook, id))?)
}

/// Override the condition for running incremental search
///
/// See [Incremental Search](../search/index.html#incremental-search) to know more on how this
Expand Down
Loading
Loading