Skip to main content

minus/
pager.rs

1//! Proivdes the [Pager] type
2
3use crate::{
4    ExitStrategy, LineNumbers, OutputSink,
5    error::MinusError,
6    hooks::{Hook, HookCallback},
7    input,
8    minus_core::commands::Command,
9};
10use crossbeam_channel::{Receiver, Sender};
11use std::fmt;
12
13#[cfg(feature = "clipboard")]
14use crate::state::ClipboardHandler;
15
16#[cfg(feature = "search")]
17use crate::search::SearchOpts;
18
19/// A communication bridge between the main application and the pager.
20///
21/// The [Pager] type which is a bridge between your application and running
22/// the running pager. Its the single most important type with which you will be interacting the
23/// most while working with minus. It allows you to send data, configure UI settings and also
24/// configure the key/mouse bindings.
25///
26/// You can
27/// - send data and
28/// - set configuration options
29///
30/// before or while the pager is running.
31///
32/// [`Pager`] also implements the [`std::fmt::Write`] trait which means you can directly call [`write!`] and
33/// [`writeln!`] macros on it. For example, you can easily do this
34///
35/// ```
36/// use minus::Pager;
37/// use std::fmt::Write;
38///
39/// const WHO: &str = "World";
40/// let mut pager = Pager::new();
41///
42/// // This appends `Hello World` to the end of minus's buffer
43/// writeln!(pager, "Hello {WHO}").unwrap();
44/// // which is also equivalent to writing this
45/// pager.push_str(format!("Hello {WHO}\n")).unwrap();
46#[derive(Clone)]
47pub struct Pager {
48    pub(crate) tx: Sender<Command>,
49    pub(crate) rx: Receiver<Command>,
50}
51
52impl Pager {
53    /// Initialize a new pager
54    ///
55    /// # Example
56    /// ```
57    /// let pager = minus::Pager::new();
58    /// ```
59    #[must_use]
60    pub fn new() -> Self {
61        let (tx, rx) = crossbeam_channel::unbounded();
62        Self { tx, rx }
63    }
64
65    /// Set the output text to this `t`
66    ///
67    /// Note that unlike [`Pager::push_str`], this replaces the original text.
68    /// If you want to append text, use the [`Pager::push_str`] function or the
69    /// [`write!`]/[`writeln!`] macros
70    ///
71    /// # Errors
72    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
73    /// could not be sent to the receiver
74    ///
75    /// # Example
76    /// ```
77    /// let pager = minus::Pager::new();
78    /// pager.set_text("This is a line").expect("Failed to send data to the pager");
79    /// ```
80    pub fn set_text(&self, s: impl Into<String>) -> Result<(), MinusError> {
81        Ok(self.tx.send(Command::SetData(s.into()))?)
82    }
83
84    /// Appends text to the pager output.
85    ///
86    /// You can also use [`write!`]/[`writeln!`] macros to append data to the pager.
87    /// The implementation basically calls this function internally. One difference
88    /// between using the macros and this function is that this does not require `Pager`
89    /// to be declared mutable while in order to use the macros, you need to declare
90    /// the `Pager` as mutable.
91    ///
92    /// # Errors
93    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
94    /// could not be sent to the receiver
95    ///
96    /// # Example
97    /// ```
98    /// use std::fmt::Write;
99    ///
100    /// let mut pager = minus::Pager::new();
101    /// pager.push_str("This is some text").expect("Failed to send data to the pager");
102    /// // This is same as above
103    /// write!(pager, "This is some text").expect("Failed to send data to the pager");
104    /// ```
105    pub fn push_str(&self, s: impl Into<String>) -> Result<(), MinusError> {
106        Ok(self.tx.send(Command::AppendData(s.into()))?)
107    }
108
109    /// Set line number configuration for the pager
110    ///
111    /// See [`LineNumbers`] for available options
112    ///
113    /// # Errors
114    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
115    /// could not be sent to the receiver
116    ///
117    /// # Example
118    /// ```
119    /// use minus::{Pager, LineNumbers};
120    ///
121    /// let pager = Pager::new();
122    /// pager.set_line_numbers(LineNumbers::Enabled).expect("Failed to communicate with the pager");
123    /// ```
124    pub fn set_line_numbers(&self, l: LineNumbers) -> Result<(), MinusError> {
125        Ok(self.tx.send(Command::SetLineNumbers(l))?)
126    }
127
128    /// Set the text displayed at the bottom prompt
129    ///
130    /// # Panics
131    /// This function panics if the given text contains newline characters.
132    /// This is because, the pager reserves only one line for showing the prompt
133    /// and a newline will cause it to span multiple lines, breaking the display
134    ///
135    /// # Errors
136    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
137    /// could not be sent to the receiver
138    ///
139    /// Example
140    /// ```
141    /// use minus::Pager;
142    ///
143    /// let pager = Pager::new();
144    /// pager.set_prompt("my prompt").expect("Failed to send data to the pager");
145    /// ```
146    pub fn set_prompt(&self, text: impl Into<String>) -> Result<(), MinusError> {
147        let text: String = text.into();
148        assert!(!text.contains('\n'), "Prompt cannot contain newlines");
149        Ok(self.tx.send(Command::SetPrompt(text))?)
150    }
151
152    /// Send a message to be displayed the prompt area
153    ///
154    /// The text message is temporary and will get cleared whenever the use
155    /// rdoes a action on the terminal like pressing a key or scrolling using the mouse.
156    ///
157    /// # Panics
158    /// This function panics if the given text contains newline characters.
159    /// This is because, the pager reserves only one line for showing the prompt
160    /// and a newline will cause it to span multiple lines, breaking the display
161    ///
162    /// # Errors
163    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
164    /// could not be sent to the receiver
165    ///
166    /// # Example
167    /// ```
168    /// use minus::Pager;
169    ///
170    /// let pager = Pager::new();
171    /// pager.send_message("An error occurred").expect("Failed to send data to the pager");
172    /// ```
173    pub fn send_message(&self, text: impl Into<String>) -> Result<(), MinusError> {
174        let text: String = text.into();
175        assert!(!text.contains('\n'), "Message cannot contain newlines");
176        Ok(self.tx.send(Command::SendMessage(text))?)
177    }
178
179    /// Set the default exit strategy.
180    ///
181    /// This controls how the pager will behave when the user presses `q` or `Ctrl+C`.
182    /// See [`ExitStrategy`] for available options
183    ///
184    /// # Errors
185    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
186    /// could not be sent to the receiver
187    ///
188    /// ```
189    /// use minus::{Pager, ExitStrategy};
190    ///
191    /// let pager = Pager::new();
192    /// pager.set_exit_strategy(ExitStrategy::ProcessQuit).expect("Failed to communicate with the pager");
193    /// ```
194    #[deprecated(
195        since = "5.7.0",
196        note = "Add a callback for [`PostPagerExit`](crate::hooks::Hook::PostPagerExit) hook. See [`hooks`](crate::hooks) for more info."
197    )]
198    pub fn set_exit_strategy(&self, es: ExitStrategy) -> Result<(), MinusError> {
199        Ok(self.tx.send(Command::SetExitStrategy(es))?)
200    }
201
202    /// Set whether to display pager if there's less data than
203    /// available screen height
204    ///
205    /// When this is set to false, the pager will simply print all the lines
206    /// to the main screen and immediately quit if the number of lines to
207    /// display is less than the available columns in the terminal.
208    /// Setting this to true will cause a full pager to start and display the data
209    /// even if there is less number of lines to display than available rows.
210    ///
211    /// This is only available in static output mode as the size of the data is
212    /// known beforehand.
213    /// In async output the pager can receive more data anytime
214    ///
215    /// By default this is set to false
216    ///
217    /// # Errors
218    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
219    /// could not be sent to the receiver
220    ///
221    /// ```
222    /// use minus::Pager;
223    ///
224    /// let pager = Pager::new();
225    /// pager.set_run_no_overflow(true).expect("Failed to communicate with the pager");
226    /// ```
227    #[cfg(feature = "static_output")]
228    #[cfg_attr(docsrs, doc(cfg(feature = "static_output")))]
229    pub fn set_run_no_overflow(&self, val: bool) -> Result<(), MinusError> {
230        Ok(self.tx.send(Command::SetRunNoOverflow(val))?)
231    }
232
233    /// Whether to allow scrolling horizontally
234    ///
235    /// Setting this to `true` implicitly disables line wrapping
236    ///
237    /// # Errors
238    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
239    /// could not be sent to the receiver
240    ///
241    /// ```
242    /// use minus::Pager;
243    ///
244    /// let pager = Pager::new();
245    /// pager.horizontal_scroll(true).expect("Failed to communicate with the pager");
246    /// ```
247    pub fn horizontal_scroll(&self, value: bool) -> Result<(), MinusError> {
248        Ok(self.tx.send(Command::LineWrapping(!value))?)
249    }
250
251    /// Set a custom input classifer type.
252    ///
253    /// An input classifier type is a type that implements the [`InputClassifier`]
254    /// trait. It only has one required function, [`InputClassifier::classify_input`]
255    /// which matches user input events and maps them to a [`InputEvent`]s.
256    /// When the pager encounters a user input, it calls the input classifier with
257    /// the event and [`PagerState`] as parameters.
258    ///
259    /// Previously, whenever any application wanted to change the default key/mouse bindings
260    /// they neededd to create a new type, implement the [`InputClassifier`] type by copying and
261    /// pasting the default minus's implementation of it available in the [`DefaultInputClassifier`]
262    /// and change the parts they wanted to change. This is not only unergonomic but also
263    /// extreemely prone to bugs. Hence a newer and much simpler method was developed.
264    /// This method is still allowed to avoid breaking backwards compatiblity but will be dropped
265    /// in the next major release.
266    ///
267    /// With the newer method, minus already provides a type called [`HashedEventRegister`]
268    /// which implementing the [`InputClassifier`] and is based on a
269    /// [`HashMap`] storing all the key/mouse bindings and its associated callback function.
270    /// This allows easy addition/updation/deletion of the default bindings with simple functions
271    /// like [`HashedEventRegister::add_key_events`] and [`HashedEventRegister::add_mouse_events`]
272    ///
273    /// See the [`input`] module for information about implementing it.
274    ///
275    /// # Errors
276    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
277    /// could not be sent to the receiver
278    ///
279    /// [`HashedEventRegister::add_key_events`]: input::HashedEventRegister::add_key_events
280    /// [`HashedEventRegister::add_mouse_events`]: input::HashedEventRegister::add_mouse_events
281    /// [`HashMap`]: std::collections::HashMap
282    /// [`PagerState`]: crate::state::PagerState
283    /// [`InputEvent`]: input::InputEvent
284    /// [`InputClassifier`]: input::InputClassifier
285    /// [`InputClassifier::classify_input`]: input::InputClassifier
286    /// [`HashedEventRegister`]: input::HashedEventRegister
287    /// [`DefaultInputClassifier`]: input::DefaultInputClassifier
288    pub fn set_input_classifier(
289        &self,
290        handler: Box<dyn input::InputClassifier + Send + Sync>,
291    ) -> Result<(), MinusError> {
292        Ok(self.tx.send(Command::SetInputClassifier(handler))?)
293    }
294
295    /// Set a callback that writes selected text to the clipboard.
296    ///
297    /// When set, the copy action (`y` or releasing the left mouse button over
298    /// a selection) writes the selected text through this callback instead of
299    /// creating a fresh `arboard::Clipboard` handle, so the application can
300    /// reuse an existing clipboard connection.
301    ///
302    /// # Errors
303    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
304    /// could not be sent to the receiver
305    #[cfg(feature = "clipboard")]
306    pub fn set_clipboard_handler(
307        &self,
308        handler: ClipboardHandler,
309    ) -> Result<(), MinusError> {
310        Ok(self.tx.send(Command::SetClipboardHandler(handler))?)
311    }
312
313    /// Adds a function that will be called when the user quits the pager
314    ///
315    /// Multiple functions can be stored for calling when the user quits. These functions
316    /// run sequentially in the order they were added
317    ///
318    /// # Errors
319    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
320    /// could not be sent to the receiver
321    ///
322    /// # Example
323    /// ```
324    /// use minus::Pager;
325    ///
326    /// fn hello() {
327    ///     println!("Hello");
328    /// }
329    ///
330    /// let pager = Pager::new();
331    /// pager.add_exit_callback(Box::new(hello)).expect("Failed to communicate with the pager");
332    /// ```
333    #[deprecated(
334        since = "5.7.0",
335        note = "Add a callback for [PostPagerExit](crate::hooks::Hook::PostPagerExit) hook. See [hooks](crate::hooks) for more info."
336    )]
337    pub fn add_exit_callback(
338        &self,
339        cb: Box<dyn FnMut() + Send + Sync + 'static>,
340    ) -> Result<(), MinusError> {
341        Ok(self.tx.send(Command::AddExitCallback(cb))?)
342    }
343
344    /// Add a function to be called when a specific [`Hook`] is triggered
345    ///
346    /// The `id` parameter is a unique identifier for the callback. If you don't care about the
347    /// `id`, pass `0` and minus will automatically assign a unique ID.
348    ///
349    /// # Panics
350    /// This function will panic if a callback with the same `id` is already registered for the
351    /// given `hook`.
352    ///
353    /// # Errors
354    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
355    /// could not be sent to the receiver
356    pub fn add_hook(&self, hook: Hook, id: u64, cb: HookCallback) -> Result<(), MinusError> {
357        Ok(self.tx.send(Command::AddHook(hook, id, cb))?)
358    }
359
360    /// Remove a callback
361    ///
362    /// This function will return `false` if the callback is not found.
363    ///
364    /// # Errors
365    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
366    /// could not be sent to the receiver
367    pub fn remove_hook(&self, hook: Hook, id: u64) -> Result<(), MinusError> {
368        Ok(self.tx.send(Command::RemoveHook(hook, id))?)
369    }
370
371    /// Override the condition for running incremental search
372    ///
373    /// See [Incremental Search](../search/index.html#incremental-search) to know more on how this
374    /// works
375    ///
376    /// # Errors
377    /// This function will returns a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
378    /// could not be send to the receiver end.
379    #[cfg(feature = "search")]
380    #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
381    pub fn set_incremental_search_condition(
382        &self,
383        cb: Box<dyn Fn(&SearchOpts) -> bool + Send + Sync + 'static>,
384    ) -> crate::Result {
385        self.tx.send(Command::IncrementalSearchCondition(cb))?;
386        Ok(())
387    }
388
389    /// Enable or disable smart case searching
390    ///
391    /// When enabled, search queries containing no uppercase characters are case-insensitive,
392    /// while queries containing uppercase characters remain case-sensitive.
393    ///
394    /// # Errors
395    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
396    /// could not be sent to the receiver end.
397    #[cfg(feature = "search")]
398    #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
399    pub fn set_smart_case(&self, smart_case: bool) -> crate::Result {
400        self.tx.send(Command::SetSmartCase(smart_case))?;
401        Ok(())
402    }
403
404    /// Control whether to show the prompt
405    ///
406    /// Many applications don't want the prompt to be displayed at all. This function can be used to completely turn
407    /// off the prompt. Passing `false` to this will stops the prompt from displaying and instead a blank line will
408    /// be displayed.
409    ///
410    /// Note that This merely stop the prompt from being shown. Your application can still update the
411    /// prompt and send messages to the user but it won't be shown until the prompt isn't re-enabled.
412    /// The prompt section will also be used when user opens the search prompt to type a search query.
413    ///
414    /// # Errors
415    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
416    /// could not be sent to the mus's receiving end
417    ///
418    /// # Example
419    /// ```
420    /// use minus::Pager;
421    ///
422    /// let pager = Pager::new();
423    /// pager.show_prompt(false).unwrap();
424    /// ```
425    pub fn show_prompt(&self, show: bool) -> crate::Result {
426        self.tx.send(Command::ShowPrompt(show))?;
427        Ok(())
428    }
429
430    /// Configures follow output
431    ///
432    /// When set to true, minus ensures that the user's screen always follows the end part of the
433    /// output. By default it is turned off.
434    ///
435    /// This is similar to [`InputEvent::FollowOutput`](crate::input::InputEvent::FollowOutput) except that
436    /// this is used to control it from the application's side.
437    ///
438    /// # Errors
439    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
440    /// could not be sent to the mus's receiving end
441    ///
442    /// # Example
443    /// ```
444    /// use minus::Pager;
445    ///
446    /// let pager = Pager::new();
447    /// pager.follow_output(true).unwrap();
448    /// ```
449    pub fn follow_output(&self, follow_output: bool) -> crate::Result {
450        self.tx.send(Command::FollowOutput(follow_output))?;
451        Ok(())
452    }
453
454    /// Set the output sink for the pager.
455    ///
456    /// By default, minus writes all output to [`std::io::stdout`]. This function allows you
457    /// to redirect the pager to another output destination, such as [`std::io::stderr`] or `/dev/tty`
458    /// (via [`std::fs::File`]).
459    ///
460    /// # Errors
461    /// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
462    /// could not be sent to the receiver.
463    ///
464    /// # Example
465    /// ```
466    /// use minus::Pager;
467    ///
468    /// let pager = Pager::new();
469    /// pager.set_output_sink(std::io::stderr()).unwrap();
470    /// ```
471    pub fn set_output_sink<S: OutputSink>(&self, sink: S) -> crate::Result {
472        self.tx.send(Command::SetOutputSink(Box::new(sink)))?;
473        Ok(())
474    }
475}
476
477impl Default for Pager {
478    fn default() -> Self {
479        Self::new()
480    }
481}
482
483impl fmt::Write for Pager {
484    fn write_str(&mut self, s: &str) -> fmt::Result {
485        self.push_str(s).map_err(|_| fmt::Error)
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    #[cfg(feature = "dynamic_output")]
492    #[test]
493    fn basic_dynamic_paging() {
494        use super::*;
495        use crate::{RunMode, input::InputEvent, minus_core::RUNMODE};
496
497        // Need to reset this since this test is run in the same process as other tests and they
498        // change the runmode, which causes this test to fail since everything assumes the runmode
499        // isn't already set.
500        *RUNMODE.lock() = RunMode::Uninitialized;
501
502        let pager = Pager::new();
503        pager.follow_output(true).unwrap();
504
505        let pager2 = pager.clone();
506
507        std::thread::scope(|s| {
508            s.spawn(move || crate::dynamic_pager::dynamic_paging(pager2));
509            s.spawn(move || {
510                // Let the pager to initialize before sending a **USER INPUT**.
511                std::thread::sleep(std::time::Duration::from_millis(50));
512                pager.tx.send(Command::UserInput(InputEvent::Exit)).unwrap();
513            });
514        });
515
516        assert_eq!(*RUNMODE.lock(), RunMode::Uninitialized);
517    }
518}