st-kitty-graphics-20240922-a0274bc.diff - sites - public wiki contents of suckless.org
(HTM) git clone git://git.suckless.org/sites
(DIR) Log
(DIR) Files
(DIR) Refs
---
st-kitty-graphics-20240922-a0274bc.diff (235999B)
---
1 From 25d9cca81ce48141de7f6a823b006dddaafd9de8 Mon Sep 17 00:00:00 2001
2 From: Sergei Grechanik <sergei.grechanik@gmail.com>
3 Date: Sun, 22 Sep 2024 08:36:05 -0700
4 Subject: [PATCH] Kitty graphics protocol support 7b717e3 2024-09-22
5
6 This patch implements the kitty graphics protocol in st.
7 See https://github.com/sergei-grechanik/st-graphics
8 Created by squashing the graphics branch, the most recent
9 commit is 7b717e38b1739e11356b34df9fdfdfa339960864 (2024-09-22).
10
11 Squashed on top of a0274bc20e11d8672bb2953fdd1d3010c0e708c5
12
13 Note that the following files were excluded from the squash:
14 .clang-format
15 README.md
16 generate-rowcolumn-helpers.py
17 rowcolumn-diacritics.txt
18 rowcolumn_diacritics.sh
19 ---
20 Makefile | 4 +-
21 config.def.h | 46 +-
22 config.mk | 5 +-
23 graphics.c | 3812 ++++++++++++++++++++++++++++++++
24 graphics.h | 107 +
25 icat-mini.sh | 801 +++++++
26 khash.h | 627 ++++++
27 kvec.h | 90 +
28 rowcolumn_diacritics_helpers.c | 391 ++++
29 st.c | 279 ++-
30 st.h | 84 +-
31 st.info | 6 +
32 win.h | 3 +
33 x.c | 411 +++-
34 14 files changed, 6615 insertions(+), 51 deletions(-)
35 create mode 100644 graphics.c
36 create mode 100644 graphics.h
37 create mode 100755 icat-mini.sh
38 create mode 100644 khash.h
39 create mode 100644 kvec.h
40 create mode 100644 rowcolumn_diacritics_helpers.c
41
42 diff --git a/Makefile b/Makefile
43 index 15db421..e79b89e 100644
44 --- a/Makefile
45 +++ b/Makefile
46 @@ -4,7 +4,7 @@
47
48 include config.mk
49
50 -SRC = st.c x.c
51 +SRC = st.c x.c rowcolumn_diacritics_helpers.c graphics.c
52 OBJ = $(SRC:.c=.o)
53
54 all: st
55 @@ -16,7 +16,7 @@ config.h:
56 $(CC) $(STCFLAGS) -c $<
57
58 st.o: config.h st.h win.h
59 -x.o: arg.h config.h st.h win.h
60 +x.o: arg.h config.h st.h win.h graphics.h
61
62 $(OBJ): config.h config.mk
63
64 diff --git a/config.def.h b/config.def.h
65 index 2cd740a..4aadbbc 100644
66 --- a/config.def.h
67 +++ b/config.def.h
68 @@ -8,6 +8,13 @@
69 static char *font = "Liberation Mono:pixelsize=12:antialias=true:autohint=true";
70 static int borderpx = 2;
71
72 +/* How to align the content in the window when the size of the terminal
73 + * doesn't perfectly match the size of the window. The values are percentages.
74 + * 50 means center, 0 means flush left/top, 100 means flush right/bottom.
75 + */
76 +static int anysize_halign = 50;
77 +static int anysize_valign = 50;
78 +
79 /*
80 * What program is execed by st depends of these precedence rules:
81 * 1: program passed with -e
82 @@ -23,7 +30,8 @@ char *scroll = NULL;
83 char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400";
84
85 /* identification sequence returned in DA and DECID */
86 -char *vtiden = "\033[?6c";
87 +/* By default, use the same one as kitty. */
88 +char *vtiden = "\033[?62c";
89
90 /* Kerning / character bounding-box multipliers */
91 static float cwscale = 1.0;
92 @@ -163,6 +171,28 @@ static unsigned int mousebg = 0;
93 */
94 static unsigned int defaultattr = 11;
95
96 +/*
97 + * Graphics configuration
98 + */
99 +
100 +/// The template for the cache directory.
101 +const char graphics_cache_dir_template[] = "/tmp/st-images-XXXXXX";
102 +/// The max size of a single image file, in bytes.
103 +unsigned graphics_max_single_image_file_size = 20 * 1024 * 1024;
104 +/// The max size of the cache, in bytes.
105 +unsigned graphics_total_file_cache_size = 300 * 1024 * 1024;
106 +/// The max ram size of an image or placement, in bytes.
107 +unsigned graphics_max_single_image_ram_size = 100 * 1024 * 1024;
108 +/// The max total size of all images loaded into RAM.
109 +unsigned graphics_max_total_ram_size = 300 * 1024 * 1024;
110 +/// The max total number of image placements and images.
111 +unsigned graphics_max_total_placements = 4096;
112 +/// The ratio by which limits can be exceeded. This is to reduce the frequency
113 +/// of image removal.
114 +double graphics_excess_tolerance_ratio = 0.05;
115 +/// The minimum delay between redraws caused by animations, in milliseconds.
116 +unsigned graphics_animation_min_delay = 20;
117 +
118 /*
119 * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set).
120 * Note that if you want to use ShiftMask with selmasks, set this to an other
121 @@ -170,12 +200,18 @@ static unsigned int defaultattr = 11;
122 */
123 static uint forcemousemod = ShiftMask;
124
125 +/* Internal keyboard shortcuts. */
126 +#define MODKEY Mod1Mask
127 +#define TERMMOD (ControlMask|ShiftMask)
128 +
129 /*
130 * Internal mouse shortcuts.
131 * Beware that overloading Button1 will disable the selection.
132 */
133 static MouseShortcut mshortcuts[] = {
134 /* mask button function argument release */
135 + { TERMMOD, Button3, previewimage, {.s = "feh"} },
136 + { TERMMOD, Button2, showimageinfo, {}, 1 },
137 { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
138 { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
139 { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
140 @@ -183,10 +219,6 @@ static MouseShortcut mshortcuts[] = {
141 { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} },
142 };
143
144 -/* Internal keyboard shortcuts. */
145 -#define MODKEY Mod1Mask
146 -#define TERMMOD (ControlMask|ShiftMask)
147 -
148 static Shortcut shortcuts[] = {
149 /* mask keysym function argument */
150 { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} },
151 @@ -201,6 +233,10 @@ static Shortcut shortcuts[] = {
152 { TERMMOD, XK_Y, selpaste, {.i = 0} },
153 { ShiftMask, XK_Insert, selpaste, {.i = 0} },
154 { TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
155 + { TERMMOD, XK_F1, togglegrdebug, {.i = 0} },
156 + { TERMMOD, XK_F6, dumpgrstate, {.i = 0} },
157 + { TERMMOD, XK_F7, unloadimages, {.i = 0} },
158 + { TERMMOD, XK_F8, toggleimages, {.i = 0} },
159 };
160
161 /*
162 diff --git a/config.mk b/config.mk
163 index fdc29a7..cb2875c 100644
164 --- a/config.mk
165 +++ b/config.mk
166 @@ -14,9 +14,12 @@ PKG_CONFIG = pkg-config
167
168 # includes and libs
169 INCS = -I$(X11INC) \
170 + `$(PKG_CONFIG) --cflags imlib2` \
171 `$(PKG_CONFIG) --cflags fontconfig` \
172 `$(PKG_CONFIG) --cflags freetype2`
173 -LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
174 +LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender \
175 + `$(PKG_CONFIG) --libs imlib2` \
176 + `$(PKG_CONFIG) --libs zlib` \
177 `$(PKG_CONFIG) --libs fontconfig` \
178 `$(PKG_CONFIG) --libs freetype2`
179
180 diff --git a/graphics.c b/graphics.c
181 new file mode 100644
182 index 0000000..64e6fe0
183 --- /dev/null
184 +++ b/graphics.c
185 @@ -0,0 +1,3812 @@
186 +/* The MIT License
187 +
188 + Copyright (c) 2021-2024 Sergei Grechanik <sergei.grechanik@gmail.com>
189 +
190 + Permission is hereby granted, free of charge, to any person obtaining
191 + a copy of this software and associated documentation files (the
192 + "Software"), to deal in the Software without restriction, including
193 + without limitation the rights to use, copy, modify, merge, publish,
194 + distribute, sublicense, and/or sell copies of the Software, and to
195 + permit persons to whom the Software is furnished to do so, subject to
196 + the following conditions:
197 +
198 + The above copyright notice and this permission notice shall be
199 + included in all copies or substantial portions of the Software.
200 +
201 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
202 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
203 + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
204 + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
205 + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
206 + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
207 + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
208 + SOFTWARE.
209 +*/
210 +
211 +////////////////////////////////////////////////////////////////////////////////
212 +//
213 +// This file implements a subset of the kitty graphics protocol.
214 +//
215 +////////////////////////////////////////////////////////////////////////////////
216 +
217 +#define _POSIX_C_SOURCE 200809L
218 +
219 +#include <zlib.h>
220 +#include <Imlib2.h>
221 +#include <X11/Xlib.h>
222 +#include <X11/extensions/Xrender.h>
223 +#include <assert.h>
224 +#include <ctype.h>
225 +#include <spawn.h>
226 +#include <stdarg.h>
227 +#include <stdio.h>
228 +#include <stdlib.h>
229 +#include <string.h>
230 +#include <sys/stat.h>
231 +#include <time.h>
232 +#include <unistd.h>
233 +#include <errno.h>
234 +
235 +#include "graphics.h"
236 +#include "khash.h"
237 +#include "kvec.h"
238 +
239 +extern char **environ;
240 +
241 +#define MAX_FILENAME_SIZE 256
242 +#define MAX_INFO_LEN 256
243 +#define MAX_IMAGE_RECTS 20
244 +
245 +/// The type used in this file to represent time. Used both for time differences
246 +/// and absolute times (as milliseconds since an arbitrary point in time, see
247 +/// `initialization_time`).
248 +typedef int64_t Milliseconds;
249 +
250 +enum ScaleMode {
251 + SCALE_MODE_UNSET = 0,
252 + /// Stretch or shrink the image to fill the box, ignoring aspect ratio.
253 + SCALE_MODE_FILL = 1,
254 + /// Preserve aspect ratio and fit to width or to height so that the
255 + /// whole image is visible.
256 + SCALE_MODE_CONTAIN = 2,
257 + /// Do not scale. The image may be cropped if the box is too small.
258 + SCALE_MODE_NONE = 3,
259 + /// Do not scale, unless the box is too small, in which case the image
260 + /// will be shrunk like with `SCALE_MODE_CONTAIN`.
261 + SCALE_MODE_NONE_OR_CONTAIN = 4,
262 +};
263 +
264 +enum AnimationState {
265 + ANIMATION_STATE_UNSET = 0,
266 + /// The animation is stopped. Display the current frame, but don't
267 + /// advance to the next one.
268 + ANIMATION_STATE_STOPPED = 1,
269 + /// Run the animation to then end, then wait for the next frame.
270 + ANIMATION_STATE_LOADING = 2,
271 + /// Run the animation in a loop.
272 + ANIMATION_STATE_LOOPING = 3,
273 +};
274 +
275 +/// The status of an image. Each image uploaded to the terminal is cached on
276 +/// disk, then it is loaded to ram when needed.
277 +enum ImageStatus {
278 + STATUS_UNINITIALIZED = 0,
279 + STATUS_UPLOADING = 1,
280 + STATUS_UPLOADING_ERROR = 2,
281 + STATUS_UPLOADING_SUCCESS = 3,
282 + STATUS_RAM_LOADING_ERROR = 4,
283 + STATUS_RAM_LOADING_IN_PROGRESS = 5,
284 + STATUS_RAM_LOADING_SUCCESS = 6,
285 +};
286 +
287 +const char *image_status_strings[6] = {
288 + "STATUS_UNINITIALIZED",
289 + "STATUS_UPLOADING",
290 + "STATUS_UPLOADING_ERROR",
291 + "STATUS_UPLOADING_SUCCESS",
292 + "STATUS_RAM_LOADING_ERROR",
293 + "STATUS_RAM_LOADING_SUCCESS",
294 +};
295 +
296 +enum ImageUploadingFailure {
297 + ERROR_OVER_SIZE_LIMIT = 1,
298 + ERROR_CANNOT_OPEN_CACHED_FILE = 2,
299 + ERROR_UNEXPECTED_SIZE = 3,
300 + ERROR_CANNOT_COPY_FILE = 4,
301 +};
302 +
303 +const char *image_uploading_failure_strings[5] = {
304 + "NO_ERROR",
305 + "ERROR_OVER_SIZE_LIMIT",
306 + "ERROR_CANNOT_OPEN_CACHED_FILE",
307 + "ERROR_UNEXPECTED_SIZE",
308 + "ERROR_CANNOT_COPY_FILE",
309 +};
310 +
311 +////////////////////////////////////////////////////////////////////////////////
312 +//
313 +// We use the following structures to represent images and placements:
314 +//
315 +// - Image: this is the main structure representing an image, usually created
316 +// by actions 'a=t', 'a=T`. Each image has an id (image id aka client id,
317 +// specified by 'i='). An image may have multiple frames (ImageFrame) and
318 +// placements (ImagePlacement).
319 +//
320 +// - ImageFrame: represents a single frame of an image, usually created by
321 +// the action 'a=f' (and the first frame is created with the image itself).
322 +// Each frame has an index and also:
323 +// - a file containing the frame data (considered to be "on disk", although
324 +// it's probably in tmpfs),
325 +// - an imlib object containing the fully composed frame (i.e. the frame
326 +// data from the file composed onto the background frame or color). It is
327 +// not ready for display yet, because it needs to be scaled and uploaded
328 +// to the X server.
329 +//
330 +// - ImagePlacement: represents a placement of an image, created by 'a=p' and
331 +// 'a=T'. Each placement has an id (placement id, specified by 'p='). Also
332 +// each placement has an array of pixmaps: one for each frame of the image.
333 +// Each pixmap is a scaled and uploaded image ready to be displayed.
334 +//
335 +// Images are store in the `images` hash table, mapping image ids to Image
336 +// objects (allocated on the heap).
337 +//
338 +// Placements are stored in the `placements` hash table of each Image object,
339 +// mapping placement ids to ImagePlacement objects (also allocated on the heap).
340 +//
341 +// ImageFrames are stored in the `first_frame` field and in the
342 +// `frames_beyond_the_first` array of each Image object. They are stored by
343 +// value, so ImageFrame pointer may be invalidated when frames are
344 +// added/deleted, be careful.
345 +//
346 +////////////////////////////////////////////////////////////////////////////////
347 +
348 +struct Image;
349 +struct ImageFrame;
350 +struct ImagePlacement;
351 +
352 +KHASH_MAP_INIT_INT(id2image, struct Image *)
353 +KHASH_MAP_INIT_INT(id2placement, struct ImagePlacement *)
354 +
355 +typedef struct ImageFrame {
356 + /// The image this frame belongs to.
357 + struct Image *image;
358 + /// The 1-based index of the frame. Zero if the frame isn't initialized.
359 + int index;
360 + /// The last time when the frame was displayed or otherwise touched.
361 + Milliseconds atime;
362 + /// The background color of the frame in the 0xRRGGBBAA format.
363 + uint32_t background_color;
364 + /// The index of the background frame. Zero to use the color instead.
365 + int background_frame_index;
366 + /// The duration of the frame in milliseconds.
367 + int gap;
368 + /// The expected size of the frame image file (specified with 'S='),
369 + /// used to check if uploading succeeded.
370 + unsigned expected_size;
371 + /// Format specification (see the `f=` key).
372 + int format;
373 + /// Pixel width and height of the non-composed (on-disk) frame data. May
374 + /// differ from the image (i.e. first frame) dimensions.
375 + int data_pix_width, data_pix_height;
376 + /// The offset of the frame relative to the first frame.
377 + int x, y;
378 + /// Compression mode (see the `o=` key).
379 + char compression;
380 + /// The status (see `ImageStatus`).
381 + char status;
382 + /// The reason of uploading failure (see `ImageUploadingFailure`).
383 + char uploading_failure;
384 + /// Whether failures and successes should be reported ('q=').
385 + char quiet;
386 + /// Whether to blend the frame with the background or replace it.
387 + char blend;
388 + /// The file corresponding to the on-disk cache, used when uploading.
389 + FILE *open_file;
390 + /// The size of the corresponding file cached on disk.
391 + unsigned disk_size;
392 + /// The imlib object containing the fully composed frame. It's not
393 + /// scaled for screen display yet.
394 + Imlib_Image imlib_object;
395 +} ImageFrame;
396 +
397 +typedef struct Image {
398 + /// The client id (the one specified with 'i='). Must be nonzero.
399 + uint32_t image_id;
400 + /// The client id specified in the query command (`a=q`). This one must
401 + /// be used to create the response if it's non-zero.
402 + uint32_t query_id;
403 + /// The number specified in the transmission command (`I=`). If
404 + /// non-zero, it may be used to identify the image instead of the
405 + /// image_id, and it also should be mentioned in responses.
406 + uint32_t image_number;
407 + /// The last time when the image was displayed or otherwise touched.
408 + Milliseconds atime;
409 + /// The total duration of the animation in milliseconds.
410 + int total_duration;
411 + /// The total size of cached image files for all frames.
412 + int total_disk_size;
413 + /// The global index of the creation command. Used to decide which image
414 + /// is newer if they have the same image number.
415 + uint64_t global_command_index;
416 + /// The 1-based index of the currently displayed frame.
417 + int current_frame;
418 + /// The state of the animation, see `AnimationState`.
419 + char animation_state;
420 + /// The absolute time that is assumed to be the start of the current
421 + /// frame (in ms since initialization).
422 + Milliseconds current_frame_time;
423 + /// The absolute time of the last redraw (in ms since initialization).
424 + /// Used to check whether it's the first time we draw the image in the
425 + /// current redraw cycle.
426 + Milliseconds last_redraw;
427 + /// The absolute time of the next redraw (in ms since initialization).
428 + /// 0 means no redraw is scheduled.
429 + Milliseconds next_redraw;
430 + /// The unscaled pixel width and height of the image. Usually inherited
431 + /// from the first frame.
432 + int pix_width, pix_height;
433 + /// The first frame.
434 + ImageFrame first_frame;
435 + /// The array of frames beyond the first one.
436 + kvec_t(ImageFrame) frames_beyond_the_first;
437 + /// Image placements.
438 + khash_t(id2placement) *placements;
439 + /// The default placement.
440 + uint32_t default_placement;
441 + /// The initial placement id, specified with the transmission command,
442 + /// used to report success or failure.
443 + uint32_t initial_placement_id;
444 +} Image;
445 +
446 +typedef struct ImagePlacement {
447 + /// The image this placement belongs to.
448 + Image *image;
449 + /// The id of the placement. Must be nonzero.
450 + uint32_t placement_id;
451 + /// The last time when the placement was displayed or otherwise touched.
452 + Milliseconds atime;
453 + /// The 1-based index of the protected pixmap. We protect a pixmap in
454 + /// gr_load_pixmap to avoid unloading it right after it was loaded.
455 + int protected_frame;
456 + /// Whether the placement is used only for Unicode placeholders.
457 + char virtual;
458 + /// The scaling mode (see `ScaleMode`).
459 + char scale_mode;
460 + /// Height and width in cells.
461 + uint16_t rows, cols;
462 + /// Top-left corner of the source rectangle ('x=' and 'y=').
463 + int src_pix_x, src_pix_y;
464 + /// Height and width of the source rectangle (zero if full image).
465 + int src_pix_width, src_pix_height;
466 + /// The image appropriately scaled and uploaded to the X server. This
467 + /// pixmap is premultiplied by alpha.
468 + Pixmap first_pixmap;
469 + /// The array of pixmaps beyond the first one.
470 + kvec_t(Pixmap) pixmaps_beyond_the_first;
471 + /// The dimensions of the cell used to scale the image. If cell
472 + /// dimensions are changed (font change), the image will be rescaled.
473 + uint16_t scaled_cw, scaled_ch;
474 + /// If true, do not move the cursor when displaying this placement
475 + /// (non-virtual placements only).
476 + char do_not_move_cursor;
477 +} ImagePlacement;
478 +
479 +/// A rectangular piece of an image to be drawn.
480 +typedef struct {
481 + uint32_t image_id;
482 + uint32_t placement_id;
483 + /// The position of the rectangle in pixels.
484 + int screen_x_pix, screen_y_pix;
485 + /// The starting row on the screen.
486 + int screen_y_row;
487 + /// The part of the whole image to be drawn, in cells. Starts are
488 + /// zero-based, ends are exclusive.
489 + int img_start_col, img_end_col, img_start_row, img_end_row;
490 + /// The current cell width and height in pixels.
491 + int cw, ch;
492 + /// Whether colors should be inverted.
493 + int reverse;
494 +} ImageRect;
495 +
496 +/// Executes `code` for each frame of an image. Example:
497 +///
498 +/// foreach_frame(image, frame, {
499 +/// printf("Frame %d\n", frame->index);
500 +/// });
501 +///
502 +#define foreach_frame(image, framevar, code) { size_t __i; \
503 + for (__i = 0; __i <= kv_size((image).frames_beyond_the_first); ++__i) { \
504 + ImageFrame *framevar = \
505 + __i == 0 ? &(image).first_frame \
506 + : &kv_A((image).frames_beyond_the_first, __i - 1); \
507 + code; \
508 + } }
509 +
510 +/// Executes `code` for each pixmap of a placement. Example:
511 +///
512 +/// foreach_pixmap(placement, pixmap, {
513 +/// ...
514 +/// });
515 +///
516 +#define foreach_pixmap(placement, pixmapvar, code) { size_t __i; \
517 + for (__i = 0; __i <= kv_size((placement).pixmaps_beyond_the_first); ++__i) { \
518 + Pixmap pixmapvar = \
519 + __i == 0 ? (placement).first_pixmap \
520 + : kv_A((placement).pixmaps_beyond_the_first, __i - 1); \
521 + code; \
522 + } }
523 +
524 +
525 +static Image *gr_find_image(uint32_t image_id);
526 +static void gr_get_frame_filename(ImageFrame *frame, char *out, size_t max_len);
527 +static void gr_delete_image(Image *img);
528 +static void gr_check_limits();
529 +static char *gr_base64dec(const char *src, size_t *size);
530 +static void sanitize_str(char *str, size_t max_len);
531 +static const char *sanitized_filename(const char *str);
532 +
533 +/// The array of image rectangles to draw. It is reset each frame.
534 +static ImageRect image_rects[MAX_IMAGE_RECTS] = {{0}};
535 +/// The known images (including the ones being uploaded).
536 +static khash_t(id2image) *images = NULL;
537 +/// The total number of placements in all images.
538 +static unsigned total_placement_count = 0;
539 +/// The total size of all image files stored in the on-disk cache.
540 +static int64_t images_disk_size = 0;
541 +/// The total size of all images and placements loaded into ram.
542 +static int64_t images_ram_size = 0;
543 +/// The id of the last loaded image.
544 +static uint32_t last_image_id = 0;
545 +/// Current cell width and heigh in pixels.
546 +static int current_cw = 0, current_ch = 0;
547 +/// The id of the currently uploaded image (when using direct uploading).
548 +static uint32_t current_upload_image_id = 0;
549 +/// The index of the frame currently being uploaded.
550 +static int current_upload_frame_index = 0;
551 +/// The time when the graphics module was initialized.
552 +static struct timespec initialization_time = {0};
553 +/// The time when the current frame drawing started, used for debugging fps and
554 +/// to calculate the current frame for animations.
555 +static Milliseconds drawing_start_time;
556 +/// The global index of the current command.
557 +static uint64_t global_command_counter = 0;
558 +/// The next redraw times for each row of the terminal. Used for animations.
559 +/// 0 means no redraw is scheduled.
560 +static kvec_t(Milliseconds) next_redraw_times = {0, 0, NULL};
561 +/// The number of files loaded in the current redraw cycle.
562 +static int this_redraw_cycle_loaded_files = 0;
563 +/// The number of pixmaps loaded in the current redraw cycle.
564 +static int this_redraw_cycle_loaded_pixmaps = 0;
565 +
566 +/// The directory where the cache files are stored.
567 +static char cache_dir[MAX_FILENAME_SIZE - 16];
568 +
569 +/// The table used for color inversion.
570 +static unsigned char reverse_table[256];
571 +
572 +// Declared in the header.
573 +GraphicsDebugMode graphics_debug_mode = GRAPHICS_DEBUG_NONE;
574 +char graphics_display_images = 1;
575 +GraphicsCommandResult graphics_command_result = {0};
576 +int graphics_next_redraw_delay = INT_MAX;
577 +
578 +// Defined in config.h
579 +extern const char graphics_cache_dir_template[];
580 +extern unsigned graphics_max_single_image_file_size;
581 +extern unsigned graphics_total_file_cache_size;
582 +extern unsigned graphics_max_single_image_ram_size;
583 +extern unsigned graphics_max_total_ram_size;
584 +extern unsigned graphics_max_total_placements;
585 +extern double graphics_excess_tolerance_ratio;
586 +extern unsigned graphics_animation_min_delay;
587 +
588 +
589 +////////////////////////////////////////////////////////////////////////////////
590 +// Basic helpers.
591 +////////////////////////////////////////////////////////////////////////////////
592 +
593 +#define MIN(a, b) ((a) < (b) ? (a) : (b))
594 +#define MAX(a, b) ((a) < (b) ? (b) : (a))
595 +
596 +/// Returns the difference between `end` and `start` in milliseconds.
597 +static int64_t gr_timediff_ms(const struct timespec *end,
598 + const struct timespec *start) {
599 + return (end->tv_sec - start->tv_sec) * 1000 +
600 + (end->tv_nsec - start->tv_nsec) / 1000000;
601 +}
602 +
603 +/// Returns the current time in milliseconds since the initialization.
604 +static Milliseconds gr_now_ms() {
605 + struct timespec now;
606 + clock_gettime(CLOCK_MONOTONIC, &now);
607 + return gr_timediff_ms(&now, &initialization_time);
608 +}
609 +
610 +////////////////////////////////////////////////////////////////////////////////
611 +// Logging.
612 +////////////////////////////////////////////////////////////////////////////////
613 +
614 +#define GR_LOG(...) \
615 + do { if(graphics_debug_mode) fprintf(stderr, __VA_ARGS__); } while(0)
616 +
617 +////////////////////////////////////////////////////////////////////////////////
618 +// Basic image management functions (create, delete, find, etc).
619 +////////////////////////////////////////////////////////////////////////////////
620 +
621 +/// Returns the 1-based index of the last frame. Note that you may want to use
622 +/// `gr_last_uploaded_frame_index` instead since the last frame may be not
623 +/// fully uploaded yet.
624 +static inline int gr_last_frame_index(Image *img) {
625 + return kv_size(img->frames_beyond_the_first) + 1;
626 +}
627 +
628 +/// Returns the frame with the given index. Returns NULL if the index is out of
629 +/// bounds. The index is 1-based.
630 +static ImageFrame *gr_get_frame(Image *img, int index) {
631 + if (!img)
632 + return NULL;
633 + if (index == 1)
634 + return &img->first_frame;
635 + if (2 <= index && index <= gr_last_frame_index(img))
636 + return &kv_A(img->frames_beyond_the_first, index - 2);
637 + return NULL;
638 +}
639 +
640 +/// Returns the last frame of the image. Returns NULL if `img` is NULL.
641 +static ImageFrame *gr_get_last_frame(Image *img) {
642 + if (!img)
643 + return NULL;
644 + return gr_get_frame(img, gr_last_frame_index(img));
645 +}
646 +
647 +/// Returns the 1-based index of the last frame or the second-to-last frame if
648 +/// the last frame is not fully uploaded yet.
649 +static inline int gr_last_uploaded_frame_index(Image *img) {
650 + int last_index = gr_last_frame_index(img);
651 + if (last_index > 1 &&
652 + gr_get_frame(img, last_index)->status < STATUS_UPLOADING_SUCCESS)
653 + return last_index - 1;
654 + return last_index;
655 +}
656 +
657 +/// Returns the pixmap for the frame with the given index. Returns 0 if the
658 +/// index is out of bounds. The index is 1-based.
659 +static Pixmap gr_get_frame_pixmap(ImagePlacement *placement, int index) {
660 + if (index == 1)
661 + return placement->first_pixmap;
662 + if (2 <= index &&
663 + index <= kv_size(placement->pixmaps_beyond_the_first) + 1)
664 + return kv_A(placement->pixmaps_beyond_the_first, index - 2);
665 + return 0;
666 +}
667 +
668 +/// Sets the pixmap for the frame with the given index. The index is 1-based.
669 +/// The array of pixmaps is resized if needed.
670 +static void gr_set_frame_pixmap(ImagePlacement *placement, int index,
671 + Pixmap pixmap) {
672 + if (index == 1) {
673 + placement->first_pixmap = pixmap;
674 + return;
675 + }
676 + // Resize the array if needed.
677 + size_t old_size = kv_size(placement->pixmaps_beyond_the_first);
678 + if (old_size < index - 1) {
679 + kv_a(Pixmap, placement->pixmaps_beyond_the_first, index - 2);
680 + for (size_t i = old_size; i < index - 1; i++)
681 + kv_A(placement->pixmaps_beyond_the_first, i) = 0;
682 + }
683 + kv_A(placement->pixmaps_beyond_the_first, index - 2) = pixmap;
684 +}
685 +
686 +/// Finds the image corresponding to the client id. Returns NULL if cannot find.
687 +static Image *gr_find_image(uint32_t image_id) {
688 + khiter_t k = kh_get(id2image, images, image_id);
689 + if (k == kh_end(images))
690 + return NULL;
691 + Image *res = kh_value(images, k);
692 + return res;
693 +}
694 +
695 +/// Finds the newest image corresponding to the image number. Returns NULL if
696 +/// cannot find.
697 +static Image *gr_find_image_by_number(uint32_t image_number) {
698 + if (image_number == 0)
699 + return NULL;
700 + Image *newest_img = NULL;
701 + Image *img = NULL;
702 + kh_foreach_value(images, img, {
703 + if (img->image_number == image_number &&
704 + (!newest_img || newest_img->global_command_index <
705 + img->global_command_index))
706 + newest_img = img;
707 + });
708 + if (!newest_img)
709 + GR_LOG("Image number %u not found\n", image_number);
710 + else
711 + GR_LOG("Found image number %u, its id is %u\n", image_number,
712 + img->image_id);
713 + return newest_img;
714 +}
715 +
716 +/// Finds the placement corresponding to the id. If the placement id is 0,
717 +/// returns some default placement.
718 +static ImagePlacement *gr_find_placement(Image *img, uint32_t placement_id) {
719 + if (!img)
720 + return NULL;
721 + if (placement_id == 0) {
722 + // Try to get the default placement.
723 + ImagePlacement *dflt = NULL;
724 + if (img->default_placement != 0)
725 + dflt = gr_find_placement(img, img->default_placement);
726 + if (dflt)
727 + return dflt;
728 + // If there is no default placement, return the first one and
729 + // set it as the default.
730 + kh_foreach_value(img->placements, dflt, {
731 + img->default_placement = dflt->placement_id;
732 + return dflt;
733 + });
734 + // If there are no placements, return NULL.
735 + return NULL;
736 + }
737 + khiter_t k = kh_get(id2placement, img->placements, placement_id);
738 + if (k == kh_end(img->placements))
739 + return NULL;
740 + ImagePlacement *res = kh_value(img->placements, k);
741 + return res;
742 +}
743 +
744 +/// Finds the placement by image id and placement id.
745 +static ImagePlacement *gr_find_image_and_placement(uint32_t image_id,
746 + uint32_t placement_id) {
747 + return gr_find_placement(gr_find_image(image_id), placement_id);
748 +}
749 +
750 +/// Writes the name of the on-disk cache file to `out`. `max_len` should be the
751 +/// size of `out`. The name will be something like
752 +/// "/tmp/st-images-xxx/img-ID-FRAME".
753 +static void gr_get_frame_filename(ImageFrame *frame, char *out,
754 + size_t max_len) {
755 + snprintf(out, max_len, "%s/img-%.3u-%.3u", cache_dir,
756 + frame->image->image_id, frame->index);
757 +}
758 +
759 +/// Returns the (estimation) of the RAM size used by the frame right now.
760 +static unsigned gr_frame_current_ram_size(ImageFrame *frame) {
761 + if (!frame->imlib_object)
762 + return 0;
763 + return (unsigned)frame->image->pix_width * frame->image->pix_height * 4;
764 +}
765 +
766 +/// Returns the (estimation) of the RAM size used by a single frame pixmap.
767 +static unsigned gr_placement_single_frame_ram_size(ImagePlacement *placement) {
768 + return (unsigned)placement->rows * placement->cols *
769 + placement->scaled_ch * placement->scaled_cw * 4;
770 +}
771 +
772 +/// Returns the (estimation) of the RAM size used by the placemenet right now.
773 +static unsigned gr_placement_current_ram_size(ImagePlacement *placement) {
774 + unsigned single_frame_size =
775 + gr_placement_single_frame_ram_size(placement);
776 + unsigned result = 0;
777 + foreach_pixmap(*placement, pixmap, {
778 + if (pixmap)
779 + result += single_frame_size;
780 + });
781 + return result;
782 +}
783 +
784 +/// Unload the frame from RAM (i.e. delete the corresponding imlib object).
785 +/// If the on-disk file of the frame is preserved, it can be reloaded later.
786 +static void gr_unload_frame(ImageFrame *frame) {
787 + if (!frame->imlib_object)
788 + return;
789 +
790 + unsigned frame_ram_size = gr_frame_current_ram_size(frame);
791 + images_ram_size -= frame_ram_size;
792 +
793 + imlib_context_set_image(frame->imlib_object);
794 + imlib_free_image_and_decache();
795 + frame->imlib_object = NULL;
796 +
797 + GR_LOG("After unloading image %u frame %u (atime %ld ms ago) "
798 + "ram: %ld KiB (- %u KiB)\n",
799 + frame->image->image_id, frame->index,
800 + drawing_start_time - frame->atime, images_ram_size / 1024,
801 + frame_ram_size / 1024);
802 +}
803 +
804 +/// Unload all frames of the image.
805 +static void gr_unload_all_frames(Image *img) {
806 + foreach_frame(*img, frame, {
807 + gr_unload_frame(frame);
808 + });
809 +}
810 +
811 +/// Unload the placement from RAM (i.e. free all of the corresponding pixmaps).
812 +/// If the on-disk files or imlib objects of the corresponding image are
813 +/// preserved, the placement can be reloaded later.
814 +static void gr_unload_placement(ImagePlacement *placement) {
815 + unsigned placement_ram_size = gr_placement_current_ram_size(placement);
816 + images_ram_size -= placement_ram_size;
817 +
818 + Display *disp = imlib_context_get_display();
819 + foreach_pixmap(*placement, pixmap, {
820 + if (pixmap)
821 + XFreePixmap(disp, pixmap);
822 + });
823 +
824 + placement->first_pixmap = 0;
825 + placement->pixmaps_beyond_the_first.n = 0;
826 + placement->scaled_ch = placement->scaled_cw = 0;
827 +
828 + GR_LOG("After unloading placement %u/%u (atime %ld ms ago) "
829 + "ram: %ld KiB (- %u KiB)\n",
830 + placement->image->image_id, placement->placement_id,
831 + drawing_start_time - placement->atime, images_ram_size / 1024,
832 + placement_ram_size / 1024);
833 +}
834 +
835 +/// Unload a single pixmap of the placement from RAM.
836 +static void gr_unload_pixmap(ImagePlacement *placement, int frameidx) {
837 + Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
838 + if (!pixmap)
839 + return;
840 +
841 + Display *disp = imlib_context_get_display();
842 + XFreePixmap(disp, pixmap);
843 + gr_set_frame_pixmap(placement, frameidx, 0);
844 + images_ram_size -= gr_placement_single_frame_ram_size(placement);
845 +
846 + GR_LOG("After unloading pixmap %ld of "
847 + "placement %u/%u (atime %ld ms ago) "
848 + "frame %u (atime %ld ms ago) "
849 + "ram: %ld KiB (- %u KiB)\n",
850 + pixmap, placement->image->image_id, placement->placement_id,
851 + drawing_start_time - placement->atime, frameidx,
852 + drawing_start_time -
853 + gr_get_frame(placement->image, frameidx)->atime,
854 + images_ram_size / 1024,
855 + gr_placement_single_frame_ram_size(placement) / 1024);
856 +}
857 +
858 +/// Deletes the on-disk cache file corresponding to the frame. The in-ram image
859 +/// object (if it exists) is not deleted, placements are not unloaded either.
860 +static void gr_delete_imagefile(ImageFrame *frame) {
861 + // It may still be being loaded. Close the file in this case.
862 + if (frame->open_file) {
863 + fclose(frame->open_file);
864 + frame->open_file = NULL;
865 + }
866 +
867 + if (frame->disk_size == 0)
868 + return;
869 +
870 + char filename[MAX_FILENAME_SIZE];
871 + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
872 + remove(filename);
873 +
874 + unsigned disk_size = frame->disk_size;
875 + images_disk_size -= disk_size;
876 + frame->image->total_disk_size -= disk_size;
877 + frame->disk_size = 0;
878 +
879 + GR_LOG("After deleting image file %u frame %u (atime %ld ms ago) "
880 + "disk: %ld KiB (- %u KiB)\n",
881 + frame->image->image_id, frame->index,
882 + drawing_start_time - frame->atime, images_disk_size / 1024,
883 + disk_size / 1024);
884 +}
885 +
886 +/// Deletes all on-disk cache files of the image (for each frame).
887 +static void gr_delete_imagefiles(Image *img) {
888 + foreach_frame(*img, frame, {
889 + gr_delete_imagefile(frame);
890 + });
891 +}
892 +
893 +/// Deletes the given placement: unloads, frees the object, but doesn't change
894 +/// the `placements` hash table.
895 +static void gr_delete_placement_keep_id(ImagePlacement *placement) {
896 + if (!placement)
897 + return;
898 + GR_LOG("Deleting placement %u/%u\n", placement->image->image_id,
899 + placement->placement_id);
900 + gr_unload_placement(placement);
901 + kv_destroy(placement->pixmaps_beyond_the_first);
902 + free(placement);
903 + total_placement_count--;
904 +}
905 +
906 +/// Deletes all placements of `img`.
907 +static void gr_delete_all_placements(Image *img) {
908 + ImagePlacement *placement = NULL;
909 + kh_foreach_value(img->placements, placement, {
910 + gr_delete_placement_keep_id(placement);
911 + });
912 + kh_clear(id2placement, img->placements);
913 +}
914 +
915 +/// Deletes the given image: unloads, deletes the file, frees the Image object,
916 +/// but doesn't change the `images` hash table.
917 +static void gr_delete_image_keep_id(Image *img) {
918 + if (!img)
919 + return;
920 + GR_LOG("Deleting image %u\n", img->image_id);
921 + foreach_frame(*img, frame, {
922 + gr_delete_imagefile(frame);
923 + gr_unload_frame(frame);
924 + });
925 + kv_destroy(img->frames_beyond_the_first);
926 + gr_delete_all_placements(img);
927 + kh_destroy(id2placement, img->placements);
928 + free(img);
929 +}
930 +
931 +/// Deletes the given image: unloads, deletes the file, frees the Image object,
932 +/// and also removes it from `images`.
933 +static void gr_delete_image(Image *img) {
934 + if (!img)
935 + return;
936 + uint32_t id = img->image_id;
937 + gr_delete_image_keep_id(img);
938 + khiter_t k = kh_get(id2image, images, id);
939 + kh_del(id2image, images, k);
940 +}
941 +
942 +/// Deletes the given placement: unloads, frees the object, and also removes it
943 +/// from `placements`.
944 +static void gr_delete_placement(ImagePlacement *placement) {
945 + if (!placement)
946 + return;
947 + uint32_t id = placement->placement_id;
948 + Image *img = placement->image;
949 + gr_delete_placement_keep_id(placement);
950 + khiter_t k = kh_get(id2placement, img->placements, id);
951 + kh_del(id2placement, img->placements, k);
952 +}
953 +
954 +/// Deletes all images and clears `images`.
955 +static void gr_delete_all_images() {
956 + Image *img = NULL;
957 + kh_foreach_value(images, img, {
958 + gr_delete_image_keep_id(img);
959 + });
960 + kh_clear(id2image, images);
961 +}
962 +
963 +/// Update the atime of the image.
964 +static void gr_touch_image(Image *img) {
965 + img->atime = gr_now_ms();
966 +}
967 +
968 +/// Update the atime of the frame.
969 +static void gr_touch_frame(ImageFrame *frame) {
970 + frame->image->atime = frame->atime = gr_now_ms();
971 +}
972 +
973 +/// Update the atime of the placement. Touches the images too.
974 +static void gr_touch_placement(ImagePlacement *placement) {
975 + placement->image->atime = placement->atime = gr_now_ms();
976 +}
977 +
978 +/// Creates a new image with the given id. If an image with that id already
979 +/// exists, it is deleted first. If the provided id is 0, generates a
980 +/// random id.
981 +static Image *gr_new_image(uint32_t id) {
982 + if (id == 0) {
983 + do {
984 + id = rand();
985 + // Avoid IDs that don't need full 32 bits.
986 + } while ((id & 0xFF000000) == 0 || (id & 0x00FFFF00) == 0 ||
987 + gr_find_image(id));
988 + GR_LOG("Generated random image id %u\n", id);
989 + }
990 + Image *img = gr_find_image(id);
991 + gr_delete_image_keep_id(img);
992 + GR_LOG("Creating image %u\n", id);
993 + img = malloc(sizeof(Image));
994 + memset(img, 0, sizeof(Image));
995 + img->placements = kh_init(id2placement);
996 + int ret;
997 + khiter_t k = kh_put(id2image, images, id, &ret);
998 + kh_value(images, k) = img;
999 + img->image_id = id;
1000 + gr_touch_image(img);
1001 + img->global_command_index = global_command_counter;
1002 + return img;
1003 +}
1004 +
1005 +/// Creates a new frame at the end of the frame array. It may be the first frame
1006 +/// if there are no frames yet.
1007 +static ImageFrame *gr_append_new_frame(Image *img) {
1008 + ImageFrame *frame = NULL;
1009 + if (img->first_frame.index == 0 &&
1010 + kv_size(img->frames_beyond_the_first) == 0) {
1011 + frame = &img->first_frame;
1012 + frame->index = 1;
1013 + } else {
1014 + frame = kv_pushp(ImageFrame, img->frames_beyond_the_first);
1015 + memset(frame, 0, sizeof(ImageFrame));
1016 + frame->index = kv_size(img->frames_beyond_the_first) + 1;
1017 + }
1018 + frame->image = img;
1019 + gr_touch_frame(frame);
1020 + GR_LOG("Appending frame %d to image %u\n", frame->index, img->image_id);
1021 + return frame;
1022 +}
1023 +
1024 +/// Creates a new placement with the given id. If a placement with that id
1025 +/// already exists, it is deleted first. If the provided id is 0, generates a
1026 +/// random id.
1027 +static ImagePlacement *gr_new_placement(Image *img, uint32_t id) {
1028 + if (id == 0) {
1029 + do {
1030 + // Currently we support only 24-bit IDs.
1031 + id = rand() & 0xFFFFFF;
1032 + // Avoid IDs that need only one byte.
1033 + } while ((id & 0x00FFFF00) == 0 || gr_find_placement(img, id));
1034 + }
1035 + ImagePlacement *placement = gr_find_placement(img, id);
1036 + gr_delete_placement_keep_id(placement);
1037 + GR_LOG("Creating placement %u/%u\n", img->image_id, id);
1038 + placement = malloc(sizeof(ImagePlacement));
1039 + memset(placement, 0, sizeof(ImagePlacement));
1040 + total_placement_count++;
1041 + int ret;
1042 + khiter_t k = kh_put(id2placement, img->placements, id, &ret);
1043 + kh_value(img->placements, k) = placement;
1044 + placement->image = img;
1045 + placement->placement_id = id;
1046 + gr_touch_placement(placement);
1047 + if (img->default_placement == 0)
1048 + img->default_placement = id;
1049 + return placement;
1050 +}
1051 +
1052 +static int64_t ceil_div(int64_t a, int64_t b) {
1053 + return (a + b - 1) / b;
1054 +}
1055 +
1056 +/// Computes the best number of rows and columns for a placement if it's not
1057 +/// specified, and also adjusts the source rectangle size.
1058 +static void gr_infer_placement_size_maybe(ImagePlacement *placement) {
1059 + // The size of the image.
1060 + int image_pix_width = placement->image->pix_width;
1061 + int image_pix_height = placement->image->pix_height;
1062 + // Negative values are not allowed. Quietly set them to 0.
1063 + if (placement->src_pix_x < 0)
1064 + placement->src_pix_x = 0;
1065 + if (placement->src_pix_y < 0)
1066 + placement->src_pix_y = 0;
1067 + if (placement->src_pix_width < 0)
1068 + placement->src_pix_width = 0;
1069 + if (placement->src_pix_height < 0)
1070 + placement->src_pix_height = 0;
1071 + // If the source rectangle is outside the image, truncate it.
1072 + if (placement->src_pix_x > image_pix_width)
1073 + placement->src_pix_x = image_pix_width;
1074 + if (placement->src_pix_y > image_pix_height)
1075 + placement->src_pix_y = image_pix_height;
1076 + // If the source rectangle is not specified, use the whole image. If
1077 + // it's partially outside the image, truncate it.
1078 + if (placement->src_pix_width == 0 ||
1079 + placement->src_pix_x + placement->src_pix_width > image_pix_width)
1080 + placement->src_pix_width =
1081 + image_pix_width - placement->src_pix_x;
1082 + if (placement->src_pix_height == 0 ||
1083 + placement->src_pix_y + placement->src_pix_height > image_pix_height)
1084 + placement->src_pix_height =
1085 + image_pix_height - placement->src_pix_y;
1086 +
1087 + if (placement->cols != 0 && placement->rows != 0)
1088 + return;
1089 + if (placement->src_pix_width == 0 || placement->src_pix_height == 0)
1090 + return;
1091 + if (current_cw == 0 || current_ch == 0)
1092 + return;
1093 +
1094 + // If no size is specified, use the image size.
1095 + if (placement->cols == 0 && placement->rows == 0) {
1096 + placement->cols =
1097 + ceil_div(placement->src_pix_width, current_cw);
1098 + placement->rows =
1099 + ceil_div(placement->src_pix_height, current_ch);
1100 + return;
1101 + }
1102 +
1103 + // Some applications specify only one of the dimensions.
1104 + if (placement->scale_mode == SCALE_MODE_CONTAIN) {
1105 + // If we preserve aspect ratio and fit to width/height, the most
1106 + // logical thing is to find the minimum size of the
1107 + // non-specified dimension that allows the image to fit the
1108 + // specified dimension.
1109 + if (placement->cols == 0) {
1110 + placement->cols = ceil_div(
1111 + placement->src_pix_width * placement->rows *
1112 + current_ch,
1113 + placement->src_pix_height * current_cw);
1114 + return;
1115 + }
1116 + if (placement->rows == 0) {
1117 + placement->rows =
1118 + ceil_div(placement->src_pix_height *
1119 + placement->cols * current_cw,
1120 + placement->src_pix_width * current_ch);
1121 + return;
1122 + }
1123 + } else {
1124 + // Otherwise we stretch the image or preserve the original size.
1125 + // In both cases we compute the best number of columns from the
1126 + // pixel size and cell size.
1127 + // TODO: In the case of stretching it's not the most logical
1128 + // thing to do, may need to revisit in the future.
1129 + // Currently we switch to SCALE_MODE_CONTAIN when only one
1130 + // of the dimensions is specified, so this case shouldn't
1131 + // happen in practice.
1132 + if (!placement->cols)
1133 + placement->cols =
1134 + ceil_div(placement->src_pix_width, current_cw);
1135 + if (!placement->rows)
1136 + placement->rows =
1137 + ceil_div(placement->src_pix_height, current_ch);
1138 + }
1139 +}
1140 +
1141 +/// Adjusts the current frame index if enough time has passed since the display
1142 +/// of the current frame. Also computes the time of the next redraw of this
1143 +/// image (`img->next_redraw`). The current time is passed as an argument so
1144 +/// that all animations are in sync.
1145 +static void gr_update_frame_index(Image *img, Milliseconds now) {
1146 + if (img->current_frame == 0) {
1147 + img->current_frame_time = now;
1148 + img->current_frame = 1;
1149 + img->next_redraw = now + MAX(1, img->first_frame.gap);
1150 + return;
1151 + }
1152 + // If the animation is stopped, show the current frame.
1153 + if (!img->animation_state ||
1154 + img->animation_state == ANIMATION_STATE_STOPPED ||
1155 + img->animation_state == ANIMATION_STATE_UNSET) {
1156 + // The next redraw is never (unless the state is changed).
1157 + img->next_redraw = 0;
1158 + return;
1159 + }
1160 + int last_uploaded_frame_index = gr_last_uploaded_frame_index(img);
1161 + // If we are loading and we reached the last frame, show the last frame.
1162 + if (img->animation_state == ANIMATION_STATE_LOADING &&
1163 + img->current_frame == last_uploaded_frame_index) {
1164 + // The next redraw is never (unless the state is changed or
1165 + // frames are added).
1166 + img->next_redraw = 0;
1167 + return;
1168 + }
1169 +
1170 + // Check how many milliseconds passed since the current frame was shown.
1171 + int passed_ms = now - img->current_frame_time;
1172 + // If the animation is looping and too much time has passes, we can
1173 + // make a shortcut.
1174 + if (img->animation_state == ANIMATION_STATE_LOOPING &&
1175 + img->total_duration > 0 && passed_ms >= img->total_duration) {
1176 + passed_ms %= img->total_duration;
1177 + img->current_frame_time = now - passed_ms;
1178 + }
1179 + // Find the next frame.
1180 + int original_frame_index = img->current_frame;
1181 + while (1) {
1182 + ImageFrame *frame = gr_get_frame(img, img->current_frame);
1183 + if (!frame) {
1184 + // The frame doesn't exist, go to the first frame.
1185 + img->current_frame = 1;
1186 + img->current_frame_time = now;
1187 + img->next_redraw = now + MAX(1, img->first_frame.gap);
1188 + return;
1189 + }
1190 + if (frame->gap >= 0 && passed_ms < frame->gap) {
1191 + // Not enough time has passed, we are still in the same
1192 + // frame, and it's not a gapless frame.
1193 + img->next_redraw =
1194 + img->current_frame_time + MAX(1, frame->gap);
1195 + return;
1196 + }
1197 + // Otherwise go to the next frame.
1198 + passed_ms -= MAX(0, frame->gap);
1199 + if (img->current_frame >= last_uploaded_frame_index) {
1200 + // It's the last frame, if the animation is loading,
1201 + // remain on it.
1202 + if (img->animation_state == ANIMATION_STATE_LOADING) {
1203 + img->next_redraw = 0;
1204 + return;
1205 + }
1206 + // Otherwise the animation is looping.
1207 + img->current_frame = 1;
1208 + // TODO: Support finite number of loops.
1209 + } else {
1210 + img->current_frame++;
1211 + }
1212 + // Make sure we don't get stuck in an infinite loop.
1213 + if (img->current_frame == original_frame_index) {
1214 + // We looped through all frames, but haven't reached the
1215 + // next frame yet. This may happen if too much time has
1216 + // passed since the last redraw or all the frames are
1217 + // gapless. Just move on to the next frame.
1218 + img->current_frame++;
1219 + if (img->current_frame >
1220 + last_uploaded_frame_index)
1221 + img->current_frame = 1;
1222 + img->current_frame_time = now;
1223 + img->next_redraw = now + MAX(
1224 + 1, gr_get_frame(img, img->current_frame)->gap);
1225 + return;
1226 + }
1227 + // Adjust the start time of the frame. The next redraw time will
1228 + // be set in the next iteration.
1229 + img->current_frame_time += MAX(0, frame->gap);
1230 + }
1231 +}
1232 +
1233 +////////////////////////////////////////////////////////////////////////////////
1234 +// Unloading and deleting images to save resources.
1235 +////////////////////////////////////////////////////////////////////////////////
1236 +
1237 +/// A helper to compare frames by atime for qsort.
1238 +static int gr_cmp_frames_by_atime(const void *a, const void *b) {
1239 + ImageFrame *frame_a = *(ImageFrame *const *)a;
1240 + ImageFrame *frame_b = *(ImageFrame *const *)b;
1241 + if (frame_a->atime == frame_b->atime)
1242 + return frame_a->image->global_command_index -
1243 + frame_b->image->global_command_index;
1244 + return frame_a->atime - frame_b->atime;
1245 +}
1246 +
1247 +/// A helper to compare images by atime for qsort.
1248 +static int gr_cmp_images_by_atime(const void *a, const void *b) {
1249 + Image *img_a = *(Image *const *)a;
1250 + Image *img_b = *(Image *const *)b;
1251 + if (img_a->atime == img_b->atime)
1252 + return img_a->global_command_index -
1253 + img_b->global_command_index;
1254 + return img_a->atime - img_b->atime;
1255 +}
1256 +
1257 +/// A helper to compare placements by atime for qsort.
1258 +static int gr_cmp_placements_by_atime(const void *a, const void *b) {
1259 + ImagePlacement *p_a = *(ImagePlacement **)a;
1260 + ImagePlacement *p_b = *(ImagePlacement **)b;
1261 + if (p_a->atime == p_b->atime)
1262 + return p_a->image->global_command_index -
1263 + p_b->image->global_command_index;
1264 + return p_a->atime - p_b->atime;
1265 +}
1266 +
1267 +typedef kvec_t(Image *) ImageVec;
1268 +typedef kvec_t(ImagePlacement *) ImagePlacementVec;
1269 +typedef kvec_t(ImageFrame *) ImageFrameVec;
1270 +
1271 +/// Returns an array of pointers to all images sorted by atime.
1272 +static ImageVec gr_get_images_sorted_by_atime() {
1273 + ImageVec vec;
1274 + kv_init(vec);
1275 + if (kh_size(images) == 0)
1276 + return vec;
1277 + kv_resize(Image *, vec, kh_size(images));
1278 + Image *img = NULL;
1279 + kh_foreach_value(images, img, { kv_push(Image *, vec, img); });
1280 + qsort(vec.a, kv_size(vec), sizeof(Image *), gr_cmp_images_by_atime);
1281 + return vec;
1282 +}
1283 +
1284 +/// Returns an array of pointers to all placements sorted by atime.
1285 +static ImagePlacementVec gr_get_placements_sorted_by_atime() {
1286 + ImagePlacementVec vec;
1287 + kv_init(vec);
1288 + if (total_placement_count == 0)
1289 + return vec;
1290 + kv_resize(ImagePlacement *, vec, total_placement_count);
1291 + Image *img = NULL;
1292 + ImagePlacement *placement = NULL;
1293 + kh_foreach_value(images, img, {
1294 + kh_foreach_value(img->placements, placement, {
1295 + kv_push(ImagePlacement *, vec, placement);
1296 + });
1297 + });
1298 + qsort(vec.a, kv_size(vec), sizeof(ImagePlacement *),
1299 + gr_cmp_placements_by_atime);
1300 + return vec;
1301 +}
1302 +
1303 +/// Returns an array of pointers to all frames sorted by atime.
1304 +static ImageFrameVec gr_get_frames_sorted_by_atime() {
1305 + ImageFrameVec frames;
1306 + kv_init(frames);
1307 + Image *img = NULL;
1308 + kh_foreach_value(images, img, {
1309 + foreach_frame(*img, frame, {
1310 + kv_push(ImageFrame *, frames, frame);
1311 + });
1312 + });
1313 + qsort(frames.a, kv_size(frames), sizeof(ImageFrame *),
1314 + gr_cmp_frames_by_atime);
1315 + return frames;
1316 +}
1317 +
1318 +/// An object that can be unloaded from RAM.
1319 +typedef struct {
1320 + /// Some score, probably based on access time. The lower the score, the
1321 + /// more likely that the object should be unloaded.
1322 + int64_t score;
1323 + union {
1324 + ImagePlacement *placement;
1325 + ImageFrame *frame;
1326 + };
1327 + /// If zero, the object is the imlib object of `frame`, if non-zero,
1328 + /// the object is a pixmap of `frameidx`-th frame of `placement`.
1329 + int frameidx;
1330 +} UnloadableObject;
1331 +
1332 +typedef kvec_t(UnloadableObject) UnloadableObjectVec;
1333 +
1334 +/// A helper to compare unloadable objects by score for qsort.
1335 +static int gr_cmp_unloadable_objects(const void *a, const void *b) {
1336 + UnloadableObject *obj_a = (UnloadableObject *)a;
1337 + UnloadableObject *obj_b = (UnloadableObject *)b;
1338 + return obj_a->score - obj_b->score;
1339 +}
1340 +
1341 +/// Unloads an unloadable object from RAM.
1342 +static void gr_unload_object(UnloadableObject *obj) {
1343 + if (obj->frameidx) {
1344 + if (obj->placement->protected_frame == obj->frameidx)
1345 + return;
1346 + gr_unload_pixmap(obj->placement, obj->frameidx);
1347 + } else {
1348 + gr_unload_frame(obj->frame);
1349 + }
1350 +}
1351 +
1352 +/// Returns the recency threshold for an image. Frames that were accessed within
1353 +/// this threshold from now are considered recent and may be handled
1354 +/// differently because we may need them again very soon.
1355 +static Milliseconds gr_recency_threshold(Image *img) {
1356 + return img->total_duration * 2 + 1000;
1357 +}
1358 +
1359 +/// Creates an unloadable object for the imlib object of a frame.
1360 +static UnloadableObject gr_unloadable_object_for_frame(Milliseconds now,
1361 + ImageFrame *frame) {
1362 + UnloadableObject obj = {0};
1363 + obj.frameidx = 0;
1364 + obj.frame = frame;
1365 + Milliseconds atime = frame->atime;
1366 + obj.score = atime;
1367 + if (atime >= now - gr_recency_threshold(frame->image)) {
1368 + // This is a recent frame, probably from an active animation.
1369 + // Score it above `now` to prefer unloading non-active frames.
1370 + // Randomize the score because it's not very clear in which
1371 + // order we want to unload them: reloading a frame may require
1372 + // reloading other frames.
1373 + obj.score = now + 1000 + rand() % 1000;
1374 + }
1375 + return obj;
1376 +}
1377 +
1378 +/// Creates an unloadable object for a pixmap.
1379 +static UnloadableObject
1380 +gr_unloadable_object_for_pixmap(Milliseconds now, ImageFrame *frame,
1381 + ImagePlacement *placement) {
1382 + UnloadableObject obj = {0};
1383 + obj.frameidx = frame->index;
1384 + obj.placement = placement;
1385 + obj.score = placement->atime;
1386 + // Since we don't store pixmap atimes, use the
1387 + // oldest atime of the frame and the placement.
1388 + Milliseconds atime = MIN(placement->atime, frame->atime);
1389 + obj.score = atime;
1390 + if (atime >= now - gr_recency_threshold(frame->image)) {
1391 + // This is a recent pixmap, probably from an active animation.
1392 + // Score it above `now` to prefer unloading non-active frames.
1393 + // Also assign higher scores to frames that are closer to the
1394 + // current frame (more likely to be used soon).
1395 + int num_frames = gr_last_frame_index(frame->image);
1396 + int dist = frame->index - frame->image->current_frame;
1397 + if (dist < 0)
1398 + dist += num_frames;
1399 + obj.score =
1400 + now + 1000 + (num_frames - dist) * 1000 / num_frames;
1401 + // If the pixmap is much larger than the imlib image, prefer to
1402 + // unload the pixmap by adding up to -1000 to the score. If the
1403 + // imlib image is larger, add up to +1000.
1404 + float imlib_size = gr_frame_current_ram_size(frame);
1405 + float pixmap_size =
1406 + gr_placement_single_frame_ram_size(placement);
1407 + obj.score +=
1408 + 2000 * (imlib_size / (imlib_size + pixmap_size) - 0.5);
1409 + }
1410 + return obj;
1411 +}
1412 +
1413 +/// Returns an array of unloadable objects sorted by score.
1414 +static UnloadableObjectVec
1415 +gr_get_unloadable_objects_sorted_by_score(Milliseconds now) {
1416 + UnloadableObjectVec objects;
1417 + kv_init(objects);
1418 + Image *img = NULL;
1419 + ImagePlacement *placement = NULL;
1420 + kh_foreach_value(images, img, {
1421 + foreach_frame(*img, frame, {
1422 + if (!frame->imlib_object)
1423 + continue;
1424 + kv_push(UnloadableObject, objects,
1425 + gr_unloadable_object_for_frame(now, frame));
1426 + int frameidx = frame->index;
1427 + kh_foreach_value(img->placements, placement, {
1428 + if (!gr_get_frame_pixmap(placement, frameidx))
1429 + continue;
1430 + kv_push(UnloadableObject, objects,
1431 + gr_unloadable_object_for_pixmap(
1432 + now, frame, placement));
1433 + });
1434 + });
1435 + });
1436 + qsort(objects.a, kv_size(objects), sizeof(UnloadableObject),
1437 + gr_cmp_unloadable_objects);
1438 + return objects;
1439 +}
1440 +
1441 +/// Returns the limit adjusted by the excess tolerance ratio.
1442 +static inline unsigned apply_tolerance(unsigned limit) {
1443 + return limit + (unsigned)(limit * graphics_excess_tolerance_ratio);
1444 +}
1445 +
1446 +/// Checks RAM and disk cache limits and deletes/unloads some images.
1447 +static void gr_check_limits() {
1448 + Milliseconds now = gr_now_ms();
1449 + ImageVec images_sorted = {0};
1450 + ImagePlacementVec placements_sorted = {0};
1451 + ImageFrameVec frames_sorted = {0};
1452 + UnloadableObjectVec objects_sorted = {0};
1453 + int images_begin = 0;
1454 + int placements_begin = 0;
1455 + char changed = 0;
1456 + // First reduce the number of images if there are too many.
1457 + if (kh_size(images) > apply_tolerance(graphics_max_total_placements)) {
1458 + GR_LOG("Too many images: %d\n", kh_size(images));
1459 + changed = 1;
1460 + images_sorted = gr_get_images_sorted_by_atime();
1461 + int to_delete = kv_size(images_sorted) -
1462 + graphics_max_total_placements;
1463 + for (; images_begin < to_delete; images_begin++)
1464 + gr_delete_image(images_sorted.a[images_begin]);
1465 + }
1466 + // Then reduce the number of placements if there are too many.
1467 + if (total_placement_count >
1468 + apply_tolerance(graphics_max_total_placements)) {
1469 + GR_LOG("Too many placements: %d\n", total_placement_count);
1470 + changed = 1;
1471 + placements_sorted = gr_get_placements_sorted_by_atime();
1472 + int to_delete = kv_size(placements_sorted) -
1473 + graphics_max_total_placements;
1474 + for (; placements_begin < to_delete; placements_begin++) {
1475 + ImagePlacement *placement =
1476 + placements_sorted.a[placements_begin];
1477 + if (placement->protected_frame)
1478 + break;
1479 + gr_delete_placement(placement);
1480 + }
1481 + }
1482 + // Then reduce the size of the image file cache. The files correspond to
1483 + // image frames.
1484 + if (images_disk_size >
1485 + apply_tolerance(graphics_total_file_cache_size)) {
1486 + GR_LOG("Too big disk cache: %ld KiB\n",
1487 + images_disk_size / 1024);
1488 + changed = 1;
1489 + frames_sorted = gr_get_frames_sorted_by_atime();
1490 + for (int i = 0; i < kv_size(frames_sorted); i++) {
1491 + if (images_disk_size <= graphics_total_file_cache_size)
1492 + break;
1493 + gr_delete_imagefile(kv_A(frames_sorted, i));
1494 + }
1495 + }
1496 + // Then unload images from RAM.
1497 + if (images_ram_size > apply_tolerance(graphics_max_total_ram_size)) {
1498 + changed = 1;
1499 + int frames_begin = 0;
1500 + GR_LOG("Too much ram: %ld KiB\n", images_ram_size / 1024);
1501 + objects_sorted = gr_get_unloadable_objects_sorted_by_score(now);
1502 + for (int i = 0; i < kv_size(objects_sorted); i++) {
1503 + if (images_ram_size <= graphics_max_total_ram_size)
1504 + break;
1505 + gr_unload_object(&kv_A(objects_sorted, i));
1506 + }
1507 + }
1508 + if (changed) {
1509 + GR_LOG("After cleaning: ram: %ld KiB disk: %ld KiB "
1510 + "img count: %d placement count: %d\n",
1511 + images_ram_size / 1024, images_disk_size / 1024,
1512 + kh_size(images), total_placement_count);
1513 + }
1514 + kv_destroy(images_sorted);
1515 + kv_destroy(placements_sorted);
1516 + kv_destroy(frames_sorted);
1517 + kv_destroy(objects_sorted);
1518 +}
1519 +
1520 +/// Unloads all images by user request.
1521 +void gr_unload_images_to_reduce_ram() {
1522 + Image *img = NULL;
1523 + ImagePlacement *placement = NULL;
1524 + kh_foreach_value(images, img, {
1525 + kh_foreach_value(img->placements, placement, {
1526 + if (placement->protected_frame)
1527 + continue;
1528 + gr_unload_placement(placement);
1529 + });
1530 + gr_unload_all_frames(img);
1531 + });
1532 +}
1533 +
1534 +////////////////////////////////////////////////////////////////////////////////
1535 +// Image loading.
1536 +////////////////////////////////////////////////////////////////////////////////
1537 +
1538 +/// Copies `num_pixels` pixels (not bytes!) from a buffer `from` to an imlib2
1539 +/// image data `to`. The format may be 24 (RGB) or 32 (RGBA), and it's converted
1540 +/// to imlib2's representation, which is 0xAARRGGBB (having BGRA memory layout
1541 +/// on little-endian architectures).
1542 +static inline void gr_copy_pixels(DATA32 *to, unsigned char *from, int format,
1543 + size_t num_pixels) {
1544 + size_t pixel_size = format == 24 ? 3 : 4;
1545 + if (format == 32) {
1546 + for (unsigned i = 0; i < num_pixels; ++i) {
1547 + unsigned byte_i = i * pixel_size;
1548 + to[i] = ((DATA32)from[byte_i + 2]) |
1549 + ((DATA32)from[byte_i + 1]) << 8 |
1550 + ((DATA32)from[byte_i]) << 16 |
1551 + ((DATA32)from[byte_i + 3]) << 24;
1552 + }
1553 + } else {
1554 + for (unsigned i = 0; i < num_pixels; ++i) {
1555 + unsigned byte_i = i * pixel_size;
1556 + to[i] = ((DATA32)from[byte_i + 2]) |
1557 + ((DATA32)from[byte_i + 1]) << 8 |
1558 + ((DATA32)from[byte_i]) << 16 | 0xFF000000;
1559 + }
1560 + }
1561 +}
1562 +
1563 +/// Loads uncompressed RGB or RGBA image data from a file.
1564 +static void gr_load_raw_pixel_data_uncompressed(DATA32 *data, FILE *file,
1565 + int format,
1566 + size_t total_pixels) {
1567 + unsigned char chunk[BUFSIZ];
1568 + size_t pixel_size = format == 24 ? 3 : 4;
1569 + size_t chunk_size_pix = BUFSIZ / 4;
1570 + size_t chunk_size_bytes = chunk_size_pix * pixel_size;
1571 + size_t bytes = total_pixels * pixel_size;
1572 + for (size_t chunk_start_pix = 0; chunk_start_pix < total_pixels;
1573 + chunk_start_pix += chunk_size_pix) {
1574 + size_t read_size = fread(chunk, 1, chunk_size_bytes, file);
1575 + size_t read_pixels = read_size / pixel_size;
1576 + if (chunk_start_pix + read_pixels > total_pixels)
1577 + read_pixels = total_pixels - chunk_start_pix;
1578 + gr_copy_pixels(data + chunk_start_pix, chunk, format,
1579 + read_pixels);
1580 + }
1581 +}
1582 +
1583 +#define COMPRESSED_CHUNK_SIZE BUFSIZ
1584 +#define DECOMPRESSED_CHUNK_SIZE (BUFSIZ * 4)
1585 +
1586 +/// Loads compressed RGB or RGBA image data from a file.
1587 +static int gr_load_raw_pixel_data_compressed(DATA32 *data, FILE *file,
1588 + int format, size_t total_pixels) {
1589 + size_t pixel_size = format == 24 ? 3 : 4;
1590 + unsigned char compressed_chunk[COMPRESSED_CHUNK_SIZE];
1591 + unsigned char decompressed_chunk[DECOMPRESSED_CHUNK_SIZE];
1592 +
1593 + z_stream strm;
1594 + strm.zalloc = Z_NULL;
1595 + strm.zfree = Z_NULL;
1596 + strm.opaque = Z_NULL;
1597 + strm.next_out = decompressed_chunk;
1598 + strm.avail_out = DECOMPRESSED_CHUNK_SIZE;
1599 + strm.avail_in = 0;
1600 + strm.next_in = Z_NULL;
1601 + int ret = inflateInit(&strm);
1602 + if (ret != Z_OK)
1603 + return 1;
1604 +
1605 + int error = 0;
1606 + int progress = 0;
1607 + size_t total_copied_pixels = 0;
1608 + while (1) {
1609 + // If we don't have enough data in the input buffer, try to read
1610 + // from the file.
1611 + if (strm.avail_in <= COMPRESSED_CHUNK_SIZE / 4) {
1612 + // Move the existing data to the beginning.
1613 + memmove(compressed_chunk, strm.next_in, strm.avail_in);
1614 + strm.next_in = compressed_chunk;
1615 + // Read more data.
1616 + size_t bytes_read = fread(
1617 + compressed_chunk + strm.avail_in, 1,
1618 + COMPRESSED_CHUNK_SIZE - strm.avail_in, file);
1619 + strm.avail_in += bytes_read;
1620 + if (bytes_read != 0)
1621 + progress = 1;
1622 + }
1623 +
1624 + // Try to inflate the data.
1625 + int ret = inflate(&strm, Z_SYNC_FLUSH);
1626 + if (ret == Z_MEM_ERROR || ret == Z_DATA_ERROR) {
1627 + error = 1;
1628 + fprintf(stderr,
1629 + "error: could not decompress the image, error "
1630 + "%s\n",
1631 + ret == Z_MEM_ERROR ? "Z_MEM_ERROR"
1632 + : "Z_DATA_ERROR");
1633 + break;
1634 + }
1635 +
1636 + // Copy the data from the output buffer to the image.
1637 + size_t full_pixels =
1638 + (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) / pixel_size;
1639 + // Make sure we don't overflow the image.
1640 + if (full_pixels > total_pixels - total_copied_pixels)
1641 + full_pixels = total_pixels - total_copied_pixels;
1642 + if (full_pixels > 0) {
1643 + // Copy pixels.
1644 + gr_copy_pixels(data, decompressed_chunk, format,
1645 + full_pixels);
1646 + data += full_pixels;
1647 + total_copied_pixels += full_pixels;
1648 + if (total_copied_pixels >= total_pixels) {
1649 + // We filled the whole image, there may be some
1650 + // data left, but we just truncate it.
1651 + break;
1652 + }
1653 + // Move the remaining data to the beginning.
1654 + size_t copied_bytes = full_pixels * pixel_size;
1655 + size_t leftover =
1656 + (DECOMPRESSED_CHUNK_SIZE - strm.avail_out) -
1657 + copied_bytes;
1658 + memmove(decompressed_chunk,
1659 + decompressed_chunk + copied_bytes, leftover);
1660 + strm.next_out -= copied_bytes;
1661 + strm.avail_out += copied_bytes;
1662 + progress = 1;
1663 + }
1664 +
1665 + // If we haven't made any progress, then we have reached the end
1666 + // of both the file and the inflated data.
1667 + if (!progress)
1668 + break;
1669 + progress = 0;
1670 + }
1671 +
1672 + inflateEnd(&strm);
1673 + return error;
1674 +}
1675 +
1676 +#undef COMPRESSED_CHUNK_SIZE
1677 +#undef DECOMPRESSED_CHUNK_SIZE
1678 +
1679 +/// Load the image from a file containing raw pixel data (RGB or RGBA), the data
1680 +/// may be compressed.
1681 +static Imlib_Image gr_load_raw_pixel_data(ImageFrame *frame,
1682 + const char *filename) {
1683 + size_t total_pixels = frame->data_pix_width * frame->data_pix_height;
1684 + if (total_pixels * 4 > graphics_max_single_image_ram_size) {
1685 + fprintf(stderr,
1686 + "error: image %u frame %u is too big too load: %zu > %u\n",
1687 + frame->image->image_id, frame->index, total_pixels * 4,
1688 + graphics_max_single_image_ram_size);
1689 + return NULL;
1690 + }
1691 +
1692 + FILE* file = fopen(filename, "rb");
1693 + if (!file) {
1694 + fprintf(stderr,
1695 + "error: could not open image file: %s\n",
1696 + sanitized_filename(filename));
1697 + return NULL;
1698 + }
1699 +
1700 + Imlib_Image image = imlib_create_image(frame->data_pix_width,
1701 + frame->data_pix_height);
1702 + if (!image) {
1703 + fprintf(stderr,
1704 + "error: could not create an image of size %d x %d\n",
1705 + frame->data_pix_width, frame->data_pix_height);
1706 + fclose(file);
1707 + return NULL;
1708 + }
1709 +
1710 + imlib_context_set_image(image);
1711 + imlib_image_set_has_alpha(1);
1712 + DATA32* data = imlib_image_get_data();
1713 +
1714 + // The default format is 32.
1715 + int format = frame->format ? frame->format : 32;
1716 +
1717 + if (frame->compression == 0) {
1718 + gr_load_raw_pixel_data_uncompressed(data, file, format,
1719 + total_pixels);
1720 + } else {
1721 + int ret = gr_load_raw_pixel_data_compressed(data, file, format,
1722 + total_pixels);
1723 + if (ret != 0) {
1724 + imlib_image_put_back_data(data);
1725 + imlib_free_image();
1726 + fclose(file);
1727 + return NULL;
1728 + }
1729 + }
1730 +
1731 + fclose(file);
1732 + imlib_image_put_back_data(data);
1733 + return image;
1734 +}
1735 +
1736 +/// Loads the unscaled frame into RAM as an imlib object. The frame imlib object
1737 +/// is fully composed on top of the background frame. If the frame is already
1738 +/// loaded, does nothing. Loading may fail, in which case the status of the
1739 +/// frame will be set to STATUS_RAM_LOADING_ERROR.
1740 +static void gr_load_imlib_object(ImageFrame *frame) {
1741 + if (frame->imlib_object)
1742 + return;
1743 +
1744 + // If the image is uninitialized or uploading has failed, or the file
1745 + // has been deleted, we cannot load the image.
1746 + if (frame->status < STATUS_UPLOADING_SUCCESS)
1747 + return;
1748 + if (frame->disk_size == 0) {
1749 + if (frame->status != STATUS_RAM_LOADING_ERROR) {
1750 + fprintf(stderr,
1751 + "error: cached image was deleted: %u frame %u\n",
1752 + frame->image->image_id, frame->index);
1753 + }
1754 + frame->status = STATUS_RAM_LOADING_ERROR;
1755 + return;
1756 + }
1757 +
1758 + // Prevent recursive dependences between frames.
1759 + if (frame->status == STATUS_RAM_LOADING_IN_PROGRESS) {
1760 + fprintf(stderr,
1761 + "error: recursive loading of image %u frame %u\n",
1762 + frame->image->image_id, frame->index);
1763 + frame->status = STATUS_RAM_LOADING_ERROR;
1764 + return;
1765 + }
1766 + frame->status = STATUS_RAM_LOADING_IN_PROGRESS;
1767 +
1768 + // Load the background frame if needed. Hopefully it's not recursive.
1769 + ImageFrame *bg_frame = NULL;
1770 + if (frame->background_frame_index) {
1771 + bg_frame = gr_get_frame(frame->image,
1772 + frame->background_frame_index);
1773 + if (!bg_frame) {
1774 + fprintf(stderr,
1775 + "error: could not find background "
1776 + "frame %d for image %u frame %d\n",
1777 + frame->background_frame_index,
1778 + frame->image->image_id, frame->index);
1779 + frame->status = STATUS_RAM_LOADING_ERROR;
1780 + return;
1781 + }
1782 + gr_load_imlib_object(bg_frame);
1783 + if (!bg_frame->imlib_object) {
1784 + fprintf(stderr,
1785 + "error: could not load background frame %d for "
1786 + "image %u frame %d\n",
1787 + frame->background_frame_index,
1788 + frame->image->image_id, frame->index);
1789 + frame->status = STATUS_RAM_LOADING_ERROR;
1790 + return;
1791 + }
1792 + }
1793 +
1794 + // Load the frame data image.
1795 + Imlib_Image frame_data_image = NULL;
1796 + char filename[MAX_FILENAME_SIZE];
1797 + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
1798 + GR_LOG("Loading image: %s\n", sanitized_filename(filename));
1799 + if (frame->format == 100 || frame->format == 0)
1800 + frame_data_image = imlib_load_image(filename);
1801 + if (frame->format == 32 || frame->format == 24 ||
1802 + (!frame_data_image && frame->format == 0))
1803 + frame_data_image = gr_load_raw_pixel_data(frame, filename);
1804 + this_redraw_cycle_loaded_files++;
1805 +
1806 + if (!frame_data_image) {
1807 + if (frame->status != STATUS_RAM_LOADING_ERROR) {
1808 + fprintf(stderr, "error: could not load image: %s\n",
1809 + sanitized_filename(filename));
1810 + }
1811 + frame->status = STATUS_RAM_LOADING_ERROR;
1812 + return;
1813 + }
1814 +
1815 + imlib_context_set_image(frame_data_image);
1816 + int frame_data_width = imlib_image_get_width();
1817 + int frame_data_height = imlib_image_get_height();
1818 + GR_LOG("Successfully loaded, size %d x %d\n", frame_data_width,
1819 + frame_data_height);
1820 + // If imlib loading succeeded, and it is the first frame, set the
1821 + // information about the original image size, unless it's already set.
1822 + if (frame->index == 1 && frame->image->pix_width == 0 &&
1823 + frame->image->pix_height == 0) {
1824 + frame->image->pix_width = frame_data_width;
1825 + frame->image->pix_height = frame_data_height;
1826 + }
1827 +
1828 + int image_width = frame->image->pix_width;
1829 + int image_height = frame->image->pix_height;
1830 +
1831 + // Compose the image with the background color or frame.
1832 + if (frame->background_color != 0 || bg_frame ||
1833 + image_width != frame_data_width ||
1834 + image_height != frame_data_height) {
1835 + GR_LOG("Composing the frame bg = 0x%08X, bgframe = %d\n",
1836 + frame->background_color, frame->background_frame_index);
1837 + Imlib_Image composed_image = imlib_create_image(
1838 + image_width, image_height);
1839 + imlib_context_set_image(composed_image);
1840 + imlib_image_set_has_alpha(1);
1841 + imlib_context_set_anti_alias(0);
1842 +
1843 + // Start with the background frame or color.
1844 + imlib_context_set_blend(0);
1845 + if (bg_frame && bg_frame->imlib_object) {
1846 + imlib_blend_image_onto_image(
1847 + bg_frame->imlib_object, 1, 0, 0,
1848 + image_width, image_height, 0, 0,
1849 + image_width, image_height);
1850 + } else {
1851 + int r = (frame->background_color >> 24) & 0xFF;
1852 + int g = (frame->background_color >> 16) & 0xFF;
1853 + int b = (frame->background_color >> 8) & 0xFF;
1854 + int a = frame->background_color & 0xFF;
1855 + imlib_context_set_color(r, g, b, a);
1856 + imlib_image_fill_rectangle(0, 0, image_width,
1857 + image_height);
1858 + }
1859 +
1860 + // Blend the frame data image onto the background.
1861 + imlib_context_set_blend(1);
1862 + imlib_blend_image_onto_image(
1863 + frame_data_image, 1, 0, 0, frame->data_pix_width,
1864 + frame->data_pix_height, frame->x, frame->y,
1865 + frame->data_pix_width, frame->data_pix_height);
1866 +
1867 + // Free the frame data image.
1868 + imlib_context_set_image(frame_data_image);
1869 + imlib_free_image();
1870 +
1871 + frame_data_image = composed_image;
1872 + }
1873 +
1874 + frame->imlib_object = frame_data_image;
1875 +
1876 + images_ram_size += gr_frame_current_ram_size(frame);
1877 + frame->status = STATUS_RAM_LOADING_SUCCESS;
1878 +
1879 + GR_LOG("After loading image %u frame %d ram: %ld KiB (+ %u KiB)\n",
1880 + frame->image->image_id, frame->index,
1881 + images_ram_size / 1024, gr_frame_current_ram_size(frame) / 1024);
1882 +}
1883 +
1884 +/// Premultiplies the alpha channel of the image data. The data is an array of
1885 +/// pixels such that each pixel is a 32-bit integer in the format 0xAARRGGBB.
1886 +static void gr_premultiply_alpha(DATA32 *data, size_t num_pixels) {
1887 + for (size_t i = 0; i < num_pixels; ++i) {
1888 + DATA32 pixel = data[i];
1889 + unsigned char a = pixel >> 24;
1890 + if (a == 0) {
1891 + data[i] = 0;
1892 + } else if (a != 255) {
1893 + unsigned char b = (pixel & 0xFF) * a / 255;
1894 + unsigned char g = ((pixel >> 8) & 0xFF) * a / 255;
1895 + unsigned char r = ((pixel >> 16) & 0xFF) * a / 255;
1896 + data[i] = (a << 24) | (r << 16) | (g << 8) | b;
1897 + }
1898 + }
1899 +}
1900 +
1901 +/// Creates a pixmap for the frame of an image placement. The pixmap contain the
1902 +/// image data correctly scaled and fit to the box defined by the number of
1903 +/// rows/columns of the image placement and the provided cell dimensions in
1904 +/// pixels. If the placement is already loaded, it will be reloaded only if the
1905 +/// cell dimensions have changed.
1906 +Pixmap gr_load_pixmap(ImagePlacement *placement, int frameidx, int cw, int ch) {
1907 + Image *img = placement->image;
1908 + ImageFrame *frame = gr_get_frame(img, frameidx);
1909 +
1910 + // Update the atime uncoditionally.
1911 + gr_touch_placement(placement);
1912 + if (frame)
1913 + gr_touch_frame(frame);
1914 +
1915 + // If cw or ch are different, unload all the pixmaps.
1916 + if (placement->scaled_cw != cw || placement->scaled_ch != ch) {
1917 + gr_unload_placement(placement);
1918 + placement->scaled_cw = cw;
1919 + placement->scaled_ch = ch;
1920 + }
1921 +
1922 + // If it's already loaded, do nothing.
1923 + Pixmap pixmap = gr_get_frame_pixmap(placement, frameidx);
1924 + if (pixmap)
1925 + return pixmap;
1926 +
1927 + GR_LOG("Loading placement: %u/%u frame %u\n", img->image_id,
1928 + placement->placement_id, frameidx);
1929 +
1930 + // Load the imlib object for the frame.
1931 + if (!frame) {
1932 + fprintf(stderr,
1933 + "error: could not find frame %u for image %u\n",
1934 + frameidx, img->image_id);
1935 + return 0;
1936 + }
1937 + gr_load_imlib_object(frame);
1938 + if (!frame->imlib_object)
1939 + return 0;
1940 +
1941 + // Infer the placement size if needed.
1942 + gr_infer_placement_size_maybe(placement);
1943 +
1944 + // Create the scaled image. This is temporary, we will scale it
1945 + // appropriately, upload to the X server, and then delete immediately.
1946 + int scaled_w = (int)placement->cols * cw;
1947 + int scaled_h = (int)placement->rows * ch;
1948 + if (scaled_w * scaled_h * 4 > graphics_max_single_image_ram_size) {
1949 + fprintf(stderr,
1950 + "error: placement %u/%u would be too big to load: %d x "
1951 + "%d x 4 > %u\n",
1952 + img->image_id, placement->placement_id, scaled_w,
1953 + scaled_h, graphics_max_single_image_ram_size);
1954 + return 0;
1955 + }
1956 + Imlib_Image scaled_image = imlib_create_image(scaled_w, scaled_h);
1957 + if (!scaled_image) {
1958 + fprintf(stderr,
1959 + "error: imlib_create_image(%d, %d) returned "
1960 + "null\n",
1961 + scaled_w, scaled_h);
1962 + return 0;
1963 + }
1964 + imlib_context_set_image(scaled_image);
1965 + imlib_image_set_has_alpha(1);
1966 +
1967 + // First fill the scaled image with the transparent color.
1968 + imlib_context_set_blend(0);
1969 + imlib_context_set_color(0, 0, 0, 0);
1970 + imlib_image_fill_rectangle(0, 0, scaled_w, scaled_h);
1971 + imlib_context_set_anti_alias(1);
1972 + imlib_context_set_blend(1);
1973 +
1974 + // The source rectangle.
1975 + int src_x = placement->src_pix_x;
1976 + int src_y = placement->src_pix_y;
1977 + int src_w = placement->src_pix_width;
1978 + int src_h = placement->src_pix_height;
1979 + // Whether the box is too small to use the true size of the image.
1980 + char box_too_small = scaled_w < src_w || scaled_h < src_h;
1981 + char mode = placement->scale_mode;
1982 +
1983 + // Then blend the original image onto the transparent background.
1984 + if (src_w <= 0 || src_h <= 0) {
1985 + fprintf(stderr, "warning: image of zero size\n");
1986 + } else if (mode == SCALE_MODE_FILL) {
1987 + imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
1988 + src_y, src_w, src_h, 0, 0,
1989 + scaled_w, scaled_h);
1990 + } else if (mode == SCALE_MODE_NONE ||
1991 + (mode == SCALE_MODE_NONE_OR_CONTAIN && !box_too_small)) {
1992 + imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
1993 + src_y, src_w, src_h, 0, 0, src_w,
1994 + src_h);
1995 + } else {
1996 + if (mode != SCALE_MODE_CONTAIN &&
1997 + mode != SCALE_MODE_NONE_OR_CONTAIN) {
1998 + fprintf(stderr,
1999 + "warning: unknown scale mode %u, using "
2000 + "'contain' instead\n",
2001 + mode);
2002 + }
2003 + int dest_x, dest_y;
2004 + int dest_w, dest_h;
2005 + if (scaled_w * src_h > src_w * scaled_h) {
2006 + // If the box is wider than the original image, fit to
2007 + // height.
2008 + dest_h = scaled_h;
2009 + dest_y = 0;
2010 + dest_w = src_w * scaled_h / src_h;
2011 + dest_x = (scaled_w - dest_w) / 2;
2012 + } else {
2013 + // Otherwise, fit to width.
2014 + dest_w = scaled_w;
2015 + dest_x = 0;
2016 + dest_h = src_h * scaled_w / src_w;
2017 + dest_y = (scaled_h - dest_h) / 2;
2018 + }
2019 + imlib_blend_image_onto_image(frame->imlib_object, 1, src_x,
2020 + src_y, src_w, src_h, dest_x,
2021 + dest_y, dest_w, dest_h);
2022 + }
2023 +
2024 + // XRender needs the alpha channel premultiplied.
2025 + DATA32 *data = imlib_image_get_data();
2026 + gr_premultiply_alpha(data, scaled_w * scaled_h);
2027 +
2028 + // Upload the image to the X server.
2029 + Display *disp = imlib_context_get_display();
2030 + Visual *vis = imlib_context_get_visual();
2031 + Colormap cmap = imlib_context_get_colormap();
2032 + Drawable drawable = imlib_context_get_drawable();
2033 + if (!drawable)
2034 + drawable = DefaultRootWindow(disp);
2035 + pixmap = XCreatePixmap(disp, drawable, scaled_w, scaled_h, 32);
2036 + XVisualInfo visinfo;
2037 + XMatchVisualInfo(disp, DefaultScreen(disp), 32, TrueColor, &visinfo);
2038 + XImage *ximage = XCreateImage(disp, visinfo.visual, 32, ZPixmap, 0,
2039 + (char *)data, scaled_w, scaled_h, 32, 0);
2040 + GC gc = XCreateGC(disp, pixmap, 0, NULL);
2041 + XPutImage(disp, pixmap, gc, ximage, 0, 0, 0, 0, scaled_w,
2042 + scaled_h);
2043 + XFreeGC(disp, gc);
2044 + // XDestroyImage will free the data as well, but it is managed by imlib,
2045 + // so set it to NULL.
2046 + ximage->data = NULL;
2047 + XDestroyImage(ximage);
2048 + imlib_image_put_back_data(data);
2049 + imlib_free_image();
2050 +
2051 + // Assign the pixmap to the frame and increase the ram size.
2052 + gr_set_frame_pixmap(placement, frameidx, pixmap);
2053 + images_ram_size += gr_placement_single_frame_ram_size(placement);
2054 + this_redraw_cycle_loaded_pixmaps++;
2055 +
2056 + GR_LOG("After loading placement %u/%u frame %d ram: %ld KiB (+ %u "
2057 + "KiB)\n",
2058 + frame->image->image_id, placement->placement_id, frame->index,
2059 + images_ram_size / 1024,
2060 + gr_placement_single_frame_ram_size(placement) / 1024);
2061 +
2062 + // Free up ram if needed, but keep the pixmap we've loaded no matter
2063 + // what.
2064 + placement->protected_frame = frameidx;
2065 + gr_check_limits();
2066 + placement->protected_frame = 0;
2067 +
2068 + return pixmap;
2069 +}
2070 +
2071 +////////////////////////////////////////////////////////////////////////////////
2072 +// Initialization and deinitialization.
2073 +////////////////////////////////////////////////////////////////////////////////
2074 +
2075 +/// Creates a temporary directory.
2076 +static int gr_create_cache_dir() {
2077 + strncpy(cache_dir, graphics_cache_dir_template, sizeof(cache_dir));
2078 + if (!mkdtemp(cache_dir)) {
2079 + fprintf(stderr,
2080 + "error: could not create temporary dir from template "
2081 + "%s\n",
2082 + sanitized_filename(cache_dir));
2083 + return 0;
2084 + }
2085 + fprintf(stderr, "Graphics cache directory: %s\n", cache_dir);
2086 + return 1;
2087 +}
2088 +
2089 +/// Checks whether `tmp_dir` exists and recreates it if it doesn't.
2090 +static void gr_make_sure_tmpdir_exists() {
2091 + struct stat st;
2092 + if (stat(cache_dir, &st) == 0 && S_ISDIR(st.st_mode))
2093 + return;
2094 + fprintf(stderr,
2095 + "error: %s is not a directory, will need to create a new "
2096 + "graphics cache directory\n",
2097 + sanitized_filename(cache_dir));
2098 + gr_create_cache_dir();
2099 +}
2100 +
2101 +/// Initialize the graphics module.
2102 +void gr_init(Display *disp, Visual *vis, Colormap cm) {
2103 + // Set the initialization time.
2104 + clock_gettime(CLOCK_MONOTONIC, &initialization_time);
2105 +
2106 + // Create the temporary dir.
2107 + if (!gr_create_cache_dir())
2108 + abort();
2109 +
2110 + // Initialize imlib.
2111 + imlib_context_set_display(disp);
2112 + imlib_context_set_visual(vis);
2113 + imlib_context_set_colormap(cm);
2114 + imlib_context_set_anti_alias(1);
2115 + imlib_context_set_blend(1);
2116 + // Imlib2 checks only the file name when caching, which is not enough
2117 + // for us since we reuse file names. Disable caching.
2118 + imlib_set_cache_size(0);
2119 +
2120 + // Prepare for color inversion.
2121 + for (size_t i = 0; i < 256; ++i)
2122 + reverse_table[i] = 255 - i;
2123 +
2124 + // Create data structures.
2125 + images = kh_init(id2image);
2126 + kv_init(next_redraw_times);
2127 +
2128 + atexit(gr_deinit);
2129 +}
2130 +
2131 +/// Deinitialize the graphics module.
2132 +void gr_deinit() {
2133 + // Remove the cache dir.
2134 + remove(cache_dir);
2135 + kv_destroy(next_redraw_times);
2136 + if (images) {
2137 + // Delete all images.
2138 + gr_delete_all_images();
2139 + // Destroy the data structures.
2140 + kh_destroy(id2image, images);
2141 + images = NULL;
2142 + }
2143 +}
2144 +
2145 +////////////////////////////////////////////////////////////////////////////////
2146 +// Dumping, debugging, and image preview.
2147 +////////////////////////////////////////////////////////////////////////////////
2148 +
2149 +/// Returns a string containing a time difference in a human-readable format.
2150 +/// Uses a static buffer, so be careful.
2151 +static const char *gr_ago(Milliseconds diff) {
2152 + static char result[32];
2153 + double seconds = (double)diff / 1000.0;
2154 + if (seconds < 1)
2155 + snprintf(result, sizeof(result), "%.2f sec ago", seconds);
2156 + else if (seconds < 60)
2157 + snprintf(result, sizeof(result), "%d sec ago", (int)seconds);
2158 + else if (seconds < 3600)
2159 + snprintf(result, sizeof(result), "%d min %d sec ago",
2160 + (int)(seconds / 60), (int)(seconds) % 60);
2161 + else {
2162 + snprintf(result, sizeof(result), "%d hr %d min %d sec ago",
2163 + (int)(seconds / 3600), (int)(seconds) % 3600 / 60,
2164 + (int)(seconds) % 60);
2165 + }
2166 + return result;
2167 +}
2168 +
2169 +/// Prints to `file` with an indentation of `ind` spaces.
2170 +static void fprintf_ind(FILE *file, int ind, const char *format, ...) {
2171 + fprintf(file, "%*s", ind, "");
2172 + va_list args;
2173 + va_start(args, format);
2174 + vfprintf(file, format, args);
2175 + va_end(args);
2176 +}
2177 +
2178 +/// Dumps the image info to `file` with an indentation of `ind` spaces.
2179 +static void gr_dump_image_info(FILE *file, Image *img, int ind) {
2180 + if (!img) {
2181 + fprintf_ind(file, ind, "Image is NULL\n");
2182 + return;
2183 + }
2184 + Milliseconds now = gr_now_ms();
2185 + fprintf_ind(file, ind, "Image %u\n", img->image_id);
2186 + ind += 4;
2187 + fprintf_ind(file, ind, "number: %u\n", img->image_number);
2188 + fprintf_ind(file, ind, "global command index: %lu\n",
2189 + img->global_command_index);
2190 + fprintf_ind(file, ind, "accessed: %ld %s\n", img->atime,
2191 + gr_ago(now - img->atime));
2192 + fprintf_ind(file, ind, "pix size: %ux%u\n", img->pix_width,
2193 + img->pix_height);
2194 + fprintf_ind(file, ind, "cur frame start time: %ld %s\n",
2195 + img->current_frame_time,
2196 + gr_ago(now - img->current_frame_time));
2197 + if (img->next_redraw)
2198 + fprintf_ind(file, ind, "next redraw: %ld in %ld ms\n",
2199 + img->next_redraw, img->next_redraw - now);
2200 + fprintf_ind(file, ind, "total disk size: %u KiB\n",
2201 + img->total_disk_size / 1024);
2202 + fprintf_ind(file, ind, "total duration: %d\n", img->total_duration);
2203 + fprintf_ind(file, ind, "frames: %d\n", gr_last_frame_index(img));
2204 + fprintf_ind(file, ind, "cur frame: %d\n", img->current_frame);
2205 + fprintf_ind(file, ind, "animation state: %d\n", img->animation_state);
2206 + fprintf_ind(file, ind, "default_placement: %u\n",
2207 + img->default_placement);
2208 +}
2209 +
2210 +/// Dumps the frame info to `file` with an indentation of `ind` spaces.
2211 +static void gr_dump_frame_info(FILE *file, ImageFrame *frame, int ind) {
2212 + if (!frame) {
2213 + fprintf_ind(file, ind, "Frame is NULL\n");
2214 + return;
2215 + }
2216 + Milliseconds now = gr_now_ms();
2217 + fprintf_ind(file, ind, "Frame %d\n", frame->index);
2218 + ind += 4;
2219 + if (frame->index == 0) {
2220 + fprintf_ind(file, ind, "NOT INITIALIZED\n");
2221 + return;
2222 + }
2223 + if (frame->uploading_failure)
2224 + fprintf_ind(file, ind, "uploading failure: %s\n",
2225 + image_uploading_failure_strings
2226 + [frame->uploading_failure]);
2227 + fprintf_ind(file, ind, "gap: %d\n", frame->gap);
2228 + fprintf_ind(file, ind, "accessed: %ld %s\n", frame->atime,
2229 + gr_ago(now - frame->atime));
2230 + fprintf_ind(file, ind, "data pix size: %ux%u\n", frame->data_pix_width,
2231 + frame->data_pix_height);
2232 + char filename[MAX_FILENAME_SIZE];
2233 + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
2234 + if (access(filename, F_OK) != -1)
2235 + fprintf_ind(file, ind, "file: %s\n",
2236 + sanitized_filename(filename));
2237 + else
2238 + fprintf_ind(file, ind, "not on disk\n");
2239 + fprintf_ind(file, ind, "disk size: %u KiB\n", frame->disk_size / 1024);
2240 + if (frame->imlib_object) {
2241 + unsigned ram_size = gr_frame_current_ram_size(frame);
2242 + fprintf_ind(file, ind,
2243 + "loaded into ram, size: %d "
2244 + "KiB\n",
2245 + ram_size / 1024);
2246 + } else {
2247 + fprintf_ind(file, ind, "not loaded into ram\n");
2248 + }
2249 +}
2250 +
2251 +/// Dumps the placement info to `file` with an indentation of `ind` spaces.
2252 +static void gr_dump_placement_info(FILE *file, ImagePlacement *placement,
2253 + int ind) {
2254 + if (!placement) {
2255 + fprintf_ind(file, ind, "Placement is NULL\n");
2256 + return;
2257 + }
2258 + Milliseconds now = gr_now_ms();
2259 + fprintf_ind(file, ind, "Placement %u\n", placement->placement_id);
2260 + ind += 4;
2261 + fprintf_ind(file, ind, "accessed: %ld %s\n", placement->atime,
2262 + gr_ago(now - placement->atime));
2263 + fprintf_ind(file, ind, "scale_mode: %u\n", placement->scale_mode);
2264 + fprintf_ind(file, ind, "size: %u cols x %u rows\n", placement->cols,
2265 + placement->rows);
2266 + fprintf_ind(file, ind, "cell size: %ux%u\n", placement->scaled_cw,
2267 + placement->scaled_ch);
2268 + fprintf_ind(file, ind, "ram per frame: %u KiB\n",
2269 + gr_placement_single_frame_ram_size(placement) / 1024);
2270 + unsigned ram_size = gr_placement_current_ram_size(placement);
2271 + fprintf_ind(file, ind, "ram size: %d KiB\n", ram_size / 1024);
2272 +}
2273 +
2274 +/// Dumps placement pixmaps to `file` with an indentation of `ind` spaces.
2275 +static void gr_dump_placement_pixmaps(FILE *file, ImagePlacement *placement,
2276 + int ind) {
2277 + if (!placement)
2278 + return;
2279 + int frameidx = 1;
2280 + foreach_pixmap(*placement, pixmap, {
2281 + fprintf_ind(file, ind, "Frame %d pixmap %lu\n", frameidx,
2282 + pixmap);
2283 + ++frameidx;
2284 + });
2285 +}
2286 +
2287 +/// Dumps the internal state (images and placements) to stderr.
2288 +void gr_dump_state() {
2289 + FILE *file = stderr;
2290 + int ind = 0;
2291 + fprintf_ind(file, ind, "======= Graphics module state dump =======\n");
2292 + fprintf_ind(file, ind,
2293 + "sizeof(Image) = %lu sizeof(ImageFrame) = %lu "
2294 + "sizeof(ImagePlacement) = %lu\n",
2295 + sizeof(Image), sizeof(ImageFrame), sizeof(ImagePlacement));
2296 + fprintf_ind(file, ind, "Image count: %u\n", kh_size(images));
2297 + fprintf_ind(file, ind, "Placement count: %u\n", total_placement_count);
2298 + fprintf_ind(file, ind, "Estimated RAM usage: %ld KiB\n",
2299 + images_ram_size / 1024);
2300 + fprintf_ind(file, ind, "Estimated Disk usage: %ld KiB\n",
2301 + images_disk_size / 1024);
2302 +
2303 + Milliseconds now = gr_now_ms();
2304 +
2305 + int64_t images_ram_size_computed = 0;
2306 + int64_t images_disk_size_computed = 0;
2307 +
2308 + Image *img = NULL;
2309 + ImagePlacement *placement = NULL;
2310 + kh_foreach_value(images, img, {
2311 + fprintf_ind(file, ind, "----------------\n");
2312 + gr_dump_image_info(file, img, 0);
2313 + int64_t total_disk_size_computed = 0;
2314 + int total_duration_computed = 0;
2315 + foreach_frame(*img, frame, {
2316 + gr_dump_frame_info(file, frame, 4);
2317 + if (frame->image != img)
2318 + fprintf_ind(file, 8,
2319 + "ERROR: WRONG IMAGE POINTER\n");
2320 + total_duration_computed += frame->gap;
2321 + images_disk_size_computed += frame->disk_size;
2322 + total_disk_size_computed += frame->disk_size;
2323 + if (frame->imlib_object)
2324 + images_ram_size_computed +=
2325 + gr_frame_current_ram_size(frame);
2326 + });
2327 + if (img->total_disk_size != total_disk_size_computed) {
2328 + fprintf_ind(file, ind,
2329 + " ERROR: total_disk_size is %u, but "
2330 + "computed value is %ld\n",
2331 + img->total_disk_size, total_disk_size_computed);
2332 + }
2333 + if (img->total_duration != total_duration_computed) {
2334 + fprintf_ind(file, ind,
2335 + " ERROR: total_duration is %d, but computed "
2336 + "value is %d\n",
2337 + img->total_duration, total_duration_computed);
2338 + }
2339 + kh_foreach_value(img->placements, placement, {
2340 + gr_dump_placement_info(file, placement, 4);
2341 + if (placement->image != img)
2342 + fprintf_ind(file, 8,
2343 + "ERROR: WRONG IMAGE POINTER\n");
2344 + fprintf_ind(file, 8,
2345 + "Pixmaps:\n");
2346 + gr_dump_placement_pixmaps(file, placement, 12);
2347 + unsigned ram_size =
2348 + gr_placement_current_ram_size(placement);
2349 + images_ram_size_computed += ram_size;
2350 + });
2351 + });
2352 + if (images_ram_size != images_ram_size_computed) {
2353 + fprintf_ind(file, ind,
2354 + "ERROR: images_ram_size is %ld, but computed value "
2355 + "is %ld\n",
2356 + images_ram_size, images_ram_size_computed);
2357 + }
2358 + if (images_disk_size != images_disk_size_computed) {
2359 + fprintf_ind(file, ind,
2360 + "ERROR: images_disk_size is %ld, but computed value "
2361 + "is %ld\n",
2362 + images_disk_size, images_disk_size_computed);
2363 + }
2364 + fprintf_ind(file, ind, "===========================================\n");
2365 +}
2366 +
2367 +/// Executes `command` with the name of the file corresponding to `image_id` as
2368 +/// the argument. Executes xmessage with an error message on failure.
2369 +// TODO: Currently we do this for the first frame only. Not sure what to do with
2370 +// animations.
2371 +void gr_preview_image(uint32_t image_id, const char *exec) {
2372 + char command[256];
2373 + size_t len;
2374 + Image *img = gr_find_image(image_id);
2375 + if (img) {
2376 + ImageFrame *frame = &img->first_frame;
2377 + char filename[MAX_FILENAME_SIZE];
2378 + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
2379 + if (frame->disk_size == 0) {
2380 + len = snprintf(command, 255,
2381 + "xmessage 'Image with id=%u is not "
2382 + "fully copied to %s'",
2383 + image_id, sanitized_filename(filename));
2384 + } else {
2385 + len = snprintf(command, 255, "%s %s &", exec,
2386 + sanitized_filename(filename));
2387 + }
2388 + } else {
2389 + len = snprintf(command, 255,
2390 + "xmessage 'Cannot find image with id=%u'",
2391 + image_id);
2392 + }
2393 + if (len > 255) {
2394 + fprintf(stderr, "error: command too long: %s\n", command);
2395 + snprintf(command, 255, "xmessage 'error: command too long'");
2396 + }
2397 + if (system(command) != 0) {
2398 + fprintf(stderr, "error: could not execute command %s\n",
2399 + command);
2400 + }
2401 +}
2402 +
2403 +/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
2404 +/// containing the information about an image and placement, and <st> is
2405 +/// specified with `st_executable`.
2406 +void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
2407 + uint32_t imgcol, uint32_t imgrow,
2408 + char is_classic_placeholder, int32_t diacritic_count,
2409 + char *st_executable) {
2410 + char filename[MAX_FILENAME_SIZE];
2411 + snprintf(filename, sizeof(filename), "%s/info-%u", cache_dir, image_id);
2412 + FILE *file = fopen(filename, "w");
2413 + if (!file) {
2414 + perror("fopen");
2415 + return;
2416 + }
2417 + // Basic information about the cell.
2418 + fprintf(file, "image_id = %u = 0x%08X\n", image_id, image_id);
2419 + fprintf(file, "placement_id = %u = 0x%08X\n", placement_id, placement_id);
2420 + fprintf(file, "column = %d, row = %d\n", imgcol, imgrow);
2421 + fprintf(file, "classic/unicode placeholder = %s\n",
2422 + is_classic_placeholder ? "classic" : "unicode");
2423 + fprintf(file, "original diacritic count = %d\n", diacritic_count);
2424 + // Information about the image and the placement.
2425 + Image *img = gr_find_image(image_id);
2426 + ImagePlacement *placement = gr_find_placement(img, placement_id);
2427 + gr_dump_image_info(file, img, 0);
2428 + gr_dump_placement_info(file, placement, 0);
2429 + if (img) {
2430 + fprintf(file, "Frames:\n");
2431 + foreach_frame(*img, frame, {
2432 + gr_dump_frame_info(file, frame, 4);
2433 + });
2434 + }
2435 + if (placement) {
2436 + fprintf(file, "Placement pixmaps:\n");
2437 + gr_dump_placement_pixmaps(file, placement, 4);
2438 + }
2439 + fclose(file);
2440 + char *argv[] = {st_executable, "-e", "less", filename, NULL};
2441 + if (posix_spawnp(NULL, st_executable, NULL, NULL, argv, environ) != 0) {
2442 + perror("posix_spawnp");
2443 + return;
2444 + }
2445 +}
2446 +
2447 +////////////////////////////////////////////////////////////////////////////////
2448 +// Appending and displaying image rectangles.
2449 +////////////////////////////////////////////////////////////////////////////////
2450 +
2451 +/// Displays debug information in the rectangle using colors col1 and col2.
2452 +static void gr_displayinfo(Drawable buf, ImageRect *rect, int col1, int col2,
2453 + const char *message) {
2454 + int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
2455 + int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
2456 + Display *disp = imlib_context_get_display();
2457 + GC gc = XCreateGC(disp, buf, 0, NULL);
2458 + char info[MAX_INFO_LEN];
2459 + if (rect->placement_id)
2460 + snprintf(info, MAX_INFO_LEN, "%s%u/%u [%d:%d)x[%d:%d)", message,
2461 + rect->image_id, rect->placement_id,
2462 + rect->img_start_col, rect->img_end_col,
2463 + rect->img_start_row, rect->img_end_row);
2464 + else
2465 + snprintf(info, MAX_INFO_LEN, "%s%u [%d:%d)x[%d:%d)", message,
2466 + rect->image_id, rect->img_start_col, rect->img_end_col,
2467 + rect->img_start_row, rect->img_end_row);
2468 + XSetForeground(disp, gc, col1);
2469 + XDrawString(disp, buf, gc, rect->screen_x_pix + 4,
2470 + rect->screen_y_pix + h_pix - 3, info, strlen(info));
2471 + XSetForeground(disp, gc, col2);
2472 + XDrawString(disp, buf, gc, rect->screen_x_pix + 2,
2473 + rect->screen_y_pix + h_pix - 5, info, strlen(info));
2474 + XFreeGC(disp, gc);
2475 +}
2476 +
2477 +/// Draws a rectangle (bounding box) for debugging.
2478 +static void gr_showrect(Drawable buf, ImageRect *rect) {
2479 + int w_pix = (rect->img_end_col - rect->img_start_col) * rect->cw;
2480 + int h_pix = (rect->img_end_row - rect->img_start_row) * rect->ch;
2481 + Display *disp = imlib_context_get_display();
2482 + GC gc = XCreateGC(disp, buf, 0, NULL);
2483 + XSetForeground(disp, gc, 0xFF00FF00);
2484 + XDrawRectangle(disp, buf, gc, rect->screen_x_pix, rect->screen_y_pix,
2485 + w_pix - 1, h_pix - 1);
2486 + XSetForeground(disp, gc, 0xFFFF0000);
2487 + XDrawRectangle(disp, buf, gc, rect->screen_x_pix + 1,
2488 + rect->screen_y_pix + 1, w_pix - 3, h_pix - 3);
2489 + XFreeGC(disp, gc);
2490 +}
2491 +
2492 +/// Updates the next redraw time for the given row. Resizes the
2493 +/// next_redraw_times array if needed.
2494 +static void gr_update_next_redraw_time(int row, Milliseconds next_redraw) {
2495 + if (next_redraw == 0)
2496 + return;
2497 + if (row >= kv_size(next_redraw_times)) {
2498 + size_t old_size = kv_size(next_redraw_times);
2499 + kv_a(Milliseconds, next_redraw_times, row);
2500 + for (size_t i = old_size; i <= row; ++i)
2501 + kv_A(next_redraw_times, i) = 0;
2502 + }
2503 + Milliseconds old_value = kv_A(next_redraw_times, row);
2504 + if (old_value == 0 || old_value > next_redraw)
2505 + kv_A(next_redraw_times, row) = next_redraw;
2506 +}
2507 +
2508 +/// Draws the given part of an image.
2509 +static void gr_drawimagerect(Drawable buf, ImageRect *rect) {
2510 + ImagePlacement *placement =
2511 + gr_find_image_and_placement(rect->image_id, rect->placement_id);
2512 + // If the image does not exist or image display is switched off, draw
2513 + // the bounding box.
2514 + if (!placement || !graphics_display_images) {
2515 + gr_showrect(buf, rect);
2516 + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
2517 + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
2518 + return;
2519 + }
2520 +
2521 + Image *img = placement->image;
2522 +
2523 + if (img->last_redraw < drawing_start_time) {
2524 + // This is the first time we draw this image in this redraw
2525 + // cycle. Update the frame index we are going to display. Note
2526 + // that currently all image placements are synchronized.
2527 + int old_frame = img->current_frame;
2528 + gr_update_frame_index(img, drawing_start_time);
2529 + img->last_redraw = drawing_start_time;
2530 + }
2531 +
2532 + // Adjust next redraw times for the rows of this image rect.
2533 + if (img->next_redraw) {
2534 + for (int row = rect->screen_y_row;
2535 + row <= rect->screen_y_row + rect->img_end_row -
2536 + rect->img_start_row - 1; ++row) {
2537 + gr_update_next_redraw_time(
2538 + row, img->next_redraw);
2539 + }
2540 + }
2541 +
2542 + // Load the frame.
2543 + Pixmap pixmap = gr_load_pixmap(placement, img->current_frame, rect->cw,
2544 + rect->ch);
2545 +
2546 + // If the image couldn't be loaded, display the bounding box.
2547 + if (!pixmap) {
2548 + gr_showrect(buf, rect);
2549 + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
2550 + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
2551 + return;
2552 + }
2553 +
2554 + int src_x = rect->img_start_col * rect->cw;
2555 + int src_y = rect->img_start_row * rect->ch;
2556 + int width = (rect->img_end_col - rect->img_start_col) * rect->cw;
2557 + int height = (rect->img_end_row - rect->img_start_row) * rect->ch;
2558 + int dst_x = rect->screen_x_pix;
2559 + int dst_y = rect->screen_y_pix;
2560 +
2561 + // Display the image.
2562 + Display *disp = imlib_context_get_display();
2563 + Visual *vis = imlib_context_get_visual();
2564 +
2565 + // Create an xrender picture for the window.
2566 + XRenderPictFormat *win_format =
2567 + XRenderFindVisualFormat(disp, vis);
2568 + Picture window_pic =
2569 + XRenderCreatePicture(disp, buf, win_format, 0, NULL);
2570 +
2571 + // If needed, invert the image pixmap. Note that this naive approach of
2572 + // inverting the pixmap is not entirely correct, because the pixmap is
2573 + // premultiplied. But the result is good enough to visually indicate
2574 + // selection.
2575 + if (rect->reverse) {
2576 + unsigned pixmap_w =
2577 + (unsigned)placement->cols * placement->scaled_cw;
2578 + unsigned pixmap_h =
2579 + (unsigned)placement->rows * placement->scaled_ch;
2580 + Pixmap invpixmap =
2581 + XCreatePixmap(disp, buf, pixmap_w, pixmap_h, 32);
2582 + XGCValues gcv = {.function = GXcopyInverted};
2583 + GC gc = XCreateGC(disp, invpixmap, GCFunction, &gcv);
2584 + XCopyArea(disp, pixmap, invpixmap, gc, 0, 0, pixmap_w,
2585 + pixmap_h, 0, 0);
2586 + XFreeGC(disp, gc);
2587 + pixmap = invpixmap;
2588 + }
2589 +
2590 + // Create a picture for the image pixmap.
2591 + XRenderPictFormat *pic_format =
2592 + XRenderFindStandardFormat(disp, PictStandardARGB32);
2593 + Picture pixmap_pic =
2594 + XRenderCreatePicture(disp, pixmap, pic_format, 0, NULL);
2595 +
2596 + // Composite the image onto the window. In the reverse mode we ignore
2597 + // the alpha channel of the image because the naive inversion above
2598 + // seems to invert the alpha channel as well.
2599 + int pictop = rect->reverse ? PictOpSrc : PictOpOver;
2600 + XRenderComposite(disp, pictop, pixmap_pic, 0, window_pic,
2601 + src_x, src_y, src_x, src_y, dst_x, dst_y, width,
2602 + height);
2603 +
2604 + // Free resources
2605 + XRenderFreePicture(disp, pixmap_pic);
2606 + XRenderFreePicture(disp, window_pic);
2607 + if (rect->reverse)
2608 + XFreePixmap(disp, pixmap);
2609 +
2610 + // In debug mode always draw bounding boxes and print info.
2611 + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES) {
2612 + gr_showrect(buf, rect);
2613 + gr_displayinfo(buf, rect, 0xFF000000, 0xFFFFFFFF, "");
2614 + }
2615 +}
2616 +
2617 +/// Removes the given image rectangle.
2618 +static void gr_freerect(ImageRect *rect) { memset(rect, 0, sizeof(ImageRect)); }
2619 +
2620 +/// Returns the bottom coordinate of the rect.
2621 +static int gr_getrectbottom(ImageRect *rect) {
2622 + return rect->screen_y_pix +
2623 + (rect->img_end_row - rect->img_start_row) * rect->ch;
2624 +}
2625 +
2626 +/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
2627 +void gr_start_drawing(Drawable buf, int cw, int ch) {
2628 + current_cw = cw;
2629 + current_ch = ch;
2630 + this_redraw_cycle_loaded_files = 0;
2631 + this_redraw_cycle_loaded_pixmaps = 0;
2632 + drawing_start_time = gr_now_ms();
2633 + imlib_context_set_drawable(buf);
2634 +}
2635 +
2636 +/// Finish image drawing. This functions will draw all the rectangles left to
2637 +/// draw.
2638 +void gr_finish_drawing(Drawable buf) {
2639 + // Draw and then delete all known image rectangles.
2640 + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
2641 + ImageRect *rect = &image_rects[i];
2642 + if (!rect->image_id)
2643 + continue;
2644 + gr_drawimagerect(buf, rect);
2645 + gr_freerect(rect);
2646 + }
2647 +
2648 + // Compute the delay until the next redraw as the minimum of the next
2649 + // redraw delays for all rows.
2650 + Milliseconds drawing_end_time = gr_now_ms();
2651 + graphics_next_redraw_delay = INT_MAX;
2652 + for (int row = 0; row < kv_size(next_redraw_times); ++row) {
2653 + Milliseconds row_next_redraw = kv_A(next_redraw_times, row);
2654 + if (row_next_redraw > 0) {
2655 + int delay = MAX(graphics_animation_min_delay,
2656 + row_next_redraw - drawing_end_time);
2657 + graphics_next_redraw_delay =
2658 + MIN(graphics_next_redraw_delay, delay);
2659 + }
2660 + }
2661 +
2662 + // In debug mode display additional info.
2663 + if (graphics_debug_mode) {
2664 + int milliseconds = drawing_end_time - drawing_start_time;
2665 +
2666 + Display *disp = imlib_context_get_display();
2667 + GC gc = XCreateGC(disp, buf, 0, NULL);
2668 + const char *debug_mode_str =
2669 + graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES
2670 + ? "(boxes shown) "
2671 + : "";
2672 + int redraw_delay = graphics_next_redraw_delay == INT_MAX
2673 + ? -1
2674 + : graphics_next_redraw_delay;
2675 + char info[MAX_INFO_LEN];
2676 + snprintf(info, MAX_INFO_LEN,
2677 + "%sRender time: %d ms ram %ld K disk %ld K count "
2678 + "%d cell %dx%d delay %d",
2679 + debug_mode_str, milliseconds, images_ram_size / 1024,
2680 + images_disk_size / 1024, kh_size(images), current_cw,
2681 + current_ch, redraw_delay);
2682 + XSetForeground(disp, gc, 0xFF000000);
2683 + XFillRectangle(disp, buf, gc, 0, 0, 600, 16);
2684 + XSetForeground(disp, gc, 0xFFFFFFFF);
2685 + XDrawString(disp, buf, gc, 0, 14, info, strlen(info));
2686 + XFreeGC(disp, gc);
2687 +
2688 + if (milliseconds > 0) {
2689 + fprintf(stderr, "%s (loaded %d files, %d pixmaps)\n",
2690 + info, this_redraw_cycle_loaded_files,
2691 + this_redraw_cycle_loaded_pixmaps);
2692 + }
2693 + }
2694 +
2695 + // Check the limits in case we have used too much ram for placements.
2696 + gr_check_limits();
2697 +}
2698 +
2699 +// Add an image rectangle to the list of rectangles to draw.
2700 +void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
2701 + int img_start_col, int img_end_col, int img_start_row,
2702 + int img_end_row, int x_col, int y_row, int x_pix,
2703 + int y_pix, int cw, int ch, int reverse) {
2704 + current_cw = cw;
2705 + current_ch = ch;
2706 +
2707 + ImageRect new_rect;
2708 + new_rect.image_id = image_id;
2709 + new_rect.placement_id = placement_id;
2710 + new_rect.img_start_col = img_start_col;
2711 + new_rect.img_end_col = img_end_col;
2712 + new_rect.img_start_row = img_start_row;
2713 + new_rect.img_end_row = img_end_row;
2714 + new_rect.screen_y_row = y_row;
2715 + new_rect.screen_x_pix = x_pix;
2716 + new_rect.screen_y_pix = y_pix;
2717 + new_rect.ch = ch;
2718 + new_rect.cw = cw;
2719 + new_rect.reverse = reverse;
2720 +
2721 + // Display some red text in debug mode.
2722 + if (graphics_debug_mode == GRAPHICS_DEBUG_LOG_AND_BOXES)
2723 + gr_displayinfo(buf, &new_rect, 0xFF000000, 0xFFFF0000, "? ");
2724 +
2725 + // If it's the empty image (image_id=0) or an empty rectangle, do
2726 + // nothing.
2727 + if (image_id == 0 || img_end_col - img_start_col <= 0 ||
2728 + img_end_row - img_start_row <= 0)
2729 + return;
2730 + // Try to find a rect to merge with.
2731 + ImageRect *free_rect = NULL;
2732 + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
2733 + ImageRect *rect = &image_rects[i];
2734 + if (rect->image_id == 0) {
2735 + if (!free_rect)
2736 + free_rect = rect;
2737 + continue;
2738 + }
2739 + if (rect->image_id != image_id ||
2740 + rect->placement_id != placement_id || rect->cw != cw ||
2741 + rect->ch != ch || rect->reverse != reverse)
2742 + continue;
2743 + // We only support the case when the new stripe is added to the
2744 + // bottom of an existing rectangle and they are perfectly
2745 + // aligned.
2746 + if (rect->img_end_row == img_start_row &&
2747 + gr_getrectbottom(rect) == y_pix) {
2748 + if (rect->img_start_col == img_start_col &&
2749 + rect->img_end_col == img_end_col &&
2750 + rect->screen_x_pix == x_pix) {
2751 + rect->img_end_row = img_end_row;
2752 + return;
2753 + }
2754 + }
2755 + }
2756 + // If we haven't merged the new rect with any existing rect, and there
2757 + // is no free rect, we have to render one of the existing rects.
2758 + if (!free_rect) {
2759 + for (size_t i = 0; i < MAX_IMAGE_RECTS; ++i) {
2760 + ImageRect *rect = &image_rects[i];
2761 + if (!free_rect || gr_getrectbottom(free_rect) >
2762 + gr_getrectbottom(rect))
2763 + free_rect = rect;
2764 + }
2765 + gr_drawimagerect(buf, free_rect);
2766 + gr_freerect(free_rect);
2767 + }
2768 + // Start a new rectangle in `free_rect`.
2769 + *free_rect = new_rect;
2770 +}
2771 +
2772 +/// Mark rows containing animations as dirty if it's time to redraw them. Must
2773 +/// be called right after `gr_start_drawing`.
2774 +void gr_mark_dirty_animations(int *dirty, int rows) {
2775 + if (rows < kv_size(next_redraw_times))
2776 + kv_size(next_redraw_times) = rows;
2777 + if (rows * 2 < kv_max(next_redraw_times))
2778 + kv_resize(Milliseconds, next_redraw_times, rows);
2779 + for (int i = 0; i < MIN(rows, kv_size(next_redraw_times)); ++i) {
2780 + if (dirty[i]) {
2781 + kv_A(next_redraw_times, i) = 0;
2782 + continue;
2783 + }
2784 + Milliseconds next_update = kv_A(next_redraw_times, i);
2785 + if (next_update > 0 && next_update <= drawing_start_time) {
2786 + dirty[i] = 1;
2787 + kv_A(next_redraw_times, i) = 0;
2788 + }
2789 + }
2790 +}
2791 +
2792 +////////////////////////////////////////////////////////////////////////////////
2793 +// Command parsing and handling.
2794 +////////////////////////////////////////////////////////////////////////////////
2795 +
2796 +/// A parsed kitty graphics protocol command.
2797 +typedef struct {
2798 + /// The command itself, without the 'G'.
2799 + char *command;
2800 + /// The payload (after ';').
2801 + char *payload;
2802 + /// 'a=', may be 't', 'q', 'f', 'T', 'p', 'd', 'a'.
2803 + char action;
2804 + /// 'q=', 1 to suppress OK response, 2 to suppress errors too.
2805 + int quiet;
2806 + /// 'f=', use 24 or 32 for raw pixel data, 100 to autodetect with
2807 + /// imlib2. If 'f=0', will try to load with imlib2, then fallback to
2808 + /// 32-bit pixel data.
2809 + int format;
2810 + /// 'o=', may be 'z' for RFC 1950 ZLIB.
2811 + int compression;
2812 + /// 't=', may be 'f', 't' or 'd'.
2813 + char transmission_medium;
2814 + /// 'd='
2815 + char delete_specifier;
2816 + /// 's=', 'v=', if 'a=t' or 'a=T', used only when 'f=24' or 'f=32'.
2817 + /// When 'a=f', this is the size of the frame rectangle when composed on
2818 + /// top of another frame.
2819 + int frame_pix_width, frame_pix_height;
2820 + /// 'x=', 'y=' - top-left corner of the source rectangle.
2821 + int src_pix_x, src_pix_y;
2822 + /// 'w=', 'h=' - width and height of the source rectangle.
2823 + int src_pix_width, src_pix_height;
2824 + /// 'r=', 'c='
2825 + int rows, columns;
2826 + /// 'i='
2827 + uint32_t image_id;
2828 + /// 'I='
2829 + uint32_t image_number;
2830 + /// 'p='
2831 + uint32_t placement_id;
2832 + /// 'm=', may be 0 or 1.
2833 + int more;
2834 + /// True if either 'm=0' or 'm=1' is specified.
2835 + char is_data_transmission;
2836 + /// True if turns out that this command is a continuation of a data
2837 + /// transmission and not the first one for this image. Populated by
2838 + /// `gr_handle_transmit_command`.
2839 + char is_direct_transmission_continuation;
2840 + /// 'S=', used to check the size of uploaded data.
2841 + int size;
2842 + /// 'U=', whether it's a virtual placement for Unicode placeholders.
2843 + int virtual;
2844 + /// 'C=', if true, do not move the cursor when displaying this placement
2845 + /// (non-virtual placements only).
2846 + char do_not_move_cursor;
2847 + // ---------------------------------------------------------------------
2848 + // Animation-related fields. Their keys often overlap with keys of other
2849 + // commands, so these make sense only if the action is 'a=f' (frame
2850 + // transmission) or 'a=a' (animation control).
2851 + //
2852 + // 'x=' and 'y=', the relative position of the frame image when it's
2853 + // composed on top of another frame.
2854 + int frame_dst_pix_x, frame_dst_pix_y;
2855 + /// 'X=', 'X=1' to replace colors instead of alpha blending on top of
2856 + /// the background color or frame.
2857 + char replace_instead_of_blending;
2858 + /// 'Y=', the background color in the 0xRRGGBBAA format (still
2859 + /// transmitted as a decimal number).
2860 + uint32_t background_color;
2861 + /// (Only for 'a=f'). 'c=', the 1-based index of the background frame.
2862 + int background_frame;
2863 + /// (Only for 'a=a'). 'c=', sets the index of the current frame.
2864 + int current_frame;
2865 + /// 'r=', the 1-based index of the frame to edit.
2866 + int edit_frame;
2867 + /// 'z=', the duration of the frame. Zero if not specified, negative if
2868 + /// the frame is gapless (i.e. skipped).
2869 + int gap;
2870 + /// (Only for 'a=a'). 's=', if non-zero, sets the state of the
2871 + /// animation, 1 to stop, 2 to run in loading mode, 3 to loop.
2872 + int animation_state;
2873 + /// (Only for 'a=a'). 'v=', if non-zero, sets the number of times the
2874 + /// animation will loop. 1 to loop infinitely, N to loop N-1 times.
2875 + int loops;
2876 +} GraphicsCommand;
2877 +
2878 +/// Replaces all non-printed characters in `str` with '?' and truncates the
2879 +/// string to `max_size`, maybe inserting ellipsis at the end.
2880 +static void sanitize_str(char *str, size_t max_size) {
2881 + assert(max_size >= 4);
2882 + for (size_t i = 0; i < max_size; ++i) {
2883 + unsigned c = str[i];
2884 + if (c == '\0')
2885 + return;
2886 + if (c >= 128 || !isprint(c))
2887 + str[i] = '?';
2888 + }
2889 + str[max_size - 1] = '\0';
2890 + str[max_size - 2] = '.';
2891 + str[max_size - 3] = '.';
2892 + str[max_size - 4] = '.';
2893 +}
2894 +
2895 +/// A non-destructive version of `sanitize_str`. Uses a static buffer, so be
2896 +/// careful.
2897 +static const char *sanitized_filename(const char *str) {
2898 + static char buf[MAX_FILENAME_SIZE];
2899 + strncpy(buf, str, sizeof(buf));
2900 + sanitize_str(buf, sizeof(buf));
2901 + return buf;
2902 +}
2903 +
2904 +/// Creates a response to the current command in `graphics_command_result`.
2905 +static void gr_createresponse(uint32_t image_id, uint32_t image_number,
2906 + uint32_t placement_id, const char *msg) {
2907 + if (!image_id && !image_number && !placement_id) {
2908 + // Nobody expects the response in this case, so just print it to
2909 + // stderr.
2910 + fprintf(stderr,
2911 + "error: No image id or image number or placement_id, "
2912 + "but still there is a response: %s\n",
2913 + msg);
2914 + return;
2915 + }
2916 + char *buf = graphics_command_result.response;
2917 + size_t maxlen = MAX_GRAPHICS_RESPONSE_LEN;
2918 + size_t written;
2919 + written = snprintf(buf, maxlen, "\033_G");
2920 + buf += written;
2921 + maxlen -= written;
2922 + if (image_id) {
2923 + written = snprintf(buf, maxlen, "i=%u,", image_id);
2924 + buf += written;
2925 + maxlen -= written;
2926 + }
2927 + if (image_number) {
2928 + written = snprintf(buf, maxlen, "I=%u,", image_number);
2929 + buf += written;
2930 + maxlen -= written;
2931 + }
2932 + if (placement_id) {
2933 + written = snprintf(buf, maxlen, "p=%u,", placement_id);
2934 + buf += written;
2935 + maxlen -= written;
2936 + }
2937 + buf[-1] = ';';
2938 + written = snprintf(buf, maxlen, "%s\033\\", msg);
2939 + buf += written;
2940 + maxlen -= written;
2941 + buf[-2] = '\033';
2942 + buf[-1] = '\\';
2943 +}
2944 +
2945 +/// Creates the 'OK' response to the current command, unless suppressed or a
2946 +/// non-final data transmission.
2947 +static void gr_reportsuccess_cmd(GraphicsCommand *cmd) {
2948 + if (cmd->quiet < 1 && !cmd->more)
2949 + gr_createresponse(cmd->image_id, cmd->image_number,
2950 + cmd->placement_id, "OK");
2951 +}
2952 +
2953 +/// Creates the 'OK' response to the current command (unless suppressed).
2954 +static void gr_reportsuccess_frame(ImageFrame *frame) {
2955 + uint32_t id = frame->image->query_id ? frame->image->query_id
2956 + : frame->image->image_id;
2957 + if (frame->quiet < 1)
2958 + gr_createresponse(id, frame->image->image_number,
2959 + frame->image->initial_placement_id, "OK");
2960 +}
2961 +
2962 +/// Creates an error response to the current command (unless suppressed).
2963 +static void gr_reporterror_cmd(GraphicsCommand *cmd, const char *format, ...) {
2964 + char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
2965 + graphics_command_result.error = 1;
2966 + va_list args;
2967 + va_start(args, format);
2968 + vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
2969 + va_end(args);
2970 +
2971 + fprintf(stderr, "%s in command: %s\n", errmsg, cmd->command);
2972 + if (cmd->quiet < 2)
2973 + gr_createresponse(cmd->image_id, cmd->image_number,
2974 + cmd->placement_id, errmsg);
2975 +}
2976 +
2977 +/// Creates an error response to the current command (unless suppressed).
2978 +static void gr_reporterror_frame(ImageFrame *frame, const char *format, ...) {
2979 + char errmsg[MAX_GRAPHICS_RESPONSE_LEN];
2980 + graphics_command_result.error = 1;
2981 + va_list args;
2982 + va_start(args, format);
2983 + vsnprintf(errmsg, MAX_GRAPHICS_RESPONSE_LEN, format, args);
2984 + va_end(args);
2985 +
2986 + if (!frame) {
2987 + fprintf(stderr, "%s\n", errmsg);
2988 + gr_createresponse(0, 0, 0, errmsg);
2989 + } else {
2990 + uint32_t id = frame->image->query_id ? frame->image->query_id
2991 + : frame->image->image_id;
2992 + fprintf(stderr, "%s id=%u\n", errmsg, id);
2993 + if (frame->quiet < 2)
2994 + gr_createresponse(id, frame->image->image_number,
2995 + frame->image->initial_placement_id,
2996 + errmsg);
2997 + }
2998 +}
2999 +
3000 +/// Loads an image and creates a success/failure response. Returns `frame`, or
3001 +/// NULL if it's a query action and the image was deleted.
3002 +static ImageFrame *gr_loadimage_and_report(ImageFrame *frame) {
3003 + gr_load_imlib_object(frame);
3004 + if (!frame->imlib_object) {
3005 + gr_reporterror_frame(frame, "EBADF: could not load image");
3006 + } else {
3007 + gr_reportsuccess_frame(frame);
3008 + }
3009 + // If it was a query action, discard the image.
3010 + if (frame->image->query_id) {
3011 + gr_delete_image(frame->image);
3012 + return NULL;
3013 + }
3014 + return frame;
3015 +}
3016 +
3017 +/// Creates an appropriate uploading failure response to the current command.
3018 +static void gr_reportuploaderror(ImageFrame *frame) {
3019 + switch (frame->uploading_failure) {
3020 + case 0:
3021 + return;
3022 + case ERROR_CANNOT_OPEN_CACHED_FILE:
3023 + gr_reporterror_frame(frame,
3024 + "EIO: could not create a file for image");
3025 + break;
3026 + case ERROR_OVER_SIZE_LIMIT:
3027 + gr_reporterror_frame(
3028 + frame,
3029 + "EFBIG: the size of the uploaded image exceeded "
3030 + "the image size limit %u",
3031 + graphics_max_single_image_file_size);
3032 + break;
3033 + case ERROR_UNEXPECTED_SIZE:
3034 + gr_reporterror_frame(frame,
3035 + "EINVAL: the size of the uploaded image %u "
3036 + "doesn't match the expected size %u",
3037 + frame->disk_size, frame->expected_size);
3038 + break;
3039 + };
3040 +}
3041 +
3042 +/// Displays a non-virtual placement. This functions records the information in
3043 +/// `graphics_command_result`, the placeholder itself is created by the terminal
3044 +/// after handling the current command in the graphics module.
3045 +static void gr_display_nonvirtual_placement(ImagePlacement *placement) {
3046 + if (placement->virtual)
3047 + return;
3048 + if (placement->image->first_frame.status < STATUS_RAM_LOADING_SUCCESS)
3049 + return;
3050 + // Infer the placement size if needed.
3051 + gr_infer_placement_size_maybe(placement);
3052 + // Populate the information about the placeholder which will be created
3053 + // by the terminal.
3054 + graphics_command_result.create_placeholder = 1;
3055 + graphics_command_result.placeholder.image_id = placement->image->image_id;
3056 + graphics_command_result.placeholder.placement_id = placement->placement_id;
3057 + graphics_command_result.placeholder.columns = placement->cols;
3058 + graphics_command_result.placeholder.rows = placement->rows;
3059 + graphics_command_result.placeholder.do_not_move_cursor =
3060 + placement->do_not_move_cursor;
3061 + GR_LOG("Creating a placeholder for %u/%u %d x %d\n",
3062 + placement->image->image_id, placement->placement_id,
3063 + placement->cols, placement->rows);
3064 +}
3065 +
3066 +/// Marks the rows that are occupied by the image as dirty.
3067 +static void gr_schedule_image_redraw(Image *img) {
3068 + if (!img)
3069 + return;
3070 + gr_schedule_image_redraw_by_id(img->image_id);
3071 +}
3072 +
3073 +/// Appends data from `payload` to the frame `frame` when using direct
3074 +/// transmission. Note that we report errors only for the final command
3075 +/// (`!more`) to avoid spamming the client. If the frame is not specified, use
3076 +/// the image id and frame index we are currently uploading.
3077 +static void gr_append_data(ImageFrame *frame, const char *payload, int more) {
3078 + if (!frame) {
3079 + Image *img = gr_find_image(current_upload_image_id);
3080 + frame = gr_get_frame(img, current_upload_frame_index);
3081 + GR_LOG("Appending data to image %u frame %d\n",
3082 + current_upload_image_id, current_upload_frame_index);
3083 + if (!img)
3084 + GR_LOG("ERROR: this image doesn't exist\n");
3085 + if (!frame)
3086 + GR_LOG("ERROR: this frame doesn't exist\n");
3087 + }
3088 + if (!more) {
3089 + current_upload_image_id = 0;
3090 + current_upload_frame_index = 0;
3091 + }
3092 + if (!frame) {
3093 + if (!more)
3094 + gr_reporterror_frame(NULL, "ENOENT: could not find the "
3095 + "image to append data to");
3096 + return;
3097 + }
3098 + if (frame->status != STATUS_UPLOADING) {
3099 + if (!more)
3100 + gr_reportuploaderror(frame);
3101 + return;
3102 + }
3103 +
3104 + // Decode the data.
3105 + size_t data_size = 0;
3106 + char *data = gr_base64dec(payload, &data_size);
3107 +
3108 + GR_LOG("appending %u + %zu = %zu bytes\n", frame->disk_size, data_size,
3109 + frame->disk_size + data_size);
3110 +
3111 + // Do not append this data if the image exceeds the size limit.
3112 + if (frame->disk_size + data_size >
3113 + graphics_max_single_image_file_size ||
3114 + frame->expected_size > graphics_max_single_image_file_size) {
3115 + free(data);
3116 + gr_delete_imagefile(frame);
3117 + frame->uploading_failure = ERROR_OVER_SIZE_LIMIT;
3118 + if (!more)
3119 + gr_reportuploaderror(frame);
3120 + return;
3121 + }
3122 +
3123 + // If there is no open file corresponding to the image, create it.
3124 + if (!frame->open_file) {
3125 + gr_make_sure_tmpdir_exists();
3126 + char filename[MAX_FILENAME_SIZE];
3127 + gr_get_frame_filename(frame, filename, MAX_FILENAME_SIZE);
3128 + FILE *file = fopen(filename, frame->disk_size ? "a" : "w");
3129 + if (!file) {
3130 + frame->status = STATUS_UPLOADING_ERROR;
3131 + frame->uploading_failure = ERROR_CANNOT_OPEN_CACHED_FILE;
3132 + if (!more)
3133 + gr_reportuploaderror(frame);
3134 + return;
3135 + }
3136 + frame->open_file = file;
3137 + }
3138 +
3139 + // Write data to the file and update disk size variables.
3140 + fwrite(data, 1, data_size, frame->open_file);
3141 + free(data);
3142 + frame->disk_size += data_size;
3143 + frame->image->total_disk_size += data_size;
3144 + images_disk_size += data_size;
3145 + gr_touch_frame(frame);
3146 +
3147 + if (more) {
3148 + current_upload_image_id = frame->image->image_id;
3149 + current_upload_frame_index = frame->index;
3150 + } else {
3151 + current_upload_image_id = 0;
3152 + current_upload_frame_index = 0;
3153 + // Close the file.
3154 + if (frame->open_file) {
3155 + fclose(frame->open_file);
3156 + frame->open_file = NULL;
3157 + }
3158 + frame->status = STATUS_UPLOADING_SUCCESS;
3159 + uint32_t placement_id = frame->image->default_placement;
3160 + if (frame->expected_size &&
3161 + frame->expected_size != frame->disk_size) {
3162 + // Report failure if the uploaded image size doesn't
3163 + // match the expected size.
3164 + frame->status = STATUS_UPLOADING_ERROR;
3165 + frame->uploading_failure = ERROR_UNEXPECTED_SIZE;
3166 + gr_reportuploaderror(frame);
3167 + } else {
3168 + // Make sure to redraw all existing image instances.
3169 + gr_schedule_image_redraw(frame->image);
3170 + // Try to load the image into ram and report the result.
3171 + frame = gr_loadimage_and_report(frame);
3172 + // If there is a non-virtual image placement, we may
3173 + // need to display it.
3174 + if (frame && frame->index == 1) {
3175 + Image *img = frame->image;
3176 + ImagePlacement *placement = NULL;
3177 + kh_foreach_value(img->placements, placement, {
3178 + gr_display_nonvirtual_placement(placement);
3179 + });
3180 + }
3181 + }
3182 + }
3183 +
3184 + // Check whether we need to delete old images.
3185 + gr_check_limits();
3186 +}
3187 +
3188 +/// Finds the image either by id or by number specified in the command and sets
3189 +/// the image_id of `cmd` if the image was found.
3190 +static Image *gr_find_image_for_command(GraphicsCommand *cmd) {
3191 + if (cmd->image_id)
3192 + return gr_find_image(cmd->image_id);
3193 + Image *img = NULL;
3194 + // If the image number is not specified, we can't find the image, unless
3195 + // it's a put command, in which case we will try the last image.
3196 + if (cmd->image_number == 0 && cmd->action == 'p')
3197 + img = gr_find_image(last_image_id);
3198 + else
3199 + img = gr_find_image_by_number(cmd->image_number);
3200 + if (img)
3201 + cmd->image_id = img->image_id;
3202 + return img;
3203 +}
3204 +
3205 +/// Creates a new image or a new frame in an existing image (depending on the
3206 +/// command's action) and initializes its parameters from the command.
3207 +static ImageFrame *gr_new_image_or_frame_from_command(GraphicsCommand *cmd) {
3208 + if (cmd->format != 0 && cmd->format != 32 && cmd->format != 24 &&
3209 + cmd->compression != 0) {
3210 + gr_reporterror_cmd(cmd, "EINVAL: compression is supported only "
3211 + "for raw pixel data (f=32 or f=24)");
3212 + // Even though we report an error, we still create an image.
3213 + }
3214 +
3215 + Image *img = NULL;
3216 + if (cmd->action == 'f') {
3217 + // If it's a frame transmission action, there must be an
3218 + // existing image.
3219 + img = gr_find_image_for_command(cmd);
3220 + if (!img) {
3221 + gr_reporterror_cmd(cmd, "ENOENT: image not found");
3222 + return NULL;
3223 + }
3224 + } else {
3225 + // Otherwise create a new image object. If the action is `q`,
3226 + // we'll use random id instead of the one specified in the
3227 + // command.
3228 + uint32_t image_id = cmd->action == 'q' ? 0 : cmd->image_id;
3229 + img = gr_new_image(image_id);
3230 + if (!img)
3231 + return NULL;
3232 + if (cmd->action == 'q')
3233 + img->query_id = cmd->image_id;
3234 + else if (!cmd->image_id)
3235 + cmd->image_id = img->image_id;
3236 + // Set the image number.
3237 + img->image_number = cmd->image_number;
3238 + }
3239 +
3240 + ImageFrame *frame = gr_append_new_frame(img);
3241 + // Initialize the frame.
3242 + frame->expected_size = cmd->size;
3243 + frame->format = cmd->format;
3244 + frame->compression = cmd->compression;
3245 + frame->background_color = cmd->background_color;
3246 + frame->background_frame_index = cmd->background_frame;
3247 + frame->gap = cmd->gap;
3248 + img->total_duration += frame->gap;
3249 + frame->blend = !cmd->replace_instead_of_blending;
3250 + frame->data_pix_width = cmd->frame_pix_width;
3251 + frame->data_pix_height = cmd->frame_pix_height;
3252 + if (cmd->action == 'f') {
3253 + frame->x = cmd->frame_dst_pix_x;
3254 + frame->y = cmd->frame_dst_pix_y;
3255 + }
3256 + // We save the quietness information in the frame because for direct
3257 + // transmission subsequent transmission command won't contain this info.
3258 + frame->quiet = cmd->quiet;
3259 + return frame;
3260 +}
3261 +
3262 +/// Removes a file if it actually looks like a temporary file.
3263 +static void gr_delete_tmp_file(const char *filename) {
3264 + if (strstr(filename, "tty-graphics-protocol") == NULL)
3265 + return;
3266 + if (strstr(filename, "/tmp/") != filename) {
3267 + const char *tmpdir = getenv("TMPDIR");
3268 + if (!tmpdir || !tmpdir[0] ||
3269 + strstr(filename, tmpdir) != filename)
3270 + return;
3271 + }
3272 + unlink(filename);
3273 +}
3274 +
3275 +/// Handles a data transmission command.
3276 +static ImageFrame *gr_handle_transmit_command(GraphicsCommand *cmd) {
3277 + // The default is direct transmission.
3278 + if (!cmd->transmission_medium)
3279 + cmd->transmission_medium = 'd';
3280 +
3281 + // If neither id, nor image number is specified, and the transmission
3282 + // medium is 'd' (or unspecified), and there is an active direct upload,
3283 + // this is a continuation of the upload.
3284 + if (current_upload_image_id != 0 && cmd->image_id == 0 &&
3285 + cmd->image_number == 0 && cmd->transmission_medium == 'd') {
3286 + cmd->image_id = current_upload_image_id;
3287 + GR_LOG("No images id is specified, continuing uploading %u\n",
3288 + cmd->image_id);
3289 + }
3290 +
3291 + ImageFrame *frame = NULL;
3292 + if (cmd->transmission_medium == 'f' ||
3293 + cmd->transmission_medium == 't') {
3294 + // File transmission.
3295 + // Create a new image or a new frame of an existing image.
3296 + frame = gr_new_image_or_frame_from_command(cmd);
3297 + if (!frame)
3298 + return NULL;
3299 + last_image_id = frame->image->image_id;
3300 + // Decode the filename.
3301 + char *original_filename = gr_base64dec(cmd->payload, NULL);
3302 + GR_LOG("Copying image %s\n",
3303 + sanitized_filename(original_filename));
3304 + // Stat the file and check that it's a regular file and not too
3305 + // big.
3306 + struct stat st;
3307 + int stat_res = stat(original_filename, &st);
3308 + const char *stat_error = NULL;
3309 + if (stat_res)
3310 + stat_error = strerror(errno);
3311 + else if (!S_ISREG(st.st_mode))
3312 + stat_error = "Not a regular file";
3313 + else if (st.st_size == 0)
3314 + stat_error = "The size of the file is zero";
3315 + else if (st.st_size > graphics_max_single_image_file_size)
3316 + stat_error = "The file is too large";
3317 + if (stat_error) {
3318 + gr_reporterror_cmd(cmd,
3319 + "EBADF: %s", stat_error);
3320 + fprintf(stderr, "Could not load the file %s\n",
3321 + sanitized_filename(original_filename));
3322 + frame->status = STATUS_UPLOADING_ERROR;
3323 + frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
3324 + } else {
3325 + gr_make_sure_tmpdir_exists();
3326 + // Build the filename for the cached copy of the file.
3327 + char cache_filename[MAX_FILENAME_SIZE];
3328 + gr_get_frame_filename(frame, cache_filename,
3329 + MAX_FILENAME_SIZE);
3330 + // We will create a symlink to the original file, and
3331 + // then copy the file to the temporary cache dir. We do
3332 + // this symlink trick mostly to be able to use cp for
3333 + // copying, and avoid escaping file name characters when
3334 + // calling system at the same time.
3335 + char tmp_filename_symlink[MAX_FILENAME_SIZE + 4] = {0};
3336 + strcat(tmp_filename_symlink, cache_filename);
3337 + strcat(tmp_filename_symlink, ".sym");
3338 + char command[MAX_FILENAME_SIZE + 256];
3339 + size_t len =
3340 + snprintf(command, MAX_FILENAME_SIZE + 255,
3341 + "cp '%s' '%s'", tmp_filename_symlink,
3342 + cache_filename);
3343 + if (len > MAX_FILENAME_SIZE + 255 ||
3344 + symlink(original_filename, tmp_filename_symlink) ||
3345 + system(command) != 0) {
3346 + gr_reporterror_cmd(cmd,
3347 + "EBADF: could not copy the "
3348 + "image to the cache dir");
3349 + fprintf(stderr,
3350 + "Could not copy the image "
3351 + "%s (symlink %s) to %s",
3352 + sanitized_filename(original_filename),
3353 + tmp_filename_symlink, cache_filename);
3354 + frame->status = STATUS_UPLOADING_ERROR;
3355 + frame->uploading_failure = ERROR_CANNOT_COPY_FILE;
3356 + } else {
3357 + // Get the file size of the copied file.
3358 + frame->status = STATUS_UPLOADING_SUCCESS;
3359 + frame->disk_size = st.st_size;
3360 + frame->image->total_disk_size += st.st_size;
3361 + images_disk_size += frame->disk_size;
3362 + if (frame->expected_size &&
3363 + frame->expected_size != frame->disk_size) {
3364 + // The file has unexpected size.
3365 + frame->status = STATUS_UPLOADING_ERROR;
3366 + frame->uploading_failure =
3367 + ERROR_UNEXPECTED_SIZE;
3368 + gr_reportuploaderror(frame);
3369 + } else {
3370 + // Everything seems fine, try to load
3371 + // and redraw existing instances.
3372 + gr_schedule_image_redraw(frame->image);
3373 + frame = gr_loadimage_and_report(frame);
3374 + }
3375 + }
3376 + // Delete the symlink.
3377 + unlink(tmp_filename_symlink);
3378 + // Delete the original file if it's temporary.
3379 + if (cmd->transmission_medium == 't')
3380 + gr_delete_tmp_file(original_filename);
3381 + }
3382 + free(original_filename);
3383 + gr_check_limits();
3384 + } else if (cmd->transmission_medium == 'd') {
3385 + // Direct transmission (default if 't' is not specified).
3386 + frame = gr_get_last_frame(gr_find_image_for_command(cmd));
3387 + if (frame && frame->status == STATUS_UPLOADING) {
3388 + // This is a continuation of the previous transmission.
3389 + cmd->is_direct_transmission_continuation = 1;
3390 + gr_append_data(frame, cmd->payload, cmd->more);
3391 + return frame;
3392 + }
3393 + // If no action is specified, it's not the first transmission
3394 + // command. If we couldn't find the image, something went wrong
3395 + // and we should just drop this command.
3396 + if (cmd->action == 0)
3397 + return NULL;
3398 + // Otherwise create a new image or frame structure.
3399 + frame = gr_new_image_or_frame_from_command(cmd);
3400 + if (!frame)
3401 + return NULL;
3402 + last_image_id = frame->image->image_id;
3403 + frame->status = STATUS_UPLOADING;
3404 + // Start appending data.
3405 + gr_append_data(frame, cmd->payload, cmd->more);
3406 + } else {
3407 + gr_reporterror_cmd(
3408 + cmd,
3409 + "EINVAL: transmission medium '%c' is not supported",
3410 + cmd->transmission_medium);
3411 + return NULL;
3412 + }
3413 +
3414 + return frame;
3415 +}
3416 +
3417 +/// Handles the 'put' command by creating a placement.
3418 +static void gr_handle_put_command(GraphicsCommand *cmd) {
3419 + if (cmd->image_id == 0 && cmd->image_number == 0) {
3420 + gr_reporterror_cmd(cmd,
3421 + "EINVAL: neither image id nor image number "
3422 + "are specified or both are zero");
3423 + return;
3424 + }
3425 +
3426 + // Find the image with the id or number.
3427 + Image *img = gr_find_image_for_command(cmd);
3428 + if (!img) {
3429 + gr_reporterror_cmd(cmd, "ENOENT: image not found");
3430 + return;
3431 + }
3432 +
3433 + // Create a placement. If a placement with the same id already exists,
3434 + // it will be deleted. If the id is zero, a random id will be generated.
3435 + ImagePlacement *placement = gr_new_placement(img, cmd->placement_id);
3436 + placement->virtual = cmd->virtual;
3437 + placement->src_pix_x = cmd->src_pix_x;
3438 + placement->src_pix_y = cmd->src_pix_y;
3439 + placement->src_pix_width = cmd->src_pix_width;
3440 + placement->src_pix_height = cmd->src_pix_height;
3441 + placement->cols = cmd->columns;
3442 + placement->rows = cmd->rows;
3443 + placement->do_not_move_cursor = cmd->do_not_move_cursor;
3444 +
3445 + if (placement->virtual) {
3446 + placement->scale_mode = SCALE_MODE_CONTAIN;
3447 + } else if (placement->cols && placement->rows) {
3448 + // For classic placements the default is to stretch the image if
3449 + // both cols and rows are specified.
3450 + placement->scale_mode = SCALE_MODE_FILL;
3451 + } else if (placement->cols || placement->rows) {
3452 + // But if only one of them is specified, the default is to
3453 + // contain.
3454 + placement->scale_mode = SCALE_MODE_CONTAIN;
3455 + } else {
3456 + // If none of them are specified, the default is to use the
3457 + // original size.
3458 + placement->scale_mode = SCALE_MODE_NONE;
3459 + }
3460 +
3461 + // Display the placement unless it's virtual.
3462 + gr_display_nonvirtual_placement(placement);
3463 +
3464 + // Report success.
3465 + gr_reportsuccess_cmd(cmd);
3466 +}
3467 +
3468 +/// Information about what to delete.
3469 +typedef struct DeletionData {
3470 + uint32_t image_id;
3471 + uint32_t placement_id;
3472 + /// If true, delete the image object if there are no more placements.
3473 + char delete_image_if_no_ref;
3474 +} DeletionData;
3475 +
3476 +/// The callback called for each cell to perform deletion.
3477 +static int gr_deletion_callback(void *data, uint32_t image_id,
3478 + uint32_t placement_id, int col,
3479 + int row, char is_classic) {
3480 + DeletionData *del_data = data;
3481 + // Leave unicode placeholders alone.
3482 + if (!is_classic)
3483 + return 0;
3484 + if (del_data->image_id && del_data->image_id != image_id)
3485 + return 0;
3486 + if (del_data->placement_id && del_data->placement_id != placement_id)
3487 + return 0;
3488 + Image *img = gr_find_image(image_id);
3489 + // If the image is already deleted, just erase the placeholder.
3490 + if (!img)
3491 + return 1;
3492 + // Delete the placement.
3493 + if (placement_id)
3494 + gr_delete_placement(gr_find_placement(img, placement_id));
3495 + // Delete the image if image deletion is requested (uppercase delete
3496 + // specifier) and there are no more placements.
3497 + if (del_data->delete_image_if_no_ref && kh_size(img->placements) == 0)
3498 + gr_delete_image(img);
3499 + return 1;
3500 +}
3501 +
3502 +/// Handles the delete command.
3503 +static void gr_handle_delete_command(GraphicsCommand *cmd) {
3504 + DeletionData del_data = {0};
3505 + del_data.delete_image_if_no_ref = isupper(cmd->delete_specifier) != 0;
3506 + char d = tolower(cmd->delete_specifier);
3507 +
3508 + if (d == 'n') {
3509 + d = 'i';
3510 + Image *img = gr_find_image_by_number(cmd->image_number);
3511 + if (!img)
3512 + return;
3513 + del_data.image_id = img->image_id;
3514 + }
3515 +
3516 + if (!d || d == 'a') {
3517 + // Delete all visible placements.
3518 + gr_for_each_image_cell(gr_deletion_callback, &del_data);
3519 + } else if (d == 'i') {
3520 + // Delete the specified image by image id and maybe placement
3521 + // id.
3522 + if (!del_data.image_id)
3523 + del_data.image_id = cmd->image_id;
3524 + if (!del_data.image_id) {
3525 + fprintf(stderr,
3526 + "ERROR: image id is not specified in the "
3527 + "delete command\n");
3528 + return;
3529 + }
3530 + del_data.placement_id = cmd->placement_id;
3531 + // NOTE: It's not very clear whether we should delete the image
3532 + // even if there are no _visible_ placements to delete. We do
3533 + // this because otherwise there is no way to delete an image
3534 + // with virtual placements in one command.
3535 + if (!del_data.placement_id && del_data.delete_image_if_no_ref)
3536 + gr_delete_image(gr_find_image(cmd->image_id));
3537 + gr_for_each_image_cell(gr_deletion_callback, &del_data);
3538 + } else {
3539 + fprintf(stderr,
3540 + "WARNING: unsupported value of the d key: '%c'. The "
3541 + "command is ignored.\n",
3542 + cmd->delete_specifier);
3543 + }
3544 +}
3545 +
3546 +static void gr_handle_animation_control_command(GraphicsCommand *cmd) {
3547 + if (cmd->image_id == 0 && cmd->image_number == 0) {
3548 + gr_reporterror_cmd(cmd,
3549 + "EINVAL: neither image id nor image number "
3550 + "are specified or both are zero");
3551 + return;
3552 + }
3553 +
3554 + // Find the image with the id or number.
3555 + Image *img = gr_find_image_for_command(cmd);
3556 + if (!img) {
3557 + gr_reporterror_cmd(cmd, "ENOENT: image not found");
3558 + return;
3559 + }
3560 +
3561 + // Find the frame to edit, if requested.
3562 + ImageFrame *frame = NULL;
3563 + if (cmd->edit_frame)
3564 + frame = gr_get_frame(img, cmd->edit_frame);
3565 + if (cmd->edit_frame || cmd->gap) {
3566 + if (!frame) {
3567 + gr_reporterror_cmd(cmd, "ENOENT: frame %d not found",
3568 + cmd->edit_frame);
3569 + return;
3570 + }
3571 + if (cmd->gap) {
3572 + img->total_duration -= frame->gap;
3573 + frame->gap = cmd->gap;
3574 + img->total_duration += frame->gap;
3575 + }
3576 + }
3577 +
3578 + // Set animation-related parameters of the image.
3579 + if (cmd->current_frame)
3580 + img->current_frame = cmd->current_frame;
3581 + if (cmd->animation_state) {
3582 + if (cmd->animation_state == 1) {
3583 + img->animation_state = ANIMATION_STATE_STOPPED;
3584 + } else if (cmd->animation_state == 2) {
3585 + img->animation_state = ANIMATION_STATE_LOADING;
3586 + } else if (cmd->animation_state == 3) {
3587 + img->animation_state = ANIMATION_STATE_LOOPING;
3588 + } else {
3589 + gr_reporterror_cmd(
3590 + cmd, "EINVAL: invalid animation state: %d",
3591 + cmd->animation_state);
3592 + }
3593 + }
3594 + // TODO: Set the number of loops to cmd->loops
3595 +
3596 + // Make sure we redraw all instances of the image.
3597 + gr_schedule_image_redraw(img);
3598 +}
3599 +
3600 +/// Handles a command.
3601 +static void gr_handle_command(GraphicsCommand *cmd) {
3602 + if (!cmd->image_id && !cmd->image_number) {
3603 + // If there is no image id or image number, nobody expects a
3604 + // response, so set quiet to 2.
3605 + cmd->quiet = 2;
3606 + }
3607 + ImageFrame *frame = NULL;
3608 + switch (cmd->action) {
3609 + case 0:
3610 + // If no action is specified, it may be a data transmission
3611 + // command if 'm=' is specified.
3612 + if (cmd->is_data_transmission) {
3613 + gr_handle_transmit_command(cmd);
3614 + break;
3615 + }
3616 + gr_reporterror_cmd(cmd, "EINVAL: no action specified");
3617 + break;
3618 + case 't':
3619 + case 'q':
3620 + case 'f':
3621 + // Transmit data. 'q' means query, which is basically the same
3622 + // as transmit, but the image is discarded, and the id is fake.
3623 + // 'f' appends a frame to an existing image.
3624 + gr_handle_transmit_command(cmd);
3625 + break;
3626 + case 'p':
3627 + // Display (put) the image.
3628 + gr_handle_put_command(cmd);
3629 + break;
3630 + case 'T':
3631 + // Transmit and display.
3632 + frame = gr_handle_transmit_command(cmd);
3633 + if (frame && !cmd->is_direct_transmission_continuation) {
3634 + gr_handle_put_command(cmd);
3635 + if (cmd->placement_id)
3636 + frame->image->initial_placement_id =
3637 + cmd->placement_id;
3638 + }
3639 + break;
3640 + case 'd':
3641 + gr_handle_delete_command(cmd);
3642 + break;
3643 + case 'a':
3644 + gr_handle_animation_control_command(cmd);
3645 + break;
3646 + default:
3647 + gr_reporterror_cmd(cmd, "EINVAL: unsupported action: %c",
3648 + cmd->action);
3649 + return;
3650 + }
3651 +}
3652 +
3653 +/// A partially parsed key-value pair.
3654 +typedef struct KeyAndValue {
3655 + char *key_start;
3656 + char *val_start;
3657 + unsigned key_len, val_len;
3658 +} KeyAndValue;
3659 +
3660 +/// Parses the value of a key and assigns it to the appropriate field of `cmd`.
3661 +static void gr_set_keyvalue(GraphicsCommand *cmd, KeyAndValue *kv) {
3662 + char *key_start = kv->key_start;
3663 + char *key_end = key_start + kv->key_len;
3664 + char *value_start = kv->val_start;
3665 + char *value_end = value_start + kv->val_len;
3666 + // Currently all keys are one-character.
3667 + if (key_end - key_start != 1) {
3668 + gr_reporterror_cmd(cmd, "EINVAL: unknown key of length %ld: %s",
3669 + key_end - key_start, key_start);
3670 + return;
3671 + }
3672 + long num = 0;
3673 + if (*key_start == 'a' || *key_start == 't' || *key_start == 'd' ||
3674 + *key_start == 'o') {
3675 + // Some keys have one-character values.
3676 + if (value_end - value_start != 1) {
3677 + gr_reporterror_cmd(
3678 + cmd,
3679 + "EINVAL: value of 'a', 't' or 'd' must be a "
3680 + "single char: %s",
3681 + key_start);
3682 + return;
3683 + }
3684 + } else {
3685 + // All the other keys have integer values.
3686 + char *num_end = NULL;
3687 + num = strtol(value_start, &num_end, 10);
3688 + if (num_end != value_end) {
3689 + gr_reporterror_cmd(
3690 + cmd, "EINVAL: could not parse number value: %s",
3691 + key_start);
3692 + return;
3693 + }
3694 + }
3695 + switch (*key_start) {
3696 + case 'a':
3697 + cmd->action = *value_start;
3698 + break;
3699 + case 't':
3700 + cmd->transmission_medium = *value_start;
3701 + break;
3702 + case 'd':
3703 + cmd->delete_specifier = *value_start;
3704 + break;
3705 + case 'q':
3706 + cmd->quiet = num;
3707 + break;
3708 + case 'f':
3709 + cmd->format = num;
3710 + if (num != 0 && num != 24 && num != 32 && num != 100) {
3711 + gr_reporterror_cmd(
3712 + cmd,
3713 + "EINVAL: unsupported format specification: %s",
3714 + key_start);
3715 + }
3716 + break;
3717 + case 'o':
3718 + cmd->compression = *value_start;
3719 + if (cmd->compression != 'z') {
3720 + gr_reporterror_cmd(cmd,
3721 + "EINVAL: unsupported compression "
3722 + "specification: %s",
3723 + key_start);
3724 + }
3725 + break;
3726 + case 's':
3727 + if (cmd->action == 'a')
3728 + cmd->animation_state = num;
3729 + else
3730 + cmd->frame_pix_width = num;
3731 + break;
3732 + case 'v':
3733 + if (cmd->action == 'a')
3734 + cmd->loops = num;
3735 + else
3736 + cmd->frame_pix_height = num;
3737 + break;
3738 + case 'i':
3739 + cmd->image_id = num;
3740 + break;
3741 + case 'I':
3742 + cmd->image_number = num;
3743 + break;
3744 + case 'p':
3745 + cmd->placement_id = num;
3746 + break;
3747 + case 'x':
3748 + cmd->src_pix_x = num;
3749 + cmd->frame_dst_pix_x = num;
3750 + break;
3751 + case 'y':
3752 + if (cmd->action == 'f')
3753 + cmd->frame_dst_pix_y = num;
3754 + else
3755 + cmd->src_pix_y = num;
3756 + break;
3757 + case 'w':
3758 + cmd->src_pix_width = num;
3759 + break;
3760 + case 'h':
3761 + cmd->src_pix_height = num;
3762 + break;
3763 + case 'c':
3764 + if (cmd->action == 'f')
3765 + cmd->background_frame = num;
3766 + else if (cmd->action == 'a')
3767 + cmd->current_frame = num;
3768 + else
3769 + cmd->columns = num;
3770 + break;
3771 + case 'r':
3772 + if (cmd->action == 'f' || cmd->action == 'a')
3773 + cmd->edit_frame = num;
3774 + else
3775 + cmd->rows = num;
3776 + break;
3777 + case 'm':
3778 + cmd->is_data_transmission = 1;
3779 + cmd->more = num;
3780 + break;
3781 + case 'S':
3782 + cmd->size = num;
3783 + break;
3784 + case 'U':
3785 + cmd->virtual = num;
3786 + break;
3787 + case 'X':
3788 + if (cmd->action == 'f')
3789 + cmd->replace_instead_of_blending = num;
3790 + else
3791 + break; /*ignore*/
3792 + break;
3793 + case 'Y':
3794 + if (cmd->action == 'f')
3795 + cmd->background_color = num;
3796 + else
3797 + break; /*ignore*/
3798 + break;
3799 + case 'z':
3800 + if (cmd->action == 'f' || cmd->action == 'a')
3801 + cmd->gap = num;
3802 + else
3803 + break; /*ignore*/
3804 + break;
3805 + case 'C':
3806 + cmd->do_not_move_cursor = num;
3807 + break;
3808 + default:
3809 + gr_reporterror_cmd(cmd, "EINVAL: unsupported key: %s",
3810 + key_start);
3811 + return;
3812 + }
3813 +}
3814 +
3815 +/// Parse and execute a graphics command. `buf` must start with 'G' and contain
3816 +/// at least `len + 1` characters. Returns 1 on success.
3817 +int gr_parse_command(char *buf, size_t len) {
3818 + if (buf[0] != 'G')
3819 + return 0;
3820 +
3821 + memset(&graphics_command_result, 0, sizeof(GraphicsCommandResult));
3822 +
3823 + global_command_counter++;
3824 + GR_LOG("### Command %lu: %.80s\n", global_command_counter, buf);
3825 +
3826 + // Eat the 'G'.
3827 + ++buf;
3828 + --len;
3829 +
3830 + GraphicsCommand cmd = {.command = buf};
3831 + // The state of parsing. 'k' to parse key, 'v' to parse value, 'p' to
3832 + // parse the payload.
3833 + char state = 'k';
3834 + // An array of partially parsed key-value pairs.
3835 + KeyAndValue key_vals[32];
3836 + unsigned key_vals_count = 0;
3837 + char *key_start = buf;
3838 + char *key_end = NULL;
3839 + char *val_start = NULL;
3840 + char *val_end = NULL;
3841 + char *c = buf;
3842 + while (c - buf < len + 1) {
3843 + if (state == 'k') {
3844 + switch (*c) {
3845 + case ',':
3846 + case ';':
3847 + case '\0':
3848 + state = *c == ',' ? 'k' : 'p';
3849 + key_end = c;
3850 + gr_reporterror_cmd(
3851 + &cmd, "EINVAL: key without value: %s ",
3852 + key_start);
3853 + break;
3854 + case '=':
3855 + key_end = c;
3856 + state = 'v';
3857 + val_start = c + 1;
3858 + break;
3859 + default:
3860 + break;
3861 + }
3862 + } else if (state == 'v') {
3863 + switch (*c) {
3864 + case ',':
3865 + case ';':
3866 + case '\0':
3867 + state = *c == ',' ? 'k' : 'p';
3868 + val_end = c;
3869 + if (key_vals_count >=
3870 + sizeof(key_vals) / sizeof(*key_vals)) {
3871 + gr_reporterror_cmd(&cmd,
3872 + "EINVAL: too many "
3873 + "key-value pairs");
3874 + break;
3875 + }
3876 + key_vals[key_vals_count].key_start = key_start;
3877 + key_vals[key_vals_count].val_start = val_start;
3878 + key_vals[key_vals_count].key_len =
3879 + key_end - key_start;
3880 + key_vals[key_vals_count].val_len =
3881 + val_end - val_start;
3882 + ++key_vals_count;
3883 + key_start = c + 1;
3884 + break;
3885 + default:
3886 + break;
3887 + }
3888 + } else if (state == 'p') {
3889 + cmd.payload = c;
3890 + // break out of the loop, we don't check the payload
3891 + break;
3892 + }
3893 + ++c;
3894 + }
3895 +
3896 + // Set the action key ('a=') first because we need it to disambiguate
3897 + // some keys. Also set 'i=' and 'I=' for better error reporting.
3898 + for (unsigned i = 0; i < key_vals_count; ++i) {
3899 + if (key_vals[i].key_len == 1) {
3900 + char *start = key_vals[i].key_start;
3901 + if (*start == 'a' || *start == 'i' || *start == 'I') {
3902 + gr_set_keyvalue(&cmd, &key_vals[i]);
3903 + break;
3904 + }
3905 + }
3906 + }
3907 + // Set the rest of the keys.
3908 + for (unsigned i = 0; i < key_vals_count; ++i)
3909 + gr_set_keyvalue(&cmd, &key_vals[i]);
3910 +
3911 + if (!cmd.payload)
3912 + cmd.payload = buf + len;
3913 +
3914 + if (cmd.payload && cmd.payload[0])
3915 + GR_LOG(" payload size: %ld\n", strlen(cmd.payload));
3916 +
3917 + if (!graphics_command_result.error)
3918 + gr_handle_command(&cmd);
3919 +
3920 + if (graphics_debug_mode) {
3921 + fprintf(stderr, "Response: ");
3922 + for (const char *resp = graphics_command_result.response;
3923 + *resp != '\0'; ++resp) {
3924 + if (isprint(*resp))
3925 + fprintf(stderr, "%c", *resp);
3926 + else
3927 + fprintf(stderr, "(0x%x)", *resp);
3928 + }
3929 + fprintf(stderr, "\n");
3930 + }
3931 +
3932 + // Make sure that we suppress response if needed. Usually cmd.quiet is
3933 + // taken into account when creating the response, but it's not very
3934 + // reliable in the current implementation.
3935 + if (cmd.quiet) {
3936 + if (!graphics_command_result.error || cmd.quiet >= 2)
3937 + graphics_command_result.response[0] = '\0';
3938 + }
3939 +
3940 + return 1;
3941 +}
3942 +
3943 +////////////////////////////////////////////////////////////////////////////////
3944 +// base64 decoding part is basically copied from st.c
3945 +////////////////////////////////////////////////////////////////////////////////
3946 +
3947 +static const char gr_base64_digits[] = {
3948 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3949 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3950 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54,
3951 + 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, -1, 0, 0, 0, 0, 1, 2,
3952 + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
3953 + 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30,
3954 + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
3955 + 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3956 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3957 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3958 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3959 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3960 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3961 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
3962 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
3963 +
3964 +static char gr_base64_getc(const char **src) {
3965 + while (**src && !isprint(**src))
3966 + (*src)++;
3967 + return **src ? *((*src)++) : '='; /* emulate padding if string ends */
3968 +}
3969 +
3970 +char *gr_base64dec(const char *src, size_t *size) {
3971 + size_t in_len = strlen(src);
3972 + char *result, *dst;
3973 +
3974 + result = dst = malloc((in_len + 3) / 4 * 3 + 1);
3975 + while (*src) {
3976 + int a = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
3977 + int b = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
3978 + int c = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
3979 + int d = gr_base64_digits[(unsigned char)gr_base64_getc(&src)];
3980 +
3981 + if (a == -1 || b == -1)
3982 + break;
3983 +
3984 + *dst++ = (a << 2) | ((b & 0x30) >> 4);
3985 + if (c == -1)
3986 + break;
3987 + *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2);
3988 + if (d == -1)
3989 + break;
3990 + *dst++ = ((c & 0x03) << 6) | d;
3991 + }
3992 + *dst = '\0';
3993 + if (size) {
3994 + *size = dst - result;
3995 + }
3996 + return result;
3997 +}
3998 diff --git a/graphics.h b/graphics.h
3999 new file mode 100644
4000 index 0000000..2e75dea
4001 --- /dev/null
4002 +++ b/graphics.h
4003 @@ -0,0 +1,107 @@
4004 +
4005 +#include <stdint.h>
4006 +#include <sys/types.h>
4007 +#include <X11/Xlib.h>
4008 +
4009 +/// Initialize the graphics module.
4010 +void gr_init(Display *disp, Visual *vis, Colormap cm);
4011 +/// Deinitialize the graphics module.
4012 +void gr_deinit();
4013 +
4014 +/// Add an image rectangle to a list if rectangles to draw. This function may
4015 +/// actually draw some rectangles, or it may wait till more rectangles are
4016 +/// appended. Must be called between `gr_start_drawing` and `gr_finish_drawing`.
4017 +/// - `img_start_col..img_end_col` and `img_start_row..img_end_row` define the
4018 +/// part of the image to draw (row/col indices are zero-based, ends are
4019 +/// excluded).
4020 +/// - `x_col` and `y_row` are the coordinates of the top-left corner of the
4021 +/// image in the terminal grid.
4022 +/// - `x_pix` and `y_pix` are the same but in pixels.
4023 +/// - `reverse` indicates whether colors should be inverted.
4024 +void gr_append_imagerect(Drawable buf, uint32_t image_id, uint32_t placement_id,
4025 + int img_start_col, int img_end_col, int img_start_row,
4026 + int img_end_row, int x_col, int y_row, int x_pix,
4027 + int y_pix, int cw, int ch, int reverse);
4028 +/// Prepare for image drawing. `cw` and `ch` are dimensions of the cell.
4029 +void gr_start_drawing(Drawable buf, int cw, int ch);
4030 +/// Finish image drawing. This functions will draw all the rectangles left to
4031 +/// draw.
4032 +void gr_finish_drawing(Drawable buf);
4033 +/// Mark rows containing animations as dirty if it's time to redraw them. Must
4034 +/// be called right after `gr_start_drawing`.
4035 +void gr_mark_dirty_animations(int *dirty, int rows);
4036 +
4037 +/// Parse and execute a graphics command. `buf` must start with 'G' and contain
4038 +/// at least `len + 1` characters (including '\0'). Returns 1 on success.
4039 +/// Additional informations is returned through `graphics_command_result`.
4040 +int gr_parse_command(char *buf, size_t len);
4041 +
4042 +/// Executes `command` with the name of the file corresponding to `image_id` as
4043 +/// the argument. Executes xmessage with an error message on failure.
4044 +void gr_preview_image(uint32_t image_id, const char *command);
4045 +
4046 +/// Executes `<st> -e less <file>` where <file> is the name of a temporary file
4047 +/// containing the information about an image and placement, and <st> is
4048 +/// specified with `st_executable`.
4049 +void gr_show_image_info(uint32_t image_id, uint32_t placement_id,
4050 + uint32_t imgcol, uint32_t imgrow,
4051 + char is_classic_placeholder, int32_t diacritic_count,
4052 + char *st_executable);
4053 +
4054 +/// Dumps the internal state (images and placements) to stderr.
4055 +void gr_dump_state();
4056 +
4057 +/// Unloads images to reduce RAM usage.
4058 +void gr_unload_images_to_reduce_ram();
4059 +
4060 +/// Executes `callback` for each image cell. `callback` may return 1 to erase
4061 +/// the cell or 0 to keep it. This function is implemented in `st.c`.
4062 +void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id,
4063 + uint32_t placement_id, int col,
4064 + int row, char is_classic),
4065 + void *data);
4066 +
4067 +/// Marks all the rows containing the image with `image_id` as dirty.
4068 +void gr_schedule_image_redraw_by_id(uint32_t image_id);
4069 +
4070 +typedef enum {
4071 + GRAPHICS_DEBUG_NONE = 0,
4072 + GRAPHICS_DEBUG_LOG = 1,
4073 + GRAPHICS_DEBUG_LOG_AND_BOXES = 2,
4074 +} GraphicsDebugMode;
4075 +
4076 +/// Print additional information, draw bounding bounding boxes, etc.
4077 +extern GraphicsDebugMode graphics_debug_mode;
4078 +
4079 +/// Whether to display images or just draw bounding boxes.
4080 +extern char graphics_display_images;
4081 +
4082 +/// The time in milliseconds until the next redraw to update animations.
4083 +/// INT_MAX means no redraw is needed. Populated by `gr_finish_drawing`.
4084 +extern int graphics_next_redraw_delay;
4085 +
4086 +#define MAX_GRAPHICS_RESPONSE_LEN 256
4087 +
4088 +/// A structure representing the result of a graphics command.
4089 +typedef struct {
4090 + /// Indicates if the terminal needs to be redrawn.
4091 + char redraw;
4092 + /// The response of the command that should be sent back to the client
4093 + /// (may be empty if the quiet flag is set).
4094 + char response[MAX_GRAPHICS_RESPONSE_LEN];
4095 + /// Whether there was an error executing this command (not very useful,
4096 + /// the response must be sent back anyway).
4097 + char error;
4098 + /// Whether the terminal has to create a placeholder for a non-virtual
4099 + /// placement.
4100 + char create_placeholder;
4101 + /// The placeholder that needs to be created.
4102 + struct {
4103 + uint32_t rows, columns;
4104 + uint32_t image_id, placement_id;
4105 + char do_not_move_cursor;
4106 + } placeholder;
4107 +} GraphicsCommandResult;
4108 +
4109 +/// The result of a graphics command.
4110 +extern GraphicsCommandResult graphics_command_result;
4111 diff --git a/icat-mini.sh b/icat-mini.sh
4112 new file mode 100755
4113 index 0000000..0a8ebab
4114 --- /dev/null
4115 +++ b/icat-mini.sh
4116 @@ -0,0 +1,801 @@
4117 +#!/bin/sh
4118 +
4119 +# vim: shiftwidth=4
4120 +
4121 +script_name="$(basename "$0")"
4122 +
4123 +short_help="Usage: $script_name [OPTIONS] <image_file>
4124 +
4125 +This is a script to display images in the terminal using the kitty graphics
4126 +protocol with Unicode placeholders. It is very basic, please use something else
4127 +if you have alternatives.
4128 +
4129 +Options:
4130 + -h Show this help.
4131 + -s SCALE The scale of the image, may be floating point.
4132 + -c N, --cols N The number of columns.
4133 + -r N, --rows N The number of rows.
4134 + --max-cols N The maximum number of columns.
4135 + --max-rows N The maximum number of rows.
4136 + --cell-size WxH The cell size in pixels.
4137 + -m METHOD The uploading method, may be 'file', 'direct' or 'auto'.
4138 + --speed SPEED The multiplier for the animation speed (float).
4139 +"
4140 +
4141 +# Exit the script on keyboard interrupt
4142 +trap "echo 'icat-mini was interrupted' >&2; exit 1" INT
4143 +
4144 +cols=""
4145 +rows=""
4146 +file=""
4147 +tty="/dev/tty"
4148 +uploading_method="auto"
4149 +cell_size=""
4150 +scale=1
4151 +max_cols=""
4152 +max_rows=""
4153 +speed=""
4154 +
4155 +# Parse the command line.
4156 +while [ $# -gt 0 ]; do
4157 + case "$1" in
4158 + -c|--columns|--cols)
4159 + cols="$2"
4160 + shift 2
4161 + ;;
4162 + -r|--rows|-l|--lines)
4163 + rows="$2"
4164 + shift 2
4165 + ;;
4166 + -s|--scale)
4167 + scale="$2"
4168 + shift 2
4169 + ;;
4170 + -h|--help)
4171 + echo "$short_help"
4172 + exit 0
4173 + ;;
4174 + -m|--upload-method|--uploading-method)
4175 + uploading_method="$2"
4176 + shift 2
4177 + ;;
4178 + --cell-size)
4179 + cell_size="$2"
4180 + shift 2
4181 + ;;
4182 + --max-cols)
4183 + max_cols="$2"
4184 + shift 2
4185 + ;;
4186 + --max-rows)
4187 + max_rows="$2"
4188 + shift 2
4189 + ;;
4190 + --speed)
4191 + speed="$2"
4192 + shift 2
4193 + ;;
4194 + --)
4195 + file="$2"
4196 + shift 2
4197 + ;;
4198 + -*)
4199 + echo "Unknown option: $1" >&2
4200 + exit 1
4201 + ;;
4202 + *)
4203 + if [ -n "$file" ]; then
4204 + echo "Multiple image files are not supported: $file and $1" >&2
4205 + exit 1
4206 + fi
4207 + file="$1"
4208 + shift
4209 + ;;
4210 + esac
4211 +done
4212 +
4213 +file="$(realpath "$file")"
4214 +
4215 +#####################################################################
4216 +# Adjust the terminal state
4217 +#####################################################################
4218 +
4219 +stty_orig="$(stty -g < "$tty")"
4220 +stty -echo < "$tty"
4221 +# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some
4222 +# horrible issues otherwise.
4223 +stty susp undef < "$tty"
4224 +stty -icanon < "$tty"
4225 +
4226 +restore_echo() {
4227 + [ -n "$stty_orig" ] || return
4228 + stty $stty_orig < "$tty"
4229 +}
4230 +
4231 +trap restore_echo EXIT TERM
4232 +
4233 +#####################################################################
4234 +# Detect imagemagick
4235 +#####################################################################
4236 +
4237 +# If there is the 'magick' command, use it instead of separate 'convert' and
4238 +# 'identify' commands.
4239 +if command -v magick > /dev/null; then
4240 + identify="magick identify"
4241 + convert="magick"
4242 +else
4243 + identify="identify"
4244 + convert="convert"
4245 +fi
4246 +
4247 +#####################################################################
4248 +# Detect tmux
4249 +#####################################################################
4250 +
4251 +# Check if we are inside tmux.
4252 +inside_tmux=""
4253 +if [ -n "$TMUX" ]; then
4254 + case "$TERM" in
4255 + *tmux*|*screen*)
4256 + inside_tmux=1
4257 + ;;
4258 + esac
4259 +fi
4260 +
4261 +#####################################################################
4262 +# Compute the number of rows and columns
4263 +#####################################################################
4264 +
4265 +is_pos_int() {
4266 + if [ -z "$1" ]; then
4267 + return 1 # false
4268 + fi
4269 + if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then
4270 + if [ "$1" -gt 0 ]; then
4271 + return 0 # true
4272 + fi
4273 + fi
4274 + return 1 # false
4275 +}
4276 +
4277 +if [ -n "$cols" ] || [ -n "$rows" ]; then
4278 + if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then
4279 + echo "You can't specify both max-cols/rows and cols/rows" >&2
4280 + exit 1
4281 + fi
4282 +fi
4283 +
4284 +# Get the max number of cols and rows.
4285 +[ -n "$max_cols" ] || max_cols="$(tput cols)"
4286 +[ -n "$max_rows" ] || max_rows="$(tput lines)"
4287 +if [ "$max_rows" -gt 255 ]; then
4288 + max_rows=255
4289 +fi
4290 +
4291 +python_ioctl_command="import array, fcntl, termios
4292 +buf = array.array('H', [0, 0, 0, 0])
4293 +fcntl.ioctl(0, termios.TIOCGWINSZ, buf)
4294 +print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))"
4295 +
4296 +# Get the cell size in pixels if either cols or rows are not specified.
4297 +if [ -z "$cols" ] || [ -z "$rows" ]; then
4298 + cell_width=""
4299 + cell_height=""
4300 + # If the cell size is specified, use it.
4301 + if [ -n "$cell_size" ]; then
4302 + cell_width="${cell_size%x*}"
4303 + cell_height="${cell_size#*x}"
4304 + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
4305 + echo "Invalid cell size: $cell_size" >&2
4306 + exit 1
4307 + fi
4308 + fi
4309 + # Otherwise try to use TIOCGWINSZ ioctl via python.
4310 + if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
4311 + cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)"
4312 + cell_width="${cell_size_ioctl% *}"
4313 + cell_height="${cell_size_ioctl#* }"
4314 + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
4315 + cell_width=""
4316 + cell_height=""
4317 + fi
4318 + fi
4319 + # If it didn't work, try to use csi XTWINOPS.
4320 + if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
4321 + if [ -n "$inside_tmux" ]; then
4322 + printf '\ePtmux;\e\e[16t\e\\' >> "$tty"
4323 + else
4324 + printf '\e[16t' >> "$tty"
4325 + fi
4326 + # The expected response will look like ^[[6;<height>;<width>t
4327 + term_response=""
4328 + while true; do
4329 + char=$(dd bs=1 count=1 <"$tty" 2>/dev/null)
4330 + if [ "$char" = "t" ]; then
4331 + break
4332 + fi
4333 + term_response="$term_response$char"
4334 + done
4335 + cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)"
4336 + cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)"
4337 + if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
4338 + cell_width=8
4339 + cell_height=16
4340 + fi
4341 + fi
4342 +fi
4343 +
4344 +# Compute a formula with bc and round to the nearest integer.
4345 +bc_round() {
4346 + LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)"
4347 +}
4348 +
4349 +# Compute the number of rows and columns of the image.
4350 +if [ -z "$cols" ] || [ -z "$rows" ]; then
4351 + # Get the size of the image and its resolution. If it's an animation, use
4352 + # the first frame.
4353 + format_output="$($identify -format '%w %h\n' "$file" | head -1)"
4354 + img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)"
4355 + img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)"
4356 + if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then
4357 + echo "Couldn't get image size from identify: $format_output" >&2
4358 + echo >&2
4359 + exit 1
4360 + fi
4361 + opt_cols_expr="(${scale}*${img_width}/${cell_width})"
4362 + opt_rows_expr="(${scale}*${img_height}/${cell_height})"
4363 + if [ -z "$cols" ] && [ -z "$rows" ]; then
4364 + # If columns and rows are not specified, compute the optimal values
4365 + # using the information about rows and columns per inch.
4366 + cols="$(bc_round "$opt_cols_expr")"
4367 + rows="$(bc_round "$opt_rows_expr")"
4368 + # Make sure that automatically computed rows and columns are within some
4369 + # sane limits
4370 + if [ "$cols" -gt "$max_cols" ]; then
4371 + rows="$(bc_round "$rows * $max_cols / $cols")"
4372 + cols="$max_cols"
4373 + fi
4374 + if [ "$rows" -gt "$max_rows" ]; then
4375 + cols="$(bc_round "$cols * $max_rows / $rows")"
4376 + rows="$max_rows"
4377 + fi
4378 + elif [ -z "$cols" ]; then
4379 + # If only one dimension is specified, compute the other one to match the
4380 + # aspect ratio as close as possible.
4381 + cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")"
4382 + elif [ -z "$rows" ]; then
4383 + rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")"
4384 + fi
4385 +
4386 + if [ "$cols" -lt 1 ]; then
4387 + cols=1
4388 + fi
4389 + if [ "$rows" -lt 1 ]; then
4390 + rows=1
4391 + fi
4392 +fi
4393 +
4394 +#####################################################################
4395 +# Generate an image id
4396 +#####################################################################
4397 +
4398 +image_id=""
4399 +while [ -z "$image_id" ]; do
4400 + image_id="$(shuf -i 16777217-4294967295 -n 1)"
4401 + # Check that the id requires 24-bit fg colors.
4402 + if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then
4403 + image_id=""
4404 + fi
4405 +done
4406 +
4407 +#####################################################################
4408 +# Uploading the image
4409 +#####################################################################
4410 +
4411 +# Choose the uploading method
4412 +if [ "$uploading_method" = "auto" ]; then
4413 + if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then
4414 + uploading_method="direct"
4415 + else
4416 + uploading_method="file"
4417 + fi
4418 +fi
4419 +
4420 +# Functions to emit the start and the end of a graphics command.
4421 +if [ -n "$inside_tmux" ]; then
4422 + # If we are in tmux we have to wrap the command in Ptmux.
4423 + graphics_command_start='\ePtmux;\e\e_G'
4424 + graphics_command_end='\e\e\\\e\\'
4425 +else
4426 + graphics_command_start='\e_G'
4427 + graphics_command_end='\e\\'
4428 +fi
4429 +
4430 +start_gr_command() {
4431 + printf "$graphics_command_start" >> "$tty"
4432 +}
4433 +end_gr_command() {
4434 + printf "$graphics_command_end" >> "$tty"
4435 +}
4436 +
4437 +# Send a graphics command with the correct start and end
4438 +gr_command() {
4439 + start_gr_command
4440 + printf '%s' "$1" >> "$tty"
4441 + end_gr_command
4442 +}
4443 +
4444 +# Send an uploading command. Usage: gr_upload <action> <command> <file>
4445 +# Where <action> is a part of command that specifies the action, it will be
4446 +# repeated for every chunk (if the method is direct), and <command> is the rest
4447 +# of the command that specifies the image parameters. <action> and <command>
4448 +# must not include the transmission method or ';'.
4449 +# Example:
4450 +# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
4451 +gr_upload() {
4452 + arg_action="$1"
4453 + arg_command="$2"
4454 + arg_file="$3"
4455 + if [ "$uploading_method" = "file" ]; then
4456 + # base64-encode the filename
4457 + encoded_filename=$(printf '%s' "$arg_file" | base64 -w0)
4458 + gr_command "${arg_action},${arg_command},t=f;${encoded_filename}"
4459 + fi
4460 + if [ "$uploading_method" = "direct" ]; then
4461 + # Create a temporary directory to store the chunked image.
4462 + chunkdir="$(mktemp -d)"
4463 + if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then
4464 + echo "Can't create a temp dir" >&2
4465 + exit 1
4466 + fi
4467 + # base64-encode the file and split it into chunks. The size of each
4468 + # graphics command shouldn't be more than 4096, so we set the size of an
4469 + # encoded chunk to be 3968, slightly less than that.
4470 + chunk_size=3968
4471 + cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_"
4472 +
4473 + # Issue a command indicating that we want to start data transmission for
4474 + # a new image.
4475 + gr_command "${arg_action},${arg_command},t=d,m=1"
4476 +
4477 + # Transmit chunks.
4478 + for chunk in "$chunkdir/chunk_"*; do
4479 + start_gr_command
4480 + printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty"
4481 + cat "$chunk" >> "$tty"
4482 + end_gr_command
4483 + rm "$chunk"
4484 + done
4485 +
4486 + # Tell the terminal that we are done.
4487 + gr_command "${arg_action},i=$image_id,m=0"
4488 +
4489 + # Remove the temporary directory.
4490 + rmdir "$chunkdir"
4491 + fi
4492 +}
4493 +
4494 +delayed_frame_dir_cleanup() {
4495 + arg_frame_dir="$1"
4496 + sleep 2
4497 + if [ -n "$arg_frame_dir" ]; then
4498 + for frame in "$arg_frame_dir"/frame_*.png; do
4499 + rm "$frame"
4500 + done
4501 + rmdir "$arg_frame_dir"
4502 + fi
4503 +}
4504 +
4505 +upload_image_and_print_placeholder() {
4506 + # Check if the file is an animation.
4507 + frame_count=$($identify -format '%n\n' "$file" | head -n 1)
4508 + if [ "$frame_count" -gt 1 ]; then
4509 + # The file is an animation, decompose into frames and upload each frame.
4510 + frame_dir="$(mktemp -d)"
4511 + frame_dir="$HOME/temp/frames${frame_dir}"
4512 + mkdir -p "$frame_dir"
4513 + if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then
4514 + echo "Can't create a temp dir for frames" >&2
4515 + exit 1
4516 + fi
4517 +
4518 + # Decompose the animation into separate frames.
4519 + $convert "$file" -coalesce "$frame_dir/frame_%06d.png"
4520 +
4521 + # Get all frame delays at once, in centiseconds, as a space-separated
4522 + # string.
4523 + delays=$($identify -format "%T " "$file")
4524 +
4525 + frame_number=1
4526 + for frame in "$frame_dir"/frame_*.png; do
4527 + # Read the delay for the current frame and convert it from
4528 + # centiseconds to milliseconds.
4529 + delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number")
4530 + delay=$((delay * 10))
4531 + # If the delay is 0, set it to 100ms.
4532 + if [ "$delay" -eq 0 ]; then
4533 + delay=100
4534 + fi
4535 +
4536 + if [ -n "$speed" ]; then
4537 + delay=$(bc_round "$delay / $speed")
4538 + fi
4539 +
4540 + if [ "$frame_number" -eq 1 ]; then
4541 + # Upload the first frame with a=T
4542 + gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame"
4543 + # Set the delay for the first frame and also play the animation
4544 + # in loading mode (s=2).
4545 + gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}"
4546 + # Print the placeholder after the first frame to reduce the wait
4547 + # time.
4548 + print_placeholder
4549 + else
4550 + # Upload subsequent frames with a=f
4551 + gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame"
4552 + fi
4553 +
4554 + frame_number=$((frame_number + 1))
4555 + done
4556 +
4557 + # Play the animation in loop mode (s=3).
4558 + gr_command "a=a,v=1,s=3,i=${image_id}"
4559 +
4560 + # Remove the temporary directory, but do it in the background with a
4561 + # delay to avoid removing files before they are loaded by the terminal.
4562 + delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null &
4563 + else
4564 + # The file is not an animation, upload it directly
4565 + gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
4566 + # Print the placeholder
4567 + print_placeholder
4568 + fi
4569 +}
4570 +
4571 +#####################################################################
4572 +# Printing the image placeholder
4573 +#####################################################################
4574 +
4575 +print_placeholder() {
4576 + # Each line starts with the escape sequence to set the foreground color to
4577 + # the image id.
4578 + blue="$(expr "$image_id" % 256 )"
4579 + green="$(expr \( "$image_id" / 256 \) % 256 )"
4580 + red="$(expr \( "$image_id" / 65536 \) % 256 )"
4581 + line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")"
4582 + line_end="$(printf "\e[39;m")"
4583 +
4584 + id4th="$(expr \( "$image_id" / 16777216 \) % 256 )"
4585 + eval "id_diacritic=\$d${id4th}"
4586 +
4587 + # Reset the brush state, mostly to reset the underline color.
4588 + printf "\e[0m"
4589 +
4590 + # Fill the output with characters representing the image
4591 + for y in $(seq 0 "$(expr "$rows" - 1)"); do
4592 + eval "row_diacritic=\$d${y}"
4593 + printf '%s' "$line_start"
4594 + for x in $(seq 0 "$(expr "$cols" - 1)"); do
4595 + eval "col_diacritic=\$d${x}"
4596 + # Note that when $x is out of bounds, the column diacritic will
4597 + # be empty, meaning that the column should be guessed by the
4598 + # terminal.
4599 + if [ "$x" -ge "$num_diacritics" ]; then
4600 + printf '%s' "${placeholder}${row_diacritic}"
4601 + else
4602 + printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}"
4603 + fi
4604 + done
4605 + printf '%s\n' "$line_end"
4606 + done
4607 +
4608 + printf "\e[0m"
4609 +}
4610 +
4611 +d0="̅"
4612 +d1="̍"
4613 +d2="̎"
4614 +d3="̐"
4615 +d4="̒"
4616 +d5="̽"
4617 +d6="̾"
4618 +d7="̿"
4619 +d8="͆"
4620 +d9="͊"
4621 +d10="͋"
4622 +d11="͌"
4623 +d12="͐"
4624 +d13="͑"
4625 +d14="͒"
4626 +d15="͗"
4627 +d16="͛"
4628 +d17="ͣ"
4629 +d18="ͤ"
4630 +d19="ͥ"
4631 +d20="ͦ"
4632 +d21="ͧ"
4633 +d22="ͨ"
4634 +d23="ͩ"
4635 +d24="ͪ"
4636 +d25="ͫ"
4637 +d26="ͬ"
4638 +d27="ͭ"
4639 +d28="ͮ"
4640 +d29="ͯ"
4641 +d30="҃"
4642 +d31="҄"
4643 +d32="҅"
4644 +d33="҆"
4645 +d34="҇"
4646 +d35="֒"
4647 +d36="֓"
4648 +d37="֔"
4649 +d38="֕"
4650 +d39="֗"
4651 +d40="֘"
4652 +d41="֙"
4653 +d42="֜"
4654 +d43="֝"
4655 +d44="֞"
4656 +d45="֟"
4657 +d46="֠"
4658 +d47="֡"
4659 +d48="֨"
4660 +d49="֩"
4661 +d50="֫"
4662 +d51="֬"
4663 +d52="֯"
4664 +d53="ׄ"
4665 +d54="ؐ"
4666 +d55="ؑ"
4667 +d56="ؒ"
4668 +d57="ؓ"
4669 +d58="ؔ"
4670 +d59="ؕ"
4671 +d60="ؖ"
4672 +d61="ؗ"
4673 +d62="ٗ"
4674 +d63="٘"
4675 +d64="ٙ"
4676 +d65="ٚ"
4677 +d66="ٛ"
4678 +d67="ٝ"
4679 +d68="ٞ"
4680 +d69="ۖ"
4681 +d70="ۗ"
4682 +d71="ۘ"
4683 +d72="ۙ"
4684 +d73="ۚ"
4685 +d74="ۛ"
4686 +d75="ۜ"
4687 +d76="۟"
4688 +d77="۠"
4689 +d78="ۡ"
4690 +d79="ۢ"
4691 +d80="ۤ"
4692 +d81="ۧ"
4693 +d82="ۨ"
4694 +d83="۫"
4695 +d84="۬"
4696 +d85="ܰ"
4697 +d86="ܲ"
4698 +d87="ܳ"
4699 +d88="ܵ"
4700 +d89="ܶ"
4701 +d90="ܺ"
4702 +d91="ܽ"
4703 +d92="ܿ"
4704 +d93="݀"
4705 +d94="݁"
4706 +d95="݃"
4707 +d96="݅"
4708 +d97="݇"
4709 +d98="݉"
4710 +d99="݊"
4711 +d100="߫"
4712 +d101="߬"
4713 +d102="߭"
4714 +d103="߮"
4715 +d104="߯"
4716 +d105="߰"
4717 +d106="߱"
4718 +d107="߳"
4719 +d108="ࠖ"
4720 +d109="ࠗ"
4721 +d110="࠘"
4722 +d111="࠙"
4723 +d112="ࠛ"
4724 +d113="ࠜ"
4725 +d114="ࠝ"
4726 +d115="ࠞ"
4727 +d116="ࠟ"
4728 +d117="ࠠ"
4729 +d118="ࠡ"
4730 +d119="ࠢ"
4731 +d120="ࠣ"
4732 +d121="ࠥ"
4733 +d122="ࠦ"
4734 +d123="ࠧ"
4735 +d124="ࠩ"
4736 +d125="ࠪ"
4737 +d126="ࠫ"
4738 +d127="ࠬ"
4739 +d128="࠭"
4740 +d129="॑"
4741 +d130="॓"
4742 +d131="॔"
4743 +d132="ྂ"
4744 +d133="ྃ"
4745 +d134="྆"
4746 +d135="྇"
4747 +d136="፝"
4748 +d137="፞"
4749 +d138="፟"
4750 +d139="៝"
4751 +d140="᤺"
4752 +d141="ᨗ"
4753 +d142="᩵"
4754 +d143="᩶"
4755 +d144="᩷"
4756 +d145="᩸"
4757 +d146="᩹"
4758 +d147="᩺"
4759 +d148="᩻"
4760 +d149="᩼"
4761 +d150="᭫"
4762 +d151="᭭"
4763 +d152="᭮"
4764 +d153="᭯"
4765 +d154="᭰"
4766 +d155="᭱"
4767 +d156="᭲"
4768 +d157="᭳"
4769 +d158="᳐"
4770 +d159="᳑"
4771 +d160="᳒"
4772 +d161="᳚"
4773 +d162="᳛"
4774 +d163="᳠"
4775 +d164="᷀"
4776 +d165="᷁"
4777 +d166="᷃"
4778 +d167="᷄"
4779 +d168="᷅"
4780 +d169="᷆"
4781 +d170="᷇"
4782 +d171="᷈"
4783 +d172="᷉"
4784 +d173="᷋"
4785 +d174="᷌"
4786 +d175="᷑"
4787 +d176="᷒"
4788 +d177="ᷓ"
4789 +d178="ᷔ"
4790 +d179="ᷕ"
4791 +d180="ᷖ"
4792 +d181="ᷗ"
4793 +d182="ᷘ"
4794 +d183="ᷙ"
4795 +d184="ᷚ"
4796 +d185="ᷛ"
4797 +d186="ᷜ"
4798 +d187="ᷝ"
4799 +d188="ᷞ"
4800 +d189="ᷟ"
4801 +d190="ᷠ"
4802 +d191="ᷡ"
4803 +d192="ᷢ"
4804 +d193="ᷣ"
4805 +d194="ᷤ"
4806 +d195="ᷥ"
4807 +d196="ᷦ"
4808 +d197="᷾"
4809 +d198="⃐"
4810 +d199="⃑"
4811 +d200="⃔"
4812 +d201="⃕"
4813 +d202="⃖"
4814 +d203="⃗"
4815 +d204="⃛"
4816 +d205="⃜"
4817 +d206="⃡"
4818 +d207="⃧"
4819 +d208="⃩"
4820 +d209="⃰"
4821 +d210="⳯"
4822 +d211="⳰"
4823 +d212="⳱"
4824 +d213="ⷠ"
4825 +d214="ⷡ"
4826 +d215="ⷢ"
4827 +d216="ⷣ"
4828 +d217="ⷤ"
4829 +d218="ⷥ"
4830 +d219="ⷦ"
4831 +d220="ⷧ"
4832 +d221="ⷨ"
4833 +d222="ⷩ"
4834 +d223="ⷪ"
4835 +d224="ⷫ"
4836 +d225="ⷬ"
4837 +d226="ⷭ"
4838 +d227="ⷮ"
4839 +d228="ⷯ"
4840 +d229="ⷰ"
4841 +d230="ⷱ"
4842 +d231="ⷲ"
4843 +d232="ⷳ"
4844 +d233="ⷴ"
4845 +d234="ⷵ"
4846 +d235="ⷶ"
4847 +d236="ⷷ"
4848 +d237="ⷸ"
4849 +d238="ⷹ"
4850 +d239="ⷺ"
4851 +d240="ⷻ"
4852 +d241="ⷼ"
4853 +d242="ⷽ"
4854 +d243="ⷾ"
4855 +d244="ⷿ"
4856 +d245="꙯"
4857 +d246="꙼"
4858 +d247="꙽"
4859 +d248="꛰"
4860 +d249="꛱"
4861 +d250="꣠"
4862 +d251="꣡"
4863 +d252="꣢"
4864 +d253="꣣"
4865 +d254="꣤"
4866 +d255="꣥"
4867 +d256="꣦"
4868 +d257="꣧"
4869 +d258="꣨"
4870 +d259="꣩"
4871 +d260="꣪"
4872 +d261="꣫"
4873 +d262="꣬"
4874 +d263="꣭"
4875 +d264="꣮"
4876 +d265="꣯"
4877 +d266="꣰"
4878 +d267="꣱"
4879 +d268="ꪰ"
4880 +d269="ꪲ"
4881 +d270="ꪳ"
4882 +d271="ꪷ"
4883 +d272="ꪸ"
4884 +d273="ꪾ"
4885 +d274="꪿"
4886 +d275="꫁"
4887 +d276="︠"
4888 +d277="︡"
4889 +d278="︢"
4890 +d279="︣"
4891 +d280="︤"
4892 +d281="︥"
4893 +d282="︦"
4894 +d283="𐨏"
4895 +d284="𐨸"
4896 +d285="𝆅"
4897 +d286="𝆆"
4898 +d287="𝆇"
4899 +d288="𝆈"
4900 +d289="𝆉"
4901 +d290="𝆪"
4902 +d291="𝆫"
4903 +d292="𝆬"
4904 +d293="𝆭"
4905 +d294="𝉂"
4906 +d295="𝉃"
4907 +d296="𝉄"
4908 +
4909 +num_diacritics="297"
4910 +
4911 +placeholder=""
4912 +
4913 +#####################################################################
4914 +# Upload the image and print the placeholder
4915 +#####################################################################
4916 +
4917 +upload_image_and_print_placeholder
4918 diff --git a/khash.h b/khash.h
4919 new file mode 100644
4920 index 0000000..f75f347
4921 --- /dev/null
4922 +++ b/khash.h
4923 @@ -0,0 +1,627 @@
4924 +/* The MIT License
4925 +
4926 + Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk>
4927 +
4928 + Permission is hereby granted, free of charge, to any person obtaining
4929 + a copy of this software and associated documentation files (the
4930 + "Software"), to deal in the Software without restriction, including
4931 + without limitation the rights to use, copy, modify, merge, publish,
4932 + distribute, sublicense, and/or sell copies of the Software, and to
4933 + permit persons to whom the Software is furnished to do so, subject to
4934 + the following conditions:
4935 +
4936 + The above copyright notice and this permission notice shall be
4937 + included in all copies or substantial portions of the Software.
4938 +
4939 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
4940 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
4941 + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
4942 + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
4943 + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
4944 + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
4945 + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4946 + SOFTWARE.
4947 +*/
4948 +
4949 +/*
4950 + An example:
4951 +
4952 +#include "khash.h"
4953 +KHASH_MAP_INIT_INT(32, char)
4954 +int main() {
4955 + int ret, is_missing;
4956 + khiter_t k;
4957 + khash_t(32) *h = kh_init(32);
4958 + k = kh_put(32, h, 5, &ret);
4959 + kh_value(h, k) = 10;
4960 + k = kh_get(32, h, 10);
4961 + is_missing = (k == kh_end(h));
4962 + k = kh_get(32, h, 5);
4963 + kh_del(32, h, k);
4964 + for (k = kh_begin(h); k != kh_end(h); ++k)
4965 + if (kh_exist(h, k)) kh_value(h, k) = 1;
4966 + kh_destroy(32, h);
4967 + return 0;
4968 +}
4969 +*/
4970 +
4971 +/*
4972 + 2013-05-02 (0.2.8):
4973 +
4974 + * Use quadratic probing. When the capacity is power of 2, stepping function
4975 + i*(i+1)/2 guarantees to traverse each bucket. It is better than double
4976 + hashing on cache performance and is more robust than linear probing.
4977 +
4978 + In theory, double hashing should be more robust than quadratic probing.
4979 + However, my implementation is probably not for large hash tables, because
4980 + the second hash function is closely tied to the first hash function,
4981 + which reduce the effectiveness of double hashing.
4982 +
4983 + Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php
4984 +
4985 + 2011-12-29 (0.2.7):
4986 +
4987 + * Minor code clean up; no actual effect.
4988 +
4989 + 2011-09-16 (0.2.6):
4990 +
4991 + * The capacity is a power of 2. This seems to dramatically improve the
4992 + speed for simple keys. Thank Zilong Tan for the suggestion. Reference:
4993 +
4994 + - http://code.google.com/p/ulib/
4995 + - http://nothings.org/computer/judy/
4996 +
4997 + * Allow to optionally use linear probing which usually has better
4998 + performance for random input. Double hashing is still the default as it
4999 + is more robust to certain non-random input.
5000 +
5001 + * Added Wang's integer hash function (not used by default). This hash
5002 + function is more robust to certain non-random input.
5003 +
5004 + 2011-02-14 (0.2.5):
5005 +
5006 + * Allow to declare global functions.
5007 +
5008 + 2009-09-26 (0.2.4):
5009 +
5010 + * Improve portability
5011 +
5012 + 2008-09-19 (0.2.3):
5013 +
5014 + * Corrected the example
5015 + * Improved interfaces
5016 +
5017 + 2008-09-11 (0.2.2):
5018 +
5019 + * Improved speed a little in kh_put()
5020 +
5021 + 2008-09-10 (0.2.1):
5022 +
5023 + * Added kh_clear()
5024 + * Fixed a compiling error
5025 +
5026 + 2008-09-02 (0.2.0):
5027 +
5028 + * Changed to token concatenation which increases flexibility.
5029 +
5030 + 2008-08-31 (0.1.2):
5031 +
5032 + * Fixed a bug in kh_get(), which has not been tested previously.
5033 +
5034 + 2008-08-31 (0.1.1):
5035 +
5036 + * Added destructor
5037 +*/
5038 +
5039 +
5040 +#ifndef __AC_KHASH_H
5041 +#define __AC_KHASH_H
5042 +
5043 +/*!
5044 + @header
5045 +
5046 + Generic hash table library.
5047 + */
5048 +
5049 +#define AC_VERSION_KHASH_H "0.2.8"
5050 +
5051 +#include <stdlib.h>
5052 +#include <string.h>
5053 +#include <limits.h>
5054 +
5055 +/* compiler specific configuration */
5056 +
5057 +#if UINT_MAX == 0xffffffffu
5058 +typedef unsigned int khint32_t;
5059 +#elif ULONG_MAX == 0xffffffffu
5060 +typedef unsigned long khint32_t;
5061 +#endif
5062 +
5063 +#if ULONG_MAX == ULLONG_MAX
5064 +typedef unsigned long khint64_t;
5065 +#else
5066 +typedef unsigned long long khint64_t;
5067 +#endif
5068 +
5069 +#ifndef kh_inline
5070 +#ifdef _MSC_VER
5071 +#define kh_inline __inline
5072 +#else
5073 +#define kh_inline inline
5074 +#endif
5075 +#endif /* kh_inline */
5076 +
5077 +#ifndef klib_unused
5078 +#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3)
5079 +#define klib_unused __attribute__ ((__unused__))
5080 +#else
5081 +#define klib_unused
5082 +#endif
5083 +#endif /* klib_unused */
5084 +
5085 +typedef khint32_t khint_t;
5086 +typedef khint_t khiter_t;
5087 +
5088 +#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2)
5089 +#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1)
5090 +#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3)
5091 +#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1)))
5092 +#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1)))
5093 +#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1)))
5094 +#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1))
5095 +
5096 +#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4)
5097 +
5098 +#ifndef kroundup32
5099 +#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
5100 +#endif
5101 +
5102 +#ifndef kcalloc
5103 +#define kcalloc(N,Z) calloc(N,Z)
5104 +#endif
5105 +#ifndef kmalloc
5106 +#define kmalloc(Z) malloc(Z)
5107 +#endif
5108 +#ifndef krealloc
5109 +#define krealloc(P,Z) realloc(P,Z)
5110 +#endif
5111 +#ifndef kfree
5112 +#define kfree(P) free(P)
5113 +#endif
5114 +
5115 +static const double __ac_HASH_UPPER = 0.77;
5116 +
5117 +#define __KHASH_TYPE(name, khkey_t, khval_t) \
5118 + typedef struct kh_##name##_s { \
5119 + khint_t n_buckets, size, n_occupied, upper_bound; \
5120 + khint32_t *flags; \
5121 + khkey_t *keys; \
5122 + khval_t *vals; \
5123 + } kh_##name##_t;
5124 +
5125 +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t) \
5126 + extern kh_##name##_t *kh_init_##name(void); \
5127 + extern void kh_destroy_##name(kh_##name##_t *h); \
5128 + extern void kh_clear_##name(kh_##name##_t *h); \
5129 + extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key); \
5130 + extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \
5131 + extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \
5132 + extern void kh_del_##name(kh_##name##_t *h, khint_t x);
5133 +
5134 +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
5135 + SCOPE kh_##name##_t *kh_init_##name(void) { \
5136 + return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t)); \
5137 + } \
5138 + SCOPE void kh_destroy_##name(kh_##name##_t *h) \
5139 + { \
5140 + if (h) { \
5141 + kfree((void *)h->keys); kfree(h->flags); \
5142 + kfree((void *)h->vals); \
5143 + kfree(h); \
5144 + } \
5145 + } \
5146 + SCOPE void kh_clear_##name(kh_##name##_t *h) \
5147 + { \
5148 + if (h && h->flags) { \
5149 + memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \
5150 + h->size = h->n_occupied = 0; \
5151 + } \
5152 + } \
5153 + SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key) \
5154 + { \
5155 + if (h->n_buckets) { \
5156 + khint_t k, i, last, mask, step = 0; \
5157 + mask = h->n_buckets - 1; \
5158 + k = __hash_func(key); i = k & mask; \
5159 + last = i; \
5160 + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
5161 + i = (i + (++step)) & mask; \
5162 + if (i == last) return h->n_buckets; \
5163 + } \
5164 + return __ac_iseither(h->flags, i)? h->n_buckets : i; \
5165 + } else return 0; \
5166 + } \
5167 + SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \
5168 + { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \
5169 + khint32_t *new_flags = 0; \
5170 + khint_t j = 1; \
5171 + { \
5172 + kroundup32(new_n_buckets); \
5173 + if (new_n_buckets < 4) new_n_buckets = 4; \
5174 + if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0; /* requested size is too small */ \
5175 + else { /* hash table size to be changed (shrink or expand); rehash */ \
5176 + new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
5177 + if (!new_flags) return -1; \
5178 + memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
5179 + if (h->n_buckets < new_n_buckets) { /* expand */ \
5180 + khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
5181 + if (!new_keys) { kfree(new_flags); return -1; } \
5182 + h->keys = new_keys; \
5183 + if (kh_is_map) { \
5184 + khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
5185 + if (!new_vals) { kfree(new_flags); return -1; } \
5186 + h->vals = new_vals; \
5187 + } \
5188 + } /* otherwise shrink */ \
5189 + } \
5190 + } \
5191 + if (j) { /* rehashing is needed */ \
5192 + for (j = 0; j != h->n_buckets; ++j) { \
5193 + if (__ac_iseither(h->flags, j) == 0) { \
5194 + khkey_t key = h->keys[j]; \
5195 + khval_t val; \
5196 + khint_t new_mask; \
5197 + new_mask = new_n_buckets - 1; \
5198 + if (kh_is_map) val = h->vals[j]; \
5199 + __ac_set_isdel_true(h->flags, j); \
5200 + while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \
5201 + khint_t k, i, step = 0; \
5202 + k = __hash_func(key); \
5203 + i = k & new_mask; \
5204 + while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \
5205 + __ac_set_isempty_false(new_flags, i); \
5206 + if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \
5207 + { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \
5208 + if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \
5209 + __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \
5210 + } else { /* write the element and jump out of the loop */ \
5211 + h->keys[i] = key; \
5212 + if (kh_is_map) h->vals[i] = val; \
5213 + break; \
5214 + } \
5215 + } \
5216 + } \
5217 + } \
5218 + if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \
5219 + h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
5220 + if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
5221 + } \
5222 + kfree(h->flags); /* free the working space */ \
5223 + h->flags = new_flags; \
5224 + h->n_buckets = new_n_buckets; \
5225 + h->n_occupied = h->size; \
5226 + h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \
5227 + } \
5228 + return 0; \
5229 + } \
5230 + SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \
5231 + { \
5232 + khint_t x; \
5233 + if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \
5234 + if (h->n_buckets > (h->size<<1)) { \
5235 + if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \
5236 + *ret = -1; return h->n_buckets; \
5237 + } \
5238 + } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \
5239 + *ret = -1; return h->n_buckets; \
5240 + } \
5241 + } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \
5242 + { \
5243 + khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \
5244 + x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \
5245 + if (__ac_isempty(h->flags, i)) x = i; /* for speed up */ \
5246 + else { \
5247 + last = i; \
5248 + while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
5249 + if (__ac_isdel(h->flags, i)) site = i; \
5250 + i = (i + (++step)) & mask; \
5251 + if (i == last) { x = site; break; } \
5252 + } \
5253 + if (x == h->n_buckets) { \
5254 + if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
5255 + else x = i; \
5256 + } \
5257 + } \
5258 + } \
5259 + if (__ac_isempty(h->flags, x)) { /* not present at all */ \
5260 + h->keys[x] = key; \
5261 + __ac_set_isboth_false(h->flags, x); \
5262 + ++h->size; ++h->n_occupied; \
5263 + *ret = 1; \
5264 + } else if (__ac_isdel(h->flags, x)) { /* deleted */ \
5265 + h->keys[x] = key; \
5266 + __ac_set_isboth_false(h->flags, x); \
5267 + ++h->size; \
5268 + *ret = 2; \
5269 + } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \
5270 + return x; \
5271 + } \
5272 + SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x) \
5273 + { \
5274 + if (x != h->n_buckets && !__ac_iseither(h->flags, x)) { \
5275 + __ac_set_isdel_true(h->flags, x); \
5276 + --h->size; \
5277 + } \
5278 + }
5279 +
5280 +#define KHASH_DECLARE(name, khkey_t, khval_t) \
5281 + __KHASH_TYPE(name, khkey_t, khval_t) \
5282 + __KHASH_PROTOTYPES(name, khkey_t, khval_t)
5283 +
5284 +#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
5285 + __KHASH_TYPE(name, khkey_t, khval_t) \
5286 + __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
5287 +
5288 +#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
5289 + KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
5290 +
5291 +/* --- BEGIN OF HASH FUNCTIONS --- */
5292 +
5293 +/*! @function
5294 + @abstract Integer hash function
5295 + @param key The integer [khint32_t]
5296 + @return The hash value [khint_t]
5297 + */
5298 +#define kh_int_hash_func(key) (khint32_t)(key)
5299 +/*! @function
5300 + @abstract Integer comparison function
5301 + */
5302 +#define kh_int_hash_equal(a, b) ((a) == (b))
5303 +/*! @function
5304 + @abstract 64-bit integer hash function
5305 + @param key The integer [khint64_t]
5306 + @return The hash value [khint_t]
5307 + */
5308 +#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11)
5309 +/*! @function
5310 + @abstract 64-bit integer comparison function
5311 + */
5312 +#define kh_int64_hash_equal(a, b) ((a) == (b))
5313 +/*! @function
5314 + @abstract const char* hash function
5315 + @param s Pointer to a null terminated string
5316 + @return The hash value
5317 + */
5318 +static kh_inline khint_t __ac_X31_hash_string(const char *s)
5319 +{
5320 + khint_t h = (khint_t)*s;
5321 + if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s;
5322 + return h;
5323 +}
5324 +/*! @function
5325 + @abstract Another interface to const char* hash function
5326 + @param key Pointer to a null terminated string [const char*]
5327 + @return The hash value [khint_t]
5328 + */
5329 +#define kh_str_hash_func(key) __ac_X31_hash_string(key)
5330 +/*! @function
5331 + @abstract Const char* comparison function
5332 + */
5333 +#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0)
5334 +
5335 +static kh_inline khint_t __ac_Wang_hash(khint_t key)
5336 +{
5337 + key += ~(key << 15);
5338 + key ^= (key >> 10);
5339 + key += (key << 3);
5340 + key ^= (key >> 6);
5341 + key += ~(key << 11);
5342 + key ^= (key >> 16);
5343 + return key;
5344 +}
5345 +#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key)
5346 +
5347 +/* --- END OF HASH FUNCTIONS --- */
5348 +
5349 +/* Other convenient macros... */
5350 +
5351 +/*!
5352 + @abstract Type of the hash table.
5353 + @param name Name of the hash table [symbol]
5354 + */
5355 +#define khash_t(name) kh_##name##_t
5356 +
5357 +/*! @function
5358 + @abstract Initiate a hash table.
5359 + @param name Name of the hash table [symbol]
5360 + @return Pointer to the hash table [khash_t(name)*]
5361 + */
5362 +#define kh_init(name) kh_init_##name()
5363 +
5364 +/*! @function
5365 + @abstract Destroy a hash table.
5366 + @param name Name of the hash table [symbol]
5367 + @param h Pointer to the hash table [khash_t(name)*]
5368 + */
5369 +#define kh_destroy(name, h) kh_destroy_##name(h)
5370 +
5371 +/*! @function
5372 + @abstract Reset a hash table without deallocating memory.
5373 + @param name Name of the hash table [symbol]
5374 + @param h Pointer to the hash table [khash_t(name)*]
5375 + */
5376 +#define kh_clear(name, h) kh_clear_##name(h)
5377 +
5378 +/*! @function
5379 + @abstract Resize a hash table.
5380 + @param name Name of the hash table [symbol]
5381 + @param h Pointer to the hash table [khash_t(name)*]
5382 + @param s New size [khint_t]
5383 + */
5384 +#define kh_resize(name, h, s) kh_resize_##name(h, s)
5385 +
5386 +/*! @function
5387 + @abstract Insert a key to the hash table.
5388 + @param name Name of the hash table [symbol]
5389 + @param h Pointer to the hash table [khash_t(name)*]
5390 + @param k Key [type of keys]
5391 + @param r Extra return code: -1 if the operation failed;
5392 + 0 if the key is present in the hash table;
5393 + 1 if the bucket is empty (never used); 2 if the element in
5394 + the bucket has been deleted [int*]
5395 + @return Iterator to the inserted element [khint_t]
5396 + */
5397 +#define kh_put(name, h, k, r) kh_put_##name(h, k, r)
5398 +
5399 +/*! @function
5400 + @abstract Retrieve a key from the hash table.
5401 + @param name Name of the hash table [symbol]
5402 + @param h Pointer to the hash table [khash_t(name)*]
5403 + @param k Key [type of keys]
5404 + @return Iterator to the found element, or kh_end(h) if the element is absent [khint_t]
5405 + */
5406 +#define kh_get(name, h, k) kh_get_##name(h, k)
5407 +
5408 +/*! @function
5409 + @abstract Remove a key from the hash table.
5410 + @param name Name of the hash table [symbol]
5411 + @param h Pointer to the hash table [khash_t(name)*]
5412 + @param k Iterator to the element to be deleted [khint_t]
5413 + */
5414 +#define kh_del(name, h, k) kh_del_##name(h, k)
5415 +
5416 +/*! @function
5417 + @abstract Test whether a bucket contains data.
5418 + @param h Pointer to the hash table [khash_t(name)*]
5419 + @param x Iterator to the bucket [khint_t]
5420 + @return 1 if containing data; 0 otherwise [int]
5421 + */
5422 +#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x)))
5423 +
5424 +/*! @function
5425 + @abstract Get key given an iterator
5426 + @param h Pointer to the hash table [khash_t(name)*]
5427 + @param x Iterator to the bucket [khint_t]
5428 + @return Key [type of keys]
5429 + */
5430 +#define kh_key(h, x) ((h)->keys[x])
5431 +
5432 +/*! @function
5433 + @abstract Get value given an iterator
5434 + @param h Pointer to the hash table [khash_t(name)*]
5435 + @param x Iterator to the bucket [khint_t]
5436 + @return Value [type of values]
5437 + @discussion For hash sets, calling this results in segfault.
5438 + */
5439 +#define kh_val(h, x) ((h)->vals[x])
5440 +
5441 +/*! @function
5442 + @abstract Alias of kh_val()
5443 + */
5444 +#define kh_value(h, x) ((h)->vals[x])
5445 +
5446 +/*! @function
5447 + @abstract Get the start iterator
5448 + @param h Pointer to the hash table [khash_t(name)*]
5449 + @return The start iterator [khint_t]
5450 + */
5451 +#define kh_begin(h) (khint_t)(0)
5452 +
5453 +/*! @function
5454 + @abstract Get the end iterator
5455 + @param h Pointer to the hash table [khash_t(name)*]
5456 + @return The end iterator [khint_t]
5457 + */
5458 +#define kh_end(h) ((h)->n_buckets)
5459 +
5460 +/*! @function
5461 + @abstract Get the number of elements in the hash table
5462 + @param h Pointer to the hash table [khash_t(name)*]
5463 + @return Number of elements in the hash table [khint_t]
5464 + */
5465 +#define kh_size(h) ((h)->size)
5466 +
5467 +/*! @function
5468 + @abstract Get the number of buckets in the hash table
5469 + @param h Pointer to the hash table [khash_t(name)*]
5470 + @return Number of buckets in the hash table [khint_t]
5471 + */
5472 +#define kh_n_buckets(h) ((h)->n_buckets)
5473 +
5474 +/*! @function
5475 + @abstract Iterate over the entries in the hash table
5476 + @param h Pointer to the hash table [khash_t(name)*]
5477 + @param kvar Variable to which key will be assigned
5478 + @param vvar Variable to which value will be assigned
5479 + @param code Block of code to execute
5480 + */
5481 +#define kh_foreach(h, kvar, vvar, code) { khint_t __i; \
5482 + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
5483 + if (!kh_exist(h,__i)) continue; \
5484 + (kvar) = kh_key(h,__i); \
5485 + (vvar) = kh_val(h,__i); \
5486 + code; \
5487 + } }
5488 +
5489 +/*! @function
5490 + @abstract Iterate over the values in the hash table
5491 + @param h Pointer to the hash table [khash_t(name)*]
5492 + @param vvar Variable to which value will be assigned
5493 + @param code Block of code to execute
5494 + */
5495 +#define kh_foreach_value(h, vvar, code) { khint_t __i; \
5496 + for (__i = kh_begin(h); __i != kh_end(h); ++__i) { \
5497 + if (!kh_exist(h,__i)) continue; \
5498 + (vvar) = kh_val(h,__i); \
5499 + code; \
5500 + } }
5501 +
5502 +/* More convenient interfaces */
5503 +
5504 +/*! @function
5505 + @abstract Instantiate a hash set containing integer keys
5506 + @param name Name of the hash table [symbol]
5507 + */
5508 +#define KHASH_SET_INIT_INT(name) \
5509 + KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal)
5510 +
5511 +/*! @function
5512 + @abstract Instantiate a hash map containing integer keys
5513 + @param name Name of the hash table [symbol]
5514 + @param khval_t Type of values [type]
5515 + */
5516 +#define KHASH_MAP_INIT_INT(name, khval_t) \
5517 + KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal)
5518 +
5519 +/*! @function
5520 + @abstract Instantiate a hash set containing 64-bit integer keys
5521 + @param name Name of the hash table [symbol]
5522 + */
5523 +#define KHASH_SET_INIT_INT64(name) \
5524 + KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal)
5525 +
5526 +/*! @function
5527 + @abstract Instantiate a hash map containing 64-bit integer keys
5528 + @param name Name of the hash table [symbol]
5529 + @param khval_t Type of values [type]
5530 + */
5531 +#define KHASH_MAP_INIT_INT64(name, khval_t) \
5532 + KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal)
5533 +
5534 +typedef const char *kh_cstr_t;
5535 +/*! @function
5536 + @abstract Instantiate a hash map containing const char* keys
5537 + @param name Name of the hash table [symbol]
5538 + */
5539 +#define KHASH_SET_INIT_STR(name) \
5540 + KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal)
5541 +
5542 +/*! @function
5543 + @abstract Instantiate a hash map containing const char* keys
5544 + @param name Name of the hash table [symbol]
5545 + @param khval_t Type of values [type]
5546 + */
5547 +#define KHASH_MAP_INIT_STR(name, khval_t) \
5548 + KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal)
5549 +
5550 +#endif /* __AC_KHASH_H */
5551 diff --git a/kvec.h b/kvec.h
5552 new file mode 100644
5553 index 0000000..10f1c5b
5554 --- /dev/null
5555 +++ b/kvec.h
5556 @@ -0,0 +1,90 @@
5557 +/* The MIT License
5558 +
5559 + Copyright (c) 2008, by Attractive Chaos <attractor@live.co.uk>
5560 +
5561 + Permission is hereby granted, free of charge, to any person obtaining
5562 + a copy of this software and associated documentation files (the
5563 + "Software"), to deal in the Software without restriction, including
5564 + without limitation the rights to use, copy, modify, merge, publish,
5565 + distribute, sublicense, and/or sell copies of the Software, and to
5566 + permit persons to whom the Software is furnished to do so, subject to
5567 + the following conditions:
5568 +
5569 + The above copyright notice and this permission notice shall be
5570 + included in all copies or substantial portions of the Software.
5571 +
5572 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
5573 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
5574 + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
5575 + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
5576 + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
5577 + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
5578 + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
5579 + SOFTWARE.
5580 +*/
5581 +
5582 +/*
5583 + An example:
5584 +
5585 +#include "kvec.h"
5586 +int main() {
5587 + kvec_t(int) array;
5588 + kv_init(array);
5589 + kv_push(int, array, 10); // append
5590 + kv_a(int, array, 20) = 5; // dynamic
5591 + kv_A(array, 20) = 4; // static
5592 + kv_destroy(array);
5593 + return 0;
5594 +}
5595 +*/
5596 +
5597 +/*
5598 + 2008-09-22 (0.1.0):
5599 +
5600 + * The initial version.
5601 +
5602 +*/
5603 +
5604 +#ifndef AC_KVEC_H
5605 +#define AC_KVEC_H
5606 +
5607 +#include <stdlib.h>
5608 +
5609 +#define kv_roundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
5610 +
5611 +#define kvec_t(type) struct { size_t n, m; type *a; }
5612 +#define kv_init(v) ((v).n = (v).m = 0, (v).a = 0)
5613 +#define kv_destroy(v) free((v).a)
5614 +#define kv_A(v, i) ((v).a[(i)])
5615 +#define kv_pop(v) ((v).a[--(v).n])
5616 +#define kv_size(v) ((v).n)
5617 +#define kv_max(v) ((v).m)
5618 +
5619 +#define kv_resize(type, v, s) ((v).m = (s), (v).a = (type*)realloc((v).a, sizeof(type) * (v).m))
5620 +
5621 +#define kv_copy(type, v1, v0) do { \
5622 + if ((v1).m < (v0).n) kv_resize(type, v1, (v0).n); \
5623 + (v1).n = (v0).n; \
5624 + memcpy((v1).a, (v0).a, sizeof(type) * (v0).n); \
5625 + } while (0) \
5626 +
5627 +#define kv_push(type, v, x) do { \
5628 + if ((v).n == (v).m) { \
5629 + (v).m = (v).m? (v).m<<1 : 2; \
5630 + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m); \
5631 + } \
5632 + (v).a[(v).n++] = (x); \
5633 + } while (0)
5634 +
5635 +#define kv_pushp(type, v) ((((v).n == (v).m)? \
5636 + ((v).m = ((v).m? (v).m<<1 : 2), \
5637 + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \
5638 + : 0), ((v).a + ((v).n++)))
5639 +
5640 +#define kv_a(type, v, i) (((v).m <= (size_t)(i)? \
5641 + ((v).m = (v).n = (i) + 1, kv_roundup32((v).m), \
5642 + (v).a = (type*)realloc((v).a, sizeof(type) * (v).m), 0) \
5643 + : (v).n <= (size_t)(i)? (v).n = (i) + 1 \
5644 + : 0), (v).a[(i)])
5645 +
5646 +#endif
5647 diff --git a/rowcolumn_diacritics_helpers.c b/rowcolumn_diacritics_helpers.c
5648 new file mode 100644
5649 index 0000000..829c0fc
5650 --- /dev/null
5651 +++ b/rowcolumn_diacritics_helpers.c
5652 @@ -0,0 +1,391 @@
5653 +#include <stdint.h>
5654 +
5655 +uint16_t diacritic_to_num(uint32_t code)
5656 +{
5657 + switch (code) {
5658 + case 0x305:
5659 + return code - 0x305 + 1;
5660 + case 0x30d:
5661 + case 0x30e:
5662 + return code - 0x30d + 2;
5663 + case 0x310:
5664 + return code - 0x310 + 4;
5665 + case 0x312:
5666 + return code - 0x312 + 5;
5667 + case 0x33d:
5668 + case 0x33e:
5669 + case 0x33f:
5670 + return code - 0x33d + 6;
5671 + case 0x346:
5672 + return code - 0x346 + 9;
5673 + case 0x34a:
5674 + case 0x34b:
5675 + case 0x34c:
5676 + return code - 0x34a + 10;
5677 + case 0x350:
5678 + case 0x351:
5679 + case 0x352:
5680 + return code - 0x350 + 13;
5681 + case 0x357:
5682 + return code - 0x357 + 16;
5683 + case 0x35b:
5684 + return code - 0x35b + 17;
5685 + case 0x363:
5686 + case 0x364:
5687 + case 0x365:
5688 + case 0x366:
5689 + case 0x367:
5690 + case 0x368:
5691 + case 0x369:
5692 + case 0x36a:
5693 + case 0x36b:
5694 + case 0x36c:
5695 + case 0x36d:
5696 + case 0x36e:
5697 + case 0x36f:
5698 + return code - 0x363 + 18;
5699 + case 0x483:
5700 + case 0x484:
5701 + case 0x485:
5702 + case 0x486:
5703 + case 0x487:
5704 + return code - 0x483 + 31;
5705 + case 0x592:
5706 + case 0x593:
5707 + case 0x594:
5708 + case 0x595:
5709 + return code - 0x592 + 36;
5710 + case 0x597:
5711 + case 0x598:
5712 + case 0x599:
5713 + return code - 0x597 + 40;
5714 + case 0x59c:
5715 + case 0x59d:
5716 + case 0x59e:
5717 + case 0x59f:
5718 + case 0x5a0:
5719 + case 0x5a1:
5720 + return code - 0x59c + 43;
5721 + case 0x5a8:
5722 + case 0x5a9:
5723 + return code - 0x5a8 + 49;
5724 + case 0x5ab:
5725 + case 0x5ac:
5726 + return code - 0x5ab + 51;
5727 + case 0x5af:
5728 + return code - 0x5af + 53;
5729 + case 0x5c4:
5730 + return code - 0x5c4 + 54;
5731 + case 0x610:
5732 + case 0x611:
5733 + case 0x612:
5734 + case 0x613:
5735 + case 0x614:
5736 + case 0x615:
5737 + case 0x616:
5738 + case 0x617:
5739 + return code - 0x610 + 55;
5740 + case 0x657:
5741 + case 0x658:
5742 + case 0x659:
5743 + case 0x65a:
5744 + case 0x65b:
5745 + return code - 0x657 + 63;
5746 + case 0x65d:
5747 + case 0x65e:
5748 + return code - 0x65d + 68;
5749 + case 0x6d6:
5750 + case 0x6d7:
5751 + case 0x6d8:
5752 + case 0x6d9:
5753 + case 0x6da:
5754 + case 0x6db:
5755 + case 0x6dc:
5756 + return code - 0x6d6 + 70;
5757 + case 0x6df:
5758 + case 0x6e0:
5759 + case 0x6e1:
5760 + case 0x6e2:
5761 + return code - 0x6df + 77;
5762 + case 0x6e4:
5763 + return code - 0x6e4 + 81;
5764 + case 0x6e7:
5765 + case 0x6e8:
5766 + return code - 0x6e7 + 82;
5767 + case 0x6eb:
5768 + case 0x6ec:
5769 + return code - 0x6eb + 84;
5770 + case 0x730:
5771 + return code - 0x730 + 86;
5772 + case 0x732:
5773 + case 0x733:
5774 + return code - 0x732 + 87;
5775 + case 0x735:
5776 + case 0x736:
5777 + return code - 0x735 + 89;
5778 + case 0x73a:
5779 + return code - 0x73a + 91;
5780 + case 0x73d:
5781 + return code - 0x73d + 92;
5782 + case 0x73f:
5783 + case 0x740:
5784 + case 0x741:
5785 + return code - 0x73f + 93;
5786 + case 0x743:
5787 + return code - 0x743 + 96;
5788 + case 0x745:
5789 + return code - 0x745 + 97;
5790 + case 0x747:
5791 + return code - 0x747 + 98;
5792 + case 0x749:
5793 + case 0x74a:
5794 + return code - 0x749 + 99;
5795 + case 0x7eb:
5796 + case 0x7ec:
5797 + case 0x7ed:
5798 + case 0x7ee:
5799 + case 0x7ef:
5800 + case 0x7f0:
5801 + case 0x7f1:
5802 + return code - 0x7eb + 101;
5803 + case 0x7f3:
5804 + return code - 0x7f3 + 108;
5805 + case 0x816:
5806 + case 0x817:
5807 + case 0x818:
5808 + case 0x819:
5809 + return code - 0x816 + 109;
5810 + case 0x81b:
5811 + case 0x81c:
5812 + case 0x81d:
5813 + case 0x81e:
5814 + case 0x81f:
5815 + case 0x820:
5816 + case 0x821:
5817 + case 0x822:
5818 + case 0x823:
5819 + return code - 0x81b + 113;
5820 + case 0x825:
5821 + case 0x826:
5822 + case 0x827:
5823 + return code - 0x825 + 122;
5824 + case 0x829:
5825 + case 0x82a:
5826 + case 0x82b:
5827 + case 0x82c:
5828 + case 0x82d:
5829 + return code - 0x829 + 125;
5830 + case 0x951:
5831 + return code - 0x951 + 130;
5832 + case 0x953:
5833 + case 0x954:
5834 + return code - 0x953 + 131;
5835 + case 0xf82:
5836 + case 0xf83:
5837 + return code - 0xf82 + 133;
5838 + case 0xf86:
5839 + case 0xf87:
5840 + return code - 0xf86 + 135;
5841 + case 0x135d:
5842 + case 0x135e:
5843 + case 0x135f:
5844 + return code - 0x135d + 137;
5845 + case 0x17dd:
5846 + return code - 0x17dd + 140;
5847 + case 0x193a:
5848 + return code - 0x193a + 141;
5849 + case 0x1a17:
5850 + return code - 0x1a17 + 142;
5851 + case 0x1a75:
5852 + case 0x1a76:
5853 + case 0x1a77:
5854 + case 0x1a78:
5855 + case 0x1a79:
5856 + case 0x1a7a:
5857 + case 0x1a7b:
5858 + case 0x1a7c:
5859 + return code - 0x1a75 + 143;
5860 + case 0x1b6b:
5861 + return code - 0x1b6b + 151;
5862 + case 0x1b6d:
5863 + case 0x1b6e:
5864 + case 0x1b6f:
5865 + case 0x1b70:
5866 + case 0x1b71:
5867 + case 0x1b72:
5868 + case 0x1b73:
5869 + return code - 0x1b6d + 152;
5870 + case 0x1cd0:
5871 + case 0x1cd1:
5872 + case 0x1cd2:
5873 + return code - 0x1cd0 + 159;
5874 + case 0x1cda:
5875 + case 0x1cdb:
5876 + return code - 0x1cda + 162;
5877 + case 0x1ce0:
5878 + return code - 0x1ce0 + 164;
5879 + case 0x1dc0:
5880 + case 0x1dc1:
5881 + return code - 0x1dc0 + 165;
5882 + case 0x1dc3:
5883 + case 0x1dc4:
5884 + case 0x1dc5:
5885 + case 0x1dc6:
5886 + case 0x1dc7:
5887 + case 0x1dc8:
5888 + case 0x1dc9:
5889 + return code - 0x1dc3 + 167;
5890 + case 0x1dcb:
5891 + case 0x1dcc:
5892 + return code - 0x1dcb + 174;
5893 + case 0x1dd1:
5894 + case 0x1dd2:
5895 + case 0x1dd3:
5896 + case 0x1dd4:
5897 + case 0x1dd5:
5898 + case 0x1dd6:
5899 + case 0x1dd7:
5900 + case 0x1dd8:
5901 + case 0x1dd9:
5902 + case 0x1dda:
5903 + case 0x1ddb:
5904 + case 0x1ddc:
5905 + case 0x1ddd:
5906 + case 0x1dde:
5907 + case 0x1ddf:
5908 + case 0x1de0:
5909 + case 0x1de1:
5910 + case 0x1de2:
5911 + case 0x1de3:
5912 + case 0x1de4:
5913 + case 0x1de5:
5914 + case 0x1de6:
5915 + return code - 0x1dd1 + 176;
5916 + case 0x1dfe:
5917 + return code - 0x1dfe + 198;
5918 + case 0x20d0:
5919 + case 0x20d1:
5920 + return code - 0x20d0 + 199;
5921 + case 0x20d4:
5922 + case 0x20d5:
5923 + case 0x20d6:
5924 + case 0x20d7:
5925 + return code - 0x20d4 + 201;
5926 + case 0x20db:
5927 + case 0x20dc:
5928 + return code - 0x20db + 205;
5929 + case 0x20e1:
5930 + return code - 0x20e1 + 207;
5931 + case 0x20e7:
5932 + return code - 0x20e7 + 208;
5933 + case 0x20e9:
5934 + return code - 0x20e9 + 209;
5935 + case 0x20f0:
5936 + return code - 0x20f0 + 210;
5937 + case 0x2cef:
5938 + case 0x2cf0:
5939 + case 0x2cf1:
5940 + return code - 0x2cef + 211;
5941 + case 0x2de0:
5942 + case 0x2de1:
5943 + case 0x2de2:
5944 + case 0x2de3:
5945 + case 0x2de4:
5946 + case 0x2de5:
5947 + case 0x2de6:
5948 + case 0x2de7:
5949 + case 0x2de8:
5950 + case 0x2de9:
5951 + case 0x2dea:
5952 + case 0x2deb:
5953 + case 0x2dec:
5954 + case 0x2ded:
5955 + case 0x2dee:
5956 + case 0x2def:
5957 + case 0x2df0:
5958 + case 0x2df1:
5959 + case 0x2df2:
5960 + case 0x2df3:
5961 + case 0x2df4:
5962 + case 0x2df5:
5963 + case 0x2df6:
5964 + case 0x2df7:
5965 + case 0x2df8:
5966 + case 0x2df9:
5967 + case 0x2dfa:
5968 + case 0x2dfb:
5969 + case 0x2dfc:
5970 + case 0x2dfd:
5971 + case 0x2dfe:
5972 + case 0x2dff:
5973 + return code - 0x2de0 + 214;
5974 + case 0xa66f:
5975 + return code - 0xa66f + 246;
5976 + case 0xa67c:
5977 + case 0xa67d:
5978 + return code - 0xa67c + 247;
5979 + case 0xa6f0:
5980 + case 0xa6f1:
5981 + return code - 0xa6f0 + 249;
5982 + case 0xa8e0:
5983 + case 0xa8e1:
5984 + case 0xa8e2:
5985 + case 0xa8e3:
5986 + case 0xa8e4:
5987 + case 0xa8e5:
5988 + case 0xa8e6:
5989 + case 0xa8e7:
5990 + case 0xa8e8:
5991 + case 0xa8e9:
5992 + case 0xa8ea:
5993 + case 0xa8eb:
5994 + case 0xa8ec:
5995 + case 0xa8ed:
5996 + case 0xa8ee:
5997 + case 0xa8ef:
5998 + case 0xa8f0:
5999 + case 0xa8f1:
6000 + return code - 0xa8e0 + 251;
6001 + case 0xaab0:
6002 + return code - 0xaab0 + 269;
6003 + case 0xaab2:
6004 + case 0xaab3:
6005 + return code - 0xaab2 + 270;
6006 + case 0xaab7:
6007 + case 0xaab8:
6008 + return code - 0xaab7 + 272;
6009 + case 0xaabe:
6010 + case 0xaabf:
6011 + return code - 0xaabe + 274;
6012 + case 0xaac1:
6013 + return code - 0xaac1 + 276;
6014 + case 0xfe20:
6015 + case 0xfe21:
6016 + case 0xfe22:
6017 + case 0xfe23:
6018 + case 0xfe24:
6019 + case 0xfe25:
6020 + case 0xfe26:
6021 + return code - 0xfe20 + 277;
6022 + case 0x10a0f:
6023 + return code - 0x10a0f + 284;
6024 + case 0x10a38:
6025 + return code - 0x10a38 + 285;
6026 + case 0x1d185:
6027 + case 0x1d186:
6028 + case 0x1d187:
6029 + case 0x1d188:
6030 + case 0x1d189:
6031 + return code - 0x1d185 + 286;
6032 + case 0x1d1aa:
6033 + case 0x1d1ab:
6034 + case 0x1d1ac:
6035 + case 0x1d1ad:
6036 + return code - 0x1d1aa + 291;
6037 + case 0x1d242:
6038 + case 0x1d243:
6039 + case 0x1d244:
6040 + return code - 0x1d242 + 295;
6041 + }
6042 + return 0;
6043 +}
6044 diff --git a/st.c b/st.c
6045 index 57c6e96..f1c5299 100644
6046 --- a/st.c
6047 +++ b/st.c
6048 @@ -19,6 +19,7 @@
6049
6050 #include "st.h"
6051 #include "win.h"
6052 +#include "graphics.h"
6053
6054 #if defined(__linux)
6055 #include <pty.h>
6056 @@ -36,6 +37,10 @@
6057 #define STR_BUF_SIZ ESC_BUF_SIZ
6058 #define STR_ARG_SIZ ESC_ARG_SIZ
6059
6060 +/* PUA character used as an image placeholder */
6061 +#define IMAGE_PLACEHOLDER_CHAR 0x10EEEE
6062 +#define IMAGE_PLACEHOLDER_CHAR_OLD 0xEEEE
6063 +
6064 /* macros */
6065 #define IS_SET(flag) ((term.mode & (flag)) != 0)
6066 #define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f)
6067 @@ -113,6 +118,8 @@ typedef struct {
6068 typedef struct {
6069 int row; /* nb row */
6070 int col; /* nb col */
6071 + int pixw; /* width of the text area in pixels */
6072 + int pixh; /* height of the text area in pixels */
6073 Line *line; /* screen */
6074 Line *alt; /* alternate screen */
6075 int *dirty; /* dirtyness of lines */
6076 @@ -213,7 +220,6 @@ static Rune utf8decodebyte(char, size_t *);
6077 static char utf8encodebyte(Rune, size_t);
6078 static size_t utf8validate(Rune *, size_t);
6079
6080 -static char *base64dec(const char *);
6081 static char base64dec_getc(const char **);
6082
6083 static ssize_t xwrite(int, const char *, size_t);
6084 @@ -232,6 +238,10 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
6085 static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
6086 static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
6087
6088 +/* Converts a diacritic to a row/column/etc number. The result is 1-base, 0
6089 + * means "couldn't convert". Defined in rowcolumn_diacritics_helpers.c */
6090 +uint16_t diacritic_to_num(uint32_t code);
6091 +
6092 ssize_t
6093 xwrite(int fd, const char *s, size_t len)
6094 {
6095 @@ -616,6 +626,12 @@ getsel(void)
6096 if (gp->mode & ATTR_WDUMMY)
6097 continue;
6098
6099 + if (gp->mode & ATTR_IMAGE) {
6100 + // TODO: Copy diacritics as well
6101 + ptr += utf8encode(IMAGE_PLACEHOLDER_CHAR, ptr);
6102 + continue;
6103 + }
6104 +
6105 ptr += utf8encode(gp->u, ptr);
6106 }
6107
6108 @@ -819,7 +835,11 @@ ttyread(void)
6109 {
6110 static char buf[BUFSIZ];
6111 static int buflen = 0;
6112 - int ret, written;
6113 + static int already_processing = 0;
6114 + int ret, written = 0;
6115 +
6116 + if (buflen >= LEN(buf))
6117 + return 0;
6118
6119 /* append read bytes to unprocessed bytes */
6120 ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
6121 @@ -831,7 +851,24 @@ ttyread(void)
6122 die("couldn't read from shell: %s\n", strerror(errno));
6123 default:
6124 buflen += ret;
6125 - written = twrite(buf, buflen, 0);
6126 + if (already_processing) {
6127 + /* Avoid recursive call to twrite() */
6128 + return ret;
6129 + }
6130 + already_processing = 1;
6131 + while (1) {
6132 + int buflen_before_processing = buflen;
6133 + written += twrite(buf + written, buflen - written, 0);
6134 + // If buflen changed during the call to twrite, there is
6135 + // new data, and we need to keep processing, otherwise
6136 + // we can exit. This will not loop forever because the
6137 + // buffer is limited, and we don't clean it in this
6138 + // loop, so at some point ttywrite will have to drop
6139 + // some data.
6140 + if (buflen_before_processing == buflen)
6141 + break;
6142 + }
6143 + already_processing = 0;
6144 buflen -= written;
6145 /* keep any incomplete UTF-8 byte sequence for the next call */
6146 if (buflen > 0)
6147 @@ -874,6 +911,7 @@ ttywriteraw(const char *s, size_t n)
6148 fd_set wfd, rfd;
6149 ssize_t r;
6150 size_t lim = 256;
6151 + int retries_left = 100;
6152
6153 /*
6154 * Remember that we are using a pty, which might be a modem line.
6155 @@ -882,6 +920,9 @@ ttywriteraw(const char *s, size_t n)
6156 * FIXME: Migrate the world to Plan 9.
6157 */
6158 while (n > 0) {
6159 + if (retries_left-- <= 0)
6160 + goto too_many_retries;
6161 +
6162 FD_ZERO(&wfd);
6163 FD_ZERO(&rfd);
6164 FD_SET(cmdfd, &wfd);
6165 @@ -923,11 +964,16 @@ ttywriteraw(const char *s, size_t n)
6166
6167 write_error:
6168 die("write error on tty: %s\n", strerror(errno));
6169 +too_many_retries:
6170 + fprintf(stderr, "Could not write %zu bytes to tty\n", n);
6171 }
6172
6173 void
6174 ttyresize(int tw, int th)
6175 {
6176 + term.pixw = tw;
6177 + term.pixh = th;
6178 +
6179 struct winsize w;
6180
6181 w.ws_row = term.row;
6182 @@ -1015,7 +1061,8 @@ treset(void)
6183 term.c = (TCursor){{
6184 .mode = ATTR_NULL,
6185 .fg = defaultfg,
6186 - .bg = defaultbg
6187 + .bg = defaultbg,
6188 + .decor = DECOR_DEFAULT_COLOR
6189 }, .x = 0, .y = 0, .state = CURSOR_DEFAULT};
6190
6191 memset(term.tabs, 0, term.col * sizeof(*term.tabs));
6192 @@ -1038,7 +1085,9 @@ treset(void)
6193 void
6194 tnew(int col, int row)
6195 {
6196 - term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
6197 + term = (Term){.c = {.attr = {.fg = defaultfg,
6198 + .bg = defaultbg,
6199 + .decor = DECOR_DEFAULT_COLOR}}};
6200 tresize(col, row);
6201 treset();
6202 }
6203 @@ -1215,9 +1264,24 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
6204 term.line[y][x-1].mode &= ~ATTR_WIDE;
6205 }
6206
6207 + if (u == ' ' && term.line[y][x].mode & ATTR_IMAGE &&
6208 + tgetisclassicplaceholder(&term.line[y][x])) {
6209 + // This is a workaround: don't overwrite classic placement
6210 + // placeholders with space symbols (unlike Unicode placeholders
6211 + // which must be overwritten by anything).
6212 + term.line[y][x].bg = attr->bg;
6213 + term.dirty[y] = 1;
6214 + return;
6215 + }
6216 +
6217 term.dirty[y] = 1;
6218 term.line[y][x] = *attr;
6219 term.line[y][x].u = u;
6220 +
6221 + if (u == IMAGE_PLACEHOLDER_CHAR || u == IMAGE_PLACEHOLDER_CHAR_OLD) {
6222 + term.line[y][x].u = 0;
6223 + term.line[y][x].mode |= ATTR_IMAGE;
6224 + }
6225 }
6226
6227 void
6228 @@ -1244,12 +1308,104 @@ tclearregion(int x1, int y1, int x2, int y2)
6229 selclear();
6230 gp->fg = term.c.attr.fg;
6231 gp->bg = term.c.attr.bg;
6232 + gp->decor = term.c.attr.decor;
6233 gp->mode = 0;
6234 gp->u = ' ';
6235 }
6236 }
6237 }
6238
6239 +/// Fills a rectangle area with an image placeholder. The starting point is the
6240 +/// cursor. Adds empty lines if needed. The placeholder will be marked as
6241 +/// classic.
6242 +void
6243 +tcreateimgplaceholder(uint32_t image_id, uint32_t placement_id,
6244 + int cols, int rows, char do_not_move_cursor)
6245 +{
6246 + for (int row = 0; row < rows; ++row) {
6247 + int y = term.c.y;
6248 + term.dirty[y] = 1;
6249 + for (int col = 0; col < cols; ++col) {
6250 + int x = term.c.x + col;
6251 + if (x >= term.col)
6252 + break;
6253 + Glyph *gp = &term.line[y][x];
6254 + if (selected(x, y))
6255 + selclear();
6256 + gp->mode = ATTR_IMAGE;
6257 + gp->u = 0;
6258 + tsetimgrow(gp, row + 1);
6259 + tsetimgcol(gp, col + 1);
6260 + tsetimgid(gp, image_id);
6261 + tsetimgplacementid(gp, placement_id);
6262 + tsetimgdiacriticcount(gp, 3);
6263 + tsetisclassicplaceholder(gp, 1);
6264 + }
6265 + // If moving the cursor is not allowed and this is the last line
6266 + // of the terminal, we are done.
6267 + if (do_not_move_cursor && y == term.row - 1)
6268 + break;
6269 + // Move the cursor down, maybe creating a new line. The x is
6270 + // preserved (we never change term.c.x in the loop above).
6271 + if (row != rows - 1)
6272 + tnewline(/*first_col=*/0);
6273 + }
6274 + if (do_not_move_cursor) {
6275 + // Return the cursor to the original position.
6276 + tmoveto(term.c.x, term.c.y - rows + 1);
6277 + } else {
6278 + // Move the cursor beyond the last column, as required by the
6279 + // protocol. If the cursor goes beyond the screen edge, insert a
6280 + // newline to match the behavior of kitty.
6281 + if (term.c.x + cols >= term.col)
6282 + tnewline(/*first_col=*/1);
6283 + else
6284 + tmoveto(term.c.x + cols, term.c.y);
6285 + }
6286 +}
6287 +
6288 +void gr_for_each_image_cell(int (*callback)(void *data, uint32_t image_id,
6289 + uint32_t placement_id, int col,
6290 + int row, char is_classic),
6291 + void *data) {
6292 + for (int row = 0; row < term.row; ++row) {
6293 + for (int col = 0; col < term.col; ++col) {
6294 + Glyph *gp = &term.line[row][col];
6295 + if (gp->mode & ATTR_IMAGE) {
6296 + uint32_t image_id = tgetimgid(gp);
6297 + uint32_t placement_id = tgetimgplacementid(gp);
6298 + int ret =
6299 + callback(data, tgetimgid(gp),
6300 + tgetimgplacementid(gp),
6301 + tgetimgcol(gp), tgetimgrow(gp),
6302 + tgetisclassicplaceholder(gp));
6303 + if (ret == 1) {
6304 + term.dirty[row] = 1;
6305 + gp->mode = 0;
6306 + gp->u = ' ';
6307 + }
6308 + }
6309 + }
6310 + }
6311 +}
6312 +
6313 +void gr_schedule_image_redraw_by_id(uint32_t image_id) {
6314 + for (int row = 0; row < term.row; ++row) {
6315 + if (term.dirty[row])
6316 + continue;
6317 + for (int col = 0; col < term.col; ++col) {
6318 + Glyph *gp = &term.line[row][col];
6319 + if (gp->mode & ATTR_IMAGE) {
6320 + uint32_t cell_image_id = tgetimgid(gp);
6321 + if (cell_image_id == image_id) {
6322 + term.dirty[row] = 1;
6323 + break;
6324 + }
6325 + }
6326 + }
6327 + }
6328 +}
6329 +
6330 void
6331 tdeletechar(int n)
6332 {
6333 @@ -1368,6 +1524,7 @@ tsetattr(const int *attr, int l)
6334 ATTR_STRUCK );
6335 term.c.attr.fg = defaultfg;
6336 term.c.attr.bg = defaultbg;
6337 + term.c.attr.decor = DECOR_DEFAULT_COLOR;
6338 break;
6339 case 1:
6340 term.c.attr.mode |= ATTR_BOLD;
6341 @@ -1380,6 +1537,20 @@ tsetattr(const int *attr, int l)
6342 break;
6343 case 4:
6344 term.c.attr.mode |= ATTR_UNDERLINE;
6345 + if (i + 1 < l) {
6346 + idx = attr[++i];
6347 + if (BETWEEN(idx, 1, 5)) {
6348 + tsetdecorstyle(&term.c.attr, idx);
6349 + } else if (idx == 0) {
6350 + term.c.attr.mode &= ~ATTR_UNDERLINE;
6351 + tsetdecorstyle(&term.c.attr, 0);
6352 + } else {
6353 + fprintf(stderr,
6354 + "erresc: unknown underline "
6355 + "style %d\n",
6356 + idx);
6357 + }
6358 + }
6359 break;
6360 case 5: /* slow blink */
6361 /* FALLTHROUGH */
6362 @@ -1403,6 +1574,7 @@ tsetattr(const int *attr, int l)
6363 break;
6364 case 24:
6365 term.c.attr.mode &= ~ATTR_UNDERLINE;
6366 + tsetdecorstyle(&term.c.attr, 0);
6367 break;
6368 case 25:
6369 term.c.attr.mode &= ~ATTR_BLINK;
6370 @@ -1430,6 +1602,13 @@ tsetattr(const int *attr, int l)
6371 case 49:
6372 term.c.attr.bg = defaultbg;
6373 break;
6374 + case 58:
6375 + if ((idx = tdefcolor(attr, &i, l)) >= 0)
6376 + tsetdecorcolor(&term.c.attr, idx);
6377 + break;
6378 + case 59:
6379 + tsetdecorcolor(&term.c.attr, DECOR_DEFAULT_COLOR);
6380 + break;
6381 default:
6382 if (BETWEEN(attr[i], 30, 37)) {
6383 term.c.attr.fg = attr[i] - 30;
6384 @@ -1813,6 +1992,39 @@ csihandle(void)
6385 goto unknown;
6386 }
6387 break;
6388 + case '>':
6389 + switch (csiescseq.mode[1]) {
6390 + case 'q': /* XTVERSION -- Print terminal name and version */
6391 + len = snprintf(buf, sizeof(buf),
6392 + "\033P>|st-graphics(%s)\033\\", VERSION);
6393 + ttywrite(buf, len, 0);
6394 + break;
6395 + default:
6396 + goto unknown;
6397 + }
6398 + break;
6399 + case 't': /* XTWINOPS -- Window manipulation */
6400 + switch (csiescseq.arg[0]) {
6401 + case 14: /* Report text area size in pixels. */
6402 + len = snprintf(buf, sizeof(buf), "\033[4;%i;%it",
6403 + term.pixh, term.pixw);
6404 + ttywrite(buf, len, 0);
6405 + break;
6406 + case 16: /* Report character cell size in pixels. */
6407 + len = snprintf(buf, sizeof(buf), "\033[6;%i;%it",
6408 + term.pixh / term.row,
6409 + term.pixw / term.col);
6410 + ttywrite(buf, len, 0);
6411 + break;
6412 + case 18: /* Report the size of the text area in characters. */
6413 + len = snprintf(buf, sizeof(buf), "\033[8;%i;%it",
6414 + term.row, term.col);
6415 + ttywrite(buf, len, 0);
6416 + break;
6417 + default:
6418 + goto unknown;
6419 + }
6420 + break;
6421 }
6422 }
6423
6424 @@ -1962,8 +2174,26 @@ strhandle(void)
6425 case 'k': /* old title set compatibility */
6426 xsettitle(strescseq.args[0]);
6427 return;
6428 - case 'P': /* DCS -- Device Control String */
6429 case '_': /* APC -- Application Program Command */
6430 + if (gr_parse_command(strescseq.buf, strescseq.len)) {
6431 + GraphicsCommandResult *res = &graphics_command_result;
6432 + if (res->create_placeholder) {
6433 + tcreateimgplaceholder(
6434 + res->placeholder.image_id,
6435 + res->placeholder.placement_id,
6436 + res->placeholder.columns,
6437 + res->placeholder.rows,
6438 + res->placeholder.do_not_move_cursor);
6439 + }
6440 + if (res->response[0])
6441 + ttywrite(res->response, strlen(res->response),
6442 + 0);
6443 + if (res->redraw)
6444 + tfulldirt();
6445 + return;
6446 + }
6447 + return;
6448 + case 'P': /* DCS -- Device Control String */
6449 case '^': /* PM -- Privacy Message */
6450 return;
6451 }
6452 @@ -2469,6 +2699,33 @@ check_control_code:
6453 if (selected(term.c.x, term.c.y))
6454 selclear();
6455
6456 + if (width == 0) {
6457 + // It's probably a combining char. Combining characters are not
6458 + // supported, so we just ignore them, unless it denotes the row and
6459 + // column of an image character.
6460 + if (term.c.y <= 0 && term.c.x <= 0)
6461 + return;
6462 + else if (term.c.x == 0)
6463 + gp = &term.line[term.c.y-1][term.col-1];
6464 + else if (term.c.state & CURSOR_WRAPNEXT)
6465 + gp = &term.line[term.c.y][term.c.x];
6466 + else
6467 + gp = &term.line[term.c.y][term.c.x-1];
6468 + uint16_t num = diacritic_to_num(u);
6469 + if (num && (gp->mode & ATTR_IMAGE)) {
6470 + unsigned diaccount = tgetimgdiacriticcount(gp);
6471 + if (diaccount == 0)
6472 + tsetimgrow(gp, num);
6473 + else if (diaccount == 1)
6474 + tsetimgcol(gp, num);
6475 + else if (diaccount == 2)
6476 + tsetimg4thbyteplus1(gp, num);
6477 + tsetimgdiacriticcount(gp, diaccount + 1);
6478 + }
6479 + term.lastc = u;
6480 + return;
6481 + }
6482 +
6483 gp = &term.line[term.c.y][term.c.x];
6484 if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) {
6485 gp->mode |= ATTR_WRAP;
6486 @@ -2635,6 +2892,8 @@ drawregion(int x1, int y1, int x2, int y2)
6487 {
6488 int y;
6489
6490 + xstartimagedraw(term.dirty, term.row);
6491 +
6492 for (y = y1; y < y2; y++) {
6493 if (!term.dirty[y])
6494 continue;
6495 @@ -2642,6 +2901,8 @@ drawregion(int x1, int y1, int x2, int y2)
6496 term.dirty[y] = 0;
6497 xdrawline(term.line[y], x1, y, x2);
6498 }
6499 +
6500 + xfinishimagedraw();
6501 }
6502
6503 void
6504 @@ -2676,3 +2937,9 @@ redraw(void)
6505 tfulldirt();
6506 draw();
6507 }
6508 +
6509 +Glyph
6510 +getglyphat(int col, int row)
6511 +{
6512 + return term.line[row][col];
6513 +}
6514 diff --git a/st.h b/st.h
6515 index fd3b0d8..c5dd731 100644
6516 --- a/st.h
6517 +++ b/st.h
6518 @@ -12,7 +12,7 @@
6519 #define DEFAULT(a, b) (a) = (a) ? (a) : (b)
6520 #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
6521 #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
6522 - (a).bg != (b).bg)
6523 + (a).bg != (b).bg || (a).decor != (b).decor)
6524 #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
6525 (t1.tv_nsec-t2.tv_nsec)/1E6)
6526 #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit)))
6527 @@ -20,6 +20,10 @@
6528 #define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b))
6529 #define IS_TRUECOL(x) (1 << 24 & (x))
6530
6531 +// This decor color indicates that the fg color should be used. Note that it's
6532 +// not a 24-bit color because the 25-th bit is not set.
6533 +#define DECOR_DEFAULT_COLOR 0x0ffffff
6534 +
6535 enum glyph_attribute {
6536 ATTR_NULL = 0,
6537 ATTR_BOLD = 1 << 0,
6538 @@ -34,6 +38,7 @@ enum glyph_attribute {
6539 ATTR_WIDE = 1 << 9,
6540 ATTR_WDUMMY = 1 << 10,
6541 ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
6542 + ATTR_IMAGE = 1 << 14,
6543 };
6544
6545 enum selection_mode {
6546 @@ -52,6 +57,14 @@ enum selection_snap {
6547 SNAP_LINE = 2
6548 };
6549
6550 +enum underline_style {
6551 + UNDERLINE_STRAIGHT = 1,
6552 + UNDERLINE_DOUBLE = 2,
6553 + UNDERLINE_CURLY = 3,
6554 + UNDERLINE_DOTTED = 4,
6555 + UNDERLINE_DASHED = 5,
6556 +};
6557 +
6558 typedef unsigned char uchar;
6559 typedef unsigned int uint;
6560 typedef unsigned long ulong;
6561 @@ -65,6 +78,7 @@ typedef struct {
6562 ushort mode; /* attribute flags */
6563 uint32_t fg; /* foreground */
6564 uint32_t bg; /* background */
6565 + uint32_t decor; /* decoration (like underline) */
6566 } Glyph;
6567
6568 typedef Glyph *Line;
6569 @@ -105,6 +119,8 @@ void selextend(int, int, int, int);
6570 int selected(int, int);
6571 char *getsel(void);
6572
6573 +Glyph getglyphat(int, int);
6574 +
6575 size_t utf8encode(Rune, char *);
6576
6577 void *xmalloc(size_t);
6578 @@ -124,3 +140,69 @@ extern unsigned int tabspaces;
6579 extern unsigned int defaultfg;
6580 extern unsigned int defaultbg;
6581 extern unsigned int defaultcs;
6582 +
6583 +// Accessors to decoration properties stored in `decor`.
6584 +// The 25-th bit is used to indicate if it's a 24-bit color.
6585 +static inline uint32_t tgetdecorcolor(Glyph *g) { return g->decor & 0x1ffffff; }
6586 +static inline uint32_t tgetdecorstyle(Glyph *g) { return (g->decor >> 25) & 0x7; }
6587 +static inline void tsetdecorcolor(Glyph *g, uint32_t color) {
6588 + g->decor = (g->decor & ~0x1ffffff) | (color & 0x1ffffff);
6589 +}
6590 +static inline void tsetdecorstyle(Glyph *g, uint32_t style) {
6591 + g->decor = (g->decor & ~(0x7 << 25)) | ((style & 0x7) << 25);
6592 +}
6593 +
6594 +
6595 +// Some accessors to image placeholder properties stored in `u`:
6596 +// - row (1-base) - 9 bits
6597 +// - column (1-base) - 9 bits
6598 +// - most significant byte of the image id plus 1 - 9 bits (0 means unspecified,
6599 +// don't forget to subtract 1).
6600 +// - the original number of diacritics (0, 1, 2, or 3) - 2 bits
6601 +// - whether this is a classic (1) or Unicode (0) placeholder - 1 bit
6602 +static inline uint32_t tgetimgrow(Glyph *g) { return g->u & 0x1ff; }
6603 +static inline uint32_t tgetimgcol(Glyph *g) { return (g->u >> 9) & 0x1ff; }
6604 +static inline uint32_t tgetimgid4thbyteplus1(Glyph *g) { return (g->u >> 18) & 0x1ff; }
6605 +static inline uint32_t tgetimgdiacriticcount(Glyph *g) { return (g->u >> 27) & 0x3; }
6606 +static inline uint32_t tgetisclassicplaceholder(Glyph *g) { return (g->u >> 29) & 0x1; }
6607 +static inline void tsetimgrow(Glyph *g, uint32_t row) {
6608 + g->u = (g->u & ~0x1ff) | (row & 0x1ff);
6609 +}
6610 +static inline void tsetimgcol(Glyph *g, uint32_t col) {
6611 + g->u = (g->u & ~(0x1ff << 9)) | ((col & 0x1ff) << 9);
6612 +}
6613 +static inline void tsetimg4thbyteplus1(Glyph *g, uint32_t byteplus1) {
6614 + g->u = (g->u & ~(0x1ff << 18)) | ((byteplus1 & 0x1ff) << 18);
6615 +}
6616 +static inline void tsetimgdiacriticcount(Glyph *g, uint32_t count) {
6617 + g->u = (g->u & ~(0x3 << 27)) | ((count & 0x3) << 27);
6618 +}
6619 +static inline void tsetisclassicplaceholder(Glyph *g, uint32_t isclassic) {
6620 + g->u = (g->u & ~(0x1 << 29)) | ((isclassic & 0x1) << 29);
6621 +}
6622 +
6623 +/// Returns the full image id. This is a naive implementation, if the most
6624 +/// significant byte is not specified, it's assumed to be 0 instead of inferring
6625 +/// it from the cells to the left.
6626 +static inline uint32_t tgetimgid(Glyph *g) {
6627 + uint32_t msb = tgetimgid4thbyteplus1(g);
6628 + if (msb != 0)
6629 + --msb;
6630 + return (msb << 24) | (g->fg & 0xFFFFFF);
6631 +}
6632 +
6633 +/// Sets the full image id.
6634 +static inline void tsetimgid(Glyph *g, uint32_t id) {
6635 + g->fg = (id & 0xFFFFFF) | (1 << 24);
6636 + tsetimg4thbyteplus1(g, ((id >> 24) & 0xFF) + 1);
6637 +}
6638 +
6639 +static inline uint32_t tgetimgplacementid(Glyph *g) {
6640 + if (tgetdecorcolor(g) == DECOR_DEFAULT_COLOR)
6641 + return 0;
6642 + return g->decor & 0xFFFFFF;
6643 +}
6644 +
6645 +static inline void tsetimgplacementid(Glyph *g, uint32_t id) {
6646 + g->decor = (id & 0xFFFFFF) | (1 << 24);
6647 +}
6648 diff --git a/st.info b/st.info
6649 index efab2cf..ded76c1 100644
6650 --- a/st.info
6651 +++ b/st.info
6652 @@ -195,6 +195,7 @@ st-mono| simpleterm monocolor,
6653 Ms=\E]52;%p1%s;%p2%s\007,
6654 Se=\E[2 q,
6655 Ss=\E[%p1%d q,
6656 + Smulx=\E[4:%p1%dm,
6657
6658 st| simpleterm,
6659 use=st-mono,
6660 @@ -215,6 +216,11 @@ st-256color| simpleterm with 256 colors,
6661 initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
6662 setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
6663 setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
6664 +# Underline colors
6665 + Su,
6666 + Setulc=\E[58:2:%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
6667 + Setulc1=\E[58:5:%p1%dm,
6668 + ol=\E[59m,
6669
6670 st-meta| simpleterm with meta key,
6671 use=st,
6672 diff --git a/win.h b/win.h
6673 index 6de960d..31b3fff 100644
6674 --- a/win.h
6675 +++ b/win.h
6676 @@ -39,3 +39,6 @@ void xsetpointermotion(int);
6677 void xsetsel(char *);
6678 int xstartdraw(void);
6679 void xximspot(int, int);
6680 +
6681 +void xstartimagedraw(int *dirty, int rows);
6682 +void xfinishimagedraw();
6683 diff --git a/x.c b/x.c
6684 index d73152b..6f1bf8c 100644
6685 --- a/x.c
6686 +++ b/x.c
6687 @@ -4,6 +4,8 @@
6688 #include <limits.h>
6689 #include <locale.h>
6690 #include <signal.h>
6691 +#include <stdio.h>
6692 +#include <stdlib.h>
6693 #include <sys/select.h>
6694 #include <time.h>
6695 #include <unistd.h>
6696 @@ -19,6 +21,7 @@ char *argv0;
6697 #include "arg.h"
6698 #include "st.h"
6699 #include "win.h"
6700 +#include "graphics.h"
6701
6702 /* types used in config.h */
6703 typedef struct {
6704 @@ -59,6 +62,12 @@ static void zoom(const Arg *);
6705 static void zoomabs(const Arg *);
6706 static void zoomreset(const Arg *);
6707 static void ttysend(const Arg *);
6708 +static void previewimage(const Arg *);
6709 +static void showimageinfo(const Arg *);
6710 +static void togglegrdebug(const Arg *);
6711 +static void dumpgrstate(const Arg *);
6712 +static void unloadimages(const Arg *);
6713 +static void toggleimages(const Arg *);
6714
6715 /* config.h for applying patches and the configuration. */
6716 #include "config.h"
6717 @@ -81,6 +90,7 @@ typedef XftGlyphFontSpec GlyphFontSpec;
6718 typedef struct {
6719 int tw, th; /* tty width and height */
6720 int w, h; /* window width and height */
6721 + int hborderpx, vborderpx;
6722 int ch; /* char height */
6723 int cw; /* char width */
6724 int mode; /* window state/mode flags */
6725 @@ -144,6 +154,8 @@ static inline ushort sixd_to_16bit(int);
6726 static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
6727 static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
6728 static void xdrawglyph(Glyph, int, int);
6729 +static void xdrawimages(Glyph, Line, int x1, int y1, int x2);
6730 +static void xdrawoneimagecell(Glyph, int x, int y);
6731 static void xclear(int, int, int, int);
6732 static int xgeommasktogravity(int);
6733 static int ximopen(Display *);
6734 @@ -220,6 +232,7 @@ static DC dc;
6735 static XWindow xw;
6736 static XSelection xsel;
6737 static TermWindow win;
6738 +static unsigned int mouse_col = 0, mouse_row = 0;
6739
6740 /* Font Ring Cache */
6741 enum {
6742 @@ -328,10 +341,72 @@ ttysend(const Arg *arg)
6743 ttywrite(arg->s, strlen(arg->s), 1);
6744 }
6745
6746 +void
6747 +previewimage(const Arg *arg)
6748 +{
6749 + Glyph g = getglyphat(mouse_col, mouse_row);
6750 + if (g.mode & ATTR_IMAGE) {
6751 + uint32_t image_id = tgetimgid(&g);
6752 + fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n",
6753 + image_id, tgetimgplacementid(&g), tgetimgcol(&g),
6754 + tgetimgrow(&g));
6755 + gr_preview_image(image_id, arg->s);
6756 + }
6757 +}
6758 +
6759 +void
6760 +showimageinfo(const Arg *arg)
6761 +{
6762 + Glyph g = getglyphat(mouse_col, mouse_row);
6763 + if (g.mode & ATTR_IMAGE) {
6764 + uint32_t image_id = tgetimgid(&g);
6765 + fprintf(stderr, "Clicked on placeholder %u/%u, x=%d, y=%d\n",
6766 + image_id, tgetimgplacementid(&g), tgetimgcol(&g),
6767 + tgetimgrow(&g));
6768 + char stcommand[256] = {0};
6769 + size_t len = snprintf(stcommand, sizeof(stcommand), "%s -e less", argv0);
6770 + if (len > sizeof(stcommand) - 1) {
6771 + fprintf(stderr, "Executable name too long: %s\n",
6772 + argv0);
6773 + return;
6774 + }
6775 + gr_show_image_info(image_id, tgetimgplacementid(&g),
6776 + tgetimgcol(&g), tgetimgrow(&g),
6777 + tgetisclassicplaceholder(&g),
6778 + tgetimgdiacriticcount(&g), argv0);
6779 + }
6780 +}
6781 +
6782 +void
6783 +togglegrdebug(const Arg *arg)
6784 +{
6785 + graphics_debug_mode = (graphics_debug_mode + 1) % 3;
6786 + redraw();
6787 +}
6788 +
6789 +void
6790 +dumpgrstate(const Arg *arg)
6791 +{
6792 + gr_dump_state();
6793 +}
6794 +
6795 +void
6796 +unloadimages(const Arg *arg)
6797 +{
6798 + gr_unload_images_to_reduce_ram();
6799 +}
6800 +
6801 +void
6802 +toggleimages(const Arg *arg)
6803 +{
6804 + graphics_display_images = !graphics_display_images;
6805 + redraw();
6806 +}
6807 +
6808 int
6809 evcol(XEvent *e)
6810 {
6811 - int x = e->xbutton.x - borderpx;
6812 + int x = e->xbutton.x - win.hborderpx;
6813 LIMIT(x, 0, win.tw - 1);
6814 return x / win.cw;
6815 }
6816 @@ -339,7 +414,7 @@ evcol(XEvent *e)
6817 int
6818 evrow(XEvent *e)
6819 {
6820 - int y = e->xbutton.y - borderpx;
6821 + int y = e->xbutton.y - win.vborderpx;
6822 LIMIT(y, 0, win.th - 1);
6823 return y / win.ch;
6824 }
6825 @@ -452,6 +527,9 @@ mouseaction(XEvent *e, uint release)
6826 /* ignore Button<N>mask for Button<N> - it's set on release */
6827 uint state = e->xbutton.state & ~buttonmask(e->xbutton.button);
6828
6829 + mouse_col = evcol(e);
6830 + mouse_row = evrow(e);
6831 +
6832 for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) {
6833 if (ms->release == release &&
6834 ms->button == e->xbutton.button &&
6835 @@ -739,6 +817,9 @@ cresize(int width, int height)
6836 col = MAX(1, col);
6837 row = MAX(1, row);
6838
6839 + win.hborderpx = (win.w - col * win.cw) * anysize_halign / 100;
6840 + win.vborderpx = (win.h - row * win.ch) * anysize_valign / 100;
6841 +
6842 tresize(col, row);
6843 xresize(col, row);
6844 ttyresize(win.tw, win.th);
6845 @@ -869,8 +950,8 @@ xhints(void)
6846 sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize;
6847 sizeh->height = win.h;
6848 sizeh->width = win.w;
6849 - sizeh->height_inc = win.ch;
6850 - sizeh->width_inc = win.cw;
6851 + sizeh->height_inc = 1;
6852 + sizeh->width_inc = 1;
6853 sizeh->base_height = 2 * borderpx;
6854 sizeh->base_width = 2 * borderpx;
6855 sizeh->min_height = win.ch + 2 * borderpx;
6856 @@ -1014,7 +1095,8 @@ xloadfonts(const char *fontstr, double fontsize)
6857 FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12);
6858 usedfontsize = 12;
6859 }
6860 - defaultfontsize = usedfontsize;
6861 + if (defaultfontsize <= 0)
6862 + defaultfontsize = usedfontsize;
6863 }
6864
6865 if (xloadfont(&dc.font, pattern))
6866 @@ -1024,7 +1106,7 @@ xloadfonts(const char *fontstr, double fontsize)
6867 FcPatternGetDouble(dc.font.match->pattern,
6868 FC_PIXEL_SIZE, 0, &fontval);
6869 usedfontsize = fontval;
6870 - if (fontsize == 0)
6871 + if (defaultfontsize <= 0 && fontsize == 0)
6872 defaultfontsize = fontval;
6873 }
6874
6875 @@ -1152,8 +1234,8 @@ xinit(int cols, int rows)
6876 xloadcols();
6877
6878 /* adjust fixed window geometry */
6879 - win.w = 2 * borderpx + cols * win.cw;
6880 - win.h = 2 * borderpx + rows * win.ch;
6881 + win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw;
6882 + win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch;
6883 if (xw.gm & XNegative)
6884 xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2;
6885 if (xw.gm & YNegative)
6886 @@ -1240,12 +1322,15 @@ xinit(int cols, int rows)
6887 xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0);
6888 if (xsel.xtarget == None)
6889 xsel.xtarget = XA_STRING;
6890 +
6891 + // Initialize the graphics (image display) module.
6892 + gr_init(xw.dpy, xw.vis, xw.cmap);
6893 }
6894
6895 int
6896 xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
6897 {
6898 - float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
6899 + float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp;
6900 ushort mode, prevmode = USHRT_MAX;
6901 Font *font = &dc.font;
6902 int frcflags = FRC_NORMAL;
6903 @@ -1267,6 +1352,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
6904 if (mode == ATTR_WDUMMY)
6905 continue;
6906
6907 + /* Draw spaces for image placeholders (images will be drawn
6908 + * separately). */
6909 + if (mode & ATTR_IMAGE)
6910 + rune = ' ';
6911 +
6912 /* Determine font for glyph if different from previous glyph. */
6913 if (prevmode != mode) {
6914 prevmode = mode;
6915 @@ -1374,11 +1464,61 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
6916 return numspecs;
6917 }
6918
6919 +/* Draws a horizontal dashed line of length `w` starting at `(x, y)`. `wavelen`
6920 + * is the length of the dash plus the length of the gap. `fraction` is the
6921 + * fraction of the dash length compared to `wavelen`. */
6922 +static void
6923 +xdrawunderdashed(Draw draw, Color *color, int x, int y, int w,
6924 + int wavelen, float fraction, int thick)
6925 +{
6926 + int dashw = MAX(1, fraction * wavelen);
6927 + for (int i = x - x % wavelen; i < x + w; i += wavelen) {
6928 + int startx = MAX(i, x);
6929 + int endx = MIN(i + dashw, x + w);
6930 + if (startx < endx)
6931 + XftDrawRect(xw.draw, color, startx, y, endx - startx,
6932 + thick);
6933 + }
6934 +}
6935 +
6936 +/* Draws an undercurl. `h` is the total height, including line thickness. */
6937 +static void
6938 +xdrawundercurl(Draw draw, Color *color, int x, int y, int w, int h, int thick)
6939 +{
6940 + XGCValues gcvals = {.foreground = color->pixel,
6941 + .line_width = thick,
6942 + .line_style = LineSolid,
6943 + .cap_style = CapRound};
6944 + GC gc = XCreateGC(xw.dpy, XftDrawDrawable(xw.draw),
6945 + GCForeground | GCLineWidth | GCLineStyle | GCCapStyle,
6946 + &gcvals);
6947 +
6948 + XRectangle clip = {.x = x, .y = y, .width = w, .height = h};
6949 + XSetClipRectangles(xw.dpy, gc, 0, 0, &clip, 1, Unsorted);
6950 +
6951 + int yoffset = thick / 2;
6952 + int segh = MAX(1, h - thick);
6953 + /* Make sure every segment is at a 45 degree angle, otherwise it doesn't
6954 + * look good without antialiasing. */
6955 + int segw = segh;
6956 + int wavelen = MAX(1, segw * 2);
6957 +
6958 + for (int i = x - (x % wavelen); i < x + w; i += wavelen) {
6959 + XPoint points[3] = {{.x = i, .y = y + yoffset},
6960 + {.x = i + segw, .y = y + yoffset + segh},
6961 + {.x = i + wavelen, .y = y + yoffset}};
6962 + XDrawLines(xw.dpy, XftDrawDrawable(xw.draw), gc, points, 3,
6963 + CoordModeOrigin);
6964 + }
6965 +
6966 + XFreeGC(xw.dpy, gc);
6967 +}
6968 +
6969 void
6970 xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y)
6971 {
6972 int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
6973 - int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
6974 + int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch,
6975 width = charlen * win.cw;
6976 Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
6977 XRenderColor colfg, colbg;
6978 @@ -1468,17 +1608,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
6979
6980 /* Intelligent cleaning up of the borders. */
6981 if (x == 0) {
6982 - xclear(0, (y == 0)? 0 : winy, borderpx,
6983 + xclear(0, (y == 0)? 0 : winy, win.hborderpx,
6984 winy + win.ch +
6985 - ((winy + win.ch >= borderpx + win.th)? win.h : 0));
6986 + ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0));
6987 }
6988 - if (winx + width >= borderpx + win.tw) {
6989 + if (winx + width >= win.hborderpx + win.tw) {
6990 xclear(winx + width, (y == 0)? 0 : winy, win.w,
6991 - ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch)));
6992 + ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch)));
6993 }
6994 if (y == 0)
6995 - xclear(winx, 0, winx + width, borderpx);
6996 - if (winy + win.ch >= borderpx + win.th)
6997 + xclear(winx, 0, winx + width, win.vborderpx);
6998 + if (winy + win.ch >= win.vborderpx + win.th)
6999 xclear(winx, winy + win.ch, winx + width, win.h);
7000
7001 /* Clean up the region we want to draw to. */
7002 @@ -1491,18 +1631,68 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
7003 r.width = width;
7004 XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1);
7005
7006 - /* Render the glyphs. */
7007 - XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
7008 -
7009 - /* Render underline and strikethrough. */
7010 + /* Decoration color. */
7011 + Color decor;
7012 + uint32_t decorcolor = tgetdecorcolor(&base);
7013 + if (decorcolor == DECOR_DEFAULT_COLOR) {
7014 + decor = *fg;
7015 + } else if (IS_TRUECOL(decorcolor)) {
7016 + colfg.alpha = 0xffff;
7017 + colfg.red = TRUERED(decorcolor);
7018 + colfg.green = TRUEGREEN(decorcolor);
7019 + colfg.blue = TRUEBLUE(decorcolor);
7020 + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &decor);
7021 + } else {
7022 + decor = dc.col[decorcolor];
7023 + }
7024 + decor.color.alpha = 0xffff;
7025 + decor.pixel |= 0xff << 24;
7026 +
7027 + /* Float thickness, used as a base to compute other values. */
7028 + float fthick = dc.font.height / 18.0;
7029 + /* Integer thickness in pixels. Must not be 0. */
7030 + int thick = MAX(1, roundf(fthick));
7031 + /* The default gap between the baseline and a single underline. */
7032 + int gap = roundf(fthick * 2);
7033 + /* The total thickness of a double underline. */
7034 + int doubleh = thick * 2 + ceilf(fthick * 0.5);
7035 + /* The total thickness of an undercurl. */
7036 + int curlh = thick * 2 + roundf(fthick * 0.75);
7037 +
7038 + /* Render the underline before the glyphs. */
7039 if (base.mode & ATTR_UNDERLINE) {
7040 - XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent * chscale + 1,
7041 - width, 1);
7042 + uint32_t style = tgetdecorstyle(&base);
7043 + int liney = winy + dc.font.ascent + gap;
7044 + /* Adjust liney to guarantee that a single underline fits. */
7045 + liney -= MAX(0, liney + thick - (winy + win.ch));
7046 + if (style == UNDERLINE_DOUBLE) {
7047 + liney -= MAX(0, liney + doubleh - (winy + win.ch));
7048 + XftDrawRect(xw.draw, &decor, winx, liney, width, thick);
7049 + XftDrawRect(xw.draw, &decor, winx,
7050 + liney + doubleh - thick, width, thick);
7051 + } else if (style == UNDERLINE_DOTTED) {
7052 + xdrawunderdashed(xw.draw, &decor, winx, liney, width,
7053 + thick * 2, 0.5, thick);
7054 + } else if (style == UNDERLINE_DASHED) {
7055 + int wavelen = MAX(2, win.cw * 0.9);
7056 + xdrawunderdashed(xw.draw, &decor, winx, liney, width,
7057 + wavelen, 0.65, thick);
7058 + } else if (style == UNDERLINE_CURLY) {
7059 + liney -= MAX(0, liney + curlh - (winy + win.ch));
7060 + xdrawundercurl(xw.draw, &decor, winx, liney, width,
7061 + curlh, thick);
7062 + } else {
7063 + XftDrawRect(xw.draw, &decor, winx, liney, width, thick);
7064 + }
7065 }
7066
7067 + /* Render the glyphs. */
7068 + XftDrawGlyphFontSpec(xw.draw, fg, specs, len);
7069 +
7070 + /* Render strikethrough. Alway use the fg color. */
7071 if (base.mode & ATTR_STRUCK) {
7072 - XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3,
7073 - width, 1);
7074 + XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent / 3,
7075 + width, thick);
7076 }
7077
7078 /* Reset clip to none. */
7079 @@ -1517,6 +1707,11 @@ xdrawglyph(Glyph g, int x, int y)
7080
7081 numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y);
7082 xdrawglyphfontspecs(&spec, g, numspecs, x, y);
7083 + if (g.mode & ATTR_IMAGE) {
7084 + gr_start_drawing(xw.buf, win.cw, win.ch);
7085 + xdrawoneimagecell(g, x, y);
7086 + gr_finish_drawing(xw.buf);
7087 + }
7088 }
7089
7090 void
7091 @@ -1532,6 +1727,10 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
7092 if (IS_SET(MODE_HIDE))
7093 return;
7094
7095 + // If it's an image, just draw a ballot box for simplicity.
7096 + if (g.mode & ATTR_IMAGE)
7097 + g.u = 0x2610;
7098 +
7099 /*
7100 * Select the right color for the right mode.
7101 */
7102 @@ -1572,39 +1771,167 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
7103 case 3: /* Blinking Underline */
7104 case 4: /* Steady Underline */
7105 XftDrawRect(xw.draw, &drawcol,
7106 - borderpx + cx * win.cw,
7107 - borderpx + (cy + 1) * win.ch - \
7108 + win.hborderpx + cx * win.cw,
7109 + win.vborderpx + (cy + 1) * win.ch - \
7110 cursorthickness,
7111 win.cw, cursorthickness);
7112 break;
7113 case 5: /* Blinking bar */
7114 case 6: /* Steady bar */
7115 XftDrawRect(xw.draw, &drawcol,
7116 - borderpx + cx * win.cw,
7117 - borderpx + cy * win.ch,
7118 + win.hborderpx + cx * win.cw,
7119 + win.vborderpx + cy * win.ch,
7120 cursorthickness, win.ch);
7121 break;
7122 }
7123 } else {
7124 XftDrawRect(xw.draw, &drawcol,
7125 - borderpx + cx * win.cw,
7126 - borderpx + cy * win.ch,
7127 + win.hborderpx + cx * win.cw,
7128 + win.vborderpx + cy * win.ch,
7129 win.cw - 1, 1);
7130 XftDrawRect(xw.draw, &drawcol,
7131 - borderpx + cx * win.cw,
7132 - borderpx + cy * win.ch,
7133 + win.hborderpx + cx * win.cw,
7134 + win.vborderpx + cy * win.ch,
7135 1, win.ch - 1);
7136 XftDrawRect(xw.draw, &drawcol,
7137 - borderpx + (cx + 1) * win.cw - 1,
7138 - borderpx + cy * win.ch,
7139 + win.hborderpx + (cx + 1) * win.cw - 1,
7140 + win.vborderpx + cy * win.ch,
7141 1, win.ch - 1);
7142 XftDrawRect(xw.draw, &drawcol,
7143 - borderpx + cx * win.cw,
7144 - borderpx + (cy + 1) * win.ch - 1,
7145 + win.hborderpx + cx * win.cw,
7146 + win.vborderpx + (cy + 1) * win.ch - 1,
7147 win.cw, 1);
7148 }
7149 }
7150
7151 +/* Draw (or queue for drawing) image cells between columns x1 and x2 assuming
7152 + * that they have the same attributes (and thus the same lower 24 bits of the
7153 + * image ID and the same placement ID). */
7154 +void
7155 +xdrawimages(Glyph base, Line line, int x1, int y1, int x2) {
7156 + int y_pix = win.vborderpx + y1 * win.ch;
7157 + uint32_t image_id_24bits = base.fg & 0xFFFFFF;
7158 + uint32_t placement_id = tgetimgplacementid(&base);
7159 + // Columns and rows are 1-based, 0 means unspecified.
7160 + int last_col = 0;
7161 + int last_row = 0;
7162 + int last_start_col = 0;
7163 + int last_start_x = x1;
7164 + // The most significant byte is also 1-base, subtract 1 before use.
7165 + uint32_t last_id_4thbyteplus1 = 0;
7166 + // We may need to inherit row/column/4th byte from the previous cell.
7167 + Glyph *prev = &line[x1 - 1];
7168 + if (x1 > 0 && (prev->mode & ATTR_IMAGE) &&
7169 + (prev->fg & 0xFFFFFF) == image_id_24bits &&
7170 + prev->decor == base.decor) {
7171 + last_row = tgetimgrow(prev);
7172 + last_col = tgetimgcol(prev);
7173 + last_id_4thbyteplus1 = tgetimgid4thbyteplus1(prev);
7174 + last_start_col = last_col + 1;
7175 + }
7176 + for (int x = x1; x < x2; ++x) {
7177 + Glyph *g = &line[x];
7178 + uint32_t cur_row = tgetimgrow(g);
7179 + uint32_t cur_col = tgetimgcol(g);
7180 + uint32_t cur_id_4thbyteplus1 = tgetimgid4thbyteplus1(g);
7181 + uint32_t num_diacritics = tgetimgdiacriticcount(g);
7182 + // If the row is not specified, assume it's the same as the row
7183 + // of the previous cell. Note that `cur_row` may contain a
7184 + // value imputed earlier, which will be preserved if `last_row`
7185 + // is zero (i.e. we don't know the row of the previous cell).
7186 + if (last_row && (num_diacritics == 0 || !cur_row))
7187 + cur_row = last_row;
7188 + // If the column is not specified and the row is the same as the
7189 + // row of the previous cell, then assume that the column is the
7190 + // next one.
7191 + if (last_col && (num_diacritics <= 1 || !cur_col) &&
7192 + cur_row == last_row)
7193 + cur_col = last_col + 1;
7194 + // If the additional id byte is not specified and the
7195 + // coordinates are consecutive, assume the byte is also the
7196 + // same.
7197 + if (last_id_4thbyteplus1 &&
7198 + (num_diacritics <= 2 || !cur_id_4thbyteplus1) &&
7199 + cur_row == last_row && cur_col == last_col + 1)
7200 + cur_id_4thbyteplus1 = last_id_4thbyteplus1;
7201 + // If we couldn't infer row and column, start from the top left
7202 + // corner.
7203 + if (cur_row == 0)
7204 + cur_row = 1;
7205 + if (cur_col == 0)
7206 + cur_col = 1;
7207 + // If this cell breaks a contiguous stripe of image cells, draw
7208 + // that line and start a new one.
7209 + if (cur_col != last_col + 1 || cur_row != last_row ||
7210 + cur_id_4thbyteplus1 != last_id_4thbyteplus1) {
7211 + uint32_t image_id = image_id_24bits;
7212 + if (last_id_4thbyteplus1)
7213 + image_id |= (last_id_4thbyteplus1 - 1) << 24;
7214 + if (last_row != 0) {
7215 + int x_pix =
7216 + win.hborderpx + last_start_x * win.cw;
7217 + gr_append_imagerect(
7218 + xw.buf, image_id, placement_id,
7219 + last_start_col - 1, last_col,
7220 + last_row - 1, last_row, last_start_x,
7221 + y1, x_pix, y_pix, win.cw, win.ch,
7222 + base.mode & ATTR_REVERSE);
7223 + }
7224 + last_start_col = cur_col;
7225 + last_start_x = x;
7226 + }
7227 + last_row = cur_row;
7228 + last_col = cur_col;
7229 + last_id_4thbyteplus1 = cur_id_4thbyteplus1;
7230 + // Populate the missing glyph data to enable inheritance between
7231 + // runs and support the naive implementation of tgetimgid.
7232 + if (!tgetimgrow(g))
7233 + tsetimgrow(g, cur_row);
7234 + // We cannot save this information if there are > 511 cols.
7235 + if (!tgetimgcol(g) && (cur_col & ~0x1ff) == 0)
7236 + tsetimgcol(g, cur_col);
7237 + if (!tgetimgid4thbyteplus1(g))
7238 + tsetimg4thbyteplus1(g, cur_id_4thbyteplus1);
7239 + }
7240 + uint32_t image_id = image_id_24bits;
7241 + if (last_id_4thbyteplus1)
7242 + image_id |= (last_id_4thbyteplus1 - 1) << 24;
7243 + // Draw the last contiguous stripe.
7244 + if (last_row != 0) {
7245 + int x_pix = win.hborderpx + last_start_x * win.cw;
7246 + gr_append_imagerect(xw.buf, image_id, placement_id,
7247 + last_start_col - 1, last_col, last_row - 1,
7248 + last_row, last_start_x, y1, x_pix, y_pix,
7249 + win.cw, win.ch, base.mode & ATTR_REVERSE);
7250 + }
7251 +}
7252 +
7253 +/* Draw just one image cell without inheriting attributes from the left. */
7254 +void xdrawoneimagecell(Glyph g, int x, int y) {
7255 + if (!(g.mode & ATTR_IMAGE))
7256 + return;
7257 + int x_pix = win.hborderpx + x * win.cw;
7258 + int y_pix = win.vborderpx + y * win.ch;
7259 + uint32_t row = tgetimgrow(&g) - 1;
7260 + uint32_t col = tgetimgcol(&g) - 1;
7261 + uint32_t placement_id = tgetimgplacementid(&g);
7262 + uint32_t image_id = tgetimgid(&g);
7263 + gr_append_imagerect(xw.buf, image_id, placement_id, col, col + 1, row,
7264 + row + 1, x, y, x_pix, y_pix, win.cw, win.ch,
7265 + g.mode & ATTR_REVERSE);
7266 +}
7267 +
7268 +/* Prepare for image drawing. */
7269 +void xstartimagedraw(int *dirty, int rows) {
7270 + gr_start_drawing(xw.buf, win.cw, win.ch);
7271 + gr_mark_dirty_animations(dirty, rows);
7272 +}
7273 +
7274 +/* Draw all queued image cells. */
7275 +void xfinishimagedraw() {
7276 + gr_finish_drawing(xw.buf);
7277 +}
7278 +
7279 void
7280 xsetenv(void)
7281 {
7282 @@ -1671,6 +1998,8 @@ xdrawline(Line line, int x1, int y1, int x2)
7283 new.mode ^= ATTR_REVERSE;
7284 if (i > 0 && ATTRCMP(base, new)) {
7285 xdrawglyphfontspecs(specs, base, i, ox, y1);
7286 + if (base.mode & ATTR_IMAGE)
7287 + xdrawimages(base, line, ox, y1, x);
7288 specs += i;
7289 numspecs -= i;
7290 i = 0;
7291 @@ -1683,6 +2012,8 @@ xdrawline(Line line, int x1, int y1, int x2)
7292 }
7293 if (i > 0)
7294 xdrawglyphfontspecs(specs, base, i, ox, y1);
7295 + if (i > 0 && base.mode & ATTR_IMAGE)
7296 + xdrawimages(base, line, ox, y1, x);
7297 }
7298
7299 void
7300 @@ -1907,6 +2238,7 @@ cmessage(XEvent *e)
7301 }
7302 } else if (e->xclient.data.l[0] == xw.wmdeletewin) {
7303 ttyhangup();
7304 + gr_deinit();
7305 exit(0);
7306 }
7307 }
7308 @@ -1957,6 +2289,13 @@ run(void)
7309 if (XPending(xw.dpy))
7310 timeout = 0; /* existing events might not set xfd */
7311
7312 + /* Decrease the timeout if there are active animations. */
7313 + if (graphics_next_redraw_delay != INT_MAX &&
7314 + IS_SET(MODE_VISIBLE))
7315 + timeout = timeout < 0 ? graphics_next_redraw_delay
7316 + : MIN(timeout,
7317 + graphics_next_redraw_delay);
7318 +
7319 seltv.tv_sec = timeout / 1E3;
7320 seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec);
7321 tv = timeout >= 0 ? &seltv : NULL;
7322 --
7323 2.43.0
7324