Skip to content

Commit 190408a

Browse files
authored
feat: Allow fetching app state (#26)
1 parent b1a479e commit 190408a

2 files changed

Lines changed: 53 additions & 8 deletions

File tree

crates/eye_declare/src/app.rs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,24 @@ impl Drop for RawModeGuard {
2828

2929
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
3030
use futures::StreamExt;
31-
use tokio::sync::mpsc;
31+
use tokio::sync::{mpsc, oneshot};
3232

3333
use crate::component::VStack;
3434
use crate::element::Elements;
3535
use crate::inline::InlineRenderer;
3636
use crate::node::NodeId;
3737

3838
type StateUpdateFn<S> = Box<dyn FnOnce(&mut S) + Send>;
39+
type StateGetFn<S> = Box<dyn FnOnce(&S) + Send>;
3940
type ViewFn<S> = Box<dyn Fn(&S) -> Elements>;
4041
type CommitCallbackFn<S> = Box<dyn FnMut(&CommittedElement, &mut S)>;
4142
type EventHandlerFn<'a, S> = Option<&'a mut dyn FnMut(&Event, &mut S) -> ControlFlow>;
4243

44+
enum AppMessage<S> {
45+
UpdateState(StateUpdateFn<S>),
46+
GetState(StateGetFn<S>),
47+
}
48+
4349
/// Information about an element that has scrolled into terminal scrollback.
4450
///
4551
/// Passed to the [`ApplicationBuilder::on_commit`] callback. Use the `key`
@@ -123,7 +129,7 @@ pub enum KeyboardProtocol {
123129
/// });
124130
/// ```
125131
pub struct Handle<S: Send + 'static> {
126-
tx: mpsc::UnboundedSender<StateUpdateFn<S>>,
132+
tx: mpsc::UnboundedSender<AppMessage<S>>,
127133
exit: Arc<AtomicBool>,
128134
}
129135

@@ -133,7 +139,20 @@ impl<S: Send + 'static> Handle<S> {
133139
/// This is non-blocking and can be called from both sync and
134140
/// async contexts.
135141
pub fn update(&self, f: impl FnOnce(&mut S) + Send + 'static) {
136-
let _ = self.tx.send(Box::new(f));
142+
let _ = self.tx.send(AppMessage::UpdateState(Box::new(f)));
143+
}
144+
145+
/// Get the current state.
146+
pub fn fetch<T: Send + 'static>(
147+
&self,
148+
f: impl FnOnce(&S) -> T + Send + 'static,
149+
) -> oneshot::Receiver<T> {
150+
let (tx, rx) = oneshot::channel();
151+
let _ = self.tx.send(AppMessage::GetState(Box::new(move |s| {
152+
let _ = tx.send(f(s));
153+
})));
154+
155+
rx
137156
}
138157

139158
/// Signal the application to exit its event loop.
@@ -385,7 +404,7 @@ pub struct Application<S: Send + 'static> {
385404
container: NodeId,
386405
dirty: bool,
387406
on_commit: Option<CommitCallbackFn<S>>,
388-
rx: mpsc::UnboundedReceiver<StateUpdateFn<S>>,
407+
rx: mpsc::UnboundedReceiver<AppMessage<S>>,
389408
exit: Arc<AtomicBool>,
390409
ctrl_c: CtrlCBehavior,
391410
keyboard_protocol: KeyboardProtocol,
@@ -652,7 +671,10 @@ impl<S: Send + 'static> Application<S> {
652671
tokio::select! {
653672
result = self.rx.recv(), if channel_open => {
654673
match result {
655-
Some(update) => {
674+
Some(AppMessage::GetState(get)) => {
675+
get(&self.state);
676+
}
677+
Some(AppMessage::UpdateState(update)) => {
656678
update(&mut self.state);
657679
self.dirty = true;
658680
}
@@ -763,10 +785,13 @@ impl<S: Send + 'static> Application<S> {
763785

764786
result = self.rx.recv(), if channel_open => {
765787
match result {
766-
Some(update) => {
788+
Some(AppMessage::UpdateState(update)) => {
767789
update(&mut self.state);
768790
self.dirty = true;
769791
}
792+
Some(AppMessage::GetState(get)) => {
793+
get(&self.state);
794+
}
770795
None => {
771796
// All Handles dropped
772797
channel_open = false;
@@ -807,8 +832,15 @@ impl<S: Send + 'static> Application<S> {
807832

808833
fn drain_updates(&mut self) {
809834
while let Ok(update) = self.rx.try_recv() {
810-
update(&mut self.state);
811-
self.dirty = true;
835+
match update {
836+
AppMessage::UpdateState(update) => {
837+
update(&mut self.state);
838+
self.dirty = true;
839+
}
840+
AppMessage::GetState(get) => {
841+
get(&self.state);
842+
}
843+
}
812844
}
813845
}
814846

docs/content/guide/application.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ tokio::spawn(async move {
4242
});
4343
```
4444

45+
### Fetching state
46+
47+
Sometimes you need to read the current state without mutating it — for example, to check a condition before deciding what to do next. `fetch()` sends a read closure into the event loop and returns a `oneshot::Receiver<T>` with the result:
48+
49+
```rust
50+
let count = handle.fetch(|s| s.messages.len()).await.unwrap();
51+
if count > 100 {
52+
handle.update(|s| s.messages.drain(..50));
53+
}
54+
```
55+
56+
Like `update()`, the closure runs on the event loop, so it sees a consistent snapshot of state. The returned receiver is `.await`-ed to get the value. Because `fetch()` does not mutate state, it does not trigger a re-render.
57+
4558
### Batching
4659

4760
Multiple `update()` calls between frames are batched into a single rebuild. This means you can call `update()` rapidly without causing unnecessary re-renders:

0 commit comments

Comments
 (0)