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

Limit refresh rate #539

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
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
25 changes: 25 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Customize line editor
use crate::Result;
use std::default::Default;
use std::time::Duration;

/// User preferences
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -37,6 +38,8 @@ pub struct Config {
enable_bracketed_paste: bool,
/// Whether to disable or not the signals in termios
enable_signals: bool,
/// To avoid freezing the UI
refresh_rate_limit: Duration,
}

impl Config {
Expand Down Expand Up @@ -207,6 +210,11 @@ impl Config {
pub(crate) fn set_enable_signals(&mut self, enable_signals: bool) {
self.enable_signals = enable_signals;
}

/// Used to batch input events before repainting edited line.
pub fn refresh_rate_limit(&self) -> Duration {
self.refresh_rate_limit
}
}

impl Default for Config {
Expand All @@ -228,6 +236,7 @@ impl Default for Config {
check_cursor_position: false,
enable_bracketed_paste: true,
enable_signals: false,
refresh_rate_limit: Duration::from_millis(500),
}
}
}
Expand Down Expand Up @@ -474,6 +483,15 @@ impl Builder {
self
}

/// Used to batch input events before repainting edited line.
///
/// By default, 500 ms
#[must_use]
pub fn refresh_rate_limit(mut self, refresh_rate_limit: Duration) -> Self {
self.set_refresh_rate_limit(refresh_rate_limit);
self
}

/// Builds a `Config` with the settings specified so far.
#[must_use]
pub fn build(self) -> Config {
Expand Down Expand Up @@ -598,4 +616,11 @@ pub trait Configurer {
fn set_enable_signals(&mut self, enable_signals: bool) {
self.config_mut().set_enable_signals(enable_signals);
}

/// Used to batch input events before repainting edited line.
///
/// By default, 500 ms
fn set_refresh_rate_limit(&mut self, refresh_rate_limit: Duration) {
self.config_mut().refresh_rate_limit = refresh_rate_limit;
}
}
69 changes: 66 additions & 3 deletions src/edit.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Command processor

use crate::Config;
use log::debug;
use std::fmt;
use std::time::{Duration, Instant};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;

Expand All @@ -16,11 +18,56 @@ use crate::layout::{Layout, Position};
use crate::line_buffer::{
ChangeListener, DeleteListener, Direction, LineBuffer, NoListener, WordAction, MAX_LINE,
};
use crate::tty::{Renderer, Term, Terminal};
use crate::tty::{RawReader, Renderer, Term, Terminal};
use crate::undo::Changeset;
use crate::validate::{ValidationContext, ValidationResult};
use crate::KillRing;

struct RefreshRateLimit {
limit: Duration,
last_refresh_time: Instant,
refresh_skipped: bool,
forced: bool,
}

impl RefreshRateLimit {
fn new(limit: Duration) -> RefreshRateLimit {
RefreshRateLimit {
limit,
last_refresh_time: Instant::now(),
refresh_skipped: false,
forced: true,
}
}
}
impl RefreshRateLimit {
/// Should be called to unconditionally refresh screen
fn force(&mut self) {
self.forced = true;
}

/// Should be called before refreshing screen
fn should_skip(&mut self) -> bool {
if self.forced || cfg!(test) {
self.forced = false;
return false;
}
if self.last_refresh_time.elapsed() < self.limit {
debug!(target: "rustyline", "refresh skipped");
//self.last_refresh_time = now;
self.refresh_skipped = true;
return true;
}
false
}

/// Should be called after refreshing screen
fn reset(&mut self) {
self.last_refresh_time = Instant::now();
self.refresh_skipped = false;
}
}

/// Represent the state during line editing.
/// Implement rendering.
pub struct State<'out, 'prompt, H: Helper> {
Expand All @@ -36,6 +83,7 @@ pub struct State<'out, 'prompt, H: Helper> {
pub ctx: Context<'out>, // Give access to history for `hinter`
pub hint: Option<Box<dyn Hint>>, // last hint displayed
pub highlight_char: bool, // `true` if a char has been highlighted
refresh_rate_limit: RefreshRateLimit,
}

enum Info<'m> {
Expand All @@ -46,6 +94,7 @@ enum Info<'m> {

impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
pub fn new(
config: &Config,
out: &'out mut <Terminal as Term>::Writer,
prompt: &'prompt str,
helper: Option<&'out H>,
Expand All @@ -65,6 +114,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
ctx,
hint: None,
highlight_char: false,
refresh_rate_limit: RefreshRateLimit::new(config.refresh_rate_limit()),
}
}

Expand All @@ -84,6 +134,11 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
ignore_external_print: bool,
) -> Result<Cmd> {
loop {
if self.refresh_rate_limit.refresh_skipped
&& !rdr.poll(self.refresh_rate_limit.limit)?
{
self.refresh_line()?;
}
let rc = input_state.next_cmd(rdr, self, single_esc_abort, ignore_external_print);
if let Err(ReadlineError::WindowResized) = rc {
debug!(target: "rustyline", "SIGWINCH");
Expand Down Expand Up @@ -161,6 +216,9 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
default_prompt: bool,
info: Info<'_>,
) -> Result<()> {
if self.refresh_rate_limit.should_skip() {
return Ok(());
}
let info = match info {
Info::NoHint => None,
Info::Hint => self.hint.as_ref().map(|h| h.display()),
Expand All @@ -187,7 +245,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
highlighter,
)?;
self.layout = new_layout;

self.refresh_rate_limit.reset();
Ok(())
}

Expand Down Expand Up @@ -271,6 +329,7 @@ impl<H: Helper> Refresher for State<'_, '_, H> {
let prompt_size = self.prompt_size;
self.hint = None;
self.highlight_char(kind);
self.refresh_rate_limit.force();
self.refresh(self.prompt, prompt_size, true, Info::Msg(msg))
}

Expand Down Expand Up @@ -359,14 +418,17 @@ impl<H: Helper> State<'_, '_, H> {
&& self.layout.cursor.col + width < self.out.get_columns()
&& (self.hint.is_none() && no_previous_hint) // TODO refresh only current line
&& !self.highlight_char(CmdKind::Other)
&& !self.refresh_rate_limit.refresh_skipped
{
// Avoid a full update of the line in the trivial case.
self.layout.cursor.col += width;
self.layout.end.col += width;
debug_assert!(self.layout.prompt_size <= self.layout.cursor);
debug_assert!(self.layout.cursor <= self.layout.end);
let bits = ch.encode_utf8(&mut self.byte_buffer);
self.out.write_and_flush(bits)
self.out.write_and_flush(bits)?;
self.refresh_rate_limit.reset();
Ok(())
} else {
self.refresh(self.prompt, prompt_size, true, Info::Hint)
}
Expand Down Expand Up @@ -762,6 +824,7 @@ pub fn init_state<'out, H: Helper>(
ctx: Context::new(history),
hint: Some(Box::new("hint".to_owned())),
highlight_char: false,
refresh_rate_limit: RefreshRateLimit::new(Duration::from_millis(0)),
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -691,8 +691,7 @@ impl<H: Helper, I: History> Editor<H, I> {

self.kill_ring.reset(); // TODO recreate a new kill ring vs reset
let ctx = Context::new(&self.history);
let mut s = State::new(&mut stdout, prompt, self.helper.as_ref(), ctx);

let mut s = State::new(&self.config, &mut stdout, prompt, self.helper.as_ref(), ctx);
let mut input_state = InputState::new(&self.config, &self.custom_bindings);

if let Some((left, right)) = initial {
Expand Down
3 changes: 3 additions & 0 deletions src/tty/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! This module implements and describes common TTY methods & traits

use std::time::Duration;
use unicode_width::UnicodeWidthStr;

use crate::config::{Behavior, BellStyle, ColorMode, Config};
Expand Down Expand Up @@ -37,6 +38,8 @@ pub trait RawReader {
fn find_binding(&self, key: &KeyEvent) -> Option<Cmd>;
/// Backup type ahead
fn unbuffer(self) -> Option<Buffer>;
/// Poll input
fn poll(&mut self, timeout: Duration) -> Result<bool>;
}

/// Display prompt, line and cursor in terminal output
Expand Down
9 changes: 9 additions & 0 deletions src/tty/test.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Tests specific definitions
use std::slice::Iter;
use std::time::Duration;
use std::vec::IntoIter;

use super::{Event, ExternalPrinter, RawMode, RawReader, Renderer, Term};
Expand Down Expand Up @@ -51,6 +52,10 @@ impl RawReader for Iter<'_, KeyEvent> {
fn unbuffer(self) -> Option<Buffer> {
None
}

fn poll(&mut self, _timeout: Duration) -> Result<bool> {
Ok(true)
}
}

impl RawReader for IntoIter<KeyEvent> {
Expand Down Expand Up @@ -88,6 +93,10 @@ impl RawReader for IntoIter<KeyEvent> {
fn unbuffer(self) -> Option<Buffer> {
None
}

fn poll(&mut self, _timeout: Duration) -> Result<bool> {
Ok(true)
}
}

#[derive(Default)]
Expand Down
17 changes: 12 additions & 5 deletions src/tty/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use buffer_redux::BufReader;
use std::cmp;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fs::{File, OpenOptions};
#[cfg(not(feature = "buffer-redux"))]
use std::io::BufReader;
Expand All @@ -12,6 +13,7 @@ use std::os::unix::net::UnixStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, SyncSender};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use log::{debug, warn};
use nix::errno::Errno;
Expand Down Expand Up @@ -302,7 +304,7 @@ impl PosixRawReader {
} else {
self.timeout_ms
};
match self.poll(timeout) {
match self.poll_input(timeout) {
// Ignore poll errors, it's very likely we'll pick them up on
// the next read anyway.
Ok(0) | Err(_) => Ok(E::ESC),
Expand Down Expand Up @@ -701,7 +703,7 @@ impl PosixRawReader {
})
}

fn poll(&mut self, timeout_ms: PollTimeout) -> Result<i32> {
fn poll_input(&mut self, timeout_ms: PollTimeout) -> Result<i32> {
let n = self.tty_in.buffer().len();
if n > 0 {
return Ok(n as i32);
Expand Down Expand Up @@ -798,7 +800,7 @@ impl RawReader for PosixRawReader {
} else {
self.timeout_ms
};
match self.poll(timeout_ms) {
match self.poll_input(timeout_ms) {
Ok(0) => {
// single escape
}
Expand Down Expand Up @@ -873,6 +875,11 @@ impl RawReader for PosixRawReader {
let (_, buffer) = self.tty_in.into_inner_with_buffer();
Some(buffer)
}

fn poll(&mut self, timeout: Duration) -> Result<bool> {
self.poll_input(PollTimeout::try_from(timeout).expect("invalid timeout"))
.map(|i| i != 0)
}
}

impl Receiver for Utf8 {
Expand Down Expand Up @@ -1116,14 +1123,14 @@ impl Renderer for PosixRenderer {
}

fn move_cursor_at_leftmost(&mut self, rdr: &mut PosixRawReader) -> Result<()> {
if rdr.poll(PollTimeout::ZERO)? != 0 {
if rdr.poll_input(PollTimeout::ZERO)? != 0 {
debug!(target: "rustyline", "cannot request cursor location");
return Ok(());
}
/* Report cursor location */
self.write_and_flush("\x1b[6n")?;
/* Read the response: ESC [ rows ; cols R */
if rdr.poll(PollTimeout::from(100u8))? == 0
if rdr.poll_input(PollTimeout::from(100u8))? == 0
|| rdr.next_char()? != '\x1b'
|| rdr.next_char()? != '['
|| read_digits_until(rdr, ';')?.is_none()
Expand Down
Loading