Skip to main content

urushi_tui_app/
subscription.rs

1//! What an application declares it wants to hear from.
2
3use std::any::Any;
4use std::fmt;
5use std::future::Future;
6use std::io;
7use std::pin::Pin;
8use std::sync::{Arc, Mutex, MutexGuard};
9use std::task::{Context, Poll};
10use std::time::{Duration, Instant};
11
12use futures_core::Stream;
13use urushi::Key;
14
15use super::delivery::{Admission, Sender};
16use super::effect::Mapper;
17use super::source::{Input, Signal, Surface};
18
19/// One application-defined value before its current subscription mapper runs.
20pub(crate) type ErasedMessage = Box<dyn Any + Send>;
21
22/// A stream a source is built from.
23pub(crate) type BoxStream = Pin<Box<dyn Stream<Item = ErasedMessage> + Send + 'static>>;
24
25/// The body of an asynchronous application-defined source.
26pub(crate) type AsyncStart = Box<dyn FnOnce(Sender<ErasedMessage>) -> BoxDone + Send + 'static>;
27
28/// The body of a blocking application-defined source.
29pub(crate) type BlockingStart = Box<dyn FnOnce(Sender<ErasedMessage>) + Send + 'static>;
30
31type BoxDone = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
32
33/// The sources an application wants to hear from, for as long as it keeps
34/// declaring them.
35///
36/// Nothing reaches `update` from a source the application did not declare —
37/// terminal input included. After each `update` the runtime reconciles the
38/// declaration against what it is running: a declared source whose [`Key`] is
39/// already running keeps running, one whose key is not running starts, and a
40/// running one whose key is no longer declared stops.
41///
42/// The function each constructor takes turns the source's own value into the
43/// application's message; the runtime does not know that type and cannot
44/// deliver without one. That function is not part of a source's identity, so a
45/// source declared again keeps running and passes its messages through the most
46/// recent declaration's function from then on.
47///
48/// ```
49/// use std::time::Duration;
50///
51/// use urushi_tui_app::{Input, Subscription};
52///
53/// enum Message {
54///     Input(Input),
55///     Tick,
56/// }
57///
58/// let subscription = Subscription::batch([
59///     Subscription::input(Message::Input),
60///     Subscription::interval("clock", Duration::from_secs(1), |_| Message::Tick),
61/// ]);
62/// ```
63pub struct Subscription<Message> {
64    sources: Vec<Source<Message>>,
65}
66
67/// One declared source: its identity, and what it is.
68pub(crate) struct Source<Message> {
69    pub(crate) key: Key,
70    pub(crate) kind: SourceKind<Message>,
71}
72
73/// What a [`Source`] is, as the runtime reads it.
74pub(crate) enum SourceKind<Message> {
75    Input(Mapper<Input, Message>),
76    Surface(Mapper<Surface, Message>),
77    Interval {
78        period: Duration,
79        map: Mapper<Instant, Message>,
80    },
81    Signal {
82        signal: Signal,
83        map: Mapper<Signal, Message>,
84    },
85    TerminalErrors(Mapper<io::Error, Message>),
86    Stream {
87        admission: Admission,
88        stream: BoxStream,
89        map: DynamicMapper<Message>,
90    },
91    Run {
92        admission: Admission,
93        start: AsyncStart,
94        map: DynamicMapper<Message>,
95    },
96    RunBlocking {
97        admission: Admission,
98        start: BlockingStart,
99        map: DynamicMapper<Message>,
100    },
101}
102
103type ErasedMapper<Message> = Arc<dyn Fn(ErasedMessage) -> Message + Send + Sync>;
104
105pub(crate) struct DynamicMapper<Message> {
106    current: Arc<Mutex<ErasedMapper<Message>>>,
107}
108
109impl<Message> Clone for DynamicMapper<Message> {
110    fn clone(&self) -> Self {
111        Self {
112            current: Arc::clone(&self.current),
113        }
114    }
115}
116
117impl<Message: 'static> DynamicMapper<Message> {
118    fn identity() -> Self {
119        Self::new(|message| {
120            *message
121                .downcast::<Message>()
122                .expect("a subscription mapper receives its source message type")
123        })
124    }
125
126    fn new(map: impl Fn(ErasedMessage) -> Message + Send + Sync + 'static) -> Self {
127        Self {
128            current: Arc::new(Mutex::new(Arc::new(map))),
129        }
130    }
131
132    pub(crate) fn apply(&self, message: ErasedMessage) -> Message {
133        let map = Arc::clone(&lock(&self.current));
134        map(message)
135    }
136
137    pub(crate) fn replace(&self, newer: &Self) {
138        let newer = Arc::clone(&lock(&newer.current));
139        *lock(&self.current) = newer;
140    }
141
142    fn map<To>(self, map: Mapper<Message, To>) -> DynamicMapper<To>
143    where
144        To: 'static,
145    {
146        DynamicMapper::new(move |message| map(self.apply(message)))
147    }
148}
149
150/// The identity of a source the runtime provides. Private, so an application's
151/// own keys cannot collide with one.
152#[derive(Hash)]
153enum RuntimeSource {
154    Input,
155    Surface,
156    TerminalErrors,
157}
158
159/// The identity of an interval: the application's name for it, and its period.
160///
161/// The period is part of the identity so that a declaration that changes it
162/// restarts the timer; without that, reconciliation would see the running key
163/// and keep waiting the old period.
164#[derive(Hash)]
165struct IntervalKey(Key, Duration);
166
167/// The identity of a signal source: the signal it handles.
168#[derive(Hash)]
169struct SignalKey(Signal);
170
171impl<Message> Subscription<Message> {
172    /// No source.
173    pub fn none() -> Self {
174        Self {
175            sources: Vec::new(),
176        }
177    }
178
179    /// Terminal key and text input.
180    ///
181    /// Input is a subscription rather than a method the runtime always calls,
182    /// so a program that must not receive input in some state — while an
183    /// external editor owns the terminal, say — declares none in that state.
184    pub fn input<F>(f: F) -> Self
185    where
186        F: Fn(Input) -> Message + Send + Sync + 'static,
187    {
188        Self::one(
189            Key::of(&RuntimeSource::Input),
190            SourceKind::Input(Arc::new(f)),
191        )
192    }
193
194    /// Observations of the surface the application draws on.
195    pub fn surface<F>(f: F) -> Self
196    where
197        F: Fn(Surface) -> Message + Send + Sync + 'static,
198    {
199        Self::one(
200            Key::of(&RuntimeSource::Surface),
201            SourceKind::Surface(Arc::new(f)),
202        )
203    }
204
205    /// A timer that fires every `period`, for as long as it is declared.
206    ///
207    /// An interval is the one runtime source an application can want more than
208    /// one of, so it names its own: two timers of one period are two timers
209    /// when their keys differ. Declaring the same key with a different period
210    /// restarts that timer at the new period.
211    ///
212    /// A timer that should fire once is [`Effect::after`](super::Effect::after)
213    /// rather than an interval the application stops declaring.
214    pub fn interval<F>(key: impl Into<Key>, period: Duration, f: F) -> Self
215    where
216        F: Fn(Instant) -> Message + Send + Sync + 'static,
217    {
218        Self::one(
219            Key::of(&IntervalKey(key.into(), period)),
220            SourceKind::Interval {
221                period,
222                map: Arc::new(f),
223            },
224        )
225    }
226
227    /// One process signal.
228    ///
229    /// A handler is installed only for a signal that is declared.
230    pub fn signal<F>(signal: Signal, f: F) -> Self
231    where
232        F: Fn(Signal) -> Message + Send + Sync + 'static,
233    {
234        Self::one(
235            Key::of(&SignalKey(signal)),
236            SourceKind::Signal {
237                signal,
238                map: Arc::new(f),
239            },
240        )
241    }
242
243    /// Failures the terminal reports while drawing.
244    ///
245    /// A draw that fails ends the run unless this is declared. With it, the
246    /// failure arrives as a message, the frame that failed is not committed,
247    /// and the application decides what to do — ignore it, record it, save and
248    /// shut down.
249    pub fn terminal_errors<F>(f: F) -> Self
250    where
251        F: Fn(io::Error) -> Message + Send + Sync + 'static,
252    {
253        Self::one(
254            Key::of(&RuntimeSource::TerminalErrors),
255            SourceKind::TerminalErrors(Arc::new(f)),
256        )
257    }
258
259    /// An application-defined source that is a stream, under the default
260    /// admission.
261    pub fn stream<S>(key: impl Into<Key>, stream: S) -> Self
262    where
263        S: Stream<Item = Message> + Send + 'static,
264        Message: Send + 'static,
265    {
266        Self::stream_with(key, Admission::default(), stream)
267    }
268
269    /// An application-defined source that is a stream, under `admission`.
270    pub fn stream_with<S>(key: impl Into<Key>, admission: Admission, stream: S) -> Self
271    where
272        S: Stream<Item = Message> + Send + 'static,
273        Message: Send + 'static,
274    {
275        Self::one(
276            key.into(),
277            SourceKind::Stream {
278                admission,
279                stream: Box::pin(EraseStream {
280                    stream: Box::pin(stream),
281                }),
282                map: DynamicMapper::identity(),
283            },
284        )
285    }
286
287    /// An application-defined asynchronous source, under the default admission.
288    pub fn run<F, Fut>(key: impl Into<Key>, f: F) -> Self
289    where
290        F: FnOnce(Sender<Message>) -> Fut + Send + 'static,
291        Fut: Future<Output = ()> + Send + 'static,
292        Message: Send + 'static,
293    {
294        Self::run_with(key, Admission::default(), f)
295    }
296
297    /// An application-defined asynchronous source, under `admission`.
298    pub fn run_with<F, Fut>(key: impl Into<Key>, admission: Admission, f: F) -> Self
299    where
300        F: FnOnce(Sender<Message>) -> Fut + Send + 'static,
301        Fut: Future<Output = ()> + Send + 'static,
302        Message: Send + 'static,
303    {
304        let start: AsyncStart =
305            Box::new(move |sender| Box::pin(f(sender.contramap(Arc::new(box_message::<Message>)))));
306        Self::one(
307            key.into(),
308            SourceKind::Run {
309                admission,
310                start,
311                map: DynamicMapper::identity(),
312            },
313        )
314    }
315
316    /// An application-defined blocking source, run on its own thread, under the
317    /// default admission.
318    pub fn run_blocking<F>(key: impl Into<Key>, f: F) -> Self
319    where
320        F: FnOnce(Sender<Message>) + Send + 'static,
321        Message: Send + 'static,
322    {
323        Self::run_blocking_with(key, Admission::default(), f)
324    }
325
326    /// An application-defined blocking source, run on its own thread, under
327    /// `admission`.
328    pub fn run_blocking_with<F>(key: impl Into<Key>, admission: Admission, f: F) -> Self
329    where
330        F: FnOnce(Sender<Message>) + Send + 'static,
331        Message: Send + 'static,
332    {
333        Self::one(
334            key.into(),
335            SourceKind::RunBlocking {
336                admission,
337                start: Box::new(move |sender| {
338                    f(sender.contramap(Arc::new(box_message::<Message>)));
339                }),
340                map: DynamicMapper::identity(),
341            },
342        )
343    }
344
345    /// Several sources.
346    pub fn batch(subscriptions: impl IntoIterator<Item = Self>) -> Self {
347        Self {
348            sources: subscriptions
349                .into_iter()
350                .flat_map(|subscription| subscription.sources)
351                .collect(),
352        }
353    }
354
355    /// The same sources with their messages passed through `f`.
356    ///
357    /// A parent program declares a child's subscriptions mapped into its own
358    /// message, so the child stays a program of its own.
359    pub fn map<To>(self, f: impl Fn(Message) -> To + Send + Sync + 'static) -> Subscription<To>
360    where
361        Message: Send + 'static,
362        To: Send + 'static,
363    {
364        let f: Mapper<Message, To> = Arc::new(f);
365        Subscription {
366            sources: self
367                .sources
368                .into_iter()
369                .map(|source| source.map_with(Arc::clone(&f)))
370                .collect(),
371        }
372    }
373
374    fn one(key: Key, kind: SourceKind<Message>) -> Self {
375        Self {
376            sources: vec![Source { key, kind }],
377        }
378    }
379
380    /// The declared sources, for the runtime that reconciles them.
381    pub(crate) fn into_sources(self) -> Vec<Source<Message>> {
382        self.sources
383    }
384
385    pub(crate) fn terminal_error_mapper(&self) -> Option<Mapper<io::Error, Message>> {
386        self.sources.iter().rev().find_map(|source| {
387            if let SourceKind::TerminalErrors(mapper) = &source.kind {
388                Some(Arc::clone(mapper))
389            } else {
390                None
391            }
392        })
393    }
394}
395
396impl<Message> Source<Message> {
397    fn map_with<To>(self, f: Mapper<Message, To>) -> Source<To>
398    where
399        Message: Send + 'static,
400        To: Send + 'static,
401    {
402        let kind = match self.kind {
403            SourceKind::Input(map) => SourceKind::Input(compose(map, f)),
404            SourceKind::Surface(map) => SourceKind::Surface(compose(map, f)),
405            SourceKind::Interval { period, map } => SourceKind::Interval {
406                period,
407                map: compose(map, f),
408            },
409            SourceKind::Signal { signal, map } => SourceKind::Signal {
410                signal,
411                map: compose(map, f),
412            },
413            SourceKind::TerminalErrors(map) => SourceKind::TerminalErrors(compose(map, f)),
414            SourceKind::Stream {
415                admission,
416                stream,
417                map,
418            } => SourceKind::Stream {
419                admission,
420                stream,
421                map: map.map(Arc::clone(&f)),
422            },
423            SourceKind::Run {
424                admission,
425                start,
426                map,
427            } => SourceKind::Run {
428                admission,
429                start,
430                map: map.map(Arc::clone(&f)),
431            },
432            SourceKind::RunBlocking {
433                admission,
434                start,
435                map,
436            } => SourceKind::RunBlocking {
437                admission,
438                start,
439                map: map.map(f),
440            },
441        };
442        Source {
443            key: self.key,
444            kind,
445        }
446    }
447}
448
449fn compose<From, Mid, To>(first: Mapper<From, Mid>, second: Mapper<Mid, To>) -> Mapper<From, To>
450where
451    From: 'static,
452    Mid: 'static,
453    To: 'static,
454{
455    Arc::new(move |value| second(first(value)))
456}
457
458fn box_message<Message: Send + 'static>(message: Message) -> ErasedMessage {
459    Box::new(message)
460}
461
462struct EraseStream<S> {
463    stream: Pin<Box<S>>,
464}
465
466impl<S, Message> Stream for EraseStream<S>
467where
468    S: Stream<Item = Message>,
469    Message: Send + 'static,
470{
471    type Item = ErasedMessage;
472
473    fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
474        self.get_mut()
475            .stream
476            .as_mut()
477            .poll_next(context)
478            .map(|item| item.map(box_message))
479    }
480}
481
482fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
483    mutex
484        .lock()
485        .unwrap_or_else(std::sync::PoisonError::into_inner)
486}
487
488impl<Message> Default for Subscription<Message> {
489    fn default() -> Self {
490        Self::none()
491    }
492}
493
494impl<Message> fmt::Debug for Subscription<Message> {
495    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
496        formatter.debug_list().entries(self.sources.iter()).finish()
497    }
498}
499
500impl<Message> fmt::Debug for Source<Message> {
501    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
502        formatter
503            .debug_struct("Source")
504            .field("key", &self.key)
505            .field("kind", &self.kind.name())
506            .finish()
507    }
508}
509
510impl<Message> SourceKind<Message> {
511    pub(crate) fn is_surface(&self) -> bool {
512        matches!(self, Self::Surface(_))
513    }
514
515    pub(crate) fn name(&self) -> &'static str {
516        match self {
517            Self::Input(_) => "input",
518            Self::Surface(_) => "surface",
519            Self::Interval { .. } => "interval",
520            Self::Signal { .. } => "signal",
521            Self::TerminalErrors(_) => "terminal_errors",
522            Self::Stream { .. } => "stream",
523            Self::Run { .. } => "run",
524            Self::RunBlocking { .. } => "run_blocking",
525        }
526    }
527}
528
529#[cfg(test)]
530#[path = "subscription_tests.rs"]
531mod tests;