diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index bdd02b0e1..26dbe121b 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -563,7 +563,7 @@ Key Value Default Description **Keys for animation control** ----------------------------------------------------------- -``s`` Positive integer ``0`` ``1`` - start animation, ``>1`` - stop animation +``s`` Positive integer ``0`` ``1`` - stop animation, ``2`` - run animation, but wait for new frames, ``3`` - run animation ``r`` Positive integer ``0`` The 1-based frame number of the frame that is being affected ``z`` 32-bit integer ``0`` The gap (in milliseconds) of this frame from the next one. A value of zero is ignored. Negative values create a *gapless* frame. diff --git a/kittens/icat/main.py b/kittens/icat/main.py index 46c2e051c..801d11b02 100755 --- a/kittens/icat/main.py +++ b/kittens/icat/main.py @@ -102,7 +102,7 @@ but you can turn it off or on explicitly, if needed. --silent type=bool-set -Do not print out anything to stdout during operation. +Do not print out anything to STDOUT during operation. --z-index -z @@ -112,9 +112,17 @@ a double minus for values under the threshold for drawing images under cell back colors. For example, --1 evaluates as -1,073,741,825. +--loop -l +default=-1 +type=int +Number of times to loop animations. Negative values loop forever. Zero means +only the first frame of the animation is displayed. Otherwise, the animation +is looped the specified number of times. + + --hold type=bool-set -Wait for keypress before exiting after displaying the images. +Wait for a key press before exiting after displaying the images. ''' @@ -236,7 +244,7 @@ def show( write_chunked(cmd, data) -def show_frames(frame_data: RenderedImage, use_number: int) -> None: +def show_frames(frame_data: RenderedImage, use_number: int, loops: int) -> None: transmit_cmd = GraphicsCommand() transmit_cmd.a = 'f' transmit_cmd.I = use_number # noqa @@ -245,7 +253,7 @@ def show_frames(frame_data: RenderedImage, use_number: int) -> None: transmit_cmd.t = 't' transmit_cmd.f = 24 if frame_data.mode == 'rgb' else 32 - def control(frame_number: int = 0, loops: Optional[int] = None, gap: Optional[int] = 0, start_animation: bool = False) -> None: + def control(frame_number: int = 0, loops: Optional[int] = None, gap: Optional[int] = 0, animation_control: int = 0) -> None: cmd = GraphicsCommand() cmd.a = 'a' cmd.I = use_number # noqa @@ -254,8 +262,8 @@ def show_frames(frame_data: RenderedImage, use_number: int) -> None: cmd.v = loops + 1 if gap is not None: cmd.z = gap if gap > 0 else -1 - if start_animation: - cmd.s = 1 + if animation_control: + cmd.s = animation_control write_gr_cmd(cmd) anchor_frame = 0 @@ -265,7 +273,7 @@ def show_frames(frame_data: RenderedImage, use_number: int) -> None: if frame.dispose < Dispose.previous: anchor_frame = frame_number if frame_number == 1: - control(frame_number, gap=frame.gap, loops=1) + control(frame_number, gap=frame.gap, loops=None if loops < 1 else loops) continue if frame.dispose is Dispose.previous: if anchor_frame != frame_number: @@ -283,7 +291,9 @@ def show_frames(frame_data: RenderedImage, use_number: int) -> None: with open(frame.path, 'rb') as f: data = f.read() write_chunked(transmit_cmd, data) - control(loops=0, start_animation=True) + if frame_number == 2: + control(animation_control=2) + control(animation_control=3) def parse_z_index(val: str) -> int: @@ -318,7 +328,7 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil else: fmt = 24 if m.mode == 'rgb' else 32 transmit_mode = 't' - if len(m) == 1: + if len(m) == 1 or args.loop == 0: outfile, width, height = render_as_single_image(path, m, available_width, available_height, args.scale_up) else: import struct @@ -332,7 +342,7 @@ def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfil align=args.align, place=parsed_opts.place, use_number=use_number ) if use_number: - show_frames(frame_data, use_number) + show_frames(frame_data, use_number, args.loop) if not can_transfer_with_files: for fr in frame_data.frames: with contextlib.suppress(FileNotFoundError): diff --git a/kitty/graphics.c b/kitty/graphics.c index 446e0af15..2b1c5e571 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -776,7 +776,7 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree rd->texture_id = img->texture_id; img->is_drawn = true; } - if (img->is_drawn && !was_drawn && img->animation_enabled && img->extra_framecnt && img->animation_duration) { + if (img->is_drawn && !was_drawn && img->animation_state != ANIMATION_STOPPED && img->extra_framecnt && img->animation_duration) { self->has_images_needing_animation = true; global_state.check_for_active_animated_images = true; } @@ -806,7 +806,7 @@ grman_update_layers(GraphicsManager *self, unsigned int scrolled_by, float scree #define _frame_number num_lines #define _other_frame_number num_cells #define _gap z_index -#define _animation_enabled data_width +#define _animation_state data_width #define _blend_mode cell_x_offset #define _bgcolor cell_y_offset #define _loop_count data_height @@ -1102,6 +1102,10 @@ handle_animation_frame_load_command(GraphicsManager *self, GraphicsCommand *g, I ABRT("ENOSPC", "Failed to cache data for image frame"); } img->animation_duration += frame->gap; + if (img->animation_state == ANIMATION_LOADING) { + self->has_images_needing_animation = true; + global_state.check_for_active_animated_images = true; + } } else { frame = frame_for_number(img, frame_number); if (!frame) ABRT("EINVAL", "No frame with number: %u found", frame_number); @@ -1199,11 +1203,22 @@ handle_animation_control_command(GraphicsManager *self, bool *is_dirty, const Gr update_current_frame(self, img, NULL); } } - if (g->_animation_enabled) { - bool was_enabled = img->animation_enabled; - img->animation_enabled = g->_animation_enabled == 1; - if (img->animation_enabled) { - if (!was_enabled) img->current_frame_shown_at = monotonic(); + if (g->_animation_state) { + AnimationState old_state = img->animation_state; + switch(g->_animation_state) { + case 1: + img->animation_state = ANIMATION_STOPPED; break; + case 2: + img->animation_state = ANIMATION_LOADING; break; + case 3: + img->animation_state = ANIMATION_RUNNING; break; + default: + break; + } + if (img->animation_state == ANIMATION_STOPPED) { + img->current_loop = 0; + } else { + if (old_state == ANIMATION_STOPPED) img->current_frame_shown_at = monotonic(); self->has_images_needing_animation = true; global_state.check_for_active_animated_images = true; } @@ -1217,7 +1232,7 @@ handle_animation_control_command(GraphicsManager *self, bool *is_dirty, const Gr static inline bool image_is_animatable(const Image *img) { - return img->animation_enabled && img->extra_framecnt && img->is_drawn && img->animation_duration && ( + return img->animation_state != ANIMATION_STOPPED && img->extra_framecnt && img->is_drawn && img->animation_duration && ( !img->max_loops || img->current_loop < img->max_loops); } @@ -1239,6 +1254,7 @@ scan_active_animations(GraphicsManager *self, const monotonic_t now, monotonic_t do { uint32_t next = (img->current_frame_index + 1) % (img->extra_framecnt + 1); if (!next) { + if (img->animation_state == ANIMATION_LOADING) goto skip_image; if (++img->current_loop >= img->max_loops && img->max_loops) goto skip_image; } img->current_frame_index = next; @@ -1571,10 +1587,10 @@ image_as_dict(GraphicsManager *self, Image *img) { } CoalescedFrameData cfd = get_coalesced_frame_data(self, img, &img->root_frame); if (!cfd.buf) { PyErr_SetString(PyExc_RuntimeError, "Failed to get data for root frame"); return NULL; } - PyObject *ans = Py_BuildValue("{sI sI sI sI sK sI sI " "sO sO sO " "sI sI sI " "sI sy# sN}", + PyObject *ans = Py_BuildValue("{sI sI sI sI sK sI sI " "sO sI sO " "sI sI sI " "sI sy# sN}", U(texture_id), U(client_id), U(width), U(height), U(internal_id), U(refcnt), U(client_number), - B(data_loaded), B(animation_enabled), "is_4byte_aligned", img->root_frame.is_4byte_aligned ? Py_True : Py_False, + B(data_loaded), U(animation_state), "is_4byte_aligned", img->root_frame.is_4byte_aligned ? Py_True : Py_False, U(current_frame_index), "root_frame_gap", img->root_frame.gap, U(current_frame_index), diff --git a/kitty/graphics.h b/kitty/graphics.h index aae91679f..5b07fba95 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -34,6 +34,7 @@ typedef struct { bool is_opaque, is_4byte_aligned, alpha_blend; } Frame; +typedef enum { ANIMATION_STOPPED = 0, ANIMATION_LOADING = 1, ANIMATION_RUNNING = 2} AnimationState; typedef struct { uint32_t texture_id, client_id, client_number, width, height; @@ -47,7 +48,8 @@ typedef struct { size_t refcnt, refcap, extra_framecnt; monotonic_t atime; size_t used_storage; - bool animation_enabled, is_drawn; + bool is_drawn; + AnimationState animation_state; uint32_t max_loops, current_loop; monotonic_t current_frame_shown_at; } Image;