summaryrefslogtreecommitdiff
path: root/files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff
diff options
context:
space:
mode:
authorShipwreckt <me@shipwreckt.co.uk>2025-10-22 22:23:00 +0100
committerShipwreckt <me@shipwreckt.co.uk>2025-10-22 22:23:00 +0100
commit1ae24861a06d773836fb674814aa03df90bbb095 (patch)
tree23c9ae7a23593be5aa1d78b1cb0d9b0cc7a255a1 /files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff
parente63a16b509b05993fc7900b6296ba8601e343976 (diff)
Updated install script, some configs, added vimwiki, added doas def
Diffstat (limited to 'files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff')
-rw-r--r--files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff7324
1 files changed, 0 insertions, 7324 deletions
diff --git a/files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff b/files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff
deleted file mode 100644
index 6049fe2..0000000
--- a/files/config/suckless/st/patches/st-kitty-graphics-20240922-a0274bc.diff
+++ /dev/null
@@ -1,7324 +0,0 @@
-From 25d9cca81ce48141de7f6a823b006dddaafd9de8 Mon Sep 17 00:00:00 2001
-From: Sergei Grechanik <sergei.grechanik@gmail.com>
-Date: Sun, 22 Sep 2024 08:36:05 -0700
-Subject: [PATCH] Kitty graphics protocol support 7b717e3 2024-09-22
-
-This patch implements the kitty graphics protocol in st.
-See https://github.com/sergei-grechanik/st-graphics
-Created by squashing the graphics branch, the most recent
-commit is 7b717e38b1739e11356b34df9fdfdfa339960864 (2024-09-22).
-
-Squashed on top of a0274bc20e11d8672bb2953fdd1d3010c0e708c5
-
-Note that the following files were excluded from the squash:
- .clang-format
- README.md
- generate-rowcolumn-helpers.py
- rowcolumn-diacritics.txt
- rowcolumn_diacritics.sh
----
- Makefile | 4 +-
- config.def.h | 46 +-
- config.mk | 5 +-
- graphics.c | 3812 ++++++++++++++++++++++++++++++++
- graphics.h | 107 +
- icat-mini.sh | 801 +++++++
- khash.h | 627 ++++++
- kvec.h | 90 +
- rowcolumn_diacritics_helpers.c | 391 ++++
- st.c | 279 ++-
- st.h | 84 +-
- st.info | 6 +
- win.h | 3 +
- x.c | 411 +++-
- 14 files changed, 6615 insertions(+), 51 deletions(-)
- create mode 100644 graphics.c
- create mode 100644 graphics.h
- create mode 100755 icat-mini.sh
- create mode 100644 khash.h
- create mode 100644 kvec.h
- create mode 100644 rowcolumn_diacritics_helpers.c
-
-diff --git a/Makefile b/Makefile
-index 15db421..e79b89e 100644
---- a/Makefile
-+++ b/Makefile
-@@ -4,7 +4,7 @@
-
- include config.mk
-
--SRC = st.c x.c
-+SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c
- OBJ = $(SRC:.c=.o)
-
- all: st
-@@ -16,7 +16,7 @@ config.h:
- $(CC) $(STCFLAGS) -c $<
-
- st.o: config.h st.h win.h
--x.o: arg.h config.h st.h win.h
-+x.o: arg.h config.h st.h win.h graphics.h
-
- $(OBJ): config.h config.mk
-
-diff --git a/config.def.h b/config.def.h
-index 2cd740a..4aadbbc 100644
---- a/config.def.h
-+++ b/config.def.h
-@@ -8,6 +8,13 @@
- static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true";
- static int borderpx = 2;
-
-+/* How to align the content in the window when the size of the terminal
-+ * doesn't perfectly match the size of the window. The values are percentages.
-+ * 50 means center, 0 means flush left/top, 100 means flush right/bottom.
-+ */
-+static int anysize_halign = 50;
-+static int anysize_valign = 50;
-+
- /*
- * What program is execed by st depends of these precedence rules:
- * 1: program passed with -e
-@@ -23,7 +30,8 @@ char *scroll = NULL;
- char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400";
-
- /* identification sequence returned in DA and DECID */
--char *vtiden = "\033[?6c";
-+/* By default, use the same one as kitty. */
-+char *vtiden = "\033[?62c";
-
- /* Kerning / character bounding-box multipliers */
- static float cwscale = 1.0;
-@@ -163,6 +171,28 @@ static unsigned int mousebg = 0;
- */
- static unsigned int defaultattr = 11;
-
-+/*
-+ * Graphics configuration
-+ */
-+
-+/// The template for the cache directory.
-+const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX";
-+/// The max size of a single image file, in bytes.
-+unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024;
-+/// The max size of the cache, in bytes.
-+unsigned graphics_total_file_cache_size = 300 * 1024 * 1024;
-+/// The max ram size of an image or placement, in bytes.
-+unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024;
-+/// The max total size of all images loaded into RAM.
-+unsigned graphics_max_total_ram_size = 300 * 1024 * 1024;
-+/// The max total number of image placements and images.
-+unsigned graphics_max_total_placements = 4096;
-+/// The ratio by which limits can be exceeded. This is to reduce the frequency
-+/// of image removal.
-+double graphics_excess_tolerance_ratio = 0.05;
-+/// The minimum delay between redraws caused by animations, in milliseconds.
-+unsigned graphics_animation_min_delay = 20;
-+
- /*
- * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set).
- * Note that if you want to use ShiftMask with selmasks, set this to an other
-@@ -170,12 +200,18 @@ static unsigned int defaultattr = 11;
- */
- static uint forcemousemod = ShiftMask;
-
-+/* Internal keyboard shortcuts. */
-+#define MODKEY Mod1Mask
-+#define TERMMOD (ControlMask|ShiftMask)
-+
- /*
- * Internal mouse shortcuts.
- * Beware that overloading Button1 will disable the selection.
- */
- static MouseShortcut mshortcuts[] = {
- /* mask button function argument release */
-+ { TERMMOD, Button3, previewimage, {.s = "feh"} },
-+ { TERMMOD, Button2, showimageinfo, {}, 1 },
- { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
- { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
- { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
-@@ -183,10 +219,6 @@ static MouseShortcut mshortcuts[] = {
- { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} },
- };
-
--/* Internal keyboard shortcuts. */
--#define MODKEY Mod1Mask
--#define TERMMOD (ControlMask|ShiftMask)
--
- static Shortcut shortcuts[] = {
- /* mask keysym function argument */
- { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
-@@ -201,6 +233,10 @@ static Shortcut shortcuts[] = {
- { TERMMOD, XK_Y, selpaste, {.i = 0} },
- { ShiftMask, XK_Insert, selpaste, {.i = 0} },
- { TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
-+ { TERMMOD, XK_F1, togglegrdebug, {.i = 0} },
-+ { TERMMOD, XK_F6, dumpgrstate, {.i = 0} },
-+ { TERMMOD, XK_F7, unloadimages, {.i = 0} },
-+ { TERMMOD, XK_F8, toggleimages, {.i = 0} },
- };
-
- /*
-diff --git a/config.mk b/config.mk
-index fdc29a7..cb2875c 100644
---- a/config.mk
-+++ b/config.mk
-@@ -14,9 +14,12 @@ PKG_CONFIG = pkg-config
-
- # includes and libs
- INCS = -I$(X11INC) \
-+ `$(PKG_CONFIG) --cflags imlib2` \
- `$(PKG_CONFIG) --cflags fontconfig` \
- `$(PKG_CONFIG) --cflags freetype2`
--LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
-+LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender \
-+ `$(PKG_CONFIG) --libs imlib2` \
-+ `$(PKG_CONFIG) --libs zlib` \
- `$(PKG_CONFIG) --libs fontconfig` \
- `$(PKG_CONFIG) --libs freetype2`
-
-diff --git a/graphics.c b/graphics.c
-new file mode 100644
-index 0000000..64e6fe0
---- /dev/null
-+++ b/graphics.c
-@@ -0,0 +1,3812 @@
-+/* The MIT License
-+
-+ Copyright (c) 2021-2024 Sergei Grechanik <sergei.grechanik@gmail.com>
-+
-+ Permission is hereby granted, free of charge, to any person obtaining
-+ a copy of this software and associated documentation files (the
-+ "Software"), to deal in the Software without restriction, including
-+ without limitation the rights to use, copy, modify, merge, publish,
-+ distribute, sublicense, and/or sell copies of the Software, and to
-+ permit persons to whom the Software is furnished to do so, subject to
-+ the following conditions:
-+
-+ The above copyright notice and this permission notice shall be
-+ included in all copies or substantial portions of the Software.
-+
-+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-+ SOFTWARE.
-+*/
-+
-+////////////////////////////////////////////////////////////////////////////////
-+//
-+// This file implements a subset of the kitty graphics protocol.
-+//
-+////////////////////////////////////////////////////////////////////////////////
-+
-+#define _POSIX_C_SOURCE 200809L
-+
-+#include <zlib.h>
-+#include <Imlib2.h>
-+#include <X11/Xlib.h>
-+#include <X11/extensions/Xrender.h>
-+#include <assert.h>
-+#include <ctype.h>
-+#include <spawn.h>
-+#include <stdarg.h>
-+#include <stdio.h>
-+#include <stdlib.h>
-+#include <string.h>
-+#include <sys/stat.h>
-+#include <time.h>
-+#include <unistd.h>
-+#include <errno.h>
-+
-+#include "graphics.h"
-+#include "khash.h"
-+#include "kvec.h"
-+
-+extern char **environ;
-+
-+#define MAX_FILENAME_SIZE 256
-+#define MAX_INFO_LEN 256
-+#define MAX_IMAGE_RECTS 20
-+
-+/// The type used in this file to represent time. Used both for time differences
-+/// and absolute times (as milliseconds since an arbitrary point in time, see
-+/// `initialization_time`).
-+typedef int64_t Milliseconds;
-+
-+enum ScaleMode {
-+ SCALE_MODE_UNSET = 0,
-+ /// Stretch or shrink the image to fill the box, ignoring aspect ratio.
-+ SCALE_MODE_FILL = 1,
-+ /// Preserve aspect ratio and fit to width or to height so that the
-+ /// whole image is visible.
-+ SCALE_MODE_CONTAIN = 2,
-+ /// Do not scale. The image may be cropped if the box is too small.
-+ SCALE_MODE_NONE = 3,
-+ /// Do not scale, unless the box is too small, in which case the image
-+ /// will be shrunk like with `SCALE_MODE_CONTAIN`.
-+ SCALE_MODE_NONE_OR_CONTAIN = 4,
-+};
-+
-+enum AnimationState {
-+ ANIMATION_STATE_UNSET = 0,
-+ /// The animation is stopped. Display the current frame, but don't
-+ /// advance to the next one.
-+ ANIMATION_STATE_STOPPED = 1,
-+ /// Run the animation to then end, then wait for the next frame.
-+ ANIMATION_STATE_LOADING = 2,
-+ /// Run the animation in a loop.
-+ ANIMATION_STATE_LOOPING = 3,
-+};
-+
-+/// The status of an image. Each image uploaded to the terminal is cached on
-+/// disk, then it is loaded to ram when needed.
-+enum ImageStatus {
-+ STATUS_UNINITIALIZED = 0,
-+ STATUS_UPLOADING = 1,
-+ STATUS_UPLOADING_ERROR = 2,
-+ STATUS_UPLOADING_SUCCESS = 3,
-+ STATUS_RAM_LOADING_ERROR = 4,
-+ STATUS_RAM_LOADING_IN_PROGRESS = 5,
-+ STATUS_RAM_LOADING_SUCCESS = 6,
-+};
-+
-+const char *image_status_strings[6] = {
-+ "STATUS_UNINITIALIZED",
-+ "STATUS_UPLOADING",
-+ "STATUS_UPLOADING_ERROR",
-+ "STATUS_UPLOADING_SUCCESS",
-+ "STATUS_RAM_LOADING_ERROR",
-+ "STATUS_RAM_LOADING_SUCCESS",
-+};
-+
-+enum ImageUploadingFailure {
-+ ERROR_OVER_SIZE_LIMIT = 1,
-+ ERROR_CANNOT_OPEN_CACHED_FILE = 2,
-+ ERROR_UNEXPECTED_SIZE = 3,
-+ ERROR_CANNOT_COPY_FILE = 4,
-+};
-+
-+const char *image_uploading_failure_strings[5] = {
-+ "NO_ERROR",
-+ "ERROR_OVER_SIZE_LIMIT",
-+ "ERROR_CANNOT_OPEN_CACHED_FILE",
-+ "ERROR_UNEXPECTED_SIZE",
-+ "ERROR_CANNOT_COPY_FILE",
-+};
-+
-+////////////////////////////////////////////////////////////////////////////////
-+//
-+// We use the following structures to represent images and placements:
-+//
-+// - Image: this is the main structure representing an image, usually created
-+// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id,
-+// specified by 'i='). An image may have multiple frames (ImageFrame) and
-+// placements (ImagePlacement).
-+//
-+// - ImageFrame: represents a single frame of an image, usually created by
-+// the action 'a=f' (and the first frame is created with the image itself).
-+// Each frame has an index and also:
-+// - a file containing the frame data (considered to be "on disk", although
-+// it's probably in tmpfs),
-+// - an imlib object containing the fully composed frame (i.e. the frame
-+// data from the file composed onto the background frame or color). It is
-+// not ready for display yet, because it needs to be scaled and uploaded
-+// to the X server.
-+//
-+// - ImagePlacement: represents a placement of an image, created by 'a=p' and
-+// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also
-+// each placement has an array of pixmaps: one for each frame of the image.
-+// Each pixmap is a scaled and uploaded image ready to be displayed.
-+//
-+// Images are store in the `images` hash table, mapping image ids to Image
-+// objects (allocated on the heap).
-+//
-+// Placements are stored in the `placements` hash table of each Image object,
-+// mapping placement ids to ImagePlacement objects (also allocated on the heap).
-+//
-+// ImageFrames are stored in the `first_frame` field and in the
-+// `frames_beyond_the_first` array of each Image object. They are stored by
-+// value, so ImageFrame pointer may be invalidated when frames are
-+// added/deleted, be careful.
-+//
-+////////////////////////////////////////////////////////////////////////////////
-+
-+struct Image;
-+struct ImageFrame;
-+struct ImagePlacement;
-+
-+KHASH_MAP_INIT_INT(id2image, struct Image *)
-+KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *)
-+
-+typedef struct ImageFrame {
-+ /// The image this frame belongs to.
-+ struct Image *image;
-+ /// The 1-based index of the frame. Zero if the frame isn't initialized.
-+ int index;
-+ /// The last time when the frame was displayed or otherwise touched.
-+ Milliseconds atime;
-+ /// The background color of the frame in the 0xRRGGBBAA format.
-+ uint32_t background_color;
-+ /// The index of the background frame. Zero to use the color instead.
-+ int background_frame_index;
-+ /// The duration of the frame in milliseconds.
-+ int gap;
-+ /// The expected size of the frame image file (specified with 'S='),
-+ /// used to check if uploading succeeded.
-+ unsigned expected_size;
-+ /// Format specification (see the `f=` key).
-+ int format;
-+ /// Pixel width and height of the non-composed (on-disk) frame data. May
-+ /// differ from the image (i.e. first frame) dimensions.
-+ int data_pix_width, data_pix_height;
-+ /// The offset of the frame relative to the first frame.
-+ int x, y;
-+ /// Compression mode (see the `o=` key).
-+ char compression;
-+ /// The status (see `ImageStatus`).
-+ char status;
-+ /// The reason of uploading failure (see `ImageUploadingFailure`).
-+ char uploading_failure;
-+ /// Whether failures and successes should be reported ('q=').
-+ char quiet;
-+ /// Whether to blend the frame with the background or replace it.
-+ char blend;
-+ /// The file corresponding to the on-disk cache, used when uploading.
-+ FILE *open_file;
-+ /// The size of the corresponding file cached on disk.
-+ unsigned disk_size;
-+ /// The imlib object containing the fully composed frame. It's not
-+ /// scaled for screen display yet.
-+ Imlib_Image imlib_object;
-+} ImageFrame;
-+
-+typedef struct Image {
-+ /// The client id (the one specified with 'i='). Must be nonzero.
-+ uint32_t image_id;
-+ /// The client id specified in the query command (`a=q`). This one must
-+ /// be used to create the response if it's non-zero.
-+ uint32_t query_id;
-+ /// The number specified in the transmission command (`I=`). If
-+ /// non-zero, it may be used to identify the image instead of the
-+ /// image_id, and it also should be mentioned in responses.
-+ uint32_t image_number;
-+ /// The last time when the image was displayed or otherwise touched.
-+ Milliseconds atime;
-+ /// The total duration of the animation in milliseconds.
-+ int total_duration;
-+ /// The total size of cached image files for all frames.
-+ int total_disk_size;
-+ /// The global index of the creation command. Used to decide which image
-+ /// is newer if they have the same image number.
-+ uint64_t global_command_index;
-+ /// The 1-based index of the currently displayed frame.
-+ int current_frame;
-+ /// The state of the animation, see `AnimationState`.
-+ char animation_state;
-+ /// The absolute time that is assumed to be the start of the current
-+ /// frame (in ms since initialization).
-+ Milliseconds current_frame_time;
-+ /// The absolute time of the last redraw (in ms since initialization).
-+ /// Used to check whether it's the first time we draw the image in the
-+ /// current redraw cycle.
-+ Milliseconds last_redraw;
-+ /// The absolute time of the next redraw (in ms since initialization).
-+ /// 0 means no redraw is scheduled.
-+ Milliseconds next_redraw;
-+ /// The unscaled pixel width and height of the image. Usually inherited
-+ /// from the first frame.
-+ int pix_width, pix_height;
-+ /// The first frame.
-+ ImageFrame first_frame;
-+ /// The array of frames beyond the first one.
-+ kvec_t(ImageFrame) frames_beyond_the_first;
-+ /// Image placements.
-+ khash_t(id2placement) *placements;
-+ /// The default placement.
-+ uint32_t default_placement;
-+ /// The initial placement id, specified with the transmission command,
-+ /// used to report success or failure.
-+ uint32_t initial_placement_id;
-+} Image;
-+
-+typedef struct ImagePlacement {
-+ /// The image this placement belongs to.
-+ Image *image;
-+ /// The id of the placement. Must be nonzero.
-+ uint32_t placement_id;
-+ /// The last time when the placement was displayed or otherwise touched.
-+ Milliseconds atime;
-+ /// The 1-based index of the protected pixmap. We protect a pixmap in
-+ /// gr_load_pixmap to avoid unloading it right after it was loaded.
-+ int protected_frame;
-+ /// Whether the placement is used only for Unicode placeholders.
-+ char virtual;
-+ /// The scaling mode (see `ScaleMode`).
-+ char scale_mode;
-+ /// Height and width in cells.
-+ uint16_t rows, cols;
-+ /// Top-left corner of the source rectangle ('x=' and 'y=').
-+ int src_pix_x, src_pix_y;
-+ /// Height and width of the source rectangle (zero if full image).
-+ int src_pix_width, src_pix_height;
-+ /// The image appropriately scaled and uploaded to the X server. This
-+ /// pixmap is premultiplied by alpha.
-+ Pixmap first_pixmap;
-+ /// The array of pixmaps beyond the first one.
-+ kvec_t(Pixmap) pixmaps_beyond_the_first;
-+ /// The dimensions of the cell used to scale the image. If cell
-+ /// dimensions are changed (font change), the image will be rescaled.
-+ uint16_t scaled_cw, scaled_ch;
-+ /// If true, do not move the cursor when displaying this placement
-+ /// (non-virtual placements only).
-+ char do_not_move_cursor;
-+} ImagePlacement;
-+
-+/// A rectangular piece of an image to be drawn.
-+typedef struct {
-+ uint32_t image_id;
-+ uint32_t placement_id;
-+ /// The position of the rectangle in pixels.
-+ int screen_x_pix, screen_y_pix;
-+ /// The starting row on the screen.
-+ int screen_y_row;
-+ /// The part of the whole image to be drawn, in cells. Starts are
-+ /// zero-based, ends are exclusive.
-+ int img_start_col, img_end_col, img_start_row, img_end_row;
-+ /// The current cell width and height in pixels.
-+ int cw, ch;
-+ /// Whether colors should be inverted.
-+ int reverse;
-+} ImageRect;
-+
-+/// Executes `code` for each frame of an image. Example:
-+///
-+/// foreach_frame(image, frame, {
-+/// printf("Frame %d\n", frame->index);
-+/// });
-+///
-+#define foreach_frame(image, framevar, code) { size_t __i; \
-+ for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \
-+ ImageFrame *framevar = \
-+ __i == 0 ? &(image).first_frame \
-+ : &kv_A((image).frames_beyond_the_first, __i - 1); \
-+ code; \
-+ } }
-+
-+/// Executes `code` for each pixmap of a placement. Example:
-+///
-+/// foreach_pixmap(placement, pixmap, {
-+/// ...
-+/// });
-+///
-+#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \
-+ for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \
-+ Pixmap pixmapvar = \
-+ __i == 0 ? (placement).first_pixmap \
-+ : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \
-+ code; \
-+ } }
-+
-+
-+static Image *gr_find_image(uint32_t image_id);
-+static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len);
-+static void gr_delete_image(Image *img);
-+static void gr_check_limits();
-+static char *gr_base64dec(const char *src, size_t *size);
-+static void sanitize_str(char *str, size_t max_len);
-+static const char *sanitized_filename(const char *str);
-+
-+/// The array of image rectangles to draw. It is reset each frame.
-+static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}};
-+/// The known images (including the ones being uploaded).
-+static khash_t(id2image) *images = NULL;
-+/// The total number of placements in all images.
-+static unsigned total_placement_count = 0;
-+/// The total size of all image files stored in the on-disk cache.
-+static int64_t images_disk_size = 0;
-+/// The total size of all images and placements loaded into ram.
-+static int64_t images_ram_size = 0;
-+/// The id of the last loaded image.
-+static uint32_t last_image_id = 0;
-+/// Current cell width and heigh in pixels.
-+static int current_cw = 0, current_ch = 0;
-+/// The id of the currently uploaded image (when using direct uploading).
-+static uint32_t current_upload_image_id = 0;
-+/// The index of the frame currently being uploaded.
-+static int current_upload_frame_index = 0;
-+/// The time when the graphics module was initialized.
-+static struct timespec initialization_time = {0};
-+/// The time when the current frame drawing started, used for debugging fps and
-+/// to calculate the current frame for animations.
-+static Milliseconds drawing_start_time;
-+/// The global index of the current command.
-+static uint64_t global_command_counter = 0;
-+/// The next redraw times for each row of the terminal. Used for animations.
-+/// 0 means no redraw is scheduled.
-+static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL};
-+/// The number of files loaded in the current redraw cycle.
-+static int this_redraw_cycle_loaded_files = 0;
-+/// The number of pixmaps loaded in the current redraw cycle.
-+static int this_redraw_cycle_loaded_pixmaps = 0;
-+
-+/// The directory where the cache files are stored.
-+static char cache_dir[MAX_FILENAME_SIZE - 16];
-+
-+/// The table used for color inversion.
-+static unsigned char reverse_table[256];
-+
-+// Declared in the header.
-+GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE;
-+char graphics_display_images = 1;
-+GraphicsCommandResult graphics_command_result = {0};
-+int graphics_next_redraw_delay = INT_MAX;
-+
-+// Defined in config.h
-+extern const char graphics_cache_dir_template[];
-+extern unsigned graphics_max_single_image_file_size;
-+extern unsigned graphics_total_file_cache_size;
-+extern unsigned graphics_max_single_image_ram_size;
-+extern unsigned graphics_max_total_ram_size;
-+extern unsigned graphics_max_total_placements;
-+extern double graphics_excess_tolerance_ratio;
-+extern unsigned graphics_animation_min_delay;
-+
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Basic helpers.
-+////////////////////////////////////////////////////////////////////////////////
-+
-+#define MIN(a, b) ((a) < (b) ? (a) : (b))
-+#define MAX(a, b) ((a) < (b) ? (b) : (a))
-+
-+/// Returns the difference between `end` and `start` in milliseconds.
-+static int64_t gr_timediff_ms(const struct timespec *end,
-+ const struct timespec *start) {
-+ return (end->tv_sec - start->tv_sec) * 1000 +
-+ (end->tv_nsec - start->tv_nsec) / 1000000;
-+}
-+
-+/// Returns the current time in milliseconds since the initialization.
-+static Milliseconds gr_now_ms() {
-+ struct timespec now;
-+ clock_gettime(CLOCK_MONOTONIC, &now);
-+ return gr_timediff_ms(&now, &initialization_time);
-+}
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Logging.
-+////////////////////////////////////////////////////////////////////////////////
-+
-+#define GR_LOG(...) \
-+ do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0)
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Basic image management functions (create, delete, find, etc).
-+////////////////////////////////////////////////////////////////////////////////
-+
-+/// Returns the 1-based index of the last frame. Note that you may want to use
-+/// `gr_last_uploaded_frame_index` instead since the last frame may be not
-+/// fully uploaded yet.
-+static inline int gr_last_frame_index(Image *img) {
-+ return kv_size(img->frames_beyond_the_first) + 1;
-+}
-+
-+/// Returns the frame with the given index. Returns NULL if the index is out of
-+/// bounds. The index is 1-based.
-+static ImageFrame *gr_get_frame(Image *img, int index) {
-+ if (!img)
-+ return NULL;
-+ if (index == 1)
-+ return &img->first_frame;
-+ if (2 <= index && index <= gr_last_frame_index(img))
-+ return &kv_A(img->frames_beyond_the_first, index - 2);
-+ return NULL;
-+}
-+
-+/// Returns the last frame of the image. Returns NULL if `img` is NULL.
-+static ImageFrame *gr_get_last_frame(Image *img) {
-+ if (!img)
-+ return NULL;
-+ return gr_get_frame(img, gr_last_frame_index(img));
-+}
-+
-+/// Returns the 1-based index of the last frame or the second-to-last frame if
-+/// the last frame is not fully uploaded yet.
-+static inline int gr_last_uploaded_frame_index(Image *img) {
-+ int last_index = gr_last_frame_index(img);
-+ if (last_index > 1 &&
-+ gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS)
-+ return last_index - 1;
-+ return last_index;
-+}
-+
-+/// Returns the pixmap for the frame with the given index. Returns 0 if the
-+/// index is out of bounds. The index is 1-based.
-+static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) {
-+ if (index == 1)
-+ return placement->first_pixmap;
-+ if (2 <= index &&
-+ index <= kv_size(placement->pixmaps_beyond_the_first) + 1)
-+ return kv_A(placement->pixmaps_beyond_the_first, index - 2);
-+ return 0;
-+}
-+
-+/// Sets the pixmap for the frame with the given index. The index is 1-based.
-+/// The array of pixmaps is resized if needed.
-+static void gr_set_frame_pixmap(ImagePlacement *placement, int index,
-+ Pixmap pixmap) {
-+ if (index == 1) {
-+ placement->first_pixmap = pixmap;
-+ return;
-+ }
-+ // Resize the array if needed.
-+ size_t old_size = kv_size(placement->pixmaps_beyond_the_first);
-+ if (old_size < index - 1) {
-+ kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2);
-+ for (size_t i = old_size; i < index - 1; i++)
-+ kv_A(placement->pixmaps_beyond_the_first, i) = 0;
-+ }
-+ kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap;
-+}
-+
-+/// Finds the image corresponding to the client id. Returns NULL if cannot find.
-+static Image *gr_find_image(uint32_t image_id) {
-+ khiter_t k = kh_get(id2image, images, image_id);
-+ if (k == kh_end(images))
-+ return NULL;
-+ Image *res = kh_value(images, k);
-+ return res;
-+}
-+
-+/// Finds the newest image corresponding to the image number. Returns NULL if
-+/// cannot find.
-+static Image *gr_find_image_by_number(uint32_t image_number) {
-+ if (image_number == 0)
-+ return NULL;
-+ Image *newest_img = NULL;
-+ Image *img = NULL;
-+ kh_foreach_value(images, img, {
-+ if (img->image_number == image_number &&
-+ (!newest_img || newest_img->global_command_index <
-+ img->global_command_index))
-+ newest_img = img;
-+ });
-+ if (!newest_img)
-+ GR_LOG("Image number %u not found\n", image_number);
-+ else
-+ GR_LOG("Found image number %u, its id is %u\n", image_number,
-+ img->image_id);
-+ return newest_img;
-+}
-+
-+/// Finds the placement corresponding to the id. If the placement id is 0,
-+/// returns some default placement.
-+static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) {
-+ if (!img)
-+ return NULL;
-+ if (placement_id == 0) {
-+ // Try to get the default placement.
-+ ImagePlacement *dflt = NULL;
-+ if (img->default_placement != 0)
-+ dflt = gr_find_placement(img, img->default_placement);
-+ if (dflt)
-+ return dflt;
-+ // If there is no default placement, return the first one and
-+ // set it as the default.
-+ kh_foreach_value(img->placements, dflt, {
-+ img->default_placement = dflt->placement_id;
-+ return dflt;
-+ });
-+ // If there are no placements, return NULL.
-+ return NULL;
-+ }
-+ khiter_t k = kh_get(id2placement, img->placements, placement_id);
-+ if (k == kh_end(img->placements))
-+ return NULL;
-+ ImagePlacement *res = kh_value(img->placements, k);
-+ return res;
-+}
-+
-+/// Finds the placement by image id and placement id.
-+static ImagePlacement *gr_find_image_and_placement(uint32_t image_id,
-+ uint32_t placement_id) {
-+ return gr_find_placement(gr_find_image(image_id), placement_id);
-+}
-+
-+/// Writes the name of the on-disk cache file to `out`. `max_len` should be the
-+/// size of `out`. The name will be something like
-+/// "/tmp/st-images-xxx/img-ID-FRAME".
-+static void gr_get_frame_filename(ImageFrame *frame, char *out,
-+ size_t max_len) {
-+ snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir,
-+ frame->image->image_id, frame->index);
-+}
-+
-+/// Returns the (estimation) of the RAM size used by the frame right now.
-+static unsigned gr_frame_current_ram_size(ImageFrame *frame) {
-+ if (!frame->imlib_object)
-+ return 0;
-+ return (unsigned)frame->image->pix_width * frame->image->pix_height * 4;
-+}
-+
-+/// Returns the (estimation) of the RAM size used by a single frame pixmap.
-+static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) {
-+ return (unsigned)placement->rows * placement->cols *
-+ placement->scaled_ch * placement->scaled_cw * 4;
-+}
-+
-+/// Returns the (estimation) of the RAM size used by the placemenet right now.
-+static unsigned gr_placement_current_ram_size(ImagePlacement *placement) {
-+ unsigned single_frame_size =
-+ gr_placement_single_frame_ram_size(placement);
-+ unsigned result = 0;
-+ foreach_pixmap(*placement, pixmap, {
-+ if (pixmap)
-+ result += single_frame_size;
-+ });
-+ return result;
-+}
-+
-+/// Unload the frame from RAM (i.e. delete the corresponding imlib object).
-+/// If the on-disk file of the frame is preserved, it can be reloaded later.
-+static void gr_unload_frame(ImageFrame *frame) {
-+ if (!frame->imlib_object)
-+ return;
-+
-+ unsigned frame_ram_size = gr_frame_current_ram_size(frame);
-+ images_ram_size -= frame_ram_size;
-+
-+ imlib_context_set_image(frame->imlib_object);
-+ imlib_free_image_and_decache();
-+ frame->imlib_object = NULL;
-+
-+ GR_LOG("After unloading image %u frame %u (atime %ld ms ago) "
-+ "ram: %ld KiB (- %u KiB)\n",
-+ frame->image->image_id, frame->index,
-+ drawing_start_time - frame->atime, images_ram_size / 1024,
-+ frame_ram_size / 1024);
-+}
-+
-+/// Unload all frames of the image.
-+static void gr_unload_all_frames(Image *img) {
-+ foreach_frame(*img, frame, {
-+ gr_unload_frame(frame);
-+ });
-+}
-+
-+/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps).
-+/// If the on-disk files or imlib objects of the corresponding image are
-+/// preserved, the placement can be reloaded later.
-+static void gr_unload_placement(ImagePlacement *placement) {
-+ unsigned placement_ram_size = gr_placement_current_ram_size(placement);
-+ images_ram_size -= placement_ram_size;
-+
-+ Display *disp = imlib_context_get_display();
-+ foreach_pixmap(*placement, pixmap, {
-+ if (pixmap)
-+ XFreePixmap(disp, pixmap);
-+ });
-+
-+ placement->first_pixmap = 0;
-+ placement->pixmaps_beyond_the_first.n = 0;
-+ placement->scaled_ch = placement->scaled_cw = 0;
-+
-+ GR_LOG("After unloading placement %u/%u (atime %ld ms ago) "
-+ "ram: %ld KiB (- %u KiB)\n",
-+ placement->image->image_id, placement->placement_id,
-+ drawing_start_time - placement->atime, images_ram_size / 1024,
-+ placement_ram_size / 1024);
-+}
-+
-+/// Unload a single pixmap of the placement from RAM.
-+static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) {
-+ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
-+ if (!pixmap)
-+ return;
-+
-+ Display *disp = imlib_context_get_display();
-+ XFreePixmap(disp, pixmap);
-+ gr_set_frame_pixmap(placement, frameidx, 0);
-+ images_ram_size -= gr_placement_single_frame_ram_size(placement);
-+
-+ GR_LOG("After unloading pixmap %ld of "
-+ "placement %u/%u (atime %ld ms ago) "
-+ "frame %u (atime %ld ms ago) "
-+ "ram: %ld KiB (- %u KiB)\n",
-+ pixmap, placement->image->image_id, placement->placement_id,
-+ drawing_start_time - placement->atime, frameidx,
-+ drawing_start_time -
-+ gr_get_frame(placement->image, frameidx)->atime,
-+ images_ram_size / 1024,
-+ gr_placement_single_frame_ram_size(placement) / 1024);
-+}
-+
-+/// Deletes the on-disk cache file corresponding to the frame. The in-ram image
-+/// object (if it exists) is not deleted, placements are not unloaded either.
-+static void gr_delete_imagefile(ImageFrame *frame) {
-+ // It may still be being loaded. Close the file in this case.
-+ if (frame->open_file) {
-+ fclose(frame->open_file);
-+ frame->open_file = NULL;
-+ }
-+
-+ if (frame->disk_size == 0)
-+ return;
-+
-+ char filename[MAX_FILENAME_SIZE];
-+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
-+ remove(filename);
-+
-+ unsigned disk_size = frame->disk_size;
-+ images_disk_size -= disk_size;
-+ frame->image->total_disk_size -= disk_size;
-+ frame->disk_size = 0;
-+
-+ GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) "
-+ "disk: %ld KiB (- %u KiB)\n",
-+ frame->image->image_id, frame->index,
-+ drawing_start_time - frame->atime, images_disk_size / 1024,
-+ disk_size / 1024);
-+}
-+
-+/// Deletes all on-disk cache files of the image (for each frame).
-+static void gr_delete_imagefiles(Image *img) {
-+ foreach_frame(*img, frame, {
-+ gr_delete_imagefile(frame);
-+ });
-+}
-+
-+/// Deletes the given placement: unloads, frees the object, but doesn't change
-+/// the `placements` hash table.
-+static void gr_delete_placement_keep_id(ImagePlacement *placement) {
-+ if (!placement)
-+ return;
-+ GR_LOG("Deleting placement %u/%u\n", placement->image->image_id,
-+ placement->placement_id);
-+ gr_unload_placement(placement);
-+ kv_destroy(placement->pixmaps_beyond_the_first);
-+ free(placement);
-+ total_placement_count--;
-+}
-+
-+/// Deletes all placements of `img`.
-+static void gr_delete_all_placements(Image *img) {
-+ ImagePlacement *placement = NULL;
-+ kh_foreach_value(img->placements, placement, {
-+ gr_delete_placement_keep_id(placement);
-+ });
-+ kh_clear(id2placement, img->placements);
-+}
-+
-+/// Deletes the given image: unloads, deletes the file, frees the Image object,
-+/// but doesn't change the `images` hash table.
-+static void gr_delete_image_keep_id(Image *img) {
-+ if (!img)
-+ return;
-+ GR_LOG("Deleting image %u\n", img->image_id);
-+ foreach_frame(*img, frame, {
-+ gr_delete_imagefile(frame);
-+ gr_unload_frame(frame);
-+ });
-+ kv_destroy(img->frames_beyond_the_first);
-+ gr_delete_all_placements(img);
-+ kh_destroy(id2placement, img->placements);
-+ free(img);
-+}
-+
-+/// Deletes the given image: unloads, deletes the file, frees the Image object,
-+/// and also removes it from `images`.
-+static void gr_delete_image(Image *img) {
-+ if (!img)
-+ return;
-+ uint32_t id = img->image_id;
-+ gr_delete_image_keep_id(img);
-+ khiter_t k = kh_get(id2image, images, id);
-+ kh_del(id2image, images, k);
-+}
-+
-+/// Deletes the given placement: unloads, frees the object, and also removes it
-+/// from `placements`.
-+static void gr_delete_placement(ImagePlacement *placement) {
-+ if (!placement)
-+ return;
-+ uint32_t id = placement->placement_id;
-+ Image *img = placement->image;
-+ gr_delete_placement_keep_id(placement);
-+ khiter_t k = kh_get(id2placement, img->placements, id);
-+ kh_del(id2placement, img->placements, k);
-+}
-+
-+/// Deletes all images and clears `images`.
-+static void gr_delete_all_images() {
-+ Image *img = NULL;
-+ kh_foreach_value(images, img, {
-+ gr_delete_image_keep_id(img);
-+ });
-+ kh_clear(id2image, images);
-+}
-+
-+/// Update the atime of the image.
-+static void gr_touch_image(Image *img) {
-+ img->atime = gr_now_ms();
-+}
-+
-+/// Update the atime of the frame.
-+static void gr_touch_frame(ImageFrame *frame) {
-+ frame->image->atime = frame->atime = gr_now_ms();
-+}
-+
-+/// Update the atime of the placement. Touches the images too.
-+static void gr_touch_placement(ImagePlacement *placement) {
-+ placement->image->atime = placement->atime = gr_now_ms();
-+}
-+
-+/// Creates a new image with the given id. If an image with that id already
-+/// exists, it is deleted first. If the provided id is 0, generates a
-+/// random id.
-+static Image *gr_new_image(uint32_t id) {
-+ if (id == 0) {
-+ do {
-+ id = rand();
-+ // Avoid IDs that don't need full 32 bits.
-+ } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 ||
-+ gr_find_image(id));
-+ GR_LOG("Generated random image id %u\n", id);
-+ }
-+ Image *img = gr_find_image(id);
-+ gr_delete_image_keep_id(img);
-+ GR_LOG("Creating image %u\n", id);
-+ img = malloc(sizeof(Image));
-+ memset(img, 0, sizeof(Image));
-+ img->placements = kh_init(id2placement);
-+ int ret;
-+ khiter_t k = kh_put(id2image, images, id, &ret);
-+ kh_value(images, k) = img;
-+ img->image_id = id;
-+ gr_touch_image(img);
-+ img->global_command_index = global_command_counter;
-+ return img;
-+}
-+
-+/// Creates a new frame at the end of the frame array. It may be the first frame
-+/// if there are no frames yet.
-+static ImageFrame *gr_append_new_frame(Image *img) {
-+ ImageFrame *frame = NULL;
-+ if (img->first_frame.index == 0 &&
-+ kv_size(img->frames_beyond_the_first) == 0) {
-+ frame = &img->first_frame;
-+ frame->index = 1;
-+ } else {
-+ frame = kv_pushp(ImageFrame, img->frames_beyond_the_first);
-+ memset(frame, 0, sizeof(ImageFrame));
-+ frame->index = kv_size(img->frames_beyond_the_first) + 1;
-+ }
-+ frame->image = img;
-+ gr_touch_frame(frame);
-+ GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id);
-+ return frame;
-+}
-+
-+/// Creates a new placement with the given id. If a placement with that id
-+/// already exists, it is deleted first. If the provided id is 0, generates a
-+/// random id.
-+static ImagePlacement *gr_new_placement(Image *img, uint32_t id) {
-+ if (id == 0) {
-+ do {
-+ // Currently we support only 24-bit IDs.
-+ id = rand() & 0xFFFFFF;
-+ // Avoid IDs that need only one byte.
-+ } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id));
-+ }
-+ ImagePlacement *placement = gr_find_placement(img, id);
-+ gr_delete_placement_keep_id(placement);
-+ GR_LOG("Creating placement %u/%u\n", img->image_id, id);
-+ placement = malloc(sizeof(ImagePlacement));
-+ memset(placement, 0, sizeof(ImagePlacement));
-+ total_placement_count++;
-+ int ret;
-+ khiter_t k = kh_put(id2placement, img->placements, id, &ret);
-+ kh_value(img->placements, k) = placement;
-+ placement->image = img;
-+ placement->placement_id = id;
-+ gr_touch_placement(placement);
-+ if (img->default_placement == 0)
-+ img->default_placement = id;
-+ return placement;
-+}
-+
-+static int64_t ceil_div(int64_t a, int64_t b) {
-+ return (a + b - 1) / b;
-+}
-+
-+/// Computes the best number of rows and columns for a placement if it's not
-+/// specified, and also adjusts the source rectangle size.
-+static void gr_infer_placement_size_maybe(ImagePlacement *placement) {
-+ // The size of the image.
-+ int image_pix_width = placement->image->pix_width;
-+ int image_pix_height = placement->image->pix_height;
-+ // Negative values are not allowed. Quietly set them to 0.
-+ if (placement->src_pix_x < 0)
-+ placement->src_pix_x = 0;
-+ if (placement->src_pix_y < 0)
-+ placement->src_pix_y = 0;
-+ if (placement->src_pix_width < 0)
-+ placement->src_pix_width = 0;
-+ if (placement->src_pix_height < 0)
-+ placement->src_pix_height = 0;
-+ // If the source rectangle is outside the image, truncate it.
-+ if (placement->src_pix_x > image_pix_width)
-+ placement->src_pix_x = image_pix_width;
-+ if (placement->src_pix_y > image_pix_height)
-+ placement->src_pix_y = image_pix_height;
-+ // If the source rectangle is not specified, use the whole image. If
-+ // it's partially outside the image, truncate it.
-+ if (placement->src_pix_width == 0 ||
-+ placement->src_pix_x + placement->src_pix_width > image_pix_width)
-+ placement->src_pix_width =
-+ image_pix_width - placement->src_pix_x;
-+ if (placement->src_pix_height == 0 ||
-+ placement->src_pix_y + placement->src_pix_height > image_pix_height)
-+ placement->src_pix_height =
-+ image_pix_height - placement->src_pix_y;
-+
-+ if (placement->cols != 0 && placement->rows != 0)
-+ return;
-+ if (placement->src_pix_width == 0 || placement->src_pix_height == 0)
-+ return;
-+ if (current_cw == 0 || current_ch == 0)
-+ return;
-+
-+ // If no size is specified, use the image size.
-+ if (placement->cols == 0 && placement->rows == 0) {
-+ placement->cols =
-+ ceil_div(placement->src_pix_width, current_cw);
-+ placement->rows =
-+ ceil_div(placement->src_pix_height, current_ch);
-+ return;
-+ }
-+
-+ // Some applications specify only one of the dimensions.
-+ if (placement->scale_mode == SCALE_MODE_CONTAIN) {
-+ // If we preserve aspect ratio and fit to width/height, the most
-+ // logical thing is to find the minimum size of the
-+ // non-specified dimension that allows the image to fit the
-+ // specified dimension.
-+ if (placement->cols == 0) {
-+ placement->cols = ceil_div(
-+ placement->src_pix_width * placement->rows *
-+ current_ch,
-+ placement->src_pix_height * current_cw);
-+ return;
-+ }
-+ if (placement->rows == 0) {
-+ placement->rows =
-+ ceil_div(placement->src_pix_height *
-+ placement->cols * current_cw,
-+ placement->src_pix_width * current_ch);
-+ return;
-+ }
-+ } else {
-+ // Otherwise we stretch the image or preserve the original size.
-+ // In both cases we compute the best number of columns from the
-+ // pixel size and cell size.
-+ // TODO: In the case of stretching it's not the most logical
-+ // thing to do, may need to revisit in the future.
-+ // Currently we switch to SCALE_MODE_CONTAIN when only one
-+ // of the dimensions is specified, so this case shouldn't
-+ // happen in practice.
-+ if (!placement->cols)
-+ placement->cols =
-+ ceil_div(placement->src_pix_width, current_cw);
-+ if (!placement->rows)
-+ placement->rows =
-+ ceil_div(placement->src_pix_height, current_ch);
-+ }
-+}
-+
-+/// Adjusts the current frame index if enough time has passed since the display
-+/// of the current frame. Also computes the time of the next redraw of this
-+/// image (`img->next_redraw`). The current time is passed as an argument so
-+/// that all animations are in sync.
-+static void gr_update_frame_index(Image *img, Milliseconds now) {
-+ if (img->current_frame == 0) {
-+ img->current_frame_time = now;
-+ img->current_frame = 1;
-+ img->next_redraw = now + MAX(1, img->first_frame.gap);
-+ return;
-+ }
-+ // If the animation is stopped, show the current frame.
-+ if (!img->animation_state ||
-+ img->animation_state == ANIMATION_STATE_STOPPED ||
-+ img->animation_state == ANIMATION_STATE_UNSET) {
-+ // The next redraw is never (unless the state is changed).
-+ img->next_redraw = 0;
-+ return;
-+ }
-+ int last_uploaded_frame_index = gr_last_uploaded_frame_index(img);
-+ // If we are loading and we reached the last frame, show the last frame.
-+ if (img->animation_state == ANIMATION_STATE_LOADING &&
-+ img->current_frame == last_uploaded_frame_index) {
-+ // The next redraw is never (unless the state is changed or
-+ // frames are added).
-+ img->next_redraw = 0;
-+ return;
-+ }
-+
-+ // Check how many milliseconds passed since the current frame was shown.
-+ int passed_ms = now - img->current_frame_time;
-+ // If the animation is looping and too much time has passes, we can
-+ // make a shortcut.
-+ if (img->animation_state == ANIMATION_STATE_LOOPING &&
-+ img->total_duration > 0 && passed_ms >= img->total_duration) {
-+ passed_ms %= img->total_duration;
-+ img->current_frame_time = now - passed_ms;
-+ }
-+ // Find the next frame.
-+ int original_frame_index = img->current_frame;
-+ while (1) {
-+ ImageFrame *frame = gr_get_frame(img, img->current_frame);
-+ if (!frame) {
-+ // The frame doesn't exist, go to the first frame.
-+ img->current_frame = 1;
-+ img->current_frame_time = now;
-+ img->next_redraw = now + MAX(1, img->first_frame.gap);
-+ return;
-+ }
-+ if (frame->gap >= 0 && passed_ms < frame->gap) {
-+ // Not enough time has passed, we are still in the same
-+ // frame, and it's not a gapless frame.
-+ img->next_redraw =
-+ img->current_frame_time + MAX(1, frame->gap);
-+ return;
-+ }
-+ // Otherwise go to the next frame.
-+ passed_ms -= MAX(0, frame->gap);
-+ if (img->current_frame >= last_uploaded_frame_index) {
-+ // It's the last frame, if the animation is loading,
-+ // remain on it.
-+ if (img->animation_state == ANIMATION_STATE_LOADING) {
-+ img->next_redraw = 0;
-+ return;
-+ }
-+ // Otherwise the animation is looping.
-+ img->current_frame = 1;
-+ // TODO: Support finite number of loops.
-+ } else {
-+ img->current_frame++;
-+ }
-+ // Make sure we don't get stuck in an infinite loop.
-+ if (img->current_frame == original_frame_index) {
-+ // We looped through all frames, but haven't reached the
-+ // next frame yet. This may happen if too much time has
-+ // passed since the last redraw or all the frames are
-+ // gapless. Just move on to the next frame.
-+ img->current_frame++;
-+ if (img->current_frame >
-+ last_uploaded_frame_index)
-+ img->current_frame = 1;
-+ img->current_frame_time = now;
-+ img->next_redraw = now + MAX(
-+ 1, gr_get_frame(img, img->current_frame)->gap);
-+ return;
-+ }
-+ // Adjust the start time of the frame. The next redraw time will
-+ // be set in the next iteration.
-+ img->current_frame_time += MAX(0, frame->gap);
-+ }
-+}
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Unloading and deleting images to save resources.
-+////////////////////////////////////////////////////////////////////////////////
-+
-+/// A helper to compare frames by atime for qsort.
-+static int gr_cmp_frames_by_atime(const void *a, const void *b) {
-+ ImageFrame *frame_a = *(ImageFrame *const *)a;
-+ ImageFrame *frame_b = *(ImageFrame *const *)b;
-+ if (frame_a->atime == frame_b->atime)
-+ return frame_a->image->global_command_index -
-+ frame_b->image->global_command_index;
-+ return frame_a->atime - frame_b->atime;
-+}
-+
-+/// A helper to compare images by atime for qsort.
-+static int gr_cmp_images_by_atime(const void *a, const void *b) {
-+ Image *img_a = *(Image *const *)a;
-+ Image *img_b = *(Image *const *)b;
-+ if (img_a->atime == img_b->atime)
-+ return img_a->global_command_index -
-+ img_b->global_command_index;
-+ return img_a->atime - img_b->atime;
-+}
-+
-+/// A helper to compare placements by atime for qsort.
-+static int gr_cmp_placements_by_atime(const void *a, const void *b) {
-+ ImagePlacement *p_a = *(ImagePlacement **)a;
-+ ImagePlacement *p_b = *(ImagePlacement **)b;
-+ if (p_a->atime == p_b->atime)
-+ return p_a->image->global_command_index -
-+ p_b->image->global_command_index;
-+ return p_a->atime - p_b->atime;
-+}
-+
-+typedef kvec_t(Image *) ImageVec;
-+typedef kvec_t(ImagePlacement *) ImagePlacementVec;
-+typedef kvec_t(ImageFrame *) ImageFrameVec;
-+
-+/// Returns an array of pointers to all images sorted by atime.
-+static ImageVec gr_get_images_sorted_by_atime() {
-+ ImageVec vec;
-+ kv_init(vec);
-+ if (kh_size(images) == 0)
-+ return vec;
-+ kv_resize(Image *, vec, kh_size(images));
-+ Image *img = NULL;
-+ kh_foreach_value(images, img, { kv_push(Image *, vec, img); });
-+ qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime);
-+ return vec;
-+}
-+
-+/// Returns an array of pointers to all placements sorted by atime.
-+static ImagePlacementVec gr_get_placements_sorted_by_atime() {
-+ ImagePlacementVec vec;
-+ kv_init(vec);
-+ if (total_placement_count == 0)
-+ return vec;
-+ kv_resize(ImagePlacement *, vec, total_placement_count);
-+ Image *img = NULL;
-+ ImagePlacement *placement = NULL;
-+ kh_foreach_value(images, img, {
-+ kh_foreach_value(img->placements, placement, {
-+ kv_push(ImagePlacement *, vec, placement);
-+ });
-+ });
-+ qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *),
-+ gr_cmp_placements_by_atime);
-+ return vec;
-+}
-+
-+/// Returns an array of pointers to all frames sorted by atime.
-+static ImageFrameVec gr_get_frames_sorted_by_atime() {
-+ ImageFrameVec frames;
-+ kv_init(frames);
-+ Image *img = NULL;
-+ kh_foreach_value(images, img, {
-+ foreach_frame(*img, frame, {
-+ kv_push(ImageFrame *, frames, frame);
-+ });
-+ });
-+ qsort(frames.a, kv_size(frames), sizeof(ImageFrame *),
-+ gr_cmp_frames_by_atime);
-+ return frames;
-+}
-+
-+/// An object that can be unloaded from RAM.
-+typedef struct {
-+ /// Some score, probably based on access time. The lower the score, the
-+ /// more likely that the object should be unloaded.
-+ int64_t score;
-+ union {
-+ ImagePlacement *placement;
-+ ImageFrame *frame;
-+ };
-+ /// If zero, the object is the imlib object of `frame`, if non-zero,
-+ /// the object is a pixmap of `frameidx`-th frame of `placement`.
-+ int frameidx;
-+} UnloadableObject;
-+
-+typedef kvec_t(UnloadableObject) UnloadableObjectVec;
-+
-+/// A helper to compare unloadable objects by score for qsort.
-+static int gr_cmp_unloadable_objects(const void *a, const void *b) {
-+ UnloadableObject *obj_a = (UnloadableObject *)a;
-+ UnloadableObject *obj_b = (UnloadableObject *)b;
-+ return obj_a->score - obj_b->score;
-+}
-+
-+/// Unloads an unloadable object from RAM.
-+static void gr_unload_object(UnloadableObject *obj) {
-+ if (obj->frameidx) {
-+ if (obj->placement->protected_frame == obj->frameidx)
-+ return;
-+ gr_unload_pixmap(obj->placement, obj->frameidx);
-+ } else {
-+ gr_unload_frame(obj->frame);
-+ }
-+}
-+
-+/// Returns the recency threshold for an image. Frames that were accessed within
-+/// this threshold from now are considered recent and may be handled
-+/// differently because we may need them again very soon.
-+static Milliseconds gr_recency_threshold(Image *img) {
-+ return img->total_duration * 2 + 1000;
-+}
-+
-+/// Creates an unloadable object for the imlib object of a frame.
-+static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now,
-+ ImageFrame *frame) {
-+ UnloadableObject obj = {0};
-+ obj.frameidx = 0;
-+ obj.frame = frame;
-+ Milliseconds atime = frame->atime;
-+ obj.score = atime;
-+ if (atime >= now - gr_recency_threshold(frame->image)) {
-+ // This is a recent frame, probably from an active animation.
-+ // Score it above `now` to prefer unloading non-active frames.
-+ // Randomize the score because it's not very clear in which
-+ // order we want to unload them: reloading a frame may require
-+ // reloading other frames.
-+ obj.score = now + 1000 + rand() % 1000;
-+ }
-+ return obj;
-+}
-+
-+/// Creates an unloadable object for a pixmap.
-+static UnloadableObject
-+gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame,
-+ ImagePlacement *placement) {
-+ UnloadableObject obj = {0};
-+ obj.frameidx = frame->index;
-+ obj.placement = placement;
-+ obj.score = placement->atime;
-+ // Since we don't store pixmap atimes, use the
-+ // oldest atime of the frame and the placement.
-+ Milliseconds atime = MIN(placement->atime, frame->atime);
-+ obj.score = atime;
-+ if (atime >= now - gr_recency_threshold(frame->image)) {
-+ // This is a recent pixmap, probably from an active animation.
-+ // Score it above `now` to prefer unloading non-active frames.
-+ // Also assign higher scores to frames that are closer to the
-+ // current frame (more likely to be used soon).
-+ int num_frames = gr_last_frame_index(frame->image);
-+ int dist = frame->index - frame->image->current_frame;
-+ if (dist < 0)
-+ dist += num_frames;
-+ obj.score =
-+ now + 1000 + (num_frames - dist) * 1000 / num_frames;
-+ // If the pixmap is much larger than the imlib image, prefer to
-+ // unload the pixmap by adding up to -1000 to the score. If the
-+ // imlib image is larger, add up to +1000.
-+ float imlib_size = gr_frame_current_ram_size(frame);
-+ float pixmap_size =
-+ gr_placement_single_frame_ram_size(placement);
-+ obj.score +=
-+ 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5);
-+ }
-+ return obj;
-+}
-+
-+/// Returns an array of unloadable objects sorted by score.
-+static UnloadableObjectVec
-+gr_get_unloadable_objects_sorted_by_score(Milliseconds now) {
-+ UnloadableObjectVec objects;
-+ kv_init(objects);
-+ Image *img = NULL;
-+ ImagePlacement *placement = NULL;
-+ kh_foreach_value(images, img, {
-+ foreach_frame(*img, frame, {
-+ if (!frame->imlib_object)
-+ continue;
-+ kv_push(UnloadableObject, objects,
-+ gr_unloadable_object_for_frame(now, frame));
-+ int frameidx = frame->index;
-+ kh_foreach_value(img->placements, placement, {
-+ if (!gr_get_frame_pixmap(placement, frameidx))
-+ continue;
-+ kv_push(UnloadableObject, objects,
-+ gr_unloadable_object_for_pixmap(
-+ now, frame, placement));
-+ });
-+ });
-+ });
-+ qsort(objects.a, kv_size(objects), sizeof(UnloadableObject),
-+ gr_cmp_unloadable_objects);
-+ return objects;
-+}
-+
-+/// Returns the limit adjusted by the excess tolerance ratio.
-+static inline unsigned apply_tolerance(unsigned limit) {
-+ return limit + (unsigned)(limit * graphics_excess_tolerance_ratio);
-+}
-+
-+/// Checks RAM and disk cache limits and deletes/unloads some images.
-+static void gr_check_limits() {
-+ Milliseconds now = gr_now_ms();
-+ ImageVec images_sorted = {0};
-+ ImagePlacementVec placements_sorted = {0};
-+ ImageFrameVec frames_sorted = {0};
-+ UnloadableObjectVec objects_sorted = {0};
-+ int images_begin = 0;
-+ int placements_begin = 0;
-+ char changed = 0;
-+ // First reduce the number of images if there are too many.
-+ if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) {
-+ GR_LOG("Too many images: %d\n", kh_size(images));
-+ changed = 1;
-+ images_sorted = gr_get_images_sorted_by_atime();
-+ int to_delete = kv_size(images_sorted) -
-+ graphics_max_total_placements;
-+ for (; images_begin < to_delete; images_begin++)
-+ gr_delete_image(images_sorted.a[images_begin]);
-+ }
-+ // Then reduce the number of placements if there are too many.
-+ if (total_placement_count >
-+ apply_tolerance(graphics_max_total_placements)) {
-+ GR_LOG("Too many placements: %d\n", total_placement_count);
-+ changed = 1;
-+ placements_sorted = gr_get_placements_sorted_by_atime();
-+ int to_delete = kv_size(placements_sorted) -
-+ graphics_max_total_placements;
-+ for (; placements_begin < to_delete; placements_begin++) {
-+ ImagePlacement *placement =
-+ placements_sorted.a[placements_begin];
-+ if (placement->protected_frame)
-+ break;
-+ gr_delete_placement(placement);
-+ }
-+ }
-+ // Then reduce the size of the image file cache. The files correspond to
-+ // image frames.
-+ if (images_disk_size >
-+ apply_tolerance(graphics_total_file_cache_size)) {
-+ GR_LOG("Too big disk cache: %ld KiB\n",
-+ images_disk_size / 1024);
-+ changed = 1;
-+ frames_sorted = gr_get_frames_sorted_by_atime();
-+ for (int i = 0; i < kv_size(frames_sorted); i++) {
-+ if (images_disk_size <= graphics_total_file_cache_size)
-+ break;
-+ gr_delete_imagefile(kv_A(frames_sorted, i));
-+ }
-+ }
-+ // Then unload images from RAM.
-+ if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) {
-+ changed = 1;
-+ int frames_begin = 0;
-+ GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024);
-+ objects_sorted = gr_get_unloadable_objects_sorted_by_score(now);
-+ for (int i = 0; i < kv_size(objects_sorted); i++) {
-+ if (images_ram_size <= graphics_max_total_ram_size)
-+ break;
-+ gr_unload_object(&kv_A(objects_sorted, i));
-+ }
-+ }
-+ if (changed) {
-+ GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB "
-+ "img count: %d placement count: %d\n",
-+ images_ram_size / 1024, images_disk_size / 1024,
-+ kh_size(images), total_placement_count);
-+ }
-+ kv_destroy(images_sorted);
-+ kv_destroy(placements_sorted);
-+ kv_destroy(frames_sorted);
-+ kv_destroy(objects_sorted);
-+}
-+
-+/// Unloads all images by user request.
-+void gr_unload_images_to_reduce_ram() {
-+ Image *img = NULL;
-+ ImagePlacement *placement = NULL;
-+ kh_foreach_value(images, img, {
-+ kh_foreach_value(img->placements, placement, {
-+ if (placement->protected_frame)
-+ continue;
-+ gr_unload_placement(placement);
-+ });
-+ gr_unload_all_frames(img);
-+ });
-+}
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Image loading.
-+////////////////////////////////////////////////////////////////////////////////
-+
-+/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2
-+/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted
-+/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout
-+/// on little-endian architectures).
-+static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format,
-+ size_t num_pixels) {
-+ size_t pixel_size = format == 24 ? 3 : 4;
-+ if (format == 32) {
-+ for (unsigned i = 0; i < num_pixels; ++i) {
-+ unsigned byte_i = i * pixel_size;
-+ to[i] = ((DATA32)from[byte_i + 2]) |
-+ ((DATA32)from[byte_i + 1]) << 8 |
-+ ((DATA32)from[byte_i]) << 16 |
-+ ((DATA32)from[byte_i + 3]) << 24;
-+ }
-+ } else {
-+ for (unsigned i = 0; i < num_pixels; ++i) {
-+ unsigned byte_i = i * pixel_size;
-+ to[i] = ((DATA32)from[byte_i + 2]) |
-+ ((DATA32)from[byte_i + 1]) << 8 |
-+ ((DATA32)from[byte_i]) << 16 | 0xFF000000;
-+ }
-+ }
-+}
-+
-+/// Loads uncompressed RGB or RGBA image data from a file.
-+static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file,
-+ int format,
-+ size_t total_pixels) {
-+ unsigned char chunk[BUFSIZ];
-+ size_t pixel_size = format == 24 ? 3 : 4;
-+ size_t chunk_size_pix = BUFSIZ / 4;
-+ size_t chunk_size_bytes = chunk_size_pix * pixel_size;
-+ size_t bytes = total_pixels * pixel_size;
-+ for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels;
-+ chunk_start_pix += chunk_size_pix) {
-+ size_t read_size = fread(chunk, 1, chunk_size_bytes, file);
-+ size_t read_pixels = read_size / pixel_size;
-+ if (chunk_start_pix + read_pixels > total_pixels)
-+ read_pixels = total_pixels - chunk_start_pix;
-+ gr_copy_pixels(data + chunk_start_pix, chunk, format,
-+ read_pixels);
-+ }
-+}
-+
-+#define COMPRESSED_CHUNK_SIZE BUFSIZ
-+#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4)
-+
-+/// Loads compressed RGB or RGBA image data from a file.
-+static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file,
-+ int format, size_t total_pixels) {
-+ size_t pixel_size = format == 24 ? 3 : 4;
-+ unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE];
-+ unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE];
-+
-+ z_stream strm;
-+ strm.zalloc = Z_NULL;
-+ strm.zfree = Z_NULL;
-+ strm.opaque = Z_NULL;
-+ strm.next_out = decompressed_chunk;
-+ strm.avail_out = DECOMPRESSED_CHUNK_SIZE;
-+ strm.avail_in = 0;
-+ strm.next_in = Z_NULL;
-+ int ret = inflateInit(&strm);
-+ if (ret != Z_OK)
-+ return 1;
-+
-+ int error = 0;
-+ int progress = 0;
-+ size_t total_copied_pixels = 0;
-+ while (1) {
-+ // If we don't have enough data in the input buffer, try to read
-+ // from the file.
-+ if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) {
-+ // Move the existing data to the beginning.
-+ memmove(compressed_chunk, strm.next_in, strm.avail_in);
-+ strm.next_in = compressed_chunk;
-+ // Read more data.
-+ size_t bytes_read = fread(
-+ compressed_chunk + strm.avail_in, 1,
-+ COMPRESSED_CHUNK_SIZE - strm.avail_in, file);
-+ strm.avail_in += bytes_read;
-+ if (bytes_read != 0)
-+ progress = 1;
-+ }
-+
-+ // Try to inflate the data.
-+ int ret = inflate(&strm, Z_SYNC_FLUSH);
-+ if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) {
-+ error = 1;
-+ fprintf(stderr,
-+ "error: could not decompress the image, error "
-+ "%s\n",
-+ ret == Z_MEM_ERROR ? "Z_MEM_ERROR"
-+ : "Z_DATA_ERROR");
-+ break;
-+ }
-+
-+ // Copy the data from the output buffer to the image.
-+ size_t full_pixels =
-+ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size;
-+ // Make sure we don't overflow the image.
-+ if (full_pixels > total_pixels - total_copied_pixels)
-+ full_pixels = total_pixels - total_copied_pixels;
-+ if (full_pixels > 0) {
-+ // Copy pixels.
-+ gr_copy_pixels(data, decompressed_chunk, format,
-+ full_pixels);
-+ data += full_pixels;
-+ total_copied_pixels += full_pixels;
-+ if (total_copied_pixels >= total_pixels) {
-+ // We filled the whole image, there may be some
-+ // data left, but we just truncate it.
-+ break;
-+ }
-+ // Move the remaining data to the beginning.
-+ size_t copied_bytes = full_pixels * pixel_size;
-+ size_t leftover =
-+ (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) -
-+ copied_bytes;
-+ memmove(decompressed_chunk,
-+ decompressed_chunk + copied_bytes, leftover);
-+ strm.next_out -= copied_bytes;
-+ strm.avail_out += copied_bytes;
-+ progress = 1;
-+ }
-+
-+ // If we haven't made any progress, then we have reached the end
-+ // of both the file and the inflated data.
-+ if (!progress)
-+ break;
-+ progress = 0;
-+ }
-+
-+ inflateEnd(&strm);
-+ return error;
-+}
-+
-+#undef COMPRESSED_CHUNK_SIZE
-+#undef DECOMPRESSED_CHUNK_SIZE
-+
-+/// Load the image from a file containing raw pixel data (RGB or RGBA), the data
-+/// may be compressed.
-+static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame,
-+ const char *filename) {
-+ size_t total_pixels = frame->data_pix_width * frame->data_pix_height;
-+ if (total_pixels * 4 > graphics_max_single_image_ram_size) {
-+ fprintf(stderr,
-+ "error: image %u frame %u is too big too load: %zu > %u\n",
-+ frame->image->image_id, frame->index, total_pixels * 4,
-+ graphics_max_single_image_ram_size);
-+ return NULL;
-+ }
-+
-+ FILE* file = fopen(filename, "rb");
-+ if (!file) {
-+ fprintf(stderr,
-+ "error: could not open image file: %s\n",
-+ sanitized_filename(filename));
-+ return NULL;
-+ }
-+
-+ Imlib_Image image = imlib_create_image(frame->data_pix_width,
-+ frame->data_pix_height);
-+ if (!image) {
-+ fprintf(stderr,
-+ "error: could not create an image of size %d x %d\n",
-+ frame->data_pix_width, frame->data_pix_height);
-+ fclose(file);
-+ return NULL;
-+ }
-+
-+ imlib_context_set_image(image);
-+ imlib_image_set_has_alpha(1);
-+ DATA32* data = imlib_image_get_data();
-+
-+ // The default format is 32.
-+ int format = frame->format ? frame->format : 32;
-+
-+ if (frame->compression == 0) {
-+ gr_load_raw_pixel_data_uncompressed(data, file, format,
-+ total_pixels);
-+ } else {
-+ int ret = gr_load_raw_pixel_data_compressed(data, file, format,
-+ total_pixels);
-+ if (ret != 0) {
-+ imlib_image_put_back_data(data);
-+ imlib_free_image();
-+ fclose(file);
-+ return NULL;
-+ }
-+ }
-+
-+ fclose(file);
-+ imlib_image_put_back_data(data);
-+ return image;
-+}
-+
-+/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object
-+/// is fully composed on top of the background frame. If the frame is already
-+/// loaded, does nothing. Loading may fail, in which case the status of the
-+/// frame will be set to STATUS_RAM_LOADING_ERROR.
-+static void gr_load_imlib_object(ImageFrame *frame) {
-+ if (frame->imlib_object)
-+ return;
-+
-+ // If the image is uninitialized or uploading has failed, or the file
-+ // has been deleted, we cannot load the image.
-+ if (frame->status < STATUS_UPLOADING_SUCCESS)
-+ return;
-+ if (frame->disk_size == 0) {
-+ if (frame->status != STATUS_RAM_LOADING_ERROR) {
-+ fprintf(stderr,
-+ "error: cached image was deleted: %u frame %u\n",
-+ frame->image->image_id, frame->index);
-+ }
-+ frame->status = STATUS_RAM_LOADING_ERROR;
-+ return;
-+ }
-+
-+ // Prevent recursive dependences between frames.
-+ if (frame->status == STATUS_RAM_LOADING_IN_PROGRESS) {
-+ fprintf(stderr,
-+ "error: recursive loading of image %u frame %u\n",
-+ frame->image->image_id, frame->index);
-+ frame->status = STATUS_RAM_LOADING_ERROR;
-+ return;
-+ }
-+ frame->status = STATUS_RAM_LOADING_IN_PROGRESS;
-+
-+ // Load the background frame if needed. Hopefully it's not recursive.
-+ ImageFrame *bg_frame = NULL;
-+ if (frame->background_frame_index) {
-+ bg_frame = gr_get_frame(frame->image,
-+ frame->background_frame_index);
-+ if (!bg_frame) {
-+ fprintf(stderr,
-+ "error: could not find background "
-+ "frame %d for image %u frame %d\n",
-+ frame->background_frame_index,
-+ frame->image->image_id, frame->index);
-+ frame->status = STATUS_RAM_LOADING_ERROR;
-+ return;
-+ }
-+ gr_load_imlib_object(bg_frame);
-+ if (!bg_frame->imlib_object) {
-+ fprintf(stderr,
-+ "error: could not load background frame %d for "
-+ "image %u frame %d\n",
-+ frame->background_frame_index,
-+ frame->image->image_id, frame->index);
-+ frame->status = STATUS_RAM_LOADING_ERROR;
-+ return;
-+ }
-+ }
-+
-+ // Load the frame data image.
-+ Imlib_Image frame_data_image = NULL;
-+ char filename[MAX_FILENAME_SIZE];
-+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
-+ GR_LOG("Loading image: %s\n", sanitized_filename(filename));
-+ if (frame->format == 100 || frame->format == 0)
-+ frame_data_image = imlib_load_image(filename);
-+ if (frame->format == 32 || frame->format == 24 ||
-+ (!frame_data_image && frame->format == 0))
-+ frame_data_image = gr_load_raw_pixel_data(frame, filename);
-+ this_redraw_cycle_loaded_files++;
-+
-+ if (!frame_data_image) {
-+ if (frame->status != STATUS_RAM_LOADING_ERROR) {
-+ fprintf(stderr, "error: could not load image: %s\n",
-+ sanitized_filename(filename));
-+ }
-+ frame->status = STATUS_RAM_LOADING_ERROR;
-+ return;
-+ }
-+
-+ imlib_context_set_image(frame_data_image);
-+ int frame_data_width = imlib_image_get_width();
-+ int frame_data_height = imlib_image_get_height();
-+ GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width,
-+ frame_data_height);
-+ // If imlib loading succeeded, and it is the first frame, set the
-+ // information about the original image size, unless it's already set.
-+ if (frame->index == 1 && frame->image->pix_width == 0 &&
-+ frame->image->pix_height == 0) {
-+ frame->image->pix_width = frame_data_width;
-+ frame->image->pix_height = frame_data_height;
-+ }
-+
-+ int image_width = frame->image->pix_width;
-+ int image_height = frame->image->pix_height;
-+
-+ // Compose the image with the background color or frame.
-+ if (frame->background_color != 0 || bg_frame ||
-+ image_width != frame_data_width ||
-+ image_height != frame_data_height) {
-+ GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n",
-+ frame->background_color, frame->background_frame_index);
-+ Imlib_Image composed_image = imlib_create_image(
-+ image_width, image_height);
-+ imlib_context_set_image(composed_image);
-+ imlib_image_set_has_alpha(1);
-+ imlib_context_set_anti_alias(0);
-+
-+ // Start with the background frame or color.
-+ imlib_context_set_blend(0);
-+ if (bg_frame && bg_frame->imlib_object) {
-+ imlib_blend_image_onto_image(
-+ bg_frame->imlib_object, 1, 0, 0,
-+ image_width, image_height, 0, 0,
-+ image_width, image_height);
-+ } else {
-+ int r = (frame->background_color >> 24) & 0xFF;
-+ int g = (frame->background_color >> 16) & 0xFF;
-+ int b = (frame->background_color >> 8) & 0xFF;
-+ int a = frame->background_color & 0xFF;
-+ imlib_context_set_color(r, g, b, a);
-+ imlib_image_fill_rectangle(0, 0, image_width,
-+ image_height);
-+ }
-+
-+ // Blend the frame data image onto the background.
-+ imlib_context_set_blend(1);
-+ imlib_blend_image_onto_image(
-+ frame_data_image, 1, 0, 0, frame->data_pix_width,
-+ frame->data_pix_height, frame->x, frame->y,
-+ frame->data_pix_width, frame->data_pix_height);
-+
-+ // Free the frame data image.
-+ imlib_context_set_image(frame_data_image);
-+ imlib_free_image();
-+
-+ frame_data_image = composed_image;
-+ }
-+
-+ frame->imlib_object = frame_data_image;
-+
-+ images_ram_size += gr_frame_current_ram_size(frame);
-+ frame->status = STATUS_RAM_LOADING_SUCCESS;
-+
-+ GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB)\n",
-+ frame->image->image_id, frame->index,
-+ images_ram_size / 1024, gr_frame_current_ram_size(frame) / 1024);
-+}
-+
-+/// Premultiplies the alpha channel of the image data. The data is an array of
-+/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB.
-+static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) {
-+ for (size_t i = 0; i < num_pixels; ++i) {
-+ DATA32 pixel = data[i];
-+ unsigned char a = pixel >> 24;
-+ if (a == 0) {
-+ data[i] = 0;
-+ } else if (a != 255) {
-+ unsigned char b = (pixel & 0xFF) * a / 255;
-+ unsigned char g = ((pixel >> 8) & 0xFF) * a / 255;
-+ unsigned char r = ((pixel >> 16) & 0xFF) * a / 255;
-+ data[i] = (a << 24) | (r << 16) | (g << 8) | b;
-+ }
-+ }
-+}
-+
-+/// Creates a pixmap for the frame of an image placement. The pixmap contain the
-+/// image data correctly scaled and fit to the box defined by the number of
-+/// rows/columns of the image placement and the provided cell dimensions in
-+/// pixels. If the placement is already loaded, it will be reloaded only if the
-+/// cell dimensions have changed.
-+Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) {
-+ Image *img = placement->image;
-+ ImageFrame *frame = gr_get_frame(img, frameidx);
-+
-+ // Update the atime uncoditionally.
-+ gr_touch_placement(placement);
-+ if (frame)
-+ gr_touch_frame(frame);
-+
-+ // If cw or ch are different, unload all the pixmaps.
-+ if (placement->scaled_cw != cw || placement->scaled_ch != ch) {
-+ gr_unload_placement(placement);
-+ placement->scaled_cw = cw;
-+ placement->scaled_ch = ch;
-+ }
-+
-+ // If it's already loaded, do nothing.
-+ Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
-+ if (pixmap)
-+ return pixmap;
-+
-+ GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id,
-+ placement->placement_id, frameidx);
-+
-+ // Load the imlib object for the frame.
-+ if (!frame) {
-+ fprintf(stderr,
-+ "error: could not find frame %u for image %u\n",
-+ frameidx, img->image_id);
-+ return 0;
-+ }
-+ gr_load_imlib_object(frame);
-+ if (!frame->imlib_object)
-+ return 0;
-+
-+ // Infer the placement size if needed.
-+ gr_infer_placement_size_maybe(placement);
-+
-+ // Create the scaled image. This is temporary, we will scale it
-+ // appropriately, upload to the X server, and then delete immediately.
-+ int scaled_w = (int)placement->cols * cw;
-+ int scaled_h = (int)placement->rows * ch;
-+ if (scaled_w * scaled_h * 4 > graphics_max_single_image_ram_size) {
-+ fprintf(stderr,
-+ "error: placement %u/%u would be too big to load: %d x "
-+ "%d x 4 > %u\n",
-+ img->image_id, placement->placement_id, scaled_w,
-+ scaled_h, graphics_max_single_image_ram_size);
-+ return 0;
-+ }
-+ Imlib_Image scaled_image = imlib_create_image(scaled_w, scaled_h);
-+ if (!scaled_image) {
-+ fprintf(stderr,
-+ "error: imlib_create_image(%d, %d) returned "
-+ "null\n",
-+ scaled_w, scaled_h);
-+ return 0;
-+ }
-+ imlib_context_set_image(scaled_image);
-+ imlib_image_set_has_alpha(1);
-+
-+ // First fill the scaled image with the transparent color.
-+ imlib_context_set_blend(0);
-+ imlib_context_set_color(0, 0, 0, 0);
-+ imlib_image_fill_rectangle(0, 0, scaled_w, scaled_h);
-+ imlib_context_set_anti_alias(1);
-+ imlib_context_set_blend(1);
-+
-+ // The source rectangle.
-+ int src_x = placement->src_pix_x;
-+ int src_y = placement->src_pix_y;
-+ int src_w = placement->src_pix_width;
-+ int src_h = placement->src_pix_height;
-+ // Whether the box is too small to use the true size of the image.
-+ char box_too_small = scaled_w < src_w || scaled_h < src_h;
-+ char mode = placement->scale_mode;
-+
-+ // Then blend the original image onto the transparent background.
-+ if (src_w <= 0 || src_h <= 0) {
-+ fprintf(stderr, "warning: image of zero size\n");
-+ } else if (mode == SCALE_MODE_FILL) {
-+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
-+ src_y, src_w, src_h, 0, 0,
-+ scaled_w, scaled_h);
-+ } else if (mode == SCALE_MODE_NONE ||
-+ (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) {
-+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
-+ src_y, src_w, src_h, 0, 0, src_w,
-+ src_h);
-+ } else {
-+ if (mode != SCALE_MODE_CONTAIN &&
-+ mode != SCALE_MODE_NONE_OR_CONTAIN) {
-+ fprintf(stderr,
-+ "warning: unknown scale mode %u, using "
-+ "'contain' instead\n",
-+ mode);
-+ }
-+ int dest_x, dest_y;
-+ int dest_w, dest_h;
-+ if (scaled_w * src_h > src_w * scaled_h) {
-+ // If the box is wider than the original image, fit to
-+ // height.
-+ dest_h = scaled_h;
-+ dest_y = 0;
-+ dest_w = src_w * scaled_h / src_h;
-+ dest_x = (scaled_w - dest_w) / 2;
-+ } else {
-+ // Otherwise, fit to width.
-+ dest_w = scaled_w;
-+ dest_x = 0;
-+ dest_h = src_h * scaled_w / src_w;
-+ dest_y = (scaled_h - dest_h) / 2;
-+ }
-+ imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
-+ src_y, src_w, src_h, dest_x,
-+ dest_y, dest_w, dest_h);
-+ }
-+
-+ // XRender needs the alpha channel premultiplied.
-+ DATA32 *data = imlib_image_get_data();
-+ gr_premultiply_alpha(data, scaled_w * scaled_h);
-+
-+ // Upload the image to the X server.
-+ Display *disp = imlib_context_get_display();
-+ Visual *vis = imlib_context_get_visual();
-+ Colormap cmap = imlib_context_get_colormap();
-+ Drawable drawable = imlib_context_get_drawable();
-+ if (!drawable)
-+ drawable = DefaultRootWindow(disp);
-+ pixmap = XCreatePixmap(disp, drawable, scaled_w, scaled_h, 32);
-+ XVisualInfo visinfo;
-+ XMatchVisualInfo(disp, DefaultScreen(disp), 32, TrueColor, &visinfo);
-+ XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0,
-+ (char *)data, scaled_w, scaled_h, 32, 0);
-+ GC gc = XCreateGC(disp, pixmap, 0, NULL);
-+ XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, scaled_w,
-+ scaled_h);
-+ XFreeGC(disp, gc);
-+ // XDestroyImage will free the data as well, but it is managed by imlib,
-+ // so set it to NULL.
-+ ximage->data = NULL;
-+ XDestroyImage(ximage);
-+ imlib_image_put_back_data(data);
-+ imlib_free_image();
-+
-+ // Assign the pixmap to the frame and increase the ram size.
-+ gr_set_frame_pixmap(placement, frameidx, pixmap);
-+ images_ram_size += gr_placement_single_frame_ram_size(placement);
-+ this_redraw_cycle_loaded_pixmaps++;
-+
-+ GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u "
-+ "KiB)\n",
-+ frame->image->image_id, placement->placement_id, frame->index,
-+ images_ram_size / 1024,
-+ gr_placement_single_frame_ram_size(placement) / 1024);
-+
-+ // Free up ram if needed, but keep the pixmap we've loaded no matter
-+ // what.
-+ placement->protected_frame = frameidx;
-+ gr_check_limits();
-+ placement->protected_frame = 0;
-+
-+ return pixmap;
-+}
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Initialization and deinitialization.
-+////////////////////////////////////////////////////////////////////////////////
-+
-+/// Creates a temporary directory.
-+static int gr_create_cache_dir() {
-+ strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir));
-+ if (!mkdtemp(cache_dir)) {
-+ fprintf(stderr,
-+ "error: could not create temporary dir from template "
-+ "%s\n",
-+ sanitized_filename(cache_dir));
-+ return 0;
-+ }
-+ fprintf(stderr, "Graphics cache directory: %s\n", cache_dir);
-+ return 1;
-+}
-+
-+/// Checks whether `tmp_dir` exists and recreates it if it doesn't.
-+static void gr_make_sure_tmpdir_exists() {
-+ struct stat st;
-+ if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode))
-+ return;
-+ fprintf(stderr,
-+ "error: %s is not a directory, will need to create a new "
-+ "graphics cache directory\n",
-+ sanitized_filename(cache_dir));
-+ gr_create_cache_dir();
-+}
-+
-+/// Initialize the graphics module.
-+void gr_init(Display *disp, Visual *vis, Colormap cm) {
-+ // Set the initialization time.
-+ clock_gettime(CLOCK_MONOTONIC, &initialization_time);
-+
-+ // Create the temporary dir.
-+ if (!gr_create_cache_dir())
-+ abort();
-+
-+ // Initialize imlib.
-+ imlib_context_set_display(disp);
-+ imlib_context_set_visual(vis);
-+ imlib_context_set_colormap(cm);
-+ imlib_context_set_anti_alias(1);
-+ imlib_context_set_blend(1);
-+ // Imlib2 checks only the file name when caching, which is not enough
-+ // for us since we reuse file names. Disable caching.
-+ imlib_set_cache_size(0);
-+
-+ // Prepare for color inversion.
-+ for (size_t i = 0; i < 256; ++i)
-+ reverse_table[i] = 255 - i;
-+
-+ // Create data structures.
-+ images = kh_init(id2image);
-+ kv_init(next_redraw_times);
-+
-+ atexit(gr_deinit);
-+}
-+
-+/// Deinitialize the graphics module.
-+void gr_deinit() {
-+ // Remove the cache dir.
-+ remove(cache_dir);
-+ kv_destroy(next_redraw_times);
-+ if (images) {
-+ // Delete all images.
-+ gr_delete_all_images();
-+ // Destroy the data structures.
-+ kh_destroy(id2image, images);
-+ images = NULL;
-+ }
-+}
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Dumping, debugging, and image preview.
-+////////////////////////////////////////////////////////////////////////////////
-+
-+/// Returns a string containing a time difference in a human-readable format.
-+/// Uses a static buffer, so be careful.
-+static const char *gr_ago(Milliseconds diff) {
-+ static char result[32];
-+ double seconds = (double)diff / 1000.0;
-+ if (seconds < 1)
-+ snprintf(result, sizeof(result), "%.2f sec ago", seconds);
-+ else if (seconds < 60)
-+ snprintf(result, sizeof(result), "%d sec ago", (int)seconds);
-+ else if (seconds < 3600)
-+ snprintf(result, sizeof(result), "%d min %d sec ago",
-+ (int)(seconds / 60), (int)(seconds) % 60);
-+ else {
-+ snprintf(result, sizeof(result), "%d hr %d min %d sec ago",
-+ (int)(seconds / 3600), (int)(seconds) % 3600 / 60,
-+ (int)(seconds) % 60);
-+ }
-+ return result;
-+}
-+
-+/// Prints to `file` with an indentation of `ind` spaces.
-+static void fprintf_ind(FILE *file, int ind, const char *format, ...) {
-+ fprintf(file, "%*s", ind, "");
-+ va_list args;
-+ va_start(args, format);
-+ vfprintf(file, format, args);
-+ va_end(args);
-+}
-+
-+/// Dumps the image info to `file` with an indentation of `ind` spaces.
-+static void gr_dump_image_info(FILE *file, Image *img, int ind) {
-+ if (!img) {
-+ fprintf_ind(file, ind, "Image is NULL\n");
-+ return;
-+ }
-+ Milliseconds now = gr_now_ms();
-+ fprintf_ind(file, ind, "Image %u\n", img->image_id);
-+ ind += 4;
-+ fprintf_ind(file, ind, "number: %u\n", img->image_number);
-+ fprintf_ind(file, ind, "global command index: %lu\n",
-+ img->global_command_index);
-+ fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime,
-+ gr_ago(now - img->atime));
-+ fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width,
-+ img->pix_height);
-+ fprintf_ind(file, ind, "cur frame start time: %ld %s\n",
-+ img->current_frame_time,
-+ gr_ago(now - img->current_frame_time));
-+ if (img->next_redraw)
-+ fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n",
-+ img->next_redraw, img->next_redraw - now);
-+ fprintf_ind(file, ind, "total disk size: %u KiB\n",
-+ img->total_disk_size / 1024);
-+ fprintf_ind(file, ind, "total duration: %d\n", img->total_duration);
-+ fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img));
-+ fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame);
-+ fprintf_ind(file, ind, "animation state: %d\n", img->animation_state);
-+ fprintf_ind(file, ind, "default_placement: %u\n",
-+ img->default_placement);
-+}
-+
-+/// Dumps the frame info to `file` with an indentation of `ind` spaces.
-+static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) {
-+ if (!frame) {
-+ fprintf_ind(file, ind, "Frame is NULL\n");
-+ return;
-+ }
-+ Milliseconds now = gr_now_ms();
-+ fprintf_ind(file, ind, "Frame %d\n", frame->index);
-+ ind += 4;
-+ if (frame->index == 0) {
-+ fprintf_ind(file, ind, "NOT INITIALIZED\n");
-+ return;
-+ }
-+ if (frame->uploading_failure)
-+ fprintf_ind(file, ind, "uploading failure: %s\n",
-+ image_uploading_failure_strings
-+ [frame->uploading_failure]);
-+ fprintf_ind(file, ind, "gap: %d\n", frame->gap);
-+ fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime,
-+ gr_ago(now - frame->atime));
-+ fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width,
-+ frame->data_pix_height);
-+ char filename[MAX_FILENAME_SIZE];
-+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
-+ if (access(filename, F_OK) != -1)
-+ fprintf_ind(file, ind, "file: %s\n",
-+ sanitized_filename(filename));
-+ else
-+ fprintf_ind(file, ind, "not on disk\n");
-+ fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024);
-+ if (frame->imlib_object) {
-+ unsigned ram_size = gr_frame_current_ram_size(frame);
-+ fprintf_ind(file, ind,
-+ "loaded into ram, size: %d "
-+ "KiB\n",
-+ ram_size / 1024);
-+ } else {
-+ fprintf_ind(file, ind, "not loaded into ram\n");
-+ }
-+}
-+
-+/// Dumps the placement info to `file` with an indentation of `ind` spaces.
-+static void gr_dump_placement_info(FILE *file, ImagePlacement *placement,
-+ int ind) {
-+ if (!placement) {
-+ fprintf_ind(file, ind, "Placement is NULL\n");
-+ return;
-+ }
-+ Milliseconds now = gr_now_ms();
-+ fprintf_ind(file, ind, "Placement %u\n", placement->placement_id);
-+ ind += 4;
-+ fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime,
-+ gr_ago(now - placement->atime));
-+ fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode);
-+ fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols,
-+ placement->rows);
-+ fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw,
-+ placement->scaled_ch);
-+ fprintf_ind(file, ind, "ram per frame: %u KiB\n",
-+ gr_placement_single_frame_ram_size(placement) / 1024);
-+ unsigned ram_size = gr_placement_current_ram_size(placement);
-+ fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024);
-+}
-+
-+/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces.
-+static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement,
-+ int ind) {
-+ if (!placement)
-+ return;
-+ int frameidx = 1;
-+ foreach_pixmap(*placement, pixmap, {
-+ fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx,
-+ pixmap);
-+ ++frameidx;
-+ });
-+}
-+
-+/// Dumps the internal state (images and placements) to stderr.
-+void gr_dump_state() {
-+ FILE *file = stderr;
-+ int ind = 0;
-+ fprintf_ind(file, ind, "======= Graphics module state dump =======\n");
-+ fprintf_ind(file, ind,
-+ "sizeof(Image) = %lu sizeof(ImageFrame) = %lu "
-+ "sizeof(ImagePlacement) = %lu\n",
-+ sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement));
-+ fprintf_ind(file, ind, "Image count: %u\n", kh_size(images));
-+ fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count);
-+ fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n",
-+ images_ram_size / 1024);
-+ fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n",
-+ images_disk_size / 1024);
-+
-+ Milliseconds now = gr_now_ms();
-+
-+ int64_t images_ram_size_computed = 0;
-+ int64_t images_disk_size_computed = 0;
-+
-+ Image *img = NULL;
-+ ImagePlacement *placement = NULL;
-+ kh_foreach_value(images, img, {
-+ fprintf_ind(file, ind, "----------------\n");
-+ gr_dump_image_info(file, img, 0);
-+ int64_t total_disk_size_computed = 0;
-+ int total_duration_computed = 0;
-+ foreach_frame(*img, frame, {
-+ gr_dump_frame_info(file, frame, 4);
-+ if (frame->image != img)
-+ fprintf_ind(file, 8,
-+ "ERROR: WRONG IMAGE POINTER\n");
-+ total_duration_computed += frame->gap;
-+ images_disk_size_computed += frame->disk_size;
-+ total_disk_size_computed += frame->disk_size;
-+ if (frame->imlib_object)
-+ images_ram_size_computed +=
-+ gr_frame_current_ram_size(frame);
-+ });
-+ if (img->total_disk_size != total_disk_size_computed) {
-+ fprintf_ind(file, ind,
-+ " ERROR: total_disk_size is %u, but "
-+ "computed value is %ld\n",
-+ img->total_disk_size, total_disk_size_computed);
-+ }
-+ if (img->total_duration != total_duration_computed) {
-+ fprintf_ind(file, ind,
-+ " ERROR: total_duration is %d, but computed "
-+ "value is %d\n",
-+ img->total_duration, total_duration_computed);
-+ }
-+ kh_foreach_value(img->placements, placement, {
-+ gr_dump_placement_info(file, placement, 4);
-+ if (placement->image != img)
-+ fprintf_ind(file, 8,
-+ "ERROR: WRONG IMAGE POINTER\n");
-+ fprintf_ind(file, 8,
-+ "Pixmaps:\n");
-+ gr_dump_placement_pixmaps(file, placement, 12);
-+ unsigned ram_size =
-+ gr_placement_current_ram_size(placement);
-+ images_ram_size_computed += ram_size;
-+ });
-+ });
-+ if (images_ram_size != images_ram_size_computed) {
-+ fprintf_ind(file, ind,
-+ "ERROR: images_ram_size is %ld, but computed value "
-+ "is %ld\n",
-+ images_ram_size, images_ram_size_computed);
-+ }
-+ if (images_disk_size != images_disk_size_computed) {
-+ fprintf_ind(file, ind,
-+ "ERROR: images_disk_size is %ld, but computed value "
-+ "is %ld\n",
-+ images_disk_size, images_disk_size_computed);
-+ }
-+ fprintf_ind(file, ind, "===========================================\n");
-+}
-+
-+/// Executes `command` with the name of the file corresponding to `image_id` as
-+/// the argument. Executes xmessage with an error message on failure.
-+// TODO: Currently we do this for the first frame only. Not sure what to do with
-+// animations.
-+void gr_preview_image(uint32_t image_id, const char *exec) {
-+ char command[256];
-+ size_t len;
-+ Image *img = gr_find_image(image_id);
-+ if (img) {
-+ ImageFrame *frame = &img->first_frame;
-+ char filename[MAX_FILENAME_SIZE];
-+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
-+ if (frame->disk_size == 0) {
-+ len = snprintf(command, 255,
-+ "xmessage 'Image with id=%u is not "
-+ "fully copied to %s'",
-+ image_id, sanitized_filename(filename));
-+ } else {
-+ len = snprintf(command, 255, "%s %s &", exec,
-+ sanitized_filename(filename));
-+ }
-+ } else {
-+ len = snprintf(command, 255,
-+ "xmessage 'Cannot find image with id=%u'",
-+ image_id);
-+ }
-+ if (len > 255) {
-+ fprintf(stderr, "error: command too long: %s\n", command);
-+ snprintf(command, 255, "xmessage 'error: command too long'");
-+ }
-+ if (system(command) != 0) {
-+ fprintf(stderr, "error: could not execute command %s\n",
-+ command);
-+ }
-+}
-+
-+/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
-+/// containing the information about an image and placement, and <st> is
-+/// specified with `st_executable`.
-+void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
-+ uint32_t imgcol, uint32_t imgrow,
-+ char is_classic_placeholder, int32_t diacritic_count,
-+ char *st_executable) {
-+ char filename[MAX_FILENAME_SIZE];
-+ snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id);
-+ FILE *file = fopen(filename, "w");
-+ if (!file) {
-+ perror("fopen");
-+ return;
-+ }
-+ // Basic information about the cell.
-+ fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id);
-+ fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id);
-+ fprintf(file, "column = %d, row = %d\n", imgcol, imgrow);
-+ fprintf(file, "classic/unicode placeholder = %s\n",
-+ is_classic_placeholder ? "classic" : "unicode");
-+ fprintf(file, "original diacritic count = %d\n", diacritic_count);
-+ // Information about the image and the placement.
-+ Image *img = gr_find_image(image_id);
-+ ImagePlacement *placement = gr_find_placement(img, placement_id);
-+ gr_dump_image_info(file, img, 0);
-+ gr_dump_placement_info(file, placement, 0);
-+ if (img) {
-+ fprintf(file, "Frames:\n");
-+ foreach_frame(*img, frame, {
-+ gr_dump_frame_info(file, frame, 4);
-+ });
-+ }
-+ if (placement) {
-+ fprintf(file, "Placement pixmaps:\n");
-+ gr_dump_placement_pixmaps(file, placement, 4);
-+ }
-+ fclose(file);
-+ char *argv[] = {st_executable, "-e", "less", filename, NULL};
-+ if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) {
-+ perror("posix_spawnp");
-+ return;
-+ }
-+}
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Appending and displaying image rectangles.
-+////////////////////////////////////////////////////////////////////////////////
-+
-+/// Displays debug information in the rectangle using colors col1 and col2.
-+static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2,
-+ const char *message) {
-+ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
-+ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
-+ Display *disp = imlib_context_get_display();
-+ GC gc = XCreateGC(disp, buf, 0, NULL);
-+ char info[MAX_INFO_LEN];
-+ if (rect->placement_id)
-+ snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message,
-+ rect->image_id, rect->placement_id,
-+ rect->img_start_col, rect->img_end_col,
-+ rect->img_start_row, rect->img_end_row);
-+ else
-+ snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message,
-+ rect->image_id, rect->img_start_col, rect->img_end_col,
-+ rect->img_start_row, rect->img_end_row);
-+ XSetForeground(disp, gc, col1);
-+ XDrawString(disp, buf, gc, rect->screen_x_pix + 4,
-+ rect->screen_y_pix + h_pix - 3, info, strlen(info));
-+ XSetForeground(disp, gc, col2);
-+ XDrawString(disp, buf, gc, rect->screen_x_pix + 2,
-+ rect->screen_y_pix + h_pix - 5, info, strlen(info));
-+ XFreeGC(disp, gc);
-+}
-+
-+/// Draws a rectangle (bounding box) for debugging.
-+static void gr_showrect(Drawable buf, ImageRect *rect) {
-+ int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
-+ int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
-+ Display *disp = imlib_context_get_display();
-+ GC gc = XCreateGC(disp, buf, 0, NULL);
-+ XSetForeground(disp, gc, 0xFF00FF00);
-+ XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix,
-+ w_pix - 1, h_pix - 1);
-+ XSetForeground(disp, gc, 0xFFFF0000);
-+ XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1,
-+ rect->screen_y_pix + 1, w_pix - 3, h_pix - 3);
-+ XFreeGC(disp, gc);
-+}
-+
-+/// Updates the next redraw time for the given row. Resizes the
-+/// next_redraw_times array if needed.
-+static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) {
-+ if (next_redraw == 0)
-+ return;
-+ if (row >= kv_size(next_redraw_times)) {
-+ size_t old_size = kv_size(next_redraw_times);
-+ kv_a(Milliseconds, next_redraw_times, row);
-+ for (size_t i = old_size; i <= row; ++i)
-+ kv_A(next_redraw_times, i) = 0;
-+ }
-+ Milliseconds old_value = kv_A(next_redraw_times, row);
-+ if (old_value == 0 || old_value > next_redraw)
-+ kv_A(next_redraw_times, row) = next_redraw;
-+}
-+
-+/// Draws the given part of an image.
-+static void gr_drawimagerect(Drawable buf, ImageRect *rect) {
-+ ImagePlacement *placement =
-+ gr_find_image_and_placement(rect->image_id, rect->placement_id);
-+ // If the image does not exist or image display is switched off, draw
-+ // the bounding box.
-+ if (!placement || !graphics_display_images) {
-+ gr_showrect(buf, rect);
-+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
-+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
-+ return;
-+ }
-+
-+ Image *img = placement->image;
-+
-+ if (img->last_redraw < drawing_start_time) {
-+ // This is the first time we draw this image in this redraw
-+ // cycle. Update the frame index we are going to display. Note
-+ // that currently all image placements are synchronized.
-+ int old_frame = img->current_frame;
-+ gr_update_frame_index(img, drawing_start_time);
-+ img->last_redraw = drawing_start_time;
-+ }
-+
-+ // Adjust next redraw times for the rows of this image rect.
-+ if (img->next_redraw) {
-+ for (int row = rect->screen_y_row;
-+ row <= rect->screen_y_row + rect->img_end_row -
-+ rect->img_start_row - 1; ++row) {
-+ gr_update_next_redraw_time(
-+ row, img->next_redraw);
-+ }
-+ }
-+
-+ // Load the frame.
-+ Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw,
-+ rect->ch);
-+
-+ // If the image couldn't be loaded, display the bounding box.
-+ if (!pixmap) {
-+ gr_showrect(buf, rect);
-+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
-+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
-+ return;
-+ }
-+
-+ int src_x = rect->img_start_col * rect->cw;
-+ int src_y = rect->img_start_row * rect->ch;
-+ int width = (rect->img_end_col - rect->img_start_col) * rect->cw;
-+ int height = (rect->img_end_row - rect->img_start_row) * rect->ch;
-+ int dst_x = rect->screen_x_pix;
-+ int dst_y = rect->screen_y_pix;
-+
-+ // Display the image.
-+ Display *disp = imlib_context_get_display();
-+ Visual *vis = imlib_context_get_visual();
-+
-+ // Create an xrender picture for the window.
-+ XRenderPictFormat *win_format =
-+ XRenderFindVisualFormat(disp, vis);
-+ Picture window_pic =
-+ XRenderCreatePicture(disp, buf, win_format, 0, NULL);
-+
-+ // If needed, invert the image pixmap. Note that this naive approach of
-+ // inverting the pixmap is not entirely correct, because the pixmap is
-+ // premultiplied. But the result is good enough to visually indicate
-+ // selection.
-+ if (rect->reverse) {
-+ unsigned pixmap_w =
-+ (unsigned)placement->cols * placement->scaled_cw;
-+ unsigned pixmap_h =
-+ (unsigned)placement->rows * placement->scaled_ch;
-+ Pixmap invpixmap =
-+ XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32);
-+ XGCValues gcv = {.function = GXcopyInverted};
-+ GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv);
-+ XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w,
-+ pixmap_h, 0, 0);
-+ XFreeGC(disp, gc);
-+ pixmap = invpixmap;
-+ }
-+
-+ // Create a picture for the image pixmap.
-+ XRenderPictFormat *pic_format =
-+ XRenderFindStandardFormat(disp, PictStandardARGB32);
-+ Picture pixmap_pic =
-+ XRenderCreatePicture(disp, pixmap, pic_format, 0, NULL);
-+
-+ // Composite the image onto the window. In the reverse mode we ignore
-+ // the alpha channel of the image because the naive inversion above
-+ // seems to invert the alpha channel as well.
-+ int pictop = rect->reverse ? PictOpSrc : PictOpOver;
-+ XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic,
-+ src_x, src_y, src_x, src_y, dst_x, dst_y, width,
-+ height);
-+
-+ // Free resources
-+ XRenderFreePicture(disp, pixmap_pic);
-+ XRenderFreePicture(disp, window_pic);
-+ if (rect->reverse)
-+ XFreePixmap(disp, pixmap);
-+
-+ // In debug mode always draw bounding boxes and print info.
-+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) {
-+ gr_showrect(buf, rect);
-+ gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
-+ }
-+}
-+
-+/// Removes the given image rectangle.
-+static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); }
-+
-+/// Returns the bottom coordinate of the rect.
-+static int gr_getrectbottom(ImageRect *rect) {
-+ return rect->screen_y_pix +
-+ (rect->img_end_row - rect->img_start_row) * rect->ch;
-+}
-+
-+/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
-+void gr_start_drawing(Drawable buf, int cw, int ch) {
-+ current_cw = cw;
-+ current_ch = ch;
-+ this_redraw_cycle_loaded_files = 0;
-+ this_redraw_cycle_loaded_pixmaps = 0;
-+ drawing_start_time = gr_now_ms();
-+ imlib_context_set_drawable(buf);
-+}
-+
-+/// Finish image drawing. This functions will draw all the rectangles left to
-+/// draw.
-+void gr_finish_drawing(Drawable buf) {
-+ // Draw and then delete all known image rectangles.
-+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
-+ ImageRect *rect = &image_rects[i];
-+ if (!rect->image_id)
-+ continue;
-+ gr_drawimagerect(buf, rect);
-+ gr_freerect(rect);
-+ }
-+
-+ // Compute the delay until the next redraw as the minimum of the next
-+ // redraw delays for all rows.
-+ Milliseconds drawing_end_time = gr_now_ms();
-+ graphics_next_redraw_delay = INT_MAX;
-+ for (int row = 0; row < kv_size(next_redraw_times); ++row) {
-+ Milliseconds row_next_redraw = kv_A(next_redraw_times, row);
-+ if (row_next_redraw > 0) {
-+ int delay = MAX(graphics_animation_min_delay,
-+ row_next_redraw - drawing_end_time);
-+ graphics_next_redraw_delay =
-+ MIN(graphics_next_redraw_delay, delay);
-+ }
-+ }
-+
-+ // In debug mode display additional info.
-+ if (graphics_debug_mode) {
-+ int milliseconds = drawing_end_time - drawing_start_time;
-+
-+ Display *disp = imlib_context_get_display();
-+ GC gc = XCreateGC(disp, buf, 0, NULL);
-+ const char *debug_mode_str =
-+ graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES
-+ ? "(boxes shown) "
-+ : "";
-+ int redraw_delay = graphics_next_redraw_delay == INT_MAX
-+ ? -1
-+ : graphics_next_redraw_delay;
-+ char info[MAX_INFO_LEN];
-+ snprintf(info, MAX_INFO_LEN,
-+ "%sRender time: %d ms ram %ld K disk %ld K count "
-+ "%d cell %dx%d delay %d",
-+ debug_mode_str, milliseconds, images_ram_size / 1024,
-+ images_disk_size / 1024, kh_size(images), current_cw,
-+ current_ch, redraw_delay);
-+ XSetForeground(disp, gc, 0xFF000000);
-+ XFillRectangle(disp, buf, gc, 0, 0, 600, 16);
-+ XSetForeground(disp, gc, 0xFFFFFFFF);
-+ XDrawString(disp, buf, gc, 0, 14, info, strlen(info));
-+ XFreeGC(disp, gc);
-+
-+ if (milliseconds > 0) {
-+ fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n",
-+ info, this_redraw_cycle_loaded_files,
-+ this_redraw_cycle_loaded_pixmaps);
-+ }
-+ }
-+
-+ // Check the limits in case we have used too much ram for placements.
-+ gr_check_limits();
-+}
-+
-+// Add an image rectangle to the list of rectangles to draw.
-+void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
-+ int img_start_col, int img_end_col, int img_start_row,
-+ int img_end_row, int x_col, int y_row, int x_pix,
-+ int y_pix, int cw, int ch, int reverse) {
-+ current_cw = cw;
-+ current_ch = ch;
-+
-+ ImageRect new_rect;
-+ new_rect.image_id = image_id;
-+ new_rect.placement_id = placement_id;
-+ new_rect.img_start_col = img_start_col;
-+ new_rect.img_end_col = img_end_col;
-+ new_rect.img_start_row = img_start_row;
-+ new_rect.img_end_row = img_end_row;
-+ new_rect.screen_y_row = y_row;
-+ new_rect.screen_x_pix = x_pix;
-+ new_rect.screen_y_pix = y_pix;
-+ new_rect.ch = ch;
-+ new_rect.cw = cw;
-+ new_rect.reverse = reverse;
-+
-+ // Display some red text in debug mode.
-+ if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
-+ gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? ");
-+
-+ // If it's the empty image (image_id=0) or an empty rectangle, do
-+ // nothing.
-+ if (image_id == 0 || img_end_col - img_start_col <= 0 ||
-+ img_end_row - img_start_row <= 0)
-+ return;
-+ // Try to find a rect to merge with.
-+ ImageRect *free_rect = NULL;
-+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
-+ ImageRect *rect = &image_rects[i];
-+ if (rect->image_id == 0) {
-+ if (!free_rect)
-+ free_rect = rect;
-+ continue;
-+ }
-+ if (rect->image_id != image_id ||
-+ rect->placement_id != placement_id || rect->cw != cw ||
-+ rect->ch != ch || rect->reverse != reverse)
-+ continue;
-+ // We only support the case when the new stripe is added to the
-+ // bottom of an existing rectangle and they are perfectly
-+ // aligned.
-+ if (rect->img_end_row == img_start_row &&
-+ gr_getrectbottom(rect) == y_pix) {
-+ if (rect->img_start_col == img_start_col &&
-+ rect->img_end_col == img_end_col &&
-+ rect->screen_x_pix == x_pix) {
-+ rect->img_end_row = img_end_row;
-+ return;
-+ }
-+ }
-+ }
-+ // If we haven't merged the new rect with any existing rect, and there
-+ // is no free rect, we have to render one of the existing rects.
-+ if (!free_rect) {
-+ for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
-+ ImageRect *rect = &image_rects[i];
-+ if (!free_rect || gr_getrectbottom(free_rect) >
-+ gr_getrectbottom(rect))
-+ free_rect = rect;
-+ }
-+ gr_drawimagerect(buf, free_rect);
-+ gr_freerect(free_rect);
-+ }
-+ // Start a new rectangle in `free_rect`.
-+ *free_rect = new_rect;
-+}
-+
-+/// Mark rows containing animations as dirty if it's time to redraw them. Must
-+/// be called right after `gr_start_drawing`.
-+void gr_mark_dirty_animations(int *dirty, int rows) {
-+ if (rows < kv_size(next_redraw_times))
-+ kv_size(next_redraw_times) = rows;
-+ if (rows * 2 < kv_max(next_redraw_times))
-+ kv_resize(Milliseconds, next_redraw_times, rows);
-+ for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) {
-+ if (dirty[i]) {
-+ kv_A(next_redraw_times, i) = 0;
-+ continue;
-+ }
-+ Milliseconds next_update = kv_A(next_redraw_times, i);
-+ if (next_update > 0 && next_update <= drawing_start_time) {
-+ dirty[i] = 1;
-+ kv_A(next_redraw_times, i) = 0;
-+ }
-+ }
-+}
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// Command parsing and handling.
-+////////////////////////////////////////////////////////////////////////////////
-+
-+/// A parsed kitty graphics protocol command.
-+typedef struct {
-+ /// The command itself, without the 'G'.
-+ char *command;
-+ /// The payload (after ';').
-+ char *payload;
-+ /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'.
-+ char action;
-+ /// 'q=', 1 to suppress OK response, 2 to suppress errors too.
-+ int quiet;
-+ /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with
-+ /// imlib2. If 'f=0', will try to load with imlib2, then fallback to
-+ /// 32-bit pixel data.
-+ int format;
-+ /// 'o=', may be 'z' for RFC 1950 ZLIB.
-+ int compression;
-+ /// 't=', may be 'f', 't' or 'd'.
-+ char transmission_medium;
-+ /// 'd='
-+ char delete_specifier;
-+ /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'.
-+ /// When 'a=f', this is the size of the frame rectangle when composed on
-+ /// top of another frame.
-+ int frame_pix_width, frame_pix_height;
-+ /// 'x=', 'y=' - top-left corner of the source rectangle.
-+ int src_pix_x, src_pix_y;
-+ /// 'w=', 'h=' - width and height of the source rectangle.
-+ int src_pix_width, src_pix_height;
-+ /// 'r=', 'c='
-+ int rows, columns;
-+ /// 'i='
-+ uint32_t image_id;
-+ /// 'I='
-+ uint32_t image_number;
-+ /// 'p='
-+ uint32_t placement_id;
-+ /// 'm=', may be 0 or 1.
-+ int more;
-+ /// True if either 'm=0' or 'm=1' is specified.
-+ char is_data_transmission;
-+ /// True if turns out that this command is a continuation of a data
-+ /// transmission and not the first one for this image. Populated by
-+ /// `gr_handle_transmit_command`.
-+ char is_direct_transmission_continuation;
-+ /// 'S=', used to check the size of uploaded data.
-+ int size;
-+ /// 'U=', whether it's a virtual placement for Unicode placeholders.
-+ int virtual;
-+ /// 'C=', if true, do not move the cursor when displaying this placement
-+ /// (non-virtual placements only).
-+ char do_not_move_cursor;
-+ // ---------------------------------------------------------------------
-+ // Animation-related fields. Their keys often overlap with keys of other
-+ // commands, so these make sense only if the action is 'a=f' (frame
-+ // transmission) or 'a=a' (animation control).
-+ //
-+ // 'x=' and 'y=', the relative position of the frame image when it's
-+ // composed on top of another frame.
-+ int frame_dst_pix_x, frame_dst_pix_y;
-+ /// 'X=', 'X=1' to replace colors instead of alpha blending on top of
-+ /// the background color or frame.
-+ char replace_instead_of_blending;
-+ /// 'Y=', the background color in the 0xRRGGBBAA format (still
-+ /// transmitted as a decimal number).
-+ uint32_t background_color;
-+ /// (Only for 'a=f'). 'c=', the 1-based index of the background frame.
-+ int background_frame;
-+ /// (Only for 'a=a'). 'c=', sets the index of the current frame.
-+ int current_frame;
-+ /// 'r=', the 1-based index of the frame to edit.
-+ int edit_frame;
-+ /// 'z=', the duration of the frame. Zero if not specified, negative if
-+ /// the frame is gapless (i.e. skipped).
-+ int gap;
-+ /// (Only for 'a=a'). 's=', if non-zero, sets the state of the
-+ /// animation, 1 to stop, 2 to run in loading mode, 3 to loop.
-+ int animation_state;
-+ /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the
-+ /// animation will loop. 1 to loop infinitely, N to loop N-1 times.
-+ int loops;
-+} GraphicsCommand;
-+
-+/// Replaces all non-printed characters in `str` with '?' and truncates the
-+/// string to `max_size`, maybe inserting ellipsis at the end.
-+static void sanitize_str(char *str, size_t max_size) {
-+ assert(max_size >= 4);
-+ for (size_t i = 0; i < max_size; ++i) {
-+ unsigned c = str[i];
-+ if (c == '\0')
-+ return;
-+ if (c >= 128 || !isprint(c))
-+ str[i] = '?';
-+ }
-+ str[max_size - 1] = '\0';
-+ str[max_size - 2] = '.';
-+ str[max_size - 3] = '.';
-+ str[max_size - 4] = '.';
-+}
-+
-+/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be
-+/// careful.
-+static const char *sanitized_filename(const char *str) {
-+ static char buf[MAX_FILENAME_SIZE];
-+ strncpy(buf, str, sizeof(buf));
-+ sanitize_str(buf, sizeof(buf));
-+ return buf;
-+}
-+
-+/// Creates a response to the current command in `graphics_command_result`.
-+static void gr_createresponse(uint32_t image_id, uint32_t image_number,
-+ uint32_t placement_id, const char *msg) {
-+ if (!image_id && !image_number && !placement_id) {
-+ // Nobody expects the response in this case, so just print it to
-+ // stderr.
-+ fprintf(stderr,
-+ "error: No image id or image number or placement_id, "
-+ "but still there is a response: %s\n",
-+ msg);
-+ return;
-+ }
-+ char *buf = graphics_command_result.response;
-+ size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN;
-+ size_t written;
-+ written = snprintf(buf, maxlen, "\033_G");
-+ buf += written;
-+ maxlen -= written;
-+ if (image_id) {
-+ written = snprintf(buf, maxlen, "i=%u,", image_id);
-+ buf += written;
-+ maxlen -= written;
-+ }
-+ if (image_number) {
-+ written = snprintf(buf, maxlen, "I=%u,", image_number);
-+ buf += written;
-+ maxlen -= written;
-+ }
-+ if (placement_id) {
-+ written = snprintf(buf, maxlen, "p=%u,", placement_id);
-+ buf += written;
-+ maxlen -= written;
-+ }
-+ buf[-1] = ';';
-+ written = snprintf(buf, maxlen, "%s\033\\", msg);
-+ buf += written;
-+ maxlen -= written;
-+ buf[-2] = '\033';
-+ buf[-1] = '\\';
-+}
-+
-+/// Creates the 'OK' response to the current command, unless suppressed or a
-+/// non-final data transmission.
-+static void gr_reportsuccess_cmd(GraphicsCommand *cmd) {
-+ if (cmd->quiet < 1 && !cmd->more)
-+ gr_createresponse(cmd->image_id, cmd->image_number,
-+ cmd->placement_id, "OK");
-+}
-+
-+/// Creates the 'OK' response to the current command (unless suppressed).
-+static void gr_reportsuccess_frame(ImageFrame *frame) {
-+ uint32_t id = frame->image->query_id ? frame->image->query_id
-+ : frame->image->image_id;
-+ if (frame->quiet < 1)
-+ gr_createresponse(id, frame->image->image_number,
-+ frame->image->initial_placement_id, "OK");
-+}
-+
-+/// Creates an error response to the current command (unless suppressed).
-+static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) {
-+ char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
-+ graphics_command_result.error = 1;
-+ va_list args;
-+ va_start(args, format);
-+ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
-+ va_end(args);
-+
-+ fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command);
-+ if (cmd->quiet < 2)
-+ gr_createresponse(cmd->image_id, cmd->image_number,
-+ cmd->placement_id, errmsg);
-+}
-+
-+/// Creates an error response to the current command (unless suppressed).
-+static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) {
-+ char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
-+ graphics_command_result.error = 1;
-+ va_list args;
-+ va_start(args, format);
-+ vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
-+ va_end(args);
-+
-+ if (!frame) {
-+ fprintf(stderr, "%s\n", errmsg);
-+ gr_createresponse(0, 0, 0, errmsg);
-+ } else {
-+ uint32_t id = frame->image->query_id ? frame->image->query_id
-+ : frame->image->image_id;
-+ fprintf(stderr, "%s id=%u\n", errmsg, id);
-+ if (frame->quiet < 2)
-+ gr_createresponse(id, frame->image->image_number,
-+ frame->image->initial_placement_id,
-+ errmsg);
-+ }
-+}
-+
-+/// Loads an image and creates a success/failure response. Returns `frame`, or
-+/// NULL if it's a query action and the image was deleted.
-+static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) {
-+ gr_load_imlib_object(frame);
-+ if (!frame->imlib_object) {
-+ gr_reporterror_frame(frame, "EBADF: could not load image");
-+ } else {
-+ gr_reportsuccess_frame(frame);
-+ }
-+ // If it was a query action, discard the image.
-+ if (frame->image->query_id) {
-+ gr_delete_image(frame->image);
-+ return NULL;
-+ }
-+ return frame;
-+}
-+
-+/// Creates an appropriate uploading failure response to the current command.
-+static void gr_reportuploaderror(ImageFrame *frame) {
-+ switch (frame->uploading_failure) {
-+ case 0:
-+ return;
-+ case ERROR_CANNOT_OPEN_CACHED_FILE:
-+ gr_reporterror_frame(frame,
-+ "EIO: could not create a file for image");
-+ break;
-+ case ERROR_OVER_SIZE_LIMIT:
-+ gr_reporterror_frame(
-+ frame,
-+ "EFBIG: the size of the uploaded image exceeded "
-+ "the image size limit %u",
-+ graphics_max_single_image_file_size);
-+ break;
-+ case ERROR_UNEXPECTED_SIZE:
-+ gr_reporterror_frame(frame,
-+ "EINVAL: the size of the uploaded image %u "
-+ "doesn't match the expected size %u",
-+ frame->disk_size, frame->expected_size);
-+ break;
-+ };
-+}
-+
-+/// Displays a non-virtual placement. This functions records the information in
-+/// `graphics_command_result`, the placeholder itself is created by the terminal
-+/// after handling the current command in the graphics module.
-+static void gr_display_nonvirtual_placement(ImagePlacement *placement) {
-+ if (placement->virtual)
-+ return;
-+ if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS)
-+ return;
-+ // Infer the placement size if needed.
-+ gr_infer_placement_size_maybe(placement);
-+ // Populate the information about the placeholder which will be created
-+ // by the terminal.
-+ graphics_command_result.create_placeholder = 1;
-+ graphics_command_result.placeholder.image_id = placement->image->image_id;
-+ graphics_command_result.placeholder.placement_id = placement->placement_id;
-+ graphics_command_result.placeholder.columns = placement->cols;
-+ graphics_command_result.placeholder.rows = placement->rows;
-+ graphics_command_result.placeholder.do_not_move_cursor =
-+ placement->do_not_move_cursor;
-+ GR_LOG("Creating a placeholder for %u/%u %d x %d\n",
-+ placement->image->image_id, placement->placement_id,
-+ placement->cols, placement->rows);
-+}
-+
-+/// Marks the rows that are occupied by the image as dirty.
-+static void gr_schedule_image_redraw(Image *img) {
-+ if (!img)
-+ return;
-+ gr_schedule_image_redraw_by_id(img->image_id);
-+}
-+
-+/// Appends data from `payload` to the frame `frame` when using direct
-+/// transmission. Note that we report errors only for the final command
-+/// (`!more`) to avoid spamming the client. If the frame is not specified, use
-+/// the image id and frame index we are currently uploading.
-+static void gr_append_data(ImageFrame *frame, const char *payload, int more) {
-+ if (!frame) {
-+ Image *img = gr_find_image(current_upload_image_id);
-+ frame = gr_get_frame(img, current_upload_frame_index);
-+ GR_LOG("Appending data to image %u frame %d\n",
-+ current_upload_image_id, current_upload_frame_index);
-+ if (!img)
-+ GR_LOG("ERROR: this image doesn't exist\n");
-+ if (!frame)
-+ GR_LOG("ERROR: this frame doesn't exist\n");
-+ }
-+ if (!more) {
-+ current_upload_image_id = 0;
-+ current_upload_frame_index = 0;
-+ }
-+ if (!frame) {
-+ if (!more)
-+ gr_reporterror_frame(NULL, "ENOENT: could not find the "
-+ "image to append data to");
-+ return;
-+ }
-+ if (frame->status != STATUS_UPLOADING) {
-+ if (!more)
-+ gr_reportuploaderror(frame);
-+ return;
-+ }
-+
-+ // Decode the data.
-+ size_t data_size = 0;
-+ char *data = gr_base64dec(payload, &data_size);
-+
-+ GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size,
-+ frame->disk_size + data_size);
-+
-+ // Do not append this data if the image exceeds the size limit.
-+ if (frame->disk_size + data_size >
-+ graphics_max_single_image_file_size ||
-+ frame->expected_size > graphics_max_single_image_file_size) {
-+ free(data);
-+ gr_delete_imagefile(frame);
-+ frame->uploading_failure = ERROR_OVER_SIZE_LIMIT;
-+ if (!more)
-+ gr_reportuploaderror(frame);
-+ return;
-+ }
-+
-+ // If there is no open file corresponding to the image, create it.
-+ if (!frame->open_file) {
-+ gr_make_sure_tmpdir_exists();
-+ char filename[MAX_FILENAME_SIZE];
-+ gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
-+ FILE *file = fopen(filename, frame->disk_size ? "a" : "w");
-+ if (!file) {
-+ frame->status = STATUS_UPLOADING_ERROR;
-+ frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE;
-+ if (!more)
-+ gr_reportuploaderror(frame);
-+ return;
-+ }
-+ frame->open_file = file;
-+ }
-+
-+ // Write data to the file and update disk size variables.
-+ fwrite(data, 1, data_size, frame->open_file);
-+ free(data);
-+ frame->disk_size += data_size;
-+ frame->image->total_disk_size += data_size;
-+ images_disk_size += data_size;
-+ gr_touch_frame(frame);
-+
-+ if (more) {
-+ current_upload_image_id = frame->image->image_id;
-+ current_upload_frame_index = frame->index;
-+ } else {
-+ current_upload_image_id = 0;
-+ current_upload_frame_index = 0;
-+ // Close the file.
-+ if (frame->open_file) {
-+ fclose(frame->open_file);
-+ frame->open_file = NULL;
-+ }
-+ frame->status = STATUS_UPLOADING_SUCCESS;
-+ uint32_t placement_id = frame->image->default_placement;
-+ if (frame->expected_size &&
-+ frame->expected_size != frame->disk_size) {
-+ // Report failure if the uploaded image size doesn't
-+ // match the expected size.
-+ frame->status = STATUS_UPLOADING_ERROR;
-+ frame->uploading_failure = ERROR_UNEXPECTED_SIZE;
-+ gr_reportuploaderror(frame);
-+ } else {
-+ // Make sure to redraw all existing image instances.
-+ gr_schedule_image_redraw(frame->image);
-+ // Try to load the image into ram and report the result.
-+ frame = gr_loadimage_and_report(frame);
-+ // If there is a non-virtual image placement, we may
-+ // need to display it.
-+ if (frame && frame->index == 1) {
-+ Image *img = frame->image;
-+ ImagePlacement *placement = NULL;
-+ kh_foreach_value(img->placements, placement, {
-+ gr_display_nonvirtual_placement(placement);
-+ });
-+ }
-+ }
-+ }
-+
-+ // Check whether we need to delete old images.
-+ gr_check_limits();
-+}
-+
-+/// Finds the image either by id or by number specified in the command and sets
-+/// the image_id of `cmd` if the image was found.
-+static Image *gr_find_image_for_command(GraphicsCommand *cmd) {
-+ if (cmd->image_id)
-+ return gr_find_image(cmd->image_id);
-+ Image *img = NULL;
-+ // If the image number is not specified, we can't find the image, unless
-+ // it's a put command, in which case we will try the last image.
-+ if (cmd->image_number == 0 && cmd->action == 'p')
-+ img = gr_find_image(last_image_id);
-+ else
-+ img = gr_find_image_by_number(cmd->image_number);
-+ if (img)
-+ cmd->image_id = img->image_id;
-+ return img;
-+}
-+
-+/// Creates a new image or a new frame in an existing image (depending on the
-+/// command's action) and initializes its parameters from the command.
-+static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) {
-+ if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 &&
-+ cmd->compression != 0) {
-+ gr_reporterror_cmd(cmd, "EINVAL: compression is supported only "
-+ "for raw pixel data (f=32 or f=24)");
-+ // Even though we report an error, we still create an image.
-+ }
-+
-+ Image *img = NULL;
-+ if (cmd->action == 'f') {
-+ // If it's a frame transmission action, there must be an
-+ // existing image.
-+ img = gr_find_image_for_command(cmd);
-+ if (!img) {
-+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
-+ return NULL;
-+ }
-+ } else {
-+ // Otherwise create a new image object. If the action is `q`,
-+ // we'll use random id instead of the one specified in the
-+ // command.
-+ uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id;
-+ img = gr_new_image(image_id);
-+ if (!img)
-+ return NULL;
-+ if (cmd->action == 'q')
-+ img->query_id = cmd->image_id;
-+ else if (!cmd->image_id)
-+ cmd->image_id = img->image_id;
-+ // Set the image number.
-+ img->image_number = cmd->image_number;
-+ }
-+
-+ ImageFrame *frame = gr_append_new_frame(img);
-+ // Initialize the frame.
-+ frame->expected_size = cmd->size;
-+ frame->format = cmd->format;
-+ frame->compression = cmd->compression;
-+ frame->background_color = cmd->background_color;
-+ frame->background_frame_index = cmd->background_frame;
-+ frame->gap = cmd->gap;
-+ img->total_duration += frame->gap;
-+ frame->blend = !cmd->replace_instead_of_blending;
-+ frame->data_pix_width = cmd->frame_pix_width;
-+ frame->data_pix_height = cmd->frame_pix_height;
-+ if (cmd->action == 'f') {
-+ frame->x = cmd->frame_dst_pix_x;
-+ frame->y = cmd->frame_dst_pix_y;
-+ }
-+ // We save the quietness information in the frame because for direct
-+ // transmission subsequent transmission command won't contain this info.
-+ frame->quiet = cmd->quiet;
-+ return frame;
-+}
-+
-+/// Removes a file if it actually looks like a temporary file.
-+static void gr_delete_tmp_file(const char *filename) {
-+ if (strstr(filename, "tty-graphics-protocol") == NULL)
-+ return;
-+ if (strstr(filename, "/tmp/") != filename) {
-+ const char *tmpdir = getenv("TMPDIR");
-+ if (!tmpdir || !tmpdir[0] ||
-+ strstr(filename, tmpdir) != filename)
-+ return;
-+ }
-+ unlink(filename);
-+}
-+
-+/// Handles a data transmission command.
-+static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) {
-+ // The default is direct transmission.
-+ if (!cmd->transmission_medium)
-+ cmd->transmission_medium = 'd';
-+
-+ // If neither id, nor image number is specified, and the transmission
-+ // medium is 'd' (or unspecified), and there is an active direct upload,
-+ // this is a continuation of the upload.
-+ if (current_upload_image_id != 0 && cmd->image_id == 0 &&
-+ cmd->image_number == 0 && cmd->transmission_medium == 'd') {
-+ cmd->image_id = current_upload_image_id;
-+ GR_LOG("No images id is specified, continuing uploading %u\n",
-+ cmd->image_id);
-+ }
-+
-+ ImageFrame *frame = NULL;
-+ if (cmd->transmission_medium == 'f' ||
-+ cmd->transmission_medium == 't') {
-+ // File transmission.
-+ // Create a new image or a new frame of an existing image.
-+ frame = gr_new_image_or_frame_from_command(cmd);
-+ if (!frame)
-+ return NULL;
-+ last_image_id = frame->image->image_id;
-+ // Decode the filename.
-+ char *original_filename = gr_base64dec(cmd->payload, NULL);
-+ GR_LOG("Copying image %s\n",
-+ sanitized_filename(original_filename));
-+ // Stat the file and check that it's a regular file and not too
-+ // big.
-+ struct stat st;
-+ int stat_res = stat(original_filename, &st);
-+ const char *stat_error = NULL;
-+ if (stat_res)
-+ stat_error = strerror(errno);
-+ else if (!S_ISREG(st.st_mode))
-+ stat_error = "Not a regular file";
-+ else if (st.st_size == 0)
-+ stat_error = "The size of the file is zero";
-+ else if (st.st_size > graphics_max_single_image_file_size)
-+ stat_error = "The file is too large";
-+ if (stat_error) {
-+ gr_reporterror_cmd(cmd,
-+ "EBADF: %s", stat_error);
-+ fprintf(stderr, "Could not load the file %s\n",
-+ sanitized_filename(original_filename));
-+ frame->status = STATUS_UPLOADING_ERROR;
-+ frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
-+ } else {
-+ gr_make_sure_tmpdir_exists();
-+ // Build the filename for the cached copy of the file.
-+ char cache_filename[MAX_FILENAME_SIZE];
-+ gr_get_frame_filename(frame, cache_filename,
-+ MAX_FILENAME_SIZE);
-+ // We will create a symlink to the original file, and
-+ // then copy the file to the temporary cache dir. We do
-+ // this symlink trick mostly to be able to use cp for
-+ // copying, and avoid escaping file name characters when
-+ // calling system at the same time.
-+ char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0};
-+ strcat(tmp_filename_symlink, cache_filename);
-+ strcat(tmp_filename_symlink, ".sym");
-+ char command[MAX_FILENAME_SIZE + 256];
-+ size_t len =
-+ snprintf(command, MAX_FILENAME_SIZE + 255,
-+ "cp '%s' '%s'", tmp_filename_symlink,
-+ cache_filename);
-+ if (len > MAX_FILENAME_SIZE + 255 ||
-+ symlink(original_filename, tmp_filename_symlink) ||
-+ system(command) != 0) {
-+ gr_reporterror_cmd(cmd,
-+ "EBADF: could not copy the "
-+ "image to the cache dir");
-+ fprintf(stderr,
-+ "Could not copy the image "
-+ "%s (symlink %s) to %s",
-+ sanitized_filename(original_filename),
-+ tmp_filename_symlink, cache_filename);
-+ frame->status = STATUS_UPLOADING_ERROR;
-+ frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
-+ } else {
-+ // Get the file size of the copied file.
-+ frame->status = STATUS_UPLOADING_SUCCESS;
-+ frame->disk_size = st.st_size;
-+ frame->image->total_disk_size += st.st_size;
-+ images_disk_size += frame->disk_size;
-+ if (frame->expected_size &&
-+ frame->expected_size != frame->disk_size) {
-+ // The file has unexpected size.
-+ frame->status = STATUS_UPLOADING_ERROR;
-+ frame->uploading_failure =
-+ ERROR_UNEXPECTED_SIZE;
-+ gr_reportuploaderror(frame);
-+ } else {
-+ // Everything seems fine, try to load
-+ // and redraw existing instances.
-+ gr_schedule_image_redraw(frame->image);
-+ frame = gr_loadimage_and_report(frame);
-+ }
-+ }
-+ // Delete the symlink.
-+ unlink(tmp_filename_symlink);
-+ // Delete the original file if it's temporary.
-+ if (cmd->transmission_medium == 't')
-+ gr_delete_tmp_file(original_filename);
-+ }
-+ free(original_filename);
-+ gr_check_limits();
-+ } else if (cmd->transmission_medium == 'd') {
-+ // Direct transmission (default if 't' is not specified).
-+ frame = gr_get_last_frame(gr_find_image_for_command(cmd));
-+ if (frame && frame->status == STATUS_UPLOADING) {
-+ // This is a continuation of the previous transmission.
-+ cmd->is_direct_transmission_continuation = 1;
-+ gr_append_data(frame, cmd->payload, cmd->more);
-+ return frame;
-+ }
-+ // If no action is specified, it's not the first transmission
-+ // command. If we couldn't find the image, something went wrong
-+ // and we should just drop this command.
-+ if (cmd->action == 0)
-+ return NULL;
-+ // Otherwise create a new image or frame structure.
-+ frame = gr_new_image_or_frame_from_command(cmd);
-+ if (!frame)
-+ return NULL;
-+ last_image_id = frame->image->image_id;
-+ frame->status = STATUS_UPLOADING;
-+ // Start appending data.
-+ gr_append_data(frame, cmd->payload, cmd->more);
-+ } else {
-+ gr_reporterror_cmd(
-+ cmd,
-+ "EINVAL: transmission medium '%c' is not supported",
-+ cmd->transmission_medium);
-+ return NULL;
-+ }
-+
-+ return frame;
-+}
-+
-+/// Handles the 'put' command by creating a placement.
-+static void gr_handle_put_command(GraphicsCommand *cmd) {
-+ if (cmd->image_id == 0 && cmd->image_number == 0) {
-+ gr_reporterror_cmd(cmd,
-+ "EINVAL: neither image id nor image number "
-+ "are specified or both are zero");
-+ return;
-+ }
-+
-+ // Find the image with the id or number.
-+ Image *img = gr_find_image_for_command(cmd);
-+ if (!img) {
-+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
-+ return;
-+ }
-+
-+ // Create a placement. If a placement with the same id already exists,
-+ // it will be deleted. If the id is zero, a random id will be generated.
-+ ImagePlacement *placement = gr_new_placement(img, cmd->placement_id);
-+ placement->virtual = cmd->virtual;
-+ placement->src_pix_x = cmd->src_pix_x;
-+ placement->src_pix_y = cmd->src_pix_y;
-+ placement->src_pix_width = cmd->src_pix_width;
-+ placement->src_pix_height = cmd->src_pix_height;
-+ placement->cols = cmd->columns;
-+ placement->rows = cmd->rows;
-+ placement->do_not_move_cursor = cmd->do_not_move_cursor;
-+
-+ if (placement->virtual) {
-+ placement->scale_mode = SCALE_MODE_CONTAIN;
-+ } else if (placement->cols && placement->rows) {
-+ // For classic placements the default is to stretch the image if
-+ // both cols and rows are specified.
-+ placement->scale_mode = SCALE_MODE_FILL;
-+ } else if (placement->cols || placement->rows) {
-+ // But if only one of them is specified, the default is to
-+ // contain.
-+ placement->scale_mode = SCALE_MODE_CONTAIN;
-+ } else {
-+ // If none of them are specified, the default is to use the
-+ // original size.
-+ placement->scale_mode = SCALE_MODE_NONE;
-+ }
-+
-+ // Display the placement unless it's virtual.
-+ gr_display_nonvirtual_placement(placement);
-+
-+ // Report success.
-+ gr_reportsuccess_cmd(cmd);
-+}
-+
-+/// Information about what to delete.
-+typedef struct DeletionData {
-+ uint32_t image_id;
-+ uint32_t placement_id;
-+ /// If true, delete the image object if there are no more placements.
-+ char delete_image_if_no_ref;
-+} DeletionData;
-+
-+/// The callback called for each cell to perform deletion.
-+static int gr_deletion_callback(void *data, uint32_t image_id,
-+ uint32_t placement_id, int col,
-+ int row, char is_classic) {
-+ DeletionData *del_data = data;
-+ // Leave unicode placeholders alone.
-+ if (!is_classic)
-+ return 0;
-+ if (del_data->image_id && del_data->image_id != image_id)
-+ return 0;
-+ if (del_data->placement_id && del_data->placement_id != placement_id)
-+ return 0;
-+ Image *img = gr_find_image(image_id);
-+ // If the image is already deleted, just erase the placeholder.
-+ if (!img)
-+ return 1;
-+ // Delete the placement.
-+ if (placement_id)
-+ gr_delete_placement(gr_find_placement(img, placement_id));
-+ // Delete the image if image deletion is requested (uppercase delete
-+ // specifier) and there are no more placements.
-+ if (del_data->delete_image_if_no_ref && kh_size(img->placements) == 0)
-+ gr_delete_image(img);
-+ return 1;
-+}
-+
-+/// Handles the delete command.
-+static void gr_handle_delete_command(GraphicsCommand *cmd) {
-+ DeletionData del_data = {0};
-+ del_data.delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0;
-+ char d = tolower(cmd->delete_specifier);
-+
-+ if (d == 'n') {
-+ d = 'i';
-+ Image *img = gr_find_image_by_number(cmd->image_number);
-+ if (!img)
-+ return;
-+ del_data.image_id = img->image_id;
-+ }
-+
-+ if (!d || d == 'a') {
-+ // Delete all visible placements.
-+ gr_for_each_image_cell(gr_deletion_callback, &del_data);
-+ } else if (d == 'i') {
-+ // Delete the specified image by image id and maybe placement
-+ // id.
-+ if (!del_data.image_id)
-+ del_data.image_id = cmd->image_id;
-+ if (!del_data.image_id) {
-+ fprintf(stderr,
-+ "ERROR: image id is not specified in the "
-+ "delete command\n");
-+ return;
-+ }
-+ del_data.placement_id = cmd->placement_id;
-+ // NOTE: It's not very clear whether we should delete the image
-+ // even if there are no _visible_ placements to delete. We do
-+ // this because otherwise there is no way to delete an image
-+ // with virtual placements in one command.
-+ if (!del_data.placement_id && del_data.delete_image_if_no_ref)
-+ gr_delete_image(gr_find_image(cmd->image_id));
-+ gr_for_each_image_cell(gr_deletion_callback, &del_data);
-+ } else {
-+ fprintf(stderr,
-+ "WARNING: unsupported value of the d key: '%c'. The "
-+ "command is ignored.\n",
-+ cmd->delete_specifier);
-+ }
-+}
-+
-+static void gr_handle_animation_control_command(GraphicsCommand *cmd) {
-+ if (cmd->image_id == 0 && cmd->image_number == 0) {
-+ gr_reporterror_cmd(cmd,
-+ "EINVAL: neither image id nor image number "
-+ "are specified or both are zero");
-+ return;
-+ }
-+
-+ // Find the image with the id or number.
-+ Image *img = gr_find_image_for_command(cmd);
-+ if (!img) {
-+ gr_reporterror_cmd(cmd, "ENOENT: image not found");
-+ return;
-+ }
-+
-+ // Find the frame to edit, if requested.
-+ ImageFrame *frame = NULL;
-+ if (cmd->edit_frame)
-+ frame = gr_get_frame(img, cmd->edit_frame);
-+ if (cmd->edit_frame || cmd->gap) {
-+ if (!frame) {
-+ gr_reporterror_cmd(cmd, "ENOENT: frame %d not found",
-+ cmd->edit_frame);
-+ return;
-+ }
-+ if (cmd->gap) {
-+ img->total_duration -= frame->gap;
-+ frame->gap = cmd->gap;
-+ img->total_duration += frame->gap;
-+ }
-+ }
-+
-+ // Set animation-related parameters of the image.
-+ if (cmd->current_frame)
-+ img->current_frame = cmd->current_frame;
-+ if (cmd->animation_state) {
-+ if (cmd->animation_state == 1) {
-+ img->animation_state = ANIMATION_STATE_STOPPED;
-+ } else if (cmd->animation_state == 2) {
-+ img->animation_state = ANIMATION_STATE_LOADING;
-+ } else if (cmd->animation_state == 3) {
-+ img->animation_state = ANIMATION_STATE_LOOPING;
-+ } else {
-+ gr_reporterror_cmd(
-+ cmd, "EINVAL: invalid animation state: %d",
-+ cmd->animation_state);
-+ }
-+ }
-+ // TODO: Set the number of loops to cmd->loops
-+
-+ // Make sure we redraw all instances of the image.
-+ gr_schedule_image_redraw(img);
-+}
-+
-+/// Handles a command.
-+static void gr_handle_command(GraphicsCommand *cmd) {
-+ if (!cmd->image_id && !cmd->image_number) {
-+ // If there is no image id or image number, nobody expects a
-+ // response, so set quiet to 2.
-+ cmd->quiet = 2;
-+ }
-+ ImageFrame *frame = NULL;
-+ switch (cmd->action) {
-+ case 0:
-+ // If no action is specified, it may be a data transmission
-+ // command if 'm=' is specified.
-+ if (cmd->is_data_transmission) {
-+ gr_handle_transmit_command(cmd);
-+ break;
-+ }
-+ gr_reporterror_cmd(cmd, "EINVAL: no action specified");
-+ break;
-+ case 't':
-+ case 'q':
-+ case 'f':
-+ // Transmit data. 'q' means query, which is basically the same
-+ // as transmit, but the image is discarded, and the id is fake.
-+ // 'f' appends a frame to an existing image.
-+ gr_handle_transmit_command(cmd);
-+ break;
-+ case 'p':
-+ // Display (put) the image.
-+ gr_handle_put_command(cmd);
-+ break;
-+ case 'T':
-+ // Transmit and display.
-+ frame = gr_handle_transmit_command(cmd);
-+ if (frame && !cmd->is_direct_transmission_continuation) {
-+ gr_handle_put_command(cmd);
-+ if (cmd->placement_id)
-+ frame->image->initial_placement_id =
-+ cmd->placement_id;
-+ }
-+ break;
-+ case 'd':
-+ gr_handle_delete_command(cmd);
-+ break;
-+ case 'a':
-+ gr_handle_animation_control_command(cmd);
-+ break;
-+ default:
-+ gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c",
-+ cmd->action);
-+ return;
-+ }
-+}
-+
-+/// A partially parsed key-value pair.
-+typedef struct KeyAndValue {
-+ char *key_start;
-+ char *val_start;
-+ unsigned key_len, val_len;
-+} KeyAndValue;
-+
-+/// Parses the value of a key and assigns it to the appropriate field of `cmd`.
-+static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) {
-+ char *key_start = kv->key_start;
-+ char *key_end = key_start + kv->key_len;
-+ char *value_start = kv->val_start;
-+ char *value_end = value_start + kv->val_len;
-+ // Currently all keys are one-character.
-+ if (key_end - key_start != 1) {
-+ gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s",
-+ key_end - key_start, key_start);
-+ return;
-+ }
-+ long num = 0;
-+ if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' ||
-+ *key_start == 'o') {
-+ // Some keys have one-character values.
-+ if (value_end - value_start != 1) {
-+ gr_reporterror_cmd(
-+ cmd,
-+ "EINVAL: value of 'a', 't' or 'd' must be a "
-+ "single char: %s",
-+ key_start);
-+ return;
-+ }
-+ } else {
-+ // All the other keys have integer values.
-+ char *num_end = NULL;
-+ num = strtol(value_start, &num_end, 10);
-+ if (num_end != value_end) {
-+ gr_reporterror_cmd(
-+ cmd, "EINVAL: could not parse number value: %s",
-+ key_start);
-+ return;
-+ }
-+ }
-+ switch (*key_start) {
-+ case 'a':
-+ cmd->action = *value_start;
-+ break;
-+ case 't':
-+ cmd->transmission_medium = *value_start;
-+ break;
-+ case 'd':
-+ cmd->delete_specifier = *value_start;
-+ break;
-+ case 'q':
-+ cmd->quiet = num;
-+ break;
-+ case 'f':
-+ cmd->format = num;
-+ if (num != 0 && num != 24 && num != 32 && num != 100) {
-+ gr_reporterror_cmd(
-+ cmd,
-+ "EINVAL: unsupported format specification: %s",
-+ key_start);
-+ }
-+ break;
-+ case 'o':
-+ cmd->compression = *value_start;
-+ if (cmd->compression != 'z') {
-+ gr_reporterror_cmd(cmd,
-+ "EINVAL: unsupported compression "
-+ "specification: %s",
-+ key_start);
-+ }
-+ break;
-+ case 's':
-+ if (cmd->action == 'a')
-+ cmd->animation_state = num;
-+ else
-+ cmd->frame_pix_width = num;
-+ break;
-+ case 'v':
-+ if (cmd->action == 'a')
-+ cmd->loops = num;
-+ else
-+ cmd->frame_pix_height = num;
-+ break;
-+ case 'i':
-+ cmd->image_id = num;
-+ break;
-+ case 'I':
-+ cmd->image_number = num;
-+ break;
-+ case 'p':
-+ cmd->placement_id = num;
-+ break;
-+ case 'x':
-+ cmd->src_pix_x = num;
-+ cmd->frame_dst_pix_x = num;
-+ break;
-+ case 'y':
-+ if (cmd->action == 'f')
-+ cmd->frame_dst_pix_y = num;
-+ else
-+ cmd->src_pix_y = num;
-+ break;
-+ case 'w':
-+ cmd->src_pix_width = num;
-+ break;
-+ case 'h':
-+ cmd->src_pix_height = num;
-+ break;
-+ case 'c':
-+ if (cmd->action == 'f')
-+ cmd->background_frame = num;
-+ else if (cmd->action == 'a')
-+ cmd->current_frame = num;
-+ else
-+ cmd->columns = num;
-+ break;
-+ case 'r':
-+ if (cmd->action == 'f' || cmd->action == 'a')
-+ cmd->edit_frame = num;
-+ else
-+ cmd->rows = num;
-+ break;
-+ case 'm':
-+ cmd->is_data_transmission = 1;
-+ cmd->more = num;
-+ break;
-+ case 'S':
-+ cmd->size = num;
-+ break;
-+ case 'U':
-+ cmd->virtual = num;
-+ break;
-+ case 'X':
-+ if (cmd->action == 'f')
-+ cmd->replace_instead_of_blending = num;
-+ else
-+ break; /*ignore*/
-+ break;
-+ case 'Y':
-+ if (cmd->action == 'f')
-+ cmd->background_color = num;
-+ else
-+ break; /*ignore*/
-+ break;
-+ case 'z':
-+ if (cmd->action == 'f' || cmd->action == 'a')
-+ cmd->gap = num;
-+ else
-+ break; /*ignore*/
-+ break;
-+ case 'C':
-+ cmd->do_not_move_cursor = num;
-+ break;
-+ default:
-+ gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s",
-+ key_start);
-+ return;
-+ }
-+}
-+
-+/// Parse and execute a graphics command. `buf` must start with 'G' and contain
-+/// at least `len + 1` characters. Returns 1 on success.
-+int gr_parse_command(char *buf, size_t len) {
-+ if (buf[0] != 'G')
-+ return 0;
-+
-+ memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult));
-+
-+ global_command_counter++;
-+ GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf);
-+
-+ // Eat the 'G'.
-+ ++buf;
-+ --len;
-+
-+ GraphicsCommand cmd = {.command = buf};
-+ // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to
-+ // parse the payload.
-+ char state = 'k';
-+ // An array of partially parsed key-value pairs.
-+ KeyAndValue key_vals[32];
-+ unsigned key_vals_count = 0;
-+ char *key_start = buf;
-+ char *key_end = NULL;
-+ char *val_start = NULL;
-+ char *val_end = NULL;
-+ char *c = buf;
-+ while (c - buf < len + 1) {
-+ if (state == 'k') {
-+ switch (*c) {
-+ case ',':
-+ case ';':
-+ case '\0':
-+ state = *c == ',' ? 'k' : 'p';
-+ key_end = c;
-+ gr_reporterror_cmd(
-+ &cmd, "EINVAL: key without value: %s ",
-+ key_start);
-+ break;
-+ case '=':
-+ key_end = c;
-+ state = 'v';
-+ val_start = c + 1;
-+ break;
-+ default:
-+ break;
-+ }
-+ } else if (state == 'v') {
-+ switch (*c) {
-+ case ',':
-+ case ';':
-+ case '\0':
-+ state = *c == ',' ? 'k' : 'p';
-+ val_end = c;
-+ if (key_vals_count >=
-+ sizeof(key_vals) / sizeof(*key_vals)) {
-+ gr_reporterror_cmd(&cmd,
-+ "EINVAL: too many "
-+ "key-value pairs");
-+ break;
-+ }
-+ key_vals[key_vals_count].key_start = key_start;
-+ key_vals[key_vals_count].val_start = val_start;
-+ key_vals[key_vals_count].key_len =
-+ key_end - key_start;
-+ key_vals[key_vals_count].val_len =
-+ val_end - val_start;
-+ ++key_vals_count;
-+ key_start = c + 1;
-+ break;
-+ default:
-+ break;
-+ }
-+ } else if (state == 'p') {
-+ cmd.payload = c;
-+ // break out of the loop, we don't check the payload
-+ break;
-+ }
-+ ++c;
-+ }
-+
-+ // Set the action key ('a=') first because we need it to disambiguate
-+ // some keys. Also set 'i=' and 'I=' for better error reporting.
-+ for (unsigned i = 0; i < key_vals_count; ++i) {
-+ if (key_vals[i].key_len == 1) {
-+ char *start = key_vals[i].key_start;
-+ if (*start == 'a' || *start == 'i' || *start == 'I') {
-+ gr_set_keyvalue(&cmd, &key_vals[i]);
-+ break;
-+ }
-+ }
-+ }
-+ // Set the rest of the keys.
-+ for (unsigned i = 0; i < key_vals_count; ++i)
-+ gr_set_keyvalue(&cmd, &key_vals[i]);
-+
-+ if (!cmd.payload)
-+ cmd.payload = buf + len;
-+
-+ if (cmd.payload && cmd.payload[0])
-+ GR_LOG(" payload size: %ld\n", strlen(cmd.payload));
-+
-+ if (!graphics_command_result.error)
-+ gr_handle_command(&cmd);
-+
-+ if (graphics_debug_mode) {
-+ fprintf(stderr, "Response: ");
-+ for (const char *resp = graphics_command_result.response;
-+ *resp != '\0'; ++resp) {
-+ if (isprint(*resp))
-+ fprintf(stderr, "%c", *resp);
-+ else
-+ fprintf(stderr, "(0x%x)", *resp);
-+ }
-+ fprintf(stderr, "\n");
-+ }
-+
-+ // Make sure that we suppress response if needed. Usually cmd.quiet is
-+ // taken into account when creating the response, but it's not very
-+ // reliable in the current implementation.
-+ if (cmd.quiet) {
-+ if (!graphics_command_result.error || cmd.quiet >= 2)
-+ graphics_command_result.response[0] = '\0';
-+ }
-+
-+ return 1;
-+}
-+
-+////////////////////////////////////////////////////////////////////////////////
-+// base64 decoding part is basically copied from st.c
-+////////////////////////////////////////////////////////////////////////////////
-+
-+static const char gr_base64_digits[] = {
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54,
-+ 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2,
-+ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
-+ 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30,
-+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
-+ 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
-+
-+static char gr_base64_getc(const char **src) {
-+ while (**src && !isprint(**src))
-+ (*src)++;
-+ return **src ? *((*src)++) : '='; /* emulate padding if string ends */
-+}
-+
-+char *gr_base64dec(const char *src, size_t *size) {
-+ size_t in_len = strlen(src);
-+ char *result, *dst;
-+
-+ result = dst = malloc((in_len + 3) / 4 * 3 + 1);
-+ while (*src) {
-+ int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
-+ int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
-+ int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
-+ int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
-+
-+ if (a == -1 || b == -1)
-+ break;
-+
-+ *dst++ = (a << 2) | ((b & 0x30) >> 4);
-+ if (c == -1)
-+ break;
-+ *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2);
-+ if (d == -1)
-+ break;
-+ *dst++ = ((c & 0x03) << 6) | d;
-+ }
-+ *dst = '\0';
-+ if (size) {
-+ *size = dst - result;
-+ }
-+ return result;
-+}
-diff --git a/graphics.h b/graphics.h
-new file mode 100644
-index 0000000..2e75dea
---- /dev/null
-+++ b/graphics.h
-@@ -0,0 +1,107 @@
-+
-+#include <stdint.h>
-+#include <sys/types.h>
-+#include <X11/Xlib.h>
-+
-+/// Initialize the graphics module.
-+void gr_init(Display *disp, Visual *vis, Colormap cm);
-+/// Deinitialize the graphics module.
-+void gr_deinit();
-+
-+/// Add an image rectangle to a list if rectangles to draw. This function may
-+/// actually draw some rectangles, or it may wait till more rectangles are
-+/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`.
-+/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the
-+/// part of the image to draw (row/col indices are zero-based, ends are
-+/// excluded).
-+/// - `x_col` and `y_row` are the coordinates of the top-left corner of the
-+/// image in the terminal grid.
-+/// - `x_pix` and `y_pix` are the same but in pixels.
-+/// - `reverse` indicates whether colors should be inverted.
-+void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
-+ int img_start_col, int img_end_col, int img_start_row,
-+ int img_end_row, int x_col, int y_row, int x_pix,
-+ int y_pix, int cw, int ch, int reverse);
-+/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
-+void gr_start_drawing(Drawable buf, int cw, int ch);
-+/// Finish image drawing. This functions will draw all the rectangles left to
-+/// draw.
-+void gr_finish_drawing(Drawable buf);
-+/// Mark rows containing animations as dirty if it's time to redraw them. Must
-+/// be called right after `gr_start_drawing`.
-+void gr_mark_dirty_animations(int *dirty, int rows);
-+
-+/// Parse and execute a graphics command. `buf` must start with 'G' and contain
-+/// at least `len + 1` characters (including '\0'). Returns 1 on success.
-+/// Additional informations is returned through `graphics_command_result`.
-+int gr_parse_command(char *buf, size_t len);
-+
-+/// Executes `command` with the name of the file corresponding to `image_id` as
-+/// the argument. Executes xmessage with an error message on failure.
-+void gr_preview_image(uint32_t image_id, const char *command);
-+
-+/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
-+/// containing the information about an image and placement, and <st> is
-+/// specified with `st_executable`.
-+void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
-+ uint32_t imgcol, uint32_t imgrow,
-+ char is_classic_placeholder, int32_t diacritic_count,
-+ char *st_executable);
-+
-+/// Dumps the internal state (images and placements) to stderr.
-+void gr_dump_state();
-+
-+/// Unloads images to reduce RAM usage.
-+void gr_unload_images_to_reduce_ram();
-+
-+/// Executes `callback` for each image cell. `callback` may return 1 to erase
-+/// the cell or 0 to keep it. This function is implemented in `st.c`.
-+void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id,
-+ uint32_t placement_id, int col,
-+ int row, char is_classic),
-+ void *data);
-+
-+/// Marks all the rows containing the image with `image_id` as dirty.
-+void gr_schedule_image_redraw_by_id(uint32_t image_id);
-+
-+typedef enum {
-+ GRAPHICS_DEBUG_NONE = 0,
-+ GRAPHICS_DEBUG_LOG = 1,
-+ GRAPHICS_DEBUG_LOG_AND_BOXES = 2,
-+} GraphicsDebugMode;
-+
-+/// Print additional information, draw bounding bounding boxes, etc.
-+extern GraphicsDebugMode graphics_debug_mode;
-+
-+/// Whether to display images or just draw bounding boxes.
-+extern char graphics_display_images;
-+
-+/// The time in milliseconds until the next redraw to update animations.
-+/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`.
-+extern int graphics_next_redraw_delay;
-+
-+#define MAX_GRAPHICS_RESPONSE_LEN 256
-+
-+/// A structure representing the result of a graphics command.
-+typedef struct {
-+ /// Indicates if the terminal needs to be redrawn.
-+ char redraw;
-+ /// The response of the command that should be sent back to the client
-+ /// (may be empty if the quiet flag is set).
-+ char response[MAX_GRAPHICS_RESPONSE_LEN];
-+ /// Whether there was an error executing this command (not very useful,
-+ /// the response must be sent back anyway).
-+ char error;
-+ /// Whether the terminal has to create a placeholder for a non-virtual
-+ /// placement.
-+ char create_placeholder;
-+ /// The placeholder that needs to be created.
-+ struct {
-+ uint32_t rows, columns;
-+ uint32_t image_id, placement_id;
-+ char do_not_move_cursor;
-+ } placeholder;
-+} GraphicsCommandResult;
-+
-+/// The result of a graphics command.
-+extern GraphicsCommandResult graphics_command_result;
-diff --git a/icat-mini.sh b/icat-mini.sh
-new file mode 100755
-index 0000000..0a8ebab
---- /dev/null
-+++ b/icat-mini.sh
-@@ -0,0 +1,801 @@
-+#!/bin/sh
-+
-+# vim: shiftwidth=4
-+
-+script_name="$(basename "$0")"
-+
-+short_help="Usage: $script_name [OPTIONS] <image_file>
-+
-+This is a script to display images in the terminal using the kitty graphics
-+protocol with Unicode placeholders. It is very basic, please use something else
-+if you have alternatives.
-+
-+Options:
-+ -h Show this help.
-+ -s SCALE The scale of the image, may be floating point.
-+ -c N, --cols N The number of columns.
-+ -r N, --rows N The number of rows.
-+ --max-cols N The maximum number of columns.
-+ --max-rows N The maximum number of rows.
-+ --cell-size WxH The cell size in pixels.
-+ -m METHOD The uploading method, may be 'file', 'direct' or 'auto'.
-+ --speed SPEED The multiplier for the animation speed (float).
-+"
-+
-+# Exit the script on keyboard interrupt
-+trap "echo 'icat-mini was interrupted' >&2; exit 1" INT
-+
-+cols=""
-+rows=""
-+file=""
-+tty="/dev/tty"
-+uploading_method="auto"
-+cell_size=""
-+scale=1
-+max_cols=""
-+max_rows=""
-+speed=""
-+
-+# Parse the command line.
-+while [ $# -gt 0 ]; do
-+ case "$1" in
-+ -c|--columns|--cols)
-+ cols="$2"
-+ shift 2
-+ ;;
-+ -r|--rows|-l|--lines)
-+ rows="$2"
-+ shift 2
-+ ;;
-+ -s|--scale)
-+ scale="$2"
-+ shift 2
-+ ;;
-+ -h|--help)
-+ echo "$short_help"
-+ exit 0
-+ ;;
-+ -m|--upload-method|--uploading-method)
-+ uploading_method="$2"
-+ shift 2
-+ ;;
-+ --cell-size)
-+ cell_size="$2"
-+ shift 2
-+ ;;
-+ --max-cols)
-+ max_cols="$2"
-+ shift 2
-+ ;;
-+ --max-rows)
-+ max_rows="$2"
-+ shift 2
-+ ;;
-+ --speed)
-+ speed="$2"
-+ shift 2
-+ ;;
-+ --)
-+ file="$2"
-+ shift 2
-+ ;;
-+ -*)
-+ echo "Unknown option: $1" >&2
-+ exit 1
-+ ;;
-+ *)
-+ if [ -n "$file" ]; then
-+ echo "Multiple image files are not supported: $file and $1" >&2
-+ exit 1
-+ fi
-+ file="$1"
-+ shift
-+ ;;
-+ esac
-+done
-+
-+file="$(realpath "$file")"
-+
-+#####################################################################
-+# Adjust the terminal state
-+#####################################################################
-+
-+stty_orig="$(stty -g < "$tty")"
-+stty -echo < "$tty"
-+# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some
-+# horrible issues otherwise.
-+stty susp undef < "$tty"
-+stty -icanon < "$tty"
-+
-+restore_echo() {
-+ [ -n "$stty_orig" ] || return
-+ stty $stty_orig < "$tty"
-+}
-+
-+trap restore_echo EXIT TERM
-+
-+#####################################################################
-+# Detect imagemagick
-+#####################################################################
-+
-+# If there is the 'magick' command, use it instead of separate 'convert' and
-+# 'identify' commands.
-+if command -v magick > /dev/null; then
-+ identify="magick identify"
-+ convert="magick"
-+else
-+ identify="identify"
-+ convert="convert"
-+fi
-+
-+#####################################################################
-+# Detect tmux
-+#####################################################################
-+
-+# Check if we are inside tmux.
-+inside_tmux=""
-+if [ -n "$TMUX" ]; then
-+ case "$TERM" in
-+ *tmux*|*screen*)
-+ inside_tmux=1
-+ ;;
-+ esac
-+fi
-+
-+#####################################################################
-+# Compute the number of rows and columns
-+#####################################################################
-+
-+is_pos_int() {
-+ if [ -z "$1" ]; then
-+ return 1 # false
-+ fi
-+ if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then
-+ if [ "$1" -gt 0 ]; then
-+ return 0 # true
-+ fi
-+ fi
-+ return 1 # false
-+}
-+
-+if [ -n "$cols" ] || [ -n "$rows" ]; then
-+ if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then
-+ echo "You can't specify both max-cols/rows and cols/rows" >&2
-+ exit 1
-+ fi
-+fi
-+
-+# Get the max number of cols and rows.
-+[ -n "$max_cols" ] || max_cols="$(tput cols)"
-+[ -n "$max_rows" ] || max_rows="$(tput lines)"
-+if [ "$max_rows" -gt 255 ]; then
-+ max_rows=255
-+fi
-+
-+python_ioctl_command="import array, fcntl, termios
-+buf = array.array('H', [0, 0, 0, 0])
-+fcntl.ioctl(0, termios.TIOCGWINSZ, buf)
-+print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))"
-+
-+# Get the cell size in pixels if either cols or rows are not specified.
-+if [ -z "$cols" ] || [ -z "$rows" ]; then
-+ cell_width=""
-+ cell_height=""
-+ # If the cell size is specified, use it.
-+ if [ -n "$cell_size" ]; then
-+ cell_width="${cell_size%x*}"
-+ cell_height="${cell_size#*x}"
-+ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
-+ echo "Invalid cell size: $cell_size" >&2
-+ exit 1
-+ fi
-+ fi
-+ # Otherwise try to use TIOCGWINSZ ioctl via python.
-+ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
-+ cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)"
-+ cell_width="${cell_size_ioctl% *}"
-+ cell_height="${cell_size_ioctl#* }"
-+ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
-+ cell_width=""
-+ cell_height=""
-+ fi
-+ fi
-+ # If it didn't work, try to use csi XTWINOPS.
-+ if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
-+ if [ -n "$inside_tmux" ]; then
-+ printf '\ePtmux;\e\e[16t\e\\' >> "$tty"
-+ else
-+ printf '\e[16t' >> "$tty"
-+ fi
-+ # The expected response will look like ^[[6;<height>;<width>t
-+ term_response=""
-+ while true; do
-+ char=$(dd bs=1 count=1 <"$tty" 2>/dev/null)
-+ if [ "$char" = "t" ]; then
-+ break
-+ fi
-+ term_response="$term_response$char"
-+ done
-+ cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)"
-+ cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)"
-+ if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
-+ cell_width=8
-+ cell_height=16
-+ fi
-+ fi
-+fi
-+
-+# Compute a formula with bc and round to the nearest integer.
-+bc_round() {
-+ LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)"
-+}
-+
-+# Compute the number of rows and columns of the image.
-+if [ -z "$cols" ] || [ -z "$rows" ]; then
-+ # Get the size of the image and its resolution. If it's an animation, use
-+ # the first frame.
-+ format_output="$($identify -format '%w %h\n' "$file" | head -1)"
-+ img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)"
-+ img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)"
-+ if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then
-+ echo "Couldn't get image size from identify: $format_output" >&2
-+ echo >&2
-+ exit 1
-+ fi
-+ opt_cols_expr="(${scale}*${img_width}/${cell_width})"
-+ opt_rows_expr="(${scale}*${img_height}/${cell_height})"
-+ if [ -z "$cols" ] && [ -z "$rows" ]; then
-+ # If columns and rows are not specified, compute the optimal values
-+ # using the information about rows and columns per inch.
-+ cols="$(bc_round "$opt_cols_expr")"
-+ rows="$(bc_round "$opt_rows_expr")"
-+ # Make sure that automatically computed rows and columns are within some
-+ # sane limits
-+ if [ "$cols" -gt "$max_cols" ]; then
-+ rows="$(bc_round "$rows * $max_cols / $cols")"
-+ cols="$max_cols"
-+ fi
-+ if [ "$rows" -gt "$max_rows" ]; then
-+ cols="$(bc_round "$cols * $max_rows / $rows")"
-+ rows="$max_rows"
-+ fi
-+ elif [ -z "$cols" ]; then
-+ # If only one dimension is specified, compute the other one to match the
-+ # aspect ratio as close as possible.
-+ cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")"
-+ elif [ -z "$rows" ]; then
-+ rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")"
-+ fi
-+
-+ if [ "$cols" -lt 1 ]; then
-+ cols=1
-+ fi
-+ if [ "$rows" -lt 1 ]; then
-+ rows=1
-+ fi
-+fi
-+
-+#####################################################################
-+# Generate an image id
-+#####################################################################
-+
-+image_id=""
-+while [ -z "$image_id" ]; do
-+ image_id="$(shuf -i 16777217-4294967295 -n 1)"
-+ # Check that the id requires 24-bit fg colors.
-+ if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then
-+ image_id=""
-+ fi
-+done
-+
-+#####################################################################
-+# Uploading the image
-+#####################################################################
-+
-+# Choose the uploading method
-+if [ "$uploading_method" = "auto" ]; then
-+ if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then
-+ uploading_method="direct"
-+ else
-+ uploading_method="file"
-+ fi
-+fi
-+
-+# Functions to emit the start and the end of a graphics command.
-+if [ -n "$inside_tmux" ]; then
-+ # If we are in tmux we have to wrap the command in Ptmux.
-+ graphics_command_start='\ePtmux;\e\e_G'
-+ graphics_command_end='\e\e\\\e\\'
-+else
-+ graphics_command_start='\e_G'
-+ graphics_command_end='\e\\'
-+fi
-+
-+start_gr_command() {
-+ printf "$graphics_command_start" >> "$tty"
-+}
-+end_gr_command() {
-+ printf "$graphics_command_end" >> "$tty"
-+}
-+
-+# Send a graphics command with the correct start and end
-+gr_command() {
-+ start_gr_command
-+ printf '%s' "$1" >> "$tty"
-+ end_gr_command
-+}
-+
-+# Send an uploading command. Usage: gr_upload <action> <command> <file>
-+# Where <action> is a part of command that specifies the action, it will be
-+# repeated for every chunk (if the method is direct), and <command> is the rest
-+# of the command that specifies the image parameters. <action> and <command>
-+# must not include the transmission method or ';'.
-+# Example:
-+# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
-+gr_upload() {
-+ arg_action="$1"
-+ arg_command="$2"
-+ arg_file="$3"
-+ if [ "$uploading_method" = "file" ]; then
-+ # base64-encode the filename
-+ encoded_filename=$(printf '%s' "$arg_file" | base64 -w0)
-+ gr_command "${arg_action},${arg_command},t=f;${encoded_filename}"
-+ fi
-+ if [ "$uploading_method" = "direct" ]; then
-+ # Create a temporary directory to store the chunked image.
-+ chunkdir="$(mktemp -d)"
-+ if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then
-+ echo "Can't create a temp dir" >&2
-+ exit 1
-+ fi
-+ # base64-encode the file and split it into chunks. The size of each
-+ # graphics command shouldn't be more than 4096, so we set the size of an
-+ # encoded chunk to be 3968, slightly less than that.
-+ chunk_size=3968
-+ cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_"
-+
-+ # Issue a command indicating that we want to start data transmission for
-+ # a new image.
-+ gr_command "${arg_action},${arg_command},t=d,m=1"
-+
-+ # Transmit chunks.
-+ for chunk in "$chunkdir/chunk_"*; do
-+ start_gr_command
-+ printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty"
-+ cat "$chunk" >> "$tty"
-+ end_gr_command
-+ rm "$chunk"
-+ done
-+
-+ # Tell the terminal that we are done.
-+ gr_command "${arg_action},i=$image_id,m=0"
-+
-+ # Remove the temporary directory.
-+ rmdir "$chunkdir"
-+ fi
-+}
-+
-+delayed_frame_dir_cleanup() {
-+ arg_frame_dir="$1"
-+ sleep 2
-+ if [ -n "$arg_frame_dir" ]; then
-+ for frame in "$arg_frame_dir"/frame_*.png; do
-+ rm "$frame"
-+ done
-+ rmdir "$arg_frame_dir"
-+ fi
-+}
-+
-+upload_image_and_print_placeholder() {
-+ # Check if the file is an animation.
-+ frame_count=$($identify -format '%n\n' "$file" | head -n 1)
-+ if [ "$frame_count" -gt 1 ]; then
-+ # The file is an animation, decompose into frames and upload each frame.
-+ frame_dir="$(mktemp -d)"
-+ frame_dir="$HOME/temp/frames${frame_dir}"
-+ mkdir -p "$frame_dir"
-+ if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then
-+ echo "Can't create a temp dir for frames" >&2
-+ exit 1
-+ fi
-+
-+ # Decompose the animation into separate frames.
-+ $convert "$file" -coalesce "$frame_dir/frame_%06d.png"
-+
-+ # Get all frame delays at once, in centiseconds, as a space-separated
-+ # string.
-+ delays=$($identify -format "%T " "$file")
-+
-+ frame_number=1
-+ for frame in "$frame_dir"/frame_*.png; do
-+ # Read the delay for the current frame and convert it from
-+ # centiseconds to milliseconds.
-+ delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number")
-+ delay=$((delay * 10))
-+ # If the delay is 0, set it to 100ms.
-+ if [ "$delay" -eq 0 ]; then
-+ delay=100
-+ fi
-+
-+ if [ -n "$speed" ]; then
-+ delay=$(bc_round "$delay / $speed")
-+ fi
-+
-+ if [ "$frame_number" -eq 1 ]; then
-+ # Upload the first frame with a=T
-+ gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame"
-+ # Set the delay for the first frame and also play the animation
-+ # in loading mode (s=2).
-+ gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}"
-+ # Print the placeholder after the first frame to reduce the wait
-+ # time.
-+ print_placeholder
-+ else
-+ # Upload subsequent frames with a=f
-+ gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame"
-+ fi
-+
-+ frame_number=$((frame_number + 1))
-+ done
-+
-+ # Play the animation in loop mode (s=3).
-+ gr_command "a=a,v=1,s=3,i=${image_id}"
-+
-+ # Remove the temporary directory, but do it in the background with a
-+ # delay to avoid removing files before they are loaded by the terminal.
-+ delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null &
-+ else
-+ # The file is not an animation, upload it directly
-+ gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
-+ # Print the placeholder
-+ print_placeholder
-+ fi
-+}
-+
-+#####################################################################
-+# Printing the image placeholder
-+#####################################################################
-+
-+print_placeholder() {
-+ # Each line starts with the escape sequence to set the foreground color to
-+ # the image id.
-+ blue="$(expr "$image_id" % 256 )"
-+ green="$(expr \( "$image_id" / 256 \) % 256 )"
-+ red="$(expr \( "$image_id" / 65536 \) % 256 )"
-+ line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")"
-+ line_end="$(printf "\e[39;m")"
-+
-+ id4th="$(expr \( "$image_id" / 16777216 \) % 256 )"
-+ eval "id_diacritic=\$d${id4th}"
-+
-+ # Reset the brush state, mostly to reset the underline color.
-+ printf "\e[0m"
-+
-+ # Fill the output with characters representing the image
-+ for y in $(seq 0 "$(expr "$rows" - 1)"); do
-+ eval "row_diacritic=\$d${y}"
-+ printf '%s' "$line_start"
-+ for x in $(seq 0 "$(expr "$cols" - 1)"); do
-+ eval "col_diacritic=\$d${x}"
-+ # Note that when $x is out of bounds, the column diacritic will
-+ # be empty, meaning that the column should be guessed by the
-+ # terminal.
-+ if [ "$x" -ge "$num_diacritics" ]; then
-+ printf '%s' "${placeholder}${row_diacritic}"
-+ else
-+ printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}"
-+ fi
-+ done
-+ printf '%s\n' "$line_end"
-+ done
-+
-+ printf "\e[0m"
-+}
-+
-+d0="̅"
-+d1="̍"
-+d2="̎"
-+d3="̐"
-+d4="̒"
-+d5="̽"
-+d6="̾"
-+d7="̿"
-+d8="͆"
-+d9="͊"
-+d10="͋"
-+d11="͌"
-+d12="͐"
-+d13="͑"
-+d14="͒"
-+d15="͗"
-+d16="͛"
-+d17="ͣ"
-+d18="ͤ"
-+d19="ͥ"
-+d20="ͦ"
-+d21="ͧ"
-+d22="ͨ"
-+d23="ͩ"
-+d24="ͪ"
-+d25="ͫ"
-+d26="ͬ"
-+d27="ͭ"
-+d28="ͮ"
-+d29="ͯ"
-+d30="҃"
-+d31="҄"
-+d32="҅"
-+d33="҆"
-+d34="҇"
-+d35="֒"
-+d36="֓"
-+d37="֔"
-+d38="֕"
-+d39="֗"
-+d40="֘"
-+d41="֙"
-+d42="֜"
-+d43="֝"
-+d44="֞"
-+d45="֟"
-+d46="֠"
-+d47="֡"
-+d48="֨"
-+d49="֩"
-+d50="֫"
-+d51="֬"
-+d52="֯"
-+d53="ׄ"
-+d54="ؐ"
-+d55="ؑ"
-+d56="ؒ"
-+d57="ؓ"
-+d58="ؔ"
-+d59="ؕ"
-+d60="ؖ"
-+d61="ؗ"
-+d62="ٗ"
-+d63="٘"
-+d64="ٙ"
-+d65="ٚ"
-+d66="ٛ"
-+d67="ٝ"
-+d68="ٞ"
-+d69="ۖ"
-+d70="ۗ"
-+d71="ۘ"
-+d72="ۙ"
-+d73="ۚ"
-+d74="ۛ"
-+d75="ۜ"
-+d76="۟"
-+d77="۠"
-+d78="ۡ"
-+d79="ۢ"
-+d80="ۤ"
-+d81="ۧ"
-+d82="ۨ"
-+d83="۫"
-+d84="۬"
-+d85="ܰ"
-+d86="ܲ"
-+d87="ܳ"
-+d88="ܵ"
-+d89="ܶ"
-+d90="ܺ"
-+d91="ܽ"
-+d92="ܿ"
-+d93="݀"
-+d94="݁"
-+d95="݃"
-+d96="݅"
-+d97="݇"
-+d98="݉"
-+d99="݊"
-+d100="߫"
-+d101="߬"
-+d102="߭"
-+d103="߮"
-+d104="߯"
-+d105="߰"
-+d106="߱"
-+d107="߳"
-+d108="ࠖ"
-+d109="ࠗ"
-+d110="࠘"
-+d111="࠙"
-+d112="ࠛ"
-+d113="ࠜ"
-+d114="ࠝ"
-+d115="ࠞ"
-+d116="ࠟ"
-+d117="ࠠ"
-+d118="ࠡ"
-+d119="ࠢ"
-+d120="ࠣ"
-+d121="ࠥ"
-+d122="ࠦ"
-+d123="ࠧ"
-+d124="ࠩ"
-+d125="ࠪ"
-+d126="ࠫ"
-+d127="ࠬ"
-+d128="࠭"
-+d129="॑"
-+d130="॓"
-+d131="॔"
-+d132="ྂ"
-+d133="ྃ"
-+d134="྆"
-+d135="྇"
-+d136="፝"
-+d137="፞"
-+d138="፟"
-+d139="៝"
-+d140="᤺"
-+d141="ᨗ"
-+d142="᩵"
-+d143="᩶"
-+d144="᩷"
-+d145="᩸"
-+d146="᩹"
-+d147="᩺"
-+d148="᩻"
-+d149="᩼"
-+d150="᭫"
-+d151="᭭"
-+d152="᭮"
-+d153="᭯"
-+d154="᭰"
-+d155="᭱"
-+d156="᭲"
-+d157="᭳"
-+d158="᳐"
-+d159="᳑"
-+d160="᳒"
-+d161="᳚"
-+d162="᳛"
-+d163="᳠"
-+d164="᷀"
-+d165="᷁"
-+d166="᷃"
-+d167="᷄"
-+d168="᷅"
-+d169="᷆"
-+d170="᷇"
-+d171="᷈"
-+d172="᷉"
-+d173="᷋"
-+d174="᷌"
-+d175="᷑"
-+d176="᷒"
-+d177="ᷓ"
-+d178="ᷔ"
-+d179="ᷕ"
-+d180="ᷖ"
-+d181="ᷗ"
-+d182="ᷘ"
-+d183="ᷙ"
-+d184="ᷚ"
-+d185="ᷛ"
-+d186="ᷜ"
-+d187="ᷝ"
-+d188="ᷞ"
-+d189="ᷟ"
-+d190="ᷠ"
-+d191="ᷡ"
-+d192="ᷢ"
-+d193="ᷣ"
-+d194="ᷤ"
-+d195="ᷥ"
-+d196="ᷦ"
-+d197="᷾"
-+d198="⃐"
-+d199="⃑"
-+d200="⃔"
-+d201="⃕"
-+d202="⃖"
-+d203="⃗"
-+d204="⃛"
-+d205="⃜"
-+d206="⃡"
-+d207="⃧"
-+d208="⃩"
-+d209="⃰"
-+d210="⳯"
-+d211="⳰"
-+d212="⳱"
-+d213="ⷠ"
-+d214="ⷡ"
-+d215="ⷢ"
-+d216="ⷣ"
-+d217="ⷤ"
-+d218="ⷥ"
-+d219="ⷦ"
-+d220="ⷧ"
-+d221="ⷨ"
-+d222="ⷩ"
-+d223="ⷪ"
-+d224="ⷫ"
-+d225="ⷬ"
-+d226="ⷭ"
-+d227="ⷮ"
-+d228="ⷯ"
-+d229="ⷰ"
-+d230="ⷱ"
-+d231="ⷲ"
-+d232="ⷳ"
-+d233="ⷴ"
-+d234="ⷵ"
-+d235="ⷶ"
-+d236="ⷷ"
-+d237="ⷸ"
-+d238="ⷹ"
-+d239="ⷺ"
-+d240="ⷻ"
-+d241="ⷼ"
-+d242="ⷽ"
-+d243="ⷾ"
-+d244="ⷿ"
-+d245="꙯"
-+d246="꙼"
-+d247="꙽"
-+d248="꛰"
-+d249="꛱"
-+d250="꣠"
-+d251="꣡"
-+d252="꣢"
-+d253="꣣"
-+d254="꣤"
-+d255="꣥"
-+d256="꣦"
-+d257="꣧"
-+d258="꣨"
-+d259="꣩"
-+d260="꣪"
-+d261="꣫"
-+d262="꣬"
-+d263="꣭"
-+d264="꣮"
-+d265="꣯"
-+d266="꣰"
-+d267="꣱"
-+d268="ꪰ"
-+d269="ꪲ"
-+d270="ꪳ"
-+d271="ꪷ"
-+d272="ꪸ"
-+d273="ꪾ"
-+d274="꪿"
-+d275="꫁"
-+d276="︠"
-+d277="︡"
-+d278="︢"
-+d279="︣"
-+d280="︤"
-+d281="︥"
-+d282="︦"
-+d283="𐨏"
-+d284="𐨸"
-+d285="𝆅"
-+d286="𝆆"
-+d287="𝆇"
-+d288="𝆈"
-+d289="𝆉"
-+d290="𝆪"
-+d291="𝆫"
-+d292="𝆬"
-+d293="𝆭"
-+d294="𝉂"
-+d295="𝉃"
-+d296="𝉄"
-+
-+num_diacritics="297"
-+
-+placeholder="􎻮"
-+
-+#####################################################################
-+# Upload the image and print the placeholder
-+#####################################################################
-+
-+upload_image_and_print_placeholder
-diff --git a/khash.h b/khash.h
-new file mode 100644
-index 0000000..f75f347
---- /dev/null
-+++ b/khash.h
-@@ -0,0 +1,627 @@
-+/* The MIT License
-+
-+ Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk>
-+
-+ Permission is hereby granted, free of charge, to any person obtaining
-+ a copy of this software and associated documentation files (the
-+ "Software"), to deal in the Software without restriction, including
-+ without limitation the rights to use, copy, modify, merge, publish,
-+ distribute, sublicense, and/or sell copies of the Software, and to
-+ permit persons to whom the Software is furnished to do so, subject to
-+ the following conditions:
-+
-+ The above copyright notice and this permission notice shall be
-+ included in all copies or substantial portions of the Software.
-+
-+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-+ SOFTWARE.
-+*/
-+
-+/*
-+ An example:
-+
-+#include "khash.h"
-+KHASH_MAP_INIT_INT(32, char)
-+int main() {
-+ int ret, is_missing;
-+ khiter_t k;
-+ khash_t(32) *h = kh_init(32);
-+ k = kh_put(32, h, 5, &ret);
-+ kh_value(h, k) = 10;
-+ k = kh_get(32, h, 10);
-+ is_missing = (k == kh_end(h));
-+ k = kh_get(32, h, 5);
-+ kh_del(32, h, k);
-+ for (k = kh_begin(h); k != kh_end(h); ++k)
-+ if (kh_exist(h, k)) kh_value(h, k) = 1;
-+ kh_destroy(32, h);
-+ return 0;
-+}
-+*/
-+
-+/*
-+ 2013-05-02 (0.2.8):
-+
-+ * Use quadratic probing. When the capacity is power of 2, stepping function
-+ i*(i+1)/2 guarantees to traverse each bucket. It is better than double
-+ hashing on cache performance and is more robust than linear probing.
-+
-+ In theory, double hashing should be more robust than quadratic probing.
-+ However, my implementation is probably not for large hash tables, because
-+ the second hash function is closely tied to the first hash function,
-+ which reduce the effectiveness of double hashing.
-+
-+ Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php
-+
-+ 2011-12-29 (0.2.7):
-+
-+ * Minor code clean up; no actual effect.
-+
-+ 2011-09-16 (0.2.6):
-+
-+ * The capacity is a power of 2. This seems to dramatically improve the
-+ speed for simple keys. Thank Zilong Tan for the suggestion. Reference:
-+
-+ - http://code.google.com/p/ulib/
-+ - http://nothings.org/computer/judy/
-+
-+ * Allow to optionally use linear probing which usually has better
-+ performance for random input. Double hashing is still the default as it
-+ is more robust to certain non-random input.
-+
-+ * Added Wang's integer hash function (not used by default). This hash
-+ function is more robust to certain non-random input.
-+
-+ 2011-02-14 (0.2.5):
-+
-+ * Allow to declare global functions.
-+
-+ 2009-09-26 (0.2.4):
-+
-+ * Improve portability
-+
-+ 2008-09-19 (0.2.3):
-+
-+ * Corrected the example
-+ * Improved interfaces
-+
-+ 2008-09-11 (0.2.2):
-+
-+ * Improved speed a little in kh_put()
-+
-+ 2008-09-10 (0.2.1):
-+
-+ * Added kh_clear()
-+ * Fixed a compiling error
-+
-+ 2008-09-02 (0.2.0):
-+
-+ * Changed to token concatenation which increases flexibility.
-+
-+ 2008-08-31 (0.1.2):
-+
-+ * Fixed a bug in kh_get(), which has not been tested previously.
-+
-+ 2008-08-31 (0.1.1):
-+
-+ * Added destructor
-+*/
-+
-+
-+#ifndef __AC_KHASH_H
-+#define __AC_KHASH_H
-+
-+/*!
-+ @header
-+
-+ Generic hash table library.
-+ */
-+
-+#define AC_VERSION_KHASH_H "0.2.8"
-+
-+#include <stdlib.h>
-+#include <string.h>
-+#include <limits.h>
-+
-+/* compiler specific configuration */
-+
-+#if UINT_MAX == 0xffffffffu
-+typedef unsigned int khint32_t;
-+#elif ULONG_MAX == 0xffffffffu
-+typedef unsigned long khint32_t;
-+#endif
-+
-+#if ULONG_MAX == ULLONG_MAX
-+typedef unsigned long khint64_t;
-+#else
-+typedef unsigned long long khint64_t;
-+#endif
-+
-+#ifndef kh_inline
-+#ifdef _MSC_VER
-+#define kh_inline __inline
-+#else
-+#define kh_inline inline
-+#endif
-+#endif /* kh_inline */
-+
-+#ifndef klib_unused
-+#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3)
-+#define klib_unused __attribute__ ((__unused__))
-+#else
-+#define klib_unused
-+#endif
-+#endif /* klib_unused */
-+
-+typedef khint32_t khint_t;
-+typedef khint_t khiter_t;
-+
-+#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2)
-+#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1)
-+#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3)
-+#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1)))
-+#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1)))
-+#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1)))
-+#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1))
-+
-+#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4)
-+
-+#ifndef kroundup32
-+#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
-+#endif
-+
-+#ifndef kcalloc
-+#define kcalloc(N,Z) calloc(N,Z)
-+#endif
-+#ifndef kmalloc
-+#define kmalloc(Z) malloc(Z)
-+#endif
-+#ifndef krealloc
-+#define krealloc(P,Z) realloc(P,Z)
-+#endif
-+#ifndef kfree
-+#define kfree(P) free(P)
-+#endif
-+
-+static const double __ac_HASH_UPPER = 0.77;
-+
-+#define __KHASH_TYPE(name, khkey_t, khval_t) \
-+ typedef struct kh_##name##_s { \
-+ khint_t n_buckets, size, n_occupied, upper_bound; \
-+ khint32_t *flags; \
-+ khkey_t *keys; \
-+ khval_t *vals; \
-+ } kh_##name##_t;
-+
-+#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \
-+ extern kh_##name##_t *kh_init_##name(void); \
-+ extern void kh_destroy_##name(kh_##name##_t *h); \
-+ extern void kh_clear_##name(kh_##name##_t *h); \
-+ extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \
-+ extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \
-+ extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \
-+ extern void kh_del_##name(kh_##name##_t *h, khint_t x);
-+
-+#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
-+ SCOPE kh_##name##_t *kh_init_##name(void) { \
-+ return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \
-+ } \
-+ SCOPE void kh_destroy_##name(kh_##name##_t *h) \
-+ { \
-+ if (h) { \
-+ kfree((void *)h->keys); kfree(h->flags); \
-+ kfree((void *)h->vals); \
-+ kfree(h); \
-+ } \
-+ } \
-+ SCOPE void kh_clear_##name(kh_##name##_t *h) \
-+ { \
-+ if (h && h->flags) { \
-+ memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \
-+ h->size = h->n_occupied = 0; \
-+ } \
-+ } \
-+ SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \
-+ { \
-+ if (h->n_buckets) { \
-+ khint_t k, i, last, mask, step = 0; \
-+ mask = h->n_buckets - 1; \
-+ k = __hash_func(key); i = k & mask; \
-+ last = i; \
-+ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
-+ i = (i + (++step)) & mask; \
-+ if (i == last) return h->n_buckets; \
-+ } \
-+ return __ac_iseither(h->flags, i)? h->n_buckets : i; \
-+ } else return 0; \
-+ } \
-+ SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \
-+ { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \
-+ khint32_t *new_flags = 0; \
-+ khint_t j = 1; \
-+ { \
-+ kroundup32(new_n_buckets); \
-+ if (new_n_buckets < 4) new_n_buckets = 4; \
-+ if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \
-+ else { /* hash table size to be changed (shrink or expand); rehash */ \
-+ new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
-+ if (!new_flags) return -1; \
-+ memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
-+ if (h->n_buckets < new_n_buckets) { /* expand */ \
-+ khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
-+ if (!new_keys) { kfree(new_flags); return -1; } \
-+ h->keys = new_keys; \
-+ if (kh_is_map) { \
-+ khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
-+ if (!new_vals) { kfree(new_flags); return -1; } \
-+ h->vals = new_vals; \
-+ } \
-+ } /* otherwise shrink */ \
-+ } \
-+ } \
-+ if (j) { /* rehashing is needed */ \
-+ for (j = 0; j != h->n_buckets; ++j) { \
-+ if (__ac_iseither(h->flags, j) == 0) { \
-+ khkey_t key = h->keys[j]; \
-+ khval_t val; \
-+ khint_t new_mask; \
-+ new_mask = new_n_buckets - 1; \
-+ if (kh_is_map) val = h->vals[j]; \
-+ __ac_set_isdel_true(h->flags, j); \
-+ while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \
-+ khint_t k, i, step = 0; \
-+ k = __hash_func(key); \
-+ i = k & new_mask; \
-+ while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \
-+ __ac_set_isempty_false(new_flags, i); \
-+ if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \
-+ { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \
-+ if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \
-+ __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \
-+ } else { /* write the element and jump out of the loop */ \
-+ h->keys[i] = key; \
-+ if (kh_is_map) h->vals[i] = val; \
-+ break; \
-+ } \
-+ } \
-+ } \
-+ } \
-+ if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \
-+ h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
-+ if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
-+ } \
-+ kfree(h->flags); /* free the working space */ \
-+ h->flags = new_flags; \
-+ h->n_buckets = new_n_buckets; \
-+ h->n_occupied = h->size; \
-+ h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \
-+ } \
-+ return 0; \
-+ } \
-+ SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \
-+ { \
-+ khint_t x; \
-+ if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \
-+ if (h->n_buckets > (h->size<<1)) { \
-+ if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \
-+ *ret = -1; return h->n_buckets; \
-+ } \
-+ } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \
-+ *ret = -1; return h->n_buckets; \
-+ } \
-+ } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \
-+ { \
-+ khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \
-+ x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \
-+ if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \
-+ else { \
-+ last = i; \
-+ while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
-+ if (__ac_isdel(h->flags, i)) site = i; \
-+ i = (i + (++step)) & mask; \
-+ if (i == last) { x = site; break; } \
-+ } \
-+ if (x == h->n_buckets) { \
-+ if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
-+ else x = i; \
-+ } \
-+ } \
-+ } \
-+ if (__ac_isempty(h->flags, x)) { /* not present at all */ \
-+ h->keys[x] = key; \
-+ __ac_set_isboth_false(h->flags, x); \
-+ ++h->size; ++h->n_occupied; \
-+ *ret = 1; \
-+ } else if (__ac_isdel(h->flags, x)) { /* deleted */ \
-+ h->keys[x] = key; \
-+ __ac_set_isboth_false(h->flags, x); \
-+ ++h->size; \
-+ *ret = 2; \
-+ } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \
-+ return x; \
-+ } \
-+ SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \
-+ { \
-+ if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \
-+ __ac_set_isdel_true(h->flags, x); \
-+ --h->size; \
-+ } \
-+ }
-+
-+#define KHASH_DECLARE(name, khkey_t, khval_t) \
-+ __KHASH_TYPE(name, khkey_t, khval_t) \
-+ __KHASH_PROTOTYPES(name, khkey_t, khval_t)
-+
-+#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
-+ __KHASH_TYPE(name, khkey_t, khval_t) \
-+ __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
-+
-+#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
-+ KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
-+
-+/* --- BEGIN OF HASH FUNCTIONS --- */
-+
-+/*! @function
-+ @abstract Integer hash function
-+ @param key The integer [khint32_t]
-+ @return The hash value [khint_t]
-+ */
-+#define kh_int_hash_func(key) (khint32_t)(key)
-+/*! @function
-+ @abstract Integer comparison function
-+ */
-+#define kh_int_hash_equal(a, b) ((a) == (b))
-+/*! @function
-+ @abstract 64-bit integer hash function
-+ @param key The integer [khint64_t]
-+ @return The hash value [khint_t]
-+ */
-+#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11)
-+/*! @function
-+ @abstract 64-bit integer comparison function
-+ */
-+#define kh_int64_hash_equal(a, b) ((a) == (b))
-+/*! @function
-+ @abstract const char* hash function
-+ @param s Pointer to a null terminated string
-+ @return The hash value
-+ */
-+static kh_inline khint_t __ac_X31_hash_string(const char *s)
-+{
-+ khint_t h = (khint_t)*s;
-+ if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s;
-+ return h;
-+}
-+/*! @function
-+ @abstract Another interface to const char* hash function
-+ @param key Pointer to a null terminated string [const char*]
-+ @return The hash value [khint_t]
-+ */
-+#define kh_str_hash_func(key) __ac_X31_hash_string(key)
-+/*! @function
-+ @abstract Const char* comparison function
-+ */
-+#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0)
-+
-+static kh_inline khint_t __ac_Wang_hash(khint_t key)
-+{
-+ key += ~(key << 15);
-+ key ^= (key >> 10);
-+ key += (key << 3);
-+ key ^= (key >> 6);
-+ key += ~(key << 11);
-+ key ^= (key >> 16);
-+ return key;
-+}
-+#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key)
-+
-+/* --- END OF HASH FUNCTIONS --- */
-+
-+/* Other convenient macros... */
-+
-+/*!
-+ @abstract Type of the hash table.
-+ @param name Name of the hash table [symbol]
-+ */
-+#define khash_t(name) kh_##name##_t
-+
-+/*! @function
-+ @abstract Initiate a hash table.
-+ @param name Name of the hash table [symbol]
-+ @return Pointer to the hash table [khash_t(name)*]
-+ */
-+#define kh_init(name) kh_init_##name()
-+
-+/*! @function
-+ @abstract Destroy a hash table.
-+ @param name Name of the hash table [symbol]
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ */
-+#define kh_destroy(name, h) kh_destroy_##name(h)
-+
-+/*! @function
-+ @abstract Reset a hash table without deallocating memory.
-+ @param name Name of the hash table [symbol]
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ */
-+#define kh_clear(name, h) kh_clear_##name(h)
-+
-+/*! @function
-+ @abstract Resize a hash table.
-+ @param name Name of the hash table [symbol]
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param s New size [khint_t]
-+ */
-+#define kh_resize(name, h, s) kh_resize_##name(h, s)
-+
-+/*! @function
-+ @abstract Insert a key to the hash table.
-+ @param name Name of the hash table [symbol]
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param k Key [type of keys]
-+ @param r Extra return code: -1 if the operation failed;
-+ 0 if the key is present in the hash table;
-+ 1 if the bucket is empty (never used); 2 if the element in
-+ the bucket has been deleted [int*]
-+ @return Iterator to the inserted element [khint_t]
-+ */
-+#define kh_put(name, h, k, r) kh_put_##name(h, k, r)
-+
-+/*! @function
-+ @abstract Retrieve a key from the hash table.
-+ @param name Name of the hash table [symbol]
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param k Key [type of keys]
-+ @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t]
-+ */
-+#define kh_get(name, h, k) kh_get_##name(h, k)
-+
-+/*! @function
-+ @abstract Remove a key from the hash table.
-+ @param name Name of the hash table [symbol]
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param k Iterator to the element to be deleted [khint_t]
-+ */
-+#define kh_del(name, h, k) kh_del_##name(h, k)
-+
-+/*! @function
-+ @abstract Test whether a bucket contains data.
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param x Iterator to the bucket [khint_t]
-+ @return 1 if containing data; 0 otherwise [int]
-+ */
-+#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x)))
-+
-+/*! @function
-+ @abstract Get key given an iterator
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param x Iterator to the bucket [khint_t]
-+ @return Key [type of keys]
-+ */
-+#define kh_key(h, x) ((h)->keys[x])
-+
-+/*! @function
-+ @abstract Get value given an iterator
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param x Iterator to the bucket [khint_t]
-+ @return Value [type of values]
-+ @discussion For hash sets, calling this results in segfault.
-+ */
-+#define kh_val(h, x) ((h)->vals[x])
-+
-+/*! @function
-+ @abstract Alias of kh_val()
-+ */
-+#define kh_value(h, x) ((h)->vals[x])
-+
-+/*! @function
-+ @abstract Get the start iterator
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @return The start iterator [khint_t]
-+ */
-+#define kh_begin(h) (khint_t)(0)
-+
-+/*! @function
-+ @abstract Get the end iterator
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @return The end iterator [khint_t]
-+ */
-+#define kh_end(h) ((h)->n_buckets)
-+
-+/*! @function
-+ @abstract Get the number of elements in the hash table
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @return Number of elements in the hash table [khint_t]
-+ */
-+#define kh_size(h) ((h)->size)
-+
-+/*! @function
-+ @abstract Get the number of buckets in the hash table
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @return Number of buckets in the hash table [khint_t]
-+ */
-+#define kh_n_buckets(h) ((h)->n_buckets)
-+
-+/*! @function
-+ @abstract Iterate over the entries in the hash table
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param kvar Variable to which key will be assigned
-+ @param vvar Variable to which value will be assigned
-+ @param code Block of code to execute
-+ */
-+#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \
-+ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
-+ if (!kh_exist(h,__i)) continue; \
-+ (kvar) = kh_key(h,__i); \
-+ (vvar) = kh_val(h,__i); \
-+ code; \
-+ } }
-+
-+/*! @function
-+ @abstract Iterate over the values in the hash table
-+ @param h Pointer to the hash table [khash_t(name)*]
-+ @param vvar Variable to which value will be assigned
-+ @param code Block of code to execute
-+ */
-+#define kh_foreach_value(h, vvar, code) { khint_t __i; \
-+ for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
-+ if (!kh_exist(h,__i)) continue; \
-+ (vvar) = kh_val(h,__i); \
-+ code; \
-+ } }
-+
-+/* More convenient interfaces */
-+
-+/*! @function
-+ @abstract Instantiate a hash set containing integer keys
-+ @param name Name of the hash table [symbol]
-+ */
-+#define KHASH_SET_INIT_INT(name) \
-+ KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal)
-+
-+/*! @function
-+ @abstract Instantiate a hash map containing integer keys
-+ @param name Name of the hash table [symbol]
-+ @param khval_t Type of values [type]
-+ */
-+#define KHASH_MAP_INIT_INT(name, khval_t) \
-+ KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal)
-+
-+/*! @function
-+ @abstract Instantiate a hash set containing 64-bit integer keys
-+ @param name Name of the hash table [symbol]
-+ */
-+#define KHASH_SET_INIT_INT64(name) \
-+ KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal)
-+
-+/*! @function
-+ @abstract Instantiate a hash map containing 64-bit integer keys
-+ @param name Name of the hash table [symbol]
-+ @param khval_t Type of values [type]
-+ */
-+#define KHASH_MAP_INIT_INT64(name, khval_t) \
-+ KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal)
-+
-+typedef const char *kh_cstr_t;
-+/*! @function
-+ @abstract Instantiate a hash map containing const char* keys
-+ @param name Name of the hash table [symbol]
-+ */
-+#define KHASH_SET_INIT_STR(name) \
-+ KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal)
-+
-+/*! @function
-+ @abstract Instantiate a hash map containing const char* keys
-+ @param name Name of the hash table [symbol]
-+ @param khval_t Type of values [type]
-+ */
-+#define KHASH_MAP_INIT_STR(name, khval_t) \
-+ KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal)
-+
-+#endif /* __AC_KHASH_H */
-diff --git a/kvec.h b/kvec.h
-new file mode 100644
-index 0000000..10f1c5b
---- /dev/null
-+++ b/kvec.h
-@@ -0,0 +1,90 @@
-+/* The MIT License
-+
-+ Copyright (c) 2008, by Attractive Chaos <attractor@live.co.uk>
-+
-+ Permission is hereby granted, free of charge, to any person obtaining
-+ a copy of this software and associated documentation files (the
-+ "Software"), to deal in the Software without restriction, including
-+ without limitation the rights to use, copy, modify, merge, publish,
-+ distribute, sublicense, and/or sell copies of the Software, and to
-+ permit persons to whom the Software is furnished to do so, subject to
-+ the following conditions:
-+
-+ The above copyright notice and this permission notice shall be
-+ included in all copies or substantial portions of the Software.
-+
-+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-+ SOFTWARE.
-+*/
-+
-+/*
-+ An example:
-+
-+#include "kvec.h"
-+int main() {
-+ kvec_t(int) array;
-+ kv_init(array);
-+ kv_push(int, array, 10); // append
-+ kv_a(int, array, 20) = 5; // dynamic
-+ kv_A(array, 20) = 4; // static
-+ kv_destroy(array);
-+ return 0;
-+}
-+*/
-+
-+/*
-+ 2008-09-22 (0.1.0):
-+
-+ * The initial version.
-+
-+*/
-+
-+#ifndef AC_KVEC_H
-+#define AC_KVEC_H
-+
-+#include <stdlib.h>
-+
-+#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
-+
-+#define kvec_t(type) struct { size_t n, m; type *a; }
-+#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0)
-+#define kv_destroy(v) free((v).a)
-+#define kv_A(v, i) ((v).a[(i)])
-+#define kv_pop(v) ((v).a[--(v).n])
-+#define kv_size(v) ((v).n)
-+#define kv_max(v) ((v).m)
-+
-+#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m))
-+
-+#define kv_copy(type, v1, v0) do { \
-+ if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \
-+ (v1).n = (v0).n; \
-+ memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \
-+ } while (0) \
-+
-+#define kv_push(type, v, x) do { \
-+ if ((v).n == (v).m) { \
-+ (v).m = (v).m? (v).m<<1 : 2; \
-+ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \
-+ } \
-+ (v).a[(v).n++] = (x); \
-+ } while (0)
-+
-+#define kv_pushp(type, v) ((((v).n == (v).m)? \
-+ ((v).m = ((v).m? (v).m<<1 : 2), \
-+ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \
-+ : 0), ((v).a + ((v).n++)))
-+
-+#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \
-+ ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \
-+ (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \
-+ : (v).n <= (size_t)(i)? (v).n = (i) + 1 \
-+ : 0), (v).a[(i)])
-+
-+#endif
-diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c
-new file mode 100644
-index 0000000..829c0fc
---- /dev/null
-+++ b/rowcolumn_diacritics_helpers.c
-@@ -0,0 +1,391 @@
-+#include <stdint.h>
-+
-+uint16_t diacritic_to_num(uint32_t code)
-+{
-+ switch (code) {
-+ case 0x305:
-+ return code - 0x305 + 1;
-+ case 0x30d:
-+ case 0x30e:
-+ return code - 0x30d + 2;
-+ case 0x310:
-+ return code - 0x310 + 4;
-+ case 0x312:
-+ return code - 0x312 + 5;
-+ case 0x33d:
-+ case 0x33e:
-+ case 0x33f:
-+ return code - 0x33d + 6;
-+ case 0x346:
-+ return code - 0x346 + 9;
-+ case 0x34a:
-+ case 0x34b:
-+ case 0x34c:
-+ return code - 0x34a + 10;
-+ case 0x350:
-+ case 0x351:
-+ case 0x352:
-+ return code - 0x350 + 13;
-+ case 0x357:
-+ return code - 0x357 + 16;
-+ case 0x35b:
-+ return code - 0x35b + 17;
-+ case 0x363:
-+ case 0x364:
-+ case 0x365:
-+ case 0x366:
-+ case 0x367:
-+ case 0x368:
-+ case 0x369:
-+ case 0x36a:
-+ case 0x36b:
-+ case 0x36c:
-+ case 0x36d:
-+ case 0x36e:
-+ case 0x36f:
-+ return code - 0x363 + 18;
-+ case 0x483:
-+ case 0x484:
-+ case 0x485:
-+ case 0x486:
-+ case 0x487:
-+ return code - 0x483 + 31;
-+ case 0x592:
-+ case 0x593:
-+ case 0x594:
-+ case 0x595:
-+ return code - 0x592 + 36;
-+ case 0x597:
-+ case 0x598:
-+ case 0x599:
-+ return code - 0x597 + 40;
-+ case 0x59c:
-+ case 0x59d:
-+ case 0x59e:
-+ case 0x59f:
-+ case 0x5a0:
-+ case 0x5a1:
-+ return code - 0x59c + 43;
-+ case 0x5a8:
-+ case 0x5a9:
-+ return code - 0x5a8 + 49;
-+ case 0x5ab:
-+ case 0x5ac:
-+ return code - 0x5ab + 51;
-+ case 0x5af:
-+ return code - 0x5af + 53;
-+ case 0x5c4:
-+ return code - 0x5c4 + 54;
-+ case 0x610:
-+ case 0x611:
-+ case 0x612:
-+ case 0x613:
-+ case 0x614:
-+ case 0x615:
-+ case 0x616:
-+ case 0x617:
-+ return code - 0x610 + 55;
-+ case 0x657:
-+ case 0x658:
-+ case 0x659:
-+ case 0x65a:
-+ case 0x65b:
-+ return code - 0x657 + 63;
-+ case 0x65d:
-+ case 0x65e:
-+ return code - 0x65d + 68;
-+ case 0x6d6:
-+ case 0x6d7:
-+ case 0x6d8:
-+ case 0x6d9:
-+ case 0x6da:
-+ case 0x6db:
-+ case 0x6dc:
-+ return code - 0x6d6 + 70;
-+ case 0x6df:
-+ case 0x6e0:
-+ case 0x6e1:
-+ case 0x6e2:
-+ return code - 0x6df + 77;
-+ case 0x6e4:
-+ return code - 0x6e4 + 81;
-+ case 0x6e7:
-+ case 0x6e8:
-+ return code - 0x6e7 + 82;
-+ case 0x6eb:
-+ case 0x6ec:
-+ return code - 0x6eb + 84;
-+ case 0x730:
-+ return code - 0x730 + 86;
-+ case 0x732:
-+ case 0x733:
-+ return code - 0x732 + 87;
-+ case 0x735:
-+ case 0x736:
-+ return code - 0x735 + 89;
-+ case 0x73a:
-+ return code - 0x73a + 91;
-+ case 0x73d:
-+ return code - 0x73d + 92;
-+ case 0x73f:
-+ case 0x740:
-+ case 0x741:
-+ return code - 0x73f + 93;
-+ case 0x743:
-+ return code - 0x743 + 96;
-+ case 0x745:
-+ return code - 0x745 + 97;
-+ case 0x747:
-+ return code - 0x747 + 98;
-+ case 0x749:
-+ case 0x74a:
-+ return code - 0x749 + 99;
-+ case 0x7eb:
-+ case 0x7ec:
-+ case 0x7ed:
-+ case 0x7ee:
-+ case 0x7ef:
-+ case 0x7f0:
-+ case 0x7f1:
-+ return code - 0x7eb + 101;
-+ case 0x7f3:
-+ return code - 0x7f3 + 108;
-+ case 0x816:
-+ case 0x817:
-+ case 0x818:
-+ case 0x819:
-+ return code - 0x816 + 109;
-+ case 0x81b:
-+ case 0x81c:
-+ case 0x81d:
-+ case 0x81e:
-+ case 0x81f:
-+ case 0x820:
-+ case 0x821:
-+ case 0x822:
-+ case 0x823:
-+ return code - 0x81b + 113;
-+ case 0x825:
-+ case 0x826:
-+ case 0x827:
-+ return code - 0x825 + 122;
-+ case 0x829:
-+ case 0x82a:
-+ case 0x82b:
-+ case 0x82c:
-+ case 0x82d:
-+ return code - 0x829 + 125;
-+ case 0x951:
-+ return code - 0x951 + 130;
-+ case 0x953:
-+ case 0x954:
-+ return code - 0x953 + 131;
-+ case 0xf82:
-+ case 0xf83:
-+ return code - 0xf82 + 133;
-+ case 0xf86:
-+ case 0xf87:
-+ return code - 0xf86 + 135;
-+ case 0x135d:
-+ case 0x135e:
-+ case 0x135f:
-+ return code - 0x135d + 137;
-+ case 0x17dd:
-+ return code - 0x17dd + 140;
-+ case 0x193a:
-+ return code - 0x193a + 141;
-+ case 0x1a17:
-+ return code - 0x1a17 + 142;
-+ case 0x1a75:
-+ case 0x1a76:
-+ case 0x1a77:
-+ case 0x1a78:
-+ case 0x1a79:
-+ case 0x1a7a:
-+ case 0x1a7b:
-+ case 0x1a7c:
-+ return code - 0x1a75 + 143;
-+ case 0x1b6b:
-+ return code - 0x1b6b + 151;
-+ case 0x1b6d:
-+ case 0x1b6e:
-+ case 0x1b6f:
-+ case 0x1b70:
-+ case 0x1b71:
-+ case 0x1b72:
-+ case 0x1b73:
-+ return code - 0x1b6d + 152;
-+ case 0x1cd0:
-+ case 0x1cd1:
-+ case 0x1cd2:
-+ return code - 0x1cd0 + 159;
-+ case 0x1cda:
-+ case 0x1cdb:
-+ return code - 0x1cda + 162;
-+ case 0x1ce0:
-+ return code - 0x1ce0 + 164;
-+ case 0x1dc0:
-+ case 0x1dc1:
-+ return code - 0x1dc0 + 165;
-+ case 0x1dc3:
-+ case 0x1dc4:
-+ case 0x1dc5:
-+ case 0x1dc6:
-+ case 0x1dc7:
-+ case 0x1dc8:
-+ case 0x1dc9:
-+ return code - 0x1dc3 + 167;
-+ case 0x1dcb:
-+ case 0x1dcc:
-+ return code - 0x1dcb + 174;
-+ case 0x1dd1:
-+ case 0x1dd2:
-+ case 0x1dd3:
-+ case 0x1dd4:
-+ case 0x1dd5:
-+ case 0x1dd6:
-+ case 0x1dd7:
-+ case 0x1dd8:
-+ case 0x1dd9:
-+ case 0x1dda:
-+ case 0x1ddb:
-+ case 0x1ddc:
-+ case 0x1ddd:
-+ case 0x1dde:
-+ case 0x1ddf:
-+ case 0x1de0:
-+ case 0x1de1:
-+ case 0x1de2:
-+ case 0x1de3:
-+ case 0x1de4:
-+ case 0x1de5:
-+ case 0x1de6:
-+ return code - 0x1dd1 + 176;
-+ case 0x1dfe:
-+ return code - 0x1dfe + 198;
-+ case 0x20d0:
-+ case 0x20d1:
-+ return code - 0x20d0 + 199;
-+ case 0x20d4:
-+ case 0x20d5:
-+ case 0x20d6:
-+ case 0x20d7:
-+ return code - 0x20d4 + 201;
-+ case 0x20db:
-+ case 0x20dc:
-+ return code - 0x20db + 205;
-+ case 0x20e1:
-+ return code - 0x20e1 + 207;
-+ case 0x20e7:
-+ return code - 0x20e7 + 208;
-+ case 0x20e9:
-+ return code - 0x20e9 + 209;
-+ case 0x20f0:
-+ return code - 0x20f0 + 210;
-+ case 0x2cef:
-+ case 0x2cf0:
-+ case 0x2cf1:
-+ return code - 0x2cef + 211;
-+ case 0x2de0:
-+ case 0x2de1:
-+ case 0x2de2:
-+ case 0x2de3:
-+ case 0x2de4:
-+ case 0x2de5:
-+ case 0x2de6:
-+ case 0x2de7:
-+ case 0x2de8:
-+ case 0x2de9:
-+ case 0x2dea:
-+ case 0x2deb:
-+ case 0x2dec:
-+ case 0x2ded:
-+ case 0x2dee:
-+ case 0x2def:
-+ case 0x2df0:
-+ case 0x2df1:
-+ case 0x2df2:
-+ case 0x2df3:
-+ case 0x2df4:
-+ case 0x2df5:
-+ case 0x2df6:
-+ case 0x2df7:
-+ case 0x2df8:
-+ case 0x2df9:
-+ case 0x2dfa:
-+ case 0x2dfb:
-+ case 0x2dfc:
-+ case 0x2dfd:
-+ case 0x2dfe:
-+ case 0x2dff:
-+ return code - 0x2de0 + 214;
-+ case 0xa66f:
-+ return code - 0xa66f + 246;
-+ case 0xa67c:
-+ case 0xa67d:
-+ return code - 0xa67c + 247;
-+ case 0xa6f0:
-+ case 0xa6f1:
-+ return code - 0xa6f0 + 249;
-+ case 0xa8e0:
-+ case 0xa8e1:
-+ case 0xa8e2:
-+ case 0xa8e3:
-+ case 0xa8e4:
-+ case 0xa8e5:
-+ case 0xa8e6:
-+ case 0xa8e7:
-+ case 0xa8e8:
-+ case 0xa8e9:
-+ case 0xa8ea:
-+ case 0xa8eb:
-+ case 0xa8ec:
-+ case 0xa8ed:
-+ case 0xa8ee:
-+ case 0xa8ef:
-+ case 0xa8f0:
-+ case 0xa8f1:
-+ return code - 0xa8e0 + 251;
-+ case 0xaab0:
-+ return code - 0xaab0 + 269;
-+ case 0xaab2:
-+ case 0xaab3:
-+ return code - 0xaab2 + 270;
-+ case 0xaab7:
-+ case 0xaab8:
-+ return code - 0xaab7 + 272;
-+ case 0xaabe:
-+ case 0xaabf:
-+ return code - 0xaabe + 274;
-+ case 0xaac1:
-+ return code - 0xaac1 + 276;
-+ case 0xfe20:
-+ case 0xfe21:
-+ case 0xfe22:
-+ case 0xfe23:
-+ case 0xfe24:
-+ case 0xfe25:
-+ case 0xfe26:
-+ return code - 0xfe20 + 277;
-+ case 0x10a0f:
-+ return code - 0x10a0f + 284;
-+ case 0x10a38:
-+ return code - 0x10a38 + 285;
-+ case 0x1d185:
-+ case 0x1d186:
-+ case 0x1d187:
-+ case 0x1d188:
-+ case 0x1d189:
-+ return code - 0x1d185 + 286;
-+ case 0x1d1aa:
-+ case 0x1d1ab:
-+ case 0x1d1ac:
-+ case 0x1d1ad:
-+ return code - 0x1d1aa + 291;
-+ case 0x1d242:
-+ case 0x1d243:
-+ case 0x1d244:
-+ return code - 0x1d242 + 295;
-+ }
-+ return 0;
-+}
-diff --git a/st.c b/st.c
-index 57c6e96..f1c5299 100644
---- a/st.c
-+++ b/st.c
-@@ -19,6 +19,7 @@
-
- #include "st.h"
- #include "win.h"
-+#include "graphics.h"
-
- #if defined(__linux)
- #include <pty.h>
-@@ -36,6 +37,10 @@
- #define STR_BUF_SIZ ESC_BUF_SIZ
- #define STR_ARG_SIZ ESC_ARG_SIZ
-
-+/* PUA character used as an image placeholder */
-+#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE
-+#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE
-+
- /* macros */
- #define IS_SET(flag) ((term.mode & (flag)) != 0)
- #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f)
-@@ -113,6 +118,8 @@ typedef struct {
- typedef struct {
- int row; /* nb row */
- int col; /* nb col */
-+ int pixw; /* width of the text area in pixels */
-+ int pixh; /* height of the text area in pixels */
- Line *line; /* screen */
- Line *alt; /* alternate screen */
- int *dirty; /* dirtyness of lines */
-@@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *);
- static char utf8encodebyte(Rune, size_t);
- static size_t utf8validate(Rune *, size_t);
-
--static char *base64dec(const char *);
- static char base64dec_getc(const char **);
-
- static ssize_t xwrite(int, const char *, size_t);
-@@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
- static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
- static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
-
-+/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0
-+ * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */
-+uint16_t diacritic_to_num(uint32_t code);
-+
- ssize_t
- xwrite(int fd, const char *s, size_t len)
- {
-@@ -616,6 +626,12 @@ getsel(void)
- if (gp->mode & ATTR_WDUMMY)
- continue;
-
-+ if (gp->mode & ATTR_IMAGE) {
-+ // TODO: Copy diacritics as well
-+ ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr);
-+ continue;
-+ }
-+
- ptr += utf8encode(gp->u, ptr);
- }
-
-@@ -819,7 +835,11 @@ ttyread(void)
- {
- static char buf[BUFSIZ];
- static int buflen = 0;
-- int ret, written;
-+ static int already_processing = 0;
-+ int ret, written = 0;
-+
-+ if (buflen >= LEN(buf))
-+ return 0;
-
- /* append read bytes to unprocessed bytes */
- ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
-@@ -831,7 +851,24 @@ ttyread(void)
- die("couldn't read from shell: %s\n", strerror(errno));
- default:
- buflen += ret;
-- written = twrite(buf, buflen, 0);
-+ if (already_processing) {
-+ /* Avoid recursive call to twrite() */
-+ return ret;
-+ }
-+ already_processing = 1;
-+ while (1) {
-+ int buflen_before_processing = buflen;
-+ written += twrite(buf + written, buflen - written, 0);
-+ // If buflen changed during the call to twrite, there is
-+ // new data, and we need to keep processing, otherwise
-+ // we can exit. This will not loop forever because the
-+ // buffer is limited, and we don't clean it in this
-+ // loop, so at some point ttywrite will have to drop
-+ // some data.
-+ if (buflen_before_processing == buflen)
-+ break;
-+ }
-+ already_processing = 0;
- buflen -= written;
- /* keep any incomplete UTF-8 byte sequence for the next call */
- if (buflen > 0)
-@@ -874,6 +911,7 @@ ttywriteraw(const char *s, size_t n)
- fd_set wfd, rfd;
- ssize_t r;
- size_t lim = 256;
-+ int retries_left = 100;
-
- /*
- * Remember that we are using a pty, which might be a modem line.
-@@ -882,6 +920,9 @@ ttywriteraw(const char *s, size_t n)
- * FIXME: Migrate the world to Plan 9.
- */
- while (n > 0) {
-+ if (retries_left-- <= 0)
-+ goto too_many_retries;
-+
- FD_ZERO(&wfd);
- FD_ZERO(&rfd);
- FD_SET(cmdfd, &wfd);
-@@ -923,11 +964,16 @@ ttywriteraw(const char *s, size_t n)
-
- write_error:
- die("write error on tty: %s\n", strerror(errno));
-+too_many_retries:
-+ fprintf(stderr, "Could not write %zu bytes to tty\n", n);
- }
-
- void
- ttyresize(int tw, int th)
- {
-+ term.pixw = tw;
-+ term.pixh = th;
-+
- struct winsize w;
-
- w.ws_row = term.row;
-@@ -1015,7 +1061,8 @@ treset(void)
- term.c = (TCursor){{
- .mode = ATTR_NULL,
- .fg = defaultfg,
-- .bg = defaultbg
-+ .bg = defaultbg,
-+ .decor = DECOR_DEFAULT_COLOR
- }, .x = 0, .y = 0, .state = CURSOR_DEFAULT};
-
- memset(term.tabs, 0, term.col * sizeof(*term.tabs));
-@@ -1038,7 +1085,9 @@ treset(void)
- void
- tnew(int col, int row)
- {
-- term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
-+ term = (Term){.c = {.attr = {.fg = defaultfg,
-+ .bg = defaultbg,
-+ .decor = DECOR_DEFAULT_COLOR}}};
- tresize(col, row);
- treset();
- }
-@@ -1215,9 +1264,24 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
- term.line[y][x-1].mode &= ~ATTR_WIDE;
- }
-
-+ if (u == ' ' && term.line[y][x].mode & ATTR_IMAGE &&
-+ tgetisclassicplaceholder(&term.line[y][x])) {
-+ // This is a workaround: don't overwrite classic placement
-+ // placeholders with space symbols (unlike Unicode placeholders
-+ // which must be overwritten by anything).
-+ term.line[y][x].bg = attr->bg;
-+ term.dirty[y] = 1;
-+ return;
-+ }
-+
- term.dirty[y] = 1;
- term.line[y][x] = *attr;
- term.line[y][x].u = u;
-+
-+ if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) {
-+ term.line[y][x].u = 0;
-+ term.line[y][x].mode |= ATTR_IMAGE;
-+ }
- }
-
- void
-@@ -1244,12 +1308,104 @@ tclearregion(int x1, int y1, int x2, int y2)
- selclear();
- gp->fg = term.c.attr.fg;
- gp->bg = term.c.attr.bg;
-+ gp->decor = term.c.attr.decor;
- gp->mode = 0;
- gp->u = ' ';
- }
- }
- }
-
-+/// Fills a rectangle area with an image placeholder. The starting point is the
-+/// cursor. Adds empty lines if needed. The placeholder will be marked as
-+/// classic.
-+void
-+tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id,
-+ int cols, int rows, char do_not_move_cursor)
-+{
-+ for (int row = 0; row < rows; ++row) {
-+ int y = term.c.y;
-+ term.dirty[y] = 1;
-+ for (int col = 0; col < cols; ++col) {
-+ int x = term.c.x + col;
-+ if (x >= term.col)
-+ break;
-+ Glyph *gp = &term.line[y][x];
-+ if (selected(x, y))
-+ selclear();
-+ gp->mode = ATTR_IMAGE;
-+ gp->u = 0;
-+ tsetimgrow(gp, row + 1);
-+ tsetimgcol(gp, col + 1);
-+ tsetimgid(gp, image_id);
-+ tsetimgplacementid(gp, placement_id);
-+ tsetimgdiacriticcount(gp, 3);
-+ tsetisclassicplaceholder(gp, 1);
-+ }
-+ // If moving the cursor is not allowed and this is the last line
-+ // of the terminal, we are done.
-+ if (do_not_move_cursor && y == term.row - 1)
-+ break;
-+ // Move the cursor down, maybe creating a new line. The x is
-+ // preserved (we never change term.c.x in the loop above).
-+ if (row != rows - 1)
-+ tnewline(/*first_col=*/0);
-+ }
-+ if (do_not_move_cursor) {
-+ // Return the cursor to the original position.
-+ tmoveto(term.c.x, term.c.y - rows + 1);
-+ } else {
-+ // Move the cursor beyond the last column, as required by the
-+ // protocol. If the cursor goes beyond the screen edge, insert a
-+ // newline to match the behavior of kitty.
-+ if (term.c.x + cols >= term.col)
-+ tnewline(/*first_col=*/1);
-+ else
-+ tmoveto(term.c.x + cols, term.c.y);
-+ }
-+}
-+
-+void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id,
-+ uint32_t placement_id, int col,
-+ int row, char is_classic),
-+ void *data) {
-+ for (int row = 0; row < term.row; ++row) {
-+ for (int col = 0; col < term.col; ++col) {
-+ Glyph *gp = &term.line[row][col];
-+ if (gp->mode & ATTR_IMAGE) {
-+ uint32_t image_id = tgetimgid(gp);
-+ uint32_t placement_id = tgetimgplacementid(gp);
-+ int ret =
-+ callback(data, tgetimgid(gp),
-+ tgetimgplacementid(gp),
-+ tgetimgcol(gp), tgetimgrow(gp),
-+ tgetisclassicplaceholder(gp));
-+ if (ret == 1) {
-+ term.dirty[row] = 1;
-+ gp->mode = 0;
-+ gp->u = ' ';
-+ }
-+ }
-+ }
-+ }
-+}
-+
-+void gr_schedule_image_redraw_by_id(uint32_t image_id) {
-+ for (int row = 0; row < term.row; ++row) {
-+ if (term.dirty[row])
-+ continue;
-+ for (int col = 0; col < term.col; ++col) {
-+ Glyph *gp = &term.line[row][col];
-+ if (gp->mode & ATTR_IMAGE) {
-+ uint32_t cell_image_id = tgetimgid(gp);
-+ if (cell_image_id == image_id) {
-+ term.dirty[row] = 1;
-+ break;
-+ }
-+ }
-+ }
-+ }
-+}
-+
- void
- tdeletechar(int n)
- {
-@@ -1368,6 +1524,7 @@ tsetattr(const int *attr, int l)
- ATTR_STRUCK );
- term.c.attr.fg = defaultfg;
- term.c.attr.bg = defaultbg;
-+ term.c.attr.decor = DECOR_DEFAULT_COLOR;
- break;
- case 1:
- term.c.attr.mode |= ATTR_BOLD;
-@@ -1380,6 +1537,20 @@ tsetattr(const int *attr, int l)
- break;
- case 4:
- term.c.attr.mode |= ATTR_UNDERLINE;
-+ if (i + 1 < l) {
-+ idx = attr[++i];
-+ if (BETWEEN(idx, 1, 5)) {
-+ tsetdecorstyle(&term.c.attr, idx);
-+ } else if (idx == 0) {
-+ term.c.attr.mode &= ~ATTR_UNDERLINE;
-+ tsetdecorstyle(&term.c.attr, 0);
-+ } else {
-+ fprintf(stderr,
-+ "erresc: unknown underline "
-+ "style %d\n",
-+ idx);
-+ }
-+ }
- break;
- case 5: /* slow blink */
- /* FALLTHROUGH */
-@@ -1403,6 +1574,7 @@ tsetattr(const int *attr, int l)
- break;
- case 24:
- term.c.attr.mode &= ~ATTR_UNDERLINE;
-+ tsetdecorstyle(&term.c.attr, 0);
- break;
- case 25:
- term.c.attr.mode &= ~ATTR_BLINK;
-@@ -1430,6 +1602,13 @@ tsetattr(const int *attr, int l)
- case 49:
- term.c.attr.bg = defaultbg;
- break;
-+ case 58:
-+ if ((idx = tdefcolor(attr, &i, l)) >= 0)
-+ tsetdecorcolor(&term.c.attr, idx);
-+ break;
-+ case 59:
-+ tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR);
-+ break;
- default:
- if (BETWEEN(attr[i], 30, 37)) {
- term.c.attr.fg = attr[i] - 30;
-@@ -1813,6 +1992,39 @@ csihandle(void)
- goto unknown;
- }
- break;
-+ case '>':
-+ switch (csiescseq.mode[1]) {
-+ case 'q': /* XTVERSION -- Print terminal name and version */
-+ len = snprintf(buf, sizeof(buf),
-+ "\033P>|st-graphics(%s)\033\\", VERSION);
-+ ttywrite(buf, len, 0);
-+ break;
-+ default:
-+ goto unknown;
-+ }
-+ break;
-+ case 't': /* XTWINOPS -- Window manipulation */
-+ switch (csiescseq.arg[0]) {
-+ case 14: /* Report text area size in pixels. */
-+ len = snprintf(buf, sizeof(buf), "\033[4;%i;%it",
-+ term.pixh, term.pixw);
-+ ttywrite(buf, len, 0);
-+ break;
-+ case 16: /* Report character cell size in pixels. */
-+ len = snprintf(buf, sizeof(buf), "\033[6;%i;%it",
-+ term.pixh / term.row,
-+ term.pixw / term.col);
-+ ttywrite(buf, len, 0);
-+ break;
-+ case 18: /* Report the size of the text area in characters. */
-+ len = snprintf(buf, sizeof(buf), "\033[8;%i;%it",
-+ term.row, term.col);
-+ ttywrite(buf, len, 0);
-+ break;
-+ default:
-+ goto unknown;
-+ }
-+ break;
- }
- }
-
-@@ -1962,8 +2174,26 @@ strhandle(void)
- case 'k': /* old title set compatibility */
- xsettitle(strescseq.args[0]);
- return;
-- case 'P': /* DCS -- Device Control String */
- case '_': /* APC -- Application Program Command */
-+ if (gr_parse_command(strescseq.buf, strescseq.len)) {
-+ GraphicsCommandResult *res = &graphics_command_result;
-+ if (res->create_placeholder) {
-+ tcreateimgplaceholder(
-+ res->placeholder.image_id,
-+ res->placeholder.placement_id,
-+ res->placeholder.columns,
-+ res->placeholder.rows,
-+ res->placeholder.do_not_move_cursor);
-+ }
-+ if (res->response[0])
-+ ttywrite(res->response, strlen(res->response),
-+ 0);
-+ if (res->redraw)
-+ tfulldirt();
-+ return;
-+ }
-+ return;
-+ case 'P': /* DCS -- Device Control String */
- case '^': /* PM -- Privacy Message */
- return;
- }
-@@ -2469,6 +2699,33 @@ check_control_code:
- if (selected(term.c.x, term.c.y))
- selclear();
-
-+ if (width == 0) {
-+ // It's probably a combining char. Combining characters are not
-+ // supported, so we just ignore them, unless it denotes the row and
-+ // column of an image character.
-+ if (term.c.y <= 0 && term.c.x <= 0)
-+ return;
-+ else if (term.c.x == 0)
-+ gp = &term.line[term.c.y-1][term.col-1];
-+ else if (term.c.state & CURSOR_WRAPNEXT)
-+ gp = &term.line[term.c.y][term.c.x];
-+ else
-+ gp = &term.line[term.c.y][term.c.x-1];
-+ uint16_t num = diacritic_to_num(u);
-+ if (num && (gp->mode & ATTR_IMAGE)) {
-+ unsigned diaccount = tgetimgdiacriticcount(gp);
-+ if (diaccount == 0)
-+ tsetimgrow(gp, num);
-+ else if (diaccount == 1)
-+ tsetimgcol(gp, num);
-+ else if (diaccount == 2)
-+ tsetimg4thbyteplus1(gp, num);
-+ tsetimgdiacriticcount(gp, diaccount + 1);
-+ }
-+ term.lastc = u;
-+ return;
-+ }
-+
- gp = &term.line[term.c.y][term.c.x];
- if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) {
- gp->mode |= ATTR_WRAP;
-@@ -2635,6 +2892,8 @@ drawregion(int x1, int y1, int x2, int y2)
- {
- int y;
-
-+ xstartimagedraw(term.dirty, term.row);
-+
- for (y = y1; y < y2; y++) {
- if (!term.dirty[y])
- continue;
-@@ -2642,6 +2901,8 @@ drawregion(int x1, int y1, int x2, int y2)
- term.dirty[y] = 0;
- xdrawline(term.line[y], x1, y, x2);
- }
-+
-+ xfinishimagedraw();
- }
-
- void
-@@ -2676,3 +2937,9 @@ redraw(void)
- tfulldirt();
- draw();
- }
-+
-+Glyph
-+getglyphat(int col, int row)
-+{
-+ return term.line[row][col];
-+}
-diff --git a/st.h b/st.h
-index fd3b0d8..c5dd731 100644
---- a/st.h
-+++ b/st.h
-@@ -12,7 +12,7 @@
- #define DEFAULT(a, b) (a) = (a) ? (a) : (b)
- #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
- #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
-- (a).bg != (b).bg)
-+ (a).bg != (b).bg || (a).decor != (b).decor)
- #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
- (t1.tv_nsec-t2.tv_nsec)/1E6)
- #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit)))
-@@ -20,6 +20,10 @@
- #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b))
- #define IS_TRUECOL(x) (1 << 24 & (x))
-
-+// This decor color indicates that the fg color should be used. Note that it's
-+// not a 24-bit color because the 25-th bit is not set.
-+#define DECOR_DEFAULT_COLOR 0x0ffffff
-+
- enum glyph_attribute {
- ATTR_NULL = 0,
- ATTR_BOLD = 1 << 0,
-@@ -34,6 +38,7 @@ enum glyph_attribute {
- ATTR_WIDE = 1 << 9,
- ATTR_WDUMMY = 1 << 10,
- ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
-+ ATTR_IMAGE = 1 << 14,
- };
-
- enum selection_mode {
-@@ -52,6 +57,14 @@ enum selection_snap {
- SNAP_LINE = 2
- };
-
-+enum underline_style {
-+ UNDERLINE_STRAIGHT = 1,
-+ UNDERLINE_DOUBLE = 2,
-+ UNDERLINE_CURLY = 3,
-+ UNDERLINE_DOTTED = 4,
-+ UNDERLINE_DASHED = 5,
-+};
-+
- typedef unsigned char uchar;
- typedef unsigned int uint;
- typedef unsigned long ulong;
-@@ -65,6 +78,7 @@ typedef struct {
- ushort mode; /* attribute flags */
- uint32_t fg; /* foreground */
- uint32_t bg; /* background */
-+ uint32_t decor; /* decoration (like underline) */
- } Glyph;
-
- typedef Glyph *Line;
-@@ -105,6 +119,8 @@ void selextend(int, int, int, int);
- int selected(int, int);
- char *getsel(void);
-
-+Glyph getglyphat(int, int);
-+
- size_t utf8encode(Rune, char *);
-
- void *xmalloc(size_t);
-@@ -124,3 +140,69 @@ extern unsigned int tabspaces;
- extern unsigned int defaultfg;
- extern unsigned int defaultbg;
- extern unsigned int defaultcs;
-+
-+// Accessors to decoration properties stored in `decor`.
-+// The 25-th bit is used to indicate if it's a 24-bit color.
-+static inline uint32_t tgetdecorcolor(Glyph *g) { return g->decor & 0x1ffffff; }
-+static inline uint32_t tgetdecorstyle(Glyph *g) { return (g->decor >> 25) & 0x7; }
-+static inline void tsetdecorcolor(Glyph *g, uint32_t color) {
-+ g->decor = (g->decor & ~0x1ffffff) | (color & 0x1ffffff);
-+}
-+static inline void tsetdecorstyle(Glyph *g, uint32_t style) {
-+ g->decor = (g->decor & ~(0x7 << 25)) | ((style & 0x7) << 25);
-+}
-+
-+
-+// Some accessors to image placeholder properties stored in `u`:
-+// - row (1-base) - 9 bits
-+// - column (1-base) - 9 bits
-+// - most significant byte of the image id plus 1 - 9 bits (0 means unspecified,
-+// don't forget to subtract 1).
-+// - the original number of diacritics (0, 1, 2, or 3) - 2 bits
-+// - whether this is a classic (1) or Unicode (0) placeholder - 1 bit
-+static inline uint32_t tgetimgrow(Glyph *g) { return g->u & 0x1ff; }
-+static inline uint32_t tgetimgcol(Glyph *g) { return (g->u >> 9) & 0x1ff; }
-+static inline uint32_t tgetimgid4thbyteplus1(Glyph *g) { return (g->u >> 18) & 0x1ff; }
-+static inline uint32_t tgetimgdiacriticcount(Glyph *g) { return (g->u >> 27) & 0x3; }
-+static inline uint32_t tgetisclassicplaceholder(Glyph *g) { return (g->u >> 29) & 0x1; }
-+static inline void tsetimgrow(Glyph *g, uint32_t row) {
-+ g->u = (g->u & ~0x1ff) | (row & 0x1ff);
-+}
-+static inline void tsetimgcol(Glyph *g, uint32_t col) {
-+ g->u = (g->u & ~(0x1ff << 9)) | ((col & 0x1ff) << 9);
-+}
-+static inline void tsetimg4thbyteplus1(Glyph *g, uint32_t byteplus1) {
-+ g->u = (g->u & ~(0x1ff << 18)) | ((byteplus1 & 0x1ff) << 18);
-+}
-+static inline void tsetimgdiacriticcount(Glyph *g, uint32_t count) {
-+ g->u = (g->u & ~(0x3 << 27)) | ((count & 0x3) << 27);
-+}
-+static inline void tsetisclassicplaceholder(Glyph *g, uint32_t isclassic) {
-+ g->u = (g->u & ~(0x1 << 29)) | ((isclassic & 0x1) << 29);
-+}
-+
-+/// Returns the full image id. This is a naive implementation, if the most
-+/// significant byte is not specified, it's assumed to be 0 instead of inferring
-+/// it from the cells to the left.
-+static inline uint32_t tgetimgid(Glyph *g) {
-+ uint32_t msb = tgetimgid4thbyteplus1(g);
-+ if (msb != 0)
-+ --msb;
-+ return (msb << 24) | (g->fg & 0xFFFFFF);
-+}
-+
-+/// Sets the full image id.
-+static inline void tsetimgid(Glyph *g, uint32_t id) {
-+ g->fg = (id & 0xFFFFFF) | (1 << 24);
-+ tsetimg4thbyteplus1(g, ((id >> 24) & 0xFF) + 1);
-+}
-+
-+static inline uint32_t tgetimgplacementid(Glyph *g) {
-+ if (tgetdecorcolor(g) == DECOR_DEFAULT_COLOR)
-+ return 0;
-+ return g->decor & 0xFFFFFF;
-+}
-+
-+static inline void tsetimgplacementid(Glyph *g, uint32_t id) {
-+ g->decor = (id & 0xFFFFFF) | (1 << 24);
-+}
-diff --git a/st.info b/st.info
-index efab2cf..ded76c1 100644
---- a/st.info
-+++ b/st.info
-@@ -195,6 +195,7 @@ st-mono| simpleterm monocolor,
- Ms=\E]52;%p1%s;%p2%s\007,
- Se=\E[2 q,
- Ss=\E[%p1%d q,
-+ Smulx=\E[4:%p1%dm,
-
- st| simpleterm,
- use=st-mono,
-@@ -215,6 +216,11 @@ st-256color| simpleterm with 256 colors,
- initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
- setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
- setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
-+# Underline colors
-+ Su,
-+ Setulc=\E[58:2:%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
-+ Setulc1=\E[58:5:%p1%dm,
-+ ol=\E[59m,
-
- st-meta| simpleterm with meta key,
- use=st,
-diff --git a/win.h b/win.h
-index 6de960d..31b3fff 100644
---- a/win.h
-+++ b/win.h
-@@ -39,3 +39,6 @@ void xsetpointermotion(int);
- void xsetsel(char *);
- int xstartdraw(void);
- void xximspot(int, int);
-+
-+void xstartimagedraw(int *dirty, int rows);
-+void xfinishimagedraw();
-diff --git a/x.c b/x.c
-index d73152b..6f1bf8c 100644
---- a/x.c
-+++ b/x.c
-@@ -4,6 +4,8 @@
- #include <limits.h>
- #include <locale.h>
- #include <signal.h>
-+#include <stdio.h>
-+#include <stdlib.h>
- #include <sys/select.h>
- #include <time.h>
- #include <unistd.h>
-@@ -19,6 +21,7 @@ char *argv0;
- #include "arg.h"
- #include "st.h"
- #include "win.h"
-+#include "graphics.h"
-
- /* types used in config.h */
- typedef struct {
-@@ -59,6 +62,12 @@ static void zoom(const Arg *);
- static void zoomabs(const Arg *);
- static void zoomreset(const Arg *);
- static void ttysend(const Arg *);
-+static void previewimage(const Arg *);
-+static void showimageinfo(const Arg *);
-+static void togglegrdebug(const Arg *);
-+static void dumpgrstate(const Arg *);
-+static void unloadimages(const Arg *);
-+static void toggleimages(const Arg *);
-
- /* config.h for applying patches and the configuration. */
- #include "config.h"
-@@ -81,6 +90,7 @@ typedef XftGlyphFontSpec GlyphFontSpec;
- typedef struct {
- int tw, th; /* tty width and height */
- int w, h; /* window width and height */
-+ int hborderpx, vborderpx;
- int ch; /* char height */
- int cw; /* char width */
- int mode; /* window state/mode flags */
-@@ -144,6 +154,8 @@ static inline ushort sixd_to_16bit(int);
- static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
- static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
- static void xdrawglyph(Glyph, int, int);
-+static void xdrawimages(Glyph, Line, int x1, int y1, int x2);
-+static void xdrawoneimagecell(Glyph, int x, int y);
- static void xclear(int, int, int, int);
- static int xgeommasktogravity(int);
- static int ximopen(Display *);
-@@ -220,6 +232,7 @@ static DC dc;
- static XWindow xw;
- static XSelection xsel;
- static TermWindow win;
-+static unsigned int mouse_col = 0, mouse_row = 0;
-
- /* Font Ring Cache */
- enum {
-@@ -328,10 +341,72 @@ ttysend(const Arg *arg)
- ttywrite(arg->s, strlen(arg->s), 1);
- }
-
-+void
-+previewimage(const Arg *arg)
-+{
-+ Glyph g = getglyphat(mouse_col, mouse_row);
-+ if (g.mode & ATTR_IMAGE) {
-+ uint32_t image_id = tgetimgid(&g);
-+ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n",
-+ image_id, tgetimgplacementid(&g), tgetimgcol(&g),
-+ tgetimgrow(&g));
-+ gr_preview_image(image_id, arg->s);
-+ }
-+}
-+
-+void
-+showimageinfo(const Arg *arg)
-+{
-+ Glyph g = getglyphat(mouse_col, mouse_row);
-+ if (g.mode & ATTR_IMAGE) {
-+ uint32_t image_id = tgetimgid(&g);
-+ fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n",
-+ image_id, tgetimgplacementid(&g), tgetimgcol(&g),
-+ tgetimgrow(&g));
-+ char stcommand[256] = {0};
-+ size_t len = snprintf(stcommand, sizeof(stcommand), "%s -e less", argv0);
-+ if (len > sizeof(stcommand) - 1) {
-+ fprintf(stderr, "Executable name too long: %s\n",
-+ argv0);
-+ return;
-+ }
-+ gr_show_image_info(image_id, tgetimgplacementid(&g),
-+ tgetimgcol(&g), tgetimgrow(&g),
-+ tgetisclassicplaceholder(&g),
-+ tgetimgdiacriticcount(&g), argv0);
-+ }
-+}
-+
-+void
-+togglegrdebug(const Arg *arg)
-+{
-+ graphics_debug_mode = (graphics_debug_mode + 1) % 3;
-+ redraw();
-+}
-+
-+void
-+dumpgrstate(const Arg *arg)
-+{
-+ gr_dump_state();
-+}
-+
-+void
-+unloadimages(const Arg *arg)
-+{
-+ gr_unload_images_to_reduce_ram();
-+}
-+
-+void
-+toggleimages(const Arg *arg)
-+{
-+ graphics_display_images = !graphics_display_images;
-+ redraw();
-+}
-+
- int
- evcol(XEvent *e)
- {
-- int x = e->xbutton.x - borderpx;
-+ int x = e->xbutton.x - win.hborderpx;
- LIMIT(x, 0, win.tw - 1);
- return x / win.cw;
- }
-@@ -339,7 +414,7 @@ evcol(XEvent *e)
- int
- evrow(XEvent *e)
- {
-- int y = e->xbutton.y - borderpx;
-+ int y = e->xbutton.y - win.vborderpx;
- LIMIT(y, 0, win.th - 1);
- return y / win.ch;
- }
-@@ -452,6 +527,9 @@ mouseaction(XEvent *e, uint release)
- /* ignore Button<N>mask for Button<N> - it's set on release */
- uint state = e->xbutton.state & ~buttonmask(e->xbutton.button);
-
-+ mouse_col = evcol(e);
-+ mouse_row = evrow(e);
-+
- for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) {
- if (ms->release == release &&
- ms->button == e->xbutton.button &&
-@@ -739,6 +817,9 @@ cresize(int width, int height)
- col = MAX(1, col);
- row = MAX(1, row);
-
-+ win.hborderpx = (win.w - col * win.cw) * anysize_halign / 100;
-+ win.vborderpx = (win.h - row * win.ch) * anysize_valign / 100;
-+
- tresize(col, row);
- xresize(col, row);
- ttyresize(win.tw, win.th);
-@@ -869,8 +950,8 @@ xhints(void)
- sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize;
- sizeh->height = win.h;
- sizeh->width = win.w;
-- sizeh->height_inc = win.ch;
-- sizeh->width_inc = win.cw;
-+ sizeh->height_inc = 1;
-+ sizeh->width_inc = 1;
- sizeh->base_height = 2 * borderpx;
- sizeh->base_width = 2 * borderpx;
- sizeh->min_height = win.ch + 2 * borderpx;
-@@ -1014,7 +1095,8 @@ xloadfonts(const char *fontstr, double fontsize)
- FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12);
- usedfontsize = 12;
- }
-- defaultfontsize = usedfontsize;
-+ if (defaultfontsize <= 0)
-+ defaultfontsize = usedfontsize;
- }
-
- if (xloadfont(&dc.font, pattern))
-@@ -1024,7 +1106,7 @@ xloadfonts(const char *fontstr, double fontsize)
- FcPatternGetDouble(dc.font.match->pattern,
- FC_PIXEL_SIZE, 0, &fontval);
- usedfontsize = fontval;
-- if (fontsize == 0)
-+ if (defaultfontsize <= 0 && fontsize == 0)
- defaultfontsize = fontval;
- }
-
-@@ -1152,8 +1234,8 @@ xinit(int cols, int rows)
- xloadcols();
-
- /* adjust fixed window geometry */
-- win.w = 2 * borderpx + cols * win.cw;
-- win.h = 2 * borderpx + rows * win.ch;
-+ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw;
-+ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch;
- if (xw.gm & XNegative)
- xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2;
- if (xw.gm & YNegative)
-@@ -1240,12 +1322,15 @@ xinit(int cols, int rows)
- xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0);
- if (xsel.xtarget == None)
- xsel.xtarget = XA_STRING;
-+
-+ // Initialize the graphics (image display) module.
-+ gr_init(xw.dpy, xw.vis, xw.cmap);
- }
-
- int
- xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
- {
-- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
-+ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp;
- ushort mode, prevmode = USHRT_MAX;
- Font *font = &dc.font;
- int frcflags = FRC_NORMAL;
-@@ -1267,6 +1352,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
- if (mode == ATTR_WDUMMY)
- continue;
-
-+ /* Draw spaces for image placeholders (images will be drawn
-+ * separately). */
-+ if (mode & ATTR_IMAGE)
-+ rune = ' ';
-+
- /* Determine font for glyph if different from previous glyph. */
- if (prevmode != mode) {
- prevmode = mode;
-@@ -1374,11 +1464,61 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
- return numspecs;
- }
-
-+/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen`
-+ * is the length of the dash plus the length of the gap. `fraction` is the
-+ * fraction of the dash length compared to `wavelen`. */
-+static void
-+xdrawunderdashed(Draw draw, Color *color, int x, int y, int w,
-+ int wavelen, float fraction, int thick)
-+{
-+ int dashw = MAX(1, fraction * wavelen);
-+ for (int i = x - x % wavelen; i < x + w; i += wavelen) {
-+ int startx = MAX(i, x);
-+ int endx = MIN(i + dashw, x + w);
-+ if (startx < endx)
-+ XftDrawRect(xw.draw, color, startx, y, endx - startx,
-+ thick);
-+ }
-+}
-+
-+/* Draws an undercurl. `h` is the total height, including line thickness. */
-+static void
-+xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick)
-+{
-+ XGCValues gcvals = {.foreground = color->pixel,
-+ .line_width = thick,
-+ .line_style = LineSolid,
-+ .cap_style = CapRound};
-+ GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw),
-+ GCForeground | GCLineWidth | GCLineStyle | GCCapStyle,
-+ &gcvals);
-+
-+ XRectangle clip = {.x = x, .y = y, .width = w, .height = h};
-+ XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted);
-+
-+ int yoffset = thick / 2;
-+ int segh = MAX(1, h - thick);
-+ /* Make sure every segment is at a 45 degree angle, otherwise it doesn't
-+ * look good without antialiasing. */
-+ int segw = segh;
-+ int wavelen = MAX(1, segw * 2);
-+
-+ for (int i = x - (x % wavelen); i < x + w; i += wavelen) {
-+ XPoint points[3] = {{.x = i, .y = y + yoffset},
-+ {.x = i + segw, .y = y + yoffset + segh},
-+ {.x = i + wavelen, .y = y + yoffset}};
-+ XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3,
-+ CoordModeOrigin);
-+ }
-+
-+ XFreeGC(xw.dpy, gc);
-+}
-+
- void
- xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y)
- {
- int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
-- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
-+ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch,
- width = charlen * win.cw;
- Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
- XRenderColor colfg, colbg;
-@@ -1468,17 +1608,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
-
- /* Intelligent cleaning up of the borders. */
- if (x == 0) {
-- xclear(0, (y == 0)? 0 : winy, borderpx,
-+ xclear(0, (y == 0)? 0 : winy, win.hborderpx,
- winy + win.ch +
-- ((winy + win.ch >= borderpx + win.th)? win.h : 0));
-+ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0));
- }
-- if (winx + width >= borderpx + win.tw) {
-+ if (winx + width >= win.hborderpx + win.tw) {
- xclear(winx + width, (y == 0)? 0 : winy, win.w,
-- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch)));
-+ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch)));
- }
- if (y == 0)
-- xclear(winx, 0, winx + width, borderpx);
-- if (winy + win.ch >= borderpx + win.th)
-+ xclear(winx, 0, winx + width, win.vborderpx);
-+ if (winy + win.ch >= win.vborderpx + win.th)
- xclear(winx, winy + win.ch, winx + width, win.h);
-
- /* Clean up the region we want to draw to. */
-@@ -1491,18 +1631,68 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
- r.width = width;
- XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1);
-
-- /* Render the glyphs. */
-- XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
--
-- /* Render underline and strikethrough. */
-+ /* Decoration color. */
-+ Color decor;
-+ uint32_t decorcolor = tgetdecorcolor(&base);
-+ if (decorcolor == DECOR_DEFAULT_COLOR) {
-+ decor = *fg;
-+ } else if (IS_TRUECOL(decorcolor)) {
-+ colfg.alpha = 0xffff;
-+ colfg.red = TRUERED(decorcolor);
-+ colfg.green = TRUEGREEN(decorcolor);
-+ colfg.blue = TRUEBLUE(decorcolor);
-+ XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor);
-+ } else {
-+ decor = dc.col[decorcolor];
-+ }
-+ decor.color.alpha = 0xffff;
-+ decor.pixel |= 0xff << 24;
-+
-+ /* Float thickness, used as a base to compute other values. */
-+ float fthick = dc.font.height / 18.0;
-+ /* Integer thickness in pixels. Must not be 0. */
-+ int thick = MAX(1, roundf(fthick));
-+ /* The default gap between the baseline and a single underline. */
-+ int gap = roundf(fthick * 2);
-+ /* The total thickness of a double underline. */
-+ int doubleh = thick * 2 + ceilf(fthick * 0.5);
-+ /* The total thickness of an undercurl. */
-+ int curlh = thick * 2 + roundf(fthick * 0.75);
-+
-+ /* Render the underline before the glyphs. */
- if (base.mode & ATTR_UNDERLINE) {
-- XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1,
-- width, 1);
-+ uint32_t style = tgetdecorstyle(&base);
-+ int liney = winy + dc.font.ascent + gap;
-+ /* Adjust liney to guarantee that a single underline fits. */
-+ liney -= MAX(0, liney + thick - (winy + win.ch));
-+ if (style == UNDERLINE_DOUBLE) {
-+ liney -= MAX(0, liney + doubleh - (winy + win.ch));
-+ XftDrawRect(xw.draw, &decor, winx, liney, width, thick);
-+ XftDrawRect(xw.draw, &decor, winx,
-+ liney + doubleh - thick, width, thick);
-+ } else if (style == UNDERLINE_DOTTED) {
-+ xdrawunderdashed(xw.draw, &decor, winx, liney, width,
-+ thick * 2, 0.5, thick);
-+ } else if (style == UNDERLINE_DASHED) {
-+ int wavelen = MAX(2, win.cw * 0.9);
-+ xdrawunderdashed(xw.draw, &decor, winx, liney, width,
-+ wavelen, 0.65, thick);
-+ } else if (style == UNDERLINE_CURLY) {
-+ liney -= MAX(0, liney + curlh - (winy + win.ch));
-+ xdrawundercurl(xw.draw, &decor, winx, liney, width,
-+ curlh, thick);
-+ } else {
-+ XftDrawRect(xw.draw, &decor, winx, liney, width, thick);
-+ }
- }
-
-+ /* Render the glyphs. */
-+ XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
-+
-+ /* Render strikethrough. Alway use the fg color. */
- if (base.mode & ATTR_STRUCK) {
-- XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3,
-- width, 1);
-+ XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3,
-+ width, thick);
- }
-
- /* Reset clip to none. */
-@@ -1517,6 +1707,11 @@ xdrawglyph(Glyph g, int x, int y)
-
- numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y);
- xdrawglyphfontspecs(&spec, g, numspecs, x, y);
-+ if (g.mode & ATTR_IMAGE) {
-+ gr_start_drawing(xw.buf, win.cw, win.ch);
-+ xdrawoneimagecell(g, x, y);
-+ gr_finish_drawing(xw.buf);
-+ }
- }
-
- void
-@@ -1532,6 +1727,10 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
- if (IS_SET(MODE_HIDE))
- return;
-
-+ // If it's an image, just draw a ballot box for simplicity.
-+ if (g.mode & ATTR_IMAGE)
-+ g.u = 0x2610;
-+
- /*
- * Select the right color for the right mode.
- */
-@@ -1572,39 +1771,167 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
- case 3: /* Blinking Underline */
- case 4: /* Steady Underline */
- XftDrawRect(xw.draw, &drawcol,
-- borderpx + cx * win.cw,
-- borderpx + (cy + 1) * win.ch - \
-+ win.hborderpx + cx * win.cw,
-+ win.vborderpx + (cy + 1) * win.ch - \
- cursorthickness,
- win.cw, cursorthickness);
- break;
- case 5: /* Blinking bar */
- case 6: /* Steady bar */
- XftDrawRect(xw.draw, &drawcol,
-- borderpx + cx * win.cw,
-- borderpx + cy * win.ch,
-+ win.hborderpx + cx * win.cw,
-+ win.vborderpx + cy * win.ch,
- cursorthickness, win.ch);
- break;
- }
- } else {
- XftDrawRect(xw.draw, &drawcol,
-- borderpx + cx * win.cw,
-- borderpx + cy * win.ch,
-+ win.hborderpx + cx * win.cw,
-+ win.vborderpx + cy * win.ch,
- win.cw - 1, 1);
- XftDrawRect(xw.draw, &drawcol,
-- borderpx + cx * win.cw,
-- borderpx + cy * win.ch,
-+ win.hborderpx + cx * win.cw,
-+ win.vborderpx + cy * win.ch,
- 1, win.ch - 1);
- XftDrawRect(xw.draw, &drawcol,
-- borderpx + (cx + 1) * win.cw - 1,
-- borderpx + cy * win.ch,
-+ win.hborderpx + (cx + 1) * win.cw - 1,
-+ win.vborderpx + cy * win.ch,
- 1, win.ch - 1);
- XftDrawRect(xw.draw, &drawcol,
-- borderpx + cx * win.cw,
-- borderpx + (cy + 1) * win.ch - 1,
-+ win.hborderpx + cx * win.cw,
-+ win.vborderpx + (cy + 1) * win.ch - 1,
- win.cw, 1);
- }
- }
-
-+/* Draw (or queue for drawing) image cells between columns x1 and x2 assuming
-+ * that they have the same attributes (and thus the same lower 24 bits of the
-+ * image ID and the same placement ID). */
-+void
-+xdrawimages(Glyph base, Line line, int x1, int y1, int x2) {
-+ int y_pix = win.vborderpx + y1 * win.ch;
-+ uint32_t image_id_24bits = base.fg & 0xFFFFFF;
-+ uint32_t placement_id = tgetimgplacementid(&base);
-+ // Columns and rows are 1-based, 0 means unspecified.
-+ int last_col = 0;
-+ int last_row = 0;
-+ int last_start_col = 0;
-+ int last_start_x = x1;
-+ // The most significant byte is also 1-base, subtract 1 before use.
-+ uint32_t last_id_4thbyteplus1 = 0;
-+ // We may need to inherit row/column/4th byte from the previous cell.
-+ Glyph *prev = &line[x1 - 1];
-+ if (x1 > 0 && (prev->mode & ATTR_IMAGE) &&
-+ (prev->fg & 0xFFFFFF) == image_id_24bits &&
-+ prev->decor == base.decor) {
-+ last_row = tgetimgrow(prev);
-+ last_col = tgetimgcol(prev);
-+ last_id_4thbyteplus1 = tgetimgid4thbyteplus1(prev);
-+ last_start_col = last_col + 1;
-+ }
-+ for (int x = x1; x < x2; ++x) {
-+ Glyph *g = &line[x];
-+ uint32_t cur_row = tgetimgrow(g);
-+ uint32_t cur_col = tgetimgcol(g);
-+ uint32_t cur_id_4thbyteplus1 = tgetimgid4thbyteplus1(g);
-+ uint32_t num_diacritics = tgetimgdiacriticcount(g);
-+ // If the row is not specified, assume it's the same as the row
-+ // of the previous cell. Note that `cur_row` may contain a
-+ // value imputed earlier, which will be preserved if `last_row`
-+ // is zero (i.e. we don't know the row of the previous cell).
-+ if (last_row && (num_diacritics == 0 || !cur_row))
-+ cur_row = last_row;
-+ // If the column is not specified and the row is the same as the
-+ // row of the previous cell, then assume that the column is the
-+ // next one.
-+ if (last_col && (num_diacritics <= 1 || !cur_col) &&
-+ cur_row == last_row)
-+ cur_col = last_col + 1;
-+ // If the additional id byte is not specified and the
-+ // coordinates are consecutive, assume the byte is also the
-+ // same.
-+ if (last_id_4thbyteplus1 &&
-+ (num_diacritics <= 2 || !cur_id_4thbyteplus1) &&
-+ cur_row == last_row && cur_col == last_col + 1)
-+ cur_id_4thbyteplus1 = last_id_4thbyteplus1;
-+ // If we couldn't infer row and column, start from the top left
-+ // corner.
-+ if (cur_row == 0)
-+ cur_row = 1;
-+ if (cur_col == 0)
-+ cur_col = 1;
-+ // If this cell breaks a contiguous stripe of image cells, draw
-+ // that line and start a new one.
-+ if (cur_col != last_col + 1 || cur_row != last_row ||
-+ cur_id_4thbyteplus1 != last_id_4thbyteplus1) {
-+ uint32_t image_id = image_id_24bits;
-+ if (last_id_4thbyteplus1)
-+ image_id |= (last_id_4thbyteplus1 - 1) << 24;
-+ if (last_row != 0) {
-+ int x_pix =
-+ win.hborderpx + last_start_x * win.cw;
-+ gr_append_imagerect(
-+ xw.buf, image_id, placement_id,
-+ last_start_col - 1, last_col,
-+ last_row - 1, last_row, last_start_x,
-+ y1, x_pix, y_pix, win.cw, win.ch,
-+ base.mode & ATTR_REVERSE);
-+ }
-+ last_start_col = cur_col;
-+ last_start_x = x;
-+ }
-+ last_row = cur_row;
-+ last_col = cur_col;
-+ last_id_4thbyteplus1 = cur_id_4thbyteplus1;
-+ // Populate the missing glyph data to enable inheritance between
-+ // runs and support the naive implementation of tgetimgid.
-+ if (!tgetimgrow(g))
-+ tsetimgrow(g, cur_row);
-+ // We cannot save this information if there are > 511 cols.
-+ if (!tgetimgcol(g) && (cur_col & ~0x1ff) == 0)
-+ tsetimgcol(g, cur_col);
-+ if (!tgetimgid4thbyteplus1(g))
-+ tsetimg4thbyteplus1(g, cur_id_4thbyteplus1);
-+ }
-+ uint32_t image_id = image_id_24bits;
-+ if (last_id_4thbyteplus1)
-+ image_id |= (last_id_4thbyteplus1 - 1) << 24;
-+ // Draw the last contiguous stripe.
-+ if (last_row != 0) {
-+ int x_pix = win.hborderpx + last_start_x * win.cw;
-+ gr_append_imagerect(xw.buf, image_id, placement_id,
-+ last_start_col - 1, last_col, last_row - 1,
-+ last_row, last_start_x, y1, x_pix, y_pix,
-+ win.cw, win.ch, base.mode & ATTR_REVERSE);
-+ }
-+}
-+
-+/* Draw just one image cell without inheriting attributes from the left. */
-+void xdrawoneimagecell(Glyph g, int x, int y) {
-+ if (!(g.mode & ATTR_IMAGE))
-+ return;
-+ int x_pix = win.hborderpx + x * win.cw;
-+ int y_pix = win.vborderpx + y * win.ch;
-+ uint32_t row = tgetimgrow(&g) - 1;
-+ uint32_t col = tgetimgcol(&g) - 1;
-+ uint32_t placement_id = tgetimgplacementid(&g);
-+ uint32_t image_id = tgetimgid(&g);
-+ gr_append_imagerect(xw.buf, image_id, placement_id, col, col + 1, row,
-+ row + 1, x, y, x_pix, y_pix, win.cw, win.ch,
-+ g.mode & ATTR_REVERSE);
-+}
-+
-+/* Prepare for image drawing. */
-+void xstartimagedraw(int *dirty, int rows) {
-+ gr_start_drawing(xw.buf, win.cw, win.ch);
-+ gr_mark_dirty_animations(dirty, rows);
-+}
-+
-+/* Draw all queued image cells. */
-+void xfinishimagedraw() {
-+ gr_finish_drawing(xw.buf);
-+}
-+
- void
- xsetenv(void)
- {
-@@ -1671,6 +1998,8 @@ xdrawline(Line line, int x1, int y1, int x2)
- new.mode ^= ATTR_REVERSE;
- if (i > 0 && ATTRCMP(base, new)) {
- xdrawglyphfontspecs(specs, base, i, ox, y1);
-+ if (base.mode & ATTR_IMAGE)
-+ xdrawimages(base, line, ox, y1, x);
- specs += i;
- numspecs -= i;
- i = 0;
-@@ -1683,6 +2012,8 @@ xdrawline(Line line, int x1, int y1, int x2)
- }
- if (i > 0)
- xdrawglyphfontspecs(specs, base, i, ox, y1);
-+ if (i > 0 && base.mode & ATTR_IMAGE)
-+ xdrawimages(base, line, ox, y1, x);
- }
-
- void
-@@ -1907,6 +2238,7 @@ cmessage(XEvent *e)
- }
- } else if (e->xclient.data.l[0] == xw.wmdeletewin) {
- ttyhangup();
-+ gr_deinit();
- exit(0);
- }
- }
-@@ -1957,6 +2289,13 @@ run(void)
- if (XPending(xw.dpy))
- timeout = 0; /* existing events might not set xfd */
-
-+ /* Decrease the timeout if there are active animations. */
-+ if (graphics_next_redraw_delay != INT_MAX &&
-+ IS_SET(MODE_VISIBLE))
-+ timeout = timeout < 0 ? graphics_next_redraw_delay
-+ : MIN(timeout,
-+ graphics_next_redraw_delay);
-+
- seltv.tv_sec = timeout / 1E3;
- seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec);
- tv = timeout >= 0 ? &seltv : NULL;
---
-2.43.0
-