diff --git a/term_img/cli.py b/term_img/cli.py index 640b8931..53840e21 100644 --- a/term_img/cli.py +++ b/term_img/cli.py @@ -394,7 +394,7 @@ def check_arg(name: str, check: Callable[[Any], bool], msg: str): ), ) - anim_options = general.add_mutually_exclusive_group() + anim_options = parser.add_argument_group("Animation Options (General)") anim_options.add_argument( "-f", "--frame-duration", @@ -405,6 +405,17 @@ def check_arg(name: str, check: Callable[[Any], bool], msg: str): "(default: Determined per image from it's metadata OR 0.1)" ), ) + anim_options.add_argument( + "-R", + "--repeat", + type=int, + default=-1, + metavar="N", + help=( + "Number of times to repeat all frames of an animated image; A negative " + "count implies an infinite loop (default: -1)" + ), + ) anim_options.add_argument( "--no-anim", action="store_true", @@ -740,6 +751,7 @@ def check_arg(name: str, check: Callable[[Any], bool], msg: str): "Number of grid renderers must be non-negative", ), ("getters", lambda x: x > 0, "Number of getters must be greater than zero"), + ("repeat", lambda x: x != 0, "Repeat count must be non-zero"), ): if not check_arg(*details): return INVALID_ARG @@ -910,6 +922,7 @@ def check_arg(name: str, check: Callable[[Any], bool], msg: str): ), scroll=args.scroll, animate=not args.no_anim, + repeat=args.repeat, check_size=not args.oversize, ) diff --git a/term_img/tui/__init__.py b/term_img/tui/__init__.py index 5c770a12..05b27846 100644 --- a/term_img/tui/__init__.py +++ b/term_img/tui/__init__.py @@ -34,6 +34,7 @@ def init( main.GRID_RENDERERS = args.grid_renderers main.MAX_PIXELS = args.max_pixels main.NO_ANIMATION = args.no_anim + main.REPEAT = args.repeat main.RECURSIVE = args.recursive main.SHOW_HIDDEN = args.all main.loop = Loop(main_widget, palette, unhandled_input=process_input) diff --git a/term_img/tui/main.py b/term_img/tui/main.py index b15d4b50..13a3d867 100644 --- a/term_img/tui/main.py +++ b/term_img/tui/main.py @@ -57,6 +57,8 @@ def next_frame(*_) -> None: image_box.original_widget is image # In case you switch from and back to the image within one frame duration and image._animator is animator + # The animator is not yet exhausted; repeat count is not yet zero + and image._animator.gi_frame and (not forced_render or image._force_render) ): image._frame_changed = True @@ -93,7 +95,9 @@ def next_frame(*_) -> None: del image._forced_anim_size_hash frame_duration = FRAME_DURATION or image._image._frame_duration - image._animator = ImageIterator(image._image, -1, f"1.1{image._alpha}")._animator + animator = image._animator = ImageIterator( + image._image, REPEAT, f"1.1{image._alpha}" + )._animator # `Image.render()` checks for this. It has to be set here since `ImageIterator` # doesn't set it until the first `next()` is called. @@ -744,5 +748,6 @@ def update_screen(): GRID_RENDERERS = None MAX_PIXELS = None NO_ANIMATION = None +REPEAT = None RECURSIVE = None SHOW_HIDDEN = None diff --git a/term_img/tui/widgets.py b/term_img/tui/widgets.py index 85060462..d010d27a 100644 --- a/term_img/tui/widgets.py +++ b/term_img/tui/widgets.py @@ -235,7 +235,10 @@ def render(self, size: Tuple[int, int], focus: bool = False) -> urwid.Canvas: if hasattr(self, "_animator"): if self._frame_changed: - self._frame = next(self._animator) + try: + self._frame = next(self._animator) + except StopIteration: + canv = __class__._placeholder.render(size) self._frame_changed = False self._frame_size_hash = hash(size) elif hash(size) != self._frame_size_hash: