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}