Skip to content

Commit 5d2c4fa

Browse files
authored
feat: new feature idle_callback to allow to set callback function while waiting for input (#1015)
* feat: new feature idle_callback to allow to set callback function while waiting for input * refactor: simplify poll interval handling
1 parent f7ac814 commit 5d2c4fa

File tree

2 files changed

+133
-12
lines changed

2 files changed

+133
-12
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ tempfile = "3.3.0"
4545
[features]
4646
bashisms = []
4747
external_printer = ["crossbeam"]
48+
idle_callback = []
4849
sqlite = ["rusqlite/bundled", "serde_json"]
4950
sqlite-dynlib = ["rusqlite", "serde_json"]
5051
system_clipboard = ["arboard"]
@@ -60,4 +61,4 @@ required-features = ["external_printer"]
6061
[package.metadata.docs.rs]
6162
# Whether to pass `--all-features` to Cargo (default: false)
6263
all-features = false
63-
features = ["bashisms", "external_printer", "sqlite"]
64+
features = ["bashisms", "external_printer", "idle_callback", "sqlite"]

src/engine.rs

Lines changed: 131 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ const POLL_WAIT: Duration = Duration::from_millis(100);
6262
// before it is considered a paste. 10 events is conservative enough.
6363
const EVENTS_THRESHOLD: usize = 10;
6464

65-
/// Maximum time Reedline will block on input before yielding control to
66-
/// external printers.
67-
#[cfg(feature = "external_printer")]
68-
const EXTERNAL_PRINTER_WAIT: Duration = Duration::from_millis(100);
65+
/// Default maximum time Reedline will block on input before yielding control
66+
/// for features that require periodic processing (e.g., external printer,
67+
/// idle callback).
68+
const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(100);
6969

7070
/// Determines if inputs should be used to extend the regular line buffer,
7171
/// traverse the history in the standard prompt or edit the search string in the
@@ -195,8 +195,18 @@ pub struct Reedline {
195195
// Whether lines should be accepted immediately
196196
immediately_accept: bool,
197197

198+
// Maximum time to block on input before yielding control for features that
199+
// require periodic processing (external printer, idle callback).
200+
// Only used when external_printer or idle_callback is configured.
201+
poll_interval: Duration,
202+
198203
#[cfg(feature = "external_printer")]
199204
external_printer: Option<ExternalPrinter<String>>,
205+
206+
// Callback function that is called periodically while waiting for input.
207+
// Useful for processing external events (e.g., GUI updates) during idle time.
208+
#[cfg(feature = "idle_callback")]
209+
idle_callback: Option<Box<dyn FnMut() + Send>>,
200210
}
201211

202212
struct BufferEditor {
@@ -271,8 +281,11 @@ impl Reedline {
271281
bracketed_paste: BracketedPasteGuard::default(),
272282
kitty_protocol: KittyProtocolGuard::default(),
273283
immediately_accept: false,
284+
poll_interval: DEFAULT_POLL_INTERVAL,
274285
#[cfg(feature = "external_printer")]
275286
external_printer: None,
287+
#[cfg(feature = "idle_callback")]
288+
idle_callback: None,
276289
}
277290
}
278291

@@ -770,6 +783,12 @@ impl Reedline {
770783
self.repaint(prompt)?;
771784

772785
loop {
786+
// Call idle callback if set (for processing external events like GUI updates)
787+
#[cfg(feature = "idle_callback")]
788+
if let Some(ref mut callback) = self.idle_callback {
789+
callback();
790+
}
791+
773792
#[cfg(feature = "external_printer")]
774793
if let Some(ref external_printer) = self.external_printer {
775794
// get messages from printer as crlf separated "lines"
@@ -805,15 +824,31 @@ impl Reedline {
805824
let mut events: Vec<Event> = vec![];
806825

807826
if !self.immediately_accept {
808-
// If the `external_printer` feature is enabled, we need to
809-
// periodically yield so that external printers get a chance to
810-
// print. Otherwise, we can just block until we receive an event.
811-
#[cfg(feature = "external_printer")]
812-
if event::poll(EXTERNAL_PRINTER_WAIT)? {
827+
// Determine if we need to poll (non-blocking) or can block on input.
828+
// We need polling if external_printer or idle_callback is configured,
829+
// using the shared poll_interval for the timeout.
830+
let needs_polling = {
831+
#[allow(unused_mut)]
832+
let mut result = false;
833+
#[cfg(feature = "external_printer")]
834+
if self.external_printer.is_some() {
835+
result = true;
836+
}
837+
#[cfg(feature = "idle_callback")]
838+
if self.idle_callback.is_some() {
839+
result = true;
840+
}
841+
result
842+
};
843+
844+
if needs_polling {
845+
if event::poll(self.poll_interval)? {
846+
events.push(crossterm::event::read()?);
847+
}
848+
} else {
849+
// Block until we receive an event
813850
events.push(crossterm::event::read()?);
814851
}
815-
#[cfg(not(feature = "external_printer"))]
816-
events.push(crossterm::event::read()?);
817852

818853
// Receive all events in the queue without blocking. Will stop when
819854
// a line of input is completed.
@@ -1976,6 +2011,59 @@ impl Reedline {
19762011
self
19772012
}
19782013

2014+
/// Sets the poll interval used when features that require periodic processing
2015+
/// are active (e.g., external printer, idle callback).
2016+
///
2017+
/// This controls how frequently Reedline yields control back to these features
2018+
/// while waiting for user input. The default is 100ms.
2019+
///
2020+
/// Common values are 33ms (~30fps) for UI updates or 100ms for less frequent tasks.
2021+
///
2022+
/// Note: This setting only takes effect when an external printer or idle callback
2023+
/// is configured. Without these features, Reedline blocks until input is received.
2024+
///
2025+
/// # Example
2026+
/// ```no_run
2027+
/// use std::time::Duration;
2028+
/// use reedline::Reedline;
2029+
///
2030+
/// let editor = Reedline::create()
2031+
/// .with_poll_interval(Duration::from_millis(50));
2032+
/// ```
2033+
pub fn with_poll_interval(mut self, interval: Duration) -> Self {
2034+
self.poll_interval = interval;
2035+
self
2036+
}
2037+
2038+
/// Sets an idle callback that is called periodically while waiting for user input.
2039+
///
2040+
/// This is useful for applications that need to process external events
2041+
/// (such as GUI updates, network events, or timer-based operations) while
2042+
/// the user is typing or the editor is waiting for input.
2043+
///
2044+
/// Use [`with_poll_interval`](Self::with_poll_interval) to control how frequently
2045+
/// the callback is invoked (default: 100ms).
2046+
///
2047+
/// ## Required feature:
2048+
/// `idle_callback`
2049+
///
2050+
/// # Example
2051+
/// ```no_run
2052+
/// use std::time::Duration;
2053+
/// use reedline::Reedline;
2054+
///
2055+
/// let editor = Reedline::create()
2056+
/// .with_poll_interval(Duration::from_millis(33))
2057+
/// .with_idle_callback(Box::new(|| {
2058+
/// // Process external events here
2059+
/// }));
2060+
/// ```
2061+
#[cfg(feature = "idle_callback")]
2062+
pub fn with_idle_callback(mut self, callback: Box<dyn FnMut() + Send>) -> Self {
2063+
self.idle_callback = Some(callback);
2064+
self
2065+
}
2066+
19792067
#[cfg(feature = "external_printer")]
19802068
fn external_messages(external_printer: &ExternalPrinter<String>) -> Result<Vec<String>> {
19812069
let mut messages = Vec::new();
@@ -2095,6 +2183,38 @@ mod tests {
20952183
f(Reedline::create());
20962184
}
20972185

2186+
#[test]
2187+
#[cfg(feature = "idle_callback")]
2188+
fn thread_safe_with_idle_callback() {
2189+
use std::sync::atomic::{AtomicUsize, Ordering};
2190+
use std::sync::Arc;
2191+
2192+
fn f<S: Send>(_: S) {}
2193+
2194+
let counter = Arc::new(AtomicUsize::new(0));
2195+
let counter_clone = counter.clone();
2196+
2197+
let reedline = Reedline::create()
2198+
.with_poll_interval(Duration::from_millis(100))
2199+
.with_idle_callback(Box::new(move || {
2200+
counter_clone.fetch_add(1, Ordering::SeqCst);
2201+
}));
2202+
2203+
// Verify that Reedline with idle_callback is still Send
2204+
f(reedline);
2205+
}
2206+
2207+
#[test]
2208+
#[cfg(feature = "idle_callback")]
2209+
fn idle_callback_builder_pattern() {
2210+
// Test that with_idle_callback can be chained with other builder methods
2211+
let _reedline = Reedline::create()
2212+
.with_quick_completions(true)
2213+
.with_poll_interval(Duration::from_millis(33))
2214+
.with_idle_callback(Box::new(|| {}))
2215+
.with_partial_completions(true);
2216+
}
2217+
20982218
#[test]
20992219
fn mouse_click_moves_cursor_in_regular_mode() {
21002220
let mut reedline = Reedline::create().with_mouse_click(MouseClickMode::Enabled);

0 commit comments

Comments
 (0)