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

use crate::{
ExitStrategy, LineNumbers,
ExitStrategy, LineNumbers, OutputSink,
hooks::{Hook, HookCallback},
input::{InputClassifier, InputEvent},
minus_core::utils::display::AppendStyle,
Expand Down Expand Up @@ -49,6 +49,7 @@ pub enum Command {
LineWrapping(bool),
SetLineNumbers(LineNumbers),
FollowOutput(bool),
SetOutputSink(Box<dyn OutputSink>),

// Configuration options
SetExitStrategy(ExitStrategy),
Expand Down Expand Up @@ -79,7 +80,8 @@ impl PartialEq for Command {
(Self::SetRunNoOverflow(d1), Self::SetRunNoOverflow(d2)) => d1 == d2,
(Self::SetInputClassifier(_), Self::SetInputClassifier(_))
| (Self::AddExitCallback(_), Self::AddExitCallback(_))
| (Self::AddHook(..), Self::AddHook(..)) => true,
| (Self::AddHook(..), Self::AddHook(..))
| (Self::SetOutputSink(_), Self::SetOutputSink(_)) => true,
(Self::RemoveHook(h1, id1), Self::RemoveHook(h2, id2)) => h1 == h2 && id1 == id2,
#[cfg(feature = "search")]
(Self::IncrementalSearchCondition(_), Self::IncrementalSearchCondition(_)) => true,
Expand Down Expand Up @@ -110,6 +112,7 @@ impl Debug for Command {
Self::SetRunNoOverflow(val) => write!(f, "SetRunNoOverflow({val:?})"),
Self::UserInput(input) => write!(f, "UserInput({input:?})"),
Self::FollowOutput(follow_output) => write!(f, "FollowOutput({follow_output:?})"),
Self::SetOutputSink(_) => write!(f, "SetOutputSink"),
Self::Io(c) => write!(f, "Io({c:?})"),
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/core/ev_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,17 @@ pub fn handle_event(
p.hooks.remove_callback(hook, id);
}
Command::ShowPrompt(show) => p.show_prompt = show,
Command::SetOutputSink(sink) => {
#[cfg(not(test))]
if sink.is_tty()
&& let Ok(size) = crossterm::terminal::size()
{
p.cols = size.0 as usize;
p.rows = size.1 as usize;
p.reformat_display();
}
*p.output_sink.lock() = sink;
}
Command::FollowOutput(follow_output)
| Command::UserInput(InputEvent::FollowOutput(follow_output)) => {
p.follow_output = follow_output;
Expand Down
76 changes: 34 additions & 42 deletions src/core/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! the [`Receiver`] held inside the [`Pager`] for events. Whenever a event is
//! detected, it reacts to it accordingly.
use crate::{
Pager, PagerState,
OutputSink, Pager, PagerState,
error::MinusError,
hooks::Hook,
input::InputEvent,
Expand All @@ -24,17 +24,13 @@ use crate::{
use crossbeam_channel::{Receiver, Sender, TrySendError};
use crossterm::event;
use std::{
io::Write,
panic,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};

#[cfg(not(test))]
use std::io::stdout;

#[cfg(feature = "search")]
use parking_lot::Condvar;
use parking_lot::Mutex;
Expand All @@ -50,11 +46,11 @@ use super::{CommandQueue, RUNMODE, utils::display::draw_for_change};
/// and creates the initial state that to be stored inside the [`PagerState`]
///
/// Then it checks if the minus is running in static mode and does some checks:-
/// * If standard output is not a terminal screen, that is if it is a file or block
/// device, minus will write all the data at once to the stdout and quit
/// * If output sink is not a terminal screen, that is if it is a file or block
/// device, minus will write all the data at once to the output sink and quit
///
/// * If the size of the data is less than the available number of rows in the terminal
/// then it displays everything on the main stdout screen at once and quits. This
/// then it displays everything on the main screen at once and quits. This
/// behaviour can be turned off if [`Pager::set_run_no_overflow`] is called
/// by the main application
// Sorry... this behaviour would have been cool to have in async mode, just think about it!!! Many
Expand All @@ -76,11 +72,6 @@ use super::{CommandQueue, RUNMODE, utils::display::draw_for_change};
#[allow(clippy::module_name_repetitions)]
#[allow(clippy::too_many_lines)]
pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusError> {
#[cfg(not(test))]
let mut out = stdout();
#[cfg(test)]
let mut out = Vec::new();

// Is the event reader running
#[cfg(feature = "search")]
let input_thread_running = Arc::new((Mutex::new(true), Condvar::new()));
Expand All @@ -96,20 +87,25 @@ pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusErr
*super::RUNMODE.lock() = rm;
ps.run_hooks(Hook::PrePagerStart);

let output_sink = ps.output_sink.clone();

// Static mode checks
#[cfg(all(feature = "static_output", not(test)))]
if *RUNMODE.lock() == RunMode::Static {
use {super::utils::display::write_raw_lines, crossterm::tty::IsTty};
// If stdout is not a tty, write everything and quit
use super::utils::display::write_raw_lines;
let mut out = output_sink.lock();
// If output sink is not a tty, write everything and quit
if !out.is_tty() {
write_raw_lines(&mut out, &[ps.screen.orig_text], None)?;
write_raw_lines(&mut *out, &[ps.screen.orig_text], None)?;
drop(out);
*RUNMODE.lock() = RunMode::Uninitialized;
return Ok(());
}
// If number of lines of text is less than available rows, write everything and quit
// unless run_no_overflow is set to true
if ps.screen.formatted_lines_count() <= ps.rows && !ps.run_no_overflow {
write_raw_lines(&mut out, &ps.screen.formatted_lines, Some("\r"))?;
write_raw_lines(&mut *out, &ps.screen.formatted_lines, Some("\r"))?;
drop(out);
ps.exit();
*RUNMODE.lock() = RunMode::Uninitialized;
return Ok(());
Expand All @@ -118,26 +114,22 @@ pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusErr

// Setup terminal, adjust line wraps and get rows
#[cfg(not(test))]
term::setup(&mut out)?;
term::setup(&mut *output_sink.lock())?;

// Has the user quit
let is_exited = Arc::new(AtomicBool::new(false));
let is_exited2 = is_exited.clone();

{
let panic_hook = panic::take_hook();
let panic_sink = output_sink.clone();
panic::set_hook(Box::new(move |pinfo| {
is_exited2.store(true, std::sync::atomic::Ordering::SeqCst);
// HACK: In test we don't care about the cleanup code so just use a separate buffer
// for panic handler.
#[cfg(test)]
let mut out2 = Vec::new();
#[cfg(not(test))]
let mut out2 = stdout();

// 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, true));
let mut out2 = panic_sink.lock();
drop(term::cleanup(&mut *out2, true));
drop(out2);
panic_hook(pinfo);
}));
}
Expand All @@ -155,11 +147,9 @@ pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusErr
std::thread::scope(|s| -> crate::Result {
let is_exited3 = is_exited.clone();
let is_exited4 = is_exited.clone();

#[cfg(test)]
let mut out2 = Vec::new();
#[cfg(not(test))]
let mut out2 = stdout();
let output_sink2 = output_sink.clone();
let ps_mutex2 = ps_mutex.clone();
let rx2 = rx.clone();

let t1 = s.spawn(move || {
let res = event_reader(
Expand All @@ -177,9 +167,9 @@ pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusErr
});
let t2 = s.spawn(move || {
let res = start_reactor(
&rx,
&ps_mutex,
&mut out2,
&rx2,
&ps_mutex2,
&output_sink2,
#[cfg(feature = "search")]
&input_thread_running,
&is_exited4,
Expand All @@ -196,7 +186,8 @@ 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, true)?;
#[cfg(not(test))]
term::cleanup(&mut *output_sink.lock(), true)?;
}

r1?;
Expand All @@ -221,20 +212,21 @@ pub fn init_core(pager: &Pager, rm: RunMode) -> std::result::Result<(), MinusErr
fn start_reactor(
rx: &Receiver<Command>,
ps: &Arc<Mutex<PagerState>>,
mut out_lock: impl Write,
output_sink: &Arc<Mutex<Box<dyn OutputSink>>>,
#[cfg(feature = "search")] input_thread_running: &Arc<(Mutex<bool>, Condvar)>,
is_exited: &Arc<AtomicBool>,
) -> Result<(), MinusError> {
let mut command_queue = CommandQueue::new();

{
let mut p = ps.lock();
let mut out_lock = output_sink.lock();

draw_full(&mut out_lock, &mut p)?;
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))?;
draw_for_change(&mut *out_lock, &mut p, &mut (usize::MAX - 1))?;
}
}

Expand All @@ -243,7 +235,7 @@ fn start_reactor(
#[cfg(feature = "dynamic_output")]
RunMode::Dynamic => loop {
if is_exited.load(Ordering::SeqCst) {
term::cleanup(&mut out_lock, true)?;
term::cleanup(&mut *output_sink.lock(), true)?;
ps.lock().run_hooks(Hook::PostPagerExit);
let mut rm = RUNMODE.lock();
*rm = RunMode::Uninitialized;
Expand All @@ -263,7 +255,7 @@ fn start_reactor(

handle_io_command(
ic,
&mut out_lock,
&mut *output_sink.lock(),
&mut p,
&mut command_queue,
#[cfg(feature = "search")]
Expand All @@ -280,7 +272,7 @@ 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, true)?;
term::cleanup(&mut *output_sink.lock(), true)?;
ps.lock().run_hooks(Hook::PostPagerExit);

let mut rm = RUNMODE.lock();
Expand All @@ -302,7 +294,7 @@ fn start_reactor(

handle_io_command(
ic,
&mut out_lock,
&mut *output_sink.lock(),
&mut p,
&mut command_queue,
#[cfg(feature = "search")]
Expand Down
8 changes: 4 additions & 4 deletions src/core/utils/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

#![allow(dead_code)]

use crate::OutputSink;
use crate::error::{CleanupError, MinusError, SetupError};
use crossterm::{
cursor, event, execute, queue,
terminal::{self, Clear},
tty::IsTty,
};
use std::io;

Expand All @@ -18,14 +18,14 @@ use std::io;
/// - Clear the entire screen and hide the cursor.
///
/// # Errors
/// The function will return with an error if `stdout` is not a terminal. It will qlso fail
/// if it cannot executo commands on the terminal See [`SetupError`].
/// The function will return with an error if `out` is not a terminal. It will also fail
/// if it cannot execute commands on the terminal See [`SetupError`].
///
/// [alternate screen]: ../../../crossterm/terminal/index.html#alternate-screen
/// [raw mode]: ../../../crossterm/terminal/index.html#raw-mode
// This function should be kept close to `cleanup` to help ensure both are
// doing the opposite of the other.
pub fn setup(out: &mut io::Stdout) -> std::result::Result<(), SetupError> {
pub fn setup(out: &mut impl OutputSink) -> std::result::Result<(), SetupError> {
if out.is_tty() {
Ok(())
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ pub mod screen;
#[cfg(feature = "search")]
#[cfg_attr(docsrs, doc(cfg(feature = "search")))]
pub mod search;
pub mod sink;
pub mod state;
#[cfg(feature = "static_output")]
mod static_pager;
Expand All @@ -213,6 +214,7 @@ pub use search::SearchMode;

pub use error::MinusError;
pub use pager::Pager;
pub use sink::OutputSink;
pub use state::PagerState;

/// A convenient type for `Vec<Box<dyn FnMut() + Send + Sync + 'static>>`
Expand Down
24 changes: 23 additions & 1 deletion src/pager.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Proivdes the [Pager] type

use crate::{
ExitStrategy, LineNumbers,
ExitStrategy, LineNumbers, OutputSink,
error::MinusError,
hooks::{Hook, HookCallback},
input,
Expand Down Expand Up @@ -414,6 +414,28 @@ impl Pager {
self.tx.send(Command::FollowOutput(follow_output))?;
Ok(())
}

/// Set the output sink for the pager.
///
/// By default, minus writes all output to [`std::io::stdout`]. This function allows you
/// to redirect the pager to another output destination, such as [`std::io::stderr`] or `/dev/tty`
/// (via [`std::fs::File`]).
///
/// # Errors
/// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
/// could not be sent to the receiver.
///
/// # Example
/// ```
/// use minus::Pager;
///
/// let pager = Pager::new();
/// pager.set_output_sink(std::io::stderr()).unwrap();
/// ```
pub fn set_output_sink<S: OutputSink>(&self, sink: S) -> crate::Result {
self.tx.send(Command::SetOutputSink(Box::new(sink)))?;
Ok(())
}
}

impl Default for Pager {
Expand Down
Loading