5
5
import time
6
6
from asyncio import (
7
7
AbstractEventLoop ,
8
+ CancelledError ,
8
9
Future ,
10
+ Task ,
9
11
ensure_future ,
10
12
get_event_loop ,
11
13
sleep ,
14
16
from traceback import format_tb
15
17
from typing import (
16
18
Any ,
19
+ Awaitable ,
17
20
Callable ,
18
21
Dict ,
19
22
FrozenSet ,
@@ -134,6 +137,10 @@ class Application(Generic[_AppResult]):
134
137
scheduled calls), postpone the rendering max x seconds. '0' means:
135
138
don't postpone. '.5' means: try to draw at least twice a second.
136
139
140
+ :param refresh_interval: Automatically invalidate the UI every so many
141
+ seconds. When `None` (the default), only invalidate when `invalidate`
142
+ has been called.
143
+
137
144
Filters:
138
145
139
146
:param mouse_support: (:class:`~prompt_toolkit.filters.Filter` or
@@ -190,6 +197,7 @@ def __init__(self,
190
197
reverse_vi_search_direction : FilterOrBool = False ,
191
198
min_redraw_interval : Union [float , int , None ] = None ,
192
199
max_render_postpone_time : Union [float , int , None ] = .01 ,
200
+ refresh_interval : Optional [float ] = None ,
193
201
194
202
on_reset : Optional [ApplicationEventHandler ] = None ,
195
203
on_invalidate : Optional [ApplicationEventHandler ] = None ,
@@ -238,6 +246,7 @@ def __init__(self,
238
246
self .enable_page_navigation_bindings = enable_page_navigation_bindings
239
247
self .min_redraw_interval = min_redraw_interval
240
248
self .max_render_postpone_time = max_render_postpone_time
249
+ self .refresh_interval = refresh_interval
241
250
242
251
# Events.
243
252
self .on_invalidate = Event (self , on_invalidate )
@@ -386,6 +395,8 @@ def reset(self) -> None:
386
395
387
396
self .exit_style = ''
388
397
398
+ self .background_tasks : List [Task ] = []
399
+
389
400
self .renderer .reset ()
390
401
self .key_processor .reset ()
391
402
self .layout .reset ()
@@ -437,7 +448,7 @@ def schedule_redraw() -> None:
437
448
async def redraw_in_future () -> None :
438
449
await sleep (cast (float , self .min_redraw_interval ) - diff )
439
450
schedule_redraw ()
440
- ensure_future (redraw_in_future ())
451
+ self . create_background_task (redraw_in_future ())
441
452
else :
442
453
schedule_redraw ()
443
454
else :
@@ -488,6 +499,19 @@ def run_in_context() -> None:
488
499
if self .context is not None :
489
500
self .context .run (run_in_context )
490
501
502
+ def _start_auto_refresh_task (self ) -> None :
503
+ """
504
+ Start a while/true loop in the background for automatic invalidation of
505
+ the UI.
506
+ """
507
+ async def auto_refresh ():
508
+ while True :
509
+ await sleep (self .refresh_interval )
510
+ self .invalidate ()
511
+
512
+ if self .refresh_interval :
513
+ self .create_background_task (auto_refresh ())
514
+
491
515
def _update_invalidate_events (self ) -> None :
492
516
"""
493
517
Make sure to attach 'invalidate' handlers to all invalidate events in
@@ -612,7 +636,7 @@ def read_from_input() -> None:
612
636
counter = flush_counter
613
637
614
638
# Automatically flush keys.
615
- ensure_future (auto_flush_input (counter ))
639
+ self . create_background_task (auto_flush_input (counter ))
616
640
617
641
async def auto_flush_input (counter : int ) -> None :
618
642
# Flush input after timeout.
@@ -638,6 +662,7 @@ def flush_input() -> None:
638
662
# Draw UI.
639
663
self ._request_absolute_cursor_position ()
640
664
self ._redraw ()
665
+ self ._start_auto_refresh_task ()
641
666
642
667
has_sigwinch = hasattr (signal , 'SIGWINCH' ) and in_main_thread ()
643
668
if has_sigwinch :
@@ -696,6 +721,12 @@ async def _run_async2() -> _AppResult:
696
721
try :
697
722
result = await _run_async ()
698
723
finally :
724
+ # Wait for the background tasks to be done. This needs to
725
+ # go in the finally! If `_run_async` raises
726
+ # `KeyboardInterrupt`, we still want to wait for the
727
+ # background tasks.
728
+ await self .cancel_and_wait_for_background_tasks ()
729
+
699
730
# Set the `_is_running` flag to `False`. Normally this
700
731
# happened already in the finally block in `run_async`
701
732
# above, but in case of exceptions, that's not always the
@@ -717,9 +748,8 @@ def run(self, pre_run: Optional[Callable[[], None]] = None,
717
748
loop = get_event_loop ()
718
749
719
750
def run () -> _AppResult :
720
- f = ensure_future (self .run_async (pre_run = pre_run ))
721
- get_event_loop ().run_until_complete (f )
722
- return f .result ()
751
+ coro = self .run_async (pre_run = pre_run )
752
+ return get_event_loop ().run_until_complete (coro )
723
753
724
754
def handle_exception (loop , context : Dict [str , Any ]) -> None :
725
755
" Print the exception, using run_in_terminal. "
@@ -752,6 +782,32 @@ async def in_term() -> None:
752
782
else :
753
783
return run ()
754
784
785
+ def create_background_task (self , coroutine : Awaitable [None ]) -> None :
786
+ """
787
+ Start a background task (coroutine) for the running application.
788
+ If asyncio had nurseries like Trio, we would create a nursery in
789
+ `Application.run_async`, and run the given coroutine in that nursery.
790
+ """
791
+ self .background_tasks .append (get_event_loop ().create_task (coroutine ))
792
+
793
+ async def cancel_and_wait_for_background_tasks (self ) -> None :
794
+ """
795
+ Cancel all background tasks, and wait for the cancellation to be done.
796
+ If any of the background tasks raised an exception, this will also
797
+ propagate the exception.
798
+
799
+ (If we had nurseries like Trio, this would be the `__aexit__` of a
800
+ nursery.)
801
+ """
802
+ for task in self .background_tasks :
803
+ task .cancel ()
804
+
805
+ for task in self .background_tasks :
806
+ try :
807
+ await task
808
+ except CancelledError :
809
+ pass
810
+
755
811
def cpr_not_supported_callback (self ) -> None :
756
812
"""
757
813
Called when we don't receive the cursor position response in time.
0 commit comments