Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add following mode #114

Merged
merged 14 commits into from
Dec 14, 2023
Merged
2 changes: 2 additions & 0 deletions src/core/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub enum Command {
SetInputClassifier(Box<dyn InputClassifier + Send + Sync + 'static>),
AddExitCallback(Box<dyn FnMut() + Send + Sync + 'static>),
ShowPrompt(bool),
FollowOutput(bool),
#[cfg(feature = "static_output")]
SetRunNoOverflow(bool),
#[cfg(feature = "search")]
Expand Down Expand Up @@ -66,6 +67,7 @@ impl Debug for Command {
#[cfg(feature = "static_output")]
Self::SetRunNoOverflow(val) => write!(f, "SetRunNoOverflow({val:?})"),
Self::UserInput(input) => write!(f, "UserInput({input:?})"),
Self::FollowOutput(follow_output) => write!(f, "FollowOutput({follow_output:?})"),
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/core/ev_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ pub fn handle_event(
prev_fmt_lines_count,
append_style,
)?;

if p.follow_output {
display::draw_for_change(out, p, &mut (usize::MAX - 1))?;
}
return Ok(());
}
}
Expand Down Expand Up @@ -266,6 +270,15 @@ pub fn handle_event(
Command::SetInputClassifier(clf) => p.input_classifier = clf,
Command::AddExitCallback(cb) => p.exit_callbacks.push(cb),
Command::ShowPrompt(show) => p.show_prompt = show,
Command::FollowOutput(follow_output)
| Command::UserInput(InputEvent::FollowOutput(follow_output)) => {
p.follow_output = follow_output;
p.format_prompt();

if !p.running.lock().is_uninitialized() {
display::draw_for_change(out, p, &mut (usize::MAX - 1))?;
}
}
Command::UserInput(_) => {}
}
Ok(())
Expand Down
85 changes: 50 additions & 35 deletions src/core/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
//! * The [`start_reactor`] function displays the displays the output and also polls
//! the [`Receiver`] held inside the [`Pager`] for events. Whenever a event is
//! detected, it reacts to it accordingly.
#[cfg(feature = "static_output")]
use crate::minus_core::utils::display;
use crate::{
error::MinusError,
input::InputEvent,
Expand All @@ -22,26 +24,25 @@ use crate::{

use crossbeam_channel::{Receiver, Sender, TrySendError};
use crossterm::event;
#[cfg(feature = "static_output")]
use crossterm::tty::IsTty;
use std::{
convert::TryInto,
io::{stdout, Stdout},
panic,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
#[cfg(feature = "dynamic_output")]
use {super::utils, std::convert::TryInto};

#[cfg(feature = "static_output")]
use super::utils::display::write_lines;
use {super::utils::display::write_lines, crossterm::tty::IsTty};

#[cfg(feature = "search")]
use parking_lot::Condvar;
use parking_lot::Mutex;

use super::{utils, RUNMODE};
use super::{utils::display::draw_for_change, RUNMODE};

/// The main entry point of minus
///
Expand Down Expand Up @@ -226,9 +227,14 @@ fn start_reactor(
) -> Result<(), MinusError> {
let mut out_lock = out.lock();

let mut p = ps.lock();
draw_full(&mut out_lock, &mut p)?;
drop(p);
{
let mut p = ps.lock();
draw_full(&mut out_lock, &mut p)?;

if p.follow_output {
draw_for_change(&mut out_lock, &mut p, &mut (usize::MAX - 1))?;
}
}

let run_mode = *RUNMODE.lock();
match run_mode {
Expand Down Expand Up @@ -296,38 +302,47 @@ fn start_reactor(
}
},
#[cfg(feature = "static_output")]
RunMode::Static => loop {
if is_exited.load(Ordering::SeqCst) {
// Cleanup the screen
//
// This is not needed in dynamic paging because this is already handled by handle_event
let p = ps.lock();
term::cleanup(&mut out_lock, &p.exit_strategy, true)?;
RunMode::Static => {
{
let mut p = ps.lock();
if p.follow_output {
display::draw_for_change(&mut out_lock, &mut p, &mut (usize::MAX - 1))?;
}
}

let mut rm = RUNMODE.lock();
*rm = RunMode::Uninitialized;
drop(rm);
loop {
if is_exited.load(Ordering::SeqCst) {
// Cleanup the screen
//
// This is not needed in dynamic paging because this is already handled by handle_event
let p = ps.lock();
term::cleanup(&mut out_lock, &p.exit_strategy, true)?;

break;
}
let mut rm = RUNMODE.lock();
*rm = RunMode::Uninitialized;
drop(rm);

if let Ok(Command::UserInput(inp)) = rx.recv() {
let mut p = ps.lock();
let is_exit_event = Command::UserInput(inp).is_exit_event();
let is_movement = Command::UserInput(inp).is_movement();
handle_event(
Command::UserInput(inp),
&mut out_lock,
&mut p,
is_exited,
#[cfg(feature = "search")]
input_thread_running,
)?;
if !is_exit_event && !is_movement {
draw_full(&mut out_lock, &mut p)?;
break;
}

if let Ok(Command::UserInput(inp)) = rx.recv() {
let mut p = ps.lock();
let is_exit_event = Command::UserInput(inp).is_exit_event();
let is_movement = Command::UserInput(inp).is_movement();
handle_event(
Command::UserInput(inp),
&mut out_lock,
&mut p,
is_exited,
#[cfg(feature = "search")]
input_thread_running,
)?;
if !is_exit_event && !is_movement {
draw_full(&mut out_lock, &mut p)?;
}
}
}
},
}
RunMode::Uninitialized => panic!(
"Static variable RUNMODE set to uninitialized.\
This is most likely a bug. Please open an issue to the developers"
Expand Down
18 changes: 18 additions & 0 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ pub enum InputEvent {
/// Move to the previous nth match in the given direction
#[cfg(feature = "search")]
MoveToPrevMatch(usize),
/// Control follow mode.
///
/// When set to true, minus ensures that the user's screen always follows the end part of the
/// output. By default it is turned off.
///
/// This is similar to [Pager::follow_output](crate::pager::Pager::follow_output) except that
/// this is used to control it from the user's side.
FollowOutput(bool),
}

/// Classifies the input and returns the appropriate [`InputEvent`]
Expand Down Expand Up @@ -166,6 +174,9 @@ where
let position = ps.prefix_num.parse::<usize>().unwrap_or(1);
InputEvent::UpdateUpperMark(ps.upper_mark.saturating_add(position))
});
map.add_key_events(&["c-f"], |_, ps| {
InputEvent::FollowOutput(!ps.follow_output)
});
map.add_key_events(&["enter"], |_, ps| {
if ps.message.is_some() {
InputEvent::RestorePrompt
Expand Down Expand Up @@ -306,6 +317,13 @@ impl InputClassifier for DefaultInputClassifier {
))
}

// Toggle output following
Event::Key(KeyEvent {
code,
modifiers: KeyModifiers::CONTROL,
..
}) if code == KeyCode::Char('f') => Some(InputEvent::FollowOutput(!ps.follow_output)),

// For number keys
Event::Key(KeyEvent {
code: KeyCode::Char(c),
Expand Down
24 changes: 24 additions & 0 deletions src/pager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,30 @@ impl Pager {
self.tx.send(Command::ShowPrompt(show))?;
Ok(())
}

/// Configures follow output
///
/// When set to true, minus ensures that the user's screen always follows the end part of the
/// output. By default it is turned off.
///
/// This is similar to [InputEvent::FollowOutput](crate::input::InputEvent::FollowOutput) except that
/// this is used to control it from the application's side.
///
/// # Errors
/// This function will return a [`Err(MinusError::Communication)`](MinusError::Communication) if the data
/// could not be sent to the mus's receiving end
///
/// # Example
/// ```
/// use minus::Pager;
///
/// let pager = Pager::new();
/// pager.follow_output(true).unwrap();
/// ```
pub fn follow_output(&self, follow_output: bool) -> crate::Result {
self.tx.send(Command::FollowOutput(follow_output))?;
Ok(())
}
}

impl Default for Pager {
Expand Down
18 changes: 16 additions & 2 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ pub struct PagerState {
/// This is helpful when we defining keybindings like `[n]G` where `[n]` denotes which line to jump to.
/// See [`input::generate_default_bindings`] for exact definition on how it is implemented.
pub(crate) lines_to_row_map: HashMap<usize, usize>,
/// Value for follow mode.
/// See [follow_output](crate::pager::Pager::follow_output) for more info on follow mode.
pub(crate) follow_output: bool,
}

impl PagerState {
Expand Down Expand Up @@ -210,6 +213,7 @@ impl PagerState {
rows,
prefix_num: String::new(),
lines_to_row_map: HashMap::new(),
follow_output: false,
};

state.format_prompt();
Expand Down Expand Up @@ -271,6 +275,7 @@ impl PagerState {
const INPUT_SPEC: &str = "\x1b[30;43m";
const MSG_SPEC: &str = "\x1b[30;1;41m";
const RESET: &str = "\x1b[0m";
const FOLLOW_MODE_SPEC: &str = "\x1b[1m";

// Allocate the string. Add extra space in case for the
// ANSI escape things if we do have characters typed and search showing
Expand Down Expand Up @@ -304,14 +309,16 @@ impl PagerState {
#[cfg(not(feature = "search"))]
let search_len = 0;

let follow_mode_str: &str = if self.follow_output { "[F]" } else { "" };

// Calculate how much extra padding in the middle we need between
// the prompt/message and the indicators on the right
let prefix_len = prefix_str.len();
let extra_space = self
.cols
.saturating_sub(search_len + prefix_len + prompt_str.len());
.saturating_sub(search_len + prefix_len + follow_mode_str.len() + prompt_str.len());
let dsp_prompt: &str = if extra_space == 0 {
&prompt_str[..self.cols - search_len - prefix_len]
&prompt_str[..self.cols - search_len - prefix_len - follow_mode_str.len()]
} else {
prompt_str
};
Expand All @@ -338,6 +345,13 @@ impl PagerState {
format_string.push_str(SEARCH_SPEC);
format_string.push_str(&search_str);
}

// add follow-mode indicator
if !follow_mode_str.is_empty() {
format_string.push_str(FOLLOW_MODE_SPEC);
format_string.push_str(follow_mode_str);
}

format_string.push_str(RESET);

self.displayed_prompt = format_string;
Expand Down
Loading